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:
btn
/btnp
: The second argument is an optional player ID (rarely needed for single-player games).sfx
: The second argument is an optional channel, and the third argument is an optional offset.music
: The second argument is an optional fade-length, and the third argument is an optional channel mask. If you want to play music from the beginning of the tracker, you can even callmusic
without any arguments.
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.
- Use when: you're passing arguments which don't change the behaviour of a function.
- Caveats: Arguments must be specified in order, so try to order your arguments such that they are specified in decreasing order of likelihood to be passed in. If the function only has one argument, the point below about calling functions with strings can probably save the same number of tokens without any overhead.
- Saves: 1 token per argument omitted, with an overhead of up to 5 tokens if you have to replace the
nil
argument with your own default value
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
- Use when: calling a function with a single literal argument.
- Caveats: Numbers are passed in as string and may need to be converted. String-to-number conversion is fine for most cases, but currently has a rounding error with negative numbers (see http://www.lexaloffle.com/bbs/?tid=27597).
- Saves: 1 token per function call
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.
- Use when: assigning multiple variables at the same time.
- Caveats: If you reference one of the variables being assigned within the comma-separated list, it will evaluate to its value before assignment, regardless of order. e.g. the statements
x,y=1,2; x,y=3,x
will result inx
having a value of3
, andy
having the value of1
. You can mix-and-match different variable types here, but you can't mix-and-matchlocal
and global variables. - Saves: 1 token per variable
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
.
- Use when: assigning multiple variables at the same time, some of which are being set to nil
- Saves: 1 token per variable
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.
- Use when: you're prepared for commitment.
- Caveats: It's really annoying to change a constant manually after taking out the variable, so try to leave this one to the end if you can.
- Saves: 3 tokens per variable
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
.
- Use when: you're relying on the editor to do the math for you.
- Caveats: Again, it's harder to edit once you've made the change so leave it to the end if you can.
- Saves: depends
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.
- Use when: you're treating a table as a mental model instead of a table.
- Caveats: This typically isn't useful if you're using OOP stuff; e.g. if your player and your enemies are all tables that have
position
and are handled in one loop, you probably won't save on tokens by taking one of them out of the system. - Saves: 1 token per 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.
- Use when: you're accessing a table multiple times within a scope block
- Caveats: The table might not even be necessary in the first place; make sure to check if it's just a mental model.
- Saves: n-1 tokens per table access, with an overhead of n+2 tokens to create the local variable (and an additional n+2 if you're modifying it and assigning it to the table at the end), where n is the number of tokens needed to access the variable
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
}
- Use when: you're declaring tables and their properties at the same time
- Caveats: Similar to the comma assignment point above, you won't be able to reference values which are being assigned simulataneously. e.g.
t = {}; t.a = 1; t.b = t.a
will result int.b == 1
, butt = {a = 1, b = a}
will result int.b == nil
. - Saves: 1 token per property declared
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.
- Use when: accessing table properties with string literals
- Caveats:
table[key]
allows for a variablekey
, whereas property access is static. - Saves: 1 token per access
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.
- Use when: you need vectors
- Caveats: If you need to loop through the properties for whatever reason, your loop structure may have to change (e.g. you might need to replace
for i in all(point)
withfor k,v in pairs(point)
. - Saves: 1 token per access. Without constructor function, costs 4 tokens per creation. With constructor function, saves additional 1 token per creation, with an overhead of 13 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 when: you're dealing with large amounts of similar data stored in tables
- Caveats: Remember that PICO-8's other limits (character, compressed, RAM) can be just as restrictive as the token limit; this technique is useful but it doesn't give you infinite storage. Also, strings aren't limited in length, but accessing data in a string with more characters than PICO-8 numbers can represent may require a bit of extra work (e.g. the
#
operator will overflow). - Saves: depends; usually a whole lot of tokens, but with a fairly large overhead
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 when: you need to run multiple functions until one returns a good value
- Caveats: logical operators can be tricky to follow when used this way, and 0 being truthy can cause problems
- Saves: about six tokens per instance
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
- Use when: a condition leads to one of two arithmetic operations
- Saves: about three tokens per instance
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.
- Use when: assigning, multiplying, or dividing negative literals.
- Caveats: Using hexadecimal numbers takes up more characters. Doesn't help with addition/subtraction because adding negative numbers can be replaced with a subtraction, and subtraction needs the
-
operator. - Saves: 1 token per number
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.