My Netcode System
- Roberto Reynoso
- Nov 25, 2024
- 10 min read
Updated: May 26
Download Netcode System:
Network System Demo:
See the Game I made with this System: https://www.reynosorobertov.com/post/online-multiplayer-racing-game
How to Incorporate my Netcode System into your Game Engine and Set up MyGame with it
(Note: My system uses winsock2 from Windows)
1. Download My Netcode Project and make sure to put it under the Filter “Engine” within Your Project solution.

2. Make sure that the needed references of the Netcode Project are added, which include “Math” and “Physics”.

3. Make sure that the Property Pages are Set Up Correctly for the Netcode Project. Within the Property Manager make sure that “Win32” Includes “OpenGL” and “EngineDefaults” for both the Debug and release versions. “x64” must include “Direct3D” and “EngineDefaults” for both the Debug and release versions.

4. The NetcodeManager within the Netcode project is implemented as a Singleton, making it easily accessible from the MyGame project. By reviewing the NetcodeManager.h file, you’ll find key variables that are essential for determining which client owns a specific game object in your game. Additionally, the Serialized Packet struct plays a crucial role, as it is used to send various types of data across the network efficiently. Lastly, within the NetcodeManager.h file, is where you will set the Server/Host’s port, by editing the #define DEFAULT_PORT.

5. The implementation of your game objects is entirely up to you! This system offers great flexibility, enabling you to design and implement game objects in a way that best fits your project’s requirements. For instance, I created a GameObjectNetcode class that inherits from both GameObjectEntity and GameObjectBase. This netcode game object includes an m_ClientID to identify which client owns each object and a targetPosition to ensure game objects are correctly resynchronized with the host. This setup makes it easier to manage client ownership and maintain synchronization across the network.


