Rolling Dice with Physics in Godot

Posted on Oct 2, 2021
Last updated Jul 27, 2023

An experiment with digital dice using the Godot engine to simulate a fair physical roll.


⚠️ The version of Godot used for this experiment is 3.5

I used to play a lot of Monopoly on the family PC when I was a kid. I was thinking about it the other day and I recalled it having digital dice that were rolled on-screen whenever you rolled for your turn.

I remembered vividly (or so I thought) that the dice would roll on the board and simulate your actual roll, seemingly with physics —or some programming trickery— at play.

Turns out, I have bad recall and it was actually just an animation that showed a pair of dice rolling past the camera view, never showing the result of the roll.

monopoly 1995 dice roll

What I was remembering was actually the second iteration of a Monopoly video game that I played: an iOS app on an iPad, years later. This was the “physics dice roll” I remembered seeing every turn of the game.

Fair Dice

So I got to thinking about how the developers simulated a fair roll:

  • Did they even care if it was “truly” fair? Or was “fair enough” sufficient?
  • Was it a “real” physics simulation? Or a programming trick?
  • If it was a physics simulation, was it easy to set up?

The last question was the thing lingering in my mind. How difficult would it be to set up a simulation of rolling a pair of dice? Once you had something working appropriately in isolation (as in, without any interference from other physical objects in the simulation) could you introduce it to a modeled game board and still have fair rolls?

I set out to at least try to investigate whether having two simulated physical dice rolling in the same environment could end up with fair results.

My main goal, however, was to play around with the concept in Godot and learn more about how to manipulate physical objects and skew the time scale of the engine to speed up the simulation.

🎲 I didn’t look into the actual game’s implementation, but I may in a future post.

Shooting Dice Around in Godot

I have been playing around with Godot for a while now and I think that it is a great tool when it comes to quickly being able to prototype ideas. Here is what I got working in a couple of hours or so, with some of that time spent making the dice models and UV-wrapped textures (for the faces) in Blender.

This simple prototype was a lot of fun to play around with.

shooting multiple different types of dice

I implemented a very simple camera movement script and bound the number keys 1 through 4 to each different type of die. Also shown is a 2D interface for selecting how many dice to shoot every key press. The dice can also be rolled by clicking on their corresponding buttons.

This gave the ability to try out simulated rolls of different dice at different angles. However, to test out the fairness of the rolls, the program would need the ability to automatically roll a die (or multiple dice), capture the resulting face of each die, and then discard the die and roll again.

Settle Down

What I wanted was to give the die itself the ability to determine when it has settled to a final stop and then output whatever face is oriented up.

dice aware of coming to rest

A working example of this outputs the final face of each roll to the console:

1
2
3
Final face (d6): 1
Final face (d6): 5
Final face (d6): 4

Basically, every frame the code checks to see how much the die has moved since the previous frame. Once this movement becomes less than a certain threshhold (which could be tuned later) the die is deemed to have settled and then it simply checks which face is pointing up.

The way that works is that the Die scene contains a Position3D node at each face, very near the surface, at the (rough) center.

🎲️ Godot uses “scenes” and “nodes” as a method of organization as opposed to other game engines that may use Entity-component Systems.

d8 faces

Each of the tri-colored axes shown in the viewport above represent a face’s “position”. At any given orientation of the scene (taking positive-y as spatially “up”), the Position3D which has the highest positional y-component corresponds to the face which is facing up.

The following image shows a front-facing view of the d8 scene where the 8-face is facing up. The described algorithm obviously assumes that the die is not resting on an edge (meaning that only a single face is facing up at one time) and, even with shoddy Position3D placement, the resulting final face should be definitively higher on the y-axis than any other face of the die.

d8 face up

A simple way to capture all of the face positions for later recall in the code is to add them to a common, named node Group called faces.

d8 faces group

Sounds Good! Wait…

So for some die types the code that determines it has come to a rest was not working perfectly. Far from it, actually. In some cases dice were still rolling across faces when the code checked for the final face and then removed them. It was good enough to move on and come back to later (which is exactly what I will do now), as I had bigger problems in the way of automating many rolls.

die type aware of coming to rest

The bigger problem, it seemed, was that the dice were sometimes deflecting in a certain way and shooting right out of the box. This was really defeating the purpose of rolling many dice if they were just going to jump out and never come to rest on a face.

Enter the invisible lid! Ooooh, aaaah.

the box itself

I added a StaticBody with no mesh and a single CollisionShape that could “trap” the dice under itself once they entered the box through the top.

dice in a box

That worked out pretty nicely! Here is a gif of many dice being thrown into the box.

🎲️ I upped the speed of the roll significantly to give a more satisfying collision with the box. It is also noteworthy that the initial rotation and applied torque of each roll is randomized on a certain range.

more dice in a box

This was made to work using PhysicsBody::add_collision_exception_with where the dice would be excluded from collision with the lid (which I called ROOF in the code, like a true programmer who doesn’t know how to name things) until passing into the box:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Excerpt from `Die.gd`

extends RigidBody

onready var ROOF = $'../ROOF'


func _ready():
    # Let the die pass down through the "roof"
    add_collision_exception_with(ROOF)


