Post

유니티3D - 1

유니티3D - 1

목표

플레이어 캐릭터 생성, 이동 구현, 카메라 회전 제어, 간단한 애니메이션

플레이어 캐릭터 생성

게임 오브젝트에 Rigidbody 컴포넌트를 이용해서 캐릭터의 움직임을 만들려 했으나 CharacterController가 더 적합한 거 같아 CharacterController를 이용해 움직임을 만들겠다. Rigidbody는 물리적객체(공, 수류탄)에 사용하는게 더 좋다고 생각하기 때문이다.

Rigidbody의 변수 설명 Mass - 질량 , Drag( 공기저항 ) - 떨어질 때 공기저항을 받아 값을 높일수록 깃털처럼 천천히 떨어진다. Angular Drag ( 회전저항 ) - 값이 커질수록 더 회전하는데 오래걸림 , Use Gravity - 중력 사용여부 Is Kinematic - 활성화하면 다른 오브젝트?의 영향으로 물리학이 적용이 안됨 (밀기, 날리기) , Interpolate(물리계산) - 물리연산을 어떤식으로 보간할지 , Collsion Detection (충돌 체크) - 기본적인 충돌 감지 방식과 정적Collider와 충돌을 감지하는데 유리한 방식과 두 방식을 번갈아 사용하는 방식이 있다. Constraints (회전, 위치 계산 설정) - 체크한 부분은 연산을 하지않는다. 내 생각엔 주로 Y축 회전만 사용하기떄문에 X,Z를 안써 연산량을 줄인다.

FixedUpdate() 와 Update() UPdate() 함수는 매 프레임 호출 ( Time.deltatime 사용 ), FixedUpdate*() 고정된 시간으로 호출 ( Time.fixedDeltaTime 사용 )
두 함수를 비교했을 때 캐릭터 이동 코드는 Update()가 맞다고 생각한다.
프레임이 밀릴경우 더 많은 이동을 해야하는 상황도 나오느데 고정된 시간의 DeltaTime으로는 매꿀수없다고 생각하기 떄문이다.
FixedUpdate()는 물리적 동기화를 사용할 때 좋은 것 같다. ex) Rigidbody를 이용한 물리 연산을 이용할 때

인풋 시스템 언리얼과 비슷하게 InputAction 으로 입력을 받는다. 이때 스키마-디바이스 매칭 누락은 프로젝트 전반의 입력 비활성로 이어질 수 있으므로 초기 설정을 확실히 한다. InputAction Player 오브젝트에 Player Input 컴포넌트를 추가하고 Behavior를 Invoke Unity Events로 설정한 뒤, 콜백을 바인딩한다. 이 과정에서 호출 여부를 반드시 로그로 확인해 초기 단계에서 문제를 조기에 드러낸다. PlayerInput함수바인딩 하지만 실행되지 않아 디버깅을하여 바인딩된 함수가 제대로 호출되는지 확인했다. 역시나 함수가 호출되지 않았고 새로운 프로젝트를 만들어 유니티에서 제공되는 Inpuc Action으로 하여 실행하니 제대로 입력이 받아졌다. 그 후에 내가 만든 Input Action으로도 잘 실행이 되어 프로젝트 설정문제인가? 싶었다. 하지만 여기서 스키마를 추가하니 똑같은 문제가 발생하였고, 어? 하면서 유니티에서 만들어준 Input Action으로 바꾸어 해도 입력이 안받아졌다.
스키마가 문제인걸 깨닫고 여기서 Device Type에 키보드를 추가하니 문제가 해결됐다.
이 원인을 찾아내면서 신기한점은 다른 Input Action에도 영향이 가는게 무서운 점이였던 것

인풋 시스템 주의점

디바이스-스키마 매칭은 입력 전체 흐름에 직결된다. 하나라도 누락되면 프로젝트 내 PlayerInput들이 활성화되지 않아 입력이 차단된다. 프로젝트 템플릿과 커스텀 InputAction 간 전환 시, 스키마 변경이 기존 액션들에 파급되니 변경 후 즉시 재검증한다.

캐릭터 이동과 회전

캐릭터 이동과 회전을 하기위해 PlayerController 컴포넌트에서 인풋 시스템에서 입력받은 액션을 제어하는 구조로 설계했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    private CharacterMovement CharacterMovement;
    private CameraMovement CameraMovement;

    private void Awake()
    {
        CharacterMovement = GetComponent<CharacterMovement>();
        CameraMovement = GetComponentInChildren<CameraMovement>();
    }

    private void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    public void OnMove(InputAction.CallbackContext context)
    {
        Vector2 input = context.ReadValue<Vector2>();
        CharacterMovement.SetMoveInput(input);
    }

    public void OnJump(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            CharacterMovement.Jump();
        }
    }

    public void OnLook(InputAction.CallbackContext context)
    {
        Vector2 input = context.ReadValue<Vector2>();
        CameraMovement.SetlookInput(input);
    }
}

