Rolling Dice with Physics in Godot
An experiment with digital dice using the Godot engine to simulate a fair physical roll.
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.
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.
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.
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.
A working example of this outputs the final face of each roll to the console:
|
|
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.
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.
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
.
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.
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.
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.
That worked out pretty nicely! Here is a gif of many dice being thrown into the 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:
|
|
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.
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).
|
|
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:
|
|
Could it really be that easy?
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):
|
|
Here are some results from some longer running tests.
1d6 - 5 minutes 0 seconds
|
|
2d6 - 8 minutes 5 seconds
|
|
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