Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Raumgröße in XR

Eines der Hauptmerkmale von XR-Projekten ist die Möglichkeit, sich in einem großen Raum frei zu bewegen. Dieser Raum ist oft durch das Zimmer begrenzt, in dem sich der Spieler befindet, und die Tracking-Sensoren sind innerhalb dieses Raums platziert. Mit dem Aufkommen von Inside-Out-Tracking sind jedoch immer größere Spielräume möglich.

Als Entwickler bringt dies eine Reihe von interessanten Herausforderungen mit sich. In diesem Dokument gehen wir auf einige der Herausforderungen ein, mit denen Sie konfrontiert werden können, und skizzieren einige Lösungen. Wir werden die Probleme und Herausforderungen für XR-Spiele im Sitzen in einem anderen Dokument diskutieren.

Bemerkung

Oft sitzen die Entwickler hinter ihrem Schreibtisch, während sie die Grundlage für ihr Spiel schaffen. In diesem Modus zeigen sich die Probleme bei der Entwicklung für die Raumgröße erst, wenn es zu spät ist. Hier ist es ratsam, so früh wie möglich mit dem Testen im Stehen und beim Herumlaufen zu beginnen. Sobald Sie sich sicher sind, dass Ihr Fundament solide ist, können Sie bequem im Sitzen entwickeln.

In traditionellen Ego-Spielen wird ein Spieler durch einen CharacterBody3D-Node dargestellt. Dieser Node wird durch die Verarbeitung traditioneller Controller-, Maus- oder Tastatureingaben bewegt. Eine Kamera ist an diesem Node an einer Stelle angebracht, an der sich ungefähr der Kopf des Spielers befindet.

Wendet man dieses Modell auf das XR-Setup an, fügt man einen XROrigin3D-Node als Child-Node des Character-Bodys und einen XRCamera3D als Child-Node des Origin-Nodes hinzu. Auf den ersten Blick scheint dies zu funktionieren. Bei näherer Betrachtung berücksichtigt dieses Modell jedoch nicht, dass es in XR zwei Formen der Bewegung gibt. Die Bewegung durch die Controller-Eingabe und die physische Bewegung des Spielers in der realen Welt.

Folglich stellt der Origin-Node nicht die Position des Spielers dar. Er stellt die Mitte oder den Anfang des Tracking-Raums dar, in dem sich der Spieler physisch bewegen kann. Wenn sich der Spieler in seinem Zimmer bewegt, wird diese Bewegung durch das Tracking des Spieler-Headsets dargestellt. Im Spiel bedeutet dies, dass die Position des Kamera-Nodes entsprechend aktualisiert wird. In jeder Hinsicht verfolgen wir einen körperlosen Kopf. Solange keine Körper-Tracking verfügbar ist, haben wir keine Kenntnis über die Position oder Ausrichtung des Spielerkörpers.

../../_images/XRRoomCenterWalk.gif

Das erste Problem, das dadurch entsteht, ist ziemlich offensichtlich. Wenn sich der Spieler mit Hilfe der Controller-Eingabe bewegt, können wir den gleichen Ansatz wie in normalen Spielen verwenden und den Spieler in Vorwärtsrichtung bewegen. Der Spieler befindet sich jedoch nicht dort, wo wir denken, dass er ist, und während wir uns vorwärts bewegen, überprüfen wir Kollisionen an der falschen Stelle.

../../_images/XRRoomWalkOffCliff.gif

Das zweite Problem zeigt sich, wenn der Spieler sich weiter vom Zentrum des Tracking-Raums entfernt und die Controller-Eingabe zum Drehen verwendet. Wenn wir den Körper unserer Figur drehen, wird der Spieler kreisförmig durch den Raum bewegt.

../../_images/XRRoomRotateOrigin.gif

Wenn wir die oben genannten Probleme lösen, werden wir ein drittes Problem finden. Wenn der Weg des Spielers in der virtuellen Welt blockiert ist, kann sich der Spieler trotzdem physisch vorwärts bewegen.

../../_images/XRRoomWalkWall.gif

Wir werden die ersten beiden Probleme mit zwei separaten Lösungen angehen und uns dann mit dem dritten Problem befassen.

Ursprungszentrierte Lösung

Beim ersten Ansatz zur Lösung dieses Problems werden wir unsere Struktur ändern. Dies ist der Ansatz, der derzeit in XR Tools implementiert ist.

../../_images/xr_room_scale_origin_body.webp

Bei dieser Einstellung wird der Character Body als oberste Ebene markiert, damit er sich nicht mit dem Ursprung bewegt.

Wir haben auch einen Helfer-Node, der uns sagt, wo sich unser Nackengelenk in Bezug auf unsere Kamera befindet. Damit können wir bestimmen, wo sich unser Körperzentrum befindet.

Die Verarbeitung unserer Charakterbewegung erfolgt nun in drei Schritten.

Bemerkung

Die Ursprungszentrierte Bewegungsdemo enthält ein ausführlicheres Beispiel für die unten beschriebene Methode.

Schritt 1

Im ersten Schritt verarbeiten wir die physische Bewegung des Spielers. Wir bestimmen, wo sich der Spieler gerade befindet, und versuchen, unseren Character Body dorthin zu bewegen.

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)

