[Udemy | C#과 Unity로 3D 게임 개발하기] Boost Project

ssu_hyun·2022년 3월 15일
0

Unity

목록 보기
3/4

Onion Design

  • 게임에서 가장 중요한 핵심 요소를 정하고 그것을 기준으로 다음으로 중요한 요소들을 순차적으로 정하는 기법
  • 중요한 것은 다음 요소를 추가할 때 그것이 '핵심 요소를 더 좋게 만들고 발전시키는지'를 고려해야 한다.




입력 바인딩의 기본

Input.GetKey

  • 사용자가 입력한 키를 이름으로 식별

    • Input.GetKey("이름")
  • 사용자가 입력한 키를 KeyCode.열거 매개 변수로 식별

    • Input.GetKey(KeyCode.매개변수)

  • Rocket 오브젝트에 적용할 스크립트 작성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Movement : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        ProcessThrust();
        ProcessRotation();
    }


    void ProcessThrust()
    {
        if (Input.GetKey(KeyCode.Space))  // spacebar 누를 경우 아래 코드 출력
        {
            Debug.Log("Pressed SPACE - Thrusting");
        }
    }


    void ProcessRotation()
    {
        if (Input.GetKey(KeyCode.A))  // A 누를 경우 아래 코드 출력
        {
            Debug.Log("Rotating left");
        }

        // else if : 위 조건이 만족되지 않을시 이 특정 조건이 실행됨 
        else if (Input.GetKey(KeyCode.D))  // D 누를 경우 아래 코드 출력  
        {
            Debug.Log("Rotating right");
        }
    }

}

=>AD를 동시에 누를 경우 조건문 순서로 인해 A의 구문이 실행된다.




AddRelativeForce() 함수

스페이스바를 누르면 로켓이 하늘로 날아 오르도록 만들기

  1. RocketRigid Body (개체에 물리 적용) 추가
  2. Movement 스크립트에서 AddRelativeForce() 함수 추가
void ProcessThrust()
    {
        if (Input.GetKey(KeyCode.Space))  // spacebar 누를 경우 아래 코드 출력
        {
            float force = 5f; // 추가한 뒤 mass 아주 작게 설정해야 실행됨
            rb.AddRelativeForce(0f, force, 0f);
            // 파라미터는 x, y, z 나타내며, 파라이터 값을 통해 특정 각도로 이동
            // 3D 게임 - Vector3로 입력 가능
        }
    }
  1. 동작 실행해보며 Rigidbody 여러 변수들과 Transform 변수 조정

오브젝트를 위로 이동시키는 코드

AddRelativeForce(0, 1, 0) = AddRelativeForce(Vector3.up)




추력 변수

  • 위로 향하는 추진력 변수 mainThrust
  • 프레임률 영향을 받지 않도록 하는 Time.deltaTime

[SerializeField] float mainThrust = 1000f;

.
.
.

void ProcessThrust()
{
    if (Input.GetKey(KeyCode.Space))  // spacebar 누를 경우 아래 코드 출력
    {
        Debug.Log("Press Space");
        rb.AddRelativeForce(Vector3.up * mainThrust * Time.deltaTime);
        //위로 향하는 추진력 변수 mainThrust
        // 프레임률 영향을 받지 않도록 하는 Time.deltaTime 
    }
}




로켓 회전

  1. transform.Rotate() 함수 추가
[SerializeField] float rotationThrust = 1f;

.
.
.

