A list of helpful libraries and code

Thought I'd start a thread where we can share libraries and bits of code we've found helpful when developing for the Playdate that others may too.

23 Likes

I'll go first!

Sampler
This is a Lua class that graphs samples collected/frame against a specified sample duration. It only keeps in memory the number of samples that match the width of the area it is drawn to. I've used it to graph memory usage over time. But since it takes a function that returns a number, you can really graph any value over time (i.e. fps, mic input, game values, etc.).

Also, from the Playdate Simulator you can use this to inspect the stored samples using the console, simply type mysampler:print()

Here's a simple example that creates a Sampler that collects 100ms worth of samples (running at 30fps, that's about 3 samples—I think), averages them out and adds the average to the sample collection and then graphs the results over time.

local mem_sampler = Sampler(100, function()
   return collectgarbage("count")
end)

function playdate.update()
   mem_sampler:draw(10, 10, 50, 30)
end

Library

import "CoreLibs/object"
import "CoreLibs/graphics"

local graphics = playdate.graphics

class("Sampler").extends()

function Sampler:init(sample_period, sampler_fn)
	Sampler.super.init()
	self.sample_period = sample_period
	self.sampler_fn = sampler_fn
	self:reset()
end

function Sampler:reset()
	self.last_sample_time = nil
	self.samples = {}
	self.current_sample = {}
	self.current_sample_time = 0
	self.high_watermark = 0
end

