Inside P8C-BUN
Thurs, 22nd Oct 2020
Writing P8C-BUN I was fairly determined not to fight with PICO-8 too much since I was so new to it and because I wasn't too concerned about what came out the other end.
I ended up doing some things that might be a bit out of the ordinary and thought somebody might find it interesting - even if it's just future me - so while I can remember, I thought I'd note some of them down. A lot of them may be rather stupid. I guess this is also a kind of post-mortem/retrospective. It's not exactly the world's most complicated, important or clever game project and there are some far more impressive things out there, but hey - maybe somebody will learn from it.
Update and Drawing Routines
In terms of structure of the code and for navigating around I don't like having a single _update and _draw function for every game mode and using an extended if/elseif and even a gamemode variable was getting me down. There's no switch statements in Lua either. Instead I set up a couple of tables with function pointers for output and draw routines. By the end of development, partly to save tokens, but mostly to save sanity, these had become 2 global variables that I pointed to the appropriate update and draw functions at a given point in time. Usually this was done by my fade routine (see below).
The actual _update and _draw routines did the bare minimum other than call these functions through the globals.
function _draw() pal() dr_func() fade() endand
function _update60() if jbox.change and stat(24)!=jbox.pat then jbox.new_track() end pulse,fr=(pulse+1)%192,(pulse\8)%3 upd_func() end
The pulse global is used by almost all of the animation in conjunction with the integer divide operator and modulus (\ and %) to govern the frame rate and number of frames for each animation. Some of this uses separate graphics, some of it uses palette cycling.
Before I go into that more I'll mention the "jukebox" stat call is to try and make the music change "track" in time i.e. at the end of a pattern. When a new track is requested it stores the current pattern number and only changes when the current pattern no longer matches it. As I can only check every 60th of a second it doesn't always work very well (it's amazing how precise timing in music has to be), but I left it in for the times it does. The idea was to convey how Pac-bun always keeps running. Deeper meaning. Stuff.
Animation and Palettes
The fr variable dictates which frame the current bun is drawn with for each of their animations and for the fox (and ghost). Three key frames of animation can do an awful lot (two is pushing it usually). Given the characters are 8x8, tweening isn't really necessary or possible. When I needed a different number of frames I derived the number directly from the pulse variable. On reflection, I could either have used more "fr"s e.g. fr2, fr3, fr4 and saved some tokens or not used fr at all.
I cut my teeth on palette manipulation with the Atari ST, which also allowed 16 colours on screen (more or less), but had far more colours to choose from. The classic demo of this was the Neochrome Waterfall (glorious isn't it? 1985... also see these even more impressive palette cycling pictures). I couldn't resist putting some water into the P8C-BUN levels using a little palette cycling of my own.
I tried out frame-based animation for water too and in the end both techniques are present in the game. The rivers have animation frames, the pond/sea are entirely palette cycling. The water levels are where I first started utilising the extra colours in PICO-8 (the extra blue: 140). These are dropped in every now and then, but I stuck to the original 16 most of the time.
The Christmas lights, flames, generally anything flashing is done with palette cycling.
Less obvious palette switching occurs to draw the bunnies. Originally I had 4 rabbits, but I'd already been toying with the idea of adding our own, real, live, eating, sleeping and pooing pets to the game (see here). So I added their markings in rather odd colours and mapped those colours to something more representative at run-time.
After that, all 9 (yes, 9) rabbits used exactly the same sprites, just with palette switching. Pac-bun, Brownie, Oreo and Macaroon are still done this way, for instance.
Mapping each of the colours in the bun sprites to different output colours with the pal() command allowed the sprites to be recycled for multiple characters. You can see the 12 total frames, four animations of three frames, for both the buns and the fox as well.
Palette switching is the only way I could see to get the fades between screens to work as well. I set up a mapping from each colour to the next darker colour (for all 32 colours) in a table then run the current palette through that mapping for different values of "dark". So doing it 4 or more times gets every colour to black. The same technique could be used to fade to white or any other colour. Using the extended palette is fine since 16 colours are only ever mapped to 16 others.
Well, at least until you want to fade the screen a bit and still have yellow text on top (the paws screen). Then you realise you've got different colours on different levels and can't just have a special bright colour without ruining the effect. So for the paws effect I scan the whole screen replacing colour 15 with another one before using that for the text. It looks fine. It kills the frame rate and is the only place where P8C-BUN drops below 60fps. Slow motion looks good for a pause screen - right?
Just to underline: the pal() command is over-powered and over-fun in PICO-8 and almost worth the price of entry by itself; palette switching for what PICO-8 calls the "screen palette" was pretty straightforward back in the day (at least for the ST) and gave you more colours to play with, but the equivalent of the "draw palette" mode of the command was never so easily available, complete or cheap (that I was aware of at least). Every drawing command (sprites, lines, shapes etc.) obeys the draw palette without further work from the programmer. It's also completely obsolete, irrelevant to and largely unavailable on modern devices that have more direct colour systems and don't use palettes so reasonably you could argue "when else are you going to get to play with this?" :)
Anyway, I showed my girlfriend at about this point and she immediately pointed out that Dandelion is a lop-eared rabbit and only raises one ear at most at a time (no good deed ever goes unpunished). I grumbled about memory, performance, the alignment of the heavens, tokens and such, hoping she and I would forget it, but it rankled me.
It was about this point where I got a bit more adventurous, in general.
I realised that 16 levels were going to fit very conveniently into the remaining map data and got interested in the specifics of the PICO-8 memory layout. I'd made such progress that I decided to treat P8C-BUN as a full project that I needed to finish (I know, right?).
I dug into performance metrics and found the little graphs available when pressing Ctl-P/Cmd-P. And found I was using almost none of the processing power of the PICO-8. At least from my experience, the idea that PICO-8 is "8-bit" is selling it rather short - I'd say it's processor is more powerful than many 16bit machines (esp when balanced against the screen resolution, cartridge size and high level language commands - try getting an 8bit machine to run Doom using BASIC). Oh to get access to the underlying machine code... ;).
Of course, in my mind, that immediately raised the Bar of Acceptable Quality and little imperfections that had been fine before began to annoy me. And things that I had no intention of bothering with suddenly became important. Like a logo. You've got to have a logo! Everyone knows that!
But I didn't want to eat into the remaining sprites available to me in PICO-8 and I'd just been reading about the relative allowances of text characters, cartridge memory, tokens etc. Besides I liked the idea of drawing a logo, encoding it and rendering it with non-standard colours. I grabbed a sprite editor (Pixen) and digital-fingerpainted the logo at the top of the page. I cranked up Python (and SDL2) to write a little CL tool to encode the PNG produced into a string and then pasted the output into PICO-8. Then I made PICO-8 extract it and draw it. It's been through a few iterations to make it a bit more efficient and works fairly nicely now. I drew the line at writing actual compression routines. Maybe I'll talk about my "Bad Bad Apple" project sometime.
Dandelion
Eventually I knew I had to do something about Dandelion.
This is Dandelion, our oldest rabbit, although you wouldn't know it. She loves to explore so much that she can open doors that are even the slightest bit ajar and is constantly getting out of where she lives with Brownie in our kitchen. She is very cuddly, likes long, snoring afternoon naps and is very good at games and toys.
Building on the "triumph" of the logo I altered the same Python code to encode a new set of rabbit sprites, this time with lop ears and pasted that in the game code.
Dandelion is pulled out of the resulting string into memory when she is going to be drawn (it's literally the same function that sets up the buns' colours) and stays until the original is reloaded from the cartridge when another bun is needed. This works well enough that I added more "custom buns" for Pinkie, Blue and Bowie. I considered doing every rabbit this way, but it didn't seem necessary. An obvious optimisation to try would be to keep the "standard" bunny sprites in memory as well instead of reloading, but I'd begun running out of tokens at this point and was nowhere near hitting performance trouble. Perhaps for P8C-BUN 2?
The same trick is used for the custom sprites for other levels: the witch and the flying saucer. Except for the ghost on level 9 which is just another sprite. I did this really early on and it never broke so I never felt like changing it.
Perhaps interestingly, this is one part of what makes the title screen the slowest view in the game - well, apart from the horrendous paws screen. Drawing all of the bunnies in one frame involves a bunch of memcpys and reloads. The other slow bits are the mini-level and the outlining.
Dandelion is a very grey bunny. If she's out in the garden and it begins to get dark she quickly becomes nearly invisible against the grass. Brownie is not a great deal better. In the game, Dandelion was pretty hard to spot unless your colour vision was _really_ good. I tried making her lighter (didn't look like Dandy and still wasn't easy to see), darker (even harder to see) and even tried drawing alternate shades of grey each frame (some monitors looked fine, others flickered very badly).
One of the very last things to be added to the game were the outlines to the game objects. By this point, I'd run out of sprites and really didn't want to have to redraw the bunnies, the fox and the rest with outlines - I had no idea how I'd fit the bigger resulting sprites into the sprite sheet nor how I'd draw them without big changes that wouldn't fit in the remaining tokens (approx 0).
I sent the game to some friends who had struggled with it, partly because picking out the objects that affected play was difficult. I'd read a few threads where other developers had added outlines to their sprites programmatically. And been horrified - the mentioned algorithm seemed to be to set the draw palette to black (ok), then draw the sprite shifted up a pixel, left a pixel etc. until 8 copies of the sprite were drawn(?!?) and then draw the sprite as normal at the original coordinates(ok) - hey presto an outline. I hope this isn't something that is done routinely by anyone since it implies a truly horrendous amount of overdraw (background, 8 layers of black, sprite). I also couldn't see why there was any need to draw the diagonals.
In desperation I tried a compromise (that stuck). All interacting elements in P8C-BUN (e.g. buns, fox, nanas, skins, kites) are drawn with an outline created by drawing a blacked out sprite a pixel in each direction (i.e. 4 times) and then the actual sprite. The result is terrible for performance - it takes a big chunk out of what I had to spare, but since I had so much left it's stayed.
Suddenly, Dandelion could be seen.
Drawing the Maps
The third reason why the title screen is the worst performing was drawing the mini-map and this is also where a huge chunk of the tokens disappeared to for P8C-BUN as a whole.
I wanted to have black items on the maps. But black is transparent by default and that had been kinda handy for most of what I'd already drawn. So I thought I'd mark all the sprites that needed black with the sprite flags that PICO-8 provides and use a different colour "pal-ed" to black. I added an if/then statement and forgot about it. Then I wanted animated background tiles. Then I was drawing the easter eggs for the Easter Island level (yes, it's a bad pun) and didn't really fancy drawing the same thing in different colours and using up multiple sprites just for that. So I took my function pointer trick and set up a table with multiple versions of the sprite command for drawing the map. And I thought I'd do the same for the mini map - so I added an if/else into each of these functions.
There's 20+ different ways to draw tiles in the map each governed by the flags on a particular sprite. This is how the fairy lights are done for the Christmas level. It's how the multi-coloured flowers are done, the dancing ghosts etc. The coordinates passed are used to shift some of the sprites in a way to break up the "grid-ness" a bit as well. On reflection, I should have kept the mini-map drawn in a single way as it would have been "good enough" and that would have saved me a huge amount of tokens for other things. I might still change that if I feel very inspired to add more to the game in the future.
Music and Sound
I'd already written tracks for the other, non-PICO-8 version of P8C-BUN so I set about making PICO-8 versions. It's pretty hairy, isn't it? I'm fairly happy with what came out although it certainly isn't the easiest music composition environment I've ever used. There are some severe limitations to the sequencer that I'm not overly enamoured of and I swear I found a bug in that a custom instrument made from another custom instrument only partly works - kinda feels like it really should be works fine or not at all. I actually prefer some of the bass lines in the PICO-8 versions to what I'd done in Logic earlier. Of course, I can port those back very easily where adding more to the PICO-8 version would be problematic.
Making silly sounds was quite fun. I think they're okay.
Conclusion
On the whole, it works and the code got better and more uniform as I went. I think if I kept going it would be even prettier, almost to the point of not being spaghetti. That's what it is though. Making the code pretty isn't the point. If I really want to add some functionality I might feel more inclined to change things in the future. I suspect the first bit to go will be the mini map sprite routines.
What would I do differently or change now?
- The mini-map and menu on the title screen. I could make it prettier and use fewer tokens.
- Spend less time worrying about the game being too easy. I had to make it easier in a couple of ways.
- That said, I'd be tempted to add another enemy or different types of obstacles. I wanted to have frogger style logs on the water, but I started running out of tokens and sprites and they didn't seem v necessary to the game.
- Making some of the hacks to add extra sprites and effects to work could be done a lot more neatly
- More random creatures would be nice - I wanted froggies, but ran out of space
- I never cracked the "turn before the corners" feature mentioned on pages like this. TBH I didn't fancy messing with the bunny code once it seemed to work.
Comments
No comments here yet.