void ProcessRotation()
{
     if (Input.GetKey(KeyCode.A))  // A 누를 경우 
     {
            transform.Rotate(Vector3.forward * rotationThrust * Time.deltaTime); // 앞 : Z방향
     }

     // else if : 위 조건이 만족되지 않을시 아래 조건 실행
     else if (Input.GetKey(KeyCode.D))  // D 누를 경우  
     {
            transform.Rotate(-Vector3.forward * rotationThrust * Time.deltaTime); // 뒤 : -Z방향
     }
 }
  1. 중복 코드 간략화위해 새로운 메서드 ApplyRotation 생성
    • ctrl + . - Extract method/메서드 추출 : 메서드 생성
    • public (공용 메서드) : 다른 클래스에서도 이 메서드를 불러올 수 있음
    • private (사설 메서드) : 오직 그 사설 메서드를 가진 클래스만 이 메서드에 접근, 호출, 변경, 수정할 수 있다. public이 존재할 경우 나머지 변수들이 private이라는 사실이 기정사실화되므로 대부분 생략한다.
    • 조건에 따라 입력받는 값을 달리해(Z, -Z) 출력값 다르게 할 것
    void ProcessRotation()
    {
        if (Input.GetKey(KeyCode.A))  // A 입력받을 경우 
        {
            ApplyRotation(rotationThrust);  // Z값 넘김
        }

        // else if : 위 조건이 만족되지 않을시 이 특정 조건이 실행됨 
        else if (Input.GetKey(KeyCode.D))  // D 입력받을 경우  
        {
            ApplyRotation(-rotationThrust); // -Z값 넘김
        }
     }

     private void ApplyRotation(float rotationThisFrame)
     {
         transform.Rotate(Vector3.forward * rotationThisFrame * Time.deltaTime);
     }

Error
Hierarchy에서 object가 선택되고 play하면 이상하게 movement가 예상대로 작동하질 않았다. (mass가 엄청난 것처럼) 알아보니 옛날 버전 unity의 이상한 버그라고 한다. 해당 object를 선택하지 않고 play할 경우 에러는 해결되었다.




Rigidbody Constraints


Freeze

  • x, y방향(왼,오,위,아래)으로만 rocket 이동
  • x, y방향으로는 회전하지 못하도록 freeze

장애물 충돌 버그

  • 장애물 충돌 버그
    : rocket은 특정 키가 눌리면 수동적으로 특정 방향으로 힘을 가하도록 명령을 받는데, obstacle에 부딪힐 경우 이 수동적인 힘과 플레이어가 지시한 반대의 힘이 충돌하면 두 시스템 사이에 문제가 발생하고 이상한 버그가 발생해 플레이어가 제대로 rocket을 조종할 수 없게된다.
  • 해결책 : Movement.cs에서 수동 제어 코드 부분 수정
void ApplyRotation(float rotationThisFrame)
     {
         rb.freezeRotation = true; // freezing rotation so we can manually rotate
         transform.Rotate(Vector3.forward * rotationThisFrame * Time.deltaTime); // 수동 제어 : 강제로 특정 방향으로 회전하라고 지시
         rb.freezeRotation = false; // unfreezing rotation so the physics system can take over
      }

공기저항 생성

  • rocket의 Rigidbody - Drag 설정

월드 중력값 설정

Edit - Project Settings - Physics - Gravity (x, y, z 방향 모두 설정 가능)




소스 컨트롤과 파일 저장소


  • 소스 컨트롤(Source control) : 내 코드 및 프로젝트의 변경 사항을 추적 및 관리하는 시스템
  • 깃(Git) : 파일의 변경 내용을 추적하는 버전 관리 시스템
  • 깃랩(Gitlab) : 온라인 리포지토리 호스팅 서비스 (웹사이트 공간)
  • 리포지토리(Repository) : 내 프로젝트를 위한 디렉터리 혹은 저장공간 (프로젝트 저장 공간)
  • 소스트리(SourceTree) : 리포(repo)를 볼 수 있도록 돕는 데스크톱 클라이언트
  • 사용이유
    • 프로젝트를 저장하는 좋은 백업
    • 프로젝트의 이전 버전으로 돌아갈 수 있어 프로젝트를 안전히 변경 가능 (실수해도 되돌아가기 가능)
    • 여러 아이디어 동시에 시도 가능
    • 공동작업 더 쉽게 가능
  • Github과 연결




유니티 오디오 소개


  • 오디오를 위해 필요한 3가지
    • Audio Listener : To 'hear' the audio
    • Audio source : To 'play' the audio
    • Audio File : The 'sounds' that get played

Audio Listener

Main Camera의 Inspector에 존재


Audio source

오디오를 생성하는 object에 추가

  • 현재 방법은 object가 단 하나의 소리를 낸다는 가정하에 진행하는 방법이다. (만약 여러 소리를 내야한다면 다른 방식을 사용해야한다.)

  • 오디오 클립 받는 방법

    ※ 효과음 파일을 찾거나 직접 녹음을 한다면 OggMP3 사용

  • 다운 받은 클립을 rocket Audio SourceAudio Clip에 넣어준다.




AudioSource SFX 재생

