유니티에서 이미지를 import하면 기본적으로 PPU(Pixels Per Unit)값이 100이다.

PPU값이 100이라는 의미는 Unity의 단위인 1유닛 안에 100 픽셀이 들어간다는 뜻이다. 즉, PPU값이 작을수록 1유닛 안에 차지하는 픽셀수가 줄어들기 때문에 이미지가 픽셀화 되며 커진다. PPU값은 이미지들끼리 통일해주는 것이 좋으며 높아질수록 고해상도가 되어 성능에도 영향을 미치기 때문에 값을 고려해야한다.

 

GameObject는 앞서 말한 것처럼 부모-자식 관계를 가지며, Transform 또한 마찬가지다.

부모인 Transform의 Position값을 바꾸면 자식인 MainSprite의 값도 따라온다. 부모의 절대 위치(World Position)가 기준이 되어 자식의 상대 위치(Local Position)가 절대 위치와 달라질 수 있는 것이다.

예를 들어, 자동차를 만든다면 핸들, 바퀴 등의 전부 다른 오브젝트들을 똑같이 이동해야하는 경로를 계산하여 만들어주기 번거롭기 때문에 자동차라는 큰 틀 아래에 핸들은 핸들 위치에, 바퀴는 바퀴 위치에 부품들을 자식으로 넣어주는 것이다.

포지션을 얻을 때 .position을 해주면 절대 위치를 가져오고, .localpostion을 해야 상대 위치를 가져온다.

 

유니티에서 오브젝트에 연결해준 스크립트를 열면 MonoBehaviour를 상속 받은 것을 볼 수 있다.

Start는 가장 첫 프레임에 동작하며 Update는 매 프레임마다 동작한다.

 

 

입력과 캐릭터 이동

캐릭터를 이동시키는 방법에는 여러가지가 있다.

Transform을 직접 좌표로 움직이는 방법이 있고 Rigidbody를 사용해서 물리적인 이동 처리를 하는 방식이 있다.

 

먼저 Edit - Project Settings - Player - Other Settings에서 Configuration의 Active Input Handling을 Both로 바꿔주자.

 

Transform을 직접 좌표로 움직이는 방법(Legacy - 절차지향)

Update()에 해당 코드를 입력한다.

float x = Input.GetAxis("Horizontal");
float y = Input.GetAxis("Vertical");

transform.position += new Vector3(x, y);

GetAxis가 가로 "Horizontal"와 세로 "Verticla"을 지칭하며 가져오고, 절대 위치인 transform.position에 새로운 Vecter3()를 만들며 x, y 좌표를 더해준다.

 

Edit - Project Settings - Input Manager에서 보면 "Horizontal"은 Left와 Right를, "Verticla"은 Down와 Up 방향을 자동으로 처리해준다. 즉, Axis(축) 값을 가지고 왼쪽, 오른쪽, 위, 아래 라는 값을 반환 받고 있는 것이다.

필요하다면 이곳에서 엑세스를 더 추가하거나 Input 클래스에 있는 GetKeyDown으로 각각의 개별적인 키 입력을 받을 수 있다.

 

그런데 이대로 실행하면 캐릭터를 이동했을 때, Update에 입력해주었기 때문에 매 프레임마다 호출하여 1초에 200~300만큼 이동하게 되어 너무 빠르다.

280프레임

성능이 다른 컴퓨터마다 프레임이 다르므로 속도 또한 제각각일 것이다. 그렇기 때문에 deltaTime을 곱해준다.

float x = Input.GetAxis("Horizontal");
float y = Input.GetAxis("Vertical");

transform.position += new Vector3(x, y) * Time.deltaTime;

deltaTime은 이전 프레임과 현재 프레임 사이의 시간이다. 게임의 프레임 속도에 상관없이 일정한 시간 간격으로 동작하게 해준다.

10프레임과 60프레임이 있다고 가정했을 때, 프레임과 프레임 사이가 10프레임은 10/1, 60프레임은 60/1일 것이다. 그런데 1초가 지나면 10/1 * 10, 60/1 * 60이 되어 결과적으로 1이 되기 때문에 같아진다. 10프레임은 1초 동안 10번 쪼개서 움직이고 60프레임은 1초 동안 60번 쪼개서 움직인 것이다. 그렇기 때문에 deltaTime을 곱하는 것이다.

deltaTime은 실제 누적 시간 또한 처리할 수 있다.

GetAxis는 0부터 1까지 도달하는 가속도가 붙는 느낌을 주며, GetAxisRaw는 0, 1, -1 딱 떨어지는 값으로 주기 때문에 스무싱 값이 없다.

float x = Input.GetAxisRaw("Horizontal");
float y = Input.GetAxisRaw("Vertical");

