Unityな日々(Unity Geek)

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

非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でも他のアプリケーションでも、ある処理を一定時間止めたい、というのはよくある要請。

この場合は、停止したい場所に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

Unityエディタをバッチで起動し、プロジェクトにコマンドライン引数を渡す

Unityエディタをバッチファイルから起動するには、

"C:\Program Files\Unity543\Editor\Unity.exe"

と書いたバッチファイルを作って、ダブルクリックすればよい。"“でくくっているのは、フォルダ名Program Filesがスペースを含んでいるため。

ちなみに、Unityエディタのショートカットのプロパティを見ると、やはり"“でくくられているはず。これをコピペすればいい)

f:id:yasuda0404:20170117175514p:plain

特定のプロジェクトを開くには、-projectPath プロジェクトのパスをつかって

"C:\Program Files\Unity543\Editor\Unity.exe" -projectPath G:\_project\unity\MyProject

とする。ただし、パスは絶対パスでないと通らない(ようだ)。

パス以外に、さらにコマンドライン引数を加えてやれば、そのコマンドライン引数はプロジェクトから参照できる。たとえば、

"C:\Program Files\Unity543\Editor\Unity.exe" -projectPath G:\_project\unity\MyProject -DataDir G:\Data

とすると、エディタ内でプレイモードを起動したときに、コマンドライン引数 -DataDir G:\Dataが渡される。

コマンドライン引数は、System.Environment.GetCommandLineArgs()で取得する。たとえば、

    string[] commands;
    commands=System.Environment.GetCommandLineArgs();

とすれば、commandsにすべてのコマンドライン引数が格納される。引数の区切りはスペース文字。

オブジェクトをきれいに配置する

等間隔スナップ

オブジェクトを等間隔で並べたり、スケーリングしたりする方法。

設定

Edit => Snap Settings... で各軸のPositionおよびScaleのスナップ幅を設定する

移動

Control(MacはCommand)キーを押しながら、Sceneビューでオブジェクトをドラッグする。

下は、X軸のSnapを3、Z軸のSnapを2に設定し、座標(0,0,0)に複数のオブジェクトを複数Duplicateして、それらを、Controlキーを押しながらX方向、Z方向に移動したもの。

f:id:yasuda0404:20161028133644p:plain

スケール

Control(MacはCommand)キーを押しながら、Sceneビューで各軸のスケールを増減する。

頂点スナップ

2つのオブジェクトの特定の頂点座標をあわせて配置する方法。

Vキーを押しながら、移動させたいオブジェクトの、基準にしたい頂点を選択。直交矢印の中心部が黄色の正方形に変わる。

f:id:yasuda0404:20161028135016p:plain

正方形をドラッグして、他のオブジェクトのあわせたい頂点に重ねる。

f:id:yasuda0404:20161028135211p:plain

表面スナップ

移動モードで、Control+Shiftキーを押す。直行矢印の中心部分が黄色の正方形に変わるので、ここをドラッグして他のオブジェクトに近づける。あるところで面に接したところにスナップされる。(調整が少し難しい)

f:id:yasuda0404:20161028141930p:plain

**表面スナップは、Centerモードでしかうまく行かない?Pivotではめり込んだ状態でスナップされてしまう。操作方法が悪いのだろうか?

f:id:yasuda0404:20161028142021p:plain

オブジェクトのフェードイン・フェードアウト

Standard Shaderを適用したモデルのフェードイン・フェードアウトは、Colorのアルファ値を変更することによって、行う。

Color値を変えるメソッドは、

Material.SetColor(string propertyName, Color color)

で、第1引数はどのColorを変えるかを指定するもの。

具体的には、Standard Shaderでは、

  • _Color
  • _EmissionColor

の2種類のColorクラスがあるので、どちらを指定する。

第2引数は、設定するColor.これは、RGBAのプロパティを持っているので、このうちのA(アルファ)を変えることで、フェードイン・アウトを実装する。

ポイントは、Materialから取得したColorに対して変更を加えるだけではだめで、変更したColorを、SetColor()で対象Materialに戻してやらなければいけない、ということ。

サンプルスクリプト

同じマテリアルを適用した複数のオブジェクトのフェードイン・フェードアウトを行うスクリプト。アルファ値の変更は、iTweenのValueTo()を使った。

public class Fader : MonoBehaviour {

    public GameObject[] targets;   //Target GameObject
    private Material[] mats; //Target Material. Shader must be set to Fade or Transparent
    public float alphaMax = 1.0f;
    public float alphaMin = 0.0f;

    private Color color;

    // Use this for initialization
    void Start () {
        mats = new Material[targets.Length];
        int i = 0;
        foreach (GameObject target in targets)
        {
            mats[i] = target.GetComponent<Renderer>().material;
            i++;
        }
        color = mats[0].GetColor("_Color"); //GetColor()で、現在のColorを取得。このスクリプトでは、すべてのオブジェクトのColorは同じと仮定している。
    }

    public void FadeOut()
    {
        Debug.Log("FadeOut()");
        iTween.ValueTo(gameObject, iTween.Hash(
            "from", alphaMax,
            "to", alphaMin,
            "onupdatetarget", gameObject,
            "onupdate", "updateFromValue",
            "time", 0.8f,
            "delay", 0.0f,
            "easeType", "easeOutQuad",
            "oncomplete", "FadeOutComplete"
            ));
    }

    public void FadeIn()
    {
        Debug.Log("FadeIn()");
        iTween.ValueTo(gameObject, iTween.Hash(
            "from", alphaMin,
            "to", alphaMax,
            "onupdatetarget", gameObject,
            "onupdate", "updateFromValue",
            "time", 0.8f,
            "delay", 0.0f,
            "easeType", "easeOutQuad",
            "oncomplete", "FadeInComplete"
            ));
    }

    private void updateFromValue(float newValue)
    {
        color.a = newValue;
        foreach (Material mat in mats)
        {
            mat.SetColor("_Color", color);    // SetColor()で、変更したColorをMaterialに戻す
        }
    }

    private void FadeOutComplete()
    {
        Debug.Log("Fade Out Complete");
    }

  private void FadeInComplete()
    {
        Debug.Log("Fade In Complete");
    }

    void Update()
    {
        if(Input.GetKeyDown("a")){
            FadeOut();
        }
        if (Input.GetKeyDown("z"))
        {
            FadeIn();
        }
    }

}

モデルデータの書き出しサイズを小さくする Mesh Compression

ビルドサイズを小さくする手段の一つに、モデルデータのメッシュサイズ圧縮がある。

これは、モデルデータのインスペクタ:Mesh Compressionで行う。

選択肢は、Off/Low/Medium/Highの4種類ある。

f:id:yasuda0404:20160915145848p:plain

オリジナルのサイズが約38MBのFBXデータをAssetに読み込み、Mesh Compressionの設定を変えてビルドしてみた。

なお、Windowsの場合、ビルドすると、任意名.exeと、任意名Data フォルダの2つのファイルができるが、Mesh Compressionでサイズが変わるのは、任意名Dataのみ。exeの方は影響を受けない。

シーンにモデルと、カメラ・ライトをおいただけのシンプルなシーンをビルドした結果の、それぞれのレンダリング画像と、***_Dataフォルダのサイズを以下に示す。

あくまでも、ひとつのケースの事例であるが、Off(Mesh Compressionなし)とLowの差は大きく、***_Dataフォルダのサイズは約半分にまで小さくなる。

一方、Highは、円筒部分の粗さが目立つ結果となった。

1. Off (***_Dataフォルダのサイズ 73.4MB) - Mesh Compression なし

f:id:yasuda0404:20160915150308p:plain

2. Low (33.8MB)

f:id:yasuda0404:20160915150349p:plain

3. Medium (31.8MB)

f:id:yasuda0404:20160915150407p:plain

4. High (27.6MB)

f:id:yasuda0404:20160915150426p:plain

