【Unity】横スクロールアクション作成のためのオブジェクト移動制御

というわけで、年越しを前にして風邪が全然治らないりべるんです。
ブログも2週間近く放置していたので、年越し前に何か書いておこうと思います!

shot2ss20161230224759374

今回はUnityで横スクロールアクションを作る際の移動制御についてになります。
最近のゲームでありがちな、「3Dで作られているけど中身は2D」な感じです。
故にUnityの2D機能とは趣旨が異なるためご注意下さい。

こんな記事を書いた理由は他でもなく、自作ゲームを3Dで作ることに挫折しかけているためです。
ステージ作成が2Dの方がやりやすいのもありますが、カメラアングルの問題を上手く解決できません。
そのあたりのセオリーが分かるまで、ステージは2D移動をベースにして作ろうと思っています。

プレイヤー移動を2Dに限定

前に記事で紹介した、「CharacterMotor使用時の移動を2Dに限定する」でプレイヤー移動を2D方向に限定できます。

【Unity】CharacterMotor使用時のキャラクター移動を2Dに限定する

ただしこれだけでは完全ではなく、Unity側の物理演算の結果によってZ軸に移動する可能性があります。
適球状のオブジェクトを配置し、横から突っ込むと分かりやすいです。
上の対応は「プレイヤー側の入力で」Z軸に移動しないだけなので・・・。

スマートに解決する方法が分からなかったので、Update() 内で強引にZ軸を制御しました。
RigidBody があれば FreezePosition を指定することで解決しますが、CharacterController を使用している関係で使用できません。

void Update () {
    transform.position = new Vector3(transform.position.x, transform.position.y, globalManager.stageManager.baseDepth);
}

スタックオーバーフローには「Z軸を0にしろ(意訳)」的なものがあったりします。
が、自分はZ軸が0になるようにステージを作っていなかったので、真に受けて設定すると異次元にぶっ飛んでしまいました。

ということで、ステージ用のマネージャクラスに「現在ベースとなっているZ軸」を持たせることにしました。
ステージ進行に合わせて変化させることも可能で、その値でプレイヤーのZ軸を固定します。
マネージャクラスについては趣旨から外れる上に我流なので割愛しますが、 単に Player クラスのフィールドに持たせても問題ありません。
その場合、Start() 内で transform.position.z を元に初期化すればOKです。

20161230_01

ダッシュ時に挙動が怪しくなる時がありますが、概ね固定されるみたいです。

オブジェクト・敵キャラクター移動を2Dに限定

RigidBody の「FreezePosition」でZを指定してあげるだけでOKです。
あまりにも簡単すぎてプレイヤー制御の苦労が嘘のようです。

shot2ss20161230230000329

・・・本当にこれだけでOKなので、あまり書くことはないです!
「RigidBody万歳!」といったところでしょうか。

強いて言えば、敵キャラクターのロジックが3D用になっているので、それを修正します。
プレイヤーと同じように左右のみ瞬時で振り向くようにします。
Slerp() で補完していたのを LookRotation() に置き換えるだけでOKです。
その際、Y軸とZ軸を0に設定して振り向かせます。

void Update() {
    if (lookTarget != null) {
        Vector3 angle = lookTarget.position - transform.position;
        angle.y = 0;
        angle.z = 0;
        transform.rotation = Quaternion.LookRotation(angle);
    }
}

lookTarget にはプレイヤーの Transform を入れます。
実際は別オブジェクトに判定を持たせ、OnTriggerEnter() でセットしています。
(これまた趣旨と外れるので割愛します)

まとめ

かなりざっくりですが、横スクロールアクションにおける移動制御について考えてみました!
自分が作りたいゲームを想像すると、何かと2Dっぽい部分が多い気がするので、とりあえずこれで行ってみようと思います!

2016年もあと24時間ちょっとです。今年も早かったですね。
来年も「ゴマちゃんフロンティア」をよろしくお願いします!

【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() を繰り返すのは避けたいところですね。