关于unreal-engine4:Exploring-in-UE4Unreal回放系统剖析下

本篇分为高低两篇,上篇内容请关注:《Exploring in UE4》Unreal回放零碎分析(上) 四、死亡回放/精彩镜头性能的实现在FPS游戏里,一个角色被击杀之后,往往会以敌方的视角回放本角色被定位、瞄准、射击的过程,这就是咱们常提到的死亡回放(DeathCameraReplay)。相似的,咱们在各种体育游戏外面常常须要在一次得分后展现精彩霎时,这种性能个别称为精彩镜头。 上一节案例应用的是基于本地文件存储的回放零碎,每次播放时都须要从新加载地图。那有没有方法实现相似实况足球的实时精彩回放呢?有的,那就是基于DuplicatedLevelCollection和内存数据流的回放计划。 思考一下,通常射击游戏里的击杀镜头、体育竞技里的精彩时刻对回放的根本需要是什么?这类回放性能往往是在某个工夫点能够无感知地立即切换到回放镜头,并在回放完结后迅速再切换到失常的游戏环境。同时,思考到联机的状况,咱们在回放时要放弃游戏世界的失常运行,从而确保不错过任何服务器的同步信息,不影响其余玩家。 简略总结就是: 能够迅速地在实在游戏与回放镜头间切换回放的时候不会影响实在游戏外面的逻辑变动4.1 回放场景与实在场景拆散为了实现上述的要求,咱们须要将回放的场景和实在的场景进行拆散,在不从新加载地图的状况下疾速地进行切换。空幻引擎给出的计划是对游戏世界World进行进一步的拆分,把所有的Level组织到了三个LevelCollection外面,别离是: DynamicSourceLevels,存储真实世界的所有标记为Dynamic的Level(蕴含外面的所有Actor)StaticLevels,存储了动态的Actor,也就是回放过程中不会发生变化的对象,通常指那些不可毁坏修建(通过关卡编辑器外面的Static选项,能够设置任何一个SubLevel是属于DynamicSourceLevels还是StaticLevels的,PersistLevel永远是Dynamic的)DynamicDuplicatedLevels,回放世界的Level(蕴含外面的所有Actor),会把DynamicSourceLevels外面的所有Level都复制一遍 在游戏地图Loading的时候,咱们就会把这三种LevelCollection全副构建并加载进来(能够通过Experimental_ShouldPreDuplicateMap来决定某张地图是否能够复制Level到DynamicDuplicatedLevels),这样在进行回放的时候咱们只有管制LevelCollection的显示和暗藏就能够霎时对真实世界和回放世界进行切换了。 判断一个对象是否处于回放世界(DynamicDuplicatedLevels)也很简略。 UWorld* World = WorldContextObject->GetWorld();ULevel* Level = Cast<ULevel>(WorldContextObject->GetTypedOuter<ULevel>());if (World && Level){ FLevelCollection* const DuplicateCollection = World->FindCollectionByType(ELevelCollectionType::DynamicDuplicatedLevels); if (DuplicateCollection) { for (auto& TempLevel : DuplicateCollection->GetLevels()) { if (TempLevel == Level) { return true; } } }}要留神的是,因为LevelCollection的引入,原来很多逻辑都变得复杂了。 不同LevelCollection的Tick是有先后顺序的,默认状况下是依照他们在数组的排列程序DynamicSourceLevels-> StaticLevels-> DynamicDuplicatedLevels,这个程序可能影响咱们的代码逻辑或者摄像机更新机会。回放世界DynamicDuplicatedLevels外面也会有很多Actor,如果不加解决的话很有可能也被录制到回放零碎中,造成嵌套录制。当一个DynamicDuplicatedLevels执行Tick的时候,会通过FScopedLevelCollectionContextSwitch来切换以后的ActiveCollection,进而批改以后World的GameState等指针,所以在回放时须要留神获取对象的正确性。(比方下图获取PC的迭代器接口,在DuplicatedLevels Tick时只能获取到回放世界的PC)。用于回放的UDemoNetDriver会绑定一个LevelCollection(通过传入PlayReplay的参数LevelPrefixOverride来决定)。当触发回放逻辑后,即UDemoNetDriver::TickDispatch每帧解析回放数据时,咱们也会通过FScopedLevelCollectionContextSwitch被动切换到以后DemoNetDriver绑定的LevelCollection,保障解析回放数据时能够通过Outer找到回放场景(DynamicDuplicatedLevels) ////3/////FScopedLevelCollectionContextSwitch::FScopedLevelCollectionContextSwitch(const FLevelCollection* const InLevelCollection, UWorld* const InWorld){ if (World) { const int32 FoundIndex = World->GetLevelCollections().IndexOfByPredicate([InLevelCollection](const FLevelCollection& Collection) { return &Collection == InLevelCollection; }); World->SetActiveLevelCollection(FoundIndex); }}void UWorld::SetActiveLevelCollection(int32 LevelCollectionIndex){ ActiveLevelCollectionIndex = LevelCollectionIndex; const FLevelCollection* const ActiveLevelCollection = GetActiveLevelCollection(); if (ActiveLevelCollection == nullptr) { return; } PersistentLevel = ActiveLevelCollection->GetPersistentLevel(); GameState = ActiveLevelCollection->GetGameState(); NetDriver = ActiveLevelCollection->GetNetDriver(); DemoNetDriver = ActiveLevelCollection->GetDemoNetDriver(); }////4////bool UDemoNetDriver::InitConnect(FNetworkNotify* InNotify, const FURL& ConnectURL, FString& Error){ const TCHAR* const LevelPrefixOverrideOption = ConnectURL.GetOption(TEXT("LevelPrefixOverride="), nullptr); if (LevelPrefixOverrideOption) { SetDuplicateLevelID(FCString::Atoi(LevelPrefixOverrideOption)); } if (GetDuplicateLevelID() == -1) { // Set this driver as the demo net driver for the source level collection. FLevelCollection* const SourceCollection = World->FindCollectionByType(ELevelCollectionType::DynamicSourceLevels); if (SourceCollection) { SourceCollection->SetDemoNetDriver(this); } } else { // Set this driver as the demo net driver for the duplicate level collection. FLevelCollection* const DuplicateCollection = World->FindCollectionByType(ELevelCollectionType::DynamicDuplicatedLevels); if (DuplicateCollection) { DuplicateCollection->SetDemoNetDriver(this); } } }4.2 回放录制与播放拆散思考到在死亡回放的时候不会影响失常较量的进行和录制,所以咱们通常也须要讲录制逻辑与播放逻辑齐全拆散。 ...

April 18, 2023 · 2 min · jiezi

关于unreal-engine4:Exploring-in-UE4Unreal回放系统剖析上

回放,是电子游戏中一项常见的性能,用于记录整个较量过程或者展现游戏中的精彩霎时。通过回放,咱们能够观摩高手之间的对决,享受游戏中的精彩霎时,甚至还能够拿到敌方玩家的较量录像进行剖析和学习。 从实现技术角度来讲,上面的这些性能实质上都属于回放的一部分 精彩霎时展现:FIFA / 实况足球 / NBA2K / 守望先锋 / 极限竞速:地平线 / 跑跑卡丁车死亡回放:守望先锋 / 彩虹六号 / 使命号召 / CODM全局较量录制、下载、播放:守望先锋 / CSGO / Dota / LOL / 魔兽争霸 / 星际争霸 / 红色警戒 / 坦克世界 / 绝地求生 / 王者光荣观战(罕用于非实时观战):CSGo / 堡垒之夜 / Dota时光倒流:Braid / 极限竞速:地平线彩虹6号中的击杀回放 早在20世纪90年代,回放零碎就曾经诞生并宽泛用于即时战略、第一人称射击以及体育竞技等类型的游戏当中,而那时存储器的容量十分无限,远远无奈与当今动辄几十T的硬盘等量齐观,面对一场数十分钟的较量,较量数据该如何存储和播放?回放该如何实现?这篇文章会通过分析UE的回放零碎,来由浅入深地帮忙大家了解其中的原理和细节。 概述其实实现回放零碎有三种思路,别离是: 逐帧录制游戏画面 播放简略,不便分享性能开销大,占用空间,不灵便逐帧录制玩家的输出操作 录制数据小,灵便跳跃、倒退艰难,计算一致性解决简单定时录制玩家以及游戏场景对象的状态 录制数据较少,开销可控,灵便逻辑简单三种计划各有优劣,但因为第一种录制画面的计划存在着“占用大量存储空间”、”加载速度慢”、“不够灵便”等比较严重的问题,咱们通常采纳后两种形式来实现游戏中的回放。 能够参考“游戏中的回放零碎是如何实现的?”来进一步理解这三种计划一、帧同步、快照同步与状态同步尽管不同游戏里回放零碎具体的实现形式与利用场景不同,但实质上都是对数据的记录和重现,这个过程与网络游戏外面的同步技术十分类似。举个例子,如果AB两个客户端进行P2P的连贯对战,A客户端上开始时并没有对于B的任何信息。当建设连贯后,B开始把本人的相干信息(坐标,模型,大小)发给A,A在本人的客户端上利用这个信息从新构建了B,实现了数据的同步。 思考一下,如果B不把这个信息发给A,而发给本人进行解决,是不是就相当于录制了本人的机器上的较量信息再进行回放呢? 没错,网络游戏中的同步信息正是回放零碎中的录制信息,因而网络同步就是实现回放零碎的技术根底! 在正式介绍回放零碎前,无妨先概括地介绍一下游戏开发中的网络同步技术。咱们常说网络同步能够简略分为帧同步、快照同步和状态同步,但实际上这几个中文概念是国内开发者一直摸索和借鉴的名词,并非严格指某种固定的算法,他们有很多变种,甚至能够联合到一起去应用。 帧同步,对应的英文概念是LockStep/Deterministic Lockstep。其基本思路是每固定距离(如0.02秒)对玩家的行为进行一次采样失去一个“Input指令” 并发送给其余所有玩家,每个玩家都缓存来自其余所有玩家的“Input指令” ,当某个玩家收到所有其余玩家的“Input指令”后,他的本地游戏状态才会推动到下一帧。 快照同步,能够翻译成Snapshot Synchronization。其思维是服务器把以后这帧整个游戏世界的状态进行一个备份,而后把这个备份发送给所有客户端,客户端依照这个备份对本人的世界状态进行批改和纠正进而实现同步。(快照,对应的英文概念是SnapShot,强调的是某一时刻的数据状态或者备份。从游戏世界的角度了解,快照就是整个世界所有的状态信息,包含对象的数量、对象的属性、地位线信息等。从每个对象的角度了解,快照就是指整个对象的各种属性,比方生命值、速度这些。所以,不同场景下快照所指的内容可能是不同的。) 状态同步,能够翻译成State(State Based)Synchronization。其思维与快照同步类似,也是服务器将世界的状态同步给客户端。但不同的是状态同步的粒度变得十分小(以对象或者对象的属性为单位),服务器不须要把一帧外面所有的对象状态进行保留和同步,只须要把客户端须要的那些对象以及须要的属性进行保留和发送即可。 拓展:快照同步其实是状态同步的前身,那时候整个游戏须要记录的数据量还不是很大,人们也天然的应用快照来代表整个世界在某一时刻的状态,通过定时地同步整个世界的快照就能够做到完满的网络同步。然而这种间接把整个世界的状态进行同步的过程是很消耗流量和性能的,思考到对象的数据是逐渐产生变动的,咱们能够只记录发生变化的那些数据,所以就有了基于delta的快照同步。更进一步的,咱们能够把整个世界拆分一下,每一帧只针对须要的对象进行delta的同步,这样就齐全将各个对象的同步拆分开来,再联合一些过滤能够进一步缩小没必要的数据同步,最初造成了状态同步的计划。更多对于网络同步技术的倒退和细节能够参考我的文章——《细谈网络同步在游戏历史中的倒退变动》。二、UE4网络同步根底在空幻引擎外面,默认实现的是一套绝对欠缺的状态同步计划,场景外面的每个对象都称为一个Actor,每个Actor都能够独自设置是否进行同步(Actor身上还能够挂N个组件,也能够进行同步),Actor某一时刻的标记Replicated属性就是所谓的状态信息。服务器在每帧Tick的时候,会去判断哪些Actor应该同步给哪些客户端,哪些属性须要进行同步,而后对立序列化成二进制(能够了解为一个以后世界状态的增量快照)发给对应的客户端,客户端在收到后还能够调用回调函数进一步解决。这种通信形式咱们称为属性同步。 此外,UE外面还有另一种通信形式叫RPC,能够像调用本地函数那样来调用远端的函数。RPC罕用于做一些跨端的事件告诉,尽管并不严格属于传统意义上状态同步的领域,但也是UE网络同步外面不可短少的一环。 为了实现下面两种同步形式,UE4通过形象分层实现了一套NetDriver + NetConnection + Channel + Actor/Uobject的同步形式(如下图)。 ...

April 11, 2023 · 3 min · jiezi