-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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:
- Entities hold components.
- Components only hold data, they don't run any logic. (Usually)
- Systems manipulate components and run most of the logic.
The general workflow for this is as follows:
- You add components onto entities.
- A signal gets sent to any systems that care about those components being added.
- If the entity has all the components the specific system cares about, the system adds it to its entities array.
- The system will process each entity in its entities array every step.
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.
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: https://github.com/babaganosch/NotificationSystem
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. So when you write a new system and inherit ISystem
, you'll want to override those functions. System Managers have code on each of those events to check if they are not undefined, and if they aren't they 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();
}
}
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);
If you have any questions, feel free to message me on Twitter or Discord (Faulty#1456).