【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メモ】ゲーム画面のUI「体力ゲージ」の作成

※ 2016/10/29 記事を一部修正しました。

というわけで、今回は所謂「体力ゲージ」の作成になります!
やはりというか、恒例の「大切なのに作っていなかった」部分です。

shot2ss20160508224101922

ダメージで体力ゲージが減少し、なくなるとゲームオーバーにする予定です。
基本的に即死するトラップや落下死は実装しません。
デザインもシンプルに、ひとまず「体力が見えるもの」を目指します!

体力ゲージの作成とUI設定

実をいうと、Unityの新UIはあまり使ったことがなかったりします。
NGUIをいじったことがあるので分かる部分もありますが、割と手探りな状態からスタート。

とりあえずゲージの素材がないと話にならないので、フォトショで適当に作ります。
作るのはメインとなる緑ゲージ、体力減少表現用に赤ゲージ、ゲージの背景、そしてゲージの枠となるフレームです。
今はグラデーションでそれっぽく描くだけで終わらせます。

Unityへインポート後、InputType を「Sprite」に設定します。
Texture のままでは透明部分が白くなってしまう上、そもそもUIのImageに設定できません。

shot2ss20160508224136610

新規で UI→Canvas を作成し、追加で Image を4つ配置します。
表示順は「Hierarchyで見て下のUIほど手前」になるらしいです。
なので「フレーム→緑ゲージ→赤ゲージ→背景」の順に並び替えます。

shot2ss20160508224232114

ポジションや縦横幅は画面に合わせてお好みで。
Anchor は横は left、縦は middle に設定し、Pivot は X:0、Y:1 にします。
画面に合わせて調整したものを1つ作り、コピーして Image の参照だけ変えるのが確実かも。

このあたりの設定はテラシュールブログさんの記事が分かりやすいです。
今回の体力ゲージ作成の際も参考にさせて頂きました!

Image コンポーネントの Image Type は Filled にします。
また、Fill Method は Horizontal、Fill Origin は Left にしておきます。

体力ゲージ増減の制御

お次はスクリプトによる制御です。
冒頭でも書きましたが、要はプレイヤーの体力を視覚化するものなので、Player クラスの life の値をそのまま反映すればOKです。
ただ、Image コンポーネントの Fill Amount の値は0~1の間でしか設定できないことに注意が必要です。

UI表示の制御をPlayerクラスでやらせると更に見にくくなるので、専用のマネージャクラスを作成します。
以下はUIオブジェクトをコントロールする「GameUIManager」クラスです。

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

public class GameUIManager : MonoBehaviour {

    private Player player;

    public Image lifeGage;
    public Image lifeRedGage;

    void Start () {
        player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();
        this.initParameter();
    }

    void Update () {
        lifeGage.fillAmount = player.life / player.maxLife;
    }

    private void initParameter() {
        lifeGage = GameObject.Find("LifeGage").GetComponent<Image>();
        lifeGage.fillAmount = 1;

        lifeRedGage = GameObject.Find("LifeRedGage").GetComponent<Image>();
        lifeRedGage.fillAmount = 1;
    }
}

プレイヤーと各種UIを Find()→GetComponent() します。
UIのオブジェクト名は変わることはない想定なので、オブジェクト名でFindしてしまいます。

あとは Update() 内でプレイヤーの体力割合に応じた値を lifeGage に設定します。
Playerクラスに最大ライフを持たせて参照することで、最大値が変わった場合にも対応できます。
これで緑ゲージがプレイヤーの体力に連動して増減するようになりました!

赤ゲージの減少処理

次は赤ゲージで、これは体力減少時の表現用に設定してあるものです。
最近のゲームの体力ゲージは「ダメージ分緑ゲージが減り、後を追うように赤いゲージに徐々に減る」といった表現のものが多い感じがします。
なので「赤ゲージ」といっても、モンハンや格ゲーのような所謂「ヴァイタルソース」的なものとは異なります。

「特定の値を徐々に変化させる」処理には iTween.ValueTo() が便利です。
これをプレイヤーがダメージを受けた際に実行し、赤ゲージの制御を行います。

Playerクラスのダメージ処理については以下の記事をご参照下さい。

【開発メモ】被ダメージ処理とノックバック・無敵時間の実装

ダメージ発生時、Player クラスの Damage() が呼ばれ、体力ゲージ (life) を減らす処理が実行されます。
その life を減少させる処理の前に iTween.ValueTo() を実行します。

以下は Player クラスの Damage() 関数の一部です。
予め GameUIManager の参照を取得しておき、iTween() をマネージャに対して実行します。

iTween.ValueTo(gameUIMnaager.gameObject, iTween.Hash(
    "from", life,
    "to", life - damagePower,
    "time", UIConstants.GAGE_CHANGE_SPEED,
    "easetype", iTween.EaseType.linear,
    "onupdate", "downLifeRedGage",
    "onupdatetarget", gameUIMnaager.gameObject
));

「iTweenを実行するオブジェクト」の指定には注意が必要です。
gameUIMnaagerスクリプトが付与されたオブジェクトを指定します。
このあたりの指定を間違えても iTween からはエラーも何もでないので、間違えた場合に分かりにくいです。

合わせて GameUIManager クラスに以下の関数を追加します。
プレイヤーのダメージに合わせて、iTween の ValueTo で赤ゲージの fillAmount を変化させます。

public void downLifeRedGage(float damagePower) {
    lifeRedGage.fillAmount = damagePower / player.maxLife;
}

上手くいけば下のGIFアニメーションのような感じになると思われます。

20160508_1

背景とフレームに関しては特にやることはありません。
ゲージに合わせて位置を調節したり、リサイズしたりする程度でしょうか。

まとめ

そんなわけで、すごく質素ですが体力ゲージを作成しました!
画面に表示するゲージとしては体力ゲージの他にあと1~2つ予定していますが、それはまた追々実装します!

【開発メモ】敵キャラクターの行動ロジック修正 攻撃編

というわけで、今回は敵キャラクターの攻撃ロジックの修正を行います!
前回から間が空いてしまいましたが、移動ロジックを簡単に作ったので、その続きになります。

【開発メモ】敵キャラクターの行動ロジック修正 移動編

shot2ss20160415224147751

敵キャラクターの種類によって攻撃方法は異なりますが、今回はオーソドックスに「近接攻撃」と「遠距離攻撃」の2つを持ったキャラを想定します。
前回までの過程で、プレイヤーを発見するとその方向へ移動する制御まではできているので、プレイヤーとの距離に応じてどちらかを選択するようにします。

攻撃方法の判定

プレイヤーを発見した敵は移動を行い、キャラクター毎に持っている攻撃手段の射程内に入った際に攻撃を行います。
「プレイヤーが射程内にいるか」はレイキャストで判定します。
攻撃手段として2つ用意されているので、それぞれでレイキャストを行います。

Raycastのイメージは以下の矢印のような感じで、赤が近距離、青が遠距離の攻撃です。
プレイヤーキャラの判定(黄色枠)に当たった場合、その攻撃を実行します。

shot2ss20160415224814713

しかし、単純にRaycast()を行うだけでは他のオブジェクトに当たって消えてしまい、プレイヤーキャラにはほとんど当たってくれません。

対処法として以下の2つが思い付きました。
・RaycastAll()を使い、当たったオブジェクトからプレイヤーを判定する
・レイヤーマスクを使い、Raycastがプレイヤーにしか当たらないようにする

効率的にはレイヤーマスクでRaycastの判定対象を絞ったほうがよさそうです。
が、個人的にレイヤーマスクは苦手なので、素直にRaycastAll()の結果をループして判定する方法で実装しました。

以下はEnemyクラスのUpdate()の一部です。
前回やった索敵・移動部分は省略してあります。

【Enemyクラス】

protected virtual void Update () {
    if (animator && canAction) {
        stateInfo = animator.GetCurrentAnimatorStateInfo(0);

        if (stateInfo.IsTag("Attack")) {
            isAttack = true;
        } else {
            isAttack = false;
        }

        // アクション範囲内にプレイヤーがいる場合
        if (enemyActionRange.isDetectPlayer) {
            actionInterval += 1f;

            // 攻撃アクション
            if (defaultActionInterval < actionInterval && !isAttack) {
                doAttackAction();
            }

        } else {
            animator.SetBool("Move", false);
        }
    }
}

【各敵キャラクター用クラス】

protected override void doAttackAction() {
    Ray attackRay = new Ray(transform.position, transform.forward);

    // 攻撃射程内にプレイヤーがいるか判定
    if (ObjectUtility.judgeRaycastHitsTag(Physics.RaycastAll(attackRay, attackRange[0]), "Player")) {
        isAttack = true;
        motionChange("Attack1", true, true);

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

【ObjectUtilityクラス】

  
using UnityEngine;
using System.Collections;

public static class ObjectUtility {
    public static bool judgeRaycastHitsTag(RaycastHit[] hits, string tag) {
        foreach (RaycastHit hit in hits) {
            if (TagUtility.getParentTagName(hit.collider.gameObject) == tag) {
                return true;
            }
        }

        return false;
    }
}

索敵範囲は「EnemyActionRange」というクラスを付けた判定で行っています。
プレイヤーを発見した場合、isDetectPlayerがtrueになります。

doAttackAction()で攻撃射程内かの判定と、実際の攻撃処理を行います。
敵キャラクターごとに内容が異なるため、各敵キャラクター用クラスでEnemyクラスのdoAttackAction()をオーバーライドして実装します。

まずRaycast用のRayを作成します。
今回は単純に「現在位置」から「前方への位置」を指定しています。
ここで作成しなくても、RaycastAll()のオーバーロードでoriginとdistanceを指定できるので、直接指定してしまってもOKです。

次に攻撃射程の判定です。
プログラムソース上で上から順番に評価されていくので、優先させたい攻撃アクションの評価ほど上に記載します。
上の例では、
・attackRange[0] = 10 (近接攻撃の射程)
・attackRange[1] = 30 (遠距離攻撃の射程)
と設定しているので、まず近接攻撃可能な距離にいるか判定され、次に遠距離攻撃が可能かが判定されます。

ObjectUtility.judgeRaycastHitsTag()は自作した静的なメソッドで、RaycastHitの配列と文字列を渡すことで、RaycastAll()の結果から「特定のタグのオブジェクトに当たったか」を判定します。
処理自体はforeachで回して判定しているだけですが、使う機会が多そうなのでユーティリティ化しておきます。

プレイヤーが攻撃射程内にいる場合はモーションを切り替え、攻撃判定を生成します。
攻撃判定については割愛しますが、以下の記事が参考になると思われます。

【再編集】Unity開発メモまとめ 「攻撃処理系」

攻撃間隔とリセット処理

攻撃を行った後、一定時間は移動・攻撃を行わないようにします。
一定時間後に再度プレイヤーを索敵→移動・攻撃を行います。

このあたりを自重せずに実装すると、「1フレーム毎に遠距離攻撃を連射」という凶悪な敵ができてしまったりするので、ゲーム的な体裁を保つ意味でも重要な間です。
特殊な敵やボスならともかく、雑魚的が連続で行動してくる必要もありません。
アクションゲーム故、敵が複数同時にいることも多いので尚更ですね。

上で載せたソースで言うと、「actionInterval」という変数で攻撃間隔を制御します。
1フレーム経過ごとにactionIntervalを加算しています。
合わせて「defaultActionInterval」というフィールドを用意しておき、attackIntervalがdefaultActionInterval以上になった場合のみdoAttackAction()を実行します。
攻撃実行後はattackIntervalを0にリセットし、再度defaultActionInterval以上になるまで攻撃は行いません。

以下はdefaultActionIntervalに120(=2秒)を指定した場合の挙動です。

20160415_1

1敵キャラクターとしてみると妥当な攻撃間隔でしょうか。
「短めの間隔で小刻みに攻撃」「長めの間隔で強力な攻撃」といった敵も作れそうです。

まとめ

そんなわけで、敵キャラクターの攻撃ロジックを考えてみました。
前回の移動ロジックと合わせて、最低限「敵」の動きになったと思われます!

実装していくと無駄な処理や「これもっとスマートにできそう」といった箇所が目についてきます。
ゲームの形にすらなっていない今はともかく、ある程度作ってきたらパフォーマンスも気にする必要がありそうです。

【開発メモ】アクション「空中攻撃」の実装

というわけで、今回はタイトル通り「空中攻撃」を作ってみます!

shot2ss20160321215002845

至って普通に空中で攻撃を行うものです。
単純ながら、ジャンプ可能なアクションゲームではほぼ必須アクションです。
むしろ「何故今までなかったのか」というレベルの話でもあります。

当初は単発攻撃の予定でしたが、「空中でも連続攻撃したい!」となったため、地上攻撃と同じように連続入力で派生するようにします。
アクション性の高いステージほど空中にいる時間は長くなるので、モーション自体の使いやすさも重要なところです。

スクリプト作成

まずはBlenderでアニメーションを作ります。
気合い入れて作る元気はないので、とりあえずマリンパにそれっぽく剣を振ってもらいます。
Unityへインポート後、AnimatorControllerに登録します。

空中攻撃の条件は「空中にいる時に攻撃ボタン」です。
PlayerクラスでCharacterMotorのisGrounded()から判定しても問題なさそうですが、「ダッシュ中は発動しない」等の条件も判定することを考えると、ジャンプ系のステートからスクリプトで遷移させる方がよさそうです。

そんなわけで、ボタン押下時にAnimator.Play()で空中攻撃に飛ばしてしまいます。
以下はボタン押下時にステート遷移を行う「SwitchTriggerByKey」クラスです。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {

    public class SwitchTriggerByKey : StateMachineBehaviour {
        // 入力するキー(ProjectSettings)
        [SerializeField]
        protected string inputKey;

        [SerializeField]
        protected string parameterName;

        // Animator.Playを用いて遷移
        [SerializeField]
        protected bool useAnimatorPlay;

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

        private void switchTrigger(Animator animator) {
            if (useAnimatorPlay) {
                animator.Play(parameterName);
            }
            animator.SetTrigger(parameterName);
        }
    }
}

inputKeyに(InputManagerの)攻撃ボタン名、parameterNameにAerialAttack1~3を設定します。
攻撃モーションが終わるまで遷移しないので、useAnimatorPlayにはチェックを入れないようにします。

この記事的には空中攻撃実装のためですが、実際は他のステートでも使えます。
自分は「待機中に攻撃ボタンで~」「移動中にダッシュボタンで~」など、いろいろなステートに付けていたりします。
条件として「ボタンが離された時」を入れておくと、チャージ攻撃などの溜めが必要な入力の場合でも対応できます。
その場合は、Triggerではなくfloat等で判断した方が良いかもしれないです。

shot2ss20160321220859023

空中攻撃2~3にも同様にスクリプトを追加し、遷移条件を設定します。
基本的にはExitTimeにチェックを入れ、インスペクターで遷移タイミングを設定していい感じに繋ぎます。

攻撃判定の生成に関しては過去記事でそこそこ話題にしているので割愛します。
以下が参考になるかもしれません。

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

攻撃ヒット時の浮遊状態の実装

連続入力で(マリンパの場合は)3段目まで出せるわけですが、地上と違ってジャンプ中だったり落下中だったりするため、そのままではフルヒットさせるのは困難です。
かと言って攻撃する度に浮くようにした場合、空中で素振りしまくって延々と飛べるのも問題です。

なので、「攻撃がヒットした時」に少しだけ浮くようにします。
敵に当たればそのまま連続入力でフルヒット・・・というのが理想ですね。

ステート側のスクリプトでやるのは効率が悪いので、攻撃判定のOnTriggerEnter()で判定後、Playerクラスの関数を呼ぶようにします。

public void callAttackHit(PlayerHit playerHit) {
    if (stateInfo.IsTag("Attack")) {
        charMotor.movement.velocity.y = 20;
    }
}

CharacterMotor.movement.velocityはキャラクターの速度です。
Y軸を一時的に20に設定することで、キャラクターが少しだけ浮きます。
やや強引な方法ですが一番簡単だったのでこれでいきます!

まとめ

ということで、空中攻撃を実装してみました!
最終的にはこんな感じに。

20160321

使う機会が多いアクションなので、余裕があればもっと改良していきたいところです。

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

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

20160313

所謂「壁ジャンプ」「壁キック」と言われるものです!
壁を蹴ってジャンプし、高い足場へも登れるようにします。

実は旧ブログでこっそりと実装していたりしましたが、実用性のない微妙なものだったので、ちょっと作り変えてみました!
両側に壁がある場合に連続で壁ジャンプできるようにしたのが大きな変更です。

基本的な仕様

完全な2Dアクションならともかく、3Dアクションでどこでも壁蹴り出来るようにするのは厳しいです。
壁との接触判定が難しい上、意図しない場面で壁ジャンプが暴発する可能性もあります。

ということで、任天堂の某SFアクションと同じように「壁ジャンプ専用の壁」を用意します。
その壁に接触中にジャンプボタンを押した場合に壁ジャンプを行うようにします。

モーションはダッシュジャンプ時と一緒で、くるくる回転させます。
専用モーションを作るのが面倒なので、とりあえず回らせれば見栄えがよくなるためです!
壁ジャンプ実行前にAnimator.Play()で切り替えてしまいます。

壁ジャンプ専用オブジェクトとスクリプトの作成

まず「壁ジャンプ専用の壁」を作ります。
Cubeを生成し、適当にそれっぽいマテリアルを作って適用します。

shot2ss20160313142132946

判定をCollisionで行うのは厳しいため、壁本体の判定とは別でTrigger用の判定を付けておきます。
Triggerの判定を壁ジャンプが可能な方向に少しずらしておきます。

次にスクリプトを作成します。
以下は壁用オブジェクトに設定する「ClimbWall」クラスです。

【ClimbWallクラス】

using UnityEngine;
using System.Collections;

public class ClimbWall : MonoBehaviour {

    public Vector3 climbVect;

    private CharacterMotor charMotor;

    protected void OnTriggerStay(Collider c) {
        if (c.gameObject.tag == "Player") {
            if (Input.GetButtonDown("Jump")) {
                charMotor = c.gameObject.GetComponent<CharacterMotor>();

                if (!charMotor.IsGrounded()) {
                    c.GetComponent<Animator>().Play("DashJump");

                    charMotor.movement.velocity.y = 0;
                    charMotor.jumping.enabled = false;
                    iTween.Stop(c.gameObject);

                    c.transform.LookAt(c.transform.position + new Vector3(climbVect.x, 0f, climbVect.z));

                    iTween.ValueTo(c.gameObject, iTween.Hash(
                        "from", climbVect,
                        "to", Vector3.zero,
                        "time", 0.5f,
                        "delay", 0.1f,
                        "onupdate", "Thrust",
                        "onupdatetarget", c.gameObject,
                        "oncomplete", "resetJump",
                        "oncompletetarget", gameObject
                    ));
                }
            }
        }
    }

    private void resetJump() {
        charMotor.jumping.enabled = true;
    }
}

「移動系ならiTween.MoveToでOK」と思ったのですが、今回はValueToを使用しています。
理由は単純で、iTweenのMove系は接触判定を突き抜ける可能性が非常に高いためです。
恐らくpositionを直接変化させているためだと思われますが・・・。
CharacterController的には CharacterController.Move() を使う必要があるので、仕方なくPlayerクラスにMove専用の関数を設定し、そこで一定時間Moveを呼んでもらうことにします。

【Playerクラス(一部)】

public abstract class Player : MonoBehaviour {
    protected void characterMove( Vector3 moveVect) {
        charControl.Move(moveVect * Time .deltaTime * 100);
    }
}

壁ジャンプする方向は「climbVect」としてフィールドに持たせ、インスペクターから設定します。
当初はプレイヤーとオブジェクトの方向から自動的に算出しようとしましたが、2点間のベクトルに斜め方向も加わってしまい、想定した動きにならなかったので挫折。
ステージに配置する際のRotationを、なるべくカメラに対して直角になるよう調節する必要があります。
一応climbVectにY軸のみを設定することで真垂直に壁キックができるというメリットはあります。
また、CharacterControllerのジャンプ機能が働いていると大ジャンプになってしまうことがあるので、iTweenの実行中は無効化しておきます。

iTweenのonupdateやoncompleteのターゲットには注意が必要です。
ClimbWallクラスで行うのか、Playerクラスで行うのかを考えると分かりやすいです。
勿論指定を誤ると上手く動きません。
そもそも移動処理をClimbWallクラスで行うのが間違っている気がしますが、そのうちいい感じにします!

まとめ

ということで「壁ジャンプ」を実装してみました!
やや挙動がおかしい上、たまに正常に動作しなかったりしますが、とりあえずこれがあることを前提にステージを作っていきます。