An Attempt at ECS in GMS (kind of)

Discussion in 'Programming' started by SpaceDoctor, Oct 1, 2019.

  1. SpaceDoctor

    SpaceDoctor Member

    Joined:
    Jul 11, 2019
    Posts:
    3
    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:
    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);
    
    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.
    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_);
    
    Entities are also just objects that contain a ds_map for their components. A create event might look like:
    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;
    
    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:
    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);
    
    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:
    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_);
    
    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:
    Code:
    global.component_ID = 0;
    global.component_ID_map = ds_map_create();
    
    enum components
    {
       none,
       transform,
       static_collider,
       kinetic_collider,
       controller
    }
    
    Lastly, the whole system comes together in the step event of the o_ECS object:
    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]);
    
    The kinetic collider is probably the most interesting, so let's look at it:
    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);
    }
    
    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.
     
  2. samspade

    samspade Member

    Joined:
    Feb 26, 2017
    Posts:
    2,019
    I don't know enough about ECS to comment, but I do think this is interesting. Hopefully people with more experience will give feedback.

    I have used various versions of components but always locally. For example, in one project I have a health component. Objects like ships can have or not have health and the ship object doesn't care and nothing interacting with the ship cares either. Health is a separate object which can be added as a component to the ship. It controls the health, handles destruction, etc.
     
  3. SpaceDoctor

    SpaceDoctor Member

    Joined:
    Jul 11, 2019
    Posts:
    3
    That's basically exactly what this is!
    Funnily enough this started in a similar way, where I had a stats object that controlled an object health, stamina, etc., but then realized that I could try to make the whole thing as generic as I can. Now, as long as the component object is defined with a type and manager you can define a component object as whatever you want.
    I'm working on adding a 'debug' sort of object that will allow me to click on any entity and view/remove/add its components. A current problem is that removing the transform component first breaks the controller and collision systems, as they use it. I need to find a way to make it impossible to remove certain components if certain systems exist first.
     
  4. SpaceDoctor

    SpaceDoctor Member

    Joined:
    Jul 11, 2019
    Posts:
    3
    for anyone interested in this, there's a much more elegant solution to adding components than my stupid enum method:
    Code:
    if ds_list_find_index(global.component_id_list, _component_object) == -1
    {
       ds_list_add(global.component_id_list, _component_object);
    }
    var _component_index = ds_list_find_index(global.component_id_list, _component_object);
    
    you can create a list of existing component object indexes and use this list index as the index to store components in entity maps. Made this change throughout and the whole system works the same. Now any component can be added without having to update the component enum list. For instance, to access an entity's transform component:
    Code:
    var _index = ds_list_find_index(global.component_id_list, o_comp_transform);
    var move_delta = _entity.component_map[? _index].move_delta;
    
    I also got rid of the 'data' variable in the components and let the object hold whatever it wants. That way we just access the component from the map and then access whatever instance variables it owns.
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice