Unityな日々(Unity Geek)

Unityで可視化アプリを開発するための試行錯誤の覚書

シェーダーでトグル・列挙型のプロパティを使う

MaterialPropertyDrawer

Shaderlabのシェーダ・プロパティに、インスペクタでトグルや列挙型(enum)などを使うには、UnityEditorのMaterialPropertyDrawerクラスを使う。

Unityの組み込みMaterialPropertyDrawerには次がある。

Drawer 記述 機能
ToggleDrawer [Toggle], [Toggle(変数名)] トグル
EnumDrawer [Enum(列挙型名)], [Enum(列挙型要素名/インデックス)] 列挙型
KeywordEnumDrawer [KeywordEnum(要素名,...)] キーワード列挙型
PowerSliderDrawer [PowerSlider(指数)] 指数スライダ
IntRangeDrawer [Toggle] 整数スライダ
Space [Space(スペース量)] インスペクタの行間スペース
Header [Header(表示テキスト)] インスペクタのヘッダ情報

MaterialPropertyDrawerをつかったサンプルシェーダー

シェーダ・プロパティにトグルを実装する

次のチュートリアルにあるシェーダ(Transparent Shader)を、透明度(Transparency)の適用・非適用を切り替えるトグルを追加する。

unity3d.com

Transparencyの適用・非適用を、ToggleDrawerを使って実装してみる。

Propertyにトグル要素を追加する。

[Toggle] _Apply_Transparency("Apply Transparency", Float) = 0

トグルの設定値は、"大文字のプロパティ名+_ON"というシェーダーキワードに、トグルの状態に応じて、0または1の値がセットされる。上の例では、_APPLY_TRANSPARENCY_ONというシェーダーキーワードを参照する。

あるいは、シェーダーキーワードを直接指定することもできる。その場合は、Toggleに引数をつけ、

[Toggle(TRANSPARENCY)] _Apply_Transparency("Apply Transparency", Float) = 0

のように記述する。

シェーダーキーワードの取得と参照

シェーダーキーワードをCGINCブロックで使うためには、シェーダーキワードを

#pragma shader_feature シェーダーキーワード

のようにする。上の例では次のようになる。

#pragma shader_feature _APPLY_TRANSPARENCY_ON

実はこれはシェーダーキーワードがない場合の省略形で、フル標記は、

#pragma shader_feature _ _APPLY_TRANSPARENCY_ON

となる。

あるいは、

#pragma multi_compile _ _APPLY_TRANSPARENCY_ON

とも書ける。

shader_featureとmulti_compileの違いは、UnityのShader Variantについて調べてみた - Qiitaを参照ください。

シェーダーキーワードの参照(評価)は、

#ifdef シェーダーキーワード
         シェーダーキーワードがTrue(1)の場合の処理
#else
          シェーダーキーワードがFalse(0)の場合の処理
#endif

のように書く。

サンプルシェーダー

冒頭のTransparent Shaderに、Transparencyの適用・非適用を指定するトグルを追加したシェーダは次の通り。

Shader "XOOMS/Test/Transparent"
{
    Properties
    {
        _MainTex ("Albedo Texture", 2D) = "white" {}
        _TintColor("Tint Color", Color) = (1,1,1,1)
        _Transparency("Transparency", Range(0.0,0.5)) = 0.25
        _CutoutThresh("Cutout Threshold", Range(0.0,1.0)) = 0.2
        _Distance("Distance", Float) = 1
        _Amplitude("Amplitude", Float) = 1
        _Speed ("Speed", Float) = 1
        _Amount("Amount", Range(0.0,1.0)) = 1

        [Toggle] _Apply_Transparency("Apply Transparency", Float) = 0
    }

    SubShader
    {
        Tags {"Queue"="Transparent" "RenderType"="Transparent" }
        LOD 100

        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _TintColor;
            float _Transparency;
            float _CutoutThresh;
            float _Distance;
            float _Amplitude;
            float _Speed;
            float _Amount;
            
            v2f vert (appdata v)
            {
                v2f o;
                v.vertex.x += sin(_Time.y * _Speed + v.vertex.y * _Amplitude) * _Distance * _Amount;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            #pragma shader_feature _APPLY_TRANSPARENCY_ON
            
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv) + _TintColor;

                #ifdef _APPLY_TRANSPARENCY_ON
                    col.a = _Transparency;
                #else
                    col.a = 1;
                #endif
                
                clip(col.r - _CutoutThresh);
                return col;
            }
            ENDCG
        }
    }
}

列挙型(enum)プロパティを実装する

同じサンプルシェーダに、列挙型(enum)プロパティを適用してみる。

列挙型プロパティの場合のシェーダーキーワードは、"プロパティ名_列挙名"となる。シェーダーキーワードは、

#pragma multi_compile シェーダーキワード( シェーダーキワード2 シェーダーキーワード3 ......)

で、CGINCブロック内で利用できるようになる。

次は、enumプロパティで、透明度を3段階に切り替えるサンプル。

Shader "XOOMS/Test/Transparent"
{
    Properties
    {
        _MainTex ("Albedo Texture", 2D) = "white" {}
        _TintColor("Tint Color", Color) = (1,1,1,1)
        _Transparency("Transparency", Range(0.0,0.5)) = 0.25
        _CutoutThresh("Cutout Threshold", Range(0.0,1.0)) = 0.2
        _Distance("Distance", Float) = 1
        _Amplitude("Amplitude", Float) = 1
        _Speed ("Speed", Float) = 1
        _Amount("Amount", Range(0.0,1.0)) = 1

    [KeywordEnum(Zero, Weak, Strong)] _Strength ("Transparant Strength", Float) = 0
    }

    SubShader
    {
        Tags {"Queue"="Transparent" "RenderType"="Transparent" }
        LOD 100

        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _TintColor;
            float _Transparency;
            float _CutoutThresh;
            float _Distance;
            float _Amplitude;
            float _Speed;
            float _Amount;
            
            v2f vert (appdata v)
            {
                v2f o;
                v.vertex.x += sin(_Time.y * _Speed + v.vertex.y * _Amplitude) * _Distance * _Amount;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            #pragma multi_compile _STRENGTH_ZERO _STRENGTH_WEAK _STRENGTH_STRONG
            
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv) + _TintColor;

        #ifdef _STRENGTH_ZERO
                    col.a = 0.1;
                #elif _STRENGTH_WEAK
                    col.a = 0.5;
        #else
                    col.a = 1;
                #endif
                
                clip(col.r - _CutoutThresh);
                return col;
            }
            ENDCG
        }
    }
}

独自のMaterialPropertyDrawerの作成

MaterialPropertyDrawerクラスを継承して、オリジナルのPropertyを作ることもできる。

docs.unity3d.com

UnityEventのコールバック関数に、動的に引数を渡す

unitygeek.hatenablog.com

上のブログに書いたように、UnityEventをpublic変数で定義すると、インスペクタにUnityEvent変数が表示され、インスペクタ上でコールバック関数を追加・削除できる。

f:id:yasuda0404:20171219125311p:plain

コールバック関数が引数をとる場合は、インスペクタ上に引数を設定するフィールドも表示される。

f:id:yasuda0404:20171219125533p:plain

UnityEventの問題(制約)

しかし、この方法には次のような問題(制約)がある。

  1. 引数は1つしか取れない。引数が2つ以上あるメソッドは、インスペクタのリストに表示されず、指定することができない。

  2. 引数はインスペクタ上で静的に指定され、実行時にスクリプトで動的に指定することができない。

コールバック関数の引数を動的に指定する

UnityEventのリファレンスを見ると、UnityEvent以外に、UnityEvent<T0>, UnityEvent<T0, T1>, UnityEvent<T0, T1, T2>, UnityEvent<T0, T1, T2, T3>の4つのGenericsがある。これを使えば、UnityEventで引数を渡せそうだ。

しかし、

public UnityEvent<string> TestEvent;

のように書いてもインスペクタには何も表示されない。

実はUnityEventクラスのGenericsは抽象クラスのため、継承クラスでしか使えない。

UnityEventで引数を渡すには(Genericsを使うには)、UnityEventのGenerics形を継承した独自クラスを定義する。

独自クラスは[Serializable]でシリアル化する(でないとインスペクタに表示されない)。

[Serializable] public class MyEvent : UnityEvent<string> { }
[SerializeField] MyEvent OnChange;
...
...
OnChange.Invoke("MyEvent");    //引数付きイベントの発行

インスペクタでのコールバック関数の設定

コールバック関数の引数を動的に指定するには、もうひとつ注意点がある。

上のように定義した独自イベントに、インスペクタ上でコールバック関数を指定する際、設定可能な関数一覧リストは、'Dynamic'と'Static'の2つに分けられている。

f:id:yasuda0404:20171219131927p:plain

'Static'は従来のとおり、インスペクタ上で「静的に」引数を指定するもの。

一方、'Dynamic'は、スクリプト内でInvoke()の引数を「動的に」指定するもの。

上の例では、同じ'ShowPage(int)'メソッドは、Dynamic/Staticの両方に表示されている。引数を動的に指定したい場合は、'Dynamic'の方を使う。

なお、UnityEventで渡せる引数は最大4つ(<T0, T1, T2, T3>)である。

入れ子のGeneric Listをインスペクタで使う

C#では、Generic Listを要素にもつGeneric List、すなわち、入れ子(多重)のGeneric Listを定義できる。

public List<List<string>> names = new List <List<string>>();

しかし、入れ子(二重)のGeneric Listは、インスペクタに表示されない。

インスペクタで入れ子Listを使うには、要素となるListをコンストラクタの引数とするクラスを定義し、そのクラスを親Listの要素にすることで実装できる。

次の事例は、'MyClass'クラスのGeneric Listを要素とするGeneric Listを作るもの。

子クラス(この事例では'ChildList')は、[System.Serializable]でシリアル化することを忘れないように。

//Generic ListをList要素とするためのクラス('PageController')
[System.Serializable]
public class ChildList
{
        public List<MyClass> list = new List<MyClass>();

        public ChildList(List<MyClass> _list)
        {
            list = _list;
        }
}

//メインクラス
public class MainClass : MonoBehaviour
{
    public List<ChildList> mainList = new List<ChildList>();
}

インスペクタでは、親List(MainList)の要素(Element0, Element1, ....)として、Listを指定できるようになる。

f:id:yasuda0404:20171219122102p:plain

子Listは、

List<MyClass> mylist = mainList[0].list

のように取り出す。

UnityEventコールバック関数の設定方法について

C#/.NETに限らず、いわゆるオブジェクト指向言語では、クラスの独立性を高めるため、Eventが使われる。

Eventを使うには、イベント・リスナーを設定する必要がある。

たとえば、クラスA(ClassA)の初期化が終わってから、クラスB(ClassB)のInit()メソッドを呼びたい、という例を考えてみる。

これをEventを使って実装すると、クラスAの初期化処理が終わったら、ClassAがUnityEvent OnInitDoneイベントを発行し、これをClassBで受け取ってClassBのInit()メソッドを実行する、というような処理になる。

ClassA、ClassBの基本形は次の通りとする。

Class Aの基本形

using UnityEngine;
using UnityEngine.Events;

public class ClassA : MonoBehaviour {

    private UnityEvent OnInitDone;

    private void Init()
    {
        OnInitDone = new UnityEvent(); //イベント・インスタンスの作成

        Debug.Log("初期化処理...");

        OnInitDone.Invoke();    //イベント発行
    }

    void Start()
    {
        Init();
    }
}

ClassBの基本形

using UnityEngine;

public class ClassB : MonoBehaviour {

    private void Init()
    {
        Debug.Log("ClassBの初期化処理...");
    }
}

このままでは、ClassAのOnInitDoneイベントと、ClassBのInit()メソッドは関連付けられていない。何らかの形でこれらを関連付けてやらねばならない。

すなわち、ClassAのOnInitDoneイベントのコールバック関数にClassB.Init()メソッドを指定する必要がある。

実際のコードはどうなるだろうか?

Unityで、(ClassA、ClassBのカプセル化をできるだけ保ちながら、)イベントとコールバック関数を関連付けるには、次の2つのアプローチがある(と思う)。

方法1. ClassAにリスナー設定・解除メソッドを定義する

ClassA

ClassAに次の、リスナー登録・解除メソッドを追加する。引数のUnityAction actionがコールバック関数。 AddListenerとともに、RemoveListnerメソッドも必ず定義する。

    public void AddListenerToInitDone(UnityAction action)
    {
        if (OnInitDone == null) OnInitDone = new UnityEvent();
        {
            OnInitDone.AddListener(action);
        }
    }
    public void RemoveListenerFromInitDone(UnityAction action)
    {
        if (OnInitDone != null) OnInitDone.RemoveListener(action);
    }

ClassB

ClassBには、ClassAのOnInitDoneイベントのコールバック関数を設定する。

リスナー削除は、OnDisable()に書く。

‘‘‘ [SerializeField] ClassA classA;

private void Awake()
{
    classA.AddListenerToInitDone(Init);
}

private void OnDisable()
{
    classA.RemoveListenerFromInitDone(Init);
}

‘‘‘

