【開発メモ】敵キャラクターとロジックの作成に関する考察 その2

というわけで、前回に引き続き、敵キャラクターとロジックに関するお話です!
出張が多かったので開発が滞っていますが、やっとロジッククラスが作成できたので記載します!

shot2ss20170205204248559

前回の記事はこちら!

【開発メモ】敵キャラクター作成とロジックに関する考察

今回は上記の記事内で考察した中の、移動ロジックと攻撃ロジックについて考えてみます。

ロジッククラス作成の準備

ロジッククラス作成の前に下準備をしておきます。
まずはロジッククラスを共通化させるためのインタフェースを定義します。

【IEnemyMoveLogic】

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IEnemyMoveLogic {
    /// <summary>
    /// 移動する速度を取得する
    /// </summary>
    Vector3 getMoveVelocity();
}

【IEnemyAttackLogic】

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IEnemyAttackLogic {

    /// <summary>
    /// 攻撃を行うか判断する
    /// </summary>
    bool attackDetermine();

    /// <summary>
    /// 攻撃判定を取得する
    /// </summary>
    GameObject getAttackHitObject();

    /// <summary>
    /// 攻撃の種類を取得する
    /// </summary>
    int getAttackType();

    /// <summary>
    /// 攻撃時に再生するモーションを取得する
    /// </summary>
    string getAttackMotion();
}

次に各ロジッククラスのスーパークラスとなる抽象クラスを作成します。
以下は大元となる「AbstractLogic」クラスです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace EnemyLogic {
    public abstract class AbstractLogic : MonoBehaviour {

        protected Enemy enemy;

        protected virtual void Start () {
            enemy = this.GetComponent<Enemy>();
        }
    }
}

更に、移動・攻撃ロジック用に AbstractLogic を継承した抽象クラスを作成します。
共通処理として、Enemy クラスにロジックを設定します。

【AbstractMoveLogic】

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace EnemyLogic.Move {
    public abstract class AbstractMoveLogic : AbstractLogic, IEnemyMoveLogic {
        public float move_speed;

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

            enemy.setMoveLogic(this);
        }

        public abstract Vector3 getMoveVelocity();
    }
}

【AbstractAttackLogic】

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace EnemyLogic.Attack {
    public abstract class AbstractAttackLogic : AbstractLogic, IEnemyAttackLogic {
        public const int CLOSE_RANGE_HIT_TYPE = 1;
        public const int RANGE_HIT_TYPE = 2;

        public GameObject attackHitObject;
        public int attack_type;
        public string attack_motion;

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

            enemy.addAttackLogic(this);
        }

        public abstract bool attackDetermine();
        public abstract GameObject getAttackHitObject();
        public abstract int getAttackType();
        public abstract string getAttackMotion();
    }
}

移動用クラスは IEnemyMoveLogic、攻撃用クラスは IEnemyAttackLogic を実装させます。
これらのインタフェースを介した実装は、「特定のロジックに依存させない」ために重要です。

ロジッククラスの作成

いきなり複雑なロジックは厳しいので、今回は簡単な移動クラスと攻撃クラスを作成してみます。

以下は移動ロジックの、「自分のいる地点から左右に往復移動」のクラスです。
(横スクロール型のアクションで使用することを想定しています)

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace EnemyLogic.Move {
    public class RoundTripMoveLogic : AbstractMoveLogic {

        // 基準点から往復するまでの距離
        public float repeat_dist;

        private Vector3 defaultPosition;

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

            defaultPosition = enemy.transform.position;
        }

        public override Vector3 getMoveVelocity() {
            // 振り向き処理
            if (repeat_dist < Mathf.Abs(enemy.transform.position.x - defaultPosition.x)) {
                Vector3 angle = new Vector3(defaultPosition.x, enemy.transform.position.y, defaultPosition.z);
                enemy.lockRotate(angle);
            }

            float forward_x = enemy.transform.forward.x * move_speed;
            Vector3 newPosition = new Vector3(forward_x, enemy.rb.velocity.y, enemy.rb.velocity.z);

            return newPosition;
        }
    }
}

ロジックに関する深い説明は割愛しますが、「往復する距離まで移動したら逆側に振り向かせる」こと、「敵キャラクターは Rigidbody で動かしているので、Rigidbody.velocity を変更する」ことに注意します。

次は攻撃ロジックの、「一定間隔で攻撃」のクラスです。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace EnemyLogic.Attack {

    public class AttackIntervalLogic : AbstractAttackLogic {

        // 攻撃を行う間隔 (フレーム)
        public int attack_interval;

        private int attack_count;

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

            enemy.addAttackLogic(this);
            attack_count = 0;
        }

        public override bool attackDetermine() {
            if (attack_interval < attack_count) {
                attack_count = 0;
                return true;
            }
            attack_count += 1;
            return false;
        }

        public override GameObject getAttackHitObject() {
            return this.attackHitObject;
        }

        public override int getAttackType() {
            return this.attack_type;
        }

        public override string getAttackMotion() {
            return this.attack_motion;
        }
    }
}

主に使用するのは attackDetermine() で、これを Enemy クラスの Update() 内で呼び出し、攻撃を行うかの判定をしています。
その他、インタフェースで定義された関数を実装していますが、ロジック上で必要なパラメータを取得するためのものです。
このあたりはゲームによって実装が異なると思います。

共通して気を付けることは、「ロジッククラスから敵キャラクターを直接操作しない」ことです。
複数のロジッククラスからあれこれ敵キャラクターを操作すると訳が分からなくなります。
ロジッククラスは条件判定後に結果を Enemy クラスへ返したり、Enemy クラスの関数を呼び出して間接的に操作したりします。

敵キャラクター共通のクラス作成

敵キャラクター共通のスーパークラス「Enemy」を作成します。
既にダメージ処理等が入ってしまっていて長いので、ロジックに関する部分のみ紹介します。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[RequireComponent (typeof (Rigidbody))]

public abstract class Enemy : MonoBehaviour, ICharacter {

    protected bool isAttack;

    // 攻撃判定の出現位置
    public GameObject attackHitOffset;
    public GameObject bulletHitOffset;

    protected Animator animator;
    protected AnimatorStateInfo stateInfo;

    public Rigidbody rb;
    protected Collider bodyCollider;

    // ロジック制御クラス
    protected IEnemyMoveLogic moveLogic;
    protected List<IEnemyAttackLogic> attackLogics;

    protected virtual void Awake() {
        // ロジック初期化
        attackLogics = new List<IEnemyAttackLogic>();
    }

    protected virtual void Start() {
        isAttack = false;

        // 各コンポーネント取得
        animator = this.GetComponent<Animator>();
        rb = this.GetComponent<Rigidbody>();
        bodyCollider = this.GetComponent<Collider>();

    }

    /// <summary>
    /// 毎フレーム実行する処理
    /// </summary>
    protected virtual void Update() {
        if (animator && canAction) {
            stateInfo = animator.GetCurrentAnimatorStateInfo(0);

            // 移動ロジック
            if (moveLogic != null && !isAttack) {
                movement(moveLogic.getMoveVelocity());
            }

            // 攻撃ロジック
            foreach (IEnemyAttackLogic logic in attackLogics) {
                if (logic.attackDetermine()) {
                    GameObject attackHitObject = logic.getAttackHitObject();
                    string attack_motion = logic.getAttackMotion();

                    switch (logic.getAttackType()) {
                        case EnemyLogic.Attack.AbstractAttackLogic.CLOSE_RANGE_HIT_TYPE:
                            // 近距離攻撃処理
                            break;
                        case EnemyLogic.Attack.AbstractAttackLogic.RANGE_HIT_TYPE:
                            // 遠距離攻撃処理
                            break;
                    }
                }
            }
        }
    }

    /// <summary>
    /// 移動処理を行う
    /// </summary>
    public void movement(Vector3 velocity) {
        rb.velocity = velocity;
    }

    /// <summary>
    /// 指定した座標へ振り向かせる
    /// </summary>
    /// <param name="angle"></param>
    public void lockRotate(Vector3 angle) {
        transform.LookAt(angle);
    }

    public void setMoveLogic(IEnemyMoveLogic moveLogic) {
        this.moveLogic = moveLogic;
    }

    public void addAttackLogic(IEnemyAttackLogic attackLogic) {
        this.attackLogics.Add(attackLogic);
    }
}

今までは Update() 内にごりごりロジックを書いていましたが、今回は各ロジッククラスを制御する命令のみに留めます。

キモとなるのは、ロジッククラスの型をインタフェースにしておくことです。
インタフェースに記載された関数の実装が約束されるため、どのロジッククラスにも依存せず共通化できること、Enemy クラスからロジッククラスを見た際に最小限の機能のみを提供するためです。

攻撃クラスのみ List<> で管理しています。
例えば「近距離攻撃と遠距離攻撃を行うキャラクター」や「2種類の近距離攻撃を持つキャラクター」を作成する際、複数のロジックを作成して対応させるためです。
また List<> ならロジックが1つでも複数あっても対応でき、特定のロジックのみを無効化しても問題なく動きます。

実際に使用する際はキャラクターごとに専用のサブクラスを作成し、実際の敵キャラクター用ゲームオブジェクトのコンポーネントとして設定します。
中身はキャラクター固有の特殊能力がある場合に実装し、特にない場合はスーパークラスの関数を呼び出すだけです。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Raidran : Enemy {

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

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

今回の敵キャラクター「レイドラン」は特に考えていないため、何も考えずに継承しているだけです。

敵キャラクターへの設定とプレハブ化

上で作った移動ロジックと攻撃ロジックを敵キャラクターのオブジェクトに設定し、その動作を確認してみます。

20170205_01

うーん・・・左右を往復するだけの雑魚っぽいにはなったでしょうか。
本ゲームにはアクションでよくある「接触被ダメージ」がないので微妙かもしれませんが・・・。

ちなみに、Unityのベストプラクティス的にはオブジェクトの種別ごとにプレハブを作るのが良いらしいです。
動的にコンポーネントを足すのもアレなので、ロジックを設定したキャラクター毎にプレハブ化します。
プレハブが多くなって管理が面倒ですが、動的にインスタンスすることを考えればこちらの方が良いです。

まとめ

そんなわけで、ロジック系クラス作成の基盤と簡単なロジックの作成をしてみました!
敵キャラクター制御が使いまわせるようになり、コンポーネント毎に付け替えできるのでいい感じです!
索敵やジャンプのロジックも作れば、更に幅広いアクションを取らせることができそうですね。

ただやっていて思うのは、かなり手探りで自分なりの設計をしているということです。
このあたりのベストプラクティスとかってあるのでしょうか。
ゲームによって異なるとは思いますが、他の方がどのように実装しているのか気になります。
そのあたりのお話をUnity道場等でやっていたらぜひ参加したいところです。

【Unity】横スクロールアクションのカメラワーク制御について

というわけで、今更ながらあけましておめでとうございます!
今年も「ゴマちゃんフロンティア」をよろしくお願いします!

例年であれば「あけおめ」専用の記事を書いているのですが、毎年振り返ってもロクなこと語っていないので、今年はやめておこうと思います。
「ゲーム開発の目標!」なんて守れたことはほとんどないので(ぁ

shot2ss20170106203043668

新年1発目は「横スクロールアクションにおけるカメラワークの制御」になります!
ゲームにおいてカメラワークは超重要な要素ですが、自分なりに作った仕組みはどれも欠陥品で、使い物になりませんでした。
他サイト様を参考にしつつ、やっとそれなりのものが出来たのでメモしておきます。

カメラの描画範囲に応じた制御

カメラワークを制御する目的の1つに、「カメラの描画範囲がステージの範囲を超えないこと」があります。
・・・何を言っているかよく分からないと思うので、以下のgifアニメーションをご参照下さい。

20170106_01

20170106_02

上はカメラとプレイヤー位置(用のオブジェクト)を親子関係でくっ付けているだけなので、カメラ移動は完全にプレイヤーの移動に依存します。例えば、「ステージが横スクロールのみで縦スクロールさせたくない!」という場合でも、プレイヤーがジャンプすればカメラが上下に動いてしまう状態です。
また、カメラから見たプレイヤーの位置は常に画面の中央です。ステージ端まで行ってもプレイヤーが中央にいるため、画面の半分近くを壁(ステージ外の空間)が占領してしまいます。

下は本記事の趣旨である「カメラ移動範囲」の制御を取り入れたものです。
こちらもカメラとプレイヤー位置を親子関係でくっ付けていますが、ステージの各セクションごとに決められた範囲(以下セクション範囲と呼称します)に応じて、カメラの描画範囲を超えないようにカメラ移動を制限します。
gifアニメーションでは少し分かりにくいですが、これならステージの範囲を超えればカメラが上下せず、画面端まで行けばカメラ移動も止まります。

実装方法ですが、基本的には下記サイトの方法で設定しています。
非常に参考になりましたorz
http://pokelabo.co.jp/creative-blog/?p=340

shot2ss20170106202106038

オブジェクトとしては、CameraController と Section、SectionArea があります。
CameraController の下に mainCamera が存在し、Section はステージに配置したオブジェクトの親にしています。
わざわざ CameraController の子にしているのは、プレイヤー位置から見た相対的なカメラ位置・回転を変更できるようにしたかったためです。
SectionArea はセクションの範囲の基準点となる空オブジェクトです。
それぞれのオブジェクトにスクリプトを設定します。

【CameraController】

using UnityEngine;
using System.Collections;

/// <summary>
/// ゲーム画面上のメインカメラの制御を行うクラス
/// </summary>

public class CameraController : MonoBehaviour {

    // カメラの幅と高さ
    private float cameraRangeWidth, cameraRangeHeight;

    // 各オブジェクトとコンポーネント
    private GameObject mainCamera;
    private GameObject player;

    // セクション範囲定義用
    private Rect SectionRect;
    private Vector3 top_left, bottom_left, top_right, bottom_right;

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

    void Update() {
        // プレイヤーキャラの位置に追従させる
        transform.position = player.transform.position;

        Vector3 newPosition;

        // カメラ描画範囲の上下左右を取得
        float distance = Vector3.Distance(mainCamera.transform.position, player.transform.position);
        bottom_left = mainCamera.GetComponent<Camera>().ViewportToWorldPoint(new Vector3(0, 0, distance));
        top_right = mainCamera.GetComponent<Camera>().ViewportToWorldPoint(new Vector3(1, 1, distance));
        top_left = new Vector3(bottom_left.x, top_right.y, bottom_left.z);
        bottom_right = new Vector3(top_right.x, bottom_left.y, top_right.z);

        cameraRangeWidth = Vector3.Distance(bottom_left, bottom_right);
        cameraRangeHeight = Vector3.Distance(bottom_left, top_left);

        // カメラ位置をセクション範囲内に収める
        float newX = Mathf.Clamp(newPosition.x, SectionRect.xMin + cameraRangeWidth/2, SectionRect.xMax-cameraRangeWidth/2);
        float newY = Mathf.Clamp(newPosition.y, SectionRect.yMin + cameraRangeHeight/2, SectionRect.yMax - cameraRangeHeight/2);

        transform.position = new Vector3(newX, newY, newPosition.z);
    }

    void OnDrawGizmos()
    {
        // カメラ描画範囲を表示
        Gizmos.color = Color.green;
        Gizmos.DrawLine(bottom_left, top_left);
        Gizmos.DrawLine(top_left, top_right);
        Gizmos.DrawLine(top_right, bottom_right);
        Gizmos.DrawLine(bottom_right, bottom_left);
    }
}

【SectionController】

using UnityEngine;
using System.Collections;

public class SectionController : MonoBehaviour {

    // セクションのカメラ範囲制御
    public Transform SectionArea;
    public float rect_width, rect_height, collider_depth;

    private Rect SectionRect;

    // マネージャ
    private GlobalManager globalManager;

    void Start () {
        // マネージャ取得
        globalManager = GlobalManager.getInstance();

        // セクション範囲定義
        SectionRect = new Rect(SectionArea.position.x, SectionArea.position.y, rect_width, rect_height);

        // セクション判定用オブジェクトに範囲を設定
        SectionArea.GetComponent<SectionArea>().setSectionRect(SectionRect);

        // CameraControllerにセクション範囲を渡すための判定定義
        SectionArea.transform.position = new Vector3(SectionRect.center.x, SectionRect.center.y, transform.position.z);
        BoxCollider boxCollider = SectionArea.GetComponent<BoxCollider>();
        boxCollider.size = new Vector3(SectionRect.width, SectionRect.height, collider_depth);
    }

    void OnDrawGizmos()
    {
        if (globalManager) {
            // セクション範囲を描画
            float base_depth = globalManager.stageManager.baseDepth;

            Vector3 lower_left = new Vector3 (SectionRect.xMin, SectionRect.yMax, base_depth);
            Vector3 upper_left = new Vector3 (SectionRect.xMin, SectionRect.yMin, base_depth);
            Vector3 lower_right = new Vector3 (SectionRect.xMax, SectionRect.yMax, base_depth);
            Vector3 upper_right = new Vector3 (SectionRect.xMax, SectionRect.yMin, base_depth);

            Gizmos.color = Color.red;
            Gizmos.DrawLine(lower_left, upper_left);
            Gizmos.DrawLine(upper_left, upper_right);
            Gizmos.DrawLine(upper_right, lower_right);
            Gizmos.DrawLine(lower_right, lower_left);
        }
    }
}

globalManager など、多少触れていない要素が含まれていますが、ただ他のコンポーネントから値を取っているだけなので適当に流して下さい。

CameraController の Mathf.Clamp() がミソのようです。
これで CameraController のX軸Y軸がセクション範囲から出ないようにコントロールしています。
計算式の意味は参考サイトの持ってきただけなので、自分でもよく分かっていませんが・・・。

shot2ss20170106202358374

SectionController の boxCollider は後述する「セクション移動時のエリア再設定」用です。
セクション範囲の大きさをインスペクターから rect_width, rect_height, collider_depth に設定します。
その際、SectionArea の位置を左下とした値に設定する必要があります。
また、こちらは動的に動かしたりはしないので、本当に定義するだけです。

20170106_04

この状態で実行しシーンビューに切り替えると、セクション範囲が赤枠で、カメラ範囲が緑枠で表示されます。
上のgifアニメーションは mainCamera を選択している状態です。緑枠が赤枠を超えた場合、mainCamera の位置が動いていないのが分かると思います。

セクション移動時のエリア再設定

実際のゲームで考えてみると、ステージ全体を通して1セクションで出来ていることはなかなかないです。
「右に進んだら上に行って、今度は下ってまた右に~」なんてことが多いのではないでしょうか。
途中で特殊なエリア (中ボス部屋とか) を挟んだりする場合もあります。

なので、セクションを複数定義した際に CameraController のセクション範囲を再定義できるようにしておきます。
また、各セクションの子オブジェクトにセクション範囲を定義したオブジェクトを作ります。
付けるスクリプトは以下になります。

using UnityEngine;
using System.Collections;

public class SectionArea : MonoBehaviour {

    private Rect SectionRect;
    private CameraController cameraController;

    /// <summary>
    /// セクション範囲を設定する
    /// </summary>
    /// <param name="rect"></param>
    public void setSectionRect(Rect rect) {
        this.SectionRect = rect;
        cameraController = GameObject.FindGameObjectWithTag("CameraController").GetComponent<CameraController>();
    }

    void OnTriggerEnter(Collider c) {
        if (TagUtility.getParentTagName(c.gameObject.tag) == "Player") {
            cameraController.setSectionRect(SectionRect);
        }
    }
}

OnTriggerEnter() 時に CameraController にセクション範囲を渡すので、胴スクリプトにセッタを作ります。
至って普通のセッタです。

public void setSectionRect(Rect SectionRect) {
    this.SectionRect = SectionRect;
}

あとはプレイヤーがセクション範囲に触れれば勝手にセットされます。

セクション移動時のカメラワーク

一通りの実装はできましたが、セクションからセクションへ移動した際のカメラワークが微妙で、カメラが一瞬で移動するので視覚的にはよろしくありません。
こういった処理では iTween を使用するのがベターですが、「移動する先」が分からないと処理しようがないです。
現状でも一瞬で移動するのは、Update() の「セクション範囲にカメラを収める」ための処理で勝手に動いている感じです。

ということで、setSectionRect() 実行時に移動先の地点を割り出すための再計算を行い、 iTween による移動処理を加えます。
再計算といってもやっていることは Update() 内と変わりません。
ついでに移動用に関数として、moveCameraPosition() も作っておきます。

public void setSectionRect(Rect SectionRect) {
    // 移動後のカメラ位置取得のため、カメラ移動範囲の再計算
    float newX = Mathf.Clamp(mainCamera.transform.position.x, SectionRect.xMin + cameraRangeWidth/2, SectionRect.xMax-cameraRangeWidth/2);
    float newY = Mathf.Clamp(mainCamera.transform.position.y, SectionRect.yMin + cameraRangeHeight/2, SectionRect.yMax - cameraRangeHeight/2);
    Vector3 newPos = new Vector3(newX, newY, mainCamera.transform.position.z);

    moveCameraPosition(mainCamera.transform.position, newPos);
    this.SectionRect = SectionRect;
}

public void moveCameraPosition(Vector3 oldPos, Vector3 newPos, float time = 1f) {
    iTween.MoveTo(mainCamera, iTween.Hash(
        "position", newPos,
        "time", time
    ));
}

20170106_03

iTween.MoveTo() により、適用前よりもなめらかにカメラが移動してセクション再設定が行われます。
なかなか良い感じですね!

まとめ

そんなわけで、横スクロールアクションにおけるカメラワークについて考えてみました!
やっとまともなカメラワークが実現できたので一安心です。

ただ、実装が完全に横スクロール向けのカメラワークなので、3Dとのハイブリッドはほぼできなくなりました。
いっそ開き直ってゲームにすることを優先し、次作るゲームに繋げるというのもありかなーと思い始めていたりします。
なので気にせずやっていこうと思います!

【開発メモ】「オブジェクトを掴む・投げる」アクションの修正

というわけで、今回は「オブジェクトを掴む・投げる」というアクションの修正になります!
「修正」ということで、既にそれっぽいアクションは実装してあります。

shot2ss20160802234413497

流れとしては以下のようになっております。
1.キー入力で掴むアクションを行い、手に掴み専用の判定を出す
2.掴み判定に当たったオブジェクトを持ち上げる
3.再度キー入力でオブジェクトを投げる

これがゲーム的にはかなりの欠陥品で、
・掴み判定に当たらないと持てない
→目の前のオブジェクトでもすぐに持てない
・掴む動作自体がモッサリなので、掴み判定と相まってテンポが悪い
・持ち上げた際のオブジェクトの位置が掴んだ位置に依存
→判定の出始めや終わり際で持つと持ち上げた際にズレまくる
・一度に複数のものを掴めてしまう
・持ち上げた状態で敵を押すと楽々場外KOが可能

などなど、実用に耐えうる代物ではありません。
特に致命的なのがテンポの悪さなので、今回はそこに重点を置いて修正を考えてみます!
アクションゲームらしく、キー連打で次々と投げまくれるようなものがいいですね!

様々なクラスやオブジェクトで処理している関係上、ソースを全て書くと長くなってしまうので、最低限のものだけ載せます!

オブジェクトを掴む処理の変更

以下のようなシンプルなものに変えてみます。
・キー入力時に前方に掴むことができるオブジェクトがあるか確認する
・掴めるオブジェクトがある場合は持ち上げる

処理としては、キー入力時にプレイヤー前方に短く RayCast を行い、当たったオブジェクトを判定、掴める場合は特定の位置に移動させながらプレイヤーの子オブジェクトにします。

上記修正から、掴み判定という概念を削除します。
モッサリ感解消の他、複数オブジェクトを一度に持ち上げてしまう問題も解決できます。

語っていてもしょうがないので、レッツコーディング(`・ω・´)
Player クラスの Update() 内に処理を追加します。

if (Input.GetButtonDown("Grab") && !stateInfo.IsTag("Throw")) {
    Ray grabRay = new Ray(transform.position, transform.forward);
    if (Physics.Raycast(grabRay, out hit, 3f)) {
        if (hit.collider.tag == "ThrowableObject") {
            StartCoroutine("setThrowingTarget", hit.collider.gameObject);
        }
    }
}

1つ目のif文で「ボタン入力時」且つ「掴み&投げ系のモーションではないこと」を判定します。
予め掴み&投げ系のステートに「Throw」というタグを割り当てておく必要があります。
また、Raycast() の際は maxDistance を指定しないとあらぬところまで届いてしまうので注意します。

if文が通ったら setThrowingTarget() であれこれ処理を行います。
詳しい処理は割愛させて頂きますが、モーション切り替えや持ち上げたオブジェクトの親子設定などを行っています。

前の仕様では敵に掴み判定を当てると専用の掴み攻撃が発動しましたが、システム的に面倒な上にゲーム的にもどうでもいい機能だったため、こちらも無くしてしまいました。

連続で入力した際の挙動の調整

掴む際のプロセスが短くなりましたが、そこから投げるまでの過程はまだテンポが悪いです。
モーション的には「掴む→持ち上げて保持→投げる」となっており、各モーションが完全に終了するまで遷移しないことが問題のようです。
そこで即投げする場合は「持ち上げて保持」のモーションを経由せず、掴みから直接投げるモーションへ遷移できるようにしてみます。

shot2ss20160802234236073

上は AnimatorController の掴む&投げるアクション用のサブステートマシン内です。

制御用にbool型パラメータとして Hold を作ってあります。
trueになった際に Grab→Hold と遷移し、falseになった際には Hold→Throw と遷移します。
上記は何れも ExitTime にチェックを入れているので、モーションが終わるまでは遷移しません。

ここに Grab から Throw へ遷移を追加し、直接遷移できるようにします。
遷移条件は Hold→Throw と同じで、Hold パラメータが false になったときです。

ということで、Grab ステートに以下のスクリプトを設定します。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {
    public class Grab : StateMachineBehaviour {
        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (Input.GetButtonDown("Grab")) {
                animator.SetBool("Hold", false);
            }
        }
    }
}

超シンプルに、掴みモーション中に入力があった場合はそのまま Hold を false にしてしまいます。
この状態で Grab→Throw への遷移で ExitTime のチェックを外せば、入力された段階でそのまま投げるモーションに遷移してくれます。

まとめ

そんなわけで、オブジェクトを掴む・投げる処理に修正を入れてみました!
システムとして要るかどうかはともかく、アクションゲームでは割とありそうなものなので、ひとまず使えるレベルのものにしておきたいです。

最近更新していなかった故に急いで書いたのでかなりざっくりです。
分かりにくいところがあればコメント等頂ければ頑張って書きます!

【開発メモ】体力0でミスとなった場合の処理の実装

というわけで、最近Googleアナリティクスを見ていると、「モグフィー」という検索ワードで来る方がいるようです。
自分としてはブログ3キャラの内の1匹に適当に付けた名前なのですが、どうやら離乳食の製品名と被ってしまっていたようです。
半分検索妨害のような感じですが、今更被ったからと名前を変えるのもアレなので、このまま行かせていただきます!

今回は「体力がなくなりミスとなった時」の処理について考えてみます。
前回の「落下時のダメージ処理」に続き、ゲームとしての体裁を保つためには必須の処理ですね。

shot2ss20160723090956792

要するに「体力なくなって死んだとき」の処理ですが、直接死亡と書くのはこの手のゲームでは避けたいので、ミスと表記することにします。
「倒れた」とか「ちからつきた」とかそんなニュアンスです。
最近のRPGでは「戦闘不能」とか「気絶」とかで、死亡扱いしているのはドラクエや硬派なゲームだけな気がします。

前回の記事はこちらになります。

【開発メモ】ステージ外へ落下した際の処理について

ミスとなった場合の処理

ゲームとしてはシンプルにゲームオーバーにします。
但し無限にコンティニューが可能で、コンティニューした場合は体力を全回復させて復帰させます。

コンティニューをしなかった場合はタイトルにでも飛ばせば良いでしょう。
まだまともなタイトル画面は作っていませんが・・・。
ちなみに Unity5.3 からシーン遷移が Application 系から SceneManager を使ったものに変わっているようなので注意します。

キャラクター的にはフラフラと倒れるようなアニメーションをさせればそれっぽくなりそうです。
また当たり前ですが、体力が0になった段階で操作できなくする必要があります。
とりあえず CharacterControoler の canControl をfalseに設定して動けないようにはしておけば、最低限の体裁は整えられそうな感じ。

ゲームオーバー時のUI作成

まずゲームオーバー時のロゴを作ってしまいます。
GIMP で適当にやります。

20160723_gameover

ゲーム開発ってデザインセンスも求められるわけですよ。
自分の水準以下のデザインセンスでこの先ゲームを作っていけるのか心配になってきます。
そんなクオリティです。
まあキャラクターデザインとかも割と適当なので、今に始まったことではありませんが。

shot2ss20160723091447812

最低限必要なものはゲームオーバーのロゴ、コンティニューボタンとENDボタンです。
それだけでは不親切なので、コンティニューボタンとENDボタンの説明文なんかも欲しいところです。
ロゴはただの画像なので配置するだけですが、コンティニューボタンとENDボタンは押下時にイベントを走らせる必要があります。
なので UI の Button として作成します。

shot2ss20160723091050222

ゲームオーバー専用の UI としてまとめて管理したいので、上のように空オブジェクトの子にしました。
その場合、親となる空オブジェクトも Transform から RectTransform にしておいた方が良いようです。

shot2ss20160723092244763

uGUI であればクリック時の処理や各種イベント処理が簡単に登録できます。
UI 上から実行したいスクリプトのオブジェクトとメソッドを選択するだけ!
OnGUI() で頑張っていた4.1の頃と比べると便利になったものです。

ボタンが反応しない場合、ヒエラルキーに「EventSystem」が存在するか確認します。
ない場合、メニューバーから「GameObject→UI→EventSystem」で追加します。

shot2ss20160723091142622

そのまま実行すると例外が発生し、「Input Button Submit is not setup.」とか言われます。
Standalone Input Module の各項目に対応する入力が InputManager 上に存在する必要があります。
上記エラーの場合、InputManager で新しく「Submit」を作成するか、モジュールの項目名を InputManager に存在する入力の何れかに変更して対応します。

ゲームオーバー時のスクリプト処理

ゲームオーバー時の処理の UI 制御をまとめたスクリプトを作成します。

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

/// <summary>
/// ゲームオーバー時のUIを制御するクラス
/// </summary>

public class GameOverUI : MonoBehaviour {

    private Text description;
    private StageManager stageManager;

    void Start () {
        stageManager = GameObject.Find("StageManager").GetComponent<StageManager>();
        description = transform.FindChild("MenuDescription").GetComponent<Text>();
    }

    /// <summary>
    /// コンティニューボタンを押下した際の処理
    /// </summary>
    /// <param name="continueButton"></param>
    public void OnClickContinue(GameObject continueButton) {
        stageManager.toContinue();
        Destroy(this.gameObject);
    }

    /// <summary>
    /// ENDボタンを押下した際の処理
    /// </summary>
    public void OnClickEnd() {
        stageManager.toEnd();
    }

    /// <summary>
    /// コンティニューボタンにカーソルを合わせた際の処理
    /// </summary>
    public void OnPointerEnterContinue() {
        String message = YamlUtility.getMessageValue("gameover.continue");
        description.text = message;
    }

    /// <summary>
    /// ENDボタンにカーソルを合わせた際の処理
    /// </summary>
    public void OnPointerEnterEnd() {
        String message = YamlUtility.getMessageValue("gameover.end");
        description.text = message;
    }

    /// <summary>
    /// コンティニューボタンからカーソルを離した際の処理
    /// </summary>
    public void OnPointerExitContinue() {
        description.text = null;
    }

    /// <summary>
    /// ENDボタンからカーソルを離した際の処理
    /// </summary>
    public void OnPointerExitEnd() {
        description.text = null;
    }
}

どうしても関数が多くなってしまうので、コメントをしっかり書いて分かるようにしておきます。
Visual Studio ならスラッシュ3回入力で自動的にsummaryが入ってくれるのでラクラクです。

メッセージにある Yaml 系に関しては以下の記事をご参照下さい。

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

UI 制御とは別で、ゲームオーバー時やコンティニュー時の処理を行うマネージャクラスを作成します。
ステージ上のゲーム進行を担うので、「StageManager」と命名してみました。
GameOverUI から呼ばれた処理はこちらで行います。

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

/// <summary>
/// ステージの進行・状態を管理するマネージャクラス
/// </summary>

public class StageManager : MonoBehaviour {

    public GameObject playerPrefab;
    public Transform checkPoint;
    public GameObject gameOverUI;
    private GameObject canvas;

    private GameObject playerObject;
    private Player player;

    void Awake() {
        playerObject = Instantiate(playerPrefab, checkPoint.position, Quaternion.identity) as GameObject;
        player = playerObject.GetComponent<Player>();
    }

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

    public void toGameOver() {
        GameObject gameOver = Instantiate(gameOverUI) as GameObject;
        gameOver.transform.SetParent(canvas.transform);
        gameOver.GetComponent<RectTransform>().localPosition = Vector3.zero;
    }

    public void toContinue () {
        player.initToContinue(checkPoint.position);
    }

    public void toEnd() {
        // タイトル画面に戻す
        SceneManager.LoadScene("title");
    }
}

プレイヤーキャラの処理は Player クラスに initToContinue() メソッドを作っておき、それを呼んで処理させます。
いろいろ処理が絡み合っているので割愛しますが、スタート地点への移動や無敵判定の解除、パラメータ類のリセットを行っています。

あとは死んで動作チェック!
プレイヤーの体力100に対し、接触時の被ダメを1000に設定した無慈悲なウニに突撃してテストします。

20160723_01

死亡するとゲームオーバー画面が表示されること、プレイヤーが動かせなくなること、コンティニュー押下時にスタート地点へ戻されることを確認します。
GIFアニメーションにこれら全て入れるのは難しかったので、画像的にはぶっ倒れるマリンパだけですが・・・。
とりあえず動作に問題はなさそうです。

チェックポイントの作成

基本的に復帰地点はスタート地点ですが、チェックポイントに触れていればそこから再開にします。
これまたアクションゲームではよくあるシステムです。
中間ポイントのようなものを設けるのが手っ取り早いですが、どこまで進むとチェックポイントを通ったか分かりにくいので、私的には好きな形式ではないです。

となれば、チェックポイントと分かるようなオブジェクトが必要ですね!
これまた適当に作ります。

shot2ss20160723091606072

貝殻!
某ヒトデのマリンアクションもチェックポイントは貝殻だった気がします。
マーメイドとかそれに類するものは入っていないので問題ないかと思われます!
触れたのか分かりやすくするため、アニメーションで貝殻を開くようにしておきます。

で、貝殻にチェックポイント用のスクリプトを追加します。

using UnityEngine;
using System.Collections;

public class CheckPoint : MonoBehaviour {

    public Transform returnPoint;

    private StageManager stageManager;
    private Animator animator;

    void Start () {
        stageManager = GameObject.Find("StageManager").GetComponent<StageManager>();
        animator = this.GetComponent<Animator>();
    }

    void OnTriggerEnter(Collider c) {
        if (c.gameObject.tag == "Player") {
            animator.SetTrigger("open");
            stageManager.checkPoint = returnPoint;
        }
    }
}

貝殻に触れるとパカっと開き、コンティニュー時の復帰地点を触れた貝殻にします。
貝殻自体を復帰地点に登録すると地形にめり込んでしまうので、復帰地点用の空オブジェクトを指定し、そこに戻すようにしています。
見た目が猛烈に地味なので、そのうちエフェクトとか付けます!

まとめ

そんなわけで、ミス時の処理とゲームオーバー処理について作ってみました!
これで最低限のゲームとしてのルールはできたと思われます。
この状態でステージ上に敵やトラップを配置すればそれっぽくはなるので、そこまで作ってしまいたいです。

にしても uGUI は使いやすいですね!
特にイベント系の処理が簡単に実装できるようになったので、メニュー画面などの複雑なUIの作成も捗りそうです。

【開発メモ】ステージ外へ落下した際の処理について

というわけで、今回はタイトル通り、「ステージ外へ落下した際の処理」について考えてみます。

shot2ss20160713230800516

この処理がないと落下しても亜空間を移動できたり、落下後に復帰できなくなってしまいます。
ゲームとしての体裁を保つためには必須の処理ですが、全然作っていなかったので実装してしまおうと思います!

実際の処理はゲーム内容によって大きく異なる部分かと思われます。
落下時は即死するのか、ダメージを受けるだけなのか、その場合どこに戻されるのか・・・とかですね。
まあ見ての通り堅苦しいゲームではないので、ペナルティは軽めにする予定です。

判定用オブジェクト作成とスクリプト設定

「落下時は一定ダメージを受けて特定の地点へ移動」 で実装してみます。
残機というシステムを入れるつもりがないこと、その上で落下=即死では難易度が高くなると感じたためです。

shot2ss20160713231310804

まず、「触れると落下した扱いになる」判定を作ります。
落下判定をする場所に配置する他、ステージ全体の下に大き目の判定で配置し、イレギュラーな挙動をしてしまった場合でも落下扱いにして戻ってこれるようにします。

次に「落下後に戻される場所」として空のオブジェクトを作成し、上で作成した判定の子オブジェクトにします。
子オブジェクトにしなくても問題はありませんが、セットで管理しやすくなるのでオススメです。
戻ってくる位置として使うだけなので、Transform 以外のコンポーネントは不要です。

その後、判定を作ったオブジェクト(=親オブジェクト)にスクリプトを設定します。

using UnityEngine;
using System.Collections;

public class DamageReturnArea : MonoBehaviour {

    public float power;

    private Transform returnPoint;

    void Start() {
        returnPoint = transform.FindChild("ReturnPoint");
    }

    private void OnTriggerEnter(Collider c) {
        string tag = TagUtility.getParentTagName(c.gameObject);

        if (tag == "Player") {
            c.GetComponent<Player>().forceDownDamage(this);
            StartCoroutine("returnCharacter", c.gameObject);
        }
    }

    private IEnumerator returnCharacter(GameObject character) {
        yield return new WaitForSeconds(1f);

        character.transform.position = returnPoint.position;
    }
}

TagUtility に関しては以下の記事をご参照下さい。

【Unity】タグの階層表示と判定方法について

判定に入ったのがプレイヤーキャラだった場合は StartCoroutine() を実行し、一定時間後に returnPoint に戻します。
ダメージ処理は forceDownDamage() から Player クラスで行っています。
Player クラスまで載せると長い上に他と絡み合っていて面倒なので、ここでは割愛します。

ちなみにエリアとして作った関係上、マグマや針びっしりの床などでも同様の処理ができます。
一般的なアクションゲームでよくある即死トラップ系はステージ外落下と同じ扱いにしてしまう予定です。

落下時のカメラワークについて

現状のカメラアングルはプレイヤーの位置に合わせて追従します。
mainCamera をプレイヤーと同位置に追従させているオブジェクトの子オブジェクトとして設定し、位置・角度を調整しています。
そのスクリプトは以下の記事に載せてあります。

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

ただし、落下時にカメラに対して何も処理をしていないため、落下後も追従しっぱなしです。
この手のゲームでステージ外落下後もプレイヤーを追従するカメラなんてほとんどないでしょう。

そんなわけで、落下判定に触れた際はカメラをプレイヤーを追従しないようにします。
今は CameraController というスクリプトからカメラを制御しており、上記の追従もここで行っているので、フラグを持たせて切り替えてしまいます。
上で載せたスクリプトを少し修正します。

using UnityEngine;
using System.Collections;

public class DamageReturnArea : MonoBehaviour {

    public float power;

    private Transform returnPoint;
    private CameraController cameraController;

    void Start() {
        returnPoint = transform.FindChild("ReturnPoint");
    }

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

    private void OnTriggerEnter(Collider c) {
        string tag = TagUtility.getParentTagName(c.gameObject);

        if (tag == "Player") {
            c.GetComponent<Player>().forceDownDamage(this);
            StartCoroutine("returnCharacter", c.gameObject);

            // カメラアングルを固定
            cameraController.fixCamera = true;
        }
    }

    private IEnumerator returnCharacter(GameObject character) {
        yield return new WaitForSeconds(1f);

        cameraController.fixCamera = false;
        character.transform.position = returnPoint.position;
    }
}

カメラの方はフィールドに fixCamera を追加し、「transform.position = player.transform.position」の前に if 文を追加して制御します。
以下は CameraController の Update() 内です。

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

    if (!fixCamera) {
        transform.position = player.transform.position;
    }
}

最終的にはこんな感じになりました!

20160714_01

よくあるアクションゲームのようなカメラワークになった気がします。
とりあえず十分な出来です。

まとめ

そんなわけで、落下時の処理について考えてみました!
実装の雰囲気だけ見ると、「メトロイドプライム」シリーズや、「伝説のスタフィー」シリーズの処理に似ています。
(後者はエリアの初めまで戻るので、そこまで似ているわけでもないかも)
まあ最近のゲームはダークソウルとか硬派なものを除くと、どれもこんな感じだったりしますが。

今日のイラスト

久しぶりにランパを描いてみました。
イラストを描くこと自体が数ヶ月ぶりだったりします。
何かバランスが変ですが、パッと見の出来はまあまあな感じ。

ranpa30

そのうちTwitterで「ランパの台詞集bot」みたいなものを作ろうかと考えていたりします。
いろいろと下準備が必要になりそうなので、暇なときにやっていく感じ・・・でしょうか。