We’re well on our way to learning how to generate great procedural content for our games. It’s a good time to cement what we’ve learned so far by using those skills to make a game.
In the previous two chapters, we learned about how to make and use nodes and tile sets. We’re going to use both of those to recreate a classic game, called Sokoban.
Sokoban is actually a whole genre of games, but I want us to focus on a simple implementation of the version published around 1980.
Pushing crates onto dots
The game is about pushing crates on to dots. It’s a puzzle game, where the order of moves is important.
Creating a level selection menu
Designing two or three levels, representing the layout of each level in an array
Using these arrays to draw tiles and nodes
Implementing player movement
Allowing the player to move boxes
Detecting when the boxes are over the dots to signal a win state
Let’s get started.
Creating Levels
- 1.
A template Screen node, with the corresponding GameScreen class script
- 2.
A LevelSelectionScreen node, with a script that extends GameScreen
- 3.
A PlayScreen node, with another script that extends GameScreen
Your folders and files should resemble this:
Starting with three screens
Next, let’s think about how we want to design each level. It would be good for us to have a template resource that defines a few types and properties common to each level. We do this by creating a new folder and adding a new script to it:
Creating new scripts
This script should inherit from the Resource class, and we can save it as level.gd:
Inheriting from Resource
This class should have properties that describe each level. We can use enums (which are lists of possible values) to define the kinds of objects we can draw:
Each level needs a name so that exported property makes sense to add. The couple that follow it need some explanation, though. I want us to take a step back and think about how levels can be build using algorithms.
The algorithm we need is one that reads level layouts from an array and draws on a tile map or with nodes. That’s why each item in the layout array is a type of block, which can represent a wall or a crate or even the player. width tells our drawing code how many blocks are in each row of the layout.
This layout array depicts an empty room surrounded by walls. It’s an example to show what kind of data we expect level designers to come up with. Custom resources like this are useful to let us define a custom data type that we can reference in other nodes.
Instead of linking this script to a scene, we need to create instances of this custom resource with the data values customized. We can right-click the resources/levels folder and select the New Resource option:
Creating a new resource in the file explorer
And we can find the custom resource in the list of possible resources to create by searching for its name:
Selecting our custom resource
This instance of our custom resource has properties we can set in the property inspector. We can decide how many blocks wide each level will be and create an array of block types that is a multiple of that Width. I’ve chosen to define a layout that is the same size as the example – seven blocks wide and seven blocks high:
Defining the layout of a level in blocks
It might be tricky to think of the layout in this way. I’d suggest, if you’re having trouble, that you use a bit of grid paper to design the level before creating this resource.
Array indices for your 7 × 7 grid layouts
Take some time to design two or three of these levels, and create their corresponding resource files. We’ll need them in the next section.
Selecting a Level
By now, we’ve created some levels and the placeholder for a level selection screen. Let’s connect the two so that we can launch our levels from the level selection screen.
First, we’ll need to export a list of levels from our level selection screen and draw buttons on the screen for each level. We can set up some nodes to make the layout of this easier:
Layout nodes for easier button placement
We need to write a script that will load each configured level resource as a button that we can use to start that level:
We can export an array of the levels we’ve designed, so we can link them through the property inspector. When the level selection screen loads, we loop through each of the linked levels. We create a new button for each, adding it to the VBoxContainer we set up.
We can also define a lambda to execute when the player presses a button. Switch back to the 2D tab, go to the property inspector, and link the levels you’ve designed:
Linking our levels
Now is a good time to launch the game to see if everything is working as expected. Select the level selection screen as the default screen to load on startup, and click on the buttons!
Clicking all the buttons
If you don’t see any buttons, or they don’t print text to the console when you click on them, then something’s wrong. Go back and look for syntax errors.
Switching Screens
Changing screens can be a bit of a mission if you’ve never done it before. It’s a balance between flexibility and simplicity. We want the mechanism we create and use to be extensible, so we can add more screens without many changes to code. We also want the functions we call to be simple to use.
A good way to achieve this is to store a lookup table of screens in a global Constants class. Let's make a new folder and a new scene from a Node node:
Creating a new Node scene
Selecting the main node of the new scene
We can attach this script to the main node of a new scene file, which we’ll autoload in a minute. This main node is where we can link the two scenes we’ve exported using the property inspector.
Linking to our different screen scenes
Linking them as exported properties makes it easy to replace the scenes or rename the files, without breaking hard-coded paths in code. We can use these constants in another global scene. This new scene remembers the current screen and swaps it out with new screens. Create another Node scene, called Screens, and attach another script to it:
We’ll call the change_screen method shortly. Before we do, we need one last global scene. It needs to store the current level we intend to play so that the play screen can draw the appropriate play space. Create yet another Node scene and attached script, called Variables:
We need to autoload these three scenes by going to Project ➤ Project Settings ➤ Autoload:
Loading our globals
To be super clear about the structure of these files, here is what my file explorer looks like:
Globals in the file explorer
Instead of printing to the debug console; we can now store the desired level, and change to the play screen:
Globals and Other Mischief
Before we move on, I want to talk a bit about globals in our code. Some people will tell you to avoid autoloaded scenes because they can cause problems. I don’t think this practice is as bad as they say.
The approach we’ve used is perfect for our needs, and we should continue to use it. This book is about procedural content generation, and not about the perils of using globals, after all.
The only other thing I want to mention is that we can’t refer to types defined in a global to type-hint variables in another script. That’s why I gave the Constants script a class name of Types.
…because Godot cannot verify the type of Constants.screens at compile time. We can autoload a scene with one global name and reference its types using a different class name.
It’s a trick I’ve only found useful in this situation. It might be cleaner to separate the types out from the constants so that we autoload one and not the other, but I’m not going to do that.
Drawing Levels
Our play screen can use the current level data to draw the level’s tiles and nodes. I think it would be fun to use another kenney.nl asset pack for this project. You can find this one at https://kenney.nl/assets/sokoban:
Kenney's take on Sokoban sprites
Download and extract the asset pack, and copy Tilesheet/sokoban_tilesheet.png into the project's images folder. Next, create a TileMap and a Node2D. You can even create the TileSet resource using your knowledge from the previous chapter:
Setting the stage for drawing our levels
CenterContainer → Center
Control → Stage
TileMap → Tiles
Node2D → Nodes
If your tiles are fuzzy after importing, set CanvasItem → Filter to Nearest. You'll also need to adjust the tile size to 64 × 64 pixels, where I've highlighted.
By now, we’ve covered the basics of creating nodes and tile maps. I don’t want to repeat too much of this. If you’re having trouble, check out the example project code and refer to the previous chapters.
We need to identify the atlas coordinates of each of the block types we care about and store them in Constants. While we’re at it, we should move the levels' blocks enum too:
This means our custom GameLevel resource must also change:
You could delete the example array if you’re not keen on maintaining this long list of block types. I like to keep these examples around so that it’s easier to figure out how to code for the resource.
You might be wondering why I have so many block types when I'm using the same sprite for all of them. It would be simpler to have a single wall type, but that would be less flexible. We could have used any number of tile sets that had different sides and corners of walls; and this enum would account for them all. You're free to simplify the code if the tile set you use doesn't have this level of detail.
We can use these new constants and atlas positions to set the blocks of our tile map on the play screen:
This is a strange loop we’re using. The tile map’s 0,0 coordinate is in the center of the screen; so we want to set blocks on either side of it. If we started at 0,0, then the top-left corner of each level would be in the center of the screen:
Drawing blocks to the left and top of 0,0
Drawing Nodes
- 1.
The player → CharacterBody2D
- 2.
The crates → CharacterBody2D
- 3.
The dots → Area2D
- 4.
The doors → Area2D
Create each of these, with their own attached scripts, until your files look like this:
Level nodes
- 1.
Create the Sprite2D node
- 2.
Set Texture ➤ AtlasTexture
- 3.
Click Edit Region
- 4.
Change Snap Mode to Grid Snap
- 5.
Change the Step values to the size of the sprite (64 × 64 pixels in this case)
- 6.
Select the sprite
Selecting sprites from an AtlasTexture
The top-left corner of the sprites and collision shapes should be at 32 × 32 pixels so that their top-left corners align with the top-left corner of each floor square. I’ve given the scenes custom class names, so we can type-hint against them later on. For now, we need to add them to Constants:
Link these scenes in the property inspector so that we can use them in the play screen code:
This new code is similar to the tile map code in terms of how we get the correct data for the type of block. The differences are to do with us creating nodes instead of drawing on a tile map.
We already have x = -4 → 2 and y = -4 → 2; so we can multiply that by the block size to get the top-left position for each node. We could put this pixel size in Constants, but we’re already hard-coding the 64 × 64 size by what we’ve selected as the tile map block size. We’d need to dynamically construct the tile map’s source if we wanted it to be completely dynamic.
I don't think it's worth the hassle.
If you find yourself repeating a lot of magic numbers, consider making them constants.
Your levels should now contain any node blocks you’ve specified in their layout:
Tiles and nodes in one of my levels
Moving the Player
It’s time to add some interactivity! We’re going to add some code to listen for player input and move the player… assuming they aren’t trying to walk into a wall:
A great way to listen for keyboard input is to add an _unhandled_key_input method to our player class. In our recreation of Sokoban, a single press of the left key means the player tries to move 64 pixels to the left.
Thus, we change the offset (which is a change to the player’s position) depending on which of the ui_* keys the player presses. We figure out if there’s a wall in the new position using a form of the same strange loops we used before in drawing.
If there’s no wall, we add the offset position to the player’s current position.
Now is a good time to relaunch the game to see if pressing the arrow keys on your keyboard will move the player. Even if you can move, there are some strange things you’ll notice.
You can walk through a crate, dot, and door without restriction. You’ll even walk underneath the crate and dot if the player appears above them in the node tree.
Avoiding Closed Doors
The player should not be able to leave through the door until all crates are on dots. Let’s add a check for this. First, we need to remember how many crates there are in a level’s layout and how many are covered by crates:
We can reset and increment these when we draw a level layout on the play screen:
Using these (and a bit of refactoring), we can prevent a player from leaving through a closed door:
Moving Crates
The last bit of movement we need to add is the ability to move crates. This is an extension of wall checking on the player. If the player is next to a crate and moves in the direction of the crate and there is space to move the crate….
We start by checking the space we want the player to move into and the space beyond it. That’s because the player might be trying to move into a space occupied by a crate; and we’d want to know if the crate can move.
- 1.
If the player is trying to push a crate
- 2.
…and there’s enough space for the crate to be pushed by the player
- 1.
If the player is not trying to push a crate
- 2.
…or trying to walk into a wall
- 3.
…or trying to walk through a closed door
Relaunch the game and try this movement code out. It’s pretty cool. Unfortunately, we still have the issue of the player and crate disappearing behind dots if the player and crate are above the dots in the node tree.
The simplest way to sort this out is to go to the Node2D properties (of the crate and player) and make the Z Index values greater than those of the dot:
Higher Z Index value means closer to the screen.
Winning a Level
All that remains is to show that the player has covered all the dots with crates by allowing the player to leave.
First, we need to connect to signals on the dots:
Connecting to the body events on dots
The best events for this are on_body_entered and on_body_existed. These are emitted when a CharacterBody2D or StaticBody2D collides with this Area2D. This is what that code looks like:
There are two bodies that could collide with each dot – a player or a crate. So we need to make sure that we only add to the covered crate count when the body that is colliding is a crate.
Before these collisions will work, we need to set up collision layers and assign the dots and crates to them:
Defining new collision layers
We assign these layers through the property inspector:
Assigning nodes to collision layers
- 1.
In Crate, set Layer to crates.
- 2.
In Crate, set Mask to dots.
- 3.
In Dot, set Layer to dots.
- 4.
In Dot, set Mask to crates.
Layers and masks can be tricky to understand. Layer is “what layers this collider is in,” and Mask is “what layers this collider will collide with.” After the preceding changes, crates will collide with dots and vice versa. We only need one side of that for our code to work, but I like to set both sides up unless there's a good reason not to.
To test this, relaunch the game and move the crate over the top of the dot. You should now be able to walk through the door. We can take this a step further by connecting to the on_body_entered signal:
Listening for door events
Now, we can respond to the player moving through the open doorway:
If the player leaves through the open door, we remove their avatar from the level and print a console message. It’s not the most flashy ending; but it’s a starting point for anything more elaborate you’d like to do with it.
One thing I noticed was that using collision shapes that are exactly 64 × 64 pixels would cause the player to collide with the door even when they are in the block next to the door. This meant the player would exit the stage even when the player's code prevented them from entering the door's space. I adjusted all my collision shapes to be 60 × 60 pixels big and to start at 30 × 30 pixels; so there's always four pixels of space between the collision shapes.
Summary
This has been a whirlwind of a chapter. We’ve used all the skills gained so far to build out a real game. It might need a bit of polish, but you’re welcome to do that so that you can release it as a game of your own.
Take a bit of time to review the mechanics you built for this game. Swap the sprites and tiles out for your own art, or a different art pack from kenny.nl. Add a more flashy win message, with particles and overlays and everything. This is a time for you to be creative and excited about what you have achieved.