sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# System.Text.Json で非数値 (NaN, Infinity) を書込/読込する方法

C#JSON を扱うときは、単純に「System.Text.Json」ケースが増えてきました。DataContractJsonSerializerNewtonsoft.Json のように他の選択肢もあるけど、徐々に移行されていくかと。

System.Text.Json」を利用するケースで、問題になることがあった例のひとつに「非数値 (NaN) の扱い」というものがありました。標準では、非数値を含むクラスなんかのシリアライズは、エラーになってしまう(JSON のフォーマットの問題)ので、独自に JsonConverter<T>設計する必要がありました

「.NET number values such as positive and negative infinity cannot be written as valid JSON」というエラーになります。

こうしたケースだと、 Newtonsoft.Json のほうが、少しだけコンバーターを楽に設計できたりしたのですが、「System.Text.Json」のバージョンが5になって非数値に対応できるようになっていました

待望の機能だったので、とりあえずやってみます。

ただし、まだ System.Text.Json 5 は RC (Release Candidate: リリース候補) の状態です。安定版ではないので注意。

f:id:shikaku_sh:20201016111424p:plain
使用したバージョン

書き込みのテストコード(エラー発生)

こんな感じで、NaN を含めるとエラーになります。ここまでは、今まで通り。

public class ClassWithInts
{
    public int NumberOne { get; set; }
    public double NumberTwo { get; set; }
}

public void Run()
{
    var data = new ClassWithInts
    {
        NumberOne = -1,
        NumberTwo = double.NaN,
    };

    var json = JsonSerializer.Serialize(data);
}

f:id:shikaku_sh:20201016111630p:plain

書き込みのテストコード

System.Text.Json は 5.0 になって、JsonSerializerOptionsNumberHandling プロパティが追加されています。シリアライズするときの処置を補足できます。

以下のようにしてみます。

public void Run()
{
    var options = new JsonSerializerOptions
    {
        NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals,
        WriteIndented = true,
    };

    var data = new ClassWithInts
    {
        NumberOne = -1,
        NumberTwo = double.NaN,
        NumberPositive = double.PositiveInfinity,
        NumberNegative = double.NegativeInfinity,
    };

    var json = JsonSerializer.Serialize(data, options);

    Console.WriteLine(json);
}

無事、出力できるようになりました。

f:id:shikaku_sh:20201016111709p:plain

読み込みのテストコード(エラー発生)

書き込みと同じで、デフォルトの状態だとエラーが発生します。ここまでは、今まで通り。

public void ReadTest(string json)
{
    // var json = @"{""NumberOne"":-1,""NumberTwo"":""NaN"",""NumberPositive"":""Infinity"",""NumberNegative"":""-Infinity""}";
    var data = JsonSerializer.Deserialize<ClassWithInts>(json, options);
}

f:id:shikaku_sh:20201016111802p:plain:w600

読み込みのテストコード

書き込みと一緒で、JsonSerializerOptionsNumberHandling プロパティを設定して、シリアライズするときの処置を補足します。

public void ReadTest(string json)
{
    var options = new JsonSerializerOptions
    {
        NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals,
    };

    var data = JsonSerializer.Deserialize<ClassWithInts>(json, options);
}

読み込みできるようになりました。コンバーターいらずなので、すごく楽。

f:id:shikaku_sh:20201016111840p:plain:w600

シリアライズのオプションの意味

追加された JsonNumberHandling のコメントはこんな感じになっています。

  • JsonNumberHandling.AllowReadingFromString
    Numbers can be read from System.Text.Json.JsonTokenType.String tokens Does not prevent numbers from being read from System.Text.Json.JsonTokenType.Number token.

  • JsonNumberHandling.AllowNamedFloatingPointLiterals
    The "NaN", "Infinity", and "-Infinity" System.Text.Json.JsonTokenType.String tokens can be read as floating-point constants, and the System.Single and System.Double values for these constants will be written as their corresponding JSON string representations.

ざっくり訳:

  • JsonNumberHandling.AllowReadingFromString
    System.Text.Json.JsonTokenType.String トークンから、数値を読み取るようにします。ただし、System.Text.Json.JsonTokenType.Number からの数値の読み取りはできません。

  • JsonNumberHandling.AllowNamedFloatingPointLiterals
    System.Text.Json.JsonTokenType.String トークンから "NaN"、"Infinity"、"-Infinity" の値を浮動小数の定数として読み込むことができるようになります。System.SingleSystem.Double の定数は、対応する JSON 文字列表現として書き込みされます。

読み取りのときは JsonNumberHandling.AllowReadingFromString を設定しなくても成功しましたが、提案時点ではつける方針だったみたいなので設定しています。

サンプル

GitHub の「Sample」の中に「TextJsonNaN」のソリューションを追加しました。

参照

Visual C# 2019パーフェクトマスター

Visual C# 2019パーフェクトマスター

Effective C# 6.0/7.0

Effective C# 6.0/7.0

SendGrid UNIX Time から EXCEL の日付+時間に値を変換する方法

SendGrid の Suppressions からダウンロードした CSV ファイルの日付を見てみると、数字が書いてあります。

問い合わせて聞いてみたところ、どうやらこれは UNIX Time で記載してあるらしいです。

f:id:shikaku_sh:20201005154536p:plain
問い合わせ

とりあえず、このままだと見てわからないので、とりあえず EXCEL で確認できる方法をメモした記事です。

そもそも UNIX Time とは

UNIX Time とは、基準時刻 (1970/01/01 00:00:00 UTC) からの経過秒数、ということみたいです。

これでわかったのは

  • 経過秒数なので、ミリ秒を含まないから整数値
  • UTC 基準だから日本標準時JST にするには +09:00 必要

なので、1970 01/01 00:01:00 UTC なら1時間経過しただけなので、  60\times60 = 3600 じゃないかと思います。大きな数字が記録されているのも納得です。

ざっくり計算してみます。

1日で 86,400。1年で 31,536,000。2020 年は 1970 年から 50 年先なので、1,576,800,000。だいぶ created の日付に近そう。

