S
SpaceDoctor
Guest
Hi all, first post here on the forums. I'm not sure if this is the right board, as it's not exactly a problem I'm facing, but instead something I've developed and want to share. For fun, I've lately been working on implementing a type of entity component system (ECS) in GMS. I want to stress that this is probably not very practical, and is more of an exercise to see if it could work right now.
My reason for this was that I eventually ran into the inevitable code spaghetti that one finds themselves with after numerous, complicated inheritances. Rather than try to find a way to get it to work in the current system, I decided to try and find a way to insert components into objects as I wanted. For the following code snippets, I've removed any error checking for simplicity.
Here's what I came up with.
Components (object)
-variables: type (enum), data (anything), manager (object)
Entities (object)
-variable: component map (ds_map)
Systems (object)
-variables: type (enum), entity list (ds_list)
In order to get everything working, I also have a global, persistent ECS object that creates and manages all systems. It's create event looks like this:
Here we are adding system objects with the script, add_system, which creates a new object from an input object index and adds it to its system map.
Entities are also just objects that contain a ds_map for their components. A create event might look like:
Here we are adding a couple components to this object. This object also inherits from an object that only contains a ds_map called component map, which occurs in event_inherited(). Let's look at what happens in the add_component script:
So, similar to the add_system script. Even though we are adding components here in the create event, there is nothing stopping you from adding a component later on by calling this script. In addition, we can also remove a component via ent_remove_component:
This script should also show the other underlying working of this whole system. Systems and components have a variable called type that uses a global enum, for instance:
Lastly, the whole system comes together in the step event of the o_ECS object:
The kinetic collider is probably the most interesting, so let's look at it:
This should give you an idea of how it generally works for all systems: we loop through all the entities that are being managed and update them accordingly. The above script works by checking collisions between moving objects (kinetic colliders) and static colliders, and therefore must do a double pass over the kinetic colliders.
Takeaway
It seems that getting a rough ECS is somewhat possible in GMS. With my current system, you can add basically any type of data you want to an entity, although I'm not sure where that all breaks down. The default data variable for components is set to none in the parent component, and then is defined in each child component. I currently have data being an array, boolean, and a game object with no problems accessing the data variable from entities and systems.
However, there is one important caveat that I had to learn the hard way: updating an objects components in its step event will break the entire system. Therefore, this approach works only if you contain everything in systems and update scripts, and leave the step events for object variables and state machines perhaps.
I hope you enjoyed letting me ramble and go through some of my code. I would love to hear anyone's thoughts on this, or maybe a way that this approach could be improved.
My reason for this was that I eventually ran into the inevitable code spaghetti that one finds themselves with after numerous, complicated inheritances. Rather than try to find a way to get it to work in the current system, I decided to try and find a way to insert components into objects as I wanted. For the following code snippets, I've removed any error checking for simplicity.
Here's what I came up with.
Components (object)
-variables: type (enum), data (anything), manager (object)
Entities (object)
-variable: component map (ds_map)
Systems (object)
-variables: type (enum), entity list (ds_list)
In order to get everything working, I also have a global, persistent ECS object that creates and manages all systems. It's create event looks like this:
Code:
system_map = ds_map_create();
// systems
add_system(id, o_sys_transform);
add_system(id, o_sys_static_collider);
add_system(id, o_sys_kinetic_collider);
add_system(id, o_sys_controller);
Code:
///@param ecs_id
///@param system_object_index
var ecs_ = argument0;
var system_index = argument1;
var system_ = instance_create_layer(ecs_.x, ecs_.y, "Instances", system_index);
system_.persistent = true;
ds_map_add(ecs_.system_map, system_.type, system_);
ds_map_add(global.system_ID_map, system_.system_id, system_);
Code:
event_inherited();
// components
ent_add_component(id, o_comp_transform);
ent_add_component(id, o_comp_kinetic_collider);
// variables
move_speed = 60/room_speed;
Code:
///@param entity
///@param component_object_index
var entity_ = argument0;
var component_index = argument1;
var component_ = instance_create_layer(entity_.x, entity_.y, "Instances", component_index);
ds_map_add(entity_.component_map, component_.type, component_);
ds_map_add(global.component_ID_map, component_.component_id, component_);
// add entity to system (component's manager)
ent_add_to_system(entity_, component_.manager);
Code:
///@param entity
///@param component_enum
var entity_ = argument0;
var component_enum = argument1;
var component_map = entity_.component_map;
var component_ = component_map[? component_enum];
// remove the component from the map and destroy the instance
ds_map_delete(component_map, component_enum);
instance_destroy(component_);
// remove the entity from the system
var manager_ = component_map[? component_enum].manager;
sys_remove_entity(manager_, entity_);
Code:
global.component_ID = 0;
global.component_ID_map = ds_map_create();
enum components
{
none,
transform,
static_collider,
kinetic_collider,
controller
}
Code:
// systems that affect transform
update_sys_controller(system_map[? systems.controller]);
// normalize movement direction
update_sys_transform(system_map[? systems.transform]);
// apply and correct movement
update_sys_kinetic_collider(system_map[? systems.kinetic_collider], system_map[? systems.static_collider]);
Code:
///@param kinetic_system
///@param static_system
var sys_kinetic = argument0;
var sys_static = argument1;
var kinetic_list = sys_kinetic.entity_list;
var static_list = sys_static.entity_list;
for(var kinetic_index = 0; kinetic_index < ds_list_size(kinetic_list); kinetic_index++)
{
var kinetic_entity = kinetic_list[| kinetic_index];
var move_delta = ent_get_component_data(kinetic_entity, components.transform);
// MOVE AND CHECK IN THE X DIRECTION //
kinetic_entity.x += move_delta[0];
// check the other kinetic colliders by looping through everyone except the above collider
for(var kinetic_index_2 = 0; kinetic_index_2 < ds_list_size(kinetic_list); kinetic_index_2++)
{
if kinetic_index_2 != kinetic_index
{
var kinetic_check = kinetic_list[| kinetic_index_2];
collision_detection(kinetic_entity, kinetic_check, axis.x);
}
}
// check the static colliders
for(var static_index = 0; static_index < ds_list_size(static_list); static_index++)
{
var static_check = static_list[| static_index];
collision_detection(kinetic_entity, static_check, axis.x);
}
// MOVE AND CHECK IN THE Y DIRECTION //
kinetic_entity.y += move_delta[1];
// check the other kinetic colliders
for(var kinetic_index_2 = 0; kinetic_index_2 < ds_list_size(kinetic_list); kinetic_index_2++)
{
if kinetic_index_2 != kinetic_index
{
var kinetic_check = kinetic_list[| kinetic_index_2];
collision_detection(kinetic_entity, kinetic_check, axis.y);
}
}
// check the static colliders
for(var static_index = 0; static_index < ds_list_size(static_list); static_index++)
{
var static_check = static_list[| static_index];
collision_detection(kinetic_entity, static_check, axis.y);
}
}
if not ds_exists(sys_kinetic.entity_list, ds_type_list) or not ds_exists(sys_static.entity_list, ds_type_list)
{
var message = "System entity lists have been destroyed!"
show_error(message, true);
}
Takeaway
It seems that getting a rough ECS is somewhat possible in GMS. With my current system, you can add basically any type of data you want to an entity, although I'm not sure where that all breaks down. The default data variable for components is set to none in the parent component, and then is defined in each child component. I currently have data being an array, boolean, and a game object with no problems accessing the data variable from entities and systems.
However, there is one important caveat that I had to learn the hard way: updating an objects components in its step event will break the entire system. Therefore, this approach works only if you contain everything in systems and update scripts, and leave the step events for object variables and state machines perhaps.
I hope you enjoyed letting me ramble and go through some of my code. I would love to hear anyone's thoughts on this, or maybe a way that this approach could be improved.