지우너

[Unity 3D Run] 캐릭터 이동 코드 수정, 카메라 추적 본문

Project

[Unity 3D Run] 캐릭터 이동 코드 수정, 카메라 추적

지옹 2023. 3. 12. 21:50

03.10

캐릭터 이동 속도 수정 파트

//player.cs
void Update()
    {
        // Movement input
        Vector3 moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, moveSpeed);
        // 입력 받은 값을 방향으로 변환하고 움직임 속도를 곱할 겁니다.
        // 입력의 방향을 얻기 위해 moveInput를 정규화(nomalized)하여 가져옵니다 (nomarlized는 방향을 가리키는 단위벡터로 만드는 연산)
        // 거기에 moveSpeed를 곱해줍니다
        Vector3 moveVelocity = moveInput.normalized * moveSpeed;
        // controller에게 velocity값을 넘겨줌
        controller.Move(moveVelocity);
    }

 

정면을 향해 달려가는 속도를 낮추고, 좌우 이동 속도를 높여야 함.

- 코드 뜯어보기

공부하다가 갑자기 정면을 향해 가는 속도를 줄이고= moveSpeed 값을 낮추고

좌우 이동속도를 높일 것 = Input.GetAxisRaw("Horizontal")*moveSpeed

이렇게 하면 해결되지 않을까라는 생각이 들었다.

moveSpeed = 3.0f 로 바꾸고, Input.GetAxisRaw("Horizontal")*moveSpeed를 해주니 이게 괜찮은 건지는 모르겠지만 해결이 됐다….? 다른 더 좋은 방법이 있을 것 같은데…

Input.GetAxisRaw(string name)

  • -1, 0, 1 세 가지 값 중 하나가 반환된다. 키보드 값을 눌렀을 때 즉시 반응해야한다면 GetAxisRaw를 사용하면 된다.

좌우 버튼을 클릭할 때마다 -1, 0, 1이 x값으로 들어가는데, z값은 그에 비해 5.0f가 들어가고 있으니, 당연히 빠를 수 밖에 없었던 것이다… 그렇게 생각하니까 Input.GetAxisRaw("Horizontal")*moveSpeed가 괜찮은 방법이었던 것도 같고…?

normalized가 조금 어려운 것 같다. velocity가 speed+direction이라서 입력 값을 방향 벡터로 바꾸고 속력을 곱해준 것 같은데 normalized가 어떤 식으로 작동하는지 모르겠다 더 큰 수면 1로 반환하는 건가??

궁금한 부분은 이거다 예를 들어 Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")을 인자로 넣어주고, 이 위치 값을 normalized를 한다고 하자. 이때 왼쪽 방향키와 위쪽 방향키를 동시에 누르면 왼쪽 위로 이동해야 한다. 그러면 Vector3.Left와 Vector3.Forward가 번갈아가면서 입력되게 되는 것인가?

그래서 (Input.GetAxisRaw("Horizontal"), 0, moveSpeed)를 해줬을 때

Vector3 moveVelocity = moveInput.normalized * moveSpeed;

좌우 이동은 응…? 좌우 이동이나 앞으로 이동하는 것이나 똑같이 크기가 1인 방향벡터로 전환되는데, 왜 left(=Vector3(-1, 0, 0)), forward(=Vector3(0, 0, 1))의 속도가 다르게 계산된 거지 크기가 1인 벡터에 속도만 곱해지는 건데 왜 이렇게 되는 거지??

방향 벡터 - 벡터의 정규화(normalized) 유니티 - Marc Studio

[Math] 정규화 (1) - 벡터와 좌표의 정규화

벡터의 정규화는 어떤 한 벡터를 벡터의 길이로 나누어서 그 벡터의 길이를 1로 만드는 것

이렇게 길이가 1이 된 벡터를 단위 벡터라고 부른다.

번갈아가면서 왼쪽과 위방향의 입력이 들어가는 게 아니라 왼쪽위 방향 벡터가 입력으로 들어가는 것.

이렇게 들어가는데 좌우 입력은 -1, 0, 1 이렇게 들어가고, 정면 방향으로의 입력은 항상 5.0f가 들어가니까 정규화 했을 때 좌우방향으로의 크기?는 작게 들어가는 반면, 정면(위쪽)으로의 입력은 좌우에 비해 5배? 크게 들어가게 되는 것이다. 그래서 방향이 좌우는 작게 앞쪽으로 크게 가니까 속도가 달라지는 것 같다.

<관련 공부>

유니티와 C# 개념 정리_007_Unity의 변수 타입

Vector3 타입은 개체의 위치를 3차원(x, y, z)으로 새로 지정할 때 사용한다. x, y, z는 모두 float 타입이다.

