ゴマちゃんフロンティア

気まぐれと勢いで作るUnityゲーム開発日記です

【開発メモ】被ダメージ処理とダウン状態の実装

time 2016/01/18

というわけで、2016年初雪な山梨です。
電車で行ける範囲が狭い山梨は車が命ですが、メインとなる道路や雪や事故で通行止めになるとかなり厳しいことになります。

今回はプレイヤーの被ダメージ処理の続きになります!
前回の記事はこちら

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

今回は主に攻撃による吹き飛ばし&ダウン処理を実装してみます!
が、前回までの仕様では困難だったので、ダメージ処理の仕様変更を先に行います。
日に日に仕様が複雑になっている気がしますが、深く考えないようにします!

ダメージ処理用のインターフェース

ダメージ処理を行うdamage()はインターフェースで定義されており、その引数は以下のような感じです。

public void damage(float damage, Vecotor3 force) {

}

powerに攻撃力、forceに吹き飛ばす力を指定します。
前回までの実装なら問題ありませんでしたが、今回吹き飛ばしを実装するにあたり、仰け反り時間や吹き飛ばすスピードもdamage()に渡したくなったわけです。
引数やパラメータを増やす度にdamage()を呼んでいる箇所を全てリファクタするなんてやりたくないですね。

ということで、「ダメージを発生させるもの」に共通するインターフェースを作成し、そのインターフェースにパラメータのゲッタ・セッタを定義します。
以下が作成した「IDamageGenerator」インターフェースです。

【IDamageGeneratorインターフェース】

using UnityEngine;
using System.Collections;

public interface IDamageGenerator {
    /// <summary>
    /// ダメージの攻撃力を取得する
    /// </summary>
    float getPower();

    /// <summary>
    /// ダメージ対象を吹き飛ばす方向を取得する
    /// </summary>
    Vector3 getForce();

    /// <summary>
    /// ダメージ対象を吹き飛ばす速度を取得する
    /// </summary>
    float getForceSpeed();
}

ダメージ発生源が攻撃判定であれば、全攻撃判定のスーパークラスとなっているHitの参照を渡せばよかったのですが、ステージ上のトラップ・障害物から受けることもあるので、インターフェース型を渡すようにします。
そもそも「仕様なんて作る前から決めとけ」という話ですが、個人且つ趣味で作っている関係上、後々からあれこれ入れたくなることは多々あるので、パフォーマンスよりも拡張性や柔軟性を重視していきます!

ダメージを発生させるオブジェクトにIDamageGeneratorインターフェースの関数を実装します。
手始めということで、シンプルな障害物で試してみます。
以下は障害物に付与するTrapクラスです。

【Trapクラス】

using UnityEngine;
using System.Collections;

public class Trap : MonoBehaviour, IDamageGenerator {

    public float power;
    public Vector3 force;
    public float forceSpeed;

    private void OnTriggerEnter(Collider c) {
        if (c.gameObject.tag == "Player") {
            c.GetComponent<Player>().damage(this);
        }
        if (c.gameObject.tag == "Enemy") {
            c.GetComponent<Enemy>().damage(this);
        }
    }

    public float getPower() {
        return this.power;
    }

    public Vector3 getForce() {
        return this.force;
    }

    public float getForceSpeed() {
        return this.forceSpeed;
    }
}

合わせてPlayer側のdamage()も修正します。
引数としてIDamageGenerator型を受け取り、各パラメータはインターフェースに定義されている関数を通して取得します。
今更ですが、インターフェース型にすると呼び出し元の余計なパラメータや関数を参照させなくするメリットがあります。
(1人で開発しているのでほとんど意味はありませんが・・・)

【Playerクラス】

public void damage(IDamageGenerator damageGenerator){
    power = damageGenerator.getPower();
    force = damageGenerator.getForce();
    forceSpeed = damageGenerator.getForceSpeed();
}

パラメータの取得自体は単純です。
今後パラメータが増えても、インターフェースの関数を増やして取得させることで、関数呼び出し部分を修正する必要がなくなります。

吹き飛ばしとダウン処理

吹き飛ばすベクトルとスピードはIDamageGeneratorの各メソッドから取得します。
スピードはともかく、吹き飛ばすベクトルはオブジェクトによって異なります。
上で実装したTrapクラスであればインスペクターからXYZ値を入力したVector3で十分ですが、キャラクターの場合は攻撃したキャラの正面方向に吹っ飛んで欲しいものです。

ということで、getForce()の内容はオブジェクト毎に変わります。
敵用の攻撃判定であるEnemyHitクラスの場合、
「(敵キャラの正面ベクトル × 吹き飛ばす量) + 上方向へ吹き飛ばす量」
という感じ。
これをgetForce()で実装します。

【EnemyHitクラス】

using UnityEngine;
using System.Collections;

public class EnemyHit : MonoBehaviour, IDamageGenerator {

    public float power, time, forceSpeed;
    public float depthForce, upForce;

    public GameObject damageEffect;
    public Enemy enemy {get; set;}

    protected void OnTriggerEnter(Collider c){
        if(c.gameObject.tag == "Player"){
            c.GetComponent<Player>().damage(this);
            Instantiate(damageEffect, transform.position, Quaternion.identity);
        }
    }

    public float getPower() {
        return this.power;
    }

    public Vector3 getForce() {
        return (enemy.transform.forward * depthForce) + new Vector3(0f, upForce, 0f);
    }

    public float getForceSpeed() {
        return this.forceSpeed;
    }
}

次にPlayerクラスのdamage()の処理を修正します。
forceが零ベクトル(=xyz全てが0)ではない場合のみ、プレイヤーキャラを吹き飛ばす処理を行います。
敵キャラクターの攻撃の場合、depthForceとupForceが両方0の場合になります。
零ベクトルの場合は普通にノックバックを発生させます。これは前回実装している部分なので割愛します。

【Playerクラス】

public void damage(IDamageGenerator damageGenerator){
    // 無敵判定
    if(!isInvincible){
        life -= damageGenerator.getPower();

        // ダメージ中の移動を制限する
        switchCharacterMovement(false);

        isInvincible = true;

        // forceが指定されている場合はダウン処理
        Vector3 force = damageGenerator.getForce();
        if (force != Vector3.zero) {
            // ダウンモーション
            motionChange("Down", true, true);

            // テクスチャ切り替え
            faceMaterial.SetTexture("_MainTex", faceTex[1]);

            // 吹き飛ばし処理
            iTween.MoveTo(gameObject, iTween.Hash(
                "position", transform.position + force,
                "speed", damageGenerator.getForceSpeed(),
                "easetype", iTween.EaseType.easeOutCubic
            ));
        } else {
            // ダメージモーション
            motionChange("Damage", true, true);

            // テクスチャ切り替え
            faceMaterial.SetTexture("_MainTex", faceTex[1]);
            StartCoroutine(damageReset(GameConstants.DAMAGE_RESET_TIME));

            // ノックバック
            iTween.MoveTo(gameObject, iTween.Hash(
                "position", transform.position - (transform.forward * 10f),
                "time", GameConstants.DAMAGE_RESET_TIME,
                "easetype", iTween.EaseType.linear,
                "oncomplete", "onInvincibleState",
                "oncompletetarget", gameObject,
                "oncompleteparams", GameConstants.DAMAGE_INVINCIBLE_TIME
            ));
        }
    }
}

最後にアニメーションの遷移部分を考えます。
吹き飛ばされたプレイヤーキャラはダウン状態にし、地面に着地した際に起き上がるようにします。
受身とかそういう要素はとりあえず考えずに実装します。

AnimatorControllerにダメージ処理用のサブステートマシンを作り、制御用のパラメータとスクリプトを作成します。
基本的な遷移は「ダウン→地面に着地→起き上がり」です。
ダウンステートと起き上がりステートにそれぞれスクリプトを付けます。

【Downクラス】

public class Down : StateMachineBehaviour {

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        if (1f < stateInfo.normalizedTime) {
            Player player = animator.gameObject.GetComponent<Player>();

            if (player.getCharacterMotor().IsGrounded()) {
                animator.SetTrigger("GetUp");
            }
        }
    }
}

【GetUpクラス】

public class GetUp : StateMachineBehaviour {

    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        animator.gameObject.SendMessage("damageReset", 0f);    
        animator.gameObject.SendMessage("onInvincibleState", GameConstants.DOWN_INVINCIBLE_TIME);
    }
}

DownクラスのOnStateUpdate()で「ダウン中かつ地面に接地しているか」を判定したかったのですが、1フレーム毎にGetComponentするという大変効率の悪いやり方になってしまっています。
スクリプト内でオブジェクトや変数の保持ができれば良いのですが・・・。
StateMachineBehaviourのベストな使い方はまだまだ分からないところですorz

AnimatorStateInfoのnormalizedTimeで「モーションがどのくらいまで再生されているか」の割合が取得できます。
この場合は1(=100%)なので、ダウンモーションが完全に再生し終わるまで判定しなくなります。

起き上がり処理のOnStateExit()で無敵時間の処理と移動制御、テクスチャのリセットを行います。
このあたりも前回とほぼ同じです。

まとめ

前に作ったビードラの攻撃にマリンパを突撃させます。
以下はdepthForceが20、upForceが10、forceSpeedが30の場合の挙動です。

20160110

横方向ならともかく、縦方向に飛ばした際の挙動がやや不自然です。
山なりに飛ばすだけならモーションパスで吹っ飛ぶ軌道をガチガチに固めれば出来ますが、実際の着地ポイントは地形に左右されるので微妙なところです。
今でも強く吹き飛ばすと判定をすり抜けてしまったりするので・・・。
ダウンから起き上がり~無敵エフェクトまでは問題なさそうですが、若干起き上がるのが遅い気がします。

ということで、
・ダメージを与えるオブジェクトにIDamageGeneratorインターフェース実装
・damage()内でインターフェースの関数を通じて各種パラメータ取得
・吹き飛ばし処理はiTween.MoveTo()を使用
・吹き飛ばされたプレイヤーキャラはダウンし、着地後に起き上がる

改善の余地は多分にありますが、ひとまず実装してみました!
次は敵キャラクターのダメージ処理の実装の話になると思われます!

今日のイラスト

新年初ランパです。
スタフィーシリーズも最新作から8年も経っていると考えると早いものです。
ランパももう「一昔前のマイナーキャラ」な感じでしょうか。

「初心者は影の濃い部分から深めに塗ると良い」と何かの本で見たので、影用レイヤーを分けて濃く付けてみました!
また塗る際のツールはエアブラシを使い、影の輪郭がぼやけるようにしています。
背景が適当なこと以外はそこそこ上手く描けている・・・気がします。

ranpa28

スポンサーリンク

down

コメントする



PHP

Twitter

イラスト

日記

未分類

ツイッター