C++ Game Engine Development – Part 35 – Projectile Velocity and Collisions

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’ll continue working on our projectile system. By the end of this tutorial we will have:

  1. Changed the arrows initial position so that it aligns with the player’s bow.
  2. Made the projectiles move through the environment.
  3. Made the projectiles collide with the environment.
It’s a lot to do so we’ll start straight away by implementing the first change: the relative position of the arrow. Depending on the direction we are facing we want the arrow to be spawned in different locations relative to the player’s bow; we want the arrow to always line up with the centre of the bow. There are a few ways of accomplishing this but one of the more straightforward is to add an offset to the arrow based on the player’s direction. This will use a similar solution to last week where we implemented an unordered_map to store the sprites texture rect based on the player’s direction. This time we’ll store the arrows offset as an sf::Vector2f with the FacingDirection once again as the key. Add the unordered_map, which I’ve called offsetDirectionBindings, to C_ProjectileAttack.
C_ProjectileAttack.hpp
class C_ProjectileAttack : public Component
{
…
    
private:
    static std::unordered_map<FacingDirection, sf::Vector2f, EnumClassHash> offsetDirectionBindings;
    
…
};

Similarly to the textureDirectionBindings I’ve made this map static for the same reasons I outlined in the previous tutorial: it reduces memory usage.

If you don’t recognise the EnumClassHash then you probably don’t need it to compile, see the previous tutorial  where I created and discussed the need for the hash class. If you didn’t need it last week then you don’t need it now and you can just remove it so the declaration looks like this:

C_ProjectileAttack.hpp
class C_ProjectileAttack : public Component
{
…
    
private:
    static std::unordered_map<FacingDirection, sf::Vector2f> offsetDirectionBindings;
    
…
};
You need to initialise the unordered_map in the source file. Remember to include/exclude the EnumClassHash as required.
C_ProjectileAttack.cpp
std::unordered_map<FacingDirection, sf::Vector2f, EnumClassHash> C_ProjectileAttack::offsetDirectionBindings = {};

We’ll insert the four offsets in the Start function. I’ve chosen the following offsets:

  • Up = 0, 0
  • Left = -8, 3
  • Down = -3, 15
  • Right = 8, 3
They are only rough estimates and can always be adjusted later.
C_ProjectileAttack.cpp
void C_ProjectileAttack::Start()
{
…
    
    offsetDirectionBindings.emplace(FacingDirection::Up, 
									sf::Vector2f());
    offsetDirectionBindings.emplace(FacingDirection::Left, 
									sf::Vector2f(-8.f, 3.f));
    offsetDirectionBindings.emplace(FacingDirection::Down, 
									sf::Vector2f(-3.f, 15.f));
    offsetDirectionBindings.emplace(FacingDirection::Right, 
									sf::Vector2f(8.f, 3.f));
}
With the offsetDirectionBindings populated, add the offset directly after we spawn the projectile in the SpawnProjectile function.
C_ProjectileAttack.cpp
void C_ProjectileAttack::SpawnProjectile()
{
    FacingDirection faceDir = direction->Get();
    
    std::shared_ptr<Object> projectile = std::make_shared<Object>(owner->context);
    
	// We add an offset depending on the direction the player is facing.
    projectile->transform->SetPosition(owner->transform->GetPosition() +
                                       offsetDirectionBindings.at(faceDir));
    
    auto projSprite = projectile->AddComponent<C_Sprite>();
    projSprite->Load(projectileTextureID);
    projSprite->SetDrawLayer(DrawLayer::Entities);
    projSprite->SetSortOrder(100);
    projSprite->SetTextureRect(textureDirectionBindings.at(faceDir));
 
    owner->context->objects->Add(projectile);
}
We set the projectiles position to equal that of the player’s position plus an offset that we retrieve from our unordered_map. If you run the game now you will notice that the projectiles spawn in different positions based on the player’s direction.
Now the arrows are being spawned in the correct(ish) position let’s move on to point number two: getting the projectiles moving. We’ll accomplish this by adding a previously written C_Velocity component (link) to the projectile object and providing the arrow with a velocity. However, the arrow’s velocity depends on the direction the player is facing. So once again we need a way of retrieving a value (the velocity) using the player’s direction. So as we have just done with the offset we will create an unordered_map called velocityDirectionBindings (last one I promise).
C_ProjectileAttack.hpp
class C_ProjectileAttack : public Component
{
…
    
private:
…
	static std::unordered_map<FacingDirection, sf::Vector2f, EnumClassHash> velocityDirectionBindings;
	float projectileVelocity;
};
It follows the same conventions as the last one we created: its static and you can remove the EnumClassHash if you are using a compiler that natively supports enums classes as a key. I’ve also added a projectileVelocity that will store the speed of the arrow regardless of the direction.
C_ProjectileAttack.cpp
std::unordered_map<FacingDirection, sf::Vector2f, EnumClassHash> 
C_ProjectileAttack::velocityDirectionBindings = {};

