Dragonfly

Multiplayer State Management with Unity and Dragonfly Cloud

Integrate Unity with Dragonfly Cloud. Transform your local game into a real-time online multiplayer experience effortlessly.

February 6, 2025

Multiplayer State Management with Unity and Dragonfly Cloud

Introduction

Multiplayer games demand low latency and real-time responsiveness for smooth gameplay. Whether synchronizing player positions, handling game rooms, or managing leaderboards, in-memory data stores like Dragonfly are ideal for handling these high-speed operations, ensuring minimal lag and a seamless gaming experience.

In this 10-minute tutorial, we’ll transform Unity’s Tanks! tutorial—a local two-player game—to enable online real-time access. We’ll create a room for two players using the GET/SET commands and synchronize player movement through the Pub/Sub API. Our setup will include:

  • A simple Node.js WebSocket server for communication.
  • Dragonfly Cloud for managing game state.
  • A Unity client handling WebSocket connections and movement sync.
Unity Tanks

Unity Tanks!

It’s important to note that this tutorial is designed to illustrate fundamental multiplayer game development concepts. It provides a foundational understanding and hands-on tutorial using the tools we all love rather than delivering a fully featured online game. In the meantime, basic knowledge of Node.js, WebSocket, Dragonfly, and Unity is helpful to follow along.


Setting Up Accounts

Before diving into the actual steps and coding examples, ensure that you have created the necessary accounts on Unity and Dragonfly Cloud.

Unity Setup

Dragonfly Cloud Setup

  • Visit Dragonfly Cloud and create an account.
  • Follow the setup wizard to deploy a new data store[1].
  • Once ready, copy the Connection URI from your data store[2].
Unity Tanks Data Store

Creating & Connecting to a Dragonfly Cloud Data Store for Unity Tanks


Building the Game State Backend

Let's set up a simple Node.js backend using Dragonfly and WebSocket to manage game state and player communication. This backend will handle player connections, sync movement data, and ensure the game state stays updated in real time.

First, install the required dependencies:

npm install redis ws

Connecting to Dragonfly Cloud

We will create two Redis clients talking to Dragonfly:

  • pubSubClient: Manages real-time updates via the Pub/Sub API.
  • commandClient: Handles persistent data, such as game rooms and player lists.
const DF_URI = '{Dragonfly_Connection_URI}';

const pubSubClient = createClient({ url: DF_URI });
const commandClient = createClient({ url: DF_URI });

await pubSubClient.connect();
await commandClient.connect();

Initializing a Simple Room

We'll use a key gameRoom to store active player IDs. Right now, it's just an array of player IDs, but we can expand it later to support more rooms or advanced matchmaking.

await commandClient.set('gameRoom', JSON.stringify([]));

Setting Up a WebSocket Server

The WebSocket server enables real-time communication between players. It listens for incoming connections, handles messages, and synchronizes player states.

const wss = new WebSocketServer({ port: PORT });

Synchronizing Player Movement with Pub/Sub

To keep all players in sync, we use a Pub/Sub channel gameState. Whenever a player moves, their updated position is broadcast to all connected players.

pubSubClient.subscribe('gameState', (message) => {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
});

Handling Player Connections & Movements

When a player connects:

  • They are assigned an ID and added to the game room.
  • If the room is full (2 players for simplicity here), new connections are rejected.
  • The player ID is sent back to the client.

When a player moves:

  • The server publishes their movement data to the gameState channel.
  • This ensures that all connected clients receive the update.
wss.on('connection', async (ws) => {
  const players = JSON.parse(await commandClient.get('gameRoom') || '[]');

  if (players.length >= 2) {
    ws.send(JSON.stringify({
      type: 'error',
      message: 'Room is full!'
    }));
    ws.close();
    return;
  }

  const playerId = players.length + 1;
  players.push(playerId);
  await commandClient.set('gameRoom', JSON.stringify(players));

  ws.send(JSON.stringify({
    type: 'playerData',
    playerData: {
      playerId: playerId
    }
  }));

  ws.on('message', async (message) => {
    try {
      const parsedMessage = JSON.parse(message.toString());
      const updatedGameState = {
        type: "movement",
        movementData: parsedMessage,
      };
      await pubSubClient.publish('gameState', JSON.stringify(updatedGameState));
    } catch (error) {
      console.error('Failed to process client message:', error);
    }
  });

});

Handling Player Disconnection

If a player disconnects, their ID is removed from gameRoom, allowing new players to join.

ws.on('close', async () => {
  const updatedPlayers = players.filter((id) => id !== playerId);
  await commandClient.set('gameRoom', JSON.stringify(updatedPlayers));
});

