Having taken part in every single main Alakajam event until now, it sometimes feels like I'm getting stuck in a rut. So this time, I decided to shake it up a little by (a) using the latest Godot 4 release candidate, instead of the stable 3.5, and (b) going full 3D instead of my usual 2D style:
I had a slow start due to other obligations, which gave me some time to think about the game design. Most of that made it into the final game, but I had some ideas that didn't work as well as I'd thought.
Originally I had implemented tiles with forks (three exit points). The idea was that the river goes down a slope, so the boat would always pick the most downwards direction. If there were multiple options, it would choose randomly. This means the player would need to take both possibilities into account until the boat had made it to the fork. However, it was still possible to make a corner where the river is forced to go upwards, which then wouldn't make physical sense. Should the boat get stuck, or start moving against the flow? Neither would be a great option. Moreover, I found during playtesting that it was easy to always point the "unwanted" exit point upwards so that it always got ignored. We could also say that the river is on a flat plane, and always choosing a random fork regardless of up/down direction, but this made the game too unpredictable.
The other idea that didn't make it was multiple boats. This would have interacted nicely with forks and joins: you'd have to make sure that the boats didn't collide. You could send both down the same path, making it easier to manage, or maintain two parallel paths to increase coverage for collecting stars. I think this idea could have worked, but didn't implement it because time was up.
Godot 4 was a very smooth experience. My entire X server crashed once, but that's unlikely to be Godot's fault; otherwise everything was pretty stable. The editor feels snappier than Godot 3 and has grown a couple of new useful features. I had some trouble with the export, but it turned out it was my own fault. I was listing files in the levels/
directory to enumerate levels at runtime, but didn't realize that the file names change upon export. Easily fixed with some hardcoding.
Working in 3D was the challenge. Having worked with Blender years ago in the days before the huge 2.5 overhaul, I had to re-learn it on the spot. Fortunately I only needed basic mesh creation and manipulation facilities; everything else could be done in Godot, including textures and materials.
Several hours went into writing shaders. The water shader is the most apparent one. It takes two noise textures, A and B, and lerps between them, using a sawtooth mix factor that ping-pongs between 0 and 1 over time. When the factor is 0, texture B (which is entirely invisible at that moment) is translated to a new location, and vice versa. This avoids a repetitive effect.
The other shader, which is not nearly as apparent, is the one that draws the terrain. The albedo comes from another noise texture, which is put through a gradient map to get some colour variety. Another noise texture is used as a normal map. That same texture is also used as a height map to displace the vertices. You might think I just produced three models for the three different tile shapes (straight, obtuse corner, acute corner), but no: the river is also carved out using the shader.
The water on each tile is just a single plane mesh that intersects the terrain. Unfortunately, that means you can see its edges glitching through the side of the tile sometimes. The obvious quick fix of shrinking the water mesh slightly resulted in the river having seams, which looked worse, and I had no time to fix it properly. The price for working in 3D, I guess.
All texture sampling is done in world space coordinates so that tiles and water match up seamlessly. However, for the tile queue on the right and the "ghost" tile attached to the mouse cursor, local coordinates are used so that these tiles don't seem to change while they move. As a result, when you place a tile, it switches from local to global coordinates and does change shape a little, so I put some smoke particles on top, tweened the vertical coordinate, and added some camera shake, all to make it less noticeable.
Changing the colour scheme for each level was an addition half an hour before the deadline. It's a pretty quick way to add some variety. Because it was done in such a hacky way, the colour of the river on the main menu also changes according to which level you last played, which is actually a bug but I'm branding it as a feature now!
Going to use Godot 4, I see RC5 just dropped! Let's see how many bugs are still left :)
Fortunately River was one of the few themes where I had a more or less concrete idea. Let's see where it takes me.
Have fun y'all!
We have a good shortlist of themes again, and I'm looking forward to jamming! Too bad that I haven't had a good night's sleep for about two weeks now. Let's see how it goes.
Toolset is the usual: Godot, Krita/Aseprite/Inkscape depending on art style, Audacity/jfxr, LMMS.
And by "computer" I mean a thing with a physical keyboard, of course; smartphones and ebooks don't count. I know there are probably ways to make a game on a phone, but poking my eyes out with a fork sounds only marginally less pleasant than that.
Instead, I'm bringing a deck of cards and will design you a card game this time! I know it says "video game" in the rules, but a conversation in Discord made it clear that this is an oversight; it was never intended to exclude physical games.
I'll only use a regular deck of 52 playing cards and maybe some tokens, and probably aim for a single-player game, so the threshold for playing and rating shouldn't be too high.
It'll be interesting.
13 Alakajams and counting! Even though I'm pretty tired and have a baby to care for, I'm still going to poop out some kind of game this weekend.
I'm thinking of trying out Twine because it's super easy to get something started quickly, but I might change my mind and switch to Godot depending on what game design I come up with.
Edit: briefly tried both Twine and Inkle, and I like Inkle better, so I'm going with that. Or Godot. We'll see.
"But thomastc, how is the procedural world in your latest Alakajam entry generated?" asked no-one ever. But since you're here, I might as well tell you. Here's the end result:
The world size is 300×150 tiles; each pixel in this image represents one tile. I chose 300 because it gives some margins when displaying the map at the 320×200 resolution of the game. I chose the 2:1 ratio because it's how an equirectangular projection of a sphere (like the Earth) is usually displayed, with one degree of latitude being the same size on the map as one degree of longitude.
Water depth is shown on the maps as four shades of blue, but in-game you can't see depth; only one tile sprite is used for all water. This is because I had plans to make your ship run aground if you tried to enter shallows, so you had to use maps to avoid that. I'm glad I never got round to that, because it would probably have been too hard! But the different shades looked pretty on the maps, so I kept them.
The base of the world generation is, as you might have guessed, simplex noise, powered by Godot's OpenSimplexNoise class. We can configure, among others:
To make it wrap, we need to call get_seamless_image
to create a 300×300 image, then crop it to 300×150. The result is an image with monochrome pixel values between 0 and 255. Most of the values are around 128. We are going to interpret these values as a height map, larger values being higher.
If we simply used 128 as the threshold to decide between water and land, about half the map would be water and half would be land, and it almost certainly wouldn't be circumnavigable! So in the code I have a variable WATER_FRACTION
, which lets me tweak what portion of the map should be water. In the end I set it to 0.75, which is slightly more than the 71% of our own planet, but always resulted in at least one possible route around the world in my tests, so I didn't bother to actually code a check for this. This means there are probably seeds that are unwinnable!
To figure out the desired water level, the code first loops through all pixels and creates a histogram, counting how often each of the 256 values occurs:
[ 0] 0
...
[126] 1894
[127] 2645
[128] 3642
[129] 3528
[130] 2974
[131] 1490
...
[255] 0
Then it runs through this array, adding each value to an accumulator. When the accumulator exceeds the desired number of water pixels, which is 300×150×0.75 = 33750, we have found our water level. Let's say it's 130 for this example.
Now we need to create the poles, because the top and bottom of the map must not be traversable (this world is a cylinder, after all). To do this, a bias is added to each pixel, where the bias depends on the y
coordinate like this:
bias = 255 * pow((abs(2 * y / 149 - 1) - 1) * 10 + 1, 3)
if bias > 0:
pixel += bias
Formulas like this are a very powerful tool in procedural generation, but they're harder to read than they are to write (drawing graphs on paper helps!). Yet it's built from a few basic primitives, so let's break it down from the inside out:
y
is between 0 and 149, inclusive. So y / 149
is between 0 and 1, inclusive.abs
olute value to get it between 0 (equator) and 1 (either pole).Next, the colours are assigned. Level 130 has a height of 0 above water level, so we say that this is the beach (yellow). The three levels above this (131, 132, 133) become light green, the next three become middle green, and so on. A similar thing happens for negative heights, which are below water. As an exception, if the pixel is near the poles (y
close to 0 or to 149), it becomes ice (blueish white), but we add the height to y
to avoid a sharp horizontal line between ice and non-ice. The result is that inland ice occurs farther from the poles than coastal ice, which makes sense because the ocean has a warming effect. Ice is not just cosmetic but also serves a gameplay purpose: it lets the player know that they are getting close to the pole and will be blocked if they go much farther in that direction.
That's it for the map. Now let's place the ports. There are up to 100 of them. I searched the web for a list of seaport names and found an Excel sheet with over 100 names, which I cleaned up a bit and copied into my code. (In jams especially, I often don't bother opening files or parsing data, I just turn the data into a literal that can be pasted directly into a script. JSON in particular is valid code in several languages!)
For each of the 100 ports, first we generate a random x, y
coordinate pair as our starting point. If it's land, it might be landlocked: too bad, try again! If it's sea, we start a random walk. Each step, we move one pixel north, south, east or west. If it's now on land, it must be coast because it was previously water. We'd like to place our port here, but first we check if there's already another one within 4 tiles. If not, we place it, otherwise we give up and try again. If we haven't found land after 75 steps, we give up and try again from a different starting point, up to 100 times. As an exception to avoid lots of polar cities (which would be unrealistic and might also be game-breaking), we also abort when we get close to the poles.
Finally, the port's inventory is decided. One random cargo type is picked as supply (with 2-7 units) and two different ones are picked as demand (at twice the regular price). We need to make sure that these are all unique, because it makes no sense to both supply a good and demand it. Your first intuition might be to pick a random type until you've found one that's not used yet, but there's an easier way: simply create an array of all possible types, shuffle it, and pop items off of it. For large arrays and small numbers of samples, this is obviously rather inefficient, but for small arrays it's fine, and it makes the code a lot easier to write.
As to equipment, in 60% of cases, a map of random size is offered for sale; in 40% of cases, one of the three powerups is offered. (In the game, you might encounter ports without any equipment. This is because when you buy the Binoculars, any Binoculars in other ports are upgraded to Telescopes, and when you buy the Telescope, all remaining Telescopes are deleted.)
As the very last step, the game decides what your starting port is going to be. This is not simply a random port! I wanted to make the player start in the "easiest" part of the map, so I added some code that counts for each port how many ports are within 15 tiles distance from it. The port with the most such neighbours becomes the starting port, and the player's ship is placed in the middle of an adjacent sea tile.
And that's it! I hope you enjoyed the read; now go and play the game if you haven't already!
Very literal connections going on here. I think this may be my most on-theme entry so far.
This is just so you can avoid making the same game as I do ;)
https://www.reddit.com/r/thalassophobia/ I didn't even know that word until this started popping up on the front page.
Going solo this time. Got rough ideas for some themes, not for all.
Do I need all of this stuff? Who knows… but a little preparation won't hurt :)