<< 返回目錄

NetCode

網路程式碼一直是遊戲開發中最難的部分之一, 守望先鋒公開了一個有名的 GDC talk: Overwatch Gameplay Architecture and Netcode, 很好地說明了為何 ECS 在遊戲網路架構中能如魚得水, 非常建議學習, 而 Unity 的 Entities 和 NetCode 無疑也借鑑了許多設計理念。

NetCode 其同步模型是脫胎於 FPSSample, 對此不瞭解(或者對 Dedicated Server 模型比較陌生)的人可以參考這個 talk: Deep dive into networking for Unity‘s FPS Sample game, 而對於最新的 NetCode 則可以參考官方文件 或者 Introduction to the DOTS Sample and the NetCode that drives it, 本文不會涉及基礎性的, 或者太寬泛的網路相關知識, 只說說 NetCode 中一些比較有趣的部分。

另外插播一句, 對於其他的網路模型, Unity 目前短期內並未考慮, GGPO 據稱在一個 Unity 內部 hack week 實現了一個原型, 所以如果你有需要, 手擼或者找其他中介軟體吧。

Ghost 程式碼生成

因為有 ECS 資料都存在 IComponentData 裡面且都是 blitable 的前提, 同步資料程式碼的生成變得相對簡單。 ghost 是 netcode 引入的一個概念, 可以簡單地理解為需要在遊戲裡進行同步的動態 prefab, 可由

GhostAuthoringComponent

自動生成用於資料在網路側的序列化/壓縮等操作的程式碼, 當然你也可以由自定義生成程式碼的模板來擴充套件型別, 比如

GhostSnapshotValueAngle。txt

就自定義了“角度” 資料該如何序列化。

DOTS Sample 目前使用的 netcode 版本為

0。0。2-preview。1

, 如果你需要升級到當前最新的

0。1。0-preview。6

那麼注意要修改

GhostSnapshotValueAngle。txt

中的模板程式碼。

Client Prediction(客戶端預測)

傳統網路遊戲裡, 只對角色移動這樣的 owned entity 場景進行預測, 而類似Projectile(拋射物)這樣的場景通常是建議交由伺服器端處理, 不過守望先鋒裡有大量的投擲類武器, 而且作為快節奏的遊戲, Interpolation 是不可接受的 (話說PUBG 的手雷丟著是真的難受), 因此守望先鋒對火箭炮這樣的拋射物也做了客戶端預測, 從設計意圖上看, NetCode 是支援全部 Entity 的預測的, 也就是說, Projectile 也可以進行預測, 這一點和守望先鋒的設計是一致的。 不過對於 rocket league (火箭聯盟)這樣的全域性預測, 還需要做額外的 Physics 整合工作。 (關於 rocket league 強烈建議觀看這個 talk: It IS Rocket Science! The Physics of Rocket League Detailed)

Lag Compensation (延遲補償)

對於一個有品質追求的 FPS 遊戲來說, HitScan 檢測的延遲補償幾乎是必需品了, 要實現延遲補償, 必須得關注兩個資料, 一個是整個遊戲的碰撞狀態歷史, 二一個則是客戶端延遲資料, 得益於 Unity。Physics 的資料結構的靈活性, 要實現前者並不難,

PhysicsWorldHistory

裡儲存最近的歷史紀錄的相關程式碼,

AbilityAutoRifle。cs

裡你可以看到具體是如何實現的。 另外一個好訊息是, NetCode 最新版本已經將 LagCompensation 這部分程式碼從 DOTS Sample 挪到官方 package 裡了, 具體使用可以參考官方示例。 核心就這幾行程式碼:

Entities

WithoutBurst

()。

ForEach

((

DynamicBuffer

<

RayTraceCommand

>

commands

in

CommandDataInterpolationDelay

delay

=>

{

collisionHistory

GetCollisionWorldFromTick

predictingTick

LagUI

EnableLagCompensation

delay

Delay

0

out

var

collWorld

);

var

rayInput

=

new

Unity

Physics

RaycastInput

();

bool

hit

=

collWorld

CastRay

rayInput

);

if

isServer

{

// hit 註冊以服務端為準, isServer 可以透過 ServerSimulationSystemGroup 來判斷

}

else

{

// 客戶端可以根據 hit 建立相關的碰撞特效,但是不參與碰撞結果的計算(比如傷害)

}

}

Player 系統

DOTS Sample 的 Player 模組實際上包含了非常多雜的功能, 包括輸入, 相機 和 GameMode和玩家的互動等, 下面詳細說說該模組裡各部分:

LocalPlayer, Player.State, PlayerModuleSettings, PlayerControlled

前文中提到過, 由於載入動態 prefab 的機制依然不夠完善, 一方面需要 PrefabRegistry 的存在, 另一方面, 對於 package 內部需要載入外部 prefab 的情況, 目前都是寫死路徑然後用 Resource。Load 來載入, 典型的例子就是

PlayerModuleSettings

。 該 ScriptableObject 儲存了兩個 Prefab 的弱引用, 一個是 PlayerState, 用來儲存需要在客戶端伺服器端同步的玩家資料, 包括玩家id,名字,得分等資訊, 這裡有一個需要注意的是,

PlayerCharacterControl

並不在 player 模組裡, 而是在 character 模組中, 但是其 Authoring Component 則在 player 模組裡。 另一個則是 LocalPlayer, 僅僅在客戶端側初始化, 用來儲存輸入, 攝像機, HUD, UI 等系統需要的資料, 同樣與之相應的也有

LocalPlayerCharacterControl

PlayerControlled。state

用來表示當前 entity 為某 player 所控制, 同時可以透過該元件訪問到 player 的 command 資料, 於此同時

Player。State。controlledEntity

又反向引用著它, 這樣方便執行清理(比如某玩家復活/退出遊戲等操作), 要注意

PlayerControlled。state

和 character 或者 health 等目前都處於同一個 entity 上, 而 Ability 則是以子 entity 形式存在的(後續文章會詳解), 這樣設計和遊戲玩法的預期是相關的, 比如遊戲裡經常會出現更換技能這樣的情況, 而很少出現更換血條這樣的操作(實際上也就是重生了)。

目前 Player 模組和其他模組(尤其是 Character )還是有不少重疊和交叉, 在你自己實現相關係統時, 最好鬧明白如何管理 Player 和其他系統。

PlayerCameraControl

相機目前並不支援pure DOTS, 因此這塊使用的是 Hybrid 方式,

PrefabAssetManager

提供了三個

CreateEntity

方法, 其中前兩個用於建立 conversion 後的 entity-based prefab entity (具體參考這裡), 另一個則基於 GameObject 建立 entity, 後者要求 prefab 上需要有

GameObjectEntity

, 後者在目前的 Conversion Workflow 是過時的, 未來可直接使用 HybridComponent+Companion GameObject 來替代, 目前只有相機是用該方法建立的。

除此之外要注意, 相機系統的輸入和其他模組也是分開的, 也就是說, 即便把相機完全刪掉, 也不影響角色的控制, 而不是用相機的資料來驅動角色的資料(比如瞄準的角度等), 這樣設計的好處自然是伺服器端可以完全把相機模組拿掉。

PlayerModuleServer, PlayerModuleClient, PlayerSystemsClient

PlayerModuleServer

做的事情十分簡單, 就是在玩家連線後建立 PlayerState Prefab, 因為該 prefab 是 Ghost Prefab, 由 netcode 裡的程式碼負責在客戶端複製和同步, 因此客戶端無需多做什麼。

PlayerModuleClient

負責建立相機等 System, PlayerSystemsClient 裡的 System 主要用來管理和同步

LocalPlayer

Player。State

的狀態。

UserCommand

在網路遊戲的開發中, 由於要面臨丟包等意外情況, 使用者輸入的資料通常不使用基於事件的模型來處理, 而一般使用 buffered data 的方式 (比如, 假設我在 OnKeyUp 時取消移動, 而這個包卻丟了, 那麼玩家就會永遠向前移動而不會停止), netcode 中提供了 Command stream 來抽象這一模式, 所以在處理 input 時, 要求所有 input 必須有預設值, 另外對於一些複雜的輸入場景也要額外小心處理 (比如長按蓄力)。

另一個需要注意的是, 由於 input 資料的高頻特性, 頻寬成為設計資料時需要考慮的因素, 比如 一個 bool 值佔用一個位元組, 而如果用 uint 的一個 bit 來表示 bool, 則瞬間頻寬省 8 倍,

UserCommand

buttons

就是這樣設計的。 很遺憾目前還沒有為

ICommandData

自動生成序列化程式碼的能力, 所以你需要實現兩套 Serialize & Deserialize 方法, 一套是網路初次同步時呼叫, 一套是增量呼叫(用於 delta compression)。

基本上這就是 Player 模組的大致結構和設計思路了, 其他細節的建議透過閱讀程式碼來知曉, 善用 Entity Debugger 可以有效地瞭解 entity 的資料結構——這也是 ECS 架構的另一個好處, 你可以透過資料形態來反推 System 到底在幹什麼。