C++ Game Engine Development – Part 28 – Projectile Animation

This is part of an ongoing series where we write a complete 2D game engine in C++ and SFML. A new tutorial is released every Monday. You can find the complete list of tutorials here and download the source code from the projects GitHub page.
This week we will add a new projectile attack animation to our player. We will be working on the projectile system over the next few weeks as we will need to make changes to our current movement system by creating a velocity and direction component, update our animation system so that we can run actions on animation frames, and update our collision system to enable objects to communicate with each other when they collide. But for this week we’ll start simply by:
  1. Creating and adding a new projectile animation for the player.
  2. Adding a new projectile animation state in our animation system.
  3. Writing a component that will run the new projectile animation.
This is a similar process to how we created our previous walk and idle animations.
Start by creating the projectile animation in SceneGame right after we create the walking animation. It uses the same sprite sheet as the other animations (Player.png), which can be found in the resources folder for this weeks tutorial.
SceneGame.cpp
void SceneGame::OnCreate()
{
    std::shared_ptr<Object> player = std::make_shared<Object>();
    
    player->transform->SetPosition(100, 700);
    
    auto sprite = player->AddComponent<C_Sprite>();
    sprite->SetTextureAllocator(&textureAllocator);
    sprite->SetDrawLayer(DrawLayer::Entities);
    
    auto movement = player->AddComponent<C_KeyboardMovement>();
    movement->SetInput(&input);
    
    auto animation = player->AddComponent<C_Animation>();
    
    int playerTextureID = textureAllocator.Add(workingDir.Get() 
											   + "Player.png");

    const unsigned int frameWidth =  64;
    const unsigned int frameHeight = 64;

    
    const FacingDirection directions[4] = {FacingDirection::Up, 
										   FacingDirection::Left, 
										   FacingDirection::Down, 
										   FacingDirection::Right};
    
    
    /*******************
     * Idle Animations *
     *******************/
    unsigned int idleYFramePos = 512;
    
    std::map<FacingDirection, std::shared_ptr<Animation>> idleAnimations;
    
    for (int i = 0; i < 4; i++)
    {
        std::shared_ptr<Animation> idleAnimation = std::make_shared<Animation>();
        
        idleAnimation->AddFrame(playerTextureID, 0, idleYFramePos, 
								frameWidth, frameHeight, 0.f);
        
        idleAnimations.insert(std::make_pair(directions[i], idleAnimation));
        
        idleYFramePos += frameHeight;
    }
    
    animation->AddAnimation(AnimationState::Idle, idleAnimations);
    
    
    /**********************
     * Walking Animations *
     **********************/
    const int walkingFrameCount = 9;
    const float delayBetweenWalkingFramesSecs = 0.1f;
    
    unsigned int walkingYFramePos = 512;
    
    std::map<FacingDirection, std::shared_ptr<Animation>> walkingAnimations;
    
    for (int i = 0; i < 4; i++)
    {
        std::shared_ptr<Animation> walkingAnimation = std::make_shared<Animation>();
        for (int i = 0; i < walkingFrameCount; i++)
        {
            walkingAnimation->AddFrame(playerTextureID, i * frameWidth, 
									   walkingYFramePos, frameWidth, 
									   frameHeight, 
									   delayBetweenWalkingFramesSecs);
        }
        
        walkingAnimations.insert(std::make_pair(directions[i], 
												walkingAnimation));
        
        walkingYFramePos += frameHeight;
    }
    
    animation->AddAnimation(AnimationState::Walk, walkingAnimations);
    
    
    /*************************
     * Projectile Animations *
     *************************/
    const int projectileFrameCount = 10;
    const float delayBetweenProjectileFramesSecs = 0.1f;

    std::map<FacingDirection, std::shared_ptr<Animation>> projectileAnimations;
    
    unsigned int projFrameYPos = 1024;
    
    for (int i = 0; i < 4; i++)
    {
        std::shared_ptr<Animation> projAnimation = std::make_shared<Animation>();
        for (int i = 0; i < projectileFrameCount; i++)
        {
            projAnimation->AddFrame(playerTextureID, i * frameWidth, 
									projFrameYPos,
									frameWidth, frameHeight, 
									delayBetweenProjectileFramesSecs);
        }
        projectileAnimations.insert(std::make_pair(directions[i], projAnimation));
        
        projFrameYPos += frameHeight;
    }
    
    animation->AddAnimation(AnimationState::Projectile, projectileAnimations);

    
    auto collider = player->AddComponent<C_BoxCollider>();
    collider->SetSize(frameWidth * 0.4f, frameHeight * 0.5f);
    collider->SetOffset(0.f, 14.f);
    collider->SetLayer(CollisionLayer::Player);
    
    auto camera = player->AddComponent<C_Camera>();
    camera->SetWindow(&window);
    
    objects.Add(player);
    
    // You will need to play around with this offset until the
    // level is in a suitable position based on the current window size.
    // This works for 1920 * 1080.
    // In future we will remove this hardcoded offset when
    // we add the ability to change resolutions.
    sf::Vector2i mapOffset(0, 180);
    //sf::Vector2i mapOffset(128, 128);
    std::vector<std::shared_ptr<Object>> levelTiles = 
		mapParser.Parse(workingDir.Get() + "House Exterior.tmx", mapOffset);
    
    objects.Add(levelTiles);
}
I’ve included the complete OnCreate function because, as well as adding the projectile animations, I’ve also changed the way we create the other animations. Instead of creating the animation directions individually we now use a loop. A small change but it reduces the size of the function considerably. If we move to a more data-driven engine (where we store all the entity data in files and parse those files to create our entities instead of creating them in code) we would be able to remove most of the initialisation code for the animations.
The projectile animation state does not yet exist so let’s add it to the projectile AnimationState enum in C_Animation.
C_Animation.hpp
enum class AnimationState
{
    None,
    Idle,
    Walk,
    Projectile // New State
};

