• 2D shoot 'em up
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
The Legend of Edgar 1.36
SDL2 map editor tutorial [UPDATED]
TBFTSS: The Pandoran War - Amiga OS4 Port
— A simple turn-based strategy game —
It's time to introduce some new ghost types. We're going to start with the least aggressive and work our way up to the most dangerous. In this part, we're going introduce a Lilac Ghost. This ghost will be a coward, and will run away from the mages whenever it sees them.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS17 to run the code. You will see a window open like the one above, showing three wizards in a maze-like map, as well as a number of ghosts. Play the game as normal. Notice how the Lilac Ghosts will aim to keep their distance from the player (note: the ghosts rely on line-of-sight and not a full map scan, to determine where the mages are). The Lilac Ghosts won't attack the mages. Once you're finished, close the window to exit.
Inspecting the code
Adding in the new ghosts is quite straightforward. The behaviour can get a little complicated, but we've got a good base to start from.
Let's start with defs.h:
We've added in a new AI enum type: AI_COWARD. This AI type will mean that our ghosts will flee the player, on sight.
Now over to ai.c:
In our switch statement for our ai types, we've added in AI_COWARD, which is calling a new function named doCoward. This is the function that will drive our Lilac Ghost's behaviour.
doCoward is quite a simple function:
This function will have the current ghost attempt to flee from the mages, by calling a function named `fallback`. If the result of this function is negative (it didn't need to flee or couldn't), it will call moveRandom. Nothing tricky so far.
The `fallback` function, however, is quite hefty:
Quite a lot happening there. Let's begin with a summary of what's going on: the ghost will count up all the mages that it can see nearby, work out where they are standing, and then attempt to flee to a part of the map that lies in the opposite direction to the average location of the mages. Now, let's go through the function one step at a time.
We're setting a variable called numMages to 0. This will be a count of the number of mages the ghost can see. We're also setting two variables called `x` and `y` to 0. These will hold the average location of our mages. We next step through all the entities in the game, looking for mages (`type` of ET_MAGE). For each one we find, we'll calculate the distance from the ghost to the mage. If the distance is 35 or less, we'll call hasLOS, to test whether the ghost has line of sight of the mage; we want to make sure that the ghost can see the mage, and isn't blocked by a wall, etc. If both of these are true, we'll increment numMages, and add the mage's (`e`) `x` and `y` to our function's `x` and `y`.
With our loop done, we're testing if numMages is greater than 0. If so, our ghosts has spotted some mages. We'll now respond. First, we divide our `x` and `y` by numMages. This will give us the average location of where the mages that were spotted are standing. With this known, we'll call calcSlope, passing through the ghost's `x` and `y`, our function's `x` and `y` (the average mage position), and `dx` and `dy` doubles. `dx` and `dy` will now hold normalized deltas for moving in the opposite direction to where the mages stand. We then multiply `dx` by MAP_RENDER_WIDTH and `dy` by MAP_RENDER_HEIGHT, to work out a location to move to, and finally set the ghost's ai's `goal` location. We're doing this by adding `dx` and `dy` to our ghost's current `x` and `y`, and then clamping the values to the bounds of the map (by using the MIN and MAX macros).
Our ghost's ai's `goal` now holds x and y values that are about a screen's distance away from where the mages are.
We then set a variable called `attempts` to 25, and enter a while-loop. The idea here is to now take the location we wish to move to (the ghost's ai's goal) and randomly pick a spot around that. The purpose of this is to jitter about the location, as it is not unlikely that the goal's location is in fact a wall or some other obstacle that the ghost will not be allowed to move to. During each loop, we're assigning `x` and `y` to the values of goal, with a random adjustment of -10 and +10. We then call isWall, passing through `x` and `y`, and if the result is negative (not a wall), we'll call createAStarRoute. Our loop will continue until we either exhaust our attempts to find a place to flee to or we successfully create an A* route.
Finally, the ghost's AI's `goal` is set to the values of `x` and `y`, and we will return whether we were able to create a route (0 or 1).
Wow, quite a lot there. But, as you can see from the game, it's quite effective at making the ghosts run away from the mages. That's all we need to do to make our cowards work.
One other function to look at before moving on, and that's a tweak to addAIUnits:
Instead of creating three white ghosts, we're creating three Lilac Ghosts, by calling initEntity and passing over "Lilac Ghost".
Finally, let's jump over to ghosts.c, where we've added a new function - initLilacGhost:
This function simply sets up our Lilac Ghost, setting its `name`, `texture`, `ap` and `hp`, moveRange, and also setting its AI `type` to AI_COWARD. Remember that this function is invoked via our entity factory (entityFactory.c), to which we've added in the appropriate key and init function values.
Done! Adding in our Lilac Ghost wasn't too difficult, at all. Since we've now defined so many other functions, our new ghost has a lot to draw upon to define its behaviour.
Next, let's add a ghost that is a bit more aggressive, but only a little so. The Blue Ghost we'll introduce next will keep its distance, but will fight back against the wizards.
The source code for all parts of this tutorial (including assets) is available for purchase:
It is also available as part of the SDL2 tutorial bundle: