Code

Download the code here!

It needs to be run on a Web server with PHP. Just unzip it in your Web server directory of choice.

Sections

CellSpace editor

Example games

CellSpace game engine

CellSpace is a game engine based on cellular automata (aka cellular spaces). It was inspired by Boulderdash and similar games, which have entities that are little more than tiles that animate by reacting to their immediate environment. The basic idea of CellSpace is that a level consists of a tile map, with a set of simple visually oriented rules that describe how tiles are changed when certain patterns of tiles occur. Everything is a tile, including the player and any other animated objects. The engine is optimised so that it handles large tile maps at high frame rates, and is suitable for action games with large scrolling playfields. With the help of animation directives, tile changes can be made to animate smoothly.

You can create maze games, platform games and shooters, which can include game mechanics like (simple) water physics, destructible environment, propagating fire, and much more.

A CellSpace game is specified in a programming language called CellScript. The CellSpace engine is based on HTML5 and WebGL, and will run on most web browsers. An online IDE is available for developing games easily. Check out the links on the right to edit/play the example games.

CellScript rules

A rule consists of a tile pattern that is changed into another tile pattern. For example, if there is a boulder with empty space below it, we want it to fall down. In CellSpace, you specify it like this:
rule: boulderfall . . .    . . .
                  . * .    . - .
                  . - .    . * .
A rule basically says:

If you see this => turn it into this.

Notice the 3x3 grid, which is the standard grid size for a CellSpace rule. Usually the object you want to manipulate is in the middle. The black squares indicate "ignore this cell", and the grey squares indicate empty space (which is just another type of tile in our game, like the boulder).

Right of the graphical representation of the rule, you see the corresponding CellScript statement. The "." indicates ignore, and "-" and "*" resp. indicate empty space and boulder. Linking characters to tile graphics is done using cell: statements (see next section).

Whenever the engine sees the 3x3 pattern on the left somewhere on the tile map, it applies (triggers) the rule. This results in the cells being overwritten by the cells on the right hand side. The empty space and boulder are swapped, resulting in the boulder falling down one tile position. Until a boulder hits something other than empty space, it will keep falling down one step per update.

Every time the game updates the screen, all rules are checked against all tiles. Lazy evaluation is used to ensure good performance for large tile maps. The result is written to a new tile grid, which becomes visible only after all tiles were checked. This ensures that rules do not interfere with each other. Otherwise, one rule's input could react on the output of another within a single screen update. Also, when two rules try to write to the same tile, the second rule is blocked, ensuring that no tiles ever get "lost" by overlapping outputs.

In Boulderdash, a boulder will also roll sideways when it is on top of another boulder. This behaviour is also easy to specify:

rule: boulderbounce . . .    . . .
                    . * -    . - *
                    . * -    . . .

Note that the boulder will only roll to the right. We should also specify a rule that makes it roll to the left. Instead of specifying a second rule, in CellSpace we can just say that this rule should be mirrored to create a second rule. In CellScript code, you specify this as: transform: mirx. In case a boulder can roll both to the left and right, one rule is chosen randomly.

Now, let's introduce a player that can move around by pressing the WSAD keys.
rule: playermove . . .    . . .
                 . @ -    . - @
                 . . .    . . .

This rule will move any player tile on the map to the right. However, we want this only to happen if the right key is pressed. We can do this by specifying an additional condition using the condfunc: statement. A condition is just a Javascript expression that must be true in order for the rule to trigger. There is a built-in function playerdir that we can use. If we specify condfunc: playerdir("right"), then the rule will trigger only if the "right" direction key is pressed (which is in the underlying engine translated to a cursor-right or swipe-right). Note that if we place multiple player tiles on the map, they will all move.

We can now add transform: rot4 which will rotate the rule 90, 180, and 270 degrees. The playerdir function takes the rotation angle as an implicit parameter, and will appropriately translate "right" into "down", "left", and "up".

Let's spice things up with some monsters. Creating a monster that moves around randomly is very easy.
rule: monstermove . . .    . . .
                  . M -    . - M
                  . . .    . . .

If we specify transform: rot4 for this rule, the monster will move randomly in 4 directions. This behaviour does not look very intelligent, so let's say we want the monster to go straight on until it hits something, and then turn. This brings us to another CellSpace feature, namely directions. Each tile has a direction associated with it, which is represented by U, D, L, R for up/down/left/right. Initially the direction is undefined, but we can set it using outdir: and check it using conddir: statements. If we specify conddir: L, the monster will only move left when its direction is left (that is, it is "facing left"). As yet, conddir: only checks the center tile, which is enough for most cases. outdir: takes 9 parameters, specifying the directions to write for each cell in the 3x3 grid when the rule is triggered.

Now, we still have to make the monster turn. We do this with the following cellscript:

