Combat, Effects, and Sounds
Now we will add Link’s sword attack and make it so Link can damage the soldiers and bushes, as well as add some visual effects, sounds, and music.
EnemyComponent Setup
Make a new subclass of Component
called EnemyComponent
. We will attach this component to any enemy in the game. For now, it needs:
- A constructor
- A destructor
- A private
CollisionComponent*
member - A public getter for the
CollisionComponent*
Add a std::vector
of EnemyComponent*
as a private member in Game.h
as well as a public getter that returns the vector by reference.
In the constructor of EnemyComponent
, add itself to the game’s enemy vector. Then use GetComponent
to get the collision component of the owner and save it in the member variable.
In the destructor of EnemyComponent
, remove itself from the game’s enemy vector.
Create an EnemyComponent
in the constructors of both Soldier
and Bush
. Make sure you create this component after the CollisionComponent
is created, so that the GetComponent
call in the EnemyComponent
can find the CollisionComponent
.
Colliding Against Enemies
Now in PlayerMove::Update
, before you check for collision against the colliders, test for collisions against the enemies as well. Use GetMinOverlap
to correctly apply the offset if a collision occurs.
Link should no longer be able to walk through the bushes or soldiers. Additionally, the soldier moving into Link should cause Link to move, like this:
Attack Animation
Now you need to setup Link to do his sword attack with the following design:
- The attack occurs on the leading edge of pressing the spacebar
-
The attack lasts 0.25 seconds
- During the attack, you should prevent the WASD keys from moving Link (although a
Soldier
pushing Link would still move him).- As an example, supposed the player is holding down the W key. Link will be walking up. If the player presses the spacebar while still holding down the W key, the attack will occur and Link will temporarily stop moving during the attack. However, if the player is still holding down the W key after the attack finishes, Link should start walking up again. The player should not have to let go and press the W key again for Link to continue moving.
- Additional spacebar presses during the attack should be ignored (you have to wait until the attack is over to start a new one)
- During the attack, Link should play one of the following animations (depending on the direction he’s facing):
- Up -
"AttackUp"
- Down -
"AttackDown"
- Left -
"AttackLeft"
- Right -
"AttackRight"
- Up -
- When you first start the attack, call
ResetAnimTimer
on theAnimatedSprite
to ensure the attack animation will always start on the first frame
Confirm that you can attack in all four directions, and that Link stops moving during the attack. It should look like this video:
Hitpoints and Killing Enemies
Now you’ll set it up so enemies have hit points, can take damage, and can die.
In EnemyComponent
, add a private int
to track the enemy’s hit points and a getter/setter for it
Next, add a public TakeDamage
function to EnemyComponent
. The basic idea for taking damage is:
TakeDamage
should do nothing if it’s been called within 0.25 seconds of the last time it was called. (Effectively, this makes the enemy invulnerable for 0.25 seconds). To implement this, you’ll have to add variables and/or overrideUpdate
.- Every time the enemy takes damage, it should reduce the hit points by 1
- If the hit points hits 0, then the enemy should die. For now, dying should just set the state of the owner to
ActorState::Destroy
If you are using SDL_GetTicks
for the invulnerable timer, you’re doing something wrong. You do not need to use it and there’s a much cleaner way to implement the invulnerable logic.
Set it up so the Soldier
has 2 hit points and the Bush
has 1 hit point.
Sword Class
Create a subclass of Actor
called Sword
. It just needs a CollisionComponent
set to size (28, 28). We will use this class to help with dealing damage, since we’ll be able to set its position precisely for where the player’s sword hits.
Create an instance of Sword
in the PlayerMove
constructor, and save it in a member variable.
During an attack, you’ll want to set the position of the Sword
to the position of the Player
plus an offset. You also need to change the dimensions of the Sword
’s CollisionComponent
based on the direction. This needs to happen every frame during the attack to account for the player getting pushed and moving.
The position offset and CollisionComponent
dimensions are as follows:
Attack Direction | Offset | Collision Size |
---|---|---|
Up | (0, -40) | (20, 28) |
Down | (0, 40) | (20, 28) |
Left | (-32, 0) | (28, 20) |
Right | (32, 0) | (28, 20) |
It’s recommended to update the sword position in a separate function.
Testing Sword Against Enemies
In PlayerMove::Update
, during the loop over all the enemies and before you do a GetMinOverlap
between the player and the enemy, you should first check if the player has an active attack. If the player is attacking, then you should test whether the Sword
intersects with that enemy, and if it does, call TakeDamage
on the enemy.
You should now be able to damage and destroy both the Bush
and Soldier
. The Soldier
should die in two hits while the Bush
will die in one hit. It should look like this:
OnDamage and OnDeath Callbacks
As discussed in lecture, callback functions are useful when you want to be able to easily customize behavior when some specific event occurs in game without needing to inherit and override functions.
In EnemyComponent.h
:
- Add an include for
<functional>
, which is required to use thestd::function
class - Add two private member variables of signature
std::function<void()>
. One for theOnDamage
callback and one for theOnDeath
callback. The template parameter passed intostd::function
means that the callback functions will take in no parameters and return nothing - Add two setters for these member variables (they can just be one-liners in the header). Note that you generally should pass a
std::function
by value, not by reference.
Then, in your TakeDamage
function, if the enemy takes damage you should call the OnDeath
callback if the damage is lethal and otherwise the OnDamage
one.
Remember that any invocation of a std::function
callback should always check that the callback is set before calling it. Calling an unset callback will crash.
Adding an OnDamage callback for Soldier
As discussed in lecture, the best way to set a callback with custom logic is with a lambda expression. Since we’ll generally want to access member functions/member variables in a callback, the general syntax of your lambdas will be something of the form:
[this]() {
// Do whatever you want to inside the callback...
}
Set it up so that when a Soldier
takes damage, it uses the OnDamage
callback to tell the Soldier
it’s “stunned” for 1 second (there is a STUN_DURATION
constant in SoldierAI
).
While stunned, the Soldier
:
- Should not move at all
- Should have their animated sprite set to paused so the animation doesn’t update
For telling the Soldier
they’re “stunned”, you are required to use the callback to receive credit for the spec. However, the logic of updating and ultimately ending the “stunned” state will require additional code in SoldierAI
.
Confirm that when you hit a soldier the first time, they stop moving for 1 second and then resume moving again. It will look like this:
Adding an OnDeath Callback for Bush
Now you’ll see it up so when the Bush
dies, it tells the PathFinder
to make the path node under it reachable. In an OnDeath
callback, call the SetIsBlocked
function on the PathFinder
, passing in false
as the 3rd parameter. The row is the Bush
position.y / 32 and the column is the Bush
position.x / 32.
The easiest way to test this is to clear out the rows of bushes near the second/third soldier. You should notice that once the bushes are cleared, the soldiers will start pacing back and forth rather than going all the way around:
Integrating the AudioSystem
In Lab 4 we were using SDL_mixer calls directly rather than the AudioSystem
, so you need to make a few changes to use AudioSystem
instead:
- Add a private
AudioSystem*
member variable toGame
- Add a public
GetAudio()
function toGame
that returns the private pointer - In
Game::Initialize()
, instead of callingMix_OpenAudio
you need to dynamically allocate anAudioSystem
and save it in the private member variable - In
Game::UpdateGame
, after you calculate delta time, but before you callUpdate
on each actor, callUpdate
on the audio system - At the start of
Game::LoadData
, callCacheAllSounds()
on the audio system - In
Game::Shutdown
, instead of callingMix_CloseAudio
, delete the audio system
Effect Class
Now we will add a new type of Actor
that we can create when we want to spawn an effect that plays both an animation and a sound before destroying itself when done.
Create a subclass of Actor
called Effect
:
- For its constructor, add an additional
Vector2
parameter for position, astd::string
parameter for an animation name, and astd::string
parameter for a sound name - Add a private lifetime variable as a
float
- Add an override of
OnUpdate
In the implementation of the Effect
constructor:
- Set the position of the actor to the position parameter
- Create an
AnimatedSprite
:LoadAnimations("Assets/Effects")
- Set the animation to the animation name parameter
- Use
GetAnimDuration
to get the time the animation lasts, and save it in the lifetime member variable
- Call
PlaySound
on the Game’sAudioSystem
to play the sound (the sound name parameter is the sound to play)
In OnUpdate
, you just need to decrement the lifetime variable by delta time and set the actor state to Destroy
once the lifetime is <= 0.0f
.
Now create Effect
s in the following cases (in each case, the position should be the position of the Bush
/Soldier
that creates it):
- In the
Bush
OnDeath callback, use"BushDeath"
as the animation and"BushDie.wav"
as the sound - In the
Soldier
OnDamage callback, use"Hit"
as the animation and"EnemyHit.wav"
as the sound - In the
Soldier
OnDeath callback, use"Death"
as the animation and"EnemyDie.wav"
as the sound
Confirm that killing the bush and damaging/killing the soldier play the effects as expected. It should look like this:
Attack Sound and Music
When the player attacks, play the "SwordSlash.wav"
sound.
For the music, there is an intro portion followed by a loop.
"MusicStart.ogg"
should play initially (not looping)- Once
"MusicStart.ogg"
is no longer playing, play"MusicLoop.ogg"
, looping
To figure out when "MusicStart.ogg"
finishes, you can save the SoundHandle
from PlaySound
and then use the GetSoundState
function on every frame to find out if the sound is stopped. (Of course, you will also have to ensure that it doesn’t start playing the loop sound again on every frame after that).
Finally, to disable the Soldier
path visualization, uncomment the SetIsVisible
line in the SoldierAI
constructor.
Now your game should behave like the original video. (Note that the video starts a couple of seconds into MusicStart.ogg
, which is why it sounds like it starts abruptly):
Once you’ve pushed your code, you should review the grading specifications to confirm you’ve satisfied them.