A list of helpful libraries and code

Put together a tiny Playdate utility to experiment with the built-in easing functions. It’s a single lua file, easy to build and run on device. Not a bad way to tune animations and experiment with additional ease settings like softness for back and amplitude/period for elastic. Also nice to get a sense of how an animation may feel on device.

main.lua.zip (2.6 KB)

up/down change selected setting.
right/left change selected value.
hold A while pressing left/right to change selected value by smaller increments.
press A to run animation again.

playdate-animation-tester-3

14 Likes

Just used this for the first time and it worked great! I had an issue trying to unsubscribe when I was using an anonymous function.

A simple solution I found while looking at similar libs is to return the function that was passed to the subscribed function and then use that for unsubscribing.

function Signal:subscribe(key, bind, fn)
	local t = self.listeners[key]
	local v = {fn = fn, bind = bind}
	if not t then
		self.listeners[key] = {v}
	else
		t[#t + 1] = v
	end
	return fn
end

And then

function Init()
	self.fn = signal:subscribe(
		"eye-colected", self, function(ref, event, x, y, eyesQuota, index)
			...
		end
	)
end

function Remove()
	signal:unsubscribe("eye-colected", self.fn)
	EyeCounter.super.remove(self)
end
1 Like

Thanks for this. I’ll think about this a bit!

Here is a set of helpers I found useful while working in C, you need to initialize asap and not use until you have initialized or you will get crashes. Doing it first thing in the event handler should be fine.

pd_memory_tools.h

#ifndef PD_MEMORY_TOOLS_H
#define PD_MEMORY_TOOLS_H

#include "pd_api.h"

void initializeMemoryTools(PlaydateAPI *playdate);

static void* pd_malloc(size_t size);
static void* pd_calloc(size_t count, size_t size);
static void* pd_realloc(void* ptr, size_t size);
static void  pd_free(void* ptr);

#endif

pd_memory_tools.c

#include "pd_memory_tools.h"

static void* pd_realloc(void* ptr, size_t size) {
    return pd->system->realloc(ptr, size);
}

static void* pd_malloc(size_t size) {
    return pd_realloc(NULL, size);
}

static void* pd_calloc(size_t count, size_t size) {
    return memset(pd_malloc(count * size), 0, count * size);
}

static void pd_free(void* ptr) {
    pd_realloc(ptr, 0);
}

static PlaydateAPI *pd = NULL;
void initializeMemoryTools(PlaydateAPI *playdate) {
    pd = playdate;
}
3 Likes

Online Json to lua table converter: JSON Lua table converter - MAGEDDO

I use this for static level data. I put the lua files in my source folder and load the resulting pdz that pdc automatically creates with file.run

3 Likes

Here's a lightweight drop-in replacement for playdate.graphics.image that adds support for animated images: Lightweight AnimatedImage Library

Based on a conversation in the Playdate Squad Discord earlier I came up with some functions for drawing bezier curves in the Playdate Lua SDK by using line segments. Both quadratic beziers (3-point) and cubic beziers (4-point, the kind you'd find in most vector drawing apps) are supported!

This site has a nice interactive demo to compare both kinds of curves and the step parameter: A Primer on Bézier Curves

6 Likes

I'll add this here too in case the announcement thread falls by the way side. I'm working on a little Amiga mod player, blast from the past but could be very useful given the memory constraints if we can get it to run fast enough on real hardware.

2 Likes

You may want to check for overflow when implementing calloc. It's one of the reason it's best to use calloc versus malloc with a total size to avoid buffer overruns (although on PlayDate it probably will not be an issue given the lack of user input on these calls).

Something like this:

void* pd_calloc(size_t nb_of_items, size_t item_size)
{
    if (item_size && (nb_of_items > (SIZE_MAX / item_size))) {
        return NULL;
    }

    size_t size = nb_of_items * item_size;
    void* memory = pd_malloc(size);
    if(memory != NULL) {
        memset(memory, 0, size);
    }
    
    return memory;
}

I built a dependency/package manager for Playdate: https://github.com/jm/toybox Just got the binary releases up (accessible at Release First release! · jm/toybox · GitHub) so I hope folks can give it a spin. I'm working on getting it published in package managers etc so it's easier to upgrade.

I also published a repo of examples for things that don't currently have single file examples so that they have focused example scripts to learn from: https://github.com/jm/playdate_examples I'll be adding to this as I experiment with more of the APIs for sure. :slight_smile:

6 Likes

Amazing! I'm working on a little eurorack modular synth app for the Playdate, but I love trackers. My Polyend Tracker is my fav piece of gear!

8 Likes

Mentioned in a discussion thread I created, but wanted to drop this here in case it's useful to anyone checking this thread in the future. I started work on a tool to more easily view and choose dither patterns directly on device:

Thanks!

2 Likes

Show Toast message (temporary text that pop up on the screen) code:

local function showToast(text, duration)
    local t = playdate.frameTimer.new(duration)
    t.updateCallback = function()
        Graphics.drawTextAligned(text, 200, 70, kTextAlignment.center)
    end
end

Feel free to change text position (200, 70)

6 Likes

I love how simple and useful it is.
Really nice function!

1 Like

Here's a function I wrote to draw an image tiled within bounds with an optional offset. The built-in image:drawTiled(...) does not take a draw offset, so the tiled image is always tiled from 0, 0 of the destination rect. Ehh, it appears to work! :wink:

function drawTiledImage(img, bounds, offset_x, offset_y)
	offset_x = offset_x or 0
	offset_y = offset_y or 0

	-- Take easy route when no offset is specified.
	if offset_x == 0 and offset_y == 0 then
		img:drawTiled(bounds)
		return
	end

	local iw, ih = img:getSize()
	local sx = math.abs(offset_x % iw) - iw + bounds.x
	local sy = math.abs(offset_y % ih) - ih + bounds.y
	
	local cx, cy, cw, ch = playdate.graphics.getClipRect()
	playdate.graphics.setClipRect(bounds)
	img:drawTiled(sx, sy, bounds.width - sx, bounds.height - sy)
	playdate.graphics.setClipRect(cx, cy, cw, ch)
end
1 Like

Converted the Roomy scene manager lib over to be useable with playdate

github

::edit::
Finally added a read me :stuck_out_tongue:

5 Likes

Here's a simple class I made to make a scrolling parallax background simple

local pd <const> = playdate
local gfx <const> = pd.graphics

class("Parallax").extends(gfx.sprite)

function Parallax:init()
 Parallax.super.init(self)
 self.layers = {}
end

function Parallax:draw(...)
 gfx.setClipRect(...)
 for _, layer in ipairs(self.layers) do
   local img = layer.image
   -- lock offset to steps of 2 to reduce flashing
   local offset = layer.offset - (layer.offset % 2)
   local w = layer.width
   img:draw(self.x+offset, self.y)
   if offset < 0 or offset > w - self.width then
     if offset > 0 then
       img:draw(self.x+offset-w, self.y)
     else
       img:draw(self.x+offset+w, self.y)
     end
   end
 end
 gfx.clearClipRect()
end

function Parallax:addLayer(img, depth)
 local w, _ = img:getSize()
 local layer = {}
 layer.image = img
 layer.depth = depth
 layer.offset = 0
 layer.width = w
 table.push(self.layers, layer)
end

function Parallax:scroll(delta)
 for _, layer in ipairs(self.layers) do
   layer.offset = math.ring(
     layer.offset + (delta * layer.depth),
     -layer.width, 0
   )
 end
 self:markDirty()
end

it also uses the math.ring function that was posted is this thread somewhere (I can't find it atm)

function math.ring(a, min, max)
  if min > max then
    min, max = max, min
  end
  return min + (a-min)%(max-min)
end

It's simple to use:

img_1 = gfx.image.new("images/parallax-1")
img_2 = gfx.image.new("images/parallax-2")

-- create
local parallax = Parallax()
parallax:setSize(400,240)
parallax:addLayer(img_1, 0.2)
parallax:addLayer(img_2, 0.6)
parallax:add()

-- scroll
parallax:scroll(10)

parallax

7 Likes

Pulled code from Lua SDK to read cranks ticks and wrapped it in an object. The reason for this is that the SDK code uses a local global vars which can be an issue if you want to read the number of ticks, or even with a different ticksPerRevolution value. Each reading resets the sampler.

So, with this code you simply create a Ticker, and sample each frame or whatever. You can reset it too during periods where you don't need samples.

You can of course create multiple Tickers each with their own ticksPerRevolution values to sample at different rates.

local ticker = Ticker.new(20)

-- elsewhere:

function update()
   local ticks = ticker:sample()
end

Anyway, I've found it helpful so thought you may too.

Ticker = {}
Ticker.__index = Ticker

function Ticker.new(ticks_per_revolution)
	local ticker = {}
	setmetatable(ticker, Ticker)
	
	ticker.ticks_per_revolution = ticks_per_revolution
	ticker.last_reading = nil
	
	return ticker
end

function Ticker:setTicksPerRevolution(ticks_per_revolution)
	self.ticks_per_revolution = ticks_per_revolution
end

function Ticker:reset()
	self.last_reading = nil
end

function Ticker:sample()
	local totalSegments = self.ticks_per_revolution
	local degreesPerSegment = 360 / self.ticks_per_revolution
	
	local thisCrankReading = playdate.getCrankPosition()
	local lastCrankReading = self.last_reading
	if lastCrankReading == nil then
		lastCrankReading = thisCrankReading
	end
	
	-- if it seems we've gone more than halfway around the circle, that probably means we're seeing:
	-- 1) a reversal in directiotn, not that the player is really cranking that fast. (a good assumption if fps is 20 or higher; maybe not as good if we're at 2 fps or similar.) 
	-- 2) a crossing of the 359->0 border, which gives the appearance of a massive crank change, but is really very small.
	-- both these cases can be treated identically.
	local difference = thisCrankReading - lastCrankReading
	if difference > 180 or difference < -180 then
		
		if lastCrankReading >= 180 then
			-- move tick_lastCrankReading back 360 degrees so it's < 0. It's the same location, just it is unequivocally lower than thisCrankReading
			lastCrankReading -= 360
		else
			-- move tick_lastCrankReading ahead 360 	degrees so it's > 0. It's the same location, just now it is unequivocally greater than thisCrankReading.
			lastCrankReading += 360
		end
	
	end
	
	-- which segment is thisCrankReading in?
	local thisSegment = math.ceil(thisCrankReading / degreesPerSegment)
	local lastSegment = math.ceil(lastCrankReading / degreesPerSegment)
	local segmentBoundariesCrossed = thisSegment - lastSegment
	
	-- save off value
	self.last_reading = thisCrankReading
	
	return segmentBoundariesCrossed	
end
2 Likes

Determine tilt angle from reference point using accelerometer.

Spent a bit of time trying to figure this out so thought I'd just share here. If you want to determine the angle a player has tilted the device vertically (along the Y), I've so far found this to work pretty well:

-- Store reference position which to measure tilt from.
local _, ay, az = playdate.readAccelerometer()
local start_v = geometry.vector2D.new(ay, az)

-- Elsewhere, measure angle off reference position
local _, ay, az = playdate.readAccelerometer()
local current_v = geometry.vectory2D.new(ay, az)
local tilt_angle = start_v:angleBetween(current_v)

I think that's it. Though I haven't tried it, it seems you'd be able to measure tilt along the X axis by replacing ay with ax (the first value off readAccelerometer that I'm ignoring in this example).

OK! Happy, um, accelerating?

7 Likes

Cheat Codes

I've been working on adding some cheats to my game, figured I'd share the class here incase anyone else would like it.

local keys = {
  a = playdate.kButtonA,
  b = playdate.kButtonB,
  up = playdate.kButtonUp,
  down = playdate.kButtonDown,
  left = playdate.kButtonLeft,
  right = playdate.kButtonRight
}

class("CheatCode").extends()

function CheatCode:init(...)
  local seq = {}
  for _, key in ipairs({...}) do
    local v = keys[key]
    assert(v, "CheatCode: unknown key given => "..tostring(key))
    table.insert(seq, v)
  end

  self._seq = seq
  self.progress = 1
  self.completed = false
  self.run_once = true
  self:setTimerDelay(400)
end

function CheatCode:update()
  -- exit early if complete
  if self.run_once and self.completed then return end

  local _, pressed, _ = playdate.getButtonState()
  -- exit early if no button currently pressed
  if pressed == 0 then return end

  if pressed == self._seq[self.progress] then
    self.progress += 1
    self._timer:reset()

    if self.progress > #self._seq then
      self.completed = true
      if type(self.onComplete) == "function" then
        self.onComplete()
      end
    end
  else
    self:reset()
  end
end

function CheatCode:reset()
  self.progress = 1
  self._timer:reset()
  self._timer:pause()
end

function CheatCode:setTimerDelay(ms)
  if self._timer then
    self._timer:remove()
  end
  self._timer = playdate.timer.new(ms, function() self:reset() end)
  self._timer:pause()
  self._timer.discardOnCompletion = false
end

function CheatCode:nextIs(key)
  return keys[key] == self._seq[self.progress]
end

Using it is simple enough

-- initialize 
local cheat = CheatCode("up", "up", "down", "down", "left", "right", "left", "right", "b", "a")
cheat.onComplete = function() print("cheat") end
-- in update function
playdate.timer.updateTimers() -- uses timers so make sure you call this
cheat:update()

By default it'll only trigger the first time the sequence is called, can be changed by setting run_once to false

CheatCode:nextIs([key]) is a helper to allow you to avoid triggering other effects while the code is being entered. eg:

if not cheat:nextIs("a") then
    -- do something else
end
4 Likes