Home

Awesome

PICO-8 Token Optimizations

A few of these are pretty obvious, a few of them are not that obvious. Almost all of them make your code more unreadable, harder to edit, and would generally be considered "bad practice" in a less constrained system.

These are mostly the result of various people brainstorming optimizations on the PICO-8 discord server. Feel free to suggest changes, corrections, or other tricks!

Rely on Default Arguments

Functions can be called without passing every argument. Any specified arguments which aren't passed in are assigned a value of nil instead. Because of this, many of the PICO-8 API functions have arguments which can often be omitted without changing program behaviour. Some common examples include:

You can also take advantage of this behaviour in user-defined functions. e.g. say you have a function foo which is often (but not always) called with the argument 5. If you change the function such that a nil argument is replaced with 5, you can remove the argument in all the places you would pass 5 in. The following 22-token program

function foo(argument)
 --do a thing
end
foo(5)
foo(5)
foo(5)
foo(5)
foo(5)
foo(5)

becomes

function foo(argument)
 argument=argument or 5
 --do a thing
end
foo()
foo()
foo()
foo()
foo()
foo()

a 21-token program.

Calling Functions with Strings or Tables

Instead of calling functions with brackets (i.e. FUNC()) you can call them using strings (i.e. FUNC"") or tables (i.e. FUNC{}). On its own, this doesn't save any tokens, but the string or table used to call the function will be passed in as the first argument at no extra token cost. This means that FUNC("STRING") or FUNC({TABLE}), 3-token statements, are the same as FUNC"STRING" or FUNC{TABLE}, 2-token statements.

In most cases, this format will also work for a single number contained in a string, e.g. BTN(0) can safely be replaced with BTN"0". It's important to note that the argument is still passed in as a string, so some user-defined functions may not work as expected in this format, e.g.

function FOO(NUMBER)
 return NUMBER==0 or type(NUMBER)=="NUMBER"
end
function BAR(NUMBER)
 NUMBER+=0
 return NUMBER==0 and type(NUMBER)=="NUMBER"
end
print(FOO"0") -- false
print(BAR"0") -- true

Assignment with Commas

Multiple variables can be declared at the same time in the format var1,var2 = value1,value2. These declarations can be chained indefinitely, removing the need for an = per variable assigned.

Assignments Default to Nil

When declaring multiple variables at the same time, any not given values will be set to nil. This is an easy way to unset existing variables. var1,var2 = value1,nil is equivalent to var1,var2 = value1.

Replace Constant Variables with Literals

When you're coding, it's almost always better to store constants as variables; e.g. if your game has gravity, you might write g=9.8 and reference g instead of writing 9.8 everywhere gravity needs to be applied. It's helpful while you're coding, but doesn't actually contribute to the final program, so once you've decided on a constant, you can replace all those variable references with the literal.

Actually Do Your Math

Similar to storing constants in variables, it's often easier to represent constant values in code using multiple literals, e.g. 1/3 and 1/21 are probably more user-friendly than 0.333... and 0.0476.... By replacing these with values calculated outside of the editor, you can save some tokens.

It's also helpful to remember to use algebra to simplify statements, e.g. 2*(variable/10) might make sense when you first write it, but it's the same as variable/5.

Replace Table Elements with Separate Variables

It's typical to use tables to store the properties of an object which "belong" to that table; e.g. you might have a table player which has a table player.position which has the elements player.position.x and player.position.y. In some cases, this is just an organizational habit and you could just have player_position_x and player_position_y as completely unrelated variables and avoid the tokens needed for table access.

Replace Multiple Table Accesses with Local Variables

If you're repeatedly accessing a single table value, you can store the value in a local variable and reference that instead. e.g. the following function:

function print_score(player)
 if player.score == 0 then print(player.score.." you lost")
 elseif player.score < 10 then print(player.score.." you lost, but not that bad")
 elseif player.score < 20 then print(player.score.." you won!")
 else print(player.score.." you won by a lot!") end
end

can be rewritten with 3 fewer tokens as:

function print_score(player)
 local score=player.score
 if score == 0 then print(score.." you lost")
 elseif score < 10 then print(score.." you lost, but not that bad")
 elseif score < 20 then print(score.." you won!")
 else print(score.." you won by a lot!") end
end

This is particularly useful if the access statement is more complex. player.score needs to be repeated 4 times before you break even, but game.scene.player[0].score.current only needs to be repeated once.

This method has a bonus of being less CPU-intensive: locals can be read faster than table accesses.

Initialize Table Properties in Declaration

Tables are commonly initialized using t={}, and then followed by table property declarations (e.g. t.x=0). This is probably a result of a mental model which goes "Create my table, now set my table's X position to 0". You just wanted a table with an X position of 0 though, and didn't really need to separate it into two steps. e.g. the following table + property declaration:

t = {}
t.one = 1
t.two = 2
t.three = 3