rule: monsterturn
. . .   . . .
. M .   . . .
. . .   . . .
outdir:
- - -
- L -
- - -
transform: rot4
This will cause the monster to turn randomly. Note that the right hand side consists of all ignores, because we are not changing any tile, just the direction of the center tile. This rule competes with monstermove, which means one is randomly chosen. If we want monsterturn to trigger only if monstermove cannot be applied, we specify monstermove to have a higher priority using priority: 2 (the default priority is 1). We now have the desired monster behaviour.

An important feature of CellScript rules is rule delays. These are specified by the delay: statement. A number indicates that the rule is checked only once per the specified number of time ticks. The enables different objects to move at different speeds.

In some cases, you want a rule to trigger immediately when appropriate, and then go into a quiescent period after the trigger. For example, when moving a player, you want the player to react immediately to a keypress, and then wait for a certain amount of time before it can move again. This can be specified using the trigger keyword, followed by an identifier of your choosing. This creates a global timer that ticks down, pausing the rule until it reaches zero. For example:

delay: 4 trigger playertimer
This specifies that the rule triggers immediately as soon as its conditions are met, but then will wait for 4 time ticks before it triggers again on any cell. Any other rules referring to the same variable playertimer will be similarly delayed.

Cell appearance and animation

The rules use single-character symbols to represent cells. The appearance of a cell can be specified by a cell: statement. For our example, we use the following piece of code:
cell: -   0 - no   - no
cell: *  80 - no   - yes
cell: @   8 - no   - yes
cell: M 147 - rot4 - yes
Each cell: statement is followed by: With animations disabled, tile changes will just be shown by instantly updating the tile map on the screen. This will look very jumpy (in fact, games like Boulderdash look similarly jumpy when pieces are moved). Fortunately, tile changes can be made to look like smooth transitions, and a series of animation frames can be specified to animate the transitions.

Smooth transitions can be specified at the cell level. If you specify that a cell is animated in the cell: statement, the engine will create a smooth transition when it believes a cell of this type moves from one location to another. For this it uses the following heuristic. When a cell of this type moves into or away from the center cell, and the cell occurs only once in both left hand side and right hand side of the rule, then it will animate the cell.

In some cases, you do not want certain transitions to be animated. To disable animations for a specific rule, you can add an anim: statement to that rule. There are four options: yes (the default), no (turn all animations off for this rule), from-center (only animate a cell that moves from the center), and to-center (only animate a cell that moves into the center).

To specify animations in even more detail at the cell level, you can use cellanim: statements. Here you can specify the following:

More details can be found in the IDE's code forms and the examples.

A complete game

CellScript also includes level definition keywords, and a number of other directives for graphical appearance, such as the image to use for a tilemap, titles and instructions, and some shorthands. To explain these, we will now look at a complete example game (with one level). Download it here: simpleboulder.txt.

A cellscript starts with a preamble which specifies some basic things:

globals:
var gems=0;

gametitle: Simple Boulderdash Clone

gamedesc:
This is an example game.<br>
Pick up all the gems and avoid the monsters.

gamebackground: #648

tilemap: 16 16 16 16 no generictileset.png

display: 48 48

background: #444

cell: -  -1 - no - no
cell: *  80 - no - yes
cell: =  86 - no - no
cell: #  19 - no - no
cell: @   8 - no - yes
cell: M 147 - rot4 - yes
cell: D 116 - no - yes
The most interesting part is the globals: definition. Here you can define global variables, and functions if you need to. Note the syntax: it's Javascript. You can put any Javascript code here, and even define your own functions.

gametitle: and gamedesc: are self-explanatory. Note that line breaks in gamedesc: are specified by <br> statements.

tilemap: specifies a sprite sheet with the tile graphics in it. generictileset.png is a built-in tileset which contains some nice placeholder tiles you can use to prototype a game.

The other parameters are resp.: tile width/height, number of horizontal and vertical tiles in the image, and whether the graphics should be smoothed.

NOTE: in this version, there are some notable limitations to the tilemap statements. First, the tile map must be square. This is due to limitations of the tile engine, which will be improved in a future version. Second, the specified URL can generally only be loaded from the website itself (due to CORS restrictions), which is pretty useless if you don't host the cellspace code on your own server.

The recommended way to specify your own tilemap is to use the sprite editor. When you've got your sprites loaded in the sprite editor, use the CellScript: export function. A tilemap: statement will appear in the textarea at the bottom, which you can copy/paste into your code. The generated tilemap statement uses a data URL, which embeds the image data in a base64 encoded string. Of course, you can also encode any image you want to use into a data URL.

display: specifies the display size of a tile, relative to a virtual resolution of 1920x1080. The actual resolution is scaled to fit the screen. Since a tile is 16x16 pixels, we specify it's blown up by a factor 3, in case our physical display size is 1920x1080.

