Skip to content

Instantly share code, notes, and snippets.

@HungryProton
Last active September 14, 2020 09:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HungryProton/1f386ea42425cedc9ee82c203a48cf7e to your computer and use it in GitHub Desktop.
Save HungryProton/1f386ea42425cedc9ee82c203a48cf7e to your computer and use it in GitHub Desktop.
ECS protoype
"""
This is the base Component class. Every component should inherit from this.
It only stores data. Its only responsibility is to notify the parent Entity when its
created. (Or we could setup something in the Entity to monitor new components
but I just find it easier this way)
"""
extends Node
class_name Component
func _ready() -> void:
get_parent().register_component(self)
set_process(false) # Not sure if it's needed if _process is not defined
"""
The main class. Don't inherit from this, just attach it to a node and add components as child nodes.
"""
extends Spatial
class_name Entity
signal layout_updated
# We store all the components here for easy access
# {component_type: component_node}
var components := {}
func _ready() -> void:
SystemManager.register_new_entity(self) # Tell the systems this entity exists
func register_component(object: Component) -> void:
if not object is Component:
print_debug("Error: ", object, " is not a component.")
return
var type = object.get_script()
if components.has(type):
print_debug("Error: ", self.name, " has multiple components ", type)
return
components[type] = object
emit_signal("layout_updated") # Systems listen to that event
func deregister_component(object: Component) -> void:
var type = object.get_script()
if components.has(type):
components.erase(type)
object.queue_free()
emit_signal("layout_updated")
# Check if the entity has a specific component
func has(type) -> bool:
return components.has(type)
# Check if the entity has all the components in that list
func has_all(list: Array) -> bool:
return components.has_all(list)
"""
Inherit your own systems from this class
"""
extends Object
class_name System
# true if the system have to run each frame. Set to false if it only do stuff to initialize entities when they first appear.
var process := true
# All the relevant entities are stored here. This array is updated when an entity gets or lose components
var entities := []
func initialize() -> void:
pass
# Override this in your system to return the list of components relevant to this systems
func get_required_components() -> Array:
return []
"""
Called when a new entity is created. This checks if the entity has the required components
and adds it to the pool if it does. When the components of that entity changes, it is
reevaluated again to make sure it is still valid.
"""
func reference_new_entity(entity: Entity) -> void:
if entity.has_all(get_required_components()):
entities.append(entity)
entity.connect("layout_updated", self, "_on_entity_layout_updated", [entity])
_on_new_entity_registered(entity)
"""
Loops over all entities and process them.
"""
func process(delta: float) -> void:
for e in entities:
execute(e, delta)
"""
Called for each matching entity. Override this in the inherited system.
"""
func execute(_entity: Entity, _delta: float) -> void:
pass
"""
Called when an entity looses a component. If it no longer have the required components, it
is removed from the local entity list.
"""
func _on_entity_layout_updated(entity) -> void:
if not entity.has(get_required_components()):
entity.disconnect("layout_updated", self, "_on_entity_layout_updated")
entities.erase(entity)
_on_entity_removed(entity)
"""
Called when a new entity is registered within the system. Override if needed.
"""
func _on_new_entity_registered(_entity: Entity) -> void:
pass
"""
Called when an entity no longer have the required components to be processed by this system. Override if needed
"""
func _on_entity_removed(_entity: Entity) -> void:
pass
"""
Define all your systems here. The array order defines the order of execution.
"""
extends Reference
class_name SystemsList
var list = [
InputController,
MotionTiledSystem,
]
"""
The system manager creates all the systems and call their process function.
Make it an autoload called SystemManager
"""
extends Node
var all := [] # Every systems created
var process := [] # Systems that are processed every frame
func _ready() -> void:
_instantiate_all_systems()
func _process(delta: float) -> void:
for s in process:
s.process(delta)
# Called from a newly created Entity. Ask all the systems if this new entity is relevant to them
func register_new_entity(entity) -> void:
for s in all:
s.reference_new_entity(entity)
"""
Instantiate all systems defined in the system list, in order.
"""
func _instantiate_all_systems() -> void:
var systems := SystemsList.new()
for system_type in systems.list:
var new_system = system_type.new()
if not new_system is System:
continue
new_system.initialize()
all.append(new_system)
if new_system.process:
process.append(new_system)
"""
A example of system. This one handle the motion on a grid in a turn based roguelike game.
"""
extends System # Don't forget to extend from system!
class_name MotionTiledSystem
# Define which components are required for this system to work. This one needs the entities
# to have 4 components.
func get_required_components() -> Array:
return [ActorTiled, MotionTiled, Commands, CharacterState]
# Automatically called every frame for each relevant entity
func execute(entity: Entity, delta: float) -> void:
# You can call get_component() safely because you know these entities have the components you
# listed in get_required_components()
var commands: Commands = entity.components[Commands]
var move = commands.actions["move"]
if move == Vector3.ZERO:
return # Character didn't try to move, abort
var actor: ActorTiled = entity.components[ActorTiled]
var motion: MotionTiled = entity.components[MotionTiled]
# That's a custom class that holds data about the tiled world
# Dimensions, layout, which entity is on which tile and so on
var world: WorldTiled = WorldManager.get_world(actor.world_id)
# Calculate the new position based on data from the components
var new_position = actor.position + (move * motion.tiles_travel)
# Perform logic only if the character lands on a valid tile
if world.is_position_valid(new_position, actor):
motion.set_path(actor.position, new_position)
world.set_actor_position(new_position, actor)
var state: CharacterState = entity.get_component("CharacterState")
state.state = CharacterState.State.MOVE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment