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.)
For the moment, ESCL isn't stable enough for official releases, so please clone the repository (see above) or download a tarball of tip to get the source code. ESCL currently depends on iterate, so be sure to install that too.
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.
(defcomponent component-name (&rest dependencies) (&rest fields))
component-nameis a symbol which specifies the name of the component.
dependenciesis a list of names of components upon which this component depends (see discussion below).
fieldsis 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,
defcomponentautomatically defines accessor macros under the same names which can be used to access the fields.
nil. Macro has compile-time side effects.
(defsystem component-name (entity-var component-var &rest dependency-vars) &body body)
component-nameis a symbol which specifies the component for which this system is being defined.
entity-varwill be bound to each entity which has the component named
component-varwill 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-varswill 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 made
bodyis a block of code which will be executed for each entity in the system. (See discussion on the system loop below.)
nil. Macro has compile-time side effects.
(make-entity prototype components &rest initargs)
prototypeis used as the initial basis for this entity. Each component associated with
prototypeis copied into the new entity before adding new components in
nilto specify no prototype.
componentsis a list of new components to add to this entity.
initargsis a plist of values used to initialize fields, similar to the CLOS
make-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.
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.
(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))
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)