Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9749

Unity + .NET Core + MagicOnion v3 環境構築ハンズオン

$
0
0

はじめに

MagicOnion は一度環境を構築してしまえば、触り心地が良くてとても使いやすいフレームワークだと思います。

しかし初期構築がやや複雑で、特に Unity と サーバー(.NET Core) の両方の経験が無い方が挑戦した場合はどこかでつまづいてしまうことも多いのではないかと思いました。

そこで、どちらかの経験が無い方でもつまずかずにポチポチと環境構築できる資料を目指して書いたのがこの記事になります。

もし途中でつまづくところがありましたら Twitter で教えていただけると喜びます。

ハンズオンの所要時間は Unity と VisualStudio をインストール済みの状態から開始して、30分~1時間程度です。

目次

1. 環境について

本記事を書くにあたって使用した OS、ツール、ソフトウェアのバージョンです。

2. Unity 側の構築

2.1. プロジェクトの新規作成、PlayerSettings の変更、各種フォルダの作成

今回は 3D プロジェクトを作成します。
プロジェクト名は任意ですが、サーバーサイドのプロジェクトと見分けやすいように Sample.Unityとします。

保存先は任意の保存先を入力してください。

image.png

Unity が起動したら PlayerSettingsを開きます。

image.png

Playerを選択して、下記の2箇所を変更します。

  • APICompatibilityLevel を .NET 4.xに変更する
  • Allow unsafe Code にチェックをいれる

image.png

次に下記のフォルダを作成します。
※Sample.Unity は自分のプロジェクト名に読み替えてください。

  • Sample.Unity\Assets\Editor
  • Sample.Unity\Assets\Plugins
  • Sample.Unity\Assets\Scripts
  • Sample.Unity\Assets\Scripts\Generated
  • Sample.Unity\Assets\Scripts\ServerShared
  • Sample.Unity\Assets\Scripts\ServerShared\Hubs
  • Sample.Unity\Assets\Scripts\ServerShared\MessagePackObjects
  • Sample.Unity\Assets\Scripts\ServerShared\Services

フォルダ構成が下記のようになっていることを確認します。

image.png

2.2. MagicOnion のインストール

GitHubから MagicOnion.Client.Unity.unitypackageをダウンロードします。

image.png

ダウンロードが終わったらダブルクリックしてインポートします。

image.png

2.3. MessagePack for C# のインストール

GitHubから MessagePack.Unity.2.1.152.unitypackageをダウンロードします。

image.png

ダウンロードが終わったらダブルクリックしてインポートします。
MagicOnion と重複するファイルがあるため警告が表示されますがこのままインポートします。

image.png

2.4. gRPC のインストール

gRPC の Daily Builds(2019/08/01)から grpc_unity_package.2.23.0-dev.zipをダウンロードします。

image.png

ダウンロードが終わったら展開し、以下のフォルダを Sample.Unity\Assets\Pluginsにコピーします。

  • Google.Protobuf
  • Grpc.Core
  • Grpc.Core.Api

image.png

2.5. サーバーへ接続するスクリプトの用意

Assets\ScenesSampleControllerスクリプトを作成します。
image.png

SampleController のコードは以下をコピペしてください。
Start() でサーバーへ接続し、OnDestroy() で切断するようになっています。

usingGrpc.Core;usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;publicclassSampleController:MonoBehaviour{privateChannelchannel;voidStart(){this.channel=newChannel("localhost:12345",ChannelCredentials.Insecure);}asyncvoidOnDestroy(){awaitthis.channel.ShutdownAsync();}}

SampleScene へ空の GameObject を追加します。

image.png

追加した GameObject に SampleController を AddComponent します。

image.png

2.6. Unity ⇔ サーバー間のコード共有の動作確認用のクラスの用意

MagicOnion を利用する際は Unity 側で作成したクラスなどをサーバーサイドと共有して使うことが一般的です。
今回は Assets\Scripts\ServerShared 以下に作成したスクリプトをすべて共有する設定を行います。
コード共有の設定は後でサーバー側で設定を行いますが、先に動作確認用のクラスを用意しておきます。

Assets\ServerShared\MessagePackObjectsPlayerスクリプトを作成します。

image.png

Player のコードは以下をコピペしてください。

usingMessagePack;usingUnityEngine;namespaceSample.Shared.MessagePackObjects{[MessagePackObject]publicclassPlayer{[Key(0)]publicstringName{get;set;}[Key(1)]publicVector3Position{get;set;}[Key(2)]publicQuaternionRotation{get;set;}}}

MagicOnion を使用する場合、Client ⇔ サーバー間の通信で使用するクラスはこのような MessagePackObject として定義します。

MessagePackObject として定義するために必要なことは下記の2つだけですので覚えておきましょう。

  • class に MessagePackObjectAttribute を付与する
  • 各プロパティに KeyAttribute を付与して番号を順番にふる

Unity 側の構築はここまでです。

3. サーバー側の構築

続いてサーバー側の構築作業を進めます。

3.1. ソリューションへサーバー側のプロジェクトを追加

ソリューションを右クリックして、新しいプロジェクトを追加します。
image.png

コンソールアプリ(.NET Core)を選択し、次へをクリックします。

image.png

任意のプロジェクト名を入力します。(ここでは Sample.Server としました)
場所はプロジェクトのルートを指定してください。
image.png

3.2. MagicOnion のインストール

NuGet から MagicOnion をインストールします。
ツール -> NuGet パッケージマネージャー -> ソリューションの NuGet パッケージの管理 を開きます。
image.png

参照 をクリックし、 MagicOnion を検索します。
検索結果から MagicOnion.Hosting を選択し、Sample.Server にチェックをいれます。
バージョンは 3.0.12 を選択し、インストールします。
image.png

変更のプレビューが表示されるので OK を押します。
image.png

ライセンスへの同意を求められるので同意します。
image.png

3.3. Program.cs の編集

Program.cs を下記の内容で上書き保存します。
これでサーバー側のプロジェクトを起動すると MagicOnion が起動するようになります。

usingMagicOnion.Hosting;usingMicrosoft.Extensions.Hosting;usingSystem;usingSystem.Threading.Tasks;namespaceSample.Server{classProgram{staticasyncTaskMain(string[]args){awaitMagicOnionHost.CreateDefaultBuilder().UseMagicOnion().RunConsoleAsync();}}}

3.4. ソリューションへクラスライブラリのプロジェクトを追加

このクラスライブラリを使って Unity 側とサーバー側のコード共有を行います。

ソリューションを右クリックして、新しいプロジェクトを追加します。
image.png

