• 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 —
Another aspect of turn-based strategy games is combat. You don't need to have combat in a game, of course. You could make a zen-like experience where everyone and everything is passive. However, in our game our wizards need to deal with an outbreak of ghosts, so they will be using magic to dispatch them. In this part, we'll look at how we're going to handle attacking. Note that our game only uses ranged attacks. No melee combat happens.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS06 to run the code. You will see a window open like the one above, showing three wizards in a small room, surrounded by walls, as well as a white ghost. Move the mages around as normal. Clicking once on the ghost will target it. Notice the red pulsing square that appears. Clicking again will result in the current wizard attacking (so long as they have AP to do so). Attacks will always hit and will always kill the target. Once you're finished, close the window to exit.
Inspecting the code
Adding in combat is quite a process, so we're going to do this over the next several parts of this tutorial. First, we're going to add in our targetting, firing, and target elimination.
To begin with, let's update structs.h:
We've updated the Entity struct and added in a field called `dead`. This is a flag to allow us to remove the entity from the game once it is no longer alive.
Next, we've added in a Bullet struct, to represent our bullet.
You'll be aware that we only fire one bullet at a time in our game. However, having this as a separate struct makes it easier to handle, as we'll see later. The bullet struct has several fields: `x` and `y` are the location of the bullet on screen (these are not map coordinates!). `dx` and `dy` are the movement deltas value, which will be used to determine the direction of travel. `life` is how long the bullet will live for before removing removed. `angle` is the animation angle (our bullets spin), while `texture` is the texture to display.
Next, we've updated Stage:
We've added a few new fields: targetEntity, which will be the entity that is targetted (such as the ghost); deadEntityHead and deadEntityTail, which will act as a linked list into which our dead entities will be placed; and `bullet`, which is our bullet.
Now let's move over to bullets.c. This is a new compilation unit, where all the code for our bullet handling lives. We'll work through this file one function at a time. Starting with doBullet:
This function is where we drive our bullet. We grab a reference to Stage's `bullet`, assigning it to a variable named `b` (to make the code a bit more readable, rather than constantly write stage.bullet ...). We then test if the bullet is alive, by checking that its `life` is greater than 0. If so, we'll decrease the value of `life`, and then move the bullet by adding its `dx` and `dy` to its `x` and `y`, respectively. Next, we'll update the bullet's `angle` so that it spins when we draw it (and also loop the value back around when it passes 360). Finally, we test if the bullet's `life` has reached 0 or less. If so, we'll call applyDamage.
Note how that we don't test for collisions, etc. In our game, we'll always assume the bullet has made contact with the target or has reached its destination once its `life` hits 0. We'll then check for damage, etc. after that.
The applyDamage function follows:
Not much to this function. We're always assuming the bullet hits its target and that it killed them. We therefore set the entity's `dead` flag to 1 (we'll see this being used in doEntities).
Next up, we have drawBullet:
Not a lot to explain here. We're testing to see if the bullet's `life` is greater than 0, before calling blitRotated and passing through the bullet's `texture`, its `x` and `y`, and its `angle`, so that it spins in place. blitRotated always draws with the image centered around the x and y.
Finally, we come to fireBullet. This is the function that is invoked whenever a unit attacks:
Quite a few things here to discuss. First up, we're extracting the current entity's unit data, and also memsetting Stage's `bullet` (another reason to use a separate struct, as it makes clearing all the bullet's data quite straightforward). As usual, we also grab a reference to the bullet and assign it to `b`.
Next, we work out the screen coordinates of both the attacking (the current) entity and the target entity, and assign these to `x1`, `y1` and `x2`, `y2`. With that done, we call a function named calcSlope. This function will calculate a normalized 2D vector from the attacker to the target, and set the results into two doubles. In this case, we're passing over bullet's `dx` and `dy`, to be populated.
Next, we want to work out how many "steps" it will take for our bullet to reach its target. We do this by taking the absolute values of `x1` - `x2` and `y1` - `y2`, and choosing the greater value (via the MAX macro). So, if the distance between `x1` and `x2` is greater than `y1` and `y2`, the value of steps will be the former. Otherwise, it will be the latter.
We're now ready to set all our bullet's values. We first the bullet's `x` and `y` as `x1` and `y1`, the attacking entity's screen coordinates. We also multiply the bullet's `dx` and `dy` by BULLET_SPEED. When it comes to the bullet's `life`, we set this as the number of steps (as a decimal, hence multiplying by 1.0), divided by BULLET_SPEED. This will mean that the bullet lives only as long as it takes to cross the screen from the attacker to the target.
Finally, we set the bullet's `texture` and also ensure the attacker is facing the correct direction, by testing where `x1` lies relative to `x2`, and also deduct one AP from the attacking unit. Again, we do this here to centralize the AP deduction logic.
With the core bullet logic in place, we now just need to incorporate it into the rest of the game. Turning first to player.c, we've updated doSelectUnit:
We're now testing the side of the entity that we've clicked on. If it's SIDE_PLAYER, we're handling the wizards as normal. If it's SIDE_AI, we will test to see if it is Stage's targetEntity. If not, we'll assign it as such. Otherwise, we'll call attackTarget. This means that clicking on an enemy twice will cause us to attack it.
The attackTarget function itself is quite basic right now:
The function takes a single argument: `u`, the Unit that is attacking. It first checks that the attacking unit has AP available, and will call fireBullet.
Almost done! We just need to make a few more updates and this part is finished. If we turn to entities.c, we've made a few tweaks. Starting with initEntities:
We've setting up our dead entity linked list, and also loading a new texture called selectedTargetTexture. This will be used to highlight the currently targetted entity.
Next, we've updated doEntities:
Since our entities now have a `dead` flag, we're going to test to see if it's set (such as when a bullet hits a unit) we're going to remove the entity from our main linked list, and add it to our dead list. We're also testing to see if the dead entity was Stage's target entity, and set it to NULL if so.
We've also made changes to drawEntities:
As well as highlighting the current entity, we're testing to see if a target entity has been set, and rendering selectedTargetTexture in a similar way.
Moving over to stage.c next, we've updated the `logic` function:
We're now calling doBullet. Also, we will consider Stage's `animating` flag to be set if our bullet's `life` is greater than 0 (meaning it's active). So now, if a unit is moving or the bullet is inflight, our game will be in animating state.
The `draw` function has also been tweaked:
Naturally, we now need to call drawBullet, so that our bullet is rendered.
Finished! We can now target and fire on our ghost, destroying it! So, while it can run, it cannot hide.
We should now expand our combat a bit, since attacks that always hit and destroy enemies immediately won't make for a very fun game. So, in our next part we'll add in hit points, accuracy, and weapons that do variable amounts of damage.
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: