I am trying to make a 3D space exploration game, where a player can walk around a planet. I have a basic working rigidbody player movement script that allows a player to move around, as well as a planet which uses an Area3D node for localised gravity - this all works well, but I have no idea how to maintain the rotation of the player so it can walk around the entirety of the sphere smoothly
I have a reference to the “down vector” supposedly where the players feet should be, but anything I’ve tried with basis and quaternions to rotate the player hasn’t worked out. if anyone can point me in a good direction for this itd be great
all the tutorials i’ve seen are only for older versions of godot . script below
extends RigidBody3D
const MOVE_SPEED = 20.0
const SENSITIVITY = 0.004
const JUMP_FORCE = 10.0
var DownVector: Vector3
@onready var head = $PlayerHead
@onready var camera = $PlayerHead/Camera3D
var _velocity = Vector3.ZERO
# Ensures we cannot see the cursoer when moving the camera
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
# Handles camera movement itself
func _unhandled_input(event):
if event is InputEventMouseMotion:
head.rotate_y(-event.relative.x * SENSITIVITY)
camera.rotate_x(-event.relative.y * SENSITIVITY)
camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(-40), deg_to_rad(60))
func _physics_process(delta: float) -> void:
# Get input direction
var direction = Vector3.ZERO
# Basic impulse to add jumping - need to check for grounded
if Input.is_action_just_pressed("jump"):
apply_impulse(Vector3.UP * JUMP_FORCE, Vector3.ZERO)
# Set the movement direction
if Input.is_action_pressed("forward"):
direction.z -= 1
if Input.is_action_pressed("backward"):
direction.z += 1
if Input.is_action_pressed("left"):
direction.x -= 1
if Input.is_action_pressed("right"):
direction.x += 1
# Normalize the direction to ensure consistent speed when moving diagonally
if direction.length() > 0:
direction = direction.normalized()
# Calculate the target velocity
_velocity = direction * MOVE_SPEED
# Ensure velocity is in the direction we are facing
_velocity = _velocity.rotated(Vector3.UP, head.rotation.y)
# Apply the final velocity
apply_central_force(_velocity)
func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
var down_vector = state.total_gravity.normalized()
You’re not making it easier. You’re making it harder. Gravity is already implemented. You can literally make a CharacterBody3D with boilerplate code and it will move around, jump, and be affected by gravity without you coding anything.
Using a RigidBody3D is like trying to turn a sliding block from Zelda into a character. To move a RigidBody3D you have to apply external force. It is not intended to be controlled as a character, but by collisions from CharacterBody3Ds and/or other RigidBody3Ds..
In the code that you’ve provided, I don’t see any torque being added to the physics body. I assume that you’ve deleted your previous attempts at rotating the body in the manner that you’re describing.
There is a variety of ways that you can manipulate a RigidBody3D. One is obviously forces, but you can also manipulate its state directly via _integrate_forces() and the supplied state-parameter.
Questions
Have you tried setting the state.transform to a transform with your desired rotation?
What have you tried before (that ended up not working)?
What is your end-goal with this character controller (see sidenote)?
How does using a RigidBody3D, over a CharacterBody3D, make gravity easier to implement?
Please answer these questions. It would help us get a better idea of what your exact problem is.
SIDENOTE
I do, somewhat, agree with @dragonforge-dev. The CharacterBody3D class is specifically designed for the typical use case of a moving character. For your specific use case, you can use CharacterBody3D.up_direction to ensure that it interprets the world in a manner that is correct for your project (e.g. setting up_direction to -down_vector).
That said, it is entirely possible to achieve a similar result with a RigidBody3D if you know what you’re doing. Just know that it requires more work. Unless you have a unique system in mind, you are, in fact…
extends RigidBody3D
const MOVE_SPEED = 20.0
const SENSITIVITY = 0.004
const JUMP_FORCE = 10.0
var DownVector: Vector3
@onready var head = $PlayerHead
@onready var camera = $PlayerHead/Camera3D
@export var align_speed: float = 50.0
var _velocity = Vector3.ZERO
var down_vector = Vector3.DOWN
# Ensures we cannot see the cursoer when moving the camera
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
# Handles camera movement itself
func _unhandled_input(event):
if event is InputEventMouseMotion:
head.rotate_y(-event.relative.x * SENSITIVITY)
camera.rotate_x(-event.relative.y * SENSITIVITY)
camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(-40), deg_to_rad(60))
func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
down_vector = state.total_gravity.normalized()
func _physics_process(delta: float) -> void:
# Get input direction
var direction = Vector3.ZERO
# Basic impulse to add jumping - need to check for grounded
if Input.is_action_just_pressed("jump"):
apply_impulse(Vector3.UP * JUMP_FORCE, Vector3.ZERO)
# Set the movement direction
if Input.is_action_pressed("forward"):
direction.z -= 1
if Input.is_action_pressed("backward"):
direction.z += 1
if Input.is_action_pressed("left"):
direction.x -= 1
if Input.is_action_pressed("right"):
direction.x += 1
# Normalize the direction to ensure consistent speed when moving diagonally
if direction.length() > 0:
direction = direction.normalized()
# Calculate the target velocity
_velocity = direction * MOVE_SPEED
# Ensure velocity is in the direction we are facing
_velocity = _velocity.rotated(Vector3.UP, head.rotation.y)
# Apply the final velocity
apply_central_force(_velocity)
rotate_to_ground()
func rotate_to_ground() -> void:
# The gravity vector already points towards the planet's center.
# We want our player's -Y axis to align with the opposite direction.
var target_down = -down_vector.normalized()
var current_down = -global_transform.basis.y
var rotation_axis = current_down.cross(target_down)
var angle = acos(clamp(current_down.dot(target_down), -1.0, 1.0))
if rotation_axis.length_squared() > 0.0001 and angle > 0.001:
rotation_axis = rotation_axis.normalized()
# Scale the torque by angle and align_speed
var torque = rotation_axis * angle * align_speed
apply_torque(torque)
as well as trying to set the state.transform with Quaternion maths, I have no idea why neither of these work?
My end goal is simple - a character that can walk around a planet (as if it were just a plane, being able to jump in the correct direction, with the players feet on the ground at all times despite the curve). I thought that using a rigidbody would work easier so that each planet can have a different gravity value using the area3d nodes to set a center of gravity. Of course the actual planets I have aren’t perfect spheres, but I imagine the same implementation would work
I am very new to godot, and did previously have a CharacterBody3D class used as a moving character, but I have no clue how to use that in this case of staying oriented upright regardless of the position on the sphere, with differing gravity values for planets - if this is easier as you say, I’d love to be pointed in the right directions but old tutorials for this all used a RigidBody3D.
This is my third day working with Godot. If walking around a sphere is possible with a CharacterBody I will be more than happy to switch over, I am just very lost at this point on how to keep the orientation working correctly around a sphere without facing gimbal lock or other issues
So there is a gravity constant in Godot, but your CharacterBody3D only uses it in its calculations if you want it to. The easiest thing to do would be to create a Marker3D at the center of your planet. (You can also use a Node3D - markers are just easier to see in the editor UI.) Then every frame update a gravity_vector that travels from your CharacterBody3D’s global_position to the Marker3D’s global_position. It will always point “down” and can be updated every frame.
Check out this thread where we’ve been discussing alternate gravities and everything that’s involved with them. @RoseWoodKitty has already created a lot of code and posted it that would be helpful to you.
If the tutorials are older than Godot 4.0, a lot of things have changed. Even more things changed in 4.3 and 4.4.
Gimbal lock is avoided by having two nodes that separately rotate the camera. Typically you have a SpringArm3D that only rotates on the y-axis and a Camera3D as a child node that only rotates on the x-axis.
Thanks for all this information, I will try to take a look at it. I’m not sure how I can use this to have different gravity for different planets though, but what you’ve mentioned will be helpful for the rotation. I’ll take a look at the thread you linked and see if I can get something working.
I admit the tutorials didn’t use that many old features, just a few changes to syntax, but I understand your point
(assuming that I have “planet” defined, I am unsure of how to make this work for multiple different planets
I don’t not how I would use this direction to actually rotate or align the player with the planet
I’m still not 100% sure if this would work the same as a marker, or how this would fit into a regular CharacterBody3D where I would have to add custom gravity to handle jumping if I set the project settings gravity to 0
You’re worrying too much about the force of gravity. It’s whatever you say it is in the _physics_process() (or anywhere else you reference it). The project’s default gravity is just a convenient global variable to grab onto and defaults to Earth’s gravity. Just define a gravity variable as a float for each planet.
I would use global_position for your vectors instead of origin, but up to you. Origins can get weird and in a 3D model they are going to be set by the person who made the model. If that’s not you, then you’ll have to do some guessing to see where it is.
Right, I think I’m beginning to understand. Irrespective of the force of gravity, I will switch to using global position for the vectors, but I still don’t know how to use this direction to rotate the player so the “feet” face down - what do I do in that regard?
You’ll create a Vector3 variable relative_up, and make it the inverse of your gravity vector. Then you’ll do a transform, lerping between the current rotation of the player and relative_up.
That’s the built in thing for CharacterBody3D mentioned by Sweatix in the original reply so that I can update which way is “upwards” aka negative gravity direction (assuming this is just like relative_up that you said)
Unfortunately I don’t know anything about Quarternion rotation, and I don’t have time to do a deep dive now. My gut reaction though is that neither of those variables is necessary and you are overcomplicating the code. Again, this is my gut, but you should just be able to use self.rotation for the player at least.
extends CharacterBody3D
const SPEED = 15.0
const JUMP_VELOCITY = 10
const SENSITIVITY = 0.004
const GRAVITY = 9.8
var gravity_direction = Vector3.DOWN
@export var planet: StaticBody3D
@onready var mesh = $PlayerMesh
@onready var head = $PlayerHead
@onready var camera = $PlayerHead/Camera3D
# Hides the cursor
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _process(_delta):
# Calculate the gravity direction from the player
gravity_direction = (planet.global_position - global_position).normalized()
# Update the relative up direction
up_direction = -gravity_direction
set_up_direction(up_direction)
# Handles mouse movement
func _unhandled_input(event):
if event is InputEventMouseMotion:
head.rotate_y(-event.relative.x * SENSITIVITY)
camera.rotate_x(-event.relative.y * SENSITIVITY)
camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(-40), deg_to_rad(60))
func _physics_process(delta):
# If we are in the air, apply gravity over time
if not is_on_floor():
velocity += gravity_direction * GRAVITY * delta
# Apply a jump force in the opposite direction of gravity
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity += -gravity_direction * JUMP_VELOCITY
var direction = _get_oriented_input()
if is_on_floor():
velocity = direction * SPEED
rotate_player(delta)
move_and_slide()
func rotate_player(delta) -> void:
var target_transform = Transform3D()
target_transform.origin = global_position
# Find the left hand side of the player
var left_axis = -gravity_direction.cross(transform.basis.z).normalized()
# Keep the z axis the same
var my_z = global_transform.basis.z
# Set target rotation
target_transform.basis = Basis(left_axis, -gravity_direction, my_z).orthonormalized()
var current_rotation = global_transform.basis.get_rotation_quaternion()
var target_rotation = target_transform.basis.get_rotation_quaternion()
# Lerp between the two quaternions
var interpolated_rotation = current_rotation.slerp(target_rotation, delta * 15.0)
global_transform.basis = Basis(interpolated_rotation)
func _get_oriented_input() -> Vector3:
var input_dir = Input.get_vector("left", "right", "backward", "forward")
# Get the camera's global transform basis
var camera_basis = camera.global_transform.basis
# Orient the input direction using the camera's basis
var direction = camera_basis.x * input_dir.x + camera_basis.z * (-input_dir.y)
return direction.normalized() # Important to normalize the direction
This is my new full script. I believe that rotate_player works correctly now, but I am strugglign to ensure that the movement is oriented corectly with get orietned input
Yeah, you seem to have figured out most of your problems. Great job!
However, as you mention, you’re experiencing that your movement vector is not oriented correctly. This is due to how you compute the direction vector in _get_oriented_input(). Currently, your direction is analogous to the camera’s basis vectors scaled by the input_dir vector. This is a solid approach if what you are creating is a flying/swimming character that moves in the direction you’re looking. However, since your character is supposed to be grounded, you should not be basing its directional movement solely on the camera’s orientation.
You have to think about the conceptual nature of the system you want to create. In your case, you want to create a system that allows a character to move along the gravitational plane i.e. move on the surface of a planet. The data that defines this plane is, for a perfect sphere, the gravity vector (gravity_direction).
As such, the goal you should have is to compute a directional vector that travels along this gravitational plane. So… how do you do that? The answer is to use vector projection – like so:
var camera_basis = camera.global_basis
var ground_normal = -gravity_direction
# Movement basis vectors
var direction_z = -camera_basis.z.slide(ground_normal).normalized()
var direction_x = direction_z.cross(ground_normal).normalized()
# NOTE: The camera's x-basis is not being utilized here since we cannot...
# ...guarantee that it aligns with the gravitational plane i.e. the movement...
# ...plane expected from the character's relation to the ground.
# ALTERNATIVELY: Project the camera's x-basis onto the ground plane in the same
# way it was done with the z-basis.
var dir = (direction_x * input_dir.x + direction_z * input_dir.y).normalized()
Hopefully this helps you solve the problems you’re experiencing regarding your movement direction. Let me know how it goes. Should you have any questions, please post them.
Thank you so much for this! This has fixed most of my movement errors, except there’s one thing I have run into now - jumping doesn’t work at all. It was working with a weird previous iteration of movement, but now it doesn’t (nor was it with my attempt at orienting the input)
I’m not sure if this is due to how the velocity is being applied while on the floor, or something to do with the rotation. is_on_floor() is detecting if I am grounded correctly, so I don’t know what is causing the issue here (especially now that the oriented input is correct)
Any ideas? Thank you again for all your patience and help