Beachten Sie, daß wir true von unserer Funktion _process_on_physical_movement zurückgeben, wenn wir unseren Spieler nicht komplett zum Ziel bewegen konnten.

Schritt 2

Der zweite Schritt besteht darin, die Rotation des Spielers als Ergebnis der Benutzereingabe zu behandeln.

Da die verwendete Eingabe je nach Bedarf unterschiedlich sein kann, rufen wir einfach die Funktion _get_rotational_input auf. Diese Funktion sollte die notwendige Eingabe erhalten und die Rotationsgeschwindigkeit in Bogenmaß pro Sekunde zurückgeben.

Bemerkung

Für unser Beispiel werden wir dies einfach und direkt halten. Wir werden uns nicht um Komfortfunktionen wie das Snap-Turning und die Anwendung einer Vignette kümmern. Wir empfehlen dringend, solche Komfortfunktionen zu implementieren.

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)

Bemerkung

Wir haben den Aufruf zur Verarbeitung unserer Drehung zu unserem Physikprozess hinzugefügt, aber wir führen ihn nur aus, wenn wir unseren Spieler vollständig bewegen konnten. Das heißt, wenn sich der Spieler an eine Stelle bewegt, an die er sich nicht bewegen sollte, wird keine weitere Bewegung ausgeführt.

Schritt 3

Der dritte und letzte Schritt ist die Bewegung des Spielers vorwärts, rückwärts oder seitwärts als Ergebnis der Benutzereingabe.

Genau wie bei der Rotation unterscheiden sich die Eingaben von Projekt zu Projekt, so dass wir einfach die Funktion _get_movement_input aufrufen. Diese Funktion sollte die erforderlichen Eingaben erhalten und einen Richtungsvektor zurückgeben, der auf die erforderliche Geschwindigkeit skaliert ist.

Bemerkung

Genau wie bei der Rotation halten wir es einfach. Auch hier ist es ratsam, zusätzliche Komforteinstellungen vorzunehmen.

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)

Charakter-zentrische Lösung

In diesem Fall werden wir unseren Character Body als Root Node beibehalten, was die Kombination mit traditionellen Spielmechaniken erleichtert.

../../_images/xr_room_scale_character_body.webp

Hier haben wir einen Standard-Character Body mit Collision Shape, und unser XR Origin-Node und unsere Kamera als normale Child-Nodes. Wir haben auch unsere Hals-Helfer-Node.

Die Verarbeitung unserer Charakterbewegung erfolgt in denselben drei Schritten, ist aber etwas anders implementiert.

Bemerkung

Die Charakterzentrierte Bewegungsdemo enthält ein ausführlicheres Beispiel für die unten beschriebene Methode.

Schritt 1

Bei diesem Ansatz ist Schritt 1 der Punkt, an dem die ganze Magie passiert. Genau wie bei unserem vorherigen Ansatz werden wir unsere physische Bewegung auf den Character Body anwenden, aber wir werden dieser Bewegung am Origin-Node entgegenwirken.

Dadurch wird sichergestellt, dass die Position des Spielers mit der Position des Charakters übereinstimmt.

# 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)

Im Wesentlichen wird der obige Code den Character Body dorthin bewegen, wo sich der Spieler befindet, und dann den Origin-Node in gleichem Maße zurückbewegen. Das Ergebnis ist, dass der Spieler über dem Character Body zentriert bleibt.

Wir beginnen mit der Anwendung der Rotation. Der Character Body sollte dorthin gerichtet sein, wohin der Spieler im vorherigen Bild geschaut hat. Wir berechnen unsere Kameraausrichtung im Raum des Character Bodys. Wir können nun den Winkel berechnen, um den der Spieler seinen Kopf gedreht hat. Wir drehen unseren Character Body um den gleichen Betrag, damit unser Character Body in die gleiche Richtung wie der Spieler schaut. Dann kehren wir die Drehung am Origin-Node um, so dass die Kamera wieder mit dem Spieler ausgerichtet ist.

Für die Bewegung machen wir das Gleiche. Der Character Body sollte sich an der Stelle befinden, wo der Spieler im vorigen Frame gestanden hat. Wir berechnen, um wie viel sich der Spieler von dieser Position aus bewegt hat. Dann versuchen wir, den Character Body an diese Stelle zu bewegen.

Da der Spieler auf einen Collision Body treffen und angehalten werden kann, verschieben wir den Ursprung nur um den Betrag zurück, um den wir den Character Body tatsächlich bewegt haben. Der Spieler kann sich also von diesem Punkt wegbewegen, aber das wird sich in der Positionierung des Spielers widerspiegeln.

Wie bei unserer vorherigen Lösung geben wir true zurück, wenn dies der Fall ist.

Schritt 2

Auch in diesem Schritt wird die Rotation auf der Grundlage der Controller-Eingabe durchgeführt. In diesem Fall ist der Code jedoch fast identisch mit dem, wie man dies in einem normalen Ego-Spiel implementieren würde.