EXCEL 日付との変換式

UNIX Time to UTC協定世界時

  • (#UNIX Time# / 86400) + 25569
    • 86,400 は1日の秒数
    • 25,569 は UNIX Time の基準時刻に相当するシリアル値

25,569 は、EXCEL のセルの書式設定で「1970/1/1」を入力してから、数値に戻すと「25,569」になります。ざっくりと EXCEL は 1900/01/01 基準で「1」なんで UNIX Time の 70 年 x 365 で 25,550。閏年は4年に一度なんで + 17.5 日。

UNIX Time to JST日本標準時 UTC+0900)

  • ((#UNIX Time# + 32400) / 86400) + 25569
    • 86,400 は1日の秒数
    • 32,400 は+9時間
    • 25,569 は UNIX Time の基準時刻に相当するシリアル値

結果

問題なく変換できました。

f:id:shikaku_sh:20201005155543p:plain:w600

CSV エクスポート

SendGrid の CSV エクスポートはこちら。

f:id:shikaku_sh:20201005155640p:plain:w600

参考

事例で学ぶサブスクリプション

事例で学ぶサブスクリプション

  • 作者:小宮紳一
  • 発売日: 2019/09/12
  • メディア: 単行本

Unity スクリプトコードを Visual Studio でステップ実行・ブレークポイントを使ってデバッグをする

f:id:shikaku_sh:20200910152454p:plain

Unity のコード書いたあとに、Visual Studio(コードエディター)を使って、コードのステップ実行やブレークポイントを設置して気になる箇所を確認するといった、典型的なデバッグ作業の(そもそもの)やり方に関するメモです。

環境構築

Visual Studio 2017 以前は、「Visual Studio Tools for Unity」を別途インストールする、設定をする必要があったりして、ややもすれば混乱しがちだったように思います。

現在の Visual Studio 2019 だと、Unity Hub のように Visual Studio Installer がインストール済の構成やバージョンを管理してくれるようになったので、設定の面での混乱が小さくなりました。おそらく、デフォルトで「Visual Studio Tools for Unity」がインストールされているかと思います。

Visual Studio Installer を開いて、Unity で使用している(する) Visual Studio の「インストールの詳細」に Unity によるゲーム開発、Visual Studio Tools for Unity が含まれているかどうかをチェックします。

f:id:shikaku_sh:20201002143458p:plain:w600

とりあえず、これで Visual Studio 側の環境設定は完了です。Unity で使用するコードエディターを確認するときは、Unity > Preferences > External Tools を開いて、External Script Editor を設定・確認してみてください。

f:id:shikaku_sh:20201002143523p:plain:w600

デバッグ(アタッチ)のやり方

テストとしてシンプルな方法:

  1. Unity Hub からデバッグをするプロジェクトを Unity で開く
  2. 問題のコードを選択して、Visual Studio を起動する
  3. Visual Studio のメニューから デバッグ > Unity デバッガ―のアタッチ を選択する
  4. 起動しているプロジェクトを選択する
  5. Visual Studio でコードにブレークポイントを設置する
  6. Unity で「実行」ボタンを押下する
  7. ブレークポイントで停止するかどうかを確認する

f:id:shikaku_sh:20201002143633p:plain
メニューから選択します
f:id:shikaku_sh:20201002143647p:plain:w400
プロジェクトを選択します

こんな感じで、最初は正しくデバッグできるかどうかをチェックします。

f:id:shikaku_sh:20201002143746p:plain
ブレークポイントで停止を確認する

デバッグの便利な設定・応用

f:id:shikaku_sh:20201002150255p:plain

Visual Studio のアタッチ設定を切り替えます。「Unity にアタッチして再生」にすると、Visual Studio 側から Unity の再生ボタンを押下して、プログラムのデバッグ開始(デバッグ再生)を指示することができるようになります。

f:id:shikaku_sh:20201002143929g:plain
Visual Studio から開始と停止ができるようになる

Visual Studio に慣れてる人だと、F5デバッグを開始したくなるんで、もう必須だと思います。

Unity からも「Unity でコーディングするときに Visual Studio ツールを最適化するためのヒント」の記事で「“アタッチして再生”を使用してデバッグをスピードアップする」より、紹介されています。オススメ。

DebuggerDisplay 属性

デバッグ機能を使えるようになると「DebuggerDisplay 属性」を設定することで、よりデバッグがやりやすくなると思います。

通常だと、名前空間.クラス名 が表示されている部分を DebuggerDisplay のテキストに置き換えるデバッグ専用の機能です。

DebuggerDisplay の記述は、ちょっと癖があって string で扱う文字列補完をすべて扱うことができません。また、コンパイル時点までは、テキストとしてしか見ていないので、途中で変数名を変更してもエラーが表示されるわけでもありません。

なので、オススメなやりかたはこうなります:

[DebuggerDisplay("{DebuggerText}")]
public class Sample
{
    public int A { get; } = 1;
    public double B { get; } = 12.345;
    public string C { get; } = null;

    public string DebuggerText => $"A = {A}, B = {B:00.00}, C = {C ?? "null"}";
}

f:id:shikaku_sh:20201002144123p:plain:w550

こんな感じで、デバッグのウォッチ情報から一覧が取得できます。これが強力になるシーンは、コレクション型のデータです。ウォッチの内容を潜っていかなくても、一覧として情報を確認することができるようになります。

f:id:shikaku_sh:20201002144139p:plain:w550

デバッグとテスト

このデバッグのやり方はとても便利なんですが人力感が強く、テストとはまた別の強みがあります。なので、Unit テスト、コードメトリクス、バグを防ぐ・修正する手法は、色々なアプローチを修めておくとよいと思います。

参考

Unity コードをマネージド プラグイン (DLL) を作成して利用する

f:id:shikaku_sh:20200910152454p:plain

C# で書いたコードをあらかじめ マネージド プラグイン として DLL 化して、Unity で利用するやり方をメモした記事です。自分で使うライブラリーの中で成熟したものは、もう DLL にしてしまってもよいかもしれません。

まず、Unity のプラグイン形式として、つぎの2つがあります:

マネージド プラグインは、通常だとソースコードを書いたソースファイルとしてプロジェクトに保存されており、ソースが変更されると Unity がコンパイルするようなものです。ただし、外部コンパイラーを使って、あらかじめ DLL 化しておくことも可能です

ネイティブ プラグインは、過去につくった sqlite の DLL のようにプラットフォームごとに別々の DLL を用意するタイプのものです。

この違いを把握しておくほうがよさそうですね。マネージドプラグインは環境に依存しづらく、ネイティブプラグインは環境(プラットフォーム)の影響を受けやすい、とか。汎用的なライブラリーを作成するなら、どっちのプラグインとして開発するでしょうか、とか。

Visual Studio 2019 でライブラリープロジェクトを作成

Visual Studio では、最初にプロジェクトを作成する際に、クラスライブラリーのコンパイル形式を決める必要があります。

Unity で利用できるクラスライブラリーを作成するには、Unity がサポートするコンパイル形式のライブラリーを作成する必要があります。

.NET Standard 2.0 .NET 4.x
.NET Standard サポートあり サポートあり
.NET Framework 制限あり サポートあり
.NET Core サポートなし サポートなし

こんな感じなので、実際的には .NET Standard か .NET Framework のクラスライブラリーを作成すればよさそうです。ポイントは .NET Core で作成しても使えないという点になりそうですね。

ただし、C# 8.0 を利用したい場合は .NET Framework だと今後は不都合な設定になるかもしれません。設定の変更によって C# 8.0 を利用できるようにもできますが、Microsoft サポート外です。まだまだ目新しいのですが .NET Standard にするほうがいいような気がします。どういうものか調べるだけでいいんで、デメリットがわからない・予想できない設定変更よりも、手間コストの先払いというだけで、やさしいと思います。

f:id:shikaku_sh:20201001104036p:plain:w600

クラスライブラリーに Unity の DLL を追加する

Unity API にアクセスできないと UnityEngine.Debug.Log() のような基本的なコードも書けません。不便なので Unity の DLL を参照に追加します。Windows だと以下のパスになります。

  • %ProgramFiles%\Unity\Editor\Data\Managed\UnityEngine.dll

f:id:shikaku_sh:20201001104119p:plain:w600 f:id:shikaku_sh:20201001104126p:plain:w600

これで、Unity 側の機能にもアクセスできるはずです。

個人的に DLL 化するなら、なおさら名前空間とうまく付き合うべきだと思います。「Unity 開発に関する 50 の Tips 〜ベストプラクティス〜2016 Edition」は考え方の参考になると思います。すべてのコードを名前空間に入れること、というTips があります。

利用

Unity のプロジェクトに対して、Plugins フォルダーの中に追加すればよいです。

ネイティブプラグインのときは、ここでも色々気をつかいましたが、プラットフォームを選ばないというのは楽ですね。

参考

C# Game Programming Cookbook for Unity

C# Game Programming Cookbook for Unity

  • 作者:Murray, Jeff W.
  • 発売日: 2021/03/15
  • メディア: ペーパーバック

C# Disposable な実装にしてイベントのメモリーリークを防ぐ

C# のメモリーリークといえば、イベントの購読/解除。(一発のミスがでかいのは、また別かもだけど)

C#ガベージコレクションは、ざっくりと「誰からも参照されていないオブジェクトがあったら消す」という仕組みなので、だれかから参照を受けていると、いつまでもメモリーが開放されないです。

このあたりは「++C++; - イベントの購読とその解除」などを参照してみてください。

特に、このケースに該当するのはイベントを発生させる側の寿命が長くて、イベントを受信する側の寿命が短いときに、実際的なメモリーリークが発生してしまいます。C++ポインター管理に近い設計をしておけばいいんだけど、このとき Reactive Extensions を活用すると、イベント購読解除する側を IDisposable にすることができるので、より似たような(便利な)設計が可能です。

ただ、Reactive Extensions をフルスペックで活用すると、独特の構文・コードに浸食されてしまうのもわかるので、この便利部分だけを抽出・練習してみた一例の記事です。

自分の技術力・練習になるなら車輪の再開発を推奨する人です。

実装(イベントの使い方)

イベントの登録と解除の仕組みは、次の2つのメソッドでできるようにします。Subscribe メソッドでイベントの購読を設定して、最後に Dispose メソッドで購読を解除することにします。

  • Subscripbe((sender, args) => { ... })
  • Dispose()

通常でもイベントの購読は += (sender, args) => { ... } のようにすればいいので、違いがありません。

しかし、解除は -= handler のようにしなければ、購読解除できないのが C# のイベントの辛いところでした。ここがポイントですね。なので、Dispose を呼び出すと引数なしでイベントの購読解除ができるとなると、便利そうだな、という設計にしましょうか。そんなわけで実装。

Disposable のコーディング

まず、IDisposable なオブジェクトにラッパー化する AnonymousDisposable を作成します。この部分は Reactive Extension のコードを参考にします。

/// <summary>
/// <see cref="ICancelable"/> インターフェースは、廃棄状態を調べるプロパティを定義します。
/// </summary>
public interface ICancelable : IDisposable
{
    /// <summary>
    /// オブジェクトが破棄されているかどうかを示す値を取得します。
    /// </summary>
    bool IsDisposed { get; }
}
/// <summary>
/// <see cref="AnonymousDisposable"/> クラスは、<see cref="Action"/> をベースにした <see cref="IDisposable"/> オブジェクトを表現したクラスです。
/// </summary>
public class AnonymousDisposable : ICancelable
{
    #region Fields

    private volatile Action _Dispose;

    #endregion

    #region Properties

    /// <summary>
    /// オブジェクトが破棄されているかどうかを示す値を取得します。
    /// </summary>
    public bool IsDisposed => _Dispose == null;

    #endregion

    #region Initializes

    /// <summary>
    /// <see cref="AnonymousDisposable"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="dispose"><see cref="IDisposable.Dispose"/> の発生時に実行されるアクション。</param>
    public AnonymousDisposable(Action dispose)
    {
        System.Diagnostics.Debug.Assert(dispose != null);

        _Dispose = dispose;
    }

    #endregion

    #region Public Methods

    /// <summary>
    /// 現在のインスタンスが破棄されていないなら、設定されている匿名アクションを実行します。
    /// </summary>
    public void Dispose()
    {
        // スレッドセーフで実行する
        Interlocked.Exchange(ref _Dispose, null)?.Invoke();
    }

    #endregion
}

/// <summary>
/// <see cref="AnonymousDisposable"/> クラスは、<see cref="Action"/> をベースにした <see cref="IDisposable"/> オブジェクトを表現したクラスです。
/// </summary>
public class AnonymousDisposable<T> : ICancelable
{
    #region Fields

    private T _State;
    private volatile Action<T> _Dispose;

    #endregion

    #region Properties

    /// <summary>
    /// オブジェクトが破棄されているかどうかを示す値を取得します。
    /// </summary>
    public bool IsDisposed => _Dispose == null;

    #endregion

    #region Initializes

    /// <summary>
    /// <see cref="AnonymousDisposable"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="state"></param>
    /// <param name="dispose"><see cref="IDisposable.Dispose"/> の発生時に実行されるアクション。</param>
    public AnonymousDisposable(T state, Action<T> dispose)
    {
        System.Diagnostics.Debug.Assert(dispose != null);

        _State = state;
        _Dispose = dispose;
    }

    #endregion

    #region Public Methods

    /// <summary>
    /// 現在のインスタンスが破棄されていないなら、設定されている匿名アクションを実行します。
    /// </summary>
    public void Dispose()
    {
        // スレッドセーフで実行する
        Interlocked.Exchange(ref _Dispose, null)?.Invoke(_State);
        _State = default;
    }

    #endregion
}

つぎに、IDisposable なオブジェクトを生成するジェネレーターを作成します。

/// <summary>
/// <see cref="Disposable"/> クラスは、<see cref="IDisposable"/> オブジェクトを作成するための静的メソッドを提供するクラスです。
/// </summary>
public class Disposable
{
    private sealed class EmptyDisposable : IDisposable
    {
        public static readonly EmptyDisposable Instance = new EmptyDisposable();

        private EmptyDisposable(){ }

        public void Dispose()
        {

        }
    }

    #region Properties

    /// <summary>
    /// 廃棄するときに何もしない <see cref="IDisposable"/> を取得します。
    /// </summary>
    public static IDisposable Empty => EmptyDisposable.Instance;

    #endregion

    #region Initializes

    /// <summary>
    /// <see cref="Disposable"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    public Disposable() { }

    #endregion

    #region Public Methods

    /// <summary>
    /// 廃棄するときに指定したアクションを呼び出す <see cref="IDisposable"/> オブジェクトを作成します。
    /// </summary>
    /// <param name="dispose"><see cref="IDisposable.Dispose"/> が最初に呼び出されたときに実行するアクション。</param>
    /// <returns>廃棄するときに与えられたアクションを実行する <see cref="IDisposable"/> オブジェクト。</returns>
    /// <exception cref="ArgumentNullException">指定した <paramref name="dispose"/><c>null</c> です。</exception>
    public static IDisposable Create(Action dispose)
    {
        if (dispose == null)
        {
            throw new ArgumentNullException(nameof(dispose));
        }

        return new AnonymousDisposable(dispose);
    }

    /// <summary>
    /// 廃棄するときに指定したアクションを呼び出す <see cref="IDisposable"/> オブジェクトを作成します。
    /// </summary>
    /// <param name="state"></param>
    /// <param name="dispose"><see cref="IDisposable.Dispose"/> が最初に呼び出されたときに実行するアクション。</param>
    /// <returns>廃棄するときに与えられたアクションを実行する <see cref="IDisposable"/> オブジェクト。</returns>
    /// <exception cref="ArgumentNullException">指定した <paramref name="dispose"/><c>null</c> です。</exception>
    public static IDisposable Create<T>(T state, Action<T> dispose)
    {
        if (dispose == null)
        {
            throw new ArgumentNullException(nameof(dispose));
        }

        return new AnonymousDisposable<T>(state, dispose);
    }

    #endregion
}

これで、Disposable なオブジェクトを作成するために必要な Reactive Extension のコードを取り出せました。Reactive Extension は大きなプロジェクトですが、こうやって切り出せるのはえらいですね。

イベントを Disposable なオブジェクトに詰め込む

ここは、新しくコーディングします。

ポイントは Subscribe するときに、EventHandler を購読/購読解除の両方を設定して管理していることです。このイベントハンドラーのインスタンスを両方に設定する書き方を楽にできないのが困るところですよね。

/// <summary>
/// <see cref="DisposableEventHandler"/> クラスは、<see cref="EventHandler"/><see cref="IDisposable"/> に対応させ、イベントの登録解除をやりやすくしたクラスです。
/// </summary>
public class DisposableEventHandler : IDisposable
{
    #region Properties

    private List<IDisposable> _Disposables = new List<IDisposable>();

    #endregion

    #region Initializes

    /// <summary>
    /// <see cref="DisposableEventHandler"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    public DisposableEventHandler() { }

    #endregion

    #region Events

    private event EventHandler Handler;

    #endregion

    #region Public Methods

    /// <summary>
    /// イベントの購読をすべて開放します。
    /// </summary>
    public void Dispose()
    {
        foreach (var disposable in _Disposables)
        {
            disposable?.Dispose();
        }

        _Disposables.Clear();
    }

    /// <summary>
    /// 指定した <see cref="EventHandler"/> を購読します。
    /// <para>
    /// 購読されたイベントハンドラーは、イベントを実行した際に実行されます。
    /// </para>
    /// </summary>
    /// <param name="handler"></param>
    public void SubScribe(EventHandler handler)
    {
        Handler += handler;

        // イベントの購読を終了する
        var disposable = Disposable.Create(() =>
        {
            // Console.WriteLine($"{handler} の購読を解除します。");

            Handler -= handler;
        });

        _Disposables.Add(disposable);
    }

    /// <summary>
    /// <see cref="EventHandler"/> のイベントを実行します。
    /// </summary>
    /// <param name="sender">実行オブジェクト。</param>
    /// <param name="args">イベント引数。</param>
    public void Raise(object sender, EventArgs args) => Handler?.Invoke(sender, args);

    /// <summary>
    /// <see cref="EventHandler"/> のイベントを実行します。
    /// </summary>
    /// <param name="sender">実行オブジェクト。</param>
    /// <param name="args">イベント引数。</param>
    public void Raise(object sender) => Handler?.Invoke(sender, EventArgs.Empty);

    #endregion
}

/// <summary>
/// <see cref="DisposableEventHandler"/> クラスは、<see cref="EventHandler{TEventArgs}"/><see cref="IDisposable"/> に対応させ、イベントの登録解除をやりやすくしたクラスです。
/// </summary>
/// <typeparam name="TEventArgs">イベントによって生成されるイベント データの型。</typeparam>
public class DisposableEventHandler<TEventArgs> : IDisposable
{
    #region Properties

    private List<IDisposable> _Disposables = new List<IDisposable>();

    #endregion

    #region Initializes

    /// <summary>
    /// <see cref="DisposableEventHandler"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    public DisposableEventHandler() { }

    #endregion

    #region Events

    private event EventHandler<TEventArgs> Handler;

    #endregion

    #region Public Methods

    /// <summary>
    /// イベントの購読をすべて開放します。
    /// </summary>
    public void Dispose()
    {
        foreach (var disposable in _Disposables)
        {
            disposable?.Dispose();
        }

        _Disposables.Clear();
    }

    /// <summary>
    /// 指定した <see cref="EventHandler{TEventArgs}"/> を購読します。
    /// <para>
    /// 購読されたイベントハンドラーは、イベントを実行した際に実行されます。
    /// </para>
    /// </summary>
    /// <param name="handler">イベントによって生成されるイベント データの型。</param>
    public void SubScribe(EventHandler<TEventArgs> handler)
    {
        Handler += handler;

        // イベントの購読を終了する
        var disposable = Disposable.Create(() =>
        {
            // Console.WriteLine($"{handler} の購読を解除します。");

            Handler -= handler;
        });

        _Disposables.Add(disposable);
    }

    /// <summary>
    /// <see cref="EventHandler{TEventArgs}"/> のイベントを実行します。
    /// </summary>
    /// <param name="sender">実行オブジェクト。</param>
    /// <param name="args">イベント引数。</param>
    public void Raise(object sender, TEventArgs args) => Handler?.Invoke(sender, args);

    #endregion
}

サンプルコード

予定通り、こんな感じで使います。public event EventHandler ... という宣言ではなくなって、クラスを定義する形になりました。

EventHandler の実態は delegate なんで、構文をそのままってわけには、どうしてもいかないと思います。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");

        var program = new Program();

        program.Run();
    }

    public DisposableEventHandler<int> Sample = new DisposableEventHandler<int>();

    public void Run()
    {
        Console.WriteLine("---");

        Test1();
        Sample.Dispose();

        Console.WriteLine("---");

        Test2();
    }

    public void Test1()
    {
        Sample.SubScribe((sender, args) =>
        {
            Console.WriteLine($"{sender}.{args}");
        });

        Sample.Raise(this, 1);
        Sample.Raise(this, 2);
    }

    public void Test2()
    {
        using (var sample = new DisposableEventHandler<string>())
        {
            sample.SubScribe((sender, args) =>
            {
                Console.WriteLine($"{sender}={args}");
            });

            sample.SubScribe((sender, args) =>
            {
                Console.WriteLine($"{sender}+{args}");
            });

            sample.Raise(this, "ランス");
            sample.Raise(this, "シィル");
        }
    }
}

実行結果はこちら。

f:id:shikaku_sh:20200928205934p:plain

予定通りの感じで動作してそうです。

サンプル

GitHub に「Sample」をあげています。今回のプロジェクトは「DisposableTest」です。

参考

Effective C# 6.0/7.0

Effective C# 6.0/7.0

Programming Reactive Extensions and LINQ (Expert's Voice in .NET)

Programming Reactive Extensions and LINQ (Expert's Voice in .NET)

  • 作者:Liberty, Jesse
  • 発売日: 2011/10/31
  • メディア: ペーパーバック

RxJavaリアクティブプログラミング

RxJavaリアクティブプログラミング

Unity ローカルプッシュ通知のやり方 (Android)

f:id:shikaku_sh:20200910152454p:plain

この記事は Unity で(ローカル)プッシュ通知をするやり方をメモした記事です。

ローカルプッシュ通知とは、端末に通知を送る際にサーバーを使用しません。端末の中に通知予定を入れて、あとから自動的に通知を発生させることが可能です。

プッシュ通知の雰囲気は以下の Android 端末の画像でわかるかと思います。

f:id:shikaku_sh:20200916104208j:plain:h300
こんな感じで、できました

よくあるのは、おつかいを設定して 30 分後になったら、おつかい完了の通知を発生させるようなやつです。

注意:この記事は Android 向けにテストしています。iOS も変わらないと思いますが、テストしていません。


Unity Mobile Notifications Package をインストールする

メニューの「Package Manager」から「Mobile Notifications」をインストールします。

f:id:shikaku_sh:20200916104329p:plain:w600

  • Window > Package Manager > (Menu) Mobile Notifications


プロジェクト設定

Android の通知に表示されるアイコンを設定します。

f:id:shikaku_sh:20200916105010p:plain:w600

  • Edit > Project Settings > (Menu) Mobile Notification Settings

用意するアイコンのサイズは、次のふたつをそれぞれ Type の icon_small と icon_large に設定します。

  • 48x48
  • 192x192

f:id:shikaku_sh:20200916104830p:plainf:id:shikaku_sh:20200916104833p:plain
サンプルで使った画像

48x48 の画像は透明部分をくりぬいた白い画像が表示されました。

設定した画像の identifier はデフォルトだと「icon_0」や「icon_1」になっていますが、このテキストの値はあとで使用するので、きちんと識別子となる名前(テキスト)を設定したほうがよいと思います。

番号をつけても、これは(正確には)連続性がないので、「Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition)」だと Tips 68 のようになっています。やっぱり、推奨とはいかないです。

Android のアイコンアセットには公式が挙げている命名規則がありますので、「アイコン デザイン ガイドライン」をここでは利用することにします。


アイコン アセット共通の命名規約の例

表にすると、こんな感じかと。

アセットタイプ 接頭辞
アイコン ic_ ic_star.png
ランチャー アイコン ic_launcher ic_launcher_calendar.png
メニュー アイコン、アクションバー アイコン ic_menu ic_menu_archive.png
ステータスバー アイコン ic_stat_notify ic_stat_notify_msg.png
タブアイコン ic_tab ic_tab_recent.png
ダイアログ アイコン ic_dialog ic_dialog_info.png

なので、ここでは次のテキストとします。

  • ic_stat_notify_small
  • ic_stat_notify_large

なお、アイコンに使う画像はテクスチャーとして利用しないので「Texture Type」を「Editor GUI and Legacy GUI」にして最適化を防ぐような設定にしました。

f:id:shikaku_sh:20200916104405p:plain


スプリプトの実装

LocalNotification を機能を実行するためのインターフェースを用意しました。この機能は AndroidiOS で別々に実装したほうがよいので、インターフェースを挟んでおいたほうが後々便利です。

using System;

public interface ILocalNotification
{
    /// <summary>
    /// Small Icon タイプのアイコン ID を表すテキストを取得または設定します。
    /// </summary>
    string SmallIcon { get; set; }

    /// <summary>
    /// Large Icon タイプのアイコン ID を表すテキストを取得または設定します。
    /// </summary>
    string LargeIcon { get; set; }

    void CreateChannel(string cannelId, string cannelName, string description);
    void CancelAll();

    int Schedule(string title, string text, DateTime fireTime);
    int ScheduleInSeconds(string title, string text, int seconds);
    int ScheduleInMinutes(string title, string text, int minutes);
    int ScheduleInHours(string title, string text, int hours);
}

Android のコードです。ここがこの記事で一番コアな部分です。

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Notifications.Android;
using UnityEngine;

public class LocalNotificationForAndroid : ILocalNotification
{
    public string ChannelId { get; set; } = "channel-id";
    public string SmallIcon { get; set; } = "icon_0";
    public string LargeIcon { get; set; } = "icon_1";

    public void CreateChannel(string cannelId, string cannelName, string description)
    {
        ChannelId = cannelId;

        var cannel = new AndroidNotificationChannel
        {
            Id = ChannelId,
            Name = cannelName,
            Importance = Importance.High,
            Description = description,
        };

        if (SmallIcon == "icon_0")
        {
            Debug.LogWarning($"{nameof(SmallIcon)} プロパティの値が初期値のままです。アイコンを正しく表示できない恐れがあります。");
        }

        if (LargeIcon == "icon_1")
        {
            Debug.LogWarning($"{nameof(LargeIcon)} プロパティの値が初期値のままです。アイコンを正しく表示できない恐れがあります。");
        }

        AndroidNotificationCenter.RegisterNotificationChannel(cannel);
    }

    public void CancelAll()
    {
        AndroidNotificationCenter.CancelAllScheduledNotifications();
        AndroidNotificationCenter.CancelAllNotifications();
    }

    public int Schedule(string title, string text, DateTime fireTime)
    {
        var notification = new AndroidNotification
        {
            Title = title,
            Text = text,
            FireTime = fireTime,
        };

        notification.SmallIcon = SmallIcon;
        notification.LargeIcon = LargeIcon;

        return AndroidNotificationCenter.SendNotification(notification, ChannelId);
    }

    public int ScheduleInSeconds(string title, string text, int seconds)
        => Schedule(title, text, DateTime.Now.AddSeconds(seconds));
    public int ScheduleInMinutes(string title, string text, int minutes)
        => Schedule(title, text, DateTime.Now.AddMinutes(minutes));
    public int ScheduleInHours(string title, string text, int hours)
        => Schedule(title, text, DateTime.Now.AddHours(hours));
}

このクラスを使って、通知をやってみることにします。注意する点として、登録・通知をしたクラス(ここだと LocalNotificationForAndroid)のインスタンスを保持しておかないと、正しく端末に通知が発生しませんでした。なので、このクラスを singleton にするか singleton クラスのプロパティとして持つようにしてみます。


singleton クラスで保持する例

簡単な singleton パターンを適用したクラスに通知用のクラスを用意しました。ポイントは、アイコンのプロパティに設定しているテキスト値が、「アイコン アセット共通の命名規約の例」で決めた2つのテキストです。

このテキストが、通知されるアイコンと紐づいてしまいます。(テキスト以外の方法で設定をやりたいけど、仕様上やりづらい+ここだけ設定すれば変更することはもう無いので、こうしました)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public sealed class NotificationManager
{
    public static NotificationManager Instance { get; } = new NotificationManager();

    public ILocalNotification Notification { get; private set; }

    private NotificationManager() 
    {

    }

    public void Initialize()
    {
        ILocalNotification notification = new LocalNotificationForAndroid();

        notification.SmallIcon = "ic_stat_notify_small";
        notification.LargeIcon = "ic_stat_notify_large";

        notification.CreateChannel("sample0916", "sampleName", "sampleDescription");

        Notification = notification;
    }
}

ここで突然 ChannelID、CannelName、Description をサンプル的な値で埋めていますが、別にこのコードでも Android で動作するのを確認しています。Android の通知仕様を確認するとわかりやすいかもしれません。(おそらくこれをアダプトしてるのだと思います)

Channel ID は、チャンネル登録の関数と、スケジュール登録の関数で同じ値が利用されています。アイコンのように Unity プロジェクト内と紐づくものは無いと思います。

拡張性の観点から挙げるポイントは、以下のコードブロックです。

ILocalNotification notification = new LocalNotificationForAndroid();

下コードの感じにするだけで、切り替えることができると思います。インターフェースでメソッドを定義してあるので、通知予約のメソッドも共通化できているので便利。(それ以外は LocalNotificationForDummy とかもいいかも)

#if UNITY_ANDROID
    ILocalNotification notification = new LocalNotificationForAndroid();
#elif UNITY_IOS
    ILocalNotification notification = new LocalNotificationForiOS();
#else
    ILocalNotification notification = new LocalNotificationForDummy();
#endif

あと、singleton パターンはとても強力なので、設計のやり甲斐があります。NotificationManager だと他に持つ機能がないかもしれません。EnvironmentManager のようなものに統合してもいいかもしれないです。


通知をテストする(+使い方)

アプリケーションの開始直後に設定をして、あとはそのインスタンスを使いまわす感じにしてみます。なので、アプリケーションのエントリーポイントでやってみることにします。

アプリケーションのエントリーポイントは次の記事で説明しています。

こんな感じになりました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class App
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void EntryPoint()
    {
        // 初期化
        var manager = NotificationManager.Instance;

        manager.Initialize();

        // サンプル用コード
        manager.Notification.ScheduleInSeconds("テストのタイトル", "テストのテキストです。", 10);

        Debug.Log("EntryPoint Called.");
    }
}

実行結果は、こうなります。問題なさそうですね。

f:id:shikaku_sh:20200916104208j:plain:h500
こんな感じで、できました


サンプル

GitHub に「unity-local-notification-practice」サンプルを公開しています。


参考

Unity 変数の値が変化したとき“1度だけ”コードを実行したい「ObservedValue」パターン

f:id:shikaku_sh:20200910152454p:plain

変数の値が変化したとき“だけ”コードを実行したいというような、ゲーム中ではよくあるイベントをどのように実装するのか、という話についてです。

この内容は「Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition)」の Tips 40 の内容に挙がっていた「記事」を個人的に訳をした内容です。

ObservedValue クラスは、著者の作成したクラスですが、とてもシンプルなクラスだったので、この記事の最後にクラスのコードを加筆しています。


「ObservedValue」はどういうものか(The new class in Extensions, ObservedValue: what is it for and how to use it)

もし、あなたがたくさんのゲームのコードを書いたことがあるなら、こんなコードを書いていたことがあるのではないか:

class MyBehaviour : MonoBehaviour
{
   int count;
   
   public void Start()
   {
       count = CalculateCurrentCount();
   }

   public void Update()
   {
       var currentCount = CalculateCurrentCount();
       
       if(currentCount != count)
       {
           count = currentCount;
           DoSeomtingExpensive();
       }
   }
}

これは、ある値を(たとえば、フレームごとに)チェック or 計算したりしているが、値が変化しときに“だけ”なんらかのアクションを実行する、という考えです。

上のコードのサンプルでは、それほど悪いものではないですが、ファイルの内にこんなコードをいくつか作っているなら、それは面倒を起こす可能性があります。

また、特に値を複数の場所で更新する場合や、Update() メソッドの代わりに値の変更後にすぐチェックを実行したいなら、エラーを作りやすいです。

ObservedValue (Assets Store) は、このコードをきれいにパッケージ化することを目的としています。これは、ひな形 (boiler plate) の部分を担うと共に、値が変更されたとき、アクションを問題なく実行し、同じコード中で使い分けることができます。

class MyBehaviour : MonoBehaviour
{
   ObservedValue<int> count;
 
   public void Start()
   {
      count = new ObservedValue(CalculateCurrentCount());
      count.OnValueChanged += DoSeomtingExpensive;
   }
 
   public void Update()
   {
      count.Value = CalculateCurrentCount();
   }
}

これは、最初のバージョン(おそらく Assets のバージョンの話)と同じ機能のことをしていますが、よりクリーンなコードになりました。

重要な使用例のひとつとしては、ユーザーが値を変更したとき、あなたがなにか(別の値など)を変更したい場合、すぐに GUI (エディター)を使用することがあります。実際、私たちがクライアントのために開発したレベルエディターには、画面に描画されるものに影響をあたえる変数が20程度あって、ツールを改良するうちに多くのバグに遭遇しはじめたため、この ObservedValue クラスを書きました。

もうひとつ役に立つ戦略は、フレームごとに複数の計算をしないようにするため、state machine パターンと組み合わせることです。(これを普通にやると、見苦しいフラグ管理になる)以下は、state machine を使わない例:

class MyBehaviour : MonoBehaviour
{
   bool isDirty;
   ObservedValue<int> count1;
   ObservedValue<int> count2;
 
   public void Start()
   {
      isDirty = false;
      count1 = new ObservedValue(CalculateCurrentCount1());
      count1 += () { isDirty = true; };
 
      count2 = new ObservedValue(CalculateCurrentCount2());
      count2 += () { isDirty = true; };
   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();
 
       if(isDirty)
       {
           DoSomethingExpensive();
           isDirty = false;
       }
   }
}

例によって、この単純なケースではそれほど悪いコードになっていません。複数のクラスにまたがっていたり、見苦しくなるコードの箇所がいくつもあったり、見苦しさのレベルがひどかったりすると、コードはより複雑になってしまいます。

つぎのコードは、state machine パターンを使ってどのように見えるのかを示す:

class MyBehaviour : MonoBehaviour
{
   public enum DirtyState {Dirty, Clean};
 
   StateMachine dirtyManager;
   ObservedValue<int> count1;
   ObservedValue<int> count2;
 
   public void Start()
   {
      dirtyManager = new StateMachine();
 
      dirtyManager.AddState(DirtyState.Clean);
      dirtyManager.AddState(
          DirtyState.Dirty, 
          null, 
          () => 
          {
             DoSomethingExpensive();
             dirtyManager.SetState(DirtyState.Clean);
          }); 
 
      count1 = new ObservedValue(CalculateCurrentCount1());
      count1 += () { dirtyManager.SetState(DirtyState.Dirty); };
 
      count2 = new ObservedValue(CalculateCurrentCount2());
      count2 += () { isDirty = true; };
 
   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();
       
       dirtyManager.Update();
   }
}

