ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

List, Array, ForEachの速度比較(BenchmarkDotNetで遊ぶ)

List.ForEach()よりはforeachループ、
Listのforeachループよりはforループ、
foreach回すだけならToListしなくても、

といったことは知ってますが、実際のところどのくらいどれが有利なの?というのをちゃんと知っておかないと、他人に説明できなくて困る。
というわけでBenchmarkDotNetを使って調べてみましょう。 BenchmarkDotNetの使い方を調べてみたら全く同じものを検証してるのが見つかりますが、自分でやるのに意味があるんだと言い聞かせ。

以下コード

using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Attributes;

namespace BenchmarkTest
{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<TestClass>();
        }
    }
    [MemoryDiagnoser]
    public class TestClass
    {
        IEnumerable<int> intEnum;

        [GlobalSetup]
        public void Setup()
        {
            intEnum = Enumerable.Range(0, 1000);
        }

        [Benchmark]
        public void ToListForEach()
        {
            intEnum.ToList().ForEach(i => { });
        }

        [Benchmark]
        public void ToListForEachLoop()
        {
            foreach (var i in intEnum.ToList()) { }
        }

        [Benchmark]
        public void ToListCastForEachLoop()
        {
            IEnumerable<int> l = intEnum.ToList();
            foreach (var i in l) { }
        }

        [Benchmark]
        public void ToListForLoop()
        {
            var list = intEnum.ToList();
            for (var i = 0; i < list.Count; i++) { }
        }

        [Benchmark]
        public void ToArrayForEachLoop()
        {
            foreach (var i in intEnum.ToArray()) { }
        }

        [Benchmark]
        public void ToArrayForLoop()
        {
            var array = intEnum.ToArray();
            for (var i = 0; i < array.Length; i++) { }
        }

        [Benchmark]
        public void ForEachLoop()
        {
            foreach (var i in intEnum) { }
        }
    }
}
Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
ToListForEach 3.904 μs 0.0611 μs 0.0542 μs 1.2894 - - 4,056 B
ToListForEachLoop 3.931 μs 0.0477 μs 0.0446 μs 1.2894 - - 4,056 B
ToListCastForEachLoop 8.409 μs 0.0894 μs 0.0837 μs 1.2970 - - 4,096 B
ToListForLoop 2.079 μs 0.0197 μs 0.0164 μs 1.2894 - - 4,056 B
ToArrayForEachLoop 1.261 μs 0.0146 μs 0.0130 μs 1.2817 - - 4,024 B
ToArrayForLoop 1.052 μs 0.0119 μs 0.0111 μs 1.2817 - - 4,024 B
ForEachLoop 5.068 μs 0.0413 μs 0.0386 μs 0.0076 - - 40 B
  • forechループの速度は、配列 > List > IEnumerable
  • ForEachは速度は実は悪くない(内部的にはforループ) referencesource.microsoft.com けどアロケーションが段違いで、大量に回す場合はこっちのほうがよっぽど影響大きい。
  • ToArrayのループも、配列なのでListよりは速いとは思っていたものの、ここまで早いとは思わなかった。
  • IEnumerableがToListしたやつよりも遅いのはMoveNextのパフォーマンスですかね。ListのGetEnumeratorが返すEnumeratorはList用に最適化されてますので、Rangeの返すクラス(Rangeはただのyield returnなのでコンパイラ生成のEnumerator)より優秀なんでしょう。
  • ToListした上でIEnumerableとして扱うのは良くないとこ取りで最悪。Enumeratorのボックス化もある。

正直ToListForEachはもっと散々な結果になると思っていましたので、意外ですね。なんか~.ToList.ForEachって記述を見かけて、この結果を見せて納得してもらおうと思っており、全てにおいて最悪!みたいなのをちょっと期待してたんですけどね。
この速度差とアロケーション差ならやはりForEachが優先されるケースは無いので十分ではありますか。

DIContainerとMessagePipeでできる設計の話

まだ全然調査しきれてないので、ちょっと触った感じ、こういう認識でいいのかなぁ、というお話です。

VContainerについて

github.com light11.hatenadiary.com - VContainerとは、導入しやすい、わかりやすいDIコンテナです。
- DIコンテナとは、色んなとこから参照されうるクラス/インスタンスをまとめて管理して、ちょっと[Inject]とか書くだけでそこに入れてもらえる(わざわざsetメソッド的なものを書く必要が無い)仕組みです。
- DIコンテナという神がいるので、クラスの依存関係をこいつに任せることができます。
- 結果、MonoBehaviourはViewとしてのみ働き、ロジックは(物理演算とかは例外になっちゃうかもしれんけど……)非MonoBehaviourなクラスに書けて嬉しいです。

