How to Oust

February 2024

Introduction

Title picture for Oust

Oust started as an attempt to combine two of my favourite games from my childhood into one. The games are: "Thrust" and "Oids". I wanted to see just how much of these I could squeeze into a PICO-8 cart. I generally prefer the gameplay of Oids, a kind of Defender meets Lunar Lander game where managing your energy and shield strength is as important as your flying skills, but I wanted to keep a bit of the "relaxing pleasure" of towing a Klystron pod back to the surface and the more vertical levels of Thrust.

After a very little thought, the name "Oust" sprung to mind, created by crashing the names of the two titles of its inspiration together; "Throids" was rejected as it sounds to me too much like something embarrassing you might be diagnosed with at a walk-in clinic.

As usual and not to my great shock, the project took on a life of its own somewhat and there are elements in there now that aren't from either game.

OustEd - The Oust Editor

Screen grab of OustEd

It became very clear, very quickly that a level editor would be needed for this project so I hatched the idea of writing that first while including a "test mode" in order to begin actually writing the game.

False Start

5

The version used for the game andthat I've made available is actually the second version of the editor I wrote for this project. The first version doesn't do any procedural generation and was a simple 16x16 tile map editor. The tiles were even the "other type" that have the edges of the caves going through the corners not through the middle of the sides of each tile. That meant there were tiny problems with connecting the tiles at every single corner. I could have changed that easily enough, but the biggest problem with this editor was that levels took up far too much cart size and I wanted lots of big levels.

The game as released has 42 asteroids, some of which are hundreds of screens in size - if I encoded them as 1bit images they'd take up approximately a whole cart by themselves without anything else being added to them. As it is, the cave generation part of the levels (i.e. the 5 proc gen values plus any edits) take up about 500 bytes over the whole lot (before compression). This is only 8% of the total level size never mind taking into account music, sprites and sfx as well.

Screen grab of the first early version of Ousted showing the bad old cave tiles
First Version of OustEd

To test the levels at this point, pressing T on the map would switch to play mode and put the ship where the cursor was. I kept this arrangement for quite a long time and it was really convenient, allowing almost instant iteration. I'd have kept going that way, but fitting the editor and fully featured game into a single PICO-8 cart quickly became impossible and there was only so much use in a nerfed version of the game for testing. Besides, the editor often struggled to fit in a cart all by itself during development.

As with many of my projects, I kinda wish I'd written Oust's editor using a totally different environment to the game itself (e.g. Love2D, Godot) so I could have more of a level on screen at once, more detailed overview maps, multiple windows, undo etc. The main thing stopping this happening recently has been that I wasn't sure how I'd get the random numbers working in another environment in a way that would match PICO-8 for the elements produced by procedural generation. If it doesn't match then the levels won't look the same in the game as the editor - not very useful. It looks like it may be possible to arrange in future though...

OustEd is now available here and information on how to use it is here.

Procedural Generation

I've played with proc gen before (e.g. for PICO Space) and so I was fairly confident I could get level generation to work for the project. My observation of using it myself and seeing other examples is that it's very easy to make a huge amount of content that isn't particularly bad, but isn't particularly interesting either. The conceit for this project is to try and combat that problem by combining proc gen with manual alterations. I've not seen too much of that in other projects. Working on Oust has made me suspect I now know why...

Cave Generation

Every level in the game takes a number of parameters. To generate the caves, it uses a width and height, a random seed value, a "density" (a threshold for the rng to use to generate rock or empty space) and then the number of iterations of cellular automata to apply to the 1-bit output to smooth the caves. This allows for a range of very "bitty" to very "blobby" and quite a bit in between; all with a total of 5 bytes of input.

I took the cellular automata algorithms from this excellent article (I think - there are quite a few similar articles about and I read a few to get a feel for the technique): Generate Random Cave Levels Using Cellular Automata.