이동 구현, 카메라 회전 제어 WASD 입력을 캐릭터 XZ 축 이동으로 반영한다. 카메라 회전 인풋의 상하 반전은 사용자 기대에 맞게 처리하고, 실제 회전은 카메라 자체가 아닌 부모 핸들(스프링암 역할)을 돌려 제어를 단순화한다.

1
2
3
4
5
6
7
8
9
    void Update()
    {
        rotX += lookInput.y * Sensitivity ;
        rotY += lookInput.x * Sensitivity ;

        rotX = Mathf.Clamp(rotX, -clamAngle, clamAngle);
        Quaternion rot = Quaternion.Euler(rotX, rotY, 0);
        transform.rotation = rot;
    }

카메라 회전 후 전진이 카메라 정면을 따르지 않는 문제는 카메라 부모 기준의 전방/우측 벡터를 가져와 방향을 구성해 해결했다(스프링암 구조로 카메라 전방은 일관됨).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 private void Update()
 {
     // 1. 입력 방향 계산 (카메라 기준)
     Vector3 moveDir = Camera.transform.right * moveInput.x + Camera.transform.forward * moveInput.y;
     moveDir.y = 0f;
     moveDir.Normalize();

     // 2. 수평 속도 처리 (XZ)
     if (moveDir.magnitude > 0.1f)
     {
         horizontalVelocity = Vector3.MoveTowards(horizontalVelocity, moveDir * maxSpeed, acceleration * Time.deltaTime);

         // 3. 회전 처리
         Quaternion toRotation = Quaternion.LookRotation(moveDir, Vector3.up);
         transform.rotation = Quaternion.Lerp(transform.rotation, toRotation, turnSpeed * Time.deltaTime);
     }
     else
     {
         horizontalVelocity = Vector3.MoveTowards(horizontalVelocity, Vector3.zero, deceleration * Time.deltaTime);
     }

     // 4. 중력 및 점프 처리 (Y)
     if (controller.isGrounded)
     {
         if (verticalVelocity < 0f)
             verticalVelocity = -1f; // 지면 안정화
     }
     else
     {
         verticalVelocity -= gravity * Time.deltaTime;
     }

     // 5. 최종 이동 벡터 조합
     Vector3 totalVelocity = horizontalVelocity;
     totalVelocity.y = verticalVelocity;

     controller.Move(totalVelocity * Time.deltaTime);
 }

애니메이션

일단 애니메이션컨트롤러를 생성한다. 언리얼의 애님클래스와 비슷한 역할인거 같다.
애니메이션끝까지 그림과 같이 종료 시간 있음을 해제한다 이유는 True면 현재 진행중인 애니메이션을 끝까지 진행시킨 후 다음 애니메이션으로 넘어가기 때문이다.

그리고 믹사모에서 YBot에 애니메이션을 가져왔다. 나는 다른 캐릭터를 사용할 것인데 여기서 문제는 애님 리타겟팅을 과정을 언리얼처럼 해야하는지 의문이다.
일단 YBot 애니메이션이 있는 컨트롤러를 넣어주고 실행시켜본 결과 애님은 제대로 실행되어 보인다.
일단 그럼 Idle -> Walk -> Run 을 하는 애니메이션 컨트롤러를 만들어보고 실행보겠다. 애니메이터에서 우클릭 후 Blend Tree를 만들어 단계별 애니메이션을 생성해보자. 언리얼의 블렌드 애니메이션과 비슷한 것 같다. 애님컨트롤러 스크립트를 만들고 캐릭터 무브먼트에서 수평이동의 벡터의 길이를 반환하여 애니메티어에 float “speed”값을 변경시켜 넣어주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;

public class AnimController : MonoBehaviour
{
    private Animator animator;
    private CharacterMovement characterMovement;

    private void Awake()
    {
        animator = GetComponent<Animator>();
        characterMovement = GetComponentInParent<CharacterMovement>();
    }

    void Update()
    {
        SetAinmSpeed();
    }

    void SetAinmSpeed()
    {
        float speed = characterMovement.GetAnimSpeed();
        animator.SetFloat("Speed", speed, 0.1f, Time.deltaTime);
    }
}

걷기/뛰기

내가 원하는대로 동작이 되기는한다 시프트키를 누르기 전에 이동하면 걷고, 누르면 달리면서 잘 된다.
하지만 카메라를 회전하거나 방향을 180도나 90도로 꺾을 때 부드럽지 않다.

#TODO 회전 시 감속 전환을 더 부드럽게 만들기(보간/속도 분리)

This post is licensed under CC BY 4.0 by the author.