【Unity】StandardAssetsの「Water4」で水中から水面上をレンダリングする方法

※本記事は「Unity5.4.1」時点の内容です。

というわけで、今回は Unity のシェーダーに関するお話です。
StandardAssets の Environment に「Water4」という水面シェーダーがあります。

shot2ss20161218223706980

Unity4 まではPro専用でしたが、Unity5 から無料でも使えるようになりました。
これによって水面のレンダリングがすごく楽に出来ます。

shot2ss20161218224017428

が、カメラが水中に入っている場合、水面上が見えなくなってしまいます。
ステージに陸上のエリアと水中エリアがある場合、かなり致命的な問題です。

適当に試行錯誤したところ、Water4 のシェーダーを修正すれば(とりあえずは)直るようです。
自分はシンプルの方を使っているので、「FXWater4Simple.shader」を修正します。
215行目付近の edgeBlendFactors が書かれている行をコメントアウトします。

baseColor = lerp (lerp (rtRefractions, baseColor, baseColor.a), reflectionColor, refl2Refr);
baseColor = baseColor + spec * _SpecularColor;

// baseColor.a = edgeBlendFactors.x; // ←この行をコメントアウト
UNITY_APPLY_FOG(i.fogCoord, baseColor);
return baseColor;

水色なので分かりにくいですが、 水面上が表示されるようになりました!

shot2ss20161218225529184

少し上に「half4 edgeBlendFactors = half4 (1.0, 0.0, 0.0, 0.0);」という宣言があるので、水面の少し上を baseColor のアルファ値に設定しているからかなーと察しています。
とはいえ、自分はシェーダーがほとんど分からないので、このアプローチがよろしいかは微妙です。
修正する場合は自己責任でお願いします!

【Unity】メッセージやパラメータをYAMLファイルで定義・参照する

この記事は「Unity 2 Advent Calendar 2016」の15日目です。

今回のお題は「Unity から YAML で定義したメッセージやパラメータを読み込む」です!
Unity 上で直接設定せずに外部の YAML ファイルに定義し、一元管理するのが目的です。

元々は仕事で「EC-CUBE3」を使う機会があり、そこで YAML が使われていたことが知ったきっかけです。
そのため、今回紹介する実装も EC-CUBE3 と似たようなものになっています。
あちらは Symfony2 というフレームワーク内でパースしており、DIコンテナ越しにキーをメッセージに変換できます。

この手のフォーマットは他に「JSON」とか「XML」とか「ini」とかありますが、私的には YAML が一番取っつきやすいです。
書き方が何種類かあることや、インデントにタブが使えない等の癖はありますが、慣れてしまえばサクサク書けます。
このあたりは好みの問題かもしれません。

ちなみに YAML は「ヤムル」or「ヤメル」と読むらしいですが、自分は前者派です。
最近は ISO も人によって「アイエスオー」だったり「イソー」だったりすることに衝撃を覚えたりしましたが・・・。

YAMLファイル読み込み用クラスの作成

YAML ファイルの読み込みに関しては、以前投稿した記事で紹介しております。

【Unity】YAMLファイルの読み込みと表示用メッセージの管理

基本は「YamlDotNet for Unity」を使用して YAML ファイルを読み込み、キーから対応する値を取得します。
単に列挙されたキー/値を取得するだけであれば YamlDotNet のドキュメント通りでいけますが、ネストさせた場合に特定の値をピンポイントで取得することが出来ませんでした。
EC-CUBE3 っぽく取得できるようにユーティリティを作成していたりしますが、取得する度に YAML を毎回ロードしていたり、所々ハードコーディング気味でイケてないです。

そんなわけで、専用に管理するクラスを作成し、インスタンス化して処理させてみます。

 
using UnityEngine;
using System.IO;
using YamlDotNet;
using YamlDotNet.RepresentationModel;

public class YamlManager {

    public YamlStream messageYaml;

    private static YamlManager yamlManager;

    // YAMLファイル保存パス
    private const string ymlPath = "Assets/Yaml/";

    private YamlManager() {
        // メッセージ用YAMLのロード
        StreamReader inputFile = new StreamReader(ymlPath + "message.yml", System.Text.Encoding.UTF8);
        messageYaml = new YamlStream();
        messageYaml.Load(inputFile);
    }

    /// <summary>
    /// 指定されたキーを元にメッセージを取得する
    /// </summary>
    public string getMessageValue(string key) {
        string message = getValue(key, messageYaml);
        return message;
    }

    /// <summary>
    /// 指定されたキーを元にYAMLから値を取得する
    /// </summary>
    private string getValue(string key, YamlStream file) {
        // キーをドットで分割
        string[] keys = key.Split('.');

        // キーの配列数(=ネストレベル)取得
        int keyCount = keys.Length;

        // ルートのマッピング取得
        YamlMappingNode mapping = (YamlMappingNode)messageYaml.Documents[0].RootNode;
        YamlScalarNode node = null;

        for (int i = 0; i < keyCount; i++) {
            // キー配列が最後の要素になった場合は ScalarNode を取得
            if (i == keyCount - 1) {
                node = (YamlScalarNode)mapping.Children[new YamlScalarNode(keys[i])];
            } else {
                // キーを元に1つ深いネストのマッピングを取得
                mapping = (YamlMappingNode)mapping.Children[new YamlScalarNode(keys[i])];
            }
        }

        return node.ToString();
    }

    /// <summary>
    /// インスタンスを取得する
    /// </summary>
   public static YamlManager getInstance() {
        if (yamlManager == null) {
            yamlManager = new YamlManager();
        }

        return yamlManager;
    }
}

シーン上のオブジェクトとして用意する必要はないと思うので、MonoBehaviour は継承しません。
またインスタンスは1つあれば十分なため、Singleton パターンを適用してみました。

getValue() 内で渡されたキーを.(ドット)毎にパースし、ネストした YAML ファイル のキーを参照→取得を繰り返します。
キーが最後まで取得し終えたら、そのノードの値を ToString() で出力します。

見直してみると、キーを間違えたり定義していなかった場合のアフターケアがないのが微妙です。
YamlDotNet.Core に YamlException があるので、例外として投げちゃうのが手っ取り早そうです。

using System.IO;
using System.Collections.Generic;
using UnityEngine;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;

public class YamlManager {

    public YamlStream messageYaml;

    private static YamlManager yamlManager;

    // YAMLファイル保存パス
    private const string ymlPath = "Assets/Yaml/";

    private YamlManager() {
        // メッセージ用YAMLのロード
        StreamReader inputFile = new StreamReader(ymlPath + "message.yml", System.Text.Encoding.UTF8);
        messageYaml = new YamlStream();
        messageYaml.Load(inputFile);
    }

    /// <summary>
    /// 指定されたキーを元にメッセージを取得する
    /// </summary>
    public string getMessageValue(string key) {
        string message = null;

        try {
            message = getValue(key, messageYaml);
        } catch (YamlException e) {
            message = "メッセージ設定エラー";
            Debug.LogError(e.ToString());

            return message;
        }

        return message;
    }

    /// <summary>
    /// 指定されたキーを元にYAMLから値を取得する
    /// </summary>
    private string getValue(string key, YamlStream file) {
        // キーをドットで分割
        string[] keys = key.Split('.');

        // キーの配列数(=ネストレベル)取得
        int keyCount = keys.Length;

        // ルートのマッピング取得
        YamlMappingNode mapping = (YamlMappingNode)messageYaml.Documents[0].RootNode;
        YamlScalarNode node = null;
        YamlNode outNode;

        for (int i = 0; i < keyCount; i++) {
            YamlScalarNode currentNode = new YamlScalarNode(keys[i]);

            // キーから値を取得できない場合はエラー
            if (!mapping.Children.TryGetValue(currentNode, out outNode)) {
                throw new YamlException();
            }

            // キー配列が最後の要素になった場合は ScalarNode を取得
            if (i == keyCount - 1) {
                node = (YamlScalarNode)outNode;
            } else {
                // キーを元に1つ深いネストのマッピングを取得
                if (outNode.GetType() != typeof(YamlMappingNode)) {
                    throw new YamlException();
                }
                mapping = (YamlMappingNode)outNode;
            }
        }

        return node.ToString();
    }

