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

news2024/11/13 16:16:16

目录

听说世界是由多个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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/480633.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GitLab统计代码量

gitlab官方文档&#xff1a;https://docs.gitlab.com/ee/api/index.html 1、生成密钥 登录gitlab&#xff0c;编辑个人资料&#xff0c;设置访问令牌 2、获取当前用户所有可见的项目 接口地址 GET请求 http://gitlab访问地址/api/v4/projects?private_tokenxxx 返回参数 …

【文章学习系列之模型】Non-stationary Transformers

本章内容 文章概况总体结构主要模块Series Stationarization&#xff08;序列平稳化模块&#xff09;De-stationary Attention&#xff08;逆平稳化注意力模块&#xff09; 实验结果消融实验总结 文章概况 《Non-stationary Transformers:Exploring the Stationarity in Time …

Docker 存储

Docker 存储 docker 默认存储方式docker 持久化存储Volumes &#xff08;卷&#xff09;简介推荐使用情况 Bind mounts &#xff08;绑定挂载&#xff09;简介推荐使用情况 绑定挂载与卷注意点 docker 非持久化存储tmpfs mounts &#xff08;tmpfs 挂载&#xff09;简介推荐使用…

leetcode100——相同的树

文章目录 题目详情分析Java完整代码 题目详情 leetcode100 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例&#xff1a; 分析 主要想法…

vue3头像上传组件

用到了自定义组件v-model的双向绑定使用input的typefile这个原生html元素&#xff0c;通过监听change事件&#xff0c;获取到选择的文件&#xff08;注意&#xff0c;选择完文件值后&#xff0c;要把这个隐藏的input的typefile元素的value置为空&#xff0c;否则&#xff0c;下…

【关于C++中----异常】

文章目录 一、C语言中处理错误的方式二、C异常概念三、异常的使用3.1 异常的抛出和捕获3.2 异常的重新抛出3.3 异常安全3.4 异常规范 四、自定义异常体系五、C标准库的异常体系六、异常的优缺点 一、C语言中处理错误的方式 C语言中常见的错误类型包括&#xff1a;语法错误、逻…

访问 virtualbox中的mysql

在 mysql.user 中存储这用户可访问的的host地址 select user,host from user;修改访问权限 可以使用sql语句 use mysql; mysql>update user set host % where user root; flush privileges或者使用mysql的权限语句 use mysql; Grant all on *.* to root% identified b…

深入浅出C++ ——异常

文章目录 一、C语言传统的处理错误的方式二、C异常概念三、异常的使用异常的抛出和捕获异常的重新抛出异常安全异常规范 四、自定义异常体系无、C标准库的异常体系六、异常的优缺点 一、C语言传统的处理错误的方式 C语言传统的处理错误的机制&#xff1a; 终止程序&#xff0…

docker打包部署spring boot应用(mysql+jar+Nginx)

文章目录 一、基本准备二、mysql部署二、jar部署三、Nginx部署 一、基本准备 小唐拿的就是之前放置在我们服务器上的应用进行部署&#xff0c;主要就是mysql和jar还有Vue的部署。 目前已经有的是jar、已经打包好的vue 项目参考&#xff1a;小破站数据大屏可视化&#xff08;…

【IPv6】IPv6有无状态地址分配及IPv6路由

IPv6有无状态区分 有状态可控、可管理。有IP地址管理者&#xff0c;能够识别客户端。根据不同客户端分配对应v6地址&#xff0c;客户端和服务器存在租期及续约。无状态无控、难管理。无IP地址管理者&#xff0c;没人识别客户端。客户端根据网关发送的相同的RA报文内容&#xf…

path/to/sdkmanager --install “cmdline-tools;latest“

执行flutter doctor时、报错Android Studio的命令行工具没有安装& 以及 android-licenses没有同意 其中提示错误语句如标题: path/to/sdkmanager --install "cmdline-tools;latest"之类的, 因为同意条款的时候,日志太多,所以把报错覆盖了.没有截图. 解决方法: …

一个简单的servlet+Jsp+MySQL/Oracle程序

一个简单的servletJspMySQL/oracle程序 1. 创建项目 使用 IDEA 创建一个 Maven 项目. 1.1、File -> New Project Name:javaservlet4 Location:选择要存放的路径 Language:Java Build system:Maven 点击Create按钮 ​​​​​​​1.2、Pom.xml配置 <dependencies…

整数在内存中的存储:原码、反码、补码 大小端字节序

本篇博客会讲解整数在内存中的存储形式&#xff0c;以及整数二进制的3种表示形式&#xff1a;原码、反码、补码&#xff0c;还有大小端的相关知识点。相信读完本篇博客&#xff0c;大家对内存的了解会上一个台阶。 注意&#xff1a;本篇博客讨论的是整数在内存中的存储&#x…

【五一创作】【数学建模】matlab的常用函数运用(1)

文章目录 1. matlab基本常识2. 常用输入输出函数2.1 输出函数2.2 拼接函数&#xff08;字符串的合并&#xff09;2.3 输入函数 3. 求和函数3.1 向量求和3.2 矩阵求和 4. 提取矩阵元素4.1 取第x行第y列的元素4.2 取指定行或列的所有元素4.3 取指定某些行的所有元素 1. matlab基本…

MongoDB聚合操作

文章目录 一、单一聚合二、聚合管道2.1 $match / $project / $count2.2 $limit / $skip / $sort2.3 $group 分组查询2.4 $unwind 展开数组2.5 $lookup 左外连接2.6 $bucket 存储桶 三、MapReduce 提示&#xff1a;以下是本篇文章正文内容&#xff0c;MongoDB 系列学习将会持续更…

域名解析出现错误,该如何解决?

域名作为网络地址&#xff0c;是我们访问网站的必经之路&#xff0c;域名解析就是把你的域名解析成一个ip地址&#xff0c;在使用的过程中遇到域名解析文件异常也是常有的事。如果域名解析出现错误&#xff0c;该怎么解决呢&#xff1f; 一、打开网页时&#xff0c;显示域名解析…

Redis主从复制、哨兵实战

环境&#xff1a;linux centos7.x &#xff0c;虚拟机3台 版本&#xff1a;redis-6.2.6 1.下载安转redis 下载地址 wget https://download.redis.io/releases/redis-6.2.6.tar.gz解压 tar -zxvf redis-6.2.6.tar.gz移动目录 mv redis-6.2.6 /usr/local/redis编译 cd /usr/…

【GORM框架】ORM介绍、GORM简单连接和高级配置详解

博主简介&#xff1a;努力学习的大一在校计算机专业学生&#xff0c;热爱学习和创作。目前在学习和分享&#xff1a;数据结构、Go&#xff0c;Java等相关知识。博主主页&#xff1a; 是瑶瑶子啦所属专栏: GORM框架学习 近期目标&#xff1a;写好专栏的每一篇文章 目录 一、简介…

配准带尺度点云的方法汇总

如果点集之间不存在缩放关系时(即尺度相同时), 可以用经典ICP( Iterative Closest Point )方法求解得到旋转矩阵R和平移向量t来进行点集对齐。 如果存在缩放关系时&#xff0c;首先估计出点集S1和S2之间的缩放倍数s, 我们就可以利用ICP算法求解。 一、尺度因子s是两个点集中线…

HashSet底层原理

特点&#xff1a;无序、不可重复 LinkedHashSet 有序、可重复&#xff08;底层通过双向链表的方式记录元素的存储顺序&#xff09; HashSet底层数据结构是哈希表 jdk1.8之前&#xff1a;哈希表组成&#xff1a;数组 链表 jdk1.8之后&#xff1a; 数组 链表 红黑树 存储…