Engine Dev - Something About CSC8503 Coursework
Video link:Languages: C++ (Custom Engine). Grade: 98%.
About
This module required me to create a small game in C++ using an engine build I had to build mostly from scratch. The game I ended up making was a third-person multiplayer game where players run around collecting crystals and bring them back to home base, whilst avoiding other players and the enemies within the level. I was required to implement the physics, enemy AI and a form of networking using the knowledge we gained from lectures and tutorials. Using the turoials I was able to set up:
- Collision: Detection and resolution between rays, Axis-Aligned Bounding Boxes and spheres.
- AI: State machines and finding algorithms.
- Networking: Basic server-client text communication.
We were also given same base graphics library provided in the previous model. A lot of elements that were crucial in making a propper engine were left out.
GitHub repo at: https://github.com/JunkyX1122/CSC8503-2023.
Majority of code that isn’t tutorial code can be found in the ‘CSC8503’ and ‘CSC8503CoreClasses’ folders.
Beyond Taught Material
Server-Client Network
Setup
For the networking, I utilised the enet library to establish connections between a server and clients. Within the start menu, you can chose to set up as a server or client.
void CourseworkGame::InitialiseGameAsServer()
{
...
levelDataBeingUsed = worldDatas_Level[levelID];
itemDataBeingUsed = worldDatas_Item[levelID];
NetworkBase::Initialise();
int port = NetworkBase::GetDefaultPort();
gameServer = new GameServer(port, MAX_CLIENTS,
[&](int peerId) { OnPlayerConnect(peerId); },
[&](int peerId) { OnPlayerDisconnect(peerId); });
gameServer->RegisterPacketHandler(Received_State, this);
...
}
void CourseworkGame::InitialiseGameAsClient()
{
...
NetworkBase::Initialise();
int port = NetworkBase::GetDefaultPort();
gameClient = new GameClient([&](int peerId) { });
gameClient->RegisterPacketHandler(Full_State, this);
gameClient->RegisterPacketHandler(Player_Info, this);
gameClient->RegisterPacketHandler(GlobalPlayer_Info, this);
gameClient->RegisterPacketHandler(Player_DrawLine, this);
gameClient->RegisterPacketHandler(Server_Information, this);
bool canConnect = gameClient->Connect(127, 0, 0, 1, port);
connected = canConnect;
clientConnectionTimer = SERVER_CONNECTION_TIMELIMIT;
...
}

