🐻
UnityとMagicOnionで、リアルタイム通信
はじめに
今回は、「StreamingHub」の機能を使って、「リアルタイム通信」を試して行きます!!
MagicOnionの使用を考えている方は、この機能がメインになるのではないでしょうか?
MagicOnionのサンプルにも、チャットアプリがありますが、
ここでは、簡略化したチャットアプリを、順を追って作成していきます!!
環境
- Windows 10
- VisualStudio 2019
- Unity 2020.3.13f1
- MagicOnion 4.3.1
- MessagePack-CSharp 2.1.194
- gRPC 2.40.0
- フォント
-
確認端末
- Google Pixel 5 (Androidバージョン 11)
- iPhone 11 Pro Max (iOS 14.6)
前提条件
- 下記の記事の環境構築が完了している、または、同等の環境が構築済みである事
UnityとMagicOnionの環境構築(IL2CPP)
UnityとMagicOnionのAndroid・iOS動作確認編(IL2CPP)
チャットアプリの概要
下記の3機能のみ!!!!!
※後から入室したユーザーには、その前に送信されているメッセージは表示されません
- 入室
- 退出
- メッセージ送信
手順
- TextMeshProで日本語を使用可能にする
- チャット画面の作成
- クライアントとサーバーのインターフェースを作成
- クライアントの実装
- サーバーの実装
- 動作確認
■TextMeshProで日本語を使用可能にする
使用フォントをダウンロード
フォントデータの「SourceHanCodeJP.ttc」をダウロードして、
任意の場所に「Fronts」フォルダを作成して、ファイルを配置
「Text Mesh Pro」にて、フォントアセットを作成
- 「Window -> TextMeshPro -> Font Asset Creator」を押下
- 下記を設定して、「Generate Font Atlas」を押下
項目 | 設定値 |
---|---|
Source Font File | 上記でダウンロードした「SourceHanCodeJP.ttc」を指定 |
Sampling Point Size | Custom Size 48 |
Padding | 5 |
Packing Method | Fast |
Atlas Resolution | 8192 × 8192 |
Character Set | Custom Characters |
Select Font Asset | None |
Custom Character List | ほとんどの日本語を網羅した文字セットを、 公開してくれてい方がいるので、使用させていただきました!! UnityのText Mesh Proでほぼ全ての日本語を表示させる |
Render Mode | SDFAA |
- 処理が完了したら、「Save as...」を押下
- 「SourceHanCodeJP.ttc」と同じディレクトリを指定して、「保存」を押下
- 「SourceHanCodeJP SDF.asset」が、作成されている事を確認
■チャット画面の作成
チャットアプリ用の新規シーンを作成
入室画面を作成
項目 | 設定値 |
---|---|
NameInput | UI -> Input Field - TextMeshPro |
JoinButton | UI -> Button - TextMeshPro |
チャット画面を作成
項目 | 設定値 |
---|---|
CommentScrollView | UI -> Scroll View |
ChatComment | UI -> Input Field - TextMeshPro |
MessageInput | UI -> Input Field - TextMeshPro |
SendButton | UI -> Button - TextMeshPro |
■クライアントとサーバーのインターフェースを作成
クライアントインターフェース(サーバー ⇒ クライアント)
下記のサーバーからクライアントへの通知を受け取る
- 入室通知
- 退出通知
- メッセージ通知
Assets/KumattaApp/Scripts/MagicOnion/Server/Hubs/IChatAppHubReceiver.cs
namespace KumattaAppServer.Hubs
{
public interface IChatAppHubReceiver
{
/// <summary>
/// 入室通知
/// </summary>
/// <param name="userName">ユーザー名</param>
void OnJoin(string userName);
/// <summary>
/// 退出通知
/// </summary>
/// <param name="userName">ユーザー名</param>
void OnLeave(string userName);
/// <summary>
/// メッセージ通知
/// </summary>
/// <param name="userName"></param>
/// <param name="message"></param>
void OnSendMessage(string userName, string message);
}
}
サーバーインターフェース(クライアント ⇒ サーバー)
下記のタイミングで、クライアントからサーバーへリクエストを行い、他のユーザーに通知
- 入室
- 退出
- メッセージ送信
Assets/KumattaApp/Scripts/MagicOnion/Server/Hubs/IChatAppHub.cs
using MagicOnion;
using System.Threading.Tasks;
namespace KumattaAppServer.Hubs
{
public interface IChatAppHub : IStreamingHub<IChatAppHub, IChatAppHubReceiver>
{
/// <summary>
/// 入室通知
/// </summary>
/// <param name="roomName">ルーム名</param>
/// <param name="userName">ユーザー名</param>
/// <returns></returns>
Task JoinAsync(string roomName, string userName);
/// <summary>
/// 退室通知
/// </summary>
/// <returns></returns>
Task LeaveAsync();
/// <summary>
/// メッセージ通知
/// </summary>
/// <param name="userName">ユーザー名</param>
/// <param name="message">メッセージ</param>
/// <returns></returns>
Task SendMessageAsync(string userName, string message);
}
}
■クライアントの実装
gRPCの初期化処理を追加
下記のオプションを設定
項目 | 設定値 | 備考 |
---|---|---|
grpc.keepalive_time_ms | 5000 | キープアライブを5秒単位 |
grpc.keepalive_timeout_ms | 5000 | キープアライブのタイムアウトを5秒 |
Assets/KumattaApp/Scripts/MagicOnion/Client/Generated/MagicOnionLoad.cs
using Grpc.Core;
using MagicOnion.Unity;
using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;
public class MagicOnionLoad
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void RegisterResolvers()
{
StaticCompositeResolver.Instance.Register(
MagicOnion.Resolvers.MagicOnionResolver.Instance,
MessagePack.Resolvers.GeneratedResolver.Instance,
StandardResolver.Instance
);
MessagePackSerializer.DefaultOptions = MessagePackSerializer.DefaultOptions
.WithResolver(StaticCompositeResolver.Instance);
}
// gRPCの初期化処理を追加
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void OnRuntimeInitialize()
{
// gRPCの初期化
GrpcChannelProviderHost.Initialize(new DefaultGrpcChannelProvider(new[]
{
// キープアライブを5秒単位に送信(デフォルト2時間)
new ChannelOption("grpc.keepalive_time_ms", 5000),
// キープアライブのpingタイムアウトを5秒(デフォルト20秒)
new ChannelOption("grpc.keepalive_timeout_ms", 5000),
}));
}
}
クライアント処理作成
ボタンイベント、サーバーへのリクエスト処理を作成
サーバーからの通知を受け取る「IChatAppHubReceiver」のインターフェスを実装
Assets/KumattaApp/Scripts/ChatApp/ChatApp.cs
using Grpc.Core;
using MagicOnion;
using MagicOnion.Client;
using System.Threading;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace KumattaAppServer.Hubs
{
public class ChatApp : MonoBehaviour, IChatAppHubReceiver
{
#region 入室ページ
/// <summary>
/// 入室ページ
/// </summary>
[SerializeField]
private GameObject joinPage;
/// <summary>
/// ユーザー名入力
/// </summary>
private TMP_InputField nameInput;
#endregion
#region チャットページ
/// <summary>
/// チャットページ
/// </summary>
[SerializeField]
private GameObject chatPage;
/// <summary>
/// チャット表示
/// </summary>
private TextMeshProUGUI chatComment;
/// <summary>
/// メッセージ入力
/// </summary>
private TMP_InputField messageInput;
#endregion
#region MagicOnion用
/// <summary>
/// 通信キャンセル用トークン
/// </summary>
private CancellationTokenSource shutdownCancellation = new CancellationTokenSource();
/// <summary>
/// 接続チャンネル
/// </summary>
private ChannelBase channel;
/// <summary>
/// サーバー呼出し用
/// </summary>
private IChatAppHub streamingClient;
#endregion
/// <summary>
/// ルーム名
/// </summary>
private string roomName = "ChatAPP";
/// <summary>
/// ユーザー名
/// </summary>
private string userName = "名無し";
private void Start()
{
// 画面を非表示
joinPage.SetActive(false);
chatPage.SetActive(false);
// 入室ページ設定
nameInput = joinPage.GetComponentInChildren<TMP_InputField>(true);
var joinButton = joinPage.GetComponentInChildren<Button>(true);
joinButton.onClick.AddListener(OnClick_JoinButton);
// チャットルーム設定
chatComment = chatPage.GetComponentInChildren<TextMeshProUGUI>(true);
chatComment.text = "";
messageInput = chatPage.GetComponentInChildren<TMP_InputField>(true);
var sendButton = chatPage.GetComponentInChildren<Button>(true);
sendButton.onClick.AddListener(OnClick_SendButton);
// 入室画面を表示
joinPage.SetActive(true);
}
private async void OnDestroy()
{
// 通信キャンセル
shutdownCancellation.Cancel();
// 切断処理
if (streamingClient != null) await streamingClient.DisposeAsync();
if (channel != null) await channel.ShutdownAsync();
}
#region ボタン処理
/// <summary>
/// 入室
/// </summary>
private async void OnClick_JoinButton()
{
if (!string.IsNullOrEmpty(nameInput.text))
{
userName = nameInput.text;
}
// サーバーへ接続
channel = GrpcChannelx.ForAddress("http://111.111.111.111:5001");
streamingClient = await StreamingHubClient.ConnectAsync<IChatAppHub, IChatAppHubReceiver>(channel, this, cancellationToken: shutdownCancellation.Token);
await streamingClient.JoinAsync(roomName, userName);
joinPage.SetActive(false);
chatPage.SetActive(true);
}
/// <summary>
/// メッセージ送信
/// </summary>
private async void OnClick_SendButton()
{
await streamingClient.SendMessageAsync(userName, messageInput.text);
messageInput.text = "";
}
#endregion
#region MagicOnion サーバー⇒クライアントの受信
/// <summary>
/// 入室通知
/// </summary>
/// <param name="userName">ユーザー名</param>
public void OnJoin(string userName)
{
chatComment.text = $"{chatComment.text}{userName}さんが入室しました。\n";
}
/// <summary>
/// 退出通知
/// </summary>
/// <param name="userName">ユーザー名</param>
public void OnLeave(string userName)
{
chatComment.text = $"{chatComment.text}{userName}さんが退出しました。\n";
}
/// <summary>
/// メッセージ通知
/// </summary>
/// <param name="userName"></param>
/// <param name="message"></param>
public void OnSendMessage(string userName, string message)
{
chatComment.text = $"{chatComment.text}{userName}:{message}\n";
}
#endregion
}
}
■サーバーの実装
クライアントのリクエストを受け取る「IChatAppHub」のインターフェスを実装
KumattaApp/KumattaApp-Server/Hubs/ChatAppHub.cs
using MagicOnion.Server.Hubs;
using System.Threading.Tasks;
namespace KumattaAppServer.Hubs
{
public class ChatAppHub : StreamingHubBase<IChatAppHub, IChatAppHubReceiver>, IChatAppHub
{
/// <summary>
/// ルーム
/// </summary>
private IGroup room;
/// <summary>
/// ユーザー名
/// </summary>
private string userName;
/// <summary>
/// 入室通知
/// </summary>
/// <param name="roomName">ルーム名</param>
/// <param name="userName">ユーザー名</param>
/// <returns></returns>
public async Task JoinAsync(string roomName, string userName)
{
this.userName = userName;
room = await Group.AddAsync(roomName);
Broadcast(room).OnJoin(userName);
}
/// <summary>
/// 退室通知
/// </summary>
/// <returns></returns>
public async Task LeaveAsync()
{
await room.RemoveAsync(this.Context);
Broadcast(room).OnLeave(userName);
}
/// <summary>
/// メッセージ通知
/// </summary>
/// <param name="userName">ユーザー名</param>
/// <param name="message">メッセージ</param>
/// <returns></returns>
public async Task SendMessageAsync(string userName, string message)
{
Broadcast(room).OnSendMessage(userName, message);
await Task.CompletedTask;
}
/// <summary>
/// 切断通知
/// </summary>
/// <returns></returns>
protected override ValueTask OnDisconnected()
{
BroadcastExceptSelf(room).OnLeave(userName);
return CompletedTask;
}
}
}
動作確認
- 確認端末にアプリをインストール
- サーバー起動
- 各端末を起動して入室
- 各端末からメッセージ送信
- いずれかの端末のアプリを落とす
「入室」、「メッセージ」、「退出」の同期が取れていれば成功!!
あとがき
ここまで読んでいただきありがとうございます!!!
次回は、フィルターにするか、それ以外にするか悩み中です!!!
Discussion