Rolling Cube

Problem

You want to make a rolling cube in 3D.

Solution

Rolling a cube is trickier than it seems. You can’t just rotate the cube around its center:

alt alt

Instead, the cube needs to be rotated around its bottom edge.

alt alt

Here’s the tricky part: which bottom edge? It depends on which direction the cube is rolling.

In preparing this recipe, I experimented with a few different solutions to this problem:

  • Pure math - calculating and applying rotation transforms
  • AnimationPlayer - using animations to key the rotations and offsets
  • Helper nodes - using Spatial(s) as rotation helpers

They all worked fine, but I found the last option the most flexible and easiest to adapt, so that’s what we’ll do here.

Node setup

Cube:  CharacterBody3D
    Pivot:  Node3D
        Mesh:  MeshInstance3D
    Collision:  CollisionShape3D
Tip

You can do this with RigidBody3D, CharacterBody3D, or Area3D as your collision node. There will be minor differences in how you handle movement. Which node you choose should depend on what other behavior you want in your game. For this recipe, we’re only concerned with the movement.

By default, everything is centered at (0, 0, 0) so the first thing we’re going to do is offset everything so that the bottom center of the cube is the CharacterBody3D’s position.

The default size of a BoxMesh3D is (1, 1, 1), so do this, move the mesh and collision nodes both up to (0, 0.5, 0), leaving the rest where they are. Now when you select the root node, its position will be the bottom of the cube:

alt alt

Now when you want to roll the cube, you’ll need to move the Pivot 0.5 in the direction you want to move. Since the mesh is attached, you need to move it the opposite amount. For example, to roll to the right (+X), you’ll end up with this:

alt alt

Now the pivot node is at the correct edge and rotating it will also rotate the mesh.

Movement script

The movement is broken in to 3 steps:

Step 1

Here we apply the two offsets shown above: shift the Pivot in the direction of movement, and shift the Mesh in the opposite direction.

Step 2

In this step we animate the rotation. We find the axis of rotation using the cross product of the direction and the down vector. Then we use a Tween to animate rotating the pivot’s transform.

Step 3

Finally, once the animation has finished, we need to reset everything so that it’s ready to happen again. In the end, we want to have the cube moved 1 unit in the chosen direction (for a cube of size 1) and have the pivot and mesh back at their original positions.

extends CharacterBody3D

@onready var pivot = $Pivot
@onready var mesh = $Pivot/MeshInstance3D

var cube_size = 1.0
var speed = 4.0
var rolling = false

func _physics_process(delta):
    var forward = Vector3.FORWARD
    if Input.is_action_pressed("ui_up"):
        roll(forward)
    if Input.is_action_pressed("ui_down"):
        roll(-forward)
    if Input.is_action_pressed("ui_right"):
        roll(forward.cross(Vector3.UP))
    if Input.is_action_pressed("ui_left"):
        roll(-forward.cross(Vector3.UP))

func roll(dir):
    # Do nothing if we're currently rolling.
    if rolling:
        return
    rolling = true

    # Step 1: Offset the pivot.
    pivot.translate(dir * cube_size / 2)
    mesh.global_translate(-dir * cube_size / 2)

    # Step 2: Animate the rotation.
    var axis = dir.cross(Vector3.DOWN)
    var tween = create_tween()
    tween.tween_property(pivot, "transform",
            pivot.transform.rotated_local(axis, PI/2), 1 / speed)
    await tween.finished

    # Step 3: Finalize the movement and reset the offset.
    transform.origin += dir * cube_size
    var b = mesh.global_transform.basis
    pivot.transform = Transform3D.IDENTITY
    mesh.position = Vector3(0, cube_size / 2, 0)
    mesh.global_transform.basis = b
    rolling = false

If your cube’s texture isn’t symmetrical, you may notice that it’s resetting after every roll. To preserve the rotation of the mesh, add the following:

In Step 1:

Change mesh.translate(-dir) to mesh.global_translate(-dir).

In Step 3:

Add two lines to keep the mesh rotation after reset:

    # Step 3: Finalize the movement and reset the offset.
	transform.origin += dir * cube_size
	var b = mesh.global_transform.basis  # Save the mesh rotation.
	pivot.transform = Transform3D.IDENTITY
	mesh.position = Vector3(0, cube_size / 2, 0)
	mesh.global_transform.basis = b  # Restore the mesh rotation.

Checking for collisions

If you plan to have obstacles in your game, you can check for collisions before moving (similar to any other grid-based movement scheme). Add a raycast check before Step 1 of the move:

# Cast a ray before moving to check for obstacles
var space = get_world_3d().direct_space_state
var ray = PhysicsRayQueryParameters3D.create(mesh.global_position,
        mesh.global_position + dir * cube_size, collision_mask, [self])
var collision = space.intersect_ray(ray)
if collision:
    return
Note

You could also use a RayCast3D node. Just remember to call force_raycast_update() before checking.

Playing with transitions

You can add a lot of “personality” to the cube’s rolling behavior by changing which TransitionType you use. The default is Tween.TRANS_LINEAR, which results in a constant speed throughout the movement.

By setting a different transition type, you can get a very different feel. For example:

var tween = create_tween().set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)

Download This Project

Download the project code here: https://github.com/godotrecipes/rolling_cube