I thought it might be fun to show the process of creating a new enemy. The process has evolved some throughout the course of development, and is reasonably simple.
However, we want to ship a large cast of unique enemy types, and we will need to further simplify this workflow to do so.
First, we decide we want to make an enemy with some specific behavior. In this case, let's say we want to make a "tektite" sort of guy: He jumps around the room to some subset of valid tiles, waits for one of several timer lengths before jumping again, and he's hit by most weapons the player has.
Once the basic behavior exists, we can tweak it to make it more interesting, maybe.
Based on the behavior, we'll then go sketch some ideas for how the enemy could look, like this:
I didn't have a ton of great ideas for this guy, but this is enough to get going.
Next, I open up Aseprite and try to make some art based on one of the ideas. In this case, I picked the weird bacteriophage crab thing.
Sometimes, this goes poorly and I spend a couple hours arguing with myself, trying and failing to depict what I want! In those situations, I try to make myself just do some temp sprites, and use them until the art block goes away.
On this occasion, the art attempt turned out ok, so I'm moving forward with it. The next step is to export a sprite sheet and animation data. These are the settings I typically use:
The files will live somewhere like this:
wizwag/assets/enemies/hopper/hopper.png
So far, we've spent about 40 minutes on this guy, inflated somewhat by trying to use Patreon's article editor...
At this point, we're ready to start creating our new Actor type. I am unhappy with how this process currently works. Nevertheless: The next step is to copy an existing enemy's source file, rename it, rename the type, and comment out, or remove, most of the code therein. :')
After this, I am left with a file with ~140 lines of code in it. Sheesh! To give some context, here's our new enemy's tick function:
(This code is fairly concise and easy to read, BUT it's largely redundant with other enemies, and other Actors in general. Maintaining it is annoying. I don't actually even want to use inheritance in Nim, or some of the other language features I use, but I've somewhat "ended up here" at the end of a long procession of concessions.)
Next, we need to do a special step to enable the map builder to spawn this new enemy type: There is a file called
REGISTER.nim
which imports every type of Actor in the game that needs to be spawned from data files.
Inside each Actor type's file, there is a call to a global function registerActorFactory
to give the actor a
named constructor for maps to use.
All right, now we have an Actor type, and it's registered. Currently, he has the same behavior as some existing enemy, but stripped down to whatever degree. This is fine, for now.
Next, we need to add the new enemy to our "spawns.tsx" tileset in Tiled. This is a special tileset where each tile comes from a separate image, and each one has the information needed to spawn a specific type of Actor:
Now that the Actor is in our tileset, we can spawn it by adding a Tile Object to a map. So, I quickly threw together a test room in an empty screen of my overworld, and dropped a spawnpoint and some Hoppers in:
At this point, I run the game, the world is rebuilt, and I warp to the room to test things. The game crashes, because I forgot to change animation names in the other enemy's code I copied. While I'm fixing that, I go to my tweaks file and set my default spawnpoint to "test_hopper" so I will go straight to this room on startup.
So I run the game again, and now my little crabby bois are wandering around aimlessly, as they should. I wack one with my cane (this kills the crab), and it dies, but it never despawns. Oops! I forgot to mark the death animation as "once" instead of looping. I fix that, then re-export from Aseprite. The problem persists.
Oh right, I say, I forgot! I need to go add a newline to the hopper.nim so that the animation data, which is built at compile time, will actually be rebuilt, because Nim does incremental builds, which never actually work in any language.
So, at this point, I have wandering crabs, I can kill them, and they deflect arrows:
All right! Now we can finally go program the enemy to behave how we want. Right now, he has a very simple brain:
All he does is wander around at fixed intervals. What a layabout! Let's fix him. So: What do we want him to do? We want him to:
Let's start by making the closest thing we can using existing parts:
With these changes, we now have killer crabs that clumsily jump after the player:
First, let's change their properties a bit to make the jumps nicer: faster horizontal speed and zero bounciness.
Whew! That's better. Now, the real work: We need to make a new BrainState that will jump to a random legal tile within some radius, and will ignore tile collisions on the way there. Well, that's one way to do it, anyhow. Another way would be to have the critter jump in a random direction, and reflect off the collision normal when it hits something. Let's try the latter first:
Honestly, in combat this feels... Pretty ok? Sometimes he pursues you, sometimes he doesn't. This enemy brings some chaos to the battlefield, and will be fun when paired with other enemies you want to remain stationary (or use stationary attacks) for.
From the snippets shown, you can imagine it's relatively easy to say, make variants of this guy: A different colored one with more health, who aggros if he gets close or you hit him once, and then he starts pursuing you relentlessly in fast, low hops. Or a version with a stone helmet who can deflect everything except explosion damage. Or a boss one of these. Et cetera!
It has taken about ~3 hours to make this enemy, art and article-writing included. That still feels way too long to me, for how simple it is. I've done a lot to speed up my workflow when adding "content" (I hate this word) to the game, but it still feels like molasses in places. This is why I can't use anybody else's game engine unless I'm being bribed lmao. Half the engines I try can't even live edit maps and artwork! Shudder.
Part of the problem is that I've accumulated a small handful of partial solutions to the problem of codepath proliferation (thanks to Ryan Fleury for this phrase, and many good tips). I have some bitflags, for certain features. I have flat composition for others. Reference-based composition for still more. Inheritance in several places (not my first pick, by a long shot). I used to have some tagged unions too, but they were so much slower than C unions, even with checks turned off, that I removed them all.
Over the past week I've done a lot of thinking about what it would take to unify all my half-baked strategies of expedience for managing complexity into a whole, a whole with sane defaults, minimal redundant systems reaching over each other, and fewer, straighter, flatter codepaths for me to keep updated as I work on the game for the next ~year.
I have some solid ideas there, and have been doing experiments to prove them out, so we'll see what happens!
What do you guys think? Does this workflow seem reasonable to you, or awful?