These are my own notes on saving token tricks that I've used previously. I'm sure what's here is not unique to me - there are other documents about this subject (e.g. https://github.com/seleb/PICO-8-Token-Optimizations) and I recommend looking at those as well, probably before this one. I intend to add and update here as I learn more.
If you see a mistake or are a true wizard that can offer wisdom to this humble novice then please feel free to comment :)
Starting out
I say "starting out", because the next few ideas concern initialisation and structure, but this might be a good place to suggest that a lot of these tips produce much nastier to read code that's more of a pain with which to work and employing all this stuff straight away, especially if you're new to coding is just going to make the whole experience more difficult and probably not much fun. I tend to bear some of the things here in mind as I code and some things are becoming (bad) habits, but usually I've only used these techniques as a second, third or an in desperation when PICO-8 won't run any more pass.
Coding your game in a more elegant way or even... cutting things(!) that aren't necessary are probably both better ways to save tokens.
Functions or "to _init() or not to _init()"
3 tokens; 19+ characters
I don't use this function at all and do all initialisation at the end of my code, that is, after any other function definitions (so that they're not undefined if calling them from the initialisation code). If you're using the PICO-8 editor I'd suggest putting it in your rightmost tab.
I'm going to end up using global scope anyway and I have yet to find a downside.
function _init() my_var=1 end |
vs
my_var=1 |
Initialising variables
1 token per variable
Every time an '=' is used a token is wasted:
a,b,c=1,2,3 |
(7 tokens)
vs
a=1 b=2 c=3 |
(9 tokens - 1 for each '=')
To make this (a bit) nicer to deal with I tend to do something like the following when this kind of list gets very long (often longer than shown here):
a,b,c,d= 1, -- a 2, -- b 3, -- c 4 -- d |
This uses the same number of tokens and I've had to resort to code minification that will remove the comments, newlines etc. for the last few projects anyway. I use this one, which is great BTW: https://pahammond.itch.io/gem-minify.
Aside - assigning nil
Need to clear a variable to nil (which conveniently evaluates as false - todo: more on booleans)?
a,n=1 |
4 tokens
vs
a=1 n=nil |
6 tokens
Pretty desperate and calling a non-returning function after your variable assignment?
n=nil nil_returning_function("some text",0,0,10) |
9 tokens
vs
n=nil_returning_function("some text",0,0,10) |
8 tokens
WARNING: You need to be very sure that the function doesn't return anything. I got caught out by this myself writing this post as I initially used print as an example here and as GPI points out below it does return a value. You need to be very desperate to do this as it really, really doesn't help with code readability and is asking for trouble if you, say, change the function to return a value at a later data. But if you're right up against the token limit...
Back on initialisation - split() is your friend
Got a lot of strings you want in a table?
tab={'baa','baa','black','sheep'} |
7 tokens
vs
tab=split('baa,baa,black,sheep') |
5 tokens
vs
tab=split'baa,baa,black,sheep' |
4 tokens
Don't use brackets for function calls or maths unless you have to, since they add a token for each pair that you use.
Note: the following is perfectly legal in lua:
rnd{1,2,4,8} |
(This returns a random value from the table {1,2,4,8})
unpack is also your friend (and itself friends with split)
a,b,c,d= 1, -- a 2, -- b 3, -- c 4 -- d |
11 tokens
vs
a,b,c,d=unpack(split'1,2,3,4') |
10 tokens
Further variables cost 1 token each i.e. each further variable you add to a statement like this will save a further token compared to the version above (and 2 compared to separate a=1 b=2 etc.)
Be warned, you may have trouble with your variables being initialed as strings, but pretty much every PICO-8 function I've tried (e.g. spr, print, rectfill, pal etc) Just Works. The problems I've had with this have been my own code. It's also very hard to know which number corresponds to which variable so I suggest only using this trick when you really need to.
Of course, you can go further:
a,b,c,d=unpack_split'1,2,3,4' |
8 tokens*
The caveat here is, obviously, that you need a function like:
function unpack_split(...) return unpack(split(...)) end |
10 tokens by itself.
However, once you have that function...
print("hello world",10,20,11) |
6 tokens
becomes
print(unpack_split"hello world,10,20,11") |
4 tokens
It's remarkably fast - I found that I only hit performance trouble if I used it within nested loops or with very high numbers of objects.
In fact, every time you have 3 or more literal values together you can save at least a token with your unpack_split function anywhere in your program (it's a flat cost for each use in fact). I used it enough that I ran into trouble with the number of characters it used and the compressed size limit - renaming the function to US or similar solves that fairly nicely, once again at the cost of making the code ever less readable.
Order is everything
Armed with these techniques the next thing to consider is when to assign values in you initialisation (or anywhere else).
The more you can bunch together, the better since e.g. fewer '='s are needed that way and you can group more literals into a single unpack_split'1,2,3,4'. Remember you can't reference an assigned value from within the same assignment though.
This doesn't work (unless you really want y to be what x was previously + 3):
x,y=20,x+3 |
todo: add multi-dimensional table routines
Example from PICO Space
Stare into the void if you're feeling brave enough...
This is the most extreme example I have and hopefully ever will commit again...
Functions
Arguments
Rely on default arguments and persistent states (like the draw colour) where you can e.g.
print(a,0,23,10,7) print(b,0,23,10,7) |
could be
print(a,0,23,10,7) print(b,0,23,10) |
Also:
camera(0,0) |
vs
camera() |
The explicit arguments cost every time.
More can be better
As well as passing fewer arguments to a function, you can also pass more - lua doesn't choke. How can more arguments save tokens?
If you are calling functions that you've assigned to variables that you can call with the same code, but require different numbers of arguments then just pass all the arguments every time e.g. draw methods that change between different entities in your game might sometimes need colours or not.
Also remember that a table index that hasn't been defined is treated as nil. So passing tab.foo to a function when tab.foo has never been assigned is just the same as passing nil.
As long as the code in the functions doesn't choke on nil values for those arguments then you don't need any clever control flow to try and get the number of arguments "right".
Example:
function make_ent(dr,x,y,c1,c2) return {dr=dr,x=x,y=y,c1=c1,c2=c2} end function draw_a(x,y,c1,c2) -- some drawing code here end function draw_b(x,y) -- some drawing code here end ents={make_ent(draw_a,0,0,8,9),make_ent(draw_b,0,20,6,7),make_ent(draw_a,20,49)} for ent in all(ents) do ent.dr(ent.x,ent.y,ent.c1,ent.c2) end |
Taking this example further, I might look to use unpack_split on the multiple literal values in the make_ent calls. I'd then wonder whether I could find a way of describing all the entities in a single string that I could process with split and unpack - there's likely to be more than three entities in my game after all.
For example, currently I'm writing a game that unpacks all the platforms, walls, floors, monsters, goodies etc. via the same function per collection (now I'm wondering whether I could do all collections together...). The number of bits of data per each entity is the first number followed by values for each entity. i.e.
-- each spid has 3 values x,y position and y vector g_spids=unpack_raw_data'3,550,160,1,540,40,1,200,70,1,300,90,1,800,25,1,864,200,-1' |
I suggest using an editor to produce strings like this rather than try to write the data by hand. The PICO-8 printh function is v handy for getting data out of an editor into a file or to the console you run PICO-8 inside. You do run PICO-8 from the command line, right? :)
Note: a lot of characters are wasted doing it this way e.g. every ','. Hex values would be more compact as well. If you can keep all values within a byte range then encoding them as raw characters works very well too and Zep has even given us the magic function to decode them here: https://www.lexaloffle.com/bbs/?tid=38692.
I use this for image data and sfx data (hope to write about both soon).
Thinking about functions again, remember that in lua the following are equivalent:
tab['a'] tab.a |
So that by putting the draw functions in the example further up into a table you could specify which function to use via an element in a data string. Something like:
function make_ent(dr,x,y,c1,c2) return {dr=draw_funcs[dr],x=x,y=y,c1=c1,c2=c2} end ... for ent in all(ents) do ent.dr(x,y,c1,c2) end |
Or even at call time:
for ent in all(ents) do draw_funcs[ent.dr](x,y,c1,c2) end |
(I suspect that would be slower - maybe easier to debug though)
If your data parsing function is clever enough then you probably don't need a separate constructor function like that at all.
Along those lines, say if every entity has position (x,y) and most have velocities (vx,vy) but one type only has a colour and no velocity then it's nice to code it with a proper index name "ent.col", but if it means writing separate construction code then consider just using ent.vy and a comment(or at least adding something like local col=ent.vy when you need it).
I haven't done this, but it may be even more efficient to dispense with named elements in your tables so you could do:
for ent in all(ents) do ent[1](unpack(ent)) end |
todo: random numbers, number indices vs named indices, caching table values in local variables (fewer tokens and faster too!)
Bonus: unpacking into memory
I'm going to add more to this post, but for now I'll end with this:
Have a table of values e.g. an image stored as bytes? Want to dump it into memory or onto the PICO-8 screen easily? You can as of the recent PICO-8 updates:
poke(0x6000,unpack(data)) |
6 tokens
Bang - straight onto the screen. Poke4 is even quicker and the same number of tokens if you have your image data nicely packed into table values. I use this for 'extra' sprite sheets, for instance.
Be warned though: down this road lies the terror of the compressed size limit...
(I tried this just now vs memcpy(0,0x8000,0x2000) and it's much quicker - not v scientifically though so YMMV)
>>there's probably not really any reason why you need to use
>>_draw()
_update()/_update60() is called every 1/30 or 1/60s.
But the _draw() function is called depending on the computer every 1/30, 1/60 or 1/15 second. On modern PCs this should be relevant, but on low-end-systems like a rasphberry pi pico it could make a big difference.
also I recommend to use _init() because you can then use every function of the code and not only the function definied before the current position.
>> n=print("some text",0,0,10)
WARNING! print doesn't return nil, it returns the x-pixel-coordinate. Don't use sideeffects, your code will become unreadable. also it is possible that a command, that returns nil at the moment will return a value in a future version and that could break your code!
>> _update()/_update60() is called every 1/30 or 1/60s.
>> But the _draw() function is called depending on the computer every 1/30, 1/60 or 1/15 second. On modern PCs this should be relevant, but on low-end-systems like a rasphberry pi pico it could make a big difference.
Thanks for the info - feedback is a big part of the reason I wrote this post. I've removed that section entirely and will bear this in mind in future. I don't have a Raspberry Pi or similar to test with and so hadn't encountered those problems. Perhaps I'll need to pick one up.
>>also I recommend to use _init() because you can then use every function of the code and not only the function definied before the current position.
Exactly why I place this code after any function definitions. It's not worth spending 3 tokens to me. I've tried to make that more obvious in the text.
>> WARNING! print doesn't return nil, it returns the x-pixel-coordinate. Don't use sideeffects, your code will become unreadable. also it is possible that a command, that returns nil at the moment will return a value in a future version and that could break your code!
Ah - I should have remembered that and it's a great example of why only to do this in real desperation. I've made it more clear that this is generally a really bad idea (I guess I could have been more clear).
When you have large "static" nested tables, you can use something like this:
-- print out a table - for debug function tableout(t,deep) deep=deep or 0 local str=sub(" ",1,deep*2) --print(str.."table size: "..#t) for k,v in pairs(t) do print(str..tostr(k).." <"..type(v).."> = "..tostr(v)) if type(v)=="table" then tableout(v,deep+1) end end end -- convert a string to a table function str2table(str) local out,s={} add(out,{}) for l in all(split(str,"\n")) do while ord(l)==9 or ord(l)==32 do l=sub(l,2) end s=split(l,"=") if (#s==1) s[1]=#out[#out]+1 s[2]=l if (s[2]=="{") s[2]={} if s[2]=="}" then deli(out) elseif l~="" then out[#out][s[1]]=s[2] if (type(s[2])=="table") add(out,s[2]) end end return out[1] end table2=str2table([[ hello=world something { a=99 b=20 c=30 doda { doublenest } } and indexed nestedkey={ something } hex=0x1001 bin=0b1101 ]]) cls() tableout(table2) |
oh, and what can also save tokens:
instead
if con then a+=1 end |
this is shorter
if (con) a+=1 |
it is in the manual, but a complete list here would be usefull.
also
instead
if a=10 then b=20 else b=30 end |
this:
b= a==10 and 20 or 30 |
if you dont use _init() function, you won't be able to run functions at the start
To add to SAVING TOKENS, you can also concatenate numbers for the new CHR()
function.
?chr(100,114,105,110,107,32,121,111,117,114,32,111,118,97,108,116,105,110,101,46) |
This post has been immensely useful as I get back into Pico-8 again.
I've been expanding on the concept you briefly talked about:
> I haven't done this, but it may be even more efficient to dispense with named elements in your tables
As pointed out, accessing object fields costs quite a few tokens, so I've been experimenting some more with having an array of object values which are unpacked, instead of named elements for my entities.
Instead of passing these unpacked values as arguments, I store them in global variables.
Usually you're only acting on one entity at a time, at best two. We can try to use that to our advantage.
Let's break it down:
Say we have this fairly typical bit of code:
function make_entity() return { sprite=4, x=16, y=24, draw=function(self) spr(self.sprite,self.x,self.y) end, update=function(self) self.x+=1 self.y+=1 end } end local ent=make_entity() ent:draw() ent:update() |
50 tokens, 249 chars, 151 bytes
Let's now use the concept I described in this second example:
function load_ent(e) ent_sprite,ent_x,ent_y, ent_draw,ent_update=unpack(e.v) end function save_ent(e) e.v=pack(ent_sprite,ent_x, ent_y,ent_draw,ent_update) end function make_entity() return { v={ 4, 16, 24, function() spr(ent_sprite,ent_x,ent_y) end, function() ent_x+=1 ent_y+=1 end } } end local ent=make_entity() load_ent(ent) ent_draw() ent_update() save_ent(ent) |
68 tokens, 432 chars, 212 bytes
In this comparison, the overhead is 21 tokens. But, here's how you can earn that back:
In the first example, introducing a new member variable costs 3 tokens, plus 2 tokens per use.
In the second example, introducing a new member variable costs 3 tokens, plus 1 token per use.
If you're having to access your member variable more than 21 times in your code, then it is worth considering this option.
For 20 tokens extra overhead (41 in total overhead) we can switch to a method where we can have more
than one object in our registry, as well as save on each member variable added.
With this third example, introducing a new member variable costs 1 token, plus 1 token per use.
This would be worth using if you have 10 or more member variables, and are accessing your more than 22/23 times.
Supporting multiple entity registers costs 1 extra token for each call to save_entity and load_entity.
local glob_reg=split[[sprite,x,y,draw,update]] function load_ent(e,prefix) for k,v in pairs(glob_reg) do _𝘦𝘯𝘷[prefix..v]=e.v[k] end end function save_ent(e,prefix) for k,v in pairs(glob_reg) do e.v[k]=_𝘦𝘯𝘷[prefix..v] end end function make_entity() return { v={ 4, 16, 24, function() spr(ent_sprite,ent_x,ent_y) end, function() ent_x+=1 ent_y+=1 end } } end local ent=make_entity() load_ent(ent,"ent_") ent_draw() ent_update() save_ent(ent,"ent_") |
92 tokens, 513 chars, 268 bytes
It obviously depends on the type of project you're working on, but this is a method worth considering.
In combination with split, example 2 and 3 has 0 cost for introducing new non-function members.
To keep things clean and readable I comment each value in the make_entity function, additionally, the prefix I use for the global entity variables is one of the special characters (e.g. diamond or character symbol), this not only saves characters but also prevents accidental collisions.
Here's an example from my current project, this function pushes an entity away from another entity:
function player_interact() load_ent(player,"웃") for o in all(objects) do if o~=player then load_ent(o,"◆") if ent_overlap() then push() end save_ent(o,"◆") end end save_ent(player,"웃") end function push() ◆vx,◆vy=set_length(◆x-웃x,◆y-웃y,4) end |
Overall, I'd say this technique works well if you have a lot entities which share similair values, but may have different implementations for its draw/update methods and/or need to access its members quite a lot.
[Please log in to post a comment]