目录
听说世界是由多个Level组成的
1.2.1 引言
1.2.2 建造大陆(ULevel)
1.2.3构建世界(World)
1.2.4总结
听说世界是由多个Level组成的
1.2.1 引言
上小节谈到Actor和Component的关系,UE利用Actor的概念组成了世间万物,并利用Component组装扩展Actor的能力,让三维游戏世界里有了形形色色的Actor们,拥有了自由表达3D世界的能力。
那么,这些Actor们,到底是怎么组织起来的呢?既然提到了世界,我们的直觉反应是采用一个"World"对象来包容所有的Actor们。但是当游戏的虚拟世界非常巨大时,这种方式就捉襟见肘了。首先,目前虽然PC的性能日益强大,但是依然内存也限制了不能一下子加载进所有的游戏资源;其次,因为玩家的活动和可见范围有限,为了最优性能,把即使是很远的跟玩家无关的对象也考虑进来也明显是不明智的。所以我们需要一种更细粒度的概念来划分世界。这时,一个新的角色出现了,在UE中,我们称它为关卡(Level),关卡可以看做是一块块的大陆。由一个或多个Level组成一个World,游戏引擎内部的资源的加载释放也往往都是和这种划分(Level)绑定在一起的。
1.2.2 建造大陆(ULevel)
ULevel结构如下:
可以从ULevel的前缀U看出来Level(大陆)也确实是继承于UObject(土壤)的。那既然同属于Object下面的各Actor们都拥有了一定的智能能力(支持蓝图脚本),Level自然也得体现出大地的意志,所以默认带了一个土地公(ALevelScriptActor),允许我们在关卡里编写脚本,可以对本关卡里的所有Actor通过名字呼之则来,关卡蓝图实际上就代表着该片大陆上的运行规则。在Level已经有了管理者之后,一开始大家都挺满意,但渐渐的就发现,好像各个Level需要的功能好像都差不多,都是修改一下光照,物理等一些属性。所以为了方便起见,UE便给每一个Level也都默认配了一个书记官(Info),他一一记录着本Level的各种规则属性,在UE需要的时候便负责相告。更重要的是,在Level需要有其他管理人员一起协助的时候,他也记录着“游戏模式”的名字来让UE可以指派。前面我们说过,有一些Actor是不“显示”的(没有SceneComponent),是不能“摆放”到Level里的,但是它依然可以在关卡里出力。其中一个家族系列就是AInfo和其之类。今天我们只简单介绍一下跟Level直接相关的一位书记官:AWorldSettings。
其实虽然名字叫做WorldSettings,但其实只是跟Level相关,我猜可能是在上古时代,当时整个世界只有一块大陆,人们就以为当前的大陆就是整个世界,所以给这块大陆的设置就起名为WorldSettings,后来等技术进步了,发现必须有其他大陆了,这个名字已经用得太多反而不好改了,就只好遗留下来了。当然也有可能是因为当Level被添加进World后,这个Level的Settings如果是主PersistentLevel,那它就会被当作整个World的WorldSettings。
注意,Actors里也保存着AWorldSettings和ALevelScriptActor的指针,所以Actors实际上确实是保存了所有Actor。
思考:为何AWorldSettings要放进在Actors[0]的位置?而ALevelScriptActor却不用?
先看一段代码:
void ULevel::SortActorList()
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_Level_SortActorList);
if (Actors.Num() == 0)
{
// No need to sort an empty list
return;
}
LLM_REALLOC_SCOPE(Actors.GetData());
TArray<AActor*> NewActors;
TArray<AActor*> NewNetActors;
NewActors.Reserve(Actors.Num());
NewNetActors.Reserve(Actors.Num());
if (WorldSettings)
{
// The WorldSettings tries to stay at index 0
NewActors.Add(WorldSettings);
if (OwningWorld != nullptr)
{
OwningWorld->AddNetworkActor(WorldSettings);
}
}
// Add non-net actors to the NewActors immediately, cache off the net actors to Append after
for (AActor* Actor : Actors)
{
if (Actor != nullptr && Actor != WorldSettings && !Actor->IsPendingKill())
{
if (IsNetActor(Actor))
{
NewNetActors.Add(Actor);
if (OwningWorld != nullptr)
{
OwningWorld->AddNetworkActor(Actor);
}
}
else
{
NewActors.Add(Actor);
}
}
}
NewActors.Append(MoveTemp(NewNetActors));
// Replace with sorted list.
Actors = MoveTemp(NewActors);
}
实际上通过这一段代码可知,Actors们的排序依据是把那些“非网络”的Actor放在前面,而把“网络可复制”的Actor们放在后面,然后加一个起始索引标记iFirstNetRelevantActor,相当于为网络Actor划分了一个缓存,从而加速了网络复制时的检测速度。AWorldSettings因为都是静态的数据提供者,在游戏运行过程中也不会改变,不需要网络复制,所以也就可以一直放在前列,而如果再加个规则,一直放在第一个的话,也能同时把AWorldSettings和其他的前列Actor们再度区分开,在需要的时候也能加速判断。ALevelScriptActor因为是代表关卡蓝图,是允许携带“复制”变量函数的,所以也有可能被排序到后列。
1.2.3构建世界(World)
终于,我们可以把大陆们(Level)拼装起来的时候了。可以用SubLevel的方式:我们先看World的结构:
UE里每个World支持一个PersistentLevel和多个其他Level。
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( const bool bCheckStreamingPersistent, const bool bChecked ) const
{
checkSlow(!IsInActualRenderingThread());
AWorldSettings* WorldSettings = nullptr;
if (PersistentLevel)
{
WorldSettings = PersistentLevel->GetWorldSettings(bChecked);
if( bCheckStreamingPersistent )
{
if( StreamingLevels.Num() > 0 &&
StreamingLevels[0] &&
StreamingLevels[0]->IsA<ULevelStreamingPersistent>())
{
ULevel* Level = StreamingLevels[0]->GetLoadedLevel();
if (Level != nullptr)
{
WorldSettings = Level->GetWorldSettings(bChecked);
}
}
}
}
return WorldSettings;
}
可以看出,World的Settings也是以PersistentLevel为主的,但这也并不意味着其他Level的Settings就完全没有作用了,本篇也无法一一列出所有配置选项来说明,简单来说,就是需要在整个世界范围内起作用的配置选项(比如VR的WorldToMeters,KillZ,WorldGravity其他大部分都是)就是需要从主PersistentLevel的配置中提取。而一些配置选项可以在单独Level中起作用的,比如在编辑Level时的光照质量配置就是一个个Level单独的,目前这种配置很少,但可能以后也会增加。在这里只是阐明一个为主其他为辅的Level配置系统。
思考:Levels们的Actors和World有直接关系吗?
当别的Level被添加进当前World之后,我们能直接在WorldOutliner里看到其他Level的Actor们。但这并不代表着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加载释放的损耗尽量减小),才不会产生比较大的帧率波动,让玩家感觉到卡帧。
1.2.4总结
Level作为Actor的容器,同时也划分了World,一方面支持了Level的动态加载,另一方面也允许了团队的实时协作,大家可以同时并行编辑不同的Level。一般而言,一个玩家从游戏开始到结束,UE会创造一个GameWorld给玩家并一直存在。玩家切换场景或关卡,也只是在这个World中加载释放不同的Level。既然Level拥有了管理者(LevelScriptActor),玩家可以编写特定关卡的逻辑。
上篇: 《LearnUE——基础指南:上篇—1》——GamePlay架构之Actor和Component