muhuk's blog

Nature, to Be Commanded, Must Be Obeyed

November 09, 2014

Making clecs Less Suicide-Inducing

After releasing the first major version of clecs I have started writing a tetris clone on it. I wanted to see if it all made sense. Sadly it wasn’t as much fun as I had intended it to be. This is a post about the pain points I discovered and how I intend to fix them.

The First Mistake in the Book

I made the first mistake in the book; I based components initially on records instead of using plain old maps. My rationale was to take advantage of record’s type hints and because they were AOT compiled. But clojure is a dynamic language and type hints are just that, hints. Also even though records are better optimized than maps, we never hold onto many components. The fact that atom-world keeps them all in memory is just an implementation detail. More on this later.

Here is what I needed to do in order to use a component:

(ns my-game.some-system
  (:require [my-game.component :refer [->MyComponent]])
  (:import [my_game.component MyComponent]))

Because I need ->MyComponent to make an instance and I need to import class MyComponent so that I can use it to query by component:

;; Making an instance:
(clecs.world/set-component world (->MyComponent eid some-value other-value))

;; Querying by components:
(doseq [eid (clecs.world/query world (clecs.query/all MyComponent))]
  (clecs.world/remove-component eid MyComponent))

I think this is a lot simpler and doesn’t require anything to be required/imported:

;; Making an instance:
(clecs.world/set-component world
                           eid
                           :my-component
                           :my-key some-value
                           :other-key other-value)

;; Querying by components:
(doseq [eid (clecs.world/query world (clecs.query/all :my-component))]
  (clecs.world/remove-component eid :my-component))

;; This syntax is not final.

Record-based components also force you to define your components beforehand. It is possible to define them run-time but it’s not very straightforward. This makes it hard to build interactive development tools on top of clecs.

Speaking of components being defined run-time, I should mention that they will have to be registered with the world. And even though you will be able to use plain old maps, your components’ contents will be type checked when you pass them to set-component. When I set out to design clecs, my goal was to create an interface, not a concrete implementation. A world can keep all it’s data in-memory, or it can store everything in a database, or it can dump it to a message queue and not care about what’s on the other end. Your game should run the same regardless of your world backend.

The only backend currently shipped with clecs is atom-world, which stores its data in an atom. If an atom will do, you can write a cleaner, better entity-component-system on top in a couple of hours. You don’t need clecs. The whole point of clecs is to support a wide variety of backends. You don’t need to register your components or type annotate them to an atom-world, but if your world will be creating tables in a relational database you are better off taking advantage of that metadata.

Systems Need to Put on Some Muscle

The biggest issue I had was importing records and requiring their constructors. This would mean 4 statements for each component, 2 for code and 2 for tests. The second biggest issue that was taking away the fun was calling transaction! within each of my systems.

Initially I thought one could check if a system needs to run via the world that implements IEditableWorld and ITransactableWorld and call transaction! only when necessary. It was supposed to allow more systems to be run in parallel. But then I’ve figured this should-i-run test had to be run again within transaction since we can’t guarantee some other system didn’t come and change game state before our transaction completed. So this second context will go away and systems will always be run within a transaction.

I mentioned above that components will be registered with the world. Similarly systems will declare components they are interested in and only those components will be available within that transaction. If a system declared a component for read-only access, it won’t be able to write to that component. I was planning to parallelize systems based on run-time analysis, but explicit declarations will make it unnecessary.

So a system would look something like this:

{:name :some-system
 :reads [:component-a :component-b]
 ;; It is implied :component-c can be read.
 :writes [:component-c]
 ;; Runs every 1000 milliseconds.
 :runs-every 1000
 :main (fn [w dt] ...)}

;; This syntax is not final.

Where :main would be a function that is run within a transaction. But the system itself would be a data structure.

Other Plans

Hopefully the changes I mentioned above will make game development with clecs more fun. I still haven’t created any tutorials or detailed documentation. For now example projects should be helpful for getting started.

I would also like to add a concept of named entities. For example if you have a player entity, it should be possible to refer it with a name instead of querying its entity id every time.

Another thing I am considering is to enhance the functionality of queries. Currently you can only build queries based on components and you get a sequence of entity ids in return. It would make sense to return a set of components along with entity ids. Or perhaps it would be nice to be able to query using values of components. This would be especially useful if you had big worlds. Database indexes for example could be exploited with this.

Have you had the chance to play with clecs?

If you have any questions, suggestions or corrections feel free to drop me a line.