유니티에서 개체의 위치는 Transform 컴퍼넌트의 Position의 각 차원 값으로 결정된다.

UnityEngine.Vector3 - Unity 스크립팅 API

Vector3 - Scripting API - Unity - Manual

Vector3 타입으로 position(위치)라는 변수를 선언하고 새 위치를 지정하고 싶으면 아래와 같이 작성한다.

Vector3 position = new Vector3 (2.0f, 3.0f, 4.0f) ;

 

더보기

Unity: 매우 혼란스러운 Vector3 사용에 대한 이해

유니티 Vector3 new 는 스택에 생성된다

C#에서 Vector는 구조체 타입이다. 그리고 이 구조체 타입은 기본 타입인 int, float, char, enum등과 같이 value type이라서 스택에 저장되며 new를 한다고 힙에 메모리할당이 일어나지 않는다.

object와 string은 다른 언어들과 동일하게 reference type이다. string이야 스택에 저장되긴 하겠지만 변수는 레퍼런스란 얘기다.

transform.position으로 가져오는 Vector3는 레퍼런스가 아니므로, 복사된 값이 저장된 Vector3 이다. 그래서 Set()을 해주면 복사된 값을 변경하는 것이라서 적용이 되질 않는다.

new를 쓰는데, Update에서 반복호출해도 괜찮을걸까? 앞 내용의 연장선인데, Vector3가 struct type이고 new는 통일성을 위해 사용하는 것이라서 실제로는 스택에 생성되기 때문에 반복된 메모리릭이나 할당/해제문제는 발생하지 않는다. 마음놓고 써도 된다는 얘기다.

참고로 Quaternion도 struct 이다. value type이라는 얘기.


위의 Unity 메뉴얼을 보면 Vector3.normalized에 관한 것도 나와 있지만 아직은 한글이 더 편하다…

Vector3의 magnitude와 normalized - 천천히 흘러가도 괜찮아

방향 벡터의 정보에는 1. 거리(크기)와 2. 실제 방향의 정보가 있다.

1. 거리(크기) magnitude

벡터의 길이를 반환(read only). 벡터의 길이는 (xx+yy+z*z)의 제곱근

2. 방향에 대한 정보 normalized

해당 벡터의 magnitude가 1인 벡터를 반환(read only). 즉, 크기가 1인 벡터인 단위 벡터를 받아올 수 있다는 뜻인데, 단위 벡터는 방향을 의미한다. 예를 들어 (1, 0, 0)은 xyz 순으로 x축이 단위가 되는 벡터.

이 값을 가지고 해당 값에 속도 연산을 하든지, Deltatime과 같이 사용자의 프레임률을 곱하여 적용시킬 때 사용된다.

방향 벡터 - 벡터의 정규화(normalized) 유니티 - Marc Studio

벡터를 정규화하는 이유는 오브젝트가 균일한 속도로 이동하도록 만들기 위해서이다.

모든 방향 벡터의 길이가 1이어야 방향에 따른 이동속도가 같아진다.

정규화하지 않은 대각선 벡터는 1.414…의 값이 나온다. 정규화하지 않고 이동을 적용한다면 대각선 이동이 1.414…배 빠르게 이동하게 되는 것이다.

참고! 대각 방향의 벡터는 (0.7071068..., 0.7071068...)로 변환

또 하나의 예시로 적은 값의 벡터도 길이를 1로 변환해줍니다.

(0.5, 0).normalized => (1, 0) : → 방향

(0.2, 0.2).normalized => (0.7071068, 0.7071068) : ↗ 방향

우리는 이렇게 정규화된 벡터를 방향 벡터라고 부릅니다.

[Math] 정규화 (1) - 벡터와 좌표의 정규화

 

카메라 이동 파트

메인 카메라 이동 (Unity) - 밸런스 있는 삶

// CameraController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    GameObject player;

    // Start is called before the first frame update
    void Start()
    {
        player = GameObject.Find("Player");
    }

    // Update is called once per frame
    void Update()
    {
        Vector3 PlayerPos = player.transform.position;
        transform.position = new Vector3(transform.position.x, transform.position.y, PlayerPos.z-7);
    }
}

위의 블로그를 보고 코드를 조금 수정하여 코드를 작성했고, 실행했을 때 생각하던 방식으로 잘 작동하는 것 같다.

그런데 몇 가지 부분이 마음에 들지 않는다.

  1. 앞의 Player 코드를 작성하면서 transform.position이 그다지 좋은 코드가 아님을 깨달았다. 이것도 MovePosition으로 바꿀 수 있을까 Rigidbody.MovePosition이니까 카메라에는 적용 못 할 것 같은데 그럼 카메라를 효율적으로 움직일 수 있는 함수는 없는 것인가?
  2. transform.position = new Vector3(transform.position.x, transform.position.y, PlayerPos.z-7); 이 부분을 일단 이렇게 쓰기는 했는데, 내가 원래 생각했던 것은 현재 카메라위치와 현재 캐릭터 위치를 고정해서? 카메라의 z값이 현재 캐릭터와 떨어진 거리 만큼 유지되면서 따라가는 것을 원했다.