There's a simple conversion step from the 1-bit map to the cave topology which essentially maps a binary value generated from four adjacent entries into a cave sprite number. Oust randomly chooses from four sets of these sprites to add a bit more variation to the output.

Cave sprites

Now I'm at the end of the project I'm aware that four sets is probably unnecessary and if I'd used only two I could have avoided a whole bunch of multiple spritesheet shenannigans, especially since I only ended up with half a screen of other sprites. It's really only still like this because I didn't need to take it out for any other reason.
I'd have liked to make a more radical amount of variation - completely different styles of caves - but the generation code around that would have been token expensive and I'd need some nice way to deal with transitions between areas. Maybe I'll do that for a "big" version of the game.

The editor allows you to manually toggle elements in the 1-bit map so that customisation can happen for the levels. Each tweak takes two stored coordinates which adds up pretty quickly, especially on the larger levels where the coordinate numbers are higher precision (up to two bytes for each tweak). On the whole, I tried to keep these changes to a minimum for that reason, and that's why the levels are less "formal garden" and more "natural landscape" (that's what I call it in my head at least).

The caves are stored in the Gfore table. Originally the background caves were in a separate Gback table, but I started to get out of memory errors with bigger levels and so the foreground and background are interleaved in the Gfore table by bitshifting values. Numbers in PICO-8 are always stored as 16.16 bit fixed point so two 6bit values fit easily into one number in the table.

This means that levels of up to 256 by 256 are possible in Oust, although I didn't actually add a level quite this large in the game. Populating really big levels sufficiently proved to be difficult without getting near PICO-8's memory limit and without using up lots of level storage to try and keep it interesting.

I think the biggest level is 256 by 128 tiles which is still 512 PICO-8 screens in size or 8.3 million pixels. This means in terms of pure size of playing area Oust is actually a smaller game than my other game, PICO Space, but still fairly big. And there's a lot more actual stuff in it.

Object Generation

Apart from the ship itself, the caves and the particles, everything else in the game is considered an "object" and lives in a big Gobjects table. For each level there's a count for each object type that can be procedurally generated. Each kind of object has a list of suitable tiles where instances can be created. The code builds a list of "candidate" positions in the level that match the values in this list and that don't have an object at that location already. Then, up to the count of objects of that kind requested, it chooses candidate positions at random and places an object there. So that the objects sit nicely on the cave walls, there's a list of offsets for their positions and whether the objects should be drawn flipped vertically or horizontally. Each time an object is created, its position and any other candidate positions immediately adjacent to it are removed from the candidate list. This avoids clashes and spaces things out a bit.

The editor allows for object deletions for proc gen objects that are in an annoying place. Usually varying the seperate random seed for objects was enough to not need this, but it was pretty handy sometimes. It's just a list of grid positions which are checked after the objects are generated and anything found there is removed. It took more tokens to implement, more level data and made debugging more tricky for the game, but mostly for the editor.

At time of writing, I've still not ironed out all the bugs in the editor with keeping proc gen objects in the right place as I've found it deceptively difficult to get right. It doesn't help that some editing of the levels actually should make the objects all change position. Reloading the level from scratch is the best way to make sure which, of course, the actual game always does.

Objects can also be placed manually in the editor and there are several types which can only be added to a level this way e.g. the teleports, text and "code" objects.

Each level has a flag to determine if the cave tweaks are applied before or after the proc gen for the objects in the level. With the tweaks after, the caves can be edited without affecting where objects appear. This is easier to deal with - as long as an object wasn't in the way of a change to the caverns you want to make.

Oust - The Game

Screen grab of Oust gameplay

Initialisation

