无极缩放初探索
从 Unity 客户端视角拆解 SLG 大地图无极缩放:观察尺度、多层 LOD、三层 AOI、资源生命周期,以及昼夜 Shader 如何协同工作。
在进入技术拆解之前,可以先通过下面的演示观察无极缩放在不同地图尺度之间的连续转换。
SLG 大地图中的“无极缩放”,并不是把一张高精度地图一直拉远。它真正做的是:用连续变化的摄像机驱动一组离散的 LOD,在不同距离下更换地形、城市、世界对象和 HUD 的表达方式。
这套实现最值得研究的不是缩放动画,而是四个互相咬合的系统:
- 摄像机距离如何映射成多层 LOD,每一层为什么存在、负责显示什么;
- 地图如何分格,AOI 如何随相机移动加载和回收内容;
- 多套地形和对象资源如何规划生命周期,既保持移动连续,又避免内存不断增长;
- 昼夜状态如何转换成灯光、材质和 Shader 的最终画面。
一、先理解 DXF:LOD 判断并不直接使用摄像机高度
摄像机配置同时包含距离、视野角、朝向等信息。系统用一个名为 DXF 的观察尺度统一描述当前镜头:
DXF = Camera Distance × Field of View
只使用摄像机高度存在一个问题:如果高度不变、FOV 变大,屏幕实际覆盖范围已经扩大,但 LOD 系统却认为镜头没有变化。DXF 把距离和 FOV 合并成一个近似的屏幕覆盖指标,因此比单独读取 Transform 高度更适合驱动大地图。
缩放过程中,摄像机会在预设的距离、FOV 和朝向关键状态之间插值,得到连续的 DXF。随后 LOD 系统把连续 DXF 映射成离散观察层级。
这里有两个不同概念:
- 地形 LOD:决定加载哪一级地图资源;
- 语义观察层级:决定城市、部队、资源点、联盟建筑和 HUD 应该怎样显示。
两者不是一一对应。地形可以不变,但城市和 HUD 已经开始切换;这正是两个过渡层存在的原因。
摄像机如何真正驱动渲染
这套无极缩放采用的是:
透视摄像机 + 固定俯视方向 + DXF 状态机 + AOI 流式渲染。
渲染使用 Built-in Render Pipeline 和普通 Perspective Camera。它不是正交摄像机缩放,也不是通过 RenderTexture 放大缩小一张地图,更不是只靠 Unity LODGroup 自动换模型。LODGroup 只能处理单个对象内部的 Renderer,而无极缩放需要同时改变地形块、城市表现、世界对象、HUD 和加载范围,因此必须由一套业务层状态机统一驱动。
1. 摄像机控制层:产生唯一的镜头状态
摄像机控制层是整条链路的状态源。输入系统改变镜头后,它负责维护:
- 地面观察中心;
- DXF;
- 摄像机距离;
- Field of View;
- 固定俯视方向下的最终 Camera Transform。
摄像机不是围绕世界原点拉近拉远,而是围绕地面观察中心移动。缩放时先确定新的 DXF,再从摄像机参数曲线中取得对应的距离和 FOV,最后沿固定观察方向重新计算 Camera 位置。
滚轮或双指缩放还会在缩放前后分别把指针位置投射到地面,然后用两个交点之间的差值修正观察中心。因此玩家指向的城市或地块不会因为 FOV 和距离同时变化而从指针下滑走。
摄像机每次应用新状态后,都会发布一条镜头状态事件,其中包含观察中心和 DXF。其他系统不直接监听鼠标滚轮,也不各自计算摄像机高度,只接收这份统一状态。
2. 地形 LOD 决策层:把连续 DXF 转成资源等级
地形 LOD 决策层负责把连续 DXF 转换为离散资源等级。
它持续读取当前观察距离,只有跨过 LOD 边界时才触发等级变化。地形等级改变后,它会:
- 更新地图单元宽度曲线;
- 更新与镜头距离相关的裁剪配置;
- 记录前后 LOD;
- 通知地图系统执行一次强制刷新。
这一层只回答“地形应该使用哪一级资源”,不处理城市、部队和 HUD。
3. 观察层级协调层:把镜头变化翻译成游戏语义
观察层级协调层订阅镜头状态事件,并接收观察中心和 DXF。它收到镜头变化后,会完成几件事:
- 根据 DXF 计算当前语义观察层级;
- 判断是否进入或离开主城;
- 判断是否跨过战术与战略视角边界;
- 分别执行第一、第二过渡层的进入和退出逻辑;
- 标记世界对象和 HUD 需要刷新;
- 广播统一的观察层级事件,让其他业务系统同步响应。
这也是为什么地形 LOD 和语义 LOD 必须分开。地形决策层关心应该加载哪一级地形资源,观察层级协调层关心玩家此时应该看到什么游戏信息。两者都由同一个 DXF 驱动,但可以在不同边界做不同事情。
4. 地图流送层:根据观察中心更新地图工作集
地图流送层不直接响应输入。它在地图数据准备完成后读取地面观察中心,再分别计算观察中心落在哪个小型单元、大型单元和逻辑 Tile 中。
观察中心没有跨格、LOD 也没有变化时,地图流送层不做地图重建。只有以下条件之一成立才更新:
- 进入新的小型单元;
- 进入新的大型单元;
- 进入新的逻辑 Tile;
- 地形决策层改变了地图单元宽度或地形 LOD;
- 外部要求强制刷新。
随后地图流送层把工作分发给三种空间单元:
- 小型单元:维护近景树林等细粒度装饰;
- 大型单元:维护地表、河流等较大范围内容;
- 动态单元:根据相机地面矩形维护中远景合并 Tile。
每个 Cell 只负责自己矩形范围内的对象。Cell 离开 AOI 时统一回收其内部实例,进入 AOI 时根据地图数据创建所需内容。
5. 世界对象和 HUD:使用配置决定最终表现
城市、部队、资源点和联盟建筑不跟随地形 Cell 一起创建。世界对象表现层为每类实体保存多套表现配置,每套配置都包含:
- 适用的最小、最大观察层级;
- 使用三维对象还是 HUD;
- 应加载哪类表现资源;
- 创建后如何绑定实体数据;
- 是否还需要额外显示条件。
镜头移动或观察层级变化后,协调层标记对象列表需要刷新,再分批检查世界实体。符合当前层级的表现被创建或保留,不再符合的表现被回收。
远景 HUD 创建后交给专门的世界坐标投影组件。它每帧检查镜头距离是否变化或摄像机是否仍在滑动,然后:
- 从世界坐标重新计算 UI 坐标;
- 读取 HUD 的屏幕缩放曲线;
- 更新 RectTransform 的位置与缩放;
- 通过 CanvasGroup 处理透明度和显隐。
因此,远景图标不会随着三维透视无限缩小,而是在屏幕空间保持可读尺寸。
6. 为什么这套连接方式能看起来像一个连续世界
缩放效果之所以自然,不是因为某个淡入淡出特别复杂,而是因为几个系统共享了同一组不变量。
首先,所有系统只认摄像机控制层输出的观察中心和 DXF。地图、城市、世界对象和 HUD 不会各自推导一套镜头状态,因此不会出现相互错层。
其次,地形 LOD 与语义 LOD 分开。地图资源可以在一个边界变化,城市代理和 HUD 在另一个边界变化,多个视觉变化不会堆在同一个瞬间。
第三,地图使用增量工作集。摄像机平移时只替换离开和进入 AOI 的一行、一列或少量 Tile,保留区域中的 GameObject 不动,因此画面不会因为地图更新整体重建。
第四,所有 LOD 共用同一套逻辑坐标。地形 Tile、城市代理和 HUD 都从同一个世界位置计算 Transform 或屏幕投影,所以表现形式改变以后,对象仍然留在原来的地理位置。
最后,三维对象和 HUD 分别处理各自最擅长的尺度。近景交给 Renderer,远景交给屏幕空间 UI;HUD 再通过独立缩放曲线抵消透视缩小。玩家看到的是信息连续,内部其实已经从场景渲染切换到了界面渲染。
二、为什么是七个观察层级
系统定义了七个有效观察层级:
| 观察层级 | 地形表现 | 世界对象与 HUD 的主要职责 |
|---|---|---|
| City | LOD0 详细地形 | 完整主城、建筑、居民、局部特效 |
| Tactical | LOD0 详细地形 | 退出城内模式,显示近距离世界对象和普通标题 |
| TacticsToStrategy1 | LOD1,但仍复用详细地形内容 | 开始创建战略代理,并切换部分 HUD |
| TacticsToStrategy2 | 合并地形 LOD2 | 完成近景对象淡出,保留可进入战略层的对象 |
| Strategic | 合并地形 LOD3 | 城市、部队、资源、联盟建筑等使用战略代理 |
| Nationwide | 合并地形 LOD4 | 移除低价值对象,只保留全国尺度仍有意义的节点 |
| Continental | 最简地形 LOD5 | 地形进入最简层,大部分信息由 HUD 和少量战略节点承担 |
这些层级不是简单按等比分段,而是围绕内容切换点设计的。
1. City:完整城市内部
City 层只覆盖最接近地面的观察区间。
这一层需要显示:
- 完整城市建筑;
- 城墙、道路和城市地表;
- 居民、巡逻单位和装饰动画;
- 建筑夜景灯光;
- 局部粒子;
- 建筑级交互。
这里的主要成本不是地形,而是大量独立建筑、SpriteRenderer、动画、粒子和行为组件。因此 City 层的范围不能过大。镜头一旦离开城内观察尺度,居民和细小装饰首先失去屏幕贡献,应立即退出。
2. Tactical:保留详细地形,切换到世界地图语义
Tactical 仍然使用 LOD0 详细地形,但交互语义已经从“操作城市内部”切换到“观察城市周边”。
此时保留:
- 详细地表;
- 树林、河流和地形装饰;
- 近距离城市模型;
- 野外单位和资源点;
- 城市名称、资源等级等普通 HUD。
City 和 Tactical 共用详细地形,是因为刚离开城市时,摄像机仍然足够近。如果此时立刻切换成整块低模地图,地面纹理和地貌轮廓会产生明显跳变。
所以第一层变化不是更换地形,而是关闭城市内部内容、打开世界地图内容。
3. TacticsToStrategy1:先切对象语义,暂不切地形
第一过渡层仍然保留详细地形。虽然它被记录为地形 LOD1,但加载内容与 LOD0 相同,不会重复创建另一套完全相同的地面对象。
这一层的主要任务是启动战略代理:
- 部队开始使用统一的战略代理;
- 城市开始使用远景城市代理;
- 木材、粮食、石料、金币等资源点开始使用类型化远景代理;
- 近景城市标题开始退出;
- 远景玩家城市 HUD 和部队 HUD 开始具备显示条件。
为什么不在同一个边界同时切地形、城市、部队、资源和 HUD?
因为这样会在一帧内改变过多内容:地面轮廓、对象尺寸、标题样式和交互目标同时变化,视觉跳变会非常明显,加载峰值也会集中在一起。
第一过渡层相当于先完成“对象语义切换”,地形仍作为稳定背景保留。
4. TacticsToStrategy2:地形开始合并,过滤低价值对象
第二过渡层使用合并地形 LOD2。地形不再由大量树林、河流、地表块逐个拼装,而是由一个预先组合的 Tile 表达整个逻辑地块。
这一层仍然保留大部分世界对象战略代理,但开始淘汰只适合近距离观察的内容。
例如:
- 普通野蛮人只显示到这一层;
- 村庄和洞穴只显示到这一层;
- 部分联盟资源对象只显示到这一层;
- 城市、部队、普通资源、联盟建筑、关隘等继续进入更远层级。
这种筛选依据不是模型面数,而是战略价值。村庄和洞穴在远距离下既难以阅读,也没有全国尺度的持续观察价值;城市、行军部队和关隘则仍然影响玩家决策。
5. Strategic:稳定的战略地图
Strategic 使用合并地形 LOD3,是战斗、行军和联盟活动最重要的观察层。
在这一层中:
- 城市使用统一战略代理;
- 行军部队和运输队使用部队代理;
- 资源点使用类型化图形;
- 联盟建筑、旗帜、圣地和关隘继续显示;
- HUD 从近景名称转向部队、联盟和战略状态;
- 城市内部建筑、居民和近景粒子已经完全退出。
这一层的目标不是尽可能显示更多对象,而是在较大覆盖范围内仍然允许玩家辨认“谁在哪里、正在做什么、哪里具有战略价值”。
6. Nationwide:从实体地图转向关系地图
Nationwide 使用合并地形 LOD4。
这个尺度下,一座普通资源点或一个小型野怪已经不足以构成有效信息。系统继续保留城市、主要部队、联盟建筑、旗帜、关隘、圣地等对象,但会进一步减少低价值实体和细节 HUD。
城市、部队和普通资源的三维战略代理大多以 Nationwide 为显示上限。再继续拉远后,仍然保留大量独立三维对象会形成严重遮挡,并且每个对象只占几个像素,渲染它们已经没有意义。
7. Continental:三维对象退场,HUD 接管
Continental 使用最简地形 LOD5。
这一层保留的是大陆轮廓和少量跨区域战略节点。大部分城市、部队和资源三维代理不再继续显示,信息由屏幕空间 HUD 接管。
例如:
- 玩家城市使用专门的远景城市标识;
- 部队和斥候使用远景部队 HUD;
- 联盟建筑和运输目标使用简化状态标识;
- 少量联盟旗帜可以继续保留到洲际层,用于表达领土结构。
远景 HUD 不是附加功能,而是对象 LOD 的最后一级。如果没有 HUD 接管,三维对象退出后玩家会失去城市和部队身份;如果三维对象不退出,洲际视角又会被大量不可读的小模型淹没。
三、地形 LOD 与语义 LOD 为什么必须分开
整个系统有六档地形资源,却有七个语义观察层级。
具体映射是:
- City 与 Tactical 共用 LOD0 详细地形;
- TacticsToStrategy1 记录为 LOD1,但仍复用详细地形内容;
- TacticsToStrategy2 使用 LOD2;
- Strategic 使用 LOD3;
- Nationwide 使用 LOD4;
- Continental 使用 LOD5。
这种设计刻意让对象切换早于地形切换。
第一次过渡先把城市、部队和 HUD 改成战略表达;第二次过渡再把详细地表换成整块 Tile。这样既避免所有系统挤在同一边界切换,也为不同类型对象提供了独立退出时机。
所以 LOD 的设计原则不是“镜头每拉远一段就减一次面”,而是:
- 当前尺度还需要哪些游戏信息;
- 这些信息应该由三维对象还是 HUD 表达;
- 地面细节在屏幕上是否仍然可辨认;
- 本层可见范围扩大后,允许同时存在多少实例;
- 哪些内容必须提前退出,给下一层留下性能空间。
四、地图如何划分:40×40 个逻辑 Tile
整张地图由 40×40 个逻辑 Tile 组成,每个 Tile 的世界尺寸为 180×180,因此完整地图覆盖 7200×7200 的世界空间。
逻辑 Tile 是地图数据、远景地形和区域索引的基础单位。每个 Tile 数据主要保存:
- Tile 类型 ID;
- 世界网格位置;
- 旋转;
- 桥梁状态;
- 河流方向;
- 省份等地图信息。
近景不会为 1600 个逻辑 Tile 分别保存一套完整实例。系统另外保存一份 Tile 模板数据,模板描述某种 Tile 内部有哪些地表、树林、河流和装饰,以及它们相对于 Tile 原点的位置。
地图数据只需要记录“这里是哪一种 Tile、旋转多少”,运行时再用模板生成具体摆放。这种做法减少了重复地图数据,也让相同 Tile 类型能够在不同位置和旋转下复用。
五、AOI 不是一套网格,而是三层空间窗口
地表并不是只用一个 180×180 网格控制加载。系统在逻辑 Tile 之下又维护了三类运行时空间单元。
1. 小型单元:3×3 的基础 45×45 网格
近景基础配置中,小型地图单元尺寸为 45×45,摄像机周围始终维护 3×3 个单元,对应 135×135 的工作区。单元宽度还保留了随镜头距离曲线调整的入口;只要曲线计算出的宽度发生变化,地图就会触发一次强制刷新。45×45 是这张地图在近景阶段实际使用的基础粒度。
它主要负责近景装饰,尤其是树林等数量较多、分布密集、只在近距离有价值的对象。
因为一个逻辑 Tile 是 180×180,所以它可以整齐地分成 4×4 个小型单元。摄像机不需要一次加载整个逻辑 Tile 的所有近景装饰,只加载视点附近的九个小格。
当观察中心越过 45 单元边界时,窗口重新居中:
- 仍属于新 3×3 范围的单元继续保留;
- 离开范围的一行或一列调用回收;
- 新进入范围的一行或一列创建单元;
- 新单元从地图模板数据中查询落在自己矩形内的装饰。
水平移动一格通常只替换三列中的一列,也就是回收三个旧单元、创建三个新单元,而不是重建整个 3×3。
2. 大型单元:3×3 的基础 90×90 网格
大型地图单元的基础尺寸为 90×90,同样维护 3×3,对应 270×270 的工作区。它也可以通过镜头距离曲线调整宽度,并在宽度变化时和小型单元一起重新定位。
它负责比树林覆盖更广的地表内容,例如地面 Plane、河流和部分树木表现。一个 180×180 逻辑 Tile 可以分成 2×2 个大型单元。
为什么同时存在 45 和 90 两种固定网格?
因为不同资源的密度和回收成本不同。树林数量多、局部变化快,使用 45 单元可以减少每次移动需要变更的实例;地表 Plane 和河流覆盖范围更大,使用 90 单元可以减少容器数量和查询次数。
摄像机越过 90 单元边界时,大型窗口才重新计算。这样小范围移动主要更新装饰层,不必同步刷新整个地面层。
3. 动态单元:相机地面矩形加预加载边界
中远景使用合并后的地形 Tile,不再适合用固定 45 或 90 单元拆分。动态层直接读取摄像机视锥与地面的四个交点,得到真实地面包围矩形。
在实际可见矩形之外,四边各扩展 270 个世界单位,也就是一个半逻辑 Tile。最终矩形的宽和高都比相机真实覆盖范围多出三个完整 Tile。
扩边的目的很直接:Tile 不能等到进入屏幕以后才开始加载。预加载区让摄像机接近边缘时,下一个 Tile 已经进入 AOI。
动态层维护一个“Tile 世界位置 → 实例”的字典:
- 字典中已经存在且仍在扩展矩形内的 Tile 保留;
- 字典中离开矩形的 Tile 立即回收;
- 地图数据中位于矩形内、但字典尚不存在的 Tile 发起加载;
- 异步回调完成时再次检查位置是否还在字典中,已经离开 AOI 的结果直接回收。
六、相机移动和缩放分别怎样触发 AOI 更新
地图流送层持续读取摄像机的地面观察中心,但不会每帧重建地图。
平移触发
观察中心会分别换算成:
- 当前所在的 45 单元;
- 当前所在的 90 单元;
- 当前所在的 180 逻辑 Tile。
只有中心跨过对应边界,相关层才更新:
- 跨过 45 边界,更新小型 3×3;
- 跨过 90 边界,更新大型 3×3;
- 跨过 180 边界,重新计算动态 Tile 范围和地图数据窗口。
因此地图平移的更新成本是分层发生的,而不是所有资源同时刷新。
缩放触发
缩放即使没有改变观察中心,也可能改变:
- 摄像机地面覆盖矩形;
- 地形 LOD;
- 小型和大型单元的有效宽度;
- 应显示的世界对象类型。
LOD 发生变化时,系统设置一次强制刷新。下一次地图更新会重新读取对应 LOD 数据,并重新计算小型、大型和动态层。
地图数据的选择规则也随 LOD 改变:
- LOD0 和 LOD1 读取装饰模板与地面 Plane;
- LOD2、LOD3、LOD4 按 Tile 类型和目标 LOD 选择整块地形;
- LOD5 读取最简地图 Tile。
这就是 AOI 和 LOD 的结合点:AOI 决定“哪些位置需要存在”,LOD 决定“这些位置应该加载哪套资源”。
七、世界对象 AOI 与地图 AOI 分开
城市、部队、资源点和联盟建筑不属于地形 Tile 实例。它们拥有独立的实体数据和表现规则。
世界对象系统会同时判断:
- 对象是否位于摄像机有效范围;
- 当前观察层级是否落在对象配置的最小、最大 LOD 之间;
- 对象是否满足阵营、状态和功能条件;
- 当前层应该创建三维代理还是 HUD。
以城市为例:
- 近距离使用完整城市或普通世界城市;
- 从第一过渡层开始创建远景城市代理;
- 城市代理保留到 Nationwide;
- 普通城市标题显示到第一过渡层;
- 远景玩家城市 HUD 从第一过渡层持续到 Continental。
部队、资源点、村庄、洞穴和联盟建筑拥有各自的显示范围。系统不是用一个总开关统一隐藏全部对象,而是让每种对象在失去战略价值时退出。
为了避免一次遍历和加载过多对象,HUD 与世界对象刷新还可以拆成分批处理。摄像机移动只标记“需要刷新”,具体对象按索引逐步更新,从而避免同一帧集中创建大量代理和 UI。
八、资源规划的核心是粒度和生命周期
这套设计不依赖某一个指定加载框架。使用 Unity 官方异步资源系统、AssetBundle 或自研资源服务都可以,真正影响无极缩放的是三件事:
- 资源按什么粒度制作;
- 什么条件触发加载和回收;
- 资源依赖由谁持有、何时真正释放。
1. 近景使用模板组合,远景使用预合并 Tile
近景地形需要树林、河流、地表和装饰拥有独立位置和表现,因此使用模板数据动态组合。
远景如果继续加载这些独立对象,即使模型已经很简单,GameObject、Transform、Renderer 和 Draw Call 数量仍然过高。因此从合并地形层开始,每个 180×180 逻辑 Tile 使用一个预合并表现。
资源结构可以概括为五层:
- 地图索引数据:保存每个逻辑 Tile 的类型、位置、旋转和地图属性;
- 近景模板数据:保存一类 Tile 内部的地表、树林、河流和装饰布局;
- 远景合并地形:为不同战略尺度准备逐级简化、逐级合并的整块 Tile;
- 世界对象代理:为城市、部队、资源点和联盟建筑准备轻量战略表现;
- 世界 HUD:在三维对象失去可读性后,继续表达实体身份和状态。
这套规划的重点是:越远的层级,资源粒度越大、实例数量越少;越近的层级,资源粒度越小、局部细节越灵活。
2. 地图数据应该常驻,表现资源按 AOI 驻留
地图的 Tile 索引和模板数据体积相对可控,它们决定后续应该加载什么,适合在进入大地图时读取并保持到地图系统退出。
真正占用大量内存的是:
- Tile 表现资源;
- 城市和世界对象表现资源;
- 纹理;
- 材质;
- 粒子;
- HUD 图集和头像。
这些表现资源只按 AOI 和观察层级驻留。
3. 加载和回收时机
地图资源的生命周期可以明确到具体触发条件:
| 资源类型 | 加载时机 | 回收时机 |
|---|---|---|
| 小型单元装饰 | 进入摄像机 3×3 小型窗口 | 离开 3×3 小型窗口 |
| 大型地表单元 | 进入摄像机 3×3 大型窗口 | 离开 3×3 大型窗口 |
| 合并 Tile | 进入相机地面矩形及扩展区 | 离开扩展区或地形 LOD 改变 |
| 城市、部队、资源代理 | 进入有效范围且观察层级满足配置 | 离开范围或超过最大观察层级 |
| 世界 HUD | 对象需要由 UI 表达时 | 对象不再可见或切回三维表达 |
| 夜景粒子 | 夜间且所属对象处于激活状态 | 白天、对象回收或退出 AOI |
4. 对象池只解决实例,不能代替资源释放
对象离开 AOI 后先进入对象池,可以让相机回拉时快速复用,减少 Instantiate 和 Destroy。
但对象池只管理实例。即使池内实例已经清空,地形、纹理和材质仍可能因为加载句柄或引用计数而留在内存。
完整生命周期应该是:
进入 AOI → 加载资源 → 创建或复用实例 → 离开 AOI → 实例回池 → 长期闲置后清池 → 资源引用归零 → 卸载资源及依赖。
资源系统按固定周期扫描无用资源,对象池在最后一次回收并持续闲置一段时间后执行清理。清理闲置实例后,再检查资源是否还有活跃对象引用;没有引用的加载句柄才释放,最后由 Unity 回收已经失去引用的底层依赖。
具体周期不是重点,它表达的是一种策略:短期回拉优先复用,长期不用才真正释放。
5. 资源所有权如何防止泄漏
每个加载资源都必须知道自己的使用者。
资源服务会把加载结果与所属 GameObject 关联。对象销毁后,资源管理器可以检查还有多少存活对象继续依赖该资源。当对象池也被清空、引用数量归零时,资源才进入释放流程。
最危险的是组件内部二次加载:
- 建筑夜景组件加载夜间材质;
- 粒子组件加载灯光效果;
- HUD 加载头像;
- 动画组件加载额外序列。
这些加载同样必须绑定到对象生命周期。否则主体已经回收,附属材质或粒子资源仍可能保留句柄,形成“场景里看不到对象,内存却不下降”的隐藏泄漏。
6. 平滑与内存控制依赖预加载边界和短期缓存
地图移动保持连续主要依赖两层缓冲:
- AOI 比真实屏幕范围更大,资源在进入屏幕前开始加载;
- 刚离开 AOI 的实例先进入对象池,短时间回拉不需要重新读取资源。
内存控制则依赖另一组约束:
- 近景只维护 3×3 小格和 3×3 大格,单元宽度由观察尺度配置;
- 远景只维护相机矩形扩展区内的合并 Tile;
- 对象超过最大观察层级后不再保留三维代理;
- 闲置池定期清理;
- 无活跃引用的资源地址必须能够淘汰;
- 所有子资源加载都必须有所有者。
平滑不是把所有资源留在内存,内存控制也不是对象一离开屏幕就立即卸载。真正有效的是一个带空间缓冲和时间缓冲的生命周期。
九、先理解昼夜 Shader 的组成
这类 SLG 地图并不完全依赖 Unity 标准光照。地形、树林、山体、城市建筑和单位中存在大量 Sprite 与自定义地图 Shader,如果只旋转方向光或修改灯光强度,很多对象的颜色不会产生统一变化。
昼夜 Shader 大体可以拆成五个部分。不同类型的 Shader 会省略其中某些步骤,但整体职责基本一致。
1. 基础纹理采样
建筑和单位通常读取主纹理;地形则可能根据混合图,在多张地表纹理或多组基础颜色之间进行组合。
这一步负责物体原本的颜色和细节,与昼夜无关。
2. 局部颜色与遮罩
部分对象还会使用:
- 顶点颜色;
- 材质自身的
_Color; - 建筑或地表遮罩;
- 阵营色、选中状态或区域颜色。
局部颜色解决的是“这个对象本身应该是什么颜色”,遮罩则决定某些效果只影响窗口、屋顶、地面区域或建筑局部。
3. 全局环境色
在基础纹理和局部颜色计算完成后,Shader 再乘上一份全局环境颜色。它负责让所有地图资源同时进入黎明、白天、黄昏或夜晚色调。
这份全局环境色就是后面会用到的 _SpriteColor。它不是某个 SpriteRenderer 的私有颜色,而是整张地图共享的 Shader 全局参数。
4. 夜景遮罩与发光
夜间建筑需要窗口灯光时,会使用夜景材质或遮罩区分“应该发亮的区域”和“仍然保持暗色的建筑主体”。
基础昼夜色负责整体变暗,夜景遮罩负责重新提亮窗口、灯带等局部区域。二者分开后,夜晚不会只是简单给整张图盖一层黑色。
5. 雾、透明度与最终混合
山体边缘、树林、迷雾和部分地图对象还会继续计算透明度、雾色或边缘混合。全局环境色会在这些 Shader 中一同参与最终输出,使远景雾、地图边界和场景主体保持相同的昼夜色调。
有了这套 Shader 约定,昼夜系统不需要理解每一种建筑和地形材质。它只需要输出统一光照状态,并更新约定好的全局参数。
十、昼夜状态机怎样驱动灯光
昼夜系统把一个完整循环拆成黎明、白天、黄昏、夜晚四个稳定状态,以及相邻状态之间的过渡阶段。
驱动时间来自哪里并不是关键,本地世界时钟、游戏逻辑时钟或网络同步时间都可以。昼夜模块真正需要的是当前循环位置,并据此得到:
- 当前稳定状态;
- 正在从哪个状态过渡到哪个状态;
- 当前过渡进度;
- 现在是否已经进入夜景模式。
每个稳定状态配置三组核心参数:
RenderSettings.ambientLight使用的环境光颜色;- 主方向光颜色;
- 主方向光强度。
进入过渡阶段后,全局灯光控制器保存前后两组光照数据,并在过渡期间执行:
- 环境光颜色
Color.Lerp; - 方向光颜色
Color.Lerp; - 方向光强度
Mathf.Lerp; - 将结果写回
RenderSettings.ambientLight和主方向光。
稳定阶段不需要持续遍历场景或修改材质。状态机只在阶段变化时更新目标参数,只有正在发生光照过渡时才逐帧插值。
十一、_SpriteColor 如何统一影响整张地图
实时灯光参数更新后,还需要把同一结果传递给不完整参与 Unity 光照的地图 Shader。
全局灯光控制器根据环境光、方向光颜色和方向光强度组合出一份场景环境色,然后写入全局 Shader 参数:
Shader.SetGlobalColor("_SpriteColor", spriteColor)
地表、树林、山体、城市建筑、单位、迷雾和联盟旗帜等 Shader 在完成基础纹理、局部颜色和遮罩计算后,再将结果与 _SpriteColor 相乘。
这样更新一次全局参数,就能让整张地图同步变色,不需要逐个遍历 Renderer,也不会因为批量访问 renderer.material 制造大量材质实例。
新加载的 LOD Tile 会自动读取正在生效的 _SpriteColor,因此缩放到另一层地形时不会短暂恢复默认白天颜色。
摄像机背景色也会结合这份全局环境色计算,使地图边界、远景填充和场景主体保持同一色调。
十二、建筑夜景不是把全局颜色调暗
全局 _SpriteColor 负责整张地图的基础明暗,但夜间建筑还需要窗口灯光、辉光和粒子等局部效果。
所有支持夜景的对象会注册到全局灯光控制器。进入夜晚稳定阶段时,控制器统一发出“开启夜景”状态;返回白天时发出“关闭夜景”状态。
为了避免整座城市的灯在同一帧同时点亮,每个夜景对象会在一个很短的随机延迟后执行切换。
夜景对象主要有三类。
1. 遮罩材质切换
带遮罩的建筑为白天和夜晚准备不同的材质表现。进入夜晚后切换到能够显示窗口发光区域的夜景材质,返回白天后恢复普通建筑材质。
建筑遮罩纹理通过 MaterialPropertyBlock 写入 _Mask,这样不同建筑可以共享材质,又能使用自己的遮罩,不必为每个 SpriteRenderer 创建独立材质副本。
这里要特别注意:夜间材质加载必须绑定到建筑对象生命周期。否则建筑回池以后,夜间材质仍可能被额外加载句柄持有。
2. 辉光面控制
部分建筑拥有独立 Glow Plane。夜晚时启用 Renderer,白天关闭。
辉光面只在中高画质启用,低画质直接跳过。这是昼夜系统与画质分级的结合点:基础色调必须保留,附加辉光可以根据设备能力裁剪。
3. 夜间粒子控制
火光、灯笼等效果由独立粒子资源实现。
夜晚时按需实例化并播放,白天销毁或回收。对象离开 AOI 时也必须停止粒子并解除资源引用,不能让已经不可见的城市继续保留夜景粒子。
十三、昼夜为什么能够跨 LOD 保持连续
昼夜状态属于全局世界状态,不属于任何一块地图或某一座城市实例。
当 LOD 切换后:
- 新地形 Shader 自动读取全局
_SpriteColor; - 新创建的夜景对象在激活时注册到灯光控制器;
- 如果此时已经是夜晚,对象立即进入夜景状态;
- 回收的对象停止局部粒子和辉光;
- 昼夜状态继续推进,不会因为资源切换重新开始循环。
因此,地形可以从 LOD0 切换到 LOD5,城市可以从完整模型切换到战略代理或 HUD,但世界仍然处于同一个时间点。
十四、总结
这套无极缩放并不是一条简单的 LODGroup 链,而是四层结构共同工作:
- DXF 将摄像机距离和 FOV 统一为观察尺度;
- 七个语义观察层决定每类游戏信息何时出现、何时退出;
- 40×40 逻辑地图、3×3 小型窗口、3×3 大型窗口和动态相机矩形共同控制 AOI;
- 近景模板、远景合并 Tile、对象代理、HUD 和资源池构成完整资源生命周期。
昼夜则独立于地图实例,通过状态机控制环境光、方向光和 Shader 全局环境色,再由夜景材质、辉光面和粒子补充建筑局部效果。
无极缩放真正的设计重点不是“准备多少个低模”,而是决定每一层还需要表达什么,并让地图粒度、对象语义、AOI 范围和资源生命周期同时服从这个目标。
评论