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.

Last week we begun work on our projectile system and quickly ran into an issue where our projectile animation wouldn’t play correctly because our movement system is continuously setting an animation state. This means we’ve now reached a point where our current movement system is no longer fit for purpose. So this week we will extend the system by introducing a velocity component, which will be responsible for storing an entities speed in a given direction. Among other things having a velocity component will allow us to have more than one force acting on an entity and it will also help us fix the issues we faced last week.

Create a class called C_Velocity.

C_Velocity.hpp
``#ifndef C_Velocity_hpp#define C_Velocity_hpp#include <math.h>#include "Component.hpp"class C_Velocity : public Component{public:    C_Velocity(Object* owner);        void Update(float deltaTime) override;        void Set(const sf::Vector2f& vel);	void Set(float x, float y);    const sf::Vector2f& Get() const;    private:    void ClampVelocity();        sf::Vector2f velocity;    sf::Vector2f maxVelocity;};#endif /* C_Velocity_hpp */``

We can set and retrieve the current velocity (a vector2f). One thing to note is that when we set the velocity it is clamped to a maximum value.

C_Velocity.cpp
``#include "C_Velocity.hpp"#include "Object.hpp"C_Velocity::C_Velocity(Object* owner) : Component(owner), maxVelocity(500.f, 500.f) { }void C_Velocity::Update(float deltaTime){    owner->transform->AddPosition(velocity * deltaTime);}void C_Velocity::Set(const sf::Vector2f& vel){    velocity = vel;        ClampVelocity();}void C_Velocity::Set(float x, float y){    velocity.x = x;    velocity.y = y;        ClampVelocity();}const sf::Vector2f& C_Velocity::Get() const{    return velocity;}void C_Velocity::ClampVelocity(){    if (fabs(velocity.x) > maxVelocity.x)    {        if (velocity.x > 0.f)        {            velocity.x = maxVelocity.x;        }        else        {            velocity.x = -maxVelocity.x;        }    }        if (fabs(velocity.y) > maxVelocity.y)    {        if (velocity.y > 0.f)        {            velocity.y = maxVelocity.y;        }        else        {            velocity.y = -maxVelocity.y;        }    }}``

In the Update function the velocity, scaled by delta time, is added to the entities position. The Update function is called every frame, for more information see the first tutorial.The clamp function clamps the value whether it is negative or positive. Ensuring the negative value is clamped prevents a similar situation to the infamous Big Rigs unclamped reverse speed.Its functionality will grow along with our needs but for now, that’s it for the velocity component. Make sure you add the new component to the player in SceneGame.

SceneGame.hpp
``#include “C_Velocity.hpp"…``
SceneGame.cpp
``void SceneGame::OnCreate(){…        player->AddComponent<C_Velocity>();        objects.Add(player);…}``

I’ve added the component just before we add the player to the object collection.

We now need to change how the keyboard component works to take advantage of the velocity component. Remove the reference to the animation component and in its place add the new velocity component.

C_KeyboardMovement.hpp
``#ifndef C_KeyboardMovement_hpp#define C_KeyboardMovement_hpp#include "Component.hpp"#include "Input.hpp"#include "C_Velocity.hpp"class C_KeyboardMovement : public Component{public:    C_KeyboardMovement(Object* owner);        void Awake() override;        void SetInput(Input* input);    void SetMovementSpeed(float moveSpeed);        void Update(float deltaTime) override;    private:    float moveSpeed;    Input* input;        // Remove line to animation and add our new velocity component    std::shared_ptr<C_Velocity> velocity;};#endif /* C_KeyboardMovement_hpp */``

I’ve also converted moveSpeed to a float instead of an integer.We need to retrieve a reference to the velocity component in the keyboards Awake function (more information on how the Awake function operates can be found here). I have also changed the default move speed to 300.

C_KeyboardMovement.cpp
``C_KeyboardMovement::C_KeyboardMovement(Object* owner) :Component(owner), moveSpeed(300.f) {}void C_KeyboardMovement::Awake(){    velocity = owner->GetComponent<C_Velocity>();}void C_KeyboardMovement::SetMovementSpeed(float moveSpeed){    this->moveSpeed = moveSpeed;}``

Remove all references to the animation component in the Update function and instead of setting the entities position directly through the transform component we set its velocity.

