This tutorial explains how to implement “grid-based movement” in 2D games. Grid-based movement refers to the movement of objects such as characters on the game screen one grid at a time.
Many puzzle games such as the popular smartphone game “Puzzle & Dragons” and the original falling game “Tetris,” as well as tactical simulation games such as the “Fire Emblem” series and “Tactics Ogre” probably employ this type of movement.
On the other hand, RPGs such as the “Final Fantasy” series and the “Dragon Quest” series also use this grid-based movement when looking closely at character movement in the era of 2D graphics, when these games appeared as software for the NES and Super Nintendo.
Thus, grid-based movement is used in games of various genres, and its versatility is quite high.
In this article, we will focus as much as possible on only the implementation of grid-based movement. In addition, a sample game that uses grid-based movement will be introduced at the end of the article, so please feel free to refer to it as well.
Note that the project file that will be created at the end of this tutorial is located in the GitHub repository . You can also directly check the project by downloading the .zip file and importing the “project.godot” file in the “End” folder with the Godot Engine.
Environment
This tutorial was created in the following environment
・Godot version: 3.4.4
・Computer OS version: macOS 11.6.5
Memo:
Please also use the following articles to help you start creating your game.
Downloading Godot
Project Manager of Godot
Creating a new project
First, we would like you to start Godot Engine and create a new project. Let’s name the project “Grid Based Movement Tutorial”.
When the editor appears, set the display size of the game first.
- Open the “Project” menu > “Project Settings”.
- Search for “window” and select “Display” > “Window” in the sidebar.
- In the “Size” section, change the values of the following items.
- Width: 256
- Height: 160
- Test Width: 1024
- Test Height: 640
- In the “Stretch” section, change the values of the following items.
- Mode: 2d
- Aspect: keep
Next, let’s download the assets from KENNEY’s site and use them. This time, We will use an asset pack called 1-Bit Pack . I can’t help but be thankful for this wonderful free material.
After downloading, drag and drop the “colored-transparent_packed.png” file in the “Tilesheet” folder into the editor’s file system dock to import it into your project.
Immediately after importing the file, the image looks blurry. The following steps will give the image the edges characteristic of pixel art.
- Make the imported asset file selected in the file system dock.
- Select “Preset” > “2D Pixel” in the import dock.
- Click the “Re-import” button at the bottom.
Creating World scene
The first scene is the game world. Let’s create a scene named “World”.
- Select “Scene” menu > “New Scene”.
- Select “Other Node” in “Generate Root Node”.
- Select a node of the “Node2D” class as the root node.
- Rename the root node to “World”.
- Save the scene at this point. Create a folder and save the scene with the file path “res://World/World.tscn”.
- Add a “TileMap” node to the root node.
The scene tree should now look like this.
Editing the TileMap node
Editing the properties of the TileMap node
Select the “TileMap” node in the scene tree dock and edit its properties in the inspector.
- Assign a “New TileSet” resource to the “TileSet” property.
- Set the value of the “Cell” > “Size” property to (x: 16, y: 16). The size was set to match the size of the texture (16 px height and width) of the sprite sheet that will be used.
Editing the TileSet resource
Continue to edit the resource “TileSet” assigned to the “TileSet” property.
- Click on the resource in the inspector.
- When the “TileSet” panel at the bottom of the Godot editor opens, drag the sprite sheet resource “res://colored-transparent_packed.png” from the file system dock to the left sidebar in the panel and add it. Click on the added resource to put it in edit mode.
- Select “New Single Tile”.
- With the “Region” tab selected, activate grid snapping.
- In the inspector, set “Snap Options” > “Step” to (x: 16, y: 16), which is the same size as one texture on the sprite sheet.
- select the “grass” and “tree” texture areas in order and register them as Single Tile.
- For the “Tree” tile, select the “Collision” tab and set the collision shape as well.
This completes the editing of the TileSet resource.
Creating a TileMap
From here, we will create a TileMap by placing tiles from the “TileSet” resource created earlier.
- Select the “TileMap” node in the scene tree dock.
- Activate grid snap from the 2D workspace toolbar.
- Click “Snapping Options” on the toolbar and select “Configure Snap”.
- When the “Configure Snap” setting panel opens, set “Grid Step” to (x: 16, y: 16) and click “OK”.
You should now see a grid separated by 16px in height and width on the 2D workspace. Since “Grid Snap” is enabled, you should be able to easily place tiles along the grid when editing the “TileMap”.
Now we will place tiles in the 2D workspace. Let’s simply surround the screen with “tree” tiles and place “grass” tiles inside them so that the player character, which will be placed later, will not be outside the screen.
This completes the editing of the “TileMap”.
Creating Player Scene
From here, we will create a scene for the player character. This object will actually be used to move the grid base in this tutorial.
- Select “Scene” menu > “New Scene”.
- Select “Other Node” under “Generate Root Node”.
- Select a node of the “KinematicBody2D” class as the root node.
- Rename the root node to “Player”.
- Save the scene at this point. Create a folder and save the scene with the file path “res://Player/Player.tscn”.
- Add a “Sprite” node to the root node.
- Add a “CollisionShape2D” node to the root node.
- Add a “RayCast2D” node to the root node.
The “Player” scene tree should now look like this.
Editing each node in the Player scene
Sprite node
The method of setting the texture of a sprite by specifying the range of textures you want to use from a single sprite sheet that contains many textures is used.
- In the Inspector, apply the resource file “res://colored-transparent_packed.png” to the “Texture” property.
- Turn off the “Offset” > “Centered” property. This will make the position of this node (“Position” property) in the upper left corner of the texture instead of the center so that it just fits in the grid square.
- Turn on “Region” > “Enabled”.
- Open the “Texture Region” panel at the bottom of the editor. The task here is to specify the region of the texture you want to use in the sprite sheet.
- Expand the panel by clicking on the expand icon to make it easier to work with.
- Select “grid snap” under “snap mode” at the top of the panel.
- Set the “step” at the top of the panel to 16px 16px. This will make the grid the same size as one texture on the sprite sheet.
- Select the “king” texture by dragging on the sprite sheet.
- Expand the panel by clicking on the expand icon to make it easier to work with.
CollisionShape2D node
This node is used to set the collision shape for the root node “Player”.
- In the inspector, apply the “New RectangleShape2D” resource to the “Shape” property.
- Set the Transform > Position property to (x: 8, y: 8) to match the texture position of the Sprite node.
- In the 2D workspace, match the collision shape to the size of the “Sprite” node texture.
- To enter directly in the inspector, the properties of the “RectangleShape2D” resource should be as follows.
- Extents: (x: 8, y: 8)
RayCast2D node
This node is very useful for collision detection in grid-based movement, represented as an arrow-shaped collision shape in the 2D workspace. When the arrow and an object overlap, a collision is detected. This can be used to detect collisions with objects in front of the player character and prevent it from moving forward.
- In the Inspector, set the “Enabled” property to On. This will enable collision detection.
- Set the “Transform” > “Position” property to (x: 8, y: 8). This is to center the “Sprite” texture.
- Set the “Cast To” property to (x: 16, y: 0). This is just a temporary initial value, and once the project is actually executed, the value will be changed each time the player character is moved by the script.
On the 2D workspace it should now look like the following screenshot. - Leave “Collide With” as default, “Areas” Off, and “Bodies” On. The “Tree” panel in the tileset you just created is a physical body, so if “Bodies” is checked, collision will be detected.
Implementing grid-based movement in the Player node
Configuring the Input Map
First, add an action to the Input Map in the Project Settings so that the player character can be moved by keyboard keystrokes.
- switch to the “Input Map” tab after selecting “Project” menu > “Project Settings”.
- add the following 4 “Actions”.
- move_right: D key
- move_left: A key
- move_down: S key
- move_up: W key
*It is also OK to assign Up, down, left, and right arrow keys.
Attaching and editing the script
Attach a script to the root node “Player”. Create a script file with the file path “res://Player/Player.gd”.
After creating the script file, open the script editor and edit the script code as follows.
extends KinematicBody2D
#1
const inputs = {
"move_right": Vector2.RIGHT,
"move_left": Vector2.LEFT,
"move_down": Vector2.DOWN,
"move_up": Vector2.UP
}
#2
var grid_size = 16
#3
onready var raycast = $RayCast2D
#4
func _unhandled_input(event):
for action in inputs.keys():
if event.is_action_pressed(action):
move(action)
#5
func move(action):
var destination = inputs[action] * grid_size
raycast.cast_to = destination
raycast.force_raycast_update()
if not raycast.is_colliding():
position += destination
I numbered the scripts #1 ~ #5 in the comments. I will explain in this order.
#1: Defined constants inputs
of dictionary type. The value of the inputs
is the direction vector of Vector2 type (vector of length 1) to be moved by each action. By the way, the built-in constants (RIGHT
, LEFT
, DOWN
and UP
) of the Vector2
class have the following values.
- RIGHT: Vector2(1, 0)
- LEFT: Vector2(-1, 0)
- DOWN: Vector2(0, 1)
- UP: Vector2(0, -1)
#2: Defined the property grid_size
. The value is set to 16
, the same as the size (px) of tiles in “TileMap”.
#3: Defined the property raycast
. This is a property that refers to the “RayCast2D” node.
#4: Override the built-in function _unhandled_input
. This is a callback function that is called as soon as there is input from the keyboard, mouse, joystick, etc. It is similar to another function _input
, but don’t worry about the details of the difference here.
In _unhandled_input
, a loop is performed on the dictionary type constants inputs
defined earlier. If the input is an input map action with the same name as a key in inputs
(such as move_left
or move_up
), the value for that key (for example, Vector2.RIGHT
if the key is move_right
) is passed as an argument and the method move RIGHT
), and calls the method named move
. This method move
will be defined later.
#5: The method move
is defined. When calling this method, it is necessary to pass a value for the argument action
.
The variable destination
is defined first. The value is the value for the key that matches the argument action
from the dictionary type constant inputs
(for example, Vector2.LEFT
if action
is passed move_left
) multiplied by the property grid_size
. In other words, if the player enters the “D” key, the value will be Vector2(1, 0) x 16 = Vector2(16, 0). This is a vector with a length of one grid in the right direction.
Next, the value of the variable destination
defined earlier is passed to the cast_to
property of the “RayCast2D” node. This replaces the direction and length of the “RayCast2D” arrow with the key entered by the player. The method force_raycast_update
is called in the next line to immediately update this replacement. This is a built-in `RayCast2D’ node.
The next line describes the if
syntax. The built-in method is_colliding
of the “RayCast2D” node returns a Bool type (true
or false
) whether this node (arrow) is currently colliding with an object. Since there is a not
after the if
, the meaning of this if
syntax is “if the RayCast2D node is not colliding with an object”.
If the RayCast2D node has not collided with other objects in the above if
syntax, the value of destination
is added to the value of the property position
of the Player
node. This means that the player character will move by the value of destination
. For example, if the current position of the “Player” node is Vector2(64, 32), and the player presses the “S” key once, the player moves Vector2(0, 16) from the current position, and the destination position is Vector2(64, 48). In other words, the position is moved one grid position down from the current position.
Now, the grid-based movement control should be implemented.
Adding a Player scene instance to the World scene
Now that the “Player” scene is complete, let’s add an instance of it to the “World” scene and move it around on the tile map.
- Open the “World.tscn” scene.
- Add an instance of the “Player.tscn” scene to the root node “World.
- In the 2D workspace, move the “Player” node to an appropriate position around the center of the screen.
The work is now complete.
Checking the operation of the grid-based movement
Let’s check if the grid-based movement actually works without any problems.
In particular, to make it easier to check the arrow-shaped collision shapes of the “RayCast2D” child node of “Player,” let’s check the “Visible Collision Shapes” checkbox in the “Debug” menu and enable it first.
Now, let’s run the project and see if the king can run around in the prairie with grid-based movement. If this is your first time executing the project, simply select “res://World/World.tscn” for the Main Scene.
It can be clearly seen that a single keystroke moves the character only one tile. We can also confirm that the character cannot move in the direction of the “tree” tile.
This completes the grid-based moving tutorial.
Sample game
We have prepared a sample game that uses this grid-based movement. The project file is located in GitHub repository , so you can download the .zip file from there and import the “project .godot” file in the “Sample” folder and import it into Godot Engine.
This sample is a so-called whack-a-mole game. In the game’s setup, the player controls a king who holds up a sacred ring to the ghosts of the kingdom’s soldiers who emerge from their graves to release their cursed souls.
The keyboard controls are as follows
- D: Move right
- A: Move left
- S: Move down
- W: Move up
- Space: Raise the ring to release the ghost’s soul only when facing the ghost.
The ghost must be released within 1 second by the ring or the king will be cursed. If the king is cursed, his Life is reduced by one. The king is cursed to death and the game is over when all 10 lives are lost.
The interval between the appearance of the next ghost is gradually shortened from 2 seconds at the beginning of the game to as short as 1 second.
By the way, my highest score is 150. I don’t know if this is great or not.
Supplemental Explanation
Finally, I would like to add some additional information about this sample game project.
During the game, when the ghost appears from the tombstone, the “Ghost” instance with the collision shape set in the “KinematicBody2D” class is overlapping the tombstone tiles with the “TileMap” collision set. In this case, “RayCast2D” of the “Player” instance is slightly modified to give priority to the collision detection with the ghost over the tombstone.
When creating the tombstone tiles by editing the “TileSet,” a collision of 1/2 the size of the tiles is set. It is easier to set the collision shape after setting “Snap Options” > “Step” to (x: 4, y: 4) in the inspector first. Of course, the collision shape of “Ghost” is (x: 16, y: 16).
Conclusion
In this tutorial, we have explained the implementation of grid-based movement in 2D games. We hope you will find it useful for your puzzle games, simulation games, and other projects you are working on in the future.
Let’s summarize the key points of grid-based movement.
- Set up the following correctly.
- Size of the tiles prepared in the “TileSet” resource (height and width)
- Size and position of the sprite of the object to be moved (in this case, “Player”)
- Size and position of the collision shape of the object to be moved
- Grid Step in the 2D workspace
- The value of the property (in this case
grid_size
) represents the distance to be moved by the script attached to the object’s node.
- Use “RayCast2D” to determine the collision between the object to be moved and other objects. The direction and size of “RayCast2D” are controlled by scripts.
- To move objects by player manipulation, register actions in the input map, and control the direction of movement for each input in scripts.
Link
- KENNEY
- Godot Docs: RayCast2D
- Godot Docs: Using TileMaps
- Godot Docs: TileSet
- KidsCanCode: Grid-based movement
- YouTube: Grid-based movement Godot 3 demo overview
- YouTube: Make your first 2D grid-based game from scratch in Godot
UPDATE
2022/06/09 Translated from Japanese into English