箱庭ハーブblog

7年目プログラマの趣味の小部屋

IEnumeratorとコルーチン、C#の使い方

ienumerator



コルーチン。C#ではイテレータ構文。
便利ですね。

便利というか、何か挙動やロジックに影響しないエフェクト書きたかったら、
コルーチンで書きたくなるのが人の思考ってもんよ。

yield return nullやWaitForSecondsなら、
全てのUpdate()の後に実行される事だけ意識すればOK。
使うだけなら何も難しくない。

Unityのライブラリは構造が洗練されているので、自分で制御するより楽。
停止と再開も自前で実装する必要がないし、とても使いやすい。

そんなですが、コルーチンを支えてるC#の文法の歴史を覚書してみます。

要約するとこんな言語機能です。

【1. .NET1.0とforeachとイテレータ】

.NET1.0時代、foreachを実現するための仕組みとして登場
仕組みはデザパタのイテレータそのままです。
 ・コレクションに、IEnumerableを実装し。イテレータを取得できるようにする
 ・イテレータそのものを表すのが、IEnumerator
 ・実際は「IEnumerableなしでも、GetEnumerator()が宣言されていればOK」(特殊文法)

【2. .NET2.0とイテレータ構文(イテレータブロック)】

.NET2.0時代に、yield return(中断)、yield break(終了)を使う構文が追加されました。
IEnumerableかIEnumeratorを返すメソッド=イテレータブロックにて使用可能です。
匿名メソッドと同じく無名○○系に見られる実装の簡略化が目的で、
イテレータを明示的にクラスとして定義する必要がなくなりました。
Unityで使用される書き方も、この仕組みを転用したものになります。

【3. Unityのコーチン】

・StartCoroutine(IEnumerator)でUnityのイベント発生に基づいてコルーチンできる。
・StopCoroutine(IEnumerator)で、該当のコルーチンを停止できる。再開もできる。
・yield returnする時、以下を返すと、nullと異なる再開タイミングになる
  ・WaitForSeconds(X) 指定した時間が経過するまで待機
  ・WaitForFixedUpdate() 次の物理演算終了後まで待機
  ・StartCoroutine(IEnumerator) 渡したコルーチンが終了するまで待機
・コルーチンの終了部にyield return hogeで、Currentから戻り値を得られる
・GameObjectが非アクティブになると動作を停止する
・コンポーネントがdisableになっても、動作は継続する

yield return で戻り値を返すことで、
イテレータ.Currentで戻り値を取得するという使い方が、なんともギーグな感じ。

ScriptableObjectとInstantiateとシリアライゼーションの使い方

scriptableobject、instantiate



2017-03-03
シリアライズルールに関して、追記しました。

ども、お疲れ様です。
知ってる人は知っている、ScriptableObjectのInstantiateのお話。

全体を俯瞰するには、下記の公式サイトをひとまず読むと良いでしょう。

スクリプトシリアライゼーション
https://docs.unity3d.com/ja/current/Manual/script-Serialization.html


ブログ内の記事では、こちらでも扱っています。

完全攻略、Unityシリアライゼーション
http://hakoniwaherb.blog.shinobi.jp/Entry/534/

ScriptableObject、便利ですね。
継承とポリモーフィズムが使えて、Prefabのようにpublicフィールドにリンク出来て、
エディタ上で編集できる。

最高です。

Serializable付加クラスと適切に使い分けることで、
大半の機能をエディタだけで処理することが可能になります。

で、今回はScriptableObjectとInstantiate。

1、Instantiateは何をしているのか

Prefabでおなじみ、Instantiateさん。複製してくれます。
Prefab(ひな型)を作ってリンクしておき、それをInstantiateするだけ。
なんとなくならすぐわかる。

しかし
  • どうしてPrefabの型はGameObjectなのか
  • どうしてInstantiateを調べるとシリアライズの話が出てくるのか
  • ScriptableObjectをInstantiateするとは、そもそもどういうことなのか

これ、仕組みを知ればすべて解決します。

単純。

Instantiateは、
シリアライズ可能なオブジェクトをシリアライズしてデシリアライズしているだけです。

うん、用語がわからない人は良くわからんですね。

シリアライズは、テキスト化です。
現在のメモリ上にあるオブジェクト(インスタンス)を、テキストに写し取ることです。

デシリアライズは、Deシリアライズです。
テキストから、オブジェクト(インスタンス)を作ることです。

「シリアライズ対象」「シリアライズされる」という単語は、
値が復元するかどうか、つまりコピーされるかどうかを表します。

このシリアライズのルールは、デフォルトでは次のようなルールになっています

大前提
1. シリアライズ可能な型は、以下の通り
 ・UnityEngine.Object継承クラス
 ・intなど基本型
 ・Serializable付加クラス
 ・Vector3、Quaternionなど、Unityシリアライザで特別に処理される構造体(※)
2. 1の配列、List<T>型もシリアライズ可能。Dictionary<TKey, TValue>は無理

(※)
これらのUnity内の構造体は、Serializableではないため、
Unityと関係ない.NETのシリアライザやサードパーティのシリアライザを用意すると、
シリアライズされない

詳細
1. privateメンバや自動プロパティの暗黙のフィールドも、ScriptableObjectなら対象である
1. privateメンバ、自動プロパティはシリアライズされない。
2. staticメンバはシリアライズされない
3. read onlyメンバはシリアライズされない
4. [NonSerialized]付加メンバは、シリアライズされない
5. UnityEngine.Object継承クラスであるメンバは、参照も復元する
6. UnityENgine.Object継承クラスでないメンバは、参照が復元しない!(事実上のクローン)

2017/03/13 追記
privateメンバや自動プロパティは、シリアライズ対象ではありませんでした。
Instantiateせずに使用した場合、値が保持されるように見えますが、
エディタを起動しなおしたり、実機実行でアプリを終了して再起動すると、
値が初期化されています。
基本的にシリアライズはMonoBehaviourと同じ挙動をする、という認識でOKです。

5と6に関しては、6が変に見えるかもしれませんが、
シリアライズの仕組みを考えると、むしろ5がすごいことをしていると言えるでしょう。
ポインタやシリアライズIDを元に参照を復元しているのですから、恐ろしい。

大前提に関して補足すると、
シリアライズ対象でない型、つまりSerializable付加でない非基本型は、
シリアライズされませんし、復元もされません。
Instantiate後のインスタンスはnull(structなら暗黙コンストラクタされた値)になります。

詳細の6に関して補足すると、
これはSerializable付加クラス型のメンバに対して起こる内容です。
Serializable付加クラス型はUnityEngine.Object継承型とは限らないため、
UnityID管理されず参照が復元されません。
しかしながら、Serializable付加クラスはシリアライズ対象ですから、値は復元されます。
結果として、Serializable付加クラス自体のインスタンスは別々になります。


2、ScriptableObjectとは何者か?

ScriptableObjectは、Prefabとよく似ています。
しかし、
Prefabと異なり、Instantiateする文化が言及されていないので、
いまいちよくわからない動作をするイメージがあると思います。

厄介なのは、Unityエディタ上とビルドしたアプリで挙動が異なることです。
Unityエディタでは、実行時の情報はアセットに反映・保存されますが、
ビルドした後は通常の手順では反映されません。

ここら辺は、下記のリンクの下の方にある説明が詳しいです。

テラシュールブログ ScriptableObjectについて
http://tsubakit1.hateblo.jp/entry/2014/07/24/030607

3、ScritableObjectとInstantiate

PrefabをInstantiateせずに使うと、どうなるでしょう。

弾のPrefabだったら、複製されないし、色々使い物になりません。
しかし、値置き場として使うなら、優秀です。

ScriptableObjectも同じです。

値置き場として使うなら、Instantiateせずに使えば優秀です。
当然、値を変更すると、
エディタ実行中ならばScriptableObjectのアセットの内容が変わっちゃいます。

========
→ ※注意
前述の通りエディタではなくアプリ実行中は、
最終的にアセットに保存反映されないため、アプリを再起動すると元に戻っています。
========

一方、Prefabと同じようにScriptableObjectをInstantiateすると、
クローンされます。

クローンされたScriptableObjectは当然元とは関係がなくなるので、
自由に挙動させることが出来るようになります。

前述の通り、ScriptableObjectが別のPrefabやScriptableObjectを
リンク(publicフィールド接続、アウトレット接続)していた場合、
その参照は復元されてしまうので、同じアセットを共有したくない場合は、
それらもInstantiateする必要があります。

まとめ

ScriptableObjectのInstantiateは、非常に強力な機能です。
ScriptableObjectをInstantiateせずに使う場合、
初期パラメータとしてしか使えません(セーブデータも無理)が、
Instantiateすれば、現在ステータスとセットで表現できるようになります。
(ステータスメンバはNonSerializedをつけることが重要です。)

目的に応じてInstantiateするかしないかを使い分けることで、
最適なパフォーマンスと構造が作れるでしょう。

Unityの回転のデフォルト

備忘録



公式サイトはここ
Unity の回転と向き
https://docs.unity3d.com/ja/current/Manual/QuaternionAndEulerRotationsInUnity.html


1. 初期配置(0,0,0)は、ワールド軸に一致。
2. Rotate(オイラー)回転の向きは、Z=X→Y、Y=Z→X、X=Y→Z
3. Rotate(オイラー)回転の順序は、Z、X、Y

一般的なやつですね

完全攻略、Unityシリアライゼーション

UNITY、Unity、unity
SERIALIZABLE、Serializable、serializable
SCRIPTABLEOBJECT、ScriptableObject、scriptableobject

2017/03/13
ScriptableObjectに関して、勘違いがあったため補足。

2017/04/22
全体的に記事を修正中(やりかけ)

 
使い方を間違えなければ、非常に強力です。
Unityのシリアライズは非常に便利なのですが、使い方を間違えると不具合地獄に落ちます。

ーー

この門をくぐる者は一切の高望みを捨てよ

ーー

まさにそんな言葉が頭によぎるレベルですね。

基本的な内容は、下記の2つの公式サイトを読むのが早いと思いますが、
詳細に関してはプログラマでも文章だけではよくわからない。


Unityのシリアライゼーション
http://japan.unity3d.com/blog/?p=1630

スクリプトシリアライゼーション
https://docs.unity3d.com/ja/current/Manual/script-Serialization.html


実際に試した方が早いということで、冒頭のスクリーンショットのようになりました。

実際にやったこと



S1.cs
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Assets.UnlimitedFairyTalesHowTo.UnityAndCSharp.SerializeSample.Parts
{
    // S1 is not UnityEngine.Object!
    [Serializable]
    public class S1
    {
        // PARAMETER / STATUS

        int _num1_private; // private member don't show on editor and don't serialize.

        [SerializeField]
        int _num2_private_serializable;

        public int _num3_public;

        [NonSerialized]
        public int _num4_public_NonSerialized;

        public int Num5_property { get; set; }

        public List _numList; // Array and List are serializable.

        // PROPERTY & FIELD / METHOD

        public void CountUp()
        {
            _num1_private++;
            _num2_private_serializable++;
            _num3_public++;
            _num4_public_NonSerialized++;
            Num5_property++;
        }

        public string GetText()
        {
            return
                "num1(private) = " + _num1_private + "\n" +
                "num2(SerializeField) = " + _num2_private_serializable + "\n" +
                "num3(public) = " + _num3_public + "\n" +
                "num4(NonSerialized) = " + _num4_public_NonSerialized + "\n" +
                "num5(Auto Property) = " + Num5_property;
        }
    }
}


MyBehaviour.cs
using UnityEngine;

namespace Assets.UnlimitedFairyTalesHowTo.UnityAndCSharp.SerializeSample.Parts
{
    public class MyBehaviour : MonoBehaviour
    {
        // PARAMETER / STATUS
#pragma warning disable 649 // SerializedField's fileds or public fields are initialized by Unity.

        public S1 _s1_public;

        S1 _s1_private; // MonoBehaviour's private fields is not initialized!

#pragma warning restore 649
        // PROPERTY & FIELD / METHOD

        // Use this for initialization
        void Start()
        {

        }

