1、3D显示原理
逻辑-几何体-光栅化处理-像素渲染。
传统游戏靠LOD来降低机能消耗,而UE5内置了类似的虚拟几何体功能,自动降低负载。
即时光线追踪,效果更真实,降低美术工时,但机能压力大。
而传统方式是预先烘焙贴图,造成高工时低效率,优点是机能消耗小。
2、事件
事件-函数-动作-变量。
Actor 是可以添加到关卡的游戏对象,通过使用动作来响应事件。
-
碰撞事件:当两个 Actor 发生碰撞或重叠时执行。
-
输入事件:这些事件由输入设备触发,例如键盘、鼠标、触摸屏和游戏手柄。
-
事件开始运行(Event BeginPlay):在游戏开始时执行,对于已经存在于关卡编辑器上的 Actor,或者如果在运行时发生,则在生成 Actor 之后立即执行。
-
Event End Play:在运行时将要移除 Actor 时执行。
-
Event Tick:这被称为游戏的每一帧。例如:如果游戏以每秒 60 帧的速度运行,则该事件将在一秒钟内被调用 60 次。
在创建蓝图脚本时,有时,一组动作会在蓝图中的多个地方使用。这些动作可以转化为宏或函数
函数的优点之一是,在一个蓝图中创建的函数可以从另一个蓝图中调用。
3、变量类型
-
布尔值(Boolean):只能保存真或假。
-
字节(Byte):这是一个 8 位数字。它可以存储 0 到 255 之间的整数值。
-
整数(Integer):这是一个 32 位数字。它可以存储介于 -2,147,483,648 和 2,147,483,647 之间的整数值。
-
Integer64:这是一个 64 位数字。它可以存储介于 -9,223,372,036,854,775,808 和 9,223,372,036,854,775,807 之间的整数值。
-
浮点(Float):这是一个 32 位浮点数。它可以存储带小数部分的数值,精度为七位小数。
-
Double:这是一个 64 位浮点数。它可以存储带小数部分的数值,精度为 16 位小数。
-
命名(Name):用作对象标识符的一段文本。
-
字符串(String):可以存储一组字母数字字符。
-
文本(Text):此类型用于将被本地化的文本,这意味着它可以更轻松地实现不同语言的翻译。
-
向量(Vector):包含代表 3D 向量的 X、Y 和 Z 浮点值。
-
旋转体(Rotator):包含 X(滚动)、Y(俯仰)和 Z(偏航)浮点值,它们表示 3D 空间中的旋转。
-
转变(Transform):可以存储位置、旋转和缩放。
4、常用运算符
add, subtract, multiply divide 加减乘除,算术运算
Greater than,less than,equal 大于小于等于,以及衍生出<= >= !=,关系运算
++ -- 自增加1,自减少1
逻辑运算
-
OR:如果任何输入值为真,则返回真值。
-
AND:当且仅当所有输入值都为 True 时,才返回 True 值。
-
NOT:只接收一个输入值,结果将是相反的值。
-
NOR:这是 NOT 和 OR 运算符的组合。如果两个输入都为 False,则返回 True,否则返回 False。
-
NAND:这是 NOT 和 AND 运算符的组合。如果两个输入都为 True,则返回 False 值,否则返回 True。
-
XOR:此运算符称为异或。如果两个输入不同(一个为 True,另一个为 False),则返回 True 值。如果两个输入相同,则返回值为 False。
5、技巧
BeginPlay、Tick、BeginOverlap、Hit (when-if-do)
cast to、Branch、
sequence,多列依次执行。有助于条理清晰。
Flipflop,两个结果交替执行。
normalize 向量归一化
DeltaSeconds
timer、timeline
set material、FieldOfView、SpawnEmitter、DestroyActor
IA_Zoom
widget、CanvasPanel
-------------------
Execute Console Command
DisableAllScreenMessages(禁用所有屏幕信息),如红色警告文字
EnableAllScreenMessages(启用所有屏幕消息)
11、UE4官方蓝图优化总结
FestEurope 2019 蓝图深入探讨|Blueprint In-depth 官方视频
UE4官方蓝图优化总结
总览
----------------------------------------------------------
蓝图主要消耗
蓝图的消耗主要是在节点之间,蓝图连线触发的消耗是一致的,但节点运行的消耗是通过C++ ,节点不同就有所不同. (意思应该是简化节点连接的数量可以降低蓝图消耗)
----------------------------------------------------------
循环节点
与循环逻辑有关的节点,性能开销都很大. 因为在循环节点中,会有很多连线. 根据实际需要来使用.
----------------------------------------------------------
遍历节点
遍历Actor的节点同样性能开销很大 , 建议使用时把遍历结果保存下来.
----------------------------------------------------------
每帧更新节点
大量使用每帧更新节点,同样开销很大. 后面会有一些方法来优化他.
----------------------------------------------------------
循环上限
蓝图是单线程执行, 循环次数上限是100万 , 所有蓝图每帧指令执行次数上限是25万.
----------------------------------------------------------
复杂的蓝图Actor
蓝图本身作为预制体不会产生性能开销. 只有预制体特别复杂的时候才会. 总的来说影响不大.
----------------------------------------------------------
开销对比
蓝图开销对比. 编辑器模式(红色)运行下效率不高. 但打包(蓝色)和开发模式(黄色)要低很多, 比C++高10倍左右.
----------------------------------------------------------
每帧运行节点
每帧运行节点
这些类似的节点都是每帧来更新的 , 包括输入,时间轴,更新动画,UMG的绑定.
优化建议:
1. 避免过多使用每帧更新, 最直接有效
2. 当使用每帧更新时,尽量思考是否必要. 80%-90%的情况是用不到的
3.
关闭tick位置
可以禁用蓝图每帧更新,如果不需要.
4.
定时器更新
定时器可以处理几乎所有的节点更新
优化定时器更新
如果场景里有很多实例,不希望在同一时间内循环,导致开销骤增,可以使用随机时间来分摊开销.
5.
UMG绑定更新VS事件更新
尽可能使用基于事件的更新, 上图中UMG绑定会每帧更新.
6.
禁用蓝图每帧更新
可以禁用蓝图每帧更新,或者修改更新间隔. 如果没有每帧更新的事件,禁不禁用没有区别.
7.
根据距离开启每帧更新
可以通过设置一些条件来启用每帧更新. 方法很简单,但效果明显.
根据距离越远更新频率越低
同上 可以通过设置一些条件来修改更新频率.
根据是否渲染设置更新频率或者关闭
有个节点Was Recently Rendered,能提供最近是否渲染.如果是false可以考虑关掉或者降低更新频率.
8. 视觉效果不一定要像逻辑处理一样每帧更新.
根据情况在适当的时候拆分逻辑
使用定时器更新碰撞体积和设置actor位置的计算
每帧更新部分,逻辑没有变,但降低了开销.
9.
图中物体在旋转
简单的动画可以使用材质来实现,不必用蓝图来写. 用材质是使用GPU, 蓝图会消耗CPU. 并且比蓝图更快.
true是使用蓝图传参数来改变材质参数实现变色, false直接使用材质来改变颜色
任何简单动画和淡入淡出效果或其他效果.都可以使用材质来处理.材质总是比蓝图快.
-------------------------------------------------------------
性能分析和调试
1.
蓝图调试
Blueprint Debugger
Window--->Developer Tools--->Blueprint Debugger 方便查看所有监测值
2. 可视化日志
可视化日志
场景中可视化查看变量变化位置和记录轨迹,需要创建或者在现有蓝图里添加
开启可视化日志
设置复杂 下面是设置在Tick事件的Heath后面,记录Heath变化和场景中发生变化的位置
然后打开Window--->Developer Tools--->Visual Logger窗口
3. 分析工具
统计数据窗口
统计数据窗口
控制台输入 Stat Game
Dumpticks命令效果
Dumpticks 命令 输出场景中所有每帧更新的Actors.
Profiler
Profiler 常规分析工具.
自动测试
自动测试 设置复杂
自动测试事件设置
蓝图要先设置测试事件和测试内容, 然后是开始测试事件和返回值.
关卡名前缀必须FTEST_
打开Window--->Developer Tools--->Session Frontend窗口
测试窗口
测试完成会返回参数窗口.
-------------------------------------------------------------
内存和加载
蓝图和C++ 内存加载上完全不同 , 蓝图属于资源内容 C++就是C++
如果使用C++ 所有的C++代码都会在初始化时加载,当你启动项目时,项目会加载所有C++类,
蓝图只有在需要时才会加载.
如果C++ 引用了内容. 那么一开始就会加载这些内容.
场景引用蓝图例子
引用查看器和搜索深度
转换
蓝图之间的互相引用都会导致加载所有相关的内容 , 如果不加以控制,就会失控.
Size Map打开位置和显示窗口
Size Map会显示加载内容
计划引用
计划并组织引用的管理方式. 务必考虑这点.
避免蓝图引用和巨量的大型资源, 试着把蓝图拆分成父类蓝图和子类蓝图
引用建议:
创建层级清晰的结构,明确有关资源引用位置的规定
1. 所有关键类都定义在C++中
2. 确保有一个蓝图继承自C++
3. 另一个蓝图以合理的方式继承自父类蓝图
4. 内容最好在第三步中引用
引用关系合理的层级结构
看起来像这样
-------------------------------------------------------------
库 (函数库,宏库,任何类型的蓝图库)
如果使用库中的任何一个,整个库都会被加载,请注意这一点
如果库中的函数,用到了引用, 这个引用也会被加载
(下图 不要在函数中引用外部资源, 应该作为传入.)
函数错误引用
函数正确的引用
分游戏模式
游戏模式也能帮助区分引用
动态资源加载
动态资源加载 , 异步加载资源(Async Load Asset) 然后引用网格体
Maps资源映射
如果你有很多需要软引用的部分,可以使用Maps来存储,通过关键字查找,方便配置和管理.
这是一种资源映射.
Engine.ini可禁用蓝图重新编译
在编辑器和最终打包出来的加载时间是完全不一样的.
Engine.ini 可以禁用”加载时重新编译” (如果蓝图损坏,编辑器可能在加载时因为重新编译崩溃.)
-------------------------------------------------------------
垃圾回收/GC
在游戏中销毁某个对象时,他不会被立即移除,而是被隐藏, 只是被简单标记为不应该存在,实际上并没有从游戏中删除,为了删除他,我们要遍历场景中所有对象,然后明确这个对象应该消失,但我们不能总这样做, 因为开销很大. 这时候就要用到垃圾回收.每隔一段时间,引擎就遍历场景中所有对象,然后删除. 这个就是垃圾回收.
垃圾回收统计
由于要遍历对象, 所以开销有点大 ,通常没有必要特别关注他. 只需要了解垃圾回收有一个统计信息.
修改垃圾回收间隔
可以修改垃圾回收的间隔时间, 默认61秒 ,游戏不建议修改. 如果是电影,根据内存情况可以修改成1小时.
蓝图集群
集群,所有蓝图中都有这个设置
(然而,集群和GC通常是复杂的,启用集群并不是一个好主意.
最好的办法是,如果你要处理大量生成和摧毁的话,最好找C++程序员来处理.)
-------------------------------------------------------------
编译
1. 编译时间对于运行时执行没有任何影响
2. 通常编译时间长就预示着涉及很多转换和引用,内容不够优化,内存占用很有可能超过应有的限度
3.
编译时间通常受三个方面的影响
(1) 节点数量
(2) 转换和引用, 导致引用的其他蓝图也编译
(3) 循环引用
4.
原文
蓝图编译有上限 64KB , 编译会提示失败信息. 正常情况是不会达到上限的, 除非犯了某些错误.
5.
使用宏不会对编译产生帮助
但是函数会带来巨大的变化
可以依赖函数来降低编译时间 ,有助于降低编译压力,还能减少编译大小. 相对于宏更好
6.
(1) 将功能拆分成不同的蓝图Actor. 例如 子类,子actor ,组件
(2) 将逻辑移到C++
(3) 实际情况很复杂, 什么时候该迁移, 迁移多少等等.
-------------------------------------------------------------
转换|Cast To
转换
转换会同时影响编译,内存和引用
当你将某个Actor(蓝图) 转换成第二个Actor(蓝图)时, 当编译第一个时,除非转换前后没有变化,否则会一同编译第二个Actor(蓝图), 可以缓存结果,但如果你改变了两者,编译这个或者那个, 就会出现连锁反应.
例如:
转换连锁反应
转换目标复杂
看起来简单的蓝图, 但转换目标是一个十分复杂的蓝图. 空变量转换失败, 打印一段字符串,但却需要编译30s . 仅仅因为cast to的目标复杂. 所以转换成功与否并不重要,只要参转换与就会有影响
转换是指蓝图之间的转换, 广义上引用也属于这种转换.
引用目标复杂
任何引用都会产生类似的效果, 举例,你想获取他的类型(Get Class)是否是特定的类, 这个转换会耗时30s
转换建议:
1.
可行的转换
玩家可能已经加载到内存,不会对内存造成压力.但会稍微降低编译时间.
2.
可行的转换
如果转换的对象很简单,也不会有什么问题
3. 如果有个蓝图只包含变量, 不包含其他内容.转换和引用他不会有太大问题
4.
最好不要的转换
如果将核心类转换到特定类, 例如将玩家类转换到交通生成类,通常就会产生问题,因为只要玩家类出现,这个交通生成类就需要编译. 所以不要这样做. 除非你的游戏始终有一个交通生成类. 但这里假设没有.
5.
可行但要注意的转换
如果你在核心类之间来回引用,从内存角度来看,可能没有区别,但会增加编译时间, 问题不大,但仍然要小心,有可能会变成这样. (下图)
相互引用连锁反应
简化理解
一般可以这样简化理解, 让偶尔出现的依赖性功能 ,引用始终存在的核心功能, 但反过来不行, 这是普遍做法.
6.
C++桥接
不断将内容转移到C++中或者采用其他技巧最小化引用的影响.
使用C++来桥接类. 经常这样做 ,方法很简单. 很多示例都这样做, 即使不是程序员也可以操作. 有助于你使用蓝图. ( 用C++代码获取一个蓝图里的变量信息,直接在另一个蓝图里像节点一样直接创建使用, 而不必要Cast To 来转换获得.)
函数操作相关代码
函数也可以这样操作.
7.
子类继承事件
当你进行转换时,请试着先转换成父类. 蓝图始终能转换成父类. 你可以用一种方法让他触发子类中的特定功能,但你只引用父类.(子类继承事件)
例如 父类中只有变量和事件,子类中事件后实现功能, 其他蓝图只用转换或引用父类事件即可直接触发子类功能.
父类事件,子类实现
只需引用父类事件
-------------------------------------------------------------
接口/ Interface
接口
最早用于蓝图通信的方法,比Cast To转换还要早.
从不引用特定的类,只获取你查看的Actor,具体是什么Actor要视情况而定, 可以是任何东西.
即使Actor 没有实现接口, 他也会忽略这点, 所以你甚至不需要检查这样做是否正确.
图例
检查接口实现
你也可以检查一下, 是否实现了接口 ,相比使用”获取类”方法,你可以检查是否实现了接口,如果实现了, 就接着完成其他操作 .避免了硬引用.
-------------------------------------------------------------
标签系统
标签
使用标签系统或者使用”游戏技能系统”插件提供的标签系统, 为场景中的对象绑定了很多标签,以便确认当前我们在处理什么内容. 而不必直接引用这些内容.
例如, 这是玩家可以拾取的物品,为他绑定标签, 这样我们可以随时判断出是否是玩家可以拾取的物品.无需获取类才能得出结论,只需检查标签.你只需指定好标签, 然后在蓝图中查找检查Actor的标签,获取标签数组,遍历他.
最佳实践往往是混用这些方法,所以没有绝对的完美方案. 不是说有些方法完全不要用, 而是权衡各种之间的利弊, 取长补短,深思熟虑,选择合适的方法.
-------------------------------------------------------------
竞态条件
竞态条件
竞态条件是指两个或多个类相互依赖, 但他们可能会暂时失效. 因为你不知道哪个会先运行
IsValid
请经常使用 ”IsValid” 确保对象存在.
-------------------------------------------------------------
C++
C++&BP
C++和蓝图是紧密联系在一起的. 蓝图所做的一切都依赖于C++
每个节点基本上都是C++ , 用C++扩展蓝图很容易.
通常 大型游戏项目中的比重大约80%C++ vs 20%BP , 非游戏项目可能不一样. 根据实际情况来.
建议团队中所有人都学习蓝图的使用.
即使是程序员也花点时间学习了解一下蓝图. 这将提供很大帮助, 有助于缩小团队之间的鸿沟,有助于让程序理解美术师和设计师在什么样的环境中工作,他们如何工作,蓝图如何工作. 以及虚幻引擎的工作方式,引擎有哪些类,常见流程是怎么样的. 理解美术师和设计师需要什么.哪些对他们会很有帮助.
另一方面建议美术师和设计师学习C++ ,以便能使用简单的C++技巧. 有助于理解程序员的工作.
拉近彼此的距离,这是促进团队成长的最佳方式.
C++更效率,更适合管理大型复杂系统,执行数学运算,无法用蓝图实现,或者无法用蓝图实现最优效率,蓝图是有局限性的. 你可以用蓝图做很多事情, 但有些功能实现起来可能很困难.
蓝图优势:原型更快,可视化能让团队成员参与进来加快速度,激发创意,编译更快,迭代更快,调整功能也更容易.
简单来说,他有助于理解逻辑流,更灵活,更容易被团队成员接受.
内存方面也更有优势, 因为他属于内容
最合理的架构
C++类使用时甚至可能是空的,你可以创建它以防万一 (核心功能,重要变量)
C++类可以一直是空的
蓝图类包含简单功能或画面效果,可以包含声音和画面有关的效果.
考虑到上面提到过的内存管理技巧,任何内容引用都最好在蓝图中实现.
通常会在一个以上地方用到的功能最好由C++实现,最好是超过两个地方用到.
例如有个背包系统, 马和玩家都用到了它.
如果某个功能每帧都要使用,特别是关联了很多Actor,使用C++更合适
如果功能很复杂且容易出错.C++更合适.
保存游戏, 网络, 关键变量和枚举. C++更好.
大多数内容引用都应该放在蓝图中, 同样要看具体情况
任何简单明了,直观可见的内容. 蓝图
一次性功能. 蓝图
处于原型并需要快速迭代的内容. 最好还是留在蓝图中
C++桥接蓝图
上面使用C++桥接蓝图的部分代码实现
C++实现蓝图函数库
使用C++实现蓝图函数库
暴露给蓝图
建议程序员把一些方法暴露给蓝图供所有人使用, 让设计师可以使用,有助于产生新的想法和让所有人参与进来. 即使最终没有使用到.
-------------------------------------------------------------
其他内容
DOP
推广“DOP”标注 表示有意断开的链接.
纯函数开启
纯函数
纯函数 没有执行引脚
生成时公开
生成时公开,生成Actor时,会得到那个属性. 位于SpawnActor节点内会出现这个变量的属性.
编辑器中调用
启用在”编辑器中调用” 在蓝图事件中勾选Call in Editor ,非运行状态下,可以调用事件测试.
变量公开到Sequencer
将一个变量公开到动画 Sequencer ?
创建一个事件 SetNameOfVariable, Set开头+变量名称, 在Sequencer时间轴运行, 事件运行, Sequencer就会运行 . 这样可以在Sequencer中的变量发生变化时,将变量值打印出来.
删除未使用的变量
删除未使用的变量
显示3D控件
显示3D控件
勾选后Actor蓝图就会在视口中显示变量的3D控件(菱形标志),可以通过拖动来改变变量,不用输入就能改变变量的值,更直观.
废弃标记
如果你想废弃某些过时和不需要的蓝图内容,又不想删除他,因为其他内容还需要依赖他,你可以标记他,让人们不会继续使用他.
Class Settings--->Deprecate
书签
书签
创建和显示窗口
创建和显示窗口, 还能显示注释框 ,方便查找功能位置
字符串表
字符串表
类似一张表格, 必须输入”key”和”source string” ,”key”负责增加条目,他会和一个字符串绑定,与之前的”Maps”映射类似,一边是字符串,一边是资源. 只是字符串换到了另一边.他对本地化这类工作很有用.
例如创建一个”key”标识符,把他标记为”主菜单1”的文本, “key”是你内部使用的信息,然后将他与实际文本绑定,如”开始”,然后你可以本地化了.
Maps
Maps类似 ,映射表示两个对象的关联关系, 你可以让某个资源始终与另一个资源关联,可以是任何类型的资源. 网格,特效,变量 等等 .能减少逻辑复杂度和管理难度.
Sets
集合Sets也类似 ,有点像一个数组, 他有一些内部逻辑, 一个对象只能添加一次 .具有唯一性.
如果是普通数组,我们要自己编写逻辑来判断是否有重复. 集合避免了这一点. 他能自动检查是否有重复的元素.
IsValid
IsValid 上面提到过了
------------------------
-
12、UE5蓝图十条新手优化建议
UE5蓝图十条新手优化建议
1.避免每帧的重计算
不要在事件或其他每帧执行的逻辑中执行复杂的计算,除非必要。尽可能将这些操作移至初始化阶段或在值变化时执行。
为什么要遵循这条建议
- Tick事件的性能影响: UE5中的Tick事件在每一帧都会被调用,用于处理游戏逻辑。如果在此事件中执行复杂计算,会导致每帧都占用大量CPU资源,尤其在硬件性能较低的设备上,这种影响会更加明显。
- 性能优化: 通过避免在Tick事件中执行重计算,可以显著减少CPU的负担,提高游戏的整体性能和响应速度。
哪些行为应避免
- 复杂的数学运算: 如矩阵变换、物理模拟等。
- 频繁的属性访问和修改: 涉及大量游戏对象。
- 频繁的内存操作: 包括对象的创建和销毁。
- 密集的I/O操作: 如文件读写或网络通信。
实际开发中的例子
假设我们正在开发一个城市建设类游戏,其中建筑的外观会受到天气和时间等因素的影响。
场景设定
- 游戏中的建筑需要根据不同的天气条件(如晴天、雨天、雪天)和一天中的不同时间(如白天、黄昏、夜晚)显示不同的外观效果。
- 例如,在雨天,建筑应显示湿润的外观,在夜晚,还需要展示建筑屋内照明效果。
开发挑战
- 在传统的方法中,可能会在每一帧的Tick事件中检查当前的天气和时间状态,然后根据这些状态更新所有建筑的外观,这会导致大量不必要的计算。
优化实现
- 基于事件的更新:当天气或时间状态发生变化时,通过事件系统触发建筑外观的更新。这样,只有在实际发生变化时,建筑的外观才会更新。
- 使用定时器:对于依赖于时间变化的效果,如日夜更替,可以使用定时器在特定时刻触发外观更新,而不是每帧检查。
- 状态缓存:维护一个当前天气和时间的状态缓存,当状态发生变化时,才触发外观更新的逻辑。
具体实例
- 在游戏中,当天气从晴天变为雨天时,系统触发一个事件,这个事件会更新所有可见建筑的外观,使它们看起来更湿润。
- 当游戏检测到时间从白天过渡到夜晚时,通过定时器触发建筑照明效果的更新。
优化效果
- 性能提升:减少了无谓的每帧计算,从而提高游戏性能。
- 资源高效利用:通过仅在必要时更新,减少了计算和渲染资源的浪费。
注意事项
- 确保及时更新:需要确保天气和时间状态的变化能够及时且正确地反映在建筑外观上。
- 平衡性能和响应速度:在优化性能的同时,需要保证游戏的响应速度和视觉效果。
通过这种方法,我们能有效避免每帧的重计算,同时确保游戏环境的动态和真实性。
2.高效使用循环
高效地管理数组和循环是提升性能的关键。特别是在处理数组时,我们应避免在循环中直接增加或删除元素,因为这样的操作会改变数组的大小并可能引起额外的性能开销。合理的做法是在循环过程中标记需要移除的元素,然后在循环结束后或在适当的时机进行批量处理。
为什么要高效使用循环
- 减少内存重新分配: 避免在循环中直接操作数组大小可以减少因重新分配内存而产生的性能开销。
- 优化性能: 通过减少不必要的数组操作,提高代码的运行效率。
- 增强代码可读性和维护性: 清晰的循环逻辑和后续的批量处理使代码更易于理解和维护。
如何实现
- 标记而非删除: 在处理数组的循环中,如果需要移除元素,先将其标记为待删除,而不是立即从数组中移除。
- 批量处理: 循环结束后,根据标记批量移除所有需要删除的元素。
- 标记待删除元素:首先,在循环中,你需要一个方式来标记那些需要被删除的元素。这通常可以通过维护一个额外的数组来实现,该数组用于存储待删除元素的索引或引用。
- 结束循环:完成循环后,你将拥有一个包含所有待删除元素索引或引用的数组。
- 批量移除元素:接下来,你需要遍历这个标记数组,并从原始数组中移除相应的元素。在这一步,你需要注意两点:
- 如果你是按索引删除,由于数组大小在删除元素时会发生变化,建议从最高的索引开始删除,这样可以避免因索引改变而导致错误。
- 如果你是按引用删除,这个问题就不会存在,因为即使数组的大小发生变化,引用仍然有效。
- 使用“Remove Item”节点:在蓝图中,你可以使用“Remove Item”节点来移除特定的元素。如果你有一个包含待删除元素引用的数组,可以遍历这个数组,对每个元素使用“Remove Item”节点。
- 效率考虑:如果待删除的元素数量接近或超过数组的一半,考虑重新构建一个新数组可能更有效率。这意味着创建一个新的数组,只添加那些不需要删除的元素,然后用这个新数组替换旧的数组。
- 避免循环中的复杂操作: 保持循环内部逻辑尽可能简单,避免引入复杂的数据操作或条件判断。
实际开发中的例子
假设我们正在开发一个策略类游戏,其中包含一个复杂的单位管理系统。
场景设定
- 大量可交互单位:游戏中有大量的单位(如士兵、工人、机器等),玩家可以指挥这些单位进行各种活动。
- 单位状态频繁变更:单位的状态(如健康值、能量等)可能会因战斗、任务或环境影响而频繁变化。
开发挑战
- 实时更新单位状态:需要在游戏的每个逻辑周期(如每个游戏帧或每秒)中更新所有单位的状态。
- 避免性能瓶颈:直接在遍历所有单位的循环中处理复杂的状态更新逻辑可能导致性能问题。
优化实现
- 循环中仅收集信息:
- 在遍历所有单位的循环中,不直接执行状态更新操作。相反,仅收集需要更新状态的单位的信息。
- 使用一个辅助数组或列表来存储需要更新状态的单位的引用或索引。
- 循环结束后批量处理:
- 在循环结束后,根据辅助数组中的信息,统一处理所有需要更新状态的单位。
- 这可以包括计算新的状态值、应用状态变化(如治疗、伤害)等。
- 优化状态更新逻辑:
- 确保状态更新逻辑尽可能高效,例如通过避免重复计算、使用高效的数据结构等。
具体实例
- 游戏中,玩家的一个单位受到攻击。在遍历所有单位的循环中,这个受损单位被标记为“需要更新状态”。
- 循环结束后,游戏逻辑检查标记数组,找到所有标记为“需要更新状态”的单位,并执行状态更新(例如减少健康值)。
- 更新操作完成后,从标记数组中清除相关单位的标记。
优化效果
- 提高性能:通过减少实时状态计算,降低了CPU的负担,提高了游戏的整体性能。
- 流畅的游戏体验:减少了由于频繁状态更新导致的潜在卡顿,使游戏运行更加流畅。
注意事项
- 及时处理关键更新:确保对游戏逻辑至关重要的状态更新能够及时处理。
- 平衡效率与及时性:在提升循环效率的同时,保持状态更新的及时性和准确性。
3.减少Cast的使用
减少对类型转换(Cast)的过度依赖是优化代码和提升性能的重要策略。过度使用Cast不仅可能反映出设计上的问题,还可能导致性能损耗。相比之下,使用接口(Interfaces)可以使代码更加灵活和高效。
为什么要减少Cast的使用
- 提高性能: 频繁的Cast操作会增加运行时的负担,特别是在需要处理大量对象的场景中。
- 增强代码可维护性: 过多的Cast操作使得代码难以理解和维护,尤其是在大型项目中。
- 增强设计灵活性: 使用接口(Interfaces)而非Cast可以使设计更加灵活,更容易适应未来的变化。
如何实现
- 使用接口替代Cast: 当需要与多种不同类型的对象进行交互时,定义并使用接口,而不是依赖于具体类的Cast。
- 设计模块化: 在设计类和组件时,尽量使其功能模块化,减少它们之间的直接依赖。
- 优化蓝图结构: 审视并优化现有的蓝图结构,减少不必要的类型转换。
实际开发中的例子
假设我们正在开发一个多人在线角色扮演游戏,在这个游戏中,玩家可以扮演不同的角色,例如骑士、法师和游侠,每个角色都有自己的技能和特性。
场景设定
- 多样化的角色和技能:游戏中包含不同的角色类型,每种角色拥有独特的技能和行为模式。
- 互动和合作:游戏鼓励玩家之间的互动和合作,例如一个角色可能需要给另一个角色施加增益效果或进行治疗。
- 需求动态处理角色行为:游戏设计要求能够根据角色类型和当前情境动态地处理不同的行为和互动。
开发挑战
- 在没有使用接口的设计中,处理不同角色之间的交互可能会过度依赖于Cast操作,以判断对象的具体类型并访问特定的方法或属性。
优化实现
- 定义角色接口:创建一个名为IPlayableCharacter的接口,该接口定义了所有角色共有的行为和属性,例如ApplyEffect、ReceiveDamage、Heal等方法。
- 实现接口:所有角色类(如Knight、Mage、Ranger等)实现这个接口,提供具体的方法实现。
- 接口驱动的交互:在游戏逻辑中,当需要处理角色之间的互动时,使用接口来引用角色,而不是具体的类。这样,无论实际角色类型如何,都可以通过接口调用相应的方法。
具体实例
- 在一个游戏场景中,法师角色需要给附近的骑士角色施加防御增益效果。
- 游戏逻辑首先通过接口IPlayableCharacter来引用附近的角色,而不是直接Cast到Knight类。
- 法师角色调用ApplyEffect方法,该方法是通过IPlayableCharacter接口在Knight类中实现的。
- 由于使用了接口,即使将来添加新的角色类型,也不需要修改现有的交互逻辑。
优化效果
- 性能提升:减少了运行时的Cast操作,提高了游戏性能。
- 代码清晰且易于维护:使用接口使代码结构更加清晰,易于理解和维护。
- 灵活性和可扩展性:接口的使用提高了代码的灵活性和扩展性,便于添加新角色和技能。
注意事项
- 精心设计接口:接口应该精心设计,避免过度泛化或过度特化。
- 及时重构:如果现有代码中存在大量的Cast,考虑进行重构以引入接口的使用。
4.使用变量缓存结果
在UE5蓝图编程中,使用变量缓存结果是一种重要的性能优化策略。这种方法通过存储不经常改变的计算结果,减少了对处理器资源的需求。它特别适用于频繁访问的组件引用或需要经过复杂计算才能得到的值。
为什么要使用变量缓存结果
- 减少重复计算: 缓存可以避免重复进行相同的计算,特别是那些计算成本较高的操作。
- 提高效率: 通过缓存,访问复杂计算的结果变得更快,从而提高了整体的运行效率。
- 节省资源: 减少了CPU的负担,特别是在处理复杂逻辑或大量数据时。
如何实现此策略
- 缓存组件引用: 在蓝图的初始化阶段,获取必要的组件引用并存储在变量中。以后需要使用这些组件时,直接从变量中获取引用,而不是每次都调用GetComponent之类的函数。
- 缓存计算结果: 对于那些只在特定事件(如游戏开始、级别加载、状态改变等)改变的复杂计算结果,将结果存储在变量中。当这些事件发生时,更新缓存中的结果。
- 更新机制: 为确保缓存的数据始终是最新的,设置适当的机制来监测和更新影响缓存值的数据变化。
实际开发中的例子
设想我们正在开发一个城市建设模拟游戏,其中玩家可以建造和管理不同的建筑物,每个建筑物都有其独特的功能和属性。
场景设定
- 复杂的属性计算: 建筑物的一些关键属性(如税收、能源消耗、人口容量)需要通过复杂的计算得出,这些计算考虑了多个因素,如建筑等级、周围环境、特殊事件等。
- 高频率属性访问: 游戏中的逻辑经常需要访问这些属性,例如在更新城市预算时计算税收,或者在玩家查看建筑信息时显示能源消耗。
开发挑战
- 性能瓶颈: 每次需要这些属性时都进行复杂的重新计算,会导致显著的性能问题,特别是在大型城市中,有成百上千个建筑时。
优化实现
- 初始化时缓存计算结果: 在建筑物创建或属性变更时,例如升级建筑或改变周围环境,执行一次完整的属性计算,并将结果缓存到变量中。
- 使用缓存的属性值: 在游戏逻辑中需要这些属性值时,直接从缓存的变量中获取,而不是重新计算。
具体实例
- 当玩家升级一个工厂建筑时,游戏逻辑计算并更新了该工厂的能源消耗和生产能力,这些计算结果被存储在工厂对象的变量中。
- 每次游戏逻辑需要获取这些信息(如在每个游戏周期更新城市资源时),它直接从这些缓存的变量中读取,而不是重新进行复杂的计算。
优化效果
- 减少计算负担: 通过避免重复计算,显著减少了CPU的工作负担,特别是在处理复杂场景时。
- 提升响应速度: 缓存机制使得属性值的访问更加快速,改善了游戏的整体响应速度和流畅度。
注意事项
- 保证缓存数据的时效性: 需要确保当影响计算结果的因素发生变化时,及时更新缓存。
- 适当管理缓存: 在某些情况下,例如建筑被拆除或显著改变时,需要适当地清除或更新相关的缓存。
5.避免使用全局搜索
在UE5蓝图编程中,谨慎使用全局搜索函数,如或,是一种重要的性能优化策略。这些函数通过遍历所有的Actor或组件来寻找匹配的项,这在包含大量对象的大型关卡中尤其耗费性能。
为什么要避免使用全局搜索
- 性能开销大: 全局搜索函数需要遍历整个游戏世界中的所有对象,这在对象数量庞大时会消耗大量的处理器资源。
- 降低帧率: 过度使用这些搜索函数可能导致游戏帧率下降,特别是在大型或复杂的关卡中。
- 影响游戏流畅性: 频繁的全局搜索可能导致游戏在执行这些操作时出现卡顿。
如何实现此策略
- 使用更高效的查找方法: 例如,如果可能的话,使用标签或自定义的索引系统来查找对象,这些方法通常比全局搜索更高效。
- 缓存结果: 如果你需要频繁查找同一类的对象,考虑在游戏开始时进行一次全局搜索,然后将结果缓存起来供后续使用。
- 局部化搜索: 限制搜索范围,例如只在特定区域或针对特定类型的Actor进行搜索。
实际开发中的例子
假设我们正在开发一个大型多人在线角色扮演游戏(MMORPG),这个游戏拥有一个庞大且详细的开放世界,其中玩家可以与各种NPC互动,完成任务,参与战斗等。
场景设定
- 庞大的游戏世界:游戏包含数千个不同类型的Actor,包括各种NPC、怪物、物品等。
- 复杂的互动机制:玩家可以与不同的NPC进行交互,比如购买物品、接取任务等。每种NPC可能有不同的功能和角色。
- 动态的游戏环境:游戏世界是动态的,NPC和其他对象可能会根据游戏情节移动或改变状态。
开发挑战
在这样的游戏中,一个常见的需求是让玩家能够快速找到特定类型的NPC,例如所有的武器商人或任务给予者。如果使用全局搜索函数(如),每次玩家请求这些信息时,游戏都需要遍历整个游戏世界中的所有Actor,这在大型关卡中会非常消耗性能。
优化实现
- 一次性全局搜索与结果缓存:
- 在游戏加载或关卡初始化时,对特定类型的NPC执行一次全局搜索。
- 将搜索结果存储在一个容易访问的数据结构中,如数组或映射表。
- 局部化搜索与动态更新:
- 实现一个系统,该系统仅在玩家周围的特定区域内搜索NPC,比如在玩家进入一个新区域时。
- 当NPC移动或游戏环境发生变化时,动态更新缓存数据。
- 事件驱动的更新:
- 使用事件监听机制,当游戏中的NPC状态发生变化时(例如,新的NPC出现,或者现有的NPC离开或改变角色),更新缓存。
具体实例
- 游戏场景:玩家进入一个名为“黄金城”的大城市,需要找到所有的装备商人。
- 实现策略:
- 当玩家第一次进入“黄金城”时,游戏执行一次针对装备商人的搜索,并缓存结果。
- 玩家在城市中移动时,系统根据玩家的位置动态调整显示的NPC列表,仅显示附近的装备商人。
- 如果新的装备商人加入或现有的离开,事件监听系统会更新缓存的列表。
优化效果
- 性能提升:这种方法大大减少了游戏在运行过程中的计算负担,尤其是在大型场景中。
- 改善玩家体验:加快了玩家查找NPC的速度,减少了等待时间和潜在的游戏卡顿。
- 灵活应对变化:能够快速适应游戏世界的动态变化,确保玩家总是获得最新的信息。
您提出的“使用事件和委托”作为优化策略是非常合理的。在UE5蓝图编程中,下面是根据您的建议编辑的完整内容:
6.使用事件和委托
在蓝图中,通过设置事件和委托,可以使系统仅在特定事件发生时才响应,从而减少了不必要的检查和计算。开发者可以设置特定的触发点(事件),当这些事件发生时,相应的函数(委托)会被自动调用。这种方法特别适用于那些状态变化不频繁或依赖于特定触发条件的场景。这种方法可以替代轮询(即频繁检查某条件是否满足),减少了在每一帧进行条件检查的需求,从而提升性能。
为什么要使用事件和委托
- 减少不必要的检查: 轮询方法需要在每一帧或以一定频率检查条件是否满足,这在许多情况下是不必要的。
- 提高效率: 事件驱动编程允许代码仅在需要时执行,这样可以节省资源,提高程序的运行效率。
- 改善代码组织: 使用事件和委托可以使代码结构更清晰,逻辑更容易理解和维护。
如何实现此策略
- 定义事件: 根据需要响应的特定条件或行为定义事件。
- 创建委托: 为这些事件创建委托函数,当事件被触发时,这些函数将被执行。
- 绑定事件与委托: 将事件与相应的委托函数绑定起来,确保在事件发生时委托得到调用。
实际开发中的例子
在捕获旗帜游戏模式中,游戏系统需要能够实时追踪并响应玩家的行为,特别是当玩家接触或捕获旗帜时。这需要游戏逻辑能够快速而准确地识别这一行为,并立即作出反应。如果使用传统的轮询方法来检测玩家是否捕获旗帜,这可能会导致响应延迟。在一个多玩家环境中,每个玩家的状态变化都需要被及时捕捉和处理,轮询方法可能无法有效地处理这种高频率和实时性的需求。
场景设定
- 旗帜互动: 游戏中的核心机制是玩家需要捕获并携带对方的旗帜。
- 玩家状态变化: 当玩家捕获旗帜时,他们的状态会改变,比如速度减慢,无法使用武器等。
- 团队协作: 团队成员需要快速响应队友捕获旗帜的情况,以提供支援或防守。
开发挑战
在没有事件和委托机制的情况下,游戏可能需要不断轮询每个玩家的状态来检测是否有人捕获了旗帜。这不仅效率低下,还可能导致服务器端和客户端之间的同步延迟。
优化实现
- 定义捕获旗帜事件: 当玩家接触到敌方旗帜时,触发一个“捕获旗帜”事件。
- 创建状态改变委托: 对于捕获旗帜的玩家,创建一个委托函数来改变其状态(如速度减慢,禁用武器)。
- 通知团队委托: 创建另一个委托函数,用于通知该玩家的队友,他们的队友已经捕获了旗帜。
- 绑定事件与委托: 将“捕获旗帜”事件与这两个委托函数绑定。
具体实例
- 游戏场景:玩家A接近并触摸到敌方的旗帜。
- 实现策略:
- 当玩家A接触旗帜时,“捕获旗帜”事件被触发。
- 触发的事件调用委托函数,改变玩家A的状态(例如,减慢移动速度,禁用武器)。
- 同时,另一个委托函数被调用,通知玩家A的队友他们的旗帜已被捕获,激活他们的UI上的特定指示器或发送战术提示。
优化效果
- 提高响应速度:通过事件和委托,游戏能够即时响应玩家的行动,无需轮询检查旗帜是否被夺走。
- 降低计算负担:显著减少了服务器和客户端的计算需求,尤其是在大量玩家同时在线的情况下。
7.优化蓝图结构
优化蓝图结构是提高执行效率和易读性的关键。一个清晰、简洁的蓝图可以更容易地被理解和维护,同时还能减少执行时的性能开销。避免过度嵌套的函数调用和逻辑分支,可以使蓝图更加高效和可靠。
为什么要优化蓝图结构
- 提高执行效率: 简洁的蓝图减少了执行时的计算量,提高了运行效率。
- 增强可读性: 清晰的逻辑结构使得蓝图更容易被理解,特别是在团队协作的环境中。
- 便于维护和扩展: 简化的结构使得未来对蓝图的修改和扩展更加容易。
如何实现此策略
- 避免过度嵌套: 减少函数内部的嵌套调用,尽量保持函数的单一职责。
- 简化逻辑分支: 使用清晰的逻辑判断,避免复杂和冗余的分支结构。
- 模块化设计: 将复杂的功能拆分成多个小的、可重用的模块。
- 使用函数和宏: 对重复使用的逻辑或功能使用函数和宏进行封装。
实际开发中的例子:策略游戏中的单位升级系统
假设我们正在开发一个策略游戏,游戏中包含一个复杂的单位升级系统。
场景设定
- 多样的单位类型:游戏中包含多种单位类型,如步兵、坦克、飞机,每种单位都有自己的升级路径。
- 复杂的升级选项:升级可以涉及增强攻击力、防御力、增加特殊技能等。
- 多重条件限制:升级受限于多种条件,如玩家的资源(金钱、材料等)、当前游戏进度、特定的游戏事件触发等。
开发挑战
- 庞大且复杂的蓝图结构:最初的设计中,所有升级逻辑都被集中在一个巨大的蓝图中,导致蓝图事件图变得庞大且复杂,包含大量的嵌套分支和重复的逻辑块。
优化实现
- 分解逻辑:
- 将单位升级逻辑分解为多个子模块,每个模块负责处理一个特定类型的单位升级,例如单独的步兵升级模块、坦克升级模块和飞机升级模块。
- 模块化资源和条件检查:
- 创建通用的函数来处理资源检查和条件判断,以便于在不同升级路径中重用。
- 使用事件和委托处理游戏进度:
- 通过事件和委托机制处理与游戏进度相关的变化,例如当玩家达到特定级别时触发特定的升级事件。
具体实例
- 游戏场景:玩家选择升级其步兵单位。
- 实现策略:
- 当玩家选择升级步兵时,游戏调用步兵升级模块。
- 此模块首先调用通用的资源检查函数,确认玩家是否有足够资源。
- 然后,根据游戏当前进度和其他条件,决定哪些升级选项对玩家可用。
- 如果玩家满足所有条件,触发相应的升级事件,并使用委托来实施具体的升级操作。
优化效果
- 简化蓝图结构:通过将复杂的逻辑分解成小模块,蓝图变得更加简洁和易于管理。
- 提高代码重用性:通用的资源检查和条件判断函数可以在多个升级路径中重用,减少了代码的冗余。
- 提升性能和可读性:简化后的蓝图结构降低了运行时的计算负担,同时也提高了代码的可读性和可维护性。
通过这种方式,我们不仅提高了蓝图的执行效率,还增强了代码的清晰度和可维护性,使得未来对升级系统的修改和扩展变得更加容易。
8.使用数据表和结构体
在UE5中使用数据表(DataTables)和结构体(Structs)来管理共享的、不经常变化的数据是一种提高效率和可维护性的做法。通过这种方式,可以避免硬编码数据,使得数据更新、修改和扩展更加灵活和方便。
为什么要使用数据表和结构体
- 提高数据管理效率: 使用数据表和结构体可以集中管理相关数据,便于查找和修改。
- 减少硬编码: 硬编码数据难以维护和更新,使用数据表和结构体可以避免这一问题。
- 增强可扩展性: 通过数据表和结构体管理数据,可以轻松地添加或修改数据,而不影响现有逻辑。
如何实现此策略
- 定义结构体: 创建结构体来定义需要管理的数据类型和格式。
- 创建数据表: 使用数据表来存储共享的、不经常变化的数据,如游戏中的道具、单位属性等。
- 集中访问数据: 在游戏逻辑中,通过访问数据表和结构体来获取所需数据,而非直接使用硬编码值。
实际开发中的例子
假设我们正在开发一个角色扮演游戏,游戏中包含多种装备和技能。
场景设定
- 多样化的装备和技能: 游戏中有大量不同的装备和技能,每种都有自己的属性和效果,如攻击力、防御力、价格等。
- 共享数据: 这些装备和技能的属性在游戏中被多处引用和共享。
开发挑战
- 数据更新和维护: 在原始设计中,装备和技能的属性被硬编码在不同的蓝图中,例如,每当创建一个新的装备类型时,就复制并修改一个现有的装备蓝图,并直接在蓝图中修改其属性值。最终导致更新和维护数据变得复杂和耗时。
优化实现
- 定义装备和技能的结构体:
- 为装备和技能定义结构体,包括名称、属性、效果等信息。例如创建一个名为SEquipmentAttributes的结构体,包含装备的所有属性。
- 创建装备和技能的数据表:
- 制作一个装备的数据表,每行包含一种装备的所有属性,使用SEquipmentAttributes结构体作为其格式。
- 在游戏逻辑中引用数据表:
- 当需要获取装备或技能信息时,从数据表中检索,而非直接引用硬编码的数据。
具体实例
- 游戏场景:玩家在商店中购买装备。
- 实现策略:
- 商店界面从装备数据表中读取所有可用装备的信息。
- 当玩家选择一个装备时,界面显示该装备的属性,这些信息直接从数据表中获取。
- 如果游戏中新增或修改装备,只需更新数据表,而无需修改商店的逻辑。
优化效果
- 提高数据管理效率:集中管理装备和技能数据,使得查找和更新变得更加方便。
- 减少硬编码:避免了数据的硬编码,使得维护和更新更加灵活。
- 增强游戏的可扩展性:简化了添加新装备和技能的过程,增强了游戏的可扩展性。
您的建议“使用引用传递”是在UE5蓝图编程中提升性能的另一项关键策略。当处理大型数据结构,如数组或结构体时,使用引用传递而非值传递(拷贝)可以显著减少内存使用和提高性能。这是因为引用传递仅传递数据的引用地址,而不是复制整个数据结构。以下是按照您的建议编辑的完整内容:
9.使用引用传递
在UE5蓝图编程中,使用引用传递大型数据结构(如数组或结构体)是一个重要的优化策略。这种方法可以避免在函数调用时发生不必要的数据复制,从而减少内存使用和提高程序的执行效率。
为什么要使用引用传递
- 减少内存开销: 引用传递不会复制整个数据结构,从而减少内存的使用。
- 提高性能: 避免数据复制可以减少执行时间,特别是在处理大型数据结构时。
- 更有效的数据管理: 使用引用传递可以确保数据的一致性,因为所有的改动都会反映在原始数据上。
如何实现此策略
- 选择引用传递: 在蓝图中创建函数或事件时,选择使用引用传递参数。
- 注意数据一致性: 使用引用传递时,任何对数据的修改都会影响原始数据,因此需要注意维护数据的一致性。
了解了。让我们尝试一个不同的、更实际的例子来阐释在UE5蓝图编程中使用引用传递的优化策略。
实际开发中的例子:角色扮演游戏的物品管理系统
假设我们正在开发一个角色扮演游戏,游戏中包含一个详细的物品管理系统。
场景设定
- 丰富的物品数据: 游戏中包含各种物品,每种物品都有一个详细的属性结构体,包括名称、类型、属性加成等信息。
- 频繁的物品交互: 玩家在游戏中频繁与物品进行交互,如查看物品属性、使用物品、物品交换等。
开发挑战
- 高内存和性能开销: 在原来的设计中,每次玩家查看或交互物品时,物品的属性通过值传递进行处理,这导致了频繁的数据复制,增加了内存使用和性能开销。
优化实现
- 使用引用传递物品属性:
- 修改物品管理系统中的函数和事件,使其通过引用而非值传递来处理物品属性。
- 维护数据一致性:
- 在处理物品属性时注意维护数据的一致性,确保任何修改都是有意和正确的。
具体实例
- 游戏场景:玩家打开背包查看物品详情。
- 实现策略:
- 当玩家点击背包中的物品时,背包界面的函数通过引用传递接收物品的属性结构体。
- 界面显示的物品信息直接从传递的引用数据中获取,而不是复制整个结构体。
- 如果玩家使用或修改了物品,这些变更直接应用于原始物品数据。
优化效果
- 减少内存和性能开销:通过引用传递而非数据复制,减少了物品交互时的内存和性能开销。
- 提高响应速度:直接操作引用数据,提高了界面更新和数据处理的速度。
通过这种方式,我们有效地减少了不必要的数据复制,提升了物品管理系统的效率和响应性,同时也简化了数据处理的流程。
10.池化对象
使用对象池模式来管理频繁创建和销毁的对象是一种非常有效的性能优化方法。特别是对于那些需要频繁创建和销毁的对象,如弹药、粒子效果、临时的游戏实体等。这种模式通过重用现有对象而非每次都创建新对象,可以显著减少内存分配和垃圾回收的开销,提高游戏性能。
为什么要使用对象池
- 减少创建和销毁的开销: 频繁地创建和销毁对象是一个资源密集的过程,尤其是在性能受限的环境中。
- 提高性能: 使用对象池可以减少因频繁分配和释放内存所造成的性能损耗。也能更精确地控制对象的生命周期和资源使用
- 减少垃圾回收负担: 在UE5的蓝图托管环境中,减少垃圾回收的次数和负担是同样是提高性能的关键。
如何实现对象池
- 创建对象池: 设计一个对象池来管理一组特定类型的对象,比如弹药或粒子效果。
- 预先分配对象: 在游戏开始或需要时,预先创建并存储一定数量的对象。
- 池中管理对象: 当需要一个新对象时,首先尝试从池中获取一个已存在的对象;如果池中没有可用对象,则创建一个新的。
- 重置对象状态: 在对象使用完毕后,回收到对象池中,重置其状态,以便下次重用时它是“干净”的。
实际开发中的例子
假设我们正在开发一个多人在线第一人称射击游戏(FPS),在这个游戏中,玩家可以使用各种枪械,每种枪械都会发射不同类型的子弹。
场景设定
- 大量子弹实例:游戏中的战斗非常激烈,玩家们频繁地射击,导致大量子弹实例被创建和销毁。
- 不同的子弹类型:游戏包含多种枪械,每种枪械发射的子弹在形状、速度和效果上都不同。
开发挑战
- 在没有对象池的情况下,游戏必须为每次射击创建新的子弹实例,这在大规模战斗中会造成严重的性能问题。
优化实现
1. 设计对象池类
首先,设计一个通用的对象池类。这个类负责管理对象的创建、重用和回收。例如,一个名为的类,专门用于管理子弹对象。
- 初始化池:在对象池类中,定义一个容器(如数组或列表)来存储可重用的对象实例。
- 配置参数:允许设置对象池的最大容量,以控制内存使用。
- 预先填充对象池
- 在游戏启动或场景加载时,预先在对象池中创建一定数量的对象实例。
- 例如,根据预期的最大玩家数和每把枪的射速,计算出所需的子弹数量,并据此初始化BulletPool。
- 实现获取对象的逻辑
- 从池中获取对象:当需要新的子弹时,首先检查对象池中是否有可用的对象。如果有,就从池中取出一个对象并重置其状态,使其看起来像新的。
- 处理池空的情况:如果对象池中没有可用对象,根据需要创建新的对象,并考虑将其添加到池中。
- 对象使用完毕后的处理
- 在子弹击中目标或飞行结束时,不直接销毁子弹实例。相反,重置子弹的状态(位置、速度、特效等)并将其返回对象池。
- 重置可能包括停止任何动画或特效,并将对象移动到一个“非激活”状态或位置。
- 管理和维护对象池
- 动态调整池大小:根据游戏的运行情况,动态增加或减少对象池中对象的数量。这需要在运行时监测性能指标和对象使用情况。
- 性能监控:定期检查对象池的性能影响,确保它不会导致内存泄漏或其他问题。
- 集成到游戏逻辑
- 将对象池的使用集成到游戏的主要逻辑中。例如,在玩家射击时,修改射击逻辑,以便从BulletPool获取子弹。
具体实例
- 玩家使用一把机枪射击时,游戏从机枪子弹的对象池中取出子弹实例并发射。
- 当子弹击中目标或飞出地图边界时,它的状态被重置(如位置、速度等),然后被放回对象池中,以备下次使用。
优势
- 减少创建和销毁的开销:显著降低了由于频繁创建和销毁子弹实例导致的性能压力。
- 提高响应速度:在激烈的战斗场景中,对象池的使用保证了游戏的流畅性和快速响应。
- 节省内存资源:减少了对内存的频繁分配和回收,从而优化了资源使用。
注意事项
- 动态调整对象池大小:根据游戏的实际情况(如玩家数量、战斗强度等)动态调整对象池的大小。
- 确保对象状态正确重置:在将对象放回池中之前,确保所有相关状态(如位置、动画、特效等)都被正确重置。
-----------------