20200808のC#に関する記事は5件です。

NpgsqlExceptionで想定通りのエラーコードを取得出来なかった事とその解決方法

C# + Npgsql で PostgreSQL へクエリを送信した際、発生したエラーの種類に応じて処理を変更したいが、
エラーコードを正しく取得出来なかった。

PostgreSQL 12のエラーコード一覧は以下
https://www.postgresql.jp/document/12/html/errcodes-appendix.html

今回は、存在しないカラムを SELECT し、エラーコード 42703 (undefined_column) という結果を得たい。

失敗例1
try {
  //SELECT 文(SELECT usr_name FROM users) を送信。
  //ただし、usr_name という名前のカラムは users テーブルに存在しないので、
  //42703 という結果を得たい。
}
catch(Npgsql.NpgsqlException ex) {
    Console.WriteLine(ex.ErrorCode);    //結果は、-2147467259
}
失敗例2
try {
  //略
}
catch(Npgsql.PostgresException ex) {
    Console.WriteLine(ex.ErrorCode);    //-2147467259
}
失敗例3
try {
  //略
}
catch(System.Data.Common.DbException ex) {
    Console.WriteLine(ex.ErrorCode);    //-2147467259
}
微妙に違う
try {
  //略
}
catch(Npgsql.PostgresException ex) {
    Console.WriteLine(ex.Message);    //42703:列"usr_name"は存在しません。
}
成功
try {
  //略
}
catch(Npgsql.PostgresException ex) {
    Console.WriteLine(ex.SqlState);    //42703
}

という事で、PostgresException をキャッチして、SqlState で想定通りのエラーコードを取得出来ました。


環境 C# (.net 5 / C #8.0)・Npgsql 4.1.4・PostgreSQL 12

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ViewModel と相互作用処理(Messanger)

はじめに

オレオレ解釈の覚書 その13

Livet に用意された相互作用処理のため実装である Messanger についてまとめます。

本文

前回の最後に紹介させて頂きましたが、Livet は Prism と同じく MVVM フレームワークの一つです。
Livet には Messanger と呼ばれる機構が用意されており、View を直接参照することなく ViewModel から相互作用処理を依頼することができます。
Prism 7.2 で廃止されてしまった InteractionRequest の代替としてこちらを利用してみます。

Messanger と InteractionRequest とでは実装方法が異なります。Messanger はインスタンス自体は一つで、ViewModel から通知を行う際に指定する MessageKey によって処理の違いを表現します。View は Messanger をバインドして MessageKey の値を指定することで、通知をハンドルし相互作用処理を実行します。

ViewModel
using Livet.Messaging;

namespace TestApp.ViewModels
{
    public class MainWindowViewModel
    {
        public InteractionMessenger Messanger { get; }

        public MainWindowviewModel()
        {
            this.Messenger = new InteractionMessenger();
        }

        public void ShowMessage()
        {
            this.Messenger.Raise(new InteractionMessage("ShowMessage"));
        }
    }
}
View
<Window x:Class="TestApp.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:p="http://prismlibrary.com/"
        xmlns:behaviors="clr-namespace:TestApp.Views.Behaviors"
        p:ViewModelLocator.AutoWireViewModel="True">
    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger MessageKey="ShowMessage" Messenger="{Binding Messenger, Mode=OneWay}">
            <behaviors:MessageAction/>
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
</Window>

おわりに

Livet の Messager についてのお話でした。
InteractionRequest と比較すると、Messanger はインスタンス数を抑えられるため実装がスッキリしています。一方で MessageKey を文字列で指定する必要があり、この辺りは好みが分かれるかもしれません。
非常に汎用性の高い仕組みであり、Prism と併用することで MVVM パータンによる開発を強力にサポートしてくれるはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

はじめに

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() で切断するようになっています。

using Grpc.Core;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SampleController : MonoBehaviour
{
    private Channel channel;

    void Start()
    {
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);
    }

    async void OnDestroy()
    {
        await this.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 のコードは以下をコピペしてください。

using MessagePack;
using UnityEngine;

namespace Sample.Shared.MessagePackObjects
{
    [MessagePackObject]
    public class Player
    {
        [Key(0)]
        public string Name { get; set; }
        [Key(1)]
        public Vector3 Position { get; set; }
        [Key(2)]
        public Quaternion Rotation { 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 が起動するようになります。

using MagicOnion.Hosting;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading.Tasks;

namespace Sample.Server
{
    class Program
    {
        static async Task Main(string[] args)
        {
            await MagicOnionHost.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>
    <Compile Include="..\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 を定義してみます。

using MagicOnion;

namespace Sample.Shared.Services
{
    public interface ISampleService : IService<ISampleService>
    {
        UnaryResult<int> SumAsync(int x, int y);
        UnaryResult<int> ProductAsync(int x, int y);
    }
}

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

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

image.png

image.png

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

image.png

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

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

using MagicOnion;
using MagicOnion.Server;
using Sample.Shared.Services;

namespace Sample.Server.Services
{
    public class SampleService : ServiceBase<ISampleService>, ISampleService
    {
        public UnaryResult<int> SumAsync(int x, int y)
        {
            return UnaryResult(x + y);
        }

        public UnaryResult<int> ProductAsync(int x, int y)
        {
            return UnaryResult(x * y);
        }
    }
}

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

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

using Grpc.Core;
using MagicOnion.Client;
using Sample.Shared.Services;
using System.Threading.Tasks;
using UnityEngine;

public class SampleController : MonoBehaviour
{
    private Channel channel;
    private ISampleService sampleService;

    void Start()
    {
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);
        this.sampleService = MagicOnionClient.Create<ISampleService>(channel);

        this.SampleServiceTest(1, 2);
    }

    async void OnDestroy()
    {
        await this.channel.ShutdownAsync();
    }

    async void SampleServiceTest(int x, int y)
    {
        var sumReuslt = await this.sampleService.SumAsync(x, y);
        Debug.Log($"{nameof(sumReuslt)}: {sumReuslt}");

        var productResult = await this.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 を作ります。

using MagicOnion;
using Sample.Shared.MessagePackObjects;
using System.Threading.Tasks;
using UnityEngine;

namespace Sample.Shared.Hubs
{
    /// <summary>
    /// CLient -> ServerのAPI
    /// </summary>
    public interface ISampleHub : IStreamingHub<ISampleHub, ISampleHubReceiver>
    {
        /// <summary>
        /// ゲームに接続することをサーバに伝える
        /// </summary>
        Task JoinAsync(Player player);
        /// <summary>
        /// ゲームから切断することをサーバに伝える
        /// </summary>
        Task LeaveAsync();
        /// <summary>
        /// メッセージをサーバに伝える
        /// </summary>
        Task SendMessageAsync(string message);
        /// <summary>
        /// 移動したことをサーバに伝える
        /// </summary>
        Task MovePositionAsync(Vector3 position);
    }

    /// <summary>
    /// Server -> ClientのAPI
    /// </summary>
    public interface ISampleHubReceiver
    {
        /// <summary>
        /// 誰かがゲームに接続したことをクライアントに伝える
        /// </summary>
        void OnJoin(string name);
        /// <summary>
        /// 誰かがゲームから切断したことをクライアントに伝える
        /// </summary>
        void OnLeave(string name);
        /// <summary>
        /// 誰かが発言した事をクライアントに伝える
        /// </summary>
        void OnSendMessage(string name, string message);
        /// <summary>
        /// 誰かが移動した事をクライアントに伝える
        /// </summary>
        void OnMovePosition(Player player);
    }
}

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

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

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

using MagicOnion.Server.Hubs;
using Sample.Shared.Hubs;
using Sample.Shared.MessagePackObjects;
using System;
using System.Threading.Tasks;
using UnityEngine;

public class SampleHub : StreamingHubBase<ISampleHub, ISampleHubReceiver>, ISampleHub
{
    IGroup room;
    Player me;

    public async Task JoinAsync(Player player)
    {
        //ルームは全員固定
        const string roomName = "SampleRoom";
        //ルームに参加&ルームを保持
        this.room = await this.Group.AddAsync(roomName);
        //自分の情報も保持
        me = player;
        //参加したことをルームに参加している全メンバーに通知
        this.Broadcast(room).OnJoin(me.Name);
    }

    public async Task LeaveAsync()
    {
        //ルーム内のメンバーから自分を削除
        await room.RemoveAsync(this.Context);
        //退室したことを全メンバーに通知
        this.Broadcast(room).OnLeave(me.Name);
    }

    public async Task SendMessageAsync(string message)
    {
        //発言した内容を全メンバーに通知
        this.Broadcast(room).OnSendMessage(me.Name, message);

        await Task.CompletedTask;
    }

    public async Task MovePositionAsync(Vector3 position)
    {
        // サーバー上の情報を更新
        me.Position = position;

        //更新したプレイヤーの情報を全メンバーに通知
        this.Broadcast(room).OnMovePosition(me);

        await Task.CompletedTask;
    }

    protected override ValueTask OnConnecting()
    {
        // handle connection if needed.
        Console.WriteLine($"client connected {this.Context.ContextId}");
        return CompletedTask;
    }

    protected override ValueTask OnDisconnected()
    {
        // handle disconnection if needed.
        // on disconnecting, if automatically removed this connection from group.
        return CompletedTask;
    }
}

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

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

using Grpc.Core;
using MagicOnion.Client;
using Sample.Shared.Hubs;
using Sample.Shared.MessagePackObjects;
using Sample.Shared.Services;
using UnityEngine;

public class SampleController : MonoBehaviour, ISampleHubReceiver
{
    private Channel channel;
    private ISampleService sampleService;
    private ISampleHub sampleHub;

    void Start()
    {
        this.channel = new Channel("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();
    }

    async void OnDestroy()
    {
        await this.sampleHub.DisposeAsync();
        await this.channel.ShutdownAsync();
    }

    /// <summary>
    /// 普通のAPI通信のテスト用のメソッド
    /// </summary>
    async void SampleServiceTest(int x, int y)
    {
        var sumReuslt = await this.sampleService.SumAsync(x, y);
        Debug.Log($"{nameof(sumReuslt)}: {sumReuslt}");

        var productResult = await this.sampleService.ProductAsync(2, 3);
        Debug.Log($"{nameof(productResult)}: {productResult}");
    }

    /// <summary>
    /// リアルタイム通信のテスト用のメソッド
    /// </summary>
    async void SampleHubTest()
    {
        // 自分のプレイヤー情報を作ってみる
        var player = new Player
        {
            Name = "Minami",
            Position = new Vector3(0, 0, 0),
            Rotation = new Quaternion(0, 0, 0, 0)
        };

        // ゲームに接続する
        await this.sampleHub.JoinAsync(player);

        // チャットで発言してみる
        await this.sampleHub.SendMessageAsync("こんにちは!");

        // 位置情報を更新してみる
        player.Position = new Vector3(1, 0, 0);
        await this.sampleHub.MovePositionAsync(player.Position);

        // ゲームから切断してみる
        await this.sampleHub.LeaveAsync();
    }

    #region リアルタイム通信でサーバーから呼ばれるメソッド群

    public void OnJoin(string name)
    {
        Debug.Log($"{name}さんが入室しました");
    }

    public void OnLeave(string name)
    {
        Debug.Log($"{name}さんが退室しました");
    }

    public void OnSendMessage(string name, string message)
    {
        Debug.Log($"{name}: {message}");
    }

    public void OnMovePosition(Player player)
    {
        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

<Target Name="GenerateMessagePack" AfterTargets="Compile">
  <MessagePackGenerator Input=".\Sample.Shared.csproj" Output="..\Sample.Unity\Assets\Scripts\Generated\MessagePack.Generated.cs" />
</Target>
<Target Name="GenerateMagicOnion" AfterTargets="Compile">
  <MagicOnionGenerator Input=".\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 のコードは下記をコピペして保存します。

using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

namespace Assets.Scripts
{
    class InitialSettings
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        static void RegisterResolvers()
        {
            // 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
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

public class BuildIos
{
    /// <summary>
    /// Handle libgrpc project settings.
    /// </summary>
    /// <param name="target"></param>
    /// <param name="path"></param>
    [PostProcessBuild(1)]
    public static void OnPostProcessBuild(BuildTarget target, string path)
    {
        var projectPath = PBXProject.GetPBXProjectPath(path);
        var project = new PBXProject();
        project.ReadFromString(File.ReadAllText(projectPath));
        var targetGuid = project.GetUnityFrameworkTargetGuid();

        // libz.tbd for grpc ios build
        project.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. 後書きと参考にさせていただいた記事などへのリンク

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

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

生存率8%・・・入門動画の再生数から分かるUnityの挫折率

※Unity初心者の自分がとあるチュートリアルに挑戦して最後まで完走したあと、自分のブログに書いたコラムです。
 

Unityの難しさ

 先日チャレンジしたチュートリアル動画の再生数が、回を追うごとに激減していったのが興味深かったのでまとめてみた。自分はゼロからではなく基本操作は問題無し、ただしスクリプトはUnity本を一冊読んだけど全然書けない、よくいる初心者のレベル。でも絶対折れないで最後までやると決めてたので完走できた。一応最後まで完走したけどやっぱりUnityは難しい。原因は主にC#、C#、C#、、、
2020-08-08_10h27_09.png

ゼロからはじめるUnity

 今回自分が挑戦した、NCC新潟コンピュータ専門学校さんが無料で配信しているUnityのチュートリアル動画。[入門編 ゲーム作り、プログラミング初心者向け講座]この動画の再生数がどんどん落ちていくのが興味深い。同じ志を持った仲間どんどん減っていきます。。。動画はチャプターごとに分かれていて全9本。

2020-08-06_13h38_30.png
1/9 インストールから基本操作
https://youtu.be/NoJsCcIX3OE

58,034回視聴

Unity覚えたい人が約6万人見にきている(^^)/
同志がたくさんいて心強く感じている、自分もがんばろう!


2020-08-06_13h40_37.png
2/9 ステージとキャラクターの作成
https://youtu.be/loz4_lb-JsM

30,852回視聴

27,182回減、約半分いなくなった( ゚Д゚)
それでもまだ3万人いる


2020-08-06_13h41_05.png
3/9 スクリプトとステージの調整
https://youtu.be/XZxSi7O-RI4

15,373回視聴

15,479回減、さらに半分いなくなった(゜ー゜)
それでもまだ1万5千人いる


2020-08-06_13h41_32.png
4/9 カメラの追尾処理と敵の出現
https://youtu.be/WqOei3Nt6KY

10,888回視聴

4,485回減、最初の人数の約1/5まで減った( ノД`)シクシク…
それでもまだ1万人いる


2020-08-06_13h43_08.png
5/9 敵オブジェクトのモーション付け
https://youtu.be/PSxQip9cXRM

9,010回視聴

1,878回減、離脱率に急ブレーキ(^_-)-☆
残り約9千人
ナカマヘッタ。。。


2020-08-06_13h43_37.png

6/9 ゴールの製作
https://youtu.be/sGgLIFlWTps

7,424回視聴

1,586回減、前回と同じくらいの離脱率( `ー´)ノ
残り約7千人弱


2020-08-06_13h43_59.png
7/9 コースレイアウトの調整
https://youtu.be/7KjAw2skglU

5,300回視聴

2,124回減、 前回から2千以上減った、難易度上がった?( ;∀;)
残り約5千人弱


2020-08-06_13h44_24.png
8/9 敵をコース上に配置
https://youtu.be/jw4f9EjRZTY

4,936回視聴

364回減、 ゴールが見えたのもあるか?ほとんど残った!(*'▽')
残り約5千人


2020-08-06_13h44_45.png
9/9 アセットストアを使ってキャラクターをインポートから完成まで
https://youtu.be/JFQceCuoA_A

4,738回視聴

198回減、 微減でフィニッシュ!
報告!生存者4,738名 (`・ω・´)ゞ


挑戦者58,034 - 最終動画到達者4,738 - 生存率8%

 最初の再生数約58,000回、最後は約47,00回という大幅減。一人で複数回視聴してるだろうから純粋な人数じゃないだろうけど、学習しようと始めた内の1割ぐらいしか残ってない。。。入門ですらこの数字。スクリプトも写経するだけで最後までいけるのにこの離脱率。Unity学習の難易度がいかに高いが分かる結果だった。スクリプトがからんでくるあたりで一気に減る。。。自分もスクリプトの意味は分かるけど、書けるかといったら基本の文法や仕組みがまだイマイチなので無理。じっくりやっていきます。
タイトルなし.png

最後まで完走したので記録に残しておいた
https://youtu.be/aLUo0An0sJw

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

System.Threading.Channelsで非同期フレンドリーな生産者・消費者(Pub/Sub)パターンを実装する

結構自作しちゃうことが多かったんですが、それなりに実装難易度が高い上にちょっと時間がないので何かないか困っていたら、良いものを教えていただいたので簡単な使い方をまとめておきます。

あえとすさんいつもありがとうございます!

これは何?

マルチスレッドプログラミングの一般的なデザインパターンのひとつ、生産者・消費者パターン(最近だとPub/Subパターンといった方が伝わりが良い?)の実装を助けてもらえるMicrosoft謹製のライブラリです。async/awaitとの親和性が高く、簡単に使えて高速に動作します。

参考資料

  1. System.Threading.Channelsを使う
  2. C# Channels - Publish / Subscribe Workflows

とくに1.の記事は、ほかのライブラリとの比較もされていて、一読することを強くお勧めします。

生産者:消費者=1:1の実装例

まずは一番簡単な例から。生産者も消費者もひとりなんですが、生産者はキューにアイテムを突っ込んだら、消費者に非同期で処理してもらいたいときに利用する実装です。こちらはほぼ参考資料のままです。シンプルで素晴らしいサンプルです。

static async Task Main(string[] args)
{
    var channel = Channel.CreateUnbounded<string>(
        new UnboundedChannelOptions
        {
            SingleReader = true,
            SingleWriter = true
        });

    var consumer = Task.Run(async () =>
    {
        while (await channel.Reader.WaitToReadAsync())
        {
            Console.WriteLine(await channel.Reader.ReadAsync());
        }
    });
    var producer = Task.Run(async () =>
    {
        var rnd = new Random();
        for (int i = 0; i < 5; i++)
        {
            await Task.Delay(TimeSpan.FromSeconds(rnd.Next(3)));
            await channel.Writer.WriteAsync($"Message {i}");
        }
        channel.Writer.Complete();
    });

    await Task.WhenAll(producer, consumer);            
    Console.WriteLine("Completed.");
}

まず最初にキュー(+α)に該当するChannelオブジェクトを生成しています。

var channel = Channel.CreateUnbounded<string>(
    new UnboundedChannelOptions
    {
        SingleReader = true,
        SingleWriter = true
    });

CreateUnboundedはサイズ制限のないキューを作成します。CreateBoundedを利用するとサイズ制限のあるキューを作成できます。

またUnboundedChannelOptionsのSingleReaderとSingleWriterをtrueとして制約をかけることで性能がやや向上するようです。デフォルトはfalseです。

つづいて、キューに登録されたアイテムを処理する消費者(Consumer)を作成します。

var consumer = Task.Run(async () =>
{
    while (await channel.Reader.WaitToReadAsync())
    {
        Console.WriteLine(await channel.Reader.ReadAsync());
    }
});

WaitToReadAsyncメソッドでChannelが閉じられる(すべての処理が完了している)かどうかチェックし、ReadAsyncでアイテムを取得して処理しています。

ではつぎは生産者(Producer)側を見てみましょう。

var producer = Task.Run(async () =>
{
    var rnd = new Random();
    for (int i = 0; i < 5; i++)
    {
        await Task.Delay(TimeSpan.FromSeconds(rnd.Next(3)));
        await channel.Writer.WriteAsync($"Message {i}");
    }
    channel.Writer.Complete();
});

Randomに3秒以内のディレイを挟みつつ、WriteAsyncでアイテムを登録しています。

5つのアイテムを書き込んだらCompleteでチャネルを閉じています。

最後は生産者と消費者の処理を待って終了します。

await Task.WhenAll(producer, consumer);            
Console.WriteLine("Completed.");

生産者:消費者=1:nの実装例

参考資料の例だとキューに入れられたアイテムの処理時間が均等であれば良いのですが、アイテムによって処理時間が異なるような場合は不適切です。

その場合、つぎのように利用しましょう。

CreateUnboundedでWriter側だけSingle制約をかけていることに注意してください。

static async Task Main(string[] args)
{
    var channel = Channel.CreateUnbounded<string>(
        new UnboundedChannelOptions
        {
            SingleWriter = true
        });

    var consumers = Enumerable
        .Range(1, 3)    // 1~3の数値を取得する
        .Select(consumerNumber =>
            Task.Run(async () =>
            {
                while (await channel.Reader.WaitToReadAsync())
                {
                    if (channel.Reader.TryRead(out var item))
                    {
                        Console.WriteLine($"Consumer:{consumerNumber} {item}");
                    }
                }
            }));
    var producer = Task.Run(async () =>
    {
        var rnd = new Random();
        for (var i = 0; i < 5; i++)
        {
            await Task.Delay(TimeSpan.FromSeconds(rnd.Next(3)));
            await channel.Writer.WriteAsync($"Message {i}");
        }
        channel.Writer.Complete();
    });

    await Task.WhenAll(consumers.Union(new[] {producer}));
    Console.WriteLine("Completed.");
}

まず最初に複数の消費者を生成します。

var consumers = Enumerable
    .Range(1, 3)    // 1~3の数値を取得する
    .Select(consumerNumber =>
        Task.Run(async () =>
        {
            while (await channel.Reader.WaitToReadAsync())
            {
                if (channel.Reader.TryRead(out var item))
                {
                    Console.WriteLine($"Consumer:{consumerNumber} {item}");
                }
            }
        }));

ここで重要なのは、アイテムの取得にReadAsyncではなくTryReadを使っている点です。

Channelでは消費者はWaitToReadAsyncでキューにアイテムが登録されるのを待ちますが、アイテムがひとつ登録されると、一旦すべての生産者が「起こされ」ます。ReadAsyncを使った場合、2番目以降にアイテムを取りに行った消費者は、アイテムがないため例外がスローされてしまいます。

そこでTryReadを利用することで、まだアイテムがあった場合だけ処理するように実装してあげる必要があります。

あとはすべての消費者と生産者の処理の終了を待機して完了です。

await Task.WhenAll(consumers.Union(new[] {producer}));
Console.WriteLine("Completed.");

生産者:消費者=n:1の実装例

参考資料の例では、複数のChannelを作ってマージする方法が紹介されていますが、元々Channel自体が複数からの入力をサポートしているので必ずしもChannelを複数作る必要はありません。

ということで以下がシンプルな例になります。CreateUnboundedでReader側だけSingle制約をかけていることに気を付けてください。

static async Task Main(string[] args)
{
    var channel = Channel.CreateUnbounded<string>(
        new UnboundedChannelOptions
        {
            SingleReader = true
        });

    var consumer = Task.Run(async () =>
    {
        while (await channel.Reader.WaitToReadAsync())
        {
            Console.WriteLine(await channel.Reader.ReadAsync());
        }
    });

    var producers = Enumerable
        .Range(1, 3)
        .Select(producerNumber =>Task.Run(async () =>
        {
            var rnd = new Random();
            for (var i = 0; i < 5; i++)
            {
                await Task.Delay(TimeSpan.FromSeconds(rnd.Next(3)));
                await channel.Writer.WriteAsync($"Producer:{producerNumber} Message {i}");
            }
        }));

    await Task.WhenAll(producers);
    channel.Writer.Complete();

    await consumer;
    Console.WriteLine("Completed.");
}

Consumer側の生成は1:1の場合と変わりありません。

違いはProducer側の実装です。1:1の時はforループ前後が次のように実装されていました。
forループの外でWriterをCompleteして閉じていましたが、これでは最初に処理が完了したProducerが閉じてしまい、以後のProducerの処理でエラーになってしまします。

for (int i = 0; i < 5; i++)
{
    await Task.Delay(TimeSpan.FromSeconds(rnd.Next(3)));
    await channel.Writer.WriteAsync($"Message {i}");
}
channel.Writer.Complete();

そのため、つぎのようにすべてのProducerの処理が完了してからCompleteするようにします。

await Task.WhenAll(producers);
channel.Writer.Complete();

ここは実際の実装はケースによって異なりますが、生産者の処理を全て待つ必要がない場合は、つぎのようにWriteAsyncではなくTryWriteを使うこともできます。

//await channel.Writer.WriteAsync($"Producer:{producerNumber} Message {i}");
channel.Writer.TryWrite($"Producer:{producerNumber} Message {i}");

Completeも閉じられる場所が一つではない場合は、TryCompleteを使うようにしましょう。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む