Making snake snake
Hello again, first tech article from my PICO-8 journey is here. This time I’m going to walk you through the ways the snake is rendered and moved in Snakyval.
One of the biggest hurdles in jam game development that should be dealt with as soon as possible is getting main game mechanics to work. In case it fails, there is still enough time to make changes to game design and try different mechanics or different approaches. In Snakyval’s case it is snake movement and scaring the snake with apples. Without figuring it out, there would be no game.
I approached the problem gradually, splitting it into easily manageable parts.
The queue
First things first, let’s sort out the basics. We need to move the snake one tile forward, keeping rest of his body and move his tail if needed. To place something in front of original snake and take something from his tail sounds like a queue, and it is the first idea that every sane programmer should get. A queue is an abstract data structure that handles this functionality.
The PICO-8 games are programmed in a dialect of Lua, which is very simple but capable and well designed programming language. But, there is no built-in ready to use queue type, you are on your own. Or, you could use existing library, copy&paste it to your project, take more precious PICO-8 tokens than needed and fight with other programmer’s choices. So, I went with a ready made queue implementation using an OOP approach with metatables which I fought with for a while as I was learning Lua together with PICO-8 and tight deadline. But the core of the queue implementation is the same I would use, so let me outline.
I use an array (table with numeric indices) for the queue data. Each cell contains snake body part coordinates, along with more data (explained later). Whenever I put into the queue, I add it as a new element to the end of the array (by calling add function). Whenever I take from the queue, I mark the first untaken element as taken by keeping first and last valid index of the queue. To avoid potential overflows, I rearrange whole queue when the first index is higher than count of tiles in a queue. And that is all I need for a queue:
So, whenever the snake moves (every 10th frame usually, but that is configurable), the game pushes coordinates of new head (based on a direction) and increases body length. If the snake is longer than it should be, the tail is popped from the queue and body length is reduced back. When I want the snake to grow, I just raise the target length. When I want to check for biting itself, I simply loop over all snake body parts and find match with coordinates of new head to be added. When I want to draw the snake, I loop over all snake body parts…
Drawing the snake
Well, drawing the snake is not that easy. In most basic Snake implementations (often found in the internet), snake body is just a single colored block and that is all. In these olden days it was not required to have distinct body parts to show the player the snake shape clearly. But in 2024, in a new game? Unacceptable. The snake shape must be recognizable, he is the hero after all.
So… how to do it? One approach is to look for coordinates of body part neighbors in the queue. If the part is not head, it should have one part before. If the part is not tail, it should have one part behind. You can then make a set of conditions and select proper sprite based on direction before and behind.
But I chose different approach. I am lazy person. Very lazy one. Copying queue from somewhere and fixing some bugs in it, ok. Writing complex set of conditions? Not my idea of time well spent. I decided to assign each body shape a number I can simply compute. I already have four directions to know where to head next - DOWN, UP, LEFT, RIGHT (any order would do, but you should be consistent from then on) represented as numbers 1, 2, 3, 4. How can I encode 2 directions in one number?
Easy - I multiply one of the directions by four and add another direction. So I get: 5 = 4 * 1 + 1, DOWN from DOWN, or 11 = 4 * 2 + 3, UP from LEFT etc. Every number from 5 to 20 is a middle section of snake, all directions covered, easily put together and error-proof. For simplicity, I have decided to reserve numbers 1 to 4 for heads. And tails? There are some combinations that don’t make sense for middle sections, more about it later.
As I have snake head with certain shape (1 to 4, which are coincidentally head directions), when adding a new head (whose body style is easy to determine), I have to upgrade the old head to middle section. And I do it simply by taking the old head style number, multiplying it by four and adding new direction to it. Bam, I have a new head with proper sprite assigned and a new middle section with proper sprite assigned without single if statement! Just a few simple arithmetic operations (and lots of PICO-8 tokens saved).
So what about the tail? They are former mid sections, and I need just one of their next direction. These can be extracted by dividing by four. But be careful - it must be integer division (using \), and 5 to 8 (9 to 12, 13 to 16, 17 to 20 respectively) should end up in the same bag, therefore subtracting 1 before division is needed. And then? I combine it with opposite direction to it! As it doesn’t make sense to have a middle section DOWN from UP, I made it a tail. So, tails are encoded as DOWN-UP, UP-DOWN, LEFT-RIGHT and RIGHT-LEFT combinations.
And how does one get the opposite direction? Well, I had already made a mistake in my direction ordering, as there are much better orderings that simplify this operation. But, even exchanging 1 with 2 and 3 with 4 is easy - just subtract one, flip the last bit using xor with 1, and add one back. Unfortunately, I didn’t have much time to spare, so I just made a table of tails and called it a day.
The fruit scare
Scaring off the snake seems to be easy - just check for apples around his head and decide new direction. But hold on. You have to account for current snake direction, as same apple arrangement can have different outcomes, see image:
This complicates things. But I am lazy. And I like tables. So, how can I employ table in this situation? The snake head has four neighbors. Each neighbor has two states - with apple or without apple. This is one bit of information per neighbor, leading to four bits, or 16 numbers (0 to 15). Together with 4 directions, I am looking at a table of 64 responses, possibly organized as a 2D array.
How does one code it in PICO-8’s dialect of Lua? Here I applaud the PICO-8’s design team, as they managed to add bit operators to their Lua, together with greatly designed tonum function that converts boolean value to 0/1. Therefore, encoding consists of checking for apples in neighboring tiles, converting these booleans to 0/1s, bit-shifting them to correct places and or’ing them together to make a number (or just multiply by powers of two and add together). To use it in Lua as a table index, one is added. The second dimension of the table is snake’s current direction.
Again, when filling in the table, comments are essential!
When releasing the game as a jam entry, instead of zeroes, I had valid directions there and used collision with itself for a failure - I directed snake backwards. But, when the snake was one tile long in the start of his run, bouncing him in opposite direction led to game crash. So this had to be changed later to fix the issue. Zeroes were put in table to signalize collision, and a check for it was added.
For v1.1, this table will be rethought and shortened, because there is one more trick to use. I know the snake direction. If I reorder directions clockwise or a counterclockwise, I could get +1 and -1 operations for direction for free and make use of it. So, in the prepared new version, the table is much shorter - 8 records, indexed by a three bit number - bit 0 = apple before snake, bit 1 = apple in +1 direction, bit 2 = apple in -1 direction. The response is +1 (turn +1), 0 (keep direction), -1 (turn -1). If there is crash into apple, the response is nil - in Lua, it is different value than zero and easily checked for.
Conclusion
I must pinpoint one thing: Be smart. Don’t spam your code with bazillions of if’s without further thought. Think about numbering your variants and making use of tables. Many times, tables are much easier to fill, check and debug than a chunk of condition spaghetti. And they usually run faster.
Next week I am going to write about carnival masks and a cutscene system.
Get Snakyval
Snakyval
These are no ordinary fruits.
Status | Released |
Author | HonzaMatousek |
Genre | Puzzle |
Tags | PICO-8 |
Languages | English |
More posts
- Packing more fun64 days ago
- The tale of masks71 days ago
- Post jam reflection84 days ago
- Post jam update and bugfixes91 days ago
Leave a comment
Log in with itch.io to leave a comment.