Photo by Francesco Ungaro

2D game development in Godot 3.4

Stephan Bester
12 min readJun 26, 2022

--

This article chronicles some things I discovered when I built my first 2D game prototype using the Godot Engine. For those interested, the game itself is available to play on itch.io. Note that, although this is written in the form of a tutorial, it’s not a complete guide to creating a game. Instead, this document is best viewed as a reference for other developers starting out with Godot.

I do not claim that any of the techniques described here are “best practices”; they are simply things that I found worked for me.

The building blocks of Godot

Before I dive into my guide, a brief introduction to the building blocks of Godot is in order. If my definitions are overly simple, it’s because my intention here is only to introduce the concepts. I recommend that you explore them and do further research.

I find it useful to think of games in Godot as primarily consisting of scenes, nodes, scripts and resources.

Scenes

Scenes are containers for nodes and scripts. Think of them as frames in a storyboard. A scene could be a title screen, a game over screen or one of the levels. The first thing you see when working on a project in Godot is the scene editor:

The 2D scene editor in Godot

Every Godot project has a default scene, called the main scene, which is loaded as soon as the game starts. You can change the Main Scene under the Application/Run section of the project settings.

Scenes also have another purpose which is to encapsulate other objects for reuse across scenes. More on this later.

Nodes

Nodes are at the heart of Godot.

Every object in your game is composed of one or more nodes. There are many types of nodes, each with a different purpose, including animation, physics and audio. Knowing what types of nodes are available and how to use them will make your life easier.

A list of 2D node types in Godot
Some of the 2D node types in Godot

By default, Godot displays a tree of nodes on the left-hand side of the editor. This tree lists every node in the current scene. (Note that nodes can be added and removed to the scene tree at runtime via code.)

An example scene tree

You can group nodes under a common parent to make it easier to move, scale and rotate them together. This applies to the editor as well as your scripts.

Scripts

Scripts are files of source code, typically written in GDScript and saved with the extension .gd. GDScript has a syntax similar to Python, but don’t confuse the two. Scripts are usually attached to nodes and allow you to customise their behaviour.

There are certain predefined functions that are invoked by the Godot Engine at runtime. For example, the _ready function is called when the associated node has loaded, and _process is called once every frame.

Resources

Resources are files saved with the extension .tres and represent secondary objects like tilesets and environment settings.

Making a scene

When you create a new game, a new scene is automatically created. Every scene needs a root node. In Godot 3.4, there are various types of root nodes. The UI helpfully prompts you to select the appropriate one.

The four Create Root Node options
Create Root Node

Since this is a 2D game, choose the option for “2D Scene”. This creates a Node2D root node. Every other object in your game will be represented as a node under the root.

Fixing the resolution

Now, if you’re making a pixel art game, you probably want the screen fixed at a lower resolution. To achieve this, change the following under the Display/Window section of the project settings:

  1. Under Size, set the Width and Height to your preferred resolution, e.g. 320x240 for a very low res game. This changes the viewport in the scene editor.
  2. Under Stretch, set the Mode to “2d” so that the picture is stretched to fill the screen. Set Aspect to “keep” in order to preserve the aspect ratio.

Warning: I’m not sure how portable this configuration is across various screen dimensions. Some experimentation is advised.

Creating the player object

In a 2D game, the player object typically consists of the following nodes:

  • A KinematicBody2D. This type of node can interact with the physics system but doesn’t automatically have physics applied to it (e.g. gravity). Instead, it provides methods allowing you to more easily code its custom movement.
  • A Sprite or AnimatedSprite, connected to the KinematicBody2D above. This node visually represents the player object.
  • A collision shape e.g. CollisionShape2D, also connected to the KinematicBody2D. This node determines how the object will collide or interact with other objects.

Creating a basic animation

Create a few frames of animation in an external program like Krita. Save these as separate .png files and import them into your Godot project. You might consider disabling the Filter property on the Import tab and reimporting to keep your pixel art from looking fuzzy.

Import options for a graphic
Filter setting on import

The filter smoothes the edges of an image and is useful for other styles of games. Now follow these steps to create an animation for your AnimatedSprite and enable it by default:

  1. Expand the Frames property and choose “New SpriteFrames”.
  2. Click the Frames property again and choose “Edit”.
  3. The animation editor appears towards the bottom of the screen, with a “default” animation already created. Drag the .png files that make up your animation into the space in this editor and they will appear numbered.
