Popcorning Guinea Pig
Search Results for

    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

    1. Minimize Data Transfer: Only send changed data
    2. Compression: Use compression for large data packets
    3. Prediction: Implement client-side prediction for responsiveness
    4. Interpolation: Smooth movement between network updates

    Security Considerations

    1. Server Authority: Server should validate all game actions
    2. Anti-Cheat: Implement basic anti-cheat measures
    3. Rate Limiting: Prevent spam and abuse
    4. 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.

    • Edit this page
    In this article
    Back to top © 2024 Popcorning Guinea Pig Game Engine. A custom C# game engine for indie developers.