Roguelite Custom Engine Game (2023)

From Scratch C++  -  7 developers -  8 weeks development time  -  Gameplay Programmer - PC/PS5

In this project, we were tasked with developing a game using our own custom engine. I joined this team that was already developing the custom engine as a gameplay developer to develop the game along side them. This made me have to adapt to a new code base that I hadn't used before in a short amount of time but I quickly got to work without much delay.


This engine used an Entity-Component System which was another thing I had to learn to use in this project but after a couple of hours I got used to and actually enjoyed working with the ECS.


My most proud contributions were the collision system and the enemy AI. For the enemy AI, I used a state machine to create simple Chase, Wander, and Shoot states which the enemy would switch between depending on the environment and the player's behaviour. Each state had an Enter, Execute, and Exit function to be able to add functionality even when transitioning between states.


This is an example of the Chase state behaviour:


void Chase::Enter(EnemyComponent* enemy)

{

    enemyComponent = enemy;

    enemyEntity = entt::to_entity(Perry::GetRegistry(), *enemy);

    enemyTransform = &Perry::GetRegistry().get<Perry::TransformComponent>(enemyEntity);

    if (!Perry::IsComponentLoaded<PlayerComponent>()) return;

    auto&& [playerComponent, a_playerTransform, a_playerCollider] = Perry::GetSingletonComponent<PlayerComponent, Perry::TransformComponent, Perry::CollisionComponent>();

    playerTransform = &a_playerTransform;

    playerCollider = &a_playerCollider;

}


void Chase::Execute()

{

    // If the enemy has reached its selected position, if player is inside the room shoot, and then pick a new position and reset timer

    if (!enemyComponent->m_ArrivedAtPosition)

    {

        if (PlayerInsideRoom(*enemyComponent, *playerTransform))

        {

            bool los = true;

            auto mapCollisions = Perry::GetRegistry().group<Perry::CollisionComponent>(entt::get<Perry::TransformComponent>, entt::exclude<Perry::SpriteComponent, Perry::MeshComponent>);

            // Check if the random position is valid (i.e., not inside a wall or too close to the player)

            for (auto&& [entity, mapCollisionComponent, mapCollisionTransform] : mapCollisions.each())

            {

                los = !LineBoxCollision(enemyTransform->m_Position, playerTransform->m_Position, glm::vec2(mapCollisionTransform.m_Position.x, mapCollisionTransform.m_Position.y) + mapCollisionComponent.GetSize(), 

glm::vec2(mapCollisionTransform.m_Position.x, mapCollisionTransform.m_Position.y) - mapCollisionComponent.GetSize());

            }


            if (los)

            {

                Perry::GetRegistry().get<Perry::SpriteComponent>(enemyEntity).SetAnimationStride(4, 11, 2.0f);

                Move(*enemyComponent, *enemyTransform);

            }

            else

            {

                enemyComponent->ChangeState(Wander::Instance());

            }

        }

    }

    // Otherwise, move enemy

    else

    {

        enemyComponent->ChangeState(Shoot::Instance());

        PickChasePosition(*enemyComponent, *enemyTransform, *playerTransform);

    }

}


void Chase::Exit() {}


The enemy checks if it has line of sight to the player and tries to follow them and shoot. If they lose line of sight, they will go to the Wander state until they regain line of sight. The enemies also make sure they never leave the map or go through a wall when moving or chasing the player.