오디오 겹침 현상 해결

AudioSource audioSource; // 참조 캐싱

          // 생략 //

void ProcessThrust()
    {
        if (Input.GetKey(KeyCode.Space))  // spacebar 누를 경우 아래 코드 출력
        {
            // float force = 5f;
            rb.AddRelativeForce(Vector3.up * mainThrust * Time.deltaTime); // 3D 게임이므로 Vector3 (x, y, z) 입력 / x 1유닛, y 1유닛, z 1유닛씩 이동해서 특정 각도로 이동
            if(!audioSource.isPlaying)  // 재생하고 있지 않을 때만 재생 (오디오가 겹치지 않기 위해)
            {
                audioSource.Play(); // 오디오 소스에 추가한 오디오 클립 재생
             }
            
         }
         else // 로켓이 추진하지 않을 경우
         {
             audioSource.Stop(); // 오디오 정지
         }
     }
     
            // 생략 //
  • Audio SourcePlay On Awake 체크 해제




Switch 구문

충돌 시스템 구현

  • 충돌 관련 object : tag 설정

  • CollisionHandler.cs - Switch문 구현

    Switch문

    • If나 Else문같은 조건문
    • 하나의 변수와 비교할 수 있게 해준다.
    • switch 문법
      switch (variableToCompare) // 변수 = 충돌한 객체의 태그
      {
      	case valueA:
        	ActionToTake();
        	break;
        case valueB:
        	OtherAction();
            break;
        default:
        	YetAnotherAction();
            break;
      }
  • 충돌 결과




SceneManager를 사용한 리스폰

다른 object에 충돌시 처음부터 다시시작

  • File - Build Settings - Add Open Scenes
  • CollisionHandler.cs 편집
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    void OnCollisionEnter(Collision other) // 부딪힌 다른 것이 무엇인지 물어보는 것
    {
        switch(other.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is friendly");
                break;
            case "Finish":
                Debug.Log("Congrats, yo, you finished!");
                break;
            case "Fuel":
                Debug.Log("You picked up fuel");
                break;
            default:
                ReloadLevel();
                break;

        }
    }

    void ReloadLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex; // buildIndex : 현재 활동하고 있는, 빌드 설정에 있는 씬의 인덱스 반환
        SceneManager.LoadScene(currentSceneIndex);  // SceneManager.LoadScene(0) : 씬의 인덱스를 통해 현재 씬 불러오기
    }
}




Load Next Level

다음 단계 로드하기

  • 기존 Scene 복제해 새로운 다음 단계 Scene 만들기
  • File - Build Settings - Add Open Scenes 통해 새로운 Scene 추가하고 Scene 순서 조정
  • CollisionHandler.cs 편집
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    void OnCollisionEnter(Collision other) 
    {
        switch(other.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is friendly");
                break;
            case "Finish":
                LoadNextLevel(); // Finish에 도달하면 다음 레벨로 이동
                break;
            case "Fuel":
                Debug.Log("You picked up fuel");
                break;
            default:
                ReloadLevel();
                break;

        }
    }
    
    void LoadNextLevel()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex; 
        SceneManager.LoadScene(currentSceneIndex + 1); // 씬 인덱스 + 1 => 다음 씬으로 이동
     }

    void ReloadLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex; 
        SceneManager.LoadScene(currentSceneIndex); 
     }

    
}
  • 맨 마지막 scene이후에 다시 처음으로 돌아오게 만들기
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    void OnCollisionEnter(Collision other) 
    {
        switch(other.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is friendly");
                break;
            case "Finish":
                LoadNextLevel();
                break;
            case "Fuel":
                Debug.Log("You picked up fuel");
                break;
            default:
                ReloadLevel();
                break;

        }
    }

    void LoadNextLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        if (currentSceneIndex <= 1)   // to ensure we don't try to load level 2
        { 
            int nextSceneIndex = currentSceneIndex + 1;
            if (nextSceneIndex == SceneManager.sceneCountInBuildSettings) // "다음 씬 인덱스 = 우리가 가진 씬들의 총 개수"일 경우
            {
                nextSceneIndex = 0; // 씬 인덱스 초기화
            }
            SceneManager.LoadScene(nextSceneIndex);
        }
    
    }

    void ReloadLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex; // buildIndex : 현재 활동하고 있는, 빌드 설정에 있는 씬의 인덱스 반환
        SceneManager.LoadScene(currentSceneIndex); // SceneManager.LoadScene(0) : 씬의 인덱스를 통해 현재 씬 불러오기
    }

