A visual debugging suite for Playdate.
Acetate is a visual debugging utility for use with the Playdate Simulator,
optimized for playdate.graphics.sprite subclasses. With Acetate, you can easily
enter a visual debug mode at any time, cycle through debug visualizations for each sprite, and optionally
customize the visuals and information shown for each from directly within your sprite classes.
Acetate wraps the built-in functionality for debug drawing, and adds:
- Visualize common sprite properties—bounds, centers, rotation, collision rects, etc.—out of the box
- Cycle through debug info for each sprite, individually
- Display rich debug strings in a bespoke monospaced font
- Perform custom debug drawing directly from your sprite classes
- Pause your game while debugging
- Customize keyboard shortcuts
- Configure appearance and behavior
Playdate is a registered trademark of Panic.
-
Clone this repo (or copy its contents) into a
librariesfolder in your project, e.g.source/libraries. -
Import
Acetatefrommain.lua -
Initialize it with an optional single argument to override any default settings
import 'libraries/acetate/Acetate' acetate.init({ -- custom settings go here })
After importing Acetate, you can take advantage of its features right away:
- Build and run your app in the Playdate Simulator.
- Press the
Dkey on your keyboard to enter debug mode. - Use
,(<) and.(>) to cycle through sprites. - Press the
?key to toggle a (customizable) debug string with additional information. - Refer to the list of keyboard shortcuts for additional options.
Out of the box, you can see the following information for each sprite:
- class name
- memory address
- size and position
- bounding box
- center point
- collision rect
- orientation orb
You can also display additional information and visualizations unique to your sprites. Read on to learn how to implement custom debug drawing for your sprite classes and customize the debug string displayed as you cycle through them in debug mode.
NOTE: If your game adjusts the draw offset, you may need to cache it so that debug drawing appears in the correct position relative to your sprites. See the Troubleshooting section for additional details.
Acetate provides several debug visualizations out-of-the-box, which are suitable for showing basic properties common to most sprites. However, you may want to visualize custom properties unique to your sprite as well. Acetate makes this easy!
You can implement debugDraw within your playdate.graphics.sprite subclasses and
Acetate will ensure it gets called automatically:
function MySprite:debugDraw()
-- perform custom debug drawing here
endAcetate prepares the graphics context for you automatically:
- The color will be set to
kColorWhite(the color used for all debug drawing). - The line width will be set to
1. - The drawing offset will be set according to the position of your sprite, so you can do all
drawing relative to your sprite (just like in your
drawfunction).
Anything you draw within this function will appear in debug mode. You can toggle your custom
debug drawing on and off using the M key, or set acetate.customDebugDrawing to true or false
from within your code.
Because fonts are rendered as images and tend to be black-on-white, regular use of drawText
variants will likely not appear. To draw text in debugDraw, change the image drawing mode so
that your text will render in kColorWhite as follows:
gfx.setImageDrawMode(gfx.kDrawModeFillWhite)
acetate.debugFont:drawText("This text will render in the debug layer!", x, y)Acetate provides a handful of extensions to the sprite class specifically designed for drawing debug info for common sprite properties. You can toggle these on and off globally using the keyboard shortcuts or Acetate settings, but occasionally you'll want certain features for particular types of sprites and not others. For example, you might want to show orientation orbs only for sprites which rotate within your game.
You can call any of Acetate's debug draw functions from your own sprite's debugDraw function so
that they appear even when they are turned off globally.
function MySprite:debugDraw()
self:drawOrientation()
-- perform additional debug drawing here
endThe following built-in debug drawing functions are supported:
drawBounds: Draw the sprite's bounding boxdrawCenter: Draw the sprite's center pointdrawOrientation: Draw an indicator of the sprite's current rotationdrawCollideRect: Draw the sprite's collision rect, if set. (This option is also provided by the simulator itself. You can use the simulator version to overlay collision rects in a contrasting color.)
If you have a small bit of custom identifying information you'd like to display — say, the
number of a pin in a bowling pin rack, or the name of a particular character — you can set
the sprite's debugName property. When set, the debug name will be shown instead of the className
of the sprite when cycling through sprites in debug mode. For instance:
function Pin:init(number)
Pin.super.init(self)
self.number = number
self.debugName = "Pin " .. number
-- more initialization
endAcetate displays a debug string for the focused sprite while debug mode is active. By default, this string indicates the size and position of the sprite. You can modify the debug string format to include the most useful information for your use case in two ways:
-
Change the default. Modify the
acetate.defaultDebugStringFormatto change the debug string shown for all of your sprites. -
Set custom strings. Implement
debugString()on your sprite. You can provide a fully formatted string, or include substitution patterns as shown in the table below. If you don't require substitutions, you may passfalseas a second return value to skip the substitution logic.function MySprite:debugString() local s -- construct `s` using any properties belonging to your sprite return s end
All substitution patterns begin with a dollar sign ($) followed by up to three alphabetical
characters. They are case sensitive.
| Pattern | Substitution |
|---|---|
$n |
debugName if given, class name otherwise |
$N |
as above, but with class name + memory addr |
$cn |
Class name |
$str |
tostring() output |
$a |
Memory address |
$p, $pos |
Position coordinate in the form (x, y) |
$x |
X position |
$y |
Y position |
$w |
Width |
$h |
Height |
$rx |
Local relative horizontal center |
$ry |
Local relative vertical center |
$rc |
Local relative center point, e.g. (0.5, 0.5) |
$o |
Origin coordinate (top left) in local space |
$ox |
Local origin X position |
$oy |
Local origin Y position |
$O |
Origin coordinate in world space |
$Ox |
Local origin X position |
$Oy |
Local origin Y position |
$c |
Center coordinate in local space |
$cx |
Local center X position |
$cy |
Local center Y position |
$C |
Center coordinate in world space |
$Cx |
World center X position |
$Cy |
World center Y position |
$r, $rad |
Rotation (radians) |
$d, $deg |
Rotation (degrees) |
$s |
Scale |
$t, $tag |
Tag number |
$q |
Opaqueness as "OPAQUE" or "TRANSPARENT" |
$u |
Update status as "UPDATING" or "DISABLED" |
$v |
Visibility as "VISIBLE" or "INVISIBLE" |
$z |
Z-index |
You can print debug information for the currently focused sprite(s) to the console by pressing the I key. This makes it easy to review, compare, or copy and paste values. This works regardless of whether the current focus is an individual sprite, a class, a debug group, or all sprites currently in the global sprite list.
You can also dump debug info for a sprite programmatically in order to easily view debug data at specific points in your program's execution:
mySprite:printDebugInfo()If desired, you can add a prefix that will print immediately before the info to provide context. Here's what that looks like if you print from within your sprite class:
-- e.g inside an overridden setSize()
self:printDebugInfo("Resized")You can override the standard debug string in order to print just the information relevant in context, while still utilizing the convenience of Acetate's debug string substitution patterns. For example, here's how you could print just the updated size and center position of your sprite:
-- e.g inside an overridden setSize()
self:printDebugInfo("RESIZED", "$n: $sz, $c")Which would output the following:
RESIZED
MySprite: 100 x 200 (50, 100)Lastly, Acetate also provides a convenience for printing the traceback to a given line of program execution. This also accepts an optional prefix and format string, just like printDebugInfo().
-- from anywhere within your sprite
self:printDebugTrace("How did we get here?")Debug visualizations, and especially debug text, can be difficult to see regardless of color when the underlying game contains high frequency patterns and visuals. Acetate offers a translucent overlay which can be used to dim the game content and help the debug layer stand out for added legibility. It does so by drawing directly to the screen buffer after your game renders each frame, which requires adding a call to acetate.update at the end of your playdate.update callback:
-- at the very end of playdate.update
acetate.update()Press the O key to toggle the [O]verlay on and off while debugging. You can also adjust its color and opacity via the overlayColor and overlayAlpha settings.
Set the showOverlayOnEnable setting to true if you want the overlay to appear by default when entering debug mode. Conversely, set hideOverlayOnDisable to true if you want to prevent it from being shown each time you re-enter debug mode. Leave both set to false to preserve the state of the overlay when toggling debug mode.
Cycling focus to obtain debug info for specific sprites is central to Acetate's functionality. Highlighting individual sprites or groups of related sprites allows you to isolate visual debug layers or access textual debug output for the things that matter in the moment, without needing to comment/uncomment and rebuild to surface that data. The beauty of Acetate is that you can define detailed debug visuals and text output for each class as part of its design, so it's all at the ready the next time you need to debug, without getting in the way in the meantime.
Acetate offers three main affordances for managing focus:
- By sprite. Cycle through all sprites in the display list.
- By class. Cycle through all sprites of a given class.
- By Group. Cycle through sprites within an explicitly defined group.
As you focus each sprite, their visual debug drawing layer will appear. You can also toggle display of the sprite's debug string with the / (?) key. While a sprite is focused, you can also press the I key to print its debug string to the console for easy viewing/copying.
Cycling through sprites one by one is Acetate's basic interaction mode. Press the comma (',') and period (.) keys to cycle backward and forward through the list of extant sprites, respectively. (As a "mnemonic" of sorts, it may be helpful to think of these as the < (backward) and > (forward) keys. When you first enable Acetate, all visible sprites in the display list are focused at once—use these keys to isolate each in sequence.
Note
Invisible sprites are excluded from the focus list by default. Press the Z key to toggle their inclusion.
Press the L key to lock focus to the currently selected sprite class. After doing so, the usual focus cycling keys will cycle through all sprites of the same class only. Press L again to unlock focus. A 🔒 symbol is displayed with the name of the sprite class while class focus is locked. You may also use ; and ' to cycle forward and backward through sprites of the currently focused class without toggling class lock.
Debug groups make it easy to inspect distinct groups of sprites together, without the visual noise of other unrelated sprites. It also enables you to swiftly cycle through sprites in a given group. Set the debugGroup property of your sprite to a value in the range [1-9] to add it to that group.
mySprite.debugGroup = 1Press the corresponding key while in debug mode to focus all sprites in that group. Alternatively, press G to cycle through defined groups. While a group is focused, the . [<] and , [>] keys will cycle through the sprites in the group. Press 0 to unfocus a debug group and restore default focus behaviors. The group number (or name, if set) will be shown while a group is focused. You can define names for each group by providing a list for the groupNames configuration setting.
Acetate allows you to cycle through sprites in the display list manually using the , and . keys. However,
you can also focus sprites programmatically, for example in response to a particular game event or
condition. This makes it easy to initiate visual debugging at the right time and for the right sprite.
-- focus a single sprite
acetate.focusSprite(mySprite)
-- release focus
acetate.releaseSpriteFocus()If Acetate's debug mode isn't active when you call this function, it will be enabled automatically.
Optionally, you can enable the auto-pause behavior by setting acetate.autoPause to true (in init,
or at runtime), in order to pause for inspection when focusing your sprite.
You can also lock the focus to a specific class, so only sprites of that class get focused when cycling with the keyboard shortcuts:
-- focus by class
acetate.focusClass(MyClass)
-- release focus
acetate.releaseClassFocus()You can focus your debug groups by number or name:
-- focus group by number
acetate.focusGroup(1)
-- focus group by name
acetate.focusGroup("Enemy")
-- unlock focus
acetate.releaseGroupFocus()Finally, you can remove all focus constraints to restore access to the entire display list:
acetate.releaseFocus()Nudge mode lets you tweak the size, position, rotation, or even other properties of your sprites interactively. This lets you visualize the desired result in real time and then codify those values, avoiding repeated guess-and-rebuild cycles.
Press the N key to enter nudge mode. You'll see an animated bounding box and overlay for the focused sprite (or sprites), along with flashing arrows to indicate that nudge mode is active. Use the arrow keys to make adjustments, and hold modifier keys as shown below to adjust different properties. Discrete key presses will adjust with pixel precision, while holding the keys enables faster adjustment.
- No modifier: Move the sprite up, down, left, or right.
- A button (
skey): Increase or decrease the size of the sprite. If the sprite has an image set on it withsetImage(), then the sprite's scale will be adjusted. Otherwise, this will adjust the width and height properties. - B button (
akey): Adjust the rotation of the sprite. This works out-of-the-box for sprites with an image set withsetImage(), but will have no effect otherwise unless you read the sprite's rotation value when drawing. You can also provide a custom override for this modifier (see below).
You can nudge multiple sprites at once by using class focus or debug groups (or even all sprites at once). Note that nudging will still adjust the properties of each sprite individually when multiple sprites are focused. For example, when rotating, each sprite will rotate in place, rather than the group rotating around some central point. Likewise, when scaling, each sprite will scale, but its position relative to the other focused sprites will not change.
Depending on your sprite, you might want to adjust other properties. For example, an arc or circular sprite might have a radius that parameterizes it, rather than a width and height. If you implement debugNudge(x, y), it will be called when the B button is pressed or held. This function receives x and y increments as arguments, which are positive or negative according to whether the left/right and/or up/down keys are pressed.
You can choose to modify different properties with up/down vs. left/right, or have them both adjust a single property. In the latter case, you can take a shortcut by redefining the increment as their sum, e.g. local increment = x + y, then simply add the increment to the property you wish to adjust.
You can also define the optional nudgeReset() function to restore the initial value of the property you adjust with your custom debugNudge function.
Acetate provides a number of keyboard shortcuts. You're welcome to change any of these shortcuts
to fit your preference, or avoid conflict with other keyPress handlers defined elsewhere. Edit
the settings object or override the defaults in your project e.g. acetate.toggleDebugKey = "0".
| Key | Function |
|---|---|
| D | Toggle Acetate's visual [D]ebugging mode on/off |
| C | Toggle drawing of sprite [C]enters while in debug mode |
| B | Toggle drawing of sprite [B]ounds while in debug mode |
| V | Toggle drawing of sprite orientation [V]ectors while in debug mode |
| X | Toggle drawing of sprite colli[X]ion rects while in debug mode |
| Z | Toggle debug drawing of invi[Z]ible sprites while in debug mode |
| M | Toggle the use of custo[M] debugDraw functions defined in your own sprites |
| F | Toggle the [F]PS display on/off |
| N | Toggle the total sprite count on/off |
| / | [?] Toggle display of the debug string while focused on an individual sprite |
| I | Dump debug string [I]nfo for the currently focused sprite(s) to the console |
| , | [<] Cycle forward through sprites to focus them one by one |
| . | [>] Cycle backward through sprites |
| < | [SHIFT <] Cycle forward through sprites of the same class |
| > | [SHIFT >] Cycle backward through sprites of the same class |
| L | [L]ock focus cycling to the focused sprite class |
| P | [P]ause/unpause the game for/while debugging |
| Q | [Q]uick-capture a screenshot of either the full screen or the focused sprite |
| [1-9] | Focus debug group (press again, or 0 to unfocus) |
| G | Cycle focus through debug groups |
Acetate provides a shortcut for capturing instantaneous screenshots from the simulator. While
not strictly a debug feature, it's certainly a useful tool to have in your workflow. Capture
a screenshot by pressing the Q key at any time (even outside debug mode), or from within your
code:
acetate.captureFullScreenshot([path, filename])NOTE: Acetate's debug layer will not appear in screenshots.
You can provide a destination path and filename, or let Acetate name it with a timestamp and save
it to the currently configured acetate.defaultScreenshotPath. This is ~/Desktop by default, but
may be changed in settings.lua or from within your app.
If you are focused on an individual sprite while in debug mode when you activate the capture shortcut, Acetate will capture an image of just that sprite, rather than the full screen. You can also capture a screenshot of an individual sprite with:
acetate.captureSpriteScreenshot(sprite, [path, filename])Acetate's settings object allows you to change a wide array of options to configure the debugging experience. You can change the configuration in one of several ways:
-
Override at
init. You can override any defaults by passing named arguments to init:acetate.init { autoPause = true, debugColor = {0, 1, 0, 0.8}, -- as many as you like }
-
Create a custom config. If you intend to change many settings, or just want to keep your initialization code to a minimum, you can duplicate the
settings.luafile, give the settings object therein a unique name (e.g.myAcetateSettings = { … }, import that file inmain.lua, and then pass the named config object toinit:acetate.init(myAcetateSettings)
-
Set individual values. You can also override individual settings from within your app at runtime following initialization, e.g.
acetate.color = {0, 1, 0, 0.8}and so on.
The following settings are available:
| Setting | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Indicates when Acetate debug mode is active. Do not set this directly; call acetate.enable() or acetate.disable() instead. |
paused |
boolean | false |
Indicates when the app is paused during debug mode. Do not set this directly; call acetate.pause() or acetate.unpause() instead. |
autoPause |
boolean | false |
Indicates whether the app should pause automatically when entering debug mode. |
| Setting | Type | Default | Description |
|---|---|---|---|
drawCenters |
boolean | true |
Center points are shown for all sprites in debug mode when true. |
drawBounds |
boolean | true |
Bounding rects are shown for all sprites in debug mode when true. |
drawOrientations |
boolean | true |
Orientation orbs are shown for all sprites in debug mode when true. |
drawCollideRects |
boolean | false |
Collision rects are shown for all sprites while debug mode is enabled. |
customDebugDrawing |
boolean | true |
Custom debug drawing (implemented in sprite debugDraw functions) is shown when true. |
customOverridesDefaults |
boolean | false |
Built-in debug drawing is hidden for sprites with custom debug drawing when true. |
| Setting | Type | Default | Description |
|---|---|---|---|
color |
{r,g,b,a} | cyan 75% | A table containing RGBA values (in range [0,1]) describing the color used for debug drawing. |
lineWidth |
number | 1 |
The default line width set for the debug drawing graphics context. |
centerRadius |
number | 2 |
The radius of the dot drawn when drawCenters is true. |
orientationOrbScale |
number | 0.5 |
Orientation orbs are drawn in proportion to the sprite they belong to. This setting describes their diameter with respect to the sprite's shortest dimension. |
minOrientationOrbRadius |
number | 10 |
The minimum radius at which the orbs are drawn for smaller sprites, to aid clarity. |
onlyDrawRotatedOrbs |
boolean | true |
Draw orientation orbs only for sprites which have a non-zero rotation. This helps keeps the view uncluttered when sprites aren't being rotated. |
| Setting | Type | Default | Description |
|---|---|---|---|
showFPS |
boolean | true |
Whether to show the current FPS (frames per second). |
FPSPersists |
boolean | false |
Whether to show the FPS (frames per second) even while debug mode isn't enabled. |
showSpriteCount |
boolean | true |
Whether to show the total number of sprites. |
spriteCountPersists |
boolean | false |
Whether to show the total number of sprites even while debug mode isn't enabled. |
showDebugString |
boolean | false |
Whether the debug string is shown while focused on a single sprite in debug mode. |
defaultDebugFormatString |
string | "$n\nX: $x\nY: $y\nW: $w\nH: $h" |
The format used for the debug string for any sprites which don't define their own. See Debug String Formats for details. |
defaultNudgeDebugFormatString |
string | "$n \n$p \n$sz \n$d \n" |
The format used for the debug string while nudging, unless the sprite provides a custom debugString. |
displayPrecision |
number |
3 | An integer value specifying the desired number of decimals used to display values for the provided format string. |
alwaysShowSpriteNames |
boolean | true |
Whether to display the highlighted sprite's name even while the debug string is hidden. |
debugStringPosition |
{x,y} | {2, 2} |
A table containing the x and y position at which the debug string is drawn. By default, it draws just beneath the FPS counter at the top left corner of the screen. |
debugFontPath |
string | "fonts/Acetate-Mono-Bold-Condensed" |
The path to the font to use for displaying the debug string. |
| Setting | Type | Default | Description |
|---|---|---|---|
retainFocusOnDisable |
boolean | true |
When true, the focused sprite will remain focused the next time debug mode is entered. |
focusInvisibleSprites |
boolean | false |
Whether to perform debug drawing for and allow focusing of sprites which are made invisible via setVisible(false). |
animateBoundsForFocus |
boolean | true. |
When true, the bounds of the focused sprite will appear as an animated "marching ants" dotted line |
| Setting | Type | Default | Description |
|---|---|---|---|
defaultScreenshotPath |
string | "~/Desktop" |
The default location that all screenshots are saved unless otherwise specified. |
spriteScreenshotsEnabled |
boolean | true | Whether the capture will contain only the sprite image, not the full screen, if taken while in debug mode and focused on a single sprite. |
| Setting | Type | Default | Description |
|---|---|---|---|
toggleDebugModeKey |
character | "d" |
Key used to toggle Acetate's visual [D]ebugging mode on/off. |
toggleCentersKey |
character | "c" |
Key used to toggle drawing of sprite [C]enters while in debug mode. |
toggleBoundsKey |
character | "b" |
Key used to toggle drawing of sprite [B]ounds while in debug mode. |
toggleOrientationsKey |
character | "v" |
Key used to toggle drawing of sprite orientation [V]ectors while in debug mode. |
toggleCollideRectsKey |
character | "x" |
Key used to toggle drawing of sprite colli[X]ion rects while in debug mode. |
toggleInvisiblesKey |
character | "z" |
Key used to toggle debug drawing of invi[Z]ible sprites while in debug mode. |
toggleCustomDrawKey |
character | "m" |
Key used to toggle use of custo[M] sprite debugDraw functions. |
toggleFPSKey |
character | "f" |
Key used to toggle [F]PS display on/off. |
toggleSpriteCountKey |
character | "n" |
Key used to toggle display of the total sprite count. |
toggleDebugStringKey |
character | "?" |
Key used to toggle debug string display while focused a single sprite. |
printDebugInfoKey |
character | "I" |
Key used to print the debug [I]nfo for the currently selected sprite(s). |
cycleForwardKey |
character | "." |
Key used to cycle forward through sprites, one by one. |
cycleBackwardKey |
character | "," |
Key used to cycle backward through sprites, one by one. |
cycleForwardInClassKey |
character | ">" |
Key used to cycle forward to the next sprite of the same class as the focused sprite. |
cycleBackwardInClassKey |
character | "<" |
Key used to cycle backward through sprites of the same class as the focused sprite. |
cycleDebugGroupKey |
character | "G" |
Key used to cycle through all defined debug groups |
toggleClassFocusKey |
character | "l" |
Key used to [L]ock focus cycling to sprites of the same class as the focused sprite. |
togglePauseKey |
character | "p" |
Key used to [P]ause/unpause the game while in debug mode. |
captureScreenshotKey |
character | "q" |
Key used to [Q]uick-capture a screenshot. |
If you can't activate Acetate debug mode for your app in the simulator, check the following:
-
Installation. Be sure you've followed the installation instructions properly, that all the Acetate files are included in the directory, and that you've imported Acetate via the correct path relative to your source file.
-
Keyboard handler. Acetate implements the
playdate.keyPressedfunction, which provides shortcuts for, among other things, toggling its debug overlay. If you implementkeyPressedyourself, it will override Acetate's. In this case, you can call Acetate's from your own:function playdate.keyPressed(key) -- let Acetate handle any debug key presses acetate.keyPressed(key) -- perform your own key handling here end
If acetate keyboard handling interferes with your own, you can modify the keyboard shortcuts with custom settings, including the key used to enable/disable the acetate debug layer.
-
Debug draw. Acetate implements the
playdate.debugDrawfunction in order to render into the debug layer of the simulator. If you implementdebugDraw, it will override Acetate's. In this case, you can call Acetate's from your own:function playdate.debugDraw() -- let Acetate do its own debug drawing acetate.debugDraw() -- perform additional debug drawing here end
Note that you may not need to implement
playdate.debugDrawyourself if you leverage Acetate's support for implementingdebugDrawwithin your individual sprite classes. (You will, however, need to do your own debug drawing for anything not associated with sprites.)
Acetate attempts to adjust the draw offset so that debug drawing aligns properly with your sprites.
However, if your project adjusts the draw offset itself (via playdate.graphics.setDrawOffset()),
Acetate may not have sufficient knowledge to do so correctly. In this case, provide Acetate with
the appropriate draw offset by calling cacheDrawOffset from within your sprite's draw function:
function MySprite:draw()
self:cacheDrawOffset()
-- draw...
endAcetate is not initialized on-device. Attempting to access its members or call its functions outside
the simulator will cause Playdate to crash. You can do so safely from within any functions you write
(e.g. debugString(), debugDraw(), etc.) or within key handlers, as these will only be called inside
the simulator. Any access outside these contexts should be wrapped within a check to ensure acetate
has been initialized:
if acetate.initialized then
-- safe to access acetate members here, for example to call `acetate.focusSprite(mySprite)`
endAcetate is distributed under the terms of the MIT License.
