# Ninja Battle

**URL:** https://heroiclabs.com/docs/nakama/tutorials/unity/ninja-battle/
**Summary:** Developed using Unity, Ninja Battle is a real-time top-down 2d battler where players control sprinting ninjas who drop deadly caltrops as they run across the grid.
**Keywords:** ninja battle, nakama
**Categories:** nakama, unity, ninja-battle

---


# Unity Ninja Battle Tutorial

Ninja Battle is an open-source game project sponsored by the [Heroic Games Grant](https://heroiclabs.com/blog/announcements/announcing-heroic-games-grant-recipients/). Developed using Unity, Ninja Battle is a real-time top-down 2d battler where players control sprinting ninjas who drop deadly caltrops as they run across the grid.

![Ninja Battle]({{< fingerprint_image "/images/pages/nakama/tutorials/unity/ninja-battle/ninja_battle.jpeg" >}})

The game, and this tutorial, highlight the following Nakama features:

* [Authentication](../../../concepts/authentication/)
* [Sessions](../../../concepts/session/)
* [Users](../../../concepts/user-accounts/)
* [Matchmaker](../../../concepts/multiplayer/matchmaker/)
* [Authoritative Multiplayer](../../../concepts/multiplayer/authoritative/)

## Prerequisites

To easily follow with this tutorial, perform the following before proceeding:

- [Install Nakama](../../../getting-started/install/docker/)
- [Download](https://unity3d.com/get-unity/download) and install Unity
- [Install the Nakama Unity SDK](https://github.com/heroiclabs/nakama-unity)
- [Download the Ninja Battle Unity project](https://github.com/heroiclabs/ninja-battle-nakama/)

## Project structure

The Ninja Battle project consists of seven [scenes](https://github.com/heroiclabs/ninja-battle-nakama/tree/main/NinjaBattle/Assets/Scenes):

{{< table name="nakama.tutorials.unity.ninja-battle.project-structure" >}}

### Helper functions

This project also contains a small set of helper functions serving as example implementations of some Nakama features. [`NakamaManager`](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/NinjaBattle/Assets/Scripts/Nakama/NakamaManager.cs) is the base script for these functions and is used to perform user login and logout, and sending RPCs to the server.

## Authentication

The `NakamaManager` scripts enables you to use three types of login:

```csharp
// Generate a random UDID and save it to PlayerPrefs
NakamaManager.Instance.LoginWithUdid();
// Use the device's unique identifier
NakamaManager.Instance.LoginWithDevice();
// Use a custom ID to handle authentication
NakamaManager.Instance.LoginWithCustomId();
```

You can see the Ninja Battle implementation in **[NakamaAutoLogin](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/NinjaBattle/Assets/Scripts/Nakama/NakamaAutoLogin.cs)**:

```csharp
using UnityEngine;

namespace Nakama.Helpers
{
    public class NakamaAutoLogin : MonoBehaviour
    {
        #region FIELDS

        [SerializeField] private float retryTime = 5f;

        #endregion

        #region BEHAVIORS

        private void Start()
        {
            NakamaManager.Instance.onLoginFail += LoginFailed;
            TryLogin();
        }

        private void OnDestroy()
        {
            NakamaManager.Instance.onLoginFail -= LoginFailed;
        }

        private void TryLogin()
        {
            NakamaManager.Instance.LoginWithUdid();
        }

        private void LoginFailed()
        {
            Invoke(nameof(TryLogin), retryTime);
        }

        #endregion
    }
}
```

The [`NakamaEvents`](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/NinjaBattle/Assets/Scripts/Nakama/NakamaEvents.cs) component exposes all `NakamaManager` events to the Unity inspector, with these events helping to handle Ninja Battle's game logic. For example, the Splash scene executes a scene change when receiving the `onLoginSuccess` event.

### Setting up usernames

Ninja Battle is designed for players to be able to join a match and start playing right away, they do not need to specifically create a username for themselves. For new accounts a random two word name is generated for players that do not want to set a custom name.

**SetDisplayName.cs**:

```csharp
using TMPro;
using UnityEngine;
using Nakama.Helpers;

namespace NinjaBattle.General
{
    public class SetDisplayName : MonoBehaviour
    {
        #region FIELDS

        private const float delay = 1f;

        [SerializeField] private TMP_InputField inputField = null;
        [SerializeField] private string[] firstPart = null;
        [SerializeField] private string[] secondPart = null;

        private NakamaUserManager nakamaUserManager = null;

        #endregion

        #region BEHAVIORS

        private void Start()
        {
            nakamaUserManager = NakamaUserManager.Instance;
            nakamaUserManager.onLoaded += ObtainName;
            inputField.onValueChanged.AddListener(ValueChanged);
            if (nakamaUserManager.LoadingFinished)
                ObtainName();
        }

        private void OnDestroy()
        {
            inputField.onValueChanged.RemoveListener(ValueChanged);
            nakamaUserManager.onLoaded -= ObtainName;
        }

        private void ObtainName()
        {
            if (string.IsNullOrEmpty(nakamaUserManager.DisplayName))
                inputField.text = firstPart[Random.Range(0, firstPart.Length)] + secondPart[Random.Range(0, secondPart.Length)];
            else
                inputField.text = nakamaUserManager.DisplayName;
        }

        private void ValueChanged(string newValue)
        {
            CancelInvoke(nameof(UpdateName));
            Invoke(nameof(UpdateName), delay);
        }

        private void UpdateName()
        {
            if (inputField.text != nakamaUserManager.DisplayName)
                nakamaUserManager.UpdateDisplayName(inputField.text);
        }

        #endregion
    }
}
```

Players can change their usernames as desired. This is handled via the `UpdateDisplayName` function in the `NakamaUserManager`:

```csharp
// ...
public async void UpdateDisplayName(string displayName)
{
    await NakamaManager.Instance.Client.UpdateAccountAsync(NakamaManager.Instance.Session, null, displayName);
}
// ...
```

## Gameplay

The gameplay logic in Ninja Battle consists of two parts, one handled via the Unity client and the other via a [custom RPC](../../../server-framework/introduction/#functionality) using the server runtime.

From the [Home scene](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/NinjaBattle/Assets/Scenes/2-Home.unity) the player presses the `Battle` button and triggers the `FindMatch` function:

```csharp
private void FindMatch()
{
    button.interactable = false;
    MultiplayerManager.Instance.JoinMatchAsync();
}
```

The [MultiplayerManager](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/NinjaBattle/Assets/Scripts/Nakama/MultiplayerManager.cs) registers for the upcoming match states:

```csharp
NakamaManager.Instance.Socket.ReceivedMatchState += Receive;
```

Using `NakamaManager` the `JoinOrCreateMatchRpc` RPC is called:

```csharp
private const string JoinOrCreateMatchRpc = "JoinOrCreateMatchRpc";
// ...
IApiRpc rpcResult = await NakamaManager.Instance.SendRPC(JoinOrCreateMatchRpc);
// ...
```

The `joinOrCreateMatch` RPC is then executed on the server. This function looks for any open match for the player to join and, if one cannot be found, creates a new match:

```typescript
let joinOrCreateMatch: nkruntime.RpcFunction = function (context: nkruntime.Context, logger: nkruntime.Logger, nakama: nkruntime.Nakama, payload: string): string
{
    let matches: nkruntime.Match[];
    const MatchesLimit = 1;
    const MinimumPlayers = 0;
    var label: MatchLabel = { open: true }
    matches = nakama.matchList(MatchesLimit, true, JSON.stringify(label), MinimumPlayers, MaxPlayers - 1);
    if (matches.length > 0) {
        return matches[0].matchId;
    }

    return nakama.matchCreate(MatchModuleName);
}
```

The client receives the `matchId` returned by the server and sends a join request:

```csharp
string matchId = rpcResult.Payload;
match = await NakamaManager.Instance.Socket.JoinMatchAsync(matchId);
```

The `onMatchJoin` event is called and the `GameManager`, subscribed to that event, switches to the [Lobby scene](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/NinjaBattle/Assets/Scenes/3-Lobby.unity):

```csharp
onMatchJoin?.Invoke();

// ...

MultiplayerManager.Instance.onMatchJoin += JoinedMatch;
...
private void JoinedMatch()
{
    ResetPlayerWins();
    GoToLobby();
}
...
private void GoToLobby()
{
    SceneManager.LoadScene((int)Scenes.Lobby);
}
```

## Client side logic

This section details the client side logic for Ninja Battle.

### MultiplayerManager

The [`MultiplayerManager`](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/NinjaBattle/Assets/Scripts/Nakama/MultiplayerManager.cs) handles the logic for [joining a match](#gameplay), and sending and receiving messages for that match on the server. These messages are described by the following `OperationCodes`:

{{< table name="nakama.tutorials.unity.ninja-battle.op-codes" >}}

The `MultiplayerManager` receives all messages and distributes them according to subscriptions, for example:

```csharp
MultiplayerManager.Instance.Subscribe(MultiplayerManager.Code.PlayerInput, ReceivedPlayerInput);
```

To send a message, send an object that can be serialized:

```csharp
private void SendData(int tick, Direction direction)
{
   MultiplayerManager.Instance.Send(MultiplayerManager.Code.PlayerInput, new InputData(tick, (int)direction));
}
```

To receive a message, use the `MultiplayerManager to deserialize to the class you want:

```csharp
private void ReceivedPlayerInput(MultiplayerMessage message)
{
   InputData inputData = message.GetData<InputData>();
   SetPlayerInput(GetPlayerNumber(message.SessionId), inputData.Tick, (Direction)inputData.Direction);
}
```

### Multiplayer identity

This script holds the unique ID of each player, the `SessionId` here:

**MultiplayerIdentity.cs**:
```csharp
using UnityEngine;

namespace Nakama.Helpers
{
    public partial class MultiplayerIdentity : MonoBehaviour
    {
        #region FIELDS

        private static int currentId = 0;

        #endregion

        #region PROPERTIES

        public string Id { get; private set; }
        public bool IsLocalPlayer { get => MultiplayerManager.Instance.Self != null && MultiplayerManager.Instance.Self.SessionId == Id; }

        #endregion

        #region BEHAVIORS

        private void Awake()
        {
            AssignIdentity();
        }

        private void AssignIdentity()
        {
            Id = currentId++.ToString();
        }

        public void SetId(string id)
        {
            Id = id;
        }

        public static void ResetIds()
        {
            currentId = default(int);
        }

        #endregion
    }
}
```

### Spawning players

The spawning of players is handled by the [Map](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/NinjaBattle/Assets/Scripts/Game/Map.cs) class, taking each player and putting them on different starting positions:

**Map.cs**:
```csharp
// ...
public void InstantiateNinja(int playerNumber, SpawnPoint spawnPoint, string sessionId)
{
    Ninja ninja = Instantiate(ninjaPrefab, transform);
    ninja.Initialize(spawnPoint, playerNumber, this, sessionId);
    Ninjas.Add(ninja);
    if (MultiplayerManager.Instance.Self.SessionId == sessionId)
        gameCamera.SetStartingPosition(spawnPoint.Coordinates);
}
// ...
```

### Rollback

Ninja Battle utilizes a basic implementation of rollback netcode. You can learn more about this concept [here](https://ki.infil.net/w02-netcode.html).

The `RollbackVar` handles saving of information on a timeline using a dictionary with `int` representing each tick of the gameloop and `T` being any desired type.

**RollbackVar.cs**:
```csharp
using System.Collections.Generic;

namespace NinjaBattle.Game
{
    public class RollbackVar<T>
    {
        #region FIELDS

        private Dictionary<int, T> history = new Dictionary<int, T>();

        #endregion

        #region PROPERTIES

        public T this[int tick]
        {
            get
            {
                return history.ContainsKey(tick) ? history[tick] : default(T);
            }

            set
            {
                if (history.ContainsKey(tick))
                    history[tick] = value;
                else
                    history.Add(tick, value);
            }
        }

        #endregion

        #region BEHAVIORS

        public bool HasValue(int tick)
        {
            return history.ContainsKey(tick);
        }

        public T GetLastValue(int tick)
        {
            for (; tick >= 0; tick--)
                if (HasValue(tick))
                    return history[tick];

            return default(T);
        }

        public void EraseFuture(int tick)
        {
            List<int> keysToErase = new List<int>();
            foreach (KeyValuePair<int, T> keyValuePair in history)
                if (keyValuePair.Key > tick)
                    keysToErase.Add(keyValuePair.Key);

            foreach (int i in keysToErase)
                history.Remove(i);
        }

        #endregion
    }
}
```

## Server side logic

This section details the server-side game logic in Ninja Battle.

### RPC

For Ninja Battle, we register an RPC so we can send information from a client to the server from outside a match. This is done inside the `InitModule` of our `main.ts` file:

```typescript
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer)
{
    initializer.registerRpc("JoinOrCreateMatchRpc", joinOrCreateMatch);
}

let joinOrCreateMatch: nkruntime.RpcFunction = function (context: nkruntime.Context, logger: nkruntime.Logger, nakama: nkruntime.Nakama, payload: string): string
{
    return "response";
}
```

This RPC can then be called from any client with an open socket connection:

```csharp
return await client.RpcAsync(session, rpc, payload);
```

### Match handler

The [`match_handler`](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/Nakama/src/match_handler.ts) is used to manage matches from beginning to end. Like the RPC above, it is also registered inside the `InitModule` of our `main.ts` file:

```typescript
const JoinOrCreateMatchRpc = "JoinOrCreateMatchRpc";
const LogicLoadedLoggerInfo = "Custom logic loaded.";
const MatchModuleName = "match";

function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer)
{
    initializer.registerRpc(JoinOrCreateMatchRpc, joinOrCreateMatch);
    initializer.registerMatch(MatchModuleName, {
        matchInit,
        matchJoinAttempt,
        matchJoin,
        matchLeave,
        matchLoop,
        matchTerminate,
        matchSignal
    });

    logger.info(LogicLoadedLoggerInfo);
}
```

All match logic is held by the `match_handler` in a dictionary that can be cast to an interface. Here we use an interface called `GameState`:

**match_handler.ts**:
```typescript
interface GameState
{
    players: Player[]
    playersWins: number[]
    roundDeclaredWins: number[][]
    roundDeclaredDraw: number[]
    scene: Scene
    countdown: number
    endMatch: boolean
}
```

#### Match loop

The `matchLoop` function is called each tick, on each tick all client messages sent during that period of time are received as a list. With this list you can then decide what to do with the messages: to send them back to the clients or execute custom logic.

**match_handler.ts**:
```typescript
// ...
let matchLoop: nkruntime.MatchLoopFunction = function (context: nkruntime.Context, logger: nkruntime.Logger, nakama: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, messages: nkruntime.MatchMessage[])
{
    let gameState = state as GameState;
    processMessages(messages, gameState, dispatcher, nakama);
    processMatchLoop(gameState, nakama, dispatcher, logger);
    return gameState.endMatch ? null : { state: gameState };
}
// ...
```

When processed by the `processMessages` function if the message code contains custom logic then a function is called, alternatively the default logic is for the message to be send to all clients:

**match_handler.ts**:
```typescript
// ...
function processMessages(messages: nkruntime.MatchMessage[], gameState: GameState, dispatcher: nkruntime.MatchDispatcher, nakama: nkruntime.Nakama): void
{
    for (let message of messages)
    {
        let opCode: number = message.opCode;
        if (MessagesLogic.hasOwnProperty(opCode)) {
            MessagesLogic[opCode](message, gameState, dispatcher, nakama);
        } else {
            messagesDefaultLogic(message, gameState, dispatcher);
        }
    }
}

function messagesDefaultLogic(message: nkruntime.MatchMessage, gameState: GameState, dispatcher: nkruntime.MatchDispatcher): void
{
    dispatcher.broadcastMessage(message.opCode, message.data, null, message.sender);
}
// ...
```

For Ninja Battle, all custom logic is registered in the [`consts.ts`](https://github.com/heroiclabs/ninja-battle-nakama/blob/main/Nakama/src/consts.ts) file.

### Time counters

Ninja Battle `Lobby` and `RoundResults` scenes employ a server run countdown to wait for the next scene:

```typescript
function matchLoopLobby(gameState: GameState, nakama: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher): void
{
    if (gameState.countdown > 0 && getPlayersCount(gameState.players) > 1)
    {
        gameState.countdown--;
        if (gameState.countdown == 0)
        {
            gameState.scene = Scene.Battle;
            dispatcher.broadcastMessage(OperationCode.ChangeScene, JSON.stringify(gameState.scene));
            dispatcher.matchLabelUpdate(JSON.stringify({ open: false }));
        }
    }
}
```

The countdown is set on `matchJoin` with a duration in seconds multiplied to the tick rate of the server:

```typescript
gameState.countdown = DurationLobby * TickRate;
```

### Determining match winners

Due to the use of rollback netcode in Ninja Battle, it is possible for erroneous winner messages to be sent by a client. For the server to trust the client message and determine a winner, all client messages must state the same outcome as occurring on the same tick:

```typescript
function playerWon(message: nkruntime.MatchMessage, gameState: GameState, dispatcher: nkruntime.MatchDispatcher, nakama: nkruntime.Nakama): void
{
    if (gameState.scene != Scene.Battle || gameState.countdown > 0)
        return;

    let data: PlayerWonData = JSON.parse(nakama.binaryToString(message.data));
    let tick: number = data.tick;
    let playerNumber: number = data.playerNumber;
    if (gameState.roundDeclaredWins[tick] == undefined)
        gameState.roundDeclaredWins[tick] = [];

    if (gameState.roundDeclaredWins[tick][playerNumber] == undefined)
        gameState.roundDeclaredWins[tick][playerNumber] = 0;

    gameState.roundDeclaredWins[tick][playerNumber]++;
    if (gameState.roundDeclaredWins[tick][playerNumber] < getPlayersCount(gameState.players))
        return;

    gameState.playersWins[playerNumber]++;
    gameState.countdown = DurationBattleEnding * TickRate;
    dispatcher.broadcastMessage(message.opCode, message.data, null, message.sender);
}
```

### Ending matches

A match ends when any player has reached three victories or all players have disconnected. To end a match, return `null` on any of the match functions:

```typescript
return gameState.endMatch ? null : { state: gameState };
```

### Incrementing user trophies

In Ninja Battle the winning player receives a trophy when the match ends. To store a player's trophies we must first perform a storage read for that user, increment the existing value by the desired amount, and then perform a storage write with the new value.

```typescript
let storageReadRequests: nkruntime.StorageReadRequest[] = [{
    collection: CollectionUser,
    key: KeyTrophies,
    userId: winner.presence.userId
}];

let result: nkruntime.StorageObject[] = nakama.storageRead(storageReadRequests);
var trophiesData: TrophiesData = { amount: 0 };
for (let storageObject of result)
{
    trophiesData = <TrophiesData>storageObject.value;
    break;
}

trophiesData.amount++;
let storageWriteRequests: nkruntime.StorageWriteRequest[] = [{
    collection: CollectionUser,
    key: KeyTrophies,
    userId: winner.presence.userId,
    value: trophiesData
}];

nakama.storageWrite(storageWriteRequests);
```