The Animation Frames editor
Animation Frames

Note that by default, the animation Speed is 5 FPS and Loop is on. Now, to have this animation play by default, go back to your scene and select your AnimatedSprite node.

  1. Make sure the Animation property is set to your default animation.
  2. Change Playing to “On”. The animation should start playing inside the 2D scene editor.

For more information, refer to 2D Sprite animation in the Godot Docs.

Capturing player input

Under your project settings, switch to the Input Map tab.

Project Settings with the Input Map tab selected
Input Map

Use the controls at the top to add custom actions e.g. Up, Down, Left, Right, and Fire. Use the plus button to add one or more “event” to each action. For keyboard controls, use a Physical Key instead of a Key if you want the key to be the same regardless of keyboard layout.

Attach a script to your KinematicBody2Dnode. In the _process function, use the methods available on the Input object to check which actions have been pressed, e.g. Input.is_action_pressed("Left"). You can use the methods of the KinematicBody2D to move the player object, e.g. move_and_collide.

Remember to multiply by delta so that your movement logic takes into account how much time has transpired since the last update. This helps to keep the player’s movement consistent regardless of the framerate the game is running at.

Reusing an object between scenes

You probably want to reuse your set of player nodes between the various levels of your game. In Godot terms, this requires saving a branch as a scene and then reusing that scene within other scenes.

Right-click the root of your player node. In this case, the root is the KinematicBody2D node.

The node context menu with the option “Save Branch as Scene” selected.
Save Branch as Scene

Save the branch as its own scene file e.g. Car.tscn. Now open up another scene, e.g. one of the levels for your game, and drag the scene file from the previous step into the editor from the FileSystem window. The scene appears in the tree as a node with a clapperboard icon next to it.

A scene graph containing a node scene with clapperboard icon
A scene node in the tree

If you click the clapperboard icon, the Godot editor opens the scene in its own window for you to edit.

Creating a level

Using tiles

It makes sense to build your levels, or at least parts of your levels, using tilesets. Once you have created a tileset from your source images, Godot allows you to easily build maps from them in the editor. Moreover, the Godot engine is optimised to handle many tiles at once.

Before creating a tileset, use your favourite graphics editor to create a single .png file that can be broken up into similarly sized tiles. Import this file into your project the same way you did the frames of your animation earlier, then follow these steps:

  1. Create a TileMap node and select it.
  2. Expand the Tile Set property and choose “New TileSet”.
  3. Click the Tile Set property again and choose “Edit”.
  4. The TileSet editor appears towards the bottom of the screen. Drag the .png file for your tileset into the space in this editor.
  5. There are various ways of creating tiles from your image. For example, you can use the New Single Tile button. Each time you click this button and select a cell in the grid, a new tile in the tileset is created (their names appear above them as demonstrated below).
Creating New Single Tiles in the TileSet editor

If the grid size is wrong at first, you can fix this using the Snap Options in the inspector once the first tile is created.

Snap Options

Once you have assigned all of the tiles for your set, click the Save icon in the inspector (“Save the currently edited resource”) and save the tileset as a .tres file among your project’s other resources. You can come back and edit it at any time. Now, go back and select your TileMap node. This opens up the map editor.

The map editor in Godot
The map editor in Godot

Here, you can use the Paint Tile, Bucket Fill, Pick Tile, and other tools to draw your map. To exit the map editor, select another node in the tree. For more information about tilemaps, refer to Using TileMaps in the Godot Docs.

Collisions with tiles

In the TileSet editor, you can assign a collision shape to each tile by selecting that tile and clicking the button for Collision mode.

The TileSet editor in Collision mode

Here, you can create rectangles or polygons and mark parts of your tile as collision shapes. The KinematicBody2D will automatically react to collisions with these shapes when you use move_and_collide or _move_and_slide.

  • move_and_collide: The body loses all momentum and ceases to move upon collision.
  • move_and_slide: The body slides along the collision shape. Warning: This method already accounts for the time delta so when you supply a vector to it from your _process function don’t multiply by delta.

In your script, you can get a collision object back from each of these methods so that you can code some custom handling. For move_and_collide, if there was a collision, this object is returned from the method. For move_and_slide, because multiple concurrent collisions are possible, you use the get_slide_count and get_slide_collision methods instead, e.g.