The server is able to select the level the clients will be playing on, and the clients will load the appropriate level when connecting with the server.
if (gameState == GAME_WAITINGFORPLAYERS || gameState == GAME_NOTSTARTED)
{
GamePacket* newPacket = nullptr;
ServerInformation* sp = new ServerInformation();
sp->levelID = levelID;
newPacket = sp;
gameServer->SendGlobalPacket(*newPacket);
delete newPacket;
}
Should the server go down, take too long to send packets or the client attempt to join a non-existant server, the client will be sent back to the main menu.
void CourseworkGame::UpdateAsClient(float dt)
{
if (gameClient)
{
clientConnectionTimer -= dt;
world->ResetObjectNetworkUpdateList();
gameClient->UpdateClient();
if (clientConnectionTimer <= 0)
{
connected = false;
return;
}
...
}
void CourseworkGame::ReceivePacket(int type, GamePacket* payload, int source)
{
switch (type)
{
case(BasicNetworkMessages::Player_Info):
{
PlayerInfoPacket* infoPacket = (PlayerInfoPacket*)payload;
...
clientConnectionTimer = CONNECTION_TIMEOUT;
}
...
}

Updating
Both server and client load in the same world. The main difference between them being that the client performs no calculations, whilst the server cannot recieve manual inputs nor renders the gameworld (For demonstration purposes, the video linked in this page renders the scene). The server sends over:
- The current states of non-static objects.
- Scoring information - specific client's score and the current highest scoring player's score.
- In-game state information - timer.
- Player-specific information - dash timer, which player object they are.
The only information the client sends over on the other hand are the player’s inputs.
struct GlobalPlayerInfoPacket : GamePacket
{
int leader;
int leaderScore;
int playerIDs[MAX_CLIENTS];
PlayerState playerStates[MAX_CLIENTS];
GlobalPlayerInfoPacket()
{
type = GlobalPlayer_Info;
size = sizeof(GlobalPlayerInfoPacket);
}
};
void CourseworkGame::ClientSendInputs()
{
ClientPacket newPacket;
bool clientLastStateUpdate = false;
for (int i = 0; i < 8; i++)
{
newPacket.buttonstates[i] = '0';
}
if (Window::GetKeyboard()->KeyPressed(KeyCodes::SPACE)) {
newPacket.buttonstates[0] = '1';
clientLastStateUpdate = true;
}
else if (Window::GetKeyboard()->KeyDown(KeyCodes::SPACE))
{
newPacket.buttonstates[0] = '2';
clientLastStateUpdate = true;
}
...
}
Capsule Collision
Capsule collision between other capsules or other volume types mainly consisted of the same logic used for sphere collisions. Given that a capsule consists of an inner length and a radius, I was able to detect capsule collisions by finding the closest point between the capsule’s inner line and other lines/points.
void CollisionDetection::ClosestPoints_TwoLines(
float* ratio1, float* ratio2,
Vector3 firstLineStart, Vector3 firstLineEnd,
Vector3 secondLineStart, Vector3 secondLineEnd)
{
Vector3 firstLineVector = firstLineEnd - firstLineStart;
Vector3 secondLineVector = secondLineEnd - secondLineStart;
Vector3 SMinusF = secondLineStart - firstLineStart;
float dotSS = Vector3::Dot(secondLineVector, secondLineVector);
float dotFF = Vector3::Dot(firstLineVector, firstLineVector);
float dotSF = Vector3::Dot(secondLineVector, firstLineVector);
float dotSMF_F = Vector3::Dot(SMinusF, firstLineVector);
float dotSMF_S = Vector3::Dot(SMinusF, secondLineVector);
float dotSF2 = dotSF * dotSF;
float dotSSdotFF = dotSS * dotFF;
float denom = (dotSF2 - dotSSdotFF);
if (denom == 0)
{
*ratio1 = 0.0f;
*ratio2 = (dotFF * (*ratio1) - dotSMF_F) / dotSF;
}
else
{
*ratio1 = (dotSMF_S * dotSF - dotSS * dotSMF_F) / denom;
*ratio2 = (-dotSMF_F * dotSF + dotFF * dotSMF_S) / denom;
}
*ratio1 = std::clamp((*ratio1), 0.0f, 1.0f);
*ratio2 = std::clamp((*ratio2), 0.0f, 1.0f);
}
void CollisionDetection::ClosestPoints_PointLine(
float* lineRatio, Vector3 point, Vector3 lineStart, Vector3 lineEnd)
{
Vector3 heading = (lineEnd - lineStart);
float magnitudeMax = heading.Length();
heading.Normalise();
Vector3 lhs = point - lineStart;
float dotP = Vector3::Dot(lhs, heading) / magnitudeMax;
*lineRatio = std::clamp((dotP), 0.0f, 1.0f);
}

Enemy AI
For the enemy AI, they currently have a state where they wander the map (move to a random location) and a state where they chase the player. Which state they are in depends on if they can see the player or not. To achieve this, a ray in the direction to each active player is cast. What was important was that this ray needed to ignore certain objects so I assigned each object created in the game world a specific layer identification. Furthermore, since an enemy can only chase one thing at a time, a check to see which visible player is closest is done before assigning that enemy a target.
vector<GameObject*> visiblePlayers;
for (auto pO : playerObjects)
{
PlayerObject* objectAsPlayer = (PlayerObject*)pO;
if (!objectAsPlayer->IsAssigned()) continue;
Ray ray(this->GetTransform().GetPosition(),
(pO->GetTransform().GetPosition() -
this->GetTransform().GetPosition()).Normalised());
RayCollision closestCollision;
std::vector<int> ignoreList = { LAYER_ENEMY, LAYER_ITEM , LAYER_TRIGGER };
if (gameWorld->Raycast(ray, closestCollision, true, nullptr, ignoreList))
{
GameObject* selectionObject = (GameObject*)closestCollision.node;
if (selectionObject->GetBoundingVolume()->collisionLayer == LAYER_PLAYER)
{
visiblePlayers.push_back(pO);
}
}
}
float range = 10000.0f;
for (auto vPO : visiblePlayers)
{
float rangeFromVPO = (this->GetTransform().GetPosition()
- vPO->GetTransform().GetPosition()).Length();
if (rangeFromVPO < range)
{
this->SetObjectTarget(vPO);
range = rangeFromVPO;
}
}
In both states, the enemy uses the A* algorithm to find their way to their destination. To move along this path, they just apply a force in the direction of the next node in the graph.
void EnemyObject::MoveAlongPath(float dt)
{
Vector3 direction = (this->GetNextPathNode() -
this->GetTransform().GetPosition()).Normalised();
this->GetPhysicsObject()->AddForce(direction * this->GetMoveSpeed() * dt);
}

Grapple Hook
It’s a grappling hook.
if (playerInputs[playerID][MOUSE_RIGHT] == IS_UP)
{
pO->ResetGrappling();
}
else if (playerInputs[playerID][MOUSE_RIGHT] == IS_DOWN && !pO->IsGrappling())
{
Ray ray = CollisionDetection::BuildRayFromCentre(world->GetMainCamera());
RayCollision closestCollision;
std::vector<int> ignoreList = { LAYER_PLAYER, LAYER_ITEM, LAYER_TRIGGER };
if (world->Raycast(ray, closestCollision, true, nullptr, ignoreList))
{
if (!pO->IsGrappling())
{
pO->SetGrapplePoint(closestCollision.collidedAt);
pO->SetGrappling(true);
}
}
else
{
pO->ResetGrappling();
}
}
if (pO->IsGrappling())
{
float grappleForce = 40.0f;
Vector3 direction = (pO->GetGrapplePoint() -
pO->GetTransform().GetPosition()).Normalised();
pO->GetPhysicsObject()->AddForce(direction * grappleForce * dt);
}
