Wombat FPS Engine is a custom game engine specifically designed for first person shooters. The engine can load in levels from the Quake level editor "TrenchBroom"
The engine runs on both windows and PS5
I worked on this project in my second year of CMGT at Breda University of applied sciences. The project had 7 team members, of which 2 Gameplay Programmers.
(I am showing some source code here, as the git repo is private)
This was my first time working on a custom engine. It taught me a lot about engine architecture, engine programming and optimization.
Learned about tooling and API creation
Improved my understanding of physics in games
Learned more about how to program a good feeling player controller.
Improved C++ knowledge in general
I implemented physics into our project using the library "bullet".
If you want an object to have collision, or physics simulated, you can add the RigidBody component to it. Via the RigidBody, the object is automatically added to the physics simulation and you can specify what kind of collider they should have. The engine had support for box, sphere, capsule and convex colliders.
The RigidBody component has the important things that you would expect on a component like this. Applying impulse, setting the gravity, setting the friction etc.
I also implemented ray casting. This is essential in a shooter. Using the bullet rayTest, I store all the information I need from the casted ray. We can then use this information for gameplay, like hurting enemies.
Source Code
std::vector<RaycastResult> CastRay(const btVector3& startPoint, const btVector3& endPoint, int32_t collisionMask, const std::string& excludedId)
{
// Setup callback
AllHitsRayResultsCallback callback = AllHitsRayResultsCallback(startPoint, endPoint);
callback.m_collisionFilterMask = collisionMask;
// Preform ray cast
m_physicsWorld->rayTest(startPoint, endPoint, callback);
std::vector<RaycastResult> results = std::vector<RaycastResult>();
results.reserve(callback.m_collisionObjects.size());
// Fill results list
for (int i = 0; i < callback.m_collisionObjects.size(); i++)
{
const btRigidBody* rigidBody = btRigidBody::upcast(callback.m_collisionObjects[i]);
OwnerInfo ownerInfo = *static_cast<OwnerInfo*>(rigidBody->getUserPointer());
if (ownerInfo.m_ownerId != excludedId)
{
glm::vec3 hitNormal = PhysicsManager::ConvertToGlmVector3(callback.m_hitNormalWorld[i]);
glm::vec3 hitPoint = PhysicsManager::ConvertToGlmVector3(callback.m_hitPointWorld[i]);
results.emplace_back(RaycastResult(hitNormal, hitPoint, ownerInfo.m_ownerId, ownerInfo.m_collisionGroup));
}
}
return results;
}
I created a gun base class that has overridable functions, allowing potential designers to easily create their own guns within the engine. You can make the gun shoot however you like by changing the base values and overriding the functions.
As seen in the video at the top of this page, I created a shotgun and an assault rifle using this base class. The shotgun shooting implementation can be seen on the screenshot below. The shoot function is virtual.
Source Code
void ShotgunComponent::Shoot()
{
glm::vec3 position = m_cameraTransform->GetPosition();
glm::vec3 forward = m_cameraTransform->GetForwardVector();
for (int i = 0; i < GetBulletsPerShot(); i++)
{
glm::vec3 direction = glm::vec3(forward.x + Utility::GenerateRandomFloat(-GetBulletSpread(), GetBulletSpread()),
forward.y + Utility::GenerateRandomFloat(-GetBulletSpread(), GetBulletSpread()),
forward.z + Utility::GenerateRandomFloat(-GetBulletSpread(), GetBulletSpread()));
std::vector<RaycastResult> results;
results = Physics::PhysicsManager::PerformRaycast(position, position + direction * 10000.f, "PlayerController", 2.f, glm::vec3(1.f, 0.f, 1.f));
for (int j = 0; j < results.size(); j++)
{
GameObject* hit = LevelManager::GetGameObjectById(results[j].m_gameObjectId);
if (hit != nullptr)
if (hit->UpcastToCharacter())
hit->OnRayCastHit(GetDamage());
}
}
}
The player controller I worked on could handle slopes, steps and bobs your camera. This code could have used some cleaning looking back. For example I could have made the camera object a variable instead of typing out the getters constantly. This would make the code much more readable. I am aware there are also some additional micro optimization opportunities here, for example checking if the move direction vector length != 0, instead of checking each individual axis.
Source Code
void PlayerController::Move(float deltaTime)
{
glm::vec3 forward;
glm::vec3 moveDir;
float angle = 0;
angle = CheckSlopeAngle();
HandleSlopes(angle);
forward = glm::normalize(glm::vec3(m_player->m_cameraObject.GetLocalTransform().GetForwardVector().x, m_player->m_cameraObject.GetLocalTransform().GetForwardVector().y, 0.f));
//Create the move direction
moveDir = (m_moveAxis.x * m_player->m_cameraObject.GetLocalTransform().GetRightVector()) + (m_moveAxis.y * forward) + (m_moveAxis.z * m_player->m_cameraObject.GetLocalTransform().GetUpVector());
if (moveDir.x != 0.f || moveDir.y != 0.f || moveDir.z != 0.f)
{
if (angle)
{
const glm::vec3& surfaceNormal = m_GroundHitData[m_firstHitIndex].m_hitNormal;
moveDir = glm::cross(surfaceNormal ,glm::cross(moveDir, surfaceNormal));
}
moveDir = glm::normalize(moveDir);
moveDir = moveDir * m_movementSpeed * m_movementSpeedMultiplier;
//Check for stairs or other objects to step on and over
HandleSteps(moveDir);
BobCamera(deltaTime);
}
else
{
t = 0;
m_player->GetCameraObject()->GetLocalTransform().SetPosition(Vec3Lerp(m_player->GetCameraObject()->GetLocalTransform().GetPosition(), glm::vec3(0.f, 0.f, 20.f), deltaTime * 10.f));
}
if (angle && !m_isJumping)
m_rigidBody->SetVelocity(glm::vec3(moveDir.x, moveDir.y, moveDir.z));
else
m_rigidBody->SetVelocity(glm::vec3(moveDir.x, moveDir.y, m_rigidBody->GetVelocity().z));
}