[Godot] Movement Jittering

progman·2022년 5월 24일
1
post-thumbnail

움직임(Movement)의 떨림(Jittering)

고도는 process와 physical process의 tickrate가 다르다. _process는 실행중인 게임의 fps를 그대로 따르며, 장면마다 호출되는 반면, _physical_process는 고정된 tick에서 동작한다. 만약 physical process의 tickrate가 30이고, 실행중인 게임의 fps가 60이라면, process는 초당 60번 실행되지만, physical process는 그것과 상관없이 초당 30번만 실행된다.

고도의 물리틱은 기본값 60으로 설정되어 있다. 만약 당신이 120hz짜리 모니터로 어떤 고도게임을 120fps로 돌리기 충분한 환경을 가지고 있을 때, 게임 또한 120fps로 실행될 것이다. 문제는 여기서 시작된다. 게임이 120fps든 240fps든 상관없이 물체의 움직임은 60fps로 움직이고, 실제로 그렇게 보인다는 것이다. 고주사율의 모니터를 쓰고도 마치 60fps의 게임을 즐기는 것처럼 움직임에 잔상과 지터링이 생기게 되고, 고주사율 모니터를 사용하는 게이머들에게 불쾌함을 가져다 준다.

이 문제를 해결하려면 어떻게 해야 할까?

1. _physical_process의 코드를 _process로 옮기기

물체를 움직이는 코드를 _physical_process가 아닌 _process로 옮김으로써, 고정된 틱의 delta가 아닌 실제 게임프레임의 delta값을 이용해 게임상의 물체를 움직이게 한다. 즉, 게임이 120fps로 돌고 있다면 물체의 움직임 또한 120fps에 맞게 움직일 것이다. 실제로 이 방법을 적용해보면 위와 같은 문제가 사라진다.

그러나 이 방법은 두 가지 문제를 안고있다:

  • 게임의 프레임에 physics가 의존하게 되면, 게임의 프레임에 따라 physics또한 요동치게 되며, 이는 게임물리에 불안정성을 가져다준다.

  • physics코드가 아주 높은 프레임에서 지나치게 많이 호출되면 리소스 부담이 가중될 뿐만 아니라, 이 자체로도 불안정성을 가져다준다.

가장 단순하고 간단한 방법이지만, 고도 개발자들조차 추천하지 않는 위험한 방법이다.

2. physics의 tickrate를 늘리기

20년도 넘은 게임인 Quake III Arena의 틱레이트는 125까지 설정가능하며, 대부분의 서버가 이 틱레이트로 운영되었다. 그러니 현대 컴퓨팅 환경에서 physics의 틱을 120정도 올리는 것이 큰 무리는 아닐 것이다.

위의 방법보다도 더 간단한 해결법이면서, 가장 무난한 방법이다. 120hz의 고주사율 모니터 사용자들은 아무런 불편함 없이 플레이가 가능하다. 그보다 더 높은 주사율 모니터 사용자 또한 60틱보다는 훨씬 쾌적하게 플레이할 수 있다. 다만 잔상에 예민한 유저들은 240hz이상의 환경에서 120틱의 게임에 대해 여전히 약간의 잔상과 불편함을 느낄 수 있다는 한계가 있다.

3. 보간

많은 고도 개발자들이 결론내린 방법이다. physics는 여전히 60틱을 따르되, Mesh와 같은 오브젝트의 시각적인 부분만 현재 fps에 맞게 보간을 하는 것이다. 게임의 근본적인 부분은 건드리지 않은 채 고주사율 모니터 유저 모두가 만족할 수 있는 방법이라 할 수 있으나, 아쉽게도 이 방법 또한 아쉬운 점들이 있다:

  • 타 게임엔진들의 구현과는 다른 이질적인 코드와 계층구조가 나오게 된다. 가령 카메라를 보간할 때, 1인칭 팔의 경우 카메라의 자식으로 달리지 않았다면 1인칭 팔이 카메라와 따로 노는 모습을 보인다.

  • 오브젝트의 시각적인 위치와 실제 데이터상의 위치가 다르다. 더 부드럽게 보간할수록, 격차는 더 커진다.

아래 코드는 플레이어 카메라에 대한 보간 코드로, 고도 공식 에셋 라이브러리의 Retro Magic - FPS Demo를 참조했다.

# Wrote on Godot 4.0 alpha 8

var camera: Camera3D
var parent: Node3D		# camera's parent
const ACCEL = 30.0

func _process(delta):
	if Engine.get_frames_per_second() > Engine.get_physics_ticks_per_second():
		# camera will not follow parent's transform
		camera.set_as_top_level(true)
		var camera_lerped_position = camera.get_global_transform().origin.lerp(parent.get_global_transform().origin, ACCEL * delta)
		camera.global_transform.origin = camera_lerped_position
		camera.set_rotation(Vector3(parent.get_rotation().x, get_rotation().y, camera.get_rotation().z))
	else:
		# camera will stick to its parent
		camera.set_as_top_level(false)
		camera.set_global_transform(parent.get_global_transform())

먼저 현재 fps가 물리틱보다 많은지 판별한다. 현재 fps가 240인데 물리틱이 60이면 카메라 보간을 실시하고, 그렇지 않다면 보간하지 않고 플레이어에 카메라위치를 고정한다.

카메라를 parent와 분리한다. parent의 transform과 카메라의 transform이 분리독립된 것이다. 이렇게 되면 플레이어가 움직여도 카메라는 그 자리에 아무런 움직임 없이 있을 것이다. 이 때 매 틱마다 바뀌는 parent의 글로번 위치로부터 카메라의 위치값을 보간한다. 회전값은 보간할 필요가 없어 parent 및 플레이어의 회전값을 설정하면 된다.

이 코드를 물체의 움직임에 적용하면 아래와 같은 모습이 나온다.

이것은 결함일까?

움직임 지터링 현상은 타 게임엔진에서는 찾아보기 힘든 고도만의 현상이자 문제점이다. 이것은 고도엔진의 결함일까?

고도의 공식 메뉴얼에서는 결함이 아니라고 서술했다. 즉, 물리 틱과 실제 프레임의 차이에 따른 지터링이 정상이라는 소리다. 그런데 자세히 읽어보면, 고도의 제작진들은 대부분의 모니터들이 60hz이므로, 이는 문제가 아니라고 설명하고 있다. 많은 게이머들이 120hz 이상의 고주사율 모니터를 사용하고 있는 지금의 현실과는 괴리가 있는 이유가 아닌가 싶다.

결국은 신경써야 한다

어쨌거나 공식적으로 결함이 아니라는 내용에 따라, 이 문제가 차기 메이저 버전에서 해결될 가능성은 기대하기 어렵고, 지터링 문제는 개발자가 직접 해결해야 한다는 소리이다.

물체의 움직임, 플레이어의 움직임, 그 외 수많은 Kinematic Body들의 움직임에는 지터링이 따르므로 모든 움직임에 보간을 수행해야 한다. 게임물리가 시간기반이 아니라 보다 작은 수의 고정된 틱기반으로 돌아간다는 것이 이해가 가지만, 한 편으로 다른 상용엔진과는 다르게 고도에서는 이러한 부분을 더 많이 신경써줘야 한다는 것이 아쉽다.

22-08-21 추가

고도 3.5 버전에서 Physics Interpolation이 추가되어 더 이상 이 부분을 신경쓰지 않아도 된다.

profile
그저 개발중인 사람입니다.

0개의 댓글