With the animation state added and the animation added to the object, we now need some way of activating the animation when the player presses a specific key. We’ll create a component to do this. Create a new class called C_ProjectileAttack.

C_ProjectileAttack.hpp
#ifndef C_ProjectileAttack_hpp
#define C_ProjectileAttack_hpp

#include "Component.hpp"
#include "C_Animation.hpp"
#include "Input.hpp"

class C_ProjectileAttack : public Component
{
public:
    C_ProjectileAttack(Object* owner);
    
    void Awake() override;

    void Update(float deltaTime) override;
    
    void SetInput(Input* input);
    
private:
    std::shared_ptr<C_Animation> animation;
    Input* input;
};

#endif /* C_ProjectileAttack_hpp */
C_ProjectileAttack.cpp
#include "C_ProjectileAttack.hpp"
#include "Object.hpp"

C_ProjectileAttack::C_ProjectileAttack(Object* owner) : Component(owner) {}

void C_ProjectileAttack::Awake()
{
    animation = owner->GetComponent<C_Animation>();
}

void C_ProjectileAttack::Update(float deltaTime)
{
    if(input->IsKeyPressed(Input::Key::E))
    {
        animation->SetAnimationState(AnimationState::Projectile);
    }
}

void C_ProjectileAttack::SetInput(Input* input)
{
    this->input = input;
}
The Update function checks if the ‘e’ key is pressed and sets the entities animation state to ‘Projectile’, which will play our new projectile attack animation.
The ‘e’ key is not currently part of our Input system so we should add it now. To add a key we first add it the Key enum in the Input class.
Input.hpp
class Input
{
public:
    enum class Key
    {
        None = 0,
        Left = 1,
        Right = 2,
        Up = 3,
        Down = 4,
        Esc = 5,
        LBracket = 6,
        RBracket = 7,
        E = 8 // New Key
    };
…
};
We then need to check if the key has been pressed at the end of the Input’s Update function.
Input.cpp
void Input::Update()
{
…
    
    thisFrameKeys.SetBit((int)Key::E, sf::Keyboard::isKeyPressed(sf::Keyboard::E));
}

For more information on how this works see the previous tutorial where we created the Input system.

The last step is to add the new component to the player. We’ll do this in SceneGame’s OnCreate function.
SceneGame.hpp
#include “C_ProjectileAttack.hpp”
…
SceneGame.cpp
void SceneGame::OnCreate()
{
    std::shared_ptr<Object> player = std::make_shared<Object>();
    
…
    
    auto projectileAttack = player->AddComponent<C_ProjectileAttack>();
    projectileAttack->SetInput(&input);
    
    objects.Add(player);
    
…
}
I add the component just before we add the player to the object collection near the end of the function. Don’t forget to set the projectile attacks input. It is a bit of a pain that we have to manually inject our component with common classes (such as the Input class), we’ll look at a better way of doing this in the next few weeks.
Now if you run the game and press the ‘e’ key absolutely nothing happens, which at first doesn’t make much sense. Surely if we are pressing the correct key the component should be playing our projectile animation. And in fact it is but it’s being overridden by the keyboard movement as it is always setting the animation to either idle or walking.
C_KeyboardMovement.cpp
void C_KeyboardMovement::Update(float deltaTime)
{
…

  	// The keyboard component updates the animation state every frame. 
	// This prevents us from playing any other animation.   
    if(xMove == 0 && yMove == 0)
    {
        animation->SetAnimationState(AnimationState::Idle);
    }
    else
    {
        animation->SetAnimationState(AnimationState::Walk);
        
       …
    }
}
To fix this we will need to create a separate velocity component and create a new way of setting the animation based on movement speed, which is what we will cover next week. However, if you would like a sneak peek of what the animations look like you can temporarily disable the keyboard controller by adding a return statement at the beginning of the Update function.
Input.cpp
void C_KeyboardMovement::Update(float deltaTime)
{
	// Add return to the beginning of the function.
    return;
…
}
Then when you run the game and press the e key you’ll see the projectile attack animation.
Projectile Attack Animation
You’ll only be able to see the animation for shooting down as we’re no longer setting the facing direction in the keyboard controller. Before you finish for the week don’t forget to remove the return statement if you added it.
Input.cpp
void C_KeyboardMovement::Update(float deltaTime)
{
	// When you’re done testing the animation remove the return.
    //return;
…
}
As always, if you have any suggestions for what you would like covered or are having any trouble implementing a feature, then let me know in the comments and I’ll get back to you as soon as I can. Thank you for reading 🙂