Error존재하지 않는 scene index를 호출해서 생긴 error. index와 전체 개수를 비교하려다보니 생긴 error였다. 그래서 현재 index+1을 전체 씬 개수와 비교하는 부분을 if문을 통해 currentSceneIndex가 1보다 작거나 같은 경우에만 실행되도록 묶어주었다. if (currentSceneIndex <= 1)




Invoke 함수 사용

  • Invoke 함수를 통해 rocket이 다른 object에 부딪힌 뒤 1초 delay 추가

    default:
        Invoke("ReloadLevel", 1f); // 부딪힌 뒤 1초 delay
        break;
  • StartCrashSequence 함수 사용

                default:
                  StartCrashSequence(); // 부딪힌 뒤 1초 delay
                  break;
    
          }
      }
    
      void StartCrashSequence()
      {
          GetComponent<Movement>().enabled = false; //rocket의 movement 컴포넌트 제어권 off | Stop player controls during the delay
          Invoke("ReloadLvel", 1f); // delay setting
      }
  • Landing Pad 도착할 경우 delay 추가 + delay second 파라미터화

    
     [SerializeField] float levelLoadDelay = 2f;
        
                // 생략 //
    
                case "Finish":
                    StartSuccessSequence();
                    break;
                 default:
                    StartCrashSequence(); // 부딪힌 뒤 1초 delay
                    break;
    
          }
      }
    
      void StartSuccessSequence()
      {
          //todo add SFX upon crash
          //todo add particle effect upon crash 
          GetComponent<Movement>().enabled = false; //rocket의 movement 컴포넌트 제어권 off | Stop player controls during the delay
          Invoke("LoadNextLevel", levelLoadDelay); // Parameterise delay setting (we can tune in the inspector)
      }
    
      void StartCrashSequence()
      {
          //todo add SFX upon crash
          //todo add particle effect upon crash
          GetComponent<Movement>().enabled = false; //rocket의 movement 컴포넌트 제어권 off | Stop player controls during the delay
          Invoke("ReloadLevel", levelLoadDelay); // Parameterise delay setting (we can tune in the inspector)
      }
      
                 // 생략 //




Multiple Audio Clips

  • AudioSource.Play : 재생하고 싶은 클립을 특정 지을 수 없다
  • AudioSource.PlayOneShot : 파라미터를 통해 재생하고 싶은 크립을 특정 지을 수 있다.

1. Audio Souce component가 아닌 Movement(Script) component에서 Audio 지정

// Movement.cs //

	// CASH
    AudioSource audioSource;
    
    // 생략 //
    
    void ProcessThrust()
    {
        if (Input.GetKey(KeyCode.Space))  
        {
            // float force = 5f;
            rb.AddRelativeForce(Vector3.up * mainThrust * Time.deltaTime); 
            if(!audioSource.isPlaying)  
            {
                audioSource.PlayOneShot(mainEngine); // 파라미터 통해 재생할 오디오 클립 지정할 수 있는 메소드
            }
            
        }

2. rocket이 다른 object, Landing Pad와 닿았을 때 나는 효과음(crash, success) 설정

// CollisionHandler.cs//


using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    // PARAMETER
    [SerializeField] float levelLoadDelay = 2f;
    [SerializeField] AudioClip success;  // success 오디오 클립 직렬화
    [SerializeField] AudioClip crash;  // crash 오디오 클립 직렬화

    // CASH
    AudioSource audioSource;  // 변수 생성

    void Start()
    {
        audioSource = GetComponent<AudioSource>();  // 캐싱
    }


 
    void OnCollisionEnter(Collision other) 
    {
        switch(other.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is friendly");
                break;
            case "Finish":
                StartSuccessSequence();
                break;
            default:
                StartCrashSequence();
                break;

        }
    }

    void StartSuccessSequence()
    {
        audioSource.PlayOneShot(success); // success 재생
        //todo add particle effect upon success 
        GetComponent<Movement>().enabled = false; 
        Invoke("LoadNextLevel", levelLoadDelay); 
    }

    void StartCrashSequence()
    {
        audioSource.PlayOneShot(crash); // crash 재생
        //todo add particle effect upon crash
        GetComponent<Movement>().enabled = false; 
        Invoke("ReloadLevel", levelLoadDelay); 
    }

    void LoadNextLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        if (currentSceneIndex <= 1)   
        { 
            int nextSceneIndex = currentSceneIndex + 1;
            if (nextSceneIndex == SceneManager.sceneCountInBuildSettings) 
            {
                nextSceneIndex = 0; 
            }
            SceneManager.LoadScene(nextSceneIndex);
        }
    
    }

    void ReloadLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex; 
        SceneManager.LoadScene(currentSceneIndex); 
    }

    
}