background: and gamebackground: specify the background colour of resp. the levels and the title screen. background: can also be defined for each individual level. Alternatively, an image URL can be given to show as background.

We're finished with the preamble. Let's now specify the rules:

rule: boulderfall
. . .    . . .
. * .    . - .
. - .    . * .
priority: 2

rule: boulderbounce
. . .    . . .
. * -    . - *
. * -    . . .
transform: mirx


rule: playermove
@ - .   - @ .
condfunc: playerdir("right")
transform: rot4
delay: 3 trigger player
outdir: - R -

rule: playerdig
@ = .   - @ .
condfunc: playerdir("right")
transform: rot4
delay: 3 trigger player
outdir: - R -

rule: playerget
@ D .   - @ .
condfunc: playerdir("right")
transform: rot4
outdir: - R -
outfunc: gems--;
delay: 3 trigger player

rule: playerpush
@ * -    - @ *
condfunc: playerdir("right")
transform: mirx
delay: 3 trigger player
outdir: - R -


rule: monsterorient
. M .    . . .
outdir:
- R -
transform: rot4

rule: monstermove
. M -    . - M
conddir: R
outdir:
- - R
transform: rot4
priority: 2

rule: playerdie
. M @    . - M
priority: 3
transform: rot4
outfunc: lose()
This mostly follows the tutorial, but includes the player digging through earth and pushing a boulder, and being killed by a monster. Note that most rules only have a 1x3 cell pattern rather than a 3x3 pattern. This is a shorthand you can use if the top and bottom line are all "ignore" cellsyms.

Also notable is the outfunc: gems-- statement. If the player picks up a gem, the global variable gems is decremented. Similarly, outfunc: lose() calls the built-in lose() function, which causes the level to fail. There is also a win() function, which completes the level.

Finally, we specify the content of a level:

level: #

================================
=@==================*****=======
====*****===========*****=======
====*=D=*=====***===**D**=======
====*=D=*=====*D*===*****=======
====*****=====*D*===*****=======
==============***===========***=
============================*D*=
============================***=
#############################===
======*====*==*====*====*=======
========*==*====*==*=*====*==*==
===#############################
===**======M------------========
===**=------==D==-=====M-------=
===**=-=D==-=====-=====-===D==-=
===**=-====-=----M------======-=
===**=--------===D==-=D-======-=
===**=-==D===-======-==--------=
===**=-======--------=========-=
===**=M-------======----------M=
===**===========================

title: Level one
desc: 
This is level one.<br>
Actually, it's the only level.

init: gems=countCells("D")
win: gems<=0
The first character after level: is the fill tile to use if tiles are not defined or for tiles outside of the level boundary. The rest of the level statement is self-evident.

Level also has a title and description. init: specifies one or more Javascript statements for initialising variables, and win: specifies an expression representing a win condition. Also there are lose:, and tick: (not used here), which specifies one or more statements which should be executed at the beginning of each screen update.

A next step is to enhance the game graphically and add some sounds. Be sure to check out the enhanced version of the simpleboulder game.

More features and function reference

There are some more useful rule features you should know about. Firstly, the cellsyms in the left hand side of a rule can include multiple characters, indicating any of these characters triggers the rule. The "!" character, when placed in front of the other characters, indicates "NOT" (all cells EXCEPT the specified cells). For example:
rule: playermovedig
. @ !DM#   . - @
indicates a player that can dig through anything except monsters, gems, and solid walls.

When you have a lot of rules that refer to the same group of cell types, you can create a shorthand using the group: statement. For example:

group: %  *D
creates a group called % that represents both boulders and gems.

There is also a probability: statement, which indicates the probability that a rule triggers if all its conditions are met. If multiple rules compete and the total probability becomes more than 1, the probabilities are interpreted as weights. It is meaningful to specify rule probabilities above 1 to indicate weights.

Finally, here is a short function and variable reference.

playerdir(dir)
returns true if the player indicates a particular direction by a keypress or swipe.
dir: "left", "right", "up", "down" (A,D,W,S)
For twin-stick style controls, you can also use:
"left1", "right1", "up1", "down1", "left2", "right2", "up2", "down2" (A,D,W,S and J,L,I,K)
keypress(key)
returns: true when key is pressed
countCells(cell_list)
returns the number of cells of the specified type(s). Note this actually counts all cells, so it's slow.
cell_list: a string of one or more characters indicating cell types
playSound(url)
play sound from the given URL
win()
wins the game
lose()
lose the game
panto(x,y)
pan the camera to center on the given position (for scrolling playfields)
x and y
when referred to from a rule (that is, in condfunc and outfunc), indicate the position of the center tile on which the rule is applied

This concludes the tutorial. Check out the examples for some more advanced usage of the engine's features.