현재 카메라가 z값 -55.61 플레이어가 z값 -49.18에 위치하고 있다. 그래서 카메라의 위치를 Player의 위치에서 7 뺀 값으로 갱신되게 코드를 수정했다.

음… 아래의 런게임 카메라컨트롤러 코드가 훨씬 괜찮아 보인다.

https://github.com/Chaker-Gamra/Endless-Runner-Game/blob/master/Assets/Scripts/Player/CameraController.cs

offset=카메라의 위치-플레이어의 위치

지금으로 치면 z만 봤을 때, 현재 카메라가 z값 -55.61 플레이어가 z값 -49.18

offset.z= (-55.61)-(-49.18) = (-55.61)+49.18=-6.43

newPosition= offset.z+target.position.z= (-49.18)+(-6.43)= -55.61

뭔가 좀 바보가 된 기분… 이 코드를 보기 전에 두 z 위치 값을 빼고 음… 이런 생각을 했었는데, 두 값 다 음수라서 어디에서 어디를 빼야 하고 이런 게 머리가 안 돌아갔다…

Lerp 관련 영상을 얼마전에 봤었는데 저런 식으로 쓰면 안 된다고 봤었던 거 같다. Lerp부분은 그때 봤던 부분을 보면서 수정해야할 것 같다.

근데 애초에 이 코드에서 Lerp를 쓰는 이유가 뭐지?? transform.position = newPosition; 이렇게 해줘도 될 것 같고, 실제로 코드를 바꿔서 해봐도 둘의 차이가 딱히 느껴지지 않는데 왜 쓴 거지

Lerp 공부를 조금 더 해봐야 할 것 같다.

 

03.11-03.12 코드를 수정하고 싶어서 Lerp()공부를 살짝 했는데 잘 안 됐다.

공식 문서에서 확인하는 Vector3.Lerp()

https://docs.unity3d.com/ScriptReference/Vector3.Lerp.html

public static Vector3 Lerp(Vector3 a, Vector3 b, float t);

즉, a값으로 반환되는 값은 a + (b-a) * t이며 점진적으로 a값이 증가할 것을 예상할 수 있습니다. a의 증가폭은 점점 줄어들면서 b값까지 도달하게 되는데 즉, 도착위치에 가까워질수록 속도가 느려집니다.

(수식은 a * (1-t) + b * t 으로 대체하여 사용할 수도 있다.)

[Unity] 유니티 3D Vector의 선형보간 Lerp 정확한 사용법 Lerp는 선형보간 함수이다. 선형 보간이란 끝점의 값이 주어졌을 때, 그 사이에 위치한 값을 추정하기 위해 직선 거리에 따라 선형적으로 계산하는 방법이다.

유니티에서는 어떤 수치에서 어떤 수치로 값이 변경되는데 한 번에 변경되지 않고, 부드럽게 변경되게 하고 싶을 때 Lerp를 사용한다.

  • Mathf.Lerp: 숫자 간의 선형 보간
  • Vector2.Lerp: Vector2 간의 선형 보간
  • Vector3.Lerp: Vector3 간의 선형 보간
  • Quaternion.Lerp: Quaternion간의 선형 보간(회전)

오브젝트를 부드럽게 이동하거나 회전할 시에 사용하곤 한다. 카메라 추적 스크립트에 이용할 수 있다.

Vector3.Lerp(시작위치, 끝위치, 0~1사이의 실수)와 같은 형태로 사용하고

Vector3.Lerp(시작위치, 끝위치, 0)은 시작위치

Vector3.Lerp(시작위치, 끝위치, 1)은 끝위치가 된다.

Time.deltatime은 지난 프레임이 완료되는 데까지 걸린 시간을 나타내며, 단위는 초를 이용한다(읽기 전용)

Update 함수는 한 프레임에 한 번 호출된다.

Update 함수에서 인자에 Time.deltatime을 넣어주게 되면 위치1이 위치2의 방향으로 둘 사이의 거리 *(전 프레임에 걸린 시간)만큼 이동하게 되는 것.

Vector3.Lerp(0, 10, Time.deltatime)

전 프레임에 걸린 시간이 0.1f 라고 가정하면 1초에 10프레임 즉, 10fps인 경우가 된다.

Update0 → 위치1 = 0

Update1 → 위치1 = 1 // 전프레임의 위치(=0)+(둘 사이의 거리(=10)*Time.deltatime(=0.1f))