Oust goes through the following when it begins:

  1. Clears screen, resets memory, reloads the cart.

    This is mostly to allow testing to be more predictable.

  2. Decompresses the LZW-style compressed data on the cart to 0x8000: sprites, music, levels, sfx, everything really.

    Sets up the menu options in the system menu.

  3. Attempts to load levels from a file "db_oust_levels.p8".

    Firstly this allows for quick iteration on the levels - OustEd writes out to "db_oust_levels.p8" so saving from there then restarting the Oust cart in another instance of PICO-8 was enough to pull in any changes made to the levels in progress. Without this, I'd have had to go through the packing process for all the assets every time I made a change. The packer implements an LZW-type compression and isn't very clever - so this would be slow.

    Secondly, this allows for modded levels to be produced and played by anyone who has the level editor and non-web version of the game (in theory).

    The latest version requires the player to hold down Z/PICO-8 O icon as the game starts to load the custom levels, but that's a very new change.

  4. The level data is pulled out into a big lua table with all the values needed to generate them.

    I kept expecting to need to only do this a level at a time, but it never really became a problem. Unpacking a level at a time might have solved my 256x256 screen level problems, but, as mentioned above, I'm not convinced a level that size would actually be very good.

  5. Oust then loads the save games, if present, using the cartdata function.

    The name of the asteroid field was grabbed from the start of the level data. This is used to make sure save games from different fields don't clash - by suffixing the field name to the cartdata string

  6. A lot of data tables are cached into memory to deal with the player ship's, levels' proc gen or the enemies' "AI" etc. in a "common_init" function that's shared with the editor.

  7. Some more specific tables of values are initialised for the game - collision points and trajectories.

  8. The sprites are then unpacked to their respective sheets.

    The cave graphics are all drawn with 4 colours so they can be stored at 2 bits per pixel. They're expanded to 4 bits per pixel onto their own 128x128 sheet here.

    I'll explain about multiple sprite sheets more below.

  9. Then it's time for the title screen "teleprinter" and the code loops waiting for the player to "press X".

    Oust hasn't reached _draw() or _update() functions here yet so it uses flip() and _update_buttons() since they're "free" to call via "fun strings".

    More on that later too.

The Game Loop

Collisions

While Oust has the usual _update and _draw functions, it relies on pixel reads for collisions - so the line between what happens in those functions is a bit messy. It may mean it doesn't work very well on some systems. I've tried to compensate for situations where the frame rate drops (which does happen occasionally) and test it where I can, but it's still possible to go wrong, I think.

The pixel tests are used for the player and enemy bullets and the ship. They all follow portions of much the same process:

  • if the pixel in question has a colour greater than 1

    • if that pixel colour is also greater than 5

      • check through objects for which other object was hit via a simple in_square check

    • Destroy bullet/register player hit etc.

The other kind of collision is a "background check", as used by the prisoners:

  • check which tile the pixel to be checked is in on the map

  • If that is a non-empty tile then

    • Get what cave sprite is used for the tile

    • get the offset into that sprite

    • read what colour the offset pixel is from the sprite sheet (rather than the screen)

    • If that colour is greater than zero then

      • Report a hit.

This technique allows for "pixel perfect" collisions off-screen and allows the prisoners to keep moving when you can't see them (a bit).

I kind of wish I'd tried implementing all the collisions like this as it would have decoupled rendering from updates a lot more.

I'm a little concerned about the performance cost for PICO-8

I didn't use it everywhere because I thought I'd completed most of the project by the time I implemented the prisoner collisions and I didn't want to break working code. In hindsight, I was probably nearer the start of development than the end so I should have tried it then. Ah well.

I suspect I'll trial this technique if I make a non-PICO-8 version of Oust, since it would avoid reading from the screen entirely - usually a good idea for the accelerated graphics other environments tend to use. I'd need to keep a track on the image/animation frame for everything in the game (kinda need to do that anyway or nothing will draw right, I guess) then keep the colour data or at least some kind of collision mask available to be scrutinised for collisions.

For the ship there's a big list of collision points for every angle the ship is drawn at which are checked against what has been previously drawn in the frame. It also tries to raise the shield if available. For the longest time in development the shield had to be manually activated by default, then I added an autoshield option, then that eventually became default and finally I removed the manual shield only option entirely. Autoshield is hacked in with a goto - but it works and is token-cheap.

