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

※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 で扱うのが良いかもしれません。
今回試してみてなかなかいい感じなので、メッセージに限らず有効活用していきたいところです。

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

というわけで、今回のお題はこちらです!

shot2ss20160626224648506

近づくとメッセージを表示する看板です。
ステージ上に配置し、主にヒントとなりえるメッセージを表示させます。

看板の作成

至って普通の看板をモデリングします。

shot2ss20160626224726970

語ることがないほどシンプルです。
ファンタジー風味な3Dアクションならこれで十分です。
文字が書いてありませんが、アクションゲーム的には律儀に文字が書いてある看板なんてほとんどないので気にしません。

shot2ss20160626224812345

Unity へインポート後、読む方向に対して BoxCollider を設定しておきます。
この範囲内にプレイヤーが入った際にメッセージを表示するようにします。

メッセージの表示

まずはメッセージウィンドウを作っておきます。
背景となるウィンドウ(Image)とメッセージ(Text)の2つを追加します。

shot2ss20160626224913929

背景はGIMPで超適当に作り、メッセージは文字サイズとフォントを変更します。
この2つを親子関係にしておき、プレハブ化してインスタンス化できるようにしておきます。
UI はヒエラルキーの上から順に描画されるので、背景を親、メッセージを子オブジェクトにしました。

次にメッセージの表示部分です。
当初は看板の前でボタン押下して「調べる」ことで表示させようと考えました。
が、調べている間プレイヤーを止めたり、UIのコントロールが面倒だったりしたので、判定内にいる間は自動的に出すようにしてしまいました。
アクションゲーム的にも悠長に看板読むというのもアレなので尚更です。

そんなわけで、次に看板に設定するスクリプトを作成します。
BoxCollider で判定し、判定に入った際にメッセージを生成、判定から出た際にメッセージを削除します。

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

public class Signboard : MonoBehaviour {

    public string message;
    public GameObject messagePref;

    private GameObject canvas;
    private GameObject messageUI;

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

    void Start () {
        canvas = GameObject.Find("Canvas");
    }

    public void OnTriggerEnter(Collider c) {
        if (c.gameObject.tag == "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 = message;

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

    public IEnumerator OnTriggerExit(Collider c) {
        if (c.gameObject.tag == "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);
            }
        }
    }
}

iTween.ScaleTo() で大きさを変えながら生成しようと思ったのですが、何か思い通りに動いてくれないので挫折。
結局 MoveFrom() と MoveTo() で画面外からフェードイン/アウトするような形に落ち着きました。
このあたりの使い方は以下のサイトを参考にさせて頂きました!

http://bribser.co.jp/blog/itween-movefrom-and-moveto-make-ui-animation-like-brave-frontier/

20160526_01

動かすとこんな感じです!
ウィンドウが地味だったり、画面レイアウト的に合ってるか微妙だったりはありますが、動き的には問題なさそう。

まとめ

そんなわけで、テキストを表示する看板を作ってみました!
操作のチュートリアルを乗せたり、ステージの説明を乗せたりと用途は広いです。

1つ問題があり、表示するテキストがインスペクター上から直打ちなので、複数配置した場合に修正するのが非常に面倒です。
Unity 上から日本語を入力するのも何か抵抗があります。
できれば外部ファイルで定義し、そこから読み込ませるようにしたいですね。
次回はそのあたりの話題になると思われます。

Cドライブが圧迫してきたので整理した件について

というわけで、すごく今更な感じの記事ですが、パソコンのCドライブが圧迫してきたので整理してみました。
今日見たら空き容量240MBと、割と極限な状態になっていたりします。
パーティションを切っているのでC全体で140GBですが、それでも空き240MBはアウトです。

※個人的に要らないと思うものを削除しました。
本記事は参考程度で、実際に行う際は自己責任でお願いします。

圧迫している原因の調査と削除

フリーソフト「diskInfo」を使って容量を取っているファイル・フォルダを調べてみます。
棒グラフと%で使用率が分かるので、とても使いやすかったです。

休止状態の無効化

Cドライブ直下に「hiberfil.sys」という隠しファイルがありました。
サイズは8.1GBと馬鹿でかいです。

調べてみると、Windowsが休止状態に移る際にデータを一時保存するためのファイルとのこと。
そもそも休止状態なんてほとんど使っていないのですが・・・。
休止状態を無効化すると勝手に消えるとのことで、無効化してしまいました。
無効化する方法は以下のサイトで紹介されております。

https://pctrouble.net/running/delete_hiberfilsys_pagefilesys.html

旧バージョンのUnityの削除

何故か「C:\Program Files (x86)」下に Unity4.6 が残っていました。
Unity が64ビットに対応したのは5からだった気がするので、その名残かもしれません。
これだけで7GB近くとっていたので削除。

CBSファイルの削除

「C:\Windows\Logs\CBS」の下に大量のでかいファイルがありました。
合計すると33GB・・・。
調べると、WindowsUpdate で更新に失敗した際に生成されるログファイル?とのことです。
すごくどうでもよさそうなのでまとめて削除します。

「C:\Windows\Temp」にも似たような「cbs_~」というファイルが残っていました。
合計して4GBくらいでしょうか。
こちらも要らないと思われるので削除。

PatchCache\Managed内のファイル削除

「C:\Windows\Installer\$PatchCache$」下にあります。
ニュアンス的には「パッチのキャッシュ」とそのままな感じです。
合計で2.4GBとそれほどでもありませんが、一応削除しておきます。
塵も積もれば~とはよく言いますね。

ディスクのデフラグ

ついでにディスクのデフラグもやってしまいます。
購入して2年弱になるPCですが、一度もデフラグを掛けていませんでした。
本日分析してみたのですが・・・。

shot2ss20160622234735296

断片率32%!
これでもマシ(?)になった方で、不要ファイル削除前は脅威の52%を記録していたりしました。
それでも32%はまずい気がするのでデフラグしてしまいます。

Windows7標準のデフラグツールで夜12時に実行し、朝7時には完了していました。
マシン性能やディスク容量にもよるかもしれませんが、それなりに早く終わった印象です。
XPの頃は1日放置とか当たり前だったので。

まとめ

ということで、Cドライブの容量不足の解消とデフラグのお話でした。
定期的にメンテナンスしないとすぐ埋まってしまいますね。
割と思い切って消してしまうタイプなので今回もバッサリいきましたが、まずはディスクのクリーンアップ等を試す方が良いかと思われます。
それでも解消しない場合のみ、調べながら削除する感じでしょうか。

shot2ss20160622234415137

今回だけで66.8GB空けることができました。
ほぼ半分近くまで回復したので、とりあえず一安心です。

【Unity】「ProBuilder Basic」を使ったシンプルなステージ作成

というわけで、今回はステージ作成についてのお話です。
アセット「ProBuilder Basic」を使ってみました!

https://www.assetstore.unity3d.com/jp/#!/content/11919

旧名では「Prototype」と呼ばれていたそうです。
所謂エディタ拡張系のものですが、非常によくできていて使いやすかったです。

ProBuilderに手を出した経緯

今までステージ作成は Terrain と Blender を組み合わせて行っていました。
・・・が、

・Unity と Blender を交互に操作する必要がある
・Blender 側を変えると Unity 側でリインポートするため、微調整だけでも時間が掛かる
・たまに Blender 側の変更が Unity 側に反映されない
・Blender はZ軸が上だけど、Unity はY軸が上で(ry
・Terrain で作った地形の微調整が難しい

などの点でやっていられなくなったので、グーグル先生に相談。
ぐぐると、テラシュールブログさんで「ProBuilder Basic」というアセットが紹介されていました。

機能はいろいろありそうですが、Unity 上でモデルをいじれるのが非常によろしいです。
複雑なモデルを作るのは厳しそうですが、シンプルなオブジェクトを作るのであれば十分。
配置後でも変更できるので、「この面だけちょっと上に・・・」といった場合でも問題ありません。
面だけでなく、辺や頂点を移動させることもできます。

現状ではゲームとして成立していないのが何よりも致命的な点です。
いくらシステムやキャラクターを作ってもテストプレイできないので、有用かどうかの妥当性を図ることができません。
「カンガルー作る前にゲーム出来るようにしろ」という知人のお言葉は正論だと思います。

そんなわけで、ビジュアル面をいじりたい気持ちを抑えつつ、ProBuilder を使ってシンプルなステージを作っていきます!
使い方はテラシュールブログさんで紹介されているので、この記事では割愛します。

ステージの作成

とりあえず適当に配置してステージっぽくしていきます。
大抵はキューブ型で十分ですが、時々階段や円状のオブジェクトも混ぜていきます。
どうせならチュートリアル的な面も兼ねていきたいので、二段ジャンプや壁ジャンプなどが必要となるよう構成してみます。
あとはセンスとイマジネーションで気合で作ります。

shot2ss20160619222553038

Vキーを押しながら左ドラッグするとピッタリくっ付けやすいです。
今までは視点変更をしながら頑張って配置していましたが、これを使うとラクラクです。
というかもっと早く知っておくべきでしたorz

配置後に特定の面を伸ばしたり縮めたりもできます。
その場合は Ctrl を押しながらドラッグすると、マス目の分だけ伸縮されるのでやりやすいです。
逆にオブジェクト生成後に Scale を変えてしまうとマス目の大きさが変わってしまうので注意します。

shot2ss20160621212214983

前に作っていた「ステージ上に配置するオブジェクト」も入れてみます。
「壊せる箱」とか「ダメージを受けるトゲ」とかですね。

shot2ss20160619222659087

壁ジャンプは専用の壁に密着している時にジャンプボタンで行えます。
詳しい実装は以下の記事をご参照下さい。

【開発メモ】アクション「壁ジャンプ」の実装

カメラワークの調整

3Dゲーム且つカメラワークはオートなものを目指しています。
ということは、ステージの構成や場面に応じてカメラを移動・回転させる必要がありますね。
このあたりの実装は以前やっていますが、ちょっと古いので再度載せておきます!

shot2ss20160619222834238

mainCamera の Transform を直接あれこれすると、「プレイヤーを中心にカメラを回す」という処理が困難です。
なので、カメラを空オブジェクトの子に設定し、その空オブジェクトにスクリプトを付けます。
また、空オブジェクトは常にプレイヤーの位置を参照・移動するようにします。

using UnityEngine;
using System.Collections;

public class CameraController : MonoBehaviour {

    private GameObject mainCamera;
    private GameObject player;

    private Vector3 defaultAngle;
    private Vector3 defaultPosition;

    public float default_angle_y;

    void Start() {
        // パラメータに値を設定
        player = GameObject.FindGameObjectWithTag("Player");
        mainCamera = GameObject.FindGameObjectWithTag("MainCamera");

        defaultPosition = new Vector3(0, 25f, -70f);
        defaultAngle = new Vector3(20f, default_angle_y, 0f);

        initAngle(default_angle_y, defaultPosition, defaultAngle);
    }

    void Update() {
        // キャラクターを見失った場合再取得
        if (player == null) {
            player = GameObject.FindGameObjectWithTag("Player");
        }

        transform.position = player.transform.position;
    }

    /// <summary>
    /// カメラアングルの初期化を行う
    /// </summary>
    /// <param name="angle">カメラのY軸初期角度</param>
    /// <param name="position">カメラの位置</param>
    /// <param name="rotation">カメラの角度</param>
    private void initAngle(float angle_y, Vector3 position, Vector3 rotation) {
        mainCamera.transform.position = transform.position;
        mainCamera.transform.localPosition = position;
        mainCamera.transform.eulerAngles = rotation;

        transform.eulerAngles = new Vector3(0f, angle_y, 0f);
        transform.position = player.transform.position;
    }

    /// <summary>
    /// カメラアングルを切り替える
    /// </summary>
    public void changeAngle(Vector3 position, Vector3 angle) {
        // カメラ位置
        mainCamera.transform.position = transform.position;
        mainCamera.transform.localPosition = position;

        // カメラ本体角度
        iTween.RotateAdd(mainCamera, iTween.Hash(
            "rotation", angle,
            "time", 1f
        ));
    }

    /// <summary>
    /// プレイヤーを中心にカメラ位置を回転させる
    /// </summary>
    public void rotateCameraPosition(Vector3 camera_rotate_angle) {
        // カメラ回転
        iTween.RotateTo(gameObject, iTween.Hash(
            "rotation", camera_rotate_angle,
            "time", 1f,
            "onupdate", "rotateCameraAngle"
        ));
    }

    public void rotateCameraAngle() {
        mainCamera.transform.LookAt(gameObject.transform.position);
    }
}

ステージ中のカメラアングル切り替え用として、専用のスクリプトを作成します。
BoxCollider などで Trigger を持たせたオブジェクトに付与して使います。

using UnityEngine;
using System.Collections;

public class AngleChange : MonoBehaviour {

    [SerializeField]
    private Vector3 position;

    [SerializeField]
    private Vector3 angle;

    [SerializeField]
    private Vector3 camera_rotate_angle;

    private CameraController cameraController;

    void Start () {
        cameraController = GameObject.FindGameObjectWithTag("PlayerManager").GetComponent<CameraController>();
    }

    void OnTriggerEnter(Collider c){
        if (c.gameObject.tag == "Player") {
            cameraController.changeAngle(position, angle);
            cameraController.rotateCameraPosition(camera_rotate_angle);
        }
    }
}

スクリプト的にはどんな角度にも回転できますが、基本は以下の3つで行こうと思います!

・サイドビュー
横から見た視点です。
あまり奥行のない2D気味なエリアはこれでいきます。

shot2ss20160619223131086

shot2ss20160619223150495

・クォータービュー
斜め上から見下ろしたような視点です。
奥行のある3Dマップならこの視点が一番でしょうか。

shot2ss20160619223029231

shot2ss20160619223059855

・トップビュー
ほぼ真上から見たような視点です。
上2つと比べて使いどころは少なそうですが、狭い空間や迷路状ではこの視点が良いかも。

shot2ss20160619223437887

shot2ss20160619223546927

細かい部分の修正

その他ステージを作っていく上で、細かい点を修正していきます。

階段の上り下り

階段を作ったわけですが・・・。

20160621_01

あうっ(´Д⊂

ということで、CharacterController のパラメータをちょっといじります。
「Step Offset」の値を適度に上げ、階段程度の段差なら上れるようにします。

20160621_02

うーん・・・どうも減衰して上手く登れません。
一応「Slope Limit」を上げると登れるのですが、傾斜もぐんぐん登れてしまうためそれはそれで問題です。
「そもそも階段要るか?」という話ではありますが・・・。
現状でもジャンプ連打で登れるので、また暇なときに調査してみます。

水と滝

「Frineds of Ocean」というタイトル的に水は外すことができない要素です。
Unity5 から無料版でもリアルな水面のアセットが使えるようになったので、これを使ってそれっぽくしました。

shot2ss20160621211941143

また、段差があるので滝みたいのも欲しいですね。
これまた前に作ったのを配置してみました。
不自然ですが、これ以上となると流体シミュレーションとかしないときつい気もするので、まあ妥協できるレベルかなーと思います。

まとめ

今回作った部分までで、上から見るとこんな感じになりました!

shot2ss20160621212057963

今後はこのステージを作り込みつつ、いろいろと要素を盛り込めていければと思います!
とりあえずゲームとしての体裁を作ってしまいます!

【開発メモ】アシストキャラクター「カンガルー」の実装

というわけで、久しぶりに Unity 側の作成になります!
今回はアシストキャラクター「カンガルー」を実装しました!

shot2ss20160613234718256

アザラシに続く2匹目のアシストキャラクターです。
あちらは遠距離タイプなので、こちらは近距離型のインファイターといった趣向で作っていきます。
見た目通りパンチとキックで戦います。
ゲームに登場するカンガルーって大抵はパンチとキックだったりしますが・・・。

【Blender】モデリング練習メモ 「カンガルー」編

モデルは前回モデリングしたカンガルーがベースです。
意外と横幅が小さかったので、全体的な体格と腕、脚を大きくなるよう修正しました。

本ゲームではアシストキャラクターの位置付けです。
プレイヤーキャラがカンガルーに乗ることで操作できます。
仕様を固めきれていませんが、概要は以下の記事をご参照下さい。

【開発メモ】アシストキャラクターの実装 その1

各種アニメーションの作成

カンガルーがベースなので他のキャラクターとはちょっと変わったモーションにします。

20160613_01

移動は両脚でぴょんぴょん跳ねながら行います。
ちょっと脚が小さい目で、モーションがぎこちないのも合わせて微妙な見栄えです。

跳ねる際にカンガルーが上下しますが、これは Blender 側のキーフレームに移動情報を登録しています。
上下移動を Unity 側で制御するのはすごく大変そうなので・・・。

ちなみに現実のカンガルーは構造上、後退が行えないらしいです。
Unity 的には PlatformInputController を使用している関係上、後退という概念自体がありません。
変なところで悩むことにならずによかったです。

shot2ss20160613235135104

攻撃は上述の通り、パンチとキックがメインです。
とりあえず通常の連続攻撃だけ作ってみましたが、手足が短いうえに間接もないため、どうしても地味になってしまいました。
ということで、体全体を大げさに動かしながら、カンガルー自身の位置も少し前に動かすことでそれっぽくしてみます。

現在は StateMachineBehaviour を継承したスクリプトを攻撃系ステートに付けており、入った際に Kangaroo クラスの attackStateEnter() が呼ばれ、そこで攻撃判定を生成しています。
このあたりの実装は以下の記事をご参照下さい。

【再編集】Unity開発メモまとめ「Animator」

【開発メモ】攻撃判定制御のリファクタリング

public override void attackStateEnter(AnimatorStateInfo stateinfo) {
    if (isRiding) {
        // 存在する攻撃判定を削除
        // destroyAttackHit();

        // 通常攻撃
        if (stateinfo.IsName("Attack1")) {
            iTween.ValueTo(gameObject, iTween.Hash(
                "from", transform.forward / 4,
                "to", Vector3.zero,
                "onupdate", "characterMove",
                "time", 1.0f,
                "easetype", "easeOutQuart")
            );

            createHitManager(attackHitManager[0], hitOffset[0]);
            createHitManager(attackHitManager[0], hitOffset[1]);
        }

        if (stateinfo.IsName("Attack2")) {
            iTween.ValueTo(gameObject, iTween.Hash(
                "from", transform.forward / 4,
                "to", Vector3.zero,
                "onupdate", "characterMove",
                "time", 1.0f,
                "easetype", "easeOutQuart")
            );

            createHitManager(attackHitManager[0], hitOffset[0]);
            createHitManager(attackHitManager[0], hitOffset[1]);
        }

        if (stateinfo.IsName("Attack3A")) {
            iTween.ValueTo(gameObject, iTween.Hash(
                "from", transform.forward / 2,
                "to", Vector3.zero,
                "onupdate", "characterMove",
                "time", 1.0f,
                "easetype", "easeOutQuart")
            );

            createHitManager(attackHitManager[0], hitOffset[2]);
        }
    }
}

攻撃判定生成前に iTween.ValueTo() でちょっとだけ前に移動させます。
MoveTo() の方が動かす量の調整がしやすいのですが、当たり判定を突き抜ける危険があるため ValueTo() にします。

20160613_02

まだ何か地味ですが、その場でブンブンするよりはマシになりました!
いいエフェクトを作って上手くごまかせればいけそうです。

ゲーム的には「リーチは短いが手数と攻撃力に優れる」といった感じで調整していきます。
カンガルー自体判定が大きいので扱いは難しくなりそうです。
その分はインファイト時の圧倒的な破壊力を持たせて埋めようと思います。

移動入力による攻撃の出し分け

これはカンガルーに限ったことではありませんが、攻撃中に移動入力されているかどうかで最後に放つ攻撃を変えたいです。
カンガルーの場合は通常攻撃3段目を分岐させ、入力なしでアッパー、入力ありでキックにしようと思います!

shot2ss20160613235536001

3段目で分岐させるので、2段目からの遷移は「Attack3A」と「Attack3B」の2つを用意します。
2段目のステートに分岐を制御するためのスクリプトを付けます。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {

    /// <summary>
    /// 特定のキー入力時に移動キー入力に応じてTriggerパラメータを切り替えるクラス
    /// </summary>
    public class SwitchTriggerVelocity : StateMachineBehaviour {

        // 入力するキー(ProjectSettings)
        [SerializeField]
        protected string inputKey;

        [SerializeField]
        private string lowerParameterName;

        [SerializeField]
        private string upperParameterName;

        private bool isPrimaryInput = true;

        // 入力受付開始時間
        [SerializeField]
        private float receptionStartTime;

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (Input.GetButtonDown(inputKey) && isPrimaryInput) {
                if (receptionStartTime < stateInfo.normalizedTime) {
                    switchTrigger(animator);

                    // 一度入力したらExitまで変化させないようにする
                    isPrimaryInput = false;
                }
            }
        }

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

        private void switchTrigger(Animator animator) {
            // 移動入力がされているか判定
            if (GameConstants.STATE_VELCOITY_BORDER < System.Math.Abs(Input.GetAxis("Horizontal")) ||
                GameConstants.STATE_VELCOITY_BORDER < System.Math.Abs(Input.GetAxis("Vertical"))) {
                animator.SetTrigger(upperParameterName);
            } else {
                animator.SetTrigger(lowerParameterName);
            }
        }
    }
}

swtichTrigger() で切り替える際、移動入力がされているかを判定しています。
Input.GetAxis() で水平・垂直の両方向の入力値が取得できるのでこれを使います。
ただし入力方向によっては負の値になるので、System.Math.Abs() を使って絶対値で判定するのがポイントです。
GameConstants.STATE_VELCOITY_BORDER はただの定数で、今回は 0.9f を指定しています。

他には isPrimaryInput という変数を使い、複数回入力が出来ないようにしています。
receptionStartTime で攻撃モーションのどのタイミングから入力受付を開始するか制御できるようにしました。
モーションとの兼ね合いもありますが、今回は0.7fに設定しているので、現モーションの7割が再生されたタイミングから入力ができるようになります。

スーパージャンプ

カンガルーってジャンプ力にも優れているというイメージがあります。
となると、「ボタン長押し→放して大ジャンプ」とか欲しいですね!
某猿の横スクロールアクションに全く同じ名前のアクションがありますが、正にそんな感じ。

制御に CharacterMotor を使っている関係で微妙に面倒です。
ジャンプボタン長押しが自然な流れですが、CharacterMotor が有無を言わさずジャンプさせてしまいます。
「一度ジャンプ→長押しで溜める→大ジャンプ」の長いプロセスはアクションゲーム的にNGです。
CharacterMotor 側をいじるのも嫌なので、ひとまずはスーパージャンプ専用のボタンを作り、そのボタンの長押しで判定します。

InputManger からFキーを専用ボタンとして設定しておきます。
AnimatorController に bool パラメータとして「SuperJumpIdle」、Trigger パラメータとして「SuperJump」を作り、遷移の条件として使用します。
SwitchBoolByKey というクラスを作成、Idle ステートに付け、専用ボタン入力時にスーパージャンプ待機用のステートへ遷移させます。

using UnityEngine;

namespace StateController.PlayerAnimator {

    public class SwitchBoolByKey : StateMachineBehaviour {

        [SerializeField]
        private string inputKey;

        [SerializeField]
        private string parameterName;

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (!animator.GetBool(parameterName)) {
                animator.SetBool(parameterName, Input.GetButtonDown(inputKey));
            }
        }
    }
}

shot2ss20160613235640913

スーパージャンプ待機→スーパージャンプへの遷移も作ります。
またまた SwitchTriggerByKey の出番で、今度はFキーを離した際(=GetButtonUp時)にトリガーをONにします。
その際に normalizeTime が0.9以上でないと遷移しないようにしておきます。

これでスーパージャンプへの遷移はできたので、あとはスーパージャンプステートにスクリプトを追加します。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {

    public class SuperJump : StateMachineBehaviour {

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            animator.gameObject.GetComponent<CharacterMotor>().movement.velocity.y = 0;
            iTween.Stop(animator.gameObject);

            iTween.ValueTo(animator.gameObject, iTween.Hash(
                "from", animator.gameObject.transform.up * 3f,
                "to", Vector3.zero,
                "onupdate", "characterMove",
                "time", 1.0f,
                "easetype", "easeOutQuart")
            );
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            animator.ResetTrigger("SuperJump");
        }
    }
}

おなじみの iTween.ValueTo() で上方向に移動させます。
CharacterMotor を使用しているため、ジャンプ前に velocity.y を0にしておきます。
ジャンプする高さは割と適当です。

これでスーパージャンプは出来ましたが、スーパージャンプ待機モーション完了前にFキーを離した場合のことも考慮します。
SuperJumpIdle ステートにスクリプトを付けます。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {

    public class SuperJumpIdle : StateMachineBehaviour {

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (Input.GetButtonUp("UniqueAction")) {
                animator.SetBool("SuperJumpIdle", false);
            }
        }
    }
}

あとは SuperJumpIdle から Exit ステートへの遷移条件に「SuperJumpIdle が false の場合」と設定します。
これでFキーを離した際に bool が flase になり、自動的に Idle ステートへ戻ります。

まとめ

そんなわけで、カンガルーを Unity へインポートして実装してみました!
仕様が煮詰まっていないのに先取りで実装している感がありますが、やる気のあるうちに出来るところまで作ってしまいたいです。

自分の記事を見返すと、かなり説明を端折っている印象を受けます。
実際は他の実装部分がモロに絡んできていて、上手く実装機能だけを説明するのが難しい状態です。
「これじゃ分からんよ」という場合はコメントを頂ければ頑張って説明するので、お気軽にどうぞ!