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.
A couple of weeks ago we started work on spawning projectile and ended up with something like this:
Whenever we spawn an arrow we draw the sprite for all four directions as we have no meaningful method of knowing which direction the player is facing. As the title of this tutorial suggests that is all about to change. This week we are going to create a component that we can add to any object with a velocity (i.e. any object that can move), which will calculate the direction the object is facing. We’ll call this component C_Direction.
#ifndef C_Direction_hpp
#define C_Direction_hpp
#include "Component.hpp"
#include "Animation.hpp"
#include "C_Velocity.hpp"
class C_Direction : public Component
{
public:
C_Direction(Object* owner);
void Awake() override;
FacingDirection Get();
private:
std::shared_ptr<C_Velocity> velocity;
FacingDirection currentDir;
};
#endif /* C_Direction_hpp */
The component stores a reference to the objects velocity component, we’ll use this to calculate the movement direction. It also stores the current FacingDirection in a variable called currentDir, which, unsurprisingly, stores the entities current direction. We do this because if the entity is not moving that frame and we cannot calculate its direction based on its velocity, we can pass this as its last known direction (this will become more clear shortly).The FacingDirection enum was created in a previous tutorial and is in the Animation header. I’ve included it below for reference.
enum class FacingDirection
{
None,
Left,
Right,
Up,
Down
};
The constructor will initialise the default value for the currentDir. We set it to ‘FacingDirection::Down’ as that is the direction the player’s sprites are facing initially. This means that even if the player hasn’t moved but attempts to shoot a projectile the projectile is facing the correct way.
The component has an Awake function that we won’t be calling directly (for more information on for the Awake and Start functions work see here) but will be used to retrieve the velocity component from the object. So the only public function that we will call is ‘Get’ which returns a FacingDirection.
#include "C_Direction.hpp"
#include "Object.hpp"
C_Direction::C_Direction(Object* owner) :
Component(owner), currentDir(FacingDirection::Down) { }
void C_Direction::Awake()
{
velocity = owner->GetComponent<C_Velocity>();
}
For the Get function, we’re going to use very similar code to that found in C_MovementAnimation. We’ll retrieve the entities current velocity, determine which axis has the highest velocity, and then on the axis with the highest velocity we’ll check if its a positive or negative force; this will let us know which way the entity is currently moving. If the velocity is 0 on both axis then we return the currentDir, which will either be the default of ‘Down’ or will hold the previously calculated moment direction.
FacingDirection C_Direction::Get()
{
const sf::Vector2f& currentVel = velocity->Get();
if(currentVel.x != 0.f || currentVel.y != 0.f)
{
float velXAbs = fabs(currentVel.x);
float velYAbs = fabs(currentVel.y);
if(velXAbs > velYAbs)
{
if(currentVel.x < 0)
{
currentDir = FacingDirection::Left;
}
else
{
currentDir = FacingDirection::Right;
}
}
else
{
if(currentVel.y < 0)
{
currentDir = FacingDirection::Up;
}
else
{
currentDir = FacingDirection::Down;
}
}
}
return currentDir;
}
Now that we determine the entities movement direction in this new component we can remove the direction calculations from C_MovementAnimation::Update. The complete function now looks like this:
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);
}
else
{
animation->SetAnimationState(AnimationState::Idle);
}
}
}
The movement animation component is now only concerned with setting the current animation based on the entities velocity.With our new component complete we need to add it to our player object. As usual, we’ll do this in SceneGame::OnCreate.
#include “C_Direction.hpp"
…
void SceneGame::OnCreate()
{
…
player->AddComponent<C_Direction>();
objects.Add(player);
…
}
As we’ve removed the code that sets the direction for the player’s animation, if you were to run the game now, you will see that while the player does alternate between walking, idle, and shooting animations he is always facing the same direction. We need to find a new home for setting the animations direction. For now, we’ll set it directly in the animation component.
#include "C_Direction.hpp"
…
class C_Animation : public Component
{
…
private:
…
std::shared_ptr<C_Direction> direction;
};
The animation component needs access to the direction component so it will store a reference and retrieve the component in the Awake function.
void C_Animation::Awake()
{
…
direction = owner->GetComponent<C_Direction>();
}
Set the animation direction in the Update function before we check for a new frame.
void C_Animation::Update(float deltaTime)
{
// Set the animation direction before we update the animation frame.
SetAnimationDirection(direction->Get());
if(currentAnimation.first != AnimationState::None)
{
bool newFrame = currentAnimation.second->UpdateFrame(deltaTime);
if(newFrame)
{
const FrameData* data = currentAnimation.second->GetCurrentFrame();
sprite->Load(data->id);
sprite->SetTextureRect(data->x, data->y, data->width, data->height);
}
}
}
Now when you run the game you will notice that once again the player’s sprite is updated based on the direction they are moving.
Back to the reason for the direction component: the projectile sprite. If you remember we had no way of selecting which sprite to use for the arrow.The current sprite sheet we are using for the arrow consists of four images showing the arrow rotated to face different directions.
The sprite sheet for our projectiles contains the arrow for all four directions.
Now we have a method of retrieving our current direction we can use that to set which sprite in the sprite sheet we use. C_ProjectileAttack (our projectile attack component) needs to store a reference to the direction component.
class C_ProjectileAttack : public Component
{
…
private:
…
std::shared_ptr<C_Direction> direction;
};
As is typical we will retrieve the pointer to the component in the Awake function.
void C_ProjectileAttack::Awake()
{
…
direction = owner->GetComponent<C_Direction>();
}
Now we have the direction component we can retrieve the entities current moving direction when we spawn a projectile in the SpawnProjectile function.
void C_ProjectileAttack::SpawnProjectile()
{
std::shared_ptr<Object> projectile = std::make_shared<Object>(owner->context);
projectile->transform->SetPosition(owner->transform->GetPosition());
// Get the current facing direction.
FacingDirection currentDir = direction->Get();
auto projSprite = projectile->AddComponent<C_Sprite>();
projSprite->Load(projectileTextureID);
projSprite->SetDrawLayer(DrawLayer::Entities);
projSprite->SetSortOrder(100);
owner->context->objects->Add(projectile);
}
However, we still have no way of converting the direction to a texture rect for the arrow sprite. To do this we’ll create an unordered_map with the FacingDirection as the key and an IntRect representing the texture rect as the value. Add the unordered_map called textureDirectionBindings to C_ProjectileAttack.
class C_ProjectileAttack : public Component
{
…
private:
static std::unordered_map<FacingDirection, sf::IntRect> textureDirectionBindings;
…
};
std::unordered_map<FacingDirection, sf::IntRect> C_ProjectileAttack::textureDirectionBindings = {};
I’ve made the map static because all the projectile attacks will use the same texture (as we only have the one texture at the moment), which means they will also use the same texture rects. As a result, there is no need to maintain separate maps for each projectile attack component. For example, if we have 100 entities that can shoot projectiles in a scene and they all had to maintain a separate map, with each maps size being 40 bytes, we are using 40 * 100 = 4000 bytes in memory. Not a huge memory footprint on today’s computers but it’s nice to be able to save memory when we can. By the way, you can calculate the size of an object using the sizeof function.
size_t tBindings = sizeof(textureDirectionBindings);
You may have trouble compiling your code now depending on which compiler you use. If you receive a compilation error saying something like: “Implicit instantiation of undefined template”, then you need to provide a hash as the third template parameter (with the enum being the first and sf::IntRect the second). We’ll create the hash in the C_ProjectileAttack class for now. If we find we need it for anything else we’ll move it to its own separate class. Again if you can compile fine then you do not need to create this hash and you can jump further on to where we initialise the unordered_map in the Start function.
struct EnumClassHash
{
template <typename T>
std::size_t operator()(T t) const
{
return static_cast<std::size_t>(t);
}
};
The hash object is a functor object, which is an object that defines the () operator and can be treated as if they are a function. If we use them in the core code for the game I’ll go through it in more detail.With the hash created we should set it as the third template parameter in our map.
static std::unordered_map<FacingDirection, sf::IntRect, EnumClassHash> textureDirectionBindings;
std::unordered_map<FacingDirection, sf::IntRect, EnumClassHash> C_ProjectileAttack::textureDirectionBindings = {};
With that complete, you should be able to compile without issues. For more information on why this was needed have a look at the question and its answers on stack overflow.
We’ll add four entries into the unordered_map, one for each direction. The IntRect accepts four parameters: left, top, width, height. We can work out what we need for each direction by examining the arrow sprite.
Using the image above we know that four directions and there corresponding texture locations are:
- Up = 0, 0, 64, 64
- Left = 64, 0, 64, 64
- Down = 128, 0, 64, 64
- Right = 192, 0, 64, 64
Add these entries in the Start function.
void C_ProjectileAttack::Start()
{
projectileTextureID = owner->context->textureAllocator->Add(owner->context->workingDir->Get() + "LPC/Weapons/arrow.png");
textureDirectionBindings.emplace(FacingDirection::Up, sf::IntRect(0, 0, 64, 64));
textureDirectionBindings.emplace(FacingDirection::Left, sf::IntRect(64, 0, 64, 64));
textureDirectionBindings.emplace(FacingDirection::Down, sf::IntRect(128, 0, 64, 64));
textureDirectionBindings.emplace(FacingDirection::Right, sf::IntRect(192, 0, 64, 64));
}
And now we can use the rectangle returned from the unordered_map to specify which sprite we want to use.
void C_ProjectileAttack::SpawnProjectile()
{
std::shared_ptr<Object> projectile = std::make_shared<Object>(owner->context);
projectile->transform->SetPosition(owner->transform->GetPosition());
FacingDirection currentDir = direction->Get();
auto projSprite = projectile->AddComponent<C_Sprite>();
projSprite->Load(projectileTextureID);
projSprite->SetDrawLayer(DrawLayer::Entities);
projSprite->SetSortOrder(100);
// Use the direction to set the texture rect.
projSprite->SetTextureRect(textureDirectionBindings.at(direction->Get()));
owner->context->objects->Add(projectile);
}
Now when we run the game and fire a projectile (‘e’ key), the correct sprite is used based on the player’s direction. Nice!
The arrow still doesn’t go anywhere but that will all change next week when we look at projectile movement and collisions.
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 🙂