Hour 17. Game 2: Bloxorz Clone
What You’ll Learn in This Hour:
Using 3D objects in a game project
Making custom transformations on physics objects
Detecting an out-of-bounds player for a losing condition
Using the GridMap node
Loading different levels dynamically
In this hour, you will make a 3D rolling block puzzle game. You’ll see the main concept of the game and how it is structured. Then you’ll see how
to create the scenes and develop each level. From there, you’ll add the scripts to handle the movements and detect winning/losing conditions.
Finally, you’ll play the game yourself and have fun!
Concept and Design
This game is a relatively simple, but challenging puzzle game. The player controls a rectangular prism, which has a square base and a height
two times taller than the base. The player needs to roll it around to fit the square hole in the middle of the floor. Once he does so, he will
advance to the next level. If he falls to the sides, he will lose and get respawned at the beginning. The floor consists of square tiles that form a
different path each level. The player needs to be creative in each level to roll the block around and finish in the right position to fall through the
ending level. Ideally, he should use the least possible number of moves.
Design Ideas
Let’s take some time to decide how to structure our game scenes. First, the moving block should be a simple mesh, so we can use a
MeshInstance node to hold it. The block itself can be a
RigidBody to make use of gravity when falling.
The floor is a square grid, which makes it easy to use a
GridMap node to lay it out. This node acts as a TileMap (as shown in Hour 3), but
for the 3D space, laying pre-made blocks in a grid. For this, you will need to also make a
MeshLibrary even though it consists of a single
tile type. The ending will be checked with an
Area, and another will be used to set the inbounds of the level. Once the player leaves this
area, he won’t be able to control the block until it respawns. Each level should have its own spawn point, which can be set with a
Position3D node.
For the code part, you will make use of a few simple state machines to determine the transitions and winning/losing conditions based on the
current orientation and rest position of the block (whether it’s standing or lying). A few
Timer nodes will set the wait times for respawning
and avoid player movement before the block is ready. Signals will be used to communicate between different scenes. A singleton will be used
to find the levels and to keep track of the current one.
Game Scenes
Break everything down in scenes to help structure the game with the employment of reusable components. Table 17.1 shows the description of
each scene.
TABLE 17.1 Bloxorz Game Scenes
Scene Description
Game The main scene. This is where the game starts and everything is connected. Put here the
WorldEnvironment, the
DirectionalLight, and the
Camera. It will also load the block and levels.
Block
The controllable block. This scene will be based on a
Spatial. It will contain the
RigidBody and
MeshInstance to
represent the block itself. There will be a couple of
Area nodes to control the out-of-bounds checking. You’ll also use a couple of
Timer nodes for some controls.
Levels Each level will be its own scene. You will make a base scene with all the components:
GridMap for layout,
Area nodes for
ending and inbounds check, and
Position3D for the spawning point. The specific levels will inherit this scene and change the
parameters.
MeshlibThere’ll be also a scene to convert into a
MeshLibrary for use with the
GridMap.
Making the Scenes
There’s not much to change in the Project Settings. Since this is a 3D project, the window size is quite flexible, and resizing just changes the
amount of area seen by the camera. Yet, there’s a bit of configuration that can make life a bit easier.
First, let’s change the default gravity. The block should fall quite quickly, so increase it to about 50. You can set that under Physic > 3d >
Default Gravity in the Project Settings. You should also give names to the physics layers to make it easier to set on the objects without getting
111111111
22222222lost (Table 17.2). This can be done under Layer Names > 3d Physics.
TABLE 17.2 Layer Names
Layer
Name
Layer 1
Floor. This is the one applied to the
GridMap.
Layer 2
Block. This will be applied to your controllable character.
Layer 3
Bound. It represents the ending and inbounds areas.
TIP
Orthogonal versus Perspective
Since this is a grid-based game, it makes sense to use the orthogonal view so that all of the squares are equal in size. You can toggle the view
between perspective and orthogonal in the Viewport menu (which is on the top left of each viewport) or by pressing the 5 key on the numeric
keypad.
Mesh Library Scene
A good place to start is the Mesh Library source scene. This is the base for the
GridMap, and can be changed later to tweak the floor if
needed.
Start by creating an empty scene. Add a
Spatial node as the root so it will be considered a 3D scene. In the
MeshLibrary, there are
a collection of
MeshInstance nodes. Add one, and call it “Floor.” For its Mesh property, add a new
CubeMesh. Set the size of the
mesh to (0.95, 0.1, 0.95). This will make it a short square tile. It will be a little smaller than the unit square to make the grid more visible.
Add a
SpatialMaterial to the resource. Note to add it to the
CubeMesh resource and not to the
MeshInstance node. This will
ensure that the material is properly applied to the final object in the
GridMap. A trick to add a simple outline is to add material to the Next
Pass property. In this new material, set it to Unshaded, add a Grow of 0.01, change the Cull mode to front, and set its color to black. This
outline is not perfect, but it should be enough for this simple game.
You’ll want the player body to fall in the starting block and avoid falling through it when the game is lost. For this, you’ll need a collision body. To
acquire that in a Mesh Library, add a
StaticBody node as a child of the
MeshInstance. This will be kept when converted. You can
then add a
CollisionShape node to it and set the Shape property to a new
BoxShape. Set its Extends to (0.45, 0.05, 0.45). This is
half the tile size, because the shape uses this property as a radius and not as length. It’s also a bit smaller than the actual size to make the
ending hole wide enough for the block to fall through.
TIP
Better Graphics
This example game is very simple, and by that standard, it uses only simple materials without any texture. You can improve on this by making
custom textures and tweaking the materials to make the visuals more vibrant.
With the mesh and its collision, the scene is complete. Save it as “meshlib.tscn” to be a reference if something needs to be changed. Then
click on Scene > Convert To > MeshLibrary to create the resource. Save this as “meshlib.meshlib.” The name is a bit redundant, but it’s not
an issue for this simple game. You can be more creative during larger projects.
Block Scene
The next scene to prepare is the Block. This is our controllable character that we can roll around when playing the game. The root of the scene
is a
Spatial node, so we can have the 3D transform and work with it. The structure of this scene can be seen in Figure 17.1.
FIGURE 17.1
Structure of the block scene.
NOTE
Spatial as Scene Root
You may remember the tip in Hour 8 to make the physics body the root of the scene and everything else children to make them follow the
transform. This tip still stands, and you can see in this structure that most of the nodes are under the
RigidBody.
The idea here is that because you need to rotate the body across an edge, you’ll use the root
Spatial as a pivot point to facilitate the
rotations. It makes the movements a little more complicated, because there’s a need to adjust the body getting away from the parent (as you’ll
see in the scripting section), but in the end, it will be easier to make the rotations.
The
MeshInstance here will be set with a
CubeMesh. Create a
SpatialMaterial for the mesh and adjust it to your liking. You can
look at the company project to see the exact parameters for our example. This mesh must have its size set to (1, 2, 1). This will make a prism
with a square base and double the height, as proposed in our design. The
CollisionShape for the body is a
BoxShape, with half of
111111111
22222222that used for its Extents property.
The Up and Down
Area nodes are for checking the out of bounds parameters. When any of those areas leave the bounds, it’s game
over. They should have a
CollisionShape equal to the floor tiles and be placed slightly above the block. You can position each area in the
center of each square base, and it will be enough.
Both
Timer nodes have their functions explicit in their names. GravityTimer will set the gravity of the body to zero when it finishes. This will
allow the block to fall naturally at the beginning, but then it’s controlled manually by the controls. Its wait time will be set to 0.5 seconds, but you
can play around with this value. The RespawnTimer represents the amount of time the game will wait after the fall before respawning the
block. One second should be enough. Both nodes should be set to One Shot, since we don’t want them to keep being triggered.
Levels
The sample game has three levels, and they all inherit from the same base scene. Again, this scene is rooted with a
Spatial to make use
of 3D transforms. The structure can be seen in Figure 17.2.
FIGURE 17.2
Base level scene structure.
The
GridMap will make the floor for the level. This is the main node to design the actual layout of the level. You’ll first need to set the
Theme property to the
MeshLibrary we created earlier. When you select this node, the editor will change to show the list of available tiles,
which will be only one in this case. You can select the tile and use your mouse to place it on the grid. Right-clicking will delete the tile. You can
also use Ctrl + mouse wheel to change the elevation of the grid and the A, S, and D keys to rotate the tile before placing it.
Since the player block spawns in a different location each level, the Start node is need. It’s simply a
Position3D to hold the transform. The
height of it is constant for every level, and in this sample, you’ll use a value of 4.
The
Ending Area node will be placed on the hole that ends the level. Its
CollisionShape is a
BoxShape with Extents set to
(0.45, 2, 0.45). The base is smaller than the grid tile to avoid any edge contact due to numerical imprecision. Areas report the entering signal
as soon as any part of the body touches them, so this size is wide enough to properly detect the winning condition.
When the player reaches the end successfully, the gravity should be turned back on, but only after the block completes its rotation, so it can fall
naturally if it’s outside of the game boundaries. That’s what the
GravityTimer is for. You can set it to One Shot, but the time itself will be set
via code to match the block movement duration, so you don’t need to change it.
The
Inbounds Area defines the boundaries of the level. When the
detection areas of the player block get outside the boundaries, the
game is over. The
CollisionPolygon makes it easy to draw a line that defines the level border. You’ll need to rotate this negative 90
degrees in the X-axis so the polygon line matches the level position. You’ll also need to set the depth property to a high value, like 15 or so,
because the block spawns on a high position, and it should be inside the boundaries.
Note this is only the base scene, and you don’t need to change the default position of the nodes. You’ll need to create inherited scenes based
on this one where you’ll fill the GridMap, move the start and ending points, and create the inbounds polygon. Create a few levels and add them
all to a directory called “levels.” This will ensure they can be easily listed and fetched by scripts.
TIP
Grid Snapping
Since the game features a perfect grid, you can use the Godot Editor features to snap the objects into it. You can enable this by clicking on the
Transform> Use Snap menu on the toolbar. Use the Transform> Configure Snap dialog to change the Translate option to 0.5. While the
grid square has side 1, you may need to snap things in the center of squares. This is quite helpful when setting the start and end positions of
the levels (Figure 17.3).
FIGURE 17.3
Grid snap configuration The Translate Snap makes things move perfectly into the grid.
NOTE
Pixel-perfect Inbounds Polygon
While you don’t need to make the polygon fit the boundaries perfectly, it is nice to do so, since the game fits into a grid. The easiest way to do
this is to simply make the polygon manually by clicking on the corners and using Inspector to change the array of points to the closest integer.
This way, the polygon will snap into the grid nicely.
TRY IT YOURSELF
Create a Level
Let’s see how to create the first level of the game (see Figure 17.4):
111111111
22222222FIGURE 17.4
The layout of the first level.
1. Create a new scene inherited from the original level scene.
2. Click on the
GridMap node.
3. Use the floor tile to design the level. You can be creative or just follow the existing example.
4. Select the
Start node.
5. Use the
Move tool to set it over the level’s spawn point. Use the snapping option to make it easier.
6. Move the
Ending node and put it over the hole at the end of the level. Its bounding box should match the square hole.
7. Select the
CollisionPolygon under the
Inbounds node. Use the
Create Polygon tool to make it go around the
level’s tiles and enclose them. You can manually adjust the values of the points to make it fit exactly onto the grid.
8. Save the scene in the “levels” folder. This will make the script load it.
Main Game Scene
The last step is to make the main game scene. This will put everything together and handle the environment settings. It will also be responsible
for making the level’s transitions and setting up the block spawn point. Figure 17.5 shows the main scene structure.
FIGURE 17.5
Main scene structure
First, you have the
WorldEnvironment. This allows you to set up the environment without relying on the default one. Use it to set up the
clear color of the world, since the generated sky does not look good with an orthogonal perspective. There’s also a
Camera, which should
be set to orthogonal with a Size Yof 10 to capture the entire level.
The
DirectionalLight is used to give illumination to the game. It’s also responsible for giving a nice shadow to the block. For that, you’ll
need to enable its Shadow property. You may need to adjust the Color, Bias, and Contact properties to make the shadow look nicer.
There is a
Timer node here to make the game wait a little after the level is won before loading the next one. In our project, we set it to 2
seconds. It should also be set to One Shot.
Last, we have an instance of the
Block scene. For testing purposes, you may also want to instance one of the levels before the level
loading is set up. After that, you can save this scene and set it as the Main Scene in Project Settings.
Scripts and Input
Before adding the scripts, set up the input mapping. This way, you can change the keys used to control the block or add joystick support
without changing the code.
The Input Map can be set in the Project Settings. In this game, you only need four: left, right, up, and down. These will be responsible to
move the block. You can expand on this to add other functionality such as respawn, pausing, and level skip.
TIP
Input Configuration
The Input Map can be altered in runtime with scripting. You can set up and delete actions as well as change the keys and buttons associated
with it. This means it’s possible to make a menu screen the player can use to set up the keys that move the block.
You can refer to Hour 7 and check the official Godot documentation about the InputMap singleton if you intend to make something like this
(Figure 17.6).
FIGURE 17.6
The Input Map configuration for the game Notice you can have multiple keys or gamepad buttons triggering the same action.
Controlling the Block
The most important and complex script is meant to move the block. It relies on state-keeping variables and a few magic numbers to move the
block in the correct way. This section will follow each function and explain the most complex portions of the code (Listing 17.1).
LISTING17.1 Block Input Code (block.gd)
Click here to view code image
extends Spatial
111111111
22222222…
enum Direction { UP, DOWN, RIGHT, LEFT }
enum Orientation { PARALLEL, ORTHOGONAL }
enum RestPosition { STANDING, LYING }
var orientation = PARALLEL
var rest_position = STANDING
var is_turning = false
var interpolation = 0
var rotation_direction = null
var won = false
var lost = false
var respawning = false
var right_shift = Vector3(0.5, -1, 0)
var down_shift = Vector3(0, -1, 0.5)
func _input(event):
if event.is_action_pressed(“right”):
start_turning(RIGHT)
elif event.is_action_pressed(“down”):
start_turning(DOWN)
elif event.is_action_pressed(“left”):
start_turning(LEFT)
elif event.is_action_pressed(“up”):
start_turning(UP)
func start_turning(direction):
if respawning or won or lost or interp != 0 or not $GravityTimer.is_stopped():
return
is_turning = true
match direction:
RIGHT:
rotation_direction = RIGHT
adjust_transform(right_shift)
DOWN:
rotation_direction = DOWN
adjust_transform(down_shift)
LEFT:
rotation_direction = LEFT
adjust_transform(right_shift Vector3(-1, 1,
0))
UP:
rotation_direction = UP
adjust_transform(down_shift Vector3(0, 1, -1))
adjust_orientation()
…
func adjust_transform(shift):
translation += $RigidBody.translation + shift
$RigidBody.translation = -shift
func adjust_orientation():
if rest_position == LYING:
match rotation_direction:
RIGHT, LEFT:
if orientation == PARALLEL:
rest_position = STANDING
UP, DOWN:
if orientation == ORTHOGONAL:
rest_position = STANDING
elif rest_position == STANDING:
rest_position = LYING
match rotation_direction:
RIGHT, LEFT:
orientation = PARALLEL
UP, DOWN:
orientation = ORTHOGONAL
func update_shifts():
if rest_position == STANDING:
right_shift = Vector3(0.5, -1, 0)
down_shift = Vector3(0, -1, 0.5)
else: match orientation:
PARALLEL:
right_shift = Vector3(1, -0.5, 0)
down_shift = Vector3(0, -0.5, 0.5)
ORTHOGONAL:
right_shift = Vector3(0.5, -0.5, 0)
down_shift = Vector3(0, -0.5, 1)
…
111111111
22222222Listing 17.1 shows part of the block script. This part of the code is responsible for handling the input and preparing the block for movement
while adjusting the state to match the direction of the movement.
The first lines define three enum structures to be used in the state code. Using these kind of constants ensures they are integers internally
instead of strings, which requires less processor usage to compare them when needed. The structures we define here are used throughout the
code to check and update the block state. It’s useful to know which direction the block is oriented and whether it’s standing or not, because its
asymmetrical nature makes it harder to calculate the rotations without this information.
Following that, you will set a few variables to keep the state. Most of them aren’t in use currently, but will be very soon. The names are quite
descriptive, except maybe for interpolation, which keeps the position of the movement animation on a scale of 0 to 1. The
right_shift and down_shift variables are basically “magic” numbers that help us move the pivot point of the rotation. Think of it like
this: from the origin (center of the block), how much in each direction do you need to move the pivot point to the edge, where it rotates? To
move it to the right, you need half a unit in the X-axis and 1 unit in the Y-axis downward (hence the negative). Those values change depending
on the block orientation, so the update_shifts() function updates the values after each movement.
The _input() function essentially checks if an action was pressed and passes it around to another function so that you can reuse the code.
This function is called automatically byGodot when there’s a user input. The start_turning() function sets the state of the object to be
rotated. This state will then be used on the process function (which we will check later). First, it checks if in a state where it cannot turn, such as
if it’s respawning or already turning. If so, it bails out of the function early. Then it checks the direction of the turn and calls a function to adjust
the transform accordingly. After that’s done, it calls another function to update the orientation of the block (it will only reach such orientation after
the rotation is completed, but you set it early on).
To adjust the transform, you’ll need to zero out the
RigidBody translation and shift it according to the direction of movement. For this, you’ll
move the root node the same amount as the body and add the shift, then set the translation of the body to zero minus the shift you need.
The orientation is adjusted by checking the current state and direction of movement. With a bit of logic, you can figure out what the next
orientation will be. Updating the shift vectors is almost the same thing, but you’ll need to reset the magic numbers once again.
Rotating the Block
The actual rotation is made in the _physics_process() function (Listing 17.2). It uses the current state of the block to apply the transform.
The most important detail of this function is the axis of rotation. Godot uses the right-hand rule. This means that if you take your right hand, stick
your thumb up, and imagine it as the axis of rotation, the closing fingers indicate the direction of rotation (Figure 17.7).
FIGURE 17.7
The right-hand rule for rotation Pointing your thumb in the axis direction, the other fingers show the rotation movement.
LISTING17.2 Block Rotation (block.gd)
Click here to view code image
const movement_duration = 0.2;
…
func _physics_process(delta):
update_labels()
if is_turning:
var step = (delta / movement_duration)
var angle = (PI / 2.0) step
var body = $RigidBody
match rotation_direction:
RIGHT:
body.transform = body.transform.rotated(Vector3(0, 0, -1), angle)
DOWN:
body.transform = body.transform.rotated(Vector3(1, 0, 0), angle)
LEFT:
body.transform = body.transform.rotated(Vector3(0, 0, 1), angle)
UP:
body.transform = body.transform.rotated(Vector3(-1, 0, 0), angle)
interpolation += step
if interpolation > 1:
is_turning = false
interpolation = 0
update_shifts()
…
The function first calculates the step, which is how much movement will be made this frame, in a ratio value. From this, you can calculate the
angle considering that by the end, the block will be rotated 90 degrees (which in radians is half of Pi). Then it rotates the transform of the block
along one of the axes, depending on the direction of movement. At the end, it updates the state to stop the animation once it gets to the end.
Winning and Losing Conditions
The rest of the block code deals with win/lose conditions and properly resetting the properties to start a new level (or restart the same one).
The functions respawn() and reset_properties() are responsible for most of that burden. They update the state to the original values
and zero the transforms. The
GravityTimer is used after respawning to disable the gravity once the block is on the floor.
111111111
22222222There are two functions that will be triggered by the level areas, namely win() and lose(). They are responsible for setting up the routines
for winning and losing conditions. The won signal is connected to the game code, which will trigger the loading of the next level.
Out of Bounds Checking
When any of the block Area nodes get out of the level boundaries, an area_exited signal is triggered. You’ll use that to call the lose()
function of the block, which will make it respawn (Listing 17.3).
LISTING17.3 Out of Bounds Check (outbounds.gd)
Click here to view code image
extends Area
func _on_Inbounds_area_exited( area ):
var body = area.get_parent()
var parent = body.get_parent()
if parent.won or parent.respawning: return
body.gravity_scale = 1
parent.lose()
body.angular_velocity = 2
Note that it gets the grandparent of the detected Area, since that will be our block node. This also increases the angular velocity of the body to
make it rotate a bit faster and create an interesting effect.
NOTE
Script Attachment and Signals
You should note the outbounds.gd script is attached to the Inbounds node. This also has implications in the signal connection, since it’s
dependent on the node path. When connecting the signal using the editor, you’ll need to select the correct node where the function is.
Winning Condition
The winning condition has a similar logic to out of bounds checking. The difference is the block doesn’t fall if it’s over the ending hole unless it
is in a standing position. There’s also a timer to reenable the gravity once the movement is completed to make the block fall. When the timer
runs out, it will call the win() function on the block, which will handle the condition (Listing 17.4).
LISTING17.4 Winning Check (ending.gd)
Click here to view code image
extends Area
const BlockClass = preload(“res://block.gd”)
func _on_Ending_body_entered( body ):
var block = body.get_parent()
if block.rest_position == BlockClass.STANDING:
block.won = true
$GravityTimer.connect(“timeout”, self, “_on_GravityTimer_timeout”, [
block, body ])
$GravityTimer.wait_time = block.movement_duration
$GravityTimer.one_shot = true
$GravityTimer.start()
func _on_GravityTimer_timeout(block, body):
block.win()
body.gravity_scale = 1
$GravityTimer.disconnect(“timeout”, self, “_on_GravityTimer_timeout”)
TIP
Accessing Constants from Scripts
You may have noticed the ending script preloads the “block.gd” file. This makes it possible to access the constants of that script. In this
example, you’ll only use the STANDING constant, so it’s not much of a problem, but in a larger game, this pattern avoids repetition of code. If
you simply tried to define the constant here, it would work, but it would need maintenance if you needed to change the value. And if you forget
that it is defined in multiple places, it would lead to hard-to-find bugs.
Loading Levels
The logic of the level listing is done by a singleton keeping the state. The game code uses this to load the next available level. This is a regular
Node, because while it needs to be a singleton in the tree, you don’t need any special functions from any of the provided nodes. You can
set this up in the AutoLoad tab of Project Settings. You’ll need to set the path to script and the name of the node that it will have when in the
scene. This name can then be used for reference. If you enable the Singleton field, it will be available directly by its name without the need to
use the get_node() functionality (Listing 17.5).
LISTING17.5 Level Keeping Singleton (levels.gd)
Click here to view code image
111111111
22222222extends Node
signal levels_ended()
const levels_path = “res://levels”
var level_list = []
var current_level
func _enter_tree():
var dir = Directory.new()
dir.open(levels_path)
dir.list_dir_begin()
var file = dir.get_next()
while file != “”:
if file == “.” or file == “..”:
file = dir.get_next()
continue
level_list.push_back(levels_path.plus_file(file))
file = dir.get_next()
level_list.sort()
current_level = -1
func get_next_level():
current_level += 1
if current_level == level_list.size():
emit_signal(“levels_ended”)
return “”
return level_list[current_level]
The main logic is on the _enter_tree() function, which will be called when the script is loaded. This will get all the files in the “levels”
directory (as provided in the levels_path constant) and put their full paths in a list. Then it will sort the list to make sure that “level1” is the
first level (Listing 17.6). Note this makes the name of the level files important. Finally, this sets the current_level variable to −1, since it will
be incremented by the game to 0, which is the first index on the list.
The other function is the get_next_level(), which will increment the level counter and return the file name as stored. If the game is over, it
will return an empty string, and the caller function will have to handle the result.
LISTING17.6 Changing Levels (game.gd)
Click here to view code image
extends Spatial
var current_level = null
func _ready():
next_level()
func _on_Block_won():
$LevelLoad.start()
func next_level():
var next_level = Levels.get_next_level()
if next_level == “”:
return
if current_level != null:
current_level.queue_free()
current_level = load(next_level).instance()
add_child(current_level)
$Block.start_point = str($Block.get_path_to(current_level)) + “/Start”
$Block.respawn()
The main game script is the last piece in the puzzle. This will be responsible for loading and instancing the levels into the scene.
First, note the current_level variable here is different from the one in the singleton. This one holds the actual level instance so that it can
be easily referenced later. Also note the timeout signal of the
LevelLoad timer is connected to the next_level() function, and that
the won signal of the block is connected to the _on_Block_won() function.
The next_level() function gets the next level path from the singleton. If it’s an empty string, it does nothing, since this means the game is
out of levels. Otherwise, it frees the current level (if there is one) and loads the next in the same variable. Then it adds the level to the game
scene and sets the start point of the block. Finally, it tells the block to respawn, which makes it appear in the correct position.
Playing the Game
With all that done, the game is finally done and ready to be played! Run the main scene, and you should be able to move the block around with
the arrow keys on the keyboard. You can roll the block around to see it falling out of the grid or getting into the winning hole (Figure 17.8).
111111111
22222222FIGURE 17.8
The final game running Move around the block and test your puzzle-solving skills.
Summary
In this hour, we created a simple 3D puzzle game. You learned how to set up the camera, lighting, and environment. You also learned how to
create your Mesh Library for use with the GridMap node. You saw how the instanced scenes come together to form a larger game, and how
scene inheritance can help with that. You learned how to make scripts, and how they can call functions in other scripts and singletons. Finally,
you saw how to put everything together and form a complete game.
Q&A
Q. Why use a GridMap instead of placing instanced scenes?
A. The
GridMap node has a great advantage in the editor, making it much easier to edit. It also uses batch instancing for the same tiles,
resulting in a better overall performance.
Q. Do you need an interpolation in the processing function? Can’t it be done with animation?
A. Theoretically, it’s possible to make it via animation, but you’d need multiple animations for each variation, which isn’t very practical. It’s
also possible to use the
Tween node to interpolate properties automatically.
Q. Why connect and disconnect the Timer signal in the ending script?
A. The connection function binds the block and body objects to be referenced when the time-out ends. Since these binds happen only at the
connection, it’s needed to disconnect and reconnect later. This is not a real problem in this game, since only one block exists, but this
would avoid issues in more complex games with multiple objects.
Workshop
Take a moment to go through these questions and make sure you have a grasp of the content.
Quiz
1. Where can you reconfigure the input keys and buttons?
2. Why do we use different physics layers?
3. True or False: You can only connect signals using script.
4. True or False: You should only attach scripts to the root of the scene.
Answers
1. In Project Settings, under the Input Map tab.
2. To categorize the objects and avoid contact between certain categories.
3. False. You can also connect using the editor interface.
4. False. You can have multiple specialized scripts for many nodes in the same scene.
Exercises
Let’s change the game a bit to make it more interesting.
1. Add a
Label node to the block scene. Update its content to show the number of rotations. Remember to use the built-in function
str() to convert the number into a string.
2. Add a few more levels to the game.
3. Change the order of the levels by renaming their scenes in the “levels” folder.
4. Add a “game over” screen that shows when all the levels are finished. Use the levels_ended signal of the singleton.
5. Advanced: The original Bloxorz game has buttons that activate blocks, opening or closing paths. Try to implement this functionality. You
can make other types of tiles for the buttons and use areas in the levels to detect the presence of the block.
111111111
22222222