Bool variable for state

효과음이 연속 실행되지 않도록 설정하기

//CollisionHandler.cs//


using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    // PARAMETER
    [SerializeField] float levelLoadDelay = 2f;
    [SerializeField] AudioClip success;
    [SerializeField] AudioClip crash;

    // CASH
    AudioSource audioSource;

    //STATE
    bool isTransitioning = false; // 부딪힌 경우에만 true가 된다.

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }


        void OnCollisionEnter(Collision other) // 부딪힌 다른 것이 무엇인지 물어보는 것
    {
        if (isTransitioning)  {return;} //(isTransitioning == true)
        
        switch(other.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is friendly");
                break;
            case "Finish":
                StartSuccessSequence();
                break;
            default:
                StartCrashSequence(); // 부딪힌 뒤 1초 delay
                break;
        }
    }

    void StartSuccessSequence()
    {
        isTransitioning = true;
        audioSource.Stop(); // success 오디오 시작 전 다른 오디오 끄기
        audioSource.PlayOneShot(success);
        //todo add particle effect upon success 
        GetComponent<Movement>().enabled = false; //rocket의 movement 컴포넌트 제어권 off | Stop player controls during the delay
        Invoke("LoadNextLevel", levelLoadDelay); // Parameterise delay setting (we can tune in the inspector)
    }

    void StartCrashSequence()
    {
        isTransitioning = true;
        audioSource.Stop(); // crash 오디오 시작 전 다른 오디오 끄기
        audioSource.PlayOneShot(crash);
        //todo add particle effect upon crash
        GetComponent<Movement>().enabled = false; //rocket의 movement 컴포넌트 제어권 off | Stop player controls during the delay
        Invoke("ReloadLevel", levelLoadDelay); 
    }

    void LoadNextLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        if (currentSceneIndex <= 1)
        { 
            int nextSceneIndex = currentSceneIndex + 1;
            if (nextSceneIndex == SceneManager.sceneCountInBuildSettings) 
            {
                nextSceneIndex = 0;
            SceneManager.LoadScene(nextSceneIndex);
        }
    
    }

    void ReloadLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex; 
        SceneManager.LoadScene(currentSceneIndex); 
    }

    
}

※ 두 개의 씬 모두에 다 적용할 것




Make Rocket Look Spiffy




How To Trigger Particles

