This week we are going to add a friend for the player. We’ll refractor the player creation code to reuse as much of it as possible and we’ll also create a new collision layer for the character and setup collisions between the player and his new friend.

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 are going to add a friend for the player. We’ll refractor the player creation code to reuse as much of it as possible and we’ll also create a new collision layer for the character and setup collisions between the player and his new friend.In the resources folder, you’ll find a new sprite sheet for the character called Skeleton.png.

The layout is the same as the player’s sprite sheet with the walking sprites on lines 9 to 12, melee attack on lines 13 to 16, and projectile attack on lines 17 to 20. Each line has the animation for a separate direction. Unlike the player, the skeleton has a dagger and no bow. Using the same layout will allow us to use the same code to initialise the animations for both characters.Before we write the code to add the skeleton to the game, we’ll move player creation to it’s own function:

SceneGame.hpp
class SceneGame : public Scene
{


private:
void CreatePlayer();


};

Cut and paste all the code associated with player creation from the OnCreate function into the new CreatePlayer function.

SceneGame.cpp
void SceneGame::CreatePlayer()
{
std::shared_ptr<Object> player = std::make_shared<Object>(&context);

player->transform->SetPosition(100, 700);

auto sprite = player->AddComponent<C_Sprite>();
sprite->SetDrawLayer(DrawLayer::Entities);

player->AddComponent<C_KeyboardMovement>();

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 *
*******************/
const bool idleAnimationLooped = false;

unsigned int idleYFramePos = 512;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> 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, idleAnimationLooped);

idleAnimations.insert(std::make_pair(directions[i], idleAnimation));

idleYFramePos += frameHeight;
}

animation->AddAnimation(AnimationState::Idle, idleAnimations);


/**********************
* Walking Animations *
**********************/
const bool walkAnimationLooped = true;
const int walkingFrameCount = 9;
const float delayBetweenWalkingFramesSecs = 0.1f;

unsigned int walkingYFramePos = 512;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> 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, walkAnimationLooped);
}

walkingAnimations.insert(std::make_pair(directions[i], walkingAnimation));

walkingYFramePos += frameHeight;
}

animation->AddAnimation(AnimationState::Walk, walkingAnimations);


/*************************
* Projectile Animations *
*************************/
const bool projectileAnimationLooped = true;
const int projectileFrameCount = 10;
const float delayBetweenProjectileFramesSecs = 0.08f;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> 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, projectileAnimationLooped);
}
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);

player->AddComponent<C_Camera>();
player->AddComponent<C_ProjectileAttack>();
player->AddComponent<C_Velocity>();
player->AddComponent<C_MovementAnimation>();
player->AddComponent<C_Direction>();

objects.Add(player);
}

Call the function in OnCreate. So with the player creation code removed, OnCreate now looks like this:

SceneGame.cpp
void SceneGame::OnCreate()
{
context.input = &input;
context.objects = &objects;
context.workingDir = &workingDir;
context.textureAllocator = &textureAllocator;
context.window = &window;

// Call our new create player function.
CreatePlayer();

sf::Vector2i mapOffset(0, 180);
std::vector<std::shared_ptr<Object>> levelTiles = mapParser.Parse(workingDir.Get() + "House Exterior.tmx", mapOffset);

objects.Add(levelTiles);
}

Add a new function to create the skeleton.

SceneGame.cpp
class SceneGame : public Scene
{


private:
void CreateFriend();


};

This will contain all of the code we use to instantiate the new character. Writing a separate function for each character is not sustainable for anything but the smallest game but it will do for now. We’ll look at different ways of doing this in future as we scale the game.