The shield has collision points too and works in a similar way - the coordinates of points that are hit are used to accelerate the ship to get the bouncing effect when you hit something. It's not particularly realistic, but I think it works well enough for this game.

The moving enemy objects (imaginatively named "UFO" and "Wallhugger") navigate tile by tile and don't collide with the caves as such. The UFO randomly moves into any empty space tile around it, but there's more chance that it heads towards the player (unless the player is dead, when it heads away to avoid too much "spawn-killing"). The wallhuggers, as their name suggests, use some tangent vector values per cave edge tile and follow that until they randomly set off at a right-angle into empty space - until they hit a wall again.

Also, if there's an object in a tile that a wallhugger moves through that is dead then the wallhugger resurrects it. I added this near the end of development to try and mitigate the benefit of the playing strategy of destroying everything in an asteroid. This is still a problem, but I figured if someone wants to spend the time to wipe out every enemy in an asteroid then they've hopefully enjoyed playing for the extended time it takes.

Technology

Feature creep is dangerous. A few of the things below I always intended to use (compression, multiple sprite sheets), but some of it only happened as I hit the token limits, but didn't want to sacrifice "The Vision". I'm not sure when, but gradually I gave into the dark side and I've used pretty much every trick I can find to squeeze the game into a single PICO-8 cart (and made up some new ones). I don't recommend it and I'm pretty determined not to go this far again.

LZW Compression

Screen grab of the "oust data" tool in action

A data compression technique to try and squeeze as many resources into the cart data as possible.

I always seem to find myself trying to squeeze as much as practical into a single PICO-8 cart. Oust was no exception. In the past I've done such things as encode images drawn with fewer than 8, 4 or even 2 colours at a lower bit depth or I've used run-length encoding schemes (or both), but this time, inspired by the Journey to POOM (by freds72 and Paranoid Cactus), I decided ahead of time to compress all the assets in the data part of the cart with the algorithm here: https://www.excamera.com/sphinx/article-compression.html

I converted the example decompressor to PICO-8, but instead of writing a compressor in PICO-8, I added the compression to a very rough tool I made using the Godot engine that already did some of the data processing mentioned before. As it was still pretty slow even then I resigned myself to having to export data in between PICO-8 and the Godot program and, worst of all, do it by copy-pasting from the terminal.

I didn't need to actually run it very often for most of development fortunately, since, until near the end, Oust would use the restore function to pull in all the graphics, sounds, music and level data from separate .p8 files as I made the game and the data. I've found that a handy approach before, especially coupled with running multiple instances of PICO-8 with the editors open for each "sub-cart" so I can swap to them, make a change and then swap straight back to run the game.

Near the end of development though, running the Godot tool became more irksome - I'd only find out if my game still fit into a PICO-8 cart when I went through the hand-cranked process. I made it a bit easier by writing a PICO-8 cart that pulled everything together, listed some values Oust needed to use the uncompressed data and then output it to the terminal as one chunk to paste into the Godot tool, but even then it was annoying. Annoying enough that eventually I implemented the compressor directly into that same PICO-8 cart instead. It turns out that I find it much better to have a really slow process that literally takes minutes, but needs only a single action to kick off and afterwards is almost entirely automated than a much quicker process, but that needs more interaction. I'd go get a cup of tea, pet a rabbit or take some other break and come back to it. I made the program show little graphs of its progress and leave it off to the side of the screen as I did something else sometimes. I even had a "ping" for completion, like a microwave.

The compression step is what allows Oust to have 25,322 bytes of data packed into 17,152 bytes of cart. It contains:

  • 128x128 pixels of cave sprites.

  • 128*64 pixels of other graphics such as the ship and the enemies.

  • 42 asteroids totalling 6,366 bytes covering 35,451,136 pixels or 2,163 PICO-8 screens
    (the PNG maps of these sum to 3.4MB by themselves).

  • 9 tracks or approximately 33 minutes of music.

  • and a whole bunch of sound effects crammed into 6 sfx.

