當(dāng)前位置: 首頁 > 資訊動(dòng)態(tài) > 企業(yè)動(dòng)態(tài)
回放,是電子游戲中一項(xiàng)常見的性能,用于記錄整個(gè)較量過程或者展現(xiàn)游戲中的精彩霎時(shí)。通過回放,咱們能夠觀摩高手之間的對(duì)決,享受游戲中的精彩霎時(shí),甚至還能夠拿到敵方玩家的較量錄像進(jìn)行剖析和學(xué)習(xí)。
FIFA / 實(shí)況足球 / NBA2K / 守望先鋒 / 極限競速:地平線 / 跑跑卡丁車
守望先鋒 / CSGO / Dota / LOL / 魔獸爭霸 / 星際爭霸 / 紅色警戒 / 坦克世界 / 絕地求生 / 王者光榮
早在 20 世紀(jì) 90 年代,回放零碎就曾經(jīng)誕生并寬泛用于即時(shí)戰(zhàn)略、第一人稱射擊以及體育競技等類型的游戲當(dāng)中,而那時(shí)存儲(chǔ)器的容量十分無限,遠(yuǎn)遠(yuǎn)無奈與當(dāng)今動(dòng)輒幾十 T 的硬盤等量齊觀,面對(duì)一場數(shù)十分鐘的較量,較量數(shù)據(jù)該如何存儲(chǔ)和播放?回放該如何實(shí)現(xiàn)?這篇文章會(huì)通過分析 UE 的回放零碎,來由淺入深地幫忙大家了解其中的原理和細(xì)節(jié)。
盡管不同游戲里回放零碎具體的實(shí)現(xiàn)形式與利用場景不同,但實(shí)質(zhì)上都是對(duì)數(shù)據(jù)的記錄和重現(xiàn),這個(gè)過程與網(wǎng)絡(luò)游戲外面的同步技術(shù)十分類似。舉個(gè)例子,如果 AB 兩個(gè)客戶端進(jìn)行 P2P 的連貫對(duì)戰(zhàn),A 客戶端上開始時(shí)并沒有對(duì)于 B 的任何信息。當(dāng)建設(shè)連貫后,B 開始把本人的相干信息(坐標(biāo),模型,大?。┌l(fā)給 A,A 在本人的客戶端上利用這個(gè)信息從新構(gòu)建了 B,實(shí)現(xiàn)了數(shù)據(jù)的同步。
思考一下,如果 B 不把這個(gè)信息發(fā)給 A,而發(fā)給本人進(jìn)行解決,是不是就相當(dāng)于錄制了本人的機(jī)器上的較量信息再進(jìn)行回放呢?
沒錯(cuò),網(wǎng)絡(luò)游戲中的同步信息正是回放零碎中的錄制信息,因而網(wǎng)絡(luò)同步就是實(shí)現(xiàn)回放零碎的技術(shù)根底!
在正式介紹回放零碎前,無妨先概括地介紹一下游戲開發(fā)中的網(wǎng)絡(luò)同步技術(shù)。咱們常說網(wǎng)絡(luò)同步能夠簡略分為幀同步、快照同步和狀態(tài)同步
,對(duì)應(yīng)的英文概念是 LockStep/Deterministic Lockstep。其基本思路是每固定距離(如 0.02 秒)對(duì)玩家的行為進(jìn)行一次采樣失去一個(gè)“Input 指令”并發(fā)送給其余所有玩家,每個(gè)玩家都緩存來自其余所有玩家的“Input 指令”,當(dāng)某個(gè)玩家收到所有其余玩家的“Input 指令”后,他的本地游戲狀態(tài)才會(huì)推動(dòng)到下一幀。
,能夠翻譯成 Snapshot Synchronization。其思維是服務(wù)器把以后這幀整個(gè)游戲世界的狀態(tài)進(jìn)行一個(gè)備份,而后把這個(gè)備份發(fā)送給所有客戶端,客戶端依照這個(gè)備份對(duì)本人的世界狀態(tài)進(jìn)行批改和糾正進(jìn)而實(shí)現(xiàn)同步。(快照,對(duì)應(yīng)的英文概念是 SnapShot,強(qiáng)調(diào)的是某一時(shí)刻的數(shù)據(jù)狀態(tài)或者備份。從游戲世界的角度了解,快照就是整個(gè)世界所有的狀態(tài)信息,包含對(duì)象的數(shù)量、對(duì)象的屬性、地位線信息等。從每個(gè)對(duì)象的角度了解,快照就是指整個(gè)對(duì)象的各種屬性,比方生命值、速度這些。所以,不同場景下快照所指的內(nèi)容可能是不同的。)
,能夠翻譯成 State(State Based)Synchronization。其思維與快照同步類似,也是服務(wù)器將世界的狀態(tài)同步給客戶端。但不同的是狀態(tài)同步的粒度變得十分?。ㄒ詫?duì)象或者對(duì)象的屬性為單位),服務(wù)器不須要把一幀外面所有的對(duì)象狀態(tài)進(jìn)行保留和同步,只須要把客戶端須要的那些對(duì)象以及須要的屬性進(jìn)行保留和發(fā)送即可。
拓展:快照同步其實(shí)是狀態(tài)同步的前身,那時(shí)候整個(gè)游戲須要記錄的數(shù)據(jù)量還不是很大,人們也天然的應(yīng)用快照來代表整個(gè)世界在某一時(shí)刻的狀態(tài),通過定時(shí)地同步整個(gè)世界的快照就能夠做到完滿的網(wǎng)絡(luò)同步。然而這種間接把整個(gè)世界的狀態(tài)進(jìn)行同步的過程是很消耗流量和性能的,思考到對(duì)象的數(shù)據(jù)是逐漸產(chǎn)生變動(dòng)的,咱們能夠只記錄發(fā)生變化的那些數(shù)據(jù),所以就有了基于 delta 的快照同步。更進(jìn)一步的,咱們能夠把整個(gè)世界拆分一下,每一幀只針對(duì)須要的對(duì)象進(jìn)行 delta 的同步,這樣就齊全將各個(gè)對(duì)象的同步拆分開來,再聯(lián)合一些過濾能夠進(jìn)一步縮小沒必要的數(shù)據(jù)同步,最初造成了狀態(tài)同步的計(jì)劃。更多對(duì)于網(wǎng)絡(luò)同步技術(shù)的倒退和細(xì)節(jié)能夠參考我的文章——《細(xì)談網(wǎng)絡(luò)同步在游戲歷史中的倒退變動(dòng)》。
在空幻引擎外面,默認(rèn)實(shí)現(xiàn)的是一套絕對(duì)欠缺的狀態(tài)同步計(jì)劃,場景外面的每個(gè)對(duì)象都稱為一個(gè) Actor,每個(gè) Actor 都能夠獨(dú)自設(shè)置是否進(jìn)行同步(Actor 身上還能夠掛 N 個(gè)組件,也能夠進(jìn)行同步),Actor 某一時(shí)刻的標(biāo)記 Replicated 屬性就是所謂的狀態(tài)信息。服務(wù)器在每幀 Tick 的時(shí)候,會(huì)去判斷哪些 Actor 應(yīng)該同步給哪些客戶端,哪些屬性須要進(jìn)行同步,而后對(duì)立序列化成二進(jìn)制(能夠了解為一個(gè)以后世界狀態(tài)的增量快照)發(fā)給對(duì)應(yīng)的客戶端,客戶端在收到后還能夠調(diào)用回調(diào)函數(shù)進(jìn)一步解決。這種通信形式咱們稱為屬性同步。
此外,UE 外面還有另一種通信形式叫 RPC,能夠像調(diào)用本地函數(shù)那樣來調(diào)用遠(yuǎn)端的函數(shù)。RPC 罕用于做一些跨端的事件告訴,盡管并不嚴(yán)格屬于傳統(tǒng)意義上狀態(tài)同步的領(lǐng)域,但也是 UE 網(wǎng)絡(luò)同步外面不可短少的一環(huán)。
網(wǎng)絡(luò)驅(qū)動(dòng)治理,封裝了同步 Actor 的基本操作,還包含初始化客戶端與服務(wù)器的連貫,建設(shè)屬性同步記錄表,解決 RPC 函數(shù),創(chuàng)立 Socket,構(gòu)建并治理 Connection 信息,接管數(shù)據(jù)包等等基本操作。
示意一個(gè)網(wǎng)絡(luò)連接。服務(wù)器上,一個(gè)客戶端到一個(gè)服務(wù)器的一個(gè)連貫叫一個(gè) ClientConnection。在客戶端上,一個(gè)服務(wù)器到一個(gè)客戶端的連貫叫一個(gè) ServerConnection。
數(shù)據(jù)通道,每一個(gè)通道只負(fù)責(zé)替換某一個(gè)特定類型特定實(shí)例的數(shù)據(jù)信息。比方一個(gè) ActorChannel 只負(fù)責(zé)解決對(duì)應(yīng) Actor 自身相干信息的同步,包含本身的同步以及子組件、屬性的同步、RPC 調(diào)用等。
聯(lián)合咱們后面提到的網(wǎng)絡(luò)同步技術(shù),如果咱們當(dāng)初想在游戲外面錄制一場較量要怎么做呢?是不是像快照同步一樣把每幀的狀態(tài)數(shù)據(jù)記錄下來,而后播放的時(shí)候再去讀取這些數(shù)據(jù)呢?沒錯(cuò)!利用網(wǎng)絡(luò)同步的思維,把游戲自身當(dāng)成一個(gè)服務(wù)器,游戲內(nèi)容當(dāng)成同步數(shù)據(jù)進(jìn)行錄制存儲(chǔ)即可。
當(dāng)然對(duì)于幀同步來說,咱們并不會(huì)去記錄不同時(shí)刻世界的狀態(tài)信息,而是把關(guān)注點(diǎn)放在了玩家的行為指令上(Input 隊(duì)列)。幀同步會(huì)默認(rèn)各個(gè)客戶端的初始狀態(tài)完全一致,只有保障同一時(shí)刻每個(gè)指令的雷同,那么客戶端上整個(gè)游戲世界的推動(dòng)和體現(xiàn)也應(yīng)該是齊全一樣的(須要解決浮點(diǎn)數(shù)精度、隨機(jī)數(shù)一致性問題等)。因?yàn)橹豁氁涗浲婕业男袨閿?shù)據(jù),所以一旦幀同步的框架實(shí)現(xiàn),其回放零碎的實(shí)現(xiàn)是十分不便和輕量化的。
錄制:就像服務(wù)器網(wǎng)絡(luò)同步一樣,每幀去記錄所有對(duì)象(Actor)的狀態(tài)信息,而后通過序列化的形式寫到一個(gè)緩存外面。
在 EpicLancher 外面引擎(我應(yīng)用的是 4.26 版本),創(chuàng)立一個(gè)第三人稱的模板工程命名為 MyTestRec;
與數(shù)據(jù)的存儲(chǔ)流程相同,當(dāng)咱們通過 PlayReplay 開始播放回放時(shí),須要先從對(duì)應(yīng)的 NetworkReplayStreamer 外面取出回放數(shù)據(jù),而后解析成 FQueuedDemoPacket 數(shù)組。隨后每幀在 TickDemoPlayback 依據(jù) Packet 外面的工夫戳繼續(xù)一直地進(jìn)行反序列化來復(fù)原場景外面的對(duì)象。到這里,咱們?cè)?jīng)整頓出了錄制和回放的大抵流程和入口地位。但為了能循序漸進(jìn)地分析回放零碎,我還成心暗藏了很多細(xì)節(jié),比如說 NetworkReplayStreamer 外面是如何存儲(chǔ)回放數(shù)據(jù)的?回放零碎如何做到從指定工夫開始播放的?想弄清這些問題就不得不進(jìn)一步剖析回放相干的數(shù)據(jù)結(jié)構(gòu)與組織思維。
無論通過哪種形式實(shí)現(xiàn)回放都肯定會(huì)波及到快進(jìn)、暫停、跳轉(zhuǎn)等相似的性能。然而,咱們目前應(yīng)用的形式并不能很好地反對(duì)跳轉(zhuǎn),次要問題在于空幻引擎默認(rèn)應(yīng)用增量式的狀態(tài)同步,任何一刻的狀態(tài)數(shù)據(jù)都是后面所有狀態(tài)同步數(shù)據(jù)的疊加,必須從最開始播放能力保障不失落掉兩頭的任何一個(gè)數(shù)據(jù)包。比方下圖的例子,如果我想從第 20 秒開始播放并且從第 5 個(gè)數(shù)據(jù)包開始加載,那么肯定會(huì)失落 Actor1 的創(chuàng)立與移動(dòng)信息。
數(shù)據(jù)流在錄制的時(shí)候兩頭是沒有明確宰割的,也就是所有的序列化數(shù)據(jù)都嚴(yán)密地連貫在一起的,無奈進(jìn)行拆分,只能從頭開始一點(diǎn)點(diǎn)讀取并反序列化解析。兩頭哪怕丟了一個(gè)字節(jié)的數(shù)據(jù)都可能造成前面的數(shù)據(jù)解析亂掉。
存檔點(diǎn),即一個(gè)殘缺的世界快照(相似單機(jī)游戲中的存檔),通過這個(gè)快照能夠齊全回復(fù)過后的游戲狀態(tài)。每隔一段時(shí)間(比方 30s)存儲(chǔ)一個(gè) Checkpoint。
一段間斷工夫的數(shù)據(jù)流,存儲(chǔ)著從上一個(gè) Checkpoint 到以后的所有序列化錄制數(shù)據(jù)。
通過這種形式,咱們?cè)谌魏螘r(shí)刻都能夠找到一個(gè)鄰近的全局快照(Checkpoint)并進(jìn)行加載,而后再依據(jù)最終目標(biāo)的工夫疾速地讀取后續(xù)的 Stream 信息來實(shí)現(xiàn)目標(biāo)地位的跳轉(zhuǎn)。拿后面的案例來說,因?yàn)槲耶?dāng)初在 20s 的時(shí)候能夠通過 Checkpoint 的加載而失去后面 Actor1 在以后的狀態(tài),所以能夠完滿地實(shí)現(xiàn)跳轉(zhuǎn)性能。在理論錄制的時(shí)候,ReplayHelper 的 FQueuedDemoPacket 其實(shí)有兩個(gè),別離用于存儲(chǔ) Stream 和 Checkpoint。
每次回放開始前咱們都能夠傳入一個(gè)參數(shù)用來指定跳轉(zhuǎn)的工夫點(diǎn),隨后就會(huì)開啟一個(gè) FPendingTaskHelper 的工作,依據(jù)指標(biāo)工夫找到后面最靠近的快照,并通過 UDemoNetDriver:: LoadCheckpoint 函數(shù)來反序列化復(fù)原場景對(duì)象數(shù)據(jù)(這一步實(shí)現(xiàn) Checkpoint 的加載)。
如果指標(biāo)工夫比快照的工夫要大,則須要在 ConditionallyReadDemoFrameInto PlaybackPackets 疾速地把這段時(shí)間差的數(shù)據(jù)包全副讀出來并進(jìn)行解決,默認(rèn)狀況下在一幀內(nèi)實(shí)現(xiàn),所以玩家并無感知(數(shù)據(jù)流太大的話會(huì)造成卡頓,能夠思考分幀)。
Archive 能夠翻譯成檔案,在空幻外面是用來存儲(chǔ)序列化數(shù)據(jù)的類。其中 FArchive 是數(shù)據(jù)存儲(chǔ)的基類,封裝了一些序列化 / 反序列化等操作的接口。咱們能夠通過繼承 FArchive 來實(shí)現(xiàn)自定義的序列化操作。
那這兩個(gè) Archive 具體是如何存儲(chǔ)和保護(hù)的呢?為了能有一個(gè)直觀的展現(xiàn),倡議大家先去依照 2.3 小結(jié)的形式去操作一下,而后就能夠在你工程下 /Saved/Demo/ 門路下失去一個(gè)回放的文件。這個(gè)文件次要存儲(chǔ)的就是多個(gè) Stream 和一個(gè) Checkpoint,關(guān)上后大略如下圖(因?yàn)槭切蛄谢闪?2 進(jìn)制,所以是不可讀的)
而在讀取播放時(shí),數(shù)據(jù)的解決流程會(huì)有一些差別。零碎會(huì)嘗試一次性從磁盤加載所有信息到一個(gè)用于組織回放的數(shù)據(jù)結(jié)構(gòu)中——FLocalFileReplayInfo
3.3.3 回放架構(gòu)梳理小結(jié)到此,咱們?cè)?jīng)對(duì)整個(gè)零碎有了更深刻的了解,再回頭看整個(gè)回放的流程就會(huì)清晰很多。
上個(gè)小結(jié)咱們?cè)?jīng)從架構(gòu)的角度上梳理了回放錄制的原理和過程,然而還有很多細(xì)節(jié)問題還沒有深究,比方:
這些問題看似簡略,但實(shí)現(xiàn)起來卻并不容易。比方咱們?cè)诓シ艜r(shí)須要?jiǎng)屿o切換特定的攝像機(jī)視角,那就須要曉得 UE 外面的攝像機(jī)零碎,包含 Camera 的治理、如何設(shè)置 ViewTarget、如何通過網(wǎng)絡(luò) GUID 找到對(duì)應(yīng)的指標(biāo)等,這些內(nèi)容都與游戲玩法高度耦合,因而在剖析錄制加載細(xì)節(jié)前倡議先回顧一下 UE 的 Gameplay 框架。
UE 的 Gameplay 根本是依照面向?qū)ο蟮男问絹碓O(shè)計(jì)的,波及到常見概念(類)如下:
一個(gè)可控的游戲單位,Character 相比 Pawn 多了很多人型角色的性能,比方挪動(dòng)、下蹲、跳躍等
所有攝像機(jī)相干的性能都通過 CameraManager 治理,比方攝像機(jī)的地位、攝像機(jī)觸動(dòng)成果等
概括來講,一個(gè)游戲場景是一個(gè) World,每個(gè)場景能夠拆分成很多子關(guān)卡(即 Level),咱們能夠通過配置 Gamemode 參數(shù)來設(shè)置游戲規(guī)則(只存在與于服務(wù)器),在 Gamestate 上記錄以后游戲的較量狀態(tài)和進(jìn)度。對(duì)于每個(gè)玩家,咱們個(gè)別至多會(huì)給他一個(gè)能夠管制的角色(即 Pawn/Character),同時(shí)把這個(gè)角色相干的數(shù)據(jù)存儲(chǔ)在 Playerstate 上。最初,針對(duì)每個(gè)玩家應(yīng)用惟一的一個(gè)控制器 Playercontroller 來響應(yīng)玩家的輸出或者執(zhí)行一些本地玩家相干的邏輯(比方設(shè)置咱們的察看對(duì)象 VIewTarget,會(huì)調(diào)用到 Camermanager 相干接口)。此外,PC 是網(wǎng)絡(luò)同步的要害,咱們須要通過 PC 找到網(wǎng)絡(luò)同步的中心點(diǎn)進(jìn)而剔除不須要同步的對(duì)象,服務(wù)器也須要依附 PC 能力判斷不同的 RPC 應(yīng)該發(fā)給哪個(gè)客戶端。
回放零碎 Gameplay 邏輯仍然遵循 UE 的根底框架,但因?yàn)橹徊暗綌?shù)據(jù)的播放還是有不少須要留神的中央。
在一個(gè) Level 里,有一些對(duì)象是默認(rèn)存在的,稱為 StartupActor。這些對(duì)象的錄制與回放可能須要非凡解決,比方回放一開始就默認(rèn)創(chuàng)立,盡量避免動(dòng)靜的結(jié)構(gòu)開銷。
UE 的網(wǎng)絡(luò)同步自身須要借助 Controller 定位到 ViewTarget(同步核心,便于做范疇剔除),所以回放錄制時(shí)會(huì)創(chuàng)立一個(gè)新的 DemoPlayerController(留神:因?yàn)樵诒镜乜赡芡瑫r(shí)存在多個(gè) PC,獲取 PC 時(shí)不要拿錯(cuò)了)。這個(gè) Controller 的主要用途就是輔助同步邏輯,而且會(huì)被錄制到回放數(shù)據(jù)外面。
回放零碎并不限度你的察看視角,然而會(huì)默認(rèn)提供一個(gè)自在挪動(dòng)的觀戰(zhàn)對(duì)象(SpectatorPawn)。當(dāng)咱們播放時(shí)會(huì)收到同步數(shù)據(jù)并創(chuàng)立 DemoPC,DemoPC 會(huì)從 GameState 上查找 SpectatorClass 配置并生成一個(gè)用于觀戰(zhàn)的 Pawn。咱們通常會(huì) Possess 這個(gè)對(duì)象并挪動(dòng)來管制攝像機(jī)的視角,當(dāng)然也能夠把觀戰(zhàn)視角鎖定在游戲中的其余對(duì)象上。
回放不倡議錄制 PlayerController(簡稱 PC),游戲中的與玩家角色相干的數(shù)據(jù)也不應(yīng)該放在 PC 上,最好放在 PlayerState 或者 Character 下面。為什么回放不解決 PC?次要起因是每個(gè)客戶端只有一個(gè) PC。如果我在客戶端下面錄制回放并且把很多重要數(shù)據(jù)放在 PC 上,那么當(dāng)你回放的時(shí)候其余玩家 PC 上的數(shù)據(jù)你就無奈拿到。
回放不會(huì)錄制 Gamemode,因?yàn)?Gamemode 只在服務(wù)器才有,并不做同步。
RPC 的目標(biāo)是跨端近程調(diào)用,九游娛樂平臺(tái)對(duì)于非多播的 RPC,他只會(huì)在某一個(gè)客戶端或者服務(wù)器下面執(zhí)行。也就是說,我在服務(wù)器上錄制就拿不到客戶端的 RPC,我在客戶端上錄制就拿不到服務(wù)器上的 RPC,總會(huì)失落掉一些 RPC。
RPC 是冗余的,可能咱們?cè)诨胤诺臅r(shí)候不想調(diào)用。比方服務(wù)器觸發(fā)了一個(gè) ClientRPC(讓客戶端播放攝像機(jī)觸動(dòng))并錄制,那么回放的時(shí)候我作為一個(gè)觀戰(zhàn)的視角不應(yīng)該調(diào)用這個(gè) RPC(當(dāng)然也能夠自定義的過濾掉)。
RPC 是一個(gè)無狀態(tài)的告訴,一旦錯(cuò)過了就再也無奈獲取。回放中常常會(huì)有工夫的跳轉(zhuǎn),跳轉(zhuǎn)之后咱們?cè)倬蜔o奈拿到后面的 RPC 了。如果咱們適度依賴 RPC 做邏輯解決,就很容易呈現(xiàn)回放體現(xiàn)不對(duì)的狀況。
綜上所述,我并不倡議在反對(duì)回放零碎的游戲外面頻繁應(yīng)用 RPC,最好應(yīng)用屬性同步來代替,這樣也能很好的反對(duì)斷線重連。
4)導(dǎo)出所有同步屬性根本信息 FieldExport GroupMap,用于播放時(shí)精確且能兼容地接管這些屬性
調(diào)用 ProcessReplayTasks 解決以后正在執(zhí)行的工作,如果工作沒有實(shí)現(xiàn)就返回(工作有很多種,比方 FGotoTime InSecondsTask 就是用來執(zhí)行工夫跳轉(zhuǎn)的工作)
拿到 StreamArchive,設(shè)置以后回放的工夫(回放工夫決定了以后回放數(shù)據(jù)加載的進(jìn)度)
去 PlaybackPackets 查找是否還有待處理的數(shù)據(jù),如果沒有數(shù)據(jù)就暫?;胤?/p>
ConditionallyReadDemo FrameIntoPlaybackPackets 依據(jù)以后的工夫,讀取 StreamArchive 外面的數(shù)據(jù),緩存到 PlaybackPackets 數(shù)組外面
ConditionallyProcess PlaybackPackets 一一去解決 PlaybackPackets 外面的數(shù)據(jù),進(jìn)行反序列化的操作(這一步是還原數(shù)據(jù)的要害,回放 Actor 的創(chuàng)立通常是這里觸發(fā)的)
FinalizeFastForward 解決快進(jìn)等操作,因?yàn)樵蹅兛赡茉谝粠臅r(shí)候解決了回放 N 秒的數(shù)據(jù)(也就是快進(jìn)),所以這里須要把被快進(jìn)掉的回調(diào)函數(shù)(OnRep)都執(zhí)行到,同時(shí)記錄到底快進(jìn)了多少工夫
在 2.3.2 大節(jié),咱們提到了 UE 的網(wǎng)絡(luò)同步形式為增量式的狀態(tài)同步,任何一刻的狀態(tài)數(shù)據(jù)都是后面所有狀態(tài)同步數(shù)據(jù)的疊加,所以必須從最開始播放能力保障不失落掉兩頭的任何一個(gè)數(shù)據(jù)包。想要實(shí)現(xiàn)快進(jìn)和工夫跳躍必須通過加載最近的 Checkpoint 能力實(shí)現(xiàn)。
在每次開始回放前,咱們能夠給回放指定一個(gè)指標(biāo)工夫,而后回放零碎就會(huì)創(chuàng)立一個(gè)
?;舅悸肥窍日业阶蠼囊粋€(gè) Checkpoint(快照點(diǎn))加載,而后疾速讀取從 Checkpoint 工夫到指標(biāo)工夫的數(shù)據(jù)包進(jìn)行解析。這個(gè)過程中有很多細(xì)節(jié)須要了解,比方咱們從 20 秒跳躍到 10 秒的時(shí)候,20 秒時(shí)刻的 Actor 是不是都要?jiǎng)h除?刪除之后要如何再去創(chuàng)立一個(gè)新的和 10 秒時(shí)刻截然不同的 Actor?無妨帶著這些問題去了解上面的流程。
3)刪除掉所有非 StartUp(StartUp:一開始擺在場景里的)的 Actor,StartUp 依據(jù)狀況選擇性刪除(在跳轉(zhuǎn)進(jìn)度的時(shí)候,整個(gè)場景的 Actor 可能曾經(jīng)齊全不一樣了,所以最好全副刪除,對(duì)于擺在場景外面的可毀壞墻,如果沒有產(chǎn)生過變動(dòng)能夠無需解決,如果被打壞了則須要?jiǎng)h除從新創(chuàng)立)。
除此之外,咱們還提到了回放的暫停。其實(shí)暫停分為兩種,一種是暫停回放數(shù)據(jù)的錄制 / 讀取,通過 UDemoNetDriver:: PauseRecording 能夠?qū)崿F(xiàn)暫停回放的錄制,通過 PauseChannels 能夠暫停回放所有 Actor 的體現(xiàn)邏輯(個(gè)別是在加載 Checkpoint、快進(jìn)、沒有數(shù)據(jù)讀取時(shí)主動(dòng)調(diào)用),然而不會(huì)進(jìn)行 Tick 等邏輯執(zhí)行。另一種暫停是暫停 Tick 更新(也能夠用于非回放世界),通過 AWorldSetting:: SetPauserPlayerState 實(shí)現(xiàn),這種暫停不僅會(huì)進(jìn)行回放數(shù)據(jù)包的讀取,還會(huì)進(jìn)行 WorldTick 的更新,包含動(dòng)畫、挪動(dòng)、粒子等,是嚴(yán)格意義上的暫停。
3.5.1 回放兼容性的意義回放的錄制和播放往往不是一個(gè)機(jī)會(huì),玩家可能了回放后過了幾天才想起來觀看,甚至還會(huì)用曾經(jīng)降級(jí)到 5.0 的游戲版本去播放 1.0 時(shí)的回放數(shù)據(jù)。因而,咱們須要有一個(gè)機(jī)制來盡可能地兼容過來一段時(shí)間游戲版本的回放數(shù)據(jù)。
因?yàn)榇a在迭代的時(shí)候,函數(shù)流程、數(shù)據(jù)格式、類的成員等都會(huì)發(fā)生變化(減少、刪除、批改),游戲邏輯是必須要依賴這些內(nèi)容能力正確執(zhí)行。舉個(gè)例子,如果 1.0 版本的代碼中類 ACharacter 上有一個(gè)成員變量 FString CurrentSkillName 記錄了游戲角色以后的技能名字,在 2.0 版本的代碼時(shí)咱們把這個(gè)成員刪掉了。因?yàn)樵?1.0 版本錄制的數(shù)據(jù)外面存儲(chǔ)了 CurrentSkillName,咱們?cè)趹?yīng)用 2.0 版本代碼執(zhí)行的時(shí)候必須得想方法繞過這個(gè)成員,因?yàn)檫@個(gè)值在以后版本外面沒有任何意義,強(qiáng)行應(yīng)用的話可能造成回放失常的數(shù)據(jù)被籠罩掉。
其實(shí)不只是回放,咱們?nèi)粘T趹?yīng)用編輯器等工具時(shí),只有同時(shí)波及到對(duì)象的序列化(通用點(diǎn)來講是固定格局的數(shù)據(jù)存儲(chǔ))以及版本迭代就肯定會(huì)遇到相似的問題,輕則導(dǎo)致引擎資源有效重則產(chǎn)生解體。
在 UE 的回放零碎外面,兼容性的問題還要更簡單一些,因?yàn)椴暗搅丝栈镁W(wǎng)絡(luò)同步的實(shí)現(xiàn)原理。
第一節(jié)咱們談到了空幻有屬性同步和 RPC 兩種同步形式,且二者都是基于 Actor 來實(shí)現(xiàn)的。在每個(gè) Actor 同步的時(shí)候,咱們會(huì)給每個(gè)類創(chuàng)立一個(gè) FClassNetCache 用于惟一標(biāo)識(shí)并緩存他的同步屬性,每個(gè)同步屬性 /RPC 函數(shù)也會(huì)被惟一標(biāo)識(shí)并緩存其相干數(shù)據(jù)在 FFieldNetCache 構(gòu)造外面。因?yàn)橥环莅姹镜目蛻舳舜a和服務(wù)器代碼雷同,咱們就能夠保障客戶端與服務(wù)器每個(gè)類的 FClassNetCache 以及每個(gè)屬性的 FFieldNetCache 都是雷同的。這樣在同步的時(shí)候咱們只須要在服務(wù)器上序列化屬性的 Index 就能夠在客戶端反序列化的時(shí)候通過 Index 找到對(duì)應(yīng)的屬性。
這種計(jì)劃的實(shí)現(xiàn)前提是客戶端與服務(wù)器的代碼必須是一個(gè)版本的。如果客戶端的類成員與服務(wù)器對(duì)應(yīng)的類成員不同,那么這個(gè) Index 在客戶端上所代表的成員就與服務(wù)器上的不統(tǒng)一,最終的執(zhí)行后果就是謬誤的。所以對(duì)于失常的游戲來說,咱們必須要放棄客戶端與服務(wù)器版本雷同。然而對(duì)于回放這種可能跨版本執(zhí)行的狀況就須要有一個(gè)新的兼容計(jì)劃。
盡管這種形式減少了同步的開銷和老本,但對(duì)于回放零碎來說是能夠承受的,而且回放的整個(gè)錄制過程中是齊全牢靠的,不會(huì)因?yàn)閬G包而產(chǎn)生播放時(shí)導(dǎo)出數(shù)據(jù)沒收到的狀況。這樣即便我新版本的對(duì)象屬性數(shù)量發(fā)生變化(比方程序發(fā)生變化),因?yàn)槲以诨胤艛?shù)據(jù)外面曾經(jīng)存儲(chǔ)了這個(gè)對(duì)象所有會(huì)被序列化的屬性信息,我肯定能找到對(duì)應(yīng)的同步屬性,而對(duì)于曾經(jīng)被刪掉的屬性,我回放時(shí)本地代碼創(chuàng)立的 FClassNetCache 不蘊(yùn)含它,因而也不會(huì)被利用進(jìn)來。
到這里,咱們基本上能夠了解空幻引擎對(duì)回放零碎的向后兼容性計(jì)劃。然而即便有了下面的計(jì)劃,咱們其實(shí)也只是兼容了類成員產(chǎn)生扭轉(zhuǎn)的狀況,保障了不會(huì)因?yàn)閷傩允涠尸F(xiàn)邏輯的謬誤執(zhí)行。然而對(duì)于新增的屬性,因?yàn)樵瓉泶鎯?chǔ)的回放文件外面基本不存在這個(gè)數(shù)據(jù),回放的時(shí)候是齊全不會(huì)有任何相干的邏輯的。因而,所謂回放零碎的兼容也只是有肯定限度的兼容,想很好地反對(duì)版本差別過大的回放文件還是絕對(duì)艱難許多的。
這是侑虎科技第 1367 篇文章,感激作者 Jerish 供稿。歡送轉(zhuǎn)發(fā)分享,未經(jīng)作者受權(quán)請(qǐng)勿轉(zhuǎn)載。如果您有任何獨(dú)到的見解或者發(fā)現(xiàn)也歡送分割咱們,一起探討。(QQ 群:465082844)