        // Update is called once per frame
        void Update()
        {

        }

        public void AllCountUp()
        {
            _s1_public.CountUp();
            if (_s1_private != null) {
                _s1_private.CountUp();
            }
        }

        public string AllGetText()
        {
            var a = "MyBehaviour's" + "\n"
                + "s1_public" + "\n"
                + _s1_public.GetText() + "\n"
                + "s1_private" + "\n";
            if (_s1_private == null) {
                a += "null";
            }
            else {
                a += _s1_private.GetText();
            }
            return a;
        }
    }
}

MyScriptableObject.cs
using UnityEngine;

namespace Assets.UnlimitedFairyTalesHowTo.UnityAndCSharp.SerializeSample.Parts
{
    [CreateAssetMenu(menuName = "UnlimitedFairyTales/HowTo/SerializeSamples/MyScriptableObject")]
    public class MyScriptableObject : ScriptableObject
    {
        // PARAMETER / STATUS
#pragma warning disable 649 // SerializedField's fileds or public fields are initialized by Unity.

        public S1 _s1_public; // ScriptableObject's field seems to be saving value on editor, but It is not actually saved.

        S1 _s1_private;

#pragma warning restore 649

        // PROPERTY & FIELD / METHOD

        public void AllCountUp()
        {
            _s1_public.CountUp();
            _s1_private.CountUp();
        }

        public string AllGetText()
        {
            return "MyScriptableObject's" + "\n"
                + "s1_public" + "\n"
                + _s1_public.GetText() + "\n"
                + "s1_private" + "\n"
                + _s1_private.GetText();
        }
    }
}


AssetUserBehaviour.cs
using UnityEngine;

namespace Assets.UnlimitedFairyTalesHowTo.UnityAndCSharp.SerializeSample.Parts
{
    public class AssetUserBehaviour : MonoBehaviour
    {
        public GameObject _myPrefab;
        public MyScriptableObject _myScriptableObject;

        // PARAMETER / STATUS

        public S1 _s1;

        GameObject _instantiated;
        MyBehaviour _instantiatedBehaviour;

        // PROPERTY & FIELD / METHOD

        // Use this for initialization
        void Start()
        {
            _instantiated = Instantiate(_myPrefab);
            _instantiatedBehaviour = _instantiated.GetComponent();
        }

        // Update is called once per frame
        void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space)) {
                _instantiatedBehaviour.AllCountUp();
                _myScriptableObject.AllCountUp();
                _s1.CountUp();
            }
            var canvasText = this.GetComponentInChildren();
            canvasText.text =
                "instantiated" + "\n"
                + _instantiatedBehaviour.AllGetText() + "\n"
                + "\n"
                + "myScriptableObject" + "\n"
                + _myScriptableObject.AllGetText() + "\n"
                + "\n"
                + "S1" + "\n"
                + _s1.GetText();
        }
    }
}



【操作】
1、実行し、5回キーを叩く
2、一度止め、再度実行し、5回キーを叩く
3、一度止め、MyScriptableObjectのインスペクタからResetを実行し、5回キーを叩く
4、一度止め、再度実行し、5回キーを叩く

結果からわかること

1、MonoBehaiviour上の非公開メンバは、newで初期化されない
(myMonoBehaviourのs1_privateがnullのまま)

2、ScriptableObjectのAssetを使用する際は、初期化済み。Asset初回使用=Deserializeなため
(myScriptableObjectのs1_privateが初期化済み)

3、ScriptableObjectの状態は、privateでも保存される
(myScriptableObjectのprivateメンバが20回叩いたことになっている)

2017/03/13
privateメンバは20になり一見記憶されているように見えるが、
実際のところシリアライズ対象でないため、ファイルとして保存されない
エディタを起動しなおすと、リセットされている

3-1、NonSerializedは、保存されない

3-2、privateは、そのままではReset対象外である

3-3、自動プロパティは、privateと同じようにエディタ上は見えないが保存される

2017/03/13
privateメンバと同様一見保存されているように見えるが、
シリアライズ対象でないため、ファイルとして保存されない
エディタを起動しなおすと、リセットされている

