캐릭터 이동을 구현하는 Script를 Player Character 게임오브젝트에 추가하자.
이 예제에서는, 입력과 액터를 나누었다.
입력: PlayerInput 스크립트는 Player의 입력을 감지하고 다른 Component에 알려주는 스크립트이다.
액터: PlayerMovement 스크립트는 입력에 다라 캐릭터를 이동/회전시킨다.
입력감지코드와 입력처리코드를 분리함으로써, 코드관리가 쉬워진다.
예를 들어, 해당 게임이 PC에서 모바일 or 콘솔로 이식할 때, 입력감지코드만 변환하여 사용가능해진다.
PlayerInput.cs
PlayerInput 스크립트는 public float로 선언된 변수를 통해 입력결과를 외부로 노출시킨다.
ChatGPT)
get; private set;은 C#에서 자동 구현 속성(auto-implemented property)을 정의할 때 사용됩니다. 이 구문은 속성의 값을 읽을 수는 있지만, 클래스 외부에서는 값을 설정할 수 없도록 합니다.
• public float move : move 속성은 float 타입이며, 외부에서 접근할 수 있습니다.
• { get; private set; } : get 접근자는 public이므로 외부에서 move 속성의 값을 읽을 수 있습니다. 하지만 set 접근자는 private이므로 클래스 내부에서만 값을 설정할 수 있습니다.
입력축/버튼이름 설정
먼저 입력축 및 Button이름을 string으로 선언한다.
public string moveAxisName = "Vertical"; // 앞뒤 움직임을 위한 입력축 이름
public string rotateAxisName = "Horizontal"; // 좌우 회전을 위한 입력축 이름
public string fireButtonName = "Fire1"; // 발사를 위한 입력 버튼 이름
public string reloadButtonName = "Reload"; // 재장전을 위한 입력 버튼 이름
Edit - Project Settings > Input > InputManager 에서 입력축이름과 버튼이름을 확인할수있다.
Horizontal과 Vertical은 Bullet Maze 게임 제작에서 이미 사용해보았, Fire1과 Reload 버튼을 확인해보자.
Fire1은 Ctrl, 왼쪽마우스버튼(mouse 0)
Reload는 국룰 R이 할당되어있다.
Property 설정: Auto Implemented Property 사용
Property는 앞서 설명하였듯, getter-setter 디자인패턴으로 선언되어있다.
Property는 변숫값을 읽거나 쓰는 과정에서 유연한 처리를 삽입할 수 있는 클래스 멤버이다.
변수처럼 보이지만, 메서드이다(자동구현 프로퍼티가 무엇인지는 이 글을 참고하자.)
아무튼, 이 코드는 아래 코드와 동일하다.
public float move{get; private set;}
EQUALS
public float move{
get {return m_move;}
private set {m_move = value;}
}
private float m_move;
참고로 value는 C#에서 set의 접근자로 사용되는 예약어이다.
이렇게 자동구현프로퍼티로 구현함으로써,
move/rotate/fire/reload 변수는
외부에서는 읽기만 가능하고, 값 할당은 PlayerInput 클래스 내부에서만 가능하게 되었다.
자, 그럼 내부에서 값 할당을 해야겠지? Update함수를 까보자.
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);
}
GamaManager는 아직 구현하지 않았다. 차후 구현할 예정이다.
이전 예제와 마찬가지로 GameManager는 singleton으로 구현되어있으며, GameManager.instance 로 선언된 것을 확인함으로써 확신할 수 있다.
또한, GameOver상태라면 강제로 Input을 초기값으로 할당해서, 플레이어가 움직이는걸 막았다.
PlayerMovement 스크립트
입력에 맞춰 플레이어를 이동하고, 적절한 애니메이션을 재생하는 기능을 담당한다.
private PlayerInput playerInput; // 플레이어 입력을 알려주는 컴포넌트
에는 아까 Input 스크립트의 PlayerInput이 그대로 들어가는걸 확인할수있다.
일단 Start() 메서드에서, 필요한 각 Component를 Player Character 오브젝트에서 GetComponent() 메서드로 찾아 변수에 할당하자.
private void Start() {
// 사용할 컴포넌트들의 참조를 가져오기
playerInput = GetComponent<PlayerInput>();
playerRigidbody = GetComponent<Rigidbody>();
playerAnimator = GetComponent<Animator>();
}
FixedUpdate 메서드를 사용하자.
FixedUpdate는 Update처럼, 주기적으로 자동실행되는 유니티 이벤트 메서드이다.
Update는 화면갱신주기마다 실행되지만, FixedUpdate는 물리적 갱신주기(default: 0.02초) 마다 실행된다.
따라서 FixedUpdate에서 이동/회전을 실행하는게 Update보다 오차확률이 줄어든다.
(컴퓨터 성능과 관계없이, 경과시간에 비례해서 이동하기 때문에.)
참고)
Time.deltaTime은 Update() 메서드의 실행간격(=프레임의 갱신주기)를 표시하지만,
Time.fixedDeltaTime은 FixedUpdate() 메서드의 실행간격(=물리정보 갱신주기)를 표시한다.
Unity에서는 편의를 위해, FixedUpdate() 내부에서 Time.deltaTime에 접근한다면 자동으로 Time.fixedDeltaTime을 출력하게 되어있다.
유니티피셜)
2. 프레임 드랍: 만약 게임이 매우 느리게 실행되어 FixedUpdate가 제때 호출되지 못하면, Unity는 물리 시뮬레이션을 보상하기 위해 여러 번의 FixedUpdate 호출을 한 프레임 내에서 수행할 수 있습니다. 이 경우 Time.deltaTime은 Time.fixedDeltaTime과 다를 수 있습니다.
Move 파라미터 값에 따라서, Animator가 영향을 받아야 하므로,
playerAnimator.SetFloat 를 통해 Move 파라미터값을 변경하게끔 한다.
// FixedUpdate는 물리 갱신 주기에 맞춰 실행됨
private void FixedUpdate() {
// 물리 갱신 주기마다 움직임, 회전, 애니메이션 처리 실행
Rotate();
Move();
playerAnimator.SetFloat("Move", playerInput.move);
}
이제 Move() 함수를 보자.
// 입력값에 따라 캐릭터를 앞뒤로 움직임
private void Move() {
Vector3 moveDistance = playerInput.move * transform.forward * moveSpeed * Time.deltaTime;
playerRigidbody.MovePosition(playerRigidbody.position + moveDistance);
}
한 프레임동안 움직일 거리를 재고, Game Object의 위치를 변경하자.
MovePosition은 이동할 위치를 입력받는다. 상대위치가 아니라 전역위치임에 주의하자.
즉, MovePosition(0,0,3)은 0,0,3만큼 이동하는게 아니라, 0,0,3 으로 이동하는거다.
transform 컴포넌트를 사용해도 동일한 이동을 구현할수있는데,
transform.position = transform.position + moveDistance;
이렇게 할 시 문제점은, transform 위치값을 직접 변경하면 물리처리를 무시하고 위치를 덮어쓴다는 것이다.
반면 RigidBody의 MovePosition 메서드를 사용하여 위칫값을 변경하면, 다른 Collider가 이동경로에 존재할 시 밀어내거나 밀려나는 물리처리가 실행된다. 따라서 벽 반대편으로 텔레포트하는 사고를 막을 수 있다.
이제 Rotate 함수를 보자.
private void Rotate() {
float turn = playerInput.rotate * rotateSpeed * Time.deltaTime;
playerRigidbody.rotation = playerRigidbody.rotation * Quaternion.Euler(0, turn, 0f);
}
Move와 비슷하지만, 쿼터니언 곱으로 회전을 구현한다는점에 유의하자.
먼저 frame당 회전할 각도를 turn 변수에 저장하고,
Quaternion.Euler 함수를 통해 회전변수의 상대적 추가회전을 구현한다.
즉, playerRigidbody.rotation = playerRigidbody.rotation * Quaternion.Euler(0, turn, 0f); 은
현재 회전상태에서 (0, turn, 0) 만큼 더 회전한 상태를 나타내는 쿼터니언이다.
마찬가지로 playerRigidBody.rotation 에 할당하면 물리처리를 감안하여 회전하게 된다.
반면 transform.rotation에 할당하면, 물리처리를 무시하고 회전하게 된다.
Scene상에서, WASD를 누르면 잘 뛰는것을 확인할수있다.
'Development > Unity Engine' 카테고리의 다른 글
[Retro유니티] 인터페이스를 통한 IDamageable 구현 및 Guns 객체생성 (0) | 2024.12.17 |
---|---|
[Retro유니티] Cinemachine 을 통한 연출구현 (0) | 2024.12.15 |
[Retro유니티] Upper Body 레이어 확인, Avatar Mask 적용 (0) | 2024.12.13 |
[Retro유니티] 플레이어 캐릭터 생성 및 Blend Tree Animator 분석 (0) | 2024.12.11 |
[Retro유니티] LightMap과 Baking (0) | 2024.12.09 |