Environment
- Unity 2020.3.1f1 (using declaration を利用しているため,C#8 が利用できる 2020.2 以上が推奨)
- MLAPI v 0.1.0
- UniRx 7.1.0
- UniTask 2.2.4
Custom Messaging Manager
平たく言うと,Photon の RaiseEvent と似たような機能です.
ServerRPC / ClientRPC は NetworkBehaviour 継承クラスでしか使えないのですが, Custom Messaging Handler を利用すれば,NetworkBehaviour を継承しなくても他プレイヤーとデータのやり取りができます. こちらの仕様は Photon の RPC と RaiseEvent との対応が似ています.
How To Use
サンプルスクリプト
ひとまずサンプルスクリプトです.
ChatMessageService.cs [Serviceクラス]
using System; using System.IO; using System.Linq; using Cysharp.Threading.Tasks; using MLAPI; using MLAPI.Messaging; using MLAPI.Serialization.Pooled; using UniRx; using UnityEngine; namespace MLAPIPractice.ChatMessage { public static class CMMConst { public static readonly string CLIENT_TO_SERVER = "CLIENT_TO_SERVER"; public static readonly string SERVER_TO_ALL_CLIENTS = "SERVER_TO_ALL_CLIENTS"; } public class ChatMessageService : MonoBehaviour { public IObservable<(ulong senderId, string message)> OnReceivedMessageAsObservable() => _onReceivedMessageSubject; private readonly Subject<(ulong senderId, string message)> _onReceivedMessageSubject = new Subject<(ulong senderId, string message)>(); private async void Awake() { // NetworkManager がリスナー状態になるまで待つ await UniTask.WaitUntil(() => NetworkManager.Singleton.IsListening); if (NetworkManager.Singleton.IsServer) { // 事前にクライアント側からのストリームを受け取る処理を登録する CustomMessagingManager.RegisterNamedMessageHandler(CMMConst.CLIENT_TO_SERVER, MessageHandler_Server_ReceiveAndSendMessageToAllClients); Debug.Log("U R SERVER. <color=red>YOU CANNOT RECEIVE MESSAGES.</color>"); } if (NetworkManager.Singleton.IsClient) { // 事前にサーバー側からのストリームを受け取る処理を登録する CustomMessagingManager.RegisterNamedMessageHandler(CMMConst.SERVER_TO_ALL_CLIENTS, MessageHandler_Client_ReceiveMessage); Debug.Log("U R CLIENT."); } } private void OnDestroy() { CustomMessagingManager.UnregisterNamedMessageHandler(CMMConst.CLIENT_TO_SERVER); CustomMessagingManager.UnregisterNamedMessageHandler(CMMConst.SERVER_TO_ALL_CLIENTS); } /// <summary> /// チャットメッセージ送信処理 /// </summary> /// <param name="message">テキストメッセージの内容</param> public void SendMessageToServerToAllClients(string message) { // ① まず最初に,クライアント側がサーバー側に向けて送る // PooledNetworkBuffer Stream の生成(送る側なので生成する) using var outputStream = PooledNetworkBuffer.Get(); // PooledNetworkBuffer Stream から Writer を生成 using var writer = PooledNetworkWriter.Get(outputStream); // テキストメッセージを PooledNetworkBuffer Streamに書きだすイメージ writer.WriteStringPacked(message); if (NetworkManager.Singleton.IsListening) { // サーバーのクライアントIDを取得し,ストリームにデータを乗せて送る var serverClientId = NetworkManager.Singleton.ServerClientId; CustomMessagingManager.SendNamedMessage(CMMConst.CLIENT_TO_SERVER, serverClientId, outputStream); } else { Debug.LogError("Cannot send message because network manager is not listening."); } } /// <summary> /// クライアント側から送られてきたテキストメッセージ入りのストリームを読み込み, /// さらに,そのストリームを全クライアントへ送り返す. /// </summary> /// <param name="senderClientId">送信者側のクライアントID(ここでは SendMessageToServerToAllClients を実行したクライアントIDになる)</param> /// <param name="inputStream">PooledNetworkBuffer Stream</param> private void MessageHandler_Server_ReceiveAndSendMessageToAllClients(ulong senderClientId, Stream inputStream) { // ② 次に,サーバー側がクライアント側から送られてきたメッセージを受け取って読み込む using var reader = PooledNetworkReader.Get(inputStream); var message = reader.ReadStringPacked(); // ③ 今度は,送られてきたメッセージをサーバー側が全クライアントに向けて送り返す using var outputStream = PooledNetworkBuffer.Get(); using var writer = PooledNetworkWriter.Get(outputStream); // 送信したクライアントのIDも乗せる writer.WriteUInt64Packed(senderClientId); writer.WriteStringPacked(message); // 全クライアントのIDリストを取得し,ストリームにデータを乗せて送る var targetClientIds = NetworkManager.Singleton.ConnectedClientsList.Select(client => client.ClientId).ToList(); CustomMessagingManager.SendNamedMessage(CMMConst.SERVER_TO_ALL_CLIENTS, targetClientIds, outputStream); } /// <summary> /// サーバー側から送られてきたテキストメッセージ入りのストリームを読み込む. /// </summary> /// <param name="senderServerId">送信者側のクライアントID(このまま利用するとサーバーのIDになるのでここでは使わない)</param> /// <param name="inputStream">PooledNetworkBuffer Stream</param> private void MessageHandler_Client_ReceiveMessage(ulong senderServerId, Stream inputStream) { // ④ 最後に,サーバー側から送られてきたストリームから送り手側のIDとメッセージ内容を読み取る using var reader = PooledNetworkReader.Get(inputStream); var senderClientId = reader.ReadUInt64Packed(); var message = reader.ReadStringPacked(); _onReceivedMessageSubject.OnNext((senderClientId, message)); } } }
ChatMessageView.cs [Viewクラス]
using System; using UniRx; using UnityEngine; using UnityEngine.UI; namespace MLAPIPractice.ChatMessage { public class ChatMessageView : MonoBehaviour { [SerializeField] private InputField inputFieldTextMessage; [SerializeField] private Button buttonTextMessage; public IObservable<string> OnSendTextMessageAsObservable() => _OnSendTextMessageSubject; private readonly Subject<string> _OnSendTextMessageSubject = new Subject<string>(); private void Awake() { // テキストメッセージが入力されてない限り,ボタンを非活性にする inputFieldTextMessage.ObserveEveryValueChanged(field => field.text) .Subscribe(message => buttonTextMessage.interactable = !string.IsNullOrEmpty(message)) .AddTo(this); buttonTextMessage.OnClickAsObservable() .Subscribe(_ => SendTextMessage(inputFieldTextMessage.text)) .AddTo(this); } private void SendTextMessage(string message) { _OnSendTextMessageSubject.OnNext(message); inputFieldTextMessage.text = string.Empty; } public void DisplayTextMessage((ulong, string) messageTuple) { var (senderClientId, message) = messageTuple; Debug.Log($"SenderClientId: {senderClientId}, Message:[{message}]"); } } }
ChatMessagePresenter.cs [Presenter クラス]
using UniRx; using UnityEngine; namespace MLAPIPractice.ChatMessage { public class ChatMessagePresenter : MonoBehaviour { [SerializeField] private ChatMessageProvider _Provider = default; [SerializeField] private ChatMessageView _View = default; private void Awake() { _Provider.OnReceivedMessageAsObservable() .Subscribe(_View.DisplayTextMessage) .AddTo(this); _View.OnSendTextMessageAsObservable() .Subscribe(_Provider.SendMessageToServerToAllClients) .AddTo(this); } } }
DebugLogDisplay.cs [Debug.Log表示用]
using UnityEngine; using System.Collections.Generic; using System.Text; namespace MLAPIPractice.ChatMessage { public class DebugLogDisplay : MonoBehaviour { [SerializeField] private int maxLogCount = 20; [SerializeField] private Rect logArea = new Rect(300, 10, 400, 400); private readonly Queue<string> _logMessages = new Queue<string>(); private readonly StringBuilder _stringBuilder = new StringBuilder(); private void Start() { Application.logMessageReceived += LogReceived; } private void LogReceived(string text, string stackTrace, LogType type) { _logMessages.Enqueue(text); while(_logMessages.Count > maxLogCount) { _logMessages.Dequeue(); } } private void OnGUI() { _stringBuilder.Length = 0; GUI.skin.label.fontSize = 30; foreach (var message in _logMessages) { _stringBuilder.Append(message).Append(System.Environment.NewLine); } GUI.Label(logArea, _stringBuilder.ToString()); } } }
処理の流れ
下手な画像ですみませんが,処理の流れのイメージは以下のようになります.
- PooledNetworkBufferストリームを生成し,特定のクライアントがメッセージをストリームに書き込んでサーバーへ送る
- サーバーは特定のクライアントから送られてきたストリームを読み込んでメッセージを受け取る
- 新しくストリームを生成し,受け取ったメッセージの内容とその送信者IDの情報をストリームに書き込んで全クライアントに送る
- 各々のクライアントはサーバーから送られてきたストリームを読み込んで,メッセージとメッセージ送信者IDの情報を受け取る
ストリームを作ってデータを送る処理を作る
メッセージ送信ボタンを押した時に走る処理です.
/// <summary> /// チャットメッセージ送信処理 /// </summary> /// <param name="message">テキストメッセージの内容</param> public void SendMessageToServerToAllClients(string message) { // ① まず最初に,クライアント側がサーバー側に向けて送る // PooledNetworkBuffer Stream の生成(送る側なので生成する) using var outputStream = PooledNetworkBuffer.Get(); // PooledNetworkBuffer Stream から Writer を生成 using var writer = PooledNetworkWriter.Get(outputStream); // テキストメッセージを PooledNetworkBuffer Streamに書きだすイメージ writer.WriteStringPacked(message); if (NetworkManager.Singleton.IsListening) { // サーバーのクライアントIDを取得し,ストリームにデータを乗せて送る var serverClientId = NetworkManager.Singleton.ServerClientId; CustomMessagingManager.SendNamedMessage(CMMConst.CLIENT_TO_SERVER, serverClientId, outputStream); } else { Debug.LogError("Cannot send message because network manager is not listening."); } }
PooledNetworkBuffer.Get() を通して,PooledNetworkBuffer のストリームを生成します. そのストリームから PooledNetworkWriter を生成し,送りたいメッセージの内容を書きだします. ここでは writer.WriteStringPacked(message) で書きだしています. 最後に,CustomMessagingManager.SendNamedMessage()で,ストリームを送ります.
CustomMessagingManager.SendNamedMessage() の第一引数は識別用のIDのようなものです. 個人的にはストリームの識別用にも連動して考えるとイメージしやすいかも.なので,IDは CLIENT_TO_SERVER にしています. 第二引数はストリームの送り先です.ここではサーバーのIDを指定しています.第三引数は送るストリームです. 一応,NetworkChannel という第四引数もあるんですがここでは割愛します.
ストリームを受け取ってデータを読み込む処理を作る
まずはサーバ側がストリームを受け取る処理を作ります. ここでは受け取った後,新たにストリームを生成して,全クライアントに送り返す処理も行っています.
/// <summary> /// クライアント側から送られてきたテキストメッセージ入りのストリームを読み込み, /// さらに,そのストリームを全クライアントへ送り返す. /// </summary> /// <param name="senderClientId">送信者側のクライアントID(ここでは SendMessageToServerToAllClients を実行したクライアントIDになる)</param> /// <param name="inputStream">PooledNetworkBuffer Stream</param> private void MessageHandler_Server_ReceiveAndSendMessageToAllClients(ulong senderClientId, Stream inputStream) { // ② 次に,サーバー側がクライアント側から送られてきたメッセージを受け取って読み込む using var reader = PooledNetworkReader.Get(inputStream); var message = reader.ReadStringPacked(); // ③ 今度は,送られてきたメッセージをサーバー側が全クライアントに向けて送り返す using var outputStream = PooledNetworkBuffer.Get(); using var writer = PooledNetworkWriter.Get(outputStream); // 送信したクライアントのIDも乗せる writer.WriteUInt64Packed(senderClientId); writer.WriteStringPacked(message); // 全クライアントのIDリストを取得し,ストリームにデータを乗せて送る var targetClientIds = NetworkManager.Singleton.ConnectedClientsList.Select(client => client.ClientId).ToList(); CustomMessagingManager.SendNamedMessage(CMMConst.SERVER_TO_ALL_CLIENTS, targetClientIds, outputStream); }
受け取る側なのでストリーム生成は不要です.PooledNetworkReader.Get (inputStream) で流れてきたストリームを受け取ります. 次に,ストリームから reader.ReadStringPacked() でメッセージの内容を受け取ります. 受け取った後,新たにストリームを生成し,メッセージの内容と送信者のIDをストリームに書き込みまし. そして,サーバから全クライアントに向けて,ストリームを送ります.識別IDは SERVER_TO_ALL_CLIENTS にしました.
/// <summary> /// サーバー側から送られてきたテキストメッセージ入りのストリームを読み込む. /// </summary> /// <param name="senderServerId">送信者側のクライアントID(このまま利用するとサーバーのIDになるのでここでは使わない)</param> /// <param name="inputStream">PooledNetworkBuffer Stream</param> private void MessageHandler_Client_ReceiveMessage(ulong senderServerId, Stream inputStream) { // ④ 最後に,サーバー側から送られてきたストリームから送り手側のIDとメッセージ内容を読み取る using var reader = PooledNetworkReader.Get(inputStream); var senderClientId = reader.ReadUInt64Packed(); var message = reader.ReadStringPacked(); _onReceivedMessageSubject.OnNext((senderClientId, message)); }
最後に,各クライアントがサーバから送られてくるストリームを受け取って内容を読み込む処理を作ります. 処理の流れはこれまでと同じ感じです.
ストリームを受け取ってデータを読み込む処理を事前登録する
private async void Awake() { // NetworkManager がリスナー状態になるまで待つ await UniTask.WaitUntil(() => NetworkManager.Singleton.IsListening); if (NetworkManager.Singleton.IsServer) { // 事前にクライアント側からのストリームを受け取る処理を登録する CustomMessagingManager.RegisterNamedMessageHandler(CMMConst.CLIENT_TO_SERVER, MessageHandler_Server_ReceiveAndSendMessageToAllClients); Debug.Log("U R SERVER. <color=red>YOU CANNOT RECEIVE MESSAGES.</color>"); } if (NetworkManager.Singleton.IsClient) { // 事前にサーバー側からのストリームを受け取る処理を登録する CustomMessagingManager.RegisterNamedMessageHandler(CMMConst.SERVER_TO_ALL_CLIENTS, MessageHandler_Client_ReceiveMessage); Debug.Log("U R CLIENT."); } } private void OnDestroy() { CustomMessagingManager.UnregisterNamedMessageHandler(CMMConst.CLIENT_TO_SERVER); CustomMessagingManager.UnregisterNamedMessageHandler(CMMConst.SERVER_TO_ALL_CLIENTS); }
ストリームを受け取る処理は初期化時に事前に登録する必要があります. 登録方法は CustomMessagingManager.RegisterNamedMessageHandler で, 解除方法は CustomMessagingManager.UnregisterNamedMessageHandler で行います.
注意点
テキストメッセージ送信処理の際に,以下のようにサーバーに向けて送信を行っています.
CustomMessagingManager.SendNamedMessage(CMMConst.CLIENT_TO_SERVER, serverClientId, outputStream);
この処理はMLAPI v0.1.0 段階では自分自身がサーバー(これは自身がホストであることも内包)である場合,動きません.
MLAPI.Messaging.InternalMessageSender.cs のソースコードの中身を見てみると,送信処理はこのようになってます.
internal static void Send(ulong clientId, byte messageType, NetworkChannel networkChannel, NetworkBuffer messageBuffer) { messageBuffer.PadBuffer(); // 自身がサーバーである かつ 送られてきたクライアントIDがサーバークライアントID だった場合,以下の処理を走らせない if (NetworkManager.Singleton.IsServer && clientId == NetworkManager.Singleton.ServerClientId) return; using (var buffer = MessagePacker.WrapMessage(messageType, messageBuffer)) { NetworkProfiler.StartEvent(TickType.Send, (uint)buffer.Length, networkChannel, NetworkConstants.MESSAGE_NAMES[messageType]); NetworkManager.Singleton.NetworkConfig.NetworkTransport.Send(clientId, new ArraySegment<byte>(buffer.GetBuffer(), 0, (int)buffer.Length), networkChannel); ProfilerStatManager.BytesSent.Record((int)buffer.Length); PerformanceDataManager.Increment(ProfilerConstants.ByteSent, (int)buffer.Length); #if !UNITY_2020_2_OR_NEWER NetworkProfiler.EndEvent(); #endif } }
ガード節の部分がまんま該当していますね. なのでホストの場合はこの処理は走らないことにご注意ください. サーバー(ホスト)役はクライアントから受け取ったメッセージをバケツリレーのように全クライアントへ渡しているイメージで.
動作確認
左上がホスト,それ以外はクライアントとして入っています. 2回目にホスト側でメッセージを送っていますが,送信処理がそもそも走れていないためにテキストメッセージが出ないですね. 1,3,4回目で送信処理を行うと,クライアント全員にはテキストメッセージが表示されています.(ホストは出ません.)