SceneGame.cpp
void SceneGame::CreateFriend()
{
std::shared_ptr<Object> npc = std::make_shared<Object>(&context);

npc->transform->SetPosition(160, 700);

auto sprite = npc->AddComponent<C_Sprite>();
sprite->SetDrawLayer(DrawLayer::Entities);

auto animation = npc->AddComponent<C_Animation>();

const int textureID = textureAllocator.Add(workingDir.Get() + "Skeleton.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 *
*******************/
const bool idleAnimationLooped = false;

unsigned int idleYFramePos = 512;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> idleAnimations;

for (int i = 0; i < 4; i++)
{
std::shared_ptr<Animation> idleAnimation = std::make_shared<Animation>();

idleAnimation->AddFrame(textureID, 0, idleYFramePos, frameWidth, frameHeight, 0.f, idleAnimationLooped);

idleAnimations.insert(std::make_pair(directions[i], idleAnimation));

idleYFramePos += frameHeight;
}

animation->AddAnimation(AnimationState::Idle, idleAnimations);


/**********************
* Walking Animations *
**********************/
const bool walkAnimationLooped = true;
const int walkingFrameCount = 9;
const float delayBetweenWalkingFramesSecs = 0.1f;

unsigned int walkingYFramePos = 512;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> 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(textureID, i * frameWidth, walkingYFramePos, frameWidth, frameHeight, delayBetweenWalkingFramesSecs, walkAnimationLooped);
}

walkingAnimations.insert(std::make_pair(directions[i], walkingAnimation));

walkingYFramePos += frameHeight;
}

animation->AddAnimation(AnimationState::Walk, walkingAnimations);


/*************************
* Projectile Animations *
*************************/
const bool projectileAnimationLooped = true;
const int projectileFrameCount = 10;
const float delayBetweenProjectileFramesSecs = 0.08f;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> 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(textureID, i * frameWidth, projFrameYPos, frameWidth, frameHeight, delayBetweenProjectileFramesSecs, projectileAnimationLooped);
}
projectileAnimations.insert(std::make_pair(directions[i], projAnimation));

projFrameYPos += frameHeight;
}

animation->AddAnimation(AnimationState::Projectile, projectileAnimations);

auto collider = npc->AddComponent<C_BoxCollider>();
collider->SetSize(frameWidth * 0.4f, frameHeight * 0.5f);
collider->SetOffset(0.f, 14.f);
collider->SetLayer(CollisionLayer::Player);

npc->AddComponent<C_Velocity>();
npc->AddComponent<C_MovementAnimation>();
npc->AddComponent<C_Direction>();

objects.Add(npc);
}

You may notice that much of this code is similar to the code we use to create the player, we’ll deal with that shortly. For now, call the CreateFriend function in OnCreate straight after we call the CreatePlayer function.

SceneGame.cppI’ve also commented out the lines where we create the level so we can focus on the new player.
Player standing with his new friend.
Player standing with his new friend.

We don’t add as many components to the friend but it is very similar to that of CreatePlayer. In fact, the majority of each function is responsible for building animations and this is the same in both so we can refractor animation construction into its own function.

SceneGame.hpp
class SceneGame : public Scene
{


private:
void AddAnimationComponent(std::shared_ptr<Object> object, const int textureID);


};

This function receives a shared pointer to the object and an id of the sprite sheet. Using this it will build the idle, walking, and projectile animations for the object.

SceneGame.cpp
void SceneGame::AddAnimationComponent(std::shared_ptr<Object> object, const int textureID)
{
auto animation = object->AddComponent<C_Animation>();

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

const FacingDirection directions[4] = {FacingDirection::Up, FacingDirection::Left, FacingDirection::Down, FacingDirection::Right};

/*******************
* Idle Animations *
*******************/
const bool idleAnimationLooped = false;

unsigned int idleYFramePos = 512;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> idleAnimations;

for (int i = 0; i < 4; i++)
{
std::shared_ptr<Animation> idleAnimation = std::make_shared<Animation>();

idleAnimation->AddFrame(textureID, 0, idleYFramePos, frameWidth, frameHeight, 0.f, idleAnimationLooped);

idleAnimations.insert(std::make_pair(directions[i], idleAnimation));

idleYFramePos += frameHeight;
}

animation->AddAnimation(AnimationState::Idle, idleAnimations);


/**********************
* Walking Animations *
**********************/
const bool walkAnimationLooped = true;
const int walkingFrameCount = 9;
const float delayBetweenWalkingFramesSecs = 0.1f;

unsigned int walkingYFramePos = 512;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> 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(textureID, i * frameWidth, walkingYFramePos, frameWidth, frameHeight, delayBetweenWalkingFramesSecs, walkAnimationLooped);
}

walkingAnimations.insert(std::make_pair(directions[i], walkingAnimation));

walkingYFramePos += frameHeight;
}

animation->AddAnimation(AnimationState::Walk, walkingAnimations);