C_KeyboardMovement.cpp
``void C_KeyboardMovement::Update(float deltaTime){    if(input == nullptr)    {        return;    }        float xMove = 0.f;    if(input->IsKeyPressed(Input::Key::Left))    {        xMove = -moveSpeed;    }    else if(input->IsKeyPressed(Input::Key::Right))    {        xMove = moveSpeed;    }        float yMove = 0.f;    if(input->IsKeyPressed(Input::Key::Up))    {        yMove = -moveSpeed;    }    else if(input->IsKeyPressed(Input::Key::Down))    {        yMove = moveSpeed;    }        velocity->Set(xMove, yMove);}``

Now we can move the player around again but his movement is no longer animated however we can now press the ‘e’ key to play the projectile animation. To re-introduce movement animations we need to create a new component, which needs to be able to set the walking or idle state and calculate our facing direction based on the entities velocity. We’ll call the component C_MovementAnimation.

C_MovementAnimation.hpp
``#ifndef C_MovementAnimation_hpp#define C_MovementAnimation_hpp#include "Component.hpp"#include "C_Velocity.hpp"#include "C_Animation.hpp"class C_MovementAnimation : public Component{public:    C_MovementAnimation(Object* owner);    	void Awake() override;    void Update(float deltaTime) override;    private:    std::shared_ptr<C_Velocity> velocity;    std::shared_ptr<C_Animation> animation;};#endif /* C_MovementAnimation_hpp */``

C_MovementAnimation.cpp
``#include "C_MovementAnimation.hpp"#include "Object.hpp"C_MovementAnimation::C_MovementAnimation(Object* owner) : Component(owner) { }void C_MovementAnimation::Awake(){    velocity = owner->GetComponent<C_Velocity>();    animation = owner->GetComponent<C_Animation>();}void C_MovementAnimation::Update(float deltaTime){    if(animation->GetAnimationState() != AnimationState::Projectile)    {        const sf::Vector2f& currentVel = velocity->Get();                if(currentVel.x != 0.f || currentVel.y != 0.f)        {            animation->SetAnimationState(AnimationState::Walk);                        float velXAbs = fabs(currentVel.x);            float velYAbs = fabs(currentVel.y);                        if(velXAbs > velYAbs)            {                if(currentVel.x < 0)                {                    animation->SetAnimationDirection(FacingDirection::Left);                }                else                {                    animation->SetAnimationDirection(FacingDirection::Right);                }            }            else            {                if(currentVel.y < 0)                {                    animation->SetAnimationDirection(FacingDirection::Up);                }                else                {                    animation->SetAnimationDirection(FacingDirection::Down);                }            }        }        else        {            animation->SetAnimationState(AnimationState::Idle);        }    }}``

The code is very similar to what we had in the keyboard controller except this time we are retrieving the player’s velocity using our new component. Right now we have to perform a check at the beginning of the function to see whether we are currently playing the projectile attack animation. Without this check, the function would override the projectile animation state and it wouldn’t play the animation at all (a problem we had last week. This solves our problem for now but what happens when we add a melee attack animation? We’ll need to add another check here. And then we’ll need further animations: death, hurt, picking up items etc. When the games finished we may have 100s of checks here and in many other places (because we don’t want the projectile attack overriding the death animation for example). This is not a sustainable way of writing an animation system. But like many of our systems, this is not its final form. We’ll soon be writing a more robust data structure, most likely a finite state machine, to handle the animation states more elegantly. There are also other options we can look at for inter-system communication including a message queue and event system but that’s for another day.

As with any new component, it won’t do anything unless we add it to an Object, in this case, the player (which is our only object at the moment).

SceneGame.hpp
``#include “C_MovementAnimation.hpp"…``
SceneGame.cpp
``void SceneGame::OnCreate(){…        player->AddComponent<C_MovementAnimation>();        objects.Add(player);    …}``

If you run the game now you’ll be able to walk around with animations to boot. You can also press the ‘e’ key to start the projectile attack animation. And because we’re setting the animation direction in our new animation component we can view the attack animation in all four directions.

Now we can see the attack animation in any direction.

But we’re not out of the woods yet, once the animation starts we are stuck with that animation. There is no way to stop the animation and it will continue to loop until we quit the game. Because the animation is set to loop and our new movement component is instructed to do nothing while the projectile attack is running we can only watch as the player shoots one non-existent projectile after another. Also, we can move while the projectile animation is in progress. These are two things we will fix next week when we start work on creating one-shot animations (animations that only play once and then transition to a different state).

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.