6. To integrate MyGame with the Netcode System, four components are required within MyGame. The first is implementing a function, which I named Initialize Console. This function enables the use of a console during the game’s runtime, allowing the host to create a lobby and the clients to connect to the specific port hosted by the server.
#ifdef _WIN32
#include <Windows.h>
#include <iostream>
void InitializeConsole()
{
AllocConsole();
FILE* fp;
freopen_s(&fp, "CONOUT$", "w", stdout);
freopen_s(&fp, "CONOUT$", "w", stderr);
freopen_s(&fp, "CONIN$", "r", stdin); // Add this line to redirect `std::cin`
std::ios::sync_with_stdio(); // Ensures standard I/O synchronization
}
#endif
Second, we need to implement a function to process incoming network data and apply it to the game objects within the game world. I named this function On Netcode Update Received. This function ensures that the state of game objects is updated correctly based on the data received over the network, maintaining synchronization across all clients. This is how I implemented the function.
// Netcode Needed Function
void eae6320::cMyGame::OnNetcodeUpdateReceived(int clientID, int dataType, const char* data, size_t dataSize)
{
int localClientID = NetcodeManager::Instance().GetLocalClientID();
int hostID = NetcodeManager::Instance().GetHostID();
switch (dataType)
{
// Movement
case 1:
{
Math::sVector velocity;
memcpy(&velocity, data, sizeof(Math::sVector));
std::cout << "This is the Client received Velocity: " << velocity.x << velocity.y << velocity.z << std::endl;
// Find and update the corresponding player object
for (auto player : netcodePlayerObjects)
{
if (player->m_ClientID == clientID)
{
std::cout << "This is the Client " << clientID << "Set received Velocity : " << velocity.x << velocity.y << velocity.z << std::endl;
player->SetRigidBodyVelocity(velocity);
}
}
break;
}
// Position
case 2:
{
Math::sVector receivedPosition;
memcpy(&receivedPosition, data, sizeof(Math::sVector));
std::cout << "This is the received Position: " << receivedPosition.x << receivedPosition.y << receivedPosition.z << std::endl;
// Host-specific validation for client positions
if (localClientID == hostID && clientID != hostID)
{
for (auto player : netcodePlayerObjects)
{
if (player->m_ClientID == clientID)
{
float deviation = (player->rigidBody.position - receivedPosition).GetLength();
const float syncThreshold = 0.001f;
// If deviation exceeds threshold, resynchronize the client
if (deviation > syncThreshold)
{
std::cout << "Resynchronizing client " << clientID << " to host position." << std::endl;
NetcodeManager::Instance().BroadcastPacket(clientID, 2, &player->rigidBody.position, sizeof(Math::sVector));
player->targetPosition = receivedPosition;
}
else
{
std::cout << "Host accepting client position update for minor deviation." << std::endl;
player->SetRigidBodyPosition(receivedPosition);
player->targetPosition = receivedPosition;
}
break;
}
}
}
// Non-hosts update their positions based on data received from the host
if (localClientID != hostID)
{
for (auto player : netcodePlayerObjects)
{
if (player->m_ClientID == clientID)
{
std::cout << "Setting Client Object Client side. " << clientID << std::endl;
player->SetRigidBodyPosition(receivedPosition);
player->targetPosition = receivedPosition;
break;
}
}
}
break;
}
}
}
Third, we need to register a callback for the On Netcode Update Received function we created. This allows the function to be automatically triggered whenever data is received over the network, ensuring the updates are promptly applied to the game world for synchronization.
// Register to callback for data reception
NetcodeManager::Instance().RegisterDataReceivedCallback(
[this](int clientID, int dataType, const char* data, size_t dataSize) {
this->OnNetcodeUpdateReceived(clientID, dataType, data, dataSize);
});
Lastly, you should implement a container that contains all of the netcode player objects and maybe another container that has all of the playerIDs within in your cMyGame.h, which can be something like this.
std::vector<Physics::GameObjectNetcode*> netcodePlayerObjects;
std::vector<int> playerIDs
Side note, this is how I initialize my player objects in my game world, so that they have the correct ownership ID, which is the client ID.
// Helper function to initialize player object
void eae6320::cMyGame::InitializePlayerObject(Physics::GameObjectNetcode*& playerObject, const Math::sVector& initialPosition, int ownerID)
{
playerObject = new Physics::GameObjectNetcode();
playerObject->rigidBody.position = initialPosition;
playerObject->m_ClientID = ownerID;
playerObject->graphicsData = new Graphics::cGraphicsData();
Graphics::cGraphicsData* graphicData = new Graphics::cGraphicsData();
if (ownerID == 0)
{
graphicData->SetUpShaders("data/Shaders/Vertex/standard.shader", "data/Shaders/Fragment/test3.shader");
playerObject->rigidBody.position.x += 5;
}
else
{
graphicData->SetUpShaders("data/Shaders/Vertex/standard.shader", "data/Shaders/Fragment/test2.shader");
}
ExtractBinaryData("data/Meshes/sphere.lua", graphicData);
playerObject->graphicsData->AddGraphicsData(graphicData);
allEntityGraphicObjects.push_back(playerObject->graphicsData);
netcodePlayerObjects.push_back(playerObject);
}
How I implemented MyGame in my project
I’d like to provide a detailed explanation of how I implemented the MyGame project to work with my network system. This guide is designed to help anyone using my system, so that they can integrate it into their game more easily and efficiently.
In the Initialize function, we first call InitializeConsole to enable console input, which allows us to easily set up a game lobby for online multiplayer. Through the console, the user specifies whether they are hosting the game as a server or connecting as a client.
Server Setup
For the server, we call StartServer with the DEFAULT_PORT as the argument. The server then waits for clients to join the game and gives the host control over when to start the game or wait for additional clients. Once the game starts, no more clients can join.
Client Setup
On the client side, the user enters the server’s IP address to connect to the host’s game. This triggers a call to StartClient, which establishes the connection to the server. After connecting, the client calls WaitForStartSignal, ensuring it waits until the host officially starts the game.
Network Synchronization
Next, we register the callback function with the OnNetcodeUpdateReceived function. This step is essential for interaction correctly with the network system, allowing us to process and apply incoming data to the game world.
Player ID Synchronization
When the game is ready to begin, we call RequestConnectedPlayerIDs. This function retrieves the IDs of all connected players, including the host, enabling the creation of all necessary netcode objects on each client’s machines.
Final Setup
Finally, we configure any game components that do not rely on the network system, completing the initialization process.
eae6320::cResult eae6320::cMyGame::Initialize()
{
// Initialize a console for the application type
InitializeConsole();
// Ask if the user is host or client
char choice;
std::cout << "Start as server (s) or client (c)? ";
std::cin >> choice;
int localClientID = -1;
// Host initialization
if (choice == 's')
{
// Start server
if (!NetcodeManager::Instance().StartServer(DEFAULT_PORT))
{
std::cerr << "Failed to start server.\n";
return eae6320::Results::Failure;
}
// Get localClientID for the host
localClientID = NetcodeManager::Instance().GetLocalClientID();
std::cout << "Here is the Host Local Id: " << localClientID << std::endl;
// Initialize the host’s player object and add it to playerObjects on the host
Physics::GameObjectNetcode* hostPlayerObject;
InitializePlayerObject(hostPlayerObject, Math::sVector(0, 0, 0), localClientID);
isGameReady = true;
}
// Client initialization
else if (choice == 'c')
{
// Connect to server
std::string serverIP;
std::cout << "Enter server IP address: ";
std::cin >> serverIP;
if (!NetcodeManager::Instance().StartClient(serverIP.c_str(), DEFAULT_PORT))
{
std::cerr << "Failed to connect to server.\n";
return eae6320::Results::Failure;
}
localClientID = NetcodeManager::Instance().GetLocalClientID();
std::cout << "Here is the Local Client ID: " << localClientID << std::endl;
// Initialize this client’s player object
Physics::GameObjectNetcode* clientPlayerObject;
InitializePlayerObject(clientPlayerObject, Math::sVector(0, 0, 0), localClientID);
// Wait for the start signal from the server before proceeding
NetcodeManager::Instance().WaitForStartSignal();
isGameReady = true; // Only set after successfully receiving the start signal
}
// Register to callback for data reception
NetcodeManager::Instance().RegisterDataReceivedCallback(
[this](int clientID, int dataType, const char* data, size_t dataSize) {
this->OnNetcodeUpdateReceived(clientID, dataType, data, dataSize);
});
// Return if game is not ready yet
if (!isGameReady)
{
return eae6320::Results::Success;
}
// Request a list of existing player IDs from the server, including the host’s
playerIDs = NetcodeManager::Instance().RequestConnectedPlayerIDs();
std::cout << "This is the localClientID: " << localClientID << std::endl;
// Initialize player objects for each received player ID (representing other clients and the host)
for (int playerID : playerIDs)
{
std::cout << "This is the Player ID: " << playerID << std::endl;
if (playerID != localClientID) // Skip creating an object for this client’s own player ID
{
Physics::GameObjectNetcode* otherPlayerObject;
InitializePlayerObject(otherPlayerObject, Math::sVector(0, 0, 0), playerID);
}
}
std::cout << "Setting Up Camera" << std::endl;
// Initialize game camera
cameraGameObject = new Physics::GameObjectCamera();
cameraGameObject->rigidBody.position = Math::sVector(0, 0, 15);
cameraGameObject->SetProjectedTransformPerspective(45.0f, 1.0f, 0.1f, 100.0f);
int ownerID = NetcodeManager::Instance().GetLocalClientID();
// Initialize static objects (other entities in the scene)
InitializeStaticObjects();
m_rgba_clearcolor->changeClearColor(1.0f, 0.0f, 2.0f, 1.0f);
eae6320::Logging::OutputMessage("GAME HAS INITIALIZED");
return Results::Success;
}
Here’s how I implemented the UpdateSimulationBasedOnInput function in MyGame to send movement data across the network and synchronize object movement on all clients.
Movement Detection
I added a Boolean check to determine whether the player has moved during a frame. This provides control over when movement data is sent over the network, which reduces unnecessary network traffic and improves performance.
Ownership Validation
The implementation includes ownership checks by comparing the localClientID with the ownership of all netcode player objects. This ensures that only the appropriate client updates and moves objects they own. Once validated, the movement data is sent across the network.
Data Broadcasting
Host: The host uses the BroadcastPacket function to send the desired movement data (which can also send any type of data you want) to all connected clients. This ensures that all clients receive and apply the same movement updates for consistency.
Clients: Clients use the SendPacketToServer function to send their movement data (which can also send any type of data you want) to the host. The host then processes this data and broadcasts it to all other clients, ensuring synchronization across the entire network.
Avoiding Race Conditions
I recommend implementing movement with drag, which automatically slows down and stops player objects without requiring you to manually set their velocity to zero when releasing a movement key. This approach helps avoid race conditions that can occur when a movement input is sent during the same frame the movement key is released. By relying on drag, you ensure smoother and more predictable movement transitions across the network.
void eae6320::cMyGame::UpdateSimulationBasedOnInput()
{
// For Moving the Player
// Initialize velocity vector
Math::sVector velocity(0, 0, 0);
bool hasMovedThisFrame = false;
// UP
if (UserInput::IsKeyPressed('W'))
{
velocity += Math::sVector(0, 3, 0);
hasMovedThisFrame = true;
eae6320::Logging::OutputMessage("Moving Player Up");
}
// DOWN
if (UserInput::IsKeyPressed('S'))
{
velocity += Math::sVector(0, -3, 0);
hasMovedThisFrame = true;
eae6320::Logging::OutputMessage("Moving Player Down");
}
// LEFT
if (UserInput::IsKeyPressed('A'))
{
velocity += Math::sVector(-3, 0, 0);
hasMovedThisFrame = true;
eae6320::Logging::OutputMessage("Moving Player Left");
}
// RIGHT
if (UserInput::IsKeyPressed('D'))
{
velocity += Math::sVector(3, 0, 0);
hasMovedThisFrame = true;
eae6320::Logging::OutputMessage("Moving Player Right");
}
int localClientID = NetcodeManager::Instance().GetLocalClientID();
int hostID = NetcodeManager::Instance().GetHostID();
if (hasMovedThisFrame)
{
hasMoveLastFrame = true;
if (localClientID == hostID)
{
// Update host's own player velocity
for (auto player : netcodePlayerObjects)
{
if (player->m_ClientID == localClientID)
{
std::cout << "Host updating its own player object locally." << std::endl;
if (velocity.GetLength() != 0)
{
player->SetRigidBodyVelocity(velocity);
NetcodeManager::Instance().BroadcastPacket(localClientID, 1, &velocity, sizeof(Math::sVector));
}
// Broadcast movement to all clients
}
}
}
else // Client-specific behavior
{
for (auto player : netcodePlayerObjects)
{
if (player->m_ClientID == localClientID)
{
std::cout << "Client sending movement data to the server." << std::endl;
if (velocity.GetLength() != 0)
{
player->SetRigidBodyVelocity(velocity);
NetcodeManager::Instance().SendPacketToServer(localClientID, 1, &velocity, sizeof(Math::sVector));
}
// Send movement data to the host (server)
}
}
}
}
else if (hasMoveLastFrame)
{
hasMoveLastFrame = false;
if (localClientID == hostID)
{
// Update host's own player velocity
for (auto player : netcodePlayerObjects)
{
if (player->m_ClientID == localClientID)
{
std::cout << "Host updating its own player object locally." << std::endl;
// Send final stop update
Math::sVector stopVelocity(0, 0, 0);
if (velocity.GetLength() != 0)
{
player->SetRigidBodyVelocity(velocity);
NetcodeManager::Instance().BroadcastPacket(localClientID, 1, &velocity, sizeof(Math::sVector));
}
// Broadcast movement to all clients
}
}
}
else // Client-specific behavior
{
for (auto player : netcodePlayerObjects)
{
if (player->m_ClientID == localClientID)
{
std::cout << "Client sending movement data to the server." << std::endl;
// Send final stop update
Math::sVector stopVelocity(0, 0, 0);
if (velocity.GetLength() != 0)
{
player->SetRigidBodyVelocity(velocity);
NetcodeManager::Instance().SendPacketToServer(localClientID, 1, &velocity, sizeof(Math::sVector));
}
// Send position data of client to host
NetcodeManager::Instance().SendPacketToServer(localClientID, 2, &(player->rigidBody.position), sizeof(Math::sVector));
std::cout << "This is the Client sending Velocity: " << velocity.x << velocity.y << velocity.z << std::endl;
// Send movement data to the host (server)
}
}
}
}
// For Moving the Camera
Math::sVector cameraVelocity(0, 0, 0);
// Forward
if (UserInput::IsKeyPressed(UserInput::KeyCodes::Up))
{
cameraVelocity += Math::sVector(0, 0, 5);
eae6320::Logging::OutputMessage("Moving Camera Forward");
}
// Back
if (UserInput::IsKeyPressed(UserInput::KeyCodes::Down))
{
cameraVelocity += Math::sVector(0, 0, -5);
eae6320::Logging::OutputMessage("Moving Camera Back");
}
// Left
if (UserInput::IsKeyPressed(UserInput::KeyCodes::Left))
{
cameraVelocity += Math::sVector(-5, 0, 0);
eae6320::Logging::OutputMessage("Moving Camera Left");
}
// Right
if (UserInput::IsKeyPressed(UserInput::KeyCodes::Right))
{
cameraVelocity += Math::sVector(5, 0, 0);
eae6320::Logging::OutputMessage("Moving Camera Right");
}
// Apply the final velocity to the camera
cameraGameObject->SetRigidBodyVelocity(cameraVelocity);
}
Here’s how my UpdateSimulationBasedOnTime function is implemented.
Incoming Data Handling
The function begins by calling CheckForIncomingData, which processes any data received over the network. This function triggers the registered callback, which then invokes the OnNetcodeUpdateReceived function. This ensures that the game remains synchronized across all clients.
Drag Implementation
Drag is also applied in this function to gradually slow down player objects over time. This eliminates the need to manually set velocities to zero when movement stops, helping to avoid race conditions and ensuring smoother motion.
void eae6320::cMyGame::UpdateSimulationBasedOnTime(const float i_elapsedSecondCount_sinceLastUpdate)
{
NetcodeManager::Instance().CheckForIncomingData();
cameraGameObject->rigidBody.Update(i_elapsedSecondCount_sinceLastUpdate);
// Update each player object
for (auto player : netcodePlayerObjects)
{
// Update physics or other related simulations
player->rigidBody.Update(i_elapsedSecondCount_sinceLastUpdate);
player->SetRigidBodyVelocity(player->rigidBody.velocity * .95f);
}
}
THIS IS VITAL INFORMATION ABOUT THE NETCODE SYSTEM AND RELATES TO THE ONNETCODEUPDATERECEIVED function and also how sending data works within the system.
Sending Data and Handling Updates (IMPORTANT!)
The Netcode system uses a dataType argument when sending data over the network. This dataType specifies the kind of data being sent and determines how the OnNetcodeUpdateReceived function processes it.
Case-Based Handling
In OnNetcodeUpdateReceived, the dataType is evaluated using a case-based approach:
Case 1: Handles movement data, ensuring objects are updated correctly across the network based on movement inputs.
Case 2: Manages position data, ensuring objects are synced up with the host, which is the true state of the game, so all objects are correctly positioned correctly across all clients in our game world.
Custom Cases: As a user of this system, you can add as many cases as needed to handle different types of data, such as end state of our game, health updates, or whatever you might need for your game.
This structure provides flexibility, allowing you to expand the system to accommodate any kind of data you need to send across the network while maintain synchronization and ensuring smooth gameplay.
Functions available Within Netcode System
StartServer

StartClient

WaitForStartSignal

BroadcastStartGame

SendPacketToServer

SendDataToServer

SendDataToClient

BroadcastPacket

RegisterDataReceivedCallback

CheckForIncomingData

SetLocalClientID & GetLocalClientID

GetHostID

RequestConnectedPlayerIDs

SerializeData

DeserializeData

MyGame Needed Functions
InitializeConsole

Registering Callback with On Netcode Update Received

OnNetcodeUpdateReceived

Comments