Particle System

  • Particle System : 게임 객체에 추가되는 컴포넌트

  • Emitter : 파티클들을 방출(Emitting)하는 물체, 공간, 혹은 지점 (월드의 한 지점)

  • 기본 Particle System 추가 : [Hierarchy] - [Effects] - [Particle System]

  • Particle System 파일 적용

    1. Particle System 파일 넣을 디렉토리 생성
    2. 로켓 Prefab에 적용
    3. [CollisionHandler Scripts 편집] - [Inspector 조정 메뉴 만들기] - [파일 적용]
    using System;
     using UnityEngine;
     using UnityEngine.SceneManagement;
    
     public class CollisionHandler : MonoBehaviour
     {
         [SerializeField] float levelLoadDelay = 2f;
         [SerializeField] AudioClip success;
         [SerializeField] AudioClip crash;
    
         // 추가할 두 개의 파티클 필드 생성 // 
         [SerializeField] ParticleSystem successParticles;
         [SerializeField] ParticleSystem crashParticles;

ParticleSystem.Play()

  • 로켓이 도착지에 도착했을 때, 로켓이 다른 물체와 부딪혔을 때의
    ParticleSystem을 Play하는 코드 추가
// CollisionHandler.cs //
    void StartSuccessSequence()
    {
        isTransitioning = true;
        audioSource.Stop(); 
        audioSource.PlayOneShot(success);
        successParticles.Play();  // add  //
        GetComponent<Movement>().enabled = false; 
        Invoke("LoadNextLevel", levelLoadDelay); 
    }

    void StartCrashSequence()
    {
        isTransitioning = true;
        audioSource.Stop(); 
        audioSource.PlayOneShot(crash);
        crashParticles.Play();  // add  //
        GetComponent<Movement>().enabled = false; 
        Invoke("ReloadLevel", levelLoadDelay); 
  • 각 Particle System의 Inspector에서 Looping, Play On Awake 체크 해제 확인




Particles For Rocket Boosters

  • 로켓 부스터 3가지 추가 : Rocket Jet, Right Thruster, Left Thruster
    * Right ThrusterLeft Thruster는 Rotation Y의 부호를 달리하면 쉽게 변경 가능

  • Movements.cs

    • Rocket Jet, Right Thruster, Left Thruster 필드 추가
    • 각 Particle의 Play(),Stop() 코드 추가
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;

public class Movement : MonoBehaviour
{

    /* 코드 작성 순서

        PARAMETERS(매개변수) - for tuning, typically set in the editor
        CACHE - e.g. references for readability or speed
        STATE - private instance (member) variables
    */


    [SerializeField] float mainThrust = 100f;
    [SerializeField] float rotationThrust = 1f;
    [SerializeField] AudioClip mainEngine;
	
    // 로켓부스터 파티클 필드 추가
    [SerializeField] ParticleSystem mainEngineParticles;
    [SerializeField] ParticleSystem leftThrusterParticles;
    [SerializeField] ParticleSystem rightThrusterParticles;
    
    Rigidbody rb;
    AudioSource audioSource;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        audioSource = GetComponent<AudioSource>();
    }

    void Update()
    {
        ProcessThrust();
        ProcessRotation();
    }


    void ProcessThrust()
    {
        if (Input.GetKey(KeyCode.Space))
        {
            rb.AddRelativeForce(Vector3.up * mainThrust * Time.deltaTime); 
            if(!audioSource.isPlaying)  
            {
                audioSource.PlayOneShot(mainEngine); 
            }
            // mainEngineParticles Play() 코드
            if(!mainEngineParticles.isPlaying) 
            {
                mainEngineParticles.Play();
            }
            
        }
        // mainEngineParticles Stop() 코드
        else
        {
            audioSource.Stop();
            mainEngineParticles.Stop();
        }
    }


    void ProcessRotation()
    {
        if (Input.GetKey(KeyCode.A))   
        {
            ApplyRotation(rotationThrust); 
            // rightThrusterParticles Play() 코드
            if(!rightThrusterParticles.isPlaying) 
            {
                rightThrusterParticles.Play();
            }
        }

        else if (Input.GetKey(KeyCode.D))    
        {
            ApplyRotation(-rotationThrust);
            // leftThrusterParticles Play() 코드
            if(!leftThrusterParticles.isPlaying) 
            {
                leftThrusterParticles.Play();
            } 
        }
        // if(A key pressed)와 else(D key pressed) 모두 실행되지 않을 경우 두 개의 ThrusterParticles 모두 중지
        else
        {
            rightThrusterParticles.Stop();
            leftThrusterParticles.Stop();
        }
     }

    void ApplyRotation(float rotationThisFrame)
    {
        rb.freezeRotation = true;
        transform.Rotate(Vector3.forward * rotationThisFrame * Time.deltaTime); 
        rb.freezeRotation = false;
    }
}
  • Movement.cs 필드에 파일 적용




Refactor with Extract Method

  • [Ctrl] + [.] - Extract method(메서드 추출) - [F2]로 rename
  • 메서드 추출로 복잡한 코드 객체화시켜 코드 단순화하기
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;

public class Movement : MonoBehaviour
{

    /* 코드 작성 순서

        PARAMETERS(매개변수) - for tuning, typically set in the editor
        CACHE - e.g. references for readability or speed
        STATE - private instance (member) variables
    */


    [SerializeField] float mainThrust = 100f;
    [SerializeField] float rotationThrust = 1f;
    [SerializeField] AudioClip mainEngine;

    [SerializeField] ParticleSystem mainEngineParticles;
    [SerializeField] ParticleSystem leftThrusterParticles;
    [SerializeField] ParticleSystem rightThrusterParticles;
    
    Rigidbody rb;
    AudioSource audioSource;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        audioSource = GetComponent<AudioSource>();
    }

    void Update()
    {
        ProcessThrust();
        ProcessRotation();
    }


    void ProcessThrust()
    {
        if (Input.GetKey(KeyCode.Space))  
        {
            StartThrusting();
        }
        else
        {
            StopThrusting();
        }
    }


    void ProcessRotation()
    {
        if (Input.GetKey(KeyCode.A)) 
        {
            RotateLeft();
        }
        
        else if (Input.GetKey(KeyCode.D))    
        {
            RotateRight();
        }
        
        else
        {
            StopRotating();
        }
    }


    void StartThrusting()
    {
        
        rb.AddRelativeForce(Vector3.up * mainThrust * Time.deltaTime);
        if (!audioSource.isPlaying)  
        {
            audioSource.PlayOneShot(mainEngine); 
        }
     
        if (!mainEngineParticles.isPlaying)
        {
            mainEngineParticles.Play();
        }
    }

    void StopThrusting()
    {
        audioSource.Stop();
        mainEngineParticles.Stop();
    }    

    void RotateLeft()
    {
        ApplyRotation(rotationThrust);
        if (!rightThrusterParticles.isPlaying)
        {
            rightThrusterParticles.Play();
        }
    }

    void RotateRight()
    {
        ApplyRotation(-rotationThrust); 
        if (!leftThrusterParticles.isPlaying)
        {
            leftThrusterParticles.Play();
        }
    }

    void StopRotating()
    {
        rightThrusterParticles.Stop();
        leftThrusterParticles.Stop();
    }

    void ApplyRotation(float rotationThisFrame)
    {
        rb.freezeRotation = true; 
        transform.Rotate(Vector3.forward * rotationThisFrame * Time.deltaTime); 
        rb.freezeRotation = false; 
    }
}




