Home

Awesome

cljan

Cljan (pronounced "Cla-jan") is game-engine independent, purely functional entity component system inspired by paldepind's Kran.

If you know Kran, then Cljan will be familiar to you: they share the same conceptual underpinning. As in Kran, one constructs a universe of interacting entities by defining components which participate in systems.

Cljan provides a state-monadic framework for using these ideas in the context of Clojure's pure, functional environmet. Cljan is frame-work agnostic, allowing you to use it with the system of your choice.

Usage

We begin with a brief example, but if you are interested in the concepts underpinning the library, see below for an explanation of the state monad and of entity component systems.

Example

We'll now walk through defining a simple example.

We begin with defining our components. One always begins with components, because we build up systems by reference to what components constitute membership and we construct entities by what components they have.

Our world will simulate some crops, which have a growth stage and a growth rate. Each piece of data will be one component:

(defn init-components []
  (state-do 
   (component :growth-stage identity)
   (component :growth-rate identity)))

The function component creates a component in the Cljan universe. It takes two arguments: the first is the name of the system and the second is a constructor. Each of our components is just a single number, so our constructors are the identity function, but we may wish, in general, to perform calculations before returning the constructed value.

We now create our system:

(defn grow [growth-stage growth-rate ent]
  (set-ent-component ent :growth-stage (+ growth-rate growth-stage)))

(defn init-systems []
  (state-do
   (system :growing-things [:growth-stage :growth-rate] 
     {:every grow})))

This code means: "Create a system called growing things which operates on any entities which contain a growth stage and a growth rate. Every time this system is executed, update the growth stage by the growth rate."

Finally, we create some entities:

(defn init-entities []
  (state-repeat 10 
                (fn [i]
                  (state-do 
                   [:bind e (entity)]
                   (add-component e :growth-state 0)
                   (add-component e :growth-rate (rand-int
  10))))))

Here we create ten entities and give them random growth rates. All that remains is to define our init and update function:

(defn init []
  (state-do 
   (init-components)
   (init-systems)
   (init-entities)))

(defn update []
  (execute-system :growing-things))

Of course, eventually we must initialize and update our systems. Cljan provides a special form to run Cljan code on a fresh Cljan state and to update a cljan state with a state-function:

(def world (atom (run-cljan-for-state (init))))

(defn update-world! []
  (swap! world (fn [world]
                 (update-cljan world (update)))))

(loop [i 0]
  (if (< i 10) (do (update-world!) (recur (+ i 1))) @world))

Here we initialize our Cljan state inside an atom and update it ten times. We can then extract the growth stage of each entity and see how they have developed:

(map (fn [key] 
       (-> ((:entities @world) key) :components :growth-stage)) 
     (keys (:entities @world)))

; -> (0 140 140 120 40 180 20 120 40 160)

And that is the whole story!

Background

Concepts

To use Cljan effectively, it is important to digest three concepts:

They relate in the following way: Entities are containers for components, which are pieces of data like "hit points" or "position". Systems collect sets of entities when they have certain combinations of components.

When using Cljan, one first declares the components that will be used in the Cljan-universe. Then one declares the systems that exist in terms of the components.

Finally, before and during the game, the programmer creates entities. During the course of the game, components are added and removed from entities, which in turn causes entities to be part of, or to not be part of systems. The systems, for their part, define the behavior of the entities in the game.

The State Monad

Cljan is entirely pure and uses a custom state-monad to orchestrate the sequentially dependent operations that are at the heart of game logic.

If you don't understand monads, don't worry! Cljan is easy to use once you become comfortable with one special form, state-do, and one idea: "state functions".

A state function is a function which takes a single argument, the current state of the cljan universe, for instance, and returns a vector of two values. The second value is the new state, while the first value any ancillary result of the operation performed by the function.

For instance:

(defn attack [state]
  (let [hp (-> state :player :health)
        new-state (assoc-in state [:player :health] (- health 1))
        dead? (= 0 (-> state :player :health))]
     [dead? new-state]))

This function attacks a player entitiy held by the state and also returns a boolean indicating whether that player is dead.

state-do is a special form built for sequencing state-functions so that future-state functions can operate on the return values, the first element of the two-element vector, without explicitly passing around the state, for instance:

(state-do 
 [:bind dead? attack]
 (if dead? attack end-game))

A state-do expression produces a state function which you can execute by passing a state to.

The body of a state-do expression must be a sequence of expressions of a restricted type. The simplest case is a Clojure expression whose result is a state function. In this case, the meaning is to execute that state-function for its side effect but throw away "return" value, the first value in the pair returned by function.

The next case is an expression of the form [:bind pattern expr]. The expr must evaluate to a state function and the :bind for ensures that the variable bindings implied by the pattern are active in all forms which appear after the bind. In the above exampe, for instance, the variable dead? is bound to the first element of the vector returned by attack. Note that attack is a state function.

Easier to understand are forms of the type [:let pattern expr ...], which introduce regular bindings, not sequenced through the state monad.

And finally, one may say [:aside expr ...], where any expressions may be placed in the list of expressions. They are executed when the complete state function is called, and can refer to any bindings introduced above them, but do not side effect the state or bind any values for subsequent expressions.

state-do provides a concise way of sequencing and working with state functions, such that users of Cljan will almost never need to write state-functions out manually.

Curried functions serve to allow the programmer to parameterize state functions. Imagine, for instance, a parameterized attack function:

(defn attack [attack-power]
  (fn [state] 
    (let [hp (-> state :player :health)
        new-state (assoc-in state 
                    [:player :health] (- health attack-power))
        dead? (= 0 (-> state :player :health))]
     [dead? new-state])))

attack is now no longer a state function, but it returns one, and would be used like so:

(state-do 
 [:bind dead? (attack 5)]
 (if dead? attack end-game))

On the Implementation

The challenging aspect of porting Kran to Clojure is that Kran is quite side-effect heavy. For instance, during the update of an entity in a system, other entities may be added or removed from that or other systems.

In Kran this is handled by storing entities in stateful doubly linked lists, so that the Kran system can modify the entities in a system on the fly, even during traversal of all entities in a system.

In order to accomplish this in Cljan, the linked lists for every system needed to either be stored in a stateful way or placed entirely into the management of the state monad, which is what we decided to do.

Each entity in the state of the Cljan universe records, for every system it belongs to, the next entity in that system and the previous. All update functions live in the state monad, and therefor can trigger the addition or removal of entities from these linked lists. We preserve, in this way, the stateful semantics of Kran in a purely functional universe.

Cljan is still alpha software, but we plan to use it to develop our next game, so it should get a lot of exercise in the coming months.

License

Copyright © 2014 Vincent Toups and Eduardo Bellani

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.