function Sampler:print()
	print("")
	
	print("Sampler Info")
	print("=================")
	print("Now: "..self.samples[#self.samples].." KB")
	print("High Watermark: "..self.high_watermark.." KB")
	
	local current_sample_avg = 0
	for i, v in ipairs(self.samples) do
		current_sample_avg += v
	end
	current_sample_avg /= #self.samples
	print("Average: "..current_sample_avg.." KB")

	print("Log:")
	for i, v in ipairs(self.samples) do
		print("\t"..v.." KB")
	end
	
	print("")
end

function Sampler:draw(x, y, width, height)
	local time_delta = 0
	local current_time <const> = playdate.getCurrentTimeMilliseconds()
	local graph_padding <const> = 1
	local draw_height <const> = height - (graph_padding * 2)
	local draw_width <const> = width - (graph_padding * 2)
	
	if self.last_sample_time then
		time_delta = (current_time - self.last_sample_time)
	end
	self.last_sample_time = current_time
	
	self.current_sample_time += time_delta
	if self.current_sample_time < self.sample_period then
		self.current_sample[#self.current_sample + 1] = self.sampler_fn()
	else
		self.current_sample_time = 0
		if #self.current_sample > 0 then
			local current_sample_avg = 0
			for i, v in ipairs(self.current_sample) do
				current_sample_avg += v
			end
			current_sample_avg /= #self.current_sample
			self.high_watermark = math.max(self.high_watermark, current_sample_avg)
			if #self.samples == draw_width then
				table.remove(self.samples, 1)
			end
			self.samples[#self.samples + 1] = current_sample_avg
		end
		self.current_sample = {}
	end
	
	-- Render graph
	graphics.setColor(graphics.kColorWhite)
	graphics.fillRect(x, y, width, height)
	graphics.setColor(graphics.kColorBlack)
	for i, v in ipairs(self.samples) do
		local sample_height <const> = math.max(0, draw_height * (v / self.high_watermark))
		graphics.drawLine(x + graph_padding + i - 1, y + height - graph_padding, x + i - 1 + graph_padding, (y + height - graph_padding) - sample_height)
	end
end
4 Likes

Signal
A Lua class for subscribing to keys and notifying subscribers of that key.

Example
In this example we create a global instance of Signal, subscribe to a key, and notify against that key elsewhere. Notice that all of the values passed to Signal:notify are passed on to the subscribed functions.

-- ... creating a global variable in main ...
NotificationCenter = Signal()

-- ... in code that needs to know when score has changed ...
NotificationCenter:subscribe("game_score", self, function(new_score, score_delta)
   self:update_score(new_score)
end)

-- ... in code that changes the score ...
NotificationCenter:notify("game_score", new_score, score_delta)

Library

import "CoreLibs/object"

class("Signal").extends()

function Signal:init()
	self.listeners = {}
end

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
end

function Signal:unsubscribe(key, fn)
	local t = self.listeners[key]
	if t then
		for i, v in ipairs(t) do
			if v.fn == fn then
				table.remove(t, i)
				break
			end
		end
		
		if #t == 0 then
			self.listeners[key] = nil
		end
	end
end

function Signal:notify(key, ...)
	local t = self.listeners[key]
	if t then
		for _, v in ipairs(t) do
			v.fn(v.bind, key, ...)
		end
	end
end

11 Likes

State
A Lua class that allows you to subscribe to keys on an instance to be notified when the value for those keys change. Think of it like a Lua table with key-value observing.

Notice: this library requires my Signal library.

Example

GameState = State()
GameState.score = 0

-- ... in code that needs to know when game score changes ...
GameState:subscribe("score", self, function(old_value, new_value)
   if old_value ~= new_value then
      self:update_game_score(new_value)
   end
end)

-- ... in code that changes the game score, all subscribers of "score" on GameState will be notified of the new value ...
GameState.score = 5

Library

import "CoreLibs/object"

local allowed_variables = {
	__data = true,
	__signal = true
}

class("State").extends()

function State:init()
	self.__data = {}
	self.__signal = Signal()
end

function State:__newindex(index, value)
	if allowed_variables[index] then
		rawset(self, index, value)
		return
	end
	
	-- Give metatable values priority.
	local mt = getmetatable(self)
	if mt[index] ~= nil then
		rawset(mt, index, value)
		return
	end
	
	-- Store value in our shadow table.
	local old_value = self.__data[index]
	self.__data[index] = value
	
	-- Notify anyone listening about the change.
	self.__signal:notify(index, old_value, value)
end

function State:__index(index)
	if allowed_variables[index] then
		return rawget(self, index)
	end
	
	-- Give metatable values priority.
	local mt = getmetatable(self)
	if mt[index] ~= nil then
		return rawget(mt, index)
	end
	
	-- Fetch value from shadow table.
	return self.__data[index]
end

function State:subscribe(key, bind, fn)
	self.__signal:subscribe(key, bind, fn)
end

function State:unsubscribe(key, fn)
	self.__signal:unsubscribe(key, fn)
end
9 Likes

super cool Dustin, thanks for contributing these.

Just now realizing this might make more sense as a public thread for everybody once more devs are on the forum :laughing:

3 Likes

You could also make this code available via our public gitlab once that's available to the general public. We're going to let people host playdate-related code there.

5 Likes

:+1: Makes sense. Didn't realize there was a plan to open it up.

We have a second gitlab at dev.panic.com that's public

2 Likes

@dustin These are great! I remember you mentioned modifying the timer lib from Bytepath. Would you mind sharing that?

Yeah, sounds good. Let me clean it up a bit. I actually added a few things like a keyRepeat timer type, and a remade tween type that uses the SDK's built-in easing functions, and an animation type that takes an image table and repeat count (I intend to add support for an array of durations so I can customize how the animation is played back).

It's quite handy though, I can simply spawn timers like:

self.timers:keyRepeat(function()
   self:moveSelectionUp()
end, "up repeat")

The use of tags in Bytepath is great and helps ensure animations get cancelled when a timer of the same name is scheduled again. And being able to tween a table of values has been very handy when I want to keep, say, x,y values of a sprite animated together plus whatever other values I need.

Lastly, though I haven't used it, the script() method is interesting. A clever use of coroutines to allow you to run a set of steps (say a specific animation or dialog) and wait while the rest of the game chugs along.

Anyway, I'll try to prep it to share soon. It's a bit of a mess since I created that other tween method.

1 Like

I'd love to have a repo for things like this to either fork or do pull requests on. Shared libs can be a hassle, but we could save each other time on other things :muscle: Either way!

Fluid
Here's a little fluid simulation library. Includes demo. Feel free to use it wherever. Also if you make any improvements let me know! :playdate:

Docs
local fluid = Fluid.new(x, y, width, height, options)
options.tension adjusts surface stiffness.
options.dampening increases or decreases wave oscillation.
options.speed adjusts speed at which waves move.
options.vertices adjust the number of points on the surface.

fluid:setBounds(x, y, width, height)
Change the bounds of the fluid. Keep in mind that the surface of the fluid may peek outside these bounds.

fluid:reset()
Reset fluid surface so that it is still.

fluid:getPointOnSurface(x)
With x between the left and right side of the fluid surface, this function returns a point containing the x and y of the surface at x.

fluid:touch(x, velocity)
Push or pull on the surface at the vertex closest to x. A high velocity creates larger waves. A negative velocity pulls upward on the surface (simulate something coming out of the fluid).

fluid:update()
Call each frame to update the fluid simulation.

fluid:fill()
Draws the fluid filled (uses current color or pattern).

fluid:draw()
Draws an outline of the fluid (uses current context line/color properties).

Library
fluid.zip (16.1 KB)

fluid-toys

26 Likes

This is GREAT, @dustin! I'll be playing with this in the coming days, and will definitely share back anything I add to it

1 Like

I've updated Betamax, my library that record game session so I thought I would post it here.

betamax.lua.zip (3.9 KB)

  • I fix a big bug. Earlier I didn't record readings from datastoreRead() so if you had any change in your save file, it would screw up the replay. Now it should read to the game the same reads that was done during the initial recording.
  • Changed how to trigger the replay. Just pressing left when booting the game. Easier and it works fine on the simulator and the device.
  • I also added a fast forward feature. During replay press right and everything will be faster.

betamax_fastforward

10 Likes

Today I finally cleaned up a library that I've been using since quite a while now and that I find very helpful to add animation.

sequence.lua.zip (2.5 KB)

It allows you to play a sequence of easings. It's so much easier to add more interesting or complex animations.

In our game I'm still using my own easing library (that I also used for quite some time) so I had to refactor a bit to use the playdate ones. Maybe that introduced some bugs. Let's hope not.

So has an example, this is how I animated out logo when you arrive on the main menu.

pick_anim_x = sequence.new():from(150):sleep(0.4):to(50, 0.3, "outCirc"):to(0, 0.5, "outExpo")
pick_anim_y = sequence.new():from(-240):sleep(0.4):to(0, 0.3, "inQuad"):to(-30, 0.2, "outBack"):to(0, 0.5, "outBounce")

pack_anim_x = sequence.new():from(150):sleep(0.2):to(50, 0.3, "outCirc"):to(0, 0.2, "outExpo"):start()
pack_anim_y = sequence.new():from(-240):sleep(0.2):to(0, 0.3, "inQuad"):to(-30, 0.2, "outBack"):to(0, 0.5, "outBounce")

pup_anim_x = sequence.new()
pup_anim_y = sequence.new():from(-240):to(0, 0.5, "outBack")

And this is the result
logo
SequenceExample.zip (27.9 KB)

In your code you just have to call the update function
sequence.update()

To trigger an animation just call
pick_anim_x:start()

And to get the value
pick_anim_x:get()

That's it :playdate_relaxed:

Update September 2022
it is on GitHub now GitHub - NicMagnier/PlaydateSequence: Create animations with simple sequences of easing functions in your playdate game

19 Likes

Super cool! I'll use it

Here a list of little functions I find handy. Nothing fancy or even technical but some good QoL.

Clamp

function math.clamp(a, min, max)
    if min > max then
        min, max = max, min
    end
    return math.max(min, math.min(max, a))
end

Not sure why clamp is still not part of lua but this one is a must have.

Ring

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

function math.ring_int(a, min, max)
    return math.ring(a, min, max+1)
end

Like clamp but instead of clamping it loop back to the start. Useful to cycle through values, for example an index in a menu
index = math.ring_int( index + 1, 1, 4)
-> 1, 2, 3, 4, 1, 2, 3, 4 etc.

Approach

function math.approach( value, target, step)
    if value==target then
        return value, true
    end

    local d = target-value
    if d>0 then
        value = value + step
        if value >= target then
            return target, true
        else
            return value, false
        end
    elseif d<0 then
        value = value - step
        if value <= target then
            return target, true
        else
            return value, false
        end
    else
        return value, true
    end
end

Got this one from the Celeste source code and it so simple but so useful. It just change a value toward a target. It returns a tuple, the new value and a boolean that says if it is on target.

Approach to infinity (but not beyond)

function math.infinite_approach(at_zero, at_infinite, x_halfway, x)
    return at_infinite - (at_infinite-at_zero)*0.5^(x/x_halfway)
end

Another approach function but with this one it never reaches the target. Useful for example to balance a game mode to always increase the difficulty but have a clear ceiling. You now the lowest and highest value (at_zero, at_infinite), you specify at which point you are midway and the rest is a nice natural curve.

So for example if you want to generate new enemies that get trickier as the playtime progress. The first enemies start with just 1 health, and eventually enemies can go up to 20. We can balance that we start to see enemies with 10 health after 5 minutes.
new_enemy.health = math.infinite_approach(1, 20, 5*60, playtime_in_seconds)

Random Element in a table

function table.random( t )
    if type(t)~="table" then return nil end
    return t[math.ceil(math.random(#t))]
end

Just return a random element in an array. Would be even better if lua had an even better RNG.

Call function for each array element

function table.each( t, fn )
	if type(fn)~="function" then return end
	for _, e in pairs(t) do
		fn(e)
	end
end

As I said, nothing special but useful.
What are your little functions you couldn't live without anymore?

8 Likes

This looks great! Looking forward to trying it out.

These are great! I have a couple functions like this, too, especially math.clamp.

I also use:

math.round and math.sign

-- from http://lua-users.org/wiki/SimpleRound
-- rounds v to the number of places in bracket, i.e. 0.01, 0.1, 1, 10, etc
function math.round(v, bracket)
  local bracket = bracket or 1
  return math.floor(v/bracket + math.sign(v) * 0.5) * bracket
end
-- round needs sign:
function math.sign(v)
  return (v >= 0 and 1) or -1
end

Deep Table Copy
Duplicates a table's contents into a new table, rather than just a reference to the original:

function deep_copy(obj)
  if type(obj) ~= 'table' then return obj end
  local res = {}
  for k, v in pairs(obj) do res[deep_copy(k)] = deep_copy(v) end
  return res
end

Random Unique UUID

function UUID()
  local fn = function(x)
    local r = math.random(16) - 1
    r = (x == "x") and (r + 1) or (r % 4) + 9
    return ("0123456789abcdef"):sub(r, r)
  end
  return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end

And I use various functional helpers from lua-users wiki: Functional Library.

3 Likes