I’ve finished my first little PICO-8 game!
Check it out! https://www.lexaloffle.com/bbs/?tid=44959
I’ve finished my first little PICO-8 game!
Check it out! https://www.lexaloffle.com/bbs/?tid=44959
PICO-8 has this nice helper function called foreach(table, func)
which will call the function for each item. I like to use this in my _draw
function to dispatch drawing to game objects. The trouble is that most of the time I want to call a method (table-bound function) instead of a function. Normally this required a helper function per-method to invoke.
Consider the following PICO-8 code (it’s just Lua with some sugar).
-- returns a table with table-bound method 'draw'
function dot(x, y)
return {x=x, y=y,
draw=function(self,col)
local c=col or 7
pset(self.x,self.y,c)
end
}
end
-- returns a function with captured upvalues
function dot2(x, y)
return function(col)
local c=col or 7
pset(x,y,c)
end
end
function _init()
drawable={}
add(drawable, dot(64, 64))
add(drawable, dot2(64, 68))
end
In this example, I’m showing two different ways to make “drawable” instances. dot
returns its instance as a table with a method named draw
. dot2
returns a function which draws itself when invoked. I’ll start with the dot2
case.
function invoke(o)
o()
end
function _draw()
cls()
foreach(drawable, invoke)
end
The nice thing about the approach of making drawable
a function closure is that you can make a generic invoke
helper function which just calls instance. This means you only need a single helper function. The downside is that you can only return one callable behavior this way which isn’t ideal for more complex objects.
function call_draw(o)
o:draw()
end
function _draw()
cls()
foreach(drawable, call_draw)
end
This approach is more flexible and allows for more methods to be defined, but now you need a call_foo
helper for each different method to call. Another problem with both approaches is you can’t pass in arguments to the method calls.
It turns out that Lua gives us a ton of flexibility to build our generic dispatch. Let’s start with the problem of dispatching by name.
-- Call Method
function callm(method)
return function(o)
o[method](o)
end
end
function _draw()
cls()
foreach(drawable, callm("draw"))
end
callm
is a pretty simple helper. The core of it just does a table lookup by name and invokes the result. callm
returns a function because foreach
expects a function that takes the object being iterated. There are a few problems with this approach. First this crashes if there isn’t a draw
method on the instance. Second, the dot.draw
method takes an argument and there’s no way to pass one in.
-- Call Method
function callm(method, ...)
local params={...}
return function(o)
if type(o)=="table" then
local m=o[method]
if type(m)=="function" then
m(o,unpack(params))
end
end
end
end
function _draw()
cls()
foreach(drawable, callm("draw", 10))
end
This version adds support for varargs that get forwarded to the method when the returned function is called. This allows parameters to be passed to the method. This also adds some type checking to ensure the instance passed in is a table and has a function named method
. You can see the foreach
call site looks pretty good.
Finally, if you want to continue to use callable objects instead of tables, we can take this same concept and make it work with those too.
-- Call function 'object'
function call(...)
local params={...}
return function(o)
if type(o)=="function" then
o(unpack(params))
end
end
end
function _draw()
cls()
foreach(drawable, call(10))
end
Final thoughts
This adds overhead to the calls, and it’s almost certainly more efficient to use for x in all(XS)
. That said, it’s a neat little piece of code and I think it’s pretty cool that it can be accomplished with Lua.
function _init()
game_items={}
-- add items
end
function _update()
-- call 'update' on each instance that implements it
foreach(game_items, callm('update')
end
function _draw()
-- call 'draw' on each instance that implements it
foreach(game_items, callm('draw')
end
After entering the PICO-8 rabbit hole, I found out about the coolest little computer… 5 years too late.
I picked one up from this site. When it got here, I charged it up and turned it on. Pretty cool little computer. You can jump right into a shell and it’s Linux. It didn’t take long until I had PICO-8 updated and I was working on my little toy game.
Unfortunately the parent company is defunct and so this little gem is stuck in the past. That’s probably ok… but it would be pretty cool to have access to a full distro, especially because it’s just so hackable. It takes me back to the early 2000s when I picked up my Sharp Zaurus 5500. There’s something especially nerdy about a tiny terminal you can bring with you.
I’m sure I’ll have more to say as I get to know it, but for now I’m using this post to bookmark a few resources.
I’ve been playing around with GLSL Shaders and was trying out a few P5.js examples on my Mac. I couldn’t figure out why my results weren’t looking like the examples.
The following is the “Hello, world” of shaders. It simply renders a gradient from black to red smoothly across the canvas.
let header =
'precision highp float;';
// the vertex shader is called for each vertex
let vs =
header +
'attribute vec3 aPosition;' +
'void main() {' +
' vec4 positionVec4 = vec4(aPosition, 1.0);' +
' positionVec4.xy = positionVec4.xy * 2.0 - 1.0;' + // Correct for GL offset bug.
' gl_Position = positionVec4;' +
'}';
// the fragment shader is called for each pixel
let fs =
header +
'uniform vec2 u_resolution;' +
'void main() {' +
' vec2 st = gl_FragCoord.xy/u_resolution.xy;' +
' gl_FragColor = vec4(st.x,st.y,0.0,1.0);' +
'}';
let myShade;
function setup() {
createCanvas(100, 100, WEBGL); // (0,0) is center
//pixelDensity(1); // <-- Uncomment to fix scaling.
// create and initialize the shader
myShade = createShader(vs, fs);
shader(myShade);
fill(64);
stroke(128);
}
function draw() {
background(192);
myShade.setUniform("u_resolution", [width, height]);
rect(-40, -40, 80, 80);
}
Unfortunately when I run this example on my Mac the gradient stops halfway across the canvas.
The problem is that by default there’s pixel scaling because of the Retina display which causes the pixel math to be incorrect. The solution is to specify the pixelDensity in the setup function. If the pixel density is set to 1, the shader works as expected.
pixelDensity(1)
in the script setup to fix the shader scaling problem.