なお、Mesh Compressionはビルド時に行われるものなので、アセット内のモデルデータは影響を受けない。

つまり、いったんHighに設定し他モデルを、シーンに配置したまま、Projectビューのモデルデータの設定をLowやOffにしても、書き出しサイズは問題なく変わる。この点は、3DCGソフトでポリゴンリダクションを行うのに比べて、結果を見ながら設定を変えられるので便利。

プロジェクトのUnityバージョンを確認する

過去のプロジェクトが、どのUnityバージョンで作ったのかを確認する(裏)ワザ。

ProjectSettings/ProjectSetting.assetを、VisualStudioなどのエディターで開く。

バイナリファイルなので、基本、意味不明の文字列が表示されるが、最初のほうにUnityバージョンの文字列が見えるはず。

↓下は、VisualStudioで開いた場合。(「メモ帳」などのテキストエディタと比べると、ダンプ形式で整然と並べてくれるので精神衛生上ベター?)

f:id:yasuda0404:20160913094522p:plain

Colorの補間

たとえば、点滅するインジケータなど、2つのカラー値を補間して表示したい場合に便利なのが、Color.Lerp()

public static Color Lerp(Color a, Color b, float t);

という形式で、t=0の時 Color a、t=1の時 Color b、t=0~1でaとbの間を線形補完する。

点滅(Blinking)するインジケータを実現するには、UpdateループとMathf.PingPong()を使って次のように実装できる。

void Update () {
    if (isBlinking)
    {
        if (myMat.HasProperty("_EmissionColor"))
        {
            Color color = Color.Lerp(colorOff, colorOn, Mathf.PingPong(Time.time * speed, 1.0F); 
            myMat.SetColor("_EmissionColor", color);
        }
    }
}

スクリプトからShaderパラメータを変更する(例:_EmissionColorについて)

Shaderのプロパティを変更するには、materialのメソッド、SetColor(), SetFloat(), SetInt(), SetTexture()を使う。( Unity - マニュアル: スクリプトを使用したマテリアルパラメーターへのアクセスと変更

それぞれのメソッドは、たとえば、SetFloat(シェーダプロパティ名, 値)という形をとる。

シェーダープロパティ名は、それぞれのシェーダに依存する。どんなシェーダプロパティがあるかは、Materialコンポーネントで、"Edit Shader..."を選択すればよい。

f:id:yasuda0404:20160730125025p:plain

UnityのStandard Shaderのプロパティは次の通り。

f:id:yasuda0404:20160730125345p:plain

ちなみに、"Compile and show code"をクリックすると、Shaderを編集できる。

たとえば、'_EmissionColor'は、'Color:Color'となっているので、

GetComponent<Renderer>().material = SetColor("_EmissionColor", Color.red)

のようにすればいい。

指定するShaderプロパティが存在しない場合は無視されるようだが、プロパティが存在するかどうかをチェックしてから値を設定したほうが、スクリプトとしてはよりロバスト。これは、material.HasProperty()を使う。

myMat = GetComponent<Renderer>().material;
if (myMat.HasProperty("_EmissionColor")) {
    myMat.SetColor("_EmissionColor", Color.black);
}

ここでひとつ疑問が。

今回は、「点滅するインジケータ」をEmissionの'Intensity'―Inspectorでは、下のEmissionのカラーボックスの右にある数字(現在1)―を上下して実現しようと考えた。しかし、Shaerプロパティに、Emission Intensityに相当するものが見当たらない。

f:id:yasuda0404:20160730130337p:plain

どうも、Emissionの強度の数字は、_EmissionColorのA(アルファ)値のようだ。

そこで、次のようなスクリプトを組むと、無事「1秒周期で点滅するインジケータ」ができた。('1.0f-val*val'の部分は、点滅パタンによって変更すればいい)

    void Update () {
        float val = Mathf.PingPong(Time.time, 1.0F);
        Color color = new Color(0f, 1.0f - val*val, 0f);    //Green + Alpha-Channel
        myMat.SetColor("_EmissionColor", color);
    }