player.gd 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. extends CharacterBody3D
  2. # Settings to control the character.
  3. @export var rotation_speed := 1.0
  4. @export var movement_speed := 5.0
  5. @export var movement_acceleration := 5.0
  6. # Get the gravity from the project settings to be synced with RigidBody nodes.
  7. var gravity := float(ProjectSettings.get_setting("physics/3d/default_gravity"))
  8. # Helper variables to keep our code readable.
  9. @onready var origin_node: XROrigin3D = $XROrigin3D
  10. @onready var camera_node: XRCamera3D = $XROrigin3D/XRCamera3D
  11. @onready var neck_position_node: Node3D = $XROrigin3D/XRCamera3D/Neck
  12. @onready var black_out: Node3D = $XROrigin3D/XRCamera3D/BlackOut
  13. ## Called when the user has requested their view to be recentered.
  14. func recenter() -> void:
  15. var xr_interface: OpenXRInterface = XRServer.find_interface("OpenXR")
  16. if not xr_interface:
  17. push_error("Couldn't access OpenXR interface!")
  18. return
  19. var play_area_mode: XRInterface.PlayAreaMode = xr_interface.get_play_area_mode()
  20. if play_area_mode == XRInterface.XR_PLAY_AREA_SITTING:
  21. push_warning("Sitting play space is not suitable for this setup.")
  22. elif play_area_mode == XRInterface.XR_PLAY_AREA_ROOMSCALE:
  23. # This is already handled by the headset.
  24. pass
  25. else:
  26. # Use Godot's own logic.
  27. XRServer.center_on_hmd(XRServer.RESET_BUT_KEEP_TILT, true)
  28. # XRCamera3D node won't be updated yet, so go straight to the source!
  29. var head_tracker: XRPositionalTracker = XRServer.get_tracker("head")
  30. if not head_tracker:
  31. push_error("Couldn't locate head tracker!")
  32. return
  33. var pose: XRPose = head_tracker.get_pose("default")
  34. var head_transform: Transform3D = pose.get_adjusted_transform()
  35. # Get neck transform in XROrigin3D space
  36. var neck_transform: Transform3D = neck_position_node.transform * head_transform
  37. # Reset our XROrigin transform and apply the inverse of the neck position.
  38. var new_origin_transform: Transform3D = Transform3D()
  39. new_origin_transform.origin.x = -neck_transform.origin.x
  40. new_origin_transform.origin.y = 0.0
  41. new_origin_transform.origin.z = -neck_transform.origin.z
  42. origin_node.transform = new_origin_transform
  43. # Finally reset character orientation
  44. transform.basis = Basis()
  45. # Returns our move input by querying the move action on each controller.
  46. func _get_movement_input() -> Vector2:
  47. var movement := Vector2()
  48. # If move is not bound to one of our controllers,
  49. # that controller will return `Vector2.ZERO`.
  50. movement += $XROrigin3D/LeftHand.get_vector2("move")
  51. movement += $XROrigin3D/RightHand.get_vector2("move")
  52. return movement
  53. # `_process_on_physical_movement()` handles the physical movement of the player
  54. # adjusting our character body position to "catch up to" the player.
  55. # If the character body encounters an obstruction our view will black out
  56. # and we will stop further character movement until the player physically
  57. # moves back.
  58. func _process_on_physical_movement(delta: float) -> bool:
  59. # Remember our current velocity, as we'll apply that later.
  60. var current_velocity := velocity
  61. # Start by rotating the player to face the same way our real player is.
  62. var camera_basis: Basis = origin_node.transform.basis * camera_node.transform.basis
  63. var forward: Vector2 = Vector2(camera_basis.z.x, camera_basis.z.z)
  64. var angle: float = forward.angle_to(Vector2(0.0, 1.0))
  65. # Rotate our character body.
  66. transform.basis = transform.basis.rotated(Vector3.UP, angle)
  67. # Reverse this rotation our origin node.
  68. origin_node.transform = Transform3D().rotated(Vector3.UP, -angle) * origin_node.transform
  69. # Now apply movement, first move our player body to the right location.
  70. var org_player_body: Vector3 = global_transform.origin
  71. var player_body_location: Vector3 = origin_node.transform * camera_node.transform * neck_position_node.transform.origin
  72. player_body_location.y = 0.0
  73. player_body_location = global_transform * player_body_location
  74. velocity = (player_body_location - org_player_body) / delta
  75. move_and_slide()
  76. # Now move our XROrigin back.
  77. var delta_movement := global_transform.origin - org_player_body
  78. origin_node.global_transform.origin -= delta_movement
  79. # Negate any height change in local space due to player hitting ramps, etc.
  80. origin_node.transform.origin.y = 0.0
  81. # Return our value.
  82. velocity = current_velocity
  83. # Check if we managed to move where we wanted to.
  84. var location_offset := (player_body_location - global_transform.origin).length()
  85. if location_offset > 0.1:
  86. # We couldn't go where we wanted to, black out our screen.
  87. black_out.fade = clampf((location_offset - 0.1) / 0.1, 0.0, 1.0)
  88. return true
  89. else:
  90. black_out.fade = 0.0
  91. return false
  92. # `_process_movement_on_input()` handles movement through controller input.
  93. # We first handle rotating the player and then apply movement.
  94. # We also apply the effects of gravity at this point.
  95. func _process_movement_on_input(is_colliding: bool, delta: float) -> void:
  96. if not is_colliding:
  97. # Only handle input if we've not physically moved somewhere we shouldn't.
  98. var movement_input := _get_movement_input()
  99. # First handle rotation, to keep this example simple we are implementing
  100. # "smooth" rotation here. This can lead to motion sickness.
  101. # Adding a comfort option with "stepped" rotation is good practice but
  102. # falls outside of the scope of this demonstration.
  103. rotation.y += -movement_input.x * delta * rotation_speed
  104. # Now handle forward/backwards movement.
  105. # Straffing can be added by using the movement_input.x input
  106. # and using a different input for rotational control.
  107. # Straffing is more prone to motion sickness.
  108. var direction := global_transform.basis * Vector3(0.0, 0.0, -movement_input.y) * movement_speed
  109. if direction:
  110. velocity.x = move_toward(velocity.x, direction.x, delta * movement_acceleration)
  111. velocity.z = move_toward(velocity.z, direction.z, delta * movement_acceleration)
  112. else:
  113. velocity.x = move_toward(velocity.x, 0, delta * movement_acceleration)
  114. velocity.z = move_toward(velocity.z, 0, delta * movement_acceleration)
  115. # Always handle gravity
  116. velocity.y -= gravity * delta
  117. move_and_slide()
  118. # `_physics_process()` handles our player movement.
  119. func _physics_process(delta: float) -> void:
  120. var is_colliding := _process_on_physical_movement(delta)
  121. _process_movement_on_input(is_colliding, delta)