A list of helpful libraries and code

I absolutely love how these chain together. Thank you for sharing this!

I have one question that might be dumb — and is basically academic, because it's not a big deal: would it be possible to write this in a way that wouldn't require sequence.update() to be called every frame? Can the computation of the value just be done when get() is called?

Oh… now that I see you've added callbacks, I guess those wouldn't work properly if you didn't have .update() called every frame.

Yeah it was already like that before the callbacks because that's a design pattern that I like. An update function is called once and all sequence object will be based on that. With just .get() you might actually have different result depending when it's called in the code because the timing is slightly different. So you might end up with two sprites using the same sequence but get slightly different results which mean they might be off by few pixels.

But that's true it would have been a simpler design to just have a getter which is also extremely valuable.

string manipulation in lua can be a costly operation so I would recommend using this round function only when you really need the precision.

I don't recall seeing any lua function benchmark. That could be useful but also a lot of work I guess (I will definitively not do that :sweat_smile:)

But for performance this lua documentation is always a good reference and in this case how string are handheld in lua.

I wanted to revisit the rounding function. So I write one that is less susceptible to float precision errors. After some testing I didn't encounter a single precision issue.

function math.round(number, precision)
	local p = math.pow(10,precision)
	local half = (number >= 0 and 0.5) or -0.5
	return (number*p+half)//1/p
end

But I also benchmarked the different versions and to my surprise the string version is definitively not as bad as I would have thought. It is slower but it is still close enough to use without concerns IMO. I also run the benchmark code from within a bigger codebase with more strings and I still similar results.

Update:
It should be noted also that the round function used by Nick is actually much more flexible. So I rewrote a new one to keep it precise and flexible and also twice as fast as before because I like this type of pointless exercise

function math.round( number, bracket )
	bracket = bracket or 1
	
	-- path for additional precision 
	if bracket<1 then
		bracket = 1//bracket
		local half = (number >= 0 and 0.5) or -0.5
		return (number*bracket+half)//1/bracket
	end

	local half = (number >= 0 and bracket/2) or -bracket/2
	return ((number+half)//bracket)*bracket
end
Rounding Benchmark - 500000 calls
string > 430 ms
splendorr > 160 ms
nic (new) > 77 ms
nic > 145 ms

This is such a common function I wonder if it would make sense to have a version written in C added to the math table. @dan ?

Sounds like it would be a good addition! Nobody's written a C version yet, right? I'd be curious to benchmark the speed difference.

This is great! I appreciate seeing all these different approaches to the same problem. Using // instead of math.floor, and Lua's short-circuit comparators instead of math.sign, are all smart changes. I no idea you could just use -bracket as an equivalent to -1 * bracket, though! I think there are... about a hundred places in my code I code make that change :sweat_smile: :playdate_crying:

I'm switching over to this newest version in my code, and will test to make sure it works everywhere as expected! Thanks as always for your expertise, @Nic! <3

Hey @dustin! Hope all your stuff is going well! I just tried the Signal and State libraries for the first time, and while they appear to mostly-work out of the box, I had some trouble with the State subscription functions. I worked out that they're receiving more arguments than the example suggested; where you have function(old_value, new_value), the function is actually receiving nil(not sure where this is coming from?), key, old_value, new_value. Could you tell me if that's expected, or if I've hooked up something wrong? It's usable as-is, but I'll just need to call the function with (_, _, old_value, new_value).

Anyway, here's a little project that illustrates the issue: press left or right to increment or decrement the number GameState.wall (because I'm tracking which wall I have selected in our game), with console output printing the (...) args passed into the subscribed function.

splendorr-state-test.zip (15.3 KB)

And the main.lua for reference, which just imports the Signal and State libraries as written in your post above:

import 'CoreLibs/graphics'
import 'Signal'
import 'State'

GameState = State()
GameState.wall = 1
print('GameState.wall starts at '.. tostring(GameState.wall))

local gfx = playdate.graphics

function playdate.update()
  gfx.clear()
  if playdate.buttonJustPressed('left') then
    print('Pressed Left! Subtracting 1')
    GameState.wall = GameState.wall - 1
  end
  if playdate.buttonJustPressed('right') then
    print('Pressed Right! Adding 1')
    GameState.wall = GameState.wall + 1
  end

  gfx.drawTextAligned(GameState.wall, 200, 120, kTextAlignment.center)