    /// <summary>
    /// インスタンスを取得する
    /// </summary>
   public static YamlManager getInstance() {
        if (yamlManager == null) {
            yamlManager = new YamlManager();
        }

        return yamlManager;
    }
}

getValue() と getMessageValue() を修正してみました!
TryGetValue() で取得できない場合と、1つ下のマッピング取得時に typeof で判定した場合で投げています。
例外で処理が止まってしまうのはまずいので、getMessageValue() 内で受け止めて仮のメッセージを表示します。

メッセージやラベルをYAMLで定義する

例えば、「ステージ○○の4つ目の看板のメッセージを~」なんてことがあった場合、「シーンを開く→ヒエラルキーから看板選択→インスペクターから修正」と考えると、げんなりしそうになりますね。
これを YAML のキーを設定する形にしておき、実行時に対応するメッセージを取得すれば、YAML ファイルを開いて直すだけになります。

パス Assets/Yaml/ の下に message.yml というファイルを作成し、メッセージを定義していきます。

# 操作
control:
  jump_input: スペースキーを押すとジャンプします
  throw_action: |-
    アクションキーを押すと物を持ち上げます
    再度押すと投げます

# ステージ
stage:
  break_block: ブロックは攻撃することで破壊できます
  trampoline: タイミングよくボタン入力で大ジャンプ!
  climbWall: 壁に近づいてボタン入力で更にジャンプできます


# システムメッセージ
gameover:
  continue: スタート地点又は直前のチェックポイントから再開します
  end: タイトル画面に戻ります

YAML の文法については割愛しますが、
・セミコロン後の半角スペースは必須
・#以降はコメント扱い
・ネストさせる場合はスペース(タブはNG)
あたりを抑えておけば何とかなると思われます。

取得する時は YamlManager をインスタンス化して getMessage() を呼びます。
Start() あたりで取ってしまうのがベターでしょうか。

 
void Start () {
    string message = YamlManager.getInstance().getMessageValue("stage.trampoline");
    Debug.Log(message);
}

以下はキーに「stage.trampoline」を設定した場合の出力です。

shot2ss20161205003818664

後は message を UI 等に出力すればOKです。
(出力方法は記事の趣旨から逸れるので割愛します)

読み込むYAMLファイルを切り替える

もし英語のゲームをやっているとき、「SELECT LANGUAGE」なんてオプション項目があったらステキですよね!
今日の家庭用ゲームでも、「日本語」「英語」を切り替えられるゲームはそれなりに見受けられます。
キーを変えずに値だけを変えた YAML ファイルを複数用意すれば、多言語への対応ができたりします。

ある程度命名規則を決めた方がやりやすいので、ここも EC-CUBE3 っぽくいきます。
先程の message.yml は日本語なので message.ja.yml に修正し、新しく作るファイルは message.en.yml にします。
しかし自分は英語が全くダメなので、最近パワーアップしたとウワサの「Google翻訳」に変換してもらいます。

# 操作
control:
  jump_input: Press space key to jump.
  throw_action: |-
  Press the action key to lift the object.
  Press again to throw.

# ステージ
stage:
  break_block: Blocks can be destroyed by attacking.
  trampoline: Timely well with big button jump!
  climbWall: You can jump further by button input as you approach the wall.

# システムメッセージ
gameover:
  continue: Restart from the start point or the previous checkpoint.
  end: Return to title screen.

何かエキサイト翻訳っぽい直訳気味なものもありますが、何となく伝わればそれでいいです(ぁ

次に先程の YamlManager を修正します。
ゲーム内で動的に切り替えることを考慮し、YAML ファイルをロードする処理を関数化しておきます。
以下、YamlManager のコンストラクタとロード用関数のみ記載します。

 
private YamlManager() {
    // メッセージ用YAMLのロード
    loadMessageYaml();
}

/// <summary>
/// メッセージ用YAMLファイルをロードする
/// </summary>
public void loadMessageYaml(string lang = "ja") {
    StreamReader inputFile = new StreamReader(ymlPath + "message." + lang + ".yml", System.Text.Encoding.UTF8);
    messageYaml = new YamlStream();
    messageYaml.Load(inputFile);
}

デフォルトは ja としてロードし、必要に応じて loadMessageYaml() で YAML ファイルを切り替えます。

void Start() {
    YamlManager yamlManager = YamlManager .getInstance();
    yamlManager.loadMessageYaml("en");
    string message = yamlManager.getMessageValue("stage.trampoline");
    Debug.Log(message);
}

キーは先程と同じく「stage.trampoline」です。

shot2ss20161205003840118

言語を2文字で指定しなければならないのが微妙ですが、とりあえず読込先を変えることができます。
後は message を UI で(ry

パラメータをYAMLで定義する

完全固定な値はC#の定数で定義しますが、ある程度固定だけど変化する値は YAML で管理してしまいます。
例としては、音量や言語などのオプション設定的なものが挙げられます。
キーコンフィグも YAML で扱いたかったのですが、InputManager を動的に変えるのはかなり面倒なようなのでパスしました。

メッセージ定義の時は文字列しかなかったので問題ありませんでしたが、パラメータとなると数値が入ってくることも考慮しなければなりません。
C# では単一の型しか返すことが出来ないわけですが、返り値を string 固定にしている時点で数値を扱うのは絶望的です。
こういった場合は自分で型や構造体を定義して返すのが良いらしいので、適当に返却用のクラス ReturnYamlNode を作成しています。
それに合わせて string で処理していた部分も ReturnYamlNode で返すように修正しました。

最終的な YamlManager が以下になります!

 
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;

public class YamlManager {

    public YamlStream messageYaml;
    public YamlStream configYaml;

    private static YamlManager yamlManager;

    // YAMLファイル保存パス
    private const string ymlPath = "Assets/Yaml/";

    private YamlManager() {
        // メッセージ用YAMLのロード
        loadMessageYaml();

        // 設定用YAMLのロード
        loadConfigYaml();
    }

    /// <summary>
    /// 指定されたキーを元にメッセージを取得する
    /// </summary>
    public string getMessageValue(string key) {
        ReturnYamlNode messageNode = null;

        try {
            messageNode = getValue(key, messageYaml);
        } catch (YamlException e) {
            string message = "メッセージ設定エラー";
            Debug.LogError(e.ToString());

            return message;
        }

        return messageNode.value;
    }

    /// <summary>
    /// メッセージ用YAMLファイルをロードする
    /// </summary>
    public void loadMessageYaml(string lang = "ja") {
        StreamReader inputFile = new StreamReader(ymlPath + "message." + lang + ".yml", System.Text.Encoding.UTF8);
        messageYaml = new YamlStream();
        messageYaml.Load(inputFile);
    }

    /// <summary>
    /// 設定用YAMLファイルをロードする
    /// </summary>
    public void loadConfigYaml() {
        StreamReader inputFile = new StreamReader(ymlPath + "config.yml", System.Text.Encoding.UTF8);
        configYaml = new YamlStream();
        configYaml.Load(inputFile);
    }

    /// <summary>
    /// 指定したキーを元に設定ファイルの値を取得する
    /// </summary>
    public ReturnYamlNode getConfig(string key) {
        return getValue(key, configYaml);
    }

    /// <summary>
    /// 指定されたキーを元にYAMLから値を取得する
    /// </summary>
    private ReturnYamlNode getValue(string key, YamlStream file) {
        // キーをドットで分割
        string[] keys = key.Split('.');

        // キーの配列数(=ネストレベル)取得
        int keyCount = keys.Length;

        // ルートのマッピング取得
        YamlMappingNode mapping = (YamlMappingNode)file.Documents[0].RootNode;
        YamlScalarNode node = null;
        YamlNode outNode;

        for (int i = 0; i < keyCount; i++) {
            YamlScalarNode currentNode = new YamlScalarNode(keys[i]);

            // キーから値を取得できない場合はエラー
            if (!mapping.Children.TryGetValue(currentNode, out outNode)) {
                throw new YamlException();
            }

            // キー配列が最後の要素になった場合は ScalarNode を取得
            if (i == keyCount - 1) {
                node = (YamlScalarNode)outNode;
            } else {
                // キーを元に1つ深いネストのマッピングを取得
                if (outNode.GetType() != typeof(YamlMappingNode)) {
                    throw new YamlException();
                }
                mapping = (YamlMappingNode)outNode;
            }
        }

        ReturnYamlNode returnNode = new ReturnYamlNode(key, node.ToString());
        return returnNode;
    }

    /// <summary>
    /// インスタンスを取得する
    /// </summary>
    public static YamlManager getInstance() {
        if (yamlManager == null) {
            yamlManager = new YamlManager();
        }

        return yamlManager;
    }
}

public class ReturnYamlNode {
    public string key;
    public string value;

    public ReturnYamlNode(string key, string value) {
        this.key = key;
        this.value = value;
    }

    public int getIntValue() {
        int intValue;
        if (int.TryParse(value, out intValue)) {
            return intValue;
        }

        throw new UnityException();
    }
}

そもそも取得時に数値だろうが文字列だろうが ToString() し始めるあたり、純正の PHPer であることは隠しようもありません。
PHP でコーディングした後に C# や Java をやったりすると、型のガチガチっぷりにビックリしたりします。
まあその型の緩さのせいで PHP も面倒なことがありますが・・・。

取得する場合はインタタンス化して getConfig() を使います。
数値が欲しい場合は更に getIntValue() で取得します。

void Start () {
    YamlManager yamlManager = YamlManager.getInstance();
    int bgm_volume = yamlManager.getConfig("bgm_volume").getIntValue();
    Debug.Log(bgm_volume);
}

ただし、最大HPや攻撃力などの「勝手に変えられると困るもの」は YAML で管理すべきではありません。
YAML は所詮テキストファイルなので、簡単に書き換えられてしまいます。
また、所謂バリデーションチェックは必ず行う必要があります。
ということを考えると、意外とパラメータを外部化するのは面倒な気もします。

ビルド後のYAMLファイル保存先について

Unity 上で開発する分には /Assets/Yaml/ で問題ありませんが、ビルドして書き出すと話が違ってきます。
というのも、Assets 以下のデータは全てまとめられてしまうため、パスで指定しても参照できなくなってしまいます。

そのため、プロジェクト直下に専用のディレクトリを作成し、そこに保存するようにします。
フィールドの ymlPath と Start() 内を修正します。

// YAMLファイル保存パス
private const string ymlPath = "Settings/";

void Start () {
    string basePath = System.IO.Directory.GetCurrentDirectory();
    System.IO.Directory.CreateDirectory(basePath + "/Settings");
}

これでビルド後も参照できるようになりました。
ただ、Unity エディタ上からアクセスできなくなるので、開発時は不便な感じです。
このあたりは割り切るしかないのかもしれません。

まとめ

そんなわけで、Unity 上で YAML をあれこれしてみました!
外部ファイルに書いている分、Unity 上で直接設定するより手間ですが、後々の修正や管理がしやすいです。
反面、入力時にエディタ側で補完してくれないので、(定数よりも)キーの指定を間違えやすいです。

余談ですが、Qiita のアドベントカレンダーとして投稿したのは今年が初めてだったりします。
他の投稿を見ると記事に自信がなくなってきますが、来年もネタがあったら書いていきたいです。

明日は @WheetTweet さんです!

【Unity】シーン上のオブジェクト取得時にシングルトンっぽい動きの実現

というわけで、時々「ブログ名変なの」とか言われるりべるんです。
「ゴマちゃん」で「フロンティア」で「バージョン2」ってだけなのですが・・・。

今回もよく分からないタイトルですが、要は「シーン上に1つしかないオブジェクトのコンポーネント」を取得する際に、シングルトンパターンっぽく出来ないかというお話です!

「Unityでシングルトン」的な話題はぐぐると幾らでもヒットしますが、MonoBehaviour を継承していないクラスでのお話だったりします。
(MonoBehaviour を継承しなければ new 演算子でインスタンス化できます)
しかし、Unity 的にはシーン上に管理用オブジェクトを配置し、インスペクターから必要なパラメータやプレハブの設定をしたい・・・という場面があります。

自分のゲームで言うと、ゲーム内の共通エフェクトをまとめた EffectManager 何かが正にそれです。
エフェクト毎にプレハブを設定しておき、マネージャから生成・破棄の管理をしています。

shot2ss20161203194259608

そんなシーン上に1つしかないオブジェクトを効率よく取得する方法について考えてみました!

単一オブジェクトのコンポーネント取得

シーン上に存在するオブジェクトを取得するには Find() か何かしてくるわけですが、そのオブジェクトを複数のクラスから参照する場合、いちいち Find() → GetComponent() は避けたいところです。
ということで、「自身の静的フィールドにインスタンスを保持し、あったらそれを返す、なかったら Find()&GetComponent() する」という動きを実装してあげます。

using UnityEngine;
using System.Collections;

public class TestManager : MonoBehaviour {

    private static TestManager testManager;

    public void hoge() {
        Debug.Log("hoge");
    }

    public static TestManager getInstance() {
        if (testManager == null) {
            testManager = GameObject.Find("TestManager").GetComponent<TestManager>();
        }

        return testManager;
    }
}

取得時はシンプルに getIntance() を呼ぶだけです。

void Start() {
    TestManager testManager = TestManager.getInstance();
    testManager.hoge();
}

GameObjectで応用

別にクラスのインスタンスに限らず、シーン上に1つしか存在しない GameObject を取得する場合にも使えます。
ちょっと変えるだけでいけそうな雰囲気。

using UnityEngine;
using System.Collections;

public class TestManager : MonoBehaviour {

    private static GameObject testManager;

    public void hoge() {
        Debug.Log("hoge");
    }

    public static GameObject getInstance() {
        if (testManager == null) {
            testManager = GameObject.Find("TestManager");
        }

        return testManager;
    }
}

取得方法も変わりません。
GameObject に対して操作するときはこれでもOKですね。

GameObject testManager = TestManager.getInstance();
Debug.Log(testManager.ToString());

シーン上に1つしかないもの・・・DirectionalLight や mainCamera とかですね。
特にカメラはあれこれ動かす機会が多くなりそうなので重宝します。
実現のために1つスクリプトを作成しなければなりませんが、複数クラスから Find() を連発するよりはマシかと思われます。

まとめ

そんなわけで、MonoBehaviour を継承したオブジェクトを何度も取得する場合について考えてみました!
Unity5 で早くなったとはいえ、Find() や GetComponent() を繰り返すのは避けたいところですね。

【Unity】StateMachineBehaviourからアニメーションの特定タイミングで処理を実行する方法

というわけで、ひさびさにUnity関係のお話です!
今回はタイトルにもある「StateMachineBehaviour」を使ったスクリプトのお話です。

shot2ss20161116195302218

「StateMachineBehaviour」を継承したクラスは、AnimatorController のステートに設定することができます。
主に「ステートに遷移した直後」「ステート中」「他のステートへ遷移する直前」のタイミングで関数が呼び出されますが、「ある特定のタイミングで関数を呼び出す」ということが出来ません。

アニメーションをインポートした際に ImportSettings の Events から出来るようですが、指定する関数名を直打ちするのがあまり好きではありません。
SendMessage() もそうですが、エディタのリファクタリング機能で置換できないので、後々変更があった場合に面倒なことになってしまいます。
アクセス修飾子が private であってもぶち抜いて実行されるのもどうかと思います。

特定タイミングで処理を実行するスクリプトの作成

冒頭で「Events をつかいたくない!」と言ったものの、現状の StateMachineBehaviour に「アニメーションの特定タイミングで処理を実行する」的な機能はありません。
なので自作するわけですが、ステート中に実行される OnStateUpdate() と 、AnimatorStateInfo.normalizedTime を組み合わせればいけそうです。

そんなわけで作ったスクリプトがこちら!

using UnityEngine;

namespace StateController.PlayerAnimator {

    public class ExecuteTest : StateMachineBehaviour {

        [SerializeField]
        protected float execute_time;

        private bool execute_flg;

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (execute_time < stateInfo.normalizedTime && !execute_flg) {
                execute_flg = true;

                // 何らかの処理
                Debug.Log("hogehoge");
            }
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }
    }
}

ポイントは OnStateEnter() と OnStateExit() 内で execute_flg をfalseに設定している部分です。
これがないと execute_flg が true から切り替わらず、2回目以降if文の中が実行されません。
Enter で行えば Exit では要らない気もしますが、念のため!

またif文の条件ですが、normalizedTime が f loat で細かく変化する関係上、「execute_time == stateInfo.normalizedTime」では不可能に近いです。
四捨五入することも考えましたが、近い値になると複数回通ってしまうためNG。

shot2ss20161116202600048

これを AnimatorController 上のステートに設定し、execute_time を設定すればOKです。
AnimatorStateInfo.normalizedTime はアニメーション実行時間に応じて0~1の値を取るので、execute_time も0~1で指定する必要があります。

ただし、ループするステートの場合は上手くいきません。
ループするステートの場合は normalizedTime が1でリセットされず、どんどん加算されていきます。

ということで、normalizedTime 小数点以下を取得して判定するように修正します。
直したのは OnStateUpdate() のif文判定だけです。

using UnityEngine;

namespace StateController.PlayerAnimator {

    public class ExecuteTest : StateMachineBehaviour {

        [SerializeField]
        protected float execute_time;

        private bool execute_flg;

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (execute_time < stateInfo.normalizedTime % 1 && !execute_flg) {
                execute_flg = true;

                // 何らかの処理
                Debug.Log("hogehoge");
            }
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }
    }
}