/*************************
* Projectile Animations *
*************************/
const bool projectileAnimationLooped = true;
const int projectileFrameCount = 10;
const float delayBetweenProjectileFramesSecs = 0.08f;

std::unordered_map<FacingDirection, std::shared_ptr<Animation>, EnumClassHash> 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(textureID, i * frameWidth, projFrameYPos, frameWidth, frameHeight, delayBetweenProjectileFramesSecs, projectileAnimationLooped);
}
projectileAnimations.insert(std::make_pair(directions[i], projAnimation));

projFrameYPos += frameHeight;
}

animation->AddAnimation(AnimationState::Projectile, projectileAnimations);
}

Now we can remove the code that creates the animation component in CreatePlayer and CreateFriend and instead call AddAnimationComponent.

SceneGame.cpp
void SceneGame::CreatePlayer()
{
std::shared_ptr<Object> player = std::make_shared<Object>(&context);

player->transform->SetPosition(100, 700);

auto sprite = player->AddComponent<C_Sprite>();
sprite->SetDrawLayer(DrawLayer::Entities);

player->AddComponent<C_KeyboardMovement>();

int textureID = textureAllocator.Add(workingDir.Get() + "Player.png");

AddAnimationComponent(player, textureID);

auto collider = player->AddComponent<C_BoxCollider>();
collider->SetSize(64 * 0.4f, 64 * 0.5f);
collider->SetOffset(0.f, 14.f);
collider->SetLayer(CollisionLayer::Player);

player->AddComponent<C_Camera>();
player->AddComponent<C_ProjectileAttack>();
player->AddComponent<C_Velocity>();
player->AddComponent<C_MovementAnimation>();
player->AddComponent<C_Direction>();

objects.Add(player);
}

void SceneGame::CreateFriend()
{
std::shared_ptr<Object> npc = std::make_shared<Object>(&context);

npc->transform->SetPosition(160, 700);

auto sprite = npc->AddComponent<C_Sprite>();
sprite->SetDrawLayer(DrawLayer::Entities);

const int textureID = textureAllocator.Add(workingDir.Get() + "Skeleton.png");

AddAnimationComponent(npc, textureID);

auto collider = npc->AddComponent<C_BoxCollider>();
collider->SetSize(64 * 0.4f, 64 * 0.5f);
collider->SetOffset(0.f, 14.f);
collider->SetLayer(CollisionLayer::Player);

npc->AddComponent<C_Velocity>();
npc->AddComponent<C_MovementAnimation>();
npc->AddComponent<C_Direction>();

objects.Add(npc);
}

This reduces the size of both functions considerably. We could continue to separate common code however as these functions are just temporary I don’t want to spend too much time refactoring.

Now if you run the game you may have noticed that the player can walk right through his friend. The friend has a box collider but its collision layer is currently set to ‘Player’. The player layer doesn’t collide with itself, so let’s create a new collision layer for our friend. First, add a new entry to the CollisionLayer enum for the layer.

C_Collider.hpp
enum class CollisionLayer
{
Default = 1, // bit 0
Player = 2, // bit 1
Tile = 3, // bit 2
Projectile = 4,
NPC = 5 // Add new entry for the skeleton
};

And then set the friends collider to that layer.

SceneGame.hpp
void SceneGame::CreateFriend()
{


collider->SetLayer(CollisionLayer::NPC);


}

When we create a new layer we need to define if it collides with other layers in S_Collidables constructor. At the moment we want the player layer to collide with the NPC layer.

SceneGame.hpp
S_Collidable::S_Collidable() : collisionTree(5, 5, 0, {0, 0, 4200, 1080}, nullptr)
{


Bitmask playerCollisions;
playerCollisions.SetBit((int) CollisionLayer::Default);
playerCollisions.SetBit((int) CollisionLayer::Tile);

// Sets player layer to collide with npc layer
playerCollisions.SetBit((int) CollisionLayer::NPC);


}

Now the player layer should collide with the default, tile, and npc layers. We can test the collisions by running the game and walking into our skeleton friend.
Player colliding with friend.
Player colliding with his friend.

Over the next few weeks, we’ll work on creating interactions between the player and his new friend. We’ll write code for raycasting and displaying text on the screen so that our friend can say hi.

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 🙂