escalator
2020-04-27
An experimental object system for high-performance games.
Upstream URL
Author
License
Escalator
Escalator, aka Entity System for Common Lisp (ESCL), is an experimental object system intended to make it easy to develop high-performance games. ESCL was inspired by this blog post, which outlines the purpose and structure of entity systems and suggests implementations for C++ and SQL. ESCL attempts provide an idiomatic implementation of entity systems for Common Lisp as a high-performance alternative to CLOS.
ESCL is still very experimental, and is not ready for production use. I have not spent much time optimizing it, although it is my hope that it will eventually perform much better than CLOS. My initial micro-benchmarks indicate that ESCL may be up to 50% faster than CLOS for certain kinds of operations. (Note that micro-benchmarks in general are problematic, and mine specifically are probably not very representative, so please take these numbers with a grain of salt.)
Obtaining
For the moment, ESCL isn't stable enough for official releases, so please clone the repository (see above) or download a tarball of master to get the source code. ESCL currently depends on iterate, so be sure to install that too.
Usage
In ESCL, entity systems are made of three types of constructs: components, systems, and entities. While there are some parallels between these constructs and those provided by CLOS and traditional object systems, the differences are fundamental enough that it might help to avoid thinking about entity systems in the context of existing object systems.
Components
(defcomponent component-name (&rest dependencies) (&rest fields))
component-name
is a symbol which specifies the name of the component.dependencies
is a list of names of components upon which this component depends (see discussion below).fields
is a list of symbols which specify fields in this component. Fields should not be defined multiple times in different components: ESCL does not combine fields the way CLOS automatically combines slots. For each field,defcomponent
automatically defines accessor macros under the same names which can be used to access the fields.- Returns
nil
. Macro has compile-time side effects.
Systems
(defsystem component-name (entity-var component-var &rest dependency-vars) &body body)
component-name
is a symbol which specifies the component for which this system is being defined.entity-var
will be bound to each entity which has the component namedcomponent-name
.component-var
will be bound to the component data for each entity. The macros defined for the fields above can be used on this variable to access the individual fields for this entity.dependency-vars
will be bound to the dependencies for each entity. If a dependency is only a computation dependency and not a data dependency, that variable can be madenil
.body
is a block of code which will be executed for each entity in the system. (See discussion on the system loop below.)- Returns
nil
. Macro has compile-time side effects.
Entities
(make-entity prototype components &rest initargs)
prototype
is used as the initial basis for this entity. Each component associated withprototype
is copied into the new entity before adding new components incomponents
. Usenil
to specify no prototype.components
is a list of new components to add to this entity.initargs
is a plist of values used to initialize fields, similar to the CLOSinitargs
parameter tomake-instance
. Each field is automatically defined to have a keyword initarg by the same name.- Returns the uuid of entity defined. Data for each component is kept in a global table which is not accessible to the user outside the system loop.
System Loop
(system-loop)
- Returns
nil
. Macro inserts the combined code for add systems which have been defined. The order which systems execute is determined by constructing a total ordering of the components based on their dependencies. (Dependencies execute before systems that depend on them.) Each system iterates over all entities which have the associated component. This is macro is intended to be used in constructing the main loop of the application.
Example
(defcomponent point () (x y z)) (defcomponent velocity (point) (vx vy vz)) (defsystem point (entity point) (format t "entity ~a at position (~a, ~a, ~a)~%" entity (x point) (y point) (z point))) (defsystem velocity (entity velocity point) (incf (x point) (vx velocity)) (incf (y point) (vy velocity)) (incf (z point) (vz velocity))) (make-entity nil '(point) :x 1 :y 2 :z 3) (make-entity nil '(point velocity) :x 4 :y 5 :z 6 :vx -1 :vy -2 :vz -3) (loop repeat 10 do (system-loop))
Output:
entity 1 at position (1, 2, 3)
entity 2 at position (4, 5, 6)
entity 1 at position (1, 2, 3)
entity 2 at position (3, 3, 3)
entity 1 at position (1, 2, 3)
entity 2 at position (2, 1, 0)
entity 1 at position (1, 2, 3)
entity 2 at position (1, -1, -3)
entity 1 at position (1, 2, 3)
entity 2 at position (0, -3, -6)
entity 1 at position (1, 2, 3)
entity 2 at position (-1, -5, -9)
entity 1 at position (1, 2, 3)
entity 2 at position (-2, -7, -12)
entity 1 at position (1, 2, 3)
entity 2 at position (-3, -9, -15)
entity 1 at position (1, 2, 3)
entity 2 at position (-4, -11, -18)
entity 1 at position (1, 2, 3)
entity 2 at position (-5, -13, -21)