「剰余演算子」なるものを使えば簡単にできるみたいです。
ちなみに normalizedTime の整数部分がループ回数に相応するようです。
覚えておくと役に立つ・・・かも。

使用例

自分が使っている一例として、「特定タイミングで iTween.MoveBy() を実行しキャラクターを移動させる」があります。
上で載せたスクリプトを少し修正します。

using UnityEngine;

namespace StateController.PlayerAnimator {

    public class MoveCharacter : StateMachineBehaviour {

        [SerializeField]
        protected float execute_time;

        [SerializeField]
        protected Vector3 velocity;

        [SerializeField]
        protected float move_time;

        private bool execute_flg;

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (execute_time < stateInfo.normalizedTime % 1 && !execute_flg) {
                execute_flg = true;

                iTween.Stop(animator.gameObject);
                iTween.MoveBy(animator.gameObject, iTween.Hash(
                    "amount", velocity,
                    "time", move_time
                ));
            }
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }
    }
}

shot2ss20161116202724703

インスペクター上で実行するタイミングと移動ベクトル、移動時間を設定します。
アニメーションの途中で「1歩踏み込んで攻撃」的なモーションがあっても安心です。

20161116_01

実際に動かすと上のような感じ。
分かりにくいですが、縦斬り→薙ぎ払いまで1モーションで、execute_time は0.5に設定してあります。

別に iTween でなくても良いので、animator.gameObject.getComponent() とかと組み合わせればもっといろいろ出来ます。
StateMachineBehaviour のスクリプト内に処理をあれこれ書きたくない方は、素直に Player クラスあたりを getComponent() して処理させるのも有りだと思います。

まとめ

そんなわけで、StateMachineBehaviour 上から特定タイミングで処理を実行する方法について考えてみました!
特定タイミングであれこれしたい場面は多いので、活用する機会は多くなりそうです。

【Unityメモ】エディタ拡張でタグを選択できるようにする

というわけで、絶賛仕事が忙しいりべるんです。
ブログのネタはおろか、Unity すらまともに触れていない日々です。
すっぽかすわけにもいかないので、何か簡単に記事にできそうなものについて書いていきます。

shot2ss20161019200247596

今回のお題はタイトル通り、「エディタ拡張でタグマネージャのタグを選択できるようにする」ことです!
ぐぐるとそれなりにヒットするので今更感はありますが、自分用のメモとして書いておきます。

拡張用クラスの作成

エディタ拡張用のクラスを作成します。
Editor ディレクトリの中に保存する必要があります。

using UnityEngine;
using UnityEditor;
using System.Collections;

[CustomEditor(typeof(Block))]
public class TagSelecter : Editor {
    SerializedProperty trigger_tag;

