👻

Unityでオンラインゲームを作るためのRiptideライブラリ入門? #1

2023/02/03に公開

はじめに

こんにちは。midraです。
今回はUnityでオンラインゲームを作るためのライブラリであるRiptideを使ってみたいと思います。'
何か面白いライブラリがないかGithubのExploreを見ていたら、偶然見つけました。
筆者はMirrorやFishNetといったライブラリを使ったことがありますが、Riptideはこれらのライブラリとは違う特徴を持っているので、紹介させていただきます。

検証環境

  • Unity 2022.1.22f1
  • Riptide 2.0.0
  • UniTask 2.3.3

サンプルプロジェクト

概要

Riptideは主にオンラインゲームで利用するために設計されたもですがUnityだけでなく、.NET Coreや.NET Frameworkでも利用できます。
2021年5月27日v0.1.0がリリースされていて、この記事の作成段階での最新版は2022年10月9日リリースされたV2.0.0のものになっております。
開発者はTom Weiland氏で、Kevin Kaymak氏の動画でからネットワークの構築方法を学び
Riptideの開発に取り組み始めたようです。公式ドキュメントの概要から推測にこれを開発していく中でネットワークの実装に関する学習もかねているのではないかと思いました。

"This is ideal if you like to be in control of your code and know what's going on under the hood."

"これは、コードを制御し、内部で何が起こっているかを知りたい場合に理想的です。"

https://riptide.tomweiland.net/manual/overview/about-riptide.html

導入方法

Unity Package Manager

UnityのPackage Managerから導入することができます。

  1. UnityのメニューバーからWindow -> Package Managerを選択します。
  2. +ボタンを押してAdd package from git URL...を選択します。
  3. 次の URL を入力してください: https://github.com/RiptideNetworking/Riptide.git?path=/Packages/Core#2.0.0。v2.0.0 以外のバージョンをインストールするには、 の2.0.0後の#を選択したバージョン番号に置き換えます。
  4. Addボタンを押してインストールします。

その他導入方法

(https://riptide.tomweiland.net/manual/overview/installation.html)

使い方

できることは単純明快で、サーバーの起動クライアントの接続メッセージの送受信クライアントの接続状況に関するコールバック となっています。(まだまだ機能はあるかもしれませんがオンラインゲームで必要な最低限の機能だけ紹介していきます。)
今回はクライアントサーバーモデルの専用サーバー型を採用していきます。

プロジェクトの作成

サンプルを見てもらったらわかる通りクライアントとサーバーのプロジェクトを分けて作成する必要があります。そして両方のプロジェクトにRiptideを導入する必要があります。

サーバーの起動

サーバー側のプロジェクトでNetworkManagerを作成します。

    public class NetworkManager : MonoBehaviour
    {
        //起動するサーバーのポート番号と最大接続数を設定
        [SerializeField] private ushort port;
        [SerializeField] private ushort maxConnections;
        //サーバーのインスタンス(RiptideのAPIにアクセスするために必要)
        private Server _server;

        private void Start()
        {
            QualitySettings.vSyncCount = 0;
            Application.targetFrameRate = 30;

#if UNITY_EDITOR
            //Riptide専用のエラーログを出力できるようにする
            RiptideLogger.Initialize(Debug.Log, Debug.Log, Debug.LogWarning, Debug.LogError, false);
#else
            //コンソール初期化
            Console.Title = "Server";
            Console.Clear();
            //(https://docs.unity3d.com/ja/2018.4/ScriptReference/Application.SetStackTraceLogType.html)
            // スタックトレースがログに出力されません
            Application.SetStackTraceLogType(UnityEngine.LogType.Log, StackTraceLogType.None);
            RiptideLogger.Initialize(Debug.Log, true);
#endif
            //サーバーのインスタンスを生成
            _server = new Server();
            //サーバー接続時、切断時のイベントを登録
            _server.ClientConnected += ClientConnected;
            _server.ClientDisconnected += ClientDisconnected;
            //サーバーの起動
            _server.Start(port, maxConnections);
        }

        private void FixedUpdate()
        {
            // サーバーが接続を受け入れてメッセージを処理できるようにする
            _server.Update();
        }

一つまみ

            QualitySettings.vSyncCount = 0;
            Application.targetFrameRate = 30;

VSyncというのは「モニターのリフレッシュレート(つまり画面を更新するタイミング)に合わせてゲーム画面を描画する」機能のことです。
サーバー側なので画面に描画する必要はないので、VSyncを無効にしています。
(https://sleepygamersmemo.blogspot.com/2017/05/unity-fixed-frame-rate.html)

デフォルトのフレームレートは達成可能な最大フレームレートになっているので、30に設定しています。
(https://bibinbaleo.hatenablog.com/entry/2021/01/31/215652)

クライアントの接続

クライアント側のプロジェクトでNetworkManagerを作成します。

    public class NetworkManager : MonoBehaviour
    {
        //サーバーのIPアドレスとポート番号を設定
        //今回はローカルホストを指定
        [SerializeField] private string ip = "127.0.0.1";
        [SerializeField] private ushort port = 7777;
        //クライアントのインスタンス(RiptideのAPIにアクセスするために必要)
        private Client _client;

        private void Awake()
        {
            //Riptide専用のエラーログを出力できるようにする
            RiptideLogger.Initialize(Debug.Log, Debug.Log, Debug.LogWarning, Debug.LogError, false);

            //クライアントのインスタンスを生成  
            _client = new Client();
            //クライアント接続時、切断時などのイベントを登録
            _client.Connected += MyClientConnect;
            _client.ClientConnected += OtherClientConnect;
            _client.ConnectionFailed += FailedToConnect;
            _client.ClientDisconnected += PlayerLeft;
            _client.Disconnected += DidDisconnect;
        }
        
        private void FixedUpdate()
        {
            // クライアントがサーバーからの接続を受け入れてメッセージを処理できるようにする
            _client.Update();
        }
        //サーバーに接続したいときに呼び出す
        public void Connect()
        {
            _client.Connect($"{ip}:{port}");
        }
        //サーバーとの接続を切断したいときに呼び出す
        public void Disconnect()
        {
            _client.Disconnect();
        }

メッセージの送受信(変数の同期からサーバー、クライアント共に処理のリクエストを送る事ができる)

処理の順序一例

名前を入力してサーバーに送信し、その更新処理を各クライアントに送信する。

クライアント -> サーバー -> クライアント

Unityで実装するとしたら…

  1. クライアント側でInputFieldに名前を入力する
  2. クライアント側でボタンを押し名前情報をサーバーに送信する
  3. サーバー側で名前情報を受け取り、名前情報を更新する
  4. サーバー側で名前情報を各クライアントに送信する

サンプルコード

クライアントサイド

View.cs

    public class View : MonoBehaviour
    {
        [SerializeField] private TMP_InputField _inputField;
        [SerializeField] private Button _connectButton;
        [SerializeField] private TMP_Text _nameText;
        
        private readonly UnityEvent<string> _connectEvent = new();

        public UnityEvent<string> OnConnectButtonClicked => _connectEvent;
        
        public string Name
        {
            set => _nameText.text = value;
        }
        
        private void Awake()
        {
            _connectButton.onClick.AddListener(() => OnConnectButtonClicked?.Invoke(_inputField.text));
        }
    }

Manager.cs

    public class Manager : MonoBehaviour
    {
        [SerializeField] private NetworkManager _networkManager;
        [SerializeField] private View _view;
        
        private void Start()
        {
            _view.OnConnectButtonClicked.AddListener((name) =>
            {
                _networkManager.SendSetPlayerNameMessage(name);
            });
            
            _networkManager.OnSetName.AddListener((id, name) =>
            {
                //名前を更新する処理
                Name = name;
                
                //idを受け取ることで、自分の名前を更新したか、他のプレイヤーの名前を更新したかを判断できるような実装も可能になる
            });
        }
    }

NetworkManager.cs

    public enum ServerToClientId : ushort
    {
        //"= 1"は確実に付ける。
        SetClientPlayerName = 1,
        AddScore,
    }

    public enum ClientToServerId : ushort
    {
        SetServerPlayerName = 1,
        AddScore,
    }

    public class NetworkManager : MonoBehaviour
    {
    
    //--------------省略----------------
    
        private static readonly UnityEvent<ushort, string> _onSetName = new();
        public UnityEvent<ushort, string> OnSetName => _onSetName;
    
        //サーバーに名前を送信する
        public void SendSetPlayerNameMessage(string name)
        {
            //サーバーサイドに送るためにMesageを作成
            //サーバーサイドのRiptide専用の受信メソッドにつくattributeのID("ClientToServerId.SetPlayerName")を指定
            var message = Message.Create(MessageSendMode.Reliable, ClientToServerId.SetPlayerName);
            //名前を送信
            message.AddString(username);
            //サーバーに名前を送信
            _client.Send(message);
        }
        
        //サーバーから名前を受信する
        //staticメソッドでなければならない
        [MessageHandler((ushort)ServerToClientId.SetClientPlayerName)]
        private static void RecieveSetPlayerName(Message message)
        {
            //サーバーから受け取ったメッセージから名前とIDを取得
            var id = message.GetUShort();
            var username = message.GetString();
            
            //staticなUnityEventを呼び出す
            //IDと名前を引数に渡す
            _onSetName?.Invoke(id, username);
        }
    }
        

サーバーサイド

Model.cs

    public class Model : MonoBehaviour
    {
        private ushort _id;
        private AsyncReactiveProperty<string> _username = new ("");
        
        public ushort Id => _id;
        public AsyncReactiveProperty<string> Username => _username;

        public ushort SetId
        {
            set => _id = value;
        }
        
        public string SetUsername
        {
            set => _username.Value = value;
        }
    }

Manager.cs

    public class Manager : MonoBehaviour
    {
        [SerializeField] private NetworkManager _networkManager;
        [SerializeField] private Model _model;

        private void Start()
        {
            _networkManager.OnSetName.AddListener((id, name) =>
            {
                //名前とidを更新する処理
                _model.SetId = id;
                _model.SetUsername = name;
            });
            
            _model.Username.Subscribe(name =>
            {
                //名前を更新したら、クライアントに名前を送信する
                _networkManager.SendSetPlayerNameMessage(_model.Id,name);
            });
        }
    }

NetworkManager.cs

    //サーバー側、クライアント側はともに同じ物を用意する事を忘れずに!!
    public enum ServerToClientId : ushort
    {
        //"= 1"は確実に付ける。
        SetClientPlayerName = 1,
        AddScore,
    }

    public enum ClientToServerId : ushort
    {
        SetServerPlayerName = 1,
        AddScore,
    }

    public class NetworkManager : MonoBehaviour
    {
    //--------------省略----------------

        private static readonly UnityEvent<ushort, string> _onSetName = new();
        public UnityEvent<ushort, string> OnSetName => _onSetName;

        public void SendSetPlayerNameMessage(ushort userid, string username)
        {
            //クライアントサイドに送るためにMesageを作成
            var message = Message.Create(MessageSendMode.Reliable, ServerToClientId.SetClientPlayerName);
            //名前とIDを送信
            message.AddUShort(userid);
            message.AddString(username);
            //クライアントに名前を送信。第二引数は送信先のクライアントID
            _server.Send(message, userid);
            
            //全てのクライアントに送信する場合は
            //_server.SendToAll(message);
            
            //特定のクライアント以外に送信する場合は
            //_server.SendToAll(message, userid);
        }
        
        //クライアントから名前とそのクライアント独自が持つIDを受信する
        //staticメソッドでなければならない
        [MessageHandler((ushort)ClientToServerId.SetServerPlayerName)]
        private static void RecieveSetPlayerName(ushort fromClientId, Message message)
        {
            var username = message.GetString();
            //クライアントIDはクライアントがサーバーに接続した際にサーバー側で自動的に割り振られる
            //メッセージを送信する際に自動的にクライアントのIDが付与してくるのでクライアント側でIDを送るための明示的な処理は不要
            _onSetName.Invoke(fromClientId, username);
        }
    }

クライアントとサーバー側の接続状況に応じたコールバックについて

クライアント側の持つ主要なコールバック

            //自分がサーバーに接続完了した時
            _client.Connected += MyClientConnect;
            //他のクライアントがサーバーに接続した時
            _client.ClientConnected += OtherClientConnect;
            //自分がサーバーに接続失敗した時
            _client.ConnectionFailed += FailedToConnect;
            //他のクライアントがサーバーから切断した時
            _client.ClientDisconnected += PlayerLeft;
            //自分がサーバーから切断した時
            _client.Disconnected += DidDisconnect;

サーバー側の持つ主要なコールバック

            //クライアントがサーバーに接続した時
            _server.ClientConnected += OnClientConnected;
            //クライアントがサーバーから切断した時
            _server.ClientDisconnected += OnClientDisconnected;

サンプルプロジェクトについて

これまでの内容をまとめたサンプルプロジェクトを作成しました。
オンラインゲームの設計についてはまだまだ経験が少ないので、間違いや改善点があればご指摘いただけると幸いです。
ざっくりとまとめた図は以下の通りです。
Architecture

見ての通りサーバー側とクライアント側は共にMVPパターンぽい設計になっています。本来であればサーバー、クライアントそれぞれにプレゼンターで直接NetworkManagerを呼び出さずに、
ネットワークからのデータを受け取る専用のモデルを生成し、そのモデルをプレゼンターが監視するようにさせたかったです。

まとめ

RiptideはAPIの数が少なく、利用方法が単純明快ですが、他のライブラリ(MirrorやFishNet、Photonなど)とは異なり、コンポーネントが用意されていないため、初心者には難しいと感じることがあります。
そういう理由もあってか、NetworkLibraryに関する理解を深めることができました。まだまだ新しいプロジェクトであると思うので、今後も機能が追加されていくことを期待しています。

MidraLab(ミドラボ)

Discussion