以上により、

  • ClassAのStart()で、ClassAのInit()がコールされる +OnInitDoneイベントが発行される(Invoke
  • ClassBがOnInitDoneイベントを受信し、コールバック関数として設定したClassBの`Init()``が実行される
  • ClassBのDisable時に、コールバック関数の登録が抹消される。

という処理が行われる。実行するとコンソールに次のように表示される。思惑通り、ClassAの初期化処理の後、ClassBの初期化処理が走る。

ClassAの初期化処理...
ClassBの初期化処理...

方法1の良い点

この方法は、イベントとコールバック関数の関連付けをスクリプトで定義するので、Unity独自の機能であるインスペクタを好まない「オーソドックス派」は受け入れやすいと思う。

また、イベントや各メソッドはPrivateで定義できるので、クラスのカプセル化面でも好ましいだろう。(イベント変数はPrivateでいい、というのは、ちょっと意外な気もするが)。

方法1の問題点

一方で、この方法の問題点のひとつは、上で書いたように、もしClassBのStart()内でClassAのイベントリスナーを設定した場合、ClassAのStart()でイベント発行した際に、リスナー定義が終わっているか保証されないことがある。

上の事例では、コールバック関数のリスナー登録をClassBの‘‘Awake()‘‘で定義することで実行順を保証したが、イベントや、実行順序を指定したいコールバック関数の数が多くなってくると煩雑で、想定した実行順序が保たれずに「オブジェクトの未定義エラー」に悩まされる恐れが大きくなる。(メソッドの実行順に関わるエラーは、Unityで共通の悩みではないだろうか)。

ClassA、ClassBの実行順を明示的に設定することもできるが(実行の順番:OnEnable, Awake, OnLevelWasLoaded, Start - Unityな日々(Unity Geek))、このような処置は実行順を設定していることに気づきにくく、視認性や保守性がよくない。

この方法のもうひとつの問題点は、ClassBのパブリック変数にClassAを指定していることだ。

ClassBは、ClassAのOnInitDoneイベントを受け取りたいだけなのに、ClassA全体を連結してしまっている。そもそものイベントのメリットであるクラスの独立性が得られない。

方法2. UnityEventをインスペクタで関連付ける.

方法1の問題に対する改善策として、イベントをPublic変数にして、インスペクタ上でコールバック関数を設定する方法がある。

ClassARev

この場合のClassAは次のようになる。UnityEvent OnInitDoneは、public、または、[SerializeField]で定義する。

AddListener...RemoveListener... メソッドは不要になり、ほぼ基本形と同じだ(UnityEventがPublicになっただけ)。

using UnityEngine;
using UnityEngine.Events;

public class ClassARev : MonoBehaviour {

    [SerializeField] UnityEvent OnInitDone;

    private void Init()
    {
        if(OnInitDone == null) OnInitDone = new UnityEvent(); //イベント・インスタンスの作成

        Debug.Log("ClassArevの初期化処理...");

        OnInitDone.Invoke();    //イベント発行
    }

    void Start()
    {
        Init();
    }
}

ClassBrev

ClassBは次のようになる。コールバック関数Init()はpublicにする。こちらもスクリプトはまったくシンプルになった。

using UnityEngine;

public class ClassBrev : MonoBehaviour {

    public void Init()
    {
        Debug.Log("ClassBの初期化処理...");
    }

}

イベントとコールバック関数の関連付けはClassAのインスペクタ上で行う。

ClassArevのインスペクタに、UnityEvent OnInitDone の設定パネルが表示されている。UnityUIで見慣れたインターフェースだと思う。

f:id:yasuda0404:20171116095413p:plain

'+'ボタンをおして、このイベントのコールバック関数を設定していく。今回の例だと、次のようになる。

f:id:yasuda0404:20171116095510p:plain

以上で、先の事例と同じく、ClassArevのInit()が実行> OnInitDoneがInvoke> ClassBrevのinit()が実行、という一連の処理が行われる。

方法2の良い点

まず、インスペクタでイベントとコールバック関数を関連付けるため、リスナー設定の実行順の問題がなくなる。

また、ClassAとClassBのやりとりはイベントとコールバック関数だけになるため、ClassBから、ClassA全体が見えてしまう、という問題もなくなった。

これらは方法1の問題点を解決する利点だ。

さらに、UnityEventをインスペクタに表示することで、インスペクタ上でイベントのコールバック関数が一目で確認できるメリットもある。(アンチ・インスペクタ派にはメリットではないかもしれないが…)

方法2の問題点

問題点としては、スクリプトを見ただけではコールバック関数がどこからコールされているのかがわからなくなることがある。これはハードリンクを避けたことで、相反する結果として出てくる問題点といえる。

結局、どちらがいいのか?

方法1がいいのか、方法2がいいのかは、コードの保守性や再利用性、拡張性、そして視認性についての考え方によると思う。方法1はよりオーソドックス、方法2は「Unity的」実装だと思う。

ちなみに僕の場合、以前は方法1で実装していたが、最近は保守性、再利用性、視認性を総体的に見て、方法2の方が良いのではないか、と考えている。

どうでしょうか?

カスタムSkyboxを設定する

f:id:yasuda0404:20171102170107p:plain

Skybox画像の読み込み

Skyboxとして使う、全天周画像を準備する。

これをUnityのプロジェクトビューに読み込み、Textureのインスペクタで、'Texture Shape'を'Cube'に設定する。

f:id:yasuda0404:20171102170310p:plain

Skybox用Materialの作成

Materialを新規作成し、

  • Shaderを'Skybox/Cubemap'
  • 'Cubemap(HDR)'に、先に読み込んだTextureをアサインする。

f:id:yasuda0404:20171102170525p:plain

Skyboxの適用

上部メニューの、Window-Lighting-Settings でLightingウィンドウを開く。

背景としてのSkyboxは'Environment'セクションで、

照明・反射光は'Environment Reflections'セクションで、

各々調整する。

Environment

  • 'Environment'セクションの、'Skybox Material'に、上で作成したSkyboxマテリアルをアサインする
  • 'Intensity Multiplier'で明るさを調整する

Environment Reflections

  • 'Source'を'Custom'を選択
  • 'Cubemap'に、Skyboxマテリアルをアサイ
  • 'Intensity Multiplier'で環境光の強さを調整する

f:id:yasuda0404:20171102170744p:plain

注)

EnvironmentとEnvironment Reflectionsは、異なるCubemapを指定してもよい。Environment Reflectionsで環境光の効果を利用する場合は、照明用のCubemapを別途作成することが多い。

f:id:yasuda0404:20171102171312p:plain

XMLからDictionaryを作成する。

unitygeek.hatenablog.com

XMLを読み込んで、タグ名と値からDictionaryを作っておくと便利な場合がある。この方法について。

サンプルXML

<?xml version="1.0" encoding="utf-8"?>
<works>
  <title>リップヴァンウィンクルの花嫁</title>
  <director>岩井俊二</director>
  <company>ロックウェルアイズ</company>
  <year>2016</year>
</works>

XMLからDicionaryに変換するコード

下の通り。

XMLのトップ階層を読み込んで、いったん、KeyValuePairを作成する。

その後、KeyValuePairをDictionaryへ変換する。

という手順。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.Xml.Linq;

namespace XOOMS.IO
{

    public class XMLDictionary : MonoBehaviour
    {

        [SerializeField] string xmlFileName = "pairs.xml";

        // Use this for initialization
        void Start()
        {
            //相対パスから環境に応じて絶対パスを取得する
            XDocument xdoc = XDocument.Load(FileUtil.GetFullPathFromRelative(xmlFileName));

            //トップ階層の要素を取り出す
            IEnumerable<KeyValuePair<string, string>> keyvaluepairs = xdoc.Root.Elements()
                                      //KeyValuePairを作成する
                                      .Select(x => new KeyValuePair<string, string>
                                      (
                                          x.Name.LocalName,
                                          x.Value
                                      ));

            //KeyValuePairからDictionaryへ変換
            Dictionary<string, string> dict = keyvaluepairs.ToDictionary(x => x.Key, x => x.Value);

            foreach (var d in dict)
            {
                Debug.Log(string.Format("{0}:{1}", d.Key, d.Value));
            }

        }

    }
}

LINQ to XMLを使ったXML操作

.NET(C#)でXMLを扱うにはいくつかの方法があるが、'LINQ to XML'を使うと簡潔に書けそうだ。

今回、Unity上で'LINK to XML'を動かしてみた。Unityバージョンは、2017.2。

XMLサンプルファイル

XMLのサンプルは、次を使う。

<?xml version="1.0" encoding="utf-8"?>
<persons>
  <person>
    <name yomi="はりそんふぉーど">Harrison Ford</name>
    <birth>1942-07-13</birth>
    <works>
      <title>Star Wars</title>
      <title>Blade Runner</title>
    </works>
  </person>
  <person>
    <name yomi="みふねとしろう">三船敏郎</name>
    <birth>1920-04-01</birth>
    <works>
      <title>七人の侍</title>
      <title>天国と地獄</title>
      <title>羅生門</title>
    </works>
  </person>
  <person>
    <name yomi="じゃんぎゃばん">Jean Gabin</name>
    <birth>1904-05-17</birth>
    <works>
      <title>ヘッドライト</title>
      <title>地下室のメロディー</title>
      <title>暗黒街のふたり</title>
    </works>
  </person>
</persons>

LINQ to XML

LINQ to XMLを使うには、

をUsingしておく。

手順としては、

  1. XDocumentクラスのLoad静的メソッドでXMLファイルを読み込む。
  2. XDocumentインスタンスRoot.Elements()メソッドで、トップ階層のタグ要素のコレクション(IEnumerable)を作成する。
  3. さらに下層の要素を取り出すには、Elements(タグ名) or Element(タグ名)メソッドを入れ子にし、タグ要素クラス(XElement)を取得する。
  4. アトリビュートがある場合は、XElementインスタンスAttribute(アトリビュート名)メソッドで、アトリビュートクラスを取得する。
  5. タグ要素、アトリビュート要素ともに、Valueプロパティで、値を文字列として取り出す

となる。

文章で書くとわかりにくいが、次のサンプルコードを見れば理解するのはそう難しくないと思う。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.Xml.Linq;

namespace XOOMS.IO
{
    public class XMLLinq : MonoBehaviour
    {
        //LINQを使ってXMLファイルを解析する

        [SerializeField] string xmlFileName = "actors.xml";
        [SerializeField] string tagName0    = "name";
        [SerializeField] string attribute01 = "yomi";
        [SerializeField] string tagName1    = "birth";
        [SerializeField] string tagName2    = "works";
        [SerializeField] string tagName20   = "title";
        

        void Start()
        {
            XDocument xdoc = XDocument.Load(Application.dataPath + "/" + xmlFileName);


            IEnumerable<XElement> xelements = xdoc.Root.Elements();   //ルート直下の要素のコレクション
            
            foreach (var xelement in xelements)
            {
                XElement xelem0     = xelement.Element(tagName0);       //タグtagNames[0]の要素
                XAttribute xattr01  = xelem0.Attribute(attribute01);    //タグ0のアトリビュートを取り出す
                XElement xelem1     = xelement.Element(tagName1);       //タグtagNames[1]の要素

                //XElement.Valueで値を取り出す
                Debug.Log(string.Format("{0}:{1} ({2}),  {3}:{4}", tagName0, xelem0.Value, xattr01.Value, tagName1, xelem1.Value ));
            }
            
        }

    }
}

これを実行すると、次のように出力される。

name:Harrison Ford (はりそんふぉーど),  birth:1942-07-13
name:三船敏郎 (みふねとしろう),  birth:1920-04-01
name:Jean Gabin (じゃんぎゃばん),  birth:1904-05-17

なお、対応するタグやアトリビュートがない場合、XElement, XAttributeはnullになる。nullオブジェクトの.Valueプロパティにアクセスするとエラーになるので、実際の利用では各要素を取り出した後にnullチェックを入れたほうがいい。

取得条件を加える

ある条件を満たす要素だけを取り出すことも、LINQ to XMLを使えば簡潔に書ける。

条件の記述は、Whereメソッドを使う。

たとえば、生年が1920年以降の人物だけを抽出するには、xelementsの抽出箇所に、次のように条件を追記する。

IEnumerable<XElement> xelements = xdoc.Root.Elements()
                    .Where(x => ((DateTime)x.Element(tagName1)).Year >= 1920);   //生年が1920年以降の要素のみ抽出する

要素を並べ替える

ある要素の値で並べ替えるには、OrderByメソッドを使う。

たとえば、よみがな順に並べ替えるには、

IEnumerable<XElement> xelements = xdoc.Root.Elements()
                    .OrderBy(x => (string)(x.Element(tagName0).Attribute(attribute01)));   //よみがなで並べ替え

OrderBy()は昇順に並べ替える。降順に並べ借るには、.OrderByDescending()メソッドを使う。

入れ子になった要素を取得する

入れ子になった要素の取得は、Element(親要素の名前).Elements(子要素の名前)と数珠繋ぎにしていけばいい。

上のXMLサンプルファイルで、各俳優の代表作はworksの子のtitleに書かれている。これを取得するには次のようにする。

foreach (var xelement in xelements)
            {
                //入れ子になった要素を取得
                IEnumerable<string> selems2 = xelement.Element(tagName2) //works要素
                                            .Elements(tagName20)        //works要素の下のtitle要素
                                            .Select(x => x.Value) ;       //title要素の値のコレクション
                foreach (string selem2 in selems2)
                {
                    Debug.Log(string.Format("title: {0}", selem2));
                }
            }

上の例では、LINQSelectメソッドを使って、一挙に作品名のstringまで取得しているが、先の例のようにIEnumerable<XElement>の取得をして、その後のforeach内でValueを取り出してもよい。

XMLの階層構造を考えずに子孫要素を取り出す

前の例では、「works要素の子のtitle要素」と階層構造が分かったうえで要素を取り出したが、このような階層構造を考えずに、とにかくある名前の要素をすべて取り出すこともできる。これには、Descendantsメソッドを使う。

            IEnumerable<XElement> xtitles = xdoc.Root.Descendants(tagName20);  //Root下の"title'タグを再帰的にすべて取り出す
            foreach(XElement xtitle in xtitles)
            {
                Debug.Log(string.Format("title: {0}", xtitle.Value));
            }

LINQSelectメソッドを使って、次のようにも書ける。

            IEnumerable<string> titles = xdoc.Root.Descendants(tagName20)
                                        .Select(x => x.Value);
            foreach(string title in titles)
            {
                Debug.Log(string.Format("title: {0}", title));
            }

参考資料

今回の記事は、出井秀行著「実戦で役立つC#プログラミングのイデオム/定石&パターン」の第11章、「XMLファイルの操作」を参考にした。

非MonoBehaviourクラスでコルーチンを使う

参考:

unitygeek.hatenablog.com

コルーチンを使うには、

StartCoroutine(戻り値IENumeratorのメソッド)

と記述する。

StartCoroutine()はMonoBehaviourクラスのメソッドである(Unity - Scripting API: MonoBehaviour.StartCoroutine)。

MonoBehaviour継承クラスは、new クラス名インスタンス化することは禁じられている(補足参照)。

通常は、GameObjectにアタッチし、'GetComponent<クラス名>()‘で取得する。

しかし、コルーチンを実行時にインスタンス化して使いたい場合も多々ある。たとえば、指定したURLやパスからデータを読み込むクラスはStartCoroutineが必須になるが、オブジェクトにアタッチして使う必然性はないし、意味的にも混乱する。

この対応策のひとつとして、「インスタンス化するクラスで、MonoBehaviour継承クラスのインスタンスを作る」方法がある。

つまり、StartCoroutine()を使うためだけの、MonoBehaviour継承インスタンススクリプトで作成し、そのStartCoroutineを呼ぶ。

例:

 public class MyStartCoroutine
    {

        public WWW GetWebImageAsByteArray(string imageFilePath)
        {
            IEnumerator coroutine = GetWWW(imageFilePath); //StartCoroutineに渡すIENumeratorインスタンス

            var myMono = new MyMonoBehaviour(); //StartCoroutineを使うためのカスタムクラス
            myMono.CallStartCoroutine(coroutine); //カスタムクラス内でStartCoroutineを呼ぶメソッド

            return www;
        }

    //StartCoroutineに渡すIEnumeratorの実装
        private IEnumerator GetWWW(string url)
        {
            www = new WWW(url);
            yield return www;
        }
    }

    //StartCoroutine()メソッドを使うために定義したMonoBehaviour継承クラス
    public class MyMonoBehaviour : MonoBehaviour
    {
        public void CallStartCoroutine(IEnumerator iEnumerator)
        {
            StartCoroutine(iEnumerator); //ここで実際にMonoBehaviour.StartCoroutine()を呼ぶ
        }
    }

(補足)

実際には、フィールドなどの定義に不足・矛盾がなければ、MonoBehaviour継承クラスを'new …‘でインスタンス化しても、(警告はでるが)ビルドは通る。が、あまりよい使い方ではない。

標準リテラルと逐語リテラル

リテラル」とは「文字通り」という意味。プログラミングにおける「リテラル」は、値や文字をそのまま書き下したものを指す。

リテラル文字列には、「標準リテラル」と「逐語リテラル」の2種類があり、特殊文字の記述方法が異なる。(2.4.4.5 リテラル文字列 (C#)

標準リテラル

文字列をダブルクオーテーションで囲んで記述。

string h = "Hello World"。

特殊文字は、「\(円マーク)+文字」で表す。

たとえば、\“はダブルクオーテーション、\\は円マーク、\tはタブなど。

string e = "Joe said \"Hello\" to me";      // Joe said "Hello" to me
string g = "\\\\server\\share\\file.txt";   // \\server\share\file.txt

逐語リテラル

文字列を、@“(アットマーク)+”(ダブルクオーテーション)文字列"(ダブルクオーテーション)で囲む。すなわち、標準リテラルの前に、@をつけた形。

string h = @"Hello World"。

逐語リテラルでは特殊文字もそのまま記述できる。

string h = @"\\server\share\file.txt";      // \\server\share\file.txt

例外は"(ダブルクオーテーション)で、、"“とダブルクオーテーション2つで、ひとつのダブルクオーテーションをあらわす。

string f = @"Joe said ""Hello"" to me";   // Joe said "Hello" to me

サンプル

画像データパスが変数imageFilePathで与えられているとき、{“url”:“画像データパス”} というJSONデータを記述する:

string data1 = "{\"url\":\"" + imageFilePath + "\"}";    // 標準リテラル形式で記述した場合
string data2 = @"{""url"":""" + imageFilePath + @"""}";    //  逐語リテラル形式で記述した場合

Oculus Rift使用時に、スピーカーから(も)音声を出力する

Oculusアプリの設定(歯車アイコン)で、'Settings'を選択。

f:id:yasuda0404:20170418151948p:plain

Devicesメニューで、'Rift'(ヘッドセット)を選択。

f:id:yasuda0404:20170418152032p:plain

‘Audio Output in VR'のドロップダウンメニューで、
  • Windows Default: OS側出力のみ
  • Both (audio mirroring):OculusとOSの両方に出力

を選択

f:id:yasuda0404:20170418152129p:plain

タスクバーのスピーカーアイコンを右クリック。

「再生デバイス」を選択。

「サウンド」ウィンドウで、音声を出力したいデバイスを選び「規定値に設定」をクリックし、規定デバイスにする。

f:id:yasuda0404:20170418152525p:plain

これでOS側の音声出力デバイスから(も)、音声が出力される。

ウェブカメラの映像を表示する

ウェブカメラの映像をテクスチャとして貼り付ける

ウェブカメラの動画を取得するには、WebCamTextureクラスを使う。

docs.unity3d.com

次は、ウェブカメラからの映像をメインテクスチャとして適用するスクリプト。このスクリプトを、たとえばPlaneオブジェクトにアタッチすれば、Plane上にウェブカメラの映像が表示される。

using UnityEngine;
using System.Collections;

public class WebcamCapture : MonoBehaviour {

    //このクラスをウェブカメラの映像をテクスチャとして貼り付けるオブジェクトに適用する

    private WebCamTexture webcamtex;

    // Use this for initialization
    void Start()
    {

        webcamtex = new WebCamTexture();   //コンストラクタ
        
        Renderer _renderer = GetComponent<Renderer>();  //Planeオブジェクトのレンダラ
        _renderer.material.mainTexture = webcamtex;    //mainTextureにWebCamTextureを指定
        webcamtex.Play();  //ウェブカムを作動させる
    }

}

width, height, FPSを明示的に指定する場合は、コンストラクタに引数として与えることもできる

        webcamtex = new WebCamTexture(1280, 800, 30);   //コンストラクタ (width, height, FPS指定)

f:id:yasuda0404:20170418094250p:plain

表示用Planeの設定

WebCamTextureを張り付けるPlaneのアスペクト比は、ウェブカメラのアスペクト比と同じにしておく。

また、表示以外の不要な機能を切っておく。具体的には、次がある。

  • MeshCollider
  • MeshRenderer
    • CastShadows
    • ReceiveShadows
    • MotionVectors
    • LightProbes
    • ReflectionProbes

f:id:yasuda0404:20170418095641p:plain

複数のウェブカメラを使う

複数のウェブカメラが接続されている場合は、WebCamTextureのコンストラクタで、デバイス名を明示的に指定する。

現在、接続されているウェブカメラ・デバイスは、WebCamTexture.devicesで取得でき、デバイス名は、WebCamDevice.nameプロパティで取得する。

    WebCamDevice[] devices = WebCamTexture.devices; //接続されているウェブカメラを取得
        foreach (WebCamDevice device in devices)
        {
            Debug.Log("WebCamDevice " + device.name);
        }
            
        //
        webcamtexA = new WebCamTexture(devices[0].name);   //コンストラクタ (デバイス指定)
        webcamtexB = new WebCamTexture(devices[1].name, 1280, 800, 30);   //コンストラクタ (デバイス, width, height, FPS指定)

ただし、Unityでは同じ種類のウェブカメラは1つしか使えない。たとえ片方のウェブカメラのドライバを強制的に変えて、見かけ上は異なるウェブカメラにしても、だめ。これは、ウェブカメラデバイスが、USBデバイスのレベルで管理されているためと思われる。

同じウェブカメラを複数使うためには、特別な対応が必要になる。これについては別途あらためて書きたい。

プロパティ(getter/setter)について

通常、C#のクラスは「フィールド」と「メソッド」で構成される。フィールドは「メンバ変数」とも言い、変数(データ)の定義箇所。メソッドはそのままメソッドの定義。

//フィールド
public bool isRunning; 
private float age;

//メソッド
public void CheckAge(){
    ....
}

もうひとつ「プロパティ」というのがある。たとえば次のParameterがプロパティ。

public class Sample : MonoBehaviour {

    private int parameter;
    public int Parameter
    {
        get
        {
            return parameter;
        }
        set
        {
            parameter = value;
        }
    }

    // Use this for initialization
    void Start () {
        Debug.Log("parameter " + parameter);
    }
}

get{…}はgetterと呼ばれ、メソッドと同じく、returnで値を返す。

set{…}はsetterで、暗黙の変数valueを介して値を受け取る。

プロパティは、そのクラスの外側から見ればフィールドのように見え、クラスの内側からみればメソッドのように見える。

//メイン
public class Main : MonoBehaviour {

    public Sample sample;   //サンプルクラス

    void Awake () {
        sample.Parameter = 3;  //プロパティはフィールドのように見える
    }

}

プロパティを使うメリット

プロパティを使うメリットは、条件付きで代入・取得時をおこなったり、代入・取得時にデバッグ出力できること。

    private int parameter;
    public int Parameter
    {
        get
        {
            Debug.Log("Accessed to parameter.");    //parameterが参照された
            return parameter;
        }
        set
        {
            if(value < 5) parameter = value;    //5未満なら代入する
        }
    }   }

また、publicフィールドとの違って、読み込み・書き込みのどちらだけしか許容しない設定も実現でき、より厳密なカプセル化ができる。

取得可・代入禁止

private int parameter;
public int Parameter
    {
        get
        {
            return parameter;
        }
    }

とすると、クラス外からParameterへは代入できなくなる。代入しようとするとエラーになる。

public Sample sample;   //サンプルクラス

void Awake () {
        Debug.Log(sample.Parameter);    //取得可能
        sample.Parameter = 3;           //エラー
    }

代入可・取得禁止

    private int parameter;
    public int Parameter
    {
        set
        {
            parameter = value;
        }
    }

    // Use this for initialization
    void Start () {
        Debug.Log("parameter " + parameter);  
    }

とすると、クラス外からParameter(内部的にはparameter)に代入はできるが、クラス外から値の取得はできない。

void Awake () { sample.Parameter = 3; //代入可能 Debug.Log(sample.Parameter); //エラー }

オートプロパティとその挙動

プロパティは、必ずしもクラス内部のprivate変数に紐づけする必要はない。

すなわち、次のように書くことができる。これは「オートプロパティ」と呼ばれる。

    public int Parameter
    {
        get;
        set;
    }

この場合、外からsetされた値を内側のクラスで使うには、

Debug.Log("Parameter " + Parameter);    //3

のようににアクセスする。(上で、プロパティは、プロパティを定義したクラス内部ではメソッドに見えると書いたが、アクセスする際は()は不要。また、オートプロパティと言っても、自動的に'private parameter`変数に作成・代入されるわけではない)

また、次のようにset;のみだとエラーになる。これはオートプロパティでget;がない場合、値をsetしても取り出せないからだろう。

    public int Parameter
    {
        set;    //エラー
    }

クラス内部からプロパティとしてアクセスする

プロパティは、必ずしもgetでprivate変数に代入しなくてもいい。クラスの内部からでもプロパティとしてアクセスできる。

public class Sample : MonoBehaviour {

    private int parameter;
    public int Parameter
    {
        get
        {
            Debug.Log("Accessed to parameter");
            return parameter;
        }
        set
        {
            parameter = value;
        }
    }

    // Use this for initialization
    void Start () {
        Debug.Log("parameter " + parameter);  //変数として取得
        Debug.Log("Parameter " + Parameter);  //プロパティとして値を取得
        Parameter = 5;                        //プロパティとして代入
        Debug.Log("Parameter " + Parameter);  //プロパティとして値を取得
    }
}

このStart()メソッドの、最初のparameterは内部のprivate変数として、それ以外はプロパティParameterとしてアクセスしている。プロパティとしてアクセスした場合は、getの中の処理を通るので、"Accessed to paramter"がデバッグ表示される。

f:id:yasuda0404:20170415142923p:plain

?と??ーNull許容型・Null条件演算子

C#のコードの中に、たまに'?‘が出てくる。たとえば次のように。

int? length = customers?.Length;  
Customer first = customers?[0];   
int? count = customers?[0]?.Orders?.Count();  

なんじゃこれは??・・・コードを見てまさに「?」と思っていたところ、これらは「Null演算子」「Null許容型」と呼ばれるものだと知った。恥ずかしながら、最近のことだ。

Null許容型

通常のint型はnullを代入できない。

int length = null;  //エラーになる

と書くと、エラーで通らないのだ。

そこで、「nullを代入できる拡張型int」として作られたのが「Null許容型int」。上の例だと、int? lengthが「Null許容型int」となる。

容易に察せられる通り、int以外の型にも「Null許容型」はある。

int? i = 10;
double? d1 = 3.14;
bool? flag = null;
char? letter = 'a';
int?[] arr = new int?[10];

Null演算子 ‘?.’ ‘?[’

次に、

int? length = customers?.Length; 

の右辺の、customersの後にある'?.‘だが、これは「Null条件演算子」と呼ばれるもの。「?の左側にあるクラスがnullでなければ該当するメンバーを、nullならばnullを返す」。

すなわち、三項演算子による記述、

int? length = customers != null?customers.Length:null;

を、より簡潔にかいたものである。さらに書き下すと

int? length;
if(customers != null){
  length = customers.Length;
}else{
  length = null;
}

と同じだ。

これくらいなら、わざわざ「Null条件演算子」と使わなくても、より可読性の高い三項演算子や、通常のif構文でいいのでは?と思うかもしれないが、「Null条件演算子」は次のような書き方もできる。

int? count = customers?[0]?.Orders?.Count();  // null if customers, the first customer, or Orders is null 

これは、「customers, customers[0], customers[0].Ordersのすべてがnullでない時のみ、customers[0].Orders.Count()を返し、どれかひとつでもnullがあればnullを返す」という評価を、1行で書けてしまう。慣れればコンパクトなコードが書けそうだ。

注)最初の、'customers?[0]‘という表記は、C#4.0、すなわち現行のUnityではエラーになってしまう。

