For a long time I have been using Unity as a game engine to implement most of my games. I considered Unity as the standard, with more tutorials, features, community. Apparently an obvious choice (especially years ago, ~2014).
Nonetheless, Unity has always been frustrating to use. The software is slow and heavy (6Gb), I find the tutorials and documentation not user-friendly, and it has been always a struggle to implement complex behaviors in my games.
Lately this frustration convinced my to give a try to Godot, and I am glad that I did!
In this post I share my experience implementing some core functionalities which are needed for most 2D games using Godot:
- Animations
- Collisions
- Movements
- Tilemaps
- Objects interactions
This experience convinced my to switch to Godot π
This is not a Godot step by step tutorial, but more of a general overview of how to get started with the game engine.
I left links to every node and class so you can read more from the documentation about each topic covered in the post.
Nodes
Each Scene
contains a tree of nodes.
godot-scene-overview
Each node encodes some functionality:
- Sprite animations
- Physics (
RigidBody2D
) - Collisions (
CollisionShape2D
) - Tilemaps (
TileMap
)
Nodes can be then nested into each other to implement more complex behaviors. Furthermore, a node itself can contain other Scene
as children.
This makes each Scene
and Node
composable, which allows to build complex behaviors and interactions.
Animations
Animations (2D) are as easy as it gets to setup and use.
I used the AnimatedSprite2D
node, which allows to select a sequence of frames (images) to build an animation.
godot-animated-sprite-2d-frames
The process is simple:
- Insert an
AnimatedSprite2D
node - Add a
SpriteFrame
from theAnimatedSprite2D
node inspector - Cut and select the frames for each animation (idle, walking, running) and give each animation a unique name identifier
- From the player script set the animation and call the
play()
function
func _ready():
$AnimatedSprite2D.animation = "idle_down"
$AnimatedSprite2D.play()
Now it is only a matter of updating the current animation each time the player state changes:
func _physics_process(delta):
var animation_state = $AnimatedSprite2D.animation
var input_direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = input_direction * speed
if velocity.length() == 0:
if animation_state.begins_with("walk_"):
if animation_state == "walk_up":
animation_state = "idle_up"
elif animation_state == "walk_x":
animation_state = "idle_x"
else:
animation_state = "idle_down"
elif velocity.length() > 0:
if velocity.x != 0:
animation_state = "walk_x"
if velocity.x < 0:
$AnimatedSprite2D.flip_h = true
else:
$AnimatedSprite2D.flip_h = false
elif velocity.y > 0:
animation_state = "walk_down"
elif velocity.y < 0:
animation_state = "walk_up"
move_and_slide()
$AnimatedSprite2D.animation = animation_state
$AnimatedSprite2D.play()
Movements
Implementing a top-down movement system is also a breeze.
I created my character using the CharacterBody2D
class. CharacterBody2D
provides a move_and_slide()
method that allows to move the node in any direction and speed specified by the node's own velocity
(Vector2
).
From the settings (Project > Project Settings... > Input Map
) I defined the input buttons for each of the 4 movement directions (left, right, top, down).
godot-input-map-project-settings
In the player script I then update the velocity
based on the clicked buttons, and then call move_and_slide()
:
func _physics_process(delta):
var input_direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = input_direction * speed
move_and_slide()
These 3 lines of code are enough to implement a complete 2d movement system.
Collisions
Collisions for the player are implemented using the CharacterBody2D
class in combination with CollisionShape2D
.
CollisionShape2D
allows to define visually from the editor the shape of the collision area (using a Shape2D
).
godot-collision-shape-2d-example
I then added a similar setup for all the nodes that are expected to collide with the player:
- I added a Physics Layer to the
TileSet
, which allows to add a collider shape to a tile before painting aTileMap
- I added a
CollisionShape2D
together with aRigidBody2D
to NPCs (Non Playable Characters)
There is more π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
Detecting collisions using Raycast
After adding collision shapes and rigidbody it becomes possible to check for collisions between nodes.
I used a RayCast2D
node to define a line in front of the player for checking collisions.
From the player script I have full control over the properties of RayCast2D
. I added some code to change the direction of the raycast based on the direction in which the player is facing:
func _physics_process(delta):
# Get current raycast direction
var raycast_direction = $RayCast2D.target_position
# Set new value for target_position
$RayCast2D.target_position = raycast_direction
This allows to then detect collisions with objects in the scene from the player perspective.
For NPCs instead, I attached an Area2D
node to each of them, which I use to check if the player is nearby (using signals, ready below π).
Signals
Signals are the real game-changer when using Godot.
Signals allow to listen for any event from any script (Observer pattern)
Instead of hard-coding connection between nodes and manually define events and interactions, you can use Signals to send and react to any event between any node.
In my example I defined a custom signal inside the NPC script (by adding a signal
at the top of the script):
signal interacted(text)
I then emit
this signal every time the NPC detects the player nearby (by listening to an Area2D
signal) and the user presses the "interaction" button:
func _process(delta):
if is_player_inside_area && Input.is_action_pressed("interact"):
interacted.emit(speech_dialogue)
func _on_area_2d_body_entered(body):
is_player_inside_area = true
I also passed a custom value text
to the signal, which represent the sentence that the NPC is going to say to the player.
godot-signals-with-custom-example-panel
This signal triggers a speech bubble and starts the interaction between the player and the NPC:
func _on_npc_interacted(text):
set_text(text)
TimeMap
I am mostly interested in 2d games. For a 2d games, having a fully-featured tilemap editor is critical. Godot provides all that you need using the TileMap
node.
I added TileMap
as root node of a new Scene
. TileMap
contains a TileSet
, which represents the actual tiles that we are then going to "paint" inside the TileMap
.
Godot allows you to:
- Create a
TileSet
from a tile sheet image, by splitting the image based on the resolution of each tile (I used a 16x16 tile sheet in my project) - Define layers and ordering for each tile in the
TileSet
- Add collision, physics, navigation (path finding), and more to the
TileSet
- Visually print your level into the
TileMap
using the tiles insideTileSet
godot-tilemap-with-tileset-layers-preview
The editor is feature complete while also being easy to navigate. I definitely found every feature that I needed for my game example by just using TileMap
+ TileSet
.
Overview - Putting all together
This is all that you need to start working on your game.
You can now add and control any animation to any node using AnimatedSprite2D
. AnimatedSprite2D
allows to control the duration of each single frame and preview the final result from the inspector.
You can use TileMap
to build a complete 2D level by adding all the objects, NPCs, and player. TileMap
allows to define collisions, layers, handles path finding and navigation, and much more.
You can use CharacterBody2D
to implement movements for your player, or you can also use RigidBody2D
.
You can use Signals to define and implement any interaction between any node (picking up objects, breaking a wall, attacking an enemy).
All of these blocks can be then composed together using Node
s and Scene
to create each level for your game.
After this initial experience I am going to switch to Godot for my next games:
- More lightweight (200Mb instead of 6Gb of Unity)
- Integrated IDE
- Documentation easier to navigate
- Open source
- Better developer experience
These are all important points for me when working with any technology. So, welcome to Godot, more content coming soon π
You can subscribe to my newsletter here below for more updates and tips π