Skip to main content

MultiplayerController

The MultiplayerController is the heart of Grimoire Infini’s multiplayer system. It manages real-time synchronization of game state, player actions, and card movements using Firebase Firestore.

Overview

The MultiplayerController handles:
  • Real-time game state synchronization
  • Player action broadcasting
  • Card position and state management
  • Firebase Firestore integration

Key Features

Real-time Sync

Automatic synchronization of game state across all connected players

Action System

Efficient action-based updates for optimal performance

State Management

Centralized game state stored in Firebase Firestore

Error Handling

Robust error handling and connection recovery

Architecture

Basic Usage

Initialization

import { MultiplayerController } from '$lib/game/managers/MultiplayerController';

const controller = new MultiplayerController(
  sceneManager,     // SceneManager instance
  "room-123",       // Room ID
  "player-456",     // Player ID
  true              // Is host (optional, default: false)
);

Moving Cards

// Move a card to a new position
await controller.moveCard("card-1", {
  x: 10,
  y: 0,
  z: 5
});

Flipping Cards

// Flip a card
await controller.flipCard("card-1", true);

Cleanup

// Always dispose when done
controller.dispose();

Core Interfaces

GameState

The central game state structure:
interface GameState {
  /** Current state of all cards in the game */
  cards: CardState[];
  /** ID of the player whose turn it currently is */
  currentPlayer: string;
  /** Current phase of the game */
  phase: "waiting" | "playing" | "finished";
  /** Current turn number */
  turn: number;
}

CardState

Represents the state of a single card:
interface CardState {
  /** Unique identifier for the card */
  id: string;
  /** Current position of the card in 3D space */
  position: Position3D;
  /** Current rotation of the card in 3D space */
  rotation: Rotation3D;
  /** Whether the card is currently flipped */
  isFlipped: boolean;
  /** ID of the player who currently owns this card */
  ownerId: string | null;
  /** ID of the zone where this card is located */
  zoneId: string | null;
}

GameAction

Represents player actions:
interface GameAction {
  /** Document ID from Firestore */
  id?: string;
  /** Type of action being performed */
  type: "MOVE_CARD" | "FLIP_CARD" | "DRAW_CARD" | "PLACE_CARD";
  /** ID of the player performing the action */
  playerId: string;
  /** ID of the card being acted upon */
  cardId: string;
  /** Additional data specific to the action type */
  data: any;
  /** Timestamp when the action was created */
  timestamp: number;
}

Implementation Details

Real-time Listeners

The controller sets up two main Firebase listeners:
  1. Game State Listener: Monitors changes to the overall game state
  2. Actions Listener: Listens for new player actions in real-time
private setupGameStateListener(): void {
  this.unsubscribeGameState = onSnapshot(
    this.gameStateRef,
    (doc: DocumentSnapshot) => {
      if (doc.exists()) {
        const gameState: GameState = doc.data() as GameState;
        this.handleGameStateUpdate(gameState);
      }
    }
  );
}

Action Broadcasting

When a player performs an action, it’s broadcast to all other players:
public async sendAction(
  action: Omit<GameAction, "id" | "playerId" | "timestamp">
): Promise<void> {
  const fullAction: GameAction = {
    ...action,
    playerId: this.playerId,
    timestamp: Date.now(),
  };

  await addDoc(this.actionsRef, fullAction);
}

Position Optimization

To prevent jitter from minor position changes, the controller uses a threshold system:
private shouldUpdatePosition(
  currentPos: any,
  targetPos: Position3D
): boolean {
  const THRESHOLD = 0.01;
  return (
    Math.abs(currentPos.x - targetPos.x) > THRESHOLD ||
    Math.abs(currentPos.y - targetPos.y) > THRESHOLD ||
    Math.abs(currentPos.z - targetPos.z) > THRESHOLD
  );
}

Best Practices

Create only one MultiplayerController instance per game session. Multiple instances can cause conflicts and duplicate listeners.
Call dispose() when leaving the game to clean up Firebase listeners and prevent memory leaks.
// In your cleanup code
controller.dispose();
Wrap multiplayer operations in try-catch blocks to handle network issues gracefully.
try {
  await controller.moveCard("card-1", position);
} catch (error) {
  console.error("Failed to move card:", error);
  // Show user feedback
}
Batch related actions together and avoid sending too many rapid updates.

Advanced Usage

Custom Action Types

You can extend the system with custom action types:
// Define new action type
type CustomAction = GameAction & {
  type: "CUSTOM_ACTION";
  data: {
    customField: string;
    value: number;
  };
};

// Handle in remote action processor
private handleRemoteAction(action: GameAction): void {
  switch (action.type) {
    case "CUSTOM_ACTION":
      this.handleCustomAction(action as CustomAction);
      break;
    // ... other cases
  }
}

Room Management

For advanced room management, you can extend the controller:
class ExtendedMultiplayerController extends MultiplayerController {
  async createRoom(roomConfig: RoomConfig): Promise<string> {
    // Custom room creation logic
  }
  
  async joinRoom(roomId: string): Promise<void> {
    // Custom room joining logic
  }
}

Troubleshooting

  • Check Firebase security rules
  • Verify all players are in the same room
  • Ensure proper error handling is in place
  • Check browser console for connection errors
  • Always call dispose() when done
  • Check for multiple controller instances
  • Verify listeners are properly unsubscribed
  • Implement action throttling for rapid updates
  • Use position thresholds to reduce unnecessary updates
  • Consider batching multiple state changes

Next Steps