transform.position += new Vector3(x, y) * Time.deltaTime;

 

speed를 public값으로 주면 동기화 되어 유니티 Inspector에서 마음대로 조절할 수 있다.

public class TopDownCharacterController : MonoBehaviour
{
    public float speed = 5f;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");

        transform.position += new Vector3(x, y) * speed * Time.deltaTime;
    }
}

 

 

만약 중요한 값이기 때문에 public으로 하고 싶지 않다면 [SerializeField]로 강제적으로 동기화 시켜도 똑같이 동작한다.

public class TopDownCharacterController : MonoBehaviour
{
    [SerializeField] private float speed = 5f;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");

        transform.position += new Vector3(x, y) * speed * Time.deltaTime;
    }
}

 

 

Rigidbody를 사용한 물리적인 이동 처리 방법(Input System - 객체지향)

이벤트 Action을 이용해보자.

public class TopDownCharacterController : MonoBehaviour
{
    // event : 외부에서 호출하지 못하게 막아주는 기능
    public event Action<Vector2> OnMoveEvent;
    public event Action<Vector2> OnLookEvent;

    public void CallMoveEvent(Vector2 direction)
    {
        OnMoveEvent?.Invoke(direction); //?. : OnLookEvent이 Null이 아닐 때만 동작
    }

    public void CallLookEvent(Vector2 direction)
    {
        OnLookEvent?.Invoke(direction);
    }
}

?.은 Move 이벤트나 Look 이벤트에 액션이 잘 걸려있을 때만 동작하도록 한다. event는 이벤트들을 외부에서 호출(Invoke)하지 못하게 막아주는 기능이다. 위의 코드에서 보다시피 본인만 호출하고 있다.

 

이제 플레이어의 Input을 처리할 수 있도록 만들어주기 위해 유니티에 Input System을 추가적으로 설치 해준다. Input System은 조이스틱이나 다른 컨트롤러 등까지 호환되기 때문에 자주 사용된다.

Window - Package Manager에서 Uniy Registry에 보이는 Input System을 Install 한다.

유니티를 재시작한 뒤, Asset에 Input 폴더를 생성하여 Create - Input Action을 추가한다.

 

추가한 컨트롤러를 열고 Add Control Scheme에서 +를 눌러 키보드와 마우스를 생성한다.

혹시나 클릭이 안된다면 해상도 문제일 수 있다.

Action Type을 Value로, Control Type을 Vector2로 바꿔준다. 이 액션을 취했을 때, 돌려받는 값이 Vector2인 것이다.

 

New action에서 아까 추가한 Up&Down&Right&Left를 누르고 키를 설정해준다.

여기까지 Action을 연결할 준비가 되었다.

 

입력 처리를 하기 위해 Player와 연결 시켜줄 스크립트를 생성하여 Input Action 스크립트를 상속시킨 뒤, 유니티 자체 엔진 클래스인 Camera를 객체로 불러온다.

public class PlayerInputController : TopDownCharacterController
{
    private Camera _camera;

    private void Awake()
    {
        _camera = Camera.main; // Camera 씬에서 태그가 main인 코드를 찾아오겠다 = 마우스와 캐릭터 포지션 위치 확인
    }
    
    private void OnMove(InputValue value)
    {
        Debug.Log("OnMove" + value.ToString());
    }

    private void OnLook(InputValue value)
    {
        Debug.Log("OnLook" + value.ToString());
    }

    private void OnFire(InputValue value)
    {
        Debug.Log("OnFire" + value.ToString());
    }
}

 

Send 메세지 방식은 만들어진 액션에 On을 붙이면 실행됐을 때, 돌려받는 형식이다. 확인하기 위해 Debug 해준다.

Player 오브젝트에 방금 작성한 스크립트와 Player Input을 컴포넌트 한 뒤, Actions에 설정해뒀던 방향 컨트롤러를 끌어다 놓고 플레이하여 방금 설정한 값들(wasd, 방향키, 마우스 등)을 입력하면 Console창에 뜬다.

 

값이 입력되는 것을 확인하고 다시 돌아와서 코드를 완성시킨다.

normalized를 하는 이유는 방향키를 두 개 동시에 입력했을 때, x와 y축 값이 합쳐져 대각선 방향의 속도가 빨라지기 때문에 그것을 방지하기 위해 단위 벡터로 만들어 주는 것이다.

private void OnMove(InputValue value)
{
    // Debug.Log("OnMove" + value.ToString());
    Vector2 moveinput = value.Get<Vector2>().normalized;
    CallMoveEvent(moveinput); // 움직일 때 벡터 값을 불러온다
}

