In my build process I manage something similar by copying the timestamp from the original file to the processed file. Then I compare file timestamps to see if I need to reprocess the file.
But I don't think it's currently possible to do this on the device using standard SDK/Lua functions. SDK can at least get modified timestamp but can't set it.
This is something I experimented with. The best solution I came up with was to have a symbolic link to the whole project folder in the save folder to be able to check the file modification time for the original file, not the one in the pdx.
But requiring proper setup of the save folder for the feature to work, I thought is was getting over-complicated so I preferred a simpler solution.
I would actually prefer to have full read/write access to my project folder to be honest, that would work way better (similar to playdate.simulator.writeToFile(image, path)). But when I thought about it, would I want all game running in the simulator to have full access to my files? mmmh not really.
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.
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.
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
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.
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
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.
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).
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: GitHub - jm/playdate_examples: Examples for the Playdate SDK I'll be adding to this as I experiment with more of the APIs for sure.
fosterdouglas
(Foster Douglas / Everyday Lemonade)
88
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:
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
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!
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
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
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