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/

image

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.

image

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.

image

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.

image

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.

image

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 the script_templates directory from the cloned repository into res:// 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.

image

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!