This game is crazier than you think (I swear)





If I don't make an extensive technical breakdown of this game I'm going to explode. There's so much weird stuff in it that you might not have noticed. There's so many tips for fellow Godot users. There's so many mistakes I want to atone for. This would usually go in a jam postmortem, but I don't want to wait until then. I may edit this to respond to feedback once more of it comes in.
You've played Rain World, right?
Grain World is primarily for people who've played the hit game Rain World for the Microsoft Windows PC and the Sony Playing Station the Fourth. Later released for the Nintendo Bottom. If you haven't played Rain World. That's fine. And I'm ok with you as a person. You probably have better things to do. But the subtleties of that game are best experienced first hand, and I'll have to explain them up front to break down the ones I tried to emulate here. So if you've heard of it and it sounds good please check it out instead of reading this. Especially if you thought Grain World was any fun.
Game Design Goals
Rain World is awesome. Rain World is good because it beats up and takes your lunch money over and over until you realize you can do one trick that doctors don't want you to know and take the lunch money back and eat it for breakfast. Man, I could go for some cereal right now. Anyways. That loop is on repeat for the entire game and it's so satisfying every time. If I could just make an experience that puts the player through that once..
Emulating that requires making mechanics, and making opportunities to use the mechanics, but then not explaining them to the player. This is a controversial game design technique that can be pretty frustrating to play against, and Rain World struggles with this. But, as long as I'm running into the same problems, I think have designed my game in the right direction.
Really, emulating Rain World directly was probably a mistake. Rain World took 6 years to make by some heavy technical talent. I'm one guy with 4 days and no official education in game development. I had to boil the game down to it's most simple mechanics. Here was the original goal:
Basic movement.
- Basic left/right movement
- A low jump that can crawl up onto ledges and has generous coyote time
- Poles, which I always referred to as "ropes"... for some reason. Poles can be grabbed by pressing up, then traversed with the arrow keys.
- Tunnels, which can be used to move between screens*. Tunnels can be entered and exited, and are traversed in the same way as
Survival
- Karma system. Eat food and rest in shelter to survive and raise karma. Die or run out of time to lose karma. Recontextualizing karma as your "cereal number" was a last-minute change.
- Food sources include fruit and dead lizards.
Entities
- Items can be picked up and thrown. Spears can stick into walls and become poles. Fruit can be knocked off branches. At this point in development, I wasn't considering that the spawning behavior for items is actually quite complex. I will address that later.
- I planned on having lizards, but ran out of time. Lizards would be able to eat you can cause a fail state, but also be killed and eaten for a full food bar
I would start with implementing the basic movement. I would make sure it's fun to move around, then give targets to move to. The idea was that the time limit, which would be shorter than Rain World's clock to work with a smaller level, would pressure the player into using their movement options to acquire food intelligently.
Seeing the list in person is intimidating even now. I was playing the process very loose and didn't write down a lot during development. That's also part of why I'm so eager to write this down. However, I was about to get completely humbled by a corner of the Godot game engine I was not previously familiar with.
Wait, why cereal?
In high school, I ate cereal pretty much every day for 4 years. I've eaten barely any cereal since then. I guess I got over it. But I think about cereal a lot. It is interesting to me how much of a mainstay of United States culture cereal is. It is even comedic to me to think of a version of a video gamee where everything is cereal and you are cereal and the game is called GrainWorld because that's a good pun.
I think "themed" video games- Games that really commit to the bit -are a rare sight. They are often good. If they commit hard enough, they can loop through all the stages of cringe multiple times. Think Franken. Think Anton Blast. Think Brazillian Drug Dealer Simulator 3: I Opened A Portal To Hell In The Favela Trying To Revive Mit Aia I NEED TO CLOSE IT, which I recently finished. Actually, I think I may have had the idea for this game some way through playing that game. Anyway:
TileMapLayer is so fu
cking dumb but also awesome but I hate it and I'm never doing a physics-based game using it again because it caused me so many problems. The best part is, if I wasn't trying to replicate Rain World directly, I wouldn't have needed it at all.
TileMapLayer is a type in Godot that provides an interface for creating tile-based maps, much like the funny sewer rat game. The tools allow for the creation of levels from a TileSet. TileSets contain multiple layers of data for each tile:
- Autotile. Using some method allows the user to create brushes, which place tiles in a grid based on simple rules to look good. For example, if there is a "floor" tile, you would want a "wall" tile to go beneath it, and a "ceiling" tile to maybe go beneath that. Autotile automates this behavior.
- Physics layers. Tiles can be given collision shapes on different layers. Then, you can detect collisions with the tilemap. When a collision is detected, the colliding object is registered as the TileMapLayer object itself. As a result, getting the actual tile you collided with is less straightforward.
There are other more subtle ones, too. But these are the most relevant.
Terrain Layers
I started the project by trying to learn autotile. That way I would have a test map. The editor was hard to learn. It feels very different from the rest of Godot's editor and it's never easy to try to learn things in a hurry. I also spend way too long drawing the extremely cursed cereal tilemap.
The autotile system lets the user define "terrains". Each terrain can assign up to 9 "terrain flags" to any tile by clicking on a 3x3 grid the editor overlays on top of the tile when using the editor. One for each corner, one for each side, and one for the center. Most tutorials I've seen on this emphasize the binary arithmetic side of this, but I think that's a distraction from what they do. Clicking a side tells the brush to only place this tile when there's another tile on that side, with the opposite side flag checked. Same for the corner flags. Basically, the brush is trying to connect as many of these side and corner flags together.
The intuition is that you draw terrain flags to cover the part of the tile map that is "solid". Here is the cereal tilemap for example:
There are three primary terrain layers. "Grain" in beige. "Tunnels" in green. And "Metal" in pink:
Ok, this is a lot of information, so lets break it down.
The central circle is the "convex" terrain. Using it, I can draw rectangles of any size with an edge made up of those edge tiles. The smaller island is the "super convex" terrain, which can be used to draw thin horizontal or vertical structures. Note that tiles with adjacency rules do not care which tiles they are adjacent to. While the layout of the spritesheet implies which tiles might often appear near each other, the only reason a 2-tile tall horizontal platform uses the same floor and ceiling tiles as the 3 tile tall ground section of the sprite sheet is because the floor tile has the correct rules to be adjacent to a ceiling below it. So, what's the point of the super convex structure? Basically nothing! I didn't understand the subtlety I just explained here. Removing the super convex portion of the structure does slightly effect the appearance of smaller platforms, but they work perfectly fine with that part of the terrain flags removed.
A good sanity check when making terrain layers is that you only typically need ONE tile for each unique flag shape, unless you intentionally want multiple tiles to have a probability of appearing in the same scenario. You can see I use this technique to create the random grafitti effect on the "Metal" layer, which makes up most of the backgrounds in the game.
The other mistake is the bit of cereal in the top-center that belongs to the "Metal" layer. My goal was something like the tunnels. The tunnels combine two layers to create a separate tunnel brush that seamlessly connects tunnels to the base Grain layer, allowing me to draw lines through existing terrain. If you look closely in-game, you will notice metal parts of the map do not benefit from these seamless connections. What I should have done is created tiles for all 8 directions in which Metal could connect with Grain, then combine pink and beige flags for each of those tiles. I also could have made a separate TileMapLayer to hold all the metal tiles, and made it draw behind the a dedicated Grain layer. That way the transparency on the Grain tiles would have handled the transition automatically.
Physics
TileSets can store multiple physics layers. Each physics layer can assign a collision shape to each tile, allowing the TileMapLayer to interact with the physics engine. The visual for this is less compelling, but also easier to explain.
I used two physics layers for my TileSet. One for the terrain, on which the player could walk. And another for the "ropes", on which the player could climb. I decided, for the sake of simplification, that both poles and tunnels count as "ropes," since they could be traversed in similar ways.
The terrain worked well enough. It's not supposed to move, and the player is supposed to stop moving when they touch it. It was pretty tedious to draw in the shapes for all the tiles by hand! Seriously, any TileMapLayer experts out there? I am willing to pay money to have all this done for me next time..
I also got the mechanic for "crawling" onto ledges for free from this. Ledges are 45 degree slopes, and the player is configured to be able to walk up slopes of up to 50 degrees. So even though ledges look like walls, you can climb them easily because they are actually floors. It even looks convincingly like the character is putting effort into crawling up, since the aggressive slope cancels out a lot of you horizontal momentum.
However, to detect if the player was touching a rope, I had to detect what tile in the tilemap they were touching. Rather than simply detecting if the player was touching or not touching a rope, I needed to know if the rope was horizontal, vertical, or a cross, and I needed to allow/restrict player movement accordingly. Recall that, when detecting collisions with a tile map layer, Godot only gives you the actual tile map object, not the tile you collided with. This makes sense from a programming perspective, since the tile isn't really an object in of itself. The TileMapLayer is just a mapping between positions and TileData objects, which are owned by the TileSet. So, if the physics engine returned TileData when the player collides with a TileMapLayer, you could actually lose information about where the collision was happening.
So, how do you get the TileData?
Umm. Uh. Good question.
GrainWorld doesn't so much solve this problem as it does put a lot of band-aids on it. Since the player doesn't collide with ropes directly, but rather detects them, the player's position is used to query the TileMapLayer for TileData, which does have a roundabout solution. The resulting code is very ugly. Here is some of it for your ugly code viewing pleasure.
Sorry, wrong picture.
Putrid. Detecting the colliding tile should be a single line of code or method I think. This code cost me a lot of time to make and revise, and this is only a taste of the problems TileMapLayer would cause for me later.
Note that, to detect the rope type: Horizontal, Vertical or Cross, I'm using a custom data layer. Custom data layers allow you to assign any data you want to tile, just like you would with physics or terrain. This led to a cool hack. In Rain World, walking across an upward-facing tunnel entrance does not put you in the tunnel. You just gracefully walk across it. In GrainWorld, upward-facing tunnel entrances were first marked, sensibly, as "Vertical" ropes. But this meant you would fall into them, and then have to climb out. Climbing off the top of a vertical rope is generally a pretty janky interaction, so it was less than ideal. But, if I simply changed the upward-facing tunnel entrance's rope type to "Cross", you could """walk""" across it. It looked just like walking, except for the fact that you were momentarily "in" a tunnel, climbing horizontally across the vertical entrance. You can still get stuck sometimes, but I think it's a pretty elegant fix in the grand scheme of this terrible codebase.
Rooms
Here's a lighter, more successful story to tide you over before worse things to come. Rain World has some formal notion of a "Room". When the player is in a room, all creatures and their dangly bits are simulated in all their glory. While creatures in "unloaded" rooms are still simulated, their "visual" effects, like vulture feathers and pole-plant leaves, are not simulated. This is a performance feature that I simply did not need. While I was planning on adding lizards at some point, they weren't going to look as complicated as Rain World's, and their behavior wouldn't be too complex either. Basically, I didn't need to do different rooms.
I also originally wanted to have two levels. The first one, cereal graveyard, and a second, industrial farm level, where you meet a farmer-iterator who kills you by picking you up and putting you in his cereal. But, it quickly became clear that this would be a stretch goal at best. So I didn't program any room, or level loading into the game. The karma door was actually one of the last things I added!
This came with the additional advantage that I didn't need to implement loading-tunnels. In Rain World, loading tunnels, once entered, are climbed through automatically and much faster than regular tunnels. They are a crucial mechanic for avoiding enemies in many scenarios. But regular tunnels can also be used for stealth and avoidance. And they are also how Rain World formally separated rooms and created loading zones. Since I wasn't doing any loading, I didn't need to implement this second tunnel type at all!
The only thing I really needed to nail the feel of rain world is the static camera bounds. To handle this, I spread a set of Area2Ds across the map, with rectangular shapes. Whenever the player entered an area, I projected the rectangle's shape through the position and scale of the Area2D, and set those to the bounds of the camera. Godot has a built-in feature for setting camera bounds, so this was all super easy to get away with. Here's the entire script for all of those areas!
Of course, if you use this yourself, make sure the player is in the group "Player", or use another method of detecting that the body entering the area is in fact the player. Of everything in this game, the camera boundary script and the base player movement are probably the most reusable if I want to do something like this again.
Items
With movement mostly out of the way, it was time to tackle items. Items in that game about cats in an accurate London simulator.. what was it called again? Right, Rain World; items in Rain World are physics objects. Rigid Bodies to be precise. And that's where shit really hit the fan.
A rigid body is an object in a physics simulation with a defined shape that cannot change. That is opposed to a "soft" body which can "deform" in response to impacts. Rigid bodies, or just RigidBody if you have brain worms like me, are the go-to objects for breathing life into your grotesque interactive entertainment works commonly known as video games. The rocks, fruit, and spears (we'll get to the spears), are all RigidBodies.
When the player picks up an item, the item should move with them. To pin a RigidBody to another body, a "Joint", or pairing between two physics bodies, can be established. I changed to the Jolt physics engine, an addon for Godot, about a third of the way through development, for it's higher fidelity joint simulation.
However, even with Jolt physics, the joints on held objects kept breaking. Particularly when entering tunnels. The code for attaching and de-attaching joints was in a constant flux. It was hard to read, hard to edit, hard to understand even though I was writing it myself. There had to be a better way. Eventually, I conceded and just froze any physics objects held by the player, and teleported the item to your hand.
I was already making the concession of only having one hand slot. I underestimated how important it is in Rain World that you have two hands. With two hands, you can preserve two items in a shelter. Plus the stomach pouch, you can get really creative with how you prepare for the next cycle! Cycle management ended up being one of the densest parts of GrainWorld's mechanics, which I will address later, so I was kind of annoyed I didn't have the time to go for two-handed item management at least.
I was also assuming at the time that, in Rain World, objects in your hand were not paused or removed from physics simulation. After all, if held objects were removed from the physics simulation, you could just pick up a vulture and carry them through a tunnel! But you can't do that. Why?
It seems the Rain World developers pulled a very smart magic trick. You feel like held objects are still simulated because they weigh you down, but this effect is likely artificial. And, when grabbing larger objects like dead creatures, only then would a proper pin joint be established between the creature and your hand. This little switch feels seamless. The hands feel the same regardless of whether you're carrying a spear or a lizard, the effect on your movement is at least organic-looking. But it actually isn't. I'm so mad I didn't see this earlier!
PinJoints do make it into the game in the form of the fruit branches. But this also causes an issue where you can sort of newton's cradle one fruit into another and knock it off its branch. In Rain World, rocks can knock fruit off branches, and spears can impale them, but I didn't get that far at all. Fruit knocking eachother off branches happens in GrainWorld because knocking a fruit off of a branch is performed by detecting an item colliding with the fruit while moving suitably fast, and swinging a fruit can easily reach this velocity.
I would go into more detail about how much time PinJoints wasted, but it's truly boring. Lots of confused changes to code without understanding why I'm making them or what exactly my goal is, a lot of wasted time that I don't wish to pass on. The hand code continues to be probably the worst code in the whole project. Probably.
Speaking of bad code..
Spooears
You heard that. The internal name for GrainWorld's spears, which are also spoons, is Spooear. Take that, liberals. When a spooear hits the terrain shortly after being thrown, it should stick to it. Sticking to the terrain involves identifying if the tile in front of the spooear is a tile that can be stuck to. And we all know where this is going.
Detecting the rope tile underneath a character moving at 200 pixels per second is one thing, but detecting the tile in front of a projectile moving at 1000 pixels per second is another beast entirely. I was also running out of steam at this point in the jam, so my solutions to things were getting pretty unhinged.
The real problem is that the way I imagine this interaction in my head is totally different from how I was implementing it. Ok, so you need a position to query the TileMapLayer for TileData. Fine, I can just use the actual position of the collision, right? RigidBodies in Godot have a feature called "Contact Monitoring" that allows them to watch for collisions. But I've always found contact monitor to be so janky.. I must be using it wrong somehow. And without reliable contact monitoring, I don't have reliable access to the collision point, which means I don't have reliable access to the TileData of the tile a RigidBody is colliding with.
Instead, I hard-coded a list of positions, relative to the spooear, where the colliding tile could be, and searched through them until I either found a tile the spooear could stick into, or ran out of options. ON TOP OF THIS, there is a single check slightly below and behind the spooear. If a tile is found here, the spooear is considered "too close to the ground" since climbing the spear at such a low height would look weird, and therefore should not stick.
The craziest part of this all is that the approach works! Sort of. In the final game, sticking spooears is really inconsistent. To the point that fans of Rain World testing for it might assume I didn't implement the feature at all. The problem is that the area on the spooear I'm using to detect tiles is too small. The area only checks for tiles once per physics step, as opposed to 4 or 8 times like a proper RigidBody detecting collisions would. So unless it is oversized, the spooear will not stick. This is a simple thing, but since everything else about the spooears was so complicated, I would have never thought to check!
Stopping the spooears also ended up being a slightly complex matter. Again, due to the way the stick detection works, a spooear may collide with a wall and not stick to it. In this case, it often slides along the wall up or down until either the area kicks in or the spooear has been colliding enough to no longer be considered airborn, or until it gets too close to the ground and the aformentioned "below and behind" check kicks in. There really are just way too many variables involved in this system.
If I had to do it again, I would use a larger area and I would use the area's position to sample the TileMapLayer instead of offsets from the spooear itself. I would put fewer conditions on the spooear sticking (there were more that I didn't mention here. Again, boring stuff). Finally, I would add a system in the stick function to align the spooear to the tile where it's landing manually, undoing any offsets caused by the area detecting a collision too early.
Getting the spooears to be climbable was actually the easiest part by far. I just added a version of the rope type detector from earlier to the player script that checks for spooears instead of tilemaps. Then, when resolving which one to grab onto, the script just runs both functions and picks either the spooear or the tile based on which one is closer to the player.
Spawning, saving, and milk
So, what's stopping you from leaving food on the ground, going to shelter, and coming back up to eat it? You may think the level reloads or something. No. It does not. I mean, not always.
You see, as a student at William & Mary, the alma mater of the greatest mind in gaming, I knew that whatever I did, it should just work.
In Rain World, it would be bad if you could leave items, especially food, out while you sleep and have it still be there when you come back. Wouldn't the rain wash it away? Wouldn't some creature come eat it? Additionally, when you eat fruit in rain world, it often doesn't come back after one, or two cycles, and sometimes even more. The wiki doesn't describe this behavior in depth, so I can't fact-check that. But I remember how my survivor playthrough, especially earlier on, was dictated by this invisible fruit growth rate, and that stockpiling fruit in my shelter was a rare, satisfying act. How would I emulate these systems?
First, it was very important that the rain were milk. Obviously. There would still be rain. But instead of an iterator causing a downpour of deadly water, it would be a farmer pouring milk in his breakfast cereal. The milk would wash away any loose items, and provide.. nutrients? Yeah, nutrients. Milk nutrients, for froot loops around the map to grow back.
I characterized this by adding a "world profile" to the game's save data. The world profile saved a NodePath, a type of location, for each of the fruit branches in the scene. The fruit branches could spawn fruit, but the world profile also kept track of their "age". When bearing fruit, the branches had age 0. Once picked, the age would count up once every cycle. The age was then applied to a list to check for the probability that the fruit branch grows back. At age 0, the probability was 1.0. This was so all fruit would grow in automatically when you load the game for the first time. At age 1, the probability was 0.1 percent., then 0.5 at age 2, and finally 1.0 again at age 3. I think I tinkered with these probabilities at some point. I think I buffed them in testing because I don't like when jam games are too difficult.
Then, at the start of each cycle, I would try to grow fruit back on each of the branches based on this probability, and save in the world profile the ages of the all the branches. Then, whenever you launch the game, all branches of age 0 instantly grow back.
Eventually I factored out this growth behavior into its own "PickableProfile" resource class, making a separate table of probabilities for spooears. Spooears only spawn at cereal number 1 or higher (the default is 0), and at age 0, spooears only have a 0.2 percent chance of spawning. Since spooears ended up being so janky, this number was begrudgingly doubled from 0.1. Making them quite common. There are 20 spooear spawners total throughout the map and about 12 fruit branches.
Loose objects are also stored in the profile. "But, loose objects are washed away on each cycle, and since I've played Rain World I know the game only saves on cycles!" You might say. But, the loose object array is needed to keep track of objects stowed in your shelter, which are not washed away by the Milk.
This all sounds nice, but implementing it had a lot of hiccoughs. Just to name a few examples:
Initially, the spooear age probabilities were the same as the fruit respawn probabilities. These probabilities belonged to files in the project using my custom PickableProfile resource class. But, these resources were also loaded into the world profile, which was saved on cycle. This caused an incredibly weird side effect where, in testing, hitting a cycle would keep overwriting my modified spooear age probabilities with the default age probabilities, creating lots of confusing scenarios where all spooears "grow back" instantly, even if I deleted my save data.
Initially, age 0 was hard-coded to have a 100% spawn rate, which caused the same bug as the previous issue, but in a completely different way. Very frustrating.
The milk and cycle animations
So, a lot of things happen at the end of a cycle. Aside from technical things like saving the user's game, I believe there is a total of three animations that play. One for the foreground components: the particle effects, the white fade, and also, for some reason, the audio; and one for the background components, like the farmer. The third one is the animation player for the cereal number/karma screen, the black screen that shows your karma changing. Splitting the karma up from the rain kind of makes sense, but the foreground and background? This level of division made it very hard to line up the timing for everything. Again, I was getting pretty winded from working about 40 hours on this project in 3 days. So I wasn't really thinking straight.
Animations in Godot are controlled by AnimationPlayers- for simple, linear animations. For more complex, behavioral animations like character animations, you use AnimationTrees. For simple, yet dynamic effects, you can also declare "tweens" in code to smoothly change a variable from one value to another over time. In the milk system, the milk animations play. The foreground animation has method call tracks to call a function on the cycle system to start the cycle animation, and the cycle animation has method call tracks of its own to initiate a tween for dynamically moving the karma level to the right position.
Somewhere in here is a bug that causes the karma symbol in the bottom left of the ui not get updated until, like, 10 seconds later. I have no idea why that's happening.
As a general rule of thumb, this is some bullshit. I should have used an AnimationTree.
Ok, hear me out. I know AnimationTrees are typically for character animations, not user interfaces or environmental effects, but I could have just had an animation tree with one milk animation and one cycle animation. When the milk animation plays, it could transition into the cycle animation via the tree. Simple as that. This would have a few advantages of method call tracks.
Method call tracks are much harder to test in the editor that "regular" animations. Playing an animation, or animation tree, in the Godot editor plays back the animation mostly how it will appear in game. But getting a method call track set up.. It might be possible, but running gdscript in the editor has always been a jank affair. I wouldn't want to try to make that happen. The reason removing the method call tracks is so effect then, is that the whole animation can be viewed, with the same timing that it will have in game, from the editor. This would have made it much easier to fix the sort of awkward timing that the cycle animation ended up with, and would have saved a lot of time tweaking the transition from the milk animation to the cycle animation.
Design criticisms
This game could have had a lot more jokes. It's silly, but it's not silly enough. The audio, for example, is too much like Rain World. It's too serious. There should have been something stupid like a Casio orchestra hit when you stick a spooear. There also should have been a better reward for beating the game. I should have gone ahead and made the farmer-ator room, even though it wouldn't make sense to place it at the end of the cereal graveyard.
One of the categories of GMTK that I feel goes ignored a lot is narrative. One does not have to make a narrative game to make a game with a good narrative. A friend of mine submitted The Harrowed, which is an awesome, frantic survival game that manages to be driven by a solid story without just being a visual novel or an rpg. Again, sticking to the Rain World formula too much led the narrative of my game too open to interpretation. There's nothing wrong with some opening text.
I also feel like I'm taking this too seriously. I mean, did you see the tech breakdown? This is the kind of detailed analysis I should be giving to a piece of enterprise software, not fucking Grain World. I solemnly swear in the next game jam I will have more fun.
That's a good arc to have. This was already orders of magnitude more fun to make than my last game jam project. So I hope to keep up that pace.
Files
GrainWorld
What if rain world but cereal?
Status | Prototype |
Author | Roboticy3 |
Genre | Survival, Platformer, Puzzle, Simulation |
Tags | cereal, weird |
Languages | English |
Leave a comment
Log in with itch.io to leave a comment.