上くらいシンプルなサンプルだと、state machine パターンを使用してもメリットはありません。恩恵を得ることができるのは、状況がもっと複雑になってからです。もちろん、上のコードにはひな形 (biler plate) になる部分があるので、あなたが DirtyManager で処理してもよいでしょう。(DirtyManager は、将来の拡張ライブラリとしてよい手です)

つぎに、そういったクラスのドラフトを示します:

public class DirtyManager
{
   private enum DirtyState {Dirty, Clean};
 
   private StateMachine dirtyManager;
   public event OnShouldCleanDirt; //needs a better name!
 
   public DirtyManager()
   {
      dirtyManager = new StateMachine();
 
      dirtyManager.AddState(DirtyState.Clean);
      dirtyManager.AddState(
          DirtyState.Dirty, 
          null, 
          () => 
          {
             if(OnShouldCleanDirt != null) 
             {
                 OnShouldCleanDirt();
             }
 
             dirtyManager.SetState(DirtyState.Clean);
          }); 
   }
 
   public void Update()
   {
       dirtyManager.Update();
   }
 
   public void Observe(ObservedValue observedValue)
   {
       observedValue.OnValueChanged += SetDirty;
   }
   
   private void SetDirty()
   {
       dirtyManager.SetState(DirtyState.Dirty);
   }
}

