First off, sorry if this is super disorganized or doesn't make much sense. This is just a super rough draft of what each of the parts of the ECS look like.

Things to keep in mind:

  1. Entities hold components.
  2. Components only hold data, they don't run any logic. (Usually)
  3. Systems manipulate components and run most of the logic.

The general workflow for this is as follows:

  1. You add components onto entities.
  2. A signal gets sent to any systems that care about those components being added.
  3. If the entity has all the components the specific system cares about, the system adds it to its entities array.
  4. The system will process each entity in its entities array every step.

Entites & Components

So every object I want as an entity in the ECS will be a child of the Entity object. This just ensures a __components struct is defined on them and also I clear any tags and remove all components from the instance in the clean up event.

Here are the various component functions to manipulate and check for components on an entity:

function component_add(entity, component) {
	// ADD COMPONENT DATA TO ENTITY
	if (is_array(component)) {
		for (var i = 0; i < array_length(component); i++) {
			var _index = asset_get_index(instanceof(component[i]));
			entity.__components[$ _index] = component[i];
			// SEND MESSAGE TO SYSTEMS THAT THIS ENTITY HAS CHANGED
			broadcast_channel(_index, CH_ENTITY_ADD, entity);
			asset_add_tags(entity, instanceof(component[i]), asset_object);
		}
	} else {
		var _index = asset_get_index(instanceof(component));
		entity.__components[$ _index] = component;
		// SEND MESSAGE TO SYSTEMS THAT THIS ENTITY HAS CHANGED
		broadcast_channel(_index, CH_ENTITY_ADD, entity);
		asset_add_tags(entity, instanceof(component), asset_object);
	}
}

function component_get(entity, component_name) {
	if (component_name == all) {
		return entity.__components;
	} else {
		return entity.__components[$ component_name];
	}
}

function component_remove(entity, component_name) {
	if (component_name == all) {
		var _names = variable_struct_get_names(entity.__components);
		for (var i = 0; i < array_length(_names); i++) {
			asset_remove_tags(entity, script_get_name(_names[i]), asset_object);
			// SEND MESSAGE TO SYSTEMS THAT THIS ENTITY HAS CHANGED
			broadcast_channel(_names[i], CH_ENTITY_REMOVE, entity);
			delete entity.__components[$ _names[i]];
		}
		entity.__components = {};
	} else if (is_array(component_name)) {
		for (var i = 0; i < array_length(component_name); i++) {
			asset_remove_tags(entity, script_get_name(component_name[i]), asset_object);
			if (variable_struct_exists(entity.__components, component_name[i])) {
				// SEND MESSAGE TO SYSTEMS THAT THIS ENTITY HAS CHANGED
				broadcast_channel(component_name[i], CH_ENTITY_REMOVE, entity);
				delete entity.__components[$ component_name[i]];
			}
		}
	} else {
		asset_remove_tags(entity, script_get_name(component_name), asset_object);
		if (variable_struct_exists(entity.__components, component_name)) {
			// SEND MESSAGE TO SYSTEMS THAT THIS ENTITY HAS CHANGED
			broadcast_channel(component_name, CH_ENTITY_REMOVE, entity);
			delete entity.__components[$ component_name];
			variable_struct_remove(entity.__components, component_name);
		}
	}
}

function component_exists(entity, component) {
	if (variable_instance_exists(entity, "__components")) {
		return variable_struct_exists(entity.__components, component);
	} else {
		return false;
	}
}

function IComponent() constructor {
	debug = false;
}

So for instance if I wanted to add a HealthComponent onto an entity with a max health of 5, I would do component_add(id, new HealthComponent(5));. Every component you make will inherit from the IComponent constructor.

Systems & System Managers

I group related systems into system managers. For example a stats system manager called sys_stats might add the HealthSystem and StaminaSystem it's systems array to process them. That would look like this:

/// @description Init the system

// Inherit the parent system manager create event
event_inherited();

system_add([
	new HealthSystem([HealthComponent]),
	new StaminaSystem([StaminaComponent])
]);

There is also a corresponding system_destroy function to remove systems. The internals of those two functions are defined here:

/// @description Init
system_updater_receiver = new Receiver([CH_ENTITY_ADD, CH_ENTITY_REMOVE]);
systems = [];

/// @param system[s]
function system_add(system) {
	if (is_array(system)) {
		for (var i = 0; i < array_length(system); i++) {
			array_push(systems, system[i]);
			if (system[i].create != undefined) {
				system[i].create();
			}
		}
	} else {
		array_push(systems, system);
		if (system.create != undefined) {
			system.create();
		}
	}
}

/// @param system[s]/all
function system_destroy(system) {
	if (system == all) {
		for (var i = 0; i < array_length(systems); i++) {
			if (systems[i].destroy != undefined) {
				systems[i].destroy();
			}
			delete begin_step_systems[i];
		}
		array_resize(systems, 0);
	} else if (is_array(system)) {
		for (var i = 0; i < array_length(system); i++) {
			__system_destroy_find(system[i]);
		}
	} else {
		__system_destroy_find(system);
	}
}