Obviously, procedural generation accounts for a lot of the "size" of the asteroids, but there's a lot of manually added things like enemies, teleports, text signs and code in there too. In theory, each asteroid could be about 55 bytes in size at 1024 screens each and allow an even bigger, but very empty playing area of about 118,000 screens in total, I guess.

I actually quite enjoyed understanding how the compression algorithm worked and how it was implemented using a bit stream class to pack data as efficiently as possible at the bit level. Then I realised, especially since it was needed for the decompressor anyway, this code was sitting there, available for other purposes...

Bit Streams

Packing data into memory at bit level granularity instead of byte level.

If you've ever set bits in memory then there's nothing particularly complicated about what I'm calling a bit stream. It just abstracts the tracking of which bits in which bytes you've written to or read from and the bit shifting needed to get and set those bits. As such it's very handy to write and read data which doesn't sit nicely in 8 bit chunks. So, for example, instead of wasting an entire byte for a value that could only ever range from 0 to 7 you can read or write 3 bits instead. Single bit flags are really easy and packed 8 to a byte without any further effort.

	--Bit stream
	function BSinit(where
	--dbg("*** BSinit $",where)
		GBSptr,GBSstart,GBSmask=where,where,1
	end
	--get a bit
	function BSget1()
		local r=tonum(@GBSptr&GBSmask!=0)--and 1 or 0
		BScycle()
	--add(debug,r)
		return r
	end,
	--get n bits
	function BSgetn(n)
		local r,n=0,tonum(n) or 8
	--dbg("Load $ bits",n)
		while n>0 do
			n-=1
			r<<=1
			r|=BSget1()
	--r=r<<1|get1(_ENV)--1 more token
		end
		return r
	end
	--set a bit; assumes memory is zero
	function BSset1(v)--
		if v!=0 then
			poke(GBSptr,@GBSptr|GBSmask)
		end
		BScycle()
	end
	--set n bits; max is 16
	function BSsetn(v,n)
		local n=n or 8
	--dbg("Store $ bits: $",n,v)
		for i=n-1,0,-1 do
			BSset1((v>>i)&1)
		end
	end,
	--bytes processed by BS since BSinit call
	function BSget_length()
		return GBSptr-GBSstart+(GBSmask==1 and 0 or 1)--(1-GBSmask)\GBSmask
	end
	function BScycle()
		GBSmask=GBSmask<<1
		if GBSmask==0x100 then
			GBSmask=1
			GBSptr+=1
	--dbg("BS+1 get")
		end
	end
	

Oust uses the bit stream stuff for:

  • Asteroid data. Each option for the asteroids only uses the required number of bits e.g. there are 32 palettes to choose from which is stored per level in 5 bits.

    Any locations are stored using more or fewer bits depending on the dimensions of that particular asteroid's map e.g. for an asteroid with width 28 and height 97, a manually placed prison's location is stored as 5 bits followed by 7 bits.

    On/off flags for the levels are single bits, values per object are restricted to as few bits as possible.

  • Save game data. Whether an asteroid has been visited, an orb retrieved or a prisoner rescued is stored in the cdata range of memory using a bit stream. This works fine until the asteroids are changed by the editor - e.g. if an orb is added halfway through the asteroids then the save game after that will be affected.

  • Cave graphics. Since the bit stream was already there I used it when upsampling the cave sprites from 2 bit/4 colour to 4 bit/16 colour.

In the end I made the decision to convert the bit stream class to global functions to save tokens. I didn't really want to do this: the big downside is that, as the bit stream relies on some variables that now have to be global, I can't have more than one "live" bitstream at a time. In Oust, I only really felt this was a problem for a single use case: it would have been nicer to unpack the asteroids data and correlate save game progress through the asteroids concurrently instead of having to unpack the save game data first, store it in a table, then track my way through it as the asteroids unpacked.
I'm still tempted to try putting it back to a class at some point to see if the token cost is less than the workaround.

Fun Strings

Or strings that are a list of function calls that are interpreted crudely at runtime by a routine in the game.

I call them "fun" strings because initially I shortened the function name to "fun_str". At the end of development I was so desperate for space in the PICO-8 cart that this became "F".
And because they're fun. Obviously.

This routine looks like:

function(...)--F
	local res
	for i,str in inext,split(format_str(...),';') do
		local f=split(str)
		res=_ENV[f[1]](unpack(f,2))
	end
	return res
end

And it's used a bit like this:

F"cls;rectfill,0,30,30,60,5;spr,10,16,16,1,1"

or

F("cls;print,something to read by $,$,$,10",name,x,y)

The first line uses 2 tokens vs 13 tokens, the second uses more, but is still far cheaper than "normal" code. It only really works because PICO-8 is so forgiving while interpreting strings as numbers. I very rarely have had to add a "tonum(x)" to some of my own code.

I call them "fun strings" because for the longest time the function name was "fun_str". They're just an upper case "F" now to save characters since I started hitting the compressed size limit.

If I do another extensive project in PICO-8 I think the only way to top this will be to try and implement a mini-language and interpreter. As a fun challenge it's really tempting, to be honest.

set() and constr()

Ways to initialise tables and values that cost fewer tokens.

Typically a table is defined in PICO-8 lua with something like:

tab={
	x=20,
	y=40,
	name='banana',
	draw=function(self)
		spr(10,self.x,self.y)
		print(name,self.x,self.y-6,10)
	end
}

This example takes 34 tokens.

In Oust I'd make the same table using F(), constr() and an unpack(split()) function (u_s) like this:

constr('draw,x,y,name,',
function(_ENV)
	F("spr,10,$,$;print,$,$,$,10",x,y,name,x,y-6)
end,
u_s'20,40,banana')

This is 18 tokens. Note the use of _ENV which is me trying to implement ideas from this thread here.

Is this readable, easy to maintain, nice to work with or a good idea at all? Probably not, but I certainly couldn't have made this game without such "innovations". I see the lengths I've had to go to squeeze functionality into Oust as a sign that I need to use a less limited environment to implement future projects of this complexity. I'm not intending to abandon PICO-8 entirely, but I think I need to make something in a different tool or I risk very much getting stuck writing more and more obtuse and complicated code; which isn't really within the spirit of the console and really gets in the way of making games. And it's becoming a bit of a chore.

Sometimes I wanted to add to an existing table in a less expensive way than something like:

tab.x,tab.y,tab.name=1,b-7,'banana'

I wrote another function to do that something like this:

set(tab,'x,y,name',1,b-7,'banana')

This function has the advantage that the table argument can also be '_ENV' so its possible to set global values. And because functions live in the global _ENV table they can be defined using "set" too. Which means that:

function a()
-- function code
end
function b()
-- function code
end
function c()
-- function code
end
function d()
-- function code
end
function e()
-- function code
end

(15 tokens) can become:

set(_env,'a,b,c,d,e',
function()--a
--code
end,
function()--b
--code
end,
function()--c
--code
end,
function()--d
--code
end,
function()--e
--code
end,
)	

14 tokens, with each function definition costing 1 token less this way. Over the extent of a whole game this adds up, especially since the cost of the set function has already been paid for by its other uses and it's only 28 tokens itself.

function set(t,k,...)
local v={...}
for i,k in inext,split(k) do
	t[k]=v[i]
end
return t
end

Multiple Sprite Sheets

The fastest way I've found to use more than the 128x128px sprite sheet with spr on PICO-8 (at this time).

PICO-8 has a hidden multi-display feature. To use it you call a function to map each display to be the current one e.g. _map_display(2). To see the extra displays you need to start PICO-8 with some command line parameters, but even if you don't do this then you can map the displays 1-3 to be current without them being visible. Anything drawn to those screens persists in its own section of memory, outwith the usual PICO-8 memory space.

Newer versions of PICO-8 also allow you to swap the address of the sprite sheet and the screen. If you do that, then you can instead use the extra displays as extra sprite sheets. To swap between them is as fast and "token-cheap" as a single function call e.g. _map_display'3' (and works nicely with my "fun_strings" too).

Oust unpacks its graphics then fills display 0 with cave sprites then display 1 with ship and object sprites. It uses display 2 for the mini-map and display 3 to store the background for the pause menu (because it was handy, available and I thought of it when I wanted it).

I made a post about the multi-display sprites with a proof of concept demo here. I made a game that uses actual multiple displays and sprite sheets here.

Code Objects

Screen grab of some code objects as seen in Ousted

In-game objects that have a "fun-string" attached to them that are executed when the player gets within range.

I'd been happily making levels using OustEd, adding the various objects I'd made, tweaking option values and poking at the map itself. I'd added a bunch of text objects to provide signs and add a bit of "lore" to the asteroids and I'd been using my "fun strings" to save tokens and let me add more and more to the game. I guess it was inevitable that I'd eventually notice that I had a means of adding strings to the level data and a means of (crudely) interpreting those strings and realise I could combine the two into a really basic kind of scripting...

In the end, the code objects actually use a slightly different version of the "fun string" code, one which allows them to reference some of a code object's variables (e.g. their position) and, after adding a few flag values to the global scope, a few "events" in the level.
Suddenly, my previous attempts at levels, particularly the training levels at the start seemed unsophisticated.

And another massively deep rabbit hole was revealed between where I was and finishing the game.

Having finally emerged from that lagomorph's burrow, I'm hoping the extra time and effort has been worth it in making the game more interesting and fun. I've certainly found entertainment in trying to wrestle the very spartan capabilities on offer through these "code" objects into achieving different things to encounter in the levels. One thing it has made me think about is how, in an environemnt that allows for proper code evaluation, there could be a huge amount of scope for further game content.

Conclusion

In general, I'm happy with how Oust is now. For a large chunk of the last year I've appreciated having a game in a genre I enjoy to tweak at and sometimes just to play. I think it's generally a success at what I was trying to do. As a learning exercise it's been pretty informative as well. It's leaner than my other games: PICO-8 is great for adding eye-candy via cute little graphic effects and in other projects I've found I spent a lot of time, tokens and storage on these. With Oust I really tried to keep a lid on that and concentrate on adding features dedicated to gameplay mechanics first; "gameplay uber alles". It still looks okay, I think.

The ability to load asteroid fields from a separate file has been a blessing and a curse. As far as allowing faster iteration through letting the game and editor work on the same data it's been essential. Keeping it so that the released game can play custom asteroids has taken a great deal of time, working on the editor and documenting it similarly has taken ages. I would really have liked to allow the music to be stored in the asteroids file so that it could be replaced for custom asteroids too. It's so difficult to squeeze anything more into the game that I don't think it's ever going to be possible to allow that, unfortunately.

What has been nice is the feeling that I'm very few steps away from making a game that can consume a separate file of level data, music, graphics etc. and so is highly flexible and acts more like its own "engine" (such a loaded term these days).With the "code" objects there's even a little game logic in there that's outside the game code itself.

I'd like Oust to have taken less time to make. I'd like the code to be less convoluted and more maintainable.

I'd like Oust to have a bigger screen; in my opinion it's the single biggest problem with actually playing the game: the ability to see threats and obstacles coming is severely limited by the small resolution of PICO-8. I made the ship as small as I dared. I let the ship's weapon fire as far as is possible without firing off the screen. I did consider using the multi-display feature mentioned above, but getting the framerate high enough with a level of visual complexity that was acceptable (to me) just doesn't seem possible (Oust drops frames fairly easily as it is). If I were to re-implement Oust in some other platform this is likely to be the one change I'd be looking at trying immediately.