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.
![]() | ![]() | ![]() |
![]() | ![]() | ![]() | 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
"-"
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: rot4This 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 playertimerThis 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:
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 - yesEach
cell:
statement is followed by:
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:
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 - yesThe 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<=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.
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.
"!"
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: % *Dcreates 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.
This concludes the tutorial. Check out the examples for some more advanced usage of the engine's features.