Decker is a flexible, malleable digital medium. At its simplest, you can build games and experiences in the vein of old Choose Your Own Adventure books by operating modal dialogs and menus to connect cards with buttons:
It is also possible to create much more elaborate toys and applications by using Lil to add scripted behaviors to cards and widgets. From time to time I get questions about whether Decker could be used to make more action-oriented styles of game. In this article we’ll explore several Decker-based takes on the arcade classic Breakout and use them to illustrate strengths, weaknesses, and tradeoffs of the platform.
Each of the approaches described here is also available as an interactive demo using the browser-based version of Decker, with links provided to specific demos in each section. This guide will assume some prior knowledge of Decker and Lil; refer to the main decker page for more tutorials and reference documentation, or The Life of Lil for another discussion of Lil programming pragmatics.
Let’s begin with an adaptation of an existing Breakout clone. The “2D Breakout Game Using Pure JavaScript” tutorial provided on the Mozilla Developer Network is a nice starting point. Some of the finer details like tracking score and lives are omitted here for brevity; adding them for yourself might be a useful exercise!
We’ll need a single 480x320-pixel Canvas Widget on a card to correspond to the <canvas>
element in the JavaScript version of the game. By defining a script for the card, we can handle events produced by that canvas. In the video clip below, I use Decker’s Listener to set the exact size of the canvas:
Here’s that card script:
ball:image["%%IMG0ABUAFQH8AAf/AA//gB//wD//4H//8H//8P//+P//+P//+P//+P//+P//+P//+H//8H//8D//4B//wA//gAf/AAH8AA=="]
ballRadius:first .5*ball.size
ballPos:(.5,1)*canvas.size-0,30
ballVel:2,-2
paddleSize:75,10
paddlePos:first .5*canvas.size-paddleSize
bricks:image[5,3].map[0 dict 1]
brickSize:75,20
brickOffset:30,30
brickPadding:10
running:1
on click do
while running
each pos in bricks.size[0] cross bricks.size[1]
if bricks[pos]
local brickPos:brickOffset+pos*brickSize+brickPadding
canvas.rect[brickPos brickSize]
if min (ballPos>brickPos) & (ballPos<brickPos+brickSize)
bricks[pos]:0
ballVel:ballVel*(1,-1)
if !bricks.hist[1] running:0 end
end
end
end
canvas.paste[ball ballPos-ballRadius 1]
paddlePos:first (pointer.pos-canvas.pos)-.5*paddleSize
canvas.rect[paddlePos,canvas.size[1]-paddleSize[1] paddleSize]
if ((canvas.size[0]-ballRadius)<ballPos[0]+ballVel[0]) | (ballRadius>ballPos[0]+ballVel[0])
ballVel:ballVel*(-1,1)
end
if ballRadius>last ballPos+ballVel
ballVel:ballVel*(1,-1)
elseif (canvas.size[1]-ballRadius)<ballPos[1]+ballVel[1]
if (ballPos[0]>paddlePos) & (ballPos[0]<paddlePos+paddleSize[0])
ballVel:ballVel*(1,-1)
else
running:0
end
end
ballPos:ballPos+ballVel
sleep[]
canvas.clear[]
end
end
You can experiment with this example yourself here as you read along.
To begin with, we define a number of variables that describe the sizes of game objects- the ball, the paddle, and the bricks- as well as the state of the game which changes over time, like the velocity and position of the ball.
The ball’s appearance is defined as a bitmap using Decker’s native image encoding format; I drew a 20x20 pixel circle manually and then copied it to the clipboard. The image[]
built-in function can be used to decode such a string into an Image Interface:
ball:image["%%IMG0ABUAFQH8AAf/AA//gB//wD//4H//8H//8P//+P//+P//+P//+P//+P//+P//+H//8H//8D//4B//wA//gAf/AAH8AA=="]
In Lil it is idiomatic to group together quantities like positions or sizes as “pairs” (length-2 lists) rather than discrete variables, since this allows us to pass them around and manipulate them together. In the JavaScript version, the position of the ball is represented in x
and y
variables:
var x = canvas.width/2;
var y = canvas.height-30;
Whereas in the Lil version it is represented with ballPos
, computed by subtracting an offset from canvas.size
(which is itself a pair) and then multiplying by another pair as a scaling factor:
ballPos:(.5,1)*canvas.size-0,30
Like Lil’s cousin Q, expressions in Lil are carried out right-to-left except when overridden by parentheses. The comma operator (,
) joins elements into lists, and (x,y)
is merely a parenthesized expression, rather than a “literal” syntax for lists.
While the JavaScript implementation uses nested lists to represent the positions and destruction status of bricks,
var bricks = []
for(var c=0; c<brickColumnCount; c++) {
bricks[c] = []
for(var r=0; r<brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 }
}
}
The Lil implementation instead uses another Image as a mutable 2D grid with handy utility functions for bulk manipulation. A newly-created Image will be populated with pattern 0
at each pixel, but we can convert those to 1s by using image.map[]
with a dictionary that pairs input patterns with their replacement value:
bricks:image[5,3].map[0 dict 1]
If we’d wanted a more complex layout of bricks it might be more convenient to scribble the board manually on a spare card background with FatBits mode, copy our scribbling to the clipboard, and then initialize bricks
from an encoded image-string like above with ball
. The following alternative would be equivalent:
bricks:image["%%IMG0AAUAA/j4+A=="]
If we collapsed most of the Breakout-specific game logic of the remainder of the program for brevity, it might look something like this:
running:1
on click do
while running
# draw and handle collision for the bricks
# draw and handle collision and motion for the paddle
# draw and handle collision and motion for the ball
sleep[]
canvas.clear[]
end
end
Clicking on the canvas widget produces a click
event. Since there’s no script defined on the canvas itself, that event bubbles up to the card script, where our on click do ... end
event handler is defined. This handler runs an indefinite loop that executes one frame’s worth of game logic, waits for the next 60hz frame with sleep[]
and then clears the canvas to begin again. This script will only halt once game logic clears the running
variable.
This approach is what we’d call a Blocking (or, if you prefer, Synchronous) Lil script. While the game is active, Decker cannot service any other events. The call to sleep[]
is not strictly necessary for such a script; Decker executes scripts with a limited quota of execution time1 and if this quota is exceeded it automatically pauses execution periodically to update the display and service user input with Decker itself. You can tell your scripts are taking a while to run by observing the “Script” menu appear while interactive widgets on the current card take on a disabled appearance.
The only input method available to blocking scripts is via the Pointer Interface, which offers a view of the status of the user’s pointing device; new values can be observed after a call to sleep[]
or any time Decker automatically “rolls over” to the next frame by quota exhaustion.
Despite their constraints, blocking scripts can be very convenient. Writing straight-line code with sleep[]
in the appropriate places makes it easy to describe sequential animations and transitions where in other programming environments one might have to reach for coroutines or write an explicit state-machine. The Dialogizer library uses this capability to great effect, describing dialog sequences and cutscenes using ordinary Lil control structures.
An alternative to blocking scripts is embracing Decker’s event-oriented nature. We’ll define an event handler that is called once per frame, and within that handler update our game state and redraw the canvas. Between game update events, Decker will have time to service other forms of input, allowing users to click on buttons, scrub sliders, edit fields, or navigate to another card.
The biggest change we’ll need to make for this event-oriented (or, if you prefer, Asynchronous) approach is how we manage game state. In Decker, local variables within scripts only stick around while a given event is being processed, after which they are discarded. This worked fine for our blocking version of Breakout, since we want to reset the game world every time we play. In order to remember information beyond the scope of a single event, we must reify (that is, make “physically” real) that information by representing it with a widget. Canvases can be used to store images like bricks
, grid widgets can store Lil tables, checkboxes can store boolean values, fields can store strings, and so on. The most general way to stash data in a widget is using the .data
attribute of a field widget, which will encode and decode LOVE2 data within the field.
Having added the above widgets (you might have to shrink the canvas a bit to fit them in Decker’s default card size), the overall script now looks like this:
ball:image["%%IMG0ABUAFQH8AAf/AA//gB//wD//4H//8H//8P//+P//+P//+P//+P//+P//+P//+H//8H//8D//4B//wA//gAf/AAH8AA=="]
ballRadius:first .5*ball.size
paddleSize:75,10
paddlePos:first .5*canvas.size-paddleSize
brickSize:75,20
brickOffset:30,30
brickPadding:10
on reset go do
bricks.paste[image[bricks.size].map[0 dict 1]]
ballPos.data:(.5,1)*canvas.size-0,30
ballVel.data:go*2,-2
canvas.animated:go
view[]
end
on click do
reset[!canvas.animated]
end
on view do
done:0
canvas.clear[]
if canvas.animated
each pos in bricks.size[0] cross bricks.size[1]
if bricks[pos]
local brickPos:brickOffset+pos*brickSize+brickPadding
canvas.rect[brickPos brickSize]
if min (ballPos.data>brickPos) & (ballPos.data<brickPos+brickSize)
bricks[pos]:0
ballVel.data:ballVel.data*(1,-1)
if !bricks.copy[].hist[1] done:1 end
end
end
end
canvas.paste[ball ballPos.data-ballRadius 1]
paddlePos:first (pointer.pos-canvas.pos)-.5*paddleSize
canvas.rect[paddlePos,canvas.size[1]-paddleSize[1] paddleSize]
if ((canvas.size[0]-ballRadius)<ballPos.data[0]+ballVel.data[0]) | (ballRadius>ballPos.data[0]+ballVel.data[0])
ballVel.data:ballVel.data*(-1,1)
end
if ballRadius>last ballPos.data+ballVel.data
ballVel.data:ballVel.data*(1,-1)
elseif (canvas.size[1]-ballRadius)<ballPos.data[1]+ballVel.data[1]
if (ballPos.data[0]>paddlePos) & (ballPos.data[0]<paddlePos+paddleSize[0])
ballVel.data:ballVel.data*(1,-1)
else
done:1
end
end
ballPos.data:ballPos.data+ballVel.data
if done reset[0] end
end
end
You can experiment with this example yourself here.
All of the local variables that are “constants” are declared as before. Setting up the “game state” for each round is now grouped into a function named reset[]
, and all accesses to this data now reference the widgets we created on the card. The running
variable is now implicitly represented by whether or not the main canvas has been set to animated
.
If we pare everything down by hiding most of the game logic, it may be easier to understand the general structure:
# set up constants...
on reset go do
# set up ball pos, vel, and bricks
canvas.animated:go
view[]
end
on click do
reset[!canvas.animated]
end
on view do
canvas.clear[]
if canvas.animated
done:0
# update and draw bricks, paddle, and ball
if done reset[0] end
end
end
As before, the click
event is fired when the player clicks on the canvas. Once canvas.animated
is enabled, the canvas will additionally fire a view
event once per frame, acting as an “event pump” for the game. Since each view
event is handled briskly, Decker has a chance to allow the user to interact with other widgets while the game runs.
The idea of reifying game state inside widgets may seem a bit odd at first, but it comes with many advantages. The contents of every widget is saved along with the deck3, across and within sessions. If you make changes with Decker’s editing tools, or even use scripts to rewrite the scripts of other widgets, the lifespan of data is always clear-cut: if it’s in widgets it sticks around, and if it’s in variables within scripts it does not4. You can tuck game state widgets away on another card or make them invisible to avoid clutter, but if you leave them visible they can provide useful insight into what’s going on within your program.
Event-oriented programs often require a bit more book-keeping than their blocking equivalents, (especially if you’re juggling lots of inter-related events at once) but are far more flexible, especially with respect to input. By allowing Decker to service multiple events per frame, you can use events like navigate
to make games respond to cursor keys or have more elaborate clickable user interfaces. The Sokoban example uses navigate
and Button widgets with shortcuts to provide a keyboard-friendly gameplay experience. The visible buttons and touchscreen adaptation of swipe gestures to navigate
events mean the same game is also playable on touchscreen-based devices like phones and tablets.
Remember, you don’t have to choose exclusively between an event-oriented style or a blocking style: mix and match, using the approach that best fits your brain and the needs of your application.
Reifying game state in a few widgets gave us some flexibility, but also made our scripts somewhat more complicated. Decker has another trick up its sleeve that will allow us to take the idea much further: Contraptions. In a nutshell, a contraption is a custom-built widget. From the inside, a contraption looks and acts like a card. From the outside, a contraption looks and acts like a widget. We can use contraptions to extend Decker’s vocabulary of parts, build containers for reifying new types of data, and encapsulate that data along with behaviors.
Let’s start over from scratch, and break down Breakout into four components, each of which will be represented by a contraption prototype:
All of these objects have positions and sizes on screen, and the ball additionally needs to keep track of its velocity. The ball needs to be able to recognize walls, bricks, and the paddle to bounce off of them, and it needs to be able to destroy bricks.
Let’s start with walls. They’re rather simple: a small, resizable contraption with no internal widgets. They expose an attribute .thud
which will return a truthy value when read to indicate that a ball can collide with them. No built-in widgets expose an attribute with this name, so trying to read their thud
attribute will return 0
.
The wall script:
on get_thud do
1
end
Next, bricks. They should also be resizable, and expose a .thud
attribute. When they’re hit, they become invisible, notify the card upon which they appear about their destruction, and cease to respond to further thud
s until (externally) reset:
The brick script:
on get_thud do
if card.show~"solid"
card.show:"none"
card.parent.event.destroy
1
end
end
The paddle is slightly more complicated. It needs to respond to clicks when the player starts the game, so we give it an internal button widget, b
. We can also mark b
as “animated” so that the paddle gets a steady stream of view
events during which it can center itself on the user’s pointer position. If you give your paddle rounded corners like mine, you’ll need to be sure to mark instances as “Show Transparent”.
The paddle script:
on get_thud do
1
end
on view do
card.pos:(pointer.pos[0]-.5*card.size[0]),card.pos[1]
end
on click do
card.parent.event.reset
end
The ball has the most complex behavior. It needs to expose a .vel
attribute for reading and manipulating its velocity. Internally this will be stored in a field widget named v
.
Like the paddle, the ball needs to continuously move itself and interact with other widgets on its container card. By marking v
as “animated”, it will likewise have a chance to perform these updates within periodic view
events. The ball queries the container card and reads the .thud
attribute of widgets which overlap its centerpoint, using the response to decide how to alter its own velocity to bounce. If the ball exits the bounds of the container card, it fires a reset
event at the container card.
The ball script:
on get_vel do v.data end
on set_vel x do v.data:x end
on view do
c:card.pos+.5*card.size
n:get_vel[]+c
p:card.parent
thuds:0,0
each w in card.name drop p.widgets
if min (n>w.pos)&(n<w.pos+w.size)
if w.thud
b:!(c>w.pos)&(c<w.pos+w.size)
if 2=sum b b:0,1 end # prefer vertical bounces
thuds:thuds|b
end
end
end
if max (n<0)|(n>p.size)
card.parent.event.reset
else
set_vel[get_vel[]*(1,-1)@thuds]
card.pos:get_vel[]+card.pos
end
end
We’re nearly done, but we need a little more plumbing to make a playable game. Create some instances of our four contraptions and arrange them as shown below. The start
widget is a button that has been set to “Show None”; we’ll use it to indicate the starting position of the ball:
We need to respond to the destroy
events sent by bricks in order to detect when all bricks have been destroyed, and end the game. Likewise, we need to respond to the reset
events sent by balls that go out of bounds and the paddle when it is initially clicked. Since both routines are concerned with enumerating the brick contraptions on the card, we also have a helper function get_bricks[]
for retrieving them:
The card script:
on get_bricks do
extract value where value..def.name="brick" from card.widgets
end
on destroy do
if !sum get_bricks[]..show="solid" reset[] end
end
on reset do
ball.pos:start.pos+.5*start.size-ball.size
ball.vel:if mag ball.vel 0,0 else 2,-2 end
get_bricks[]..show:"solid"
end
With all that handled, we now have a playable Breakout game. Try it for yourself here.
Making the contraptions may seem a bit more conceptually complex up-front, since it breaks the logic of the game up into many small pieces. Each of the components, however, has fairly simple scripts, and even the logic for the “ball” is less complex than the first Lil implementation of Breakout.
We’ve also gained some very valuable properties along the way. Both of our previous game implementations contained a large number of “hardcoded” values for the size and positioning of game elements; this information is now represented with the bounding boxes of the contraption instances themselves on a card. Instead of browsing through a script and imagining pixel dimensions to alter these values, a user need only switch to Widgets mode and reposition or resize the contraptions through direct manipulation.
Our contraptions are independent from one another (or, if you prefer, Loosely-Coupled); they only communicate via the convention of the .thud
attribute for objects the ball collides with. We could easily change the appearance or behaviors of contraptions or introduce new types of game elements without needing to make changes to the existing parts. Since the state of each contraption is neatly packaged within it, nothing in our code assumes a specific number of walls, bricks, or paddles; supporting multiple balls at once would only require minor changes to the reset[]
function in the card script.
By expanding Decker’s conceptual vocabulary, we have not only implemented Breakout, we have provided ourselves and others with a toolkit of modular parts that can be used to create a wide variety of similar games, and all the parts are as easy to work with as Decker’s built-in widgets.
A few ideas to try implementing yourself:
thud
s to destroyWe have seen three ways of building a simple arcade game within Decker:
Each of these techniques build upon one another and offer useful tradeoffs for designing different sorts of programs. Hopefully you’ve come away with a better understanding of the how and why of scripting within Decker. If you’re excited about making stuff with Decker, why not join us on the community forum? We’d love to have you.
Decker‘s imperfect–but–reasonably–effective accounting strategy for script execution time measures the number of “ops” executed by the Lil interpreter’s bytecode engine. Taking advantage of language features like list conforming conserves ops and therefore produces both faster wall–clock execution time and makes it possible to get more work done within quota. To gain more insight into how much of the quota your decks are consuming over time, try enabling “Script Profiler” from the “Decker” menu and observe the histogram of quota utilization in the top right corner of the screen. ↩︎
Lil Object–Value Notation (LOVE) is a superset of JSON used in the Lil ecosystem which can round–trip all of Lil's basic datatypes, including dictionaries with non–string keys, table values, and common interface types like Image. There are many JSON supersets in the world, and this one is ours. ↩︎
If you have widgets whose state shouldn't be (or doesn't need to be) preserved when a deck is saved, like these big canvases we've been constantly repainting, it might be a good idea to mark them as Volatile. ↩︎
The exception being local variables within Modules; a module script is run once when a module is loaded or modified and anything it exports retains access to its originating closure. Modules can be stateful as a performance–aiding escape–hatch, but this approach is often brittle in practice. Beware! ↩︎