サンプルはこうなります:

class MyBehaviour : MonoBehaviour
{
   DirtyManager dirtyManager;
   ObservedValue<int> count1;
   ObservedValue<int> count2;
 
   public void Start()
   {
      count1 = new ObservedValue(CalculateCurrentCount1());
      count2 = new ObservedValue(CalculateCurrentCount2());
 
      dirtyManager = new DirtyManager();
 
      dirtyManager.Observe(count1);
      dirtyManager.Observe(count2);
 
      dirtyManager.OnShouldCleanDirt += DoSomethingExpensive; 
   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();
       
       dirtyManager.Update();
   }
}

コードはよりクリーン(わかりやすく)なり、追加する複雑さに対してより強かになりました。

最後に、テストコードは state machine パターンが本当に必要ですか? 答えは「必要ありません」。簡単なコードを保つように、bool を使います。

しかし、より複雑なケースでは、state machine パターンは価値を提供することができます。見苦しさのレベル、特別な状況に対処するためなどに使えます。(たとえば、なにか他のことが起こるまで、計算を遅らせたい場合など)


補足(ObservedValue のコード確認)

以下の内容は、新しく私のブログで補足として調べた内容です。この記事のメインコンテンツである ObservedValue は UniRx の一部分(ObserveEveryValueChanged など)のようなものですが、下記コードのように実装はとてもシンプルのようです。

Gamelogic Extensions
Version 2.5 Feb 21, 2019:
Assets>Gamelogic>Extensions>Plugins>Scripts>Patterns

public class ObservedValue<T>
    {
        private T currentValue;
        public event Action OnValueChange;

        public ObservedValue(T initialValue)
        {
            currentValue = initialValue;
        }

        public T Value
        {
            get { return currentValue; }

            set
            {
                if (!currentValue.Equals(value))
                {
                    currentValue = value;

                    if (OnValueChange != null)
                    {
                        OnValueChange();
                    }
                }
            }
        }

        public void SetSilently(T value)
        {
            currentValue = value;
        }
    }

基本的には、Value プロパティに対して値を設定するのがこのクラスの用途のようですね。このときに、値が一致しなければ !Equals(value)OnValueChange イベントを実行するという仕組みです。

値を設定するときに、このイベントを実行したくないときは、SetSilently() メソッド経由で現在値を更新しています。こんな感じで、趣味でプログラミングをしていて UniRx を無免許運転するくらいなら、車輪の再開発をして、こうしたコードテクニック自体をちょっとずつ覚え、地に足をつけたコーディング力を身につけるのも悪くないと私は思います。


パフォーマンスに関する意見

見てわかるとおり、もともとのコードは if 文をひとつ入れて処理していた内容が、クラスを定義して作ったプロパティは、if 文が2つにイベントを実行すると、パフォーマンスの上では間違いなく(僅かに)低くなった実装になっていることが伺えます。

このテクニックが良いことか悪いことかを考えてみます。この答えのひとつを挙げると、リファクタリングの善し悪しを、審美的なものにして、個人の好みとするのは微妙です。

単なる好みではない面からいくと、一般的な意味での良いコートとは、どれだけ変更が容易なのかを示す柔軟性・拡張性で決まっています。なので、ほんの少し低速になる代わりに、(この頻出するだろう問題に対して)健全なコードになるようリファクタリングし、(元のコードよりも)生産性をよりよい状態にした ObservedValue はよいコードといえるかもしれません。

僅かなパフォーマンスとリファクタリングの関係についてもっと知りたい場合は、リファクタリング関係の本を読むといいです。

最初の説明にもあったように、シンプルなコードなら、こうしたリファクタリングは不要だと思います。しかし、見た目に冗長であること+コード設計として劣化を早めやすい傾向のコードなら、すくなくとも注意が必要です。

コンピューターがプログラムを実行する際に、CPU サイクルを余分に少し必要にするという事態よりも、コーダーの都合を大切にするようなコーディングを選択するような選択は年々増えてきています。

パフォーマンスを要求するゲームでも、細かい部分は作業性・メンテナンス性をとりますよ、と。(そもそも Unity + C# だし、そりゃそうだよね)


参考

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

  • 作者:結城 浩
  • 発売日: 2004/06/19
  • メディア: 大型本