Null合体演算子 '??'

??と?を2つ並べたのがNull合体演算子。A??Bは、「AがNullでないならAを、AがNullならBを返す」もの。

Console.WriteLine(s ?? "Unspecified");

上述したNull演算子と組み合わせて、

A?.B?.C?[0] ?? E 

と書くと、A.B.C[0]が評価でき、nullでなければA.B.C[0]を、nullならばEを返す、ということになる。

int? a = null;
string astr = (a??9999).ToString();
Debug.Log("astr= " + astr); //9999

null許容型の書き方とエラー例(2017/5/20追記)

null許容型変数

int? age = null;
int? myAge = age;
Debug.Log(string.Format("My Age {0}", myAge));   // 出力:"My Age "

※次はビルドエラーになる。null許容型変数を宣言しただけではnullは代入されない。

//ビルドエラー
int? age;   //変数定義のみ
int? myAge = age;   //エラー 'Use of unassigned local variable `age'

null許容型要素を持つ配列

int?[] age = new int?[]{ null};   //null要素をもつnull許容型int配列
int? myAge = age[0];   //OK
Debug.Log(string.Format("My Age {0}", myAge));    // 出力:"My Age "

※次はビルドエラーになる。配列変数宣言だけでは、要素にnullは代入されない //ビルドエラー int?[] age; //変数定義のみ int? myAge = age[0]; //エラー 'Use of unassigned local variable `age'

※次は、ビルドは通るが実行時エラーとなる。int?[]は要素にNullを許容するものであって、配列自体をNull許容型にするものではない。

//実行時エラー
int?[] age = null;   //配列にnullを代入
int? myAge = age[0];   //実行時エラー "NullReferenceException: Object reference not set to an instance of an object"

※次も、C#4.0、すなわち現行のUnityではエラーになってしまう。

int?[] age = null;
int? myAge = age?[0];   //C#4.0ではエラーになる

クラスの場合

クラスメンバーにnull許容型を定義した場合

public class NewBehaviourScript : MonoBehaviour {

    void Start () {
        NullTest nullTest = new NullTest();
        Debug.Log(string.Format("My Age {0}", nullTest.id));  //出力 "My Age "
    }
}

public class NullTest
{
    public int? id = null;
}

※次はエラー

void Start () {
    NullTest nullTest = null;
    Debug.Log(string.Format("My Age {0}", nullTest.id));  //実行時エラー "NullReferenceException: Object reference not set to an instance of an object"
}

※次の書き方もC#4.0では不可。

void Start () {
    NullTest nullTest = null;
    Debug.Log(string.Format("My Age {0}", nullTest?.id));  //ビルドエラー C#4.0ではこのような書き方はできない
    }

C# (.NET Framework)の名前の付け方

UnityでC#のコードを作成するとき、変数やメソッドなどの名前の付け方に迷うことが多々ある。「何が正しいか」は一意に決められないとは思うが、基本的には本家Microsoftの考える「名前の付け方ガイドライン」に従うのがいい。そうすれば、他のメンバーや、社外の人がコードを見た際にも混乱がすくなくてすむ。(作成者自身も、将来、どんな意図でコーディングしたかを忘れるものだから。)

ということで、MSDNガイドラインから、Unity/C#開発に関係が深そうなものを抜粋した。

参考:

.NET Framework-Development Guide-Framework Design Guidelines

Naming Guidelines(英語).aspx)

(日本語の.NET Frameworkのデザインガイドライン:名前付けのガイドライン.aspx)もあるが、翻訳がおかしい箇所が多い)

大文字・小文字の使い分け

  • 変数はCamel形式

  • その他はPascal形式

名前の付け方の一般論

  • わかりやすさを重視
    • 単語の順番は英語と同じに ×AlignmentHorizontal  〇HorizontalAlignment
    • 一般的な単語を使う ×ScrollableX  〇CanScrollHorizontally ('X'が何を示しているか不明瞭)
    • 省略形を使用しない ×OnBtnClk  〇OnButtonClick
    • できるだけ意味を特定する ×GetInt  〇GetLength
  • 英数字以外の文字は使用しない('_‘アンダースコア、’-‘ハイフンなども使用しない)
  • 言語のキーワードを使用しない 

名前空間

<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>]
  • Pascal記法を用い、単語を'.‘でつなぐ -ただしブランド名固有の大文字・小文字の使い方がある場合はそれを優先してよい
  • Company:会社名をプレフィクスとして用いる
  • Product|Technology バージョンに依存しない安定した製品名・技術名
  • 名前空間と同じ名前をクラスの中で使用しない

メソッド

  • Pascal表記
  • 動詞または動詞句を使う

例:

public class String {  
    public int CompareTo(...);  
    public string[] Split(...);  
    public string Trim();  
}  

プロパティ

  • Pascal表記
  • 名詞または形容詞を使う
    • プロパティ名に'Get|Set'はつけないこと(メソッドと混乱するため)
  • コレクションの場合は複数形にする(—List, —Collectionなどと書かない)
  • Booleanの場合は、否定形ではなく肯定形のみ使用する ×CantSeek 〇CanSeek
    • 先頭に'Is', ‘Has’, ‘Can'をつけてもよい。ただし意味がある場合のみ。

イベント

  • Pascal表記
  • 動詞または動詞句を使う 例:Clilcked, DroppedDown, Painting
  • イベントの前後を示すのに、'Before|After'接頭句はつけない。動詞の過去形、現在形で表現する。
  • イベントハンドラは、「イベント名+EventHandler」とする。 
  • イベントハンドラの中では、イベント送信者は'sender'、イベントは'e'で表記する 例:public delegate void ClickedEventHandler(object sender, ClickedEventArgs e);
  • イベント引数のクラスは、'—EventArgs'接尾句をつける

変数

  • Camel表記
  • 変数の型ではなく、変数の意味で名前をつける。

より上位の「C#コーディング規則」については次を参照のこと。 C# のコーディング規則 (C# プログラミング ガイド)

コルーチンについての覚書