Update2 → 위치1 = 1.9 // 전프레임의 위치(=1)+(둘 사이의 거리(=9)*Time.deltatime(=0.1f))

Update3 → 위치1 = 2.71 // 전프레임의 위치(=1.9)+(둘 사이의 거리(=8.1)*Time.deltatime(=0.1f))

Update? → 위치1= 위치2

점점 적은 수치만큼 증가하다가 결국 위치1과 2가 일치하게 된다.

유니티 카메라 플레이어 추적 → lerp가 필요한 이유

lerp가 없으면 플레이어의 속도가 빨라질수록 카메라가 순간이동하는 것처럼 보일 수 있다. lerp를 사용하면 시작지점과 도착지점의 중간값을 구해 좀 더 부드럽게 카메라를 이동할 수 있다.

유니티 Vector3.Lerp() 란 무엇인가?

public class Move : MonoBehaviour
{
    public Transform startPosition;
    public Transform endPosition;
    
    float lerpTime = 0.5f;
    float currentTime = 0;

    void Start()
    {
    	this.transform.position = startPosition.position;
    }

    void Update()
    {
    	// currentTime은 시간 흐름에 따라 증가함
    	currentTime += Time.deltaTime;
        // 이 코드를 넣어주면 0부터 증가하다가 최대 0.5(lerpTime)까지만 증가함
        if (currentTime>=lerpTime)
        {
        	currentTime = lerpTime;
        }
        // 이전에 넣어뒀던 보간값을 t에 저장하고
        float t = currentTime/lerpTime;
        // sine 형태의 계산을 가져온다.
        t = Mathf.Sin(t * Mathf.PI * 0.5f);
        
    	this.transform.position = Vector3.Lerp(startPosition.position, endPosition.position, t);
    }
}

사실 더 좋은 방법은 Update문에 이렇게 쓰는 게 아니라 별도의 코루틴을 하나 만들어서 필요한 만큼만 코루틴이 동작해서 움직이게 만드는 것이 더 좋다. Update문에 쓰게 되면 매 프레임마다 호출되기 때문에 최대한  Update문에 안 쓰는 것이 좋다(많이 쓰면 성능이 안 좋아짐).

위에 깃허브에서 lateUpdate에 이동 코드를 작성해서 왜 그런가 하고 lateUpdate에 대해 알아봤다.

Update() , FixedUpdate() , LateUpdate() 의 차이점 - Developug

  • Update() - 스크립트가 enabled 상태일때, 매 프레임마다 호출됩니다. 일반적으로 가장 빈번하게 사용되는 함수이며, 물리 효과가 적용되지 않은 오브젝트의 움직임이나 단순한 타이머, 키 입력을 받을 때 사용됩니다.
  • FixedUpdate() - 프레임을 기반으로 호출되는 Update 와 달리 Fixed Timestep에 설정된 값에 따라 일정한 간격으로 호출됩니다. 물리 효과가 적용된(Rigidbody) 오브젝트를 조정할 때 사용됩니다(Update는 불규칙한 호출임으로 물리엔진 충돌검사 등이 제대로 안될 수 있음).
  • LateUpdate() - 모든 Update 함수가 호출된 후, 마지막으로 호출됩니다. 주로 오브젝트를 따라가게 설정한 카메라는 LateUpdate 를 사용합니다(카메라가 따라가는 오브젝트가 Update함수 안에서 움직일 경우가 있기 때문).

<GameObject player 를 만드는 것과 private Transform target을 만드는 것>

player = GameObject.Find(”Player”);

player.transform.position.x라고 사용

 

target = GameObject.FindGameObjectWithTag("Player").transform;

transform.position.x라고 사용

결국 코드는

using UnityEngine;

public class CameraController : MonoBehaviour
{
    private Transform target;
    private Vector3 offset;

    float lerpTime = 0.6f;
    float currentTime = 0;

    void Start()
    {
        target = GameObject.FindGameObjectWithTag("Player").transform;
        offset = transform.position - target.position;
    }

    void LateUpdate()
    {
        // currentTime은 시간 흐름에 따라 증가함
        currentTime += Time.deltaTime;
        // 이 코드를 넣어주면 0부터 증가하다가 최대 0.5(lerpTime)까지만 증가함
        if (currentTime >= lerpTime) currentTime = lerpTime;
        float t = currentTime / lerpTime;

        // 플레이어 이동에 따라 변하는 카메라 위치
        Vector3 newPosition = new Vector3(transform.position.x, transform.position.y, offset.z + target.position.z);
        transform.position = Vector3.Lerp(transform.position, newPosition, t);
    }
}

이렇게 해두고 나중에 만들면서 수정이 필요하다고 생각하면 수정하기로 했다.