In this lesson, we’ll get started with the project’s setup. We’ll go over the foundations we’ll later use to run our simulation.
We’ll explain the project’s inputs, place the player and the world’s floor, and write the Simulation
script’s first lines.
It’s a script we’ll revisit often; it’s the gateway for other systems to announce or act on the simulation.
To get started, open the start project. This should automatically open the main scene we prepared for you: Simulation.tscn
. It contains a Node
named Simulation
and a Node2D
named GameWorld
.
Before adding more nodes to it, let’s go over the game’s input mappings.
We prepared the input map for you in this project to save you time, as this is an intermediate-level series.
You can find all mappings in the Project Settings… -> Input Map.
As we designed the game for desktop only, the input mappings are only for keyboard and mouse. We chose the keys so you can access almost all of them without moving the left hand and keep your right hand on the mouse.
Let’s run through the input mappings.
The “up”, “down”, “left”, and “right” inputs are mapped to WASD and move the player’s avatar.
The "quickbar_*" actions are mapped to the ten number keys, from 1 through 0, and are shortcuts to select an item from the ten-item bar at the bottom of the view.
The action “toggle_inventory” will open and close the inventory. It is mapped to E.
When placing an entity that has facing information in the game world, like the battery, the “rotate_blueprint” will rotate it. It’s mapped to R.
We’ll use the “left_click” and “right_click” actions to interact with entities and inventory cells with the mouse.
The “drop” action will cause the player to drop a held item on the ground by pressing Q
There’s also a “sample” action to allow the player to sample an item in the world. If they have this item in their inventory, it’ll instantly pop in their hand. It’s mapped to Q and triggers when you hover an interactable entity in the game world.
Let’s now work on the game map, followed by the controllable player avatar.
Head back to the Simulation scene
and add the following nodes as children of GameWorld
:
TileMap
named GroundTiles
for the ground.YSort
node we’ll use to sort most game objects. We will place all the world entities inside of it.As explained in the previous lesson, YSort
nodes allow entities to appear in front of others depending on their vertical position.
We also prepare the ground tileset resource for you, Tileset.tres
. It contains all the sprites for the entities and the ground tiles in a single sprite sheet, both for convenience and performance reasons. In Godot 3.2, doing so can help the engine draw the sprites in batches instead of one by one.
Select the GroundTiles node and:
Tileset.tres
resource to its TileSet property.100
x 50
pixels, the size of one tile in the tileset texture.Notice the purple tile named “barrier”, illustrated in the image below. We’ll use it to draw the map’s limits in the editor, and when running the game, we’ll later replace those with the invisible tile that has a collision box, named “barrier_invisible” in the tile selector. That way, the player won’t be able to escape the game world and wander around the void.
Notice how we drew the collision box of the transparent tile to encompass the entire purple cube.
Due to that, when drawing the map, you want to leave a one cell-wide gap between the blue ground tile and the purple block representing the world’s limits. Here is a picture illustrating that, with the player we’ll create in a moment.
Notice the light blue circle at the character’s feet. It’s the player’s collision shape. As soon as it touches even the bottom of a pink cube, the player will collide and stop moving. Keeping a one-tile gap between the blue blocks and the pink ones will make it so the collisions fit the edge of our ground area.
Now, before adding the player, you want to draw the map. With the Ground node selected, select the blue tile to draw the ground. Click and drag while holding the Ctrl and Shift keys down to draw a rectangle of cells of any size you want.
Then, select the purple tile to draw the limits of the world. This time, only hold the Shift key down and click and drag to draw a line. Create a border around the blue rectangle you just drew.
We have a world, so we can now add a simple player entity to move around in it.
Create a new scene for the player, with KinematicBody2D named Player as its root, a Sprite, and circular CollisionShape2D. Also, add a Camera2D to the scene.
Select the Sprite and assign the pawn.svg
texture to it.
Then, select the Camera2D and set its Current and Smoothing -> Enabled properties to true
. The first property makes the camera active, while the second will make it smoothly follow the character, with a slight delay.
You also want to set the Process Mode to Physics. As we’ll use _physics_process()
to move the Player node, we need the camera to use the same callbacks to follow it smoothly.
Also, move the sprite up so that its base is the origin of the scene, instead of the middle of its body. We’ll place the origin of every entity at their base to get consistent sorting in the project.
The circular collision shape should at the player’s base too. This will make collisions feel natural when moving around the world.
Your scene should look like this.
To let the player move the pawn around the world, attach a script to it, with the following code.
extends KinematicBody2D ## Movement speed in pixels per second. export var movement_speed := 200.0 func _physics_process(_delta: float) -> void: # We move the player at a constant speed based on the input direction. # The `_get_direction()` function calculates the move direction based on the player's input. var direction := _get_direction() move_and_slide(direction * movement_speed) ## Returns a normalized direction vector based on the current input actions that are pressed. func _get_direction() -> Vector2: return Vector2( # As we're using an isometric view, with a 2:1 ratio, we have to double the horizontal input for horizontal movement to feel consistent. (Input.get_action_strength("right") - Input.get_action_strength("left")) * 2.0, Input.get_action_strength("down") - Input.get_action_strength("up") # We then normalize the vector to ensure it has a length of 1, making it a direction vector. ).normalized()
Instantiate the player under the main YSort
where it will share space with other entities.
We have big visible purple blocks for the world’s limit in the editor. When running the game, we don’t want them to show up.
To replace them with an invisible wall, attach a new script to the Simulation node with the following code.
extends Node # The following constants hold the IDs of the purple block tile, named "barrier" below, and the # and collision we'll replace it with. # The IDs are generated by the GroundTiles' TileSet resource, and they depend on the order in which you created # the tiles, starting with `0` for the first tile. const BARRIER_ID := 1 const INVISIBLE_BARRIER_ID := 2 # The GroundTiles node is the tilemap that holds our floor, where we want to replace # the purple blocks with invisible barriers. onready var _ground := $GameWorld/GroundTiles func _ready() -> void: # Get an array of all tile coordinates that use the purple barrier block. var barriers: Array = _ground.get_used_cells_by_id(BARRIER_ID) # Iterate over each of those cells and replace them with the invisible barrier. for cellv in barriers: _ground.set_cellv(cellv, INVISIBLE_BARRIER_ID)
Like that, we make sure the player can’t go wandering out of the map.
Right now, the world’s desperately empty. In the next lesson, we will add our first machine to place on the game grid.
Here’s the complete Player.gd
script.
extends KinematicBody2D export var movement_speed := 200.0 func _physics_process(_delta: float) -> void: var direction := _get_direction() move_and_slide(direction * movement_speed) func _get_direction() -> Vector2: return Vector2( (Input.get_action_strength("right") - Input.get_action_strength("left")) * 2.0, Input.get_action_strength("down") - Input.get_action_strength("up") ).normalized()
And Simulation.gd
.
extends Node const BARRIER_ID := 1 const INVISIBLE_BARRIER_ID := 2 onready var _ground := $GameWorld/GroundTiles func _ready() -> void: var barriers: Array = _ground.get_used_cells_by_id(BARRIER_ID) for cellv in barriers: _ground.set_cellv(cellv, INVISIBLE_BARRIER_ID)