Unity Example

Multiplayer authoritative game
in 75 lines of code.

Start with one queue call, one score submit, and one move payload. Then see the full Unity TicTacToe controller that handles matching, turn flow, vote validation, and game end state without a custom backend.

Quick Snippets

Simple API, iterate fast.

// Find Match
// Queue one player into your relay config and start as soon as another client joins.
await Relay.MatchWithAnyone("player1", ExampleConfig.Slug);

// Submit leaderboard score
await Leaderboard.SubmitScore("player1", 5);

// Send a move
Relay.SendJson(index.ToString());
Relay.EndMyTurn();

// React to relay messages
Relay.OnMoveMade += (message, _) => OnMoveMade(message);
Relay.OnVoteFailed += OnVoteFailed;
Relay.OnMatchStarted += (_, _) => { statusText.text = "Game started"; };
Full Example: Turns, Voting, Json

Unity Tic Tac Toe game.

One script handles match start, sending json, per-turn validation, cheat detection, this specific game rules and win resolution. No separate authoritative game server code is required here.

using System.Collections.Generic;
using System.Linq;
using TurnKit.Internal.ParrelSync;
using UnityEngine;
using UnityEngine.UI;

namespace TurnKit.Example
{
    public class TicTacToeControllerExample : MonoBehaviour
    {
        [SerializeField] private List<Text> texts;
        [SerializeField] private InputField playerIdText;
        [SerializeField] private Text gameEndText;
        [SerializeField] private Text statusText;
        [SerializeField] private Toggle allowInvalidMovesToggle;
        private readonly int[][] _winConditions = { new[] {0, 1, 2}, new[] {3, 4, 5}, new[] {6, 7, 8},
            new[] {0, 3, 6}, new[] {1, 4, 7}, new[] {2, 5, 8},
            new[] {0, 4, 8}, new[] {2, 4, 6}
        };

        private void Awake()
        {
#if UNITY_EDITOR
            playerIdText.text = ClonesManager.IsClone() ? "player2" : "player1";
#endif
            Relay.OnMoveMade += (message, _) => OnMoveMade(message);
            Relay.OnVoteFailed += OnVoteFailed;
            Relay.OnMatchStarted += (_, _) => { statusText.text = "Game started"; };
        }

        public async void FindMatch()
        {
            foreach (var text in texts) text.text = "";
            gameEndText.text = "";
            statusText.text = "Waiting for opponent, connect with another client";
            await Relay.MatchWithAnyone(playerIdText.text, ExampleConfig.Slug);
        }

        public void OnCellClick(int index)
        {
            if (!string.IsNullOrEmpty(texts[index].text) && !allowInvalidMovesToggle.isOn) return;
            Relay.SendJson(index.ToString());
            Relay.EndMyTurn();
        }

        private void OnVoteFailed(VoteFailedMessage voteFailedMessage)
        {
            gameEndText.text = "Cheating detected game ended";
            statusText.text = "Game ended";
        }

        private void OnMoveMade(MoveMadeMessage message)
        {
            int cellIndex = int.Parse(message.json);
            bool isLegalMove = string.IsNullOrEmpty(texts[cellIndex].text) || allowInvalidMovesToggle.isOn;
            Relay.Vote(message.moveNumber, isLegalMove);

            if (!isLegalMove) return;

            string symbol = message.moveNumber % 2 == 0 ? "O" : "X";
            texts[cellIndex].text = symbol;
            if (CheckWin()) EndGame($"{symbol} won ! Press Find Match to replay it");
            else if (message.moveNumber == 9) EndGame("A draw, close one! Press Find Match to replay it");
        }

        private void EndGame(string gameEndReason)
        {
            Relay.EndGame();
            gameEndText.text = gameEndReason;
            statusText.text = "Game ended";
        }

        private bool CheckWin() => _winConditions.Any(l => !string.IsNullOrEmpty(texts[l[0]].text) && texts[l[0]].text == texts[l[1]].text && texts[l[0]].text == texts[l[2]].text);
    }
}
Full Example: Hand Hiding, Ownership

Unity Rock Paper Scissor game.

In addition to features covered in previous example here is manipulation of server lists, showcasing hand hiding, ownership of lists

using System.Collections.Generic;
using System.Linq;
using TurnKit.Internal.ParrelSync;
using UnityEngine;
using UnityEngine.UI;

namespace TurnKit.Example
{
    public class RockPaperScissorsControllerExample : MonoBehaviour
    {
        [SerializeField] private InputField playerIdText;
        [SerializeField] private Text gameEndText;
        [SerializeField] private Text statusText;
        [SerializeField] private Text opponentText;

        private RelayList myHand;
        private RelayList opponentHand;
        private RelayList revealedList;
        private bool isSignPicked;
        private readonly string[] validSigns = {"ROCK", "PAPER", "SCISSORS"};
        private void Awake()
        {
#if UNITY_EDITOR
            playerIdText.text = ClonesManager.IsClone() ? "player2" : "player1";
#endif
            Relay.OnMoveMade += OnMoveMade;
            Relay.OnTurnChanged += OnTurnChanged;
            Relay.OnVoteFailed += OnVoteFailed;
            Relay.OnMatchStarted += (msg,initialLists) =>
            {
                myHand = Relay.GetMyLists(ExampleConfig.Tag.hand).First();
                opponentHand = Relay.GetOpponentsLists(ExampleConfig.Tag.hand).First();
                revealedList = Relay.GetMyLists(ExampleConfig.Tag.table).First();
                statusText.text = "Game started";
            };
        }

        public async void FindMatch()
        {
            isSignPicked = false;
            opponentText.text = "";
            gameEndText.text = "";
            statusText.text = "Waiting for opponent, connect with another client";
            await Relay.MatchWithAnyone(playerIdText.text, ExampleConfig.Slug);
        }

        public void SignChosen(string sign)
        {
            if (isSignPicked) return;
            myHand.Spawn(sign);
            Relay.EndMyTurn();
        }

        private void OnVoteFailed(VoteFailedMessage voteFailedMessage)
        {
            gameEndText.text = "Cheating detected game ended";
            statusText.text = "Game ended";
        }

        private void OnMoveMade(MoveMadeMessage msg, IReadOnlyList<RelayList> arg2)
        {
            Relay.Vote(msg.moveNumber, IsMoveValid(msg));
            if (msg.playerId != playerIdText.text) opponentText.text = "Opponent chosen sign";
            else isSignPicked = true;
            
            if (revealedList.Count == 2) //signs are revealed
            {
                var mySign = revealedList.Items.First(x => x.CreatorSlot == Relay.MySlot).Slug;
                var opponentSign = revealedList.Items.First(x => x.CreatorSlot != Relay.MySlot).Slug;
                opponentText.text = $"Opponent chose {opponentSign}";
                if (mySign == opponentSign)
                {
                    EndGame("A draw, close one! Press Find Match to replay it");
                    return;
                }
                
                bool iWin = (mySign == "ROCK" && opponentSign == "SCISSORS") ||
                            (mySign == "PAPER" && opponentSign == "ROCK") ||
                            (mySign == "SCISSORS" && opponentSign == "PAPER");
                EndGame(iWin ? "You won ! Press Find Match to replay it" : "You lost! Press Press Find Match to replay it");
            }
        }

        private bool IsMoveValid(MoveMadeMessage msg)
        {
            foreach (var change in msg.changes)
            {
                if (change.type == ChangeType.SPAWN) // player adding to not owned list is covered by server, it checks ownership
                {
                    if (change.toList.Items.Count > 1) return false; // tried to pick a second sign.
                    if (!validSigns.Contains(change.toList.Items.First().Slug)) return false; // {act.slug} is not a valid sign
                }

                if (change.type == ChangeType.MOVE)
                {
                    if (change.items.Length != 1) return false; // 1 sign moved from each list
                    if (myHand.Items.Count != 0 || opponentHand.Items.Count != 0) return false; // must be no items remaining
                }
            }
            return true;
        }
        
        private void OnTurnChanged(TurnChangedMessage message)
        {
            if (myHand.Items.Count == 1 && opponentHand.Items.Count == 1 && Relay.IsMyTurn) // both players picked their sign
            {
                Debug.Log("Both players ready. Executing Reveal...");
                myHand.Move(SelectorType.ALL).To(revealedList);
                opponentHand.Move(SelectorType.ALL).IgnoreOwnership().To(revealedList);
                Relay.EndMyTurn();
            }
        }
        
        private void EndGame(string gameEndReason)
        {
            Relay.EndGame();
            gameEndText.text = gameEndReason;
            statusText.text = "Game ended";
        }
    }
}