把 AI 生成的建筑、道路和路灯整理成模块化城市后,编辑器里的完整场景可能一切正常;进入运行模式,玩家从 A 区走到 B 区,却突然多出一栋楼、少一段道路,返回旧区域后甚至还会撞上看不见的墙。
这类问题不能只靠扩大加载距离解决。场景分块后,每个运行时对象都需要明确的身份、归属和生命周期;视觉、碰撞、导航与任务依赖也未必在同一时刻准备完成。可靠的排查顺序是:固定复现路线,再核对对象编号、分区归属、坐标变换、实例所有权、依赖状态和卸载回收。
完整场景正常,为什么分块运行就出错?
在编辑器中打开完整城市时,建筑、道路和路灯都来自同一份场景数据,许多引用关系会自然成立。流式加载则不同:系统需要根据玩家位置动态决定加载哪些分区、创建哪些实例,以及何时销毁已经离开的内容。
如果这些规则不清楚,就会出现三类典型异常:
- 重复加载:同一个运行时对象被两个分区或两次加载请求重复创建。
- 内容缺失:对象没有被任何分区负责,或依赖尚未准备完成就被标记为可用。
- 卸载残留:视觉对象已经消失,碰撞、导航、触发器或任务引用仍留在场景中。
完整场景能够显示,只能证明静态数据拼在一起时没有明显缺口,不能证明运行时的分区边界、实例引用和加载生命周期正确。
图注:城市从块面到成品适合说明分区与模块组织,但运行时是否会缺块、重复加载或正确回收,仍需真实日志验证。图片不能证明存在可流式加载世界、唯一对象编号、正确实例引用、分区卸载或性能数据;画面文案不是运行证据。
第一步:固定路线与加载窗口,建立可复现基线
“走到这里少了一栋楼”不是足够精确的错误记录。流式加载通常受玩家位置、移动速度、加载半径、异步任务耗时和设备性能共同影响。测试路线不固定,就很难判断异常究竟来自对象归属,还是一次加载任务恰好没有及时完成。
先建立一条最小路线:从district_A出发,穿过分区边界进入district_B,停留数秒后再返回 A 区。每次使用同一构建版本、同一移动速度和同一加载范围,并记录:
build_version | player_position | active_cells loading_cells | unloading_cells | pending_jobs first_error_position | object_id | error_type
异常必须落到具体对象上。不要只写“远处少了一片”,而要记录缺失的是哪栋建筑、哪段道路或哪个碰撞体。只有知道对象身份,才能继续追查它本应属于哪个分区、由谁创建,以及是否已经被旧任务回收。
扩大加载半径可以延长预加载时间,却也会增加内存和同时活动的对象数量。它可能暂时遮住缺块,无法修复重复实例、错误坐标和卸载残留。
第二步:核对对象编号、资源编号与分区归属
实例化场景中最容易混淆的是“资源”和“对象”。多栋建筑可以共享同一份网格与材质资源,因此拥有相同的asset_id;但每栋放进城市里的建筑仍应有独立的object_id、变换数据和分区归属。
如果复制分区数据时沿用了同一个object_id,系统可能把两个实例误认为同一对象;如果边界建筑同时写入 A、B 两个分区,又没有去重规则,则可能被创建两次。
建议至少记录:
object_id | asset_id | owner_cell | requested_by instance_count | runtime_state
其中:
object_id表示场景中的具体实例;asset_id表示它引用的网格、预制体或资源;owner_cell表示谁负责它的创建和销毁;requested_by用于追踪本次加载由哪个任务发起。
边界上的桥梁、道路和大型建筑最好只有一个主所有者。确实需要被多个分区访问时,可以共享引用或由上层常驻分区管理,但不能简单复制成多份独立对象。
运行日志最终要能回答三个问题:对象是谁?由哪个分区负责?当前场景里有几份?如果同一个object_id在一次运行周期中同时处于两个活动实例中,应先修复身份或创建逻辑,而不是调整可见距离。
第三步:统一世界坐标、局部原点和父级变换
有时对象并没有缺失,只是被加载到了错误位置:建筑落到地底、道路偏移到相邻街区,或者同一对象因重复叠加父级偏移而产生重影。
这类问题通常来自世界坐标和局部坐标混用。
- 世界坐标:对象在整座城市中的位置;
- 局部坐标:对象相对分区根节点或父对象的位置;
- 分区原点:局部数据换算到世界空间时使用的基准。
可以把它理解为“城市地图坐标”和“街区内部坐标”。如果导出时记录的是街区内部位置,加载时却直接当成城市坐标使用,对象自然会出现在错误位置;如果分区偏移已经写入对象坐标,运行时又加了一次父级偏移,就会出现双重位移。
对异常对象比较三组数据:
- 编辑器中的最终位置;
- 分区文件或序列化数据中的位置;
- 运行时实例的世界位置。
同时核对父级、旋转、缩放和单位。所有城市分区应统一约定:原点如何定义、保存局部坐标还是世界坐标、单位采用什么标准,以及父级变换在哪个阶段应用。
不要通过扩大加载范围来补偿坐标错误。对象被提前加载,也不会因此回到正确位置。
第四步:检查实例、合批与跨区引用的所有权
建筑主体只出现一份,不代表它的窗户、装饰、碰撞代理和触发器也只有一份。复杂模块可能由多个子对象组成;跨区合批、父子层级和共享引用处理不当,会让这些部分拥有不同的生命周期。
这里的“所有权”指谁负责创建、更新和销毁对象,而不是谁能够看见它。一个对象可以被多个系统引用,但最好只有一个明确的生命周期所有者。
重点检查四类情况:
- 静态合批是否把不同分区的对象合到同一个运行单元;
- 分区 A 卸载父对象后,分区 B 是否仍持有子对象或碰撞体引用;
- 视觉实例与碰撞代理是否由不同任务重复创建;
- 对象池回收后,旧实例是否仍保留激活状态或旧分区标记。
不同引擎、渲染管线和场景系统对静态合批、实例化与分区卸载的处理并不完全相同。调试阶段可以暂时关闭跨区合批,让对象边界和生命周期更容易追踪;确认创建与回收正确后,再逐步恢复优化。
性能优化不能建立在错误的对象管理上。即使合批降低了绘制调用,只要一个分区卸载会误删另一区的碰撞或装饰,这套方案仍然没有通过验收。
第五步:分别管理视觉、碰撞、导航和任务依赖
建筑已经显示,不代表新区已经可以进入。视觉模型、碰撞、导航、触发器和任务绑定通常来自不同数据,也可能通过不同异步任务完成。
如果系统只用“场景对象已创建”作为加载完成条件,可能出现:
- 建筑已经可见,碰撞尚未启用,玩家直接穿墙;
- 道路已经显示,导航数据还不能查询,NPC 停在分区边界;
- 视觉对象已卸载,旧碰撞仍留在原地;
- 任务先绑定到尚未创建或已经卸载的目标对象;
- 触发器重复注册,玩家进入一次却执行两次事件。
建议把分区状态拆开记录:
visual_ready | collision_ready | nav_ready trigger_ready | gameplay_ready | allow_entry
只有当前玩法真正依赖的内容全部准备完成,才把分区标为gameplay_ready。例如,玩家步行进入新区至少需要视觉与碰撞;NPC 需要跨区寻路时,还要等待导航;任务目标位于新区时,必须等对象创建和触发器绑定完成。
不要用“加载后延迟两秒”代替状态判断。开发机上的两秒可能足够,最低目标设备上却未必;反过来,固定等待也会让已经准备完成的场景无故停顿。
第六步:测试往返、快速穿越和过期任务回收
从 A 区走到 B 区一次,只能证明最简单路径可能正常。流式加载更容易在多次往返、快速移动和异步任务交错时暴露问题。
建议做三轮测试。
第一轮:正常步行 A→B→A
检查返回旧区域后,建筑、路灯、道路、碰撞和触发器数量是否恢复到第一次进入时的状态。加载一次、卸载一次后,对象总数不应无理由增加。
第二轮:高速穿越多个分区
让玩家或调试相机快速经过 A、B、C 三个区域。重点检查旧加载任务是否在玩家离开后才完成,并把已经过期的对象重新写回场景。
可以为每次请求增加request_id或分区状态版本。任务完成时先确认自己仍是当前有效请求;已经取消或过期的结果应被丢弃或安全回收,不能覆盖更新后的分区状态。
第三轮:在边界暂停、传送和重开
在加载与卸载同时发生时暂停,再把玩家传送回旧区域或执行完整重开。检查待处理任务是否被正确取消,对象池中的实例是否恢复初始状态,旧碰撞、触发器和任务引用是否已经清除。
每轮记录:
load_count | unload_count | active_instances duplicate_instances | orphan_objects | pending_jobs memory_before | memory_after | stale_request_result
如果实例数或内存随着往返次数持续增加,即使画面暂时正常,也说明卸载、对象池复位或引用回收尚未完成。
在整理大场景的空间关系时,可以使用世界模型作为构思和组织入口。但它不能替代流式加载验收、对象唯一编号检查、异步任务管理或运行时回收测试。
上线前检查这6项
城市分块加载进入项目之前,可以按下面六项验收:
- 复现路线是否固定?A 区到 B 区再返回的异常能否稳定重现?
- 对象身份是否清楚?
asset_id与object_id是否分开,每个运行时实例是否唯一? - 分区归属是否唯一?边界对象是否有明确所有者,共享对象是否有统一规则?
- 运行时变换是否一致?编辑器、序列化数据和运行实例的位置、旋转、缩放是否对应?
- 依赖是否分别就绪?视觉、碰撞、导航、触发器和任务是否按真实状态报告 ready?
- 加载与回收是否可重复?多次往返、快速穿越和重开后,实例数、内存与功能状态是否恢复正常?
AI 生成的建筑、道路和城市模块可以加快白模搭建与场景组织,但“完整城市能够打开”与“城市能够稳定流式加载”是两个验收层级。进入 Unity、Unreal 或其他实时环境后,分区边界、实例身份、异步依赖和对象生命周期仍需单独设计与验证。
排查时,先回答“哪个对象、属于哪个分区、由哪次请求创建、现在有几份”,再调整加载半径和性能参数。这样才能分清建筑重复、道路缺块和旧碰撞残留分别来自哪里。
你的大场景更常出现建筑重复、道路缺块,还是返回旧区域后碰撞仍然残留?