Lag Compensation
概述
伺服器模式
及主機模式
延遲補償解決了快節奏多人遊戲中的一個基本問題:為客戶提供所見即所得的體驗(儘管無法信任)。
問題是,網路上的機器在遊戲中沒有一台處於完全相同的刷新。從客戶端自己的角度來看,他們所看到和採取行動的依據是100%正確。一個經典的例子是檢測到遠處物體的精確射擊——即使客戶端將目標直接放在十字準線中,實際上它已經移動了。
- 如果權威伺服器僅根據其對世界的感知來進行命中檢測,那麼沒有人能有意命中任何東西。
- 如果客戶端被賦予授權,並且可以告訴伺服器他們攻擊了什麼,那麼系統將極易受到簡單的遊戲破壞漏洞的攻擊。
延遲補償允許伺服器暫時從每個客戶端的角度看待世界,並決定他們是否真的有能力進行這個不可能的射擊。不幸的是,這意味著即使目標覺得自己已經安全地躲在牆後,目標也可能被擊中。然而,這不太可能引人注目。
Fusion保留了以前命中框所在位置的歷史記錄,並知道與目前伺服器狀態相比,每個客戶端的視圖落後了多遠。 使用此歷史記錄,Fusion能透過過去的光線投射來補償延遲。
為了實現超高精度,Fusion進一步實作了延遲補償。AAA遊戲的幀率通常高於網路的刷新率,因此玩家在畫面上實際看到的通常不是離散的刷新,而是兩個刷新之間的內插補點。Fusion確切地知道延遲補償光線投射在兩個刷新之間的距離,並可以利用這一點來實現子刷新光線投射的準確性。
延遲補償功能
對於使用Fusion的遊戲開發者來說,所有這些魔法幾乎都是看不見的。所需要的只是預先組建的HitboxRoot
和Hitbox
元件。
命中框根
為了在已連網物件上設定延遲補償的命中框,必須首先將HitboxRoot
元件連接到遊戲物件的最頂部節點。HitboxRoot
用於對遊戲物件和/或其下層上的所有Hitbox
元件進行分組。
HitboxRoot
還為命中框提供了一個邊界球體,用於延遲補償系統的廣譜資料結構。此區塊可以通過根的BroadRadius
和Offset
欄位進行配置,並且必須包含所有分組的命中框,舉例而言包括它們在物件生命週期內可能因動畫而佔用的區塊。
命中框
每個Hitbox
代表一個可以透過延遲補償査詢的單個區塊。要在已連網的遊戲物件上設定延遲補償的命中框,需要執行以下步驟:
在遊戲物件的最頂部節點上需要一個
HitboxRoot
元件。具有常規Unity
Collider
元件的動態物件應與所有Hitbox
節點和靜態幾何體保持在分離的層中。這是避免撞擊動態碰撞器的最快且唯一可靠的方法,同時仍然允許延遲補償的光線投射撞擊所有靜態幾何體,而無需Hitbox
元件。完全刪除動態碰撞器不是一種選擇,因為它們是物理互動所必需的,但動態物件的延遲補償命中檢測應該完全依賴於命中框。解決方案是將動態碰撞器保持在一個可以被延遲補償光線投射忽略的層上。
注意: 命中框使用定義它們的遊戲物件層。不同的命中框不需要共享同一層,即使它們被分組在同一個HitboxRoot
下。當Hitbox
的層被登錄到延遲補償緩衝區時,它會被快取。如果稍後要更改該層,請使用Hitbox.SetLayer(int layer)
方法,用於正確更改新層並將其快取到Hitbox
中。
注意: 每個HitboxRoot
最多只能有31個Hitbox
節點。如果在單個物件或預製件中需要更多的子節點,則必須分解層次結構並將其分佈在多個根上。
命中框層次結構的具體結構完全取決於每個單獨遊戲的需求。
查詢
Fusion的延遲補償API支持將光線投射、球體重疊和框重疊作為物理査詢,語法與PhysX的對等査詢非常相似。還可以査詢特定Hitbox
的確切位置和旋轉。
C#
using System.Collections.Generic;
using Fusion;
using UnityEngine;
public class LagCompensationExampleBehaviour : NetworkBehaviour {
public float rayLength = 10.0f;
public float cooldownSeconds = 1.0f;
public LayerMask layerMask = -1;
[Networked]
public TickTimer ActiveCooldown { get; set; }
private readonly List<LagCompensatedHit> _hits = new List<LagCompensatedHit>();
public override void FixedUpdateNetwork() {
if (GetInput<NetworkInputPrototype>(out var input)) {
if (ActiveCooldown.ExpiredOrNotRunning(Runner) && input.Buttons.IsSet(NetworkInputPrototype.BUTTON_FIRE)) {
// reset cooldown
ActiveCooldown = TickTimer.CreateFromSeconds(Runner, cooldownSeconds);
// perform lag-compensated query
Runner.LagCompensation.RaycastAll(transform.position, transform.forward, rayLength, player: Object.InputAuthority, _hits, layerMask, clearHits: true);
for (var i = 0; i < _hits.Count; i++) {
// proceed with gameplay logic
}
}
}
}
}
player
參數指定應從哪個角度解析査詢,通常歸屬於Object.InputAuthority
,它控制著這個命中掃描/拋射物。換句話說,光線投射將根據與控制它的特定玩家客戶端查看的 時間框架 匹配的資料進行。所有這些都是自動發生的——不需要做任何複雜的數學運算,儘管提供精確的刷新和內插補點參數也是一種選擇。
除了査詢命中框之外,如果在命中選項中指定了IncludePhysX
標誌,則延遲補償光線投射還可以査詢常規PhysX碰撞器。請注意,在這種情況下,常規Unity碰撞器不會進行延遲補償,而是在査詢解析時以當前狀態顯示。
注意: 在同一Unity執行個體上運行多個同儕節點時,可以透過Runner.GetPhysicsScene
或GetPhysicsScene2D
擷取同儕節點各自的物理場景。這是由包括常規Unity碰撞器的延遲補償査詢自動完成的,但在多個同儕節點的脈絡中執行常規物理査詢時需要注意。
子刷新準確性
預設下,根據標準化內插補點因數(alpha)以及視覺效果更接近哪個狀態,延遲補償査詢會根據客戶端上用於視圖內插補點的「From」或「To」狀態進行解析。
儘管這種準確度水準對於許多遊戲來說已經足夠了,但透過在其命中選項中包含SubtickAccuracy
標誌,從而使用玩家在輪詢輸入時看到的精確內插補點因數,査詢這兩個刷新之間的內插補點狀態,可以進一步提高査詢的準確度。
C#
Runner.LagCompensation.OverlapSphere(position, radius, player: Object.InputAuthority, hits, options: HitOptions.SubtickAccuracy);
篩選命中
圖層遮罩和標誌
所有延遲補償査詢都使用 圖層遮罩 來定義應該考慮哪些層,並篩選掉其他圖層上的命中框。在命中選項中包含IgnoreInputAuthority
標誌將導致屬於執行査詢的玩家控制的物件(輸入授權)的所有命中框被自動忽略。
C#
// this can be cached or defined on the Inspector with a LayerMask field
var layerMask = LayerMask.GetMask("Player", "Destructible");
var options = HitOptions.IgnoreInputAuthority;
Runner.LagCompensation.Raycast(transform.position, transform.forward, rayLength, player: Object.InputAuthority, out var hit, layerMask, options);
篩選回調
還可以提供一個回調來預先處理査詢的廣譜解析找到的所有HitboxRoot
條目,從而允許更多特定於遊戲的篩選邏輯。
C#
using System.Collections.Generic;
using Fusion;
using Fusion.LagCompensation;
public class MyBehaviourFoo : NetworkBehaviour {
private readonly List<LagCompensatedHit> _hits = new List<LagCompensatedHit>();
private PreProcessingDelegate _preProcessingCachedDelegate;
private void Awake() {
// Caching a delegate avoids recurrent delegate allocation from method.
// Using a lambda expression (not a closure) will also prevent that (delegate cached by compiler).
_preProcessingCachedDelegate = PreProcessHitboxRoots;
}
public override void FixedUpdateNetwork() {
if (GetInput<NetworkInputPrototype>(out var input) && input.Buttons.IsSet(NetworkInputPrototype.BUTTON_FIRE)) {
var hitsCount = Runner.LagCompensation.RaycastAll(transform.position, transform.forward, 10, Object.InputAuthority, _hits, preProcessRoots: _preProcessingCachedDelegate);
if (hitsCount > 0) {
// proceed with gameplay logic
}
}
}
private static void PreProcessHitboxRoots(ref Query query, List<HitboxRoot> candidates, HashSet<int> preProcessedColliders) {
// HB root candidates can be iterated over (in reverse order, in order to remove entries while iterating)
for (var i = candidates.Count - 1; i >= 0; i--) {
var root = candidates[i];
if (root.name == "Please Ignore Me") {
// removed roots will not be processed any further
candidates.RemoveAt(i);
continue;
}
// it is possible to iterate over the hitboxes of each root
for (var j = 0; j < root.Hitboxes.Length; j++) {
var hb = root.Hitboxes[j];
// e.g. bypass the layer mask and Hitbox Active-state checks
if (hb.name == "Always Narrow-Check Me") {
preProcessedColliders.Add(hb.ColliderIndex);
}
}
}
}
}
物件也可以作為自訂參數與延遲補償査詢一起傳遞,以便允許預先處理方法或Lambda的自我包含而不建立封閉。
C#
var query = SphereOverlapQuery.CreateQuery(transform.position, 10.0f, player: Object.InputAuthority,
preProcessRoots: (ref Query query, List<HitboxRoot> candidates, HashSet<int> preProcessedColliders) => {
if (query.UserArgs is MyCustomUserArgs myUserArgs) {
Log.Info($"User Arg: {myUserArgs.Arg}");
}
});
query.UserArgs = new MyCustomUserArgs { Arg = 42 };
var hitCount = Runner.LagCompensation.ResolveQuery(ref query, _hits);
2D遊戲中的延遲補償
目前,Hitbox
只能被描述為3D形狀:球體或長方體。然而,透過在鎖定平面(例如XY平面)中定位和査詢這些形狀,它們仍然可以分別用於模擬2D或長方體。
C#
if (Runner.LagCompensation.Raycast(transform.position, Vector2.right, length: 10, Object.InputAuthority, out var hit)) {
if (hit.Hitbox.TryGetComponent(out NetworkRigidbody2D nrb)) {
nrb.Rigidbody.AddForceAtPosition(Vector2.up, hit.Point);
}
}
注意: 在査詢選項中使用IncludePhysX
標誌將始終査詢Unity的3D物理引擎(PhysX)。為了査詢常規的2D碰撞器,可以在同儕節點各自的Physics2D場景中執行單獨的査詢。
C#
var hitboxHitsCount = Runner.LagCompensation.RaycastAll(transform.position, Vector2.right, 10, Object.InputAuthority, hitboxHits);
var colliderHitsCount = Runner.GetPhysicsScene2D().Raycast(transform.position, Vector2.right, 10, results: colliderHits);
Back to top