본문 바로가기

Unity/▶ Game Development: Zombie Game

[Zombie Game] #1. 캐릭터 입력 제어 및 이동 구현

728x90
반응형

[ 메서드 정리 포스팅 ]

3D 게임에서 일반적으로 사용되는 메서드와 함수는 아래의 포스팅에서 한번에 다루도록 한다.

https://udangtangtang-cording-oldcast1e.tistory.com/209

 

[Unity] ✨3D 게임 핵심 메서드 및 응용✨

라이팅 설정 Light Map(라이트 맵) Level Art 프리팹을 씬에 추가하면 컴퓨터가 느려지거나 팬이 도는 현상이 발생하는데 이는 유니티 내부에서 라이트 맵과 라이팅 정보를 포함한 라이팅 에셋을 굽

udangtangtang-cording-oldcast1e.tistory.com

캐릭터 입력 제어

캐릭터의 이동 구현 파트에서는 이전과는 다르게 입력을 다루는 소스와 입력에 따른 캐릭터의 움직임을 만드는 소스를 나누어 작성한다.

 

PlayerInput 플레이어의 입력을 감지하고 이를 다른 컴포넌트에 전송
PlayerMovement 플레이어의 입력에 따라 캐릭터를 앞뒤로 움직이고 좌우로 회전

이렇게 입력과 출력을 나누면서 코드의 수정 범위를 줄일 수 있고 불필요한 코드 사용을 막을 수 있다.

PlayerInput 스크립트 (1) : 입력축과 입력버튼

가장 먼저 입력축과 버튼을 string 변수로 선언한다.

 

public string moveAxisName = "Vertical"; // 앞뒤 움직임을 위한 입력축 이름
public string rotateAxisName = "Horizontal"; // 좌우 회전을 위한 입력축 이름

public string fireButtonName = "Fire1"; // 발사를 위한 입력 버튼 이름
public string reloadButtonName = "Reload"; // 재장전을 위한 입력 버튼 이름

위 코드에서 Vertical Horizontal 입력 축인 반면 Fire1 Reload 입력버튼이다.

[ 입력축 ]: Vertical / Horizontal

아래 링크 참조

 

[Unity] ⭐️유니티 용어 및 메서드 모음⭐️

클래스와 상속 CreatedClass name = new CreatedClass(); 접근제한자 · public: 클래스 외부에서 멤버에 접근 · private: 클래스 내부에서만 멤버에 접근 · protected: 클래스 내부와 파생 클래스에서만 멤버에 접

udangtangtang-cording-oldcast1e.tistory.com

[ 입력 버튼 ]: Fire1 / Reload

축은 감지된 입력값으로 숫자가 반환되는 반면 입력 버튼은 눌렸을 때 true 버튼을 누르지 않았을 때 false를 반환한다.


Fire1: 입력 매니저에 기본 추가되어 있는 입력버튼으로 좌 Ctrl과 왼쪽 마우스가 할당됨.

Reload는 사용자 설정 입력버튼이다.

 

PlayerInput 스크립트 (2) : 프로퍼티

감지할 입력축과 입력 버튼에 대한 변수 다음에는 감지된 입력값을 나태날 프로퍼티가 있다.

[ 프로퍼티 ]

프로퍼티는 변숫값을 읽거나 쓰는 과정에서 유연한 처리를 삽입할 수 있는 클래스 멤버이다.

이때 프로퍼티는 변수처럼 보이나, 변수가 아닌 특수한 형태의 메서드임을 주의하자!

 

아래 [더보기]의 예시를 확인하자.

 

아래의 예시는 디스크의 용량을 기록하는 volumeInfo 클래스로, 바이트 단위로 디스크의 용량을 계산하며 다른 바이트의 단위로의 변환을 제공한다.

더보기
public class volumeInfo{
    public float megaBytes{
        get{ return m_bytes * 0.000001f; }
        set{
            if(valsue <= 0){
                m_bytes = 0;
            }else{
                m_bytes = value * 100000f;
            }
        }
    }

