Step‐by‐Step Example Tutorial - ratmarrow/godot-ecs-wrapper GitHub Wiki
Written here is a step-by-step tutorial on how to make something using the Godot ECS Wrapper. It's gonna be wordy, but that's so no child is left behind in the perils of my code.
Setting up your project
The Godot ECS Wrapper is very simple to get up and running.
Clone the repository
First, you need to clone the repository into your project directory. Alternatively, you can download and unzip the files into your directory.
git clone https://github.com/ratmarrow/godot-ecs-wrapper.git
There may be errors when the scripts initially get imported, this is because you have to set up the three core autoloads.
Setting up the autoloads
Go to Project > Project Settings > Globals
to add the Entities, Components, and Systems scripts as autoloads. monitor.gd
is optional, but provides quick and dirty information on resources used and the state of the ECS wrapper.
These can be found in godot-ecs-wrapper/scripts/base/autoload/
Create component directory
Next, you want to create the directory where your component scene resources will be stored. This is necessary so the Components autoload can preload the components on game launch.
The Components autoload automatically looks for a res://components
directory. You can change this by going into godot-ecs-wrapper/scripts/autoload/components.gd
and replacing the path passed to _load_components
.
After all these steps have been achieved, we are now ready to start writing game logic.
Creating a game
For this tutorial, we are going to throw together a very rudimentary top-down character.
Creating the components
Component creation is as simple as making a script and/or a scene resource. For this example, we are going to make three component:
- One that uses a custom script.
- One that uses a built-in Godot type.
- One that is meant to be a child of a component.
Input Receiver Component
We will start with the Input Receiver component. This will be in charge of telling the world that our entity wants to receive player input, as well as storing the received input values.
Create a new script inheriting Node
, call it input_receiver_component.gd
, and give it the class name InputReceiverComponent
.
You also want to define a Vector2 called direction
. When all is said and done you should have this:
class_name InputReceiverComponent extends Node
var direction : Vector2
Next, we need to make a scene resource of this script. We will do this by creating a new scene of our new InputReceiverComponent
type in our components directory. I have called it input-receiver.tscn
because I want the name of the component in code to be input-receiver
.
Character Body Component
Now that we have input, we are going to need a CharacterBody2D
to control. For this example, we won't need to make a new script for the component, we will just need to make a scene for it.
In your component directory, create a new scene of type CharacterBody2D
. I have called it "character-body.tscn".
The newly created scene will throw a warning because it doesn't have a collision shape. You can either make it a component, or you can make it part of the character-body.tscn
tree. For this example, we will do the latter. Add a CollisionShape2D
as a child of the character-body
node, and give it a CircleShape2D
with a radius of 64 pixels.
Sprite Component
Finally, create a new scene of type Sprite2D
in your component directory. This will be our child component. I have called mine "sprite.tscn" and made the sprite the default icon.svg
picture, because it's funny.
We are now able to start making systems.
Writing our systems
For this tutorial, we will be making three systems:
-
One for gathering input and relaying it to
InputReceiverComponents
-
One for moving our character.
-
One for spawning characters.
When creating systems, you can either make them of type Node
, or type System
. The System
type comes with an empty, annotated template for systems, and can allow you to write logic that runs on all your systems if you so choose. Figured it would be nice to have.
This tutorial will make systems inherit from the System
type.
If you intend to use the
System
node type in system creation, you can drag out thescript_templates
directory from the cloned repository intores://
so that Godot can recognize the template.
Input System
We will start by creating a new script called input_system.gd
of type System
, and we will give it the class name InputSystem
.
If you used the System Framework template, you would see a large comment block on the top of the script. I have taken the liberty to exclude them in this tutorial.
You will then create a new archetype containing the input-receiver
component.
class_name InputSystem extends System
# I heavily encourage you to change the names of your archetype definitions to be more specific.
# Components are represented as `StringName`, so you can compare using an identical `String`.
var receivers := Archetype.new(["input-receiver"])
From here, we will establish our input gathering loop. We will start with creating a simple input vector from Godot's built-in keybindings.
func _physics_process(_delta: float) -> void:
# We will collect the input outside of the entity loop
# so we don't recalculate it for every single input receiver in world.
var input := Input.get_vector("ui_left", "ui_right", "ui_down", "ui_up")
And after this, we will make our loop.
func _physics_process(_delta: float) -> void:
# We will collect the input outside of the entity loop
# so we don't recalculate it for every single input receiver in world.
var input := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
for ent in receivers.get_entities():
# You don't HAVE to cast the type explicitly, I just prefer it for code completion.
var c_receiver = Entities.get_components(ent)["input-receiver"] as InputReceiverComponent
c_receiver.direction = input
Character Motion System
Create a system script as you have previously, call it character_motion_system.gd
, and give it the class name CharacterMotionSystem
.
Define an archetype that looks for the previously created input-receiver
and character-body
components.
class_name CharacterMotionSystem extends System
var characters := Archetype.new(["input-receiver", "character-body"])
In _physics_process
, create an entity loop that grabs references to the necessary components.
func _physics_process(delta: float) -> void:
for ent in characters.get_entities():
var components := Entities.get_components(ent)
var input = components["input-receiver"] as InputReceiverComponent
var body = components["character-body"] as CharacterBody2D
From here, we can now apply our motion.
func _physics_process(delta: float) -> void:
for ent in characters.get_entities():
# To avoid writing `Entities.get_components(ent)[""]` everytime,
# we can just cache the list and access it directly.
var components := Entities.get_components(ent)
var c_input = components["input-receiver"] as InputReceiverComponent
var c_body = components["character-body"] as CharacterBody2D
c_body.velocity = c_input.direction * 500
c_body.move_and_slide()
Character Spawning System
Obviously, our entities can't move around if they don't exist, so we have to make a system to spawn in entities.
Do the same song and dance as the previous two systems, name this one spawner_system.gd
, and give it the class name SpawnerSystem
.
class_name SpawnerSystem extends System
For this particular system, we can actually remove the predefined archetype, as this system doesn't care about the entities in the world.
In _physics_process
, we will check if the player hits the "ui_accept"
bind, and construct an entity if the bind is pressed.
func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("ui_accept"):
# Create a blank entity.
var ent : int = Entities.create_entity()
# Add a character body to the entity, and cache it so we can attach a sprite component as a child of the body.
var body = Entities.add_component("character-body", ent) as CharacterBody2D
# Add a sprite to the entity, and cache it so we can randomize the color.
#
# We pass the body component into the function so the wrapper knows to instantiate
# the sprite as a child of the body.
var sprite = Entities.add_component("sprite", ent, body) as Sprite2D
# Add an input receiver to the entity.
# We don't have to cache it because we don't need to do anything else with it.
Entities.add_component("input-receiver", ent)
# Randomize the new sprite's tint.
sprite.modulate = Color(randf_range(0, 1), randf_range(0, 1), randf_range(0, 1))
Now that we have created all our systems, it's time to instantiate them to make the game playable and close out this tutorial.
Initializing our systems
Initializing a system can be done in a single line of code inside of the Systems autoload.
Inside of the Systems _initialize_systems
function, you can simply write add_child(SystemClassHere.new())
.
For our example project, we need to initialize InputSystem
, CharacterMotionSystem
, and SpawnerSystem
as such:
func _initialize_systems() -> void:
print("Signal 'components_loaded' received. Initializing systems...")
if systems_initialized == true: return
# System instancing goes here.
add_child(InputSystem.new())
add_child(CharacterMotionSystem.new())
add_child(SpawnerSystem.new())
systems_initialized = true
print("Systems initialized!")
Now, you should be able to run the game, press ENTER and spawn in a bunch of icon.svg
that are all controllable.
Conclusion
Now you should have all the knowledge needed to make games using the Godot ECS Wrapper! There will be a little more overhead, but the benefits of such a structure will (hopefully) shine through.
Happy coding!