private void OnLook(InputValue value)
{
    // Debug.Log("OnLook" + value.ToString());
    Vector2 newAim = value.Get<Vector2>(); // 마우스 포지션을 받아온다
    Vector2 worldPos = _camera.ScreenToWorldPoint(newAim); // 마우스 위치를 절대 위치로 바꾼다
    newAim = (worldPos - (Vector2)transform.position).normalized; // 캐릭터와 마우스 커서까지의 거리와 방향

    //if (newAim.magnitude >= .9f) // magnitude : 크기(normalized 했기 때문에 1) 혹시 모를 오류(0, 0처럼 0으로 나누는 경우) 방지를 위해 제한
    //{
    //    CallLookEvent(newAim);
    //} // 이미 normalized 해주었기 때문에 안해도 됨. 강의에서 실수한 듯!
}

private void OnFire(InputValue value)
{
    Debug.Log("OnFire" + value.ToString());
}

Move, Look, Fire 이벤트들을 어떤 행동할 때 알려주는 것을 걸어놓는 것을 '구독한다(Subscribe)'라고 표현한다. CallMoveEvent에서 이벤트 액션들을 Invoke하면 구독한 것들에 신호를 준다. 이것을 Observer 패턴이라고 한다.

 

이제 이동을 처리하는 스크립트를 만들어준다.

GetComponent는 Inspector 안에서 Component끼리 서로 인지할 수 있는 방법으로 상위의 Compenent인 TopDownCharacterController를 가져온다. 같은 방식으로 Rigidbody를 가져온다. 키보드 값이 입력되면 ApplyMevment에서 direction을 받아오고 속도를 곱해서 가속도를 저장해준다. 그리고 rigidbody가 그 가속도만큼 움직인다.

FixedUpdate()는 Update()보다 호출이 느리기 때문에 물리처리가 끝난 이후에 호출이 되도록 한다.

 private TopDownCharacterController _controller;

 private Vector2 _movementDirection = Vector2.zero;
 private Rigidbody _rigidbody;

 private void Awake()
 {
     _controller = GetComponent<TopDownCharacterController>(); // GetComponent : Inspector 안의 Compenent를 가져온다
     _rigidbody = GetComponent<Rigidbody>();
 }

 private void Start()
 {
     _controller.OnMoveEvent += Move; // Move 구독
 }

 private void FixedUpdate() // 물리처리 이후에 이동처리 메서드 호출
 {
     ApplyMovment(_movementDirection);
 }

 private void Move(Vector2 direction)
 {
     _movementDirection = direction; // 키보드 입력값 설정
 }

 private void ApplyMovment(Vector2 direction) // 이동처리
 {
     direction = direction * 5; // 5라는 속도로 이동

     _rigidbody.velocity = direction;
 }

 

_controller.OnMoveEvent에 Move를 구독했기 때문에 키보드를 눌렀을 때, PlayerInputController의 상위인 TopDownCharacterController에 전달하고 거기서 구독하고 있는 TopDownMovement가 실행된다.

Player Input에서 키보드 입력을 받으면 CharacterController를 부르고 구독하고 있는 Move로 movement가 들어오게 된다.

 

 

Unity(유니티)
  • 에셋(Asset)

- 게임에 필요한 모든 리소스(이미지, 사운드, 모델, 코드 등).- 프로젝트의 에셋 폴더에 저장된다.

  • 씬(Scene)

- 게임의 각 장면 또는 화면.

- 메뉴 씬, 게임 플레이 씬, 엔딩 씬 등 게임의 특정 부분을 담당한다.

  • 게임 오브젝트(GameObject)

- 씬에 배치되는 모든 요소.

- 에셋을 이용하여 씬에 생성되며, 게임의 동작과 상호작용을 담당한다.

- 게임 오브젝트는 계층 구조로 구성되어 부모-자식 관계를 가지며, 이를 통해 그룹화하고 조작한다.

 

Unity 인터페이스

  • Scene 뷰 : 씬의 3D 또는 2D 뷰. 씬 구성 요소 편집
  • Game 뷰 : 게임이 실제로 실행되는 뷰. 플레이어가 게임을 플레이하는 화면을 실시간으로 확인한다.
  • Hierarchy 뷰 : 현재 씬의 게임 오브젝트 계층 구조 표시 및 편집
  • Inspector 뷰 : 선택된 게임 오브젝트의 속성 및 구성 요소 편집
  • Project 뷰 : 프로젝트의 에셋 표시 및 관리
  • Console 뷰 : 게임 실행 중의 로그 및 메시지 표시 (* 오류 확인을 위해 켜두는 것이 좋다.)

 

