Between the Boughs by Evergreen Games

thoughts on lightweight enemy programming

lately I've been exploring the gameboy library, and in the process became inspired to create some small projects in pico8. this experience has given me some thoughts on how to structure enemies programmatically, in a way that might be especially applicable to retro-style games.

the idea is to separate enemies into the following interchangeable elements:

  1. visual
  2. behavior
  3. parameters

basically this is a derivative of a MVC (Model-View-Control) model, tailored to the needs of a type of game where you would be interested in mixing and matching these 3 elements!

so to define an enemy, it might look like so:

enemy_dict = {
	{
		sprite="enemy1.png",
		behavior=BEHAVIOR_SPIN_AND_SHOOT,
		params={
			name = "Shlooper",
			maxhp = 10,
			spin_frequency = 5,
			shoot_frequency = 30
		}
	}
}

enemies = {
	{
		index = 0, 
		x = 0, 
		y = 0,
		hp = 10,
		dir = 0
	}
}

in this case the values are a file path, a constant and a dictionary of parameters. the exact form of data organization can vary depending on the needs of your project. worth noting is that these parameters can include both stats and parameters read by the behavior.

and then, while updating your enemies, it can look something like this:

for e in all(enemies) do 
	local edict = enemy_dict[e.index]
	if edict.behavior == BEHAVIOR_SPIN_AND_SHOOT then
		if frametime % edict.spin_frequency == 0 then
			e.dir += PI/2
		end
		if frametime % edict.shoot_frequency == 0 then
			add_bullet(...)
		end
	end
end

or (this kind of thing is less data-driven, but it might feel nicer organized and more flexible)

function spin_and_shoot(e)
	if frametime % edict.spin_frequency == 0 then
		e.dir += PI/2
	end
	if frametime % edict.shoot_frequency == 0 then
		add_bullet(...)
	end
end

function update_enemies()
	for e in all(enemies) do
		e.behavior(e) 
		-- here a function is given instead of a constant
		-- it's basically an OOP class in a lighter package.
		-- you could also use the constants from before
		-- to refer to a function to call, for an in-between approach.
	end
end

this simple templatizing lets you remix your enemies easily. for example, you can reskin the same enemy into something with different narrative resonance for another area in your game. or, by adjusting a few parameters, one behavior template can become a fresh new enemy. an enemy that spins slowly and shoots rapidly versus one that shoots less frequently but spins rapidly have a very different play dynamic!

this is especially useful for a kind of game that wants to get as much variety of enemies out of as little space as possible, such as in a pico8 project.

in a game with more complex enemy behaviors, I could see using bitflags or lists for behaviors, lending to a sort of lightweight component system.


I've found that in lightweight projects, I prefer writing separate systems for different types of actors, such as the player and enemies. while I've been tempted to write a base entity system, so far I've found it more neat and concise to keep everything separate. this gets me thinking architecturally about the game systems with much more care and precision, whereas in a universal entity system I find my code fragmenting and requiring refactor much more easily. in other words, it gets me to do the careful engineering work earlier rather than later, so that the project never becomes messy and cumbersome.

reflecting on this has emphasized to me that coding approaches can be a personal matter just as much a question of engineering. especially in games, how we program can push and pull us creatively in different ways. it makes sense to choose methods of development that serve and inspire us rather that pursuing any sort of objective best, especially as our needs are often contextual to our goals.

hope this may be of use! <3