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...
Escala de sala (Room scale) em XR
Um dos elementos principais dos projetos de XR é a capacidade de andar livremente em um grande espaço. Este espaço é frequentemente limitado pela sala em que o jogador está fisicamente, com sensores de rastreamento posicionados dentro deste espaço. Com o advento do rastreamento de dentro para fora (inside-out tracking), no entanto, espaços de jogo cada vez maiores são possíveis.
Como desenvolvedor, isso introduz uma série de desafios interessantes. Neste documento, examinaremos vários dos desafios que você pode enfrentar e delinearemos algumas soluções. Discutiremos os problemas e desafios para jogos de XR sentados em outro documento.
Nota
Muitas vezes, os desenvolvedores sentam-se atrás de suas mesas enquanto constroem a base de seu jogo. Neste modo, os problemas de desenvolvimento para a escala de sala (room scale) não se manifestam até que seja tarde demais. O conselho aqui é começar a testar em pé e andando o mais cedo possível. Uma vez que você esteja satisfeito que sua base está sólida, você pode desenvolver confortavelmente enquanto permanece sentado.
Em jogos tradicionais em primeira pessoa, um jogador é representado por um nó CharacterBody3D. Este nó é movido processando a entrada tradicional de controle, mouse ou teclado. Uma câmera é anexada a este nó em um local aproximadamente onde a cabeça do jogador estaria.
Aplicando este modelo à configuração de XR, adicionamos um nó XROrigin3D como filho do corpo do personagem e adicionamos uma XRCamera3D como filha do nó de origem. À primeira vista, isso parece funcionar. No entanto, após um exame mais detalhado, este modelo não leva em consideração que existem duas formas de movimento em XR: o movimento através da entrada do controle e o movimento físico do jogador no mundo real.
Como resultado, o nó de origem não representa a posição do jogador. Ele representa o centro, ou o início, do espaço de rastreamento no qual o jogador pode se mover fisicamente. À medida que o jogador se move pela sala, esse movimento é representado através do rastreamento do headset do jogador. No jogo, isso se traduz na atualização correspondente da posição do nó da câmera. Para todos os efeitos, estamos rastreando uma cabeça desincorporada. A menos que o rastreamento corporal esteja disponível, não temos conhecimento da posição ou orientação do corpo do jogador.
O primeiro problema que isso causa é bastante óbvio. Quando o jogador se move com a entrada do controle, podemos usar a mesma abordagem de jogos normais e mover o jogador em uma direção para a frente. No entanto, o jogador não está onde pensamos que está e, conforme avançamos, estamos verificando colisões no local errado.
O segundo problema realmente se manifesta quando o jogador se afasta do centro do espaço de rastreamento e usa a entrada do controle para girar. Se rotacionarmos o corpo do nosso personagem, o jogador será movido ao redor da sala de maneira circular.
Se corrigirmos os problemas acima, encontraremos um terceiro problema. Quando o caminho para o jogador é bloqueado no mundo virtual, o jogador ainda pode se mover fisicamente para a frente.
Examinaremos a resolução dos dois primeiros problemas com duas soluções separadas e, em seguida, discutiremos como lidar com o terceiro.
Solução centrada na origem
Olhando para a primeira abordagem para resolver isso, vamos mudar nossa estrutura. Esta é a abordagem atualmente implementada no XR Tools.
Nesta configuração, marcamos o corpo do personagem como nível superior (top level) para que ele não se mova com a origem.
Também temos um nó auxiliar que nos diz onde está a articulação do nosso pescoço em relação à nossa câmera. Usamos isso para determinar onde está o centro do nosso corpo.
O processamento do movimento do nosso personagem agora é feito em três etapas.
Nota
O demo de movimento centrado na origem contém um exemplo mais elaborado da técnica descrita abaixo.
Passo 1
Na primeira etapa, vamos processar o movimento físico do jogador. Determinamos onde o jogador está agora e tentamos mover o corpo do nosso personagem para lá.
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)
Note que estamos retornando true da nossa função _process_on_physical_movement quando não conseguimos mover nosso jogador por completo.
Passo 2
A segunda etapa é lidar com a rotação do jogador como resultado da entrada do usuário.
Como a entrada usada pode diferir com base nas suas necessidades, estamos simplesmente chamando a função _get_rotational_input. Esta função deve obter a entrada necessária e retornar a velocidade de rotação em radianos por segundo.
Nota
Para o nosso exemplo, vamos manter isso simples e direto. Não vamos nos preocupar com recursos de conforto, como rotação em estalos (snap turning) e aplicação de vinheta. Recomendamos fortemente a implementação de tais recursos de conforto.
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)
Nota
Adicionamos a chamada para processar nossa rotação ao nosso processo de física, mas só a executamos se fomos capazes de mover nosso jogador totalmente. Isso significa que se o jogador se mover para algum lugar onde não deveria, não processamos mais movimentos.
Passo 3
A terceira e última etapa é mover o jogador para a frente, para trás ou para os lados como resultado da entrada do usuário.
Assim como na rotação, as entradas diferem de projeto para projeto, então estamos simplesmente chamando a função _get_movement_input. Esta função deve obter a entrada necessária e retornar um vetor direcional escalonado para a velocidade exigida.
Nota
Assim como na rotação, estamos mantendo as coisas simples. Aqui também é aconselhável considerar a adição de configurações de conforto.
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)
Solução centrada no corpo do personagem
Nesta configuração, vamos manter o corpo do nosso personagem como nosso nó raiz e, como tal, é mais fácil de combinar com mecânicas de jogo tradicionais.
Aqui temos um corpo de personagem padrão com formato de colisão, e nosso nó de origem XR e câmera como filhos normais. Também temos nosso nó auxiliar de pescoço.
O processamento do movimento do nosso personagem é feito nas mesmas três etapas, mas implementado de forma ligeiramente diferente.
Nota
O demo de movimento centrado no personagem contém um exemplo mais elaborado da técnica descrita abaixo.
Passo 1
Nesta abordagem, a etapa 1 é onde toda a mágica acontece. Assim como na nossa abordagem anterior, aplicaremos nosso movimento físico ao corpo do personagem, mas compensaremos esse movimento no nó de origem.
Isso garantirá que a localização do jogador permaneça em sincronia com a localização do corpo do personagem.
# 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)
Em essência, o código acima moverá o corpo do personagem para onde o jogador está e, em seguida, moverá o nó de origem de volta em quantidades iguais. O resultado é que o jogador permanece centralizado acima do corpo do personagem.
Começamos aplicando a rotação. O corpo do personagem deve estar voltado para onde o jogador estava olhando no frame anterior. Calculamos a orientação da nossa câmera no espaço do corpo do personagem. Podemos agora calcular o ângulo pelo qual o jogador rotacionou sua cabeça. Rotacionamos o corpo do nosso personagem na mesma quantidade para que o corpo do nosso personagem fique voltado para a mesma direção do jogador. E então revertemos a rotação no nó de origem para que a câmera termine alinhada com o jogador novamente.
Para o movimento, fazemos quase o mesmo. O corpo do personagem deve estar onde o jogador estava no frame anterior. Calculamos o quanto o jogador se moveu a partir deste local. Então, tentamos mover o corpo do personagem para este local.
Como o jogador pode atingir um corpo de colisão e ser parado, só movemos o ponto de origem de volta na quantidade que realmente movemos o corpo do personagem. O jogador pode, assim, se afastar deste local, mas isso será refletido no posicionamento do jogador.
Assim como em nossa solução anterior, retornamos verdadeiro se este for o caso.
Passo 2
Nesta etapa, aplicamos novamente a rotação com base na entrada do controle. No entanto, neste caso, o código é quase idêntico a como se implementaria isso em um jogo normal em primeira pessoa.
Como a entrada usada pode diferir com base nas suas necessidades, estamos simplesmente chamando a função _get_rotational_input. Esta função deve obter a entrada necessária e retornar a velocidade de rotação em radianos por segundo.
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)
Passo 3
Para a etapa três, aplicamos novamente o movimento com base na entrada do controle. No entanto, assim como na etapa 2, agora podemos implementar isso como faríamos em um jogo normal em primeira pessoa.
Assim como na rotação, as entradas diferem de projeto para projeto, então estamos simplesmente chamando a função _get_movement_input. Esta função deve obter a entrada necessária e retornar um vetor direcional escalonado para a velocidade exigida.
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)
Quando o jogador caminha para onde não deveria
Pense em uma situação em que o jogador está do lado fora de uma sala trancada. Você não quer que o jogador entre nessa sala até que a porta esteja destrancada. Você também não quer que o jogador veja o que está nesta sala.
A lógica para mover o jogador através da entrada do controle evita isso perfeitamente. O jogador encontra um corpo estático e o código impede que o jogador se mova para dentro da sala.
No entanto, com XR, nada impede o jogador de dar um passo real para a frente.
Com ambas as abordagens detalhadas acima, evitaremos que o corpo do personagem se mova para onde o jogador não pode ir. Como o jogador se moveu fisicamente para este local, a câmera agora terá se movido para dentro da sala.
A solução lógica seria impedir totalmente o movimento e ajustar o posicionamento do ponto de origem XR para que o jogador permaneça fora da sala.
O problema com essa abordagem é que o movimento físico agora não é replicado no espaço virtual. Isso causará náuseas ao jogador.
O que muitos jogos de XR fazem, em vez disso, é medir a distância entre onde o jogador está fisicamente e onde o corpo virtual do jogador foi deixado para trás. À medida que essa distância aumenta, geralmente para uma distância de alguns centímetros, a tela escurece lentamente (blackout).
Nossas soluções acima nos permitiriam adicionar essa lógica ao código no final da etapa 1.
Melhorias adicionais ao código apresentado poderiam ser:
permitir a entrada do controle desde que essa distância ainda seja pequena,
ainda aplicar gravidade ao jogador mesmo quando a entrada do controle estiver desativada.
Nota
As demonstrações de movimento em nosso repositório de demos contêm um exemplo de escurecimento da tela quando um usuário caminha em áreas restritas.
Sugestões adicionais para melhorias
O texto acima fornece duas boas opções como pontos de partida para a implementação de jogos de XR em escala de sala.
Mais algumas coisas que valem a pena apontar que você provavelmente desejará implementar:
A altura da câmera pode ser usada para detectar se o jogador está em pé, agachado, pulando ou deitado. Você pode ajustar o tamanho e a orientação do formato de colisão de acordo. Pontos de bônus extras por adicionar múltiplos formatos de colisão para que a cabeça e o corpo tenham seus próprios formatos, com tamanhos mais precisos.
Quando uma cena é carregada pela primeira vez, o jogador pode estar longe do centro do espaço de rastreamento. Isso pode fazer com que o jogador surja em uma sala diferente do nosso ponto de origem. O jogo agora tentará, e falhará, mover o corpo do jogador do ponto de partida para onde o jogador está em pé. Você deve implementar uma função de redefinição que mova o ponto de origem para que o jogador fique na posição inicial correta.
Ambas as melhorias acima exigem que o jogador esteja pronto e de pé. Não há garantia, pois o jogador ainda pode estar colocando o headset.
Muitos jogos, incluindo o XR Tools, resolvem isso introduzindo uma tela de introdução ou tela de carregamento onde o jogador deve pressionar um botão quando estiver pronto. Este ambiente inicial é frequentemente um local amplo onde o posicionamento do jogador tem pouco impacto no que ele vê. Quando o jogador está pronto e pressiona o botão, este é o momento em que você registra a posição e a altura da câmera.