    void OnEnable() {
        trigger_tag = serializedObject.FindProperty("trigger_tag");
    }

    public override void OnInspectorGUI() {
        base.OnInspectorGUI();

        serializedObject.Update();

        trigger_tag.stringValue = EditorGUILayout.TagField("Trigger Tag", trigger_tag.stringValue);

        serializedObject.ApplyModifiedProperties();
    }
}

Unity5.3から SerializedProperty を使用した形式でないと値が保存されないようです。
OnInspectorGUI() 内で Update() と ApplyModifiedProperties() を呼ぶようにします。

ちなみにエディタ拡張で指定した際、クラスのフィールドがインスペクターに表示されなくなってしまいますが、親の OnInspectorGUI() を呼んであげれば上手くいくようです。
割と盲点だったので気を付けるようにします。

拡張先のクラス修正

タグマネージャから選択させる理由として、「ブロックが特定のタグと接触した場合のみ壊れる」といったことをしたいためです。
インスペクターから直接打ち込んでも良いのですが、タグマネージャから選択できるほうが便利で確実です。

ゲーム上のブロックは Block クラスを継承しているため、Block クラスにエディタ拡張を設定します。
エディタ拡張クラスで [CustomEditor] 属性で Block を指定しておきます。

using UnityEngine;
using System.Collections;

public class Block : MonoBehaviour {

    [HideInInspector]
    public string trigger_tag;

    public void OnTriggerEnter(Collider c){
        if (c.gameObject.tag == trigger_tag) {
            Destroy(gameObject);
        }
    }
}

とりあえず最低限の動作ですが、こんな感じでしょうか。
フィールド名はエディタ拡張クラスの FindProperty() の引数と合わせる必要があります。

public の場合、フィールド自体の項目とエディタ拡張の項目で二重に表示されてしまいます。
他から変えてもらっても困るので private か protected が妥当なところですが、今の構造上変えるのも面倒だったので、[HideInInspector] 属性を付けてインスペクター上から隠してみました。

まとめ

すごく短いですが、エディタ拡張でタグを選択できるようにしてみました!
所謂「マジックストリング」的なコーディングは避けたいところなので、こういった便利機能を入れていきたいですね。