for i in get_slide_count():
var collision = get_slide_collision(i)
print("Boom! " + collision.collider.name)

Reusing tilemaps

Just as with the player object earlier on, you can save a tilemap as its own scene using the “Save Branch as Scene” action. This allows you to e.g. build maps in segments and drag and drop these into your scenes.

Creating tilemaps in code

Once you’ve saved your tilemap as a separate scene, you can dynamically create instances of it at runtime using code. Attach a new script to a node in your scene, e.g. the root node, and use code like the following:

extends Nodevar straight_6x6 = preload("res://Road/Straight6x6.tscn")func _ready():
var instance = straight_6x6.instance()
instance.position = Vector2(0, 0)
self.add_child(instance)

This code creates an instance of the TileMap saved as Straight6x6.tscn with its top-left corner at the (0, 0) position. Hint: This does not only apply to tilemaps — you can use this code to dynamically create instances of anything you saved as a scene.

Responding to events

So an object has collided with another. Your game now needs to react to this event. Two prominent questions arise.

  1. The first question is, what types of objects have collided? Has the player connected with a wall or a collectable item? Has a bullet hit an enemy?
  2. The next question is, once we know what objects have collided, how can we tell them how to react? Part of the reaction code might belong in one object’s script and part of it in another. How does one address this?

A common answer to the first question is the use of groups.

Using groups to keep track of what’s what

Groups in Godot are like tags in other systems. These are arbitrary strings of text that you can assign to nodes and use in your code for a variety of purposes. A given node can belong to multiple groups, which are listed in the Groups section of the Node tab.

Groups on the Node tab
Groups in the Node tab

In your code, you can check whether a node belongs to a particular group using the is_in_group method, e.g.

if collision.collider.is_in_group("enemies"):
print("You have collided with an enemy!")

Groups help to decouple your scripts from concrete scene types.

Calling functions on other objects

An event could have consequences for all involved parties. For instance, when a collision occurs between the player and an enemy, you might want to execute code on both objects.

In traditional object-oriented programming, you might want to invoke a method on the Enemy class. Well, in Godot, you can do the same thing by executing a function in a script attached to the Enemy node. For example, the script for the Enemy node might have the following code:

var health = 100func hurt(damage):
health = health - damage
print("Ouch!")

You can call this function directly on the node as if it were a method, e.g.

var enemyNode = get_node("Enemy")
enemyNode.hurt(20)

Scripting tips

Global variables

You can declare variables in a script and set that script to load automatically in the AutoLoad tab of the project settings.

Project Settings with the AutoLoad tab selected
AutoLoad in the Project Settings

These variables are public and can be accessed from any node in any scene. If you tick the Enabled box, accessing them is as simple as this:

func _process(delta):
move_local_y(delta * GameConsts.baseSpeed)

For more information, refer to Singletons (AutoLoad) in the Godot Docs.

Exposing properties through the UI

Variables declared in a scene’s associated script can be exposed so that you can set them directly through the Godot editor. This is useful when you’re saving node branches as scenes and reusing them throughout your game. For example, the following code exposes the behaviour variable:

export(Behaviours.Enemy) var behaviour = Behaviours.Enemy.default

In the example above, the type of the property is given as Behaviours.Enemy, which in this case is the following enum:

enum Enemy {
default,
move_toward_centre
}

Now, if I select an instance of my scene in Godot, the exported property appears in the inspector under the section “Script Variables”. In my case, the enum values appear in a dropdown box:

Script variables in the inspector
Script Variables in the inspector

See GDScript exports in the Godot Docs for more about exporting variables.

Function references

Unlike Python, GDScript does not treat functions as first-class objects. This means you cannot store functions in variables, pass functions as arguments to other functions, etc. However, GDScript does support the concept of a function reference, which is similar.

To create a function reference, call the built-in funcref function, passing in the following parameters:

  • The object that contains the referenced function.
  • The name of the function (as a string).

For example:

var my_func_ref = funcref(self, "my_func")

To invoke the referenced function, call the call_func method, e.g.

my_func_ref.call_func()

Refer to FuncRef in the Godot Docs for more detail.

Used for this article

  • Godot Engine v3.4.4
  • Krita 5.0.6
  • Windows 10 Pro

--

--

Stephan Bester

Software developer walking the edge between legacy systems and modern technology. I also make music: stephanbmusic.com