もしも、あるいはそれ以外のこと

Unityを使ったゲーム作りや、ゲームについて考えたりしています。

Unity&Corgi Engineを使用したアクションゲームづくり ~近接武器で殴りたい①~

少しバタバタとしてました更新ができてませんでした。
ゲームづくり自体は続けてますよ(`・ω・´)
Anima2Dを使ってキャラクターのスケルタルアニメーション関連とかを勉強中です。

前回はアニメーション関連をいじってました。当たり判定の追従やらを行いましたが、いかんせん近接武器で殴りたいのに銃撃用の無駄なパラメーターが多いので見通しが悪いです。
これから改造することも考えて、また内部の構造を知るためにスクリプトを組み替えましょう。
(本来はパラメーターの調整などを行う前にやるべきですが、先に仕組みを知りたいのでこうなりました)
前回はこちらです。
blue-mist.hatenablog.jp

■武器の構成
Corgi Engineで使われている武器の構成は以下のような構造になっています。

  • CharacterHandleWeapon
    • 所持する武器
    • 入力受付
    • 状態管理
  • Weapon
    • 攻撃時間
    • アニメーション
    • 攻撃範囲、威力クラス
    • 攻撃状態管理
    • 弾の管理など
  • MeleeWeapon
    • 攻撃範囲の生成/消滅

CharacterHandleWeaponが入力を受け付け、所持している武器に対して攻撃開始の指示を出します。
対応する武器(MeleeWeapon)が攻撃範囲を生成します。
武器の状態は
攻撃開始⇒攻撃前⇒攻撃中⇒攻撃終了待機⇒攻撃終了
という状態を遷移していきますが、こちらはMeleeWeaponのもとのクラス、Weaponクラスが握っています。
終了後、MeleeWeaponは攻撃範囲をオフにして、CharacterHandleWeaponに管理を戻します。

ということで修正するべきスクリプトは先に挙げた三点ですね。
先に改造後のスクリプト名称を決めておきます(何のひねりもないですが)。
ちなみに少し手間がかかってももとのスクリプトを残したまま、継承などを使い組み直したほうがいいと思います。Corgi Engineでは関数をoverrideするだけで使えるように構成されていることも多いです。

  • CharacterHandleWeapon
    • ->CharacterHandleWeaponController
  • Weapon
    • ->SwordWeapon
  • MeleeWeapon
    • ->MeleeAttackWeapon

では元のCharacterHandleWeaponのスクリプトを改造、もしくはコピーして新たにスクリプトを作ります。
HandleInput()も銃関連が混じっているのでごくシンプルになります。

   protected override void HandleInput ()
    {           

        if ((_inputManager.ShootButton.State.CurrentState == MMInput.ButtonStates.ButtonDown) || 
(ContinuousPress && (CurrentWeapon.TriggerMode == Weapon.TriggerModes.Auto) && 
(_inputManager.ShootButton.State.CurrentState == MMInput.ButtonStates.ButtonPressed)))
        {
            ShootStart();
        }
    }

Animator関連のパラメータ項目の修正は、

   /// <summary>
    /// 必要なアニメータパラメータが存在する場合、アニメータパラメータリストに追加します。
    /// </summary>
    protected override void InitializeAnimatorParameters() {
        if (CurrentWeapon == null) { return; }

        RegisterAnimatorParameter(CurrentWeapon.IdleAnimationParameter,
 AnimatorControllerParameterType.Bool);
        RegisterAnimatorParameter(CurrentWeapon.StartAnimationParameter,
 AnimatorControllerParameterType.Bool);
        RegisterAnimatorParameter(CurrentWeapon.DelayBeforeUseAnimationParameter,
 AnimatorControllerParameterType.Bool);
        RegisterAnimatorParameter(CurrentWeapon.DelayBetweenUsesAnimationParameter,
 AnimatorControllerParameterType.Bool);
        RegisterAnimatorParameter(CurrentWeapon.StopAnimationParameter,
 AnimatorControllerParameterType.Bool);
        RegisterAnimatorParameter(CurrentWeapon.SingleUseAnimationParameter,
 AnimatorControllerParameterType.Bool);
        RegisterAnimatorParameter(CurrentWeapon.UseAnimationParameter,
 AnimatorControllerParameterType.Bool);
    }

    /// <summary>
    ///これをオーバーライドして、キャラクターのアニメーターにパラメーターを送信します。
    ///これは、キャラクターによってサイクルごとに1回呼び出されます
    /// class、Early、normal、Late processの後)。
    /// </summary>
    public override void UpdateAnimator() {
        if (CurrentWeapon == null) { return; }

        MMAnimator.UpdateAnimatorBool(_animator, CurrentWeapon.IdleAnimationParameter, 
(CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponIdle),
 _character._animatorParameters);
        MMAnimator.UpdateAnimatorBool(_animator, CurrentWeapon.StartAnimationParameter,
 (CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponStart),
 _character._animatorParameters);
        MMAnimator.UpdateAnimatorBool(_animator, CurrentWeapon.DelayBeforeUseAnimationParameter,
 (CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponDelayBeforeUse),
 _character._animatorParameters);

        MMAnimator.UpdateAnimatorBool(_animator, CurrentWeapon.UseAnimationParameter, 
(CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponDelayBeforeUse || 
CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponUse || 
CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponDelayBetweenUses),
 _character._animatorParameters);

        MMAnimator.UpdateAnimatorBool(_animator, CurrentWeapon.SingleUseAnimationParameter,
 (CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponUse),
 _character._animatorParameters);
        MMAnimator.UpdateAnimatorBool(_animator, CurrentWeapon.DelayBetweenUsesAnimationParameter,
 (CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponDelayBetweenUses),
 _character._animatorParameters);
        MMAnimator.UpdateAnimatorBool(_animator, CurrentWeapon.StopAnimationParameter,
 (CurrentWeapon.WeaponStates.CurrentState == SwordWeapon.SwordWeaponStates.WeaponStop),
 _character._animatorParameters);
    }

ほとんどが消しただけです。
ChangeWeapon関数部分がWeaponをキャストしている部分があるので、これを近接武器専用クラス、仮にSwordWeaponという基底クラスを作って組み替えます。

元のWeaponクラスはReload、Ammo、Magazineなどの銃に関連した用語部分を消していく形です。
Animatorのパラメータ部分はCharacterHandleWeaponの項目と連動していますのでそちらに合わせるようにします。
また、Weapon States項目を近接武器用に、そちらも余計な項目を消して、enumとして新しく定義します。

       // Weapon State
        public enum SwordWeaponStates {WeaponIdle, WeaponStart, WeaponDelayBeforeUse,
 WeaponUse, WeaponDelayBetweenUses, WeaponStop }

        [Header("Use")]
        // the delay before use, that will be applied for every shot
        public float DelayBeforeUse = 0f;
        // the time (in seconds) between two shots     
        public float TimeBetweenUses = 0f;

        [Header("Position")]
        /// an offset that will be applied to the weapon once attached to 
        /// the center of the WeaponAttachment transform.
        public Vector3 WeaponAttachmentOffset = Vector3.zero;
        /// キャラクターが反転した時に同じく反転する必要があるか?
        public bool FlipWeaponOnCharacterFlip = true;
        /// FlipValue は、フリップ上でモデルの transform's localscale を乗算するために使用されます。
        /// 通常は-1, 1, 1, しかし、あなたのモデルの仕様に合わせてそれを変更することをお気軽に 
        public Vector3 FlipValue = new Vector3(-1, 1, 1);

        [Header("Hands Position")]
        /// キャラクターの左手につけるべき変換
        public Transform LeftHandHandle;
        /// キャラクターの右手につけるべき変換
        public Transform RightHandHandle;

        [Header("Effects")]
        /// 武器を使用した際に発生させるエフェクト
        public List<ParticleSystem> ParticleEffects;

        [Header("Movement")]
        /// これがtrueなら、武器がactiveな間、乗数は動きに適用されるでしょう
        public bool ModifyMovementWhileAttacking = false;
        /// 攻撃中の動きに対する乗数
        public float MovementMultiplier = 0f;

        [Header("Animation Parameter Names")]
        /// Idle:これは、武器が使用されている場合を除き、すべての時間を true になります
        public string IdleAnimationParameter;
        /// Start:武器が使用され始めているフレームにTrue
        public string StartAnimationParameter;
        /// DelayBeforeUse:武器がアクティブになっているが、まだ使用されていない場合は true
        public string DelayBeforeUseAnimationParameter;
        /// SingleUse:Shotがtrueの後、次のいずれかまたは武器の停止の前に
        public string SingleUseAnimationParameter;
        /// Use:武器が発射を開始したが、まだ停止していない各フレームで true
        public string UseAnimationParameter;
        /// DelayBetweenUses:武器が使用中の場合はTrue
        public string DelayBetweenUsesAnimationParameter;
        /// DelayBetweenUses:武器が使用中の場合はTrue
        public string DelayBetweenUsesAnimationIntegerParameter;
        /// Stop:武器が使用された後の次のいずれかの武器の使用前にTrue
        public string StopAnimationParameter;

        [Header("Sounds")]
        /// the sound to play when the weapon starts being used
        public AudioClip WeaponStartSfx;
        /// the sound to play while the weapon is in use
        public AudioClip WeaponUsedSfx;
        /// the sound to play when the weapon stops being used
        public AudioClip WeaponStopSfx;
        /// the sound to play when the weapon gets reloaded
        public AudioClip WeaponReloadSfx;
        /// the sound to play when the weapon gets reloaded
        public AudioClip WeaponReloadNeededSfx;

        /// the name of the inventory item corresponding to this weapon.
        /// Automatically set (if needed) by InventoryEngineWeapon
        public string WeaponID { get; set; }
        /// the weapon's owner
        public Character Owner { get; protected set; }
        /// the weapon's owner's CharacterHandleWeapon component
        public CharacterHandleWeapon CharacterHandleWeapon { get; set; }
        /// if true, the weapon is flipped
        public bool Flipped { get; protected set; }
        /// the WeaponAmmo component optionnally associated to this weapon
        //public WeaponAmmo WeaponAmmo { get; protected set; }

        /// 武器のステートマシン
        public MMStateMachine<SwordWeaponStates> WeaponStates;
        public HandleWeaponCategory WeaponCategory;
        protected SpriteRenderer _spriteRenderer;
        protected CharacterGravity _characterGravity;
        protected CharacterHorizontalMovement _characterHorizontalMovement;
        protected float _movementMultiplierStorage = 1f;
        protected bool _triggerReleased = false;

        protected Vector3 _weaponOffset;
        protected Vector3 _weaponAttachmentOffset;
        
        /// 武器の使用前などを測るためのカウンタ
        protected float _delayBeforeUseCounter = 0f;
        protected float _delayBetweenUsesCounter = 0f;

武器はProcessWeaponState()で状態管理がなされています。ここから少し組みなおさないといけなくなります。
銃の場合はShootRequest関数で

  • 次の弾が入っているか?
  • 弾が装填されて発射可能か?

といった状態を見て武器を使用状態に変更し攻撃を開始させます。
この辺りは近接武器なので必要ない項目になっていきますので削除していきます。

       /// <summary>
        /// Called every lastUpdate, processes the weapon's state machine
        /// </summary>
        protected virtual void ProcessWeaponState() {
~~~~
            case SwordWeaponStates.WeaponUse:
                WeaponUse();
                _delayBetweenUsesCounter = TimeBetweenUses; 
                WeaponStates.ChangeState(SwordWeaponStates.WeaponDelayBetweenUses);
                break;

            case SwordWeaponStates.WeaponDelayBetweenUses:
                _delayBetweenUsesCounter -= Time.deltaTime;
                if (_delayBetweenUsesCounter <= 0) {
                    TurnWeaponOff();
                }
                break;

            case SwordWeaponStates.WeaponStop:
                WeaponStates.ChangeState(SwordWeaponStates.WeaponIdle);
                break;
            }

        }
        /// <summary>
        /// 武器が発射できるかどうかを判断する
        /// </summary>
        protected virtual void ShootRequest() {
            // そのままWeponUseに切り替え
            WeaponStates.ChangeState(SwordWeaponStates.WeaponUse);
        }

        /// <summary>
        /// 武器を使用しているときの状態
        /// </summary>
        protected virtual void WeaponUse() {
            SetParticleEffects(true);
            SfxPlayWeaponUsedSound();
        }

        /// <summary>
        /// 武器を使用不可に
        /// </summary>
        public virtual void TurnWeaponOff() {
            _triggerReleased = true;
            SfxPlayWeaponStopSound();
            WeaponStates.ChangeState(SwordWeaponStates.WeaponStop);
            ResetMovementMultiplier();
        }

微調整はいりますがおおむねこんな感じになります。
作成したスクリプトを継承して、SwordWeaponクラスを継承した近接武器クラスMeleeAttackWeaponを新規作成します。
こちらはほぼもとのMeleeWeaponと変更は大差ないので割愛します。

このスクリプトを作成した後、キャラクターにCharacterHandleWeaponControllerを直接アタッチして使用できるようにします。

このとき、前回までのMeleeWeaponやCorgi SwordなどのWeaponを継承して作成したプレハブは使用できなくなりますので、MeleeWeaponも新しく作成し、CharacterHandleWeaponControllerのInitialWeaponに付け直すことを忘れずに行います。
f:id:blazwel:20180712212040p:plain
動きが特に変わらず、攻撃判定が行われていれば成功です。

◆今後のカスタム
上述の通り、これで最低構成の形態を引き継ぐことができるようになります。
ですが、最初に考案した通り、今後コマンドやコンボ攻撃などを実装する際にはいろいろと不都合が生じます。
Corgi Engineでは一つの武器に対して

  • 攻撃の威力
  • 攻撃のノックバック値
  • 攻撃時間、待機時間、攻撃硬直時間
  • 攻撃範囲

などが紐づいて管理されています。ですから現時点では

  • 攻撃の威力が固定のため、コンボ攻撃のメリットが少ない、攻撃の種類を増やすメリットがない
  • ノックバック値が固定のため、攻撃①(ノックバック弱)⇒攻撃②(ノックバック強)という使い分け方ができない
  • 敵を打ち上げたりといった処理(ノックバック値をY方向に)が行えない

といった不都合が生じます。
これを改善するには一つの攻撃に対して一つの武器として管理し、攻撃ごとに武器を変更する、という処理が一番構成を崩さず管理ができそうに思えます。
つまり、4種類のパラメータを武器ごとに調整して、攻撃を管理する方式ですね。
とりあえず思いつく限りはこんな感じになります。
f:id:blazwel:20180712212046p:plain
攻撃は5種類ですが、コンボ攻撃の1~2段目といった共通化できそうな部分は共通武器を使います。
最後の攻撃だけノックバックと攻撃の威力を強めたり、といった微調整ができるようにします。

こうすることで攻撃アニメーションを追加しても後に武器パラメータを変更した違う武器を用意して対応することができます。

理想としては今回追加したMeleeAttackWeaponに図にあるような攻撃カテゴリを用意し、複数の武器を用意します。
そして現在では一種類しか所持することができないCharacterHandleWeaponControllerに武器種類を最初に定義し、アニメーションごとに武器を交換します。

といった流れになりますね。
次回はこれらの実装を行うことを目標にします。

今回の記事はおもにブログでソースコードを書くには、って感じの自分用の記事になりました('ω') ソースコードの埋め込み大きさでブログ全体のサイズ変わっちゃうのなんとかならないかなぁ……。