Clone wiki

escalator / Home

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 tip 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 named component-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 made nil.
  • 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 with prototype is copied into the new entity before adding new components in components. Use nil 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 CLOS initargs parameter to 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.

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)

Updated