Log In  


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

g_part_expl,g_vel,g_rand_cloud,S,g_sun_pal,
 g_candy,
 Sc2,
 g_syls,
 g_scpal_map,
 C,
 g_news,
 M,
 D,
 U,
 g_ratings,
 g_npc_chat,
 g_diff,
 g_diffs,g80,g81,
 ss1,ss2,F,-- start of literals
 g_near,g_far,g_edge,g_msp,g_gal_size,
 g_gal_time,
 g_fin,
 g_sys_p,
 Q,--g_progress?
 g_award,
 p,
 g_music,
 g_gal_seed,
 g_sys,
 O,
 g_cmdr,
 g_kills,g_max_energy =
  {vel,vel,rand_cloud},{vel},{rand_cloud},--g_part_expl
 {c=8,sp=.3,en=8,dam=1},-- ship
 { --g_sun_pal
 split"10,9,8,4,2",-- yellow
 split"7,10,9,8,2",-- white/yellow
 split"7,12,13,15,1",-- white/blue
 split"7,6,13,5,1",-- white/grey
 split"12,13,15,5,1",-- blue
 split"7,11,3,5,1",-- white/green
 split"10,11,3,5,1", --yellow/ green
 },
 split"10,12,11,8",--g_candy
 split"0,1,1,2,1,5,12,2,4,8,3,15,2,2,1",-- Sc2
 split"ca,bal,da,gar,non\-en,pol,der,arp,bun,duc,kit,poo,v\-evee,zir,buf,v\-evil,xan,frak,ing,out,re,far,do,tel,tri,cry,quack,er,dog,pup,sno,ger,bil,pa,n\-ena,jan\-en,es,on",--g_syls
 {[0]=0,unpack_split'0,1,1,2,1,13,6,2,4,9,3,15,5,4,1'},--g_scpal_map
 {x=0,y=0},--C
 { -- g_news
 gal={
 split_comma'president #2 welcomes gerbil delegates to the #1 galaxy on state visit.|government says there are no broccoli or carrot shortages.|vice-president: reports of space weevil incursions are fake news and no cause for alarm.|recipe book by great aunt dahlia using substitutes for carrots and broccoli returns to bestseller chart, four hundred years after first edition|cats complain that kangaroo boxing title challenger hit their reigning champion, "she is supposed to sit in the boxes not punch other animals" ',
 split_comma'president dismisses rumours of a weevil invasion.|"no carrot shortage," says vice-president, "just eat a potatoe instead"|duck wins round-galaxy race by a bill from bunny. bunny claims galaxy is not round.|president #2 denies spending entire security budget on jacuzzi: "no-one will attack us anyway and i like the bubbles".|dog moral philosophy professors to discuss exactly who, if any of them, is a positive young male role model and also the location of "it"|bears win hugging title for twentieth year running',
 split_comma'"things are not getting worse," says president despite weevil presence noticeably increasing.|carrots vanishingly scarce as prices rise sharply. rabbits lobby parliament|"has anyone actually met a weevil?" asks vice president|government officials deny that gerbil delegates left because of weevils eating their carrots|"#1 shall have a universe-beating track and trace system for the weevil incidents," says #2|rhinos record victory in world rugby championship against mice team who have never qualified previously, but had elephants running scared in previous game',
 split_comma'president #2 says: "we are following the best scientific advice on the weevil problems," despite no obvious actions being taken|top military advisers say, "this is not the time to panic, but it could be soon."|"i grew my own broccoli," says mouse, "but weevils stole and ate it."|reports of weevils in almost every system.|what is your family doing to cope with the weevil invasion?|princess macaroon wins jousting tournament for sixth year running despite many jousters staying away due to weevils, "i could joust the weevils too," says macaroon',
 split_comma'#1 becoming overrun by space weevil menace.|what do the weevils want? beeb news asks the experts.|raccoon caught selling parsnips dyed orange as fake carrots says "most animals never even noticed." could you tell the difference?|frog croaking championship described as "riveting"|vice president distributes personal broccoli to poor parrots. parrots respond saying they "do not eat the stuff".|our reporters present 10 tips to cope with a hostile invasion from another species in style|snakes record first win in football. defeated lion captain said after the match: "they really used their heads".',
 split_comma'president #2: "we did everything we could to stop weevils, but it has been an unprecedented situation"|top military advisers say "this is the time to panic!"|"we need a hero," says panda, "someone should find the weevil base and take out the queen weevil." who could possibly do that?|president and vice-president still say carrots and broccoli supplies are fine - does anyone still believe them?|"i, for one, welcome our new weevil overlords," says president #2|giraffe sees image of dog in his breakfast toast|panda says he is tired of people expecting him to pursue car thieves in his own vehicle.',
 split_comma'#1 galaxy saved from weevils by lone pilot. all thankful.|president and vice president arrested accused of hoarding carrots and broccoli.|galaxy looking forward to eating better again.|colonists wanted for mission to former weevil system.|sloths, snails and tortoise alliance begin discussions about weevil sightings.|cats still refusing to commit on entering #1 galactic union- are they in or out?',
 },x=-128,item=0},
 {who=0,system=0,station=0,x=-64}, -- M
 dr_start, -- D
 {}, -- U
 split'\fbharn\-enless,\febit of a softy,\fca little harn\-enful,\f6son\-enetin\-enes dangerful,\f9a v\-evee spot deadly,\fabit of a predator,\fbbug hunter,\f7death incarnate,\f8the extern\-eninator',
 split"got any carrots?|searching for some broccoli\nyou got any?|have you heard there\nare weevils invading?|i don't believe in weevils.|i heard there's a planet\nwith naked apes on it\nwho are obsessed with\na thing called 'money'\n- crazy huh?|i'm heading for the \n#1|weevils are\na government hoax|how are you liking\n#2?|my father was called\n#3|make #4\ngreat again!,#2 should be\nan independent system\noutside the\n galactic union|weevils are a government\ntactic to distract us from\nthe real issues|if #2\nwere independent we'd\nnot have this weevil\nproblem|i'm sure our government\nwill sort everything out\nsoon|aunt dahlia's recipes\nare really good\nbut i miss carrots|i think i saw a\nweevil yesterday|do you have any\nbroccoli captain\n#3?|i hope there aren't any\nweevils raiding around\nthe #1,i still haven't found any carrots|what do i have to do\nto find broccoli in\nthis galaxy?|the weevils are just the\ntip of the iceberg\nmark my words|#3 is\na nice name|i'm #2 born and bred!|the #4\nunion needs us more\nthan we need them!,h-have you s-seen\nany weevils\nround here?\nthe news scares me|that's a nice ship you\nhave captain #3|i miss broccoli more\nthan carrots but i\nstill miss carrots|the government has no\nidea what it's doing|i trust our president\nto fix this weevil\nproblem,my brother still refuses\nto believe in weevils|psst - i know\nwhere you can\nget carrots still|what kind of weevils\nhave you seen\ncaptain #3?|don't let the government\nvaccinate you against\nweevil infection\nthey'll put a chip\nin you!|there are some really nasty\nweevils out there now|i told my parents not to\ntravel anymore|i hope the #1\nis still there,i was told that the\nweevils took over an entire\nsystem as a base|this species of weevil\nis supposed to be led\nby a huge queen|#4 has\nno regard for the rights\nof #2!|when i get to\nthe #1 i\nhope they have\nbroccoli|have you seen the\npresident recently?|i heard that the\n#4 fleet has\nrun away from\nthe weevils!|i don't know what i\nwouldn't do for\na carrot right now,hey captain #3 -\nyou're the best!|thanks for saving the\n#4 galaxy|i still can't find\nany broccoli|it's a lot quieter in \n#2 now|i heard they found carrots\nat the #1|#2 would\nhave coped with the weevils\nwithout the interference\nof #4|are you the real\ncaptain #3?",
 2, -- g_diff
 split'\fb  easy,\fcnorn\-enal,\f8  hard', --g_diffs
 {},{},
 unpack_split'0,0,0,1000,10000,10240,2,1024,0,1,0,0,0,0,0,2307,1,1,23,0,5' -- other globals


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


This works, for example:

ent=split'1,e,1,2,8'

cls()

dr_funcs={
function(d,...)
print(...)
end
}

dr_funcs[tonum(ent[1])](unpack(ent))

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)

22


3

>>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]