top of page
  • LinkedIn
  • GitHub
  • X
Search

My Netcode System

  • Writer: Roberto Reynoso
    Roberto Reynoso
  • Nov 25, 2024
  • 10 min read

Updated: May 26

Download Netcode System:



Network System Demo:

Network System


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.


ree

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


ree

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.


ree

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.


ree

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.

ree

ree

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

ree

StartClient

ree

WaitForStartSignal

ree

BroadcastStartGame

ree

SendPacketToServer

ree

SendDataToServer

ree

SendDataToClient

ree

BroadcastPacket

ree

RegisterDataReceivedCallback

ree

CheckForIncomingData

ree

SetLocalClientID & GetLocalClientID

ree

GetHostID

ree

RequestConnectedPlayerIDs

ree

SerializeData

ree

DeserializeData

ree

MyGame Needed Functions


InitializeConsole

ree

Registering Callback with On Netcode Update Received

ree

OnNetcodeUpdateReceived

ree


 
 
 

Recent Posts

See All

Comments


Roberto Valentino Reynoso

bottom of page