The Making of Shadow Forest
Introduction / TL;DR
In this article I am going to describe in detail how I developed the game “Shadow Forest” during the 2023 Minetest Game Jam. This includes development up to version 1.0.1. Later updates are ignored.
This article is for developers and nerds who want to understand my design reasons for Shadow Forest. If you’re interested in the nitty-gritty details, read on. If not, then you can skip this. 😛
Developing Shadow Forest
This year’s game jam theme was “Unexpected” and I really disliked it. At the end, I mostly ignored the theme. The one unexpected thing in the game is just a minor plot twist near the end of the game. I simply did not have a great idea to fit the theme.
I used most of the first 2 days for thinking and making notes and sketches.
The initial idea is similar to the final game, but there are important differences:
First of all, the game was meant to be in the adventure/mystery or horror genre, actually. And this was the initial idea:
You’re a random person with no memory. You’re lost in a mysterious forest. You know that danger is lurking somewhere so you have to solve a couple of quests to escape it. Dark shadows are coming to get you if you’re not careful …
The Shadow Forest was originally meant to be a single large open world map with many interconnected areas with the main forest in the center. The main forest would connect the following areas, some of which would house an artifact:
- Dead forest
- Young trees
- Tree of Life (but as a regular area, a not level hub)
- Abandoned village
- And others
The list of ideas was not set in stone since it was still very rough.
Some of the areas would be locked off by a large barrier of shadow bushes (basically just a dark thorny bush) and you have to collect artifacts to break it. In the final game there is only one shadow bush barrier because of time constraints. The dead forest would is the direct path to the final reveal (which would where the “Unexpected” theme would come in): You would go on to see a giant, and I mean giant excavation for a coal mine/plant that has destroyed a huge portion of the landscape. And this was supposedly is the source of all the evil shadow creatures. The End.
My idea was this reveal would come pretty surprising/unexpected after you’ve walked the whole game through a seemingly endless forest. This idea kinda made it in the final game, although much less bombastic. There is no excavation, and it’s nowhere near as giant as I imagined. Turns out, building anything giant that doesn’t look repetitive is hard and time-consuming.
Each major area would also have a campfire which would not only serve as an upgrade station but also for fast travel between the areas (teleportation).
Also, the forest would be almost completely dead (no leaves).
Picking a genre
Originally, I planned this game to be a horror/mystery game.
But this idea didn’t last very long. On day 5 I made an important decision to switch the genre. Rather than being some random unknown person who is lost in a forest, rather than making a horror/adventure/mystery game, I decided to go the fantasy route instead. Because of the lack of photorealistic textures, I had to accept it will not become a good horror game. That’s when I decided the player character is actually a wizard. The Wizard of Light.
The following sections detail how I’ve developed the important features I made. This is not in chronological order. In reality, I often didn’t develop one feature in one go, but went pretty much all over the place.
I organized the text that way to make it easier to follow.
Not much code was written on the first day because I needed the time for planning.
On days 1-2, I made a lot of photos and sound recordings. Most of the sound recordings made it in the final game but the photos remain unused. Initially I wanted to create photorealistic high-resolution textures for the entire game but when I fired up GIMP to edit them, I struggled to make them tile nicely and realized I lacked the skills to do it right so I quickly abandoned the idea of photorealistic textures.
I added a few first dummy blocks just for testing out some ideas and of course all of them only had dummy textures.
I already wrote the basics of the upgrade system on day 2.
On day 2, I have written the first world generator code but it was very bare-bones. At this point, it just loaded the world from a single schematic file which is very inefficient for large worlds.
The light orb
The game was initially meant to be very dark. In fact, so dark that you can’t see far. For that reason, I had the idea for the light orb, a floating glowing thing that follows you through the travels. I’ve spent a lot of time coding and polishing the movement.
Here’s how it works: First, the game calculates a position a few meters away from you and tried to fly there. If this position is air, it will replace the air block with an invisible glowing block that generates the light. This block will automatically turn into air again when the light orb moves away. So far, so simple. However, there are many cases where the light orb would end up in a solid block or where it couldn’t light up the area. In that case, it will check a different position that is higher or closer to you or directly over your head. I’ve written a long list of fallback cases it will check in order. If fallback 1 fails, it tries fallback 2, and so on. If everything fails, the light orb just moves directly towards you but this final case should rarely happen, basically when you get stuck in solid blocks (which would be a bug anyway).
The goal of this algorithm is to make sure the light orb always (well, almost) targets a reasonable position.
There is one edge case I haven’t covered: The first check looks if in x meters in front if you there is air (i.e. an empty block). This mostly works fine. But what if there is a wall between you and that air? Then the light orb would light up the area behind the wall which isn’t really useful. I basically worked around this small problem by designing most levels so that there are no such walls where it would be a problem (like in the caves).
I tried to find a solution to even that rare edge case anyway but I quickly decided it was not worth the effort.
I also added a simple special case: When you’re very far away from the light orb, it will teleport to you.
Finally, I wanted to avoid stutter and ensure the code is performant. So I used a couple of tricks:
- First, I’m not using any mob API at all. The light orb behavior was directly coded into the entity definition with no further abstractions. This made development much simpler.
- The position is normally checked at every server step, i.e. as often as possible. That’s a lot of calculations but necessary for smooth movement.
- I added a little bit of caching. If the light orb’s target hasn’t changed and the calculations were already made, it won’t recalculate but instead use the result it has stored earlier. I haven’t tested this a lot so I don’t how how large the impact of this is.
- The light orb doesn’t collide with anything. This makes everything so much simpler because with collisions, I would have covered so many additional edge cases; I would have basically needed to also add a full-blown pathfinding algorithm.
I’ve spent far more with polishing the light orb than with the shadow enemies.
At the end, I was pretty happy with how the light orb turned out. It is very performant, the movement looks very natural and it does not stutter/lag at all. Something which can’t be said for many mobs in Minetest
The shadow enemies (mobs)
The shadow enemies are the part of the game I am least satisfied with. From here on I will just call them “mobs”.
There are 3 mobs in the game:
The crawler slowly moves towards the player and attacks when close. It is very simple and has no pathfinding at all. Additionally, the crawler is also able to “crawl” up walls if it is in the way. This means the player can’t just avoid the mob by just reaching high ground.
The flyershooter (internal name) is a mob that floats through the air and shoots projectiles. It will randomly either stand still or move to random direction. It will start to shoot projectiles straight to the player at regular intervals. Like the crawler, this mob is not very smart and the movement code is extremely simple.
This behavior was actually only meant as a dummy behavior that I wanted to improve later on but unfortunately, I wasn’t able to improve on that much further.
The 3rd mob is the shadow orb, the final boss. It’s shaped like the light orb, but larger and in black.
Developing the boss went smoother than I thought and I basically just copied and re-used a lot of code I wrote for the flyershooter before. The only unique thing about the boss is that it occasionally spawns flyershooter. It’s a very simple boss, but it works.
Like for the light orb, the shadow enemies don’t use any mob API.
I’ve spent an ungodly amount of time with creating many tools the player never sees but which I absolutely needed to quickly build the world. Like with Glitch from 2022, I added a hidden “Editor Mode” in which I could edit the world more easily.
Most of the tools I added are for manipulating larger chunks of the terrain at once. Here’s a list of tools I added:
- Block Breaker: Remove blocks
- Turbo Block Breaker: Remove blocks super-fast
- Mass Block Breaker: Remove a spherical area of the map
- Leveled Node Remover: Removes “leveled” blocks (will be explained later)
- Leveled Node Filler: Turns “leveled” blocks into their “full” version
- Leaves Protruder: Adds nice-looking “extensions” around leaves
- Mass Block Spawner: Create many blocks within a spherical area
- Reverber: Turns blocks into a variant with a “reverb” sound. I only used this for Fog Chasm
- State Changer: Originally meant to change the “state” of all blocks but ended up only for rotating blocks and changing the level of leveled blocks
- Terrain Height Changer: Increase or decrease the height of the terrain within a radius
Many of these tools can be adjusted to cover a large or small area. These tools have saved me so much time with building the levels and trees because they allowed me to change large parts of the terrain quickly. Apart from my own tools, I also heavily used WorldEdit, a very popular mod for manipulating many blocks, but WorldEdit was not enough for me.
Additionally, I also added a world export function which I called manually when needed. This function exports the world into so-called schematic files.
The screenshot show the state of the development/testing world on day 7. Here I tested all the various blocks, items and more. It is not included in the game.
Very early on (day 1, in fact), I decided it would be cool if the terrain isn’t blocky like in most Minetest games, but instead each block differs in its height in a much more fine-grained way. The result of this is that each block has a version of it which can be modified in height. So called “leveled” blocks. Think of it like a slab, but with multiple possible heights. I ended up with a total of 16 possible heights plus including the “full height” block.
Of course, this means world building would be more challenging as well.
At day 7, I implemented the first version of the terrain generator. Minetest has a built-in terrain generator but it was useless for my purposes because of the leveled blocks but also I also needed to place a pre-built world. The screenshot shows one of the very first experiments.
The simple terrain shown in the screenshot is based on a 2D Perlin noise. This is also used by the “v6” map generator that is built-in in Minetest. 2D Perlin noise can be used to generate terrain at which every horizontal coordinate has a certain height, but it cannot generate “hanging” terrain.
The special thing about the Shadow Forest terrain generator is that it also supports the height for leveled blocks. The Minetest built-in map generators only generate “full-sized” blocks.
In this game, there are no real levels, but instead it’s one continuous large world. The various areas the portals lead to are technically just different areas of the same large map.
Originally, I planned to make an open world game but actually building a large open world turned out to be harder than I thought so quite late during the game jam I decided to add a portal system instead, including the Tree of Life.
So technically, the world is still a single large world, but to the player it seems like it’s sectioned off in levels. This wasn’t intended, the fact it’s a large single world is basically just an artifact from me being forced to switch to a portal system due to time constraints. If I would have planned to use a traditional level system to begin with, I would have saved a lot of time, probably.
The very first version of the world generator was very primitive: I just stored the entire world in a single large schematic file, i.e. a file to store a portion of the world. And the game would load this file to generate the world on its first startup. However, as the world became larger, this solution became very inefficient, with long loading times.
So I decided to completely rewrite world generation and split the world into multiple schematics: Each schematic file covers an area of 16×16×16 blocks. When the player starts a new world, the game will then place these schematics at the appropriate world coordinates. It’s more efficient but needs a lot of disk space. In the game jam version, there are over 5000 such schematic files.
A small trick I used to simplify making the world is that I didn’t build the finished trees directly in the world, but I placed “tree generator nodes” on the floor. The game will turn these into full trees. They kinda work like saplings in Minetest Game.
The screenshot shows an early very small test world together with the tree generator nodes (in gray) and world border (see below).
Not only the world itself is generated. There is also an area outside the main world area.
This is how the world is built: In the center, there is the world which has been built using Editor Mode, mostly. This includes all regions of the game. The diagram shows the entire world from a top-down view, with X and Z as the horizontal and vertical axis, respectively.
The main world in the center is simply loaded from the schematic files.
But the area around the center is generated. Meaning it’s not loaded from files, but generated using a 2D Perlin noise. The cool thing is the border between the actual world and the out-of-map area is completely seamless.
This works because I initially generated the terrain using a 2D Perlin noise when I started to build the world. For the out-of-map area, I use the exact same Perlin noise parameters. And since the custom-built world and the generated out-of-map area use the same parameters, the basic terrain shape on the dirt ground will match perfectly.
The out-of-map area is sectioned in 4 rectangular main areas: left of the world, right of the world, in front of the world, and behind the world. These sections extend all the way to the maximum legal coordinates for blocks in Minetest. These 4 sections are also invisible to the player but are required so the game doesn’t generate out-of-map terrain inside the main world, causing conflicts.
As a final extra, the world border is falling into darkness. The further you go from the main world area, the darker it becomes. You will take darkness damage if you walk away too far. However, this feature is basically unused in the final game because the player normally never reaches the world border.
I like the way how the terrain/world generator works, although I think some performance optimizations could have been made.
I’ve built a system in which I easily could build a large world and then export it into schematic files to load them into the game later. And as a bonus, it is able to seamlessly insert the custom-built world into generated terrain; the world does not float in open space.
While Shadow Forest didn’t really need this rather sophisticated system in the end (I could have just used a simple level-based system), this system could be quite useful for other projects in future, where a pre-built world is required.
Trees are an essential part of Shadow Forest, obviously. Here’s how I did it:
The small trees are mostly built by hand. But for the larger trees, I used a tool called “L-System Tree Utility”, something I wrote years ago. This tool allows to generate tree-like structures based on simple rules and recursion, using so-called L-systems. Minetest supports L-systems natively so my tool is actually just a graphical interface for that.
This has saved me a lot of time since with a little bit of experimentation, I could create many different and interesting-looking trees. The tool is actually very old but this is the first time I’ve actually used it productively.
Building the world
Actually coming up with good ideas for how the final world will actually look was pretty hard. The first step was easy: Pre-generate a simple 2D Perlin noise using one of the terrain generator functions. Then I added a bunch of random trees on top. As mentioned before, not the final trees, but only the special tree spawner blocks. I decided to use this trick because it made moving trees around in the Editor much easier: I only had to move a single block.
And the rest, well, was just building, building, building. I made heavy use of WorldEdit, of course.
The screenshot shows for the final version of the first level in Editor Mode.
Portals were added pretty late to the game; they were mostly a panic addition since I completely underestimated how much work it is to build an open world. So I decided instead of throwing my world away, I just connect the few sections I already did complete with portals. Originally, the portals weren’t there and the player was meant to reach every area just by walking.
The screenshot shows an early version of a “level hub” in which the player could walk into portals to reach the 4 levels. This was just a dummy. I’ve replaced it with the Tree of Life later on. I’m glad I did that; this dummy hub just looks just boring.
A hidden aspect in the game are zones. It’s simple but very useful.
Zones are basically large invisible cuboids defined by the coordinates of two of their corners. Zones are used to define the sections of the world (e.g. Fog Chasm) as well as sub-sections (e.g. a crystal shrine).
The game will constantly check in which zone or zones the player is currently located. The game also notices when a player leaves or enters a zone.
Entering or leaving a zone can trigger various events, like changing the music, sky or triggering a dialog. This is also the reason why I created a zone system in the first place.
However, this simple system was a little buggy so I needed to do some bugfixes after the game jam.
There are only two weapons in the game: The Staff of Light and Dagger of Light.
I didn’t have time for more. But frankly, I was lacking good ideas for more weapons as well.
The Staff of Light originally started as a generic “fireball” staff; it shot fireball projectiles.
The Dagger of Light originally was a generic “fighting stick”; a slow and weak weapon meant to punch the creatures. But I found this didn’t really fit the light vs shadow narrative so I used a faster dagger instead.
In practice, it turned out the Staff of Light would be the main weapon in the game which is also why it’s the only weapon with possible upgrades.
Well, this is interesting. I almost have zero experience with making music and also don’t play any instruments. But in Summer 2023, I have walked through a tutorial for LMMS, a music-authoring software and it was surprisingly simple. Then I just forgot about it.
But for this year’s game jam, I wanted to challenge myself to really do literally everything myself, including the music. In the previous year (for Glitch), I made everything except the music.
And for this game jam I actually ended up making 3 simple music tracks and I am still surprised I somehow managed to pull it off. How I did it? I don’t really know myself. I basically just activated the recording mode and mashed some keys on the keyboard (computer keyboard to be precise) and tried to get some melody that “sounded nice”. The second step was finding a good “instrument” (basically just a sample or one of LMMS’ many built-in synthesizers). And I just tried to tweak knobs, press buttons. At least I knew the basics on how LMMS works but I still don’t think I knew exactly what I was doing.
To save time, the 3 tracks are actually just the same melody, just the instrument is different. Some sections of a game have a different music. For example, when you approach a shrine, the music changes. I programmed Shadow Forest in such a way that the transition between each track is perfectly smooth and the music doesn’t just restart.
I needed to do a little hack to do that: The problem was, Minetest doesn’t tell me the time of the currently played track which was a problem. My workaround was to just play all 3 tracks all the time (looped) but at a different volume. The “active” track uses the full volume while the other two “inactive” tracks have their volume set to near-zero so the player doesn’t hear them. Whenever the game changes the track, the volume of the old active track is slowly reduced to near-zero while the new active track gets its volume increased to the maximum. So basically a very simple crossfade effect.
My personal favourite track is the one for the Fog Chasm, and it was also the first one I made. It gives a feel of mystery and wind, at least that’s what I tried to do.
Last day panic bugfixes
On day 21 (the final day), I made two important bugfixes I want to highlight:
The first bugfix I made was with the message system (when the light orb “talks” to you). I noticed on small resolution, the text doesn’t fit. So I added a simple workaround by making the font smaller if the window width is smaller than 1920 pixels. I tested it briefly and it worked. Job done, right? Well, no. After the ratings came in, I noticed this bugfix was broken itself. My test was too lazy, because I didn’t test the longer messages, too, and those broken. Basically the threshold of 1920 was still too low. Ouch!
The second bugfix was quite close to the deadline. I found a very serious bug I introduced in the final preparation before uploading the game. I made the very final gameplay test to make sure everything still works and I just walked through the levels until the first mobs attacked me. Then I suddenly noticed: I don’t take damage! Damage has been completely force-disabled, the player is immortal and enemies are worthless! Oops.
This was because of a mistake in
game.conf. The fix was trivial but it would have been a disaster if I had not found this very critical bug.
After the game jam, I did the usual bugfixes which aren’t very interesting but I want to highlight one specific thing: Mob spawning.
The problem with mob spawning was that simply way too many mobs spawn and killing them doesn’t help you to stop them. So the simple solution I did was to make the mob spawners remember when they last spawned a mob and then go to “sleep” for 5 minutes in which they can’t spawn anything. Only after the 5 minutes, a spawner can spawn another mob. This not only reduced the mob spawning greatly but also allowed the player to actually “clear” an area.
There were some other minor tweaks to mob spawning, like preventing a mob spawner to spawn a mob inside another mob or very close to the player.
- Day 1: Picked game idea for a horror game playing in a dark forest, first rough game design and sketches, first dummy nodes added
- Day 2: Picked game title “Shadow Forest”, made photos and sound recordings, world generator, player spawning, basic upgrade system
- Day 3: Applied sounds, puddle/snow/mud particles
- Day 4: Added crawler and flyershooter mob, some fighting aspects and particles
- Day 5: Important change of project goal: From horror game to fantasy game. Added ‘fireball’ weapon
- Day 6: New sound recordings, editor improvements, built schematics for trees, wizard tower (unused), crystal shrine
- Day 7: Terrain generator, many new blocks, improved block graphics, added graphics for dialog windows
- Day 8: Damage indicator, new test map, player physics, zone system, small tweaks, light crystal
- Day 9: New 3D models for player, crawler mob and shadow projectile. Added light orb
- Day 10: Message system. More mob balancing. Wasted half the day trying to get Blender to export animated models but failed
- Day 11: World exporting tool in editor mode and implemented basic world loading. Light orb improvement
- Day 12: Upgrade improvements, health HUD, sound effects,
sf_dialog, built the first actual level (cave, would be included in fog chasm later)
- Day 13: Map now generates fully outside the boundaries of the world border; mapgen optimization, damage when walking out of bounds
- Day 14: Built tree schematics and generate trees in mapgen
- Day 15: Started to built the actual large open world. First with the dead forest
- Day 16: Built the fog chasm level and pasted the cave from day 12 into it. Added shadow factor in dead forest level, added smoke emitter, light orb is even more responsive
- Day 17: Rewrote/optimized mapgen for the new large map. Placed random trees. First iteration of Snow Mountain level. Built a simple wooden hut and village (to be incorporated in the Forest level later)
- Day 18: Major direction change: Instead of building one large world, the game is now segmented into levels separated by portals. Defined the level zones. Sky updates now. Campfires now give upgrades. Falling in the fog chasm kills you now. Decorated Snow Mountain with snow and trees
- Day 19: More world improvements, mob spawning, river sound, composed 3 music tracks, added gravel sound
- Day 20: Added vases, healing essence, more deco blocks. Added Tree of Life (central hub), balanced the upgrades, bugfixes, dialog texts
- Day 21: Finalized the story: Shadow bush barrier can now be destroyed, added boss mob and boss area in Dead Forest level. Added Forest level. Lose shadow fragment on dead. More upgrade improvements. Added intro segment near the Tree of Life. Added final sounds. Final cleanup and bugfixes before submission
My personal judgement
What went well
What I am most happy about is the light orb. It’s very performant yet the movement looks very fluid and natural.
I am more or less happy with most of the levels although I’ve hoped for more. I personally really like the Tree of Life.
I also was satisfied with how the terrain generator turned because it can just effortlessly drop a pre-built world and surround it with an infinite generic terrain. Too bad at the end of the game jam, the out-of-map terrain is irrelevant.
I was very surprised and happy I was able to compose some music that doesn’t suck.
What went poorly
Definitely the thing I was most disappointed with were the mobs. And indeed: The most common complaint about the game were how the mobs were spawning is large, alarming numbers. The problem was I simply didn’t have time to balance them. They spawn too frequently and once you killed them, they will almost immediately respawn, leading to a seemingly endless stream of mobs.
In general, I would say I didn’t plan my time well. I’ve wasted a lot of time trying to get model animations to work because Minetest requires an ancient file format and my tools don’t support them natively. There is a way to make it work but it’s very convoluted. So I lost about 2 days and still didn’t have animated models. It would have been better to have just give up model animations much earlier.
What I completely underestimated was the size of the world. Making a large interesting open world is a lot of work so many of the original ideas I had for areas of the forest had to be scrapped.
Unfortunately, the work on the terrain generator went mostly to waste since I actually planned a large single open world game but due to time constraints I had to switch to a traditional approach with levels connected by portals.
Another problem was with the theme “Unexpected” because I just did not have any good game idea to begin with so I just basically forced the theme into the game with a mini plot twist. I think to respect the theme, I should have made an entirely different game, actually.
My overall judgement
Overall, I am semi-happy with how the game turned out. I’m definitely not as satisfied as I was with Glitch.
Basically, with Shadow Forest I took a huge risk because I tried to do things where I have little experience: Mobs and music. The music surprisingly turned out well, the mobs … not so well, unfortunately. Also, aiming for an open world game was another pointless risk.
As for the gameplay itself, it still feels somewhat “meh” even after I fixed the mob spawning after the jam. The fighting doesn’t feel that engaging. I think the biggest problem with the game to me is that the whole concept doesn’t really fit together as you just run through the world evading enemies and collecting things.
Maybe this concept could have worked out with better level design or more diverse mobs, but this stuff takes time.
But oh well, that’s the price for taking a risk. It’s hard to predict whether a concept will work out until you try it.
I have used the following tools for development:
- The GIMP (2D graphics, textures)
- Blender (3D models for the mobs)
- Blockbench (3D models for the mobs)
- LMMS (sounds and music)
- Audacity (sound editing)
- LS-10 (sound recording device (not a software))
- Mod: Schematic Editor (schematics)
- Mod: WorldEdit (makes world-building easier)
- Mod: Get Item (basically a creative inventory)
- Mod: Perlin Explorer (for finding a good 2D Perlin noise for the terrain)
- Mod: L-System Tree Utility (for creating many diverse trees quickly)
- Minetest itself (for level building and playtesting)
As of version 1.0.1 (the version I submitted to the game jam), these are the game stats:
- Size of all files: ca. 32.2 MiB
- Number of mods: 38
- Number of sound/music files: 62
- Number of texture files: 170
- Size of game world: 480×96×512 blocks
- Lines of code (including comments): 7078
- Number of Git commits: 321
- Development time frame: 21 days
- Number of developers: 1
I created a few features during development that have been discarded or are completely unused.
I coded a simple functional door which is basically the same as in many other games for Minetest. 2 blocks tall, and you open it with rightclick. It was mostly functional.
I also created a grass block but it looked really ugly.
At the boundaries of the world, the world becomes pitch black. When the player approaches the world border, the light orb will first warn you. When you enter the darkness, you will start to take darkness damage.
This was my method to prevent the player from running out of the map because I originally planned it as an open world game.
This feature is fully functional and there even is some dialog included. Unfortunately, it is also technically an unused feature since the player will practically never experience darkness damage because the levels themselves are already surrounded by tall walls that prevent the player from escaping.
I’ve built an incomplete wizard tower which looks very basic. Originally this was meant as the spawn location.
At the top of the tower, there would be a large destroyed light crystal to which you were supposed to bring the fragments of light back, or something.
I completely discarded this.
On days 6 and 14, I’ve designed a lot of trees but some of them were too ugly. A couple of trees didn’t make it into the final game.
I added a large mob that would just sit on the ground, have lots of health and spawn crawlers in regular intervals. The idea was the player had to kill the crawlerspawner to prevent the creation of more crawlers, but it’ll take lots of hits. I didn’t really have a good idea how to use it in the game, tho.