クラスライブラリ(C# .NET Standard)を選択します。
image.png

任意のプロジェクト名を入力します。(ここでは Sample.Shared としました)
場所はプロジェクトのルートを指定してください。
image.png

自動的に作成される Class1.cs は不要なので削除します。
image.png

3.5. クラスライブラリへ MagicOnion.Abstractions をインストール

先ほどの MagicOnion のインストールと同じ要領で、NuGet から MagicOnion.Abstractions を検索してインストールします。
対象のプロジェクトは Sample.Shared を選択し、バージョンは 3.0.12 を選択します。
image.png

3.6. クラスライブラリへ MessagePack.UnityShims をインストール

同じ要領で Sample.Shared へ MessagePack.UnityShims をインストールします。
image.png

3.7. クラスライブラリから Unity 側のコードを参照する

Sample.Shared をダブルクリックして Sample.Shared.csproj を開き、赤枠部分の設定を追加します。
image.png

追加する設定はこちらをコピペしてください。
※Sample.Unity の部分は自分の Unity 側のプロジェクト名に読み替えてください。

<ItemGroup><CompileInclude="..\Sample.Unity\Assets\Scripts\ServerShared\**\*.cs"/></ItemGroup>

この状態でソリューションエクスプローラーを見てみると、Unity 側で用意した ServerShared フォルダ以下のファイルがクラスライブラリに読み込めたことがわかります。
image.png

3.8. サーバー側のプロジェクトからクラスライブラリを参照する

Sample.Server を右クリックし、追加、プロジェクト参照を選択します。
image.png

Sample.Shared を選択して OK を押します。
image.png

これで Unity とコード共有したサーバーの用意ができました。

4. API の実装と動作確認

ここからは API の実装と動作確認を行います。
MagicOnion は普通の API 通信とリアルタイム通信の2種類の通信が利用できますので、それぞれをテストしてみます。

4.1. 普通の API通信

まずは普通の API 通信から試してみます。

4.1.1. Unity 側で API の定義を作る

Assets\Scripts\ServerShared\Services 以下に SampleService スクリプトを作ります。
image.png

SampleService の中身は以下をコピペして保存してください。
今回は足し算をしてくれる API と掛け算をしてくれる API を定義してみます。

usingMagicOnion;namespaceSample.Shared.Services{publicinterfaceISampleService:IService<ISampleService>{UnaryResult<int>SumAsync(intx,inty);UnaryResult<int>ProductAsync(intx,inty);}}

4.1.2. サーバー側で API を実装する

Sample.Server を右クリックして、新しいフォルダを追加します。
名前は Services とします。

image.png

image.png

次に Services フォルダ内にクラスを追加します。

image.png

名前は SampleService.cs とします。
image.png

SampleService.cs の中身は以下をコピペして保存してください。

usingMagicOnion;usingMagicOnion.Server;usingSample.Shared.Services;namespaceSample.Server.Services{publicclassSampleService:ServiceBase<ISampleService>,ISampleService{publicUnaryResult<int>SumAsync(intx,inty){returnUnaryResult(x+y);}publicUnaryResult<int>ProductAsync(intx,inty){returnUnaryResult(x*y);}}}

4.1.3. Unity 側で API を呼ぶコードを実装する

SampleController に SampleService を呼び出すコードを追加します。
下記のコードをコピペして上書きしてください。

usingGrpc.Core;usingMagicOnion.Client;usingSample.Shared.Services;usingSystem.Threading.Tasks;usingUnityEngine;publicclassSampleController:MonoBehaviour{privateChannelchannel;privateISampleServicesampleService;voidStart(){this.channel=newChannel("localhost:12345",ChannelCredentials.Insecure);this.sampleService=MagicOnionClient.Create<ISampleService>(channel);this.SampleServiceTest(1,2);}asyncvoidOnDestroy(){awaitthis.channel.ShutdownAsync();}asyncvoidSampleServiceTest(intx,inty){varsumReuslt=awaitthis.sampleService.SumAsync(x,y);Debug.Log($"{nameof(sumReuslt)}: {sumReuslt}");varproductResult=awaitthis.sampleService.ProductAsync(2,3);Debug.Log($"{nameof(productResult)}: {productResult}");}}

4.1.4. API 通信の動作確認

まずはサーバーを起動します。

Sample.Server を右クリックして、スタートアッププロジェクトに設定をクリックします。
image.png

これによって、普段は Unity にアタッチと表示されていたボタンが Sample.Serverの表示に変わります。
image.png

このボタンを押すとサーバーを起動することができます。

image.png

※スタートアッププロジェクトを元に戻す場合は Assembly-CSharpを右クリックしてスタートアッププロジェクトに指定します。
※Unity にアタッチしつつサーバーを起動したい場合は マルチスタートアッププロジェクトを使用します。(後述)

続いてサーバーを起動した状態で Unity の Scene を再生します。

Unity の Console にログが表示されました。
image.png

4.2. リアルタイム通信

続いてリアルタイム通信を試してみます。

4.2.1. Unity 側で API の定義を作る

普通の API 通信と同じく、まずは API の定義から作ります。
Assets\Scripts\ServerShared\Hubs 以下に SampleHub スクリプトを作ります。

image.png

SampleHub の中身は以下をコピペして保存してください。
今回はゲームにログイン、チャットで発言、位置情報を更新、ゲームから切断、という4つの API を作ります。

usingMagicOnion;usingSample.Shared.MessagePackObjects;usingSystem.Threading.Tasks;usingUnityEngine;namespaceSample.Shared.Hubs{/// <summary>/// CLient -> ServerのAPI/// </summary>publicinterfaceISampleHub:IStreamingHub<ISampleHub,ISampleHubReceiver>{/// <summary>/// ゲームに接続することをサーバに伝える/// </summary>TaskJoinAsync(Playerplayer);/// <summary>/// ゲームから切断することをサーバに伝える/// </summary>TaskLeaveAsync();/// <summary>/// メッセージをサーバに伝える/// </summary>TaskSendMessageAsync(stringmessage);/// <summary>/// 移動したことをサーバに伝える/// </summary>TaskMovePositionAsync(Vector3position);}/// <summary>/// Server -> ClientのAPI/// </summary>publicinterfaceISampleHubReceiver{/// <summary>/// 誰かがゲームに接続したことをクライアントに伝える/// </summary>voidOnJoin(stringname);/// <summary>/// 誰かがゲームから切断したことをクライアントに伝える/// </summary>voidOnLeave(stringname);/// <summary>/// 誰かが発言した事をクライアントに伝える/// </summary>voidOnSendMessage(stringname,stringmessage);/// <summary>/// 誰かが移動した事をクライアントに伝える/// </summary>voidOnMovePosition(Playerplayer);}}

4.2.2 サーバー側で API を実装する

普通の API の実装の時と同じ要領で、Sample.Server 以下に Hubs フォルダを作り、その中に SampleHub.cs を作ります。
image.png

SampleHub.cs の中身は以下をコピペして保存してください。

usingMagicOnion.Server.Hubs;usingSample.Shared.Hubs;usingSample.Shared.MessagePackObjects;usingSystem;usingSystem.Threading.Tasks;usingUnityEngine;publicclassSampleHub:StreamingHubBase<ISampleHub,ISampleHubReceiver>,ISampleHub{IGrouproom;Playerme;publicasyncTaskJoinAsync(Playerplayer){//ルームは全員固定conststringroomName="SampleRoom";//ルームに参加&ルームを保持this.room=awaitthis.Group.AddAsync(roomName);//自分の情報も保持me=player;//参加したことをルームに参加している全メンバーに通知this.Broadcast(room).OnJoin(me.Name);}publicasyncTaskLeaveAsync(){//ルーム内のメンバーから自分を削除awaitroom.RemoveAsync(this.Context);//退室したことを全メンバーに通知this.Broadcast(room).OnLeave(me.Name);}publicasyncTaskSendMessageAsync(stringmessage){//発言した内容を全メンバーに通知this.Broadcast(room).OnSendMessage(me.Name,message);awaitTask.CompletedTask;}publicasyncTaskMovePositionAsync(Vector3position){// サーバー上の情報を更新me.Position=position;//更新したプレイヤーの情報を全メンバーに通知this.Broadcast(room).OnMovePosition(me);awaitTask.CompletedTask;}protectedoverrideValueTaskOnConnecting(){// handle connection if needed.Console.WriteLine($"client connected {this.Context.ContextId}");returnCompletedTask;}protectedoverrideValueTaskOnDisconnected(){// handle disconnection if needed.// on disconnecting, if automatically removed this connection from group.returnCompletedTask;}}

4.2.3. Unity 側で API を呼ぶコードを実装する

SampleController に SampleHub の各 API を呼び出すコードを追加します。
下記のコードをコピペして上書きしてください。

usingGrpc.Core;usingMagicOnion.Client;usingSample.Shared.Hubs;usingSample.Shared.MessagePackObjects;usingSample.Shared.Services;usingUnityEngine;publicclassSampleController:MonoBehaviour,ISampleHubReceiver{privateChannelchannel;privateISampleServicesampleService;privateISampleHubsampleHub;voidStart(){this.channel=newChannel("localhost:12345",ChannelCredentials.Insecure);this.sampleService=MagicOnionClient.Create<ISampleService>(channel);this.sampleHub=StreamingHubClient.Connect<ISampleHub,ISampleHubReceiver>(this.channel,this);// 普通の API の呼び出しはコメントアウトしておきます// 残しておいても問題はないです(リアルタイム通信と両方動きます)//this.SampleServiceTest(1, 2);this.SampleHubTest();}asyncvoidOnDestroy(){awaitthis.sampleHub.DisposeAsync();awaitthis.channel.ShutdownAsync();}/// <summary>/// 普通のAPI通信のテスト用のメソッド/// </summary>asyncvoidSampleServiceTest(intx,inty){varsumReuslt=awaitthis.sampleService.SumAsync(x,y);Debug.Log($"{nameof(sumReuslt)}: {sumReuslt}");varproductResult=awaitthis.sampleService.ProductAsync(2,3);Debug.Log($"{nameof(productResult)}: {productResult}");}/// <summary>/// リアルタイム通信のテスト用のメソッド/// </summary>asyncvoidSampleHubTest(){// 自分のプレイヤー情報を作ってみるvarplayer=newPlayer{Name="Minami",Position=newVector3(0,0,0),Rotation=newQuaternion(0,0,0,0)};// ゲームに接続するawaitthis.sampleHub.JoinAsync(player);// チャットで発言してみるawaitthis.sampleHub.SendMessageAsync("こんにちは!");// 位置情報を更新してみるplayer.Position=newVector3(1,0,0);awaitthis.sampleHub.MovePositionAsync(player.Position);// ゲームから切断してみるawaitthis.sampleHub.LeaveAsync();}#regionリアルタイム通信でサーバーから呼ばれるメソッド群publicvoidOnJoin(stringname){Debug.Log($"{name}さんが入室しました");}publicvoidOnLeave(stringname){Debug.Log($"{name}さんが退室しました");}publicvoidOnSendMessage(stringname,stringmessage){Debug.Log($"{name}: {message}");}publicvoidOnMovePosition(Playerplayer){Debug.Log($"{player.Name}さんが移動しました: {{x:{player.Position.x},y:{player.Position.y},z:{player.Position.z}}}");}#endregion}

4.2.4. リアルタイム通信の動作確認

普通の API 通信の動作確認と同じ要領でサーバーを起動し、その後で Unity で Scene を再生します。

Unity の Console にログが表示されました。
image.png

これで普通の API 通信とリアルタイム通信の両方の動作確認ができました。

5. IL2CPP 対応(コードジェネレーターによるコード生成)

UnityEditor 上で動かすなら今のままでも問題ないのですが、IL2CPP を使う場合(例えば Platform を iOS にしたとき)はこのようなエラーが発生します。

image.png

IL2CPP は動的なコード生成に対応していないため、コードジェネレーターを使用して事前に必要なコードを生成する必要があります。

5.1. MagicOnion.MSBuild.Tasks のインストール

NuGet で MagicOnion.MSBuild.Tasks をインストールします。
プロジェクトは Sample.Shared を選択し、バージョンは 3.0.12 を選択します。
image.png

5.2. MessagePack.MSBuild.Tasks のインストール

同じ要領で MessagePack.MSBuild.Tasks もインストールします。
image.png

5.3. Sample.Shared.csproj の編集

Sample.Shared をダブルクリックし、Sample.Shared.csproj を開きます。
赤枠部分のコードを追加して保存します。
コードは以下をコピペしてください。

image.png

<TargetName="GenerateMessagePack"AfterTargets="Compile"><MessagePackGeneratorInput=".\Sample.Shared.csproj"Output="..\Sample.Unity\Assets\Scripts\Generated\MessagePack.Generated.cs"/></Target><TargetName="GenerateMagicOnion"AfterTargets="Compile"><MagicOnionGeneratorInput=".\Sample.Shared.csproj"Output="..\Sample.Unity\Assets\Scripts\Generated\MagicOnion.Generated.cs"/></Target>

この状態で Sample.Server をビルドすると Sample.Unity\Assets\Scripts\Generated 以下にコードジェネレーターによってコードが生成されます。

image.png

5.4. 生成されたコードの使用

次にこのコードを使用する設定を行います。
Scripts フォルダに C# Script を作り、名前を InitialSettings とします。

image.png

InitialSettings のコードは下記をコピペして保存します。

usingMessagePack;usingMessagePack.Resolvers;usingUnityEngine;namespaceAssets.Scripts{classInitialSettings{[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]staticvoidRegisterResolvers(){// NOTE: Currently, CompositeResolver doesn't work on Unity IL2CPP build. Use StaticCompositeResolver instead of it.StaticCompositeResolver.Instance.Register(MagicOnion.Resolvers.MagicOnionResolver.Instance,MessagePack.Resolvers.GeneratedResolver.Instance,BuiltinResolver.Instance,PrimitiveObjectResolver.Instance,MessagePack.Unity.UnityResolver.Instance);MessagePackSerializer.DefaultOptions=MessagePackSerializer.DefaultOptions.WithResolver(StaticCompositeResolver.Instance);}}}

これで IL2CPP の環境でも動作するようになりました。

6. iOS ビルド対応

iOS 用のビルドではさらに以下の追加作業が必要です。

  • Disable Bitcode
  • Add libz.tbd

Assets\Editor に C# Script を追加し、名前を BuildIos とします。
image.png

BuildIos のコードは以下をコピペします。

#if UNITY_IPHONE
usingSystem.IO;usingUnityEditor;usingUnityEditor.Callbacks;usingUnityEditor.iOS.Xcode;publicclassBuildIos{/// <summary>/// Handle libgrpc project settings./// </summary>/// <param name="target"></param>/// <param name="path"></param>[PostProcessBuild(1)]publicstaticvoidOnPostProcessBuild(BuildTargettarget,stringpath){varprojectPath=PBXProject.GetPBXProjectPath(path);varproject=newPBXProject();project.ReadFromString(File.ReadAllText(projectPath));vartargetGuid=project.GetUnityFrameworkTargetGuid();// libz.tbd for grpc ios buildproject.AddFrameworkToProject(targetGuid,"libz.tbd",false);// libgrpc_csharp_ext missing bitcode. as BITCODE exand binary size to 250MB.project.SetBuildProperty(targetGuid,"ENABLE_BITCODE","NO");File.WriteAllText(projectPath,project.WriteToString());}}#endif

環境構築は以上で終了です。お疲れさまでした。

7. マルチスタートアッププロジェクトについて

途中で説明を割愛したマルチスタートアッププロジェクトの利用方法です。
ソリューションを右クリックして、スタートアッププロジェクトの設定をクリックします。

image.png

マルチスタートアッププロジェクトにチェックをいれ、Assembly-CSharpSample.Serverのアクションを 開始にして OK を押します。

この状態で 開始を押すとデバッガーを Unity にアタッチしながらサーバーを起動することができます。
image.png

8. 後書きと参考にさせていただいた記事などへのリンク

こんなに長い記事を最後まで読んでいただいてありがとうございます。
少しでも役に立つことがあれば幸いです。

環境構築に成功して、より技術的な内容や実践的なコードが必要になった際は下記の記事などがおすすめです。


Viewing all articles
Browse latest Browse all 9749

Trending Articles