Beast aims to be a thin layer over CLOS, and so has a fairly small user-facing API.
Aspects are facets/traits of your game objects that you want to model. Some examples could be things like: location, moveable, visible, edible, sentient.
To define an aspect you use
(define-aspect location x y) (define-aspect edible nutrition-value)
define-aspect takes the name of the aspect and zero or more slot definitions.
The names of aspect slots will be have the aspect name prepended to them with
a slash to avoid clashing between aspects, and the
will be added for you. So for example, this:
(define-aspect inspectable text) (define-aspect readable text)
Would macroexpand into something roughly like:
(defclass inspectable () ((inspectable/text :initarg :inspectable/text :accessor inspectable/text))) (defclass readable () ((readable/text :initarg :readable/text :accessor readable/text)))
You can include extra slot options when defining an aspect's slots, and they'll
be passed along to the
defclass. This is especially handy for
(define-aspect container (contents :initform nil)) (define-aspect throwable (accuracy :type single-float) (damage :type integer))
In the end it's just CLOS though, so if you want to add some
:allocation :class then go nuts!
When you define an aspect named
foo Beast also defines a
foo? predicate that
(typep object 'foo), which comes in handy when using higher-order
(defun whats-for-dinner? () (remove-if-not #'edible? (container/contents *fridge*)))
Once you've got some aspects you'll want to define some entity classes that mix them together.
You define entity classes using
(define-entity dart (throwable)) (define-entity bread (edible)) (define-entity pie (edible throwable)) (define-entity icebox (container))
The resulting classes will inherit from
entity and each of the aspects (in
order). This example would expand (roughly) into:
(defclass dart (entity throwable) ()) (defun dart? (object) (typep object 'dart)) (defclass bread (entity edible) ()) (defun bread? (object) (typep object 'bread)) (defclass pie (entity edible throwable) ()) (defun pie? (object) (typep object 'pie)) (defclass icebox (entity container) ()) (defun icebox? (object) (typep object 'icebox))
You can also specify slot definitions at the entity level, and they'll be passed
(define-entity cheese (edible) (variety :type (member :swiss :cheddar :feta) :initarg :variety :reader :cheese-variety))
Note that slot definitions on entities are passed along raw, without the name-mangling or default-slot-option-adding that's done for aspects. This may change in the future.
After you've defined your entity classes you can can create some entities using
(defparameter *my-fridge* (create-entity 'icebox)) (dotimes (i 30) (push (create-entity 'cheese :edible/nutrition-value 10 :variety (nth (random 3) '(:swiss :cheddar :feta))) (container/contents *my-fridge*)))
create-entity is a thin wrapper around
make-instance that handles some extra
bookkeeping. When you create an entity, Beast will keep track of it in a global
index. We'll see the reason for this in the next section.
To destroy an entity (i.e. remove it from Beast's index) you can use
(destroy-entity the-entity). You can wipe the slate clean and remove all
entities at once with
Beast also defines two generic functions called
entity-destroyed which don't do anything by default, but are there for you to
add methods on if you want. For example:
(define-aspect location x y) (defvar *world* (make-array (100 100) :initial-element nil)) (defmethod entity-created :after ((e location)) (push e (aref *world* (location/x e) (location/y e)))) (defmethod entity-destroyed :after ((e location)) (with-slots ((x location/x) (y location/y)) e (setf (aref *world* x y) (delete e (aref *world* x y)))))
Beast's aspects and entities are just very thin sugar over CLOS, but systems provide extra functionality that comes in handy when writing games.
A system is essentially a function that takes an entity as an argument with zero or more aspects as type specifiers. When you run a system the function will be run on every entity that meet the requirements. For example:
; No specifiers, this just runs on every entity. (define-system log-all-entities (entity) (print entity)) ; Runs on entities with the lifetime aspect. (define-system age ((entity lifetime)) (when (> (incf (lifetime/age entity)) (lifetime/lifespan entity)) (destroy-entity entity))) ; Run on entities with both the visible and location aspects. (define-system render ((entity visible location)) (draw entity (location/x entity) (location/y entity) (visible/color entity)))
Systems with more than one argument are currently supported, but should be considered experimental. The API may change in the future.
; Run on all PAIRS of entities that have the appropriate aspects. (define-system detect-collisions ((e1 location collidable) (e2 location collidable)) ; ... )
define-system defines a function with the same name as the system, and
run-... function that will do the actual running for you:
(define-system log-all-entities (entity) (print entity)) (run-log-all-entities)
You should always use the
run-... function, but the other one can be handy to
have around for tracing/debugging/disassembling purposes.
That's most of Beast in a nutshell. If you've gotten this far you can dive in and make something, or take a look at the API Reference.
Beast also does some stuff not discussed here like caching entities by aspect/system and type-hinting system functions. If you're curious about how it works you can read the source.