たまに使いたくなるが、いまいち理解できていないもののひとつが、コルーチン(Coroutine)。

ということで、いくつかメモ書きを。

コルーチン(Coroutine)とは何か?

Coroutineとは何だろうか? Wikipediaには次のようにある。

「コルーチン(英: co-routine)とはプログラミングの構造の一種。サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。接頭辞 co は協調を意味するが、複数のコルーチンが中断・継続により協調動作を行うことによる。 サブルーチンと異なり、状態管理を意識せずに行えるため、協調的処理、イテレータ、無限リスト、パイプなど、継続状況を持つプログラムが容易に記述できる。」

簡潔に言うと、コルーチンとは、プログラムを任意の箇所で中断・再開するしくみ、と言っていいだろう。

Coroutineはどう記述するか?

Coroutineとして中断・再開するメソッドは、

  • IEnumerator型を戻り値とし
  • 中断・再開する場所に、yield return 「真偽判定結果」を置く

が基本形。

このメソッドを、

  • StartCoroutine(メソッドを代入したIEnumerator型メソッド、または、それを代入した変数) または、
  • StartCoroutine("メソッド名の文字列")

として呼び出す。

前者の記述方法は、メソッドに引数がある場合・ない場合の両方に使えるが、後者は引数がない場合にしか使えない。

Coroutineはどんな時に使うか?

1.一定時間処理を止めたいとき

Unityでも他のアプリケーションでも、ループの中で、ある処理を一定時間止めたい、というのはよくある要請。

この場合は、ループ(for, while)の中の停止したい場所にyield returnを置き、真偽判定条件として、WaitForSecondsクラスを設定する。

次の例では、0.2秒ごとにアルファ値が0.1小さくなる。

IEnumerator Fade() {
    for (float f = 1f; f >= 0; f -= 0.1f) {
        Color c = renderer.material.color;
        c.a = f;
        renderer.material.color = c;
        yield return new WaitForSeconds(0.2f);  //毎ループここで0.2秒停止する
    }
}

void Update() {
    if (Input.GetKeyDown("f")) {
        StartCoroutine("Fade");
    }
}

2.呼び出されるごとに、処理を変えたい時

yield returnを複数個記述すれば、呼び出されるごとに前のyield returnの次から処理が続けられ、次にあたったyield returnで処理が停止する。これによって戻り値を順番に変えることができる。

IEnumerator coRoutine(){
    msgText.text = "Andy ";
    yield return null;
    msgText.text = "Bob";
    yield return null;
    msgText.text = "Carter ";
    yield return null;
    msgText.text = "Dave";
}

3.あるメソッドの(for)ループを、Updateループに同期して進めたいとき

UnityでのCoroutineの特徴的な使い方としては、Updateループ外のメソッドのループをUpdateフレームに対応させて進ませる、というのがある。次は、Unityマニュアルにある具体例

IEnumerator Fade() {
    for (float f = 1f; f >= 0; f -= 0.1f) {
        Color c = renderer.material.color;
        c.a = f;
        renderer.material.color = c;
        yield return null;
    }
}

void Update() {
    if (Input.GetKeyDown("f")) {
        StartCoroutine("Fade");
    }
}

yield return nullは、ここで処理を中断し、次のフレームで再開するための記述法。これにより、Updateループが1回進むごとに、Fade()内のforループも1回進むことになる。

この例では、yield returnの真偽判定に'null'が設定されている。これは、単にyield returnと書いてもいい。

「ここで制御を呼び出し側に戻すけど、次に呼び出されたら、無条件にこの場所から再開するよ」と解釈される。

コルーチンから戻り値を受け取る

コルーチンから戻り値を受け取りたい時はどうするか?これは次を参考に!

qiita.com

カスタムコルーチン(Custom Coroutine)

Unity5.3から「カスタムコルーチン」が使えるようになり、yield returnの処理中断・再開条件を、自分でカスタマイズできるようになった。(関連Unityブログ)。

次はカスタムコルーチンのサンプル。指定したwaitTime秒待つメソッド(WaitForSecondsと同じ機能)の改良版。(注:ソースコードに誤りがあり訂正しました。また、WaitForSecondsRealtimeはすでにUnity本体に実装されたようです。2017/4/13)

using UnityEngine;
using System.Collections;

public class WaitForSecondsRealtime: CustomYieldInstruction {

    private float waitTime;

    public override bool keepWaiting
    {
        get { return Time.realtimeSinceStartup < waitTime; }
    }

    public void WaitForSecondsRealtime(float time)
    {
        waitTime = Time.realtimeSinceStartup + time;
    }
}

カスタムコルーチン・クラスのポイントは、

  • CustomYieldInstructionクラスを継承する
  • public bool keepWaitingをoverrideし、中断・再開の条件を記述する

の2点。

using UnityEngine;
using System.Collections;

public class CoTest : MonoBehaviour {

    private IEnumerator coroutine;

    // Use this for initialization
    IEnumerator Sub(float delay = 1.0f)
    {
        Debug.Log("Sub Before "+Time.realtimeSinceStartup);
        yield return new WaitForSecondsRealtime(delay);
        Debug.Log("Sub After" + Time.realtimeSinceStartup);
    }

    // Update is called once per frame
    private void Start()
    {
        Debug.Log("Start Before "+Time.realtimeSinceStartup);
        coroutine = Sub(3.0f);
        StartCoroutine(coroutine);
        Debug.Log("Start After " + Time.realtimeSinceStartup);
    }
}

Start()の中のStartCoroutine(coroutine);でコルーチンを呼び出している。(変数coroutineは、そのひとつ前の行でSub(3.0fとして定義している)

これを実行した結果(Console出力):"Sub Before"~"Sub After"の間が約3秒空いていることがわかる。

f:id:yasuda0404:20170412175441p:plain

なお、コルーチン呼び出し側の処理は止まらないことに注意。すなわち、呼び出し側では、"Start Before"から"Start After"は、即座に実行される。

コルーチンの実体

コルーチンは、コレクション(Collections)と深くつながっている。

Collectionsの要素をひとつずつ取り出して処理する場合、foreach(){...}を使う。このforeachループで次々に値を取得できるようにしているのが、IENumerableインターフェースである。

IEnumerableインファーフェースを実装したクラスに、IEnumeratorを戻り型とするGetIEnumeratorメソッドを実装することで、列挙を可能にしている(ややこしい!)。

IEnumerableインターフェースには、Collectionsの現在の要素を取得したり、次の要素に進めたり(MoveNext)、最初の要素に戻ったり(Reset)する機能が定義されている。

そして、yieldは、Collectionsの次の要素をforeachで処理できるようにするためのものである。

つまり、IEnumerable/IEnumerator/yieldという組み合わせは、もともとSystem.Collectionsに列挙可能性を付加する機能で、それをコルーチンの遅延実行に利用している、と考えてもいいかな。あるいは、その逆に、遅延実行機能を、Collectionsの列挙可能性に利用した、でもいいのかも。

unity3d.com