플레이어 이동
(Input System 사용만을 고려함)
(3D 환경을 고려함)
사전 정보
Transform: 씬에 존재하는 모든 게임 오브젝트가 반드시 가지고 있는 컴포넌트(클래스)(빈 게임 오브젝트 생성을 해도 Transform은 포함되어 있다). 해당 게임 오브젝트의 위치, 회전, 크기 정보를 담고 있다.
Rigidbody: 게임 오브젝트가 씬 내의 물리법칙을 따르도록 하는 컴포넌트(클래스). 중력, 질량 등을 설정하고 영향받는다.Collider 컴포넌트를 통해 충돌 처리를 하므로 보통 같이 사용한다.
Update(): 매 프레임마다 호출됨. 프레임은 렌더링과 관련됨. 프레임은 렌더링을 통해 만들어지기 때문. 기기의 성능에 따라 렌더링 성능이 높으면, 프레임을 많이(적게) 생성하고(게임에 따라 다를 수 있다), 따라서 Update호출 횟수도 그에 맞춰진다.
FixedUpdate(): 프레임과 상관없이 고정된 시간 간격으로 호출됨. 프로젝트 세팅에서 값을 변경해줄 수 있음.
Time.deltaTime: 어느 프레임의 다음 프레임을 생성하는 데에 걸리는 시간이다. 값이 실시간으로 변하는데, 바로 전의 프레임 생성 시간이 위 시간이 된다. 평균적으로 생각하면, 1초에 60프레임을 생성한다고 가정하면 Time.deltaTime 값은 1/60 일 것이다.
1. Transform 을 통한 이동
게임 오브젝트의 좌표에 직접 손을 대는 것. 물리적인 상호작용(충돌 등)에 크게 개의치 않는 상황에서 사용하는 이동법.
1.1 transform.Translate: 트랜스폼 내부의 Translate 메소드 사용.
void Update()
{
transform.Translate(moveDir * speed * Time.deltaTime, Space.World);
}
Update에서 사용함. moveDir은 당연히 정규화된 벡터(길이1)임. 맨 뒤에 Space,World, Space.Self를 통해 이동 기준을 절대, 상대로 선택할 수 있음.(생략하면 기본값 로컬.)
Update는 프레임에 종속되어 호출됨. Time.deltaTime이 없었다면 생성되는 프레임에 따라 코드 호출 횟수가 가변적이게 되고, 그럼 속도도 가변적이게 되는 것.
Time.deltaTime은 1초를 1초 동안 생성된 프레임 개수로 나눈 것으로 볼 수 있으므로, 분자 분모 곱해져서 1만 남게 됨. 속도가 모든 게임 환경에서 동일하게 됨. (코드 호출 횟수가 변하는 만큼 보정해주는 작업이 Time.deltaTime을 곱해주는 것.)
방향성+속도만 있으므로, 코드가 호출되는 한 움직임을 멈추지 않음.
속도라는 게 비유인거지, 사실을 기존의 3차원 position에 방향성과 길이가 있는 벡터를 더한 걸 다시 position에 값 할당해주는 것과 같으므로, 코드가 호출될 때마가 speed의 값(벡터의 길이)만큼 '순간 이동'하는 것과 마찬가지. 즉 프레임 당, 코드 호출 당 위치가 불연속적으로 변함. 물리적인 연속을 띄지 않음. (얇은 벽 통과 문제 있음. 터널링 현상.)(그래서 물리적 엄밀함이 요구되는 때에는 사용 X)
1.2 transform.position에 곧바로 벡터를 더해주거나 값을 할당하기.
void Update()
{
transform.position += moveDir * speed * Time.deltaTime;
}
위 코드는 Translate와 완전히 같은 작용을 한다.
transform.position =
Vector3.Lerp(transform.position, target.position, lerpSpeed * Time.deltaTime);
transform.position =
Vector3.MoveTowards(transform.position, target.position, moveSpeed * Time.deltaTime);
Vector3.Lerp를 사용하면 거리에 비례하여 점차 이동속도가 느려진다. 무한 수렴 문제를 조심해야한다.
(주로 코루틴을 사용하여 n초 동안 a에서 b까지 이동을 구현.)
MoveTowards는 등속 운동하여 목적지에 도달한다.
결국 transform을 직접 건드리는 이동법은 물리적인 요소를 크게 고려하지 않는 경우(주로 rigidbody를 사용하지 않는 오브젝트)에 대해서만 사용해야 한다.
(transform을 건드릴 때 왜 FixedUpdate를 쓰면 안되는가? -> 렌더링 주기, 프레임 생성 주기에 맞추어 실행되므로 움직임이 자연스럽게 따라가주는 것이므로, FixedUpdate같이 프레임 생성 주기를 무시하는 경우에는 논리적인 이동은 균일하나 눈에 보이는 '화면' 수준에서 끊김 문제가 발생함.)
2. Rigidbody를 통한 이동
유니티의 물리 엔진을 활용한 이동이다.
2.1 Rigidbody.linearVelocity 값 부여
private Rigidbody rb;
...
void FixedUpdate()
{
rb.linearVelocity = new Vector3(moveDir.x * speed, rb.velocity.y, moveDir.z * speed);
}
어느 게임 오브젝트의 리지드바디가 가진 속도값을 지정한다. FixedUpdate 내에 쓰이므로 게임 오브젝트의 속도값이 주변 환경과 무관하게 코드의 Vector3 값으로 유지된다. (y값은 중력의 영향력을 그대로 받도록 기존의 값을 반영하게 써준다.)
당연히 일회성으로 한 번 호출하기만 하면, 해당 속도가 지정된 직후, 물리 환경의 영향을 받아 감속 등이 일어난다.
(주로 비현실적인 움직임을 일으키므로 비추천됨. 1인칭 FPS에서 점프와 같은 일을 수행할 때에 즉각적인 속도 변화를 위해 사용됨.)
2.2 Rigidbody.AddForce 부여
void FixedUpdate()
{
rb.AddForce(moveDir * speed, ForceMode.Force);
}
리지드바디에 반응하는 힘을 특정한 방향으로, 특정한 정도로, 특정한 방법으로 가하여 움직이게 한다.
앞부분은 다른 것과 비슷하게 Vector3 값을 넣는 것이고, 주목할 만한 부분은 힘을 주는 방식을 적는 부분이다.
ForceMode.
Force: 연속적으로 물리적인 힘을 가한다. 해당 오브젝트의 질량(Mass)의 영향을 받으며, 현실과 가장 유사하다.
Acceleration: 연속적으로 물리적인 힘을 가한다. 질량이 무시된다.
Impluse: 불연속적으로, 순간적으로 힘을 가한다. 질량의 영향을 받는다.
VelocityChange: 순간적으로 힘을 가한다. 질량을 무시한다.
연속적으로 힘을 가한다는 의미: 코드에 지시된 벡터값만큼이 1초 동안 물리계산 주기에 맞추어 나뉘어 가해져서, 최종적으로 가해지는 힘의 양이 같도록 한다. 따라서 FixedUpdate 내에 배치하여, 물리계산 주기에 맞게 코드가 실행되도록 한다.
불연속적이란?: 코드가 실행되는 단 1회에 코드상의 벡터값만큼의 힘을 그대로 가한다. Update, FixedUpdate에서 사용하면 안되겠다.
AddForce를 사용하면 애매하게 미끄러지며 움직임이 딱 맞게 마무리되지 않는 경우가 빈번히 발생한다. 다양한 물리 요소의 개입 때문이다. 신경 써 주어야 한다.
2.3 Rigidbody.MovePosition 목표 위치로 물리적인 이동을 수행
void FixedUpdate()
{
// 현재 위치 + (방향 * 속력 * 물리엔진 시간)
Vector3 targetPosition = transform.position + (moveDir * speed * Time.fixedDeltaTime);
rb.MovePosition(targetPosition);
}
입력된 백터 상으로 이동한다. transform.position = ... 과 유사하나, 출발지점과 목표지점의 직선 경로를 계산하여 해당 경로를 따라서 빠르게 이동한 것으로 간주하여 물리 계산을 한다. (순간이동 한 것은 맞지만, 경로 상의 물리 계산은 빠트리지 않는 것. transform.position은 정말로 순간이동이다.)
2.4 Rigidbody.position 값 부여하기
transform.position을 이용하는 경우, transform이동 후 물리 엔진의 데이터 동기화 과정과, 충돌 판정을 위한 주변 환경 캐싱 과정이 필연적으로 발생한다. Rigidbody.position을 사용하면 그러한 과정이 곧바로 리지드바디 단에서 계산되므로 성능 면에서 이점이 있다.
즉 Rigidbody 컴포넌트가 붙는 오브젝트라면, 리지드바디의 포지션을 사용하는 것이 이득이란 것이다.
점프
상술한 방식을 y축에 대해서 적용하여 다양하게 구현할 수 있다.
1. Rigidbody.AddForce의 Impluse로 구현하기. 질량의 영향을 받아 점프하도록 할 수 있음.
2. Rigidbody.linearVelocity에서 기존 이동관성은 유지하고 y값만 갈아끼우기. 일정한 높이와 타이밍을 유지함.
...
Transform 기반 이동과 점프:
- 물리엔진의 영향을 받지 않고 의도대로 정확히 움직임. 목표 좌표까지 명확하게 이동 가능.
- 관성 없이 즉각적인 조작감.
- 물리엔진 개입이 없으므로 성능 부하가 낮다.
- 순간이동 문제, 물리적 상호작용 부재, 복잡한 상황에 대한 직접 구현.
Rigidbody 기반 이동과 점프:
- 물리엔진에 종속되어 있으므로 주변 환경 및 중력, 관성 등 자연스러운 움직임을 구현함.
- 충돌 판정이 자연스럽고 쉬움.
- 머터리얼 교체로 조작감이 자연스럽게 바뀜.
- 물리 엔진 특유의 물리적 돌발상황, 관성 제어 문제, Transform 대비 연산 비용 문제.
각각의 장단점이 확실하다.
오브젝트들의 용도에 따라 명확히 구분해서 사용하자.