という認識。

DIコンテナを使用すると、Unityで qiita.com この記事で言うところのレベル4を実現することができます。
設計レベルは高ければいいというものでもない、というのは賛成ですが、
(「絶対的な答えなど無く、各々の状況に応じた最善を常に考えよ」という話なので、これは「設計レベルは高ければ高いほどいい」と言い切るよりもある意味厳しい結論と言えます)
ただ、個人的な感覚で言うと「MonoBehaviourはViewだけやってろ!」という思考は、一度に考えることを減らせて嬉しいことがほとんどじゃないかなと思います。
それを綺麗に実現できるならば、学習コストを考えても、多くのプロジェクトで導入を検討する価値は高いと思います。
まぁ「学習コスト」の中身は、ツールの使い方というよりも、それで何が嬉しいのかの理解の方が重いので、使い方がわかればOKという話では全然ないのが罠ですが……

MessagePipeについて

github.com qiita.com (↑わかりやすい!)
さて、Viewとその他(例えばPresenterとModel)の関係性はDIコンテナの導入で解決できたとします。
が、実際にコードを書いていくと全てを疎結合というのは無理だよな~という現実にブチ当たります。実体験では。
まぁ何でもかんでも疎結合にする必要も無いとは思いますが(めっちゃ自然な親子関係とか)、ただ、他クラスを参照するようなケースはできるだけ避けたいものです。数が多くなったり階層が深くなると見づらくて嫌でもありますしね。
これまでは例えばUniRxでIObservableだけ公開してイベントの通知のみを行うなどして、比較的緩やかな繋がりを実現していたと思われますが、そうは言ってもクラスやインターフェースのプロパティとしては参照しちゃってます。
私も、

public IObservable<T> OnClick => m_fooClass.OnClick;

こんなんで深いところのIObservableがViewとかModelの一番親のクラスまで伝って行く景色をよく見ました。
クソでかシーンになるとViewもModelも結構大きくなり、この作り方では参照の矢印がねずみ講のように伸びてしまいそう。
じゃあ各クラスに関して、「何を外に伝えうるのか」「何が外から伝わりうるのか」も切り分けて、それもDIしちゃおうぜ! という。
UniRxにもMessageBrokerという、シングルトンなSubjectを使ってイベントの通知ができるみたいな仕組みはあったので、それでいいことも多いとは思いますが。
でも、VContainerやZenjectを既に使っているならばとりあえず利用できそうなところで利用していいんじゃないかなぁと思います。
特に、AsyncなPub/Sub(特に特にAsyncなRequestHandler)がいいですね。なんかもう何でもできそうって感じ(雑)。

正直設計まだまだ甘ちゃんなので、全然違うわボケ! っていう可能性も大いにある気がしますが、そのときは教えてください!

UIElementsとUIBuilder

UIElementsとUIBuilderについて

まずはUIElementsとUIBuilderについて軽く触れておきます。
Unity2019.1から新しく導入されたUIElementsについて。
簡単に使い方やどんなところが良さそうなどメモっておこうと思います。

UIElementsのいいところ

見た目をすごく綺麗に整えられる

従来のOnGUIではUIのレイアウトをちゃんと整えようとすると地獄のようなコードを書く必要がありましたが、UIElementsは見た目の部分を完全に分離しているためそのようなことは起こりません。
さらに、UIBuilderを使用することで(基本的に)完成状態のUIを確認しながら作っていけるようになりました。
f:id:u_osusi:20210305210004p:plain 感覚的にはWPFとかなり近いですね。

動作が軽い

OnGUIのように「カーソル動かしてる間毎フレーム描画更新するぜー」なんてことないので、大規模なUIを作っても重くなりにくいです。

つまり?

選択中のSpriteを9Sliceしてくれる、みたいな、UIどうでもいいようなエディタ拡張に使うには大げさすぎますが、
使用者があんまりUnityに慣れていなかったりするケースなど、
「とにかく誰でも見たら何すればいいかわかるUI」
が必要ならUIElementsで見た目を整えるのは有効かと思います。
ただし、いろんな人が好き勝手にUIを作ってしまうと見た目がバラバラになってしまうので、ゲーム内のUIを作るとき同様しっかりデザインルールは設けた方がいいですね。
UIパーツをプレハブみたいに扱う方法もあるのでそういうのも活用しましょう。

学び方