Shutting Down the Server Gracefully

When the server shuts down, we ensure that Redis clients connecting to Dragonfly are properly closed and the WebSocket server shuts down cleanly.

process.on('SIGINT', async () => {
  await pubSubClient.quit();
  await commandClient.quit();
  wss.close(() => {
    process.exit(0);
  });
});

Unity Integration

Now, it's time to get the Unity project to communicate with the backend. We'll do this by creating a WebSocketController to handle the WebSocket communication and updating the TankMovement.cs file to send and receive data. The full code snippet can be found in the links section below.

Receiving Data from the Server

The WebSocketController handles server communication. When a message is received, it checks its type and processes the data accordingly. If the message contains player data, it assigns a player ID. If it contains position updates, it triggers the event to update the corresponding player.

private void ConnectToServer()
{
  ws = new WebSocket(serverUrl);
  ws.OnMessage += (sender, e) => {
    ServerMessage serverMessage = JsonUtility.FromJson < ServerMessage > (e.Data);
    if (serverMessage.type == "playerData")
      playerId = serverMessage.playerData.playerId;
    if (serverMessage.type == "movement")
      OnServerMessage?.Invoke(serverMessage);
  };
  ws.Connect();
}

Sending Data to the Server

Players need to send movement updates to keep the server and other players informed. The function below ensures that data is sent efficiently by limiting updates to a specific frequency.

public void SendMovementToServer(MovementData data)
{
  timeSinceLastUpdate += Time.deltaTime;
  if (timeSinceLastUpdate < updateRate)
    return;
  timeSinceLastUpdate = 0f;
  ws.Send(JsonUtility.ToJson(data));
}

Subscribing to Server Updates

To keep movement updates in sync, we subscribe to server messages in TankMovement.cs when a tank is enabled and unsubscribe when it is disabled.

private void OnEnable()
{
  WebSocketController.Instance.OnServerMessage += ProcessServerMessage;
}

private void OnDisable()
{
  WebSocketController.Instance.OnServerMessage -= ProcessServerMessage;
}

Processing Server Messages

When a movement update is received, we check if it belongs to another player and update their position accordingly. Local player movement remains instant, while remote updates ensure synchronization.

private void ProcessServerMessage(ServerMessage message)
{
  if (message.type != "movement")
    return;
  if (message.movementData.playerId == WebSocketController.Instance.playerId)
    return;
  if (message.movementData.playerId != m_PlayerNumber)
    return;

  remotePosition = message.movementData.position;
  remoteRotation = message.movementData.rotation;
  m_MovementInputValue = message.movementData.movement;
  m_TurnInputValue = message.movementData.turn;
}

Moving the Tank

Each tank processes movement locally, but only remote player positions are updated via the network. This ensures smooth local input handling while keeping other players in sync. To send movement data back to the server, we call the SyncWithServer function.

private void FixedUpdate()
{
  if (m_PlayerNumber != WebSocketController.Instance.playerId) {
    ApplyRemotePosition();
    return;
  }

  m_MovementInputValue = Input.GetAxis("Vertical2");
  m_TurnInputValue = Input.GetAxis("Horizontal2");

  Move();
  Turn();
  SyncWithServer();
}

private void SyncWithServer()
{
  WebSocketController.Instance.SendMovementToServer(
    new MovementData {
      playerId = m_PlayerNumber,
    position = m_Rigidbody.position, rotation = m_Rigidbody.rotation,
    movement = m_MovementInputValue, turn = m_TurnInputValue
  });
}

Next Steps

By leveraging Dragonfly Cloud, we’ve built a basic online multiplayer system in Unity that efficiently manages game state, synchronizes player movement, and maintains low-latency interactions. To expand on this foundation, you can add support for more rooms and players, incorporating additional gameplay elements like shots, hit points, and score tracking, and implementing authentication and matchmaking to create a seamless player experience. With these fundamentals in place, you have a strong foundation to build even more complex game mechanics.

Happy coding! 🚀


Related Resources


Notes

  • [1] Detailed configuration options of Dragonfly Cloud data stores can be found here.
  • [2] This endpoint of the data store is publicly accessible, which is great for our development and tests here. However, in production, private networks and peering connections have many advantages for in-memory data stores like Dragonfly. Read more about private networks and peering connections here.
Dragonfly Wings

Stay up to date on all things Dragonfly

Join our community for unparalleled support and insights

Join

Switch & save up to 80% 

Dragonfly is fully compatible with the Redis ecosystem and requires no code changes to implement. Instantly experience up to a 25X boost in performance and 80% reduction in cost