我发现了新大陆!

引言

上文谈到Actor和Component的关系,UE利用Actor的概念组成一片游戏对象森林,并利用Component组装扩展Actor的能力,让世界里拥有了形形色色的Actor们,拥有了自由表达3D世界的能力。
那么,这些Actor们,到底是怎么组织起来的呢?

既然提到了世界,我们的直觉反应是采用一个"World"对象来包容所有的Actor们。但是当游戏的虚拟世界非常巨大时,这种方式就捉襟见肘了。首先,目前虽然PC的性能日益强大,但是依然内存也限制了不能一下子加载进所有的游戏资源;其次,因为玩家的活动和可见范围有限,为了最优性能,把即使是很远的跟玩家无关的对象也考虑进来也明显是不明智的。所以我们需要一种更细粒度的概念来划分世界。
不同的游戏引擎们,看待这个过程的角度和理念也不一样。Cocos2dx会认为游戏世界是由Scene组成的,Scene再由一个个Layer层叠表现,然后再有一个Director来导演整个游戏。Unity觉得世界也是由Scene组成的,然后一个Application来扮演上帝来LoadLevel,后来换成了SceneManager。其他的,有的会称为关卡(Level)或地图(map)等等。而UE中把这种拆分叫做关卡(Level),由一个或多个Level组成一个World。
不要觉得这种划分好像很随意,只是个名字不同而已。实际上一个游戏引擎的“世界观”关系到了一整串后续的内容组织,玩家的管理,世界的生成,变换和毁灭。游戏引擎内部的资源的加载释放也往往都是和这种划分(Level)绑定在一起的。

Level

在UE的世界中,我们之前已经有了空气(C++),土壤(UObject),物件(Actor)。而现在UE又施展神力创建了一片片大陆(Level),在这片大陆上(.map文件),Actor们秩序井然,各种地形拔地而起,植被繁茂,天空雾云缭绕,圣光普照,这也是玩家们降生开始精彩冒险的地方。

LevelAndActors.png-56.5kB

可以从ULevel的前缀U看出来Level(大陆)也确实是继承于UObject(土壤)的。那既然同属于Object下面的各Actor们都拥有了一定的智能能力(支持蓝图脚本),Level自然也得体现出大地的意志,所以默认带了一个土地公(ALevelScriptActor),允许我们在关卡里编写脚本,可以对本关卡里的所有Actor通过名字呼之则来,关卡蓝图实际上就代表着该片大陆上的运行规则。
在Level已经有了管理者之后,一开始大家都挺满意,但渐渐的就发现,好像各个Level需要的功能好像都差不多,都是修改一下光照,物理等一些属性。所以为了方便起见,UE便给每一个Level也都默认配了一个书记官(Info),他一一记录着本Level的各种规则属性,在UE需要的时候便负责相告。更重要的是,在Level需要有其他管理人员一起协助的时候,他也记录着“游戏模式”的名字来让UE可以指派。
前面我们说过,有一些Actor是不“显示”的(没有SceneComponent),是不能“摆放”到Level里的,但是它依然可以在关卡里出力。其中一个家族系列就是AInfo和其之类。今天我们只简单介绍一下跟Level直接相关的一位书记官:AWorldSettings。

Level_Settings_Options_Menu.jpg-79.2kB

其实虽然名字叫做WorldSettings,但其实只是跟Level相关,我猜可能是在上古时代,当时整个世界只有一块大陆,人们就以为当前的大陆就是整个世界,所以给这块大陆的设置就起名为WorldSettings,后来等技术进步了,发现必须有其他大陆了,这个名字已经用得太多反而不好改了,就只好遗留下来了。当然也有可能是因为当Level被添加进World后,这个Level的Settings如果是主PersistentLevel,那它就会被当作整个World的WorldSettings。
注意,Actors里也保存着AWorldSettings和ALevelScriptActor的指针,所以Actors实际上确实是保存了所有Actor。

