Direction of gravity
Since you might need RigidBody3D
to change their direction of gravity… Have the Area3D
override gravity (you can set its gravity_space_override
, and set its gravity_direction
). This direction is global, so you need to update it with the rotation of your Area3D
:
Code in the Area3D
gravity_space_override = Area3D.SPACE_OVERRIDE_REPLACE
gravity_direction = global_transform.basis * Vector3.DOWN
Since you might need CharacterBody3D
to follow the modified direction of gravity, have it read the gravity from the world space. For example:
Code in the CharacterBody3D
var direct_state := PhysicsServer3D.body_get_direct_state(get_rid())
var total_gravity := direct_state.total_gravity
However, you also need to tell the CharacterBody3D
what is the new up which is used to identify what is a wall, a floor, or a ceiling:
up_direction = -total_gravity.normalized()
Then it gets tricky, because presumably you want the CharacterBody3D
to rotate so it is standing up according to the new gravity direction.
I do not know how your code looks like, but you will have to review anywhere you have hardcoded Vector3.UP
, and consider if it should be up_direction
instead. For example, anywhere you are using look_at
, you probably need to change it.
Character controllers often assume Vector3.UP
is more implicit ways, for example mapping motion input to the horizonal axis, that code might need to be updated too.
Moving inside the spaceship
Up to this point I have only talked about gravity. If the "spaceship" need to actually move it will bring more trouble.
First of all, having the CharacterBody3D
is snapping to the floor will take care of situations of the floor moving away from it (at least at slow speeds).
The recommendation for a moving platform is an AnimatableBody3D
and with sync_to_physics
set to true. The CharacterBody3D
when standing on the AnimatableBody3D
moves with it.
Thus: make the spaceship an AnimatableBody3D
.
But the CharacterBody3D
won't change its orientation with whatever it is standing on. For that you need to detect the ground (e.g. use a RayCast3D
), identify if it is the rotating platform, get how much it rotated last frame and rotate the CharacterBody3D
.
I suggest you decompose the rotation in two parts:
- Rotation to align with the
up_direction
(make global_transform.basis * Vector3.UP
go to up_direction
). Do this one first.
- Rotation around the
up_direction
(the same way as the ground does).
Be aware that the CharacterBody3D
rotates around its own origin. If you want to have the origin in one place, but rotate around another ("center of mass") you need to handle do the translation that compensate for the difference after you rotated the CharacterBody3D
.
To do either of the mentioned rotations you want to get an axis and angle to have the CharacterBody3D
rotate.
You might find it useful to use the cross
product between vectors to use as axis. Be aware that cross
product will be the zero vector if the inputs are either the same direction or opposite. You can use the dot
product to discriminate which is the case.
It is also very useful to get an arbitrary perpendicular vector to another vector for these sort of things. This is what I'm currently using for that:
func get_perpendicular(direction:Vector3) -> Vector3:
if direction.is_zero_approx():
return Vector3.ZERO
direction = direction.normalized()
var vectors:= PackedVector3Array([Vector3.RIGHT, Vector3.UP, Vector3.BACK])
for vec in vectors:
var attempt = vec.slide(direction)
if not attempt.is_equal_approx(Vector3.ZERO):
return attempt.normalized()
return Vector3.ONE.slide(direction).normalized()
This will return Vector3.ZERO
if the direction
given is zero. Otherwise it will try to get a perpendicular vector by taking the X, Y, Z axis vectors and substracting their projection on direction
. Since there is no way the direction
is perpendicular to all of them, one of them is bound to give you a valid perpendicular vector. As a result the last line shouldn't run ever, but it is there so Godot does not complain that the method does not return, which is fine, since it does not handle pathological cases (i.e. NAN
and INF
coordinates), you should get an error trying to project on those.
You might also find this Q&A useful: How to compare a vector to a rotation in Godot 4.1?.
High speed
The above solution is bound to break a high speed. While you can try continuous collision detection on the RigidBody3D
, the CharacterBody3D
will always discrete collision detection.
Furthermore, at long distances you might run into floating point issues.
See Moving player inside of moving spaceship?.
Thus DMGregory suggestion in comments is not unfounded.
You can keep move everything so the "spaceship" is near the origin, by translating the root of the 3D world.
Yet, for rotating everything except your "spaceship", you want to set top_level
on the "spaceship" so it is not affected by the transformation of the root of the 3D world (so you would also have to handle its offset translation). When you rotate the 3D world, since, presumably you want the rotation to happen around the "spaceship" you translate so the origin of the 3D world is on the "spaceship", rotate, and translate back.
And yes, it could also be done visually...
Doing it with materials is possible. But this is not as easy in Godot, since it imply making a transformation in the vertex
shader for everything and before their regular material. So it can be quite an undertaking to set it up. At least, if you use Shader Global (you define them in project settings) you could control the transformation centrally.
Instead, I'd suggest to use a SubViewport
s and Camera3D
s trick: Have a SubViewport
set to be rendered on top the regular world, and will have own_world_3d
set to true. And inside of it you will have the "spaceship", and it will not move at all. To fake the motion, synchronize a Camera3D
moving on the regular world with the local Camera3D
of the "spaceship" scene plus the pretended motion of the "spaceship" itself.
Although this means that lights from the world won't affect the interior of the "spaceship", so you might want to replicate directional light and any other nearby lights that might affect it... And there will be edge cases (e.g. the light in the world would be inside the ship), but presumably you would have the "spaceship" fly at a safe distance from everything by design.
RigidBody3D
or aCharacterBody3D
? Do you need this to work for other objects "in the spaceship"? Also, is the ground aStaticBody3D
or something else? \$\endgroup\$CharacterBody3D
and the platform is aRigidBody3D
, but I am willing to change those if neccessary. \$\endgroup\$