コマンド技を先行入力で出せるまでのロードマップ
前回までの記事で作ってた天狗ちゃんのパリィトレーニングは、Unityroomにて公開中です。
ぜひプレイしてみてください。
これを作ったのは、モニターの遅延を測定できる手段を創りたかったからでした。シビアな設定にして遊べば、お使いのモニターの応答速度をチェックすることができるでしょう…
このパリトレ用に作ったアセットを更に作り進めて、2.5D/3Dアクションゲームのシステムを作りました。
パリィ・弾きシステムと競合せず維持したまま、特殊移動技・コンボ技を実装し、簡単に調整できるようにするのを目標にして…(なおinvectorベースで作ろうとしたけどこの仕様ではどうにもいかなかったので、ほとんどすべてを自作することにしました)
最も重要なシステムである技のキャンセル・先行入力の仕組みを自分なりに考察・構築したので、記事に記します。
コードは公開できたものではないので、概要だけです。Unityで2Dか3Dでキャラを動かす程度はプログラムできるって方向けです。
今回のやり方は、各工程を一つずつ動作テストしながら作り進めることができます。
1
まず、パンチとかキックとか技に使うアニメーションクリップをすべてプレイヤーキャラのアニメーターコントローラに放り込み、
そのアニメーションステート名(≒アニメーションクリップ名)、説明、ID、再生時間、などを設定するクラスで「モーションリスト」を作ります。
こんな感じの
[System.Serializable]
public class MotionData
{
[Header("モーション説明"), Tooltip("")]
public string Description;
[Header("モーション名"), Tooltip("アニメーターでのステート名を入力")]
public string AnimeMotionName;
[Header("モーション再生時間(sec)"), Tooltip("この時間だけ再生してデフォルト=歩行ステートに戻ります")]
public float MotionPlayTime;
}
[Header("モーションリスト")]
public MotionData[] motionData;
[Header("今再生中のモーションID 何もしてない=デフォルトなら-1")]
public int NowMotionID;
インスペクタ上だとこんな感じの
このアニメーションの再生にはアニメーターのステート遷移を使わず、animator.CrossFadeInFixedTimeで直に再生します。再生してるモーションリストの要素数をNowMotionIDに控え、モーション再生時間のタイマーをカウントします。このように↓
public void SetPoseImmdiate(int ID)
{
ID = Mathf.Clamp(ID, 0, motionData.Length);
animator.CrossFadeInFixedTime(motionData[ID].AnimeMotionName, 0.1f);
MotiontransitionTime = motionData[ID].MotionPlayTime;
MotiontransitionCount = MotiontransitionTime;
NowMotionID = ID;
}
タイマーをカウントして、モーション再生時間が終わったら(もしくは強制終了したら)デフォルトのアニメーションを再生します。(しないと歩行できなくなります)そしてNowMotionIDを-1にします。
またアニメーションが終わる前に別のアニメーションを再生させたら、モーションがキャンセルされて次のモーションが出ることになります。これで連続技を表現できます。
2
次に、技のヒットボックスと各種エフェクトをいつどこに発生させるかを記したクラスで、「ヒットボックスリスト」を作ります。
[System.Serializable]
public class HitBoxProperty
{
public string Description;
[Header("対応モーション名"), Tooltip("public class MotionDataのそれと同一にします")]
public string MotionName;
[Header("攻撃コライダー"), Tooltip("攻撃コライダーは開始時、強制的に無効化・トリガー化する")]
public Collider HitBox;
//HitBoxはたいてい別のオブジェクト=武器の先とかにつけるので、そこでの衝突情報を送れるスクリプトが必要です
public HitReciever hitReciever;
[SerializeField, Header("攻撃対象のレイヤー")]
public int HitLayer;
[Header("攻撃の特性"), Tooltip("ガード不能攻撃など作れる")]
public bool CanGuard, CanParry, CanJGuard, CanSway;
[Header("各種ダメージ量"), Tooltip("体力のダメージ、パリィレギュでのガードしたときのスタミナダメージ、弾きレギュでのガードしたときの体幹ダメージ、被ダメ時の吹っ飛び耐性しきい値")]
public float Damage, GuardDamage, TaikanDamage, impulseValue;
[Header("被ダメ時の吹っ飛びベクトル")]
public Vector3 impulse;
//ここから下は演出用です。ゲームには必須だけど今回のお題に関係が薄いので説明省略
public UnityEvent OnStartNotice;
public UnityEvent OnBeforeAttack;
public UnityEvent OnDuringAttack;
public UnityEvent OnEndAttack;
public GameObject OnHitParticle;
public GameObject OnGuardParticle;
}
[Header("ヒットボックスリスト")]
public HitBoxProperty[] HitBoxs;
[Header("確認用:今出してるヒットボックスの要素数ID")]
public int NowHitBox;
インスペクタ上だとこんな感じ(実物はこの数倍長い)
ヒットボックスを取り付ける場所はたいていRoot以外=武器の先とかにつけるので、そこからキャラ本体に衝突情報を送れるスクリプト( 上記のコードにある HitReciever )が必要です。
アニメーションクリップの始動の完璧なタイミングがスクリプトで取得できないぽいので、(詳細は下記記事に)
アニメーションステートに使われてるアニメーションクリップのイベントキーを使うことで、モーションリストに記した対応するアニメーションステート名と再生の瞬間のタイミングを取得させます。(アニメーションステートとアニメーションクリップが混乱しますがしかたない。)
/// summary>
/// イベントキーから現在再生中のアニメーションステート名を受け取り、
/// 同名のヒットボックスを有効化する
/// </summary>
public void MotionStateChange(string ClipName)
{
for (int i = 0; i < HitBoxs.Length; i++)
{
if (HitBoxs[i].MotionName == ClipName)
{
//ヒットボックスを初期化(すでに出ている場合はキャンセル処理してから)
if (NowCount > -1) { EndHitBox();}
NowHitBox = ID;
break;
}
}
}
/// <summary>
/// ヒットボックスのアクティブをアニメーションのイベントキーから操作する
/// </summary>
public void HitBoxOn()
{
HitBoxs[NowHitBox].HitBox.enabled = true;
HitBoxs[NowHitBox].OnBeginAttack.Invoke();
}
public void HitBoxOff()
{
HitBoxs[NowHitBox].HitBox.enabled = false;
}
/// <summary>
/// 攻撃が当たったらヒットボックスを無効化
/// </summary>
public void EndHitBox()
{
HitBoxs[NowHitBox].HitBox.enabled = false;
}
/// <summary>
/// 攻撃キャンセル。ヒットボックスを無効化、エフェクトも止める
/// </summary>
public void AbortHitBox()
{
//色々な終了処理 省略
}
モーションが再生されると、アニメーションクリップの0fにセットしたイベントキーからアニメーションステート名がMotionStateChange(string ClipName)に送られて、NowHitBoxが変更。それに対応する複数のヒットボックスがイベントキーHitBoxOn()・HitBoxOff()でオンオフされる。
これでモーションを再生しただけで自動でヒットボックス・エフェクト・SEが出て消えるようになります。この作りにしておくと、あとの事が楽になります。
3.
ここまでは、キャラクターコントローラがなくても動作確認できます。
コンボ技を作るにはキャラが様々な状態=ステートが変化しないと実用性に欠けるため、キャラの移動。ジャンプを制御するキャラクターコントローラと、そもそもキャラが行動可能かを管理するステートマネージャが必要になります。それぞれ別のスクリプトで作ります。
3Dアクションだと、ステートは2カテゴリあります。
歩き、走り、ジャンプなど移動に関するステートと、HPの有無(つまり死亡)、攻撃中、被ダメージ中、ダウン中、吹っ飛び中などのステートです。
両者をからめた状況を想定した管理をする必要があります。
キャラクターコントローラは、従来の3Dアクションの作り方を普通に踏襲して作ります。ステートに関しては待機と移動とジャンプ、上昇と落下だけ担当します。それ以外のステートはステートマネージャ側で多投しますが、ふっとばし攻撃・打ち上げ技を食らったり突進技をだすなど、ベクトルを渡したら自動的に重力無視のオプション付きで動けるようなメソッドをキャラクターコントローラ側で用意します。
4.
キャラクターコントローラかステートマネージャのどこか、あるいは別の場所に、
モーション名、キャンセル可能タイミング、対応するボタン入力、どの状況やタイミングで出せるか(つまり特定のステートや技ヒット中など)、現在のモーションから派生できるモーションのリスト、を記したクラスで「コンボリスト」を作ります。こんな感じ↓
[System.Serializable]
public class ActionArtsProperty
{
public string Description;//技の説明
[Header("技ID"), Tooltip("モーションリストのIDと同じにする")]
public int ID;
[SerializeField, Header("発動ボタン"), Tooltip("")]
public ActionButton ButtonAssign;
[Header("出せる状況 地上、空中、アクション、ダウン、ヒット時"), Tooltip("地上、空中、アクション中にしか出せない、ダウン中にしか出せない…の組み合わせで")]
public bool CanGround;
public bool CanAir;
public bool CanAction;
public bool CanDown;
public bool CanIsHit;
[Header("移動技の場合設定"), Tooltip("")]
public Vector3 MoveVector;
[Header("ベクトル上書き/重力無視する"), Tooltip("ジャンプ攻撃、突進技など一時的に重力を無視したり加算したいい場合必要")]
public bool OverWriteOrAdd,IgnoreGravity;
[Header("このアクションから出せる技のID"), Tooltip("MotionPose_v2と一致させる")]
public int[] CanCancelMotions;
[Header("この技のキャンセルタイミング"), Tooltip("技を出してからCanCancelTimingBeginーCanCancelTimingEndの間に発動条件を満たすと、モーションをキャンセルして次の技が出る")]
public float CanCancelTimingBegin;
public float CanCancelTimingEnd;
}
[Header("コンボリスト")]
ActionArtsProperty[] ActionArtsProperties;
[SerializeField,Header("確認用:いま出してる技ID 出してないなら-1")]
int NowWaza=-1;
[SerializeField,Header("確認用:次に出せる技ID")]
int[] ConboWazaID;
//ヒット時のみ発動できるコンボ技用のフラグ HitSenderから受ける
[SerializeField]
bool IsHit;
//ヒット時扱いする猶予時間タイマー
float IsHitTimer = 0.5f;
float IshitCount;
発動条件と実装の例を挙げると、
・特定の攻撃モーション中に特定の攻撃ボタンが押されてると出せる=コンボ攻撃
・走り中にだけ出せる=ダッシュ攻撃
・ガード時+パワーゲージが残ってる時だけ出せる=ガードキャンセル
・被ダメージ吹っ飛び時のみ出せる=受け身
・ダウン時だけ出せる=転がり起き上がり
などが考えられます。
このあと発動条件の判定メソッド、void MatchingAction()のコードを書くのですが中身はカオスすぎて載せられません。やることはひたすらforとifでコンボリストの各フラグ・ステート・変数の総チェックです。
発動条件には、一時的なパワーアップや獲得したスキルで更に技が派生できるとか色々考え付きますが、まずは簡単な条件でテストして、後でフラグと判定を追加していきます。技が増えるほど条件が重複しやすくなって調整が難しくなります。
また、コンボリストは今回はキャラクターコントローラ内に作りましたが、ScriptableObjectとかにして独立して作るのが本当はいいでしょう。(と思ったけど最適解は分かりかねます)
5.
ここまで作れてたら一応連続技は出せますが、先行入力が実装できないと快適に技は出せません。
先行入力の実装は割と簡単で、List<ボタン情報>という入力キャッシュリストを作り、inputsystemやinputmanagerで得たボタンのうち技に関する情報だけをAdd([ボタン情報])でリストに記録、技が出る状況になったらRemoveAt(0)で要素を消化、とやります。
ボタン超連射でキャッシュが無限に貯まるので、入力が一定時間途切れた、技が出せない状況が一定時間過ぎた、コンボ技が出終わった、空中>着地に切り替わった、などの条件で、入力キャッシュをリセットします。
コマンド技…レバー入力とボタンの組み合わせをやりたい場合は、これとは別系統でレバー入力キャッシュのリストを作り、236や41236とかの入力が成立したかを判定するフラグを作り、成立したら一定時間だけ保持。
コンボリストの発動条件に236成立や41236成立の項目を追加することで、実現できるでしょう。(未確認)
キャラのステート遷移とヒットボックスからの情報の受け渡しがちゃんとしていれば、特定のモーションを再生中&特定の再生時間に敵に攻撃がヒットした時だけ次の技が出せることができます。同じボタンを押しても立ち、ダッシュ、しゃがみ、ジャンプ中で別の技が出ます。
こんな解説で伝わるか怪しいですが、ここまで後付行き当たりばったりで作ってもパリィ・弾き・スウェイ・致命と競合することなく全てしっかり使えたのを確認したので、なかなか正解に使いのではと自負してます。
(理論上はニンジャガ+SEKIROのアクションが全てできる最強アクションゲームテンプレートができた)
私のモデリングとゲーム開発の最新情報は、Discoadを御覧ください。
https://discord.gg/Ur2pmF7ptw