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
rule: boulderfall . . . . . . . * . . - . . - . . * .
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
"*" resp. indicate empty space and boulder.
Linking characters to tile graphics is done using
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. 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:
mirx. In case a boulder can roll both to the left and right, one rule
is chosen randomly. If you want one rule to have priority over another, you
can use the
Now, let's introduce a player that can move around by pressing the cursor 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
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). 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" respectively 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
for up/down/left/right, and for example
UL for the up-left
diagonal. Initially the direction is undefined, but we can set it using
outdir: and check it using
conddir: statements. If
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
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: rot4This will cause the monster to turn randomly. This rule competes with
monstermove, which means one is randomly chosen. If we want
monsterturnto trigger only if
monstermovecannot be applied, we specify
monstermoveto have a higher priority using
priority: 2(the default priority is 1). We now have the desired monster behaviour.
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. tilemap: generictileset7.png empty: . cell: - 0 - false cell: * 80 - false cell: = 86 - false cell: # 19 - false cell: @ 8 - false cell: M 147 - true cell: D 116 - falseThe most interesting part is the
gamedesc: are self-explanatory. Note
that line breaks in
gamedesc: are specified by <br>
tilemap: specifies a sprite sheet with the tile graphics in it.
generictileset7.png is a built-in tileset which contains some
nice placeholder tiles you can use to prototype a game. All tiles must be
16x16, packed into the sprite sheet without any space around them. The 16x16
restriction will be removed in future versions of CellSpace.
Then follow the cell definitions.
empty: specifies which
character to use for the "ignore" cellsym. Each
is followed by a cellsym (a character denoting the cell), a tile number
(indicating the tile to use, counted from the top left), a base rotation ("-"
indicates none, "L", "R", "U" indicates left, right, and U-turn respectively.
Finally a Boolean value specifies if the tile should be rotated when the cell
has a direction associated with it, with Up being the base rotation.
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 rule: playerdig . @ = . - @ condfunc: playerdir("right") transform: rot4 rule: playerget . @ D . - @ condfunc: playerdir("right") transform: rot4 outfunc: gems-- rule: playerpush @ * - - @ * condfunc: playerdir("right") transform: mirx 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. 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.
outfunc: lose() calls the built-in
function, which causes the level to fail. There is also a
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=12 win: gems<=0The 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, though note that the maximum level size for the current version is 32x22.
Level also has a title and description.
init: specifies one
win: specifies an expression representing a win condition. Also
tick: (not used here), which
specifies one or more statements which should be executed at the beginning of
each screen update.
There are a couple more useful 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.
Secondly, there is a
probability: statement, which indicates the
probability that a rule triggers if all its conditions are met, and
delay: which indicates how often the rule should be triggered
(with 1 being the fastest, and 3 being the default), enabling objects to move
at different speeds.
Finally, there is one more built-in function
which can be used to detect a specific key. Use " " for a space, and "up",
"down", "left", "right" for cursor keys. "shift", "alt", "ctrl", and "enter"
are also recognised.
This concludes the tutorial. Also check out the bigger example game, Candy Cane Dash, which demonstrates most features of the CellSpace engine, and Flush the Fish, which is my latest CellSpace game, featuring water physics.
It's written in Java. How to use:
java -jar CellSpace.jar MyCellScriptFile.txt
This is very much a prototype/alpha version. There are lots of things to do before version 1.0: