ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

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

time 2016/06/30

UnityYamlMerge の記事ではありませんのでご了承下さい

というわけで、今回は「Unity で YAML ファイルを読み込んでメッセージ(文字列)を取得したい」というお話です。
記事的には前回の「メッセージを表示する看板」の続きです。

【Unity】テキストをUGUIで表示する看板の作成

要するに「画面に表示するメッセージをファイルで管理したい!」といった趣旨です。
例えば看板を複数設置してそれぞれにメッセージを設定した場合、後から修正するには「ヒエラルキーから選択→メッセージを修正」という感じになると思います。
看板の数が少なければ問題ありませんが、多くなってくると大変な作業です。

そこでメッセージを別ファイルで key と value で管理し、看板からは key を指定するようにすれば、誤字脱字があったとしてもファイルの value だけを修正すれば済みますね。
そんなメッセージ管理の実現に YAML ファイルを使ってみました!

C# の定数でも出来そうですが、YAML はインデントを入れることでネストさせることができるので管理しやすいです。
いちいち「public static const ~」と定義するのも面倒なので・・・。
XML や JSON を使う手もありますが、仕事で使って慣れている YAML が一番行けそうだったので決定しました。

ライブラリの導入

C# で Yaml を扱うにはライブラリが必要らしいです。
ぐぐった感じでは以下の3つが候補になりました。

・YamlDotNet
・YamlDotNet for Unity
・YamlSerializer

結論から言うと「YamlDotNet for Unity」にしました!
インポート時のファイル量が多めですが我慢します。
YamlDotNet は Android や iOS で使えないらしいので(一応)見送り、Serlalizer は使い方がよく分からない上、数年前で更新が止まっていたので見送りました。
YamlDotNet で妥協するのであれば、dll ファイルを Assets/Plugins に突っ込むだけで使えるのでお手軽です。

YAMLファイルの作成

メッセージを定義した YAML ファイルを作成します。
以下は作成した message.yml ファイルです。

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

# キャラクター
character:
  marinpa:
    attack: 攻撃ボタンを連打することで、剣で連続攻撃を行います
  mogffy:
    attack: 攻撃ボタンを連打することで、槍で連続攻撃を行います
  shilriss:
    attack: 攻撃ボタンを連打することで光弾を連続発射します

# ステージ
stage:
  break_block: ブロックは攻撃することで破壊できます

注意すべきポイントをいくつか書いておきます。
・セミコロン後の半角スペースは必須
・ネストさせる場合は半角スペースで行い、タブは使わない
・行中に # を入れた場合、その以降はコメント扱いになる

これらを間違えると漏れなく構文エラーとなるので注意しましょう。
特に「セミコロン後の半角スペース」が慣れるまでは曲者です。

ちなみに上のようなセミコロンで区切る方法は「マッピング」と呼ばれています。
ダッシュ(-)を入れる方法は「シーケンス」と呼ばれるそうです。
シーケンスが配列、マッピングが連想配列のようなものでしょうか。

今回の看板に限らず、メッセージ的なものは全て外部ファイルに出してしまいたいです。
直書きしてしまうと修正が非常に面倒なので。

YAMLファイルの読み込み

上で作った YAML ファイルをスクリプトから読み込みます。
看板以外でも使いそうなので、パースする処理をユーティリティ化してみました。

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

public static class YamlUtility {

    private static string ymlPath = "Assets/GUI/";

    public static string getMessageValue(string key) {
        // YAMLファイルのロード
        StreamReader inputFile = new StreamReader(ymlPath + "message.yml", System.Text.Encoding.UTF8);
        YamlStream yaml = new YamlStream();
        yaml.Load(inputFile);

        // キーをドットで分割
        string[] keys = key.Split('.');

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

        // ルートのマッピング取得
        YamlMappingNode mapping = (YamlMappingNode)yaml.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();
    }
}

単にキーから値を取得するだけであれば、YamlDotNet のドキュメント通りでいけます。
が、ネストされた YAML ファイルの特定のキーから値を取得する方法が分かりませんでした。
mapping.RootNode ではネスト最上位の要素しか取得できず、mapping.AllNodes はネストに関係なく全ての要素を取得してしまいます。
てっきり「getValue(string key)」的な関数があるかと思いきや、そんな楽な話ではなかったようです。

悩んでもぐぐっても分からなかったので、結局自分で実装してしまいました。
Symfony2 のようにネストをドット(.)で区切って指定し、区切られた回数だけ for でループさせます。
区切った要素分だけ mapping.Children で YamlMappingNode を再設定し、最後の要素になったら YamlScalarNodeを取得します。
YamlScalarNode の ToString() で値を返して終わりです。
Symfony2 のYAMLパーサも正規表現を使って強引にパースしていたりするので、そこまでこだわらなくていい部分かもしれません。

看板のスクリプトから YamlUtility の getMessageValue() を呼び出します。
インスペクターからキー値を指定し、対応するメッセージを取得するようにします。

UI関係のソースの意味は前回の記事をご参照下さい。
Start() 内で YAML からメッセージを取得するよう変更してあります。

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class Signboard : MonoBehaviour {

    public string messageKey;
    public GameObject messagePref;

    private GameObject canvas;
    private GameObject messageUI;

    private float fadein_x = 100;
    private float fadeout_x = 300;
    private string messageValue;

    void Start () {
        canvas = GameObject.Find("Canvas");
        messageValue = YamlUtility.getMessageValue(messageKey);
    }

    public void OnTriggerEnter(Collider c) {
        if (TagUtility.getParentTagName(c.gameObject) == "Player") {
            if (!messageUI) {
                // メッセージ表示用Ui生成
                messageUI = Instantiate(messagePref) as GameObject;
                messageUI.transform.SetParent(canvas.transform, false);

                // メッセージ設定
                Text messageUIText = messageUI.transform.FindChild("SignboardMessageText").GetComponent<Text>();
                messageUIText.text = messageValue;

                // メッセージを画面内へ移動
                iTween.MoveFrom(messageUI, iTween.Hash(
                    "position", messageUI.transform.position + new Vector3(fadein_x, 0, 0),
                    "time", 1
                ));
            }
        }
    }

    public IEnumerator OnTriggerExit(Collider c) {
        if (TagUtility.getParentTagName(c.gameObject) == "Player") {
            if (messageUI) {
                // メッセージを画面外へ移動
                iTween.MoveTo(messageUI, iTween.Hash(
                    "position", messageUI.transform.position + new Vector3(fadeout_x, 0, 0),
                    "time", 1
                ));

                yield return new WaitForSeconds(0.5f);

                // メッセージ削除
                Destroy(messageUI);
            }
        }
    }
}

あとは実行するだけ!
message.yml のキー値を入力し、看板に近づいてメッセージを表示させます。

shot2ss20160628225235438

見事な文字化け!
上で作った message.yml の文字コードが「SJIS」になっていたようです。
IT系の仕事に就いているとは思えない致命的なミスに頭を抱えつつ、UTF-8 のBOMなしで保存し直します。

shot2ss20160628225317549

今度はしっかり日本語で表示されました!
上は messageKey に control.jump_input を指定した場合になります。

shot2ss20160628230733166

|- で改行した場合も問題なさそうです。

まとめ

そんなわけで、YAML からメッセージを取得し、UI に表示させてみました!
頻繁に修正する可能性のある定義だったり、ユーザーが設定するパラメータなどは YAML で扱うのが良いかもしれません。
今回試してみてなかなかいい感じなので、メッセージに限らず有効活用していきたいところです。

down

コメントする