Fighter System
概述
FighterSystem
是所有遊戲遊玩管理的方面匯聚在一起的地方。因此它是在格鬥範例中的唯一的多用途系統,也位於遊戲迴圈的核心;任何遊戲遊玩客製化將在這裡完成。
對戰
FighterSystem
在OnInit()
中初始化全域對戰設定,方法是從與這個遊戲執行個體關聯的RuntimeConfig
來讀取。
狀態資產
所有對戰狀態都由MatchStateData
資產基礎類別的一個具體執行方式來定義。狀態被鏈接在一起以建立對戰流。
C#
public abstract unsafe partial class MatchStateData{
public FP timerLength = FP._0;
public bool ignoreInput;
public void OnEnter(Frame f){
f.Global->ignoreInput = ignoreInput;
f.Global->matchStateTimer = timerLength;
OnStateEnter(f);
}
public virtual void OnStateEnter(Frame f) { }
public void OnUpdate(Frame f) {
f.Global->matchStateTimer -= f.DeltaTime;
if (f.Global->matchStateTimer <= FP._0) {
OnStateTimerComplete(f);
} else {
OnStateUpdate(f);
}
}
public virtual void OnStateTimerComplete(Frame f) { }
public virtual void OnStateUpdate(Frame f) { }
public virtual void OnExit(Frame f) { }
public void GoToState(Frame f, AssetRefMatchStateData nextStateAsset) {
OnExit(f);
f.Global->matchState = nextStateAsset;
var nextState = f.FindAsset<MatchStateData>(nextStateAsset.Id);
nextState.OnEnter(f);
}
public static void GoToStateCall(Frame f, AssetRefMatchStateData nextStateAsset) {
var state = f.FindAsset<MatchStateData>(f.Global->matchState.Id);
state.GoToState(f, nextStateAsset);
}
初始化及更新
FighterSystem
初始化在OnInit()
中的第一個狀態,方法是使用在RuntimeConfig.startingState
欄位中找到的狀態。
在FighterSystem
的Update()
中,目前的狀態的更新也被運行,除非其中一名玩家目前被冷凍。
C#
var matchState = f.FindAsset<MatchStateData>(f.Global->matchState.Id);
var freezeList = f.ResolveList(f.Global->freeze);
if (freezeList[0] <= FP._0 && freezeList[1] <= FP._0){
matchState.OnUpdate(f);
}
freezeList
用於處理命中停止;也就是,在玩家受到命中之後暫時凍結遊戲,以協助玩家識別已經落地的命中。
流
如同以下的圖表所描述,格鬥範例執行對戰流。
RuntimeConfig
知道Starting State
,並且FighterSystem
使用該資產的參照來初始化遊戲。所有其他資產只知道它們可以過渡到的下一個可用的狀態,但是不知道在它們之前的狀態。
對戰的大部分時間將在 4_對戰活躍中狀態 中度過。當滿足其中一個結束條件(計時器已到期或至少一名玩家被擊倒),目前的對戰結束並且
移動到下一個狀態。
- 如果目前的對戰還剩下一些回合還沒遊玩,7_重新設定 狀態重新設定玩家並且開始另一個回合。
- 如果對戰已經遊玩了所有可用回合,或是一名玩家已經連續贏了2次,將開始一個新的對戰。
保持角色在邊界之內
為了確保角色留在遊玩區域之內,FighterSystem
將它們包夾在RuntimeConfig
中定義的minBounds
及maxBounds
之間。這是在KeepFightersInBounds()
方法中
完成的。
該方法也注意處理角色分離,以避免兩個模型重疊。它同時對兩個角色執行包夾檢查;這樣做是為了處理兩種邊際情況:
- 兩個角色太靠近彼此,一個角色被移回競技場邊界,模型被推入對手的角色;及
- 讓一個角色優先於另一個角色,將使得第一個角色能夠在競技場上自由移動,同時拖動第二個角色,但是第二個角色不能反過來這樣做。
拋射物及互動
在格鬥範例中,當玩家執行一個特殊移動時,將生成拋射物,該移動有一個與該移動的動畫狀態行為關聯的QTABCreateProjectile
資產。UpdateProjectiles()
使用HitBox
元件來篩選掉活躍中的拋射物,並且使用Projectile
元件上參照的ProjectileData
資產來更新它們。
C#
private static void UpdateProjectiles(Frame f){
var projectiles = f.Filter<Projectile, HitBox, Transform3D>();
while (projectiles.NextUnsafe(out EntityRef proj, out var p, out var hb, out var t)){
var projData = f.FindAsset<ProjectileData>(p->data.Id);
projData.Update(f, proj, p, t, hb);
}
}
可收集物、環境動作及其他玩家可互動元素應該以類似的方式來處理。必要的部分是在執行命中相關的方法之前更新所有可能影響命中註冊的元素。
命中
FighterSystem
執行一個自訂命中偵測/碰撞檢查。它在為了目前動畫狀態設定的,在目前執行的攻擊的命中盒及對手的傷害盒之間進行重疊檢查(請參見狀態行為編輯器頁面以取得更多資訊)。
命中註冊分為3個步驟:
- 命中偵測
- 擊倒檢查
- 攻擊傷害應用。
當執行可能導致命中的動畫時——比如發射一個拋射物(QTABCreateProjectile
)或發動一個攻擊(ATABAttackState
)——,就會建立一個附有HitBox
元件的實體。這用於針對命中偵測來執行一個重疊。
C#
// Extracted from QTABAttackState.cs
var hitBoxEntity = frame.Create();
var hitBoxComponent = new HitBox();
如果找到一個重疊:
- 向對手提供他們剛剛被命中的攻擊的資料;及,
- 銷毀攜帶目前的攻擊的命中盒資料的實體,以避免在已經落地的攻擊之後多次造成相同的攻擊處理傷害。
C#
var hitboxFilter = f.Filter<HitBox>();
while (hitboxFilter.Next(out EntityRef e, out HitBox hb)){
if (hb.active == false)
continue;
if (!f.Unsafe.TryGetPointer(hb.target, out Fighter* targetFighter)) continue;
var hurtBoxList = f.ResolveList(targetFighter->hurtBoxList);
for (int h = 0; h < hurtBoxList.Count && targetFighter->hitByAttack == false; h++){
if (!hb.bounds.Intersects(hurtBoxList[h])) continue;
if (!f.FindAsset<AttackData>(hb.attackData.Id).CanTestCollision(f, targetFighter)) continue;
targetFighter->hitByAttack = true;
targetFighter->hitPosition = FP._0_50 * (hb.bounds.Center + hurtBoxList[h].Center);
targetFighter->hitByAttackData = hb.attackData;
f.Destroy(e);
break;
}
}
為了避免出現邊際情況,當玩家在相同幀中進行投擲攻擊時,各個攻擊都會在AttackData.Priority
中獲得優先順序,該優先順序在檢查攻擊是否落地之前進行評估。
最後,使用TestIfPlayerHitByAttack()
方法來施加傷害。
C#
private static void TestIfPlayerHitByAttack(Frame f, ref QList<FP> freezeList, ref bool kod, EntityRef pE, Fighter* pF, AttackData p1AtkDataHitBy){
if (!pF->hitByAttack) return;
var pA = f.Unsafe.GetPointer<CustomAnimator>(pE);
var pT = f.Unsafe.GetPointer<Transform3D>(pE);
if (p1AtkDataHitBy.OnHit(ref freezeList, pF->index, f, pE, pF, pA, pT)) {
kod = true;
}
pF->hitByAttack = false;
}
Back to top