思考:为何AWorldSettings要放进在Actors[0]的位置?而ALevelScriptActor却不用?

void ULevel::SortActorList()
{//[...]TArray<AActor*> NewActors;TArray<AActor*> NewNetActors;NewActors.Reserve(Actors.Num());NewNetActors.Reserve(Actors.Num());// The WorldSettings tries to stay at index 0NewActors.Add(WorldSettings);// Add non-net actors to the NewActors immediately, cache off the net actors to Append afterfor (AActor* Actor : Actors){if (Actor != nullptr && Actor != WorldSettings && !Actor->IsPendingKill()){if (IsNetActor(Actor)){NewNetActors.Add(Actor);}else{NewActors.Add(Actor);}}}iFirstNetRelevantActor = NewActors.Num();NewActors.Append(MoveTemp(NewNetActors));Actors = MoveTemp(NewActors);   // Replace with sorted list.// Add all network actors to the owning world//[...]
}复制代码

实际上通过这一段代码可知,Actors们的排序依据是把那些“非网络”的Actor放在前面,而把“网络可复制”的Actor们放在后面,然后加一个起始索引标记iFirstNetRelevantActor,相当于为网络Actor划分了一个缓存,从而加速了网络复制时的检测速度。AWorldSettings因为都是静态的数据提供者,在游戏运行过程中也不会改变,不需要网络复制,所以也就可以一直放在前列,而如果再加个规则,一直放在第一个的话,也能同时把AWorldSettings和其他的前列Actor们再度区分开,在需要的时候也能加速判断。ALevelScriptActor因为是代表关卡蓝图,是允许携带“复制”变量函数的,所以也有可能被排序到后列。

思考:既然ALevelScriptActor也继承于AActor,为何关卡蓝图不设计能添加Component?
观察到,平常我们在创建Actor的时候,我们蓝图界面是可以创建Component的。
那为什么在关卡蓝图里,却不能这么做(没有提供该界面功能)?
我虽然在图里标出了Level中拥有ModelComponents,但那其实只是针对BSP应用的一个子集。通过源码发现,其实UE自己也是在C++里往ALevelScriptActor添加UInputComponent来实现关卡蓝图可以响应事件。

