ゴマちゃんフロンティア

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

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

time 2015/09/24

※本記事は旧ブログの記事を再編集し、再度投稿したものです。
 一部内容に差異がある可能性がありますので、予めご了承ください。

[2016/04/10]
記事内容を加筆・修正しました。

攻撃判定の作成

攻撃判定はアクションゲームにおいて重要なものです。
ちょっと考えるだけでも、「判定の大きさ」「持続時間」「攻撃力」といった要素があります。

Unity的には判定の大きさはCollider系コンポーネントで扱います。
大雑把な判定でよければBoxCollider、よりリアルに当たったか判定したい場合はMeshColliderが使えそうですね。
処理的にはSphereColliderが一番高速らしいです。
判定として扱うので、isTriggerにチェックを入れます。

後々使うRigidBodyコンポーネントも付けておき、isKinematicにチェックを入れておきます。
これにチェックがないと何かの拍子に吹っ飛んでしまったります。

攻撃力や持続時間をUnity側のコンポーネントに任せるのは難しいので、攻撃判定用にスクリプトを1つ作ってしまいます。
以下は攻撃判定用の「PlayerHit」クラスです。

using UnityEngine;
using System.Collections;

[RequireComponent (typeof (Rigidbody))]
public class PlayerHit : MonoBehaviour {

    public float power, time;
    public GameObject damageEffect;
    public Player pc;

    protected override void Start () {
        if (time != 0) {
            StartCoroutine(DestroyHit());
        }
    }

    protected virtual IEnumerator DestroyHit(){
        yield return new WaitForSeconds(time);
        Destroy(gameObject);
    }

    protected virtual void OnTriggerEnter(Collider c){
        if (TagUtility.getParentTagName(character.getGameObject().tag) == "Player") {
            if (TagUtility.getParentTagName(c.gameObject) == "Enemy") {
                c.GetComponent<Enemy>().damage(power);
                Instantiate(damageEffect, transform.position, Quaternion.identity);
            }
        }
    }
}

フィールドに攻撃判定に必要なパラメータを設定しておきます。
実際にぶつかった時の処理はOnTriggerEnter()で行います。
OnTriggerEnterの引数(Collider型)から、タグが「Enemy」のオブジェクトに当たった場合のみ処理を行います。
Enemyクラスにdamage()を実装しておき、GetComponent()して呼び出します。
damage()内でEnemyクラスのパラメータ(体力など)を変化させることで、敵キャラクターにダメージを与える処理を作ることができます。

・・・ということを踏まえ、攻撃判定用のオブジェクトを作ります。
シーン上にCubeを追加し、上記のスクリプトを設定します。
実際の攻撃判定は見えるものではないので、MeshRendererのチェックは外して非アクティブにしておくか、コンポーネントを削除します。

攻撃する度に生成・削除されるものなので、プレハブ化しておきましょう。
攻撃毎に異なる攻撃力・範囲を設定したい場合、プレハブを複製(Ctrl+D)して複数個作っておきます。

攻撃判定の生成

↑で作った攻撃判定を実際し生成し、判定として機能するようにします。
が、単に生成するだけではキャラクターの動きについてきてくれません。

例えば剣に攻撃判定を持たせ、振りに合わせて追従させたい場合、Armatureとしてキャラクターの剣にboneを持たせ、そのboneの子オブジェクトに設定する必要があります。
キャラクター本体のMeshの子オブジェクトにしても追従してくれないので注意します。

以下は攻撃判定を生成するattackInstantiate()です。
Playerクラス(プレイヤーキャラに付けているスクリプト)内のメソッドとして実装します。

 
protected void attackInstantiate(GameObject hitObject, GameObject hitOffset) {
    GameObject hit;

    hit = Instantiate(hitObject, hitOffset.transform.position, transform.rotation) as GameObject;
    hit.transform.parent = hitOffset.transform;
    hit.GetComponent<PlayerHit>().pc = this;
}

Playerクラスに以下のフィールドを追加しておきます。

・hitObject
先程作った攻撃判定のプレハブを設定します。

・hitOffset
攻撃判定を生成する位置で、剣に設定したboneの下に存在する空のGameObjectです。
boneだけでは攻撃判定の位置を調整しにくいので、この空オブジェクトの位置に生成させることで微調整できるようにします。

shot2ss20160408204748536

上はマリンパの剣攻撃のhitOffsetです。
(見やすいようにCubeをレンダリングしています)
剣の中腹部分に空オブジェクトがくるようにし、ここから攻撃判定を生成させています。

Instantiate()の返り値で生成したもの(=攻撃判定)をGameObject型にキャストして取得し、Transform.parentで親オブジェクトをhitOffsetに設定します。
これで位置や回転情報がhitOffset(の親のbone)から継承され、剣の振りにあわせて動いてくれます。

攻撃判定のスクリプトにフィールドを持たせ、Player自身の参照を渡しておくと便利です。
「攻撃が当たったらゲージが増える」などの制御も、pcという変数を通してPlayerクラスのフィールド/メソッドを呼び出すことで実現できます。
その場合、PlayerHitクラスにフィールドとして増加量を追加・設定しておくと、攻撃判定ごとに増加量を設定できたりします。

あとは実際にattackInstantiate()を呼ぶだけです。
シンプルに行くのであれば、PlayerクラスのUpdate内で「攻撃ボタンが押下されたか」を判定し、それに合わせて生成するのが簡単です。
合わせて攻撃モーションに切り替えるのを忘れないようにします。

if (Input.GetButtonDown("Attack1")) {
    animator = this.GetComponent<Animator>();
    animator.Play("Attack1");
    attackInstantiate(hitObject[0], hitOffset[0]);
}

hitObjectとOffsetは配列で複数指定できます。
「通常攻撃の1段目」と「3段目」で威力が異なったりする場合、インスペクターから対応する攻撃判定を設定し、添え字を切り替えてあげます。
その場合、animatorから「どのモーションを実行しているか」を取得し分岐させましょう。

今はanimatorのGetComponent()をif文の中でやっていますが、本来はStart()あたりでやって参照を取っておくほうが良いと思われます。
animator.Play()の引数はAnimatorControllerのステート名に合わせましょう。

AnimatorControllerを使っているのであれば、StateMachineBehaviourのEnter系を使って生成させると、アニメーションと攻撃判定生成のタイミングが合って確実です。
長くなるのでここでは割愛させて頂きます。

20160408_2

剣を振ったときに攻撃判定が付いてくるのが分かります。
(見やすいように攻撃判定をレンダリングしています)

遠距離攻撃の生成

判定生成の仕組みは同じですが、生成後の判定をhitOffsetから切り離し、弾として独立させる必要があります。
また、進む方向と速度を定義し、移動させる必要があります。

 using UnityEngine;
using System.Collections;

public class BulletHit : PlayerHit {

    public float bulletSpeed;

    protected Vector3 forward;
    private Rigidbody rb;

    protected override void Start(){
        base.Start();

        forward = hitManager.transform.forward;

        rb = this.GetComponent<Rigidbody>();
    }

    protected override void Update(){
        base.Update();

        rb.velocity = forward * bulletSpeed;
    }

    protected override void OnTriggerEnter(Collider c){
        base.OnTriggerEnter(c);

        if(TagUtility.getParentTagName(c.gameObject) == "Enemy"){
            Destroy(gameObject);
        }
    }
}

フィールドとして「弾速度」を表すbulletSpeedを追加します。
単に前に打ち出すだけであれば、transform.forwardで前方方向のベクトルが取得できるので、これにbulletSpeedを掛けてあげればOKです。
あとはRigidBodyのvelocityに代入すればそのベクトルへ進みます。

判定生成時に通常の攻撃判定のようにhitOffsetの子オブジェクトに設定してしまうと、発射後もプレイヤーの動きに連動してしまうため、parentの設定はしないようにします。

ホーミング弾

遠距離攻撃の応用で、敵を自動的に追尾する弾です。
弾発射前にSphereCast()を行い、前方に敵がいたらその敵に対してホーミングするようにします。
SphereCast()時に余計なもの(敵以外)にぶつからないようにするためには、以下の2通りの方法があります。

・レイヤーを「Ignore Raycast」にする
レイヤーをIgnore Raycastに設定すると、そのオブジェクトはレイキャストを無視するようになります。
これをTerrainなどに適用すれば、床に当たってしまうことはなくなります。

しかし、ホーミングさせたいのは敵だけです。
だからといって、他のプレイヤーや破壊可能なものにもIgnore Raycastを設定するのはよろしくないです。
敵側もレイキャストを行っているのでなおさらですね。

・新規レイヤーを作り、そのレイヤーとしか衝突しないようにする
例えば Enemy というレイヤーを作り、そのレイヤーに設定したオブジェクトとしか衝突しないようにします。
敵だけをキャストしたいなら、この方法が一番かと思います。

敵のプレハブのレイヤーをEnemyに設定し、Spherecast()の第5引数でレイヤーマスクを設定します。
弾の攻撃判定のStart()内でSphereCast()を行います。

protected override void Start(){
    base.Start();

    angle = angle / 100f;

    if(Physics.SphereCast(transform.position, 10f, 
                            forward, out hit, 100f, 1<<8)){
        if(hit.collider.tag == "Enemy"){
            target = hit.collider.transform;
        }
    }
}

敵の方へ誘導する処理はQuaternion.Slerp()を使います。
第1引数の角度と第2引数の角度との差を、指定秒数分だけ補完した結果を返す関数です。
弾の攻撃判定のUpdate内に処理を追加します。

protected override void Update(){
    base.Update();

    if(target != null){
        transform.rotation = Quaternion.Slerp(
                         transform.rotation, 
                         Quaternion.LookRotation(
                           target.position - transform.position),
                         angle);
        rigidbody.velocity = transform.forward * bulletSpeed;
    } else {
        rigidbody.velocity = forward * bulletSpeed;
    }
}

敵が存在したらQuaternion.Slerp()を使い、徐々に向き直すことでホーミングを行います。
angleに渡す値でホーミングの度合いを調節できます。
基本的に0~1の間で設定し、ゲームや弾の性質に合わせて調整するのがよさそうです。

拡散弾

ホーミング弾と同じく遠距離攻撃の応用です。
弾1つ1つに攻撃判定を持たせるため、各々をInstantiateする形にします。
拡散弾専用の「DiffusionBullet」クラスを作り、攻撃判定となる弾に予めスクリプトを設定しておきます。

 
using UnityEngine;
using System.Collections;
 
public class DiffusionBullet : MonoBehaviour {
 
     [Range(0f, 1f)]
     public float diffusionAngle;
     public float bulletSpeed;
     
     private Vector3 forward;
     private Rigidbody rb;
 
     protected void Start () {
          rb = this.GetComponent<Rigidbody>();
 
          float angle_x = Random.Range(-diffusionAngle, diffusionAngle);
          float angle_y = Random.Range(-diffusionAngle, diffusionAngle);
          float angle_z = Random.Range(-diffusionAngle, diffusionAngle);
 
          forward = pc.transform.forward + new Vector3(angle_x, angle_y, angle_z);
          this.transform.parent = null;
     }
     
     protected void Update () {
          rb.velocity = forward * bulletSpeed;
     }
}

Start()内でforwardを設定し、進む方向をVector3型で定義します。
固定値だと全ての弾が同じ方向になってしまうので、Vector3の各軸をランダムで設定し、forwardと足し合わせます。
diffusionAngleの値を大きくすると拡散角度が広がる仕組みです。

オブジェクト自体を回転させても進行方向は変わらないことに注意が必要です。
forwardは発射時のプレイヤーの方向を参照しており、且つStart時に1回だけ定義しているためです。
発射後も方向を変えたい場合はUpdate内でforwardを変更する必要があります。

[Range]属性はフィールドの最小値~最大値の値を制御します。
インスペクター上にはスライダーが表示されるようになります。
簡単でかつ便利なので、入力する範囲が決まっているフィールドには付けてみましょう。

ヒットストップ処理

攻撃がヒットした際にコルーチンを実行します。
一瞬だけAnimatorのスピードを遅くすることでヒットストップを実現します。
Playerクラスに以下の関数を持たせ、攻撃判定からコルーチンとして呼び出します。

public IEnumerator attackHitStop(float time){
    animator.speed = 0.1f;
    yield return new WaitForSeconds(time);
    animator.speed = 1.0f;
}

実際にやってみるとこんな感じになります。
gifアニメーションだとあまり伝わらないかも・・・。

20160408_1

ちなみにTimeManagerのTimeScaleはゲーム全体の速度なので、ヒットストップの処理には適しません。

攻撃時の軸補正処理

3Dアクションということで、多少なりとも敵のいる方向に自動で振り向いて欲しかったので考えてみました。
攻撃時に「Physics.SphereCast()」を使い、敵の位置情報を取得します。
攻撃対象に振り向く処理はQuaternion.Slerp()を使用します。
Transform.LookAt()だと一瞬で振り向いてしまうので不自然になります。

PlayerクラスのUpdate()内に処理を追加します。
ステートのタグがAttackであれば敵の方に振り向くようにします。
予めコントローラから攻撃系ステートにタグを振っておきましょう。

 
if(animator){
    stateInfo = animator.GetCurrentAnimatorStateInfo(0);
 
    if (isAttack && SystemConstants.ATTACK_AUTO_ROTATE) {
        Debug.DrawRay(
            transform.position,
            transform.forward * 10f,
            new Color(255, 255, 255),
            1.5f
        );
        if (Physics.SphereCast(
            transform.position, 10f, transform.forward, out hit, 0.1f, 1 << 8)) {
            if (hit.collider.tag == "Enemy") {
                autoRotateTarget = hit.collider.transform;
            }
        }
        if (autoRotateTarget != null) {
            Vector3 to = Quaternion.LookRotation(
                autoRotateTarget.position - transform.position
            ).eulerAngles;
            to.x = 0;
            to.z = 0;
            transform.rotation = Quaternion.Slerp(
                transform.rotation,
                Quaternion.Euler(to),
                SystemConstants.ATTACK_AUTO_ROTATE_ANGLE
            );
        }
    } else {
        autoRotateTarget = null;  
    }
}

ATTACK_AUTO_ROTATEは単なる定数で、Bool型で機能の有効/無効をコントロールしています。
Debug.DrawRay()を使うとレイキャストが視覚化できるようになり、方向や距離が把握できるようになります。

スポンサーリンク

down

コメントする



ツイッター