良い解説記事がたくさんあるので基本的にはそちらに丸投げ……
私が初めて使ったときにはほとんど解説記事が無く、参考にしたのはこちらのUnity公式の動画だったんですが https://learning.unity3d.jp/4077/ もうUIBuilderの見た目も違いますね。でも基本的なことは変わっていないので、今でも参考にできそうです。

比較的新しいものですと、こちらがすごくわかりやすく基本を押さえてくれています。 light11.hatenadiary.com

このあたりの解説を見て以下のような流れで見ていけば使えるようになってると思います。

  • UIはVisualElementsのツリー構造で管理されていることを理解する
  • 見た目は.uxmlファイル、動作はcsコードで書く
  • .uxmlはUIBuilderで編集することもできる
  • 基本的なUIパーツの使い方はサンプルがあるのでそれを見る

↓サンプル

f:id:u_osusi:20210305205812p:plain f:id:u_osusi:20210305210050p:plain

SerializeReferenceでシリアライズしたときyamlファイルはどうなってるのか

先日投稿いたしました、こちらの記事の補足、というか余談です。

nigiri.hatenablog.com

Unityのアセット、シーンやプレハブ、ScriptableObjectはyamlファイルとして保存されています。

SerializeReferenceを用いた場合、yamlファイルはどうなっているのでしょうか。

上記の記事で作成したsampleの.assetをテキストエディタなどで開いてみると

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 0}
  m_Name: New Sample Class
  m_EditorClassIdentifier: Assembly-CSharp::SampleClass
  _array:
  - id: 0
  - id: 1
  - id: 2
  references:
    version: 1
    00000000:
      type: {class: SampleClassA, ns: , asm: Assembly-CSharp}
      data:
        _name: 
        _value: 0
    00000001:
      type: {class: SampleClassB, ns: , asm: Assembly-CSharp}
      data:
        _name: 
        _description: 
    00000002:
      type: {class: , ns: , asm: }

このようになっています。

おや、これはめちゃくちゃ見やすいのでは……

asm(アセンブリ)、ns(名前空間、グローバルだと空文字っぽい)、class(クラス名)と型を明記したうえでシリアライズされているおかげで、 すごく見通しが良く感じます。

シリアライズされている参照先インスタンスにIDが振られている形ですので、同じインスタンスを指定したければ同じIDを指定すればいいようです。

SerializeReferenceとSubclassSelectorがすごい便利

Unity2019.3から使用可能なSerializeReferenceと、

docs.unity3d.com

下記で公開くださっているSubclassSelectorがすっごい便利でしたので布教したくなりました。

github.com

SerializeReferenceについても素晴らしく丁寧な説明をしてくださっています。

light11.hatenadiary.com

なお、今回の記事はUnity2020.2f1で動作を確認したものとなります。

使ってみた

/// <summary>
/// SerializeReference用サンプルScriptableObject
/// </summary>
[CreateAssetMenu]
public class Sample : ScriptableObject
{
    [SerializeReference, SubclassSelector] private ISample[] _array;
}

/// <summary>サンプル用インターフェース</summary>
public interface ISample { }

/// <summary>サンプル用クラスA</summary>
public class SampleClassA : ISample
{
    [SerializeField] private string _name;
    [SerializeField] private int _value;
}

/// <summary>サンプル用クラスB</summary>
public class SampleClassB : ISample
{
    [SerializeField] private string _name;
    [SerializeField] private string _description;
}

これが、こうな……る……? あれ? f:id:u_osusi:20210204193120p:plain

しまった。基本的にどんなプロジェクトでもエディタ拡張アセットのOdinが使えるようにしているのでOdinアリの見た目になってました。 どうやらOdinさんはもともとSerializeReferenceだと代入可能な型を選択できる機能が備わっていたようですね。 さすが大人気アセット。隙がねぇな。

assetstore.unity.com

さて、Odin無い通常バージョンでの見た目を改めて確認すると f:id:u_osusi:20210204193719p:plain

期待した通りになってますね!

インターフェース型変数をシリアライズしたいんだよなぁというケースはめちゃくちゃあったので、これはすごくありがたいですね!

ちょっと改良してみる

仕事だと、実際にエディタで色々と調整を施すのはプランナーなのが多い、と思います。(小規模ならそうでもないことも多そうですけどね) なので、クラスの名前がそのまま表示されちゃうのはちょっと困るかも。そんなときは表示用のメタデータ

public class NameAttribute : Attribute
{
    public string Name { get; }
    public NameAttribute(string name) => Name = name;
}
/// <summary>サンプル用クラスA</summary>
[Name("サンプルA")]
public class SampleClassA : ISample
{
    [SerializeField] private string _name;
    [SerializeField] private int _value;
}

