Making a 2D platform game with Godot 3.0

A few weeks ago I stumbled upon the game engine Godot and I decided to give it a try by learning how to use it for creating a 2D platform game. This blog post contains a walk-through of the different parts of this endeavour. The source code for my game is completely free and is available from GitHub so just go ahead and download and experiment with it as you like.

About Godot

Godot is an open-source game engine with a light-weight development environment that runs on OS X, Windows and Linux. The games can be deployed to the mentioned OS:es and also to iOS, Android and HTML (with WebAssembly and WebGL). I’m using Godot 3.0 that  was released in late January 2018. The engine supports both 2D and 3D development but the focus in this tutorial is solely on 2D. For scripting the game logic, Godot supports several different languages (among them C# with mono in Godot 3.0). I use the default, Python-like, GDScript.

There are so many things I like about Godot. It’s free, open-source, multi-platform for development, multi-target for deployment and very easy to use. The main benefit for me is that Godot allows for building the game objects in different scenes which makes development easier and it also encourages re-use of the game objects.

As the stable version of Godot 3.0 is quite new, there are not many tutorials available yet. There are many guides on Godot 2.0, but as much has changed for Godot 3.0 all 2.0-information is not applicable to the new version.

Game play

The goal for this project was to learn more about Godot and GDScript-programming, so I have not invested much in the actual game play. In fact it is rather rudimentary:

  • The player should capture all items on each level…
  • …while avoiding being battered by the enemies
  • If the player’s health goes down to 0% it is game over
  • If the player captures all items, the player is promoted to the next level
  • For bonus points, the player can capture gems hidden in boxes and jump on enemies

Scenes, nodes and game objects

Games are built with scenes in Godot. A scene contains a tree of nodes that in turn can be other scenes. Thanks to Godot’s scene system, the game objects can be mapped to scenes that can be developed and tested separately.

Nodes for a scene tree can be defined in the Godot editor but you can also instantiate nodes from code which is very useful when you need to spawn many objects with random parameters.

Graphic assets

To get some inspiration for a 2D platform game I browsed opengameart.org and found some nice open-source art by kenney. I use this public domain package for my game (with some slight modifications):

The player character

I started out by creating the player character. This is implemented in a separate scene (stored in the player.tscn file). The player is a KinematicBody2D object. This object type handles collisions with other objects automatically and it also sees to that the object is moved to a non-collision state after a collision. For custom detection on when a KinematicBody2D object overlaps another object you need do some coding. More on this further down in this post.

player_scene2.jpg

The player scene has an animated sprite with a Particle2D object that creates a visual effect when the object is hit. The CollisionPolygon2D object covers the essential part of the object for collision detection. The object also has some AudioStreamPlayers for sound effects and two timers that triggers the the sound effects and animations when the object has been hit.

The player scene has a Camera2D object. This makes it possible to scroll the view as the character moves.

Using a TileMap for level building

For rapid development of the core of 2D levels, a TileMap can be used. A TileMap is a node where you can draw with tiles from a TileSet.

Creating a tile set

A tile set can be created by first creating a separate scene where you add sprites with collision nodes. Then you can export this as a tile set resource that can be referenced from a TileMap.

tileset.jpg
TilSet scene with a tree of sprite+StaticBody2D+Collision nodes. This scene can be export to a TileSet resource file.

Using a tile set

Create a new scene that contains a TileMap object. On this object you can link to your created tile set resource file. After this you can draw with your tiles. If you set the grid size of the TileMap cells to map your tile size, they will align properly in the editor.

tilemap
TileMap with the tiles from the TilSet available to the left.

Items

Spawning items

In my game items are objects that can be collected by the player for gaining points. On the first level, the items are stars. These are implemented as simple sprites. As I don’t want to manually instantiate a sprite in the editor for each item, I came up with a this approach instead:

  1. Draw Path2D points on the level
  2. When the level is loaded, create a Star instance for each point in the Path2D-object

This allows me to easily mark on the TileMap where each star should be placed without having to manually create a sprite for each Star. The level loading code looks like this:


extends Node2D
export (PackedScene) var Star
export (int) var total_stars = 0
export (int) var stars_found = 0
func _ready():
var pos_curve = $ItemPositions.get_curve()
total_stars = pos_curve.get_point_count()
for i in range(0, total_stars):
var star = Star.instance()
add_child(star)
star.position = pos_curve.get_point_position(i)

view raw

level.gd

hosted with ❤ by GitHub

And the Path2D path in Godot looks like this:

Godot_path2d_item_positions.jpg
Every red dot on the blue line will be a position for a Star item

Detecting item collisions in code

The player object is a KinematicBody2D object and I have not found a good way to do custom collision detection with other objects from within KinematicBody2D. Thus, I go the other way around: From a collision between my player object and an item, I let the item send a notification about the collision. This is possible because the items are Area2D objects that implements a body_entered event.

The items (Stars on level 1) are instantiated from a star scene. The star scene is an Area2D object that holds a sprite object for the star image and CollisionPolygon2D object that defines the boundaries for the star:

star_scene.png

When a star collides with a an object, it checks if it is a player object, and if so, it sends a signal that a star has been taken and then removes itself from the game (with queue_free()):


extends Area2D
signal star_taken
func _on_Area2D_body_entered( body ):
if (not body.get("is_player") == null):
$CollisionPolygon2D.disabled = true
hide()
emit_signal("star_taken")
queue_free()

view raw

star.gd

hosted with ❤ by GitHub

The level scene is updated to handle the star_taken signal. The level code routes the signal so that it can be handled by the main scene (for counting the score/points).


extends Node2D
export (PackedScene) var Star
signal star_was_taken
export (int) var total_stars = 0
export (int) var stars_found = 0
func _ready():
var pos_curve = $ItemPositions.get_curve()
total_stars = pos_curve.get_point_count()
for i in range(0, total_stars):
var star = Star.instance()
add_child(star)
star.position = pos_curve.get_point_position(i)
star.connect("star_taken", self, "on_star_taken")
func on_star_taken():
stars_found += 1
emit_signal("star_was_taken")

view raw

level.gd

hosted with ❤ by GitHub

Enemies

The enemies are implemented in a similar way as the items. I draw a Path2D points on the level but I want the enemy objects to follow the path between the points. For this purpose the Path2D object has a PathFollow2D sub item. The PathFollow2D allows for getting an arbitrary offset position along the Path2D curve and these positions are used for moving the enemies around the path.

enemies_path_level1.jpg

The level scene code spawns 10 bats (enemies) and sends the PathFollow2D object to each object and listens to “player_hit” signals from each bat/enemy object:


extends Node2D
export (PackedScene) var Bat
func _ready():
for i in range(0,10):
var bat = Bat.instance()
bat.path = $BatPath/PathFollow2D
bat.connect("player_hit", self, "on_player_hit")
add_child(bat)

view raw

level.gd

hosted with ❤ by GitHub

A bat object is an Area2D object with an animated sprite and a CollisionShape2D object:

bat
Enemy scene with an animated sprite with a rectangular collision shape

Each bat object gets the PathFollow2D (the path variable) from the level object and uses a random start position along the curve. The speed and direction of each bat is also random. To get the sprite to point in the correct direction, the sprite is flipped depending on if it moves to the right or to the left:


extends Area2D
signal player_hit
var path = null
var prevX = 0
var pathIdx = 0
var speed = 0
var direction = 0
func _ready():
pathIdx = randi() % 10000
speed = randi() % 3 + 1
direction = randi() % 2
func _process(delta):
if position.x > prevX:
$AnimatedSprite.flip_h = true
else:
$AnimatedSprite.flip_h = false
prevX = position.x
if not path == null:
path.set_offset(pathIdx)
position = path.position
if direction == 0:
pathIdx += speed
else:
pathIdx -= speed
func _on_Bat_body_entered( body ):
if (not body.get("is_player") == null):
emit_signal("player_hit")

view raw

bat.gd

hosted with ❤ by GitHub

Detecting when on a ladder

In the tile map there is a particular tile that should have special treatment – the ladder. The player should not collide with this tile, but when the player is on the ladder, the player character should have a special animation and be able to climb up and down. Also in this state, the player object should not be affected by gravity.

climbing_ladder.png
Player character on ladder

To achieve this I define the ladder tile in the tile set to be a sprite without StaticBody2D– and collision nodes:

ladder_tile
Ladder tile without StaticBody2D- and collision object

In the script for the player scene I check if there is a tile map node available as parent. If so, I convert the player’s position to a tile map position (with world_to_map) and check what tile exists on that position. If it is a ladder tile, the player character is on the ladder.


var on_ladder = false
func _physics_process(delta):
var tilemap = get_parent().get_node("TileMap")
if not tilemap == null:
var map_pos = tilemap.world_to_map(position)
var id = tilemap.get_cellv(map_pos)
if id > -1:
if tilemap.get_tileset().tile_get_name(id) == "ladder":
on_ladder = true
else:
on_ladder = false
else:
on_ladder = false

view raw

player.gd

hosted with ❤ by GitHub

Jumping on enemies and open boxes with gems

I’ve added so that the player can incapacitate the enemies by jumping on top of them. Another extra feature is that you can open special box tiles by bumping the player’s head into them. This reveals diamond gems for extra points. You don’t need to collect all gems to complete a level though. Here is a video on how it looks:

HUD

The HUD (Heads Up Display) shows the current score and level, health and level complete status. This is implemented with Godot’s label control objects that are wrapped within split containers. The HUD controls has a CanvasLayer parent which makes the controls stick on the same position on the screen even though the rest of the game world is scrolling.

Menu

When the game is running you can press ESC for pause. This shows a menu where you can resume, restart and quit. There are also menu options available when you reach the states Level complete, Game over and Game complete.

Sounds

I created short background tracks with Logic and Absynth that I exported to ogg format for each game level. I also recorded some sounds for the player character (also exported as ogg files). The sounds are handled by AudioStreamPlayer objects and are triggered at different times within the game.

Parallax background

To get a more vivid background I added a ParallaxBackground object with a ParallaxLayer sub node with a tiled background.

Conclusions

Godot is great!! Love it! I still have much to learn but I am very eager to continue working with this game engine.

The source for this game can be downloaded from GitHub: https://github.com/LarsBergqvist/star_hunter_game

I’ve also uploaded a pre-release (currently only for OSX) with 3 levels that you can download from the release page:
https://github.com/LarsBergqvist/star_hunter_game/releases

2 Thoughts

Leave a comment