    public float killoBytes{
        get{ return m_bytes * 0.001f; }
        set{
            if(valsue <= 0){
                m_bytes = 0;
            }else{
                m_bytes = value * 1000f;
            }
        }
    }

    public float bytes{
        get{ return m_bytes; }
        set{
            if(valsue <= 0){
                m_bytes = 0;
            }else{
                m_bytes = value;
            }
        }
    }

    private float m_bytes = 0;
}

더보기란의 코드를 이용해 새로운 디스크 용량을 기록하고 출력하는 코드를 생생해보자.

volumeInfo info = new volumeInfo();//새로운 디스크 용량 정보 생성: 참조 변수

volumeInfo.bytes = 100000;//bytes의 set 실행(bytes 단위로 생성)
Debug.Log(info.killoBytes);//킬로바이트로 변환 출력
Debug.Log(info.megaBytes);//메가 바이트로 변환 출력

info.megaBytes = 4;//megaBytes의 set실행
Debug.Log(info.bytes);//바이트 반위로 변환 출력

위 코드를 보면 새로운 참조 변수를 생성하고 특정 바이트에 값을 선언(=기호 이용)하는 것은 set이 실행되어 값을 초기화하고 값을 선언하는 것이 아닌 값을 가져오려고 하는 경우 get이 실행되어 m_bytes에 변환하기 위한 수를 곱한 값이 return 된다.

 

이를 표로 정리하면 아래와 같다.

 

상황 사용되는 접근자 예시
특정 바이트에 값을 선언(=기호 이용) set volumeInfo.bytes = 100000;
값을 참조(가져옴) get Debug.Log(info.killoBytes)

이때 set 함수에서는 trash 변수를 처리하기 위한 조건문이 존재하는데, 0보다 작은 값인 경우 쓰레기 값이자 예상치 못한 연산이 발생할 수 있으므로 1차적으로 값을 걸러준다.

 

이처럼 프로퍼티를 이용하면 여러 단위의 값으로 즉시 출력할 수 있으며 데이터를 안전하게 다루는데 도움이 된다.

또한 원하는 접근자만 private 클래스로 범위를 제한하여 개발자가 원하는 경우 입력 혹은 출력만 접근하게 설정할 수 있다.

[ 스크립트 내의 프로퍼티 ]

사용할 프로퍼티는 아래와 같다.

 

public float move { get; private set; } // 감지된 움직임 입력값
public float rotate { get; private set; } // 감지된 회전 입력값

public bool fire { get; private set; } // 감지된 발사 입력값
public bool reload { get; private set; } // 감지된 재장전 입력값

PlayerInput 클래스에서 구현된 위와 같은 프로퍼티는 자동 구현 프로퍼티로, get과 set의 접근 권한을 분리하는 것 이외의 처리가 필요하지 않을 때 사용한다.


자동 구현 프로퍼티: get과 set의 접근 권한을 분리하는 것 이외의 처리가 필요하지 않을 때 사용

 

PlayerInput 클래스에 구현된 move 프로퍼티를 펼치면 아래와 같다.

public float move{
	get { return m_move; }
    private set { m_move = value; }
    }

private float m_move;

쉽게 말해, 위에서 사용된 표현은 외부에서는 값을 출력(접근)할 수 있지만 값의 설정(초기화)은 PlayerInput 내부에서만 설정할 수 있도록 권한 범위를 설정했다는 뜻이다.

PlayerInput 스크립트 (3) : 입력 확인

update() 메서드 상단에서는 게임매니저가 씬에 존재하며 게임오버 상태가 아닌 경우 입력감지 변수를 초기화한다.

 

다음으로는 입력을 감지하고 감지된 입력값을 프로퍼티에 할당한다.

 

move = Input.GetAxis(moveAxisName); // move에 관한 입력 감지
rotate = Input.GetAxis(rotateAxisName); // rotate에 관한 입력 감지

fire = Input.GetButton(fireButtonName); // fire에 관한 입력 감지

reload = Input.GetButtonDown(reloadButtonName); // reload에 관한 입력 감지

입력에 대한 메서드는 오른쪽 링크를 참조하자. [링크]

