HooECS is an Entity Component System framework for game development with lua. Originally written for the LÖVE 2D game engine, it is actually compatible with pretty much any game that uses lua! It is inspired by Richard Lords Introduction to ECS. If you don't have any idea what this entity component stuff is all about, click that link and give it a read! It's totally worth it! The original software was written by Arne Beer and Rafael Epplee under the name lovetoys. For the foreseeable future. All lovetoys projects will stay 100% compatible with HooECS. The unit tests of lovetoys are merely expanded upon. No core behavior is changed!
HooECS is a full-featured game development framework, not only providing the core parts like Entity, Component and System classes but also a Publish-Subscribe messaging system as well as a Scene Graph, enabling you to build even complex games easily and in a structured way.
Though I have not reached version 1.0 yet, the software is well-tested and considered stable. If you happen to find any bugs, please create an issue and report them. Or, if you're feeling adventurous, create a pull request :)
The recommended way of installing HooECS is by creating a submodule and cloning it right into your git repo.
To require HooECS and initialize it with the default options, use the following:
local HooECS = require('HooECS')
HooECS.initialize()
For an example on how to integrate the HooECS with love2d, have a look at the lovetoys example example repository.
After requiring, configure HooECS by passing a configuration table to the initialize
function.
For example, if you want debug output, pass the option debug = true
:
HooECS.initialize({
debug = true
})
The following table lists all available options:
Name | Type | Default | Meaning |
---|---|---|---|
debug | boolean | false | Makes HooECS print warnings and notifications to stdout. |
globals | boolean | false | If true, HooECS will make all its classes available via the global namespace. (e.g. Entity instead of HooECS.Entity) |
Note: Once you've called initialize
, the configuration will be the same every time you require('HooECS')
.
HooECS primarily consists of a few classes that are implemented using middleclass. By default, they are available via the HooECS object:
local HooECS = require('HooECS')
-- Example: using constructors
local entity = HooECS.Entity()
local system = HooECS.System()
local engine = HooECS.Engine()
local component = HooECS.Component()
local eventManager = HooECS.EventManager()
-- the middleclass `class` object
local class = HooECS.class ()
Sidenote: All new functionality in HooECS is labeled as HooECS specific.
The Entity is the basic building block of your game. You can loosely think of it as an object in your game, such as a player or a wall. From the technical side, it basically represents a collection of components.
- parent (Entity) - Parent entity
This function returns a new instance of an entity. If you specify a parent entity, you can later access it via the .parent
property. Also, the parent entity will get a reference to its newly created child, accessible by its .children
table.
Note: If you add an entity without a parent to the engine, the engine will set the root entity as its parent.
This is a reference to the eventManager of the engine this entity has been added to. Therefore there will be no eventManager until the entity is added to an engine.
- parent (Entity) - Parent entity
Use this method to set a new parent on an entity. It will take care of removing itself from the children of its previous parent and registering as a child of the new parent.
Returns the entities parent.
- component - (Component) Instance of a component.
Adds a component to this particular entity.
HooECS specific
Returns a reference to the entity to allow for the following syntactic suggar:
entity:add(component1)
:add(component2)
:add(component3)
- components - (List) A list containing instances of components.
Adds multiple components to the entity at once.
- component - (Component) Instance of a component.
Adds the component to this particular entity. If there already exists a component of this type the previous component will be overwritten.
- name (String) - Name of the component class
Removes a component from this particular entity.
HooECS specific
Returns a reference to the entity to allow for the following syntactic suggar:
entity:remove(component1)
:remove(component2)
:remove(component3)
- name (String) - Name of the component class
Returns boolean depending on if the component is contained by the entity.
- name (String) - Name of the component class
Returns the component or nil
if the Entity has no component with the given name
.
Returns the list that contains all components.
- components (List) - A list containing instances of components.
Returns whether the entity is active or not.
An inactive entity will still be part of the engine, but it will be removed from all systems and won't be updated or drawn. Activation will add the entity to all appropriate systems.
If not already active, adds the entity to all appropriate systems. The entity needs to belong to an engine for this to work.
If active, removes the entity from all systems without removing the entity from the engine.
Returns the engine this entity belongs to.
Returns the children this entity has or nil if there are none.
- componentList (List) - A table containing components
Returns a new entity with a deep copy of all components. This means there will be a copy of all components and all values will be the same.
If a componentList is supplied, all components contained will be set (overwrite components with the same name).
- componentList (List) - A table containing components
Returns a new entity with a shallow copy of all components. This means modifying the component of one of the two entities will cause modifications to both.
If a componentList is supplied, all components contained will be set (overwrite components with the same name).
Overwrite this function to get an update callback before any system is updated. Useful for modifying component data before system processing. This function has to be set before the entity is added to the engine. Otherwise use Entity:setUpdate() instead.
- newUpdateFunction (Function) - The new function that should be called on update.
Provide an update function to the entity and add it to the engine entity update list if already added to an engine. If called without parameter, the current update function is removed and the entity is removed from the engine entity update list.
Collection of functions for creating and working with Component classes. There is no Component
class; As components are only 'data bags', you can just use standard middleclass classes to implement your components.
local Color = class('Color');
function Color:initialize(r, g, b)
self.r = r
self.g = g
self.b = b
end
The Component.create()
function can automatically generate simple component classes like these for you.
- path A path in a format accepted by require()
Register the component for loading it conveniently with Component.load.
- components A list containing component names
Load the specified components.
local Color, Transform, Drawable = Component.load({"Color", "Transform", "Drawable"})
-- Create a component for the color black
Color(0, 0, 0)
- fields (Table) - A list of Strings specifying the property names of the new component. The constructor of the component class will accept each of these properties as arguments, in the order they appear in the
fields
list. - defaults (Table) - Key value pairs where each pair describes the default value for the property named like the pairs key.
Create a new component class.
-- Create a Color component with the default color set to blue
local Color = Component.create("Color",
{"r", "g", "b"},
{r = 0, g = 0, b = 255})
-- Create a component for the color violet
Color(255)
Overwrite this method in your component if you want to react when the component is added to the entity.
Overwrite this method in your component if you want to react when the component is removed from it's entity.
Systems provide the functionality for your game. The engine manages all Systems and assigns all entities with the right components to them.
All your systems have to be derived from the System
class. An example how to do this can be found below.
There are two types of Systems: "update" and "draw" Systems. Update systems perform logic operations, like moving a player and updating physics. Their update
method is called by the engine.
Draw systems are responsible for rendering your game world on screen. Their draw
method is also called by the engine.
To implement functionality in your game, you create custom Systems. You inherit from the System
class and override some methods to specify your System's behavior.
To create your own system, you use the class
function provided by MiddleClass. The first argument is the name of your new System, the second is the Class you want to inherit from. The specific methods you can override are specified below.
local CustomSystem = class("CustomSystem", System)
function CustomSystem:initialize(parameter)
System.initialize(self)
self.parameter = parameter
end
function CustomSystem:update(dt)
for key, entity in pairs(self.targets) do
local foo = entity:get("Component1").foo
entity:get("Component2").bar = foo
end
end
function CustomSystem:requires()
return {"Component1", "Component2"}
end
return CustomSystem
- name (String) - The name of the system
Note: Shorthand for class(name, System)
This function defines what kind of entities shall be managed by this System. The function has to be overwritten in every System or it won't get any entities! The strings inside the returned table define the components a entity has to contain, to be managed by the System. Those entities are accessible in self.targets
.
If you want to manage different kinds of entities just return a table that looks like this:
return {name1 = {"Componentname1", "Componentname2"}, name2 = {"Componentname3", "Componentname4"}}
The different entities are now accessible under system.targets.name1
and system.targets.name2
.
An entity can be contained by the same system multiple times in different target pools if it matches the varying component constellations.
- dt (Number) - The time passed since the last update, in seconds.
This function is going to be called by the engine every tick.
This method is going to be called by the engine every draw.
- entity (Entity) - The entity added
Overwrite this method in your system if you want to react when new entities are added to it.
- entity (Entity) - The entity removed
Overwrite this method in your system if you want to react when an entity is removed from it.
The engine is the most important part of the HooECS and the most frequently used interface. It manages all entities, systems and their requirements, for you.
Creates a new engine object. Every engine creates a rootEntity which becomes parent of all entities that don't have a parent yet.
Returns the root entity. Modify it to your needs!
- system (System) - Instance of the system to be added
- type (String) - optional; Should be either "draw" or "update"
This function registers the system in the engine. The systems' functions will be called in the order they've been added. As long as the system implements either the update
or the draw
function, HooECS will guess the type
parameter for you.
Note: If a system implements both draw
and update
functions, you will need to specify the type
and add the system twice, once to draw and once to update. Otherwise HooECS couldn't know in what order to execute the update
and draw
methods.
- system (String) - the name of the system
If you want a system to stop, just call this function. It's draw/update function won't be called anymore until you start it again.
- system (String) the name of the system
Call this to start a stopped system.
- system (String) the name of the system
If the system is running, stop it. If it is stopped, start it.
- entity (Entity) - Instance of the Entity to be added
Adds an entity to the engine and registers it with all systems that are interested in its component constellation.
- entity - (Entity) - Instance of the Entity to be removed
- removeChildren - (Boolean) Default is false
- newParent - (Entity) - Instance of another entity, which should become the new Parent
Removes the particular entity instance from the engine and all systems.
Depending on removeChildren
all Children are going to deleted recursively as well.
If there is a new Parent defined, this entity becomes the new Parent of all children, otherwise they become children of engine.rootEntity
.
- component (String) - Class name of the component
Returns a list with all entities that contain this particular component.
- component (Number) - Returns the count of entities that contain this particular component.
- dt (Number) - Time since the last update, in seconds
Updates all logic systems.
Updates all draw systems.
For a more detailed and commented version with collisions and some other examples check the main.lua file of the lovetoys example game.
-- Importing HooECS
require("lib.HooECS")
function love.load()
engine = Engine()
world = love.physics.newWorld(0, 9.81*80, true)
world:setCallbacks(beginContact, endContact)
eventmanager = EventManager()
end
function love.update(dt)
-- Engine update function
engine:update(dt)
world:update(dt)
end
function love.draw()
-- Engine draw function
engine:draw()
end
function love.keypressed(key, isrepeat)
eventmanager:fireEvent(KeyPressed(key, isrepeat))
end
function love.mousepressed(x, y, button)
eventmanager:fireEvent(MousePressed(x, y, button))
end
This class is a simple eventmanager for sending events to their respective listeners.
- eventName (String) - Name of the event-class to be added
- listener (Listener) - The first entry is the table/class that will is supposed to be passed as
self
to the called function. - listenerFunction (Function) - The function that should be called.
Adds a function that is listening to the Event.
An example for adding a Listener: EventManager:addListener("EventName", listener, listener.func)
. The resulting function call will be func(table, event)
.
We need to do this so we can work with self
as we are used to, as lua doesn't provide a native class implementation.
- eventName (String) - Name of the event-class
- listener (String) - Name of the listener to be deleted
Removes a listener from this particular Event.
- event (Event) - Instance of the event
This function pipes the event through to every listener that is registered to the class-name of the event and triggers.
HooECS specific
Events can return data which will be stored in an array table. Each entry will be the return value(s) from a specific listener. Useful to get feedback on whether the event was used or generally return data. Returns nil if no data was returned by the listeners.
returns = listener:fireEvent(event)
for _, dataEntry in ipairs(returns) do
-- Handle return values
end
You can find the tests in the spec
folder. They are defined using the busted test framework.
To run the suite, install busted and simply execute busted
in the HooECS directory.
Base Framework Copyright © 2016 Arne Beer (@Nukesor) and Rafael Epplée (@raffomania)
Further development Copyright © 2017 Hoodini (@Hoodini)
This Software is published under the MIT License.
For further information check LICENSE.md
.