This project was created for the Explosion Community challenge, and it goes a bit beyond the initial scope.
Shooting Circles is a simple game example built using only the ECS architecture for Defold (I think it pretty close to Pure ECS). Below, you will find a detailed description of the different parts of the project. If you are interested in the code structure and ECS, welcome!
- Use the tiny-ecs library without modifications.
- Do not use any wrappers around the tiny-ecs library.
- Entities are created using regular tables
{}
. - No external
require()
calls in systems to maintain portability between projects. - Components can only be modified within one system. For example,
entity.transform.x = 10
cannot be set in a non-transform system. - Entities should only contain data, not logic.
- Sure no any global variables.
- The GUI collection should be able to run as the game’s bootstrap collection.
I overuse here event entities to avoid using the update()
function in systems for checking each entity's actions. This results in a delay between actions, especially in the following sequence:
- The physics callback handler creates a
collision_event
entity. - One frame is skipped.
- The
on_collision_explosion
system creates anexplosion_command
entity. - One frame is skipped.
- The explosion system creates a
physics_command
entity. - One frame is skipped.
- Physics handles the command.
For more reactive logic in ECS, the update()
function could be used to iterate and check each entity. However, I prefer to avoid this constant update checking in systems. I'm considering an "event" bus, but I’m not sure about it yet.
The initial script is /loader/loader.script
, which initializes all libraries and loads the game.collection.
The /game/game.script
script creates a world, loads systems, and loads a level with level_loader_command.
All other logic is handled through the ECS systems, located in the /systems
folder.
- All systems are placed in the
/systems
folder. - Systems can return multiple sub-systems.
- Systems are divided into "system", "system_command", and "system_event":
- System: Filters entities by required components and processes them. It usually returns up to three sub-systems: system, system_command, and system_event, and contains all system logic.
- System Command: Describes an external API for the system with the "system_command" component and a list of "event" components to be triggered by the system. To spawn a command for the system, create an entity with the "system_command" component. This entity will be processed and removed by the system in the next frame.
- System Event: Describes the event fields generated by the system. To spawn an event for the system, create an entity with the "system_event" component. This entity will be processed and removed by the system in the next frame.
- Since systems can return a set of systems, we can't use
world:addSystem(...)
, so we useworld:add(...)
instead for them.
To create a new system, I use the system_*
template located in /decore/templates
and replace TEMPLATE
with the system name. Then, add the system to the game.script
in the system list. That’s all it takes to create a new system.
Decore is a library that manages data collections for ECS and allows the creation of entities from prefabs.
- All game components are described in the
resources/components.json
file. Decore uses this file to create components by their prefab_id and fill default values for components. - Entities that are described manually (not in Tiled Editor) are placed in
resources/entities.json
. Items like bullets and different utility entities are easily described in this file rather than creating entities in a Tiled tileset. - At game start, all components, entities, and worlds are registered in Decore.
Detiled is a library that converts Tiled maps and tilesets to Decore entities. It allows setting up components, entities, and worlds from the Tiled map editor.
- Rotation and scale changes for entities are supported. However, if an entity has a physics body, the scale should be uniform, as the physics body will be scaled by the same value as the entity.
To view the Tiled project, open tiled/game_shooting_circle.tiled-project
.
Each map and tileset is exported as JSON and placed in custom resources folders:
- Maps are placed in the
resources/maps
folder. - Tilesets are placed in the
resources/tilesets
folder. resources/maps_list.json
contains the list of maps to be loaded by the game.resources/tilesets_list.json
contains the list of tilesets to be loaded by the game.
This loading happens in the loader.script
in the init_detiled()
function.
Tiled has a Custom Types Editor that is well-suited for describing ECS entities. Open View -> Custom Types Editor
to see the components defined for this project.
To use these components, add them to an entity in the tileset or instance in the world and override the fields. You can also use the "object" type of property field to link to other entities in the world.
Default values are always used in the custom types. Only describe the fields you need to manipulate; other fields will use the default values from resources/components.json
.
- Add components and set default property values for each tileset entity.
- In maps, you can override the properties of the entity. However, doing so will replace the entire component with the new one. Therefore, changes to Tileset Entity properties will not be reflected in the map.
I included a few tests to check how we can add unit tests to the systems. Look at /test
folder for more details.
- How to correctly work with events?
- How to make system excluding works correct? If components are still created, commenting out the
game_object
system will cause other systems requiring thegame_object
component to fail when trying accessgame_object
component fields like "root" or "game_objects." - How to debug the chain of events? I want to see the chain of events.
- What tools are needed to debug and interact with ECS? Possibly an admin panel or entity viewer.