A list of helpful libraries and code

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

Kubrick
A library to help organize and execute logic for action sequences in your game. It sits on coroutines and is based on the ideas in this post by Elias Daler: Redirecting to https://edw.is/

kubrick.zip (701 Bytes)

Example project coming soon.

Here's a rough example:

-- cutscene.lua

class("StopPlayerAction").extends(kubrick.Action)
function StopPlayerAction:update(dt, player)
	local a <const> = player.acceleration * dt
	player.velocity.x += math.cos(player.rotation) * a
	player.velocity.y += math.sin(player.rotation) * a
	local velocity_magnitude <const> = player.velocity:magnitude()
	if velocity_magnitude <= a then
		ship.velocity.x = 0
		ship.velocity.y = 0
		self.finished = true -- Marks the action as finished.
	end
end

function cutscene_main(dt)
	kubrick.Wait(2.0) -- wait 2 seconds.
	StopPlayerAction(globals.player) -- stop player
	kubrick.Together(
		function()
			print("this function gets run")
			kubrick.Wait(1.0)
			print("in parallel the other functions.")
		end,
		function()
			kubrick.Wait(3.0)
			print("This function finishes last because it takes the longest.")
		end
	) -- run 2 or more routines in parallel. 
end


-- main.lua

import "cutscene"

local my_cutscene <const> = kubrick.create(cutscene_main)
local cutscene_started = false

function playdate.update()
	if cutscene_started then
		kubrick.update(my_cutscene, dt)
		if kubrick.finished(my_cutscene) then
			cutscene_started = false
		end
	end
end
12 Likes

FYI, the SDK has a similar function table.deepcopy(source) which, I believe, is implemented in C so should be even faster.
Copying table is handy but it is important to specify that most versions of such function can break if there is cross reference of element in the array which would lead in a stack overflow. There is versions of deep copy that make sure it doesn't happen and some also copy all metatable infos. Anyway is most cases a simple deepcopy works very well.

1 Like

Sorry to spam this thread.

I was a bit annoyed how I was managing my sprites Z index by putting number directly in them. It's always annoying since you have to kind of remember or check the Z Index of other sprites. And you always increment you Z Index by ten just in case you need to insert stuff in between without having to change manually all indexes.

enums would be perfect for that so I wrote a dump enum function:

function enum( t )
	local result = {}

	for index, name in pairs(t) do
		result[name] = index
	end

	return result
end

I just declare my layer list at launch:

layers = enum({
	"background",
	"enemies",
	"player",
	"clouds"
})

and just use to setup my sprites
sprite:setZIndex(layer.player)

12 Likes

Is it possible to add methods to the chain? Say if you want some logic to execute some custom logic somewhere in the sequence. Apologies, I haven't looked at the code yet.

ah! yeah it's on the list of things to add. :grinning:
I also wanted that feature to trigger sounds.

Not sure when I will add the feature but I will post it here when it's ready.

1 Like

New version for sequence
sequence.lua.zip (3.1 KB)

  • Removed few sneaky bugs
  • Added callbacks
local _crash = sequence.new()
	:from( 240)
	:to( 0, 2.0,"outQuad")
	:callback(function() print("reached high") end)
	:to( 240, 0.6,"outBounce")
	:callback(function() print("crash") end)
	:loop()
	:start()
9 Likes

If a callback function could return true or false whether it has completed or not, then you could use callbacks to handle actions unaccounted for by the library that take multiple frames. Perhaps that's going to far. Assume true if no bool is returned so it works as it does today?

I am not sure exactly in which context you want to use it.

A callback would pause the whole animation and be called every frame update until it returns true. Is that what you are thinking?

It would say allow me to add a custom animation or wait for input before the animation continues. I’m specifically thinking this could replace what I’m using for action sequences but I need custom logic to, say, turn the ship around and accelerate until velocity is zero. Though now that I look at this, I think this blocks until complete so it wouldn’t work for me anyway as I need to execute the rest of the game logic while this animation does its work each frame?

Perhaps just ignore me as is a good default. :laughing:

No I think It's great to explore how something might be use in a way I would not have expected.

While such feature could be implemented a massive refactoring would be necessary to enable it. Adding logic to it would also goes away from the simple idea of just playing animations.
The callback idea was a great suggestion because you want to be able to trigger sound for example. So I would not ignore you as a default :stuck_out_tongue_closed_eyes: