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

というわけで、最近 Twitter の bot 制作の話ばかりでしたが、今回はゲーム開発のお話です!

テーマは「敵キャラクターとロジックに関するお話」です!
定期的にこんな話は載せていますが、どれも中途半端な実装とロジックなので、もう一度ベストな形を考え直す意味合いです。特に敵キャラクターのロジック部分はしっかり考える必要があります。

結論から言うと、「移動や攻撃などの役割毎にコンポーネントとして分解する」ようにするのが目的です。
なるべくロジックを分解し、パーツとして組み合わせることができるうな形が理想です。

敵キャラクターの作成

自作ゲームである以上、敵キャラクターも全て自分で考えます。
ロジックどうこうの前に、敵キャラクターを用意しないと話になりません。

とはいえ、今まともな敵と言えるものは「バブぴょん」くらいです。
開発初期の方に作ったので設定も何もなく、ロジックもかなり適当です。

shot2ss20170122094910458

敵キャラクターを作っていく場合、相応に種類が必要になるので、ある程度設定を考え、一貫性を持たせたほうがモデリングやモーション入れもラクです。
今考えている設定に「共通で結晶や角がある」「雷と氷を放つ」があるので、とりあえずそれを盛り込んで作りました。
前者は角ばっている方がモデリングが楽だからで、後者の「雷と氷」というのは、「海(≒水)を脅かす属性」として、一番メジャーで妥当かと思ったためです。

そんなわけで、ざっくりと2匹作ってみました!
上記のバブぴょんは「敵っぽくなさ」に定評がありましたが、今回のはそれなりに敵っぽくなっております!

shot2ss20170122093840735

1体は二足歩行の至ってシンプルな見た目です。
槍状の結晶による突きと角からのビームを放ち、敵軍団の主力となるような敵を目指します。
暫定的な名前として「レイドラン」と付けています。

shot2ss20170122093938413

もう1体は地を這う幽霊っぽい見た目です。
動作は遅めですが、両手で薙ぎ払う攻撃と防御を得意とする敵を目指します。
幽霊っぽく、目はパーティクルで表現しています。

ロジック部分の設計について

敵キャラクターが取る行動について、役割ごとにざっくりと洗い出してみます。

【索敵】
・索敵をしない (プレイヤーの行動に影響されない)
・前方を索敵
・周囲を索敵

【移動】
・移動しない
・常に前進
・一定の範囲を往復
・プレイヤーを追跡

【ジャンプ】
・ジャンプしない
・一定間隔でジャンプ
・攻撃時のみジャンプ
・常に浮いている

【攻撃】
・攻撃しない
・常に一定間隔で攻撃
・プレイヤーが索敵範囲内にいる場合に攻撃

概ねこんな感じでしょうか。
このロジック毎にクラスを作成し、敵キャラクターにコンポーネントとして追加するだけで設定が可能な状態を目指します。キャラクターの固有クラスでロジックを作成すると使い回しがきつくなる上、クラスの役割が肥大化するので避けたいところです。
その分クラスが多くなりますが、命名規則を決めたり(頭文字をAttack~、Move~にするなど)、名前空間を指定するなどでカバーします。

この他、キャラ固有の特殊能力などは個別のサブクラスに実装してしまいます。
また、各ステージのボスは専用のロジックを作成する予定です。

あくまで自己流の設計なので、これがベストとは言えないと思いますが、そこまでたくさんキャラクター(とロジック)作るアイデアも時間もないので、これで行ってみます!

まとめ

そんなわけで、敵キャラクターとロジックについて考えてみました!
実装の際のポイントは
・役割とそのロジック毎にクラスを作成
・コンポーネントとして付け替え/設定ができるように
・命名規則と名前空間をしっかり決めておく

あたりでしょうか!

次回から作っていくつもりですが、最近出張やら外泊やらが多いので、そこそこ先の話になってしまうかもしれません。
妥協するところは妥協しないといつまで経っても完成しないので、そのあたりの線引きも大切ですね。

【開発メモ】アシストキャラクター「カンガルー」の実装

というわけで、久しぶりに Unity 側の作成になります!
今回はアシストキャラクター「カンガルー」を実装しました!

shot2ss20160613234718256

アザラシに続く2匹目のアシストキャラクターです。
あちらは遠距離タイプなので、こちらは近距離型のインファイターといった趣向で作っていきます。
見た目通りパンチとキックで戦います。
ゲームに登場するカンガルーって大抵はパンチとキックだったりしますが・・・。

【Blender】モデリング練習メモ 「カンガルー」編

モデルは前回モデリングしたカンガルーがベースです。
意外と横幅が小さかったので、全体的な体格と腕、脚を大きくなるよう修正しました。

本ゲームではアシストキャラクターの位置付けです。
プレイヤーキャラがカンガルーに乗ることで操作できます。
仕様を固めきれていませんが、概要は以下の記事をご参照下さい。

【開発メモ】アシストキャラクターの実装 その1

各種アニメーションの作成

カンガルーがベースなので他のキャラクターとはちょっと変わったモーションにします。

20160613_01

移動は両脚でぴょんぴょん跳ねながら行います。
ちょっと脚が小さい目で、モーションがぎこちないのも合わせて微妙な見栄えです。

跳ねる際にカンガルーが上下しますが、これは Blender 側のキーフレームに移動情報を登録しています。
上下移動を Unity 側で制御するのはすごく大変そうなので・・・。

ちなみに現実のカンガルーは構造上、後退が行えないらしいです。
Unity 的には PlatformInputController を使用している関係上、後退という概念自体がありません。
変なところで悩むことにならずによかったです。

shot2ss20160613235135104

攻撃は上述の通り、パンチとキックがメインです。
とりあえず通常の連続攻撃だけ作ってみましたが、手足が短いうえに間接もないため、どうしても地味になってしまいました。
ということで、体全体を大げさに動かしながら、カンガルー自身の位置も少し前に動かすことでそれっぽくしてみます。

現在は StateMachineBehaviour を継承したスクリプトを攻撃系ステートに付けており、入った際に Kangaroo クラスの attackStateEnter() が呼ばれ、そこで攻撃判定を生成しています。
このあたりの実装は以下の記事をご参照下さい。

【再編集】Unity開発メモまとめ「Animator」

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

public override void attackStateEnter(AnimatorStateInfo stateinfo) {
    if (isRiding) {
        // 存在する攻撃判定を削除
        // destroyAttackHit();

        // 通常攻撃
        if (stateinfo.IsName("Attack1")) {
            iTween.ValueTo(gameObject, iTween.Hash(
                "from", transform.forward / 4,
                "to", Vector3.zero,
                "onupdate", "characterMove",
                "time", 1.0f,
                "easetype", "easeOutQuart")
            );

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

        if (stateinfo.IsName("Attack2")) {
            iTween.ValueTo(gameObject, iTween.Hash(
                "from", transform.forward / 4,
                "to", Vector3.zero,
                "onupdate", "characterMove",
                "time", 1.0f,
                "easetype", "easeOutQuart")
            );

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

        if (stateinfo.IsName("Attack3A")) {
            iTween.ValueTo(gameObject, iTween.Hash(
                "from", transform.forward / 2,
                "to", Vector3.zero,
                "onupdate", "characterMove",
                "time", 1.0f,
                "easetype", "easeOutQuart")
            );

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

攻撃判定生成前に iTween.ValueTo() でちょっとだけ前に移動させます。
MoveTo() の方が動かす量の調整がしやすいのですが、当たり判定を突き抜ける危険があるため ValueTo() にします。

20160613_02

まだ何か地味ですが、その場でブンブンするよりはマシになりました!
いいエフェクトを作って上手くごまかせればいけそうです。

ゲーム的には「リーチは短いが手数と攻撃力に優れる」といった感じで調整していきます。
カンガルー自体判定が大きいので扱いは難しくなりそうです。
その分はインファイト時の圧倒的な破壊力を持たせて埋めようと思います。

移動入力による攻撃の出し分け

これはカンガルーに限ったことではありませんが、攻撃中に移動入力されているかどうかで最後に放つ攻撃を変えたいです。
カンガルーの場合は通常攻撃3段目を分岐させ、入力なしでアッパー、入力ありでキックにしようと思います!

shot2ss20160613235536001

3段目で分岐させるので、2段目からの遷移は「Attack3A」と「Attack3B」の2つを用意します。
2段目のステートに分岐を制御するためのスクリプトを付けます。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {

    /// <summary>
    /// 特定のキー入力時に移動キー入力に応じてTriggerパラメータを切り替えるクラス
    /// </summary>
    public class SwitchTriggerVelocity : StateMachineBehaviour {

        // 入力するキー(ProjectSettings)
        [SerializeField]
        protected string inputKey;

        [SerializeField]
        private string lowerParameterName;

        [SerializeField]
        private string upperParameterName;

        private bool isPrimaryInput = true;

        // 入力受付開始時間
        [SerializeField]
        private float receptionStartTime;

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (Input.GetButtonDown(inputKey) && isPrimaryInput) {
                if (receptionStartTime < stateInfo.normalizedTime) {
                    switchTrigger(animator);

                    // 一度入力したらExitまで変化させないようにする
                    isPrimaryInput = false;
                }
            }
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            isPrimaryInput = true;
        }

        private void switchTrigger(Animator animator) {
            // 移動入力がされているか判定
            if (GameConstants.STATE_VELCOITY_BORDER < System.Math.Abs(Input.GetAxis("Horizontal")) ||
                GameConstants.STATE_VELCOITY_BORDER < System.Math.Abs(Input.GetAxis("Vertical"))) {
                animator.SetTrigger(upperParameterName);
            } else {
                animator.SetTrigger(lowerParameterName);
            }
        }
    }
}

swtichTrigger() で切り替える際、移動入力がされているかを判定しています。
Input.GetAxis() で水平・垂直の両方向の入力値が取得できるのでこれを使います。
ただし入力方向によっては負の値になるので、System.Math.Abs() を使って絶対値で判定するのがポイントです。
GameConstants.STATE_VELCOITY_BORDER はただの定数で、今回は 0.9f を指定しています。

他には isPrimaryInput という変数を使い、複数回入力が出来ないようにしています。
receptionStartTime で攻撃モーションのどのタイミングから入力受付を開始するか制御できるようにしました。
モーションとの兼ね合いもありますが、今回は0.7fに設定しているので、現モーションの7割が再生されたタイミングから入力ができるようになります。

スーパージャンプ

カンガルーってジャンプ力にも優れているというイメージがあります。
となると、「ボタン長押し→放して大ジャンプ」とか欲しいですね!
某猿の横スクロールアクションに全く同じ名前のアクションがありますが、正にそんな感じ。

制御に CharacterMotor を使っている関係で微妙に面倒です。
ジャンプボタン長押しが自然な流れですが、CharacterMotor が有無を言わさずジャンプさせてしまいます。
「一度ジャンプ→長押しで溜める→大ジャンプ」の長いプロセスはアクションゲーム的にNGです。
CharacterMotor 側をいじるのも嫌なので、ひとまずはスーパージャンプ専用のボタンを作り、そのボタンの長押しで判定します。

InputManger からFキーを専用ボタンとして設定しておきます。
AnimatorController に bool パラメータとして「SuperJumpIdle」、Trigger パラメータとして「SuperJump」を作り、遷移の条件として使用します。
SwitchBoolByKey というクラスを作成、Idle ステートに付け、専用ボタン入力時にスーパージャンプ待機用のステートへ遷移させます。

using UnityEngine;

namespace StateController.PlayerAnimator {

    public class SwitchBoolByKey : StateMachineBehaviour {

        [SerializeField]
        private string inputKey;

        [SerializeField]
        private string parameterName;

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

shot2ss20160613235640913

スーパージャンプ待機→スーパージャンプへの遷移も作ります。
またまた SwitchTriggerByKey の出番で、今度はFキーを離した際(=GetButtonUp時)にトリガーをONにします。
その際に normalizeTime が0.9以上でないと遷移しないようにしておきます。

これでスーパージャンプへの遷移はできたので、あとはスーパージャンプステートにスクリプトを追加します。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {

    public class SuperJump : StateMachineBehaviour {

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            animator.gameObject.GetComponent<CharacterMotor>().movement.velocity.y = 0;
            iTween.Stop(animator.gameObject);

            iTween.ValueTo(animator.gameObject, iTween.Hash(
                "from", animator.gameObject.transform.up * 3f,
                "to", Vector3.zero,
                "onupdate", "characterMove",
                "time", 1.0f,
                "easetype", "easeOutQuart")
            );
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            animator.ResetTrigger("SuperJump");
        }
    }
}

おなじみの iTween.ValueTo() で上方向に移動させます。
CharacterMotor を使用しているため、ジャンプ前に velocity.y を0にしておきます。
ジャンプする高さは割と適当です。

これでスーパージャンプは出来ましたが、スーパージャンプ待機モーション完了前にFキーを離した場合のことも考慮します。
SuperJumpIdle ステートにスクリプトを付けます。

using UnityEngine;
using System.Collections;

namespace StateController.PlayerAnimator {

    public class SuperJumpIdle : StateMachineBehaviour {

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (Input.GetButtonUp("UniqueAction")) {
                animator.SetBool("SuperJumpIdle", false);
            }
        }
    }
}

あとは SuperJumpIdle から Exit ステートへの遷移条件に「SuperJumpIdle が false の場合」と設定します。
これでFキーを離した際に bool が flase になり、自動的に Idle ステートへ戻ります。

まとめ

そんなわけで、カンガルーを Unity へインポートして実装してみました!
仕様が煮詰まっていないのに先取りで実装している感がありますが、やる気のあるうちに出来るところまで作ってしまいたいです。

自分の記事を見返すと、かなり説明を端折っている印象を受けます。
実際は他の実装部分がモロに絡んできていて、上手く実装機能だけを説明するのが難しい状態です。
「これじゃ分からんよ」という場合はコメントを頂ければ頑張って説明するので、お気軽にどうぞ!

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

というわけで、新年度に入りつつもブログを放置気味なりべるんです。
やろうとしていることが難解だったり記事にしにくかったりでほとんど更新できていません。
とはいえ、「Unityゲーム開発日記」と名打っている以上、定期的に話題は出していきたいところです。

今回は敵キャラクターのロジックや行動ルーチンの見直しになります!
これまた前に適当に作って以来、手をつけていなかった部分です。

現状では、
・プレイヤーを見つけると一直線に突撃
・どの方向から近づいても瞬時にプレイヤーを発見する
・障害物があると回り込まず、完全に詰まりっぱなしになる
・プレイヤーが止まっている場合は攻撃を行わない
・ダメージモーション中も移動する
・ダメージモーションをカットして攻撃をしてくる
・押すと簡単にひっくり返る

等々、アクションゲームとしてみると欠陥品です。
行動ルーチンだけじゃどうしようもない部分もありますが、これを気に敵関連のオブジェクトやスクリプトを作り直すことにしました!

といっても全て直すのはかなり骨が折れるので、今回は移動関係のロジックを見直してみます。
ボスとか特殊な敵はひとまず考慮しないことにします。

shot2ss20160404235912272

テスト対象は↑の敵キャラクター「バブぴょん」です。
所謂スタンダードな雑魚的で、いろいろな場面に登場させる予定です。

敵キャラクター側の索敵について

基本的に雑魚敵はプレイヤーを発見した場合のみ行動を行います。
敵キャラ毎に索敵範囲を作り、それでプレイヤーが範囲内にいるかを判定します。

shot2ss20160404235946605

上はバブぴょんの場合の索敵範囲で、正面をBoxColliderでカバーしています。
ステルスアクションでも硬派なFPSでもないので、索敵に関しては割と単純です。
音とかそういう要素もありません。

敵キャラクター本体にも移動&ダメージ判定用のコライダーがあるので、索敵用オブジェクトにBoxColliderを付け、それを本体の子に設定しています。
合わせて以下のスクリプトを作成&アタッチします。

using UnityEngine;
using System.Collections;

public class EnemyActionRange : MonoBehaviour {

    public Transform lookTarget;
    public bool isDetectPlayer;

    private Enemy enemy;
    private Vector3 angle;

    void Start () {
        enemy = GetComponentInParent<Enemy>();
    }

    void Update () {

    }

    void OnTriggerEnter(Collider c){
        if (TagUtility.getParentTagName(c.gameObject) == "Player"){
            isDetectPlayer = true;
            lookTarget = c.transform;
        }
    }

    void OnTriggerExit(Collider c){
        if (TagUtility.getParentTagName(c.gameObject) == "Player") {
            isDetectPlayer = false;
            lookTarget = null;
        }
    }
}

TagUtilityに関しては以下の記事をご参照下さい。
ここでは使う意味はほとんどなかったりしますが・・・。

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

前はこれに敵本体の移動制御まで書いており、無駄に複雑な作りになっていました。
敵の移動や攻撃はEnemyクラスで行い、シンプルに「敵が索敵範囲内にいるか」のみを設定するようにします。

敵キャラクターの移動制御

プレイヤーを発見した敵キャラクターは接近して攻撃しようとします。
攻撃部分は後々実装するので、今は「移動→攻撃→一定時間停止→移動・・・」の移動部分のみを作ってみます。

以下は敵キャラクターに付与するEnemyクラスの一部です。
(他の機能も作成中なので不要な変数や処理もありますがご了承くださいorz)

 
using UnityEngine;
using System.Collections;

[RequireComponent (typeof (Rigidbody))]

public abstract class Enemy : MonoBehaviour, ICharacter {

    public float life, attackRange, moveSpeed, rotateSpeed;
    public float defaultActionInterval;
    protected bool canAction;
    protected bool isMovement, isRotation, isAttack;

    [SerializeField]
    protected float actionInterval;

    protected Animator animator;
    protected AnimatorStateInfo stateInfo;

    protected EnemyActionRange enemyActionRange;
    protected Rigidbody rigidbody;

    protected virtual void Start () {
        actionInterval = defaultActionInterval;

        canAction = true;
        isMovement = false;
        isRotation = false;
        isAttack = false;

        animator = this.GetComponent<Animator>();
        enemyActionRange = this.GetComponentInChildren<EnemyActionRange>();
        rigidbody = this.GetComponent<Rigidbody>();
    }

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

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

                // 移動制御
                if (isMovement) {
                    Vector3 forward = new Vector3(transform.forward.x, -0.1f, transform.forward.z);
                    animator.SetBool("Move", true);
                    transform.localPosition += forward * moveSpeed;
                } else {
                    animator.SetBool("Move", false);
                }

                // 回転制御
                if (isRotation && enemyActionRange.lookTarget != null) {
                    Vector3 angle = enemyActionRange.lookTarget.position - transform.position;
                    angle.y = 0;
                    transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(angle), rotateSpeed);
                }
            } else {
                animator.SetBool("Move", false);
            }
        }
    }
}

とりあえず敵を発見したら移動、発見していないor見失った場合は止まるようにしてみました。
移動制御と回転制御を別々にしているため、後々「その場から動かないが回転だけする」といった敵を作る際にも対応できます。

実際に動かすとこんな感じ。
索敵用コライダー内にプレイヤーがいる場合のみ行動しています。

まとめ

というわけで、敵キャラクターのシンプルな移動制御について考えてみました!
次は今回の移動に攻撃ロジックを乗せていこうと思います。

【開発メモ】アシストキャラクターの実装 その2

というわけで、今回は前に実装したアシストキャラクターの続きになります!
前回の記事はこちら

【開発メモ】アシストキャラクターの実装 その1

前回でアシストキャラクターへの乗り降りまで実装したので、今回は実際にアシストキャラクターを操作する部分を実装してみようと思います!
基本操作はプレイヤーに合わせ、移動・攻撃・ジャンプなどの基本アクションを作っていきます。
処理を流用できる部分も多いため、実装だけならあまり苦戦はしなかったです。

移動系

基本的な移動はプレイヤーと同じく、CharacterMotorとPlatformInputControllerで行います。
今はアザラシをベースに作っているため、移動速度は少し遅めにしてみます。

また、アシストキャラには回避や二段ジャンプなどの特殊な移動は実装しない予定です。
そういう意味ではプレイヤーキャラに比べて単純かもしれません。
その分各キャラに特殊な能力を付けたいと思いますが、それはまた追々ということで!

攻撃系

攻撃判定の生成ロジックはプレイヤーと一緒です。
参考になりそうな過去記事はこちら

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

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

同じこと書いてもしょうがないので、アザラシに持たせる攻撃方法について考えます。
これは前々から決めていて、強力な遠距離攻撃を持たせる予定です。
通常攻撃は高威力の弾を発射し、特殊攻撃には敵を追尾するレーザーを放ちます。

shot2ss20151226120459482

使わなくなったパーティクルで良いものがあったため移植し、通常攻撃としました。
口から出すには弾が大きい気がしますが、あまり気にしないことにします!
特殊攻撃のレーザーはロジックが難しそうなので後回しにします。

問題点

今回までの実装で抱えている問題点を挙げてみます。

乗り降りの際の円形エフェクトの表示

円形エフェクトはEffekseerを使って表示していますが、エフェクト毎に個別でインスタンス化されているわけではなく、Effekseerオブジェクトから全体をコントロールしている(?)ようです。
それ故、「特定のエフェクトだけを有効・無効にする」ということが困難です。

EffekseerのUnityマニュアルを見てみると、コンポーネントの他にスクリプトからでも再生ができるようです。
これを使えば個別でエフェクトをコントロールできそう。
使う機会は多いと思われるため、EffectUtilityというユーティリティクラスを作成し、その中でハンドルを生成する関数を実装します。

using UnityEngine;
using System.Collections;

public static class EffectUtility {
    public static EffekseerHandle createEffekseerHandle(string fileName, Transform transform) {
        EffekseerHandle handle = EffekseerSystem.PlayEffect(fileName, transform.position);
        handle.SetRotation(transform.rotation);
        handle.SetScale(transform.localScale);

        return handle;
    }
}

このEffekseerHandle型にはStop()という関数が含まれており、これを実行することで止めることができます。
もっと効率の良い方法がありそうですが、とりあえずこれで解決!

AnimatorのAnyStateからの割り込み

ある意味最大の問題点です。
アシストキャラに乗っている場合、プレイヤーキャラは待機orダメージモーションのみで他のモーションは入りません。
が、AnyStateから設定されているダメージや回避などは割り込んで再生されてしまいます。

Controllerを動的に切り替えることができれば行けそうですが、Animatorを動的に変えること自体がかなり怖いです。
遷移しないようにスクリプトから無理矢理制御できなくもなさそうです。
とてもスマートとは言えませんが・・・。

CharacterControllerの当たり判定

アザラシは横長な動物なわけですが、CharacterControllerのカプセルコライダーは横向きにすることができません。
仕方なくHeightを小さくし、Radiusで調整するようにしましたが・・・

shot2ss20151226120714778

見ての通り胴体前後の判定がスッカスカです。
NPCの敵が相手なので致命的ではありませんが、これが対戦ゲームとかだったらクレーム殺到なレベルですね。
CharacterController自体の判定は移動制御のみに使い、別でダメージ判定用のコライダーを付けれれば何とかなりそうです。

まとめ

今まで「まとめ」が全然まとめじゃなかったので、今度から箇条書きでやったこと/分かったことを書いていくようにします!

・アシストキャラクターの基本コントロールはプレイヤーと同じ
・アザラシは遠距離型キャラクターにする
・Effekseerはスクリプトから生成することでhandleから個別に停止できる
・いろいろと問題あり

細かい部分に難がありますが、見てくれだけは一応完成。
上手くゲームシステムやステージ構成に組み込んで行きたいです。

今日のイラスト

またまたシルリスちゃんです。
輪郭はパスツールで、影はペンタブ+エアブラシで塗ってみました!
マウスで適当に影を付けるよりは味が出ている・・・気がする。

shilriss5

【開発メモ】アシストキャラクターの実装 その1

というわけで、前回の投稿から時間が空いてしまいましたが、今回のお題はこれです!

shot2ss20151215210405763

新要素として「アシストキャラクター」を実装してみようと思います!
プレイヤーキャラとは別にステージ上にいる仲間キャラで、乗ることで操作することができるというものです。

元々はプレイヤーキャラとしてアザラシやカンガルーも入れようとしたのですが、開発中に厳しいと判断し3キャラに絞った経緯があります。
そのキャラを何とかゲームに組み込めないかと思い、「ステージ上で一時的に操作できるキャラ」という結論に至りました。

今回はプレイヤーキャラがアシストキャラに乗り降りするシステムを作ってみます。
焦っても雑になってしまうので、まったりやっていきます!

下準備

当たり前ですが、アシストキャラクターのモデルを作る必要があります。
ということでさくっと作ります!

shot2ss20151215210217326

作成したアザラシ(のつもり)です。
カンガルーとどちらにしようか迷いましたが、アザラシの方が乗るのに違和感なさそうという点と、単純に自分が大好きな動物なのでチョイスしました!
見直すとあまりアザラシっぽくないですが、そのうち修正します!
今回は乗り降りだけなので、モーションも作っていません。

乗る場合の処理

乗っていないアシストキャラには円形のエフェクトを出し、その範囲内でボタンを押すことでプレイヤーキャラが飛び乗るようにします。
アシストキャラの位置はプレイヤーの操作以外で動かすことはないと思われます。

円形の判定はアシストキャラクター本体に子オブジェクトとして作ります。
エフェクトはEffekseerのサンプルを参考にを作成・使用しています。
専用のスクリプト「RideArea」を作成します。

using UnityEngine;
using System.Collections;

public class RideArea : MonoBehaviour {

    private AssistCharacter assistCharacter;

    public void OnTriggerStay(Collider c) {
        if (TagUtility.getParentTagName(c.gameObject) == "Player") {
            if (Input.GetButtonDown("Action")) {
                Player player = c.GetComponent<Player>();
                assistCharacter.setPlayer(player);
                player.onRideOnAssistCharacter(assistCharacter);
            }
        }
    }
}

判定内でボタンが入力された場合、Playerクラスの「onRideOnAssisstCharacter」を実行します。
プレイヤーキャラをアシストキャラの上に飛び乗るように移動させ、transform.parentにアシストキャラを設定して子オブジェクトにします。
また、各種コントロール系を無効化します。

public void onRideOnAssistCharacter(AssistCharacter assistCharacter) {
    // コントロール系の無効化
    charControl.enabled = false;
    charMotor.enabled = false;
    platForm.enabled = false;

    // プレイヤーのtransform修正
    transform.parent = assistCharacter.transform;
    transform.rotation = assistCharacter.transform.rotation;

    // ジャンプ時のモーションパス定義
    var pathList = new List<Vector3>();
    pathList.Add(transform.position);
    pathList.Add((transform.position + assistCharacter.ridePoint.transform.position) / 2 + new Vector3(0f, 10f, 0f));
    pathList.Add(assistCharacter.ridePoint.transform.position);

    iTween.Stop(gameObject);
    iTween.MoveTo(gameObject, iTween.Hash(
        "path", pathList.ToArray(),
        "time", 1f,
        "delay", 0.01f,
        "easetype", "easeOutSine",
        "oncomplete", "onPlayerRideOn",
        "oncompletetarget", assistCharacter.gameObject
    ));
}

ジャンプ時の移動処理はiTweenで行います。
アシストキャラクターの子オブジェクトとして「ridePoint」という空オブジェクトを用意しておきます。
iTween.MoveTo()の移動先は”position”で指定しますが、今回は”path”を使ってみました!
Vector3型のListを作り、「プレイヤーキャラ→ジャンプの頂点→ridePoint」となるように挿入してします。
あとは”easetype”を”easeOutSine”にすればジャンプが放物線っぽい挙動になります。

iTween.Stop()とdelayを入れている理由は以下の記事を参照して頂ければと思います。

【Unity】iTweenの停止と再実行について

最後はアシストキャラ本体に付与するスクリプトを作成します。
以下はAssistCharacterクラスですが、実際にはそれを継承したSealクラスを付けています。
現状ではスーパークラスの関数を呼んでいるだけなのでほとんど意味はありませんが、後々増やすことも考え、専用に作っておきます。

Start()やUpdate()は特に面白いことをしていないので、プレイヤーが乗った際の関数のみ記載します。

public void onPlayerRideOn() {
    isRiding = true;

    charControl.enabled = true;
    charMotor.enabled = true;
    platForm.enabled = true;
}

関数はiTweenの実行後に呼ばれるよう、oncompleteで指定します。
isRidingフラグをONにし、各種コントロールを有効化します。

プレイヤーの参照としてPlayerクラスも渡して設定しておきます。
どのプレイヤーが乗っているか把握したり、何かと処理で使う機会が多いためです。

降りる場合の処理

乗っている最中にボタンを押すことで飛び降ります。
飛び降りる方向はアシストキャラの後ろ側です。
後ろが崖の状態で飛び降りるとそのまま落下しそうですが、現状ではどうしようもない部分なので気にせずいきます!

AssistCharacterクラスのUpdate()内で「ボタン押下かつ地面にいる状態」を判定し、降りる際の処理を記述した関数を呼んでいきます。
降りた後はアシストキャラを操作しなくなるので、コントロール系を無効化します。

public void onPlayerRideOff() {
    isRiding = false;

    charControl.enabled = false;
    charMotor.enabled = false;
    platForm.enabled = false;

    player.onRideOffAssistCharacter(this);
}

アシストキャラの処理後にPlayerクラスの「OnRideOffAssistCharacter」を実行します。
iTweenで飛び降りる処理と、コントロールを有効化する処理を書きます。

public void onRideOffAssistCharacter(AssistCharacter assistCharacter) {
    // コントロール系の有効化
    charControl.enabled = true;
    charMotor.enabled = true;
    platForm.enabled = true;

    // ジャンプ時のモーションパス定義
    Vector3 assistCharBack = assistCharacter.transform.position - (assistCharacter.transform.forward * 10f);
    var pathList = new List<Vector3>();
    pathList.Add(assistCharacter.ridePoint.transform.position);
    pathList.Add((transform.position + assistCharBack) / 2 + new Vector3(0f, 10f, 0f));
    pathList.Add(assistCharBack);

    transform.parent = null;
    iTween.Stop(gameObject);
    iTween.MoveTo(gameObject, iTween.Hash(
        "path", pathList.ToArray(),
        "time", 1f,
        "delay", 0.01f,
        "easetype", "easeOutSine"
    ));
}

「キャラクターの後ろ側に移動する」という点でちょっと詰まりました。
transform.positionを元にtransform.forward + Vector3() で上手くずらすような感じになります。
transofrm.forwardをそのまま参照すると原点近くまで吹っ飛んでしまうので気をつけます。

iTweenは乗る際と同じく”path”で指定します。
今度は「ridePoint→ジャンプの頂点→アシストキャラの背後」となるように設定してあります。

20151215

実際に乗り降りするとこんな挙動になります!
概ね自分の想定していた挙動になってくれてよかったです。

まとめ

そんなわけで、アシストキャラクターと乗り降りの部分を作ってみました!
次はアシストキャラクターを操作する部分を実装していく予定です。
難しそうなので遅れるかもしれませんが、まったりやります!