Makes sense. Didn't realize there was a plan to open it up.
@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.
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 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!
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)
This is GREAT, @dustin! I'll be playing with this in the coming days, and will definitely share back anything I add to it
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.
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
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
Update September 2022
it is on GitHub now GitHub - NicMagnier/PlaydateSequence: Create animations with simple sequences of easing functions in your playdate game
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?
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.
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
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.
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)
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.
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.
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()
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?