전체 코드는 아래 [더보기]를 확인한다.

더보기
using UnityEngine;

// 플레이어 캐릭터를 조작하기 위한 사용자 입력을 감지
// 감지된 입력값을 다른 컴포넌트들이 사용할 수 있도록 제공
public class PlayerInput : MonoBehaviour {
    public string moveAxisName = "Vertical"; // 앞뒤 움직임을 위한 입력축 이름
    public string rotateAxisName = "Horizontal"; // 좌우 회전을 위한 입력축 이름
    public string fireButtonName = "Fire1"; // 발사를 위한 입력 버튼 이름
    public string reloadButtonName = "Reload"; // 재장전을 위한 입력 버튼 이름

    // 값 할당은 내부에서만 가능
    public float move { get; private set; } // 감지된 움직임 입력값
    public float rotate { get; private set; } // 감지된 회전 입력값
    public bool fire { get; private set; } // 감지된 발사 입력값
    public bool reload { get; private set; } // 감지된 재장전 입력값

    // 매프레임 사용자 입력을 감지
    private void Update() {
        // 게임오버 상태에서는 사용자 입력을 감지하지 않는다
        if (GameManager.instance != null && GameManager.instance.isGameover)
        {
            move = 0;
            rotate = 0;
            fire = false;
            reload = false;
            return;
        }

        // move에 관한 입력 감지
        move = Input.GetAxis(moveAxisName);
        // rotate에 관한 입력 감지
        rotate = Input.GetAxis(rotateAxisName);
        // fire에 관한 입력 감지
        fire = Input.GetButton(fireButtonName);
        // reload에 관한 입력 감지
        reload = Input.GetButtonDown(reloadButtonName);
    }
}

캐릭터 이동 구현

PlayerMovement 스크립트는 플레이어 입력에 맞춰 플레이어 캐릭터를 이동하고 적절한 애니메이션을 재생한다.

먼저 해당 스크립트 내에서 사용할 변수와 컴포넌트/리지드바디/애니메이터 변수를 선언하고 start 매서드에서 이를 연결한다.

PlayerMovement 스크립트 

[ 변수 선언과 초기화 ]

변수 선언 코드는 아래 [더보기]를 확인한다.

 

moveSpeed 앞뒤 움직임의 속도
rotateSpeed 좌우 회전 속도

[ 움직임, 회전, 애니메이션 처리 실행 ]

// FixedUpdate는 물리 갱신 주기에 맞춰 실행됨
private void FixedUpdate() {
    // 물리 갱신 주기마다 움직임, 회전, 애니메이션 처리 실행
    // 회전 실행
    Rotate();
    //움직임 실행
    Move();

    playerAnimator.SetFloat("Move",playerInput.move);
}

move 파라미터는 애니메이터의 Movement 상태의 블렌드 트리에서 사용된다.

이때 'playerAnimator.SetFloat'문장에서 볼 수 있듯 사용자의 입력에 따라 캐릭터의 걷고 뛰는 애니메이션이 자연스럽게 변경된다.

위에서 언급했듯이 입력이 변화함에 따라 인계값이 변화하고, 이는 애니메이션의 변화로 나타남을 알 수 있다.

 

FixedUpdate는 물리 갱신 주기에 맞춰 실행되는 내장 함수로, Time.fixedDeltaTime이 물리 정보의 갱신 주기를 사용하는 것과 같다.

유니티 내부에서는 FixedUpdate() 내부에서 Time.DeltaTime에 접근할 경우 자동으로 Time.fixedDeltaTime의 값으로 출력한다.


FixedUpdate: 물리 갱신 주기에 맞춰 실행되는 내장 함수(Time.fixedDeltaTime)

 

FixedUpdate에 대한 자세한 내용은 Time 라이브러리의 fixedDeltaTime를 다룬 오른쪽 링크를 참고한다. [링크]

[ Move 메서드 ]

private void Move() {
    Vector3 moveDistance =
        playerInput.move * transform.forward * moveSpeed *Time.deltaTime;

    playerRigidbody.MovePosition(playerRigidbody.position + moveDistance);
}
거리(moveDistance) =
(사용자의 입력값/축) * { 방향(transform.forward) *  속력(moveSpeed) * 시간(Time.deltaTime) }

 

