Multiplayer Integration
The Popcorning Guinea Pig engine provides multiplayer functionality through Steam networking and custom networking solutions. This guide covers implementing multiplayer features in your games.
Overview
The engine supports several multiplayer approaches:
- Steam Peer-to-Peer - Steam networking for seamless multiplayer
- Custom Networking - HTTP/WebSocket-based multiplayer
- Local Multiplayer - Split-screen and hot-seat gameplay
- Hybrid Approaches - Combining multiple networking methods
Steam Networking
Steam Lobby System
Create and manage Steam lobbies for matchmaking:
public class SteamLobbyManager
{
private CSteamID currentLobbyId;
public async Task<bool> CreateLobbyAsync(int maxPlayers)
{
var createLobbyCall = SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypePublic, maxPlayers);
// Wait for callback (simplified)
// In real implementation, use proper callback handling
await Task.Delay(1000);
return currentLobbyId.IsValid();
}
public void JoinLobby(CSteamID lobbyId)
{
SteamMatchmaking.JoinLobby(lobbyId);
}
public void LeaveLobby()
{
if (currentLobbyId.IsValid())
{
SteamMatchmaking.LeaveLobby(currentLobbyId);
currentLobbyId = CSteamID.Nil;
}
}
public void SetLobbyData(string key, string value)
{
if (currentLobbyId.IsValid())
{
SteamMatchmaking.SetLobbyData(currentLobbyId, key, value);
}
}
public string GetLobbyData(string key)
{
if (currentLobbyId.IsValid())
{
return SteamMatchmaking.GetLobbyData(currentLobbyId, key);
}
return null;
}
}
Steam P2P Networking
Send and receive data between players:
public class SteamNetworkManager
{
private readonly Dictionary<CSteamID, PlayerData> connectedPlayers = new();
public void SendDataToPlayer(CSteamID steamId, byte[] data)
{
SteamNetworking.SendP2PPacket(steamId, data, (uint)data.Length,
EP2PSend.k_EP2PSendReliable, 0);
}
public void BroadcastData(byte[] data)
{
foreach (var playerId in connectedPlayers.Keys)
{
SendDataToPlayer(playerId, data);
}
}
public void Update()
{
// Check for incoming packets
uint packetSize;
while (SteamNetworking.IsP2PPacketAvailable(out packetSize, 0))
{
byte[] data = new byte[packetSize];
CSteamID remoteSteamId;
uint bytesRead;
if (SteamNetworking.ReadP2PPacket(data, packetSize, out bytesRead,
out remoteSteamId, 0))
{
ProcessReceivedData(remoteSteamId, data, bytesRead);
}
}
}
private void ProcessReceivedData(CSteamID senderId, byte[] data, uint size)
{
// Process received multiplayer data
var message = NetworkMessage.Deserialize(data);
HandleNetworkMessage(senderId, message);
}
private void HandleNetworkMessage(CSteamID senderId, NetworkMessage message)
{
switch (message.Type)
{
case NetworkMessageType.PlayerPosition:
UpdatePlayerPosition(senderId, message.Data);
break;
case NetworkMessageType.GameAction:
ProcessGameAction(senderId, message.Data);
break;
case NetworkMessageType.ChatMessage:
DisplayChatMessage(senderId, message.Data);
break;
}
}
}
Custom Networking
HTTP-Based Multiplayer
For turn-based or less real-time games:
public class HttpMultiplayerManager
{
private readonly HttpClient httpClient;
private readonly string serverUrl;
private string gameSessionId;
public HttpMultiplayerManager(string serverUrl)
{
this.serverUrl = serverUrl;
httpClient = new HttpClient();
}
public async Task<string> CreateGameSessionAsync(GameSettings settings)
{
var request = new CreateGameRequest
{
MaxPlayers = settings.MaxPlayers,
GameMode = settings.GameMode,
Settings = settings
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"{serverUrl}/api/games", content);
response.EnsureSuccessStatusCode();
var responseData = await response.Content.ReadAsStringAsync();
var gameSession = JsonSerializer.Deserialize<GameSession>(responseData);
gameSessionId = gameSession.Id;
return gameSessionId;
}
public async Task<bool> JoinGameSessionAsync(string sessionId, PlayerInfo player)
{
var request = new JoinGameRequest
{
SessionId = sessionId,
Player = player
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"{serverUrl}/api/games/join", content);
if (response.IsSuccessStatusCode)
{
gameSessionId = sessionId;
return true;
}
return false;
}
public async Task<GameState> SendMoveAsync(GameMove move)
{
var request = new GameMoveRequest
{
SessionId = gameSessionId,
Move = move
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"{serverUrl}/api/games/move", content);
response.EnsureSuccessStatusCode();
var responseData = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<GameState>(responseData);
}
public async Task<GameState> GetGameStateAsync()
{
var response = await httpClient.GetAsync($"{serverUrl}/api/games/{gameSessionId}");
response.EnsureSuccessStatusCode();
var responseData = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<GameState>(responseData);
}
}
WebSocket Real-Time Communication
For real-time multiplayer experiences:
public class WebSocketMultiplayerManager
{
private ClientWebSocket webSocket;
private readonly string serverUrl;
private bool isConnected = false;
public event Action<NetworkMessage> OnMessageReceived;
public event Action OnConnected;
public event Action<string> OnDisconnected;
public WebSocketMultiplayerManager(string serverUrl)
{
this.serverUrl = serverUrl;
}
public async Task<bool> ConnectAsync()
{
try
{
webSocket = new ClientWebSocket();
await webSocket.ConnectAsync(new Uri(serverUrl), CancellationToken.None);
isConnected = true;
OnConnected?.Invoke();
// Start listening for messages
_ = Task.Run(ListenForMessages);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"WebSocket connection failed: {ex.Message}");
return false;
}
}
public async Task SendMessageAsync(NetworkMessage message)
{
if (!isConnected) return;
try
{
var json = JsonSerializer.Serialize(message);
var buffer = Encoding.UTF8.GetBytes(json);
var segment = new ArraySegment<byte>(buffer);
await webSocket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to send WebSocket message: {ex.Message}");
}
}
private async Task ListenForMessages()
{
var buffer = new byte[4096];
while (webSocket.State == WebSocketState.Open)
{
try
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var json = Encoding.UTF8.GetString(buffer, 0, result.Count);
var message = JsonSerializer.Deserialize<NetworkMessage>(json);
OnMessageReceived?.Invoke(message);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
isConnected = false;
OnDisconnected?.Invoke("Connection closed by server");
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"WebSocket listening error: {ex.Message}");
isConnected = false;
OnDisconnected?.Invoke(ex.Message);
break;
}
}
}
public async Task DisconnectAsync()
{
if (webSocket != null && webSocket.State == WebSocketState.Open)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnect", CancellationToken.None);
isConnected = false;
}
}
}
Local Multiplayer
Split-Screen Implementation
Support multiple players on the same screen:
public class SplitScreenManager
{
private readonly List<Player> localPlayers = new();
private readonly List<Camera> playerCameras = new();
public void AddLocalPlayer(Player player)
{
localPlayers.Add(player);
// Create camera for this player
var camera = new Camera();
ConfigureCameraForPlayer(camera, localPlayers.Count);
playerCameras.Add(camera);
}
public void RemoveLocalPlayer(Player player)
{
int index = localPlayers.IndexOf(player);
if (index >= 0)
{
localPlayers.RemoveAt(index);
playerCameras.RemoveAt(index);
// Reconfigure remaining cameras
ReconfigureAllCameras();
}
}
public void Update(double deltaTime)
{
// Update each player
for (int i = 0; i < localPlayers.Count; i++)
{
localPlayers[i].Update(deltaTime);
UpdateCameraForPlayer(playerCameras[i], localPlayers[i]);
}
}
public void Render(int screenWidth, int screenHeight)
{
if (localPlayers.Count == 1)
{
// Single player - full screen
RenderPlayerView(0, 0, 0, screenWidth, screenHeight);
}
else if (localPlayers.Count == 2)
{
// Two players - split horizontally
RenderPlayerView(0, 0, 0, screenWidth, screenHeight / 2);
RenderPlayerView(1, 0, screenHeight / 2, screenWidth, screenHeight / 2);
}
else if (localPlayers.Count <= 4)
{
// Four players - quadrants
int halfWidth = screenWidth / 2;
int halfHeight = screenHeight / 2;
RenderPlayerView(0, 0, 0, halfWidth, halfHeight);
RenderPlayerView(1, halfWidth, 0, halfWidth, halfHeight);
if (localPlayers.Count > 2)
RenderPlayerView(2, 0, halfHeight, halfWidth, halfHeight);
if (localPlayers.Count > 3)
RenderPlayerView(3, halfWidth, halfHeight, halfWidth, halfHeight);
}
}
private void RenderPlayerView(int playerIndex, int x, int y, int width, int height)
{
if (playerIndex >= localPlayers.Count) return;
// Set viewport for this player
Raylib.SetRenderTarget(null); // Reset to default
Raylib.SetViewport(x, y, width, height);
// Render with player's camera
var camera = playerCameras[playerIndex];
Raylib.BeginMode3D(camera.GetRaylibCamera());
// Render game world for this player
RenderGameWorld(localPlayers[playerIndex]);
Raylib.EndMode3D();
// Render UI for this player
RenderPlayerUI(localPlayers[playerIndex], x, y, width, height);
}
private void ConfigureCameraForPlayer(Camera camera, int playerCount)
{
// Configure camera settings based on number of players
switch (playerCount)
{
case 1:
camera.SetFOV(90f);
break;
case 2:
camera.SetFOV(80f);
break;
default:
camera.SetFOV(70f);
break;
}
}
}
Network Message System
Message Serialization
Efficient network message handling:
public enum NetworkMessageType
{
PlayerJoined,
PlayerLeft,
PlayerPosition,
GameAction,
ChatMessage,
GameState,
Ping,
Pong
}
[Serializable]
public class NetworkMessage
{
public NetworkMessageType Type { get; set; }
public string SenderId { get; set; }
public DateTime Timestamp { get; set; }
public byte[] Data { get; set; }
public static NetworkMessage Create<T>(NetworkMessageType type, string senderId, T data)
{
return new NetworkMessage
{
Type = type,
SenderId = senderId,
Timestamp = DateTime.UtcNow,
Data = JsonSerializer.SerializeToUtf8Bytes(data)
};
}
public T GetData<T>()
{
return JsonSerializer.Deserialize<T>(Data);
}
public byte[] Serialize()
{
return JsonSerializer.SerializeToUtf8Bytes(this);
}
public static NetworkMessage Deserialize(byte[] data)
{
return JsonSerializer.Deserialize<NetworkMessage>(data);
}
}
[Serializable]
public class PlayerPositionData
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public float Rotation { get; set; }
public PlayerState State { get; set; }
}
[Serializable]
public class GameActionData
{
public string ActionType { get; set; }
public Dictionary<string, object> Parameters { get; set; }
public float Timestamp { get; set; }
}
Network Synchronization
Keep game states synchronized:
public class NetworkSyncManager
{
private readonly Dictionary<string, NetworkedObject> networkedObjects = new();
private float lastSyncTime = 0;
private const float SyncInterval = 1/20f; // 20 times per second
public void RegisterNetworkedObject(string id, NetworkedObject obj)
{
networkedObjects[id] = obj;
}
public void UnregisterNetworkedObject(string id)
{
networkedObjects.Remove(id);
}
public void Update(double deltaTime, INetworkManager networkManager)
{
lastSyncTime += (float)deltaTime;
if (lastSyncTime >= SyncInterval)
{
SynchronizeObjects(networkManager);
lastSyncTime = 0;
}
}
private void SynchronizeObjects(INetworkManager networkManager)
{
foreach (var kvp in networkedObjects)
{
var obj = kvp.Value;
if (obj.HasChanged)
{
var syncData = new ObjectSyncData
{
ObjectId = kvp.Key,
Position = obj.Position,
Rotation = obj.Rotation,
State = obj.GetSyncState()
};
var message = NetworkMessage.Create(NetworkMessageType.GameAction,
"local", syncData);
networkManager.BroadcastMessage(message);
obj.MarkSynced();
}
}
}
public void HandleSyncMessage(NetworkMessage message)
{
var syncData = message.GetData<ObjectSyncData>();
if (networkedObjects.TryGetValue(syncData.ObjectId, out var obj))
{
obj.ApplySyncData(syncData);
}
}
}
public abstract class NetworkedObject
{
public Vector3 Position { get; protected set; }
public float Rotation { get; protected set; }
public bool HasChanged { get; private set; }
protected void MarkChanged()
{
HasChanged = true;
}
public void MarkSynced()
{
HasChanged = false;
}
public abstract Dictionary<string, object> GetSyncState();
public abstract void ApplySyncData(ObjectSyncData data);
}
Best Practices
Network Performance
- Minimize Data Transfer: Only send changed data
- Compression: Use compression for large data packets
- Prediction: Implement client-side prediction for responsiveness
- Interpolation: Smooth movement between network updates
Security Considerations
- Server Authority: Server should validate all game actions
- Anti-Cheat: Implement basic anti-cheat measures
- Rate Limiting: Prevent spam and abuse
- Data Validation: Validate all incoming network data
Error Handling
public class NetworkErrorHandler
{
public static void HandleNetworkError(Exception ex, string context)
{
switch (ex)
{
case TimeoutException:
Console.WriteLine($"Network timeout in {context}");
// Implement retry logic
break;
case HttpRequestException:
Console.WriteLine($"HTTP error in {context}: {ex.Message}");
// Handle HTTP-specific errors
break;
case WebSocketException:
Console.WriteLine($"WebSocket error in {context}: {ex.Message}");
// Handle WebSocket reconnection
break;
default:
Console.WriteLine($"Unknown network error in {context}: {ex.Message}");
break;
}
}
}
Testing Multiplayer
Local Testing
public class MultiplayerTestHelper
{
public static void SimulateNetworkDelay(int milliseconds)
{
Task.Delay(milliseconds).Wait();
}
public static void SimulatePacketLoss(float lossRate)
{
var random = new Random();
if (random.NextSingle() < lossRate)
{
throw new NetworkException("Simulated packet loss");
}
}
public static byte[] CorruptData(byte[] data, float corruptionRate)
{
var random = new Random();
var corrupted = new byte[data.Length];
Array.Copy(data, corrupted, data.Length);
for (int i = 0; i < corrupted.Length; i++)
{
if (random.NextSingle() < corruptionRate)
{
corrupted[i] = (byte)random.Next(256);
}
}
return corrupted;
}
}
Further Reading
- Steam Integration - Steam platform features
- Services - Service architecture for networking
- Game Architecture - Overall engine patterns
Multiplayer functionality enhances your game's replay value and social aspects. Choose the approach that best fits your game's requirements and technical constraints.