__system_destroy_find = function(system) {
	// SEARCH BEGIN STEP SYSTEMS
	for (var i = array_length(systems) - 1; i >= 0; i--) {
		if (systems[i].name == system) {
			if (systems[i].destroy != undefined) {
				systems[i].destroy();
			}
			delete systems[i];
			array_delete(systems, i, 1);
		}
	}
}
Note: Receiver is the way signals get sent and received. That is found here, though feel free to use your own messaging framework: babaganosch/NotificationSystem: Lightweight framework for signals in GameMaker Studio 2.3+ (github.com)

All systems will all inherit from the ISystem constructor. Systems pass in an array of component requirements. So basically when you add a component to an entity, it'll send a message to all systems that might care for that specific component. If the entity has all the required components that the system cares about, the system will add the entity into it's entities array. For example, if we add a HealthComponent onto our player entity, it'll send a message to sys_stats which will call a corresponding function on our HealthSystem and if all the requirements are met, it'll add the player to its list of entities to process.

The ISystem constructor looks like this:

function ISystem(requirements) constructor {
	self.__requirements = requirements;
	self.__pausable = true;
	
	self.name = instanceof(self);
	self.entities = [];
	self.entity_count = 0;
	self.manager = other.id;
	
	debug = false;
	
	if (requirements != undefined) {
		// SET UP UPDATE MESSAGES
		if (is_array(requirements)) {
			for (var i = 0; i < array_length(requirements); ++i) {
				manager.system_updater_receiver.on(requirements[i], CH_ENTITY_ADD, function(entity) {
					updateEntities(entity, CH_ENTITY_ADD);
				});
				manager.system_updater_receiver.on(requirements[i], CH_ENTITY_REMOVE, function(entity) {
					updateEntities(entity, CH_ENTITY_REMOVE);
				});
			
			}
		} else {
			manager.system_updater_receiver.on(requirements, CH_ENTITY_ADD, function(entity) {
				updateEntities(entity, CH_ENTITY_ADD);
			});
			manager.system_updater_receiver.on(requirements, CH_ENTITY_REMOVE, function(entity) {
				updateEntities(entity, CH_ENTITY_REMOVE);
			});
		}
	}
	
	static create = undefined;
	
	static beginStep = undefined;
	
	static step = undefined;
	
	static endStep = undefined;
	
	static drawBegin = undefined;
	
	static draw = undefined;
	
	static drawEnd = undefined;
	
	static drawGUI = undefined;
	
	static roomStart = undefined;
	
	static roomEnd = undefined;
	
	static destroy = undefined;
	
	static enterSystem = undefined;
	
	static exitSystem = undefined;
	
	static updateEntities = function(entity, operation) {
		switch (operation) {
			case CH_ENTITY_ADD:
				if (__requirements_met(entity) and !array_contains(entities, entity)) {
					array_push(entities, entity);
					entity_count = array_length(entities);
					if (enterSystem != undefined) {
						enterSystem(entity);
					}
				}
				break;
			case CH_ENTITY_REMOVE:
				var _index = array_find(entities, entity);
				if (_index != -1) {
					array_delete(entities, _index, 1);
					entity_count = array_length(entities);
					if (exitSystem != undefined) {
						exitSystem(entity);
					}
				}
				break;
		}
	}
	
	static pausable = function(value) {
		__pausable = value;
	}
	
	static is_pausable = function() {
		return __pausable;
	}
	
	static __requirements_met = function(entity) {
		var _has_components = true;
		
		if (is_array(__requirements)) {
			for (var i = 0; i < array_length(__requirements); ++i) {
				var _comp_check = entity.__components[$ __requirements[i]];
				_has_components = _has_components and !is_undefined(_comp_check);
			}
		} else {
			var _comp_check = entity.__components[$ __requirements];
			_has_components = !is_undefined(_comp_check);
		}
		return _has_components;
	}
}

You can see that a lot of common GM events are functions. System Managers have code on each of those events to check if they are defined, and if they are to execute those functions. That looks something like this:

for (var i = 0; i < array_length(systems); i++) {
	var _system = systems[i];
	if (global.pause and _system.is_pausable()) { continue; }
	
	if (_system.step != undefined) {
		_system.step();
	}
}

Worlds

World objects just defined what systems are currently active and spawned. You don't need the same systems to be active on the main menu as you have when you are playing the game. So you would create a world_main_menu and world_overworld, and those would spawn different system managers depending on what was needed. For example, in my world_overworld's create event, I have:

/// @description Init World
/// This world is used for general overworld gameplay

instance_create_layer(0, 0, "Systems", sys_state_machine);
instance_create_layer(0, 0, "Systems", sys_ai);
instance_create_layer(0, 0, "Systems", sys_effects);
instance_create_layer(0, 0, "Systems", sys_layers);
instance_create_layer(0, 0, "Systems", sys_animation);
instance_create_layer(0, 0, "Systems", sys_player_controller);
instance_create_layer(0, 0, "Systems", sys_stats);
instance_create_layer(0, 0, "Systems", sys_utilities);
instance_create_layer(0, 0, "Systems", sys_camera);
instance_create_layer(0, 0, "Systems", sys_combat);
instance_create_layer(0, 0, "Systems", sys_shadows);
instance_create_layer(0, 0, "Systems", sys_movement);
This is an incomplete list and a WIP. This just shows the types of systems you might have for general gameplay.

End

If you have any questions, feel free to message me on Twitter or Discord (Faulty#1456).