/// <summary>サンプル用クラスB</summary>
[Name("サンプルB")]
public class SampleClassB : ISample
{
    [SerializeField] private string _name;
    [SerializeField] private string _description;
}

こうしてあげると f:id:u_osusi:20210204193726p:plain

よさげ。

注意点

SerializeReferenceはその名の通り、参照をシリアライズします。 そのため、SubclassSelectorは「指定された型のインスタンスを生成してその参照をシリアライズ」という手法を取っています。 SubclassSelectorDrawer.csを見ていただくとわかるんですが、インスタンスの生成にはActivator.CreateInstanceを使用しています。 Activator.CreateInstanceはnewと同じですしこれは正しいのですが、生成したい型に引数無しのコンストラクタが無いとエラーが出てしまいます。

(インスペクタ表示用のクラスにデフォルト以外のコンストラクタを用意するのは悪手だとは思いますけどね)

これについての説明と対処法はこちらの記事で解説しています。

nigiri.hatenablog.com

余談

f:id:u_osusi:20210204195422p:plain

Unity2020.2f1で、順番入れ替え可能なReordableListがデフォルトで使えるようになっているようです。 前まではinternalで存在していて、知る人ぞ知るものだったんですが嬉しいですね。 またUnity2020.2からはC#バージョンも8に上がり(.NET Standardは2.0のままですが…)、Switch式など超便利な記述が多数可能になっていて、これも見逃せません。

【Unity】transform.positionのメンバ変数を直接変更できない理由

小ネタです。(言語をちゃんと理解してる人には当たり前の話です。)

transform.position.x = 1f;

Unityを使い始めの頃、だれしもこんなコードを書いたことと思います。 そしてエディタに怒られたことでしょう。
f:id:u_osusi:20190917224101p:plain
これを解決するために、

var newVec = new Vector3(1f, 0, 0);
transform.position = newVec;

わざわざVector3をnewして代入する方法を取ったのではないかと思います。 (SetメソッドというのがVector3にはあるのですが、positionに使うのは罠です。なぜ罠かは後述します。)

解決方法はこれでいいとしてそもそもなぜ

transform.position.x = 1f;

は許されなかったのか。

理由はエディタが教えてくれているように、transform.positionが変数ではなくプロパティだから。
プロパティということは実際にはその裏にある変数を

public Vector3 get()
{
    return _position;
}

こんな感じで返しているのと同じです。
さて、ここで重要なのは、Vector3がstructであるということ。
structをreturnする際、実際には、returnの直後に書かれているstructではなくそのコピーが返されています。
f:id:u_osusi:20190917225855p:plain
このように、

transform.position.x = 1f;

はもしコンパイルエラーが起こらなかったとしても、本当に変更したい値ではなく、一瞬で消え去るそのコピーを変更することになっているわけです。 こういう無駄なことをさせないためにコンパイルエラーにしてくれているというわけですね。

さて、Vector3にはSetメソッドがあり、

github.com

transform.position.Set(1f, 0, 0);

のように書くことができます。これはコンパイル通ります。
が、実際には1fを代入していた場合と同様、コピーを変更しているだけなので、本当に変更したいpositionはなんの変更も受けません。

もちろんちゃんと勉強した人は値型と参照型の違いなんて知っているんですが、知っていても書いてるときに気付けないのがstructの怖いところ。
ただやはりstructは細かいところでパフォーマンス向上に貢献してくれます。 最近ではreadonlyを付けるなど、危険性を無くした安全なstructの使用が可能になってきているので、是非そういった機能を活用しましょう。

ufcpp.net

【Unity】UnityEventのRemoveAllListenersはインスペクタから登録したコールバックを削除しない

小ネタです。

[SerializeField] private Button button;
button.onClick.AddListener(callback);

ButtononClickにはスクリプトからコールバックを登録できます。 そして、

button.onClick.RemoveAllListeners();

スクリプトから、登録したコールバックを削除することもできます。

が、
f:id:u_osusi:20190917221203p:plain
インスペクタから登録したコールバックは削除されません。

理由は、UnityEventの基底クラス、UnityEventBaseの中身を見るとわかります。

github.com

private InvokableCallList m_Calls;
[FormerlySerializedAs("m_PersistentListeners")]
[SerializeField] private PersistentCallGroup m_PersistentCalls;

AddListenerとかでいじられているのは全部m_Callm_PersistentCallsは無傷で、これがインスペクタから登録するやつです。 persistentの名前の通り、簡単には削除できないようですね。(リフレクションでごり押したら知らんけど)