Vector math for PICO-8!
Have you ever been looking at your code and been like:
"Hmm, i sure do add two numbers to another set of two numbers a lot," then do i have the solution for you!
At the low low price of like 400 tokens, PICO-8 vector math can be yours!
I originally made this to streamline a platformer I may or may not ever finish, but I realized that other people might want this, flagrant waste of tokens or not.
Features!
The code
There are two versions of the whole program. One handles errors more gracefully, while the other has a smaller token footprint
Required functions:
The metatable depends heavily on these two functions
Fault tolerant metatable
Fault intolerant metatable
Normalize function
Well, i dont know if this is mathematically correct, but here, -(x,y) is just (-x,-y)
Edit: I realized the post below may come across as too critical, but I want to emphasize that I'm only pointing out some stuff that can be improved. Don't take this to mean your implementation is bad. It's pretty good, nice and compact, and aligns for the most part with the kind of stuff I do. You're a little more forgiving with the caller than I am, but that's because I'm usually my own caller and I expect myself to do things explicitly and to be able to interpret stack traces where they simply mean I've supplied bad args. The stuff below is just some things to minimize unnecessary code and execution time.
Some comments from someone who's spent years iterating on large vector libraries in both C/C++ and Lua for commercial games...
This code does more than it needs to:
function vecargs(a,b)
if(type(a)=="number")a,b=b,a
if type(b)=="number" then
return {a,vec2(b)}
elseif getmetatable(b)==vec2mt then
return {a,b}
end
end
|
I can't see any reason for you to be constructing and returning a table of the two vectors when you could return a straight tuple and eliminate all of your "c=vecargs(a,b) a,b=c[1],c[2]" temporary assignments.
Also, I personally wouldn't take the time (performance hit) to check the metatable. Act like a template. If someone passes you something that's compatible with a vec2, i.e. it has x,y coords, then just let it be used. Otherwise it'll fail on an x,y member access soon enough to alert the caller.
Reduced function:
function vecargs(a,b) if(type(a)=="number") return vec2(a),b if(type(b)=="number") return a,vec2(b) return a,b end |
Reduced usage:
__add = function(a,b) a,b=vecargs(a,b) return vec2(a.x+b.x,a.y+b.y) end, |
Also, while it's very good to write modular, concise, robust, easy-to maintain code like yours, it's also true that this is simple leaf-call code which is seldom read, pretty much never breaks, and pretty much never needs maintenance once written, i.e. you can write ugly code that has less overhead when used.
Thus I'd avoid doing things like these
__sub = function(a,b) a,b=vecargs(a,b) return a+(-b) end, |
function norm(vec) local power=vec^2 return vec/sqrt(power.x+power.y) end |
(Side note: you could save a parens token in this version of __sub() by returning -b+a.)
And try to make them as leaf-call-ish as possible:
__sub = function(a,b) a,b=vecargs(a,b) return vec2(a.x-b.x,a.y-b.y) end, |
function norm(vec) local len=sqrt(vec.x*vec.x+vec.y*vec.y) return vec2(vec.x/len,vec.y/len) end |
(Side note: v^2 executes via an internal pow(v,2.0) function, which is more expensive than a straight v*v multiply.)
I'd also probably have an internal version of vec2() for returns, which assumes it's being called with exactly the right args and doesn't need to do anything except construct the table and set its metatable. Not so much difference with 2D vectors, but the overhead to check for optional params starts to add up when extended to 3D and 4D.
Finally, as a purely stylistic choice, I'd recommend against overriding the .. operator for dot products. I'd recommend literally just using that to append a stringified version of the vector. Instead have a global vec2dot() or a metatable function you can call on the vector so that the implied operator is still between the operands, e.g. v1=v2:dot(v3). Same for cross products, etc.
Well, I guess I'll assess your points in order
- The vecargs function
I just didn't know that tuples were a thing you could get our of a function.
The list thing felt wrong, but I didn't know if another way - The bit about leaf code ( I don't know what that means)
I was optimizing this for towns specifically, the point of this was to save tokens in that platformer
As for the internal vec2, I would use that if I was optimizing speed, but, like I said, tokens.
(also, I meant to release this whole thing to public domain and then forgot to do so in three first post)
Hey, it's okay if you need to save tokens more than you need to save performance. I'm giving feedback, not making demands. :)
I think you could still do the tuple thing and swap the expression order in __sub() to eliminate the parens though of course, thus reducing token count even more.
As I said, the way you've done it is good, and now that you've specifically said you were after tokens, I'd say it's exactly the right approach aside from the couple of ways I said you could save more tokens.
Thought I'd weigh in with an alernative implementation. It's only 142 tokens but it provides all the features I generally use (extra member functions like dot/cross products, angle, etc can be added in if needed as the __index is set).
Usage notes:
- construct vectors like this:
vec2{x,y}
This saves a few tokens in the vec2 function by using Lua function-call-on-table-literal syntax.
- I've abused the Lua # length operator to give vector length
- Only scalar multiplcation on the left is supported (scalar*vector), which saves a lot of tokens at the expense of being less error tolerant. In my experience this is the only common case, componentwise vector multiplication is not common (for me anyway).
You suffer a token loss per []-style table reference, unfortunately: referencing "v.x" is just 2 tokens vs. "v[1]" at 3 tokens. Your exact code, but with .x/y references and direct component passing, is just 129 tokens:
And code outside the library would be smaller too, if it manipulates vector components directly.
[Please log in to post a comment]