func _process(_delta):
    # `4` being the y coordinate of the bottom side of the "roof"
    if global_transform.origin.y < 4:
        remove_collision_exception_with(ROOF)

The Home Stretch

I got tired of looking at the bright checker setup and decided to change it up to a dark them with little lighting and add some cool looking dots to the 6-sided die.

new look

I also added a viewport to the scene with a second camera always facing top-down. That made it so that the top faces of the dice can always be seen, even if the main camera is moved around.

I noticed a slight problem with the auto-rolling, especially on dice other than the d6. The dice seemed like they were not properly settling before being “checked” for their final face and then removed from the scene. Sometimes the die would be slowly cresting while rolling over an edge which would cause an unpredictable final face of whichever of the joining faces happened to be slightly higher elevated, which was just wrong since the die hadn’t even settled on a face.

A quick fix for this was to make the check for the dice “settling” more sensitive.

This is probably a bit simplistic (read: not exactly the “correct” check), however, it seemed to work with a bit of tweaking to the stable_threshhold and min_stable_frames values (which were left as 0.02 and 40, respectively).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Excerpt from `_process` in `Die.gd`

# Wait a certain number of frames before considering the die to have settled
if stable_frames < min_stable_frames:
    var current_transform = global_transform

    # Vector math to check if any of the `Transform` vectors have moved "significantly"
    # (as in, "past the threshhold value")
    var x = (current_transform.basis.x - last_transform.basis.x).length() < stable_threshhold
    var y = (current_transform.basis.y - last_transform.basis.y).length() < stable_threshhold
    var z = (current_transform.basis.z - last_transform.basis.z).length() < stable_threshhold
    var origin = (current_transform.origin - last_transform.origin).length() < stable_threshhold

    if x and y and z and origin:
        stable_frames += 1
    else:
        # If any component of the `Transform` is not "settled", reset the count
        stable_frames = 0

    # Save the "previous" `Transform` for next comparison
    last_transform = current_transform

Time to Speed Things Up

Now that the auto-rolling was working adequately, the goal was to speed it all up so that many hundreds or thousands of rolls could be done in seconds or minutes instead of hours.

This took me all of a few minutes going through the Godot docs to find the (turns out it was two) Engine variables that I needed:

1
2
3
4
5
# Excerpt from the main scene being used for the test: `Test.gd`

func _ready():
    Engine.time_scale = 100
    Engine.iterations_per_second = 360

Could it really be that easy?

dice roll fast

It actually took a bit (probably like 20-30 minutes) of playing around with those two properties to land on values that would work well at speeding up the overall process without completely breaking some of the (hacky) code for auto-rolling, settling, and checking faces.

For example, since the code for checking if the die has settled on a single face is being called during the _process method, if the time_scale is changed independently of the physics iterations_per_second, then the settling will be checked either way-too-often or way-too-rarely compared to the actual movement of the die due to physics in the engine. This obviously leads to false-positives (and -negatives, if I am thinking about it correctly) that cause the die to be checked for the wrong final face: leading to poor results.

Finally, the program can be made to run unattended, rolling hundreds of dice and seeing what happens.

Upon closing the game program, the results of all the rolls are printed to the console. This shows the type of die being rolled, the total number of rolls made, and the breakdown of how many times each face was rolled (with the percentage beside the count):

1
2
3
4
5
6
7
8
d6:
  total: 100.00% (86)
  1:  17.44% (15)
  2:  13.95% (12)
  3:  12.79% (11)
  4:  22.09% (19)
  5:  15.12% (13)
  6:  18.60% (16)

Here are some results from some longer running tests.

🎲️ DISCLAIMER: I only spent time running extended tests for d6. The reason for this was that I only added cool green dots to the d6 and I felt that I got what I wanted to learn from this experiment for the time being. Maybe I will come back to it and finish up some more tests another time.

1d6 - 5 minutes 0 seconds

1
2
3
4
5
6
7
8
d6:
  total: 100.00% (370)
  1:  19.73% (73)
  2:  16.22% (60)
  3:  13.51% (50)
  4:  15.95% (59)
  5:  20.27% (75)
  6:  14.32% (53)

2d6 - 8 minutes 5 seconds

1
2
3
4
5
6
7
8
d6:
  total: 100.00% (930)
  1:  19.14% (178)
  2:  17.31% (161)
  3:  15.38% (143)
  4:  15.91% (148)
  5:  18.60% (173)
  6:  13.66% (127)

Final Thoughts

So, was it a fair physical dice roll simulation?

Probably not enough testing to come to a conclusion on that.

What about adding variability like other obstacles and more dice-per-roll?

I feel like I got enough out of this experiment and learned more about Godot (which was really my main goal) so I was satisfied leaving things where they ended up.

Is that it?

I may come back to this topic/project again in the future, but I have some other things that I want to work on and write about for now.

If you would like to see the code/project, you can check it out here: https://github.com/timjklein36/godot-3d-dice.

There are obviously some things that could be changed to make the simulation run more dice rolls per second. Additionally, some code could be written to automatically cycle through throw speeds, rotations, torques, and initial positions to get a broader picture of the accuracy/fairness of the simulation. At this point, I am leaving it for a rainy day.


Edited Jul 27, 2023: removed typo parenthesis, add Godot version disclaimer