void ALevelScriptActor::PreInitializeComponents()
{if (UInputDelegateBinding::SupportsInputDelegate(GetClass())){// create an InputComponent object so that the level script actor can bind key eventsInputComponent = NewObject<UInputComponent>(this);InputComponent->RegisterComponent();UInputDelegateBinding::BindInputDelegates(GetClass(), InputComponent);}Super::PreInitializeComponents();
}复制代码

其实既然ALevelScriptActor是个Actor,那意味着我们当然可以为它添加组件,实际上也确实可以这么做。比如你可以在关卡蓝图里这么干:

AddLevelAudioComponent.png-37.6kB

而如果你实际意识到关卡蓝图本身就是一个看不见的Actor,你就可以在上面用Actor的各种操作:

LevelGetActorLocation.png-19.6kB

在关卡蓝图里的self其实也是个Actor!虽然一般这么干也没什么毛用。
那么好好想想,为啥UE要给你这么一个关卡蓝图界面呢?

LevelBluePrint.png-16.7kB

在此,我也只能进行一番猜测,ALevelScriptActor作为一个特化的Actor,却把Components列表界面给隐藏了,说明UE其实是不希望我们去复杂化关卡构成的。
假设说UE开放了关卡Component,那么我们在创建组件时就必然要考虑一个问题:哪些是ActorComponent,哪些是LevelComponent,再怎么ALevelScriptActor本质是个Actor,但Level的概念还是要突出,ALevelScriptActor的Actor本质是要隐藏的。所以用户就会多一些心智负担,可能混淆。而如果像这样不开放,大家的思路就都转向先创建个Actor,然后再往之上添加component,思路会比较统一清晰。
再之,从游戏逻辑的组织上来说,Level其实更应该表现为一个Actor的容器。UE其实也是不鼓励在Level里编写太复杂的逻辑的。所以才接着会有了之后的GameMode,Controller那些真正的逻辑控制类(后续会再细讨论)。
所以游戏引擎也并不是说最大化的暴露一切功能给你就是最好的,有时候选择太多了反而容易出错。在这一点上,我觉得UE很好的保持了克制,为我们提供了一个优秀的清晰的不易出错的框架,同时也对高阶用户保留了灵活性。

World

终于,到了把大陆们(Level)拼装起来的时候了。可以用SubLevel的方式:

LevelsWindows.png-62kB

也支持WorldComposition的方式自动把项目里的所有Level都组合起来,并设置摆放位置:

world_layout.jpg-116.8kB

具体摆放的操作和技巧并不是本文的重点。简单本质来说,就是一个World里有多个Level,这些Level在什么位置,是在一开始就加载进来,还是Streaming运行时加载。
UE里每个World支持一个PersistentLevel和多个其他Level:

WorldAndLevel.png-39.5kB

Persistent的意思是一开始就加载进World,Streaming是后续动态加载的意思。Levels里保存有所有的当前已经加载的Level,StreamingLevels保存整个World的Levels配置列表。PersistentLevel和CurrentLevel只是个快速引用。在编辑器里编辑的时候,CurrentLevel可以指向其他Level,但运行时CurrentLevel只能是指向PersistentLevel。

思考:为何要有主PersistentLevel?
首先,World至少得有一个Level,就像你也得先出生在一块大陆上才可以继续谈起去探索别的新大陆。所以这块玩家出生的大陆就是主Level了。当然了,因为我们也可以同时配置别的Level一开始就加载进来,其实跟PersistentLevel是差不多等价的,但再考虑到另一问题:Levels拼接进World一起之后,各自有各自的worldsetting,那整个World的配置应该以谁的为主?

AWorldSettings* UWorld::GetWorldSettings( bool bCheckStreamingPesistent, bool bChecked ) const
{checkSlow(IsInGameThread());AWorldSettings* WorldSettings = nullptr;if (PersistentLevel){WorldSettings = PersistentLevel->GetWorldSettings(bChecked);if( bCheckStreamingPesistent ){if( StreamingLevels.Num() > 0 &&StreamingLevels[0] &&StreamingLevels[0]->IsA<ULevelStreamingPersistent>()) {ULevel* Level = StreamingLevels[0]->GetLoadedLevel();if (Level != nullptr){WorldSettings = Level->GetWorldSettings();}}}}return WorldSettings;
}复制代码

可以看出,World的Settings也是以PersistentLevel为主的,但这也并不以为着其他Level的Settings就完全没有作用了,本篇也无法一一列出所有配置选项来说明,简单来说,就是需要在整个世界范围内起作用的配置选项(比如VR的WorldToMeters,KillZ,WorldGravity其他大部分都是)就是需要从主PersistentLevel的配置中提取。而一些配置选项可以在单独Level中起作用的,比如在编辑Level时的光照质量配置就是一个个Level单独的,目前这种配置很少,但可能以后也会增加。在这里只是阐明一个为主其他为辅的Level配置系统。

思考:Levels们的Actors和World有直接关系吗?
当别的Level被添加进当前World之后,我们能直接在WorldOutliner里看到其他Level的Actor们。

LevelsWorldOutliner.png-31.3kB

但这并不代表着World直接引用了Level里的Actor们。TActorIteratorBase(World的Actor迭代器)内部的实现也只是在遍历Levels来获得所有Actor。当然World为了更快速的操作Controllers和Pawn也都保存了引用。但Levels却共享着World的一个PhysicsScene,这也意味着Levels里的Actors的物理实体其实都是在World里的,这也好理解,毕竟物理的碰撞之类的当然要是全局的了。再说到导航,World在拼接Level的时候,也是会同时把两个Level的导航网格给“拼接”起来的。当然目前还不是深入细节的时候,现在只要从大局上明白World-Level-Actor的关系。

思考:为什么要在Level里保存Actors,而不是把所有Map的Actors配置都生成在World一个总Actors里?
这肯定也是一种实现方式,好处是把整个World看成一个整体,所有的actors都从属于world,这样就不存在Level边界,可以更整体的处理Actors的作用范围和判定问题,实现上也少了拼接导航等步骤。当然坏处也是模糊了Level边界,这样在加载进一个Level之后,之后再动态释放,就需要再重新再从整体中抽离出部分来释放,这个筛选过程也会产生比较大的损耗。试着去理解UE的权衡,应该是尽量的把损耗平摊(这里是把Level加载释放的损耗尽量减小),才不会产生比较大的帧率波动,让玩家感觉到卡帧。

总结

Level作为Actor的容器,同时也划分了World,一方面支持了Level的动态加载,另一方面也允许了团队的实时协作,大家可以同时并行编辑不同的Level。一般而言,一个玩家从游戏开始到结束,UE会创造一个GameWorld给玩家并一直存在。玩家切换场景或关卡,也只是在这个World中加载释放不同的Level。既然Level拥有了管理者(LevelScriptActor),玩家可以编写特定关卡的逻辑,那么我们能否对World这种层次编写逻辑呢?答案是肯定的,不过本文篇幅有限,敬请期待下篇。

下篇:GamePlayer架构(三)WorldContext,GameInstance,Engine

修订

###LevelCollection 4.14
每种Type只有一个,所以其实只是分类成3个

The levels of a world are now categorized into different collections: dynamic, static, or duplicated. Levels default to dynamic, and this preserves existing behavior.
Streaming levels can be marked as static in the Levels Details panel. This distinction has no effect on how the levels are rendered, but it will cause the levels to be placed into the corresponding collection.
Games may opt-in to duplicating the dynamic levels when the world is loaded, and they will be maintained separately from the original levels. The game may use these duplicated levels at runtime, if desired.

/** Indicates the type of a level collection, used in FLevelCollection. */
enum class ELevelCollectionType
{/*** The dynamic levels that are used for normal gameplay and the source for any duplicated collections.* Will contain a world's persistent level and any streaming levels that contain dynamic or replicated gameplay actors.*/DynamicSourceLevels,/** Gameplay relevant levels that have been duplicated from DynamicSourceLevels if requested by the game. */DynamicDuplicatedLevels,/*** These levels are shared between the source levels and the duplicated levels, and should contain* only static geometry and other visuals that are not replicated or affected by gameplay.* These will not be duplicated in order to save memory.*/StaticLevels
};复制代码

UE4.13.2


知乎专栏:InsideUE4
UE4深入学习QQ群:456247757(非新手入门群,请先学习完官方文档和视频教程)
微信公众号:aboutue,关于UE的一切新闻资讯、技巧问答、文章发布,欢迎关注。
个人原创,未经授权,谢绝转载!

转载于:https://juejin.im/post/5875cc56128fe1006b4a2435

《InsideUE4》GamePlay 架构(二)Level 和 World相关推荐

  1. 《LearnUE——基础指南:上篇—2》——GamePlay架构之Level和World

    目录 听说世界是由多个Level组成的 1.2.1 引言 1.2.2 建造大陆(ULevel) 1.2.3构建世界(World) 1.2.4总结 听说世界是由多个Level组成的 1.2.1 引言 上 ...

  2. 《InsideUE4》GamePlay架构学习_Level和World

    <InsideUE4>GamePlay架构学习 Level和World 前话 Unity To UE 思考 为什么AWorldSettings[0]的位置,而ALevelScriptAct ...

  3. 《InsideUE4》GamePlay架构(十)总结

    世界那么大,我想去看看 引言 通过对前九篇的介绍,至此我们已经了解了UE里的游戏世界组织方式和游戏业务逻辑的控制.行百里者半九十,前述的篇章里我们的目光往往专注在于特定一个类或者对象,一方面固然可以让 ...

  4. UE4 GamePlay架构

    UE4 GamePlay架构 前言 GamePlay架构_1_Actor和Component GamePlay架构_2_Level和World GamePlay架构_3_GameInstance Ga ...

  5. 为何使用云原生应用架构 二 :独霸天下之四大绝技

    文章目录 为何使用云原生应用架构 二 :独霸天下之四大绝技 安全是生产的第一要素 如何才能做到即安全又快速呢? 可视化 错误隔离 容错 自动恢复 Q&A 附录 为何使用云原生应用架构 二 :独 ...

  6. 窥探现代浏览器架构(二)

    前言 本文是笔者对Mario Kosaka写的inside look at modern web browser系列文章的翻译.这里的翻译不是指直译,而是结合个人的理解将作者想表达的意思表达出来,而且 ...

  7. 【架构二】后端高可用架构演进

    单机架构 以淘宝作为例子,在网站最初时,应用数量与用户数都较少,可以把Tomcat和数据库部署在同一台服务器上. 浏览器往www.taobao.com发起请求时,首先经过DNS服务器(域名系统)把域名 ...

  8. UE4 GamePlay架构学习篇

    本帖为原创文章,转载请注明出处. 现在UE4刚免费不久,网上的资料还很少,有一些UE3的大佬出了一些学习的帖子.通过参考前辈的文章+通过查阅官方文档和官方的模板案例测试得出如下结论,供学习参考: 1& ...

  9. 经典的卷积网络架构(二)——GoogLeNet [inception v1] 详解

    GoogLeNet出自论文Going Deeper With Convolutions 如有错误,欢迎指正! (未完待续) 目录 论文阅读 补充知识点:1*1卷积的作用 网络结构分析 参考博文 参考代 ...

最新文章

  1. 39.拖曳排序插件——sortable
  2. SAX解析xml (遍历DOM树各节点)
  3. 7_linux下PHP、Apache、Mysql服务的安装
  4. windows服务与网页交互_戴尔高级主动式触控笔-PN579X评测:Windows也有好触控
  5. Python 面向对象-如何查看类的父类,外部如何获取类的名字
  6. Android 网络请求HttpURLConnection 和 HttpClient详解
  7. Matplotlib 日期格式转换
  8. Python enumerate索引迭代
  9. jdbc增删改查有哪些步骤_用Mybatis如何实现对数据库的增删改查步骤
  10. 用SHA1或MD5 算法加密数据(示例:对用户身份验证的简单实现)
  11. python 获取文件列表_Python3 - 获取文件夹中的文件列表
  12. myeclipse下使用Junit4进行单元测试
  13. 基于OpenCV的计算机视觉入门(2)图片几何变换入门
  14. PHP报错 File:E:\\...\index\\controller\\Test.php Line(18) Illegal offset type in isset or empty
  15. animate auto
  16. 项目-字典-更新字典分组
  17. nodejs下载安装及配置环境
  18. linux win10 mac地址修改,两种方法教你修改Win10专业版MAC物理地址
  19. DOM操作简易年历案例
  20. Mac版word空格变成小点,多了很多“分节符(下一页)”和“窗体顶端”和“窗体底端”等字样,怎么解决?

热门文章

  1. LeetCode OJ1:Reverse Words in a String
  2. 基于消息的异步套接字
  3. Linux下的Asp.Net配置指南
  4. ASP.NET 2.0中实现跨页面提交
  5. vue中this.$nextTick()的使用---SpringCloud Alibaba_若依微服务框架改造_ElementUI---工作笔记017
  6. 大数据_Hbase-API访问_Java操作Hbase_MR-数据迁移-代码测试---Hbase工作笔记0017
  7. 微服务升级_SpringCloud Alibaba工作笔记0007---spring gateway搭建
  8. Docker工作笔记001---Docker的简介
  9. 即时通讯学习笔记007---在windows下安装openfire_并且使用自定义的数据库这里用mysql
  10. python编程(mysql操作)