4、MonoBehaviourのメンバであるS1は、保存されない
(Instantiateされたものを使用しているので、元Prefabと直接関係ない)

個々を見ると謎めいた動きですが、
全ての結果を並べると、動作自体は非常に単純な仕様であることが分かります。

・ScriptableObjectは参照。共有される。保存される
・Editor上に表示されていないメンバは、そのままではResetされない
・Instantiateなどで参照が維持されるのは、Unityオブジェクトのみ
 (今回の調査対象外ですが、このような前提が存在しています)

なお、ScriptableObjectを複製したい場合は、
Prefabと同じようにInstantiateすると良いです。

また、ScriptableObjectAssetに値を保存したくない場合
(パラメータではなくステータス)は、privateにするか、
NonSerialized属性を付加すればよいです。

UnityのStartとAwakeの違い

UNITY、Unity、unity、Start、start、Awake、awake



基本的に、順序詳細に関しては下記URLを見れば分かります。

公式はここ
https://docs.unity3d.com/ja/current/Manual/ExecutionOrder.html

unity-の-instantiate-と-awake-とか-startが流れるタイミングを把握する
http://jigax.jp/unity-の-instantiate-と-awake-とか-startが流れるタイミングを把握する/

概要

・前提
非アクティブは未考慮

・要約
1. 公式のInitialization中にInstantiateされたオブジェクトは、同フレーム中にUpdateする。
  ただし、事実上シーンロード直後の第1フレームでしかこの状況は発生しない(※1)
2. 公式のGame Logic中にInstantiateされたオブジェクトは、同フレーム中にUpdateしない(※2)
3. AwakeはInstantiateが返る前に呼ばれる
4. StartはInstantiateを呼び出した呼び出し元の大元(※3)が終わるまで呼ばれない
5. 公式のGame Logic中にInstantiateされたオブジェクトは、同フレーム中にStartする

・Instantiate詳細
1. Behaviourコンストラクタ(アタッチされたコンポーネント群のうちの1つである。)
2. Behaviourインスペクタの読み込み
3. 他のアタッチされたコンポーネントを含む初期化が完了し、取得が許可される
4. BehaviourのAwake
5. Instantiateが戻り値を返す
6. Instantiateを呼び出した呼び出し元の大元が終わるまで
7. BehaviourのStart
8. (InstantiateされたのがStart以前だった場合=シーンロード後の第1フレーム)BehaviourのUpdate

(※1)
2フレーム目以降は、Game Logic(呼び出し元がUpdateなど)でしかInstantiateされることはないので。

(※2)
FixedUpdate中のInstantiateは試していないので、知りたい場合は試すの推奨。

(※3)
全てのスクリプトは誰かのMonoBehaviourのAwake、Start、Updateあるいはコルーチンなどから呼ばれることになる。
その呼び出しが終わるまで、InstantiateされたオブジェクトのStartは呼ばれない

使い分け

Awakeがいい
・オブジェクト単体で完結する初期化
・Instantiateが返る前に初期化しておきたい内容

Startがいい
・Instantiate呼び出し元による加工(弾の方向や配置、親子関係バインド)してから処理したい

前提

例えばこんな時
・ScriptableObjectで発射システムを作った
・全く別に、加速弾やカーブを表現できるBehaviour&Serializableパラメータのペアを作った
  ・なお、このSerializableパラメータは初期方向や初期速度もある

組み方

普通に組めば、以下の順番
Instantiate > 配置・パラメータ調整 > ロジックへパラメータを渡す

ので、
・ロジック自体、あるいは関連部品のnewはAwakeで行う
・Serializableパラメータのロジックへの適用は、Startで行う

フリーエリア

takemori
Twitter : @takemori_kondo

1. Unityと戯れてます
2. Cake3は劣化じゃないRails

iOS
coming soon...

Windows
Html Editor - Nazuna
Managed DirectX サンプル集

beginning since
2006.08.17
renewaled on
2011.06.03

最新コメント

[2018/11/20 スーパーコピーブランド専門店]
[2013/06/14 ミューネ]
[2012/08/30 ノートPC]