游戏引擎学习第331天: Activating Entities by Brain
我们昨天刚完成了一些 bug 修复。现在还有几个小问题需要检查,其中一个 float 类型的问题之前遗漏了,需要处理。其余大部分问题都已经解决,但还有一个宏相关的问题没有处理,需要有人提供可用的宏示例才能完全修复。因为目前没有这个例子,如果要测试,需要安装 Clang 才能验证,但现在这不是一个优先任务。
回顾并为今天的内容做铺垫
我们昨天刚完成了一些 bug 修复。现在还有几个小问题需要检查,其中一个 float 类型的问题之前遗漏了,需要处理。
其余大部分问题都已经解决,但还有一个宏相关的问题没有处理,需要有人提供可用的宏示例才能完全修复。因为目前没有这个例子,如果要测试,需要安装 Clang 才能验证,但现在这不是一个优先任务。
game_shared.h
:修复 ReadVarArgFloat()
的逻辑
在清理 ReadVarArgFloat()
的逻辑时,发现之前遗漏了一个情况。在可变参数函数(vararg)中,参数会被自动扩展为特定类型后再压入栈中,例如:
- 8 位的 char 会扩展为 32 位整数
- short 会扩展为 int
- int 会保持为 int
- float 会自动提升为 double
- double 保持为 double
也就是说,压入栈的类型和弹出的类型不一定相同,因此不能直接按原类型读取。之前的实现遗漏了 float 自动提升为 double 的情况,导致在读取 float 时可能出错。Clang 对此进行了警告,提示 float 会被转换为 double,警告是准确的。
修复方法是针对长度为 8(即 double)的情况进行正确处理,确保从栈中读取时类型匹配,避免潜在的错误。
运行游戏,确保功能仍正常,并关闭该问题
我们运行游戏并验证修改后的功能仍然正常,确认所有相关功能都没有问题。修复完成后,我们关闭了这个问题。
剩下的部分主要涉及 Clang 兼容性的问题:如果有人使用 Clang 并能够找到解决方法,使我们的调试宏在 Clang 下正常工作,我们可以应用这些修改,使得在 Clang 下编译不会出现问题。目前除了 Clang 相关的宏定义问题,其它地方已经在不同平台上兼容了。
总结来说,功能验证通过,原有问题已关闭,剩余工作只涉及 Clang 特定环境下的宏定义调整。
todo.txt
:查看待办事项列表
我们查看了待办事项列表,以便确定接下来需要完成的工作并制定计划。考虑到我们刚刚结束了一段较长的休整,需要先明确哪些任务是优先的。
首先,大部分工作集中在修复 Z 轴相关的问题,以及对渲染器做少量检查,确保当前架构不会导致游戏代码出现问题。渲染器的改进(比如增加灯光效果)可以随时进行,对游戏视觉效果有提升,但如果渲染器的设计存在架构性缺陷,可能会影响到所有游戏代码,因此需要确认数据如何被传入渲染器不会引发潜在问题。整体来看,由于渲染流程较窄且主要在实体处理的末端,所以问题影响面有限,但仍需检查确保架构合理。
接着,我们看了调试相关的待办事项:目前调试代码大体上已经完成,但仍有一些问题需要修正,例如帧视图未能显示错误帧,以及所有数据仍通过复杂的图排序流程处理,而对于调试用途而言,排序可以简化。
在敌人系统方面,待办事项主要是处理地理上分散的实体,它们可能只部分进入模拟区域,但需要作为一个整体进行移动,这涉及到如何正确管理多段实体的同步和流式加载。
总结来看,当前待办事项主要包括:
- 修复 Z 轴相关问题。
- 检查并确认渲染器的数据输入架构合理,避免未来修改渲染器时引发大规模代码调整。
- 修复调试代码中的帧显示和排序问题。
- 处理多段或地理分散实体的同步与流式加载问题。
整体目标是确保现有架构稳健、调试功能完善,同时为后续渲染器升级和实体系统扩展留下空间。
运行游戏,并考虑多段蛇形实体是如何被流式加载的
我们在运行游戏时,考虑到多段蛇形实体的流式加载问题。假设有一个五段长的蛇形生物,每一段都是独立的实体。流式加载时,这五段会分别被加载,这在某种程度上是我们希望的行为,但可能会产生问题:如果尾部的一部分位于尚未加载的区域,那么整条蛇就会出现“部分存在”的状态,即有些实体被加载,而有些实体未被加载,可能导致逻辑异常或状态不一致。
针对这个问题,可以考虑通过实体系统来避免这种情况,避免实体被拆分。一个思路是利用“围栏(apron)”概念:在激活区域之外也加载一小部分区域,但不将这些区域内的实体标记为活跃。然后,如果某个实体属于一个活跃的脑(brain),但其所在区域处于非活跃状态,则将其标记为活跃。这样可以保证多段实体不会因为流式加载区域的划分而被拆分。
需要注意的是,这种方法是否真的有必要,以及它会不会引入更多问题,尚不确定。一般来说,如果实体主要局限在房间内部,问题只会在实体跨越房间边界时出现,例如从一个门口走向另一房间时,才可能被拆分。因此,虽然存在潜在问题,但其发生的频率可能较低,需要权衡是否值得处理。
总结来说,我们关注的核心是:
- 多段实体的每一段是独立实体,流式加载时可能出现部分加载的情况。
- 可以通过扩展激活区域的边界和条件激活策略,避免实体被拆分。
- 需要评估这种解决方案是否值得实施,以及可能带来的复杂性或其他副作用。
决定如果多段实体中至少有一段在活动模拟区域内,就将整个实体视为活跃
我们决定,如果多段实体中至少有一段位于活动模拟区域内,就将整个实体视为活跃。具体做法如下:
- 每个被流式加载的实体都有自己的标记(flag),用来指示是否活跃。默认情况下,位于模拟区域内的实体标记为活跃,区域外的实体标记为非活跃,不会被动画处理。
- 在处理某个脑(brain)之前,遍历该脑下的所有实体,如果发现其中任何一段是活跃的,则将该脑下的所有实体都标记为活跃。
- 这样可以确保那些位于“围栏(apron)”区域内的实体也会被激活,从而避免多段实体出现部分激活或拆分的情况。
总体上,这是为了保证实体的完整性,即使部分实体跨出了活动区域,也不会被忽略或分割。这个方案是否最佳还不确定,但值得尝试,因此我们准备在实体标记中增加一个 env flag 来实现这一逻辑。
game_entity.h
:引入 EntityFlag_Active
我们在实体系统中引入了 EntityFlag_Active
来标记实体是否活跃。具体操作如下:
- 已经存在“可更新边界(updatable bounds)”的概念,用于判断实体是否需要更新。这与普通边界不同。
- 在遍历实体块(chunks)时,如果实体与可更新边界重叠,则将其标记为活跃(EntityFlag_Active)。
- 这个标记已经被用于实体的更新和渲染逻辑中,因此无需额外实现新的机制。
- 在脑(brain)系统中,我们增加了一个预处理步骤:在执行脑逻辑之前,先遍历脑中所有实体,如果某个实体属于一个活跃的脑,则将该实体标记为活跃。
这样可以确保实体和脑系统的活跃状态一致,实现了多段实体和活动区域逻辑的统一管理。
game_entity.h
game_entity.cpp
game_sim_region.cpp
game_brain.cpp
:引入 MarkBrainActives()
我们在脑(brain)系统中引入了 MarkBrainActives()
函数,用于在执行脑逻辑之前预处理脑内的实体,使其活跃状态正确。具体逻辑如下:
- 在执行脑逻辑前做一个预处理(pre-pass),遍历脑中的所有实体,标记哪些属于活跃脑,从而将这些实体设为活跃。
- 遍历脑中的实体数组,需要知道数组中实体的最大数量。由于实体数量不是直接可用,我们通过计算脑结构大小减去数组起始偏移量,再除以单个实体指针大小,得到脑中实体的最大数量。
- 这样可以系统化地处理不同脑类型和脑中实体数量,确保不会越界访问,同时能够为每个实体正确地应用
EntityFlag_Active
。 - 该方法类似处理多段蛇形实体的逻辑,保证如果脑内任意一个实体活跃,其余实体也会被标记为活跃,从而保持整体实体状态一致。
通过这种方式,脑系统和实体的活跃状态能够在模拟和渲染前统一管理,提高逻辑的正确性和稳定性。
game_platform.h
game_brain.h
game_world_mode.cpp
game_brain.cpp
:实现 MarkBrainActives()
我们需要在 game_brain.cpp
里实现一个 MarkBrainActives()
。
具体思路可以整理如下:
- 遍历 brain 的所有实体槽位
- 我们先确定一个索引,从 0 到 brain 里允许的最大实体数量(
MaxBrainSlotCount
)。 - 对于每一个槽位,需要能根据槽位索引拿到对应的实体(需要实现
GetEntityInSlot(brain, slotIndex)
这样的功能)。
- 我们先确定一个索引,从 0 到 brain 里允许的最大实体数量(
- 检测实体是否处于激活状态
- 对每一个槽位的实体,我们读取它的
flags
。 - 如果实体包含
EntityFlag_Active
,则标记该槽位的isActive = true
。
- 对每一个槽位的实体,我们读取它的
- 累计 flags,提取整体状态
- 为了未来扩展,也为了更方便处理,我们可以在循环中累加所有实体的 flags 到一个
brainFlags
变量里。 - 遍历结束后,如果
brainFlags
中包含EntityFlag_Active
,则说明至少有一个实体是激活的。
- 为了未来扩展,也为了更方便处理,我们可以在循环中累加所有实体的 flags 到一个
- 回写到所有实体
- 如果检测到整个 brain 中有至少一个实体激活,则将该状态回写给所有在该 brain 中的实体。
- 也就是说,不仅单独实体保持激活,而且整个 brain 的所有实体都会被统一标记为激活。
- 需要额外实现的部分
- 目前缺少
GetEntityInSlot()
这样的函数,用来根据 brain 和槽位索引返回实体。 - 由于内部结构里已经有
offsetof
相关的处理逻辑,可以复用它来计算索引位置,避免重新设计。
整体效果是:
- 目前缺少
MarkBrainActives()
会扫描 brain 内的所有槽位,收集状态并统一标记激活状态。- 只要有一个实体激活,那么整个 brain 下的所有实体都会被拉入“激活”集合中。
game_brain.h
game_brain.cpp
game_world_mode.cpp
game_platform.h
:引入 OffsetOf()
我们需要在 game_platform.h
里引入一个通用的 OffsetOf()
宏或函数。
整理思路如下:
- 需求背景
- 在代码里已经有两个地方使用到了“获取结构体成员相对于结构体起始地址的偏移量”的操作。
- 目前是手动写的,需要统一成一个通用工具。
- 实现方式
- 给定一个结构体类型和其中的一个成员,
OffsetOf(type, member)
就能计算出该成员在结构体里的字节偏移。 - 本质上就是拿到成员的地址,再减去结构体的基地址。
- 给定一个结构体类型和其中的一个成员,
- 好处
- 避免重复代码,以后需要获取偏移时直接使用
OffsetOf()
。 - 代码可读性更高,清晰地表达出这是在获取结构体成员偏移。
- 可维护性更强,后续需要修改或扩展只需改一处。
- 避免重复代码,以后需要获取偏移时直接使用
- 存放位置
- 由于这是一个常用基础工具,适合放到
game_platform.h
这种全局可见的头文件里。 - 这样后续在任何模块里都能直接使用。
- 由于这是一个常用基础工具,适合放到
- 应用场景
- 例如在实现
GetEntityInSlot()
时,可以通过OffsetOf()
计算出某个槽位内实体的正确偏移地址。 - 在内存管理、数据对齐、序列化等场景里,也能复用该工具。
- 例如在实现
game_brain.h
:引入 GetEntityInSlot()
我们在 game_brain.h
里需要引入 GetEntityInSlot()
,其设计与实现要点可以整理如下:
- 函数目的
- 根据传入的 brain 和槽位索引,返回该槽位对应的实体指针。
- 这样我们就能遍历 brain 内部所有实体槽位,逐个访问实体。
- 参数与校验
- 传入
brain
(当前大脑结构体指针)和slotIndex
(槽位索引)。 - 在函数里需要断言
slotIndex < MaxBrainSlotCount
,保证不会越界。
- 传入
- 索引方式
- brain 内部保存的其实是一个“指向实体指针的数组”。
- 我们可以把它当作一个数组来处理,直接用
brain->EntityPointers[slotIndex]
的形式拿到指定槽位的实体。
- 返回结果
- 直接返回
slotIndex
对应的实体指针。
- 直接返回
- 空槽位处理
- 槽位可能是空的(即对应的指针为
null
)。 - 在返回结果时,需要判断该实体指针是否为空。
- 调用方在拿到结果后,也要进行检查,确保只在非空情况下对实体进行修改。
- 槽位可能是空的(即对应的指针为
- 整体效果
GetEntityInSlot()
提供了一个安全的接口,既能保证索引合法,又能处理空槽位。- 结合
MarkBrainActives()
,我们就能可靠地遍历并操作 brain 内的所有实体。
game_brain.h
game_brain.cpp
运行游戏并更新待办事项列表
我们在运行游戏并整理待办事项列表时,主要做了以下思考和规划:
- 关于大脑与实体激活的处理
- 当前的逻辑只是架构性地放进去,还不确定是否完全正确,但至少可以保证在向前推进时有这个概念存在。
- 之前担心的“非激活区域的实体可能移动到不可用格子”的问题,在新的流式加载机制下被部分缓解。因为未被加载的格子根本无法移动到,所以不会出现越界或非法移动。
- 调试与次要问题
- 调试相关的代码暂时搁置,留待以后更有兴趣时处理。
- 修复点击音频样本末尾的 bug 也是低优先级问题,不影响架构,后续代码清理时再考虑。
- 重要待办与架构方向
- 多模拟区域/实体时钟:这是一个较大方向的工作,与 AI 路径规划类似,都属于架构层面问题。
- AI 路径规划:小范围的寻路在单屏内可行,但大规模世界寻路需要新的策略。倾向于维护一个全局的路径网格或路径网络,避免为每个实体单独做大规模搜索。
- AI 存储:直接存放在实体本身即可,不是大问题。
- 世界生成:在生成世界时需要引入连通性概念,用于大规模路径规划(例如门、地形骨架等),这样在巨大地图中仍能高效寻路。
- 渲染相关的待办事项
- Z 方向处理:需要进一步整理,确保坐标系统彻底清晰,特别是涉及飞行单位越过矮墙之类的情况。
- 绘制优化:18,000 次绘制矩形主要来自字体,不是严重问题。硬件渲染已支持渲染到纹理,因此性能基本满足需求。
- 着色器:非当前重点,但如果未来需要实现光照,必须引入着色器,因为固定管线无法支持。
- 最终优化:软件渲染器只做了基础优化,未来如果需要让软件渲染更高效,可以进一步深入,但当前不是重点。
- 后续渲染功能
- 粒子系统:优先级较高,影响渲染管线的组织。
- 光照系统:需在粒子系统之后再做,且要结合新美术资源一起考虑。光照实现本质上是近似模拟,需提前规划。
- 美术资源切换
- 当前测试资源存在大量问题(空白区域过大、对齐错误,导致排序效率低)。
- 已有新的完整资源集(包含多种角色、敌人、地形),视角和比例正确。
- 计划在粒子系统完成后切换到真实美术资源,再进行光照相关工作。
- 同时清理多部件实体及动画逻辑,使其更接近传统动画系统,避免 ad hoc 的临时实现。
- 碰撞与动画
- 碰撞检测仍需继续完善,尤其是 Z 方向上的交互。
- 动画基本令人满意,只需在切换到新美术时清理相关逻辑即可。
整体来看,当前的重点路径是:
完成 Z 方向的整理 → 实现粒子系统 → 切换到新美术 → 实现光照 → 清理多部件实体动画。
黑板讲解:多个模拟区域与活跃实体
我们在黑板上梳理“多个模拟区域与活跃实体”的问题时,可以总结如下:
- 问题背景
- 游戏世界可能非常庞大,甚至大到需要花 30 分钟才能从一端走到另一端。
- 在这样的大世界里,希望支持“即使玩家不在场,世界里依然有自治行为在发生”。
- 举例:一条龙在地图另一侧飞行,如果玩家正好在路径下方,就能看到它掠过天空。
- 需求
- 必须能在玩家所在区域之外,独立更新世界的部分区域。
- 这意味着需要 多个模拟区域(Sim Regions) 并行更新。
- 活跃实体机制
- 可以维护一个 活跃实体列表。
- 当某个区域内存在活跃实体时,对该区域执行时间步更新,就像玩家所在区域一样。
- 这样即使玩家不在附近,带有活跃实体的区域依然会被推进。
- 实现思路
- 目前模拟系统的核心流程(
BeginSim
)本质上是独立的。 - 除了“从世界中取出数据(pull out)”和“写回世界(push in)”这两步涉及全局资源外,其余部分都是在局部数据结构中独立运行。
- 因此,多个模拟区域可以在不同线程、不同 CPU 核心上并行运行,不会互相干扰。
- 目前模拟系统的核心流程(
- 潜在难点
- 真正棘手的部分是 区域重叠或交汇时的处理。
- 如果两个区域的模拟范围有重合,或者两个活跃实体跨越区域边界,就需要决定如何同步与协调。
- 这会带来复杂性,例如:一个实体同时出现在两个区域内时,怎样避免重复模拟或状态冲突。
- 总结
- 多个模拟区域的框架本身并不难建立,技术上也能利用多核并行提升效率。
- 主要挑战在于 区域交汇与状态一致性 的处理,这需要额外的设计和同步策略。
黑板讲解:当模拟区域重合时
在讨论模拟区域重合时,我们梳理出以下关键点和问题:
- 重合场景示例
- 假设有一个龙和一个英雄,每个都有自己的模拟区域。
- 当两者靠近时,模拟区域会出现交集。
- 如果不加处理,先处理的区域会把交集部分抓取到自己的模拟中,后处理的区域就无法更新这些内容,这会导致逻辑错误和各种 bug。
- 简单解决思路
- 可以在更新前检查区域是否相交。
- 如果两个区域相交,可以只更新其中一个区域,避免重复模拟。
- 对 NPC 区域来说,这种方式可行,因为它们不是每帧必须更新。
- 英雄区域的特殊性
- 英雄的模拟区域必须每帧更新。
- 如果与龙的区域重合,龙的更新就可能被阻止或延迟,这会导致行为异常(比如龙停滞,直到玩家接近),体验不合理。
- 顺序更新的问题
- 顺序调用更新区域也不可行,因为交集部分会被更新两次,相当于进行两次时间步,这会破坏物理和逻辑一致性。
- 可能的解决方案——每实体时钟(Per-Entity Clocking)
- 给每个实体一个独立的时钟。
- 当某个模拟区域覆盖该实体时,检查其时钟是否已经在本帧更新过。
- 如果已经更新过,则跳过该实体的模拟,但仍保留它用于碰撞检测等逻辑。
- 潜在问题
- 每实体时钟方案可能对大脑(Brain)逻辑造成问题,因为某些区域无法被“部分更新”。
- 这意味着不能简单地跳过区域中的一部分,否则会破坏整体一致性。
- 目前还不清楚如何完全解决这个重叠问题,仍需要进一步探索和设计。
黑板讲解:合并模拟区域
在讨论合并模拟区域时,我们梳理出以下内容和问题:
- 合并模拟区域的思路
- 如果性能允许,可以考虑将重叠的模拟区域合并为一个区域。
- 模拟区域不必是矩形,可以从不同矩形区域合并成一个非矩形、凸形的区域。
- 这样可以避免重叠区域被重复更新或遗漏的问题。
- 潜在问题
- 合并后的模拟区域会增加模拟成本,因为区域范围变大,涉及的实体更多。
- 在最坏情况,例如英雄、龙、蛇怪、九头蛇、狮鹫等都在不同位置,但区域稍微重叠,最终可能合并成一个大区域。
- 这样会导致单次模拟的计算量成倍增加,同时无法利用之前的优化手段。
- 优化手段受限
- 原本可以通过多线程对不同模拟区域并行更新,但合并后所有实体在同一个区域,就不能再按区域分配线程。
- 也无法进行时间片轮转(Time-slicing)处理,因为整个区域必须一次更新完成。
- 可控场景的性能优化
- 如果有大量实体(比如 300 个),可以通过以下方式降低每帧负载:
- 每 10 帧更新一次每个实体,这样每帧只需更新 30 个实体。
- 利用多核 CPU,每个核心分配部分实体计算,每帧只需处理较少的更新任务。
- 这种方式使性能控制更可行,但前提是模拟区域保持相对独立,不发生大规模合并。
- 如果有大量实体(比如 300 个),可以通过以下方式降低每帧负载:
- 核心矛盾
- 合并模拟区域可以解决重叠带来的重复更新问题,但同时会失去多线程和时间片优化的优势,导致最坏情况下性能崩溃。
- 因此,在有大量实体或复杂交互的世界里,如何平衡区域合并与性能优化仍是一个难题,目前没有简单的解决方案。
整体来看,合并模拟区域是一个逻辑可行的方案,但在实际性能管理上存在显著风险,需要仔细权衡。
问答环节
分离区域的原因是什么?
分离模拟区域的原因主要有两个方面:
- 性能控制
- 游戏世界可能包含数百万实体,如果每帧都模拟整个世界,计算量会非常大,无法承受。
- 因此,将世界划分为独立的模拟区域,可以让大部分区域保持“休眠”,只在必要时激活更新。
- 必要时激活的区域通常是自治的,或者其中有事件、活跃实体正在发生动作。
- 统一计算空间
- 模拟区域用于将所有实体映射到一个统一的浮点空间中进行计算。
- 这样可以保证计算精度,即使世界非常大,也不会因为坐标过大导致浮点精度问题。
- 目前几乎可以不依赖瓦片概念,区域可以非常大,给世界规模提供了灵活性。
总结:分离区域既是为了性能优化,让不活跃区域休眠,又是为了保证数值计算的精度和一致性。
Bob 是树上的那个人吗?
是的,Bob 指的是树上的那个人。
(回应 JayPhi 的问题):我最近在玩《Kingdom: New Lands》,发现当战斗发生在世界另一侧时(远离镜头所在位置),帧率会很低……
在讨论远离镜头的战斗导致帧率下降的问题时,我们总结出以下关键点:
- 问题本质
- 当世界中有大规模战斗发生在摄像机不在的区域时,帧率会显著下降。
- 这种现象不是游戏逻辑错误,而是架构层面的限制。
- 随着世界规模扩大,如果没有对不关键区域的模拟进行降级处理,计算量会过大,导致性能崩溃。
- 架构应对策略
- 可以设计系统,使世界的不同区域可以独立模拟,并支持多线程处理。
- 对不在视野或不关键的区域,可以使用时间片(time-slice)更新或降低更新频率,从而节省计算资源。
- 目前的架构允许几乎免费地对世界各部分进行多线程和分段更新。
- 架构局限性
- 当多个模拟区域合并时(例如多个重叠区域需要同时更新),多线程和时间片的优势就无法发挥。
- 这种合并情况是架构性能的瓶颈,也是导致帧率问题的关键场景。
- 总结
- 为了在大规模世界中维持性能,需要架构上支持区域化、多线程更新和不活跃区域的模拟降级。
- 即便如此,当区域合并或重叠时,仍然可能出现性能瓶颈,需要进一步设计解决方案。
有没有考虑只与主角所在区域合并,并停用其他区域,或者将区域对齐到网格/房间,以避免重叠?
考虑只与主角所在区域合并并停用其他区域,或者将区域对齐到网格以避免重叠时,我们得出以下结论:
- 只合并主角区域并停用其他区域的局限
- 不能简单停用其他区域,因为这些区域的实体仍然需要被更新,否则会导致它们停止移动或产生逻辑错误。
- 例如,如果只合并主角区域,像 Basilisk 或 Manticore 这样的实体可能会被冻结,造成潜在的游戏性问题。
- 虽然可以通过引入一些小的游戏漏洞来暂时解决问题,但我们更希望找到一个普适的解决方案,让系统在所有情况下都能稳定工作。
- 将区域对齐到网格以避免重叠的难点
- 对齐到网格的方法在理论上可行,但实际实现存在困难。
- 当前的系统中已有模拟区域的概念,但如何精确对齐并避免重叠,还没有清晰可行的方法。
总结:
- 停用非主角区域会导致实体逻辑中断,不理想。
- 网格对齐有潜力,但实现难度较大,需要进一步设计。
- 我们倾向于寻找一个在各种情况下都稳健的解决方案,而不是依赖临时折衷。
黑板讲解:活跃区域 + 边界区域(Apron)
关于活跃区域(Active Region)和边界区域(Apron)的讨论,可以总结如下:
- 边界区域(Apron)的重叠问题
- 即使我们确保活跃区域不重叠,边界区域仍然可能重叠。
- 要完全避免边界区域重叠,就必须限制实体(如龙)与主角之间的最小距离,这与游戏设计目标相反。
- 因此,边界区域是当前模拟区域重叠问题的核心难点。
- 活跃区域的重叠问题
- 活跃区域通常包括所在房间以及周围扩展的两层房间。
- 当主角和龙在相邻或相同房间时,活跃区域也会重叠,从而可能需要合并区域进行模拟。
- 如果龙远离主角超过一房间距离,则活跃区域重叠问题减少,但无法彻底解决。
- 潜在解决思路
- 可以考虑为其他实体的模拟区域不使用房间边界扩展,这样可能减少或避免重叠。
- 这样做可以让活跃区域的更新更加局部化,但仍需谨慎处理以保证实体行为逻辑正确。
总结:
- Apron 的重叠是模拟区域冲突的主要问题,直接限制实体距离不可取。
- 活跃区域在相邻房间也可能重叠,需要设计合理的合并或缩减策略。
- 对非主角实体取消房间边界扩展可能是缓解重叠的一种可行方法。
是否可以使用更小的区域,同时加载九个活跃区域,以保证每个边界区域都被加载?
关于是否可以使用更小的区域并同时加载九个活跃区域以保证每个边界区域都被加载,可以总结如下:
- 提议内容
- 想法是将模拟区域划分得更小,然后在每帧同时加载九个活跃区域,以确保每个边界区域都被包括在内。
- 可行性与问题
- 理论上,如果更新总是与网格对齐(比如按房间对齐),可以尝试让非主角的区域不包含重叠的边界部分,从而避免部分冲突。
- 但是这种方法实际上可能无法解决问题,因为即使区域变小,边界区域的重叠问题仍然存在。
- 因此,单纯通过缩小区域并加载多个活跃区域的方式,不能完全保证边界区域冲突得到妥善处理。
- 总结
- 使用更小的区域并同时加载多个活跃区域的想法在概念上可行,但实际操作中仍面临边界区域重叠的问题,不能简单地依靠这种方法来解决冲突。
黑板讲解:需要加载相邻区域的一部分
在模拟一个房间时,为了让角色能够走出门,我们至少需要加载相邻房间的一部分区域,以确保角色可以落在门外的可行走瓦片上。如果没有加载这部分相邻区域,角色就无法顺利通过门,这意味着在任何情况下,我们总会遇到需要加载相邻区域一部分的问题。
能否确保被边界区域拉入的区域不会激活自身的边界区域?
即使确保被边界区域(Apron)拉入的区域自身不激活其边界区域,也无法彻底解决问题,因为问题的根源在于活跃区域及其边界区域的重叠。当活跃区域或其边界区域相互重叠时,即使对方未标记为活跃,仍然无法直接多线程处理,需要采用其他方法处理。但这种情况下,我们仍然可以对更新进行时间切片(time slicing),以管理处理顺序和性能。
黑板讲解:时间片轮转与只与一个重叠区域合并
我们考虑时间片轮转的方式来缓解问题,即只与主角所在区域合并,但只合并到一个相邻区域。如果两个实体的区域重叠,我们不合并它们,也不与其他外围区域合并。这样可以部分控制重叠区域的处理。然而,这仍然存在时钟(clocking)问题:当该区域在下一帧被另一个区域更新时,里面的实体可能会被更新两次,而主角所在区域的实体只更新一次,这会导致更新次数不一致,目前尚不确定如何完全解决这个问题。
当两个活跃区域重叠时,实际问题是什么?
两个活跃区域重叠的实际问题主要是性能消耗。如果有足够多的重叠区域,模拟它们会消耗过多 CPU 资源,这是唯一的主要问题。如果性能不是问题,我们理论上可以总是将重叠区域合并为一个单一的区域,并运行该区域的模拟,虽然这样可能导致帧率下降,但功能上是可行的。
可以人工让非玩家代理远离彼此
可以通过人为方式让非玩家代理彼此保持距离,从而降低活跃区域重叠的可能性。如果没有更好的技术方案,这种方法可能是最可行的解决办法。
你觉得在每个可更新对象上增加一个布尔标记如何?每次更新时翻转标记,每帧交替检查哪个值
考虑在每个可更新对象上增加一个布尔标记,每次更新时翻转标记,然后每帧交替检查标记值。这种方法类似于每个实体的时钟机制。具体做法是在实体中记录上一次更新的时间戳,每次更新时根据当前帧的时间戳判断上次更新与现在之间经过了多少帧,从而确定可以给予实体的移动范围或状态变化量。这种机制可以确保即使模拟区域重叠,也能正确计算每个实体的状态而不重复更新。
为什么仅使用所有重叠区域的总面积(并拒绝超过限制)不够?
仅使用所有重叠区域的总面积,并设置一个上限来限制模拟量是可行的,但有几个问题:随着上限增加,模拟会变慢;当有足够多的实体聚集在一起超过上限时,会出现不良行为或异常表现。虽然这种方法可能是实际可行的,但并没有一个完全干净的解决方案,也不一定是必须实现的功能。其核心目标是希望在玩家不在的区域仍能有世界事件发生,但目前还不确定这种功能的可行性和实际需求。游戏本身并不强制要求实现这个机制,只是考虑到现有的模拟区域系统,理论上可以部分实现这种效果,因此值得探索和测试,但并不是当前必须解决的问题。
更多推荐
所有评论(0)