C_ProjectileAttack::C_ProjectileAttack(Object* owner) : 
Component(owner), 
projectileVelocity(400.f) // velocity defaults to 400
{}

We’ll populate the map in the Start function (again). Each direction will be stored along with the normalised velocity for that direction:

  • Up = 0, -1
  • Left = -1, 0
  • Down = 0, 1
  • Right = 1, 0
It will become clear how we will use these values shortly but for now add them to the map in the Start function.
C_ProjectileAttack.cpp
void C_ProjectileAttack::Start()
{
…

velocityDirectionBindings.emplace(FacingDirection::Up, 
								  sf::Vector2f(0.f, -1.f));
    velocityDirectionBindings.emplace(FacingDirection::Left, 
									  sf::Vector2f(-1.f, 0.f));
    velocityDirectionBindings.emplace(FacingDirection::Down, 
									  sf::Vector2f(0.f, 1.f));
    velocityDirectionBindings.emplace(FacingDirection::Right, 
									  sf::Vector2f(1.f, 0.f));
}
Add the C_velocity component to the projectile and set it’s velocity directly before the projectile is added to the object collection.
C_ProjectileAttack.cpp
void C_ProjectileAttack::SpawnProjectile()
{
    FacingDirection faceDir = direction->Get();
    
…
    
    auto velocity = projectile->AddComponent<C_Velocity>();
    velocity->Set(velocityDirectionBindings.at(faceDir) * projectileVelocity);
 
    owner->context->objects->Add(projectile);
}
By multiplying the normalised velocity retrieved from the unordered_map by the desired velocity our projectiles should go flying in the correct direction! You can test this for yourself by running the game and firing projectiles in different directions.
Projectile with Velocity!
Projectiles with velocity!

You’ll notice that the arrows do not collide with the environment and will fly off into the abyss. To fix this we’ll move onto task number three: enabling the projectiles to collide with the environment. Luckily we have already written a collision system that we can use by adding a C_BoxCollider component to each new arrow we spawn.

Before we add the component we need to add a new collision layer for the projectiles. Collision layers are used to control which objects collide. For example, we can add an object to the collision layer of ‘Projectile’ and set that to collide with another collision layer called ‘Tile’. Using layers we can minimise the number of collision checks we perform. For more information check out the previous tutorial where we wrote the layer system. To add a new collision layer we need to first add a new entry to the CollisionLayer enum in C_Collider.hpp

C_Collider.hpp
enum class CollisionLayer
{
    Default = 1,    // bit 0
    Player = 2,     // bit 1
    Tile = 3,       // bit 2
    Projectile = 4	// New collision layer
};

We assign the Projectile layer the number 4. For each layer, we create a bitmask (link) that sets bits at specific locations to determine whether a layer collides with another. For example, if we assign a layer a bitmask with the bits at position 1 and 3 set then we know that layer collides with the ‘Default’ and ‘Tile’ layer. For more information on why we assign the entries in the enum specific numbers have a look at the tutorial where we created the Collision system. We initialise the layer with it’s associated bitmask in the constructor of S_Collidable.

S_Collidable.cpp
S_Collidable::S_Collidable() : collisionTree(5, 5, 0, {0, 0, 4200, 1080}, nullptr)
{
…
    
    Bitmask projectileCollisions;
	// Projectiles will only collide with tiles for now.
    projectileCollisions.SetBit((int) CollisionLayer::Tile);
	collisionLayers.insert(std::make_pair(CollisionLayer::Projectile, projectileCollisions));
}
Setting the bit at the tile layer informs the collision system that the projectile layer should collide with the tile layer.
Now that the projectile layer has been initialised return to the C_ProjectileAttack component and add a collider to any projectiles spawned in SpawnProjectile.
C_ProjectileAttack.cpp
void C_ProjectileAttack::SpawnProjectile()
{
…

    auto collider = projectile->AddComponent<C_BoxCollider>();
    collider->SetSize(32, 32);
    collider->SetLayer(CollisionLayer::Projectile);
 
    owner->context->objects->Add(projectile);
}

After the C_BoxCollider is added to the projectile, the colliders size is set to 32 * 32 pixels, which is larger than the arrow image but will help show the collision using the debug drawing system. As the name suggests a C_BoxCollider uses a box to check for collisions. We’re using a box collider for the arrow because it is relatively quick to check a collision between two squares (i.e. a tile and the box around the projectile).

If you run the game now, you can shoot arrows and they will collide with tiles that also have a collider component attached.
Arrows colliding with the environment
Projectiles colliding with the environment
That’s it for this week, we achieved a lot: we now spawn the arrows in a position relative to the player’s direction, the arrows now move through the environment (useful if we are going to hit anything), and talking about hitting things, the arrows now collide with the environment! Granted they stick around when they should probably disappear but that’s something we’ll change soon (not next week but the week after). Next week we’ll change when we spawn the projectiles. Currently, they are spawned as soon as the projectile attack button is pressed, but they should be spawned at the correct moment in the animation so that when the player releases the arrow during its animation, a projectile is spawned and released.
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 🙂