Networking and Multiplayer System¶
The Networking system in Evoker Engine provides TCP-based client/server multiplayer functionality with support for entity synchronization, inventory updates, dimension changes, and more.
Overview¶
The networking system consists of:
- NetworkServer: Host multiplayer games
- NetworkClient: Connect to multiplayer games
- NetworkConnection: Manages individual client-server connections
- NetworkMessage: Base class for all network messages
- NetworkingSystem: ECS integration for multiplayer
Creating a Server¶
Basic Server Setup¶
using EvokerEngine.Networking;
// Create and start server
var server = new NetworkServer(port: 7777);
server.Start();
// Handle client connections
server.OnClientConnected += (connection) =>
{
Logger.Info($"Client connected: {connection.ConnectionId}");
};
server.OnClientDisconnected += (connection) =>
{
Logger.Info($"Client disconnected: {connection.ConnectionId}");
};
// Handle messages
server.OnMessageReceived += (connection, message) =>
{
switch (message.Type)
{
case MessageType.ChatMessage:
var chat = (ChatMessage)message;
Logger.Info($"{chat.Sender}: {chat.Message}");
// Broadcast to all clients
await server.BroadcastAsync(chat);
break;
}
};
// When done
server.Stop();
Using NetworkingSystem as Server¶
using EvokerEngine.Networking;
var networkSystem = new NetworkingSystem();
// Start as server
networkSystem.StartServer(port: 7777);
// Handle player joins
networkSystem.OnPlayerJoined += (clientId, playerName) =>
{
Logger.Info($"Player joined: {playerName}");
// Create player entity
var scene = Application.Instance.ActiveScene;
var playerEntity = scene.CreateEntity(playerName);
// Associate entity with client
networkSystem.AssociateEntityWithClient(playerEntity, clientId);
};
// Send entity updates
await networkSystem.SendEntityUpdateAsync(
entity,
position,
rotation,
velocity
);
Creating a Client¶
Basic Client Setup¶
using EvokerEngine.Networking;
// Create client
var client = new NetworkClient(host: "127.0.0.1", port: 7777);
// Handle connection
client.OnConnected += () =>
{
Logger.Info("Connected to server!");
};
client.OnDisconnected += () =>
{
Logger.Info("Disconnected from server");
};
// Handle messages
client.OnMessageReceived += (message) =>
{
switch (message.Type)
{
case MessageType.EntityUpdate:
var update = (EntityUpdateMessage)message;
// Update entity position
break;
}
};
// Connect
await client.ConnectAsync();
// Send messages
var chatMsg = new ChatMessage
{
Sender = "Player1",
Message = "Hello, World!",
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
await client.SendAsync(chatMsg);
// Disconnect
client.Disconnect();
Using NetworkingSystem as Client¶
var networkSystem = new NetworkingSystem();
// Connect to server
await networkSystem.ConnectAsClientAsync(
host: "127.0.0.1",
port: 7777,
playerName: "MyPlayer"
);
// Handle connection events
networkSystem.OnConnectedToServer += () =>
{
Logger.Info("Connected!");
};
networkSystem.OnDisconnectedFromServer += () =>
{
Logger.Info("Disconnected!");
};
Network Messages¶
Built-in Message Types¶
// Connect/Disconnect
var connectMsg = new ConnectMessage
{
PlayerName = "Player1",
Version = "1.0.0"
};
var disconnectMsg = new DisconnectMessage
{
Reason = "Player quit"
};
// Ping/Pong for latency
var pingMsg = new PingMessage
{
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
// Chat
var chatMsg = new ChatMessage
{
Sender = "Player1",
Message = "Hello!",
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
// Entity updates
var entityMsg = new EntityUpdateMessage
{
EntityId = 123,
Position = new Vector3(10, 0, 10),
Rotation = new Vector3(0, 90, 0),
Velocity = new Vector3(1, 0, 0)
};
// Inventory updates (uses namespace:key format)
var inventoryMsg = new InventoryUpdateMessage
{
EntityId = 123,
SlotIndex = 5,
ItemId = "game:diamond_sword", // namespace:key
Quantity = 1
};
// Dimension changes (uses namespace:key format)
var dimensionMsg = new DimensionChangeMessage
{
EntityId = 123,
DimensionId = "evoker:nether", // namespace:key
Position = new Vector3(100, 64, 200)
};
Creating Custom Messages¶
public class CustomGameMessage : NetworkMessage
{
public override MessageType Type => MessageType.Custom;
public string CustomData { get; set; } = string.Empty;
public override byte[] Serialize()
{
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms);
WriteString(writer, CustomData);
return ms.ToArray();
}
public override void Deserialize(byte[] data)
{
using var ms = new MemoryStream(data);
using var reader = new BinaryReader(ms);
CustomData = ReadString(reader);
}
}
Complete Multiplayer Example¶
Server Layer¶
public class MultiplayerServerLayer : Layer
{
private NetworkingSystem _network;
private Dictionary<Guid, Entity> _players;
public override void OnAttach()
{
_network = new NetworkingSystem();
_players = new Dictionary<Guid, Entity>();
_network.OnPlayerJoined += OnPlayerJoined;
_network.OnPlayerLeft += OnPlayerLeft;
_network.StartServer(7777);
Logger.Info("Multiplayer server started");
}
private void OnPlayerJoined(Guid clientId, string playerName)
{
var scene = Application.Instance.ActiveScene;
var playerEntity = scene.CreateEntity(playerName);
// Add components
var transform = scene.Registry.AddComponent<TransformComponent>(playerEntity);
transform.Position = new Vector3(0, 0, 0);
var inventory = scene.Registry.AddComponent<InventoryComponent>(playerEntity);
_network.AssociateEntityWithClient(playerEntity, clientId);
_players[clientId] = playerEntity;
Logger.Info($"Player joined: {playerName}");
}
private void OnPlayerLeft(Guid clientId)
{
if (_players.TryGetValue(clientId, out var entity))
{
var scene = Application.Instance.ActiveScene;
scene.DestroyEntity(entity);
_players.Remove(clientId);
}
}
public override void OnUpdate(float deltaTime)
{
_network.Update(deltaTime);
// Sync player positions
var scene = Application.Instance.ActiveScene;
foreach (var kvp in _players)
{
var entity = kvp.Value;
var transform = scene.Registry.GetComponent<TransformComponent>(entity);
if (transform != null)
{
_ = _network.SendEntityUpdateAsync(
entity,
transform.Position,
transform.Rotation,
Vector3.Zero
);
}
}
}
public override void OnDetach()
{
_network?.Shutdown();
}
}
Client Layer¶
public class MultiplayerClientLayer : Layer
{
private NetworkingSystem _network;
private Entity _localPlayer;
public override void OnAttach()
{
_network = new NetworkingSystem();
_network.OnConnectedToServer += () =>
{
Logger.Info("Connected to server!");
CreateLocalPlayer();
};
_ = _network.ConnectAsClientAsync("127.0.0.1", 7777, "MyPlayer");
}
private void CreateLocalPlayer()
{
var scene = Application.Instance.ActiveScene;
_localPlayer = scene.CreateEntity("LocalPlayer");
var transform = scene.Registry.AddComponent<TransformComponent>(_localPlayer);
transform.Position = new Vector3(0, 0, 0);
}
public override void OnUpdate(float deltaTime)
{
if (_network.IsClient && _localPlayer.Id != 0)
{
var scene = Application.Instance.ActiveScene;
var transform = scene.Registry.GetComponent<TransformComponent>(_localPlayer);
if (transform != null)
{
// Send position updates
_ = _network.SendEntityUpdateAsync(
_localPlayer,
transform.Position,
transform.Rotation,
Vector3.Zero
);
}
}
}
public override void OnDetach()
{
_network?.Shutdown();
}
}
Items, Dimensions, and Inventories with Colon Notation¶
The networking system fully supports the ResourceKey namespace:key format for items, dimensions, and inventories:
// Item synchronization
var itemMsg = new InventoryUpdateMessage
{
ItemId = "minecraft:diamond_sword", // namespace:key format
Quantity = 1
};
// Dimension synchronization
var dimMsg = new DimensionChangeMessage
{
DimensionId = "evoker:nether", // namespace:key format
Position = new Vector3(100, 64, 200)
};
// Parse received items
var itemKey = ResourceKey.Parse(itemMsg.ItemId);
var item = ItemRegistry.Get(itemKey);
// Parse received dimensions
var dimKey = ResourceKey.Parse(dimMsg.DimensionId);
var dimension = DimensionRegistry.Instance.Get(dimKey);
Best Practices¶
✅ Do's¶
- Use NetworkingSystem for ECS integration
- Handle disconnections gracefully
- Validate all received data
- Use async/await for network operations
- Implement timeout/retry logic
- Compress large messages
- Use ResourceKey format for items/dimensions
❌ Don'ts¶
- Don't send messages too frequently (throttle updates)
- Don't trust client input without validation
- Don't block the main thread with network operations
- Don't send large amounts of data per message (> 10MB)
- Don't forget to dispose connections
Performance Tips¶
- Throttle Updates: Don't send entity updates every frame
- Batch Messages: Combine multiple updates when possible
- Compression: Compress data for large messages
- Delta Updates: Only send changed values
- Priority System: Send critical updates first
See Also¶
- ECS - Entity Component System
- Inventory - Inventory system
- Dimensions - Dimension system