Add Cheat / Debug Keys

  • L : 다음 레벨로 이동
  • C : 충돌 없애 게임 무제한 실행
using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    // PARAMETER
    [SerializeField] float levelLoadDelay = 2f;
    [SerializeField] AudioClip success;
    [SerializeField] AudioClip crash;

    [SerializeField] ParticleSystem successParticles;
    [SerializeField] ParticleSystem crashParticles;

    // CASH
    AudioSource audioSource;

    //STATE
    bool isTransitioning = false; 
    bool collisionDisabled = false;

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void Update()
    {
        RespondToDebugKeys();
    }
	
    
    void RespondToDebugKeys()
    {	
        // L Key
        if (Input.GetKeyDown(KeyCode.L))
        {
            LoadNextLevel();
        }
        // C Key
        else if (Input.GetKeyDown(KeyCode.C))
        {
            collisionDisabled = !collisionDisabled;  // toggle collision
        }
    }
	
    void OnCollisionEnter(Collision other) 
    {	
        if (isTransitioning || collisionDisabled)  {return;} // isTransitioning or collisionDisabled == true
        
        switch(other.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is friendly");
                break;
            case "Finish":
                StartSuccessSequence();
                break;
            default:
                StartCrashSequence();
                break;
        }
    }


    void StartSuccessSequence()
    {
        isTransitioning = true;
        audioSource.Stop(); 
        audioSource.PlayOneShot(success);
        successParticles.Play();
        GetComponent<Movement>().enabled = false; /
        Invoke("LoadNextLevel", levelLoadDelay); // Parameterise delay setting (we can tune in the inspector)
    }

    void StartCrashSequence()
    {
        isTransitioning = true;
        audioSource.Stop(); 
        audioSource.PlayOneShot(crash);
        crashParticles.Play();
        GetComponent<Movement>().enabled = false; 
        Invoke("ReloadLevel", levelLoadDelay);
    }

    void LoadNextLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        if (currentSceneIndex <= 1)   
        { 
            int nextSceneIndex = currentSceneIndex + 1;
            if (nextSceneIndex == SceneManager.sceneCountInBuildSettings) 
            {
                nextSceneIndex = 0; 
            }
            SceneManager.LoadScene(nextSceneIndex);
        }
    
    }

    void ReloadLevel()
    {   
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex; 
        SceneManager.LoadScene(currentSceneIndex);
    }

    
}