Da die verwendete Eingabe je nach Bedarf unterschiedlich sein kann, rufen wir einfach die Funktion _get_rotational_input auf. Diese Funktion sollte die notwendige Eingabe erhalten und die Rotationsgeschwindigkeit in Bogenmaß pro Sekunde zurückgeben.

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)

Schritt 3

In Schritt drei wenden wir die Bewegung wieder auf der Grundlage der Controller-Eingabe an. Doch genau wie bei Schritt 2 können wir dies nun wie in einem normalen Ego-Spiel umsetzen.

Genau wie bei der Rotation unterscheiden sich die Eingaben von Projekt zu Projekt, so dass wir einfach die Funktion _get_movement_input aufrufen. Diese Funktion sollte die erforderlichen Eingaben erhalten und einen Richtungsvektor zurückgeben, der auf die erforderliche Geschwindigkeit skaliert ist.

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)

Wenn der Spieler irgendwo hingeht, wo er nicht hingehen sollte

Stellen Sie sich eine Situation vor, in der sich der Spieler vor einem verschlossenen Raum befindet. Sie möchten nicht, dass der Spieler den Raum betritt, bevor die Tür nicht aufgeschlossen ist. Sie wollen auch nicht, dass der Spieler sieht, was sich in diesem Raum befindet.

Die Logik für die Bewegung des Spielers durch die Controllereingabe verhindert dies auf angenehme Weise. Der Spieler trifft auf einen Static Body, und der Code verhindert, dass er sich in den Raum bewegt.

Doch mit XR hindert nichts den Spieler daran, einen echten Schritt nach vorne zu machen.

Mit den beiden oben ausgearbeiteten Ansätzen verhindern wir, dass sich der Character Body dorthin bewegt, wo der Spieler nicht hin kann. Da sich der Spieler physisch an diesen Ort bewegt hat, wird die Kamera nun in den Raum bewegt.

Die logische Lösung wäre, die Bewegung ganz zu verhindern und den XR-Ursprungspunkt so zu platzieren, dass der Spieler außerhalb des Raumes bleibt.

Das Problem bei diesem Ansatz ist, dass die physische Bewegung im virtuellen Raum nicht mehr nachgebildet wird. Dies wird beim Spieler Übelkeit hervorrufen.

Bei vielen XR-Spielen wird stattdessen die Entfernung zwischen dem physischen Standort des Spielers und dem Ort, an dem der virtuelle Body des Spielers zurückgelassen wurde, gemessen. Wenn dieser Abstand zunimmt, in der Regel bis zu einer Entfernung von einigen Zentimetern, wird der Bildschirm langsam schwarz.

Unsere obigen Lösungen würden es uns ermöglichen, diese Logik am Ende von Schritt 1 in den Code einzufügen.

Weitere Verbesserungen des vorgestellten Codes könnten sein:

  • Controller-Eingaben zu ermöglichen, solange dieser Abstand noch gering ist,

  • Schwerkraft auf den Spieler ausüben, auch wenn die Controller-Eingabe deaktiviert ist.

Bemerkung

Die Bewegungsdemos in unserem Demo-Repository enthalten ein Beispiel für die Verdunkelung des Bildschirms, wenn ein Benutzer in gesperrte Gebiete geht.

Weitere Vorschläge für Verbesserungen

Die obigen Ausführungen bieten zwei gute Ausgangspunkte für die Umsetzung von XR-Spielen in Raumgröße.

Es gibt noch ein paar weitere Dinge, die Sie wahrscheinlich umsetzen wollen werden:

  • Anhand der Höhe der Kamera lässt sich erkennen, ob der Spieler steht, hockt, springt oder liegt. Sie können die Größe und Ausrichtung der Collision Shape entsprechend anpassen. Extra-Bonuspunkte gibt es für das Hinzufügen mehrerer Collision Shapes, so dass der Kopf und der Körper ihre eigenen, genauer bemessenen Shapes haben.

  • Wenn eine Szene zum ersten Mal geladen wird, kann es sein, dass der Spieler weit von der Mitte des Tracking-Raums entfernt ist. Dies könnte dazu führen, dass der Spieler in einem anderen Raum als unserem Ursprung spawnen wird. Das Spiel wird nun versuchen, den Body des Spielers vom Ursprung dorthin zu bewegen, wo der Spieler steht, was jedoch fehlschlägt. Sie sollten eine Rücksetzfunktion implementieren, die den Ursprung so verschiebt, dass sich der Spieler in der richtigen Startposition befindet.

Beide oben genannten Verbesserungen setzen voraus, dass der Spieler bereit ist und aufrecht steht. Es gibt keine Garantie, da der Spieler noch dabei sein kann, das Headset aufzusetzen.

Viele Spiele, darunter auch XR-Tools, lösen dieses Problem durch die Einführung eines Intro- oder Ladebildschirms, auf dem der Spieler eine Taste drücken muss, wenn er bereit ist. Diese Startumgebung ist oft ein großer Ort, an dem die Position des Spielers wenig Einfluss darauf hat, was der Spieler sieht. Wenn der Spieler bereit ist und die Taste drückt, ist dies der Moment, in dem Sie die Position und Höhe der Kamera aufzeichnen.