end

--[[ The provided example has 2 args:
GameState:subscribe("score", self, function(old_value, new_value) end
but the usage below receives 4, which are: unknown, key, old_value, new_value
My question is, what's the first nil (implicit self?) and is the key expected, even though the example doesn't have it?
]]--

GameState:subscribe('wall', self, function(...)
  print('Subscription args:')
  print(...)
  print('GameState.wall is '.. tostring(GameState.wall))
end)

And here's the console output, running the game and then pressing right twice:

GameState.wall starts at 1
Pressed Right! Adding 1
Subscription args:
nil	wall	1	2
GameState.wall is 2
Pressed Right! Adding 1
Subscription args:
nil	wall	2	3
GameState.wall is 3

Thanks!

So looking at your code, you're passing in self which is undefined in this context. I wanted to allow you to use this nicely within the context of an object where you would want self bound to your object. Lua passes self as the first argument.

I think I need to detect if the context is nil then call the function without it so this would work as expected. Also I will change it where subscribe accepts just key and fn (no bind arg) so you can use this in a global context.

Thanks for the report!

Riiiiight, that makes sense! I thought the self was probably not referring to anything; but I'd tried just passing in nil and, as you say, it's not designed for that!

BUT I would like to say that this has been SO helpful. I used it to set up dialogue event triggers based on different state changes, and am seeing other places to use it immediately. I remain so grateful to you for sharing your experiments and knowledge, @dustin! :smiley:

Found this helpful when constructing patterns for drawing with.

function utils.printPattern(rows_of_bits)
  local pattern = {}
  for k, row in ipairs(rows_of_bits) do
    local bit_position = #row
    local row_value = 0x00
    for i, bit in ipairs(row) do
      if bit == 1 then
        row_value = row_value + (math.pow(2, bit_position)//2)
      end
      bit_position -= 1
    end
    pattern[#pattern + 1] = row_value
  end
  
  local print_string = "{ "
  for i, v in pairs(pattern) do
    print_string = print_string .. string.format("0x%02X", v)
    if i < #pattern then
      print_string = print_string .. ", "
    end
  end
  print_string = print_string .. " }"
  
  print(print_string)
end

Which allows me to prototype a pattern in code quick and output a value that I can pass into, say, playdate.graphics.setPattern(...).

	utils.printPattern({
		{1, 1, 1, 1, 1, 1, 1, 1},
		{1, 1, 1, 1, 1, 1, 1, 1},
		{0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0},
		{1, 1, 1, 1, 1, 1, 1, 1},
		{1, 1, 1, 1, 1, 1, 1, 1},
		{0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0}
	})

Outputs:

{ 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00 }

If there is an easier way to do this, I will not be surprised. :wink:

HAHA tonumber has a optional base param :playdate_shocked:

Thanks! I'll just delete all of this now.

To work with patterns in the Playdate, I recommend using this small tool: GFXP · Pattern Editor for Playdate
I hope it will be useful to you.

I just discovered the strict mode script in lua that check if the variables you use have been properly initialized. It is useful to prevent making typos in your variable that can go easily unnoticed and create bugs.

http://www.lua.org/extras/5.2/strict.lua

In my code I just added a check so that it would be used only in the simulator

-- strict.lua
-- checks uses of undeclared global variables
-- All global variables must be 'declared' through a regular assignment
-- (even assigning nil will do) in a main chunk before being used
-- anywhere or assigned to inside a function.
-- distributed under the Lua license: http://www.lua.org/license.html

if not playdate.isSimulator then
  return
end

local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget

local mt = getmetatable(_G)
if mt == nil then
  mt = {}
  setmetatable(_G, mt)
end

mt.__declared = {}

local function what ()
  local d = getinfo(3, "S")
  return d and d.what or "C"
end

mt.__newindex = function (t, n, v)
  if not mt.__declared[n] then
    local w = what()
    if w ~= "main" and w ~= "C" then
      error("assign to undeclared variable '"..n.."'", 2)
    end
    mt.__declared[n] = true
  end
  rawset(t, n, v)
end
  
mt.__index = function (t, n)
  if not mt.__declared[n] and what() ~= "C" then
    error("variable '"..n.."' is not declared", 2)
  end
  return rawget(t, n)
end

Update: It seems this piece of code is actually already in the SDK you just have to import 'CoreLibs/strict'

This is random but I just wrote this function to generate an array of midi notes. Maybe someone some day will find it useful besides me.

function generateMidiNotes(octave, offset, note_count, reverse_order)
	local base <const> = {24, 26, 28, 29, 31, 33, 35}
	local notes = table.create(note_count)
	
	local index_start <const> = reverse_order and (note_count-1) or 0
	local index_end <const> = reverse_order and 0 or (note_count-1)
	local index_inc <const> = reverse_order and -1 or 1
	local note_i = 0
	
	for i = index_start, index_end, index_inc do
		notes[note_i + 1] = base[((offset + i) % #base) + 1] + (12 * (math.floor((offset + i) / #base) + octave))
		note_i += 1
	end
	
	return notes
end

I'll happily be your first customer on this one, Dustin : D
I'm having a hard time understanding how to generate good random musical scales for my game, it's all very new to me.

Here a very simple parser to get paths from a svg file.

It returns a table of paths which are tables or coordinate { x1, y1, x2, y2, x3, y3, ... }

function getSvgPaths( svg_filepath )
	local file, file_error = playdate.file.open( svg_filepath, playdate.file.kFileRead )
	assert(file, "getSvgPaths(), Cannot open file", svg_filepath," (",file_error,")")

	local push = table.insert
	local commandArgCount = { M=2, L=2, T=2, H=1, V=1, C=6, S=6, A=7, Z=0}

	-- read the whole file
	local fileContent = ""
	repeat
		local line = file:readline()
		if line then
			fileContent = fileContent..line
		end
	until not line

	local result = table.create( 8 )
	for path in fileContent:gmatch("<path.-/>") do
		local previousX, previousY = 0, 0
		local newPath = table.create( 8 )

		local name = path:match("id=\"(.-)\"")
		if not name then name = #result + 1 end
		result[name] = newPath

		local d_content = path:match(" d=\"(.-)\"")
		for command in d_content:gmatch("%a[%-%d%., ]*") do
			local first_character = command:sub(1,1)
			local command_letter = first_character:upper()
			local absolute_coordinates = command_letter==first_character

			local args = table.create( 6 )
			for number in command:gmatch("[-%d%.]+") do
				push(args, tonumber(number))
			end
			local argCount = commandArgCount[ command_letter ]

			local argIndex = 0
			while argIndex+argCount<=#args do
				local relativeX, relativeY = 0, 0
				if not absolute_coordinates then
					relativeX, relativeY = previousX, previousY
				end

				if command_letter=="M" or command_letter=="L" or command_letter=="T" then
					push( newPath, args[argIndex+1] + relativeX)
					push( newPath, args[argIndex+2] + relativeY)
				elseif command_letter=="H" then
					push( newPath, args[argIndex+1] + relativeX)
					push( newPath, previousY)
				elseif command_letter=="V" then
					push( newPath, previousX)
					push( newPath, args[argIndex+1] + relativeY)
				elseif command_letter=="C" then
					push( newPath, args[argIndex+5] + relativeX)
					push( newPath, args[argIndex+6] + relativeY)
				elseif command_letter=="S" then
					push( newPath, args[argIndex+3] + relativeX)
					push( newPath, args[argIndex+4] + relativeY)
				elseif command_letter=="A" then
					push( newPath, args[argIndex+6] + relativeX)
					push( newPath, args[argIndex+7] + relativeY)
				elseif command_letter=="Z" then
					push( newPath, newPath[1])
					push( newPath, newPath[2])
				end

				previousX = newPath[#newPath-1]
				previousY = newPath[#newPath]

				argIndex = argIndex + math.max(argCount, 1)
			end
		end
	end

	for rect in fileContent:gmatch("<rect.-/>") do
		local width = tonumber( rect:match("width=\"([-%d%.]+)\"") )
		local height = tonumber( rect:match("height=\"([-%d%.]+)\"") )
		local x = tonumber( rect:match("x=\"([-%d%.]+)\"") )
		local y = tonumber( rect:match("y=\"([-%d%.]+)\"") )

		local name = rect:match("id=\"(.-)\"")
		if not name then name = #result + 1 end
		result[name] = {
			x, y,
			x+width, y,
			x+width, y+height,
			x, y+height,
			x, y,
		}
	end

	return result
end

Update

  • Fix various bugs
  • return a hashmap with the names of the path
  • Increase compatibility
  • added support for rectangle primitive

So when I was working on the LDtk level loader, something that was bothering me was that parsing a level was not fast enough. To solve this issue, after reading a level, I was exporting the result in a lua file that can be used instead of the json file.

I wanted to share a simpler version of this that could be used more generally. But the general idea:

  1. When running in the simulator, after parsing a file we write the result as a lua file in the save folder (In the SDK folder)
  2. The lua file is copied in the project folder
  3. When running on the console if the lua file is present, load it instead of parsing the file
  4. Save the frame!

The first piece of the puzzle is the following function that export a table as a lua file

function writeLua( filepath, table_to_export )
	assert( filepath, "writeLua, filepath required")
	assert( table_to_export, "writeLua, table_to_export required")

	local file, file_error = playdate.file.open( filepath, playdate.file.kFileWrite)
	if not file then
		print("writeLua, Cannot open file ", filepath," (", file_error, ")")
		return
	end

	local _isArray = function( t )
		if type(t[1])=="nil" then return false end

		local pairs_count = 0
		for key in pairs(t) do
			pairs_count = pairs_count + 1
			if type(key)~="number" then
				return false
			end
		end

		return pairs_count==#t
	end

	local _write_entry
	_write_entry = function( entry, name )
		local entry_type = type(entry)

		if entry_type=="table" then
			file:write("{")
			if _isArray( entry ) then
				for key, value in ipairs(entry) do
					_write_entry(value, key)
					file:write(",")
				end
			else
				for key, value in pairs(entry) do
					file:write("[\""..tostring(key).."\"]=")
					_write_entry(value, key)
					file:write(",")
				end
			end
			file:write("}")
		elseif entry_type=="string" then
			file:write("\""..tostring(entry).."\"")
		elseif entry_type=="boolean" or entry_type=="number" then
			file:write(tostring(entry))
		else
			file:write("nil")
		end
	end

	file:write("return ")
	_write_entry( table_to_export )

	file:close()
end

To actually parse or load the lua file directly I have the following code

-- set _enable to false to always load the original file
local _enable = true

-- folder in the project folder where the pre parsed file will be
local _folder = "preParsed/"

function parseFile( parser_fn, filename, ...)
	local pdzFilename = _folder..filename..".pdz"

	if _enable then
		if playdate.file.exists( pdzFilename ) then
			return playdate.file.run( pdzFilename )
		else
			print( "parseFile(): The following file is not pre-parsed", filename)
		end
	end

	return parser_fn( filename, ...)
end

if playdate.isSimulator then
	parseFile = function( parser_fn, filename, ...)
		local result = parser_fn( filename, ...)

		-- save result in lua file
		local luaFilename = _folder..filename..".lua"
		playdate.file.mkdir( luaFilename:match("^(.-)[^/]*$") )
		writeLua( luaFilename, result)

		return result
	end
end

To use it I simply replace a call to a parsing function with it

So for example instead of
level = json.decodeFile( "level_1-1.json" )

I would call it this way
level = parseFile( json.decodeFile, "level_1-1.json" )

Using "level_1-1.json" from the SDK example as a comparaison, loading the pre-parsed lua file is 5 times faster on the playdate than parsing the json file normally. For the svg parser I posted earlier the advantage is even more pronounced since this is 10 times faster.

Big caveats
The biggest drawback of this technique is that you need to be aware of the cached files otherwise you might have edit the original file and the game will still load the previous pre-parsed version. It get even trickier since at the moment files are not deleted when uploading a game to the playdate (so you might delete all the pre-parsed in your project but on playdate it will still load some file you don't even know are still there)

Tips
I created a symbolic link in my save folder to the "preParsed/" folder in my project so that I don't have to manually copy the files.
I also wrote a function in my project to pre-parsed all files in one go. Right now I call it when the game start since there is not that much files but later I might simply call it from the simulator console when I need to create a build.