Skip to main content Link Search Menu Expand Document (external link)

Theme: auto

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"
  • When you first start the attack, call ResetAnimTimer on the AnimatedSprite 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 override Update.
  • 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 the std::function class
  • Add two private member variables of signature std::function<void()>. One for the OnDamage callback and one for the OnDeath callback. The template parameter passed into std::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 to Game
  • Add a public GetAudio() function to Game that returns the private pointer
  • In Game::Initialize(), instead of calling Mix_OpenAudio you need to dynamically allocate an AudioSystem and save it in the private member variable
  • In Game::UpdateGame, after you calculate delta time, but before you call Update on each actor, call Update on the audio system
  • At the start of Game::LoadData, call CacheAllSounds() on the audio system
  • In Game::Shutdown, instead of calling Mix_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, a std::string parameter for an animation name, and a std::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’s AudioSystem 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 Effects 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.