Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
루트 스케일
XR 프로젝트의 핵심 중 하나는 넓은 공간을 자유롭게 돌아다닐 수 있다는 것입니다. 이 공간은 종종 플레이어가 물리적으로 있는 방에 의해 제한되며 이 공간 내에 배치된 추적 센서로 인해 제한됩니다. 그러나 인사이드 아웃 추적의 출현으로 더 큰 플레이 공간이 가능해졌습니다.
개발자로서 이는 여러 가지 흥미로운 과제를 야기합니다. 이 문서에서는 귀하가 직면할 수 있는 여러 가지 과제를 살펴보고 몇 가지 솔루션을 간략하게 설명합니다. 다른 문서에서 좌석형 XR 게임의 문제와 과제에 대해 논의하겠습니다.
참고
개발자들은 게임의 기반을 구축하는 동안 책상 뒤에 앉아 있는 경우가 많습니다. 이 모드에서는 룸 스케일 개발과 관련된 문제가 너무 늦을 때까지 나타나지 않습니다. 여기서 조언은 가능한 한 일찍 일어 서서 걸어 다니면서 테스트를 시작하는 것입니다. 일단 행복해지면 기초가 탄탄해지면 앉은 채로 편안하게 발전할 수 있습니다.
전통적인 1인칭 게임에서 플레이어는 CharacterBody3D 노드로 표시됩니다. 이 노드는 기존 컨트롤러, 마우스 또는 키보드 입력을 처리하여 이동합니다. 이 노드에는 대략 플레이어의 머리가 있을 위치에 카메라가 부착되어 있습니다.
이 모델을 XR 설정에 적용하여 XROrigin3D 노드를 캐릭터 본체의 하위로 추가하고 :ref:`XRCamera3D <class_xrcamera3d>`를 원본 노드의 하위로 추가합니다. 액면 그대로 이것은 효과가 있는 것 같습니다. 그러나 자세히 살펴보면 이 모델은 XR에 두 가지 형태의 움직임이 있다는 점을 고려하지 않습니다. 컨트롤러 입력을 통한 움직임과 현실 세계에서 플레이어의 물리적 움직임입니다.
결과적으로 원점 노드는 플레이어의 위치를 나타내지 않습니다. 이는 플레이어가 물리적으로 이동할 수 있는 추적 공간의 중심 또는 시작을 나타냅니다. 플레이어가 방 주위를 이동할 때 이 움직임은 플레이어의 헤드셋 추적을 통해 표현됩니다. 게임에서 이는 카메라 노드의 위치가 그에 따라 업데이트되는 것으로 해석됩니다. 모든 의도와 목적을 위해 우리는 실체 없는 머리를 추적하고 있습니다. 신체 추적이 가능하지 않으면 플레이어 신체의 위치나 방향을 알 수 없습니다.
이로 인해 발생하는 첫 번째 문제는 매우 분명합니다. 플레이어가 컨트롤러 입력으로 이동할 때 일반 게임에서 동일한 접근 방식을 사용하여 플레이어를 앞으로 이동할 수 있습니다. 그러나 플레이어는 우리가 생각하는 곳에 있지 않으며 앞으로 나아갈 때 잘못된 위치에서 충돌을 확인하고 있습니다.
두 번째 문제는 플레이어가 추적 공간의 중심에서 더 멀리 걸어가고 컨트롤러 입력을 사용하여 회전할 때 실제로 나타납니다. 캐릭터 몸을 회전하면 플레이어는 원형 방식으로 방 주위로 이동합니다.
위의 문제를 해결하면 세 번째 문제가 발견됩니다. 가상 세계에서 플레이어의 경로가 차단되어도 플레이어는 여전히 물리적으로 앞으로 나아갈 수 있습니다.
두 가지 개별 솔루션을 사용하여 처음 두 가지 문제를 해결한 다음 세 번째 문제를 다루는 방법에 대해 논의하겠습니다.
원산지 중심 솔루션
이 문제를 해결하기 위한 첫 번째 접근 방식을 살펴보면서 구조를 변경해 보겠습니다. 이것이 현재 XR Tools에 구현된 접근 방식입니다.
이 설정에서는 캐릭터 몸체를 최상위 레벨로 표시하여 원점과 함께 움직이지 않도록 합니다.
또한 카메라와 관련하여 목 관절의 위치를 알려주는 도우미 노드도 있습니다. 우리는 이것을 신체 중심이 어디에 있는지 결정하는 데 사용합니다.
이제 캐릭터 움직임 처리가 세 단계로 완료됩니다.
참고
이 문서 외에 여러가지 Godot 데모 프로젝트들도 살펴보면 좋습니다.
프로시저 단위 실행
첫 번째 단계에서는 플레이어의 물리적 움직임을 처리하겠습니다. 우리는 플레이어가 지금 어디에 있는지 파악하고 캐릭터 몸을 그곳으로 이동하려고 시도합니다.
func _process_on_physical_movement(delta):
# Remember our current velocity, we'll apply that later
var current_velocity = $CharacterBody3D.velocity
# Remember where our player body currently is
var org_player_body: Vector3 = $CharacterBody3D.global_transform.origin
# Determine where our player body should be
var player_body_location: Vector3 = $XRCamera3D.transform * $XRCamera3D/Neck.transform.origin
player_body_location.y = 0.0
player_body_location = global_transform * player_body_location
# Attempt to move our character
$CharacterBody3D.velocity = (player_body_location - org_player_body) / delta
$CharacterBody3D.move_and_slide()
# Set back to our current value
$CharacterBody3D.velocity = current_velocity
# Check if we managed to move all the way, ignoring height change
var movement_left = player_body_location - $CharacterBody3D.global_transform.origin
movement_left.y = 0.0
if (movement_left).length() > 0.01:
# We'll talk more about what we'll do here later on
return true
else:
return false
func _physics_process(delta):
var is_colliding = _process_on_physical_movement(delta)
플레이어를 끝까지 이동할 수 없을 때 _process_on_physical_movement 함수에서 ``true``를 반환한다는 점에 유의하세요.
프로시저 단위 실행
두 번째 단계는 사용자 입력의 결과로 플레이어의 회전을 처리하는 것입니다.
사용되는 입력은 필요에 따라 다를 수 있으므로 간단히 함수 ``_get_rotational_input``를 호출합니다. 이 함수는 필요한 입력을 얻고 초당 라디안 단위의 회전 속도를 반환해야 합니다.
참고
우리의 예에서는 이것을 간단하고 간단하게 유지하겠습니다. 스냅 터닝, 비네트 적용 등 편의 기능은 걱정하지 않겠습니다. 이러한 편의 기능을 구현하는 것이 좋습니다.
func _get_rotational_input() -> float:
# Implement this function to return rotation in radians per second.
return 0.0
func _copy_player_rotation_to_character_body():
# We only copy our forward direction to our character body, we ignore tilt
var camera_forward: Vector3 = -$XRCamera3D.global_transform.basis.z
var body_forward: Vector3 = Vector3(camera_forward.x, 0.0, camera_forward.z)
$CharacterBody3D.global_transform.basis = Basis.looking_at(body_forward, Vector3.UP)
func _process_rotation_on_input(delta):
var t1 := Transform3D()
var t2 := Transform3D()
var rot := Transform3D()
# We are going to rotate the origin around the player
var player_position = $CharacterBody3D.global_transform.origin - global_transform.origin
t1.origin = -player_position
t2.origin = player_position
rot = rot.rotated(Vector3(0.0, 1.0, 0.0), _get_rotational_input() * delta)
global_transform = (global_transform * t2 * rot * t1).orthonormalized()
# Now ensure our player body is facing the correct way as well
_copy_player_rotation_to_character_body()
func _physics_process(delta):
var is_colliding = _process_on_physical_movement(delta)
if !is_colliding:
_process_rotation_on_input(delta)
참고
물리 프로세스에 회전 처리 호출을 추가했지만 플레이어를 완전히 이동할 수 있는 경우에만 이를 실행합니다. 이는 플레이어가 움직여서는 안 되는 곳으로 이동하면 더 이상 이동을 처리하지 않는다는 의미입니다.
프로시저 단위 실행
세 번째이자 마지막 단계는 사용자 입력에 따라 플레이어를 앞으로, 뒤로 또는 옆으로 움직이는 것입니다.
회전과 마찬가지로 입력은 프로젝트마다 다르므로 간단히 _get_movement_input 함수를 호출하면 됩니다. 이 함수는 필요한 입력을 얻고 필요한 속도로 스케일링된 방향 벡터를 반환해야 합니다.
참고
회전과 마찬가지로 단순하게 유지합니다. 여기에서도 편안함 설정을 추가하는 것을 살펴보는 것이 좋습니다.
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
func _get_movement_input() -> Vector2:
# Implement this to return requested directional movement in meters per second.
return Vector2()
func _process_movement_on_input(delta):
# Remember where our player body currently is
var org_player_body: Vector3 = $CharacterBody3D.global_transform.origin
# We start with applying gravity
$CharacterBody3D.velocity.y -= gravity * delta
# Now we add in our movement
var input: Vector2 = _get_movement_input()
var movement: Vector3 = ($CharacterBody3D.global_transform.basis * Vector3(input.x, 0, input.y))
$CharacterBody3D.velocity.x = movement.x
$CharacterBody3D.velocity.z = movement.z
# Attempt to move our player
$CharacterBody3D.move_and_slide()
# And now apply the actual movement to our origin
global_transform.origin += $CharacterBody3D.global_transform.origin - org_player_body
func _physics_process(delta):
var is_colliding = _process_on_physical_movement(delta)
if !is_colliding:
_process_rotation_on_input(delta)
_process_movement_on_input(delta)
스프라이트 애니메이션
이 설정에서는 캐릭터 몸체를 루트 노드로 유지하므로 기존 게임 메커니즘과 결합하기가 더 쉽습니다.
여기에는 충돌 모양의 표준 캐릭터 몸체가 있고 XR 원점 노드 및 카메라는 일반 자식 노드입니다. 목 도우미 노드도 있습니다.
캐릭터 움직임 처리는 동일한 세 단계로 수행되지만 구현 방식은 약간 다릅니다.
참고
이 문서 외에 여러가지 Godot 데모 프로젝트들도 살펴보면 좋습니다.
프로시저 단위 실행
이 접근 방식에서는 1단계에서 모든 마법이 발생합니다. 이전 접근 방식과 마찬가지로 물리적 움직임을 캐릭터 몸체에 적용하지만 원점 노드에서 해당 움직임에 대응합니다.
이렇게 하면 플레이어의 위치가 캐릭터 신체의 위치와 동기화된 상태로 유지됩니다.
# Helper variables to keep our code readable
@onready var origin_node = $XROrigin3D
@onready var camera_node = $XROrigin3D/XRCamera3D
@onready var neck_position_node = $XROrigin3D/XRCamera3D/Neck
func _process_on_physical_movement(delta) -> bool:
# Remember our current velocity, we'll apply that later
var current_velocity = velocity
# Start by rotating the player to face the same way our real player is
var camera_basis: Basis = origin_node.transform.basis * camera_node.transform.basis
var forward: Vector2 = Vector2(camera_basis.z.x, camera_basis.z.z)
var angle: float = forward.angle_to(Vector2(0.0, 1.0))
# Rotate our character body
transform.basis = transform.basis.rotated(Vector3.UP, angle)
# Reverse this rotation our origin node
origin_node.transform = Transform3D().rotated(Vector3.UP, -angle) * origin_node.transform
# Now apply movement, first move our player body to the right location
var org_player_body: Vector3 = global_transform.origin
var player_body_location: Vector3 = origin_node.transform * camera_node.transform * neck_position_node.transform.origin
player_body_location.y = 0.0
player_body_location = global_transform * player_body_location
velocity = (player_body_location - org_player_body) / delta
move_and_slide()
# Now move our XROrigin back
var delta_movement = global_transform.origin - org_player_body
origin_node.global_transform.origin -= delta_movement
# Return our value
velocity = current_velocity
if (player_body_location - global_transform.origin).length() > 0.01:
# We'll talk more about what we'll do here later on
return true
else:
return false
func _physics_process(delta):
var is_colliding = _process_on_physical_movement(delta)
본질적으로 위의 코드는 캐릭터 몸체를 플레이어가 있는 곳으로 이동한 다음 원점 노드를 동일한 양으로 다시 이동합니다. 결과적으로 플레이어는 캐릭터 몸체 중앙에 위치하게 됩니다.
회전을 적용하는 것부터 시작합니다. 캐릭터 몸체는 플레이어가 이전 프레임을 보고 있던 곳을 향해야 합니다. 캐릭터 몸체 공간에서 카메라 방향을 계산합니다. 이제 플레이어가 머리를 회전한 각도를 계산할 수 있습니다. 캐릭터 몸을 같은 양만큼 회전시켜 캐릭터 몸이 플레이어와 같은 방향을 향하도록 합니다. 그런 다음 원점 노드에서 회전을 반전시켜 카메라가 플레이어와 다시 정렬되도록 합니다.
움직임에 있어서도 우리는 거의 같은 일을 합니다. 캐릭터 몸체는 플레이어가 이전 프레임에 서 있던 위치에 있어야 합니다. 플레이어가 이 위치에서 얼마나 이동했는지 계산합니다. 그런 다음 캐릭터 몸체를 이 위치로 이동하려고 합니다.
플레이어가 충돌 몸체에 부딪혀 멈출 수 있으므로 실제로 캐릭터 몸체를 이동한 양만큼만 원점을 뒤로 이동합니다. 따라서 플레이어는 이 위치에서 멀어질 수 있지만 이는 플레이어의 위치에 반영됩니다.
이전 솔루션과 마찬가지로 이 경우 true를 반환합니다.
프로시저 단위 실행
이 단계에서는 컨트롤러 입력에 따라 회전을 다시 적용합니다. 그러나 이 경우 코드는 일반적인 1인칭 게임에서 이를 구현하는 방법과 거의 동일합니다.
사용되는 입력은 필요에 따라 다를 수 있으므로 간단히 함수 ``_get_rotational_input``를 호출합니다. 이 함수는 필요한 입력을 얻고 초당 라디안 단위의 회전 속도를 반환해야 합니다.
func _get_rotational_input() -> float:
# Implement this function to return rotation in radians per second.
return 0.0
func _process_rotation_on_input(delta):
rotation.y += _get_rotational_input() * delta
func _physics_process(delta):
var is_colliding = _process_on_physical_movement(delta)
if !is_colliding:
_process_rotation_on_input(delta)
프로시저 단위 실행
3단계에서는 컨트롤러 입력에 따라 움직임을 다시 적용합니다. 그러나 2단계와 마찬가지로 이제 일반적인 1인칭 게임에서와 마찬가지로 이를 구현할 수 있습니다.
회전과 마찬가지로 입력은 프로젝트마다 다르므로 간단히 _get_movement_input 함수를 호출하면 됩니다. 이 함수는 필요한 입력을 얻고 필요한 속도로 스케일링된 방향 벡터를 반환해야 합니다.
func _get_movement_input() -> Vector2:
# Implement this to return requested directional movement in meters per second.
return Vector2()
func _process_movement_on_input(delta):
var movement_input = _get_movement_input()
var direction = global_transform.basis * Vector3(movement_input.x, 0, movement_input.y)
if direction:
velocity.x = direction.x
velocity.z = direction.z
else:
velocity.x = move_toward(velocity.x, 0, delta)
velocity.z = move_toward(velocity.z, 0, delta)
move_and_slide()
func _physics_process(delta):
var is_colliding = _process_on_physical_movement(delta)
if !is_colliding:
_process_rotation_on_input(delta)
_process_movement_on_input(delta)
플레이어를 트리 어딘가로 이동시킵니다.
플레이어가 잠겨진 방 밖에 있는 상황을 생각해 보세요. 문이 잠금 해제될 때까지 플레이어가 그 방에 들어가는 것을 원하지 않습니다. 또한 플레이어가 이 방에 무엇이 있는지 보는 것도 원하지 않습니다.
컨트롤러 입력을 통해 플레이어를 이동하는 논리는 이를 방지합니다. 플레이어는 정적인 몸체를 만나게 되고 코드는 플레이어가 방으로 이동하는 것을 방지합니다.
그러나 XR을 사용하면 플레이어가 실제 앞으로 나아가는 것을 방해하는 것은 없습니다.
위에서 설명한 두 가지 접근 방식을 통해 플레이어가 갈 수 없는 곳으로 캐릭터 몸이 움직이는 것을 방지할 수 있습니다. 플레이어가 물리적으로 이 위치로 이동했으므로 이제 카메라도 방으로 이동하게 됩니다.
논리적인 해결책은 움직임을 완전히 방지하고 플레이어가 방 밖에 머물도록 XR 원점 위치를 조정하는 것입니다.
이 접근 방식의 문제점은 이제 물리적 움직임이 가상 공간에서 복제되지 않는다는 것입니다. 이는 플레이어에게 메스꺼움을 유발할 것입니다.
대신 많은 XR 게임이 수행하는 작업은 플레이어가 물리적으로 있는 위치와 플레이어의 가상 신체가 남겨진 위치 사이의 거리를 측정하는 것입니다. 이 거리가 증가하면(보통 몇 센티미터 정도) 화면이 천천히 어두워집니다.
위의 솔루션을 사용하면 1단계 끝에서 이 논리를 코드에 추가할 수 있습니다.
제시된 코드에 대한 추가 개선 사항은 다음과 같습니다.
이 거리가 여전히 작은 한 컨트롤러 입력을 허용합니다.
컨트롤러 입력이 비활성화된 경우에도 여전히 플레이어에 중력을 적용합니다.
참고
데모 저장소의 움직임 데모에는 사용자가 제한된 구역에 들어갈 때 화면이 검게 변하는 예가 포함되어 있습니다.
개선을 위한 추가 제안
위의 내용은 룸 스케일 XR 게임 구현을 위한 출발점으로 두 가지 좋은 옵션을 제공합니다.
구현하고 싶을 가능성이 있는 지적할 가치가 있는 몇 가지 추가 사항은 다음과 같습니다.
카메라 높이를 사용하여 플레이어가 서 있는지, 웅크리고 있는지, 점프하는지, 누워 있는지 감지할 수 있습니다. 이에 따라 충돌 모양의 크기와 방향을 조정할 수 있습니다. 여러 충돌 모양을 추가하여 머리와 몸체가 보다 정확한 크기의 고유한 모양을 갖도록 추가 보너스 포인트를 제공합니다.
씬이 처음 로드되면 플레이어가 추적 공간의 중심에서 멀리 떨어져 있을 수 있습니다. 이로 인해 플레이어가 원점과 다른 방에서 생성될 수 있습니다. 이제 게임은 플레이어 몸을 시작 지점에서 플레이어가 서있는 곳으로 이동하려고 시도했지만 실패했습니다. 플레이어가 올바른 시작 위치에 있도록 원점을 이동하는 재설정 기능을 구현해야 합니다.
위의 두 가지 개선 사항 모두 플레이어가 준비되어 똑바로 서 있어야 합니다. 플레이어가 여전히 헤드셋을 착용하고 있을 수 있으므로 보장할 수 없습니다.
XR 도구를 포함한 많은 게임에서는 플레이어가 준비가 되었을 때 버튼을 눌러야 하는 인트로 화면이나 로딩 화면을 도입하여 이 문제를 해결합니다. 이 시작 환경은 플레이어의 위치가 플레이어가 보는 것에 거의 영향을 미치지 않는 넓은 위치인 경우가 많습니다. 플레이어가 준비되면 버튼을 누르는 순간, 카메라의 위치와 높이를 기록하는 순간입니다.