Posted September 22, 2019 by -hexcavator-
#pixel art #shaders #LOVE #roguelike #graphics #procgen
Now that I've introduced VERGER, my hope for this devlog is to do a series of semi-technical design and development posts. I am very much self-taught in this work, so apologies in advance for making mountains out of molehills, over/understating the obvious, etc
** AHEM **
VERGER's aesthetic is a hybrid of PICO-8 constraints, my fondness for low-res pixel art, and a not-too-strict nod to the utilitarianism of classic roguelikes. Don't get me wrong, I love ASCII, but I wanted to bump the fidelity up a notch, if only to give myself room to experiment.
The palette I'm using is identical to PICO-8's (thanks @lexoffle!) with a modified white value. The "old TV" effect is achieved through Moonshine, a library of mostly public domain post-processing shaders adapted for LOVE by Matthias Richter.
What I want to focus on in this post is sprite management. VERGER's in-game art is a mix of static color sprites and palette-swapped greyscale tiles. I became interested in the latter technique while working on BIT RAT: Singularity. At the time, I was looking for a way to quickly test color variations, but I quickly became attached to the subtle variations and unexpected constrasts of this approach.
While creating my 7DRL demo, I used Pico-8's built-in palette swapping (I'm guessing some sort of bitwise operation). LOVE doesn't have a comparable function, so I dug around and ended up adapting the pixel shader discussed here: https://love2d.org/forums/viewtopic.php?t=83780 . The nuts and bolts of shaders are absolute sorcery to me, so full credit to these folks for working this out!
As development progressed and I added more content to the game, I faced a dilemma. Because I'd drawn all my greyscale sprites with the same four values, I ended up having to constantly swap these for different colors from the larger palette at runtime. This meant passing new values to the shader over and over for almost every visual asset in the game.
(Why didn't I just use more greyscale values? Because the point of the four-tone system was to create palpable, consistent aesthetic constraints during the drawing process. For me, the disadvantages of trying to remember a 10% white grey on monsters has equivalent in-game brightness to, say, a 25% white grey on terrain features far outweigh a more programmatic solution.)
After nine months of avoiding the issue, my draw loop was total spaghetti. Meanwhile, I'd picked up a ClockworkPi Gameshell, and thought it would be cool to try to play a Debian build of VERGER on it. The game ran -- albeit EXTREMELY slowly -- but the palette switching I relied on was completely broken.
This led to a lot of probably pointless fretting about optimization and my ignorance a programmer. I resolved to find a way to pre-populate the spritesheet with everything needed for a given season, thereby eliminating the need for shaders. Before you protest with some completely reasonable explanation of how I ought to have handled this, kindly note that I am stubborn and crazy and will not listen.
LOVE supports framebuffers (canvases) that have basically the same functionality as any other image object. After some initial experimentation, I decided I would use this feature to chop up my convoluted, shader-dependent external spritesheet and create a tidier internal one with pre-colored sprites for quick reference from the draw loop.
I still needed a method to palette swap greyscale tiles, but this would only happen once per "season" when the internal spritesheet was created. Using canvases also solved a problem that had emerged when I implemented "character creation"; namely, I'd been drawing the player as five separate (and separately colored) sprite layers every draw step based on which body/hair/etc variants were active. The new internal sheet would flatten these into a single set of quads.
After a few days of picking at the problem, I ended up with the above. The far right of the image is the external spritesheet, which remains an arbitrary mix of color and greyscale tiles of various sizes. To the left are the virtual spritesheets (canvases) created programmatically as the need arises in-game.
The number labels superimposed over the canvases are their in-game sprite indices. There are separate lists for 8x8,16x16, and 32x32 sprites. The character creation options at center-right only exist until the player creates a character, at which point that canvas is released and the flattened in-game player sprites are generated and rendered to the virtual spritesheet around index 170.
Now, when I want to draw sprites in-game, I just point to the appropriate index on the 'virtual' sheet. No more shaders in the draw loop, no more complex layering and coloring of player sprites. Player status effects (charging mana, being wounded, teleporting) are handled with simple index offsets. I'm still playing around with how I implement these ideas, but the basic approach seems sound.
There are some caveats -- most notably, changes to screen/window size using love.window.setmode automatically clear any canvases currently in use. So far I've worked around that by dumping the canvases into raw ImageData if a graphics mode change is happening, then re-drawing that data to a newly created canvas afterwards. There's probably a smarter way to do this, but for now it works without major issues.
Anyhow, that's a bit more of, uh, where things are, and where they're headed. Thanks for indulging this unwarranted glimpse into my process. And, as ever, if it's helpful or confounding or you have any kind of feedback, I'm always available in the comments!
Thanks in the meantime for reading; hope to see ya again here soon!
b