【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クラスで行うのが間違っている気がしますが、そのうちいい感じにします!

まとめ

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

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

というわけで、前回「走り移動」を追加したので、その派生アクションとして「ダッシュジャンプ」を実装します!
走り中やダッシュ中にジャンプでより遠くまで飛べるものです。

20160205_s

モーションは回転ジャンプです!
(相変わらずgif画像を撮るのが下手ですが・・・)
現状では空中攻撃や2段ジャンプに派生できないため、着地するまで回転し続けます。
空中攻撃はともかく、ダッシュジャンプ中に2段ジャンプを出来るようにするかは考える必要があります。

モーション制御とスクリプト作成

AnimatorController上で「Run」「Dash」が既にあるので、ここからジャンプ入力でダッシュジャンプへ派生するようにします。
自分の場合、ジャンプ関連のステートを「Jumping」というサブステートマシンにまとめてあるので、ここに追加する形となります。
また、新しく「DashJump」というパラメータも作っておきます。

20160306_1

DashJumpステートにスクリプトを追加します。
OnStateEnter() と OnStateExit() にそれぞれ処理を書きます。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {

    public class DashJump : StateMachineBehaviour {

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            animator.SetBool("Run", false);
            animator.gameObject.GetComponent<Player>().getCharacterMotor().movement.maxForwardSpeed = GameConstants.DASH_MOVE_SPEED;
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            animator.ResetTrigger("DashJump");
            animator.gameObject.GetComponent<Player>().getCharacterMotor().movement.maxForwardSpeed = GameConstants.DEFAULT_MOVE_SPEED;
        }
    }
}

ジャンプボタンを押したら即反応して欲しいので、Animator.Play()を使用します。
AnimatorController上からモーションの中断を行うのはかなり難しいです。
というか自分が挑戦してまともに動いたことはありませんorz
まだまだ理解が足りていない部分ですね。

これでモーション的にはダッシュジャンプになりましたが、移動速度が通常と同じなので「ダッシュしながらジャンプ」している意味がありません。
CharacterMotor の maxForwardSpeed を変更し、移動速度を変化させます。

まとめ

・走り中かダッシュ中にジャンプでダッシュジャンプ
・OnStateEnter() と OnStateExit() で移動速度を変更

ということで、ざっくりとダッシュジャンプを実装してみました。
こういうアクションを実装すると「アクションゲーム」っぽくなりますね!
リアリティのあるモッサリ感は不要なので、大胆に作っていきたいところです。