Log In  

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!


-Constructor function: vec2(x,y) will make a vector, vec2(x) copies x twice
-The four basic functions, plus exponetation
-The concentation operator (..) does dot products
-A normalize function

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

  • Constructor

This function creates and gives the right metatable to the given two numbers, saving a few tokens every time you or the metatable itself wants to make a new metatable

function vec2(x,y)
 if (not y) y=x
 return setmetatable({x=x,y=y},vec2mt)
end
  • Argument preperation

This function returns a list of two vectors, either the two vectors passed, or a vector and then a doubles number. This function means you cannot divide a number by a vector, or subtract a vector from a number.

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

Fault tolerant metatable

vec2mt={
 __index={x=0,y=0},
 __newindex=function (tble, k, v)
  if k=='x' or k=='y' then
   rawset(tble,k,v)                      
  else
   printh("bad key")
  end
 end,
 __unm = function(tble)
  return vec2(-tble.x,-tble.y)
 end,
 __add = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return vec2(a.x+b.x,a.y+b.y)
 end,
 __mul = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return vec2(a.x*b.x,a.y*b.y)
 end,
 __div = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return vec2(a.x/b.x,a.y/b.y)
 end,
 __sub = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return a+(-b)
 end,
 __pow = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return vec2(a.x^b.x,a.y^b.y)
 end,
 __concat = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return a.x*b.x+a.y*b.y --15
 end,
}

Fault intolerant metatable

vec2mt={
 __unm = function(tble)
  return vec2(-tble.x,-tble.y)
 end,
 __add = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return vec2(a.x+b.x,a.y+b.y)
 end,
 __mul = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return vec2(a.x*b.x,a.y*b.y)
 end,
 __div = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return vec2(a.x/b.x,a.y/b.y)
 end,
 __sub = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return a+(-b)
 end,
 __pow = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return vec2(a.x^b.x,a.y^b.y)
 end,
 __concat = function(a,b)
  local c=vecargs(a,b) a,b=c[1],c[2]
  return a.x*b.x+a.y*b.y --15
end,
}

Normalize function

function norm(vec)
 local power=vec^2
 return vec/sqrt(power.x+power.y)
end

P#73745 2020-03-08 17:37

Thanks for sharing!

I see __unm is defined, I looked it up and it implements unary minus (example a=-b). What does that mean for a vector?

P#73760 2020-03-08 21:36

Well, i dont know if this is mathematically correct, but here, -(x,y) is just (-x,-y)

P#73777 2020-03-09 01:46
3

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.

P#73767 2020-03-09 02:34 ( Edited 2020-03-09 05:59)

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)

P#73782 2020-03-09 07:33

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.

P#73783 2020-03-09 11:44 ( Edited 2020-03-09 11:44)

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).

vec2mt={
    __add=function(v1,v2)
        return vec2{v1[1]+v2[1],v1[2]+v2[2]}
    end,
    __sub=function(v1,v2)
        return vec2{v1[1]-v2[1],v1[2]-v2[2]}
    end,
    __unm=function(self)
        return vec2{-self[1],-self[2]}
    end,
    __mul=function(s,v)
        return vec2{s*v[1],s*v[2]}
    end,
    __len=function(self)
        return sqrt(self[1]*self[1]+self[2]*self[2])
    end,
    __eq=function(v1,v2)
        return v1[1]==v2[1] and v1[2]==v2[2]
    end,
}
vec2mt.__index=vec2mt

function vec2(t)
    return setmetatable(t,vec2mt)
end

P#73787 2020-03-09 19:40 ( Edited 2020-03-09 19:41)
1

@mrh

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:

vec2mt={
    __add=function(v1,v2)
        return vec2(v1.x+v2.x,v1.y+v2.y)
    end,
    __sub=function(v1,v2)
        return vec2(v1.x-v2.x,v1.y-v2.y)
    end,
    __unm=function(self)
        return vec2(-self.x,-self.y)
    end,
    __mul=function(s,v)
        return vec2(s*v.x,s*v.y)
    end,
    __len=function(self)
        return sqrt(self.x*self.x+self.y*self.y)
    end,
    __eq=function(v1,v2)
        return v1.x==v2.x and v1.y==v2.y
    end,
}
vec2mt.__index=vec2mt

function vec2(x,y)
    return setmetatable({x=x,y=y},vec2mt)
end

And code outside the library would be smaller too, if it manipulates vector components directly.

P#73813 2020-03-11 04:18 ( Edited 2020-03-11 04:18)

> Well, i dont know if this is mathematically correct, but here, -(x,y) is just (-x,-y)

Thanks! My question was more «what is it used for?», and the answer is «reversing the vector».

Thanks to Felice too for showing a function to normalize a vector :)

P#76395 2020-05-11 20:57

@Felice My son and I have only been playing around with pico-8 for a couple weeks and I have already found myself stumbling across multiple incredibly helpful comments from you on the BBS. Thank you for your valuable contributions!

I am new to game programming and have been doing a crash-course/refresher on linear algebra. I found this post because I wanted to see what others had come up with for representing vectors in pico-8. I quickly discovered that if I would use vec2 to represent coordinates corresponding to pixels on the screen, then the naive implementation of normalize is limited to vectors with values from -128 to 128. (Or to put it another way, the largest length that can be correctly computed is sqrt(32768) or 181.0193)

This realization sent me on a hunt to figure out how to handle this limitation, and I stumbled across your incredibly helpful explanation of fixed-point numbers - I'd never had reason to work with these before.

My solution was to bit-shift x and y before squaring them so the intermediate length calculation wouldn't overflow, then perform the opposite shift on the result.

function norm(vec)
 local x=vec.x>>4
 local y=vec.y>>4
 local len=sqrt(x*x+y*y)<<4
 return vec2(vec.x/len,vec.y/len)
end

I guess exactly how much to bit shift by really depends on whether or not my vec2's are being used to store precision decimals, and how far off-screen things can get in my game. In a system where vec2 would mostly be used to map to integer coordinates that correspond to the screen (and things that are off-screen but still being tracked), it seems like there is not really any drawback to this implementation.

Anyways, the point of this post is mainly to say thanks, then to maybe help others that are on a similar journey as myself, and lastly to ask you for any thoughts or feedback on this approach - is there anything I'm missing? Any insight into whether using intermediate variables for x and y should be faster or slower than just inlining (and effectively shifting the same value twice)?

P#114140 2022-07-09 20:12

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-03-28 20:20:13 | 0.015s | Q:24