3D - A Playable Rubik's Cube (Difficulty: 6)
CreatiCode last edited by
In an earlier tutorial, you learned how to build a 3 by 3 by 3 Rubik’s Cube. However, that cube can not be played. In this tutorial, you will build a game that can be played by multiple people together: they can all use the same “seed”, which gives them the same random state of the cube.
This tutorial is fairly long. It will be broken into 2 parts:
- How to build a cube that the player can play with
- How to automatically rotate the cube to generate a new puzzle
Create a new project on the CreatiCode playground, and name it “Playable Rubik’s Cube”. Remove the “Sprite1” with the dog costumes, and rename the “Empty1” sprite as “Cubes”.
As usual, start with a simple scene such as “Empty”, and add the 3D axis. Note that we will make each cube about the size of 1, so the 3D axis length is also very small:
Now let’s define a new block to add the cubes. Make sure you check “Run without screen refresh”, so it will not update the screen until all cubes are added, which will be a bit faster.
Also, run this block after initializing the scene:
This step is already explained in the previous tutorial, so we will quickly go through it. We need to create 3 new variables “x”, “y” and “z”, and use 3 for-loops to make them iterate from -1 to 0 and then to 1. These 3 variables will then be used to place each cube in the corresponding position. For example, the first box will be placed at x of -1, y of -1, and z of -1.
In addition, we are using the size of 0.98 instead of 1, so that there is a small gap between the boxes. Otherwise, the 27 boxes will appear to be one big box. Feel free to pick any colors you like.
If you zoom in, you will see them like this:
To make it easier for us to refer to these boxes, we will name them using the numbers 1 to 27. We can use a new variable "ID’, set it to 1 initially, and increase it by 1 after we add each box.
You can verify they are all added correctly using the object list at the bottom right:
Note that the box at the center of the cube has the ID of 14, which is exactly the middle between 1 and 27.
Obviously, we need to move the camera much closer to the cube. More importantly, we need to make sure the user won’t be able to rotate or shift the camera, since the user input will be used to control the boxes themselves. We can achieve both goals with the following block:
The camera distance is set to 8. The v-angle of 60 will make the camera look down at the cube, so it can see the top face of the cube. The h-angle of 45 ensures that we see 2 side faces symmetrically. Both the key input and point input is set to “No”, so the user can not control the camera using the keyboard or the pointer.
When you run this block, the camera will have a fixed view like this:
Since the users can’t move the camera by the pointer anymore, we need to provide another way for them to look at different parts of the cube. This logic is fairly self-contained, so we will put it in a new sprite called “Camera Control”.
In the “Cubes” sprite, after adding the boxes, send a message of “add camera controls” to add the camera control:
Then in the “Camera Control” sprite, receive this message to handle it:
First, let’s add a button using the button widget, and place it at the bottom right corner.
Since the button is fairly small, it’s better to use an emoji symbol than writing some words on it. You can copy the right arrow emoji here:
The name of the button is “rightbutton”.
To make the emoji bigger and colorful, we can update its style using this block:
Feel free to adjust its style any way you like. Now the button would look like this:
Please add 3 more buttons by duplicating these 2 blocks. They will rotate the camera left, or up/down, or to the opposite direction. You can copy the 3 emojis from here: ️, and ↺
They look like this:
When the “rightbutton” is clicked, we should rotate the camera to the right by updating its h-angle. This task appears simple since we just need to set the new h-angle to be the current h-angle minus 90 over a period of 0.2 seconds. Note that when we leave other inputs empty, their existing values will not be affected.
However, as we test this button, sometimes it doesn’t work well:
The reason is that the “camera h-angle” block always reports a degree value between -180 and 180, so its value will suddenly jump from -135 to 135 when we change it by 90 degrees. How can we fix this issue?
The solution is to use a new variable to store the current h-angle value of the camera, and we can make sure this value never “jumps” when we update it each time.
First, we need to set it to the initial h-angle of the camera when the program starts, which is 45.
Next, each time we rotate the camera to the right, we need to change its value by 90, and then use the new value to update the camera.
Now we get a smooth rotation no matter how many times we rotate the button:
To rotate the camera to the left, the code is very similar:
When the ️ button is clicked, the camera will toggle between looking at the top face and the bottom face. Obviously, we need to change the “v-angle” of the camera. The v-angle starts at 60, and we need to change it to 120 when the ️ button is clicked. And then we need to toggle it back to 60 when the ️ button is clicked again. Essentially the next value for the v-angle is always 180 minus its current value. This can be done using these 2 blocks:
You can now test these 3 buttons:
The ↺ button is supposed to rotate the camera both horizontally and vertically, so the player can quickly review the other 3 faces of the cube. We can combine our solution for the previous 3 buttons: the v-angle of the camera will toggle between 60 and 120, and the h-angle will be calculated using the “Camera H Angle” variable:
Here is the result:
Now we are done with the camera controls, and let’s switch back to the “Cubes” sprite, and start working on rotating the boxes themselves.
First, to trigger a rotation, the player will need to start by pressing down the pointer on a cube. We can enable the “picking event” using this block:
Note that the event is triggered when the pointer is pressed down, not when it is released, because we want to handle the “swipe” gesture: we will record the pointer position when it is pressed down, and compare that with the pointer position when it is released. That will tell us how the player wants to rotate the boxes.
When the picking event is triggered, that means the player has just pressed down the mouse pointer (or finger on mobile devices). At this point, we don’t need to rotate any box yet. We just need to store some information about this event, so we can use them later when the pointer is released. Specifically, we should store these 6 values:
- start x/y: they will tell us the 2D position of the pointer on the stage where the user has pressed down the pointer
- picked box: this is the name of the box right under the pointer. This box will determine which boxes will be rotated.
- picked point x/y/z: they will tell us the 3D position of the exact point on the picked box that was under the pointer. We can use them to help determine which face of the Rubik’s Cube was clicked.
Now we just need to wait for the pointer release event. When the picking event is triggered, the mouse point must be down, so “mouse down” is true. Therefore, we can simply use the “wait until” block to wait until the mouse is no longer pressed down:
When the mouse pointer is released, we need to determine which way the player wants to rotate the boxes. We can calculate the direction from the point where the pointer is pressed down initially (start x and start y) and the point where the pointer is released (mouse x and mouse y). We will store this information in a variable called “direction”:
Now you can try to drag the mouse pointer in a few different directions, and observe the value of the “direction” variable:
Let’s start with the case where the camera looks down at the cube. Overall, there are 8 types of rotation actions we need to handle:
- To keep it simple, we will only allow the player to click on the right side face or the left side face, and we will ignore it if the player clicks on the top or bottom faces.
- On the right face (the yellow region), when the player swipes perfectly, the direction will be about 4° for rotating up, -176° for rotating down, 60° for rotating right, and -120° for rotating left.
- On the left face (the white region), the angles are -4° for rotating up, 176° for rotating down, 120° for rotating right, and -60° for rotating left.
To start simple, let’s focus on one rotation action first: suppose the player clicks the right side face, and swipes to the right to rotate horizontally.
We know that the player’s swipe won’t be perfect. Suppose the player swipes to the right/up direction. How do we know if the player wants to swipe up or right? We can find the average angle between 4° and 60°, which is 32°. So if the swipe direction is greater than 32°, we take that as swiping to the right. Similarly, we can find the average angle between 60° and -176° (equivalent to 184°), which is 122°. So if the swiping direction is less than 122°, we claim the player is swiping horizontally.
We can express this idea using the following blocks:
Similarly, when the player swipes to the left, we can use a similar range: the average of 4° and -120° is -58°, and the average of -120° and -176° is -148°.
So we can add another if-else block for this scenario:
Now let’s define the block that’ll carry out the horizontal rotation. It should take one input parameter “angle”, which controls how many degrees to rotate (90° or -90°) .
Once this new block is defined, we can place it in our if-else branches:
Each time we make a move on the Rubik’s Cube, we are rotating 9 out of the 27 boxes. For horizontal rotation, we need to know which of the 3 layers needs to be rotated. We can determine that based on the Z position of the cube the player has picked. Therefore, we can select the box named “picked box”, and read its z position.
Now if you try to drag different boxes on the right face, their Z positions will be printed out, which should be -1, 0 or 1.
Now we know the Z layer of the 9 boxes we want to rotate horizontally, we need to select all of them and set the center box (ID = 13) to be the parent of all of them. This way, if we rotate the center box around the Z axis, all 9 boxes will be rotated around the Z axis.
Since this process will go through all 27 boxes, it might be slow. Therefore, we will define another custom block named “select by z”. It will take the z layer as the input, and select all boxes with the same z position. We need to make this block “Run without screen refresh”, so it runs faster.
The goal of “select by z” is to go through all 27 boxes (with ID of 1 to 27), and select the 9 of them that have the same Z position as the input argument. That can be done using the following for-loop:
Note that whenever we select a box as the “sprite object”, the “z position” block tells us the z position of that box, so we just need to check if “z position” is the same as the input argument “z”.
To make all 9 boxes in a layer rotate around the Z-axis, we need to assign a parent to all of them. Then we just need to rotate the parent. The best choice is the box at the center, with an ID of 14. This box will never be visible to the user, which means we can rotate it any way we need.
That completes our “select by z” block.
Now we go back to work on the “Horizontal Rotation” block. We will first select 9 boxes by the z position of the box the player has picked using the “select by z” block.
After the “select by z” block, the center box is the parent of the 9 boxes. So we can select the center box (using its ID of 14), and then rotate it over a period of 0.2 seconds. After the rotation, we will unlink all its children, so in the future, we can use the center box to rotate some other boxes. We also need to reset the rotation of the center box.
Now we are done with the “Horizontal Rotation” block. To illustrate how it works, here is an example with some boxes set to transparent:
As shown, when we swipe the mouse pointer, the center box rotates together with all 9 boxes, and then it resets back to the initial rotation by itself.
Now let’s go back to the logic of “when an object from this sprite is picked”. We already handle the case for swiping right or left. Now let’s add the conditions for swiping up and down.
As shown, if the direction of the pointer is between -58° and 32°, the player is trying to swipe upwards:
In our code, we need another if-else block:
When we rotate the boxes vertically, depending on the camera position and the box picked, we may need to rotate the boxes around the X or the Y axis. So we need to be able to select 9 boxes based on a given x position or y position. To prepare for that, let’s first define 2 new blocks of “select by x” and “select by y”. They are very similar to the “select by z” block. Make sure you check the “Run without screen refresh” option.
There are a few ways to determine if we need to rotate around the X or the Y axis. One method is to make use of the picked point’s position.
First, let’s observe the x and y position of the picked point as we rotate the camera around the cube:
As shown, since we are clicking on a side face, either the x or the y position of the picked point is 1.49 or -1.49. For example, if the box’s x position is 1, and its size is 0.98, then its face is 0.49 units from its center, so its face is at x of 1.49.
So how do we know whether to rotate around the X axis or Y axis? Let’s look at one example first:
In this case, we are clicking a side face that’s facing the position X-axis, and the picked point has an x position of 1.49. If we rotate vertically, it must be around the Y-axis, and the 9 boxes must all have the same y position.
We can implement this logic like this:
This is very similar to the “Horizontal Rotation” block, with a few differences:
- We check the x position of the picked point to make sure the player is swiping on the side face that faces the positive x direction. Note that we can’t simply check if “picked point x = 1.49”, since sometimes the picked point x variable may give us a value like 1.489999. So it is safer to compare it with a slightly smaller value like 1.48.
- We are selecting all other boxes with the same y position as the picked box, and setting the center box as their parents
- We are rotating the center box around the Y axis.
Now we can go back to the “when an object from this sprite picked” block, and make use of the “Vertical Rotation” block for swiping up or down:
And it should be working now when we swipe the side face in the positive X direction:
Now let’s continue to implement the “Vertical Rotation” block. For the case when the player swipes the face that is in the negative X direction (picked point x is close to -1.49), the logic is almost the same, except that we need to rotate in the opposite direction:
And now we can test that as well:
When the player swipes on the positive or negative Y faces, we need to select 9 boxes with the same x position, and rotate all of them around the X axis:
As highlighted, the rotation angle needs to be reverted because the X and Y axis rotation works slightly differently.
By now, the vertical rotation works for all 4 side faces when we swipe on the right half of the stage:
When the player swipes on the left side face of the cube in the stage, the logic is the same. We will look at the direction of the swipe, and decide which way to rotate and by how much. As shown below, the boundaries between the 4 directions are drawn in yellow:
We can duplicate the logic for “start x > 0” and update the conditions. We also need to update the rotation angles for some cases through trial and error:
By now the player can swipe on the left half of the stage as well:
So far we have only handled the case when the camera is looking down, at a v-angle of 60. When the v-angle is 120, the swiping directions will be different:
However, since the camera v-angle is also symmetric between 60° and 120° (horizontal view angle is 90°), we find that the directions between the left side and right side switch with each other.
Therefore, all that we have to do is to add an extra condition like this:
This new condition says that
- if the v-angle is 60, do the following when the player swipes on the right half of the stage;
- Or if the v-angle is 120, do the following when the player swipes on the left half
The other opposite cases will all be covered by the “else” branch, which doesn’t need to be changed. By now, we are finally done with all the user input handling:
Now let’s add a new sprite called “Randomizer”. It will be used to generate a random puzzle.
First, when the green flag is clicked, it should add a new button to the stage. The text on the button is “Randomize”.
The button will look like this:
When the player clicks the “Randomize” button, we will ask the player to input a “seed number”. This number will be used to generate a random formation of the cube. One thing special is that given the same seed number, the random numbers it generates are the same. This makes it possible for 2 players to use the same seed number on their own computers and get the same puzzle, so that they can complete who will solve it first.
We can add the following blocks to the “Randomizer” sprite. We will show a label that says “Specify a seed number:”, so the player knows who they need to input. We use the “ask and wait” block to take the player’s answer. Note that the input is empty here, since for 3D projects, the words input here won’t be displayed anyway. Lastly, when we get the number from the player, we will remove the label widget.
Given the seed number, we will use it to generate 40 random numbers between 0 and 1, and store them in a list variable called “numbers”:
You can try to run this a few times, and confirm that when the seed number is the same (such as 22), the random numbers in the list are always the same from previous runs.
After the random numbers are ready, we will send a message of “randomize” from the Randomizer sprite.
We will handle that message in the “Cubes” sprite. That’s because to rotate the boxes, we have to select them, and they are all created from the Cubes sprite. We can use a variable “i” to go through the list of random numbers, and change the boxes based on each number’s value:
Now we need to define some new blocks to make it easy to rotate the boxes. First, we will define a new block called “Z Rotation”. It will take one input argument “z”, and it will rotate 9 boxes with that z position. As you can see, this block is very similar to the “Horizontal Rotation” block, except we can easily specify which z layer to rotate.
You can test it using z values of -1, 0 or 1:
Similarly, we can define 2 more blocks to rotate around the X or Y axis:
Now we have defined 3 blocks to rotate the cube around the 3 Axes. That means we can take any of these 9 actions:
- X Rotation (-1)
- X Rotation (0)
- X Rotation (1)
- Y Rotation (-1)
- Y Rotation (0)
- Y Rotation (1)
- Z Rotation (-1)
- Z Rotation (0)
- Z Rotation (1)
We need to find a way to map each random number to one of these actions, and make sure for the same random number, the action is the same.
We know the random numbers are all between 0 and 1. A simple solution is to multiply each number by 10, then round it to a whole number. Then if the number is between 1 to 9, we will take the corresponding action. This way, random numbers are converted to random actions.
For example, we can start with the action 1 like this. Whenever the “action” is 1, we will rotate the 9 boxes at X of -1.
The other actions are very similar:
Now our randomization code is also complete. You can test it out. Using the same seed 2 times (such as 22) will give you the same random result:
Now let’s use a better-looking skybox as the background, and hide the 3D axis. Note that we also need to re-configure the camera’s visible range, since the skybox is far away.
Here is a final demo of the game:
Congratulations on completing such a fairly complex project. There are still many more enhancements that can be made based on this project. Here are some ideas worth exploring:
- Timer: You can add a timer to show how many seconds have passed since the player starts the game
- Reversing the Random Moves: you can add another button to automatically take the opposite of the 40 random moves backwards. This will restore the cube back to the initial state step by step. It will make a very cool animation video.
- Saving Game State: You can add a “save” button to save the current position/rotation of all 27 boxes using the “save private data” block, and another button to “load” the data and restore the position/rotation of these boxes.
- Recording of a Solution: Whenever the player makes a move, you can save that move in a list or table. Then when the player has solved the puzzle, you can replay the entire process.
- Tutorial on how to solve a puzzle: You can create a series of tutorials that teach how to solve a puzzle, such as how to move a particular box to a target position. You can set the cube to any position and ask the player to practice solving it.
- AI to solve a puzzle automatically: You can work on an algorithm that solves any puzzle. To enable that, you would first need to provide information on the colors of the 9 squares on each of the 6 faces. That information can be derived given the position and rotation of each of the 27 boxes.