Make Environment From Cubes

  • Lighting탭 생성
  • Skybox Material 생성
  • [Skybox Inspector] - [Shader → Skybox/Procedural] - 옵션 설정
  • 배경 완전 검은색 만들기
    • [Main Camera Inspector] - [Clear Flags] - [Solid Color]
    • [Main Camera Inspector] - [Background] → 검정색
  • 추가 object 생성해 environment 꾸미기



How To Add Lights In Unity

  • [Hierarchy] - [Light] - Light 종류 선택해 위치시키기



Move Obstacle With Code & Mathf.Sin() For Oscillation

  • 움직이는 장애물 생성
  • Movement Factor 만들기 위해 Oscillator.cs 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Oscillator : MonoBehaviour
{

    Vector3 startingPosition;
    [SerializeField] Vector3 movementVector;
    [SerializeField] [Range(0, 1)]float movementFactor;
    [SerializeField] float period = 2f;



    void Start()
    {
        startingPosition = transform.position;
        Debug.Log(startingPosition);
    }


    void Update()
    {
        float cycles = Time.time / period;  // 시간에 따라 계속 증가함

        const float tau = Mathf.PI * 2;  // 6.283의 일정한 값
        float rawSinWave = Mathf.Sin(cycles * tau); // -1~1로 움직임

        movementFactor = (rawSinWave + 1f) / 2f;  // 0~1로 움직이도록 다시 계산

        Vector3 offset = movementVector * movementFactor; // x, y, z 각 축에 movementFactor만큼 곱해주기
        transform.position = startingPosition + offset;
    }
}
  • 움직일 object 정한 뒤 옵션값 조정



Protect Against NaN Error

void Update()
    {
        if (period == 0f) { return; }  // 분모가 0이 되지 않도록 
        float cycles = Time.time / period;  // 시간에 따라 계속 증가함

        const float tau = Mathf.PI * 2;  // 6.283의 일정한 값
        float rawSinWave = Mathf.Sin(cycles * tau); // -1~1로 움직임

        movementFactor = (rawSinWave + 1f) / 2f;  // 0~1로 움직이도록 다시 계산

        Vector3 offset = movementVector * movementFactor; // x, y, z 각 축에 movementFactor만큼 곱해주기
        transform.position = startingPosition + offset;
    }
  • 데이터타입 float
    • 두 개의 floats는 매우 작은 양으로 다를 수 있다.
    • ==period0f가 정말 일치할지 알 수 없다.
    • 따라서 항상 허용 오차 범위를 설정해야 한다.
    • Unity의 가장 작은 floatMathf.Epsilon이다.
    • 그래서 0과 비교할 때는 Mathf.Epsilon을 사용한다.
// float의 특징으로 인해 변경된 코드 //

void Update()
    {
        if (period <= Mathf.Epsilon) { return; }  // period에 0이 입력되어도 아무런 error가 발생하지 않음
        float cycles = Time.time / period;  // 시간에 따라 계속 증가함

        const float tau = Mathf.PI * 2;  // 6.283의 일정한 값
        float rawSinWave = Mathf.Sin(cycles * tau); // -1~1로 움직임

        movementFactor = (rawSinWave + 1f) / 2f;  // 0~1로 움직이도록 다시 계산

        Vector3 offset = movementVector * movementFactor; // x, y, z 각 축에 movementFactor만큼 곱해주기
        transform.position = startingPosition + offset;
    }



Quit Application

  • 게임 종료 방법 추가
  • QuitApplication.cs 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class QuitApplication : MonoBehaviour
{
    void Start()
    {
        if(Input.GetKeyDown(KeyCode.Escape))
        {
            Debug.Log("we pushed escape");
            Application.Quit();
        }
    }

    void Update()
    {
        
    }
}
  • Rocket에 위 script 추가




How To Build & Publish A Game

PC, Mac & Linux Standalone

  • [File] - [Build Settings]
    • Build할 Scene 선택
    • Platform 선택
    • Build

WebGL

  • WebGL이 Editor에 설치되지 않았을 경우 Unity Hub에서 WebGL Build Support 다운
  • [Player Setting] - [Publishing Settings] - [Compression Format] Disabled로 설정

0개의 댓글