playerInput.move는 사용자의 입력값으로 이를 통해 거리계산의 조건 판단을 실행한다.

 

전진: 1 정지: 0 (거리 -> 0) 후진: -1

 


MovePosition: 이동할 Vector3의 위치를 입력받고 해당 위치로 이동

 

리지드 바디의 MovePosition 메서드는 이동할 Vector3의 위치를 입력받으며 이때의 위치는 전역 위치임에 주의한다.

따라서 변경할 위치는 "현재 플레이어의 위치(playerRigidbody.position) + 이동할 거리(moveDistance)"가 된다.

 

이때 tansform 컴포넌트를 이용해도 되나, tansform컴포넌트는 물리처리를 무시하고 실행하기 때문에 예상치 못한 버그가 발생할 수 있다. 따라서 상대 위치를 변경하고자 하는 경우는 리지드바디의 MovePosition메서드를 사용하여 물리처리를 실행하여 사고를 방지한다.

[ Rotate 메서드 ]

Rotate 메서드는 플레이어 회전에 관한 입력값 playerInput.rotate를 사용하여 캐릭터를 회전시킨다.

// 입력값에 따라 캐릭터를 좌우로 회전
private void Rotate() {
    float turn = playerInput.rotate * rotateSpeed * Time.deltaTime;
    playerRigidbody.rotation = 
        playerRigidbody.rotation * Quaternion.Euler(0,turn,0f);

}
회전각(turn) = 
(사용자의 입력값/축) * {회전 속도(rotateSpeed) * 회전 시간(Time.deltaTime) }

 

회전 각을 구했다면 회전각만큼 Y방향으로 회전을 진행한다. 이때 쿼터니언 곱 오일러 회전을 실행한다.

 

계산된 쿼터니언 회전값을 리지드바디에 할당하여 플레이어의 회전값을 변경할 수 있다. 이때 transform 컴포넌트를 사용할 수 있으나 Move 메서드에서 언급했듯이 물리 처리를 무시하기 때문에 위와 같은 방법을 사용하도록 하자.

전체 코드는 아래 [더보기]를 확인한다.

더보기
using UnityEngine;

// 플레이어 캐릭터를 사용자 입력에 따라 움직이는 스크립트
public class PlayerMovement : MonoBehaviour {
    public float moveSpeed = 5f; // 앞뒤 움직임의 속도
    public float rotateSpeed = 180f; // 좌우 회전 속도


    private PlayerInput playerInput; // 플레이어 입력을 알려주는 컴포넌트
    private Rigidbody playerRigidbody; // 플레이어 캐릭터의 리지드바디
    private Animator playerAnimator; // 플레이어 캐릭터의 애니메이터

    private void Start() {
        // 사용할 컴포넌트들의 참조를 가져오기
        playerInput = GetComponent<PlayerInput>();
        playerRigidbody = GetComponent<Rigidbody>();
        playerAnimator = GetComponent<Animator>();
    }

    // FixedUpdate는 물리 갱신 주기에 맞춰 실행됨
    private void FixedUpdate() {
        // 물리 갱신 주기마다 움직임, 회전, 애니메이션 처리 실행
        // 회전 실행
        Rotate();
        //움직임 실행
        Move();

        playerAnimator.SetFloat("Move",playerInput.move);
    }

    // 입력값에 따라 캐릭터를 앞뒤로 움직임
    private void Move() {
        Vector3 moveDistance =
            playerInput.move * transform.forward * moveSpeed *Time.deltaTime;

        playerRigidbody.MovePosition(playerRigidbody.position + moveDistance);
    }

    // 입력값에 따라 캐릭터를 좌우로 회전
    private void Rotate() {
        float turn = playerInput.rotate * rotateSpeed * Time.deltaTime;
        playerRigidbody.rotation = 
            playerRigidbody.rotation * Quaternion.Euler(0,turn,0f);

    }
}
728x90
반응형
댓글