can be rewritten with 3 fewer tokens as:

t = {
  one = 1,
  two = 2,
  three = 3
}

Prefer Property Access to Array Indexing

Accessing tables through array indexing (e.g. table["key"]) is a 3-token statement, whereas accessing a property (e.g. table.key) is a 2-token statement.

Vector Dimensions as Properties Instead of Array Elements

A common, but more complex example of the above point is the way you might handle PICO-8 vectors: you could create a table in the format point={x,y}, and then access the x and y values using point[1] and point[2], or you could create a table in the format point={x=x,y=y} and access them using point.x and point.y. The former costs more tokens to access (3 vs. 2), but the latter costs more tokens to create (9 vs 5). You can reduce the creation cost by including a constructor function, e.g. function vec(x,y) return {x=x,y=y} end. This function has an overhead, but reduces the token cost of creating vectors to vec(x,y), a 4-token statement. Table access is typically more common than table creation, so even without the constructor function the latter format will usually save tokens.

Note that vectors is simply a common example of this optimization; it can be applied to other objects as well

Parsing Data from String Literals

If you're working on a larger project, you might run into a situation where you've hit the token limit because you're trying to store large amounts of data. This could be enemy coordinates, lines of NPC dialogue, level configurations, item properties, etc.; whatever it is, chances are it's being stored in a big table somewhere in your project, taking up a bunch of tokens. Instead of storing data in tables, you can often save tokens by storing it in strings and parsing it into tables at runtime.

One of the added benefits of this method is that, depending on your data, you might actually be able to reduce your character count by moving the string data into unused portions of the cartridge memory (e.g. empty map space, empty sfx, etc.) and extract it at runtime.

The implementation of this can, of course, vary quite a bit from project to project. The following is a simple example of how one might convert a table of single-digit numbers into a string parsed at runtime:

data={0,1,2,3,4,5,6,7,8,9,4,6,7,2,8,8,2,8,9,1,6,5,3,3,6,8,9,3,3,6,7,9,0,3,1,2,2,3,5,7,8,9,0}

for i in all(data) do
 print(i)
end
data_string="0,1,2,3,4,5,6,7,8,9,4,6,7,2,8,8,2,8,9,1,6,5,3,3,6,8,9,3,3,6,7,9,0,3,1,2,2,3,5,7,8,9,0"
data=split(data_string)
for i in all(data) do
 print(i)
end

Both programs produce the same result, but the first is 56 tokens and the second is only 18. Notably, each additional element in the first program's data table requires an additional token, but the second program stays the at same token count for an arbitrary length data_string.

Note that this example is just for demonstration purposes, and not meant to show an optimal method for either storing or parsing data.

Use Logical Short Circuiting

Lua logical operators (and and or) stop evaluating and resolve when they don't need to go any farther to fulfil the logical condition. foo() or bar() will never run bar() if foo() returns a truthy value, and the whole expression will resolve to the return value of foo() if it's truthy or the return value of bar otherwise, and the opposite goes for and. thing=foo() or bar() serves to replace thing=foo() if(not thing) thing=bar()

Use Logical Short Circuiting for Arithmetic

The trick above can also be used as part of an arithmetic expression, and is handy when you are using a condition to choose between two different arithmetic operations to perform. if foo() then a = b + c else a = b + d end can be replaced with a = foo() and b+c or b+d

Outdated tricks

Negative Literals as Hexadecimals - Pico8 < v0.2.0

This trick is no longer needed since v0.2.0 but if using an older version of pico8 may still apply

The negative sign counts as a token, so instead of writing negative numbers as you would normally (e.g. -10) you can write them using large hexadecimal numbers (e.g. 0xFFF6). This results in the same number. As a quick shorthand, 0xFFFF is -1, 0xFFFE is -2, etc.

Manually tokenizing a string - Pico8 < v0.2.1b

Most of these steps are not required since the introduction of split() in verion v0.2.1b

data={0,1,2,3,4,5,6,7,8,9,4,6,7,2,8,8,2,8,9,1,6,5,3,3,6,8,9,3,3,6,7,9,0,3,1,2,2,3,5,7,8,9,0}

for i in all(data) do
 print(i)
end
data_string="0,1,2,3,4,5,6,7,8,9,4,6,7,2,8,8,2,8,9,1,6,5,3,3,6,8,9,3,3,6,7,9,0,3,1,2,2,3,5,7,8,9,0"
data={}
while #data_string > 0 do
 local d=sub(data_string,1,1)
 if d!="," then
  add(data,d)
 end
 data_string=sub(data_string,2)
end

for i in all(data) do
 print(i)
end

Both programs produce the same result, but the first is 56 tokens and the second is only 44. Notably, each additional element in the first program's data table requires an additional token, but the second program stays the at same token count for an arbitrary length data_string.

Note that this example is just for demonstration purposes, and not meant to show an optimal method for either storing or parsing data.