마우스 조작

  • 마우스 왼쪽 클릭 : 게임오브젝트 선택
  • 마우스 오른쪽 클릭 : w,s,a,d,q,e 키로 씬 뷰를 1인칭 이동가능, 드래그시 씬 뷰 회전
  • 마우스 스크롤 : 줌 인 & 아웃
  • 마우스 스크롤 클릭 : 씬 뷰 이동
  • alt + 마우스 왼쪽 드래그 : 씬 뷰 화면 중심 회전
  • alt + 마우스 오른쪽 드래그 : 줌 인 & 아웃

 

툴바

  • Hand Tool (손 도구, 단축키 : Q) : 씬 뷰를 이동시킴.
  • Move Tool (이동 도구, 단축키 : W) : 게임오브젝트 이동
  • Rotate Tool (회전 도구, 단축키 : E) : 게임오브젝트 회전
  • Scale Tool (스케일 도구, 단축키 : R) : 게임오브젝트 스케일 조절
  • Rect Tool (사각형 도구, 단축키 : T) : 게임오브젝트 스케일을 사각형 방향으로 조절함.
  • Transform Tool (좌표변형 도구, 단축키 : Y) : MoveTool, RotateTool, ScaleTool 을 동시에 사용함.

 

유니티 스크립트 작성 방법

오브젝트에 연결하여 사용하려면 기본적으로 MonoBehaviour를 상속 받은 상태로 클래스를 작성한다. 그 외의 용도로 사용하는 경우에는 굳이 상속 받지 않아도 된다. Start(), Update(), FixedUpdate() 등 유니티에서 제공하는 함수들은 오버라이딩 하여 사용한다.

 

스크립트 라이프 사이클

게임 오브젝트가 살아있는 생명 주기 동안 동작하는 스크립트들

  • Awake : 게임 오브젝트가 생성될 때 호출되는 메서드. 주로 초기화 작업을 수행한다.
  • Start : 게임 오브젝트가 활성화되어 게임 루프가 시작될 때 호출되는 메서드. 초기 설정 및 시작 작업을 수행한다.
  • Update : 매 프레임마다 호출되는 메서드. 게임 로직의 주요 업데이트가 이루어진다. 매 프레임 호출되는 만큼 불필요한 계산을 피하고 최적화하기 위해 사용하지 않으면 지워주는 것이 좋다.
  • FixedUpdate : 물리 엔진 업데이트 시 호출되는 메서드. 물리적인 시뮬레이션에 관련된 작업을 처리할 때 사용된다.
  • LateUpdate : Update 메서드 호출 이후에 호출되는 메서드. 다른 오브젝트의 업데이트가 완료된 후에 작업을 수행하는 데 유용하다.
  • OnEnable : 게임 오브젝트가 활성화될 때 호출되는 메서드.
  • OnDisable : 게임 오브젝트가 비활성화될 때 호출되는 메서드.
  • OnDestroy : 게임 오브젝트가 파괴될 때 호출되는 메서드. 자원 정리 및 해제 작업을 수행한다.

이벤트 함수의 실행 순서

 

이벤트 함수의 실행 순서 - Unity 매뉴얼

Unity 스크립트를 실행하면 사전에 지정한 순서대로 여러 개의 이벤트 함수가 실행됩니다. 이 페이지에서는 이러한 이벤트 함수를 소개하고 실행 시퀀스에 어떻게 포함되는지 설명합니다.

docs.unity3d.com

 

컴포넌트

게임 오브젝트에 부착하는 기능 모듈. 각각 기능들로 구현을 만드는 것.

  • Transform : 게임 오브젝트의 위치, 회전, 크기 등을 조정
  • Rigidbody : 물리적인 효과를 게임 오브젝트에 적용
  • Collider : 충돌 감지 처리
  • SpriteRenderer : 2D 그래픽 표시
  • AudioSource : 사운드 재생

사용자가 필요에 따라 컴포넌트를 직접 작성하고 추가할 수도 있다. 우리가 작성한 스크립트 또한 컴포넌트이다. 이를 통해 게임의 특정한 동작이나 기능을 개발자가 원하는 대로 커스터마이즈할 수 있다.

 

그 외

Collider

- Is Trigger : 물리적인 충돌이 일어났을 때, 실제로 물리충돌을 할 것인지 여부(켜져있으면 충돌을 인지만 하고 통과된다.)

Rigidbody2D

- Gravity Scale : 중력을 가지고 싶지 않으면 0으로 조절해준다.

- Constraints - Freeze : 충격을 받고 날아거나 이동할 때, 회전하거나 등 물리적으로 처리할 것인지 여부

 

Material

SpriteRenderer - Material : 눈에 보이는 재질

Rigidbody - Material : 물질적인 재질

 

 

+ Recent posts