RTS Terrain Studio
在 Unity 编辑器和你的游戏中构建经典风格的 RTS 地图:高地、棱角分明的悬崖、坡道、地面涂层、森林、水体和装饰物一应俱全。战争迷雾、寻路和地形查询均已内置。
概述
RTS Terrain Studio 是一款面向经典风格即时战略(RTS)游戏的地图编辑器,专为 Unity 6.3 LTS 和 Universal Render Pipeline (URP) 17.3 打造。你可以用它塑造高地及其棱角分明的垂直崖壁、绘制地面、种植森林、注水形成湖泊和河流、放置装饰物、标记玩家出生点,并检查单位能在哪里行走、建筑能建在哪里。完成后,同一张地图即可直接用于游戏:战争迷雾、寻路和地形查询都已就绪,而且这些功能完全不需要碰撞体。
编辑器可以在两个地方使用,两处的工具和地图文件格式(.rtsmap)完全相同:
- 在 Unity 编辑器中:通过 和 Scene 视图使用。你在这里构建随游戏一起发布的地图。
- 在游戏内:通过
RTSRuntimeEditor组件使用。它是一个完整的编辑器,界面基于 UI Toolkit。你的玩家和模组作者可以在你发布的游戏中用它制作、保存、加载和试玩地图。
本包不含任何第三方美术资源,但只需一条菜单命令,就能生成整套演示内容:纹理、低多边形树木和道具、两个图块集、三张地图以及三个可直接游玩的场景。导入包后不到一分钟,你就能开始探索了。
本手册面向所有人,内容涵盖从制作第一张地图到扩展源代码的方方面面。如果你是新手,请先按照下方上手五分钟一节操作,然后按顺序阅读各章。如果你是程序员,源代码有完整的注释,每个公共类型都有 XML 文档,文件夹结构一节则说明了各部分所在的位置。
地图的构成
如果你用过经典 RTS 游戏的地图编辑器,会对其中大部分概念感到熟悉。地图是由正方形单元格组成的栅格(默认每格宽 2 m)。单元格的角点就是顶点,大部分数据按顶点存储,或者存储在覆盖整张地图的高分辨率纹理中。
| 概念 | 说明 |
|---|---|
| 悬崖层级 | 每个顶点都有一个 0 到 15 之间的整数高度层级。相邻顶点处于不同层级时,会形成一道垂直崖壁。默认层级高度为 2 m,新地图的初始层级为 2,因此你可以向下挖出湖泊和河流。 |
| 精细高度 | 叠加在悬崖层级之上的平滑高度偏移,用于制作丘陵和山谷。顶点的地面高度为 CliffLevel × CliffLevelHeight + Height。 |
| 坡道 | 角点带有坡道标记的单元格会从一个层级平滑过渡到下一个层级,让单位可以走上去。 |
| 悬崖类型 | 崖壁的材质(例如岩石悬崖或长草的悬崖)。每个图块集最多包含 4 种。 |
| 地面图层 | 最多 16 种可绘制的地面纹理之一(草地、泥土、岩石……),存储在 Splat 贴图中。 |
| 水体 | 每个顶点的水面高度。水深决定该处是浅水(可行走)还是深水(不可行走)。 |
| 细节与树木 | 绘制的草和花(按纹素存储密度),以及逐棵放置的树木。 |
| 装饰物 | 你放置在地图上的任意预制件:岩石、废墟、木箱、栅栏、建筑。 |
| 通行 | 每个通行单元格的状态:可行走、可建造、可飞行、浅水或深水、悬崖、被阻挡或陡峭。这些数据根据地图自动推导,你也可以在其上绘制通行覆盖。 |
| 标记点 | 命名区域(矩形,可带有供游戏逻辑使用的键值属性)、玩家出生点,以及摄像机位置等命名点。 |
| 图块集 | 定义外观的 RTSTileset 资源:地面图层、悬崖类型和水体样式,类似经典 RTS 地图编辑器中的“Summer”或“Winter”图块集。 |
| 库 | 一个 RTSVegetationLibrary(细节和树木)和一个 RTSDecorLibrary(装饰物预制件)。它们就是你绘制和放置时所用的选择面板。 |
在场景中,地图位于一个带有 RTSTerrain 组件的 GameObject 上。菜单 还会为它添加各个子系统组件(RTSVegetationRenderer、RTSWaterRenderer 和 RTSDecorManager)。地形从项目中的 .rtsmap 文件(即 RTSMapAsset)读取地图。它显示的区块网格、草和水体都由该文件生成,并且永远不会保存到场景中,因此你的场景始终保持精简。
上手五分钟
这是亲眼看到所有功能运行起来的最快方式。除了导入包之外,无需任何配置。
- 打开 ,然后单击 Fix All。这会配置 URP、战争迷雾渲染器功能、水体所用的摄像机纹理以及色彩空间。在实际修改之前,确认对话框会先说明具体的改动。
- 运行 。生成纹理、模型、图块集、地图和场景期间会显示进度条。完成后,Project 窗口中会选中 RTS Terrain Studio - Gameplay Demo 场景。
- 双击该场景将其打开,然后单击 Play。
- 用鼠标左键选择你的单位(单击或拖出选框),然后右键单击地面让它们移动。用 WASD 或方向键平移视角,用鼠标滚轮缩放,按 F1 查看所有操作方式。
- 退出 Play 模式并打开 RTS Terrain Studio - Editor Playground 场景。然后打开 ,选择一个工具,在 Scene 视图中绘制。按住 Shift 可执行相反的操作(例如降低而不是升高)。按 Ctrl+Z 撤销。
- 打开 RTS Terrain Studio - In-Game Editor Demo 场景并单击 Play。这就是你的玩家将会用到的编辑器。按 F5 带单位试玩地图,按 F2 显示或隐藏编辑器。
窗口在每个项目中只会自动打开一次,并以按钮形式提供相同的步骤。你可以随时从菜单重新打开它。
从哪里开始
| 我想…… | 从这里开始 |
|---|---|
| 正确配置项目 | 项目配置和 |
| 一步步制作我的第一张地图 | 快速入门和 |
| 了解编辑器窗口和每个工具 | 编辑器窗口和 Scene 视图 |
| 让玩家在我的游戏中制作地图 | 游戏内编辑器 |
| 将我的游戏或地图编辑器发布到网页浏览器中运行 | Web(WebGL)构建 |
| 在单位探索之前隐藏地图 | 战争迷雾 |
| 使用 Mirror、A* Pathfinding Project 或 Unity NavMesh | 集成 |
| 查询地形,或用 C# 编写自己的工具 | 脚本编写 |
| 修复显示或运行不正常的问题 | 故障排除 |
功能特性
下面列出的每个编辑工具在 Unity 编辑器和游戏内编辑器中都可用,因为两者使用相同的工具类(RTSTool 的子类)。列表中的名称就是你在用户界面中看到的名称。

地形与悬崖
- 地图每边 16 到 1024 个单元格,以 16 为步长。New Map Wizard 提供 Tiny (64)、Small (96)、Medium (128)、Large (192)、Huge (256)、Epic (384) 和 Epic XL (512) 这些预设,也可以使用任意自定义尺寸。单元格尺寸和悬崖层级高度均可自由设置。
- 16 个悬崖层级(0–15),配有程序化生成的圆润崖壁。每个悬崖单元格内部的子栅格上会描出一条轮廓线,噪声和倾斜则让崖壁看起来像天然岩石(这些都在
RTSTerrainSettings中设置)。 - 层级之上还有精细高度,因此高地上也可以有丘陵。平滑地面的每个单元格每边可使用 1 到 4 个四边形:1 呈现经典 RTS 外观,2 到 4 则让丘陵更加平滑。
- Height 工具:Raise / Lower、Smooth、Flatten、Set Height、Noise、Terrace、Erode。
- Cliffs 工具:Raise / Lower Level、Set Level、Ramp、Cliff Type、Clean Cliffs。
- 每个图块集最多 4 种悬崖类型,按顶点绘制,崖壁使用三平面纹理映射。
- 可选为每个区块添加
MeshCollider,并放在你指定的层上。即使没有它,不依赖物理系统的查询也照样可用:GetHeight、GetNormal、Raycast、GetCliffLevel、IsWater、GetPathing、IsWalkable和IsBuildable。 - 一个场景中可包含任意数量的地形,每个地形都有自己的位置和 Y 轴旋转。
RTSTerrain.FindAt(worldPosition)和RTSTerrain.RaycastAll(ray, out hit)可以找到对应的地形。 - 可借助锚点向任一方向调整地图尺寸(扩大或缩小)。支持以 16-bit PNG 或 16-bit RAW 格式导入和导出高度图,并可将 Splat 贴图导出为 PNG。
绘制
- 每个图块集有 16 个地面图层,绘制结果存入四张 RGBA Splat 贴图。默认每个单元格有 4 个 Splat 纹素(可选 1 到 8),因此绘制出的边界比单元格栅格精细得多。
- 绘制工具:Paint Layer(软笔刷)、Tile Paint(经典 RTS 风格的硬边绘制,按单元格对齐)、Fill / Replace、Smooth Paint 和 Auto Paint。
- Auto Paint 按照
RTSAutoPaintProfile资源中存储的规则进行绘制。规则可以检测高度、坡度、悬崖层级、与悬崖的距离、与水体的距离以及噪声。你可以把 Auto Paint 工具当作笔刷使用,也可以让地图生成器将配置文件应用到整张地图。 - 图层之间基于高度的混合、逐图层的反平铺、作用于整个图块集的宏观变化,以及逐图层的色调、平滑度、金属度和法线强度。
- 图层可以在其占主导的区域改变通行数据。例如,演示图块集让 Rock 和 Blight 不可建造,就像经典 RTS 游戏中不可建造的地块一样。
- 笔刷:Circle、Square、Diamond,或使用任意纹理作为图章的 Custom(
RTSBrushShape资源)。你可以设置半径、强度、硬度、可选的衰减曲线、旋转、随机旋转、间距和抖动。 - 对称绘制,用于制作公平的竞技地图:Mirror X、Mirror Z、Mirror XZ、Rotate 2(180° 中心对称)、Rotate 4(90°,仅限正方形地图)和 Mirror Diagonal(仅限正方形地图)。对称适用于所有工具,包括对象放置。
植被
- 草、花和小型植物等细节:按图层绘制密度,由 GPU 以网格或交叉面片的形式渲染,并带有风力摆动。
- 树木可以逐棵放置,也可以用笔刷散布。每种树木既可以通过 GPU 实例化绘制(可选碰撞体代理),也可以生成为 GameObject,以支持伐木等游戏玩法。
- 树木会通过其占地范围自动阻挡通行。
- 植被工具:Paint Details、Erase Details、Place Trees、Erase Trees。
- 着色器
RTS Terrain Studio/Vegetation提供风力摆动、透光效果以及逐实例的色调。任何其他启用了 GPU 实例化的材质也都可以使用。 RTSVegetationRenderer上的质量设置:细节密度倍数、细节距离倍数和树木距离倍数。
水体
- 每个顶点都有水面高度,因此湖泊可以位于不同高度。水体以分区块的透明网格绘制,这些网格由 Burst 作业构建。
- 浅水和深水由水深决定(
WaterProfile.shallowDepthLimit)。与经典 RTS 游戏一样,浅水可行走,深水不可行走。 - 着色器
RTS Terrain Studio/Water支持随水深变化的颜色、滚动法线、波浪、折射、岸边泡沫以及柔和的岸边渐隐。样式来自图块集的WaterProfile。 - 水体工具:Paint Water、Erase Water、Flood Fill。
装饰物(doodad)
- 任何预制件都可以作为装饰物。原型存放在
RTSDecorLibrary中,并带有自动生成的缩略图。 - 每个原型可以生成为 GameObject(适用于需要与游戏玩法交互的对象),也可以通过 GPU 实例化绘制(适用于大量静态道具)。
- 随机旋转、缩放和色调,贴合坡面,对齐栅格,以及通行占地范围(圆形或方形)。
- 装饰物工具:Place Decor(单个放置或散布)、Erase Decor、Select Decor(移动、旋转、缩放、复制、删除)。
通行与寻路
- 通行图自动推导,并在你编辑时实时保持最新。它会综合考虑悬崖、坡度、水深、树木和装饰物的占地范围以及地面图层规则。
- 通行工具:Paint Pathing(绘制不可行走、不可建造或强制可行走等覆盖)和 Clear Pathing。两个编辑器中的通行叠加层会显示结果。
- 内置寻路器(
RTSPathfinder):在通行栅格上运行、经过 Burst 编译的 A* 算法,支持 8 邻域、禁止穿角、考虑单位半径(净空)以及路径平滑。它不需要任何第三方包。 - 通过
RTSPathfindingService为大量单位发起异步寻路请求。
战争迷雾
- 经典 RTS 规则:每个队伍拥有各自的视野,已探索区域会被记住。默认情况下,单位只能看到自身所在的悬崖层级及更低的层级。树木和装饰物也可以遮挡视线(
LineOfSightMode以及原型的blocksVision选项)。 - 最多 32 个队伍、盟友之间的共享视野(
RTSFogOfWarManager.SetAlliance)、平滑模糊的迷雾纹理,并支持小地图。 - URP 渲染器功能(
RTSFogOfWarRendererFeature)会让屏幕上的所有物体变暗,无论其使用何种材质。缺少该功能时,包自带的着色器会自行应用迷雾。 - 带有
RTSFogOfWarRevealer的单位会揭示地图,带有RTSFogOfWarHideable的对象在不可见时会被隐藏。建筑和道具一旦被发现,就可以作为“残影”保持可见。 - 可见性由 Burst 作业计算。
游戏内编辑器
- 通过 添加。它是一个基于 UI Toolkit 的界面,包含菜单栏、类别栏和工具栏、带选择面板和缩略图的工具面板、状态栏、工具提示和通知。
- 支持 New、Open、Save 和 Save As,并配有地图浏览器。默认情况下,地图存储在
Application.persistentDataPath下;在 Web 构建中则保存在浏览器的存储空间中,你可以通过 Download 和 Import 在浏览器与电脑之间转移地图。你还可以生成地图、编辑地图信息以及调整地图尺寸。 - 带内存预算的撤销和重做、键盘快捷键、对称,以及栅格、通行和标记点叠加层。
- 试玩开关(F5):在你正在编辑的地图上启动示例游戏玩法,结束后返回编辑器。
- 拥有自己的编辑器摄像机(
RTSRuntimeEditorCamera)和 Gizmo(辅助图标),并且不依赖UnityEditorAPI,因此可以在 Player 构建中运行。
地图生成器
- 基于种子的程序化生成器(
RTSMapGenerator),提供以下风格预设:Plains、Hills、Highlands、Islands、Canyon 和 Arena。 - 它可以生成高地和悬崖、坡道(带连通性检查)、湖泊和海洋、地图边界、细节、森林、装饰物和出生点,并执行自动绘制,支持任意对称模式。
- 相同的种子总是生成相同的地图。你可以将设置保存为
RTSMapGeneratorPreset资源。 - 你可以从 New Map Wizard、编辑器窗口、游戏内编辑器或你自己的代码中运行它。
示例游戏玩法
- RTS 摄像机(平移、边缘滚屏、缩放、旋转、地图边界限制)、单位选择(单击、框选、Shift、双击、编队)以及命令(按阵型移动、命令排队、停止、原地待命、巡逻)。
- 占位单位会贴合地形高度、驱散战争迷雾,并可使用任意寻路提供器。
- 可单击移动摄像机的小地图、HUD、遵循可建造标记的建筑放置演示,以及伐木演示。
RTSDemoGame可以在任何带有出生点的地形上自动配置以上全部内容。通过 添加。
集成
这些集成都是可选的。每个集成只在其依赖项已安装时才会编译,因此移除依赖项绝不会破坏你的项目。
| 集成 | 提供的功能 |
|---|---|
| Mirror(多人联机) | 主机会将地图流式传输给加入的客户端,因此玩家可以在线游玩用户制作的地图,下载的地图会被缓存。单位由服务器权威控制,每个客户端都有各自队伍的战争迷雾,并且 RTSFogOfWarInterestManagement 绝不会发送被迷雾隐藏的敌人(防全图挂)。 |
| A* Pathfinding Project | 根据 RTS 通行图生成栅格图(Grid Graph),并在每次编辑后增量更新;每种单位尺寸对应一张图;单位由 AIPath 驱动移动,并使用 RVO 局部避让。 |
| Unity AI Navigation (NavMesh) | 根据地形区块和通行图构建 NavMesh,编辑后增量更新;另有一个寻路提供器,可返回 NavMesh 路径或驱动 NavMeshAgent。 |
编辑器工作流程
- 一个可停靠的 RTS Terrain Studio 窗口,包含 8 个工具类别的选项卡,以及 Map 和 Settings 选项卡。窗口中有带图标的大号工具按钮、随上下文变化的设置、带缩略图的选择面板和状态栏。
- 与 Scene 视图集成。笔刷由地形着色器绘制并投影到地形上。此外还有栅格和通行叠加层、对称辅助线以及快捷键:[ / ] 调整笔刷大小,- / = 调整强度,1–8 切换工具类别,G 切换栅格,P 切换通行叠加层,R 切换随机旋转。你可以在 (macOS 上为 )中重新绑定这些快捷键。
- 地图编辑会纳入 Unity 的撤销历史,因此 Ctrl+Z 和 Ctrl+Y 照常可用,并会显示各步骤的名称。撤销内存预算可以配置。
- 为图块集(拖放图层、预览、自动构建纹理数组)、植被库和装饰物库、笔刷形状、自动绘制配置文件以及
RTSTerrain组件提供自定义 Inspector。.rtsmap导入器会显示小地图预览。 - Welcome 窗口、New Map Wizard,以及支持一键修复的项目验证器。
性能
- 地形网格划分为 16、32(默认)或 64 个单元格大小的区块。区块在 Burst 作业中通过
Mesh.MeshDataArray构建,并且只会重建你编辑过的区块。 - 繁重的工作都在经过 Burst 编译的作业(Unity 作业系统)中运行:笔刷操作、网格生成、通行推导、净空计算、A*、战争迷雾、植被实例、水体网格、地图生成器、自动绘制和小地图烘焙。
- 细节通过
Graphics.RenderMeshPrimitives以程序化方式绘制,树木和实例化装饰物使用Graphics.RenderMeshInstanced。它们都会按分块进行视锥体剔除和距离剔除。 - 着色器兼容 SRP Batcher,并且所有区块共享同一个地形材质。
- 纹理数组会针对当前平台进行压缩(BC7、ASTC 6×6、ETC2 或 DXT5),分辨率可自选(256–2048)。
- 热路径中没有逐帧垃圾分配,组件禁用时会释放所有原生内存。
设计决策一览
下面是大家对 RTS 地形工具最常提出的问题,以及 RTS Terrain Studio 给出的答案。
| 问题 | 解答 |
|---|---|
| 一张地图可以使用多少种地面纹理? | 每个图块集最多支持 16 个地面图层和 4 种悬崖类型,图块集的数量则不受限制。这个上限有技术上的原因:16 个图层正好可以放进 4 张 RGBA Splat 贴图(每个图层占一个通道),而全部 16 个地面图层和 4 个悬崖图层都打包在两个纹理数组中(反照率/高度和法线,各 20 个切片),因此只需一个地形材质就能渲染所有区块。 |
| 地形有碰撞体吗?能否不借助物理系统查询地形? | 两者都可以。每个区块都可以在你选择的层上生成一个 MeshCollider(RTSTerrainSettings.colliderMode)。查询 API(GetHeight、GetNormal、Raycast、GetCliffLevel、IsWater、TryGetWaterHeight、GetPathing、IsWalkable、IsBuildable)完全不使用物理系统。它在编辑模式下、运行时以及关闭碰撞体时都能正常工作,返回的正是你所看到的表面。 |
| 能否用多张地图拼成大型世界? | 可以。一个场景中可以放置任意数量的 RTSTerrain 组件,每个组件都有自己的位置和 Y 轴旋转。RTSTerrain.All、RTSTerrain.FindAt 和 RTSTerrain.RaycastAll 都可以跨所有这些组件使用。得益于按区块渲染,512×512 个单元格的地图也切实可行。 |
| 即使开启了显示缩放,笔刷也会准确落在光标所在的位置吗? | 是的。Unity 编辑器使用 HandleUtility.GUIPointToWorldRay,它能正确处理 DPI 缩放;游戏内编辑器则使用 Camera.ScreenPointToRay,并配合 Input System 提供的指针位置。笔刷光标由地形着色器绘制并投影到地表上,因此你看到的位置就是实际绘制的位置。 |
| 可以使用哪些纹理格式和尺寸? | 任意尺寸和格式的普通 Unity 纹理都可以。图块集的 Inspector 窗口会自动调整它们的尺寸,并打包成 Texture2DArray(每个切片 256 到 2048 像素,可选择压缩或不压缩)。通道布局有明确的文档说明:反照率存放在 RGB 通道,可选的高度存放在 Alpha 通道或单独的高度贴图中,另外还有一张法线贴图。 |
| 必须把包放在固定的文件夹中吗? | 不必。编辑器通过包的运行时程序集定义找到包的根目录,因此你可以把 RTSTerrainStudio 移动到 Assets 下的任意位置(例如 Assets/Plugins/RTSTerrainStudio)。 |
| 之后还能更改地图尺寸吗? | 可以。在 New Map Wizard 中可以选择任意尺寸,之后还能用 Resize 扩大或缩小地图。调整时你需要选择一个锚点,它决定在哪几侧添加或移除单元格。 |
| 我能阅读和修改代码吗? | 可以。代码按程序集定义拆分,每个程序集对应一个命名空间。公共 API 带有 XML 文档注释,内部实现也有注释,Burst 作业放在单独的 *Jobs.cs 文件中,此外还有 EditMode 单元测试。 |
初识 API
为了让程序员对 API 有个初步印象,下面给出一个完整的脚本。它会向地形查询你单击位置的信息,并用内置寻路器规划一条通往该位置的路径。它既不需要碰撞体,也不需要额外的组件:只需在包含 RTSTerrain 的场景中,把它挂到任意 GameObject 上即可。
using System.Collections.Generic;
using RTSTerrainStudio;
using UnityEngine;
/// <summary>Click on the ground: logs what the terrain knows about that point and plans a path to it.</summary>
public class ClickInfoExample : MonoBehaviour
{
// Reused for every click, so no new list is allocated each time.
readonly List<Vector3> path = new List<Vector3>();
void Update()
{
// RTSInput works with the Input System package and with the old Input Manager.
if (!RTSInput.GetMouseButtonDown(0) || Camera.main == null) return;
Ray ray = Camera.main.ScreenPointToRay(RTSInput.PointerPosition);
// Analytic ray cast against every RTSTerrain in the scene. Colliders are not needed.
if (!RTSTerrain.RaycastAll(ray, out RTSTerrainHit hit)) return;
RTSTerrain terrain = hit.terrain;
// Gameplay questions, answered from the map data and the pathing map.
int level = terrain.GetCliffLevel(hit.point);
bool buildable = terrain.IsBuildable(hit.point);
bool water = terrain.IsWater(hit.point, out float depth);
Debug.Log($"Cell {hit.cell}: cliff level {level}, buildable {buildable}, water {water} ({depth:0.00} m)");
// Built-in Burst A* on the pathing map, for a unit with a radius of 0.6 m.
PathStatus status = RTSPathfinder.FindPath(terrain, transform.position, hit.point,
PathRequestOptions.ForUnit(0.6f), path);
Debug.Log($"Path from {name}: {status}, {path.Count} waypoints");
}
}
PathStatus 的取值为 Success、Partial(最近的可到达点)、NoPath 或 InvalidStart。完整的 API 说明参见“脚本编写”一章。
环境要求
Unity 与渲染管线
| 要求 | 版本 | 备注 |
|---|---|---|
| Unity | 6.3 LTS(6000.3)或更高版本 | 使用 6000.3.3f1 开发和测试。 |
| 通用渲染管线(URP) | 17.3(Unity 6.3 自带的版本) | 必须在 Graphics 和 Quality 设置中启用一个 URP 资源。开发时使用的是 Forward+ 渲染路径。地形着色器还为 Deferred 路径提供了 GBuffer Pass。 |
| 色彩空间 | Linear(线性) | Gamma 也能用,但颜色、光照和水体会显得发白、褪色。 |
| 图形 | Shader Model 3.5 或更高 | 地形、植被、细节和水体着色器的目标级别为 #pragma target 3.5,因此也能在 WebGL 2.0 和 OpenGL ES 3.0 上运行。只有地形的 GBuffer Pass(Deferred 渲染路径)和细节着色器的结构化缓冲区路径会使用 4.5,且仅在 GPU 支持时使用。 |
不支持内置渲染管线(Built-in Render Pipeline)和 HDRP。所有着色器都是为 URP 编写的,战争迷雾渲染器功能使用的是 URP 的 RenderGraph API。
导入到了没有 URP 的项目中?这时本包会保持停用状态,而不会让 Console 窗口里满是错误:它的脚本不参与编译,着色器也会被跳过,你的项目照常工作。在每个编辑器会话中,RTS Terrain Studio requires URP 对话框都会出现一次,告诉你该怎么做:
- 内置渲染管线:单击 Install URP。Unity 编译完本包后,打开 ,然后单击 Use URP。它会创建一个 URP 资源,并在 Graphics 和 Quality 设置中指定该资源。
- URP 版本低于 17.3:单击 Open Package Manager,然后把 Universal Render Pipeline 更新到你的编辑器自带的版本。
- Unity 版本低于 6.3:URP 17.3 需要 Unity 6.3 LTS,因此对话框只提供 Open Manual 和 Later 两个按钮。用 Unity 6.3 LTS 或更高版本打开项目即可。
- HDRP 或自定义渲染管线:改为把本包导入到 URP 项目中,例如用 Universal 3D 模板新建的项目。
可以再次打开该对话框。Unity 还可能输出警告,提示本包的着色器不受支持;安装 URP 后,这些警告就会消失。
必需的包
这些包来自 Unity Registry()。 窗口会检测缺失的包,并可一键安装(对应检查项 Burst, Collections and Mathematics)。
| 包 | 名称 | 已测试版本 | 用途 |
|---|---|---|---|
| Burst | com.unity.burst | 1.8.27 | 将作业编译为高效的原生代码。 |
| Collections | com.unity.collections | 2.6.2 | 为地图数据和作业提供原生容器。 |
| Mathematics | com.unity.mathematics | 1.3.3 | 作业和数据模型中的向量数学运算。 |
| Universal RP | com.unity.render-pipelines.universal | 17.3.0 | 渲染。 |
输入处理
所有运行时输入都通过静态类 RTSInput 处理。它支持两种输入后端:
- Input System 包(
com.unity.inputsystem,已使用 1.17.0 测试)。如果已安装该包,且 Active Input Handling 设为 Input System Package (new) 或 Both,就会使用这一后端。 - Input Manager (old)。当项目使用旧版输入处理时,它作为后备方案使用。
该选项在 中设置。项目验证器的 Input handling 检查项会显示当前启用的模式;参见“项目配置”一章中的“输入处理”。
集成所需的可选包
| 集成 | 依赖项 | 版本 | 检测方式 |
|---|---|---|---|
| Mirror | Mirror(Asset Store 或 GitHub) | 96 或更高版本 | 脚本定义符号 MIRROR,由 Mirror 自行添加。 |
| A* Pathfinding Project | com.arongranberg.astar | Pro 5.4.4 至 6.0(不含 6.0),已使用 5.4.7 测试 | 版本定义符号 RTS_ASTAR。添加 RTS_ASTAR_DISABLED 可将其关闭(使用免费版时必须这样做,因为免费版使用相同的包名)。 |
| Unity NavMesh | AI Navigation,com.unity.ai.navigation | 2.0 或更高版本,已使用 2.0.9 测试 | 版本定义符号 RTS_AI_NAVIGATION。 |
寻路集成需要 A* Pathfinding Project 的 Pro 版本,因为该集成使用了 RVO 局部避让和 TraversalConstraint。删除 Mirror 之前,请先阅读“移除 Mirror”:Mirror 会遗留它的定义符号,而 RTS Terrain Studio 自带一个小型编辑器程序集,可以清理这些符号。
推荐的项目设置
项目验证器会检查以下所有设置项,并可以帮你自动修复:
| 设置项 | 原因 |
|---|---|
| 已启用 URP 资源 | 本包的着色器只能在 URP 中渲染。 |
| URP 资源上已开启 Depth Texture 和 Opaque Texture | 水体通过深度纹理让水深颜色、泡沫和柔和的岸边贴合立在水中的物体,并通过不透明纹理实现折射。摄像机自身的设置会覆盖 URP 资源中的设置,水体会按每台摄像机各自的设置渲染。演示摄像机会自行开启这两项,因此即使没有此设置,演示场景在 Game 视图中也能正确显示(Scene 视图始终使用 URP 资源的设置)。 |
| 渲染器上已添加战争迷雾渲染器功能 | 使迷雾中的所有物体变暗,包括你自己的材质。没有它,只有本包的着色器会显示迷雾。 |
| Player 构建中已保留雾效模式 | Unity 会剥离没有任何构建场景使用的雾效着色器变体,但 RTSEnvironment 会在运行时开启雾效。如果不保留这些雾效模式,带雾效的地图在构建版本中就不会显示雾效。 |
| Linear 色彩空间 | 保证颜色和光照正确。 |
| 着色器引用和图块集纹理数组为最新状态 | 这样着色器和压缩后的纹理数组才会包含在 Player 构建中。每次 Player 构建前,系统都会自动刷新着色器引用资源,并重建过时的纹理数组。 |
平台
运行时没有任何仅限编辑器的依赖项,因此在 URP 支持的所有平台上,都可以在 Player 构建中加载、渲染和编辑地图,Web 构建(WebGL 2.0)也不例外。着色器的目标级别为 Shader Model 3.5,这是支持纹理数组的最低级别。地形用于 Deferred 渲染路径的 GBuffer Pass 需要 Shader Model 4.5,在基于 OpenGL 的图形 API 上会被跳过,这一点与 URP 自带 Lit 着色器的 GBuffer Pass 相同。草类细节会在运行时选择实例数据的存储方式:GPU 支持时使用结构化缓冲区,在 WebGL 2.0 和 OpenGL ES 3 上则使用浮点纹理(显示效果相同)。
纹理数组烘焙器会根据当前构建目标选择压缩格式:桌面平台和主机使用 BC7,Android、iOS、tvOS 和 visionOS 使用 ASTC 6×6,Web 使用构建设置中的纹理压缩格式(DXT5、ASTC 6×6 或 ETC2),其他平台使用 DXT5。游戏内编辑器可以在所有平台上运行,专为鼠标和键盘操作而设计。其中用于显示地图文件夹和退出应用程序的命令只会在平台支持时出现:只有桌面平台的 Player 和编辑器可以显示文件夹,而 WebGL、iOS 和 Android 无法退出应用程序。Web 构建还有其他一些差异,参见“Web(WebGL)构建”。
文件夹结构
整个包位于一个文件夹中,默认是 Assets/RTSTerrainStudio。你可以移动或重命名该文件夹:编辑器通过 RTSTerrainStudio.Runtime.asmdef 的位置找到包的根目录,演示内容和文档也以它为基准按相对路径查找。
RTSTerrainStudio/
├── Runtime/ Core, rendering, tools and runtime systems (RTSTerrainStudio.Runtime)
├── Editor/ Editor window, Scene View, inspectors, setup, demo content (RTSTerrainStudio.Editor)
├── Shaders/ URP shaders and HLSL includes
│ ├── Hidden/ Internal shaders (fog blur, minimap fog, gizmos, texture array blit)
│ └── Include/ Shared HLSL (fog of war, brush cursor, wind, terrain, vegetation, water)
├── Samples/
│ ├── Scripts/ Sample gameplay: camera, units, selection, orders, minimap, HUD (RTSTerrainStudio.Samples)
│ └── Scenes/ Demo scenes (created by Create Demo Content)
├── Integrations/
│ ├── Mirror/ Multiplayer (compiled only with Mirror)
│ ├── AStarPathfinding/ A* Pathfinding Project (compiled only with A* Pro 5.4.4+)
│ └── UnityNavMesh/ Unity AI Navigation (compiled only with com.unity.ai.navigation 2.0+)
├── Content/ Generated demo content (empty until you run Create Demo Content)
├── Resources/
│ └── RTSTerrainStudio/ Runtime UI style sheets and theme, shader references, panel settings
├── Documentation/ This manual (index.html) and its images
└── Tests/
└── Editor/ EditMode unit tests (RTSTerrainStudio.Tests.Editor)
顶层文件夹
| 文件夹 | 内容 | 能否删除? |
|---|---|---|
Runtime | 游戏中运行的所有内容:RTSTerrain 组件、地图数据、渲染、所有编辑工具、植被、水体、装饰物、战争迷雾、通行数据、地图生成、游戏内编辑器、图标和输入。 | 不能。 |
Editor | 仅包含 Unity 编辑器功能,这些内容都不会进入 Player 构建。 | 不能。 |
Shaders | RTS Terrain Studio/Terrain、RTS Terrain Studio/Vegetation、RTS Terrain Studio/Water、Hidden/RTS Terrain Studio/Detail、Hidden/RTS Terrain Studio/FogOfWarFullscreen、Hidden 中的内部着色器,以及 Include 中的 HLSL 包含文件(RTSCommon.hlsl 存放战争迷雾、笔刷光标和风的全局变量)。 | 不能。 |
Samples | 示例玩法脚本和演示场景。编辑器程序集和各个集成都引用了示例程序集(例如为了使用 RTSUnit 和 RTSDemoGame)。 | 只能与各个集成一起删除。场景可以随时删除。 |
Integrations | 每个第三方集成占用一个文件夹,各自包含自己的程序集、一个 README.md,以及一个存放演示场景构建器的 Editor 文件夹。演示场景会创建在 Demo 子文件夹中。 | 可以,每个集成文件夹都可以单独删除。删除 Mirror 前请先阅读相应步骤。 |
Content | 演示用的纹理、材质、网格、预制件、图块集、库、笔刷形状、地图和渲染设置。这里的所有内容都是自动生成的。 | 可以,你随时可以重新生成它。不过前提是你自己的地图没有用到其中的内容。 |
Resources/RTSTerrainStudio | UI/RTSRuntimeEditor.uss 和 UI/RTSRuntimeTheme.tss(游戏内 UI 样式)。着色器引用资源(由编辑器自动创建和刷新)以及 UI/RTSRuntimePanelSettings.asset(由 Create Demo Content 创建)也会添加到这里。它们确保本包的着色器和 UI Toolkit 运行时着色器会包含在构建中。 | 不能。 |
Documentation | 本手册。 和所有 ? 按钮都会打开它。 | 可以,但这样帮助按钮就无法打开任何内容了。 |
Tests | 针对地图数据、几何体、笔刷、对称、撤销、序列化器、通行数据、地图生成器、地形查询、工具注册表和图标的 EditMode 测试。可在 中运行。 | 可以。 |
Runtime 文件夹中的内容
| 文件夹 | 内容 |
|---|---|
Core | RTSTerrain(主组件、地图生命周期、变更追踪、查询)、MapData、MapTypes、MapGeometryView(网格、查询和通行共用的参考表面)、PathingMap、RTSTerrainSettings、着色器属性 ID、RTSShaderReferences、RTSRuntimeEditorEvents。 |
Data | 资源类型:RTSTileset、RTSVegetationLibrary、RTSDecorLibrary、RTSMapAsset。 |
Serialization | RTSMapSerializer:二进制 .rtsmap 格式(文件段、deflate 压缩)以及文件读写。 |
Brushes | BrushSettings、RTSBrushShape、SymmetrySettings、BrushUtility。 |
Tools | RTSTool(基类)、RTSEditSession、MapUndo,以及每个类别各一个子文件夹:Height、Cliffs、Paint、Vegetation、Decor、Water、Pathing、Markers。 |
Mesh | 区块几何体的生成及其 Burst 作业。 |
Rendering | RTSTerrain 的渲染部分、RTSTerrainChunk、Splat 纹理、图层数据和 RTSTextureArrayBuilder。 |
Vegetation | RTSVegetationRenderer、RTSTreeInstance。 |
Water | RTSWaterRenderer、RTSWaterChunk、水体网格作业。 |
Decor | RTSDecorManager、RTSDecorInstance。 |
FogOfWar | RTSFogOfWarManager、RTSFogOfWarRevealer、RTSFogOfWarHideable、RTSFogOfWarRendererFeature。 |
Pathing | RTSTerrain 的通行部分、RTSPathingSettings、PathingMapBuilder、ClearanceMap、RTSPathfinder、RTSPathfindingService、通行叠加层。 |
Generation | RTSMapGenerator、MapGeneratorSettings、RTSMapGeneratorPreset、RTSAutoPaintProfile、RTSAutoPaint、HeightmapIO,以及 Auto Paint 工具。 |
Minimap | RTSMinimapBaker。 |
Environment | RTSEnvironment(应用地图的太阳光、环境光和雾效)。 |
Validation | MapValidator(检查地图中的问题,例如无法到达的出生点)。 |
RuntimeEditor | 游戏内编辑器:RTSRuntimeEditor、RTSRuntimeEditorCamera、RuntimeToolGizmos 及其 UI Toolkit 界面。 |
Input | RTSInput 和 RTSKey。 |
UI | RTSIcons:编辑器和游戏内 UI 共用的程序化图标。 |
Plugins/WebGL | 仅供 Web 构建使用的 JavaScript 插件(.jslib):浏览器存储、下载、导入和离开页面时的提醒(RTSWebFileSystem.jslib);键盘布局和浏览器快捷键(RTSWebKeyboard.jslib);指针离开页面的处理(RTSPointer.jslib)。 |
link.xml | 在启用代码剥离的构建中保留编辑工具(参见“代码剥离与自定义工具”)。 |
Editor 文件夹中的内容
| 文件夹 | 内容 |
|---|---|
Core | RTSEditorContext(共享的编辑会话和视图选项)、RTSEditorPaths、工具设置的持久化存储。 |
Window | RTS Terrain Studio 窗口(RTSTerrainStudioWindow)及其各个页面。 |
SceneView | Scene 视图中的绘制操作、笔刷和 Gizmo(辅助图标)的显示、叠加层、快捷键,以及与 Unity 撤销系统的桥接。 |
Inspectors | 图块集、各类库、笔刷形状、自动绘制配置文件和组件的 Inspector 界面。 |
Importers | .rtsmap 导入器及其预览,以及 菜单项。 |
Building | 压缩纹理数组烘焙器、Player 构建前的检查(会重建过时的纹理数组)、缩略图生成器和资源工厂。 |
Setup | Welcome 窗口、New Map Wizard、项目验证器、GameObject 创建菜单、着色器引用更新器,以及 Web 模板(WebGLTemplate)及其安装器。 |
Requirements | 项目中没有 URP 17.3 时显示的提示(参见“Unity 与渲染管线”)。除了 Mirror 定义符号清理之外,它是本包中唯一在没有 URP 时也能编译的部分。 |
RuntimeEditorSupport | 菜单。 |
DemoContent | 程序化演示内容生成器(RTSDemoContent)。 |
程序集
代码拆分为多个程序集定义,因此编译速度快,可选部分也可以移除。每个程序集对应一个命名空间。
| 程序集(asmdef) | 文件夹 | 命名空间 | 平台 | 编译条件 |
|---|---|---|---|---|
RTSTerrainStudio.Runtime | Runtime | RTSTerrainStudio | 全部 | 已安装 URP 17.3 或更高版本时(RTS_URP)。安装了 Input System 包时还会定义 RTS_INPUT_SYSTEM。 |
RTSTerrainStudio.Samples | Samples/Scripts | RTSTerrainStudio.Samples | 全部 | 已安装 URP 17.3 或更高版本时。 |
RTSTerrainStudio.Editor | Editor | RTSTerrainStudio.Editor | 编辑器 | 已安装 URP 17.3 或更高版本时。 |
RTSTerrainStudio.Requirements.Editor | Editor/Requirements | RTSTerrainStudio.Requirements | 编辑器 | 仅在缺少 URP 17.3 或更高版本时。它会显示本包需要 URP 的提示。 |
RTSTerrainStudio.Tests.Editor | Tests/Editor | RTSTerrainStudio.Tests | 编辑器 | 定义了 UNITY_INCLUDE_TESTS 时(Unity Test Framework)。 |
RTSTerrainStudio.Integrations.MirrorRTSTerrainStudio.Integrations.Mirror.Editor | Integrations/Mirror | RTSTerrainStudio.Integrations.Mirror | 全部 / 编辑器 | 定义了 MIRROR 时。 |
RTSTerrainStudio.Integrations.Mirror.DefineCleanup.Editor | Integrations/Mirror/Editor/DefineCleanup | RTSTerrainStudio.Integrations.Mirror | 编辑器 | 始终编译。它不依赖 Mirror,会在 Mirror 被删除后移除残留的 Mirror 定义符号。 |
RTSTerrainStudio.Integrations.AStarRTSTerrainStudio.Integrations.AStar.Editor | Integrations/AStarPathfinding | RTSTerrainStudio.Integrations.AStar | 全部 / 编辑器 | 定义了 RTS_ASTAR(已安装 A* 5.4.4 至 6.0 以下版本)且未定义 RTS_ASTAR_DISABLED 时。 |
RTSTerrainStudio.Integrations.NavMeshRTSTerrainStudio.Integrations.NavMesh.Editor | Integrations/UnityNavMesh | RTSTerrainStudio.Integrations.NavMesh | 全部 / 编辑器 | 定义了 RTS_AI_NAVIGATION 时(已安装 AI Navigation 2.0 或更高版本)。 |
除了表中列出的条件外,测试程序集和各集成程序集还需要 URP 17.3 或更高版本。没有 URP 时,只有 Mirror 定义符号清理和环境要求提示会参与编译,因此本包不会引发编译错误。
RTSTerrainStudio.Runtime、RTSTerrainStudio.Samples 和 RTSTerrainStudio.Editor 都设置为自动引用。项目默认程序集(Assembly-CSharp)中的脚本无需任何配置即可使用它们。如果你的代码有自己的程序集定义,请添加对 RTSTerrainStudio.Runtime 的引用(如果用到了示例玩法类,还需引用 RTSTerrainStudio.Samples)。
游戏内编辑器位于 Runtime/RuntimeEditor。如果你的游戏不需要它,可以删除该文件夹:Unity 编辑器工具和演示内容生成器仍然可以正常编译,只是此时创建的 In-Game Editor Demo 场景中不会包含游戏内编辑器。
包含内容
本包以源代码、着色器和本手册的形式发布。所有视觉内容(纹理、模型、图块集、地图和场景)都由脚本在你的电脑上生成。这样可以让下载体积保持很小,而且这些内容的许可方式与代码完全相同。
演示内容生成器
会运行 RTSDemoContent.CreateAll。它使用固定种子以程序化方式生成所有演示资源,因此每次运行的结果都完全相同:
- 它只写入自己的文件夹:
Content、Samples/Scenes,以及Resources/RTSTerrainStudio中的两个资源。它绝不会改动你自己的资源。 - 再次运行时,它会原地重新生成这些资源,因此对它们的引用依然有效。内容没有变化的地图文件不会被重新写入,所以版本控制中也不会显示任何更改。
- 它绝不会关闭你已打开的场景。构建演示场景期间,它会以叠加方式打开这些场景。
- 最后,它会询问是否将演示场景添加到构建设置中。除非你单击 Add Scenes,否则不会添加任何内容。
| 生成项 | 位置(位于包根目录下) | 内容 |
|---|---|---|
| 纹理 | Content/Textures/(Ground、Cliffs、Water、Details、Props、Brushes) | 可平铺的地面、悬崖、水体和道具纹理(512 px),草和花的精灵图以及笔刷图章(256 px),均附带相应的法线贴图。 |
| 材质、网格、预制件 | Content/Materials、Content/Meshes、Content/Prefabs | 低多边形的树木、岩石、废墟和道具,以及它们的材质。 |
| 图块集 | Content/Tilesets/Summer.asset、Winter.asset | 两个图块集,各含 10 个地面图层和 2 种悬崖类型。Summer:Grass、Lush Grass、Dirt、Rocky Dirt、Sand、Cobblestone、Dark Forest Floor、Rock、Snow 和 Blight,悬崖类型为 Rock Cliff 和 Grassy Cliff。Winter:上述图层的冰冻版本,悬崖类型为 Ice Cliff 和 Snowy Cliff。 |
| 自动绘制配置文件 | Content/Tilesets/Summer Auto Paint.asset | 用于 Summer 图块集的规则,例如在平缓的斜坡上生成泥土斑块、沿岸边铺沙。 |
| 植被库 | Content/Libraries/Demo Vegetation.asset | 细节:Grass、Tall Grass、Flowers Yellow、Flowers Purple、Fern。树木:Pine、Broadleaf、Dead Tree、Bush。 |
| 装饰物库 | Content/Libraries/Demo Decor.asset | Rock A、Rock B、Rock C、Boulder、Ruined Pillar、Crate、Barrel、Fence、Tree Stump、Mushrooms。其中既有实例化渲染的原型,也有 GameObject 原型,并且都设置了通行占地范围。 |
| 笔刷形状 | Content/Brushes | 自定义图章:Noise Blob、Splatter、Star、Soft Square。 |
| 地图 | Content/Maps | 三个 .rtsmap 文件(见下文)。 |
| 渲染设置 | Content/Settings | 演示场景使用的后处理体积配置文件。 |
| 场景 | Samples/Scenes | 三个演示场景(见下文)。 |
| Resources 文件夹 | Resources/RTSTerrainStudio | 着色器引用资源和 UI/RTSRuntimePanelSettings.asset,确保着色器和 UI 会包含在构建版本中。 |
演示地图
三张地图均使用 2 m 的单元格尺寸、2 m 的悬崖层级高度和 Summer 图块集。
| 地图 | 尺寸 | 对称 | 说明 |
|---|---|---|---|
Two Rivers (1v1).rtsmap | 128 × 128 个单元格 | Rotate 2 | 两名玩家,两条河流。河上处处都有可供穿越的浅滩,中央的湖泊则是深水。坡道通往林木茂密的高地,高地上有废墟和废弃的营地。 |
Four Kingdoms (4p).rtsmap | 192 × 192 个单元格 | Rotate 4 | 四个王国,各自占据一片高地。坡道向下通往共享的低地,地图中央是一片腐化之地,其中有一座古老神庙的废墟。 |
Sandbox.rtsmap | 96 × 96 个单元格 | None | 一张小型地图,地势大多平坦,适合试用各种工具。 |
演示场景
| 场景 | 地图 | 显示内容 |
|---|---|---|
| RTS Terrain Studio - Gameplay Demo | Two Rivers (1v1) | 单击 Play 即可直接游玩:出生点上的单位(敌方单位会四处游荡)、战争迷雾、RTS 摄像机、单位选择、命令下达、小地图、HUD、建筑放置(B)和伐木(T)。 |
| RTS Terrain Studio - In-Game Editor Demo | Sandbox | Play 模式下的游戏内编辑器。编辑地图后按 F5,即可带着单位试玩该地图。 |
| RTS Terrain Studio - Editor Playground | Four Kingdoms (4p) | 一张不含游戏玩法的地图,用于在 Scene 视图中试用 Unity 编辑器工具。 |
每个演示场景都包含一个太阳光源、一个后处理体积、一个请求深度纹理和不透明纹理的摄像机,以及一个带有 RTSTerrain、RTSVegetationRenderer、RTSWaterRenderer、RTSDecorManager 和 RTSEnvironment 的地形。
每个集成都有自己的演示场景构建器。它需要先有演示内容,并会在该集成的 Demo 文件夹中创建场景:
- 会创建
RTS Mirror Demo.unity。 - 会创建
RTS AStar Demo.unity。 - 会创建
RTS NavMesh Demo.unity。
示例脚本
Samples/Scripts 文件夹(RTSTerrainStudio.Samples)是一个小巧易读的 RTS 框架,你可以从中学习,也可以在它的基础上继续开发:
| 功能模块 | 类 |
|---|---|
| 启动引导 | RTSDemoGame、RTSUnitSpawner |
| 单位与移动 | RTSUnit、RTSUnitFactory、RTSUnitRegistry、RTSFormation、RTSWanderAI |
| 寻路提供器 | IRTSUnitPathProvider、RTSBuiltinPathProvider、RTSUnitPathProviderComponent、RTSUnitPathProviderRegistry |
| 摄像机、输入、选择、命令 | RTSCameraController、RTSGameInput、RTSSelectionManager、RTSCommandController |
| UI | RTSMinimapUI、RTSDemoHUD、RTSRuntimeUI |
| 游戏玩法演示 | RTSBuildingPlacementDemo、RTSDemoBuilding、RTSTreeHarvestDemo、RTSFallingTreeEffect |
| 辅助类 | RTSMoveMarker、RTSGroundRing、RTSSampleMaterials、RTSSampleMeshes、RTSSampleUtility |
图标
编辑器窗口、Inspector 窗口和游戏内编辑器中的所有图标都由 RTSIcons 以程序化方式绘制,因此在任何尺寸下都保持清晰,在构建版本中也无需纹理资源即可使用。
快速入门
本章将带你从一个空的 Unity 项目起步,亲手制作一张地图,并在有单位、战争迷雾和小地图的游戏中试玩它。你不需要任何编程知识。按顺序完成带编号的步骤即可,每个步骤都会写明要使用的具体菜单、窗口或按钮。
| 部分 | 你要做的事 | 用时 |
|---|---|---|
| 安装与配置 | 导入包,让项目验证器修复 URP 设置,然后生成演示内容。 | 约 5 分钟 |
| 演示场景 | 游玩一张现成的地图,试用游戏内编辑器,在 Editor Playground 场景中自由探索。 | 5 分钟 |
| 你的第一张地图 | 创建地形,绘制高度、悬崖、纹理、树木和水体,放置出生点并保存。 | 10 分钟 |
| 你的第一次试玩 | 用演示游戏试玩你的地图,或者在运行中的游戏里编辑并试玩。 | 5 分钟 |
| 10 分钟教程 | 利用对称绘制,从头到尾制作一张公平的 1v1 地图。 | 10 分钟 |
你随时可以通过 Welcome 窗口()中的 Quick Start Guide 链接打开本章。通过 可以打开完整手册。
安装与配置
环境要求
| 条目 | 版本 | 备注 |
|---|---|---|
| Unity | 6.3 LTS (6000.3) | 使用本包适配的编辑器版本。 |
| 通用渲染管线(URP) | 17.3 | 必需。所有地形、水体、植被和战争迷雾着色器都是 URP 着色器。新建项目时,请从 Universal 3D 项目模板开始。没有 URP 时,本包会保持未激活状态,并显示一条提示(参见环境要求)。 |
| Burst、Collections、Mathematics | 来自 Unity Registry | 必需。繁重的计算(网格构建、笔刷、通行数据、战争迷雾、寻路)都在 Burst 作业中运行。 |
| Input System | 来自 Unity Registry | 推荐。游戏内编辑器、RTS 摄像机和示例都会使用它。旧版 Input Manager 可作为后备方案。 |
| Mirror、A* Pathfinding Project、AI Navigation | 可选 | 仅在使用对应的集成时需要。安装相应的包后,集成会自动编译。参见集成。 |
导入包
- 在 Unity 6.3 中打开你的项目。确保项目使用 URP(基于 Universal 3D 模板新建的项目已默认使用)。
- 打开 ,选择 My Assets,找到 RTS Terrain Studio,单击 Download,然后单击 Import。
- 在导入对话框中保持全部勾选,然后单击 Import。包会导入到
Assets/RTSTerrainStudio中。 - 等待 Unity 完成编译。Welcome 窗口会自动打开(见下文)。
你可以把 RTSTerrainStudio 文件夹移动到 Assets 中的任意位置。无论该文件夹位于何处,包都能找到自己的文件,因此重新整理项目结构时不会出现任何问题。
如果导入后 Console 窗口立即显示编译错误,最可能的原因是缺少 Burst、Collections 或 Mathematics 包。请通过 安装它们。安装完成后,这些错误就会消失。
如果出现的不是 Welcome 窗口,而是 RTS Terrain Studio requires URP 对话框,说明项目中没有 URP 17.3。此时本包会保持未激活状态,不做任何更改。请按照对话框中的步骤操作,或阅读 Unity 与渲染管线。
Welcome 窗口
首次在项目中加载本包时,Welcome 窗口会自动打开。你可以随时通过 再次打开它。如果希望每次启动 Unity 时都显示该窗口,请勾选底部的 Show at startup。此设置按用户和项目分别存储,因此绝不会出现在版本控制中。
左栏是快速入门列表,每个步骤对应一个按钮:
| 步骤 | 按钮 | 作用 |
|---|---|---|
| 1. Validate & fix the project(验证并修复项目) | Validate | 打开 Project Validator。步骤下方的状态行会显示“Project is ready”,或者显示发现的错误和警告数量。 |
| 2. Create the demo content(创建演示内容) | Create | 运行 。 |
| 3. Open a demo scene(打开演示场景) | 场景列表 + Open | 演示场景生成后会在此列出,并打开你选中的场景(打开前会询问是否保存当前已打开的场景)。 |
| 4. Create your own map(创建自己的地图) | New Map | 打开 New Map Wizard。 |
| 5. Start editing(开始编辑) | Editor Window | 打开 RTS Terrain Studio 窗口。 |
右栏包含指向本手册的 Learn 链接(Documentation、Quick Start Guide、Scripting API、Integrations、Troubleshooting),以及一个 Integrations 列表,显示项目中是否已安装 Mirror、A* Pathfinding Project 和 Unity AI Navigation。
验证项目
Project Validator 会检查本包所需的一切,只需单击一次即可修复大多数问题。导入后运行一次;之后每当出现异常(材质变成粉色、水面看起来很平、单位透过迷雾仍然可见)时,再运行一次。
- 打开 ,或在 Welcome 窗口中单击 Validate。
- 阅读顶部的摘要。每项检查都是一张带有状态徽章(OK、Info、Warning 或 Error)的卡片。展开卡片上的 Details,即可查看具体发现了哪些问题。
- 单击 Fix All。在做出任何修改之前,对话框会列出所有更改。单击 Apply Fixes。
- 单击 Run Again,确认没有剩余的错误或警告。Info 卡片无需处理:它们只是向你提供信息。
Fix All 会分多轮执行。如果 URP 尚未启用,第一轮会先将项目切换到 URP,随后在同一次单击中应用依赖 URP 的摄像机纹理修复和战争迷雾修复。如果你更喜欢逐项修复,每张卡片也都有自己的修复按钮。
| 检查项 | 问题级别 | 修复按钮 | 修复内容及其重要性 |
|---|---|---|---|
| Rendering | |||
| Universal Render Pipeline is active | Error | Use URP | 将项目中的某个 URP 资源(如果没有则新建一个)指定为默认渲染管线,并指定给所有使用其他管线的质量级别。没有 URP 时,地形、水体和植被会渲染成粉色,或者完全不渲染。 |
| Depth and Opaque Texture enabled | Warning | Enable Both | 在质量级别所使用的每个 URP 资源上开启 Depth Texture 和 Opaque Texture。水体使用深度纹理,使水深颜色、泡沫和岸边效果能贴合立在水中的物体;使用不透明纹理实现折射。缺少它们,水面效果会简单一些。摄像机仍可以在其 Rendering 部分覆盖这些设置。 |
| Fog of War renderer feature | Warning | Add Feature | 将 RTS Fog Of War 渲染器功能添加到项目所用的每个 URP 资源的默认渲染器中,并指定其着色器。缺少该功能时,只有本包自带的着色器会显示迷雾;使用其他材质的单位和道具会一直可见。 |
| Fog kept in player builds | Warning | Keep Fog Modes | 当构建场景或已打开场景中的 RTSEnvironment 应用了雾效时,会显示此项检查。将 设置为 Custom,并保留正在使用的雾效模式。否则 Unity 会剥离雾效的着色器变体,带雾效的地图在构建版本中便不会显示雾效。参见Player 构建中的雾效。 |
| Project | |||
| Linear color space | Warning | Switch to Linear | 将 设置为 Linear。在 Gamma 空间下,颜色、光照和水体看起来会发白、褪色。切换后会重新导入所有纹理。 |
| Burst, Collections and Mathematics | Error | Install | 通过 Package Manager 添加缺少的包。缺少这些包,本包的脚本将无法编译。 |
| Input handling | Info | 无 | 报告是否已安装 Input System 包,以及当前启用的是哪种输入处理方式。旧版 Input Manager 同样可用。 |
| WebGL(仅当 Web 为当前构建平台时) | |||
| Web quality levels have Depth and Opaque Texture | Warning | Enable Both | 在 Web 构建所用质量级别的 URP 资源上开启 Depth Texture 和 Opaque Texture。编辑器通常使用桌面平台的质量级别渲染,因此如果没有这项检查,你只有在浏览器中才会看到效果较简单的水面。 |
| MSAA is off for Web builds | Error | Turn MSAA Off | 如果某个 Web 质量级别还用到了 Depth Texture 或 Opaque Texture,就在其 URP 资源中关闭 MSAA。URP 在 WebGL 2 上无法渲染这种组合,因此游戏在浏览器中会一直黑屏。 |
| Web template fills the browser window | Warning | Install Template | 安装本包的 Web 模板并将其选中。Unity 的 Default 和 Minimal 模板会把游戏显示在一个固定大小的小框中,游戏内编辑器、HUD 和小地图在其中难以看清。参见全窗口模板。 |
| Content | |||
| Package shaders compile | Error | Reimport | 重新导入本包中存在编译错误的着色器(如果着色器是在 URP 加载完成之前导入的,这样做会有帮助)。如果着色器完全缺失,请从 Asset Store 重新导入本包。 |
| Shaders included in builds | Error | Update | 在本包的 Resources 文件夹中创建或更新 RTSShaderReferences.asset 和 DecorPlaceholder 材质。仅通过代码创建的着色器如果没有任何引用,就会在 Player 构建时被剥离,导致构建出的游戏中地形显示为粉色。此外,脚本重新加载后、着色器发生变化时以及每次 Player 构建之前,该资源也会自动刷新。 |
| Tileset texture arrays built | Info | Build Arrays | 为尚无纹理数组或纹理数组已过时的图块集构建压缩纹理数组。没有它们,图块集也能工作,但编辑器会在加载地图时构建未压缩的数组,从而占用更多内存并延长加载时间。Player 构建时会自动重建过时的数组;从未构建过数组的图块集在构建出的游戏中同样使用未压缩的数组。 |
| GPU instancing on instanced prototypes | Warning | Enable Instancing | 在实例化渲染的树木、细节或装饰物所使用的材质上勾选 Enable GPU Instancing。 |
| Scene(仅限已打开的场景) | |||
| Terrains have a tileset | Warning | Assign '<tileset>' | 为每个没有图块集的地形指定一个图块集(可撤销)。没有图块集时,地形会显示为中性的占位外观。 |
| Terrains save to a map file | Info | Create Map Files | 对于每个没有 .rtsmap 资源的地形,将其地图保存到场景旁边的新文件中,并指定给该地形。没有地图文件时,关闭场景后所做的编辑就会丢失。 |
在 Play 模式下不提供场景修复。请先退出 Play 模式。
创建演示内容
本包不附带大型二进制美术资源,而是在你的项目中生成一整套演示资源,因此一切开箱即用,你可以立即开始绘制。
- 选择 ,或在 Welcome 窗口中单击 Create。
- 进度条会依次显示以下步骤:Textures、Materials、Meshes and prefabs、Tilesets, libraries and brushes、Shader references and UI settings、Demo maps、Demo scenes。首次运行可能需要一些时间。
- 最后会弹出一个对话框,询问是否将演示场景添加到构建设置中。除非你想构建包含演示场景的 Player,否则单击 Not Now(以后可以在 中添加)。只有 Add Scenes 才会更改构建设置。
- Project 窗口中会选中游戏玩法演示场景。你已打开的场景仍保持打开状态,不会替换任何内容。
生成的内容:
| 文件夹(位于包文件夹内) | 内容 |
|---|---|
Content/Textures | 地面、悬崖、水体、细节、道具和笔刷纹理。 |
Content/Materials、Content/Meshes、Content/Prefabs | 低多边形的树木、灌木、岩石、道具及其材质。 |
Content/Tilesets | Summer 和 Winter 图块集,以及 Summer Auto Paint 配置文件。Summer 包含地面图层 Grass、Lush Grass、Dirt、Rocky Dirt、Sand、Cobblestone、Dark Forest Floor、Rock、Snow 和 Blight,以及悬崖类型 Rock Cliff 和 Grassy Cliff。 |
Content/Libraries | Demo Vegetation(草、高草、花、蕨类;Pine、Broadleaf、Dead Tree 和 Bush 树木)和 Demo Decor(岩石、巨石、树桩和其他道具)。 |
Content/Brushes | 笔刷形状图章。 |
Content/Maps | 三张地图:Two Rivers (1v1).rtsmap(128 × 128,中心对称)、Four Kingdoms (4p).rtsmap(192 × 192,四重旋转对称)和 Sandbox.rtsmap(96 × 96,地势大多平坦,有一块高地和一个池塘)。 |
Samples/Scenes | 下文介绍的三个演示场景。 |
Resources/RTSTerrainStudio | 着色器引用和运行时 UI 面板设置。 |
再次运行生成器时,它会让你在 Regenerate 和 Cancel 之间选择。只有生成的演示资源和演示场景会被覆盖;你自己的资源绝不会受到影响。内容未发生变化的文件不会重写。
如果你的 URP 渲染器中缺少战争迷雾渲染器功能,生成器也会自动添加。它不会更改 URP 资源的深度纹理和不透明纹理设置:演示场景的摄像机会自行请求这些纹理。要让你自己场景中的水体正常显示,请按上文所述运行 Validate Project。
打开演示场景
演示场景位于 RTSTerrainStudio/Samples/Scenes 中。在 Project 窗口中双击即可打开,也可以在 Welcome 窗口的第 3 步中选择场景,然后单击 Open。
| 场景 | 地图 | 场景内容 | 试试看 |
|---|---|---|---|
| RTS Terrain Studio - Gameplay Demo | Two Rivers (1v1) | RTS 摄像机、战争迷雾,以及一个会自动开始游戏的 RTS Demo Game 对象。 | 单击 Play。你的单位(玩家 1)会在你的出生点生成;敌方单位四处游荡,并在战争迷雾中时隐时现。 |
| RTS Terrain Studio - In-Game Editor Demo | Sandbox | 游戏内地图编辑器(RTS In-Game Editor 对象),以及同样的游戏玩法系统,这些系统在你开始试玩之前保持关闭。 | 单击 Play,用游戏内编辑器进行绘制,然后单击 Play Test(F5)。按 Esc 返回编辑器。 |
| RTS Terrain Studio - Editor Playground | Four Kingdoms (4p) | 只有地形、光源和摄像机,不含游戏玩法。 | 保持在编辑模式下,打开 ,在一张现成的地图上试用各种工具。 |
游戏玩法演示中的操作
| 输入 | 操作 |
|---|---|
| W A S D / 方向键、屏幕边缘、按住鼠标中键拖动 | 平移摄像机。选中单位时,S 会让单位停止,而不是平移;方向键则始终用于平移。 |
| 鼠标滚轮 | 缩放 |
| Q / E、Alt + 右键拖动 | 旋转视角 |
| 左键单击 / 左键拖动 | 选择单位 / 拖出选择框 |
| Shift + 单击 | 加选或减选单位 |
| 双击、Ctrl + 单击 | 选中屏幕上所有同类型的单位 |
| 右键单击 | 以阵型移动(按住 Shift 可将命令加入队列) |
| S / H / P | 停止 / 原地驻守 / 巡逻 |
| Ctrl + 0…9、Shift + 0…9、0…9 | 设定编队 / 将当前选中的单位加入编队 / 调出编队(连按两次可将摄像机居中到该编队) |
| 小地图 | 左键单击移动摄像机,右键单击让选中的单位移动到该处 |
| B、Delete | 放置建筑 / 移除光标下的建筑 |
| T、右键单击树木 | 砍倒光标下的树木 / 让选中的工人去砍伐该树 |
| Esc | 取消当前模式,或清除选择 |
| F1 | 显示或隐藏帮助面板 |
你的第一张地图
地图位于场景中的 RTS Terrain GameObject 上,并存储在项目中的 .rtsmap 文件里。场景只引用该文件;你的编辑会保存到该文件中。先新建一个场景,以便自由尝试:
- 选择 ,然后选择一个 URP 场景模板(例如 Basic (URP))。
- 立即按 Ctrl+S 保存场景,例如保存为
Assets/MyMaps/FirstMap.unity。新的地图文件会创建在已保存场景的旁边,这样地图和场景就能放在一起。
创建地形
创建地形有两种方式。两种方式创建的都是相同的 RTS Terrain 对象,其中已经设置好 RTSTerrain、RTSVegetationRenderer、RTSWaterRenderer 和 RTSDecorManager 组件。
| 方式 | 菜单 | 适用场景 |
|---|---|---|
| 一键创建 | 你想立即开始绘制。你会得到一张 128 × 128 个单元格的平坦地图(单元格尺寸 2 m,悬崖层级 2),它使用项目的默认图块集和库,放置在 Scene 视图的轴心点上并处于选中状态。同时会在场景文件旁边写入一个名为 <Scene name> Map.rtsmap 的新地图文件(如果场景尚未保存,则写入 Assets/Maps/New Map.rtsmap)。 | |
| 向导式 | 你想自己选择名称、尺寸、图块集和基础纹理,或者生成一张随机的可玩地图。 |
如果项目中还没有图块集,一键创建菜单会先提供 Create Demo Content 选项。如果选择 Continue Without,地形会使用中性的棋盘格外观,直到你为它指定图块集。
New Map Wizard 共有五个步骤。使用 Next 和 Back 切换,或者单击顶部栏中的某个步骤:
| 步骤 | 设置项 |
|---|---|
| 1. Basics | Map Name(同时也是文件名)、Author(会记住并用于下一张地图)、Description、Players(1–16;竞技地图通常为 2、4 或 8 名玩家设计)。 |
| 2. Size | 尺寸卡片:Tiny 64(快速 1v1 对决和测试)、Small 96(1v1 和 2v2)、Medium 128(2 到 4 名玩家)、Large 192(4 到 6 名玩家)、Huge 256(6 到 8 名玩家)、Epic 384(8 名以上玩家、战役)、Epic XL 512(超大型世界)或 Custom(16 的倍数)。此外还有 Cell Size (m)(不确定时保持 2 m)、Cliff Level Height (m)、Base Cliff Level(默认值为 2,这样你就可以像经典 RTS 地图那样向下挖出湖泊),以及包含纹理、细节和通行分辨率的 Advanced 折叠栏。 |
| 3. Look | Tileset(单击一个地面纹理,将其设为初始时覆盖整张地图的 BASE 图层)、Vegetation Library、Decor Library。没有图块集时会显示 Create Demo Content 按钮。 |
| 4. Terrain | Flat(位于基础悬崖层级上的一张空白平坦地图,非常适合学习各种工具)或 Generate(根据 Preset 和 Seed 生成高地、坡道、水体、出生点、植被和装饰物,并提供 Symmetry、Start Locations、Scatter Vegetation、Scatter Decor 设置项和 Preview 按钮)。 |
| 5. Create | New terrain object 或 Replace map of selected terrain、Map File 路径(Browse...)、Add Environment(使用地图中的太阳光、环境光和雾效;如果场景中还没有太阳光,会创建一个 Sun 光源)、Open Editor Window,以及一份摘要。最后单击 Create Map。 |
向导会写入地图文件、创建地形、在 Scene 视图中聚焦显示它,并打开编辑器窗口。之后你可以在编辑器窗口的 Map 选项卡中修改所有设置。
打开编辑器窗口
打开 (向导和 Welcome 窗口也可以替你打开它,RTS Terrain 组件的 Inspector 中还有一个 Open RTS Terrain Studio 按钮)。将它停靠在 Inspector 窗口旁边,这样你可以同时看到 Scene 视图。

| 区域 | 显示内容 |
|---|---|
| Header(标题栏) | 地形选择器、地图名称(圆点表示有未保存的更改)、Save、Save As…、Open…、Reload、带步骤名称的 Undo / Redo、帮助按钮,以及 Editing On / Editing Off 开关。 |
| Tab bar(选项卡栏) | 八个工具类别(Height、Cliffs、Paint、Vegetation、Decor、Water、Pathing、Markers),之后是 Map 和 Settings。圆点标示当前工具所属的类别。 |
| Tools page(工具页) | 大号工具按钮、一张显示当前工具说明及其实时状态的卡片、Brush 和 Symmetry 卡片,以及该工具自身的设置和选择面板(图层、悬崖层级、玩家、树木、装饰物)。 |
| Status bar(状态栏) | 光标所在的单元格,鼠标下方的悬崖层级、高度和通行状态,以及撤销内存占用。 |
八个类别及其工具:
| 按键 | 类别 | 工具 |
|---|---|---|
| 1 | Height | Raise / Lower、Smooth、Flatten、Set Height、Noise、Terrace、Erode |
| 2 | Cliffs | Raise / Lower Level、Set Level、Ramp、Cliff Type、Clean Cliffs |
| 3 | Paint | Paint Layer、Tile Paint、Fill / Replace、Smooth Paint、Auto Paint |
| 4 | Vegetation | Paint Details、Erase Details、Place Trees、Erase Trees |
| 5 | Decor | Place Decor、Erase Decor、Select Decor |
| 6 | Water | Paint Water、Erase Water、Flood Fill |
| 7 | Pathing | Paint Pathing、Clear Pathing |
| 8 | Markers | Start Locations、Regions、Markers |
绘制基础
- 在编辑器窗口中单击一个工具按钮。这会开启编辑(标题栏显示 Editing On)。再次单击当前工具会关闭编辑。
- 在 Scene 视图中将鼠标移到地形上。笔刷圆圈会直接显示在地面上,正好是你将要绘制的位置。
- 按住鼠标左键并拖动即可绘制。
| 输入(Scene 视图) | 操作 |
|---|---|
| 鼠标左键 | 绘制一次笔触(对于 Start Locations 等点击工具则为单击) |
| Shift + 鼠标左键 | 反向操作:降低、擦除、移除 |
| Ctrl + 单击 | 对于支持此功能的工具,拾取光标下的值(高度、地面图层、水面高度……) |
| [ / ] | 缩小 / 放大笔刷 |
| - / = | 降低 / 提高强度 |
| 1 … 8 | 切换到某个工具类别(再次按下可在该类别的工具之间循环切换) |
| G / P | 显示或隐藏栅格 / 循环切换通行叠加层(Off、Walkability、Buildability、Combined) |
| R | 开启或关闭笔刷随机旋转 |
| Ctrl+Z / Ctrl+Y | 撤销 / 重做地形编辑(Edit 菜单会显示步骤名称) |
| Esc | 取消正在进行的笔触,或停止编辑 |
Scene 视图中还有一个 RTS Terrain Studio 叠加层,包含编辑开关、类别和工具按钮、Size 和 Strength 滑块以及 Symmetry 设置;底部还有一行状态信息,显示鼠标悬停处的单元格、悬崖层级和高度。大部分工作你都可以留在 Scene 视图中完成。
Scene 视图中的摄像机导航照常使用(按住鼠标右键拖动可飞行,Alt + 左键拖动可环绕,中键拖动可平移)。如果 Unity 的 Hand 工具(Q)处于激活状态,左键拖动会平移视图而不是绘制;按 W 即可恢复绘制。所有 RTS Terrain Studio 快捷键都可以在 (macOS 上为 )中的 RTS Terrain Studio 类别下重新绑定(如果在你的布局中 2 与 Unity 的 2D 模式切换键冲突,这会很有用)。
绘制高度
- 打开 Height 选项卡(1),然后单击 Raise / Lower。
- 在地形上拖动,堆出山丘。按住鼠标键不放会持续升高。按住 Shift 可改为降低。
- 切换到 Smooth,在尖锐的部位上拖动,将其柔化。
- 试试用 Noise 制作粗糙的地面,用 Flatten(Ctrl+单击拾取高度)整平建筑区域。
高度只是叠加在悬崖层级之上的精细起伏:高地上的山丘会始终留在这块高地上。
绘制悬崖和坡道
悬崖是经典 RTS 地图的核心。地图使用 0 到 15 的悬崖层级,初始时整张地图位于基础层级(默认为 2)。每个层级对应一级棱角分明的垂直悬崖台阶。
- 打开 Cliffs 选项卡(2),然后单击 Raise / Lower Level。
- 在地形上单击:地面升高一个悬崖层级。按住鼠标键不放并拖动,可以在同一层级上扩展你开始绘制的那块高地。
- 在高地内部单击并再次拖动,即可在其上叠加第二个层级。
- 按住 Shift 可降低一个层级。降到基础层级以下可以挖出用于湖泊和河流的盆地。
- 单击 Ramp,横跨高差为一个层级的悬崖边缘进行绘制,将其变为可行走的斜坡。单元格的四个角点都必须是坡道,所以绘制时要宽裕地覆盖边缘两侧。Shift 可移除坡道。
- 在杂乱的边缘上使用 Clean Cliffs,可以移除单个顶点形成的尖柱和凹坑。Cliff Type 可以绘制另一种悬崖材质(例如用 Grassy Cliff 代替 Rock Cliff)。
绘制纹理
- 打开 Paint 选项卡(3),然后单击 Paint Layer。
- 在图层选择面板中单击一个地面图层,例如 Dirt。
- 在地形上拖动,绘制道路和空地。Shift 擦除该图层;Ctrl+单击拾取光标下的图层。
- 要绘制经典 RTS 风格的硬边图块,使用 Tile Paint。要处理大面积区域,使用 Fill / Replace。
- 想快速获得自然的外观,可以使用 Paint 选项卡上的 Auto Paint Whole Map 卡片,并将 Profile 设为 Summer Auto Paint:它会在悬崖附近绘制泥土,在岸边绘制沙子,在陡坡上绘制岩石,等等。
放置树木和草
- 打开 Vegetation 选项卡(4),然后单击 Place Trees。
- 在 Trees 中选择一种或多种树木类型,例如 Pine 和 Broadleaf。多种类型会随机混合。
- 保持 Mode 为 Scatter,沿地图边缘拖动以种植森林。Density 控制每个笔印添加多少棵树。树木不会放置在坡度大于 Max Slope 的斜坡上,也不会放置在水下(Avoid Water)。
- 将 Mode 设为 Single,每次单击放置一棵树。Shift 可移除树木。
- 单击 Paint Details,在 Details 中选择 Grass 或 Flowers Yellow,然后绘制草地。Target Density 设置笔刷绘制的密度。
植被工具要求地形上有植被库。执行 Create Demo Content 之后创建的地形会自动获得 Demo Vegetation。装饰物的用法相同:使用 Decor 选项卡(5)上的 Place Decor 和 Demo Decor 库。
添加水体
水体会填充低于目标水面高度的地面。最简单的方法是先用悬崖工具挖出盆地,再将其填满。
- 在 Cliffs 选项卡上,按住 Shift 使用 Raise / Lower Level,在比基础层级低一个层级的位置挖出湖泊或河流(在默认地图上为层级 1)。
- 打开 Water 选项卡(6),然后单击 Flood Fill。
- 保持默认的 Water Height 模式(悬崖层级加偏移),并使用 Level 1。默认偏移会让水面恰好位于基础层级下方,因此盆地会被填满,而周围的陆地保持干燥。
- 在盆地内部单击,一次单击即可填满。Shift+单击可排干相连的水体。
- 使用 Paint Water 手动绘制水体,使用 Erase Water 移除水体。
浅水和深水会根据水深自动区分,并影响通行:地面单位可以穿过浅水(但那里不能建造任何东西,因为建造需要干燥的地面),而深水会阻挡它们。
如果水面没有折射,而且岸边效果忽略了立在水中的物体,说明 URP 的 Depth Texture 和 Opaque Texture 未开启。运行 ,然后应用 Enable Both。
放置出生点
出生点是玩家的初始生成位置。演示游戏会在那里生成每个玩家的单位。
- 打开 Markers 选项卡(8),然后单击 Start Locations。
- 在玩家选择面板中选择 P1(玩家 1,红色)。
- 在地形上单击玩家 1 应该出生的位置。每个玩家只有一个出生点,因此再次单击会移动它。使用 Yaw 设置朝向。
- 选择下一个玩家并再次单击。Shift+单击可移除最近的出生点。
开启对称后,单击一次即可放置所有玩家:只要勾选了 Symmetry Assigns Next Players,对称副本就会依次分配给后续的玩家(P1、P2……)。参见 10 分钟教程。
将出生点放在平坦、干燥且远离悬崖边缘的地面上。Map 选项卡上的 Validate Map(位于 Validation 卡片中)会报告缺少出生点、出生点位于悬崖边缘或深水中等问题。
保存地图
在你将编辑保存到 .rtsmap 文件之前,它们只存在于内存中:
- 单击编辑器窗口标题栏中的 Save。存在未保存的更改时,该按钮会高亮显示,并且地图名称旁会出现一个圆点。
- Save As… 会在
Assets中的任意位置写入一个新的.rtsmap文件,并将其指定给该地形。 - 保存场景(Ctrl+S)时也会保存场景中各个地形的地图。你可以在 Scene 视图叠加层的选项菜单中关闭此功能(Save Maps When Saving the Scene)。
- RTS Terrain 组件的 Inspector 和 Scene 视图叠加层菜单中也有 Save 和 Save As 菜单项。
当地图有未保存的更改时,如果你进入 Play 模式、关闭场景或退出 Unity,会弹出对话框询问是否保存这些更改。在 Project 窗口中选中一个 .rtsmap 文件,即可看到它的小地图预览和 Open In Scene 按钮。
没有地图文件的地形只会将地图保存在内存中。如果你看到对话框提示“The map of '…' is not saved as a map asset yet”,选择 Save As… 即可创建该文件。
你的第一次试玩
试玩地图有两种方式:在 Play 模式下使用 Demo Game,或者在运行中的游戏里使用游戏内编辑器及其 Play Test 按钮。
使用 Demo Game 试玩
- 打开包含你的地形的场景。确保其中至少有一个出生点(否则单位会在两个相对的角落生成)。
- 选择 。这会创建并选中一个 RTS Demo Game 对象。
- 按下 Play。出现提示时保存地图。
- 演示游戏会自行设置所需的一切:战争迷雾、RTS 摄像机、单位选择、移动命令、小地图、HUD、建筑放置和伐木。你的单位(玩家 1)会在你的出生点生成;其他玩家的单位会四处游荡。
- 按 F1 打开帮助面板。操作方式列在游戏玩法演示中的操作中。
- 退出 Play 模式。游戏中砍倒的树木会被恢复,因此地图会保持你制作时的样子。
RTS Demo Game 组件提供 Local Team、Units Per Player、Unit Prefabs(留空则使用占位单位)、Fog Of War、Minimap 和 Hud 等选项。它使用场景的主摄像机。
如果单位在战争迷雾中仍然可见,说明缺少 RTS Fog Of War 渲染器功能。运行 Validate Project,然后应用 Add Feature。
在游戏中编辑和试玩
游戏内编辑器把同一套工具搬进了运行中的游戏,并配有自己的 UI。借助它,玩家和模组作者可以在发布的游戏中制作并试玩地图。
- 打开包含你的地形和摄像机的场景。如果场景中还没有 RTS Demo Game,按上文所述添加一个。
- 选择 。
- 按下 Play。编辑器打开后会显示顶部栏(File、Map、撤销和重做、Symmetry、Grid、Pathing、Markers、Play Test)、左侧的类别与工具栏、右侧的工具面板以及状态栏。演示游戏会一直等待,直到你开始试玩。
- 绘制方式与在 Unity 编辑器中相同:鼠标左键绘制,Shift 反转,Ctrl+单击拾取,[ ] 和 - = 调整笔刷,1…8 切换类别。使用 W A S D、右键或中键拖动、Q/E 以及鼠标滚轮移动摄像机。
- 单击 Play Test 或按 F5。单位随即生成,战争迷雾和 RTS 摄像机也会开启。
- 按 Esc 返回编辑器。地图会恢复到试玩之前的状态。
游戏内编辑器中的常用按键:F1 帮助、F2 显示或隐藏整个编辑器、H 隐藏面板、G 栅格、P 通行叠加层、M 标记点、F 聚焦光标下的点、Ctrl+Z / Ctrl+Y 撤销和重做。
在游戏内编辑器中,(Ctrl+S)和 (Ctrl+Shift+S)会将地图文件写入游戏的地图文件夹(默认是 Application.persistentDataPath 中的 Maps 文件夹),方式与发布的游戏完全相同。在 Unity 编辑器中编辑后退出 Play 模式时,会弹出对话框询问“Save map changes made in Play Mode?”:Save 会将更改写入地形的 .rtsmap 资源,Discard 则会丢弃这些更改。
游戏内编辑器的完整说明详见游戏内编辑器。
面向程序员:响应试玩
你自己的游戏玩法代码可以监听演示游戏所使用的同一个事件。以下示例会在试玩开始时记录每个玩家的出生点:
using RTSTerrainStudio;
using UnityEngine;
// Add this component to any GameObject in a scene with an RTS Terrain and an in-game editor.
public class PlaytestStartLogger : MonoBehaviour
{
void OnEnable()
{
// Raised by the in-game editor when a play test starts (true) or stops (false).
RTSRuntimeEditorEvents.PlaytestChanged += OnPlaytestChanged;
}
void OnDisable()
{
RTSRuntimeEditorEvents.PlaytestChanged -= OnPlaytestChanged;
}
void OnPlaytestChanged(bool playing)
{
if (!playing) return;
// The first enabled terrain of the scene.
RTSTerrain terrain = RTSTerrain.Main;
if (terrain == null || terrain.Map == null) return;
foreach (StartLocation start in terrain.Map.StartLocations)
{
// Start locations are stored in map-local coordinates: convert to world space
// and move the point onto the ground.
var local = new Vector3(start.position.x, start.position.y, start.position.z);
Vector3 world = terrain.SnapToGround(terrain.LocalToWorld(local));
Debug.Log($"Player {start.player + 1} starts at {world}, facing {start.yaw:0} degrees");
}
}
}
有关地形查询、加载地图和工具 API,参见脚本编写。
10 分钟教程:你的第一张 1v1 地图
在本教程中,你将为两名玩家制作一张小巧而公平的地图:每个玩家都在一块带坡道的高地上拥有一个基地,一条河流将地图分为两半,森林环绕在地图四周。对称绘制可以确保两侧完全相同,因此你只需制作一半。
开始之前,请确认项目已通过验证,并且已创建演示内容(参见安装与配置)。
1. 创建地图(1 分钟)
- 选择 ,选择一个 URP 模板,并将场景保存为
Assets/MyMaps/Duel.unity。 - 打开 。
- Basics:Map Name 填写
Duel,Author 填写你的名字,Players 设为 2。单击 Next。 - Size:单击 Small(96 × 96,1v1 和 2v2)。保持 Cell Size 为 2 m、Base Cliff Level 为 2。单击 Next。
- Look:Tileset 选择 Summer,单击 Grass 将其设为基础图层,Vegetation Library 选择 Demo Vegetation,Decor Library 选择 Demo Decor。单击 Next。
- Terrain:选择 Flat。单击 Next。
- Create:保持 New terrain object,勾选 Add Environment 和 Open Editor Window,然后单击 Create Map。
2. 开启对称(10 秒)
- 在编辑器窗口中打开 Cliffs 选项卡。在 Brush 卡片下方找到 Symmetry 卡片。
- 选择 Rotate 180°。将对称中心保持在地图中心(0.5, 0.5)。
- 将鼠标移到地形上:现在你会在地图的另一侧看到第二个笔刷,Scene 视图中也会绘制出对称辅助线。
所有工具共享同一个对称设置,因此切换工具时它会保持开启。各模式如下:
| 模式(窗口) | 副本数 | 典型用途 |
|---|---|---|
| Off | 1 | 战役或剧情地图 |
| Mirror X | 2 | 左右镜像 |
| Mirror Z | 2 | 上下镜像 |
| Mirror XZ | 4 | 沿两个轴镜像(4 名玩家) |
| Rotate 180° | 2 | 中心对称,经典的 1v1 布局(角对角) |
| Rotate 90° | 4 | 适用于 4 名玩家的四重旋转对称;需要正方形地图 |
| Diagonal | 2 | 沿对角线镜像;需要正方形地图 |
Scene 视图叠加层和游戏内编辑器提供相同的模式(叠加层显示的是枚举名称,例如 Rotate 2 对应 Rotate 180°)。
3. 建造基地(2 分钟)
- 单击 Raise / Lower Level。用 [ 和 ] 设置一个较大的笔刷。
- 在地图左下方的四分之一区域中单击并拖动,绘制一块宽度约为地图四分之一的高地。右上方的四分之一区域会出现同样的高地。
- 单击 Ramp,横跨朝向地图中心的高地边缘进行绘制。坡道至少要有两到三个单元格宽,以便成群的单位通过。
- 单击 Clean Cliffs,沿高地轮廓绘制一遍,移除零散的单顶点凸起。
4. 挖出河流(2 分钟)
- 仍然使用 Raise / Lower Level,按住 Shift 从左上角向地图中心拖出一条河流。对称副本会从右下角延续这条河,因此河流会贯穿整张地图。
- 在中心附近留下一两座陆桥,让玩家能够到达对方的区域。
- 打开 Water 选项卡,单击 Flood Fill,保持 Level 为 1,然后单击河床。对任何尚未填满的部分再单击一次。
5. 绘制地面(2 分钟)
- 打开 Paint 选项卡。在 Auto Paint Whole Map 卡片中将 Profile 设为 Summer Auto Paint(如果留空,则使用与图块集图层名称相匹配的内置规则),然后单击 Auto Paint Whole Map。确认对话框;整次重绘只算一个撤销步骤。悬崖附近会出现泥土,河岸出现沙子,陡坡上出现岩石。
- 单击 Paint Layer,选择 Cobblestone,在每块高地上主建筑将要矗立的位置绘制一个方块(一个笔印就够了,对称副本会绘制另一个基地)。
- 选择 Dirt,绘制一条从基地坡道通往陆桥的道路。
- 可选:在 Height 选项卡上,以较低的强度(-)使用 Raise / Lower,在两个基地之间添加几座平缓的山丘,再用 Smooth 将其柔化。
6. 森林、草和岩石(2 分钟)
- 打开 Vegetation 选项卡并单击 Place Trees。选择 Pine 和 Broadleaf,保持 Scatter。
- 沿地图边界和每个基地后方绘制森林。保持坡道、道路和陆桥畅通;Shift 可移除树木。
- 单击 Paint Details,选择 Grass 和 Flowers Yellow,绘制几片草地。
- 打开 Decor 选项卡,单击 Place Decor,选择几种岩石,然后在河岸附近单击。
7. 放置出生点(30 秒)
- 打开 Markers 选项卡,然后单击 Start Locations。
- 选择玩家 P1,并确保已勾选 Symmetry Assigns Next Players。
- 在左下方基地的中间单击一次。P1 会被放置在那里,P2 则出现在对面的基地上;如果你设置了指向中心的 Yaw,两者都会面朝中心。
8. 验证、保存并试玩(1 分钟)
- 打开 Map 选项卡,在 Validation 卡片中单击 Validate Map。修复它报告的所有错误(例如位于悬崖边缘的出生点)。
- 单击窗口标题栏中的 Save。地图会写入
Assets/MyMaps/Duel.rtsmap。 - 选择 ,然后按下 Play。
- 选中你的单位,右键单击将它们派往陆桥对面,然后看着敌人从战争迷雾中现身。
想要四名玩家?创建一张正方形地图(例如 Medium 128 × 128),选择 Rotate 90°,并将 Players 设为 4。这样使用 Start Locations 工具单击一次即可放置 P1 到 P4。也可以交给地图生成器来完成:在向导的 Terrain 步骤中选择 Generate,设置 Symmetry 和 Start Locations,单击 Preview,然后手动完善结果。
后续步骤
- 编辑器窗口:详细介绍每个选项卡、卡片和设置项,包括 Map 选项卡(调整尺寸、地图生成器、高度图导入与导出、小地图、环境)。
- 各工具章节,每个类别一章:Height、Cliffs、Paint、Vegetation、Decor、Water、Pathing、Markers。
- 游戏内编辑器:随游戏一起发布编辑器,让玩家自己制作地图。
- 脚本编写:通过代码查询高度、悬崖层级和通行数据,在运行时加载地图,并响应编辑操作。
- 集成:使用 Mirror 实现多人联机,使用 A* Pathfinding Project 或 Unity NavMesh 实现寻路。
- 故障排除:材质显示为粉色、水面看起来很平、绘制位置偏移以及其他常见问题。
项目配置
RTS Terrain Studio 需要几项项目设置:启用两张摄像机纹理的通用渲染管线(URP)、战争迷雾渲染器功能、Linear 色彩空间,以及三个 Unity 包。 窗口会检查所有这些设置,并能一键修复其中的大部分,因此大多数人根本用不到本章中的手动步骤。如果某项检查始终显示为红色、你想了解某项修复具体会更改什么,或者你要手动配置项目,请继续阅读本章。
Project Validator 中每张卡片上的帮助按钮都会打开本章中的对应小节。
Project Validator(项目验证器)
打开 ,或者在 Welcome 窗口中单击 Validate。窗口一打开,检查就会自动运行。所有检查分为五个类别:
| 类别 | 检查内容 |
|---|---|
| Rendering | URP 已激活,Depth Texture 和 Opaque Texture 已启用,战争迷雾渲染器功能已安装,Player 构建保留了 RTS Environment 所用的雾效模式。 |
| Project | 使用 Linear 色彩空间;已安装 Burst、Collections 和 Mathematics;当前启用的是哪种输入处理方式。 |
| WebGL | 仅在 Web 为当前构建平台时检查:Web 构建所用的质量级别启用了 Depth Texture 和 Opaque Texture,并且 Web 模板会填满浏览器窗口(参见Project Validator 中的 Web 检查)。 |
| Content | 包内着色器可以正常编译并已包含在构建中,图块集纹理数组已构建,实例化原型已启用 GPU 实例化。 |
| Scene | 已打开的场景中,每个 RTS Terrain 都有图块集和地图文件。 |
每个检查结果都会显示以下四种状态之一:
| 状态 | 含义 |
|---|---|
| OK | 所有设置均正确。 |
| Info | 提示或建议。没有任何功能出问题。 |
| Warning | 某项功能只能降级运行,例如水体没有折射效果。建议修复。 |
| Error | 缺少必需的设置项。本包的部分功能将无法工作。 |
Fix All 会应用所有可用的修复。对话框会先列出全部更改,在你单击 Apply Fixes 之前不会修改任何内容。依赖 URP 的修复(摄像机纹理和战争迷雾渲染器功能)会在渲染管线指定完成后自动运行,因此只需单击一次即可配置好一个全新的项目。Run Again 会重新运行所有检查。完整的检查列表及对应的修复按钮参见验证项目。
Project Validator 检查的是你的项目。要检查单张地图(出生点、可达性、缺失的原型、性能限制),可使用 Map 选项卡上的地图验证器;参见地图验证。
通过代码运行检查
RTSProjectValidator.RunChecks() 返回检查结果,不做任何更改;RTSProjectValidator.FixAll() 则会应用修复。你可以在构建脚本或持续集成作业中使用它们。下面的示例必须放在 Editor 文件夹中。
using UnityEditor;
using UnityEngine;
using RTSTerrainStudio.Editor;
// Command line: Unity -batchmode -projectPath <path> -executeMethod ProjectSetupCheck.Run -quit
public static class ProjectSetupCheck
{
public static void Run()
{
// Runs every check. Nothing is modified.
var results = RTSProjectValidator.RunChecks();
// Applies every available fix without dialogs, then checks again.
int applied = RTSProjectValidator.FixAll(results, interactive: false);
if (applied > 0) results = RTSProjectValidator.RunChecks();
// Logs warnings and errors ("[Status] Title: Message").
foreach (var result in results)
if (result.IsProblem) Debug.LogWarning(result.ToString());
// Fails the job when a required setting is still missing.
if (RTSProjectValidator.WorstStatus(results) == RTSValidationStatus.Error)
EditorApplication.Exit(1);
}
}
针对缺失包的 Install 修复只会发起一个 Package Manager 请求,在请求完成之前,本包的脚本无法编译。在批处理模式下运行本包之前,请先安装 Burst、Collections 和 Mathematics。
通用渲染管线(URP)
本包的所有着色器都是为 URP 编写的。没有 URP,地形、水体和植被会显示为粉色,甚至完全不显示。Use URP 修复会选用项目中的一个 URP 资源(或者创建带有渲染器的
Assets/Settings/RTS URP.asset),将其指定为默认渲染管线,并指定给所有使用其他渲染管线的质量级别。手动操作步骤如下:
- 如果项目中还没有 URP 资源,在 Project 窗口中通过 创建一个。
- 在 中将其指定为 Default Render Pipeline。
- 打开 。对于每个设置了 Render Pipeline Asset 覆盖的质量级别,确认所指定的同样是 URP 资源。
Depth Texture 和 Opaque Texture
水体使用摄像机深度纹理,让水深颜色、泡沫和柔和的岸边能贴合立于水中的物体;使用不透明纹理实现折射。没有这两张纹理,水面效果会更简单。选中质量级别所用的每个 URP 资源,在其 Rendering 部分勾选 Depth Texture 和 Opaque Texture,或者在验证器中单击 Enable Both。
摄像机可以在其自身 Inspector 窗口的 Rendering 部分覆盖这两项设置,而水体会遵循渲染它的每个摄像机的设置。如果 Game 视图中的水面仍然比预期简单,请检查你的游戏摄像机是否关闭了它们。Scene 视图始终使用 URP 资源中的设置。另请参阅水体的 URP 要求。
战争迷雾渲染器功能
RTS Fog Of War 渲染器功能(RTSFogOfWarRendererFeature)会在屏幕上的所有内容之上绘制迷雾,包括使用你自己材质的单位和道具。没有它,只有本包自带的着色器才会显示迷雾。Add Feature
会将它添加到每个正在使用的 URP 资源的默认渲染器中。要手动添加,选中 Universal Renderer Data 资源,单击 Add Renderer Feature,然后选择
RTS Fog Of War。详情参见渲染:渲染器功能与着色器回退。
色彩空间
本包是针对 Linear 色彩空间(URP 的默认设置)设计的。在 Gamma 色彩空间中,颜色、光照和水体与截图相比会显得发灰、褪色。将 设置为 Linear,或者在验证器中单击 Switch to Linear。
切换色彩空间会重新导入所有纹理。在大型项目中,这可能需要一些时间。
必需的包
运行时程序集的作业和数据模型使用了 Burst(com.unity.burst)、Collections(com.unity.collections)和 Mathematics(com.unity.mathematics)。只要缺少其中任何一个,本包的脚本就无法编译。你可以通过
安装它们,或者在验证器中单击 Install,它会通过一次 Package Manager 请求添加所有缺失的包。经过测试的版本列在必需的包中。
如果导入后出现编译错误,并且 菜单没有出现,那么验证器也无法运行,因为它本身就是本包的一部分。请手动安装这三个包,然后再打开验证器。
输入处理
所有运行时输入(游戏内编辑器、示例摄像机和单位)都经由静态类 RTSInput 处理。安装了 Input System 包(com.unity.inputsystem)后,运行时程序集会在编译时定义符号 RTS_INPUT_SYSTEM,并使用 Input System。否则,或者当项目只使用旧版 Input Manager 时,RTSInput 会回退到旧版
UnityEngine.Input API。
| Active Input Handling | 已安装 Input System | 结果 |
|---|---|---|
| Input System Package (new) | 是 | OK。使用 Input System。 |
| Both | 是 | OK。使用 Input System。 |
| Input Manager (old) | 是 | Info。使用回退方案。 |
| 任意 | 否 | Info。使用回退方案。可以正常工作,但不建议在新项目中使用。 |
该设置位于 。更改此设置后,Unity 会要求重启编辑器。Input handling 检查从不会替你更改此设置,因为如何设置取决于你的游戏。另请参阅“环境要求”中的输入处理。
着色器与内容
- Package shaders compile. 如果某个包内着色器存在编译错误(受影响的对象显示为粉色),Reimport 会重新导入该着色器。如果着色器是在 URP 加载完成之前导入的,这样做通常能解决问题。如果必需的着色器完全缺失,请从 Asset Store 重新导入本包。参见故障排除。
- Shaders included in builds. 参见构建游戏。
- Tileset texture arrays built. 纹理数组未构建或已过期的图块集仍然可以使用,但在重新构建数组之前,编辑器会在加载地图时构建未压缩的数组,这会额外占用内存并延长加载时间。Build Arrays 会为每个已过期的图块集构建数组。Player 构建会自动重建已过期的数组;从未构建过数组的图块集在 Player 构建中同样会使用未压缩的数组。参见纹理数组和 Player 构建中的纹理数组。
- GPU instancing on instanced prototypes. 实例化树木、Mesh 模式细节和实例化装饰物的材质应勾选 Enable GPU Instancing。否则会改用临时副本进行绘制,而且其实例化变体可能会在 Player 构建中被剥离。Enable Instancing 会在列出的每个可编辑材质上勾选该选项。模型或包内的材质是只读的:请先将其提取出来。参见 GPU 实例化。
- Terrains have a tileset. 没有图块集的地形会显示为中性的占位外观。该修复会为每个没有图块集的地形指定项目中的一个图块集(可撤销)。参见图块集。
在 Play 模式下不提供场景修复,因为在 Play 模式中对场景所做的更改会在退出 Play 模式时丢失。
场景地形的地图文件
RTS Terrain 将地图保存在 .rtsmap 资源中(由其 Map Asset 字段引用)。没有此资源的地形,其地图只存在于内存中,关闭场景时你所做的编辑就会丢失。Terrains save to a map file
检查会列出这些地形。Create Map Files 会将列出的每个地形的当前地图保存为其场景所在文件夹中的一个新 .rtsmap 文件(如果场景尚未保存,则保存到
Assets/Maps),并将该文件指定给地形。
使用 New Map Wizard 创建的地形和演示内容中的地形已经带有地图文件。要了解地图资源、Save 和 Save As 的工作方式,请阅读地图资源与地图文件。
构建游戏
只有当构建中的某个材质使用了某个着色器时,Unity 才会将该着色器放入 Player 构建。RTS Terrain Studio 在运行时通过代码创建地形、水体和战争迷雾的材质,因此这些着色器由包文件夹内的资源
Resources/RTSTerrainStudio/RTSShaderReferences.asset 引用。该资源还引用了
DecorPlaceholder 材质,从而让 URP Lit 的实例化变体保留在构建中。每次脚本重新加载后、每次导入着色器时以及每次 Player 构建之前,系统都会自动检查并更新该资源。如果 Shaders included in builds 检查显示为红色,单击 Update。
构建之前,请逐项检查以下清单:
- 运行 ,修复所有错误和警告。
- 检查目标平台的每个质量级别是否都使用了启用 Depth Texture 和 Opaque Texture 的 URP 资源;如果你使用战争迷雾,还要检查其渲染器是否添加了 RTS Fog Of War 功能。
- 为图块集构建一次纹理数组(在验证器中单击 Build Arrays,或在图块集的 Inspector 窗口中单击 Build Texture Arrays),以免地形在加载时以未压缩形式构建它们。每次 Player 构建之前,已过期的数组都会自动重建(参见下文)。
- 如果你的地图使用雾效,请确保 Fog kept in player builds 检查的状态为 OK(参见Player 构建中的雾效)。
- 在 Map 选项卡上验证你的地图(参见地图验证)。
- 将你的场景添加到构建配置文件的场景列表中。场景中指定给 RTS Terrain 的地图会和其他被引用的资源一样包含在构建中。
- 如果你要随游戏发布游戏内编辑器,请阅读发布游戏内编辑器。玩家在运行时保存的地图会存放到
RTSMapSerializer.StorageRoot中的Maps文件夹(即Application.persistentDataPath;在 Web 构建中则是浏览器的存储空间);参见运行时的地图文件。 - 如果你将 Managed Stripping Level 调高到 Minimal 以上,并且编写了自己的编辑工具,请阅读代码剥离与自定义工具。
- 对于 Web 构建,还请阅读Web(WebGL)构建。
不要移动或重命名 RTSShaderReferences.asset 或本包的 Resources 文件夹:本包在运行时通过 Resources.Load 加载这些资源。
Player 构建中的纹理数组
Player 会原样使用图块集已构建的纹理数组,因为只有编辑器才能将它们与源纹理比较。为了确保构建版本中不会带上旧的地面纹理,每次 Player 构建开始之前,系统都会检查每个曾经构建过数组的图块集:
- 如果数组与纹理、分辨率或压缩设置不再一致,或者是为其他平台或其他纹理压缩格式构建的,系统会以普通压缩质量重新构建它们。进度条会显示重建进度。
- 从未构建过数组的图块集不受影响。加载地图时,Player 会为它们构建未压缩的数组。
- 如果重建被取消或失败,并且纹理或设置已经更改,系统会弹出一个对话框,提供 Try Again、Cancel Build 和 Build Anyway 三个选项。仅格式不同的数组仍然能显示正确的纹理,因此只会记录一条警告。
- 在这种情况下,批处理模式下的构建会停止。在命令行中添加
-rtsAllowOutdatedTextureArrays即可继续构建,每个图块集会记录一条警告。 - 没有图形设备的编辑器(
-nographics,在构建服务器上很常见)无法构建数组。此时构建会继续进行,每个已过期的图块集记录一条警告,Player 会使用上一次构建的数组。进行此类构建之前,请先在编辑器中构建好数组。 - 只会为当前构建平台重建数组。如果你为其他平台构建,构建过程只会记录一条警告:请先切换到该平台。
- 单独构建的 AssetBundle 和 Addressables 内容不会运行此检查。请先构建好数组。
过期检查依据的是源纹理的导入状态,而 Unity 升级可能会改变该状态。因此,升级后的第一次 Player 构建可能会重建所有已构建图块集的数组,包括演示图块集。
已构建的数组以可读形式存储在图块集资源中。Player 会在首次使用这些数组时释放这份 CPU 副本,在 512 px 下每个图块集约可节省 14 MB,在 1024 px 下约可节省 56 MB。
Player 构建中的雾效
游戏运行时,RTSEnvironment 会开启地图中存储的雾效。Unity 会剥离它认为未使用的雾效模式的着色器变体:当 设置为 Automatic 时,它只会保留保存时开启了雾效的构建场景所用的雾效模式。RTS Environment 在每次保存前都会恢复场景自身的雾效设置,因此它所应用的雾效不会被计入,带雾效的地图在构建版本中便不会显示雾效。
Fog kept in player builds 检查会读取当前构建配置文件中的构建场景以及已打开的场景,并列出 RTS Environment 在其中使用的雾效模式。Keep Fog Modes(也包含在 Fix All 中)会将 Fog Modes 设置为 Custom,并恰好保留这些模式,外加之前已经保留的模式。每个保留的模式都会为构建增加着色器变体,因此未使用的模式不会被添加。如果当前构建配置文件覆盖了 Graphics 设置,请同样检查其中的 Fog Modes。
Web(WebGL)构建
RTS Terrain Studio 可以在网页浏览器中运行,包括地形、植被、水体、装饰物、战争迷雾、游戏内编辑器和示例。不过,浏览器的工作方式与桌面平台不同:玩家无法打开任何文件夹,部分按键归浏览器所有,所有代码都在一个线程上运行,而且整个游戏共享一个大小有限的内存堆。本节介绍本包如何应对这些差异,以及如何构建、测试和发布 Web 构建。
简要步骤:
- 打开 ,选择 Web,然后单击 Switch Platform(或者将你的 Web 构建配置文件设为当前配置文件)。
- 运行 ,然后单击 Fix All。当 Web 为当前平台时,验证器还会安装全窗口模板,并检查 Web 构建的质量级别。
- 如果你的地图使用雾效,请检查 Fog kept in player builds 的状态是否为 OK。
- 单击 Build And Run。Unity 会构建游戏并在浏览器中打开它。
浏览器中支持的功能
| 主题 | 支持情况 |
|---|---|
| 图形 API | WebGL 2.0,即 Web 构建的默认设置(Auto Graphics API)。本包的所有着色器都能在其上运行。本节介绍的是 WebGL 2.0;Unity 作为可选项提供的 WebGPU 不在本节讨论范围内。 |
| 浏览器 | Unity 的 Web Player 所支持的当前版本桌面浏览器,需配合鼠标和键盘使用。 |
| 手机和平板电脑 | 页面可以加载,但游戏内编辑器、RTS 摄像机和示例单位都是为鼠标和键盘设计的。不支持触摸输入。本包的模板会在移动设备上显示一条提示。 |
| 渲染 | 地形、植被、水体、装饰物和战争迷雾的外观与桌面平台相同。草存储在浮点纹理中,而不是结构化缓冲区中(参见渲染与性能)。 |
| 游戏内编辑器 | 所有工具、地图浏览器和试玩功能均可使用。地图保存在浏览器的存储空间中,可以下载和导入(参见浏览器中的地图)。地图尺寸和撤销内存的上限更低(参见浏览器中的游戏内编辑器)。 |
| 线程 | Web 构建没有工作线程,因此所有作业都在主线程上依次运行。 |
全窗口模板
Web 构建外围的页面来自 Web 模板。在桌面浏览器中,Unity 自带的 Default 和 Minimal 模板会把游戏显示在一个固定大小的框中(除非你修改,否则为 960 × 600 像素)。游戏内编辑器、HUD 和小地图会随该框缩放,因此其中的文字会变得难以阅读。为此,本包自带了一个模板:
- 游戏会填满浏览器窗口(下方只留一条显示游戏名称和全屏按钮的细窄页脚),并随窗口调整大小。在手机和平板电脑上,游戏会填满整个页面。
- 保留了加载进度条(带百分比)、全屏按钮和 Unity 的消息横幅。
- 在高分辨率屏幕上,渲染分辨率上限为 CSS 像素尺寸的 1.5 倍,以保持较高的帧率。要调整此值,请修改已安装模板的
index.html中的maxDevicePixelRatio(值越低越快,最低为 1;值越高画面越清晰)。 - 游戏保存的文件(例如游戏内编辑器的地图)会立即写入浏览器的存储空间。
- 页面不会向其他服务器发出任何请求;页面所用的图标都内嵌在页面中。
安装方法:
- 选择 ,或者在 Project Validator 的 Web template fills the browser window 卡片上单击 Install Template。
- 模板会被复制到
Assets/WebGLTemplates/RTSTerrainStudio(Unity 只会在Assets/WebGLTemplates中查找模板),并在 中被选中。 - 如果模板已经安装,菜单命令会询问你是 Keep and Select(保留并选中你的副本),还是用本包的版本 Replace(替换)它。该副本属于你的项目:你可以随意编辑,本包绝不会在未经询问的情况下覆盖它。
构建配置文件:构建配置文件可以覆盖 Player Settings,此时它会保留自己的模板选择。安装模板之前,请先将你的 Web 构建配置文件设为当前配置文件,这样该设置才会写入该配置文件。如果你是在其他平台处于活动状态时安装的模板,请在 Web 构建配置文件的 Player Settings 覆盖中手动选择模板 RTSTerrainStudio。
该模板基于 Unity 的 Default 模板,并且只使用 Unity 自带的模板变量,因此 Player Settings 中不会出现任何新内容。在手机和平板电脑上,它会显示提示 This game is best played on a computer with a mouse and keyboard. 几秒钟;如果你的游戏支持触摸操作,请从 index.html 中删除这一行。
在编辑器脚本(例如构建脚本)中,RTSWebGLTemplateInstaller.InstallAndSelect(false) 会在不弹出对话框的情况下安装模板,保留已有的副本并将其选中。IsInstalled 和 IsSelected 用于报告当前状态。
Project Validator 中的 Web 检查
当 Web 为当前构建平台时,验证器会显示一个 WebGL 组,其中包含以下三项检查。
| 检查 | 修复 | 检查内容 |
|---|---|---|
| Web quality levels have Depth and Opaque Texture | Enable Both | 在 的 Web 列中勾选的质量级别(Web 的默认级别列在最前面),以及每个级别渲染时所用的 URP 资源。编辑器通常使用桌面平台的质量级别进行渲染,因此 Web 构建中可能出现你在编辑器中从未见过的、效果更简单的水面。每张纹理每帧都需要一次全屏复制。通用检查 Depth and Opaque Texture enabled 可能会报告同一个资源;Fix All 会同时处理这两项。 |
| MSAA is off for Web builds | Turn MSAA Off | 如果某个 Web 质量级别所用的 URP 资源同时启用了 MSAA 和 Depth Texture 或 Opaque Texture,则显示错误。URP 无法在 WebGL 2 上渲染这种组合:画布会一直保持黑屏,浏览器控制台中会显示 Render Graph Execution error。修复操作会在这些资源中关闭 MSAA。与之共用同一资源的桌面质量级别也会随之失去 MSAA,因此应为 Web 平台设置一个使用独立 URP 资源的质量级别。 |
| Web template fills the browser window | Install Template / Reinstall | 使用 Unity 的 Default 和 Minimal 模板时显示警告;选中了本包的模板但其文件夹缺失时显示错误(构建会失败);使用本包的模板时显示 OK。PWA 模板同样会填满窗口,也显示 OK,但它的 Service Worker 会缓存构建版本:每次上传时都要提高 。其他任何自定义模板都会显示一条信息,但不提供修复。 |
Rendering 类别中的 Fog kept in player builds 检查适用于所有平台,对 Web 构建同样重要(参见Player 构建中的雾效)。
推荐的构建设置
| 设置项 | 位置 | 建议 |
|---|---|---|
| Texture Compression | 你的 Web 构建配置文件 | 面向桌面浏览器时使用 Use Player Settings 或 DXT。面向移动浏览器的构建请选择 ASTC 或 ETC2。图块集的纹理数组会遵循此设置(DXT5、ASTC 6×6 或 ETC2 RGBA8),并在构建之前重建。如果浏览器无法使用所选格式,Unity 会为其解压纹理,这大约需要四倍的内存。 |
| Maximum Memory Size | 保持默认值 2048 MB。游戏内编辑器在浏览器中的各项限制都是针对这个堆大小设计的。 | |
| Compression Format | Brotli 的下载体积最小,但只能通过 HTTPS 使用。Gzip 也可以通过 HTTP 使用。两者都需要服务器设置正确的响应头(参见发布到网站)。 | |
| Decompression Fallback | 如果你无法设置服务器的响应头(例如在游戏门户网站上),请开启此项。这样游戏在任何地方都能加载,只是启动时间会稍长一些。 | |
| Data Caching | 开启。浏览器会保留已下载的游戏数据,下次访问时启动更快。 | |
| Enable Exceptions | 发布版本使用 Explicitly Thrown Exceptions Only(默认值)。在此模式下,空引用不会作为异常报告,因此排查错误时请使用设置为 Full With Stacktrace 的开发版本。 | |
| Managed Stripping Level | 本包适用于任何级别。级别越高,下载体积越小;此时自定义编辑工具需要添加 [Preserve](参见代码剥离与自定义工具)。 | |
| 质量级别和 URP 资源 | 对于不在浏览器中使用的桌面质量级别,取消勾选其 Web 列,以免它们的 URP 资源为构建增加着色器变体。在 Web 所用的 URP 资源中,除非场景需要,否则请保持光照 Cookie 和反射探针混合处于关闭状态:WebGL 2.0 每次绘制最多只允许使用 16 张纹理(参见移动平台注意事项)。该资源中的 Anti Aliasing (MSAA) 也要保持关闭:它与 Depth Texture 或 Opaque Texture 同时启用时,URP 无法在 WebGL 2 上渲染。 | |
| Web Template | RTSTerrainStudio(参见全窗口模板)。 |
在你的电脑上测试 Web 构建
- Build And Run 是最简单的方法。Unity 会启动一个小型本地 Web 服务器,并在你的默认浏览器中打开游戏。
- 双击构建出的
index.html是行不通的。浏览器不会运行来自file://地址的 Web 构建,页面只会显示一条错误消息。 - 如果之后要再次打开已完成的构建文件夹,可以用任意本地 Web 服务器托管它。不设置压缩响应头的简易服务器只能托管开启了 Decompression Fallback 或禁用了压缩的构建。
- 在目标浏览器中把以下按键测试一遍:F1、F5、Ctrl+S、Alt+N,以及全屏模式下的 Esc(参见浏览器中的游戏内编辑器)。
发布到网站
- 上传整个构建文件夹:
index.html、Build和TemplateData。 - 压缩构建需要为
Build中的文件设置两个响应头:Content-Encoding: br(Brotli)或Content-Encoding: gzip(Gzip),以及正确的Content-Type,例如.wasm文件对应application/wasm。缺少Content-Encoding时页面会显示错误消息,Content-Type错误则会使启动变慢。如果你无法配置服务器,请开启 Decompression Fallback。 - itch.io 等游戏门户网站会在
iframe中显示游戏。请上传一个 ZIP 文件,并将index.html放在其顶层。带沙盒的iframe需要allow-downloads才能使用 Download Map,需要allow-modals才能显示 Leave site? 询问。请在实际页面上检查这两项。 - 保存的地图归属于网站。浏览器按网站地址(协议、主机和端口)保存地图。向同一网站重新上传时,地图会保留;同一款游戏放在其他网站上时,则从没有地图的状态开始。
- Safari 可能会删除七天内未使用的网站所存储的数据,而且 iPhone 和 iPad 上的 Safari 会忽略 Leave site? 询问。请提醒玩家下载想要保留的地图。
- 在
iframe中,只有玩家在游戏区域内单击之后,游戏才能接收按键,并且 Esc 总是会退出浏览器全屏(见下文)。
浏览器中的地图
在浏览器中,Application.persistentDataPath 是一个位于内存中的文件系统。只有在发出请求时,它才会被复制到浏览器的数据库(IndexedDB)中。本包的模板会在每次更改后发出此请求。使用其他模板时,本包会在每次保存、删除和导入后自行发出请求,因此重新加载页面后,已保存的地图依然存在。你自己的代码可以通过 RTSWebFileSystem.Flush() 实现同样的效果(参见Web 构建的脚本 API)。
persistentDataPath 取决于页面地址,如果以其他地址重新上传(游戏门户网站通常每次上传都会使用新地址),游戏就会从没有地图的状态开始。因此,在 Web 构建中,地图保存在 RTSMapSerializer.StorageRoot 中,这是同一浏览器存储空间中一个以 Player Settings 的公司名称和产品名称命名的文件夹。在其他所有平台上,StorageRoot 就是 persistentDataPath。游戏内编辑器的地图文件夹是其中的 Maps。
玩家无法以文件夹的形式打开这个存储空间,因此由游戏内编辑器自己负责在浏览器和电脑之间转移地图:
| 命令 | 位置 | 作用 |
|---|---|---|
| Download Map | File 菜单 | 让浏览器将当前地图(包括未保存的更改)下载为 .rtsmap 文件(通常保存到“下载”文件夹)。 |
| Import Map... | File 菜单 | 打开浏览器的文件选择器。所选的 .rtsmap 文件会被添加到此浏览器中保存的地图里,并在常规的未保存更改询问之后打开。 |
| Import... | 地图浏览器(Open) | 与 Import Map 相同,位于文件夹行旁边。 |
| Download | 地图浏览器(Open) | 下载所选的地图文件。 |
| 拖放 | 游戏画布 | 拖放到游戏上的 .rtsmap 文件会被导入并打开。其他文件会被拒绝并显示一条消息,这样浏览器就不会离开当前页面去显示它们。 |
- 文件在存储之前会经过完整检查,最大可接受 128 MB 的文件。
- 导入永远不会替换其他地图。再次导入同一个文件时会复用已有的地图;同名的不同文件会保存为 Name (2)、Name (3),依此类推。地图打开时会写入预览图。
- 地图浏览器会显示 Saved in this browser. Use Download to keep a copy on your computer.,而不是文件夹路径。Show Maps Folder 和 Quit 在浏览器中不存在。
- 删除该网站的浏览数据也会删除已保存的地图。
浏览器中的游戏内编辑器
在有未保存更改时关闭标签页
当地图有未保存的更改且 Confirm Quit With Unsaved Changes 处于开启状态时,浏览器会在关闭或重新加载标签页之前询问 Leave site?。该询问的文字由浏览器决定。使用 Unity 自带的模板时(这些模板不会立即存储文件),如果某次保存仍在写入浏览器的存储空间,浏览器也会发出询问,与此选项无关。
关闭编辑器后重新打开
在浏览器中,功能键常常是媒体键(在 Mac 和许多笔记本电脑上),而手机则根本没有功能键。因此,当玩家使用关闭按钮(×)关闭编辑器时,左下角会出现一个 Open Map Editor (F2) 按钮,用于重新打开编辑器。没有键盘时,或者 Toggle Key 为 None 时,按钮文字为 Open Map Editor。该按钮只会在使用关闭按钮后出现;如果是你的游戏代码隐藏了编辑器,则由你的游戏决定如何让它重新出现。在浏览器中,即使 Toggle Key 为 None,关闭按钮也可用。
键盘
- New Map 的快捷键是 Alt+N。浏览器把 Ctrl+N 留给新窗口,永远不会将其传递给页面。Alt+N 在桌面平台上同样可用。Ctrl+T 和 Ctrl+W 也永远不会到达页面。
- 浏览器快捷键会被拦截。编辑器启用期间,浏览器不会响应 F1 到 F10 以及 Ctrl(或 Cmd)+ S、O、D 和 A,因此这些按键可以用作编辑器快捷键。F11、F12、Ctrl+F5、Ctrl+R、复制、粘贴以及在页面文本框中输入的按键仍由浏览器处理。
- F5 用于开始和结束试玩,因此你无需依赖 Esc。
- 全屏模式下的 Esc:浏览器使用 Esc 退出全屏。在 Chrome 和 Edge 中,如果页面是安全页面(HTTPS 或
localhost)且不在iframe中,短按 Esc 会传递给游戏,长按 Esc 则退出全屏。在其他情况下,第一次按 Esc 就会退出全屏。替代方法:按 F5 或单击 Back to Editor 结束试玩,单击 × 按钮关闭对话框,按 Ctrl+Z 撤销一次笔触。帮助窗口(F1)中也有这些说明。 - 键盘布局:字母快捷键(Ctrl+Z、G、P 等)以键帽上印的字母为准,在德语(QWERTZ)和法语(AZERTY)键盘上也是如此。笔刷按键以输入的字符为准,因此在需要按 AltGr+8 输入 [ 的键盘上,它同样有效。桌面平台上也是如此。
- 游戏玩法演示:在大多数浏览器中,Ctrl+1 到 Ctrl+9(编队)会切换浏览器标签页。网页游戏请选用其他按键。
限制
地图、地图的纹理和撤销历史共享一个最大 2 GB 的内存堆(Maximum Memory Size 的默认值),而且这个堆永远不会缩小。因此,编辑器在浏览器中使用更低的限制:
| 限制 | 浏览器 | 其他平台 |
|---|---|---|
| New Map 和 Resize 中的最大地图尺寸 | 每边的单元格数为 2048 ÷ Splat 分辨率:默认 Splat 分辨率为 4 时为 512,为 8 时为 256。更大的预设不会显示,对话框会提示 In the browser, maps can be up to N x N cells. Resize 会保留从文件打开的较大地图的尺寸,但不会再增大它。 | 1024 |
| 撤销内存 | 最多 192 MB。如果 Undo Memory MB 的值更低,则保留该值。 | Undo Memory MB(默认 512) |
| 导入的地图文件 | 128 MB | 无导入命令 |
其他差异
- 以 CSS 像素计算,UI 永远不会缩小到 85% 以下,因此在高分辨率屏幕和小窗口中也能保持清晰可读。
- 当鼠标离开页面或游戏所在的
iframe时,编辑器摄像机和示例 RTS 摄像机的边缘滚屏会停止。
渲染与性能
- 草:WebGL 2.0 没有结构化缓冲区,细节渲染器改为将每个地块(patch)的实例存储在浮点纹理中(每个实例占两个纹素)。它会在运行时自动选择这一路径,草的外观保持不变。
- 地形:前向渲染 Pass 使用了 WebGL 2.0 每次绘制所允许的 16 张纹理中的 10 张。请让 Web 构建的 URP 资源保持精简(参见移动平台注意事项)。
- 单线程:由于没有工作线程,针对整张地图的操作耗时会明显长于桌面平台,并且期间页面不会响应,包括 Generate、New Map、Resize、打开大型地图,以及 Auto Paint 的 Apply to Whole Map。每边 128 到 256 个单元格的地图仍然可以流畅使用。
- 纹理数组:使用以 Web 构建格式预先构建的数组(参见推荐的构建设置)。Player 会释放已构建数组的 CPU 副本,这在页面有限的内存中非常重要。
- 渲染分辨率:模板将像素比上限设为 1.5;降低该值可以提高帧率。
代码剥离与自定义工具
编辑工具是通过反射(RTSToolRegistry)查找和创建的,因此没有任何代码直接引用它们。当 Managed Stripping Level 高于 Minimal 时(为了减小 Web 和 IL2CPP 构建的体积,这种设置很常见),Unity 无法看出这些工具被使用了,因而会将其移除。本包的 Runtime/link.xml 会保留 RTSTerrainStudio 命名空间中所有名称以 Tool 结尾的类,因此内置工具始终会被保留。
请为你自己的工具类添加 [UnityEngine.Scripting.Preserve](它会保留该类及其无参构造函数),或者在项目的 link.xml 文件中列出这些类。如果 Player 完全找不到任何工具,会在日志中记录 No editing tools were found in this build.
using RTSTerrainStudio;
using UnityEngine.Scripting;
// [Preserve] keeps the tool in builds with a high Managed Stripping Level.
[Preserve]
public sealed class CraterTool : RTSTool
{
// ...
}
Web 构建的脚本 API
| 成员 | 说明 |
|---|---|
RTSWebFileSystem.IsSupported | 在 WebGL Player 中为 true。在其他平台和 Unity 编辑器中,该类不执行任何操作。 |
RTSWebFileSystem.Flush() | 将页面的文件系统复制到浏览器的存储空间,使通过 System.IO 写入或删除的文件在重新加载页面后依然有效。同一帧内的所有调用会合并为一次写入。RTSMapSerializer.SaveToFile 会自动为你调用它。 |
RTSWebFileSystem.DownloadFile(fileName, data, mimeType) | 让浏览器将字节数据作为文件下载。在其他平台上或浏览器拒绝下载时返回 false。 |
RTSMapSerializer.StorageRoot | 用于在会话之间保留文件的文件夹:即 Application.persistentDataPath;在 Web 构建中则是浏览器存储空间中一个不依赖页面地址的文件夹。 |
RTSMapSerializer.DefaultMapsFolder | StorageRoot 中的 Maps 文件夹。 |
RTSRuntimeEditor.RunsInBrowser | 在 Web 构建中为 true,此时编辑器使用浏览器中的限制,并使用 Alt+N 作为 New Map 的快捷键。 |
RTSRuntimeEditor.StoresMapsInBrowser | 地图保存在浏览器的存储空间中时(WebGL Player)为 true。 |
RTSRuntimeEditor.HasReliableFunctionKeys | 在网页浏览器和移动平台上为 false,此时关闭按钮会显示重新打开按钮。 |
editor.DownloadMap()、editor.ImportMap() | 对应 File 菜单中的命令。浏览器只会在响应单击或按键时显示文件选择器,因此请在单击处理程序中调用 ImportMap。 |
RTSInput.SetBrowserShortcutGuard(bool) | 拦截浏览器对 F1 到 F10 以及 Ctrl+S/O/D/A 的默认操作,并让全屏模式下短按的 Esc 传递给游戏(Chrome、Edge)。每次以 true 调用都需要一次对应的 false 调用。游戏内编辑器和 RTSDemoHUD 在启用期间会开启它。在其他平台上不执行任何操作。 |
RTSInput.GetKeyDownByLabel(RTSKey) | 与 GetKeyDown 类似,但针对的是在玩家键盘布局上显示该字母的按键。适用于 Ctrl+Z 等字母快捷键。 |
RTSInput.GetCharacterDown(char) | 当本帧输入了该字符时为 true,与键盘布局无关。可用于 [ ] - + 等符号键。在获得焦点的文本框中输入的字符也会到达这里,因此请先检查 UI 焦点。 |
RTSInput.PointerInsideScreen | 指针位于游戏窗口内时为 true。在 Web 构建中,鼠标还必须仍在页面上方。 |
using System.IO;
using RTSTerrainStudio;
public static class MyGameFiles
{
// Kept between sessions on every platform; in Web builds in the storage of the browser.
static string SettingsPath => Path.Combine(RTSMapSerializer.StorageRoot, "settings.json");
public static void SaveSettings(string json)
{
Directory.CreateDirectory(RTSMapSerializer.StorageRoot);
File.WriteAllText(SettingsPath, json);
// Web builds: write the change to the browser storage, so it survives a reload.
// Does nothing on other platforms.
RTSWebFileSystem.Flush();
}
/// <summary>Lets the player keep a copy of the current map on the computer (Web builds only).</summary>
public static bool DownloadMap(RTSTerrain terrain)
{
if (!RTSWebFileSystem.IsSupported || terrain.Map == null) return false;
byte[] bytes = RTSMapSerializer.Serialize(terrain.Map);
return RTSWebFileSystem.DownloadFile("My Map" + RTSMapSerializer.FileExtension, bytes);
}
}
核心概念
本章介绍 RTS Terrain Studio 如何看待一张地图:存储了哪些数据、以什么单位计量、可见的地表如何根据这些数据构建,以及哪些资源(图块集、库、地图文件)属于地图。制作第一张地图之前无需阅读本章;但当你想知道某些东西为什么会这样表现,或者开始针对地图数据编写代码时,可以回来阅读本章。
简而言之:
- 地图是由正方形单元格组成的栅格。高度、悬崖层级和水体都存储在单元格的角点(顶点)上。
- 悬崖层级是 0 到 15 之间的整数。相邻角点的层级不同时,就会出现垂直崖壁,除非该单元格的四个角点都被标记为坡道。
- 地面纹理、草的密度和通行覆盖属于栅格层,每个单元格包含多个纹素。
- 树木、装饰物、区域、出生点和标记点是使用地图局部坐标的对象列表。
- 外观来自图块集(地面、悬崖和水体纹理);对象来自植被库和装饰物库。地图只存储索引和 ID,因此你无需改动地图就能更换外观。
- 所有内容都保存在一个紧凑的二进制
.rtsmap文件中,Unity 编辑器和游戏内编辑器都使用该文件。
地图模型
所有地图数据都存放在 MapData 类(命名空间 RTSTerrainStudio)的一个对象中。每个
RTSTerrain 组件拥有一个 MapData(terrain.Map),负责将其渲染为区块网格,持续更新由它派生出的通行图,并回答诸如“这里的地面有多高?”之类的查询。
单元格与顶点
地图是由 Width × Height 个单元格组成的栅格。每个单元格的四个角上各有一个顶点,相邻单元格共享这些角点。因此,一张 4 × 2 个单元格的地图有 5 × 3 个顶点:
z
2 v10 ---- v11 ---- v12 ---- v13 ---- v14
| cell | cell | cell | cell |
| (0,1) | (1,1) | (2,1) | (3,1) |
1 v5 ----- v6 ----- v7 ----- v8 ----- v9
| cell | cell | cell | cell |
| (0,0) | (1,0) | (2,0) | (3,0) |
0 v0 ----- v1 ----- v2 ----- v3 ----- v4
x=0 1 2 3 4
vertex index = z * VertexCountX + x (VertexCountX = Width + 1)
- 顶点层(精细高度、悬崖层级、标记、悬崖类型、水面高度)包含
(Width + 1) × (Height + 1)个条目,按行存储。用map.VertexIndex(x, z)获取索引。 - 单元格尺寸(
map.CellSize,默认 2 m)是单个单元格在世界空间中的尺寸。一张 128 × 128、单元格尺寸为 2 m 的地图覆盖 256 × 256 m(map.WorldSize)。 - 一个位置所属的单元格为
floor(localXZ / CellSize);map.LocalToCell(localXZ)会完成这一计算,并将结果限制在地图范围内。 - 渲染时,单元格按区块分组(每边 16、32 或 64 个单元格,由
RTSTerrain组件上的 Chunk Size 设置项决定,默认 32)。只有编辑涉及的区块才会重建。区块大小不影响地图数据。
地图尺寸
地图尺寸在创建地图时确定(通过 New Map Wizard、游戏内的 New Map 对话框,或在代码中通过
MapCreationSettings)。超出有效范围的值会由 MapCreationSettings.Validate() 修正。
| 设置项 | 默认值 | 有效值 | 说明 |
|---|---|---|---|
width、height | 128 | 16–1024,取整为 16 的倍数 | 沿 X 轴和 Z 轴方向的单元格数。New Map Wizard 提供从 Tiny (64) 到 Epic XL (512) 的预设。 |
cellSize | 2 | ≥ 0.1 | 单个单元格的世界尺寸,以米为单位。 |
cliffLevelHeight | 2 | ≥ 0.05 | 一个悬崖层级的世界高度,以米为单位。 |
baseCliffLevel | 2 | 0–15 | 每个顶点的初始悬崖层级。层级 2 为向下挖出湖泊和河流留出了空间。 |
baseGroundLayer | 0 | 0–15 | 初始时覆盖整张地图的地面图层。 |
splatResolution | 4 | 1, 2, 4, 8 | 每个单元格边长上的地面纹理(Splat)纹素数。 |
detailResolution | 2 | 1, 2, 4 | 每个单元格边长上的细节(草)密度纹素数。 |
pathingResolution | 2 | 1, 2, 4 | 每个地图单元格边长上的通行单元格数。 |
name | "New Map" | 任意文本 | 地图名称(存储在地图信息中)。 |
不是 2 的幂的分辨率会向上取整(3 变为 4)。地图一经创建,单元格尺寸、层级高度和三种栅格分辨率就固定不变。Resize 功能(RTSTerrain.ResizeMap)只改变单元格数量:它会保留现有内容,按偏移量平移内容,并用选定的悬崖层级和地面图层填充新增区域。
MapData.Resize(newWidth, newHeight, offsetCells, fillCliffLevel, fillGroundLayer) 返回一张新地图(原地图保持不变)。位于新边界之外的树木、装饰物和出生点会被移除。标记点和区域会随之平移并保留,即使最终落在地图之外也是如此。尺寸会被限制在 16–1024(MapData.MinSize–MaxSize)之间,但从代码调用时不会取整为 16 的倍数。RTSTerrain.ResizeMap 会替换地图(触发 MapLoaded),将其标记为未保存,并清除各编辑会话的撤销历史。
地图包含的内容
每一项数据在 MapLayers 枚举中都有对应的标记。这些标记用于变更通知、撤销记录和序列化。
| 数据 | 存储单位 | MapData 成员 | 类型 | MapLayers 标记 |
|---|---|---|---|---|
| 精细高度 | 顶点 | Heights | NativeArray<float>,米 | Heights |
| 悬崖层级 | 顶点 | CliffLevels | NativeArray<byte>,0–15 | CliffLevels |
| 顶点标记(坡道、水体) | 顶点 | VertexFlags | NativeArray<byte>,元素为 VertexFlags | VertexFlags |
| 悬崖类型 | 顶点 | CliffTypes | NativeArray<byte>,0–3 | CliffTypes |
| 水面高度 | 顶点 | WaterHeights | NativeArray<float>,地图局部 Y 坐标 | Water |
| 地面图层权重 | Splat 纹素 | SplatMaps[0..3] | NativeArray<Color32> × 4 | Splat |
| 草 / 植物密度 | 细节纹素 | DetailLayers | 每个原型一个 DetailLayer(每个纹素一个字节) | Details |
| 树木 | 实例 | Trees、TreePrototypeIds | List<TreeInstanceData> | Trees |
| 装饰物(doodad) | 实例 | Decor、DecorPrototypeIds | List<DecorInstanceData> | Decor |
| 手动通行覆盖 | 通行单元格 | PathingOverrides | NativeArray<byte>,元素为 PathingOverride | PathingOverrides |
| 区域 | 列表 | Regions | List<MapRegion> | Regions |
| 出生点 | 列表 | StartLocations | List<StartLocation> | StartLocations |
| 标记点 | 列表 | Markers | List<MapMarker> | Markers |
| 地图信息 | 地图 | Info | MapInfo | Info |
| 环境(光照) | 地图 | Environment | EnvironmentSettings | Environment |
有两个组合标记很实用:MapLayers.Geometry(高度、层级、顶点标记、悬崖类型)和
MapLayers.PathingInputs(所有影响通行图的数据)。MapLayers.All 涵盖全部数据。
通行图(terrain.Pathing,类 PathingMap)是派生数据:它根据上述各层计算得出,从不保存。参见通行图。
VertexFlags.User0 和 VertexFlags.User1 保留给你自己的代码使用。本包从不使用它们,而且它们会随地图一起保存,因此你无需自定义文件段,就能标记特殊顶点(例如“野怪刷新区域”)。
MapData 持有原生内存。你自行创建或加载的地图(MapData.Create、RTSMapSerializer.LoadFromFile、RTSMapAsset.LoadMap、map.Clone())必须调用
Dispose() 释放,除非你通过 terrain.SetMap(map) 将其交给地形:此后该地图归地形所有,地形会在地图被替换或组件被禁用时将其释放。
悬崖层级、精细高度与悬崖类型
经典 RTS 地形有两种高度:大幅度的离散台阶(悬崖)和细小平滑的起伏(丘陵)。RTS Terrain Studio 在每个顶点上分别存储这两种高度。
悬崖层级
- 每个顶点都有一个悬崖层级,取值范围为 0 到 15(
MapData.MaxCliffLevel= 15)。 - 每个层级高
CliffLevelHeight米(默认 2 m)。 - 新地图从层级 2 开始,因此地面以下有两个层级可用于湖泊、河流和峡谷,地面以上有十三个层级可用于高地。
- 使用悬崖工具编辑层级:Raise / Lower Level、Set Level、Ramp、Cliff Type 和 Clean Cliffs。
- 战争迷雾使用悬崖层级判断视线:单位能看到自身所在层级及更低的层级,更高的地面会遮挡其视线。
精细高度
在层级之上,每个顶点还有一个以米为单位的精细高度(Heights),可正可负。高度工具(Raise / Lower、Smooth、Flatten、Set Height、Noise、Terrace、Erode)只改变精细高度,因此你可以在高地上堆出丘陵,而不会让它们变成悬崖。顶点的地面高度为:
vertex height = CliffLevels[i] × CliffLevelHeight + Heights[i] (map-local Y, metres)
示例:层级为 3、层级高度为 2 m、精细高度为 0.4 m 时,顶点位于地图原点上方 6.4 m 处。map.GetVertexHeight(x, z) 和 map.GetVertexPosition(x, z) 可以替你完成这一计算。
悬崖类型
一个图块集最多可以包含 4 个悬崖图层(例如“Rock”和“Grassy Cliff”)。每个顶点都记录了要使用其中哪一个(CliffTypes,0–3,用 Cliff Type 工具绘制)。崖壁使用其所在单元格最高角点的悬崖类型。如果某个悬崖类型索引在图块集中没有对应的图层,崖壁会回退使用悬崖图层 0。
表面的构建方式(参考表面)
结构体 MapGeometryView 定义了地形唯一真实的表面。区块网格、游戏玩法查询(GetHeight、Raycast……)、通行图和战争迷雾都遵循同一套规则,因此你看到的表面就是单位实际行走的表面。每个单元格按其四个角点分类:
| 单元格种类 | 条件 | 表面 |
|---|---|---|
| 平滑单元格 | 四个角点的悬崖层级都相同。 | 由四个角点高度构成的双线性曲面片。 |
| 坡道单元格 | 四个角点都带有 VertexFlags.Ramp 标记(层级可以不同)。 | 双线性曲面片:各层级平滑过渡为斜坡。参见坡道。 |
| 悬崖单元格 | 角点层级不同,且该单元格不是坡道单元格。 | 平坦台阶,由沿圆润轮廓线分布的垂直崖壁隔开。 |
圆润的轮廓线。在悬崖单元格内,对于每个层级阈值 t,指示值“角点层级 ≥ t”(取 1 或 0)会在整个单元格范围内做双线性插值。一个点属于插值后指示值至少为 0.5 的最高层级(MapGeometryView.LevelAt)。这是一种 Marching Squares(移动方块)规则:单个抬高的角点会形成四分之一圆而不是对角线,长悬崖线则保持笔直。崖壁本身在轮廓线处是垂直的。精细高度始终以双线性方式插值并叠加在上面,在悬崖单元格内也是如此。
渲染出的崖壁外观可在 RTSTerrain 组件(Settings)上调整:
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Surface Subdivisions | 2 | 平滑地面上每个单元格边长的四边形数(1 = 经典 RTS 外观,2–4 = 更平滑的丘陵)。 |
| Cliff Subdivisions | 4 | 在悬崖单元格内描绘圆润轮廓线时使用的子栅格分辨率。 |
| Cliff Noise | 0.18 | 崖壁顶点的随机水平偏移(以单元格尺寸的比例计),让岩石外观更自然。 |
| Cliff Noise Scale | 1.6 | 悬崖噪声的缩放,以世界单位计。 |
| Cliff Slant | 0.12 | 崖壁顶边向后收进的距离(以单元格尺寸的比例计),使崖壁略微倾斜。 |
| Cliff Wall Segments | 3 | 崖壁几何体在每个悬崖层级上的垂直分段数。 |
用 map.GetGeometryView() 获取视图。它是由原生数组组成的结构体,因此也可以传入 Burst 作业。所有成员都接受地图局部坐标:
| 成员 | 说明 |
|---|---|
SampleHeight(localXZ) | 地面高度(地图局部 Y 坐标)。 |
SampleHeightInCell(cell, uv)、SampleLevelHeightInCell(cell, uv) | 同上,但在已知单元格内采样(uv 0–1),分别包含或不包含精细高度。 |
SampleNormal(localXZ)、SampleSlope(localXZ) | 表面法线;以度为单位的坡度。 |
SampleCliffLevel(localXZ) | 某位置的悬崖层级。 |
SampleWater(localXZ, out waterHeight)、SampleWaterDepth(localXZ) | 是否有水及其水面高度;水深,无水时为 0。 |
IsCliffCell(cell)、IsRampCell(cell) | 对单元格分类(见上表)。 |
LocateCell(localXZ, out cell, out uv) | 某位置所在的单元格,以及该位置在单元格内的 0–1 坐标。 |
Raycast(origin, direction, maxDistance, out hitPoint, out hitDistance) | 针对参考表面的解析射线检测,无需碰撞体。 |
static LevelAt(l00, l10, l01, l11, uv)、static Bilerp(v00, v10, v01, v11, uv) | 悬崖单元格内的层级规则,以及双线性插值。 |
width、height、cellSize、cliffLevelHeight、WorldSize | 以单元格计的地图尺寸、单元格尺寸、层级高度和世界尺寸。 |
Cliff Noise 和 Cliff Slant 只会让渲染出的崖壁顶点偏移单元格尺寸的一小部分。查询使用的是精确轮廓线,因此紧贴悬崖边缘处,可见的岩石与参考表面可能略有差异。通行图通过其 Cliff Margin 设置项来弥补这一点:它会沿每面崖壁保留一条狭窄的不可行走带。
坡道
坡道让单位可以从一个悬崖层级走到相邻的层级。规则很简单:
当单元格的四个角点都带有坡道标记时,该单元格就是坡道。哪怕只有一个角点缺少该标记,该单元格仍是带垂直崖壁的悬崖单元格。
- 该标记是每个顶点上的
VertexFlags.Ramp。Ramp 工具(IDcliff.ramp)用于绘制此标记;按住 Shift 可将其移除。 - 坡道单元格是在其角点高度(层级加精细高度)之间的平滑双线性斜坡。
- 坡道最适合跨越恰好一个层级。更陡的坡道同样允许,但状态栏会对跨越多个层级的坡道单元格发出警告。
- 通行:对于有效的单层级坡道,只根据其精细高度形成的坡度检测是否可行走(它所跨越的层级高差不会让它被判定为“过陡”)。坡道永远不可建造。
- 在坡道上调用
GetCliffLevel会返回插值后取整的层级:下半部分返回较低层级,上半部分返回较高层级。
- 用 Raise / Lower Level 将一块高地抬高一个层级。
- 选择 Ramp 工具,在需要坡道的位置横跨悬崖边缘绘制。笔刷至少要覆盖崖壁两侧的顶点,确保中间单元格的每个角点都带有标记。
- 崖壁消失,出现一道斜坡。沿边缘绘制更多顶点即可加宽坡道。
地面图层与 Splat 贴图
地面纹理绘制在 Splat 贴图中:共四张 RGBA 纹理(MapData.SplatMapCount = 4),每张有
SplatWidth × SplatHeight 个纹素。16 个通道各自存储图块集中一个地面图层的权重:
| Splat 贴图 | R | G | B | A |
|---|---|---|---|---|
SplatMaps[0] | 图层 0 | 图层 1 | 图层 2 | 图层 3 |
SplatMaps[1] | 图层 4 | 图层 5 | 图层 6 | 图层 7 |
SplatMaps[2] | 图层 8 | 图层 9 | 图层 10 | 图层 11 |
SplatMaps[3] | 图层 12 | 图层 13 | 图层 14 | 图层 15 |
- 图层 i 存储在贴图
i / 4的通道i % 4中。这就是图块集恰好有 16 个地面图层的原因:4 张贴图 × 4 个通道。 - 同一纹素在全部 16 个通道上的权重之和始终为 255。所有绘制工具都会维持这一不变量;写入 Splat 数据的代码也必须维持它。
- Splat 分辨率是每个单元格边长上的纹素数(默认 4)。单元格尺寸为 2 m 时,即每 0.5 m 一个纹素。
- 纹素 (x, z) 的中心在地图局部 XZ 坐标中位于
((x + 0.5), (z + 0.5)) × CellSize / SplatResolution。同一公式也适用于细节纹素和通行纹素,只需换用它们各自的分辨率。 - 辅助方法:
map.SplatIndex(x, z)、map.GetSplatWeight(texelIndex, layer)、map.GetDominantLayer(texelIndex)、MapData.GetChannel/SetChannel以及map.FillSplat(layer)。
地图按索引(0–15)而不是按名称存储地面图层。在图块集中重新排序或删除地面图层,会改变已绘制地图的显示内容。如果已有地图在使用该图块集,请将新图层添加到末尾。
using RTSTerrainStudio;
using Unity.Mathematics;
using UnityEngine;
public static class SplatExample
{
/// <summary>Paints one map cell 100 % with a ground layer (like the Tile Paint tool, without undo).</summary>
public static void PaintCell(RTSTerrain terrain, int2 cell, int layer)
{
MapData map = terrain.Map;
if (map == null || !map.IsValidCell(cell.x, cell.y)) return;
layer = math.clamp(layer, 0, MapData.MaxGroundLayers - 1);
int res = map.SplatResolution;
for (int tz = cell.y * res; tz < (cell.y + 1) * res; tz++)
{
for (int tx = cell.x * res; tx < (cell.x + 1) * res; tx++)
{
int texel = map.SplatIndex(tx, tz);
for (int m = 0; m < MapData.SplatMapCount; m++)
{
// All channels 0, except the one of our layer: the sum stays exactly 255.
var c = new Color32(0, 0, 0, 0);
if (layer / 4 == m) MapData.SetChannel(ref c, layer % 4, 255);
map.SplatMaps[m][texel] = c;
}
}
}
// Tell the terrain which data changed and where (in cells). Textures and pathing update in LateUpdate.
terrain.NotifyChanged(MapLayers.Splat, new RectInt(cell.x, cell.y, 1, 1));
}
}
水体
- 带有
VertexFlags.Water标记的顶点位于水下。其水面高度(地图局部 Y 坐标,单位为米)存储在WaterHeights中;该值仅在设置了此标记的位置才有意义。 - 如果某点所在单元格四个角点的水体标记经双线性插值后的覆盖率至少为 0.5,该点就被视为有水。有水单元格中不带水体标记的角点会使用该单元格内最高的水面高度,因此岸线保持水平。
- 水深 = 水面高度 − 地面高度。水深决定通行情况:水深超过图块集 Shallow Depth Limit(默认 0.9 m)的水为深水(不可行走),较浅的水为浅水(可行走,默认不可建造)。
- 水体工具包括 Paint Water、Erase Water 和 Flood Fill。水体编辑只会发出
MapLayers.Water通知,因此绘制水体时不会重建地形区块。 - 查询方法:
terrain.IsWater(world, out depth)和terrain.TryGetWaterHeight(world, out waterWorldY)。
水体外观(颜色、波浪、泡沫)来自图块集的水体配置;参见水体配置。
细节密度(草和小型植物)
- 细节不是单独的对象。地图上使用的每个细节原型在
map.DetailLayers中都有一个DetailLayer:每个细节纹素一个字节(DetailWidth×DetailHeight,默认每个单元格边长 2 个纹素)。 - 密度 0 表示空,255 表示每个纹素生成 Max Density 个实例(细节原型的一个字段,默认 6)。每个纹素生成
density / 255 × maxDensity个实例;渲染器以确定性方式散布这些实例,因此同一张地图看起来总是一样的。 - 各层通过
prototypeId与植被库匹配。map.GetOrAddDetailLayer(id)按需创建细节层;当该原型未被使用时,map.FindDetailLayer(id)返回 null。 - 用 Paint Details 绘制,用 Erase Details 擦除。
树木与装饰物实例
树木(TreeInstanceData)和装饰物(DecorInstanceData)以普通列表的形式存储。每个实例存储的不是原型名称,而是一个很小的原型索引(palette index),它指向地图上所存库 ID 列表中的某一项:
map.TreePrototypeIds = [ "a3f0…", "77c1…" ] <- ids of TreePrototype entries in the vegetation library map.Trees[n].prototype = 1 <- this tree uses "77c1…"
- 使用
map.GetOrAddTreePrototype(id)/map.GetOrAddDecorPrototype(id)获取某个库 ID 对应的原型索引(如果列表中还没有该 ID,会自动添加)。 - 由于地图通过 ID 引用原型,你可以放心地重命名、重新排序或扩展库。对于库中缺失的原型,植被渲染器会直接跳过,装饰物管理器则将其显示为品红色占位立方体;重新添加一个 ID 相同的原型即可让它恢复。通行方面:原型缺失的树木不会被绘制,但仍会阻挡一个半径为
PathingMapBuilder.DefaultTreeFootprintRadius(0.8 m)× 树木缩放的圆形区域。在原型恢复之前,装饰物占位符不会在通行图中写入占地范围。 - 每个实例在地图内都有一个唯一且稳定的
id。使用map.AllocateInstanceId()获取新 ID。 - 位置采用地图局部坐标。所有旋转都遵循同一约定:以度为单位,俯视时按顺时针方向计算,0 = +Z,90 = +X(与 Unity 的 Y 轴旋转一致)。
position.y 约定
position.y 是高出地面的额外偏移,通常为 0。它不是绝对高度。渲染器每次刷新时都会重新采样地面,因此即使你编辑了树木和装饰物下方的地形,它们也会始终贴合地面。
| 对象 | 渲染高度(地图局部 Y) |
|---|---|
| 树木 | 地面高度 + position.y |
| 装饰物 | 地面高度 + position.y + 原型的 Y Offset |
带有 DecorFlags.KeepHeight 的装饰物 | position.y,视为地图局部空间中的绝对高度(不随地形编辑而变化) |
切勿把原型的 yOffset 复制到 position.y 中,渲染时会再加上它。DecorMath.ComputeLocalPosition 和 DecorMath.ToStoredHeight 用于在存储位置与渲染位置之间转换。
| 字段 | TreeInstanceData | DecorInstanceData |
|---|---|---|
id | uint,在地图内唯一 | uint,在地图内唯一 |
prototype | 指向 TreePrototypeIds 的 ushort 索引 | 指向 DecorPrototypeIds 的 ushort 索引 |
variant | – | byte,0 = 主预制件,1 及以上 = variants 中的条目 |
flags | – | DecorFlags:IgnorePathing、Locked(工具无法选中或擦除它)、KeepHeight |
position | float3,地图局部坐标,y = 偏移 | float3,地图局部坐标,y = 偏移(使用 KeepHeight 时为绝对高度) |
| 旋转 | yaw,以度为单位 | rotation,地图局部空间中的 quaternion |
| 缩放 | scale,统一缩放的 float | scale,float3 |
tint | Color32,与材质颜色相乘 | Color32,与材质颜色相乘 |
using RTSTerrainStudio;
using Unity.Mathematics;
using UnityEngine;
public static class InstanceExample
{
/// <summary>Adds a tree of the first tree prototype of the terrain's vegetation library at a world position.</summary>
public static uint AddTree(RTSTerrain terrain, Vector3 worldPosition, float yawDegrees)
{
MapData map = terrain.Map;
RTSVegetationLibrary library = terrain.VegetationLibrary;
if (map == null || library == null || library.trees.Count == 0) return 0;
Vector3 local = terrain.WorldToLocal(worldPosition);
var tree = new TreeInstanceData
{
id = map.AllocateInstanceId(),
prototype = map.GetOrAddTreePrototype(library.trees[0].id), // palette index of the library id
position = new float3(local.x, 0f, local.z), // y = offset above the ground, normally 0
yaw = yawDegrees, // clockwise from above, 0 = +Z
scale = 1f,
tint = new Color32(255, 255, 255, 255)
};
map.Trees.Add(tree);
// Notify the cell that changed: vegetation, pathing (tree footprint) and fog blockers update.
int2 cell = map.LocalToCell(new float2(local.x, local.z));
terrain.NotifyChanged(MapLayers.Trees, new RectInt(cell.x, cell.y, 1, 1));
return tree.id;
}
/// <summary>Adds a decor object (by library id) standing upright on the ground.</summary>
public static uint AddDecor(RTSTerrain terrain, string decorPrototypeId, Vector3 worldPosition, float yawDegrees)
{
MapData map = terrain.Map;
if (map == null) return 0;
Vector3 local = terrain.WorldToLocal(worldPosition);
var decor = new DecorInstanceData
{
id = map.AllocateInstanceId(),
prototype = map.GetOrAddDecorPrototype(decorPrototypeId),
variant = 0, // main prefab
flags = DecorFlags.None,
position = new float3(local.x, 0f, local.z), // the prototype's yOffset is added at render time
rotation = DecorMath.Yaw(yawDegrees),
scale = new float3(1f, 1f, 1f),
tint = new Color32(255, 255, 255, 255)
};
map.Decor.Add(decor);
int2 cell = map.LocalToCell(new float2(local.x, local.z));
terrain.NotifyChanged(MapLayers.Decor, new RectInt(cell.x, cell.y, 1, 1));
return decor.id;
}
}
直接修改 MapData 的代码不会被记录到撤销历史中。要让编辑器脚本中的编辑支持撤销,可以通过编辑会话的 RTSEditSession.RunOperation(name, layers, operation) 来执行,它会把指定的数据层记录为一个撤销步骤。
通行图
通行图用于回答“单位能否在这里行走、建筑能否放在这里?”这类问题。它是由通行单元格组成的栅格(每个地图单元格每边 PathingResolution 个,默认为 2,因此在单元格尺寸为 2 m 的地图上,通行单元格的边长为 1 m),每个通行单元格对应一个 PathingFlags 字节。通行图由系统自动推导得出,只要任一输入发生变化(几何形状、水体、地面图层、树木、装饰物、通行覆盖),就会增量重建。通行图本身从不保存,保存的只有手动设置的通行覆盖。
PathingFlags | 含义 |
|---|---|
Walkable | 地面单位可以在此行走。 |
Buildable | 可以在此放置建筑。 |
Flyable | 空中单位可以在此飞行。所有位置都会设置此标记,除非被 Unflyable 效果移除:该效果可能来自绘制的通行覆盖、图块集的地面图层规则,或装饰物原型的 Footprint Effect。 |
ShallowWater | 浅水(默认可行走、不可建造)。 |
DeepWater | 深水(仅限海军单位)。 |
Cliff | 该单元格与崖壁相接。 |
Blocked | 被树木或装饰物的占地范围阻挡。 |
Steep | 对地面单位而言过陡。 |
标记的推导方式
- 地形:崖壁(外加 Cliff Margin 范围)会设置
Cliff;坡度超过 Max Walkable Slope(默认 42°)时设置Steep;水体根据水深设置ShallowWater或DeepWater;所有位置都设置Flyable;Buildable= 可行走、坡度 ≤ Max Buildable Slope(默认 18°)且没有水(开启 Shallow Water Buildable 时,浅水也可以)。之后再应用主导地面图层的 Pathing Effect(例如粗糙岩石图层上的“Unbuildable”)。 - 占地范围:树木会阻挡一个圆形区域(半径 = Footprint Radius × 树木缩放 × Tree Footprint Scale);装饰物会在圆形或旋转后的矩形区域内应用其 Footprint Effect,除非该实例带有
DecorFlags.IgnorePathing。 - 手动覆盖(用 Paint Pathing / Clear Pathing 绘制)最后应用。
这些规则的设置项位于 RTSTerrain 组件上(RTSPathingSettings,属性 terrain.PathingSettings)。
PathingOverride | 效果 |
|---|---|
Unwalkable | 移除 Walkable(以及 Buildable),并设置 Blocked。 |
Unbuildable | 移除 Buildable。 |
Unflyable | 移除 Flyable。 |
ForceWalkable | 即使推导规则得出相反的结果,也设置 Walkable。 |
ForceBuildable | 即使推导规则得出相反的结果,也设置 Buildable。 |
地面图层的通行效果、装饰物的占地效果和绘制的通行覆盖都使用同一组 PathingOverride 标记。可以用 terrain.GetPathing(world)、terrain.IsWalkable(world) 和 terrain.IsBuildable(world) 查询结果,也可以直接读取 terrain.Pathing.Flags。
区域、出生点与标记点
这些是供游戏逻辑使用的轻量对象。它们不会在游戏中渲染,编辑器会将它们绘制为 Gizmo(辅助图标)。
| 类型 | 字段 | 典型用途 |
|---|---|---|
MapRegion(Regions 工具) | name、rect(地图局部 XZ 平面中的矩形,单位为米:rect.x = X,rect.y = Z)、color、properties(键值列表) | 触发器、生成区域、野怪营地。Contains(localXZ)、GetProperty(key, fallback)。 |
StartLocation(Start Locations 工具) | player(从 0 开始)、position、yaw | 玩家出生点。示例单位生成器会使用它们。 |
MapMarker(Markers 工具) | name、tag(自由文本,例如“camera”或“goldmine”)、position、rotation、properties | 过场动画摄像机、兴趣点、脚本控制的生成点。 |
对于出生点和标记点,工具会存储 position.y = 0,并像树木一样把它视为相对地面的偏移。
using RTSTerrainStudio;
using Unity.Mathematics;
using UnityEngine;
public static class RegionExample
{
/// <summary>Returns the "gold" property of the first region that contains the world position, or 0.</summary>
public static int GetGoldAt(RTSTerrain terrain, Vector3 worldPosition)
{
if (terrain.Map == null) return 0;
Vector3 local = terrain.WorldToLocal(worldPosition);
foreach (MapRegion region in terrain.Map.Regions)
{
// Regions are rectangles in map-local XZ.
if (region.Contains(new float2(local.x, local.z)) &&
int.TryParse(region.GetProperty("gold", "0"), out int gold))
return gold;
}
return 0;
}
}
地图信息与环境
MapInfo 字段 | 默认值 | 说明 |
|---|---|---|
name | "New Map" | 显示名称(同时存储在未压缩的文件头中)。 |
author | "" | 作者名称。 |
description | "" | 在地图浏览器中显示的自由文本。 |
maxPlayers | 2 | 推荐的玩家数量。 |
tilesetId | "" | 制作该地图时所用图块集的 ID(RTSTileset.tilesetId)。如果地形指定了图块集,会自动填写。当指定的图块集与之不符时,RTSTerrain 组件的 Inspector 会显示警告。 |
createdUtcTicks、modifiedUtcTicks | 自动设置 | 创建时间和最后保存时间(UTC 计时周期数,即 ticks)。每次序列化地图时,RTSMapSerializer.Write 都会设置修改时间,包括 SaveToFile、编辑器中的 Save 操作、Serialize(map) 和 RTSMapAsset.CreateFromMap。 |
EnvironmentSettings 存储地图的光照设置。它由 RTSEnvironment 组件()应用到场景中;如果没有该组件,这些值会被保存,但不会生效。
EnvironmentSettings 字段 | 默认值 | 说明 |
|---|---|---|
sunColor | 暖白色 (1, 0.956, 0.84) | 太阳光的颜色。 |
sunIntensity | 1.2 | 太阳光的强度。 |
sunRotation | (50, −30) | 太阳的旋转角度,单位为度(x = 俯仰角,y = 偏航角)。 |
ambientColor | (0.45, 0.5, 0.58) | 环境光颜色。 |
fog | 关 | 启用距离雾效。 |
fogColor | (0.6, 0.68, 0.75) | 雾效颜色。 |
fogDensity | 0.004 | 雾效密度。 |
变更通知
通过代码修改 MapData 后,需要告诉地形改了什么、改在哪里(以单元格为单位):
terrain.NotifyChanged(MapLayers layers, RectInt cellRect)会立即触发MapChanged,并将重建加入队列。terrain.NotifyChangedAll(layers)将整张地图标记为已更改(适用于运行生成器或导入之后)。- 待处理的更改会在
LateUpdate中处理(在 Play 模式之外,每次编辑器更新时也会处理),也可以调用terrain.ProcessPendingChanges()立即处理。之后会触发ChangesProcessed,此时网格、纹理和通行数据均已更新。 - 一个顶点最多与四个单元格相接。可以用
BrushUtility.VertexRectToCellRect(map, vertexRect)将顶点矩形转换为单元格矩形,用BrushUtility.TexelRectToCellRect(map, texelRect, resolutionPerCell)转换纹素矩形。
using RTSTerrainStudio;
using UnityEngine;
public static class CliffExample
{
/// <summary>Raises a square plateau by one cliff level around a world position.</summary>
public static void RaisePlateau(RTSTerrain terrain, Vector3 worldCenter, int radiusInVertices)
{
MapData map = terrain.Map;
if (map == null) return;
// Nearest vertex to the centre (vertices sit on multiples of the cell size).
Vector3 local = terrain.WorldToLocal(worldCenter);
int cx = Mathf.RoundToInt(local.x / map.CellSize);
int cz = Mathf.RoundToInt(local.z / map.CellSize);
int x0 = Mathf.Max(0, cx - radiusInVertices), x1 = Mathf.Min(map.Width, cx + radiusInVertices);
int z0 = Mathf.Max(0, cz - radiusInVertices), z1 = Mathf.Min(map.Height, cz + radiusInVertices);
for (int z = z0; z <= z1; z++)
{
for (int x = x0; x <= x1; x++)
{
int i = map.VertexIndex(x, z);
// Levels are bytes from 0 to 15: always clamp.
map.CliffLevels[i] = (byte)Mathf.Min(map.CliffLevels[i] + 1, MapData.MaxCliffLevel);
}
}
// Convert the changed vertices to the cells around them and notify the terrain.
var vertexRect = new RectInt(x0, z0, x1 - x0 + 1, z1 - z0 + 1);
RectInt cells = BrushUtility.VertexRectToCellRect(map, vertexRect);
terrain.NotifyChanged(MapLayers.CliffLevels, cells);
}
}
坐标与多个地形
所有地图数据都存储在地图局部空间中。RTSTerrain GameObject 的 Transform 负责把该空间放置到世界中。
地图局部空间
- 原点是顶点 (0, 0),即单元格 (0, 0) 的角点。X 和 Z 的范围是 0 到
map.WorldSize(单位为米),Y 轴朝上。 - 顶点 (x, z) 位于局部坐标
(x × CellSize, height, z × CellSize);单元格 (x, z) 的中心位于((x + 0.5) × CellSize, …, (z + 0.5) × CellSize)。 - 局部 Y = 0 对应层级 0、精细高度为 0。如果新地图的基础层级为 2,每个层级高 2 m,那么它的地面位于局部 Y = 4 m。
- 区块 GameObject 都位于地形的隐藏子对象 RTS Chunks(
RTSTerrain.ChunkRoot)之下。该根对象没有局部偏移或旋转,并会抵消地形的缩放,而每个区块的局部 Transform 都是单位变换,因此区块网格的对象空间就是地图局部空间。为地形编写着色器时,这一点很重要。
世界空间与地图空间的转换
方法(RTSTerrain) | 说明 |
|---|---|
WorldToLocal(Vector3) / LocalToWorld(Vector3) | 在世界空间与地图局部空间之间转换位置。 |
WorldToLocalDirection / LocalToWorldDirection | 转换方向(射线、法线)。 |
WorldToCell(Vector3) | 包含某个世界位置的单元格(限制在地图范围内)。 |
CellCenterToWorld(int2) | 单元格中心在地面上的世界位置。 |
VertexToWorld(int2) | 顶点的世界位置。 |
Contains(Vector3) | 世界位置位于地图的 XZ 区域内时返回 true。 |
WorldBounds | 整张地图在世界空间中的近似边界,包含可能的最高悬崖层级。 |
GetHeight、SnapToGround、GetNormal、GetCliffLevel、IsWater、TryGetWaterHeight、GetPathing、Raycast | 接受并返回世界空间值的查询方法,内部会自动完成转换。 |
GetGeometry() | 返回一个 MapGeometryView,用于在地图局部空间中快速查询(也可在 Burst 作业中使用)。 |
using RTSTerrainStudio;
using Unity.Mathematics;
using UnityEngine;
/// <summary>Add to any GameObject and use the context menu to see the map data under it.</summary>
public class TerrainProbe : MonoBehaviour
{
[ContextMenu("Probe Terrain")]
void Probe()
{
// Several terrains can exist: find the one below this object.
RTSTerrain terrain = RTSTerrain.FindAt(transform.position);
if (terrain == null || terrain.Map == null) { Debug.Log("Not above an RTS Terrain."); return; }
MapData map = terrain.Map;
// World -> map-local (origin at vertex 0,0; position and Y rotation only).
Vector3 local = terrain.WorldToLocal(transform.position);
int2 cell = terrain.WorldToCell(transform.position);
// Nearest vertex.
int vx = Mathf.Clamp(Mathf.RoundToInt(local.x / map.CellSize), 0, map.Width);
int vz = Mathf.Clamp(Mathf.RoundToInt(local.z / map.CellSize), 0, map.Height);
int v = map.VertexIndex(vx, vz);
// Splat texel below the object (SplatResolution texels per cell side).
int tx = Mathf.Clamp((int)(local.x / map.CellSize * map.SplatResolution), 0, map.SplatWidth - 1);
int tz = Mathf.Clamp((int)(local.z / map.CellSize * map.SplatResolution), 0, map.SplatHeight - 1);
int dominantLayer = map.GetDominantLayer(map.SplatIndex(tx, tz));
Debug.Log($"Cell {cell}, vertex ({vx},{vz}): level {map.CliffLevels[v]}, fine height {map.Heights[v]:0.00} m, " +
$"vertex height {map.GetVertexHeight(vx, vz):0.00} m. Ground at world Y {terrain.GetHeight(transform.position):0.00}, " +
$"cliff level {terrain.GetCliffLevel(transform.position)}, dominant ground layer {dominantLayer}, " +
$"pathing {terrain.GetPathing(transform.position)}.");
}
}
位置、旋转与缩放规则
| Transform 部分 | 是否支持 | 备注 |
|---|---|---|
| 位置 | 是 | 移动整张地图。可用于将多个地形并排放置,或放在不同的高度上。 |
| Y 轴旋转 | 是 | 让地图绕其原点(顶点 0, 0)旋转。查询、工具、笔刷光标和实例的旋转都会随之调整。 |
| X / Z 轴旋转 | 否 | 只应使用 Y 轴旋转。RTSTerrain 组件的 Inspector 会显示警告并提供 Fix 按钮,它会保留 Y 轴旋转并重置 X 和 Z。 |
| 缩放 | 忽略 | 区块会抵消地形及其父对象的任何缩放。Inspector 窗口会显示警告并提供 Reset Scale 按钮。要让地图变大,请使用更多单元格或更大的单元格尺寸。 |
多个地形
一个场景可以包含任意数量的 RTSTerrain 组件。每个组件都有自己的地图、图块集、库、区块和通行图。
RTSTerrain.All列出所有已启用的地形;RTSTerrain.Main返回第一个地形(适合单地图游戏;临时预览地形会被跳过)。RTSTerrain.FindAt(worldPosition)返回 XZ 区域包含该位置的地形。RTSTerrain.RaycastAll(ray, out hit)对所有地形执行射线检测,并返回最近的命中结果(hit.terrain会指明命中的是哪个地形)。RTSTerrain.TerrainEnabled/TerrainDisabled是静态事件,供需要跟踪地形的系统使用。- 各个地形相互独立:地图在边界处不会拼接,寻路(
RTSPathfinder.FindPath(terrain, …))每次也只作用于一个地形。要让两张地图无缝衔接,需要让它们边界顶点的高度保持一致。
using RTSTerrainStudio;
using UnityEngine;
public static class TerrainLayout
{
/// <summary>Places "east" directly next to "west" (along west's local +X), with the same rotation.</summary>
public static void PlaceNextTo(RTSTerrain west, RTSTerrain east)
{
if (west.Map == null) return;
float widthMetres = west.Map.WorldSize.x; // map-local X extent in metres
east.transform.SetPositionAndRotation(
west.transform.position + west.transform.rotation * new Vector3(widthMetres, 0f, 0f),
west.transform.rotation);
}
}
地图资源与地图文件
地图保存为 .rtsmap 文件。Unity 编辑器(作为项目中的资源)和游戏内编辑器(作为磁盘上的文件)使用相同的二进制格式,因此玩家制作的地图可以在 Unity 中打开,反之亦然。
项目中的 .rtsmap 文件
当 Unity 在 Assets 文件夹中发现 .rtsmap 文件时,导入器 RTSMapImporter 会将其转换为 RTSMapAsset。该资源存储压缩后的文件字节、一个小型文件头(名称、作者、尺寸、玩家数、图块集 ID)以及一张小地图预览图,这张预览图也用作 Project 窗口中的缩略图。
创建地图资源
- :引导式设置(基本信息、尺寸、外观、地形);写入文件并为其创建地形,或将其指定给选中的地形。
- :在选中的文件夹中创建一张使用默认设置、尺寸为 128 × 128 的平坦地图。
- RTS Terrain Studio 窗口中的 Save As…:将某个地形的当前地图写入新文件。
- 将游戏内编辑器制作的
.rtsmap文件复制到Assets文件夹中,Unity 会像导入其他文件一样导入它。
地图资源的 Inspector 窗口
选中一个地图资源,即可查看其预览图、名称、作者、描述、尺寸、玩家数、图块集和格式版本,以及统计信息(单元格数、世界尺寸、单元格尺寸、层级高度、出生点、树木、装饰物、区域、标记点、细节图层、文件大小、内存占用、最后修改时间)。
| 控件 | 说明 |
|---|---|
| Open In Scene | 将地图指定给选中的地形(或场景中唯一的地形;有多个地形时会询问)。场景中没有地形时会创建一个。在 Play 模式下不可用。 |
| ▾ 菜单 | Open In New Terrain,或使用 Open On 在场景中的某个指定地形上打开。 |
| Show In Explorer(macOS 上为 Reveal In Finder) | 在操作系统的文件浏览器中显示该文件。 |
| Tileset Id (Copy, Select) | 地图中存储的图块集 ID。Select 会选中对应的图块集资源。 |
| Preview Size | 烘焙预览图的最长边:128、256(默认)或 512 像素。 |
| Preview Tileset | 用于预览颜色的图块集。留空 = 使用地图所存储 ID 对应的图块集(自动查找)。 |
| Re-bake Preview, Apply / Revert | 重建预览图 / 应用已更改的导入设置。 |
当地图所记录的 ID 对应的图块集被添加、移除,或其小地图颜色发生变化时,预览图会自动重新烘焙。重建纹理数组不会重新导入地图。损坏的文件会作为带错误图标的空资源导入,同时 Console 窗口中会出现一条导入错误。
编辑、Save 与 Save As
将地图资源指定给 RTSTerrain 的 Map Asset 字段。地形启用时会将地图加载到内存中,所有编辑都在这份内存副本上进行。场景文件只存储对该资源的引用,从不存储地图数据本身。
- Save(位于 RTS Terrain Studio 窗口标题栏,或 Scene 视图中 RTS Terrain Studio 叠加层选项菜单里的 Save Map)会将地图写回其
.rtsmap文件并重新导入。存在未保存的更改时,Save 会高亮显示。 - Save As… 会写入新文件并将其指定给地形,且不会重新加载地图,因此你的撤销历史会保留下来。
- Open… 加载另一个地图资源;Reload 从资源重新加载地图,并丢弃未保存的更改。
- 没有地图资源的地形会根据其 Default Map Settings 创建地图。在你使用 Save As 之前,这张地图只存在于内存中。
- 保存场景(Ctrl+S)时,也会保存该场景中各地形已编辑的地图,并为尚无文件的新地图提供 Save As 选项。这两种行为都可以在 Scene 视图叠加层的选项菜单中关闭(Save Maps When Saving the Scene、Offer Save As for New Maps)。
- 如果存在未保存的地图编辑,在进入 Play 模式、关闭场景或退出 Unity 之前,系统会询问你是否保存。在 Play 模式下所做的编辑(无论是通过 Scene 视图工具还是游戏内编辑器)也会在你退出 Play 模式时提示保存。
地图编辑不属于场景的一部分。Unity 自带的“Save scene?”对话框不会处理这些编辑,RTS Terrain Studio 自己的提示对话框才会。如果某个 .rtsmap 文件为只读或被版本控制锁定,Save 会显示一个对话框,而不会写入文件。
运行时的地图文件
在构建出的游戏中,地图就是磁盘上的普通文件:
RTSMapSerializer.DefaultMapsFolder是RTSMapSerializer.StorageRoot下的Maps文件夹。该存储根目录就是Application.persistentDataPath,因此在 Windows 上,这个文件夹通常为%USERPROFILE%\AppData\LocalLow\<Company>\<Product>\Maps。在 Web 构建中,StorageRoot是浏览器存储空间中的一个文件夹(参见浏览器中的地图)。- 游戏内编辑器(
RTSRuntimeEditor)在其 Maps Folder 中保存和打开地图(留空 = 上述默认文件夹;相对路径以RTSMapSerializer.StorageRoot为基准)。开启 Write Preview Images 后,它会在每张已保存的地图旁写入一张小尺寸的小地图图像Name.preview.png,供其地图浏览器使用。 - 要分享地图,可将
.rtsmap文件复制到其他玩家的地图文件夹,或复制到 Unity 项目中。在网页浏览器中,则应改用游戏内编辑器的 Download Map 和 Import Map。 - 随游戏发布的地图可以直接作为地图资源,由地形或脚本引用;它们会和其他资源一样包含在构建版本中。
| API | 说明 |
|---|---|
terrain.LoadFromFile(path) / terrain.SaveToFile(path) | 将文件加载到地形中 / 保存地形上的地图。两者都会设置 terrain.CurrentFilePath。 |
terrain.MapAsset = asset | 指定地图资源;地形处于启用状态时会立即加载它。 |
terrain.SetMap(mapData) | 显示你创建或加载的 MapData;地形将接管其所有权。 |
terrain.HasUnsavedChanges | 自上次加载或保存以来有过编辑时为 true。 |
RTSMapAsset.LoadMap() | 将资源反序列化为新的 MapData(所有权归你)。 |
RTSMapAsset.CreateFromMap(map, preview) | 创建仅存在于内存中的地图资源,例如用于在场景之间传递地图。 |
RTSMapSerializer.Serialize(map) / Deserialize(bytes) | 与字节数据相互转换,例如用于通过网络发送地图。 |
RTSMapSerializer.TryReadHeaderFromFile(path, out header) | 只读取未压缩的文件头(速度快,适用于地图列表)。 |
RTSMapSerializer.TryReadHeader(bytes, out header) | 功能同上,但针对内存中的地图字节(例如通过网络接收到的数据)。 |
terrain.CreateNewMap(MapCreationSettings) | 创建并显示一张新的平坦地图,相当于 New Map 对话框的代码版本。会根据地形的图块集填写 Info.tilesetId,清空 CurrentFilePath,并将地图标记为未保存。 |
terrain.LoadFromAsset() | 从 MapAsset 重新加载地图,并丢弃未保存的编辑。如果无法读取该资源,会记录一条错误,并回退为根据 DefaultMapSettings 创建的地图。 |
terrain.SaveToMapAsset() | 仅限编辑器:将地图写回其 .rtsmap 资源。地形没有地图资源时返回 false。 |
terrain.PendingLayers | 已发出更改通知、但尚未处理的数据层。 |
RTSMapAsset.Header / Preview / Data | 地图文件头(无需解压地图即可读取)、小地图预览图(可能为 null)以及原始文件字节。 |
RTSMapAsset.SetData(bytes, preview) | 替换地图资源的内容(文件头从字节中读取)。 |
using System.IO;
using RTSTerrainStudio;
using UnityEngine;
public class SimpleMapList : MonoBehaviour
{
[SerializeField] RTSTerrain terrain;
/// <summary>Logs all maps in the default folder without loading them, then opens the first one.</summary>
[ContextMenu("List And Open Maps")]
void ListAndOpen()
{
string folder = RTSMapSerializer.DefaultMapsFolder;
if (!Directory.Exists(folder)) { Debug.Log("No maps folder yet: " + folder); return; }
string[] files = Directory.GetFiles(folder, "*" + RTSMapSerializer.FileExtension);
foreach (string path in files)
{
// The header is stored uncompressed, so this is fast even for big maps.
if (RTSMapSerializer.TryReadHeaderFromFile(path, out RTSMapHeader h))
Debug.Log($"{h.name} by {h.author}: {h.width} x {h.height} cells, {h.maxPlayers} players, saved {h.Modified}");
}
if (files.Length > 0 && terrain != null)
{
try
{
terrain.LoadFromFile(files[0]); // the terrain owns and disposes the loaded map
}
catch (InvalidDataException e)
{
Debug.LogError("The map file is damaged: " + e.Message);
}
}
}
}
图块集
图块集(RTSTileset)定义地图的外观:最多 16 个地面图层、最多 4 个悬崖图层以及水体样式,类似于经典 RTS 地图编辑器中的夏季、冬季或沙漠图块集。你可以创建任意数量的图块集,并随时切换地形所用的图块集:已绘制的地图只存储图层索引,因此同一张地图既可以呈现夏季风貌,也可以呈现冬季风貌。
- 通过 创建图块集。
- 将地面纹理拖放到 Inspector 窗口的 Ground Layers 部分。每张反照率纹理会创建一个图层。匹配的法线贴图和高度贴图会根据文件名后缀自动识别(例如
_n、_normal、_nrm、_normalgl、_h、_height、_disp);粗糙度、AO 和金属度贴图会被忽略,因此你可以直接拖入下载的整个纹理文件夹。 - 以同样的方式在 Cliff Layers 中添加悬崖图层。
- 将图块集指定给
RTSTerrain的 Tileset 字段。 - 在 Texture Arrays 部分中单击一次 Build Texture Arrays(参见纹理数组)。
图块集字段
| 设置项 | 默认值 | 说明 |
|---|---|---|
Tileset Id (tilesetId) | 自动生成 | 存储在地图文件中的稳定标识符,使地图能记住制作时所用的图块集。以只读方式显示,并附带 Copy 按钮。 |
Display Name (displayName) | "New Tileset" | 在各编辑器中显示的名称。 |
Ground Layers (groundLayers) | 空 | 最多 16 个 GroundLayer 条目。多余的条目会被移除。 |
Cliff Layers (cliffLayers) | 空 | 最多 4 个 CliffLayer 条目。 |
Water (water) | 蓝绿色 | WaterProfile,见下文。 |
Resolution (arrayResolution) | 1024 | 纹理数组的切片尺寸:256、512、1024 或 2048。源纹理会自动缩放。 |
Compress (compressArrays) | 开 | 压缩构建出的数组(BC7 / ASTC)。关闭可获得最快的迭代速度。 |
Material Override (materialOverride) | 无 | 可选材质,用于替代默认地形材质。它必须使用 RTS Terrain Studio/Terrain 着色器或与之兼容的着色器。 |
Macro Variation (macroVariation) | 0.25 | 大尺度变化,可在大面积区域中打破纹理的重复感。 |
Triplanar Sharpness (triplanarSharpness) | 6 | 崖壁上三平面混合的锐度(1–16)。 |
地面图层字段
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Name | "Layer" | 显示在选择面板中。 |
| Albedo | 无 | 基础颜色(RGB)。可使用任意尺寸和格式。 |
| Normal | 无 | 切线空间法线贴图(以 Normal Map 类型导入)。 |
| Height Map | 无 | 可选的高度数据(R 通道),用于高度混合。指定后会取代反照率的 alpha。 |
| Height From Albedo Alpha | 关 | 未指定高度贴图时,使用反照率的 alpha 作为高度。 |
| Tint | 白色 | 与反照率相乘的颜色。 |
| Tile Size | 6 | 纹理每重复一次所覆盖的世界尺寸,以米为单位(≥ 0.1)。 |
| Normal Strength | 1 | 0–2。 |
| Smoothness | 0.1 | 0–1。 |
| Metallic | 0 | 0–1。 |
| Height Blend | 0.5 | 与相邻图层进行高度混合的强度(0 = 线性混合)。需要高度来源。 |
| Anti Tiling | 0.3 | 通过第二次带变化的采样打破明显的平铺重复(0–1)。 |
| Pathing Effect | None | 在此图层占主导的位置应用的通行规则,例如为崎岖岩石设置 Unbuildable,为熔岩设置 Unwalkable。 |
| Minimap Color | 黑色 | 在小地图和地图预览中的颜色。黑色 = 根据反照率计算。 |
悬崖图层字段
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Name | "Cliff" | 显示在 Cliff Type 选择面板中。 |
| Albedo | 无 | 崖壁颜色。未指定时,此类型的悬崖会显示深灰色占位符。 |
| Normal | 无 | 切线空间法线贴图。 |
| Tint | 白色 | 与反照率相乘的颜色。 |
| Tile Size | 4 | 纹理每重复一次所覆盖的世界尺寸,以米为单位。 |
| Normal Strength | 1 | 0–2。 |
| Smoothness | 0.15 | 0–1。 |
| Top Ground Layer | −1(无) | 自动绘制在此类型悬崖顶部的地面图层。供 Auto Paint 使用。 |
| Edge Ground Layer | −1(无) | 悬崖工具沿此类型新悬崖边缘绘制的地面图层,例如一圈石质边沿(参见自动悬崖边缘)。在悬崖卡片上显示为 Edge Layer。−1 = 不自动绘制边缘。 |
| Minimap Color | 黑色 | 小地图颜色。黑色 = 根据反照率计算。 |
崖壁在地图局部空间中使用三平面映射贴图,因此悬崖纹理不需要特殊的 UV 布局。
水体配置
水体配置是图块集的一部分,供水体渲染器、通行图和小地图使用。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Shallow Color | (0.22, 0.55, 0.6, 0.55) | 浅水的颜色;alpha = 不透明度。 |
| Deep Color | (0.03, 0.16, 0.27, 0.95) | 深水的颜色;alpha = 不透明度。 |
| Color Depth | 3 | 完全呈现深水颜色时的水深,以米为单位。 |
| Shallow Depth Limit | 0.9 | 深于此值的水为深水(不可行走)。较浅的水可以行走,与经典 RTS 游戏一致。 |
| Normal Map | 无 | 波浪法线贴图。 |
| Normal Strength | 0.6 | 0–2。 |
| Normal Tile Size | 8 | 法线贴图每重复一次所覆盖的世界尺寸,以米为单位。 |
| Flow Speed | (0.03, 0.02) | 法线贴图的滚动速度。 |
| Wave Height | 0.05 | 0–1。 |
| Smoothness | 0.92 | 0–1。 |
| Refraction | 0.03 | 0–0.2。 |
| Foam Color | 白色,alpha 0.8 | 岸边泡沫颜色。 |
| Foam Distance | 0.6 | 岸边泡沫的距离,以米为单位。 |
| Foam Texture | 无 | 可选的泡沫图案。 |
| Minimap Color | (0.1, 0.3, 0.55) | 水体在小地图上的颜色。 |
通过代码编辑图块集
图块集是 ScriptableObject。通过代码修改字段后,调用 tileset.NotifyChanged(),让地形刷新其材质。对于未使用的槽位,tileset.GetGroundLayer(index) 和 GetCliffLayer(index) 返回 null。
纹理数组
地形着色器在单个 Pass 中绘制全部 20 个图层,因此所有图层纹理都打包进两个 GPU 纹理数组中。你无需亲自制作这些数组:只要提供任意尺寸和格式的普通 Unity 纹理,数组就会自动生成。
布局
| 数组 | 切片 | 通道 | 色彩空间 |
|---|---|---|---|
_RTS_AlbedoArray | 0–15:地面图层,16–19:悬崖图层 | RGB = 反照率,A = 用于高度混合的高度 | sRGB |
_RTS_NormalArray | 0–15:地面图层,16–19:悬崖图层 | RGB = 重映射到 0–1 的切线空间法线(n × 0.5 + 0.5) | 线性 |
- 第一个悬崖切片为
RTSTileset.CliffSliceOffset(16);切片总数为RTSTileset.TotalSlices(20)。 - 高度(alpha)取自图层的 Height Map(R 通道);如果没有,且开启了 Height From Albedo Alpha,则取自反照率的 alpha;否则为常量 0.5。
- 法线会在构建过程中解包,因此你的法线贴图可以照常使用 Unity 的法线贴图导入方式(DXT5nm / BC5)。没有法线贴图的图层会得到平坦法线。
- 没有反照率纹理的图层会得到带有细微图案的中性灰色切片;着色器仍会应用色调。
- 每个切片都有完整的 Mip 链,并使用三线性过滤。任意尺寸的源纹理都会缩放到所选的 Resolution。
预构建数组与运行时回退
数组有两种生成方式:
| 预构建数组(推荐) | 运行时回退 | |
|---|---|---|
| 创建方式 | 图块集 Inspector 窗口中的 Build Texture Arrays 按钮 | 不存在最新的预构建数组时,由渲染器自动创建 |
| 存储位置 | 作为子资源存储在图块集资源内(包含在 Player 构建中) | 仅存在于 GPU 内存中,每次加载时重建 |
| 格式 | BC7(桌面平台、主机),ASTC 6×6(Android、iOS、tvOS、visionOS),Web 平台采用构建所设置的纹理压缩格式(DXT5、ASTC 6×6 或 ETC2 RGBA8),DXT5(其他平台);关闭 Compress 时为 RGBA32 | 未压缩(每像素 4 字节) |
| 1024 px 时的内存占用 | 使用 BC7 时两个数组合计约 56 MB | 两个数组合计约 224 MB |
Texture Arrays 部分会显示一个状态徽章:
- Not built:地形正在使用运行时数组。单击一次 Build Texture Arrays 即可。
- Up to date:显示格式、尺寸、Mip 数量和内存占用。
- Outdated:纹理、分辨率或压缩设置已更改,或者数组是为其他平台或其他纹理压缩格式构建的。重新构建即可,也可以交给下一次 Player 构建来完成。
- Building:构建正在进行或已排队等待。
该部分还会显示当前活动构建平台的目标格式、预计大小和构建时间;构建大小超过 256 MB 时,会先请求你确认。Clear 会移除预构建数组。
Player 构建
Player(构建出的游戏)会原样使用预构建数组,因为只有编辑器才能将它们与源纹理进行比较。因此,已过期的数组会在每次 Player 构建开始前自动重建,而从未构建过数组的图块集会继续使用运行时数组。详细信息(包括批处理模式和 -rtsAllowOutdatedTextureArrays)参见Player 构建中的纹理数组。Player 还会在首次使用预构建数组时释放它们在 CPU 端的副本,节省的内存大约与这些数组占用的 GPU 内存相当。
通过代码:tileset.HasValidBuiltArrays 表示是否会使用压缩数组。在编辑器中,数组必须存在且与 ComputeContentHash() 匹配。Player 会一直信任预构建数组,直到你的代码在运行时更改图层纹理、分辨率或压缩设置,随后回退到运行时数组。ComputeContentHash() 的值只能在编辑器内部或同一次 Player 会话内相互比较,因为编辑器计算哈希时使用的是纹理的导入状态。HasValidBuiltArrays 为 false 时,RTSTerrain 会构建未压缩的运行时数组(RTSTextureArrayBuilder.Acquire)。tileset.BuiltAlbedoArray / BuiltNormalArray 提供对预构建数组的访问。在你自己的运行时代码中,请使用 RTSShaderReferences.FindShader(name)(以及 FindMaterial(name))代替 Shader.Find,这样查找在 Player 构建中也能正常工作。
Auto Build
开启 Auto Build(默认开启,这是一项适用于所有图块集的编辑器偏好设置)后,之前构建过的数组会在纹理、分辨率或压缩设置更改一秒后,使用快速压缩自动重建。首次构建始终需要手动启动。被取消或失败的自动构建在内容或目标格式发生变化之前不会重试。手动单击 Build Texture Arrays 按钮时使用正常的压缩质量,效果略好,因此请在发布前单击一次。
预估大小的算法很简单:分辨率² × 20 个切片 × 2 个数组 × 4/3(Mip 开销)× 每像素字节数(BC7 为 1,ASTC 6×6 约为 0.44,未压缩为 4)。在移动平台上可将分辨率降至 512,内存占用只有原来的四分之一。
Player 构建必须包含 RTS Terrain Studio/Terrain 和 Hidden/RTS Terrain Studio/TextureArrayBlit(供运行时回退使用)这两个着色器。 会在 Resources/RTSTerrainStudio 中创建 RTSShaderReferences 资源,从而让所有包内着色器保留在构建中。
植被库
植被库(RTSVegetationLibrary)列出可用于绘制的草、植物和树木。通过 创建植被库,并将其指定给 RTSTerrain 的 Vegetation Library 字段。地形还需要 RTSVegetationRenderer 组件才能显示植被(RTSTerrain 的 Inspector 窗口会提供添加该组件的选项)。
植被库的 Inspector 窗口有 Details 和 Trees 两个选项卡,各带一个缩略图列表。将预制件(树木)、纹理(草片)或网格(细节网格)拖放到列表上即可添加条目。每个条目都有一个供地图使用的稳定 id;复制出的条目会自动获得新的 ID。
细节原型
细节是按密度绘制、数量众多的小型植物(参见细节密度)。它们按 16 × 16 个单元格的块生成,并使用内部程序化着色器 Hidden/RTS Terrain Studio/Detail 绘制,该着色器负责处理风、战争迷雾和颜色变化。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Id | 自动生成 | 存储在地图文件中的稳定标识符。 |
| Name | "Grass" | 显示在选择面板中。 |
| Render Mode | Crossed Quads | CrossedQuads:贴有 Texture 的交叉面片(经典草地效果)。Mesh:使用 Mesh,并采用 Material 的主纹理和颜色。 |
| Mesh | 无 | Mesh 模式使用的网格。 |
| Material | 无 | Mesh 模式使用的材质。细节着色器会使用其主纹理、颜色和 alpha 裁剪阈值。 |
| Texture | 无 | Crossed Quads 模式使用的带 alpha 通道的纹理。 |
| Healthy Color / Dry Color | 浅绿色 / 稻草色 | 各实例根据噪声在两种颜色之间混合。 |
| Min / Max Width | 0.6 / 1.0 | 随机宽度范围,以米为单位。 |
| Min / Max Height | 0.4 / 0.8 | 随机高度范围,以米为单位。 |
| Max Density | 6 | 满密度(255)时每个细节纹素的实例数。范围 1–32。 |
| Color Noise Scale | 12 | 用于混合健康颜色与干枯颜色的噪声缩放。 |
| Wind Strength | 0.5 | 0–1。风向和风速在 RTSVegetationRenderer 上设置。 |
| Render Distance | 70 | 超出此摄像机距离的实例会被剔除(在此之前会平滑缩小直至消失)。 |
| Cast Shadows | 关 | 细节投射阴影的开销很大;草类请保持关闭。 |
| Align To Ground | 开 | 倾斜实例,使其贴合地面法线。 |
| Max Slope | 40 | 不在坡度大于此值(度)的斜坡上生成。 |
| Avoid Water | 开 | 不在水下生成。 |
| Icon | 无 | 选择面板图标;留空时由编辑器自动生成。 |
树木原型
树木是通过 Place Trees 放置的独立实例(参见树木与装饰物实例)。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Id | 自动生成 | 存储在地图文件中的稳定标识符。 |
| Name | "Tree" | 显示在选择面板中。 |
| Prefab | 无 | 包含 MeshRenderer 和可选 LODGroup 的预制件(使用 LOD0 和 LOD1)。 |
| Spawn Mode | Instanced | Instanced:GPU 实例化,不创建 GameObject,速度最快。GameObject:每棵树对应一个预制件实例,适用于砍伐等游戏玩法。 |
| Min / Max Scale | 0.85 / 1.2 | 随机等比缩放范围。 |
| Random Rotation | 开 | 放置时随机设置偏航角。 |
| Tint A / Tint B | 白色 / 淡绿色 | 每个实例的随机色调范围。 |
| Footprint Radius | 0.8 | 在通行图中阻挡的半径,以米为单位(随树木缩放)。 |
| Blocks Vision | 开 | 在战争迷雾中,树木会遮挡视线,与经典 RTS 游戏一致。 |
| Min Spacing | 1.6 | 散布时与其他树木的最小距离。 |
| Colliders | 开 | Instanced 模式:创建池化的胶囊碰撞体代理。GameObject 模式:使用预制件自身的碰撞体。 |
| Collider Radius / Height | 0.4 / 4 | 胶囊代理的尺寸(Instanced 模式)。 |
| Render Distance | 250 | 超出此摄像机距离的树木会被剔除。 |
| Cast Shadows | 开 | 投射阴影。 |
| Icon | 无 | 选择面板图标;留空时由编辑器自动生成。 |
GPU 实例化
以 Instanced 模式生成的树木通过 GPU 实例化绘制,因此其材质应开启 Enable GPU Instancing。如果某个材质未开启该选项,渲染器会自动使用该材质的隐藏实例化副本(在你编辑时与原材质保持同步),同时植被库的 Inspector 窗口会显示一条信息提示,附带可直接修复材质本身的 Enable 按钮。细节的材质无需开启实例化,因为细节始终使用程序化细节着色器绘制。
为树木和植物预制件的材质使用 RTS Terrain Studio/Vegetation 着色器,即可获得风力摆动、透光效果、战争迷雾和逐实例色调。其他支持实例化的 URP 材质同样可用,但没有色调效果。
通过代码:使用 RTSVegetationRenderer.RemoveTree(id) 移除树木实例(例如工人砍伐一棵树时),使用 TryFindNearestTree 查找树木,使用 GetTreeObject(id) 获取其 GameObject 或碰撞体代理。
通过代码编辑库
地图存储的是一张原型 ID 表,因此需要通过库解析原型索引:
- 树木:
vegetationLibrary.FindTree(map.TreePrototypeIds[tree.prototype])。 - 细节图层:
vegetationLibrary.FindDetail(layer.prototypeId)。 - 装饰物:
decorLibrary.Find(map.DecorPrototypeIds[d.prototype])。使用prototype.GetPrefab(d.variant)获取变体预制件:0 表示主预制件,无效或空的变体会回退到主预制件。VariantCount包含主预制件。 decorLibrary.GetCategories()按显示顺序列出各类别(没有类别的原型会列在 Misc 下)。vegetationLibrary.EnsureUniqueIds()会为重复的条目分配新 ID。
通过代码修改库后,调用 library.NotifyChanged()(它会触发库的 Changed 事件),使 RTSTerrain 触发 LibrariesChanged,渲染器随之刷新。这与图块集的规则相同。
装饰物库
装饰物库(RTSDecorLibrary)列出了你可以放置的场景道具(doodad):岩石、废墟、栅栏、箱子、建筑或任何其他预制件。通过 创建一个装饰物库,并将其指定给 RTSTerrain 的 Decor Library 字段。地形上的 RTSDecorManager 组件负责让场景与已放置的实例保持同步。
Inspector 窗口以列表形式显示各个原型及其自动生成的缩略图,每个类别对应一个选项卡(另有一个 All 选项卡)。将预制件拖放到列表上,即可将其添加到当前类别;右键单击类别选项卡可以重命名该类别。没有类别的原型显示在 Misc 下。
装饰物原型字段
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Id | 自动生成 | 存储在地图文件中的稳定标识符。 |
| Name | "Decor" | 显示在选择面板中的名称。 |
| Category | "Props" | 原型所在的选择面板选项卡。 |
| Prefab | 无 | 要放置的对象。 |
| Variants | 空 | 可选的替代预制件;放置时会随机选择一个变体。实例中存储的是变体索引(0 = 主预制件)。 |
| Spawn Mode | GameObject | GameObject:每个实例对应一个 GameObject(支持脚本、碰撞体和动画)。Instanced:GPU 实例化,不生成 GameObject,仅适用于静态道具,速度最快。 |
| Min / Max Scale | 0.9 / 1.1 | 随机等比缩放范围。 |
| Random Yaw | 开 | 绕向上轴随机旋转。 |
| Yaw Step | 0 | 将偏航角对齐到该角度的整数倍(0 = 不限制),例如墙壁和栅栏可设为 90。 |
| Max Random Tilt | 0 | 随机倾斜角度,以度为单位(0–45)。 |
| Align To Normal | 0 | 让向上轴与地面法线对齐(0 = 保持直立,1 = 完全对齐)。 |
| Snap To Grid | 关 | 将位置对齐到单元格中心。 |
| Y Offset | 0 | 渲染时附加的垂直偏移,以米为单位(负值会让对象沉入地面)。 |
| Tint A / Tint B | 白色 / 白色 | 每个实例的随机色调范围。 |
| Footprint Shape | Circle | None、Circle 或 Box。 |
| Footprint Size | (0.75, 0.75) | Circle:X 为半径。Box:沿对象 X 轴和 Z 轴的半边长。以米为单位,随实例一起缩放。 |
| Footprint Effect | Unwalkable, Unbuildable | 写入占地范围内的 PathingOverride 标记。 |
| Blocks Vision | 关 | 在战争迷雾中遮挡视线。 |
| Min Spacing | 1 | 散布时与其他装饰物之间的最小距离。 |
| Cast Shadows | 开 | 投射阴影。 |
| Icon | 无 | 选择面板图标;留空时由编辑器自动生成。 |
占地范围
- 圆形占地范围以 Footprint Size 的 X 值为半径,再乘以实例在水平方向上的最大缩放值。
- 矩形占地范围会跟随实例的旋转(将其 X 轴和 Z 轴投影到地面上)以及 X/Z 缩放,因此旋转后的墙壁恰好只阻挡自身所占的区域。
- 当形状为
None、效果为None,或实例带有DecorFlags.IgnorePathing标记时,不会写入占地范围。 - 对于单位可以从上面走过的扁平装饰物(花朵、碎石),只使用
Unbuildable;对于实体对象,则使用Unwalkable。
生成模式
| GameObject | Instanced | |
|---|---|---|
| 场景对象 | 每个实例一个,位于名为“RTS Decor”的子对象下(不随场景保存) | 无 |
| 脚本、碰撞体、动画 | 支持(每个根对象都会添加一个 RTSDecorInstance 组件) | 不支持 |
| 渲染 | 使用预制件自身的普通渲染器 | 通过 Graphics.RenderMeshInstanced 绘制 LOD0,按分块(patch)剔除 |
| 最适合 | 可交互的道具、金矿、可破坏的物体 | 成千上万的静态岩石和植物 |
Instanced 模式的装饰物与实例化树木具有相同的 GPU 实例化行为:未勾选 Enable GPU Instancing 的材质会通过一个隐藏的实例化副本进行绘制。生成的对象会放入对象池;RTSDecorManager.GetInstanceObject(id) 返回某个实例生成的对象,RTSDecorInstance.FromCollider(collider) 则将物理检测的命中结果映射回对应的实例。
.rtsmap 文件格式
本节面向需要用其他工具读取地图、编写转换器或添加自定义数据的程序员。如果只是使用本包,则无需了解这些内容:RTSMapSerializer 会处理好一切。
二进制布局
所有数值均采用小端序(little-endian)。字符串采用 .NET BinaryWriter 格式:先是以 7 位编码的长度,后跟 UTF-8 字节。
| 部分 | 类型 | 内容 |
|---|---|---|
| 魔数(Magic) | 4 字节 | ASCII RTSM |
| 格式版本 | int32 | RTSMapSerializer.CurrentVersion(当前为 1) |
| 头部长度 | int32 | 其后头部数据的字节数 |
| 头部 | 字节 | string name、string author、string description、int32 width、int32 height、float cellSize、int32 maxPlayers、string tilesetId、int64 modifiedUtcTicks |
| 主体 | 原始 deflate 流(RFC 1951,不带 zlib 或 gzip 封装),一直延续到文件末尾 | 由若干段组成的序列,以值为 0 的 int32 标签结束 |
头部未经压缩,因此地图浏览器无需解压地图就能显示名称、作者、尺寸和玩家人数(RTSMapSerializer.TryReadHeader、TryReadHeaderFromFile、结构体 RTSMapHeader)。读取器根据头部长度直接跳到头部末尾,因此以后的版本可以追加头部字段。
文件段
每个段的结构为 [int32 tag][int32 length][payload]。标签是以小端序存储的 FourCC 代码,因此第一个字符位于最低字节(DIMS = 0x534D4944)。栅格数据数组的写法是:先写入 int32 元素数量,再写入原始元素。
| 标签 | 值 | 负载数据 |
|---|---|---|
DIMS | 0x534D4944 | int32 width、int32 height、float cellSize、float cliffLevelHeight、int32 splatResolution、int32 detailResolution、int32 pathingResolution、uint32 nextInstanceId。必须是第一个段。 |
INFO | 0x4F464E49 | string name、author、description;int32 maxPlayers;string tilesetId;int64 createdUtcTicks、modifiedUtcTicks |
HGHT | 0x54484748 | 精细高度:每个顶点一个 float |
LEVL | 0x4C56454C | 悬崖层级:每个顶点一个 byte |
VFLG | 0x474C4656 | 顶点标记:每个顶点一个 byte |
CTYP | 0x50595443 | 悬崖类型:每个顶点一个 byte |
WATR | 0x52544157 | 水面高度:每个顶点一个 float |
SPLT | 0x544C5053 | int32 贴图数量(4),然后每张 Splat 贴图各一个数组,每个纹素存储 RGBA 字节 |
DETL | 0x4C544544 | int32 图层数量,然后每个图层依次为:string prototypeId,以及一个按细节纹素存储密度的 byte 数组 |
TREE | 0x45455254 | 原型 ID 列表(int32 数量 + 字符串),int32 树木数量,然后每棵树依次为:uint32 id、uint16 prototype、float3 position、float yaw、float scale、4 字节 tint(共 30 字节) |
DECO | 0x4F434544 | 原型 ID 列表,int32 数量,然后每个实例依次为:uint32 id、uint16 prototype、byte variant、byte flags、float3 position、float4 rotation(x, y, z, w)、float3 scale、4 字节 tint(共 52 字节) |
PATH | 0x48544150 | 手动通行覆盖:每个通行单元格一个 byte |
REGN | 0x4E474552 | int32 数量,然后每个区域依次为:string name、float x、z、width、depth、float4 colour、properties |
STRT | 0x54525453 | int32 数量,然后每个出生点依次为:int32 player、float3 position、float yaw |
MARK | 0x4B52414D | int32 数量,然后每个标记点依次为:string name、string tag、float3 position、float4 rotation、properties |
ENVR | 0x52564E45 | float4 sunColor、float sunIntensity、float2 sunRotation、float4 ambientColor、bool fog、float4 fogColor、float fogDensity |
“properties”(属性)的写法是:先写入 int32 数量,再写入若干键/值字符串对。派生出的通行图不会被存储,只存储手动通行覆盖。
未知段、损坏的文件与版本管理
- 读取器会根据长度跳过未知段。因此,旧版本的包也能读取带有额外段的新文件,你的自定义段也绝不会导致其他读取器出错。
- 如果文件的格式版本比读取器新,读取器会记录一条警告,但仍会读取该文件;未知数据会被跳过。
- 如果某个栅格层的元素数量与地图尺寸不符,会记录一条警告;能对应上的部分会被读取,其余部分则被跳过。
- 被截断或损坏的文件只会抛出一种异常类型:
System.IO.InvalidDataException。魔数不正确的文件会被报告为“Not an RTS Terrain Studio map file”。 RTSMapSerializer.SaveToFile会先写入<path>.tmp,然后再替换原文件,因此即使保存过程中发生崩溃,也绝不会留下写了一半的地图。- 该格式的兼容性规则:新数据总是以新段或追加头部字段的形式加入,绝不通过修改现有段来实现。你自己的段也应遵循这一规则,并在负载数据中保存一个版本号。
自定义段
你可以在地图文件中存储自己的数据,例如游戏规则、触发器或任务数据。这要借助 RTSMapSerializer 上的两个静态钩子来实现:
| 钩子 | 类型 | 调用时机 |
|---|---|---|
CustomSectionWriter | Action<MapData, Action<int, byte[]>> | 每次保存时在写完内置段之后调用一次。对于每个要写入的段,都用你的标签和负载数据调用传入的 action。 |
CustomSectionReader | Action<MapData, int, byte[]> | 读取器每遇到一个无法识别的段就调用一次,并传入该段的标签和负载数据。此时 MapData 已经存在(DIMS 总是排在最前面)。 |
- 选择不会与内置标签冲突的标签:让它们以
X开头(例如XGLD)。切勿使用 0,因为它标志着所有段的结束。 - 这两个钩子都是普通委托:用
+=添加处理程序,用-=移除处理程序,这样多个系统都可以添加各自的段。 - 在读取器中检查标签,忽略不属于你的段。
MapData中没有存放你自己数据的位置,因此需要把数据与地图关联起来单独存放(示例中使用了ConditionalWeakTable)。以这种方式保存的数据不会被MapData.Clone()或ResizeMap复制,也不会被记录到撤销历史中。- 在加载或保存任何地图之前注册钩子。在 Unity 编辑器中,导入器也会读取地图,Save 也会写入地图,因此还需要为编辑器注册(示例中使用了
InitializeOnLoadMethod)。
using System;
using System.IO;
using System.Runtime.CompilerServices;
using RTSTerrainStudio;
using UnityEngine;
/// <summary>Stores a "starting gold" value in every map file, in a custom section "XGLD".</summary>
public static class StartingGoldSection
{
// FourCC, little-endian like the built-in tags: the first character is in the lowest byte.
const int Tag = 'X' | ('G' << 8) | ('L' << 16) | ('D' << 24);
const int PayloadVersion = 1;
/// <summary>Our data for one map.</summary>
public sealed class Data
{
public int startingGold = 500;
}
// One Data object per MapData. Entries disappear automatically together with their map.
static readonly ConditionalWeakTable<MapData, Data> s_Data = new ConditionalWeakTable<MapData, Data>();
/// <summary>Returns the data of a map (created with defaults when the map has none).</summary>
public static Data Get(MapData map) => s_Data.GetOrCreateValue(map);
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoadMethod] // editor: importer and Save
#endif
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] // player and Play Mode
static void Register()
{
// Remove first so a second registration does not add the handlers twice.
RTSMapSerializer.CustomSectionWriter -= Write;
RTSMapSerializer.CustomSectionWriter += Write;
RTSMapSerializer.CustomSectionReader -= Read;
RTSMapSerializer.CustomSectionReader += Read;
}
static void Write(MapData map, Action<int, byte[]> writeSection)
{
if (!s_Data.TryGetValue(map, out Data data)) return; // nothing to save for this map
using (var stream = new MemoryStream())
{
using (var writer = new BinaryWriter(stream))
{
writer.Write(PayloadVersion); // our own version, so the payload can grow later
writer.Write(data.startingGold);
}
writeSection(Tag, stream.ToArray());
}
}
static void Read(MapData map, int tag, byte[] payload)
{
if (tag != Tag) return; // another system's section: ignore it
using (var reader = new BinaryReader(new MemoryStream(payload)))
{
int version = reader.ReadInt32();
if (version >= 1) Get(map).startingGold = reader.ReadInt32();
}
}
}
在游戏代码中通过 StartingGoldSection.Get(terrain.Map).startingGold 使用它。在编辑器中修改该值后,需要将地图标记为已更改(例如调用 terrain.NotifyChanged(MapLayers.Info, terrain.Map.CellBounds),或设置 terrain.HasUnsavedChanges = true),这样地图才会被保存。
在 Unity 编辑器中编辑
本章介绍在 Unity 编辑器中制作地图时用到的全部内容:RTS Terrain Studio 窗口、在 Scene 视图中绘制、键盘快捷键、笔刷、对称、撤销、每个编辑工具及其全部设置、Auto Paint,以及 RTS Terrain 组件本身。
游戏内编辑器使用的也是同一套工具、笔刷和对称设置(参见游戏内编辑器),因此你在这里学到的内容在那里同样适用。
五步完成第一次编辑。如果你从未用过这个包,可以试试以下步骤:
- 通过 创建一个地形。如果项目中还没有图块集,系统会提示你先创建演示内容,接受即可。
- 打开 。
- 在 Height 选项卡中单击 Raise / Lower,编辑会自动开启。
- 在 Scene 视图中将鼠标移到地形上,按住鼠标左键,就会隆起一座小山。按住 Shift 则改为向下挖掘。
- 按 2 切换到悬崖工具,在小山上单击以抬升出一块高地,完成后按 Esc。按 Ctrl+Z 可撤销任何一步。
RTS Terrain Studio 窗口
该窗口是编辑器的控制中心。它本身不保存任何编辑状态,而是显示并修改一个共享的编辑会话;Scene 视图、Scene 视图叠加层以及 RTS Terrain 组件的 Inspector 也都使用这个会话。你在任何一处所做的更改都会立即反映到其他地方。
你可以通过以下任一方式打开该窗口:
- RTS Terrain 组件 Inspector 中的 Open RTS Terrain Studio 按钮
- Scene 视图中 RTS Terrain Studio 叠加层上的窗口按钮

窗口分为四个区域:顶部的标题栏、选项卡栏、可滚动的页面,以及底部的状态栏。
标题栏
标题栏显示你正在编辑的地形和地图,并提供文件命令和撤销命令。
| 控件 | 作用 |
|---|---|
| 地形选择器 | 选择窗口和 Scene 视图工具要编辑的地形。它会列出已打开场景中的所有 RTS Terrain,并提供一个 Create New Terrain 条目。在 Hierarchy 窗口中选中某个地形,或在 Scene 视图中单击另一个地形,也会切换当前编辑的地形。切换之前,会先提交尚未确认的文本输入。 |
| Select 按钮 | 在 Hierarchy 窗口中选中地形 GameObject,并将其高亮定位(ping)。 |
| 地图名称和圆点 | 地图的名称(来自 Map 选项卡)。名称旁的圆点表示地图有未保存的更改。名称下方显示地图尺寸和 .rtsmap 资源的名称。 |
| Save | 将地图写入对应的 .rtsmap 资源。如果没有地图资源,其行为与 Save As 相同。存在未保存的更改时,该按钮会高亮显示。如果文件为只读或已被锁定,会弹出对话框说明问题,并提供 Save As 选项。 |
| Save As… | 将地图另存为项目中的一个新 .rtsmap 资源,并将其指定给该地形。 |
| Open… | 在此地形上打开项目中的另一个 .rtsmap 资源。Assets 文件夹以外的文件必须先复制到项目中。 |
| Reload | 从地图资源重新加载地图,并丢弃未保存的更改。需要有地图资源。 |
| Undo / Redo | 撤销或重做上一次地形编辑。按钮上会显示步骤名称,例如 Undo Raise / Lower。参见撤销与重做。 |
| ? | 打开本手册中关于编辑器窗口的章节。 |
| Editing On / Editing Off | 开启或关闭 Scene 视图中的地形编辑。如果尚未选择任何工具,开启编辑时会自动选中当前显示的选项卡中的第一个工具。 |
如果当前地图有未保存的更改或撤销历史,Open… 和 Reload 在替换它之前会先询问你,因为加载地图会清空撤销历史。在 Play 模式下,会有一条警告提醒你:除非保存地图,否则退出 Play 模式后所做的编辑都会丢失。
当场景中还没有地形时,标题栏会改为显示 Create Terrain 按钮。它通过菜单项 创建一个带有植被、水体和装饰物组件的 RTS Terrain GameObject。
选项卡栏
选项卡栏共有十个选项卡:八个工具类别 Height、Cliffs、Paint、Vegetation、Decor、Water、Pathing 和 Markers,后面是 Map 和 Settings。你当前正在使用的绘制工具所属的类别会用一个小圆点标出。窗口会记住你上次使用的选项卡。
当窗口拥有键盘焦点时,按 1 到 8(小键盘上的数字键也可以)可在各个类别选项卡之间切换。在文本框中输入时,这些按键会被忽略。
工具页面
每个类别选项卡中都有一列纵向排列的卡片。单击卡片标题即可将其折叠。
| 卡片 | 内容 |
|---|---|
| Category Tools | 工具列表。单击某个工具即可在 Scene 视图中用它进行绘制;再次单击当前工具则停止编辑。将鼠标悬停在工具上可查看其说明和修饰键。你自己编写的自定义工具会自动出现在这里。 |
| 工具卡片 | 显示所选工具的名称、说明和提示,一个可打开本手册对应章节的 ? 按钮,以及一行实时状态信息。将鼠标移到地形上即可看到状态信息,例如高度工具显示 Ground 4.25 m | Fine +0.25 m | Level 2,悬崖工具显示 Level 2 → 3。 |
| Getting Started | 在未选择该类别中的任何工具时显示。如果你正在使用其他类别的工具绘制,这里会显示 Active tool: X,并提供 Go to Tool 按钮。 |
| Brush | 形状、半径、强度、硬度、衰减曲线、旋转、随机旋转、间距和抖动。所有笔刷工具共享这些设置。对于不使用笔刷的工具,此卡片会隐藏。参见笔刷。 |
| Symmetry | 对称模式和对称中心(拖拽工具不显示此卡片)。参见对称。 |
| Tool Settings | 所选工具的设置,根据工具的代码自动生成。即使重启编辑器,这些设置也会保留。工具参考中的表格列出了全部设置项。 |
| 类别操作 | 部分类别特有的额外卡片:Auto Paint Whole Map(Paint)、Selection(Decor,仅在 Select Decor 处于激活状态时)、Water at Cursor(Water)和 Pathing Overlay(Pathing)。 |
编辑暂停时,会出现一条横幅提示 Editing is paused – the Scene View uses Unity's tools。单击其中的 Resume 按钮即可重新开启编辑。

工具设置与选择面板
设置卡片会根据每个设置项的类型为其选择相应的控件:
| 设置项类型 | 控件 |
|---|---|
| 有取值范围的数值 | 带数值输入框的滑块 |
| 开/关值 | 开关 |
| 选项(枚举) | 下拉菜单 |
| 颜色 / 文本 | 颜色字段 / 文本框 |
| 地面图层 | 图块集中各地面图层的选择面板,带纹理缩略图。空槽位也会显示,其工具提示会说明如何填充。 |
| 悬崖类型 | 图块集中各悬崖图层的选择面板。 |
| 悬崖层级 | 层级 0 到 15 的按钮。每个按钮的工具提示会显示该层级的地面高度,选择面板还会标出光标下方的层级。 |
| 玩家 | 16 种玩家颜色(Red、Blue、Teal、Purple、Yellow、Orange、Green、Pink……)。 |
| 细节、树木或装饰物原型 | 库中原型的列表,带预览图和搜索框;装饰物还带有类别筛选。单击选中一个原型,Ctrl+单击添加或移除一个原型,Shift+单击选择一个范围。None 会清空选择,此时放置工具将使用库中的第一个原型。 |
| 资源引用 | 对象字段(例如 Auto Paint 配置文件)。在类型允许的情况下,也可以指定场景对象。 |
如果选择面板需要某个地形尚未配置的资源,卡片会说明缺少什么,并提供用于创建该资源的按钮:Create Tileset、Create Vegetation Library 或 Create Decor Library。你也可以在旁边的字段中指定一个现有资源。每个选择面板还有一个 Edit 按钮,它会选中对应的图块集或库,方便你在 Inspector 窗口中添加条目。
在 Scene 视图中用 Ctrl+单击拾取的值也会同步更改窗口中的设置,例如所选的地面图层或整平高度。
状态栏
窗口底部的状态栏从左到右依次显示:
- 鼠标所指的位置:单元格、世界坐标、悬崖层级、地面高度、水深(有水时)以及主导地面图层的名称;
- 该点的通行标记(可行走、可建造等);
- 笔刷信息,例如 Brush 6.0 m · 50%;
- 撤销历史,例如 Undo 12 (+2) · 48 MB:分别表示可撤销的步骤数、可重做的步骤数,以及它们占用的内存。
Map 选项卡
Map 选项卡包含作用于整张地图的设置和操作。除非对话框另有说明,所有更改都可以撤销。顶部的 ? 按钮会打开本节。
Map
| 设置项 | 说明 |
|---|---|
| Name | 地图名称,显示在地图浏览器和游戏大厅中,也会显示在窗口标题栏中。 |
| Author | 地图的作者。 |
| Description | 向玩家显示的简短说明。 |
| Max Players | 推荐的玩家人数(1–16)。 |
| Map Asset | 该地形加载和保存所用的 .rtsmap 资源。指定其他资源会打开那张地图,并像 Open… 一样先请你确认。 |
字段下方显示地图尺寸、Splat 层、细节层和通行层的每单元格分辨率,以及地图占用的内存。在这些字段中输入内容后,离开字段时会生成一个撤销步骤(Edit Map Info)。
New Map
用一张新的平坦地图替换地形上的地图。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| 尺寸预设 | – | 一组按钮,用于创建边长为 64、96、128、192、256、384 或 512 个单元格的正方形地图。 |
| Width / Height | 取自地形的 Default Map(128) | 以单元格为单位的尺寸。必须是 16 的倍数,范围为 16 到 1024。 |
| Cell Size | 2 m | 单个单元格的尺寸,以米为单位。经典 RTS 地图大约使用 2 m。 |
| Cliff Level Height | 2 m | 一个悬崖层级在世界空间中的高度。 |
| Base Cliff Level | 2 | 整张地图的初始悬崖层级。设为 2 可以留出向下挖掘湖泊和山谷的空间。 |
| Base Ground Layer | 0 | 初始时覆盖整张地图的地面图层。 |
| Splat | 4 | 每个单元格的地面涂层纹素数(1、2、4 或 8)。4 是不错的默认值;8 可以实现非常精细的绘制效果。 |
| Details | 2 | 每个单元格的草地密度纹素数(1、2 或 4)。 |
| Pathing | 2 | 每个地图单元格包含的通行单元格数(1、2 或 4)。数值越高,单位的放置就越精确。 |
| Name | New Map | 新地图的名称。 |
在你单击 Create New Map 之前,会有一行摘要显示最终的世界尺寸。
创建新地图会清空撤销历史,并且无法撤销。旧的地图资源会与地形解除关联,但其内容不会改变。使用 Save As… 可将新地图保存为单独的文件。
Resize
在保留现有内容的前提下扩大或缩小地图。
- 输入以单元格为单位的 New Width 和 New Height(16 的倍数)。
- 单击九个锚点按钮之一。箭头表示地图扩展的方向;现有内容会保留在锚定的一侧。
- 为新增区域选择 Fill Level(悬崖层级)和 Fill Layer(地面图层)。
- 单击 Resize Map。按钮上方的提示会显示内容的移动距离以及新的世界尺寸。
缩小地图会删除新区域以外的地形、树木、装饰物和标记点。调整尺寸会清空撤销历史。
Generator
根据种子生成完整的地图布局,包括高地、坡道、水体、地面涂层、植被、装饰物和出生点。相同的种子和设置总是生成相同的地图。
| 控件 | 说明 |
|---|---|
| Seed、Style、Symmetry | 种子、风格(Plains、Hills、Highlands、Islands、Canyon、Arena)以及布局的对称方式(默认值为 Rotate 2,即 180° 布局;参见对称)。 |
| Noise、Cliffs、Ramps、Water、Border、Paint、Vegetation、Decor、Start Locations | 折叠栏,包含各生成阶段的详细生成器设置。 |
| Randomize Seed | 随机选取一个新种子。 |
| Apply Style Preset | 加载所选风格的噪声、悬崖、坡道和水体参数。 |
| Reset | 将所有生成器设置重置为所选风格的默认值。 |
| Generate | 替换高度、悬崖、水体、地面涂层、植被、装饰物和出生点,但保留地图尺寸。整个操作是一个撤销步骤。 |
地图生成器作用于窗口标题栏中绑定的地形。如果当前选中的是另一个地形,会弹出对话框,要求你先在标题栏中选择这个地形。生成过程中会显示进度条,完成后按钮下方会显示生成内容的摘要。
Heightmap
以 16-bit PNG 或 RAW 文件导入或导出地形高度,例如与 World Machine、Gaea 或 Photoshop 配合使用。
| 设置项 | 说明 |
|---|---|
| Mode(导入) | Fine Heights Only:在现有悬崖层级之上叠加丘陵起伏。Cliff Levels And Fine Heights:将图像拆分为悬崖层级和精细高度,使地面与图像完全一致(最适合台阶状的高度图;跨越层级的平滑斜坡会变成小悬崖)。Absolute Heights Keep Levels:图像表示绝对地面高度,但保留悬崖;可用于重新导入你在其他软件中编辑过的 Ground Height 导出图。 |
| Height Scale | 完整 16 位取值范围所代表的米数。0 = 自动。 |
| Height Offset | 导入:为每个高度增加的米数。导出:写入为 0 的高度值。 |
| Flip Vertically | 沿 Z 轴翻转图像。如果地图看起来呈镜像,就开启此项。 |
| RAW Width / RAW Height | RAW 文件的尺寸。0 = 根据文件大小推算(仅限正方形文件)。 |
| RAW Little Endian | RAW 文件的字节序。Windows 和大多数工具使用小端序。 |
| Source(导出) | Ground Height:悬崖层级加精细高度。Fine Heights:仅包含层级之上的精细高度。 |
可用的按钮有 Import PNG…(8 位或 16 位灰度 PNG、JPG 或 EXR)、Import RAW…、Export PNG…、Export RAW… 和 Export Splat PNG…(以主导图层颜色表示地面涂层的图像)。尺寸不同的图像会被重新采样。地图在每个方向上的顶点数都比单元格数多一个,因此尺寸为 256 的地图使用 257 × 257 个采样点。每次导入对应一个撤销步骤。
Minimap
为地图烘焙一张小地图图像。选择长边的 Size(128、256、512 或 1024 px),并调整 Bake Options:地面颜色(主导图层或混合图层)、悬崖层级亮度、山体阴影、悬崖、水体、树木、装饰物和出生点。Bake Preview 在卡片中显示结果;Save PNG… 会烘焙图像并将其保存为文件。如果烘焙后你又编辑了地图,卡片会提示你重新烘焙。
Validation
Validate Map 会检查地图中的问题,例如缺失纹理、出生点无法到达或触及性能限制。每个问题都会以错误、警告或信息的形式列出,并附有修复提示。单击带有位置信息的问题,即可在 Scene 视图中显示该位置。如果验证后你又编辑了地图,卡片会说明该列表描述的是最近几次编辑之前的地图。
Environment
太阳光、环境光和雾效存储在地图中,并由 RTS Environment 组件应用到场景。
| 设置项 | 说明 |
|---|---|
| Sun Colour | 太阳光的颜色。 |
| Sun Intensity | 太阳光的强度(0–8)。 |
| Sun Rotation | 以度为单位:x = 俯仰角(地平线以上的高度角),y = 偏航角。 |
| Ambient Colour | 环境光(天空光)的颜色。 |
| Fog | 启用距离雾效。 |
| Fog Colour / Fog Density | 指数雾效的颜色和密度(0–0.05)。 |
Capture From Scene 将当前场景的光照复制到地图中(计为一个撤销步骤)。Apply 立即将地图的环境设置应用到场景,Select Component 则会选中 RTS Environment 组件。如果地形上没有 RTS Environment,卡片会提供 Add Environment Component 按钮。
Settings 选项卡
| 卡片 | 内容 |
|---|---|
| View | Show Grid(在正在编辑的地形上显示单元格栅格,也可按 G)、Show Markers(即使没有激活任何标记点工具,也显示出生点、区域和标记点)、Pathing Overlay(Off、Walkability、Buildability、Flyability、Water、Combined)以及 Overlay Opacity。 |
| Terrain Component | 正在编辑的 RTS Terrain 组件的完整 Inspector(参见 RTS Terrain 组件),以及一个 Select Terrain 按钮。 |
| Undo History | Max Steps(10–1000,默认 100)、Memory Limit (MB)(32–4096,默认 512)、当前内存占用以及 Clear History。参见限制与内存。 |
| Shortcuts | Scene 视图快捷键及其当前的按键绑定(从 Unity 的 Shortcut Manager 中读取)。Edit Shortcuts… 会打开 Shortcut Manager;更改绑定后,Refresh 会重新读取绑定。 |
| Help & Maintenance | Open Documentation、Validate Project(运行 )以及 Reset All Tool Settings;后者会将笔刷、对称以及所有工具的设置重置为默认值,并清空地形的撤销历史。 |
| About | 产品版本和 Unity 版本。 |
在 Scene 视图中绘制
所有编辑都在 Scene 视图中进行。开启地形编辑并选中一个工具后,鼠标左键会使用该工具进行绘制,而不是选择对象。你可以通过窗口中的 Editing 按钮、叠加层中的 Start Editing 按钮、Inspector 窗口中的 Edit Terrain 按钮,或直接单击某个工具来开启编辑。按 Esc 可关闭编辑。
鼠标与修饰键
| 输入 | 操作 |
|---|---|
| 鼠标左键 | 使用当前工具绘制(按下、拖动、松开)。一次笔触对应一个撤销步骤。许多工具在你按住鼠标不动时会持续生效,例如 Raise / Lower 会不断堆高山丘。 |
| Shift(按住) | 反转工具效果:降低而非升高,擦除而非绘制,移除而非放置。笔刷光标会变得偏红,状态面板显示 (inverted)。 |
| Ctrl+单击 | 拾取光标下的值而不进行绘制:高度、地面图层、悬崖层级、悬崖类型或水面高度。没有拾取功能的工具会显示 This tool has nothing to pick,并且不会绘制。 |
| Alt+拖动 | 照常环绕旋转摄像机,始终不会绘制。 |
| 鼠标右键 / 中键 | 保留其在 Unity 中的功能(飞行、平移)。 |
| Hand 工具(Q) | 使用 Unity 的 Hand 工具时,左键拖动会平移摄像机,不会绘制。状态面板显示 Hand tool (Q) active: press W to paint, or drag to pan。 |
| Esc | 执行以下第一个适用的操作:取消正在进行的装饰物手柄拖动;取消正在进行的笔触(撤回其更改);清空装饰物选区(Select Decor);或停止编辑。Esc 由拥有键盘焦点的 Scene 视图读取(先在视图中单击一下);仅将鼠标悬停在视图上是不够的。当 RTS Terrain Studio 窗口拥有焦点时,Esc 会取消笔触并关闭编辑。 |
| 单击另一个地形 | 将该地形设为正在编辑的地形(通知显示 Editing X)。鼠标悬停在该地形上时,状态面板显示 Click to edit 'X'。 |
在 macOS 上,本手册中所有提到 Ctrl 的地方都可以使用 Cmd。
指针与地形的命中通过数学计算得出,因此不需要碰撞体。笔刷光标由地形着色器绘制,位置与你实际绘制的位置完全一致。打开多个 Scene 视图时,只有鼠标下方的视图会进行绘制,并且一次笔触始终停留在它开始时所在的视图中。
只有满足以下条件的地形才能编辑:已启用、已加载地图,并且属于当前舞台(Stage)。在预制件模式下,请编辑预制件内部的地形。
RTS Terrain Studio 叠加层
只要场景中包含 RTS Terrain,Scene 视图中就会出现一个紧凑的 RTS Terrain Studio 叠加层。无需打开完整窗口,它就能提供最常用的控件:
- Start Editing / Editing (Esc to stop) 按钮、一个用于打开窗口的按钮,以及一个选项菜单;
- 正在编辑的地形(有未保存的更改时显示 Terrain: X • unsaved);
- 八个类别按钮(按键 1–8)以及所选类别中的工具;
- Size(一个便于精细调节小笔刷的滑块,外加一个精确数值字段)和 Strength;
- Symmetry 下拉菜单,当所选模式需要正方形地图时会显示警告;
- Grid 和 Markers 开关,以及 Pathing 叠加层下拉菜单。
选项菜单包含 Show Status Panel、Save Map、Save Map As…、Save Maps When Saving the Scene、Offer Save As for New Maps、Shortcuts… 和 Documentation。与所有 Unity 叠加层一样,它可以移动、停靠或折叠,Unity 会记住它的位置。
Unity 的 Tools 叠加层
选中 RTS Terrain 时,Unity 自带的 Tools 叠加层中会出现一个 RTS Terrain Studio 工具。激活该工具会将所选地形设为正在编辑的地形,并开启编辑。切换到其他 Unity 工具会再次关闭编辑。
例外是 Select Decor:当它处于激活状态时,Unity 的 Move、Rotate、Scale 和 Transform 工具用于选择装饰物选区所使用的手柄,因此编辑会保持开启。参见装饰物选区手柄。
状态面板与通知
Scene 视图底部的状态面板显示两行信息:
- 当前工具(按住 Shift 时显示 (inverted))、笔刷大小和强度(仅限笔刷工具)以及对称模式,当该模式需要正方形地图时会显示警告;
- 鼠标悬停处的单元格、悬崖层级和高度,工具状态或提示,以及此刻按 Esc 会执行的操作(Esc: cancel stroke、Esc: cancel edit、Esc: clear selection 或 Esc: stop editing)。
使用叠加层选项菜单中的 Show Status Panel 可以隐藏或显示状态面板。快捷键执行后,会在 Scene 视图中以简短通知确认所执行的操作,例如 Brush 8.0 m、Strength 55%、Grid on 或 Stroke cancelled。如果 Unity 启动时编辑仍处于上次会话遗留的开启状态,会显示一次性提示 Terrain editing is on (Esc to stop)。
预览与参考线
- 笔刷光标:以工具的颜色投射到地形上,每个对称副本处也各显示一个光标。
- 半透明预览:在 Single 模式下,Place Trees 和 Place Decor 会在光标处以半透明方式预览下一个将要放置的对象。
- 对称参考线:开启对称模式时显示的品红色轴线和中心点。
- 标记点:出生点(以玩家颜色显示)、区域和标记点;在开启 Show Markers 或激活标记点工具时绘制。
- 栅格(G)和通行叠加层(P):由地形着色器绘制。
装饰物选区手柄
激活 Select Decor 后,单击对象即可选中,拖出矩形可进行框选,按住 Shift 可添加到选区或切换选中状态。选区会在所选对象的中心显示 Unity 风格的手柄:
| Unity 工具 | 手柄 |
|---|---|
| Move(W) | 位置手柄。 |
| Rotate(E) | 偏航圆盘:装饰物只绕竖直向上的轴旋转。按住 Ctrl 可吸附。 |
| Scale(R) | 等比缩放手柄。激活 Select Decor 时,R 快捷键会选择此工具,而不是切换随机旋转。 |
| Transform(Y) | 位置手柄和偏航圆盘。 |
| Rect(T) | 位置手柄。 |
| Hand(Q) | 无手柄;左键拖动平移摄像机。 |
Delete 删除选中的对象。Ctrl+D 复制选中的对象,副本会在 X 和 Z 方向上各偏移一个单元格,以便看清;之后可用手柄移动它们。复制后会显示 Duplicated 3 objects 之类的通知加以确认。Ctrl+A 选中所有未锁定的装饰物对象,F 将视图聚焦到选区。这些是 Unity 自带的 Delete、Duplicate、Select All 和 Frame Selected 命令,因此会遵循你在 Shortcut Manager 中设置的绑定。每次手柄拖动对应一个撤销步骤。拖动过程中按 Esc 会撤回该拖动,且不会产生撤销步骤。更多命令位于 Decor 选项卡的 Selection 卡片中(参见 Decor 工具)。
保存与保存保护
编辑期间,地图保存在内存中,而不在场景文件里。因此,Unity 自带的 Save scene? 提示并不涵盖地图。RTS Terrain Studio 为此添加了自己的保护机制:
| 时机 | 结果 |
|---|---|
| 保存场景 | 场景中各地形的地图会自动保存到各自的 .rtsmap 资源中(叠加层选项 Save Maps When Saving the Scene,默认开启)。对于没有地图资源的地形,每个编辑器会话会提示一次 Save As…,并提供 Not Now 和 Never Ask Again 两个选项(叠加层选项 Offer Save As for New Maps)。 |
| 进入 Play 模式 | 询问 Save map changes before entering Play Mode?,选项为 Save、Cancel(留在编辑模式)和 Don't Save。Unity 会重新加载场景,地图也随之从其资源重新加载。如果 Enter Play Mode Options 同时禁用了场景重载和域重载,则跳过此提示。 |
| 退出 Play 模式 | 询问 Save map changes made in Play Mode?,选项为 Save 或 Discard。以下情况会出现此提示:在 Play 模式下用 Scene 视图工具编辑过地图;或者场景中包含游戏内编辑器(RTSRuntimeEditor)且地图有未保存的更改。在其他场景中,诸如树木被采伐之类的游戏过程更改不会触发此提示。 |
| 关闭场景、退出 Unity | 询问是否保存已更改的地图。 |
覆盖地图文件之前,本包会请求你的版本控制系统签出该文件。如果签出失败,会弹出对话框说明应如何处理。在批处理模式下或构建 Player 期间,这些保护机制从不显示对话框。
构建脚本和编辑器测试可以临时关闭保存保护:
using RTSTerrainStudio.Editor;
using UnityEditor;
static class MyBuildSteps
{
[MenuItem("Tools/My Project/Run Map Batch")]
static void RunMapBatch()
{
// No save prompts while this scope is open. Scopes can be nested.
using (RTSMapSaveGuard.Suppress())
{
// ... open scenes, edit or generate maps, enter and leave Play Mode ...
}
}
}
键盘快捷键
Scene 视图快捷键注册在 Unity 的 Shortcut Manager 中的 RTS Terrain Studio 类别下。只有同时满足以下条件时它们才会生效:地形编辑已开启;Scene 视图或某个 RTS Terrain Studio 编辑器窗口拥有键盘焦点(或鼠标位于 Scene 视图上方);并且你没有在文本或数字字段中输入。不在编辑状态时,R 或数字键等按键保持其在 Unity 中的常规功能。
| 默认按键 | Shortcut Manager 条目 | 操作 |
|---|---|---|
| [ | RTS Terrain Studio/Brush Smaller | 笔刷半径 × 0.87。 |
| ] | RTS Terrain Studio/Brush Larger | 笔刷半径 × 1.15。 |
| - | RTS Terrain Studio/Strength Down | 笔刷强度 − 5%。 |
| = | RTS Terrain Studio/Strength Up | 笔刷强度 + 5%。 |
| R | RTS Terrain Studio/Toggle Random Rotation | 切换笔刷随机旋转。激活 Select Decor 时,改为选择 Unity 的 Scale 工具。 |
| G | RTS Terrain Studio/Toggle Grid | 显示或隐藏单元格栅格。 |
| P | RTS Terrain Studio/Cycle Pathing Overlay | 按 Off → Walkability → Buildability → Combined → Off 的顺序循环切换。 |
| 1 | RTS Terrain Studio/Height Tools | 选择该类别中的第一个工具。再次按下该键可在该类别的工具之间循环切换。 |
| 2 | RTS Terrain Studio/Cliff Tools | |
| 3 | RTS Terrain Studio/Paint Tools | |
| 4 | RTS Terrain Studio/Vegetation Tools | |
| 5 | RTS Terrain Studio/Decor Tools | |
| 6 | RTS Terrain Studio/Water Tools | |
| 7 | RTS Terrain Studio/Pathing Tools | |
| 8 | RTS Terrain Studio/Marker Tools |
以下按键和鼠标操作由本包自身处理,无法重新绑定:
| 按键 | 位置 | 操作 |
|---|---|---|
| Shift(按住) | Scene 视图 | 反转工具效果(降低、擦除、移除)。 |
| Ctrl+单击(macOS 上为 Cmd+单击) | Scene 视图 | 拾取一个值(高度、图层、层级、悬崖类型、水面高度)。 |
| Esc | 拥有键盘焦点的 Scene 视图 | 取消手柄拖动或笔触,清空装饰物选区,或停止编辑。 |
| 1–8(小键盘亦可) | RTS Terrain Studio 窗口 | 切换类别选项卡。 |
| Esc | RTS Terrain Studio 窗口 | 取消笔触并停止编辑。 |
| Tab、Enter、Space | RTS Terrain Studio 窗口 | 在工具和选项卡之间移动焦点;激活拥有焦点的项。 |
| Ctrl+单击、Shift+单击 | 原型列表 | 添加或移除单个原型;选择一个范围。 |
以下按键属于 Unity 自带的快捷键和命令。RTS Terrain Studio 会响应它们,但它们遵循你在 中 Unity 类别(Main Menu/Edit、Tools)下设置的绑定:
| 默认按键 | 位置 | 操作 |
|---|---|---|
| Ctrl+Z / Ctrl+Y | Unity 中任意位置 | 通过 Unity 的 Edit 菜单撤销 / 重做地形编辑。 |
| W E Y | Scene 视图,Select Decor | Unity 的 Move、Rotate 和 Transform 工具:用于选择装饰物选区的手柄。 |
| Delete、Ctrl+D、Ctrl+A、F | Scene 视图,Select Decor | Delete、Duplicate、Select All(未锁定的装饰物)和 Frame Selected。 |
Select Decor 下的 R 是本包自身的 RTS Terrain Studio/Toggle Random Rotation 条目:当 Select Decor 处于激活状态时,它会切换到 Unity 的 Scale 工具,而不是切换随机旋转。
重新绑定快捷键
- 打开 (macOS 上为 ),或单击 Settings 选项卡中的 Edit Shortcuts…。
- 选择 RTS Terrain Studio 类别。
- 为上面列出的条目指定新按键。
- 单击 Settings 选项卡中的 Refresh,即可在窗口中看到新的绑定。
如果某个按键与你的设置中的其他快捷键冲突,例如 Scene 视图中绑定在 2 上的 Toggle 2D Mode,Unity 会询问一次要使用哪一个。你可以在 Shortcut Manager 中重新绑定其中任意一个。
笔刷
所有笔刷工具共用同一个笔刷。你可以在任意工具页面的 Brush 卡片中、在叠加层中(Size 和 Strength)或通过快捷键来设置它。笔刷设置会在编辑器会话之间保留。
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Shape | Circle、Square、Diamond、Custom | Circle | 笔刷的轮廓。Square 和 Diamond 可以旋转。Custom 使用 Brush Shape 资源。 |
| Custom Shape | – | 无 | Custom 形状所使用的 Brush Shape 资源。单击 Custom 形状按钮后,卡片会将项目中的每个 Brush Shape 资源显示为缩略图,并附带一个 Custom Shape 字段。单击某个缩略图(或在该字段中指定资源)即可使用它。 |
| Radius | 0.25–128 m | 6 m | 笔刷半径,以米为单位。滑块采用对数刻度,便于精细调节小笔刷。快捷键为 [ 和 ]。 |
| Strength | 0.01–1 | 0.5 | 每个笔印的效果强度。快捷键为 - 和 =。 |
| Hardness | 0–1 | 0.35 | 0 = 边缘在整个半径范围内柔和过渡,1 = 硬边缘。开启 Falloff Curve 时禁用。 |
| Falloff Curve | 开/关 | 关 | 使用自定义曲线代替 Hardness。 |
| Curve | – | 从 1 到 0 的缓入缓出 | 从笔刷中心(x = 0)到边缘(x = 1)的权重。 |
| Rotation | 0–360° | 0 | 正方形、菱形和自定义形状的旋转角度,从上方俯视为顺时针。选择 Circle 形状或开启 Random Rotation 时禁用。 |
| Random Rotation | 开/关 | 关 | 随机旋转每个笔印。非常适合搭配自定义图章使用。快捷键为 R。 |
| Spacing | 0.05–2 | 0.2 | 相邻笔印之间的距离(相对于半径)。数值越小,笔触越平滑。 |
| Jitter | 0–1 | 0 | 每个笔印的随机偏移量(相对于半径)。 |
笔印的权重计算方式
每个笔印都会为每个顶点、Splat 纹素或对象分配一个介于 0 和 1 之间的权重,工具再将该权重与 Strength 相乘。使用 Hardness 时,权重在 hardness × radius 范围内为 1,然后平滑衰减,到边缘处降为 0。开启 Falloff Curve 时,改由曲线定义权重:x 是到中心的距离(0 = 中心,1 = 边缘),y 是权重。对于正方形和菱形形状,“距离”按形状轮廓计算,因此正方形笔刷沿各条边的强度是均匀的。
有些工具把笔刷当作硬选区使用:悬崖、水体和通行工具只更改权重至少为 0.5 的部分,或笔刷中心下方的顶点。此时,柔和边缘意味着“范围稍小”,而不是“效果更弱”。
间距、抖动与重复
- Spacing:拖动时,沿鼠标路径每隔
radius × spacing米放置一个笔印,但间隔至少为十分之一个单元格。吸附到顶点的工具所放置的笔印之间至少相隔半个单元格。 - Jitter:每个笔印都会随机移动一段距离,最大为
jitter × radius。抖动仅适用于使用笔刷的工具。 - 重复:许多工具在你按住鼠标不动时会持续施加笔印,例如 Raise / Lower 每秒施加 30 个笔印。工具表格中列出了哪些工具具有此行为。
旋转约定
RTS Terrain Studio 中的所有旋转(笔刷旋转、树木和装饰物的偏航角、出生点和标记点的朝向)都使用相同的约定:以度为单位,从上方俯视为顺时针,0° 指向 +Z(北),90° 指向 +X(东)。因此,旋转后的笔刷或对象的对称副本,其旋转和镜像方式与其位置完全一致。开启 Random Rotation 时,每个笔印抽取一个随机角度,并由其所有对称副本共享,因此对称笔触始终保持对称。
自定义笔刷形状
Brush Shape 资源可以将任意灰度纹理转换为笔刷图章:白色区域施加完整效果,黑色区域不产生效果。
- 导入一张灰度纹理,例如岩石轮廓或泼溅图案。该纹理无需设为可读。
- 通过 创建该资源,并指定这张纹理。
- 在 Inspector 窗口中检查预览。预览会按笔刷实际使用的方式精确显示烘焙后的遮罩,并显示其覆盖率。
- 在 Brush 卡片中,单击 Custom 形状按钮。卡片随后会将每个 Brush Shape 资源显示为缩略图;单击新形状的缩略图即可。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Texture | 无 | 灰度或 Alpha 纹理。白色 = 完整效果。 |
| Use Red Channel | 开 | 读取红色通道。关闭后改为读取 Alpha 通道。 |
| Invert | 关 | 反转遮罩。 |
| Resolution | 64 | 烘焙遮罩的分辨率(16–256)。 |
每次更改后,遮罩都会自动烘焙。Bake Mask 会立即烘焙遮罩。自定义图章覆盖以笔刷中心为中心的正方形区域,并且仍会与硬度或衰减曲线相乘。降低硬度可获得柔和的图章边缘,设为 1 则按原样使用图章。
对称
对称功能会在镜像或旋转后的位置重复每个笔印、每次单击和每个放置的对象。制作公平的竞技地图就靠它:每位玩家都能获得相同的山丘、悬崖、坡道、树木和金矿。在 Symmetry 卡片或叠加层中选择模式。Scene 视图中的品红色参考线会显示对称轴和中心。
对称适用于笔刷工具和点击工具。拖拽工具(Regions 和 Select Decor)会忽略对称,并且当其中任一工具处于激活状态时,窗口会隐藏 Symmetry 卡片。镜像区域需要手动创建。
| 窗口中的名称 | 叠加层 / 代码中的名称 | 副本数 | 作用 | 适用场景 |
|---|---|---|---|---|
| Off | None | 1 | 只有笔刷本身进行绘制。 | 自由形态地图、战役地图、不应重复的细节。 |
| Mirror X | Mirror X (MirrorX) | 2 | 沿穿过中心的竖直轴进行左 ↔ 右镜像。 | 东西对峙的两名玩家或两支队伍。 |
| Mirror Z | Mirror Z (MirrorZ) | 2 | 沿水平轴进行上 ↔ 下镜像。 | 南北对峙的两名玩家或两支队伍。 |
| Mirror XZ | Mirror XZ (MirrorXZ) | 4 | 沿两条轴同时镜像。 | 四角基地,或每支队伍各占一侧的 2v2 地图。 |
| Rotate 180° | Rotate 2 (Rotate2) | 2 | 绕中心点呈中心对称。 | 经典的 1v1 布局,基地位于对角。与镜像不同,旋转能让双方玩家的左右保持一致:位于一名玩家左侧的道路,对另一名玩家而言也在左侧。 |
| Rotate 90° | Rotate 4 (Rotate4) | 4 | 以四分之一圈为单位的旋转对称。需要正方形地图。 | 经典的四人自由混战。 |
| Diagonal | Mirror Diagonal (MirrorDiagonal) | 2 | 沿对角线镜像(x ↔ z)。需要正方形地图。 | 基地位于左上角和右下角的 1v1 地图。 |
Centre 是以归一化地图坐标表示的对称中心:(0.5, 0.5) 为地图中心,也是默认值。Reset 会将其移回该位置。如果移动了中心,某些副本可能会落在地图之外并被跳过;卡片会显示警告。
Rotate 90° 和 Diagonal 会交换 X 轴和 Z 轴。在非正方形地图上,副本会落到地图之外或分布不均。Symmetry 卡片、叠加层和状态面板都会发出警告。请将地图尺寸调整为正方形,或选择其他模式。
副本的行为方式
- 与另一个副本的距离小于半个单元格(或笔刷半径的十分之一,取两者中较大者)的副本会被合并。因此,在对称轴上绘制时,效果绝不会施加两次。
- 放置的树木、装饰物、出生点和标记点会随其副本一起旋转。镜像副本的朝向也会被镜像。
- 旋转和镜像的笔刷及自定义图章会随其副本一起转向(参见旋转约定)。
- Start Locations:开启 Symmetry Assigns Next Players(默认)时,单击一次会放置所选玩家的出生点,其副本则放置后续玩家的出生点。使用 Rotate 90° 并选中 Player 1 时,单击一次即可放置玩家 1 到 4 的出生点。
- Raise / Lower Level 根据原始笔印计算目标层级,因此每个副本绘制的都是同一层级。
- Fill / Replace 在 Replace Dominant Layer 或 Fill All 模式下作用于整张地图,并且只执行一次。Fill Connected 则从每个副本处开始填充。
先开启对称来构建地图的整体形态(悬崖、坡道、水体、出生点),然后关闭对称再添加小装饰,这样地图就不会显得像是复制粘贴出来的。通行叠加层(P)可以快速检查每个基地是否拥有相同的路径。
撤销与重做
每次地形编辑都会记录在地图自身的撤销历史中,该历史与 Unity 的撤销系统相关联。Ctrl+Z、Ctrl+Y 和 菜单对地形编辑的作用与对其他任何更改完全相同,Edit 菜单还会显示步骤名称,例如 Undo Raise / Lower。窗口顶部的 Undo 和 Redo 按钮作用相同,并会让 Unity 的历史保持同步。在此期间撤销对其他对象的更改不会干扰地形历史。
| 操作 | 撤销步骤 |
|---|---|
| 一次笔刷笔触(从按下到松开)或一次单击 | 一个以工具命名的步骤,例如 Paint Layer。所有对称副本都属于同一个步骤。 |
| 装饰物手柄拖动 | 一个步骤(Move Decor、Rotate Decor、Scale Decor)。 |
| Selection 卡片中的按钮(Delete、Duplicate、Lock 等) | 每次单击计为一个步骤。 |
| Generate、高度图导入、Auto Paint Whole Map、Capture From Scene | 各为一个步骤(Generate Map、Import Heightmap、Auto Paint Map 等)。 |
| 地图信息和环境字段 | 离开字段时记录一个步骤(Edit Map Info、Edit Environment)。 |
| 用 Esc 取消的笔触 | 不产生步骤。该笔触的更改会被撤回。 |
| New Map、Resize、Open、Reload、切换到其他地形 | 无法撤销。这些操作会清空历史(其对话框会说明这一点)。 |
限制与内存
每个步骤只存储它所更改的数据层。栅格层(高度、悬崖层级、水体、Splat 贴图、细节、通行覆盖)以编辑区域周围 16 × 16 个单元格的分块形式存储。对象层(树木、装饰物、区域、出生点、标记点、地图信息、环境)以整个列表的副本形式存储,因此在树木非常多的地图上,一次树木笔触占用的内存与地图上的树木总数成正比。所以,一次小范围的高度或绘制笔触只占用几 KB,而生成整张地图则会存储地图的所有数据层。状态栏和 Undo History 卡片会显示当前使用的内存。
你可以在 Settings 选项卡中设置两项限制。超出任一限制时,最旧的步骤会最先被丢弃;最新的步骤始终保留。
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Max Steps | 10–1000 | 100 | 保留的撤销步骤数量。 |
| Memory Limit (MB) | 32–4096 | 512 | 撤销历史可使用的内存。 |
这些限制按用户存储在编辑器偏好设置中,即使窗口关闭也会生效。Clear History 会删除所有地形撤销和重做步骤,例如可在大规模生成后用它释放内存。
在自己的编辑器脚本中实现可撤销的编辑
RTSEditSession.RunOperation 会记录整张地图上指定的数据层,运行你的代码,然后把结果作为一个步骤推入撤销历史。之后就可以像撤销任何工具操作一样,用 Ctrl+Z 撤销它。
using RTSTerrainStudio;
using RTSTerrainStudio.Editor;
using UnityEditor;
static class MyTerrainCommands
{
[MenuItem("Tools/My Project/Raise Whole Map 1 m")]
static void RaiseWholeMap()
{
// The terrain edited by the RTS Terrain Studio window and the Scene View tools.
RTSTerrain terrain = RTSEditorContext.Terrain;
if (terrain == null || terrain.Map == null) return;
RTSEditSession session = RTSEditorContext.Session;
// Records the Heights layer, runs the code and pushes one undo step named "Raise Whole Map".
session.RunOperation("Raise Whole Map", MapLayers.Heights, ctx =>
{
var heights = ctx.Map.Heights; // fine height per vertex, on top of the cliff level
for (int i = 0; i < heights.Length; i++)
heights[i] = TerrainToolUtility.ClampFineHeight(ctx.Map, heights[i] + 1f);
// Tell the terrain which layers changed so meshes, water and pathing are rebuilt.
ctx.Terrain.NotifyChangedAll(MapLayers.Heights);
});
}
}
游戏内编辑器使用相同的撤销历史类型(MapUndoStack),并带有自己的 Undo 和 Redo 按钮。只有在 Unity 编辑器中,它才会与 Unity 的 Ctrl+Z 联动。
工具参考
以下各节逐一介绍八个类别中的所有工具。所有工具在 Unity 编辑器和游戏内编辑器中的用法完全相同。有几条约定对所有工具都适用:
- ID 是工具的唯一标识。可以在代码中使用它,例如
RTSEditorContext.ActivateTool("height.raise")。 - 输入:笔刷工具在拖动时持续绘制。点击工具每次单击执行一次。拖拽工具通过按下、拖动和松开来操作(矩形、框选)。
- 重复频率:按住鼠标按键且不移动时,每秒产生的笔印数。
- 在表格注明的情况下,Shift 用于反转,Ctrl+单击用于拾取。
- 每个设置项都列出了范围和默认值。设置会在编辑器会话之间保留。Settings 选项卡上的 Reset All Tool Settings 可恢复默认值。
悬崖层级与精细高度
每个顶点的地面高度由两部分组成。悬崖层级(0–15)由悬崖工具设置,用于形成垂直崖壁;每个层级高 Cliff Level Height 米(默认 2 m)。精细高度由高度工具设置,在层级之上叠加平滑的丘陵和洼地。精细高度限制在 ±8 个悬崖层级以内(层级高 2 m 时为 ±16 m)。正因为两者相互独立,你才能在起伏的地面上绘制出清晰利落的经典 RTS 风格悬崖。
添加自定义工具
任何派生自 RTSTool(或某个类别基类,例如 HeightTool)并具有公共无参构造函数的类,都会自动出现在 RTS Terrain Studio 窗口、叠加层和游戏内编辑器中。标记了 [ToolSetting] 的公共字段会自动获得 UI。下面的示例在每次单击时将地面升高或降低一个固定步长:
using RTSTerrainStudio;
// Appears on the Height tab after the built-in tools (Order 100).
public sealed class StepTool : HeightTool
{
public override string Id => "mygame.step";
public override string DisplayName => "Step";
public override string Description => "Raises the ground under the brush by a fixed step per click.";
public override string Hint => "Shift: step down";
public override int Order => 100;
// One dab per click; keep the brush panel visible for the radius and falloff.
public override ToolInputMode InputMode => ToolInputMode.Click;
public override bool UsesBrush => true;
[ToolSetting("Step", Min = 0.05f, Max = 4f, Order = 0, Tooltip = "Height change per click in metres at full strength.")]
public float step = 0.5f;
// Called once per click and once for every symmetry copy.
public override void ApplyDab(RTSToolContext ctx, in BrushDab dab)
{
var map = ctx.Map;
// Computes the vertex weights and records the Heights layer for undo.
bool any = BeginVertexEdit(ctx, dab, out var w, out var cells);
using (w) // the weights must always be disposed
{
if (!any) return;
float sign = dab.invert ? -1f : 1f; // Shift inverts
var heights = map.Heights;
for (int z = w.rect.yMin; z < w.rect.yMax; z++)
for (int x = w.rect.xMin; x < w.rect.xMax; x++)
{
float weight = w[x, z];
if (weight <= 0f) continue;
int i = map.VertexIndex(x, z);
heights[i] = TerrainToolUtility.ClampFineHeight(map, heights[i] + sign * step * dab.strength * weight);
}
}
// Rebuilds the chunks, water and pathing of the changed cells.
EndVertexEdit(ctx, cells);
}
}
完整的工具 API(拖拽工具、拾取、Gizmo 和状态文本)参见脚本编写。如果构建使用较高的 Managed Stripping Level,请用 [UnityEngine.Scripting.Preserve] 标记你的工具(参见代码剥离与自定义工具)。
高度工具
高度工具用于雕刻精细高度:在悬崖层级之上塑造丘陵、山谷、道路和自然细节。它们从不改变悬崖层级,因此崖壁始终清晰利落。状态栏会显示光标下的地面高度、精细高度和悬崖层级。
| 工具 | ID | 作用 | 输入与修饰键 |
|---|---|---|---|
| Raise / Lower | height.raise | 升高笔刷下的地面;按住鼠标按键不放可持续堆高丘陵。 | 笔刷,重复频率 30/s。Shift:降低。 |
| Smooth | height.smooth | 柔化地面的凸起和尖锐边缘,且不会抹平崖壁。 | 笔刷,重复频率 20/s。 |
| Flatten | height.flatten | 将地面整平到同一高度,非常适合建筑用地和道路。 | 笔刷,重复频率 20/s。Ctrl+单击:拾取高度。 |
| Set Height | height.set | 将地面设为精确的高度;强度最大时,笔刷中心一个笔印即可达到该高度。 | 笔刷,重复频率 20/s。Ctrl+单击:拾取高度。 |
| Noise | height.noise | 基于带种子的程序化噪声,添加自然的凸起、丘陵或山脊。 | 笔刷,重复频率 10/s。Shift:减去噪声。 |
| Terrace | height.terrace | 将斜坡变成边缘圆润的平坦台阶,就像梯田或层状岩石一样。 | 笔刷,重复频率 15/s。 |
| Erode | height.erode | 模拟物质沿陡坡滑落,使丘陵呈现自然的风化外观。 | 笔刷,重复频率 15/s。 |
Raise / Lower 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Speed | 0.01–2 | 0.15 | 笔刷强度最大时,每个笔印造成的高度变化(米)。 |
Smooth 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Filter Radius | 1–4 | 1 | 参与平均的邻域范围(以顶点为单位)。值越大,去除的地形起伏越大。 |
| Iterations | 1–8 | 2 | 每个笔印的平滑遍数。 |
Flatten 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Target | Stroke Start、Fixed Height | Stroke Start | Stroke Start 使用笔触开始时光标下的高度。Fixed Height 使用 Target Height。 |
| Target Height | −32–32 m | 0 | 相对于悬崖层级的固定精细高度。Ctrl+单击可拾取该值。超出地图精细高度限制(±8 个悬崖层级,层级高 2 m 时为 ±16 m)的值在应用时会被限制在该范围内。 |
| Mode | Flatten、Raise Only、Lower Only | Flatten | 分别为:双向整平;只填高低于目标高度的地面;只削去高于目标高度的地面。 |
Set Height 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Height | −32–64 m | 0 | 要设置的高度,其含义取决于 Height Space。Ctrl+单击可拾取该值。超出地图精细高度限制(±8 个悬崖层级,层级高 2 m 时为 ±16 m)的值在应用时会被限制在该范围内。 |
| Height Space | Fine Height、Ground Height | Fine Height | 相对于悬崖层级的精细高度,或绝对地面高度。 |
Noise 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Noise Type | Fbm、Ridged、Billow | Fbm | Fbm:连绵起伏的丘陵。Ridged:尖锐的山脊。Billow:圆润的凸起。 |
| Scale | 2–256 m | 24 | 最大地形起伏的尺寸。 |
| Amplitude | 0.01–4 m | 0.25 | 强度最大时每个笔印增加的高度。 |
| Octaves | 1–8 | 4 | 细节层数。倍频越多,细节越精细。 |
| Roughness | 0.2–0.8 | 0.5 | 相邻倍频之间的振幅比。值越高,地面越粗糙。 |
| Seed | 0–9999 | 1 | 改变噪声图案。相同的种子总是生成相同的图案。 |
Terrace 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Step Height | 0.1–8 m | 1 | 每级台阶的高度。 |
| Sharpness | 0–1 | 0.7 | 0 = 柔和圆润的台阶。1 = 清晰陡直的台阶立面。 |
| Offset | −4–4 m | 0 | 将所有台阶整体上移或下移。 |
Erode 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Talus Angle | 5–70° | 30 | 坡度大于此角度的斜坡会被侵蚀。 |
| Erosion Strength | 0.05–1 | 0.5 | 每次迭代中移动的多余物质所占的比例。 |
| Iterations | 1–50 | 8 | 每个笔印的侵蚀遍数。 |
制作建筑用地时,使用 Flatten 并设置 Target = Stroke Start:从高度正合你意的位置起笔,然后涂满整个区域。要修一条精确高度的道路,先 Ctrl+单击某个点拾取其高度,将 Target 切换为 Fixed Height,然后绘制。
悬崖工具
悬崖工具用于更改顶点的悬崖层级(0–15),以及层级之间崖壁的外观。笔刷会吸附到顶点并作为硬选区生效,因此你绘制的总是完整的顶点,绝不会出现半个层级。状态栏会显示光标所在的层级和即将绘制的层级,例如 Level 2 → 3。
| 工具 | ID | 作用 | 输入与修饰键 |
|---|---|---|---|
| Raise / Lower Level | cliff.raise | 将地面升高一个悬崖层级;拖动可扩展起笔处所在的高地。 | 笔刷。Shift:降低一个层级。 |
| Set Level | cliff.set | 绘制一个精确的悬崖层级,适合制作大型高地、盆地以及快速搭建粗略布局。 | 笔刷。Ctrl+单击:拾取层级。 |
| Ramp | cliff.ramp | 将悬崖边缘变为可行走的斜坡;横跨高一个层级的悬崖绘制,使单元格的四个角点都带有坡道标记。 | 笔刷。Shift:移除坡道。 |
| Cliff Type | cliff.type | 更改崖壁的材质,例如让岩石悬崖与长满草的悬崖相邻。 | 笔刷。Ctrl+单击:拾取悬崖类型。 |
| Clean Cliffs | cliff.smoothEdges | 移除孤立的单顶点凸柱和凹坑,使悬崖轮廓干净整齐。 | 笔刷。 |
Raise / Lower Level 设置
目标层级取自笔触开始时光标下的顶点:比它高一个层级(按住 Shift 时低一个层级)。拖动时始终绘制同一个层级,因此你可以扩展高地而不会叠加层级。该工具只会升高(或只会降低)顶点。
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Break Ramps | 开/关 | 开 | 移除层级发生变化的顶点上的坡道标记。 |
| Paint Cliff Edges | 开/关 | 开 | 笔触结束时,沿新的悬崖边缘绘制该悬崖类型的边缘图层(参见自动悬崖边缘)。 |
| Edge Width | 0.25–4 个单元格 | 1 | 所绘边缘的宽度,从崖壁开始计算。 |
| Edge Strength | 0.1–1 | 0.9 | 边缘图层在紧贴崖壁处对地面的覆盖强度。 |
| Edge Sides | Both / Top / Bottom | Both | 绘制高地边沿、悬崖底部,或两者都绘制。 |
| Clean Up Old Edges | 开/关 | 开 | 笔触结束后,在已不再有边缘的位置移除边缘图层(悬崖被移动、新增了坡道)。 |
Set Level 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Level | 0–15(选择面板) | 2 | 要绘制的悬崖层级。 |
| Break Ramps | 开/关 | 开 | 移除层级发生变化的顶点上的坡道标记。 |
| 边缘设置 | 同上 | Paint Cliff Edges、Edge Width、Edge Strength、Edge Sides 和 Clean Up Old Edges 的作用与 Raise / Lower Level 中相同。Ramp 和 Clean Cliffs 也有这五个设置项。 | |
Ramp
Ramp 没有设置项。只有当单元格的四个角点都带有坡道标记,并且该单元格恰好跨越一个层级的高差时,它才会成为坡道。状态栏可以帮你检查:它会统计笔刷下有效的坡道单元格数量,提示带有坡道标记但没有层级差的单元格,并在坡道跨越多个层级时发出警告(此时坡道会被拉伸)。
Cliff Type 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Cliff Type | 0–3(选择面板) | 0 | 要绘制的图块集悬崖图层。 |
Clean Cliffs 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Strictness | 5–8 | 8 | 8 个相邻顶点中至少要有多少个处于另一层级,该顶点才会改用那个层级。设为 8 时只移除完全孤立的顶点;更低的值还会把锯齿状的拐角修圆。地图边界上的顶点相邻顶点较少,需要在其中达到相同的比例。 |
| Break Ramps | 开/关 | 开 | 移除层级发生变化的顶点上的坡道标记。 |
自动悬崖边缘
在经典 RTS 地图编辑器中,抬高悬崖时也会同时改变旁边的地面。悬崖工具也会这样做:笔触结束时,每条新悬崖边缘沿线的地面都会用该悬崖类型的边缘地面图层绘制,例如一圈多石的“Rocky Dirt”边沿。边沿沿着圆润的悬崖轮廓分布,在 Edge Width 范围内逐渐淡出,外缘也略带不规则,因此看起来自然,而不是一格一格的方块形状。
- 选中图块集并打开 Cliff Layers。在每张悬崖卡片上选择一个 Edge Layer(演示图块集使用 Rocky Dirt)。
- 选择 Raise / Lower Level、Set Level、Ramp 或 Clean Cliffs,并保持 Paint Cliff Edges 处于开启状态。
- 像平常一样绘制悬崖。松开鼠标按键后,边沿就会出现。
- 一个撤销步骤:层级变化和边缘绘制会一起被撤销。
- 对称:所有对称副本的边缘都会在同一遍处理中一并绘制。
- 坡道:坡道单元格永远不会生成边沿。启用 Clean Up Old Edges 时,横跨边缘绘制坡道会移除该处的边沿。
- 移动过的悬崖:启用 Clean Up Old Edges 后,已消失的边缘(高地被降低或被扩展)上的边沿会随之移除。如果你曾在那里手动绘制过边缘图层,它也会一并被移除。
- 可选:可以为单个工具关闭 Paint Cliff Edges,或将悬崖类型的 Edge Layer 设为 None,从而对整个图块集禁用。如果该选项已开启但未设置边缘图层,状态栏会给出提示。
如果你通过代码自行修改了悬崖层级(例如在自定义生成器中),请在修改后调用 CliffEdgePainter.PaintEdges(map, tileset, cellRect, CliffEdgeOptions.Default),然后调用 terrain.NotifyChanged(MapLayers.Splat, returnedRect)。
典型的工作流程:用 Set Level 粗略铺出高地,用 Raise / Lower Level 细化其边缘,用 Clean Cliffs 清理轮廓,然后在单位需要走上去的地方添加 Ramps(坡道)。最后用通行叠加层(P)检查结果。
绘制工具
绘制工具用于绘制图块集中的地面图层(最多 16 个)。每个 Splat 纹素都存储所有图层的权重,这些权重之和始终为 100%。如果所选图层在图块集中不存在或没有纹理,状态栏会给出提示,而不会悄悄地用备用纹理绘制。
| 工具 | ID | 作用 | 输入与修饰键 |
|---|---|---|---|
| Paint Layer | paint.layer | 用边缘柔和的笔刷绘制地面纹理图层。 | 笔刷,重复频率 10/s。Shift:擦除该图层。Ctrl+单击:拾取光标下的图层。 |
| Tile Paint | paint.tile | 像经典 RTS 地图编辑器那样,用地面图层绘制整个单元格。中心位于笔刷轮廓内的每个单元格都会以 100% 覆盖率绘制。 | 笔刷,吸附到单元格中心。Shift:从单元格中移除该图层。Ctrl+单击:拾取。 |
| Fill / Replace | paint.fill | 单击一次即可在全图替换某个地面图层、泛洪填充一个区域,或填充整张地图。 | 点击。Ctrl+单击:拾取。 |
| Smooth Paint | paint.smooth | 模糊笔刷下的地面图层过渡区,使混合更柔和。 | 笔刷,重复频率 15/s。 |
| Auto Paint | paint.auto | 使用自动绘制配置文件,根据高度、坡度、悬崖和水体绘制地面图层。 | 笔刷,重复频率 8/s。Shift:绘制配置文件的基础图层。 |
Paint Layer 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Layer | 选择面板 | 0 | 要绘制的图块集地面图层。 |
| Opacity | 0.05–1 | 1 | 该图层能达到的最高覆盖率。值越低,该图层与下方地面混合得越多。 |
Tile Paint 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Layer | 选择面板 | 0 | 要绘制的图块集地面图层。 |
| Soft Border | 开/关 | 关 | 在所绘单元格周围混合一个纹素宽的过渡,使边缘稍微柔和一些。 |
Fill / Replace 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Layer | 选择面板 | 0 | 用于填充区域的地面图层。 |
| Mode | Replace Dominant Layer、Fill Connected、Fill All | Fill Connected | Replace Dominant Layer:地图上主导图层与所单击图层相同的所有纹素。Fill Connected:泛洪填充与单击处相连且主导图层相同的纹素。Fill All:整张地图。 |
| Preserve Blends | 开/关 | 开 | 只把所单击的图层换成新图层,并保留与其他图层的混合(适用于替换和泛洪填充)。 |
每次单击对应一个撤销步骤,并且只记录实际发生变化的区域。
Smooth Paint 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Blur Radius | 1–3 | 1 | 模糊核的大小(以 Splat 纹素为单位)。值越大,平滑得越快。 |
Auto Paint 设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Profile | 无 | 笔刷绘制时使用的 Auto Paint Profile 资源。留空 = 使用按图块集图层名称匹配的内置规则。参见 Auto Paint。 |
Paint 选项卡上还会显示 Auto Paint Whole Map 卡片,它可以根据配置文件一步重新绘制整张地图。
植被工具
植被工具使用地形的 Vegetation Library 绘制草和小型植物(细节)并放置树木。如果地形没有植被库,工具页面会提供 Create Vegetation Library。在原型列表中选择原型;如果未选择任何原型,则使用库中的第一个原型。
| 工具 | ID | 作用 | 输入与修饰键 |
|---|---|---|---|
| Paint Details | veg.details | 按密度绘制草、花和其他小型植物。 | 笔刷,重复频率 10/s。Shift:擦除所选细节。 |
| Erase Details | veg.detailsErase | 移除笔刷下的草和其他细节植被。 | 笔刷,重复频率 10/s。 |
| Place Trees | veg.trees | 用笔刷散布树木,或单击放置单棵树木。 | Scatter:笔刷,Shift 移除树木。Single:点击,Shift+单击移除最近的一棵树。 |
| Erase Trees | veg.treesErase | 移除笔刷下的树木。强度较低时只擦除笔刷中心附近的树木。 | 笔刷。 |
Paint Details 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Details | 原型列表 | 无 | 要绘制的细节原型。 |
| Target Density | 0–1 | 0.6 | 笔刷绘制时趋近的目标密度(1 = 该原型的最大密度)。 |
| Mode | Raise、Set | Raise | Raise 只增加密度。Set 还会将密度更高的区域降低到目标密度。 |
Erase Details 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Only Selected | 开/关 | 关 | 只擦除所选的细节原型,而不是全部细节。 |
| Details | 原型列表 | 无 | 启用 Only Selected 时要擦除的细节原型。 |
Place Trees 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Trees | 原型列表 | 无 | 随机放置的树木原型。 |
| Mode | Scatter、Single | Scatter | Scatter 用笔刷绘制树木。Single 每次单击放置一棵树,并显示半透明预览。 |
| Density | 0.05–5 | 0.6 | 强度最大时,每个笔印在每 100 m² 内生成的树木数量。原型的最小间距仍然有效。 |
| Max Slope | 0–90° | 35 | 散布的树木不会放置在比此值更陡的斜坡上。 |
| Avoid Water | 开/关 | 开 | 不在水下散布树木。 |
Erase Trees 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Only Selected | 开/关 | 关 | 只移除所选原型的树木。 |
| Trees | 原型列表 | 无 | 启用 Only Selected 时要移除的树木原型。 |
装饰物工具
装饰物工具用于放置、擦除和编辑地形 Decor Library 中的场景道具,例如岩石、废墟和围栏。如果地形还没有装饰物库,工具页面会提供 Create Decor Library 按钮。原型列表带有搜索框和类别筛选器。
| 工具 | ID | 作用 | 输入与修饰键 |
|---|---|---|---|
| Place Decor | decor.place | 逐个放置道具、岩石等装饰物对象,或用笔刷批量散布。 | Single:单击;Shift+单击移除最近的对象。Scatter:笔刷;按住 Shift 移除笔刷下的装饰物。 |
| Erase Decor | decor.erase | 移除笔刷下的装饰物对象。已锁定的对象会保留。强度较低时,只会擦除笔刷中心附近的对象。 | 笔刷。 |
| Select Decor | decor.select | 通过单击或拖出矩形框选择装饰物对象,以便移动、旋转、缩放或删除。 | 拖动。单击:选择。拖动:框选。Shift:添加 / 切换选择。 |
Place Decor 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Mode | Single, Scatter | Single | Single 模式每次单击放置一个对象,并显示半透明预览。Scatter 模式用笔刷绘制对象。 |
| Decor | 原型列表 | 无 | 放置时随机选用的装饰物原型。未选择任何原型时,使用库中的第一个原型。 |
| Density | 0.05–5 | 0.4 | Scatter:满强度时,每个笔印在每 100 m² 内生成的对象数。 |
| Use Prototype Defaults | 开/关 | 开 | 随机偏航角、贴合地面和对齐栅格这几项设置均取自原型。 |
| Random Yaw | 开/关 | 开 | 绕竖直轴随机旋转(不使用原型默认值时)。 |
| Yaw | 0–360° | 0 | Random Yaw 关闭时,绕竖直轴的旋转角度。 |
| Scale | 0.1–5 | 1 | 应用于原型随机缩放的倍数。 |
| Align To Ground | 0–1 | 0 | 0 = 竖直,1 = 顺应地面坡度(不使用原型默认值时)。 |
| Snap To Grid | 开/关 | 关 | 将对象放置在单元格中心。 |
| Random Variant | 开/关 | 开 | 随机使用原型的某个预制件变体。 |
| Max Slope | 0–90° | 90 | Scatter:跳过比该值更陡的斜坡。90 = 不限制。 |
| Avoid Water | 开/关 | 关 | Scatter:不在水下放置对象。 |
Erase Decor 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Only Selected | 开/关 | 关 | 只移除所选原型的对象。 |
| Decor | 原型列表 | 无 | Only Selected 开启时要移除的装饰物原型。 |
Select Decor 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Pick Radius | 0.25–10 m | 1 | 单击时仍能选中对象的最小距离(较大的对象只要点在其占地范围内即可选中)。 |
| Select Locked | 开/关 | 关 | 允许选择已锁定的对象,例如为了解锁它们。 |
Selection 卡片
Select Decor 处于激活状态时,Decor 选项卡会显示 Selection 卡片,其中列出已选对象的数量。每个编辑按钮(Delete、Duplicate、Snap to Ground、Rotate 90°、Lock、Unlock)的操作都对应一个撤销步骤。Select All 和 Clear 只改变选区。
| 按钮 | 操作 |
|---|---|
| Select All | 选择地图上的所有装饰物对象(跳过已锁定的对象)。 |
| Clear | 清空选区。 |
| Delete | 删除所选对象。 |
| Duplicate | 复制选区,并沿对角线偏移一个单元格(+X 和 +Z 方向)。副本会成为新的选区,且处于未锁定状态。 |
| Snap to Ground | 将所选对象重新放回地面。 |
| Rotate 90° | 将选区绕其中心顺时针旋转 90°。 |
| Lock / Unlock | 已锁定的对象无法再被工具选择或擦除。开启 Select Locked 后可以选择并解锁它们。即使以这种方式选中,已锁定的对象也不受 Delete 和手柄的影响。 |
使用手柄移动、旋转和缩放的方法详见装饰物选区手柄。
水体工具
水体按顶点存储:每个顶点要么有水(带有水面高度),要么无水。与悬崖工具一样,水体工具使用硬选区。Water at Cursor 卡片显示鼠标下方的水位和水深,并根据图块集的浅水深度上限,指明该处属于浅水(可行走)还是深水。
| 工具 | ID | 作用 | 输入与修饰键 |
|---|---|---|---|
| Paint Water | water.paint | 按目标高度绘制湖泊和河流;高于水面的地面保持干燥。 | 笔刷。Shift:擦除水体。Ctrl+单击:拾取高度。 |
| Erase Water | water.erase | 移除笔刷下顶点上的水体。 | 笔刷。 |
| Flood Fill | water.flood | 单击一次,即可将光标下方的盆地注水至目标高度。 | 单击。Shift+单击:排干相连的水体。Ctrl+单击:拾取高度。 |
水面高度设置(Paint Water 和 Flood Fill)
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Water Height | Cliff Level Plus Offset, Absolute Height, Picked Height | Cliff Level Plus Offset | 确定水面高度的方式。 |
| Level | 0–15(选择面板) | 1 | 水面所基于的悬崖层级(Cliff Level Plus Offset 模式)。 |
| Offset | −8–16 m | 0.6 个层级 | 高出该悬崖层级的米数。在你修改之前,它始终等于 0.6 × 地图的悬崖层级高度(层级高度为 2 m 时即 1.2 m)。 |
| Absolute Height | −16–64 m | 3.2 | 以地图局部坐标(米)表示的水面高度(Absolute Height 模式)。 |
| Picked Height | −16–64 m | 3.2 | 通过 Ctrl+单击拾取的高度(Picked Height 模式)。拾取点有水时取水面高度,否则取地面高度,同时将模式切换为 Picked Height。 |
Paint Water 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Force | 开/关 | 关 | 同时标记地面高于水面的顶点,例如用于制作瀑布或沼泽。 |
Flood Fill 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Max Vertices | 100–1,000,000 | 20,000 | 安全上限:如果一次注水或排水会覆盖更多顶点,操作将被取消,不做任何更改。 |
默认值(层级 1 + 0.6 个层级)可以填满比新地图基础层级 2 低一个层级的湖泊。先用 Raise / Lower Level 配合 Shift 挖出盆地,再用 Flood Fill 单击盆地内部。
通行工具
通行图(可行走、可建造、可飞行、水体)会根据坡度、悬崖、水体、地面图层、树木和装饰物自动推导。通行工具在此基础上绘制手动覆盖,并使用以通行单元格为单位的硬选区。在 Pathing Overlay 卡片上或按 P 开启叠加层,即可查看结果。
| 工具 | ID | 作用 | 输入与修饰键 |
|---|---|---|---|
| Paint Pathing | pathing.paint | 绘制不可见的通行规则,例如阻挡区、禁止建造区或强制可行走区域。 | 笔刷。Shift:移除所选的覆盖类型。 |
| Clear Pathing | pathing.clear | 移除所有已绘制的通行覆盖,使自动推导的通行数据重新生效。 | 笔刷。 |
Paint Pathing 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Override | 见下文 | Blocker | 要绘制的通行覆盖类型。 |
| 覆盖类型 | 效果 |
|---|---|
| Blocker | 不可行走且不可建造:相当于一堵不可见的墙,常用于地图边界和装饰区域。 |
| Unwalkable | 地面单位无法在此行走。 |
| Unbuildable | 无法在此放置建筑,例如用于确保通道和分矿点不被占用。 |
| Unflyable | 空中单位无法在此飞行。 |
| Force Walkable | 即使坡度、水体或占地范围会造成阻挡,地面单位也能在此行走。 |
| Force Buildable | 即使自动推导的规则会禁止,也可以在此放置建筑。 |
通行叠加层颜色
| 叠加层 | 颜色 |
|---|---|
| Walkability | 绿色 = 可行走,红色 = 被阻挡,深红色 = 悬崖,橙色 = 过陡,蓝色 = 深水,青色 = 浅水。 |
| Buildability | 绿色 = 可建造,黄色 = 可行走但不可建造,红色 / 深红色 = 不可行走。 |
| Flyability | 浅蓝色 = 可飞行,品红色 = 不可飞行。 |
| Water | 青色 = 浅水,蓝色 = 深水。 |
| Combined | 同时显示以上全部内容,类似经典 RTS 地图编辑器中的通行视图。 |
自动推导部分所用的规则(最大坡度、悬崖边距等)在 RTS Terrain 组件上设置,参见 Pathing Rules。
标记点工具
标记点工具用于向地图添加游戏玩法信息:玩家出生点、供触发器使用的命名区域,以及摄像机位置等命名点。当标记点工具处于激活状态或 Show Markers 开启时,标记点会显示在 Scene 视图中。
| 工具 | ID | 作用 | 输入与修饰键 |
|---|---|---|---|
| Start Locations | markers.start | 放置玩家的出生点。再次单击可移动出生点。 | 单击。Shift+单击:移除最近的出生点。 |
| Regions | markers.region | 拖出矩形,创建供游戏逻辑使用的命名区域。 | 拖动。Shift+单击:删除光标下方的区域。 |
| Markers | markers.marker | 放置带朝向的命名点,例如摄像机或路径点。 | 单击。Shift+单击:移除最近的标记点。 |
Start Locations 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Player | Player 1–16(选择面板) | Player 1 (Red) | 要放置其出生点的玩家。每个玩家只有一个出生点。 |
| Yaw | 0–360° | 0 | 出生点的朝向。 |
| Symmetry Assigns Next Players | 开/关 | 开 | 启用对称时,对称副本会放置后续玩家的出生点。关闭时,只在原始单击处放置或移动出生点。 |
Regions 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Color | 颜色 | 半透明蓝色 | 新区域的颜色。 |
| Snap To Cells | 开/关 | 开 | 使区域边界对齐单元格栅格。 |
新区域会自动获得唯一的名称。
Markers 设置
| 设置项 | 范围 | 默认值 | 说明 |
|---|---|---|---|
| Name | 文本 | Marker | 新标记点的名称。如果名称已被使用,会自动追加数字。 |
| Tag | 文本 | 空 | 游戏代码用来查找标记点的任意文本,例如 camera 或 waypoint。 |
| Yaw | 0–360° | 0 | 新标记点的朝向。 |
Auto Paint(自动绘制)
Auto Paint 根据地形的形状绘制地面图层:平地铺草,陡坡和悬崖沿线铺岩石,岸边铺沙,再加上被噪声打散的泥土斑块。这是让刚雕刻或生成的地图看起来像成品的最快方法,之后还可以手动细化。
有三种使用方式:
- Paint 选项卡上的 Auto Paint 笔刷工具:在笔刷范围内按规则绘制,并根据笔刷权重和强度与现有的地面涂层混合。按住 Shift 则改为绘制基础图层,从而清除规则绘制的结果;
- Paint 选项卡上的 Auto Paint Whole Map 卡片:重新绘制地图上的所有地面纹素,整个操作为一个撤销步骤。该卡片包含 Profile 字段和 Create Profile 按钮;
- 配置文件的 Inspector 窗口中的 Apply To Map 按钮,以及地图生成器的 Paint 阶段。
未指定配置文件时,使用内置的默认规则。默认规则根据图块集中地面图层的名称来选择所用图层。
自动绘制配置文件
- 在 Paint 选项卡的 Auto Paint Whole Map 卡片上单击 Create Profile。这会针对地形的图块集创建一个包含默认规则的配置文件,并自动指定它。也可以通过 创建。
- 选中该配置文件。在 Inspector 窗口中指定 Preview Tileset,规则卡片就会显示图层名称和缩略图。该图块集仅用于显示,配置文件本身适用于任何图块集。
- 编辑规则,然后单击 Apply To Map,重新绘制所选或正在编辑的地形。如果有多个地形且均未选中,会弹出菜单让你选择其中一个。
- 单击 Use In Auto Paint Tool,让笔刷工具使用此配置文件进行绘制。
| 配置文件设置项 | 默认值 | 说明 |
|---|---|---|
| Base Layer | 0 | 应用规则之前覆盖所有纹素的图层。 |
| Start From Existing | 关 | 以当前的 Splat 权重而非基础图层作为起点,这样在没有规则匹配的地方会保留手绘内容。 |
| Rules | – | 从上到下依次应用;后面的规则会覆盖前面的规则。每张规则卡片包含启用开关、名称、所用图层、用于调整顺序的 ▲ / ▼、复制和删除按钮,以及 Conditions 折叠栏。列表下方是 Add Rule 和 Reset To Defaults。 |
对于每个 Splat 纹素,Auto Paint 先以 100% 的基础图层(或现有权重)为起点,从上到下应用已启用的规则,再将结果归一化。规则中每个已启用的条件都会给出 0 到 1 之间的权重,并在范围边界处平滑过渡。这些权重相乘后,再经噪声打散,并按规则的强度缩放。
规则设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Name | Rule | 规则的显示名称。 |
| Layer | 0 | 此规则绘制的地面图层(图块集索引)。 |
| Enabled | 开 | 已禁用的规则会被跳过。 |
| Use Height, Min Height, Max Height | 关,0,20 | 仅在地图局部地面高度(米)处于该范围内的位置绘制。 |
| Height Falloff | 0.5 | 高度范围之外的平滑过渡宽度,单位为米(0 = 硬边)。 |
| Use Slope, Min Slope, Max Slope | 关,0,90 | 仅在坡度(度)处于该范围内的位置绘制。 |
| Slope Falloff | 5 | 坡度范围之外的平滑过渡宽度,单位为度。 |
| Use Cliff Level, Min Cliff Level, Max Cliff Level | 关,0,15 | 仅在这些悬崖层级上绘制(硬边)。 |
| Near Cliff Distance | 0 | 仅在距崖壁该距离以内绘制,单位为单元格(0 = 关闭)。在外侧一半范围内逐渐淡出。 |
| Near Water Distance | 0 | 仅在距水体该距离以内绘制,单位为单元格(0 = 关闭)。在外侧一半范围内逐渐淡出。 |
| Noise Scale | 16 | 噪声特征的尺寸,单位为米。 |
| Noise Amount | 0 | 噪声打散规则的程度(0 = 无噪声,1 = 斑块状)。 |
| Noise Seed | 0 | 种子不同的规则会产生不同的斑块。 |
| Strength | 1 | 规则的整体强度(0–1)。 |
| Blend Mode | Replace | 此规则与其上方规则的组合方式(见下文)。 |
| 混合模式 | 效果 |
|---|---|
| Add | 在现有结果之上叠加规则的图层。归一化时,现有图层会按比例减少。 |
| Replace | 按规则权重将所有图层向规则的图层混合。这是最常用的选择。 |
| Multiply | 将规则图层的现有权重乘以规则遮罩,从而在规则不匹配的地方移除该图层。 |
默认规则
内置规则把名称中包含 grass、meadow 或 green 的图层用作基础图层,并按类似的关键字查找 dirt、rock 和 sand 图层(没有图块集时使用图层 0–3)。
| 规则 | 绘制内容 | 条件 |
|---|---|---|
| Dirt Patches | 泥土 | 噪声斑块(缩放 14 m),强度 0.55。 |
| Sand Shore | 沙地 | 距水体 3 个单元格以内,略带噪声打散。 |
| Rock Slopes | 岩石 | 坡度为 28° 及以上(衰减 8°)。 |
| Rock Near Cliffs | 岩石 | 距崖壁 1.6 个单元格以内,经噪声打散,强度 0.85。 |
通过代码使用 Auto Paint
using RTSTerrainStudio;
using UnityEngine;
// Repaints the whole map with a profile, for example after your own procedural edits.
public class RepaintWithProfile : MonoBehaviour
{
public RTSTerrain terrain;
public RTSAutoPaintProfile profile;
void Start()
{
if (terrain == null || terrain.Map == null || profile == null) return;
// Evaluates the rules for every splat texel inside the rectangle (here: the whole map).
RTSAutoPaint.Apply(terrain.Map, profile, terrain.Map.CellBounds);
// Let the terrain upload the new splat maps and update dependent systems (pathing ground rules).
terrain.NotifyChangedAll(MapLayers.Splat);
terrain.ProcessPendingChanges();
}
}
RTS Terrain 组件
RTS Terrain 组件用于显示一张地图。它按区块构建地形网格,使用图块集渲染地面和悬崖,使通行图保持最新,并提供查询功能,例如查询某个世界坐标处的高度、水体和通行数据。可以通过 创建该组件,或通过 将其添加到任意 GameObject 上。
该菜单项会创建一个可直接编辑的地形:一个名为 RTS Terrain 的 GameObject,带有 RTS Terrain、Vegetation Renderer、Water Renderer 和 Decor Manager 组件,使用项目的默认图块集和库,并在当前活动场景旁(或 Assets/Maps 中)新建一个平坦的 .rtsmap 文件。如果项目中还没有图块集,会提示你先创建演示内容。
Inspector 窗口
| 区域 | 内容 |
|---|---|
| Header(标题区) | 地图名称、世界尺寸、单元格尺寸和地图资源,Unsaved changes 徽标,以及实时统计信息:Cells、Chunks、Map Memory、Trees、Decor、Detail Layers、Regions、Start Locations 和 Markers。 |
| Actions(操作) | Open RTS Terrain Studio、Edit Terrain(为此地形开启或关闭 Scene 视图编辑)、Save、Save As… 和 Reload。 |
| Warnings(警告) | 列出各种问题,并尽可能提供一键修复:URP 未激活、缺少着色器、没有图块集(附带按钮,可改用制作该地图时所用的图块集)、纹理数组未构建或已过期(Build)、地图是为其他图块集制作的、地图中有植被或装饰物但没有对应的库、地形绕 X 或 Z 轴旋转(Fix)或被缩放(Reset Scale),以及缺少子系统组件(Add)。 |
| Assets(资源) | Map、Tileset、Vegetation 和 Decor 字段,每个字段都带有 New 按钮,用于创建并指定新资源。 |
| Chunks & Geometry, Cliff Shape, Physics, Rendering | 地形设置(见下表)。 |
| Pathing Rules | 通行图的推导方式(见下表)。修改后会自动重建。 |
| Default Map | 未指定地图资源时所创建地图的尺寸和默认值。 |
| Rebuild Terrain | 销毁并重新创建所有区块、纹理和通行图;植被、水体和装饰物也会一并重建。通过代码修改设置后,或某些内容看起来没有更新时,可以使用此按钮。 |
RTS Terrain Studio 窗口 Settings 选项卡的 Terrain Component 部分也嵌入了同样的 Inspector 界面。如何指定图块集以及纹理数组的相关内容,参见图块集;地图文件参见地图资源。
地形只使用其 Transform 的 Y 轴旋转,并忽略缩放。要扩大地图,请使用 Resize(RTSTerrain.ResizeMap)添加单元格,或使用更大的 Cell Size(MapCreationSettings.cellSize)新建地图。
地形设置
这些字段属于 RTSTerrainSettings(属性 RTSTerrain.Settings)。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Chunks & Geometry | ||
| Chunk Size | 32 | 区块每条边的单元格数(16、32 或 64)。区块越小,编辑时重建越快;区块越大,Draw Call 越少。 |
| Surface Subdivisions | 2 | 平滑地面上每个单元格每条边的四边形数(1–4)。1 = 经典 RTS 外观,2–4 = 更平滑的丘陵。 |
| Cliff Subdivisions | 4 | 在悬崖单元格内描绘圆润悬崖轮廓线时使用的子栅格分辨率(2–8)。 |
| Cliff Shape | ||
| Cliff Noise | 0.18 | 崖壁顶点的随机水平偏移量,以单元格宽度的比例表示(0–0.5),可呈现自然的岩石外观。 |
| Cliff Noise Scale | 1.6 | 悬崖噪声的缩放,以世界单位表示(最小 0.1)。 |
| Cliff Slant | 0.12 | 悬崖顶边向后收进的距离,使崖壁略微倾斜,以单元格宽度的比例表示(0–0.4)。 |
| Cliff Wall Segments | 3 | 崖壁几何体在每个悬崖层级上的垂直分段数(1–8)。 |
| Physics | ||
| Collider Mode | Mesh Collider | None:不生成碰撞体,查询时使用地形自带的射线检测。Mesh Collider:每个区块一个 MeshCollider,编辑后重建。 |
| Chunk Layer | Default (0) | 区块 GameObject 所在的层,供碰撞体和摄像机使用。 |
| Rendering | ||
| Cast Shadows | On | 区块的阴影投射模式。 |
| Receive Shadows | 开 | 区块是否接收阴影。 |
| Material Override | 无 | 可选材质。留空时,会创建使用 RTS Terrain Studio/Terrain 着色器的材质。 |
| Show Grid | 关 | 显示单元格栅格,在 Play 模式和构建出的游戏中同样有效。编辑器中的 G 键开关与此相互独立,仅在编辑时起作用。 |
| Grid Opacity | 0.35 | 栅格的不透明度(0–1)。 |
Pathing Rules
这些字段属于 RTSPathingSettings(属性 RTSTerrain.PathingSettings),决定坡度、悬崖、水体和地面图层如何转化为可行走和可建造区域。用通行工具绘制的覆盖会在此基础上叠加应用。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Max Walkable Slope | 42° | 坡度超过此角度的地面不可行走。 |
| Max Buildable Slope | 18° | 坡度超过此角度的地面不可建造。 |
| Cliff Margin | 0.25 | 被阻挡区域超出崖壁的延伸距离,以通行单元格宽度的比例表示(0–1)。值越大,单位离悬崖边缘越远。 |
| Shallow Water Buildable | 关 | 允许在浅水中放置建筑。 |
| Tree Footprint Scale | 1 | 应用于每棵树通行占地半径的倍数。 |
| Use Ground Layer Rules | 开 | 应用主导地面图层的通行效果,例如让粗糙岩石变为 Unbuildable。 |
Default Map
未指定地图资源时创建的地图。Map 选项卡上的 New Map 卡片也以这些值作为初始值。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Width / Height | 128 / 128 | 以单元格为单位的尺寸,必须是 16 的倍数(16–1024)。 |
| Cell Size | 2 | 单个单元格的尺寸,以世界单位表示。 |
| Cliff Level Height | 2 | 一个悬崖层级在世界空间中的高度。 |
| Base Cliff Level | 2 | 每个顶点的初始悬崖层级(0–15)。 |
| Base Ground Layer | 0 | 初始时覆盖整张地图的地面图层。 |
| Splat Resolution | 4 | 每个单元格的 Splat 纹素数(1、2、4 或 8)。 |
| Detail Resolution | 2 | 每个单元格的细节密度纹素数(1、2 或 4)。 |
| Pathing Resolution | 2 | 每个地图单元格包含的通行单元格数(1、2 或 4)。 |
子系统组件
地形本身只绘制地面和悬崖。地图的其他部分由同一 GameObject 上或场景中其他位置的组件负责显示。如果缺少下表前四个组件中的任何一个,RTS Terrain 组件的 Inspector 会发出警告,并提供 Add 按钮。
| 组件 | 是否由创建菜单添加 | 用途 |
|---|---|---|
| Vegetation Renderer | 是 | 以 GPU 实例化或预制件实例的方式渲染绘制的草和植物以及树木。只重新生成发生变化的区域。 |
| Water Renderer | 是 | 按区块构建并渲染水面,外观取决于图块集的水体配置。为获得最佳效果,请在 URP 资源中(或在摄像机上)启用 Depth Texture 和 Opaque Texture。 |
| Decor Manager | 是 | 显示地图中的装饰物,并在高度变化时让它们保持贴合地面。 |
| Environment | 由 New Map Wizard 添加(可选),或通过 添加 | 将地图的太阳光、环境光和雾效应用到场景。禁用该组件时会恢复场景原有的光照,场景文件中的光照设置也永远不会被改写。 |
| Fog of War Manager | 为本地队伍计算战争迷雾。每个场景一个。 | |
| Pathfinding Service | ,或由 RTSPathfindingService.GetOrCreate() 按需创建 | 在地形的通行图上执行内置的路径搜索。 |
通过代码更改地形设置
using RTSTerrainStudio;
using UnityEngine;
public class TerrainQualityOptions : MonoBehaviour
{
public RTSTerrain terrain;
// Called from a graphics options menu, for example.
public void SetSmoothHills(bool smooth)
{
// Settings fields can be changed directly, but the chunks are only rebuilt on request.
// Use 1, 2 or 4: an odd value such as 3 rounds Cliff Subdivisions up to a multiple of 6 (expensive).
terrain.Settings.surfaceSubdivisions = smooth ? 2 : 1;
terrain.RebuildAll();
}
public void UseStricterSlopes()
{
// Assigning new pathing settings rebuilds the pathing map automatically.
RTSPathingSettings rules = terrain.PathingSettings.Clone();
rules.maxWalkableSlope = 35f;
rules.maxBuildableSlope = 12f;
terrain.PathingSettings = rules;
}
}
如果你修改的是当前 PathingSettings 实例的字段,而不是赋值一个新实例,请在修改后调用 terrain.RebuildPathing()。
游戏内编辑器
游戏内编辑器把 RTS Terrain Studio 的地图编辑器带进你构建出的游戏中。玩家和模组作者无需 Unity 就能创建、绘制、保存、加载和试玩地图,就像经典 RTS 游戏附带的地图编辑器一样。它基于 UI Toolkit 运行,使用与 Unity 编辑器窗口相同的工具(Height、Cliffs、Paint、Vegetation、Decor、Water、Pathing 和 Markers),并输出同样的 .rtsmap 文件。在游戏中制作的地图可以复制到 Unity 项目中直接打开,无需任何修改,反之亦然。

所有功能都集中在一个组件 RTSRuntimeEditor 中。把它添加到包含 RTS Terrain 和摄像机的场景中,按下 Play,就能得到一个可用的编辑器。本章介绍配置方法、界面的各个部分、所有键盘和鼠标操作、Play Test 按钮、游戏代码如何响应试玩、地图的存储位置、如何更改 UI 样式,以及发布前需要检查的事项。
在 Unity 编辑器中,Shortcuts & Help 窗口(按 F1 或单击顶部栏中的 ? 按钮打开)中的 Open Manual 按钮会打开本章。在构建版本中,该按钮会打开你在 Manual Url 中填写的页面,例如你自己的模组制作指南。如果该字段为空,构建版本中会隐藏此按钮。
两分钟快速体验
体验游戏内编辑器最快的方式是使用包中附带的演示场景。
- 选择 。这会生成演示用的纹理、库、地图和场景。此操作只需执行一次。
- 在 Project 窗口中打开
Samples/Scenes/RTS Terrain Studio - In-Game Editor Demo。该场景包含 Sandbox 地图、一个 RTS In-Game Editor 对象、一个 RTS Demo Game 对象和一个战争迷雾管理器。 - 按下 Play。编辑器打开时会显示欢迎提示。在左侧选择一个工具(默认激活的是 Raise / Lower 高度工具),然后用鼠标左键在地形上绘制。
- 绘制时按住 Shift 可降低地面。用 [ 和 ] 调整笔刷大小。按住鼠标右键拖动可移动摄像机,滚动鼠标滚轮可缩放。
- 按 F5(或单击顶部栏中的 Play Test)。演示游戏会在出生点生成单位,并开启战争迷雾。选中单位后右键单击即可移动它们。
- 按 Esc 或 F5 返回编辑器。地图会完全恢复到试玩前的状态。
- 按 Ctrl+S,输入名称后按 Enter 即可保存地图。
将游戏内编辑器添加到场景中
环境要求
- 场景中有一个带地图的 RTS Terrain()。
- 一台摄像机。默认情况下,编辑器使用
Camera.main,即带有 MainCamera 标签的摄像机。你也可以指定任意摄像机。 - 包中的
Resources/RTSTerrainStudio/UI文件夹,其中包含 UI 主题和样式表。
步骤
- 打开你的场景,选择 。这会创建一个名为 RTS In-Game Editor 的对象,上面带有
RTSRuntimeEditor组件。如果你在 Hierarchy 窗口中右键单击某个对象并使用其中的菜单,新对象会成为该对象的子对象。 - 如果 Console 窗口警告没有 RTS Terrain,请添加一个。如果警告没有摄像机带有 MainCamera 标签,请为你的摄像机添加该标签,或将其指定给 Target Camera。
- 可选:显式指定地形和摄像机,并调整其他设置(参见 Inspector 参考)。
- 按下 Play,编辑器随即出现;如果你关闭了 Start Visible,请按 F2 打开它。
该菜单项在 Play 模式下不可用。如果场景中已经有游戏内编辑器,Unity 会询问是 Create Another(再创建一个)还是 Select Existing(选择现有的)。每个场景有一个编辑器就足够了。也可以通过 Add Component ▸ RTS Terrain Studio ▸ In-Game Editor 添加该组件,同一个 GameObject 上只能有一个该组件。
组件在运行时创建的内容
你不需要自己搭建任何 UI 或摄像机装置。RTSRuntimeEditor 启用后会创建以下内容:
| 对象 | 用途 |
|---|---|
RTSEditSession | 编辑会话,可通过 Session 访问。它保存工具、笔刷、对称设置、选区和撤销历史,与 Unity 编辑器窗口使用的是同一个类。 |
| 子对象 RTS In-Game Editor UI | 持有一个 UIDocument,其中包含完整的编辑器界面。组件禁用时,该子对象会随之销毁。 |
RTSRuntimeEditorCamera | 在 Use Built In Camera 开启时添加到目标摄像机上。如果摄像机上已有该组件,则直接使用它。编辑器自己添加的摄像机组件会在编辑器销毁时一并移除。 |
RuntimeToolGizmos | 在 Game 视图中绘制笔刷轮廓、选择框、装饰物预览、出生点和对称辅助线。可通过 Gizmos 访问。 |
PanelSettings | 当你将 Panel Settings 留空时创建。它使用 RTS 主题,并随屏幕缩放。如果你指定的面板设置随屏幕尺寸缩放且不渲染到纹理中,编辑器会改用它们的运行时副本,除非 Enforce Minimum UI Scale 已关闭(参见自定义 UI)。 |
编辑器如何查找地形和摄像机
- 地形:如果指定了地形,编辑器会在该地形启用后立即使用它。如果该字段为空,编辑器会使用
RTSTerrain.Main,即场景中的第一个 RTS Terrain。只要找不到地形,编辑器就会每秒重新检查两次,并显示居中提示 No RTS Terrain found。 - 多个地形:如果场景中有多个 RTS Terrain,Map 菜单会在 Edited terrain 下列出它们。切换到其他地形会清空撤销历史。
- 摄像机:如果 Target Camera 为空,编辑器会使用
Camera.main,并持续查找。在摄像机出现之前,它会显示居中提示 No camera found。
Inspector 参考
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Scene | ||
| Terrain | 空 | 要编辑的地形。为空时使用场景中的第一个 RTS Terrain(RTSTerrain.Main)。 |
| Target Camera | 空 | 用于拾取、Gizmo(辅助图标)和内置编辑器摄像机的摄像机。为空时使用 Camera.main。 |
| Use Built In Camera | 开 | 在目标摄像机上添加 RTSRuntimeEditorCamera,并在编辑器可见时控制该摄像机。试玩期间内置摄像机会关闭。如果你希望在编辑时由自己的摄像机脚本移动摄像机,请关闭此项。 |
| Behaviour | ||
| Start Visible | 开 | 场景启动时显示编辑器。关闭此项时,玩家需要按切换键打开编辑器,或者由你的代码设置 EditorVisible。 |
| Toggle Key | F2 | 显示或隐藏整个编辑器。选择 None 可禁用该按键。此时顶部栏中的关闭按钮也会隐藏,因此你的游戏需要自行打开和关闭编辑器。在网页浏览器以及手机和平板电脑上,关闭按钮仍会保留,因为在这些平台上可以通过 Open Map Editor 按钮重新打开编辑器。 |
| Start Tool | height.raise | 启动时激活的工具 ID,例如 paint.layer、veg.trees、decor.place 或 markers.start。如果该 ID 不存在,则使用第一个工具。 |
| Show First Time Hint | 开 | 编辑器首次打开时显示欢迎提示,直到玩家单击 Got it 或将其关闭。该选择保存在 PlayerPrefs 中。 |
| Show Quit Command | 开 | 添加 File ▸ Quit,用于关闭应用程序。在 Unity 编辑器中,它会停止 Play 模式。该命令在 WebGL、iOS 和 Android 上从不显示。 |
| Confirm Quit With Unsaved Changes | 开 | 地图有未保存的更改时,在应用程序退出前先询问;即使退出请求来自编辑器之外(窗口关闭按钮、Alt+F4、Application.Quit)也是如此。此时会先显示编辑器,并停止正在运行的试玩。Web 构建无法退出,此时由浏览器在页面关闭或重新加载前询问 Leave site?。 |
| Undo Memory MB | 512 | 撤销历史最多可使用的内存,以兆字节为单位(最小 16)。内存用满时,最早的步骤会被丢弃。此外,撤销历史最多保留 100 个步骤。Web 构建最多使用 192 MB,因为浏览器只为整个游戏提供一个大小有限的内存堆。 |
| Manual Url | 空 | 在构建版本中,帮助窗口里的 Open Manual 所打开的网页。为空时,构建版本中会隐藏该按钮。在 Unity 编辑器中,字段为空时会打开本手册。 |
| Files | ||
| Maps Folder | 空 | 用于列出、加载和保存地图的文件夹。为空时使用存储文件夹 RTSMapSerializer.StorageRoot 中的 Maps 文件夹。相对路径(例如 UserMaps)以存储文件夹为基准。绝对路径则按原样使用。存储文件夹为 Application.persistentDataPath;在 Web 构建中,它是浏览器存储空间中的一个文件夹,即使页面地址改变也保持不变(参见浏览器中的地图)。 |
| Write Preview Images | 开 | 在每张已保存的地图旁写入一张小地图图像(192 × 192 像素),供地图浏览器显示。 |
| User Interface | ||
| Panel Settings | 空 | 可选的 UI Toolkit 面板设置。为空时,编辑器会在运行时使用 RTS 主题创建面板设置。这些面板设置会随屏幕尺寸缩放(参考分辨率为 1920 × 1080),且缩放比例不低于 85%,因此在 1280 × 720 下文字依然清晰可读。 |
| Enforce Minimum UI Scale | 开 | 让编辑器在小窗口和浏览器画布中保持清晰可读。编辑器会在运行时复制已指定且随屏幕尺寸缩放的面板设置(始终不会修改资源本身),并且该副本不会把 UI 缩小到 85% 以下。关闭此项可按原样使用指定的资源,例如当编辑器与你的游戏 HUD 共用同一个面板时。 |
| Ui Scale | 1 | 额外的缩放系数,范围为 0.5 到 2。对编辑器在运行时创建的面板设置以及已指定面板设置的运行时副本生效。 |
| Sorting Order | 1000 | 编辑器 UI 的排序顺序。较高的默认值让编辑器显示在游戏 HUD 之上。 |
界面导览
屏幕分为五个区域。将指针悬停在任意按钮、字段或状态项上片刻,就会显示工具提示,禁用的按钮也不例外。如果你不确定某个元素的作用,把指针悬停在它上面即可。
| 区域 | 位置 | 内容 |
|---|---|---|
| 顶部栏 | 顶部边缘 | File 和 Map 菜单、撤销和重做、对称、视图开关、Play Test、帮助、隐藏面板、关闭。 |
| 工具栏 | 左侧边缘 | 八个类别按钮(按键 1 到 8)以及所选类别中的工具。 |
| 工具面板 | 右侧边缘 | 当前工具的名称和说明、笔刷、对称、工具设置和额外命令。面板可以折叠。 |
| 状态栏 | 底部边缘 | 光标所在位置的信息、工具状态、笔刷、撤销内存和地图名称。 |
| 叠加层 | 地图上方 | 浮动提示、工具提示、对话框、欢迎提示、试玩栏和居中提示。 |
顶部栏
| 控件 | 作用 |
|---|---|
| File | 打开 File 菜单(见下文)。 |
| Map | 打开 Map 菜单(见下文)。 |
| Undo / Redo | 撤销(Ctrl+Z)和重做(Ctrl+Y)。它们的工具提示会显示步骤名称,例如 Undo "Raise / Lower"。没有可撤销或可重做的内容时,按钮会变灰。 |
| Symmetry: Off | 打开对称菜单。开启对称后,每次笔触都会被镜像或旋转,从而轻松制作公平的竞技地图。按钮会显示当前模式,例如 Symmetry: 180°。 |
| Grid | 显示或隐藏单元格栅格(G)。 |
| Pathing | 打开通行叠加层菜单。叠加层开启时,按钮会显示其模式,例如 Pathing: Walkability。按 P 可以用上次使用的模式开启叠加层,或将其关闭。 |
| Markers | 在使用其他工具时显示或隐藏出生点、区域和标记点(M)。默认开启。 |
| Play Test | 开始试玩(F5)。 |
| Help (?) | 打开 Shortcuts & Help 窗口(F1)。 |
| Eye(眼睛图标) | 隐藏面板,以便清楚地查看地图(H)。编辑功能仍然可用。单击显示 Panels hidden - press H to show them 的小按钮可让面板重新显示。 |
| Close (×) | 隐藏编辑器,并显示一条浮动提示,告知重新打开编辑器的按键。当 Toggle Key 为 None 时,此按钮会隐藏。在网页浏览器以及手机和平板电脑上,由于功能键不可靠,它还会在左下角显示一个 Open Map Editor 按钮,而且即使没有切换键,关闭按钮也可用(参见浏览器中的游戏内编辑器)。 |
File 菜单
| 条目 | 快捷键 | 作用 |
|---|---|---|
| New Map... | Ctrl+N(在网页浏览器中为 Alt+N) | 打开 New Map 对话框。如果当前地图有未保存的更改,编辑器会先询问。Alt+N 在所有平台上都有效。 |
| Open... | Ctrl+O | 打开地图浏览器以选择地图。如果当前地图有未保存的更改,编辑器会先询问。 |
| Save | Ctrl+S | 保存到地图的来源文件。首次保存时,会打开 Save Map As 询问名称。 |
| Save As... | Ctrl+Shift+S | 打开地图浏览器,以便用新名称保存。 |
| Download Map | 仅限 Web 构建。将地图的副本(包括未保存的更改)以 .rtsmap 文件的形式保存到玩家的电脑上(通常位于“下载”文件夹中)。参见浏览器中的地图。 | |
| Import Map... | 仅限 Web 构建。从玩家的电脑上打开一个 .rtsmap 文件。该文件会被添加到此浏览器中保存的地图里,并随即打开。 | |
| Show Maps Folder | 在操作系统的文件浏览器中打开地图文件夹。此条目仅在 Windows、macOS 和 Linux 上提供,适用于 Unity 编辑器和桌面 Player。 | |
| Quit | 关闭应用程序,并先询问如何处理未保存的更改。仅在 Show Quit Command 开启时显示,且在 WebGL、iOS 或 Android 上从不显示。 |
Map 菜单
| 条目 | 作用 |
|---|---|
| Map Info... | 名称、作者、描述和推荐玩家数量。 |
| Resize... | 放大或缩小地图。 |
| Generate... | 根据种子生成随机地图布局。可以一步撤销。 |
| Frame Whole Map | 移动编辑器摄像机,使整张地图可见。仅在使用内置摄像机时可用。 |
| Edited terrain | 仅当场景中有多个 RTS Terrain 时显示。用于选择要编辑的地形。切换会清空撤销历史。 |
对称菜单
| 模式 | 副本数 | 典型用途 |
|---|---|---|
| Off | 1 | 绘制时不使用对称。 |
| Mirror Left / Right | 2 | 沿垂直中心线镜像。 |
| Mirror Top / Bottom | 2 | 沿水平中心线镜像。 |
| Mirror 4-Way | 4 | 沿两条轴同时镜像。 |
| Rotate 180° | 2 | 围绕对称中心的中心对称,常用于 1v1 地图。 |
| Rotate 90° (4 players) | 4 | 适用于四人地图的旋转对称。需要正方形地图;在非正方形地图上该条目会变灰。 |
| Mirror Diagonal | 2 | 沿对角线镜像。需要正方形地图;在非正方形地图上该条目会变灰。 |
Reset Centre 会将对称中心移回地图正中。工具面板的 Symmetry 分区提供 Centre X 和 Centre Z 滑块,用于移动对称中心。地形上的紫色辅助线表示对称轴。
通行叠加层菜单
叠加层根据单位在各处能做什么为地形着色。可选择 Off、Walkability(绿色 = 可行走,红色 = 被阻挡)、Buildability、Flyability、Water(浅水和深水)或 Combined(在一个视图中同时显示可行走、可建造、水体和被阻挡的单元格)。Opacity 条目可将叠加层的不透明度设为 30%、55%(默认)或 80%。
工具栏(左侧)
上方一列中,每个工具类别各有一个按钮,按钮上的小数字就是它的按键。下方一列以图标和名称列出所选类别中的工具。将指针悬停在工具上可查看其说明和提示。
| 按键 | 类别 | 说明 | 工具 |
|---|---|---|---|
| 1 | Height | 抬高、降低、平滑和塑造地面。 | Raise / Lower, Smooth, Flatten, Set Height, Noise, Terrace, Erode |
| 2 | Cliffs | 经典 RTS 风格的悬崖层级、坡道和悬崖材质。 | Raise / Lower Level, Set Level, Ramp, Cliff Type, Clean Cliffs |
| 3 | Paint | 绘制地面纹理。 | Paint Layer, Tile Paint, Fill / Replace, Smooth Paint, Auto Paint |
| 4 | Vegetation | 草、花和树木。 | Paint Details, Erase Details, Place Trees, Erase Trees |
| 5 | Decor | 放置、擦除和选择装饰物(doodad)。 | Place Decor, Erase Decor, Select Decor |
| 6 | Water | 湖泊、河流和海洋。 | Paint Water, Erase Water, Flood Fill |
| 7 | Pathing | 决定单位可以在哪里行走、建造和飞行。 | Paint Pathing, Clear Pathing |
| 8 | Markers | 出生点、区域和命名标记点。 | Start Locations, Regions, Markers |
编辑器会记住你在每个类别中上次使用的工具。通过按钮或数字键选择某个类别时,会重新选中该工具。如果按下当前所在类别的数字键,则会切换到该类别中的下一个工具。你注册的自定义工具会自动出现在这里,其设置界面根据它们的 [ToolSetting] 字段自动生成。
工具面板(右侧)
标题栏显示当前工具的图标和名称。将指针悬停在名称上可阅读工具说明。单击箭头按钮可折叠面板。标题栏下方是若干可折叠的分区。你折叠的分区在切换工具后仍保持折叠。
| 分区 | 适用于 | 内容 |
|---|---|---|
| (简介) | 所有工具 | 工具的说明和用法提示,例如 Click: place / move · Shift+click: remove the nearest。 |
| Brush | 笔刷工具 | Shape(Circle、Square、Diamond,设置了自定义图章时还有 Stamp)、Size(一个对数滑块加一个以米为单位的输入字段)、Strength、Hardness、Rotation、Random Rotation、Spacing、Jitter,以及分区标题中的 Reset 按钮。所有工具共用同一个笔刷。 |
| Symmetry | 所有工具 | 对称模式、Centre X 和 Centre Z,以及当前模式需要正方形地图时显示的警告。 |
| Settings | 有选项的工具 | 当前工具的选项,例如目标高度、纹理选择面板、树木和装饰物缩略图、悬崖层级或玩家。 |
| Actions | 部分工具 | 额外命令:Apply to Whole Map(Auto Paint)、New Random Layout(Place Trees、Place Decor)等。 |
| Selection | Select Decor | Delete、Duplicate、旋转(−15°、+15°、+90°)、微移半个单元格(−X、+X、−Z、+Z)、Smaller / Larger(10%)、贴合地面、Lock / Unlock、Select All、Clear。 |
当地形没有地图时,面板和状态栏都会显示 There is no map to edit. 这条消息。
状态栏(底部)
| 条目 | 示例 | 含义 |
|---|---|---|
| 单元格 | Cell 42, 17 | 光标所在的单元格(x, z)。 |
| 高度 | Height 3.5 m | 光标处的地面高度,以米为单位。 |
| 层级 | Level 2 | 光标处的悬崖层级(0 到 15)。 |
| 通行 | Walkable, Buildable | 单位在此处能做什么,例如 Shallow water、Cliff、Too steep、Blocked 或 No flying。 |
| 水体 | Water 1.2 m 或 Dry | 光标处的水深。 |
| 工具状态 | 来自当前工具的信息,或该工具的提示。 | |
| 笔刷 | Brush 8.0 m 50 % | 笔刷半径和强度。 |
| 撤销 | Undo 12 4.2 MB | 撤销步骤数及其占用的内存。 |
| 地图名称 | ● Sandbox | 地图的名称。圆点表示有未保存的更改,按 Ctrl+S 即可保存。 |
浮动提示、工具提示与居中提示
- 浮动提示是出现在状态栏上方并自动淡出的简短消息,例如 Saved "Sandbox" 或 Undo: Raise / Lower。共有四种类型:Info 和 Success 显示约 3 秒,Warning 显示 5 秒,Error 显示 6.5 秒。单击浮动提示可将其关闭。同一时间最多显示四条。编辑器隐藏时浮动提示仍然可见,因此可以看到 Editor closed. Press F2 to open it again. 这条消息。
- Open Map Editor:在网页浏览器以及手机和平板电脑上,使用关闭按钮后,左下角会出现此按钮,用于重新打开编辑器。如果有键盘,按钮上还会显示切换键,例如 Open Map Editor (F2)。
- 工具提示会在指针悬停于任意控件约半秒后出现,在禁用的控件上以及试玩期间也同样如此。单击会隐藏工具提示,直到指针离开该控件。
- 欢迎提示:一个小面板,列出四个最重要的操作,并提供 All Shortcuts (F1) 和 Got it 按钮。你可以通过帮助窗口中的 Open Welcome Hint 重新显示它。
- 居中提示:当编辑器无法工作时,屏幕中央会显示 No RTS Terrain found 或 No camera found,并附有简短说明,告诉你需要修复什么。
- 试玩栏:试玩期间,屏幕顶部中央会出现一个小栏,显示 Play test running,并提供 Back to Editor (Esc or F5) 按钮。
对话框
对话框是模态的:打开对话框时无法编辑地图,快捷键也会失效。按 Enter 会触发高亮显示的(主)按钮,按 Esc 会关闭对话框。
| 对话框 | 打开方式 | 内容 |
|---|---|---|
| New Map | File ▸ New Map... | Name;Size 预设:Tiny (64 × 64)、Small (96)、Medium (128)、Large (192)、Huge (256)、Epic (384)、Giant (512);Width 和 Depth(16 到 1024 个单元格,取整为 16 的倍数;在网页浏览器中上限更低,参见限制);Cell Size 为 1、2 或 4 m(2 m 与经典 RTS 地图一致);Base Level(悬崖层级 0 到 15;设为 2 可留出向下挖掘湖泊和河流的空间);Base Layer(地面纹理)。下方的一行显示以米为单位的最终尺寸。新建地图操作无法撤销,因此请先保存。 |
| Generate Map | Map ▸ Generate... | Style(会重置形状选项)、带 Random 按钮的 Seed、Symmetry;Terrain 分区:Cliffs、Plateaus、Ramps、Ramp Count、Water、Lake Coverage、Island、Border;Content 分区:Textures、Vegetation、Forests、Decor、Decor Count、Start Locations、Players。生成结果会替换当前地图的内容,整个过程算作一个撤销步骤。下次打开该对话框时,这些设置仍会保留。 |
| Map Info | Map ▸ Map Info... | Name、Author、Description(显示在地图浏览器中)、Players(1 到 16),以及统计信息:尺寸、出生点、树木、对象、区域。应用更改算作一个撤销步骤。 |
| Resize Map | Map ▸ Resize... | Width 和 Depth(16 的倍数,网页浏览器中的上限与 New Map 相同)、Anchor(一个 3 × 3 的栅格,决定地图的哪一部分保持不动)、用于新区域的 Fill Level 和 Fill Layer。调整尺寸会清空撤销历史。地图缩小时,新范围之外的内容会被移除。 |
| Open Map / Save Map As | File ▸ Open... / Save As... | 地图浏览器。 |
| Unsaved Changes | 在有未保存的更改时执行 New、Open 或 Quit | Save(先保存;如果地图还没有名称,会询问名称)、Don't Save、Cancel。 |
| Shortcuts & Help | F1 或 ? 按钮 | 所有鼠标和键盘操作(参见操作与快捷键)、Open Welcome Hint、Open Manual(有可用手册时)和 Close。 |
地图浏览器
File ▸ Open...(Ctrl+O)和 File ▸ Save As...(Ctrl+Shift+S)打开的是同一个浏览器,标题分别为 Open Map 和
Save Map As。浏览器会列出地图文件夹中的所有 .rtsmap 文件。
| 部分 | 说明 |
|---|---|
| 文件夹行 | 地图文件夹的完整路径。Refresh 会重新读取该文件夹,比如在你把地图复制进去之后。在 Web 构建中,这一行显示的是 Saved in this browser. Use Download to keep a copy on your computer.,并且打开地图时还会多出一个 Import... 按钮(参见浏览器中的地图)。 |
| 地图列表 | 每张地图占一行,显示名称、尺寸、玩家数量和最后修改日期,最新的排在最前。无法读取的文件会标记为 Unreadable or damaged file,且无法打开。将鼠标悬停在某一行上可查看其文件路径(在 Web 构建中只显示文件名)。 |
| 详细信息 | 所选地图的小地图预览、名称、尺寸(以单元格和米为单位)、作者、玩家数量、修改日期和描述。只有在保存地图时开启了 Write Preview Images,才会有预览图。 |
| Map Name | 仅在 Save As 时可用。文件名由此名称生成。单击列表中的某张地图会将其名称复制到该字段中,这样你就可以覆盖该地图。 |
| Delete | 经你确认后,删除所选地图文件及其预览图像。此操作无法撤销。 |
| Download | 仅在 Web 构建中打开地图时可用。将所选地图的副本以 .rtsmap 文件的形式保存到玩家的电脑上。 |
| Cancel / Open / Save | Open 加载所选地图。双击某一行或按 Enter 效果相同。Save 以输入的名称保存地图。 |
浏览器只读取每个文件开头很小的文件头,因此即使文件夹中有许多大型地图,也能很快打开。
以新名称保存
- 按 Ctrl+Shift+S。名称字段中已填入当前地图的名称。
- 输入新名称,例如
Glacier Pass (2v2),然后按 Enter。 - 如果已存在另一个同名文件,编辑器会弹出 Replace Map? 对话框。按 Enter 时默认选中的是 Cancel,因此你必须单击 Replace 才能覆盖该文件。
- 地图随即保存,同时写入预览图像,浮动提示 Saved "Glacier Pass (2v2)" 会确认保存成功。之后按 Ctrl+S 会直接保存到此文件,不再询问。
如果以其他名称保存,地图本身的名称(显示在 Map Info 中)也会随之更改。这次重命名会记录为一个普通的撤销步骤,名为 Rename Map。
操作与快捷键
在编辑器中按 F1 也能查看同样的列表。在 macOS 上,凡是列出 Ctrl 的地方都可以使用 Cmd。数字键指的是主键盘上的数字行。字母快捷键以按键上印的字母为准,因此在德语(QWERTZ)和法语(AZERTY)键盘上,Ctrl+Z 同样对应标有 Z 的那个键。笔刷按键以按下后输入的字符为准,因此无论 [ 和 ] 位于你键盘上的哪个位置(例如需要配合 AltGr 输入),都可以使用。在网页浏览器中,少数按键的行为有所不同,参见浏览器中的游戏内编辑器。
鼠标
| 输入 | 操作 |
|---|---|
| 鼠标左键 | 绘制或使用当前工具。按住并拖动可完成一次笔触。整次笔触对应一个撤销步骤。 |
| Shift + 鼠标左键 | 执行相反操作:降低而非升高、擦除而非绘制、移除而非放置。 |
| Ctrl + 左键单击 | 对于支持此功能的工具,从地形中拾取一个值(高度、地面图层、悬崖层级……)。 |
| 按住鼠标右键或中键拖动 | 平移摄像机(“抓住地面”拖动)。 |
| Alt + 按住鼠标左键或右键拖动 | 旋转摄像机。按住 Alt 时,左键不会绘制。 |
| 鼠标滚轮 | 朝光标处的点放大或缩小。 |
摄像机(内置编辑器摄像机)
| 按键 | 操作 |
|---|---|
| W A S D 或方向键 | 平移。按住 Shift 可加快移动。 |
| Q / E | 向左 / 向右旋转。 |
| F | 将摄像机聚焦到光标处的点。 |
笔刷与工具
| 按键 | 操作 |
|---|---|
| [ / ] | 缩小 / 放大笔刷(每按一次约 15%)。 |
| - / =(或 +) | 降低 / 提高强度(每按一次 5%)。小键盘上的 - 和 + 同样有效。 |
| 1 到 8 | 选择工具类别(Height、Cliffs、Paint、Vegetation、Decor、Water、Pathing、Markers)。再按一次可切换到该类别中的下一个工具。 |
| Esc | 取消正在绘制的笔触。地形会恢复到你按下鼠标键时的状态。 |
文件与历史记录
| 按键 | 操作 |
|---|---|
| Ctrl+N 或 Alt+N | 新建地图。在网页浏览器中只有 Alt+N 有效,因为浏览器会将 Ctrl+N 保留用于打开新窗口。 |
| Ctrl+O | 打开地图(地图浏览器)。 |
| Ctrl+S | 保存。首次保存时会询问名称。 |
| Ctrl+Shift+S | 另存为。 |
| Ctrl+Z | 撤销。 |
| Ctrl+Y 或 Ctrl+Shift+Z | 重做。 |
视图与编辑器
| 按键 | 操作 |
|---|---|
| G | 显示或隐藏单元格栅格。 |
| P | 开启(使用上次的模式,首次为 Combined)或关闭通行叠加层。 |
| M | 显示或隐藏出生点、区域和标记点。 |
| H | 隐藏或显示编辑器面板。 |
| F1 | 打开 Shortcuts & Help。 |
| F5 | 开始试玩,或结束正在进行的试玩。 |
| Esc(试玩期间) | 结束试玩并返回编辑器。 |
| F2(即 Toggle Key) | 显示或隐藏整个编辑器。试玩期间此键无效。 |
装饰物选区(仅限 Select Decor 工具)
| 输入 | 操作 |
|---|---|
| 单击 / 拖出选框 | 选择对象。 |
| Shift + 单击 / 选框 | 将对象添加到选区,或将其从选区中移除。 |
| 拖动选中的对象 | 移动选区。整个拖动过程对应一个撤销步骤。拖动时按 Esc 会将对象放回原处。已锁定的对象无法拖动。 |
| 单击多选选区中的某个对象 | 仅选择该对象。 |
| Delete / Backspace | 删除选中的对象。 |
| Ctrl+D | 复制选区,并将副本沿对角线偏移一个单元格(+X 和 +Z)。 |
| Ctrl+A | 选择所有未锁定的对象。 |
| Esc | 清除选区。 |
Esc 键的作用
Esc 会按以下顺序执行第一项适用的操作:
- 关闭打开的弹出菜单。
- 让文本字段或滑块失去键盘焦点。
- 关闭最上层的对话框。
- 取消装饰物拖动(对象会回到原处)。
- 取消正在绘制的笔触(并显示浮动提示 Stroke cancelled)。
- 清除装饰物选区(Select Decor 工具)。
- 如果面板已用 H 隐藏,则重新显示面板。
快捷键没有反应?当文本字段拥有键盘焦点时,字母键会输入到该字段中。对话框打开时,只有 Enter 和 Esc 有效。单击地图或按 Esc,然后再试一次。此外,指针位于面板上方时,也无法绘制或用鼠标滚轮缩放。
内置编辑器摄像机
开启 Use Built In Camera 后,编辑器会向目标摄像机添加一个 RTSRuntimeEditorCamera 组件。这是一个环绕摄像机,围绕地面上的一个焦点旋转。它始终位于地形边界外加一段边距的范围内,绝不会低于地面,而且镜头拉得越高,俯视角度越大。它使用未缩放时间(unscaled time),因此在你的游戏暂停时(Time.timeScale = 0)仍能正常工作。编辑器可见时会开启该摄像机,编辑器隐藏时以及试玩期间则会关闭它。试玩结束后,摄像机会回到之前的位置。
要更改其行为,可以自行将 RTSRuntimeEditorCamera 添加到你的摄像机上(Add Component ▸ RTS Terrain Studio ▸ In-Game Editor Camera)并设置各个字段。编辑器会使用已有的组件,而不会再添加新组件。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Movement | ||
| Pan Speed | 18 | 最低高度下的键盘平移速度,单位为米/秒。高度越高,速度越快。 |
| Fast Multiplier | 2.5 | 按住 Shift 时的平移速度倍数。 |
| Edge Pan | 关 | 鼠标触碰屏幕边缘时平移。 |
| Edge Pan Margin | 12 | 用于边缘滚屏的屏幕边缘宽度,单位为像素。 |
| Rotate Speed | 100 | Q / E 的旋转速度,单位为度/秒。 |
| Mouse Rotate Speed | 0.25 | Alt + 鼠标拖动时的旋转量,单位为度/像素。 |
| Zoom | ||
| Min Height | 8 | 摄像机高出焦点的最低高度,单位为米。 |
| Max Height | 120 | 摄像机高出焦点的最高高度,单位为米。 |
| Zoom Step | 0.14 | 鼠标滚轮每滚动一格的高度变化量,以当前高度的比例表示。 |
| Zoom Towards Cursor | 开 | 朝光标处的点缩放,而不是朝屏幕中心缩放。 |
| Min Pitch | 40 | 最低高度时的俯仰角,即低于地平线的度数。 |
| Max Pitch | 70 | 最高高度时的俯仰角。 |
| Pitch Curve | 缓入缓出 | 按归一化高度从 Min Pitch(0)过渡到 Max Pitch(1)的曲线。 |
| Limits & Smoothing | ||
| Ground Clearance | 3 | 摄像机与其下方地面之间允许的最小距离,单位为米。 |
| Bounds Padding | 8 | 地形周围允许焦点移入的额外空间,单位为米。 |
| Smooth Time | 0.1 | 平滑时间,单位为秒(0 = 不平滑)。 |
在代码中,RTSRuntimeEditor.EditorCamera 会返回该组件;内置摄像机关闭时则返回 null:
| 成员 | 说明 |
|---|---|
void Focus(Vector3 worldPosition, bool instant = false) | 将焦点移动到某个世界位置;除非 instant 为 true,否则会平滑移动。 |
void FrameTerrain(bool instant = false) | 将摄像机置于地形中心上方,并拉远以显示地形的大部分区域。新地图和调整过尺寸的地图会自动取景。 |
void SnapToTargets() | 不经平滑,直接跳到目标位置、旋转和高度。 |
Vector3 FocusPoint、float Yaw、float Height | 当前的环绕状态(只读)。 |
bool IsDragging、bool WantsAltDrag | 摄像机正在被拖动时,或按住 Alt 时为 true。 |
bool PointerBlocked、bool KeyboardBlocked | 当指针位于 UI 上方、文本字段拥有焦点或有对话框打开时,由编辑器每帧设置。 |
RTSTerrain Terrain | 用于确定边界和地面高度的地形。 |
编辑时使用你自己的摄像机脚本:关闭 Use Built In Camera。此后编辑器只会读取目标摄像机,用于拾取和显示 Gizmo。请确保在 RTSRuntimeEditor.IsPointerOverUI 为 true 时,你的摄像机会忽略输入,以免滚动面板时摄像机也随之缩放。
Play Test:在你的游戏中试玩地图
Play Test(F5)会让编辑器暂时变成你的游戏:编辑器 UI 让出空间,你的游戏玩法代码生成单位并开启自己的摄像机,玩家就可以试玩这张地图。按 Esc、F5 或单击 Back to Editor 都会结束试玩,地图会完全恢复到试玩之前的状态。单位、建筑、被采伐的树木以及更改过的通行数据都会消失。

编辑器对你的游戏一无所知。它通过静态类 RTSRuntimeEditorEvents 报告试玩的开始和结束,由你的游戏决定如何响应。示例组件 RTSDemoGame()已经在监听这些事件。将它添加到你的编辑器场景中,无需编写代码,试玩时就能获得单位、战争迷雾、小地图和 HUD。
执行过程详解
| 时机 | 编辑器会…… |
|---|---|
| 按下 Play Test 时 |
|
| 试玩期间 | 忽略所有编辑输入、撤销和文件命令(此时会显示 Stop the play test first.),也忽略切换键。游戏玩法代码可以随意修改地图。 |
| 按 Esc、F5 或单击 Back to Editor 时 |
|
撤销历史:编辑器会将快照复制回同一个地图实例,因此你的撤销历史在试玩后依然保留。如果游戏玩法代码在试玩期间替换、重新加载了地图或调整了地图尺寸,快照会改为以新地图的形式加载。此时浮动提示中会附带 (undo history was reset)。
RTSRuntimeEditorEvents 参考
RTSRuntimeEditorEvents 位于核心运行时(Runtime/Core)中。即使你从项目中移除游戏内编辑器,你的游戏玩法代码也能正常编译和运行。
| 成员 | 说明 |
|---|---|
static bool IsPlaytesting | 试玩进行中时为 true。 |
static event Action<bool> PlaytestChanged | 试玩开始(true)或结束(false)时触发。 |
static event Action<RTSTerrain> MapOpened | 游戏内编辑器从文件打开地图或新建地图后触发。 |
static void ConsumeEscape() | 如果你的游戏玩法代码在某一帧中自行使用了 Esc(例如用于取消建筑放置),请在该帧中调用此方法。这样编辑器就不会在该帧结束试玩。 |
static bool EscapeConsumedThisFrame | 当前帧中调用过 ConsumeEscape 时为 true。 |
static void SetPlaytesting(bool playtesting) | 设置 IsPlaytesting 并触发 PlaytestChanged。此方法由编辑器调用。如果要从你自己的代码开始试玩,请改为调用 RTSRuntimeEditor.StartPlaytest(),因为只有编辑器会创建地图快照。 |
static void RaiseMapOpened(RTSTerrain terrain) | 触发 MapOpened。此方法由编辑器调用。 |
进入 Play 模式时,这些事件的所有订阅都会被清空。请在场景对象的 OnEnable(或 Awake /
Start)中订阅,而不要在静态构造函数或编辑器脚本中订阅,并在 OnDisable 中取消订阅。
示例:在试玩时生成单位
试玩开始时,这个组件会在每个出生点生成一个单位,并开启你的游戏玩法系统(RTS 摄像机、战争迷雾、HUD)。试玩结束时,它会移除这些单位,并再次关闭这些系统。把它添加到编辑器场景中的任意对象上,然后指定一个单位预制件。
using System.Collections.Generic;
using UnityEngine;
using RTSTerrainStudio;
/// <summary>Starts a tiny "game" whenever the in-game editor runs a play test.</summary>
public class MyPlaytestGame : MonoBehaviour
{
[Tooltip("Spawned once at every start location of the map.")]
[SerializeField] GameObject unitPrefab;
[Tooltip("Gameplay components that should only run during a play test, " +
"for example your RTS camera script, the fog of war manager or your HUD.")]
[SerializeField] Behaviour[] gameplaySystems = new Behaviour[0];
readonly List<GameObject> spawned = new List<GameObject>();
void OnEnable()
{
// Static event: always pair += in OnEnable with -= in OnDisable.
RTSRuntimeEditorEvents.PlaytestChanged += HandlePlaytestChanged;
SetSystemsEnabled(RTSRuntimeEditorEvents.IsPlaytesting);
}
void OnDisable()
{
RTSRuntimeEditorEvents.PlaytestChanged -= HandlePlaytestChanged;
DespawnAll();
}
void HandlePlaytestChanged(bool playtesting)
{
if (playtesting) StartGame();
else StopGame();
}
void StartGame()
{
var terrain = RTSTerrain.Main; // the first RTS Terrain in the scene
if (terrain == null || terrain.Map == null) return;
foreach (var start in terrain.Map.StartLocations)
{
// Start locations are stored in map-local space: convert to world space
// and put the unit on the ground.
Vector3 position = terrain.LocalToWorld(start.position);
position.y = terrain.GetHeight(position);
Quaternion rotation = Quaternion.Euler(0f, terrain.transform.eulerAngles.y + start.yaw, 0f);
var unit = Instantiate(unitPrefab, position, rotation);
unit.name = "Player " + (start.player + 1) + " Unit";
spawned.Add(unit);
}
SetSystemsEnabled(true);
}
void StopGame()
{
// Called BEFORE the editor restores the map, so everything is still in place.
DespawnAll();
SetSystemsEnabled(false);
}
void DespawnAll()
{
foreach (var unit in spawned)
if (unit != null) Destroy(unit);
spawned.Clear();
}
void SetSystemsEnabled(bool enabled)
{
foreach (var system in gameplaySystems)
if (system != null) system.enabled = enabled;
}
}
StartLocation.position 的类型是 Unity.Mathematics.float3,它会自动转换为 Vector3。如果你的脚本位于单独的程序集定义中,需要添加对 RTSTerrainStudio.Runtime 和 Unity.Mathematics 的引用。
示例:与你的游戏共用 Esc 键
许多 RTS 游戏用 Esc 键取消命令或建筑放置。在使用该键的那一帧调用 ConsumeEscape(),编辑器就不会结束试玩。编辑器在 LateUpdate 中检查这一点,因此你的脚本按什么顺序执行都无关紧要。
using UnityEngine;
using RTSTerrainStudio;
public class MyBuildingPlacement : MonoBehaviour
{
bool placing; // true while a building ghost follows the cursor
void Update()
{
if (placing && RTSInput.GetKeyDown(RTSKey.Escape))
{
placing = false; // cancel the placement ...
RTSRuntimeEditorEvents.ConsumeEscape(); // ... and keep the play test running
}
}
}
示例脚本 RTSBuildingPlacementDemo、RTSCommandController 和 RTSSelectionManager 已经这样处理了。在演示中,第一次按 Esc 会清除选择,再按一次则返回编辑器。
适用于你自己游戏的试玩检查清单
- 在场景中让你的 RTS 摄像机脚本、战争迷雾管理器和 HUD 保持禁用状态,并在
PlaytestChanged(true)中启用它们。演示场景对RTSCameraController和RTSFogOfWarManager也是这样处理的。 - 试玩期间,编辑器的内置摄像机处于关闭状态,因此你的摄像机脚本可以完全控制摄像机。
- 在
PlaytestChanged(false)中移除你生成的所有对象。你不需要撤销对地图的更改,因为编辑器会恢复地图。 - 在多人联机(Mirror 集成)中,游戏运行期间对地图所做的编辑不会实时流式传输。调用
RTSNetworkMapSync.SendMapToAll()可以重新发送地图。
在构建版本中保存和加载地图
地图的存储位置
地图保存在 RTSRuntimeEditor.MapsFolder 中。默认路径为 RTSMapSerializer.StorageRoot 中的 Maps 文件夹,也可以通过 RTSMapSerializer.DefaultMapsFolder 获取。除 Web 平台外,StorageRoot 在所有平台上都是 Application.persistentDataPath。地图文件夹在首次保存时创建。persistentDataPath 的具体位置由 Unity 按平台决定。在 Windows 上,它位于 %USERPROFILE%\AppData\LocalLow\<Company>\<Product> 下,其中的公司名称和产品名称取自 Player Settings。在桌面平台上,File ▸ Show Maps Folder 会打开该文件夹,地图浏览器中也会显示完整路径。
在 Web 构建中,地图保存在浏览器的存储空间里,玩家可以通过 File ▸ Download Map 和 File ▸ Import Map... 在浏览器与自己的电脑之间转移地图。参见浏览器中的地图。
每张地图最多对应两个文件:
| 文件 | 示例 | 说明 |
|---|---|---|
| 地图 | Glacier Pass (2v2).rtsmap | 完整的地图:高度、悬崖、纹理、水体、植被、装饰物、通行数据、出生点、区域、标记点、地图信息和环境。它与 Unity 编辑器中的地图使用相同的格式。 |
| 预览图 | Glacier Pass (2v2).preview.png | 供地图浏览器使用的 192 × 192 小地图图像。只有开启 Write Preview Images 时才会写入,删除地图时也会一并删除。 |
文件名根据地图名称生成:文件名中不允许出现的字符会替换为 _,末尾的句点会被去掉,名称为空时则使用 New Map。
未保存的更改
- 存在未保存的更改时,状态栏会在地图名称前显示一个圆点。
- New Map、Open 和 Quit 会先进行询问,可选择 Save、Don't Save 或 Cancel。
- 如果地图存在未保存的更改时玩家关闭了游戏窗口,编辑器会取消退出并同样进行询问,除非关闭了 Confirm Quit With Unsaved Changes。在网页浏览器中,则改由浏览器本身在关闭或重新加载标签页之前询问 Leave site?,此行为同样受该选项控制。
- 试玩不会改变未保存状态,编辑器会随地图一起恢复该状态。
- 场景启动时使用的地图(指定给地形的地图资源)在地图文件夹中还没有对应的文件,因此第一次按 Ctrl+S 时会要求输入名称。
在 Unity 编辑器中测试:如果你在 Play 模式下用游戏内编辑器编辑地图,然后退出 Play 模式,Unity 会询问 Save map changes made in Play Mode?,选择 Save 会把更改写入项目中该地形的地图资源。而用 Ctrl+S 保存的地图则会存入地图文件夹。你可以把这样的 .rtsmap 文件复制到 Assets 文件夹中,作为地图资源使用。
随游戏发布地图
地图浏览器只列出地图文件夹中的内容。如果想为玩家提供你自己制作的地图作为起点,可以在游戏首次启动时把它们复制到该文件夹。下面的示例会复制 StreamingAssets/Maps 中的所有文件(地图和预览图像),并且绝不会覆盖玩家编辑过的版本:
using System.IO;
using UnityEngine;
using RTSTerrainStudio;
/// <summary>Copies bundled maps into the in-game editor's maps folder (desktop platforms).</summary>
public class InstallBundledMaps : MonoBehaviour
{
[SerializeField] RTSRuntimeEditor editor;
void Awake()
{
// Put your .rtsmap files (and optional .preview.png files) into Assets/StreamingAssets/Maps.
string source = Path.Combine(Application.streamingAssetsPath, "Maps");
// On Android and WebGL, StreamingAssets is not a normal folder: use UnityWebRequest there instead.
if (!Directory.Exists(source)) return;
string target = editor.MapsFolder; // StorageRoot/Maps unless you changed it
Directory.CreateDirectory(target);
foreach (string file in Directory.GetFiles(source))
{
if (file.EndsWith(".meta")) continue; // only matters when running in the Unity editor
string destination = Path.Combine(target, Path.GetFileName(file));
if (!File.Exists(destination)) File.Copy(file, destination);
}
}
}
在游戏中游玩玩家制作的地图
你的游戏无需编辑器也能从地图文件夹加载地图,例如在自定义游戏大厅中。RTSMapSerializer.TryReadHeaderFromFile 可以在不加载整张地图的情况下读取名称、尺寸、作者和玩家数,RTSTerrain.LoadFromFile 则负责加载地图。
using System;
using System.IO;
using UnityEngine;
using RTSTerrainStudio;
public static class MapLobby
{
/// <summary>Logs every map in the default maps folder.</summary>
public static void ListMaps()
{
string folder = RTSMapSerializer.DefaultMapsFolder;
if (!Directory.Exists(folder)) return;
foreach (string path in Directory.GetFiles(folder, "*" + RTSMapSerializer.FileExtension))
{
// Reads only the small header: fast, even for large maps.
if (RTSMapSerializer.TryReadHeaderFromFile(path, out RTSMapHeader header))
Debug.Log(header.name + " by " + header.author + ": " + header.width + " x " + header.height +
" cells, " + header.maxPlayers + " players, modified " + header.Modified);
}
}
/// <summary>Loads a map file into the main terrain. Returns false when the file cannot be read.</summary>
public static bool Play(string path)
{
var terrain = RTSTerrain.Main;
if (terrain == null) return false;
try
{
terrain.LoadFromFile(path); // throws when the file is missing or damaged
return true;
}
catch (Exception e)
{
Debug.LogError("Could not load " + path + ": " + e.Message);
return false;
}
}
}
地图按 ID 存储其内容(地面图层、植被原型和装饰物原型)。只有使用与编辑器场景中地形相同的图块集、植被库和装饰物库,玩家制作的地图才能正确显示,因此请为游戏场景中的地形指定相同的资源。
通过代码控制编辑器
UI 中能做的一切,都可以通过 RTSRuntimeEditor(命名空间 RTSTerrainStudio)用代码完成。最常用的成员如下:
| 成员 | 说明 |
|---|---|
| 状态 | |
bool EditorVisible { get; set; } | 显示或隐藏整个编辑器(与切换键作用相同)。隐藏时会结束当前笔触、关闭对话框,并把地形视图交还给游戏。 |
bool PanelsVisible { get; set; } | 仅隐藏面板(H),编辑功能仍然可用。 |
bool IsPlaytesting { get; } | 由此编辑器启动的试玩正在运行时为 true。 |
RTSKey ToggleKey { get; set; } | 显示/隐藏编辑器的按键。设为 RTSKey.None 会禁用该按键,同时隐藏关闭按钮(在网页浏览器以及手机和平板电脑上除外,参见 HasReliableFunctionKeys)。 |
RTSTerrain Terrain { get; set; } | 正在编辑的地形。设置此属性会切换地形并清空撤销历史。 |
Camera TargetCamera { get; set; } | 用于拾取和显示 Gizmo 的摄像机。 |
RTSRuntimeEditorCamera EditorCamera { get; } | 内置摄像机;没有时为 null。 |
RTSEditSession Session { get; } | 编辑会话:工具、笔刷、对称、选区、撤销。 |
ToolCategory ActiveCategory { get; } | 当前工具所属的类别。 |
bool HasHover, Vector3 HoverWorldPosition | 指针是否位于地形上方,以及指针下方的世界坐标。 |
bool IsPointerOverUI { get; } | 指针位于编辑器面板上方时为 true。 |
bool ShowGrid, bool ShowMarkers | 对应 G 和 M 开关。 |
PathingOverlayMode? PathingOverlay, float OverlayOpacity | 通行叠加层(null 表示关闭)及其不透明度(0 到 1)。 |
string MapsFolder { get; set; } | 地图文件夹。留空表示使用 RTSMapSerializer.DefaultMapsFolder;相对路径以 RTSMapSerializer.StorageRoot 为基准。 |
bool ShowQuitCommand, string ManualUrl | File ▸ Quit 菜单项和使用手册链接(参见 Inspector 窗口)。 |
bool HasManual { get; } | 当 Open Manual 有可打开的页面时为 true。 |
static bool CanShowFolders, static bool CanQuitApplication | 平台检查:是否可以使用桌面文件浏览器,以及是否可以退出应用(WebGL、iOS 和 Android 上不可以)。 |
static bool RunsInBrowser, static bool StoresMapsInBrowser | 在 Web 构建中,RunsInBrowser 为 true,此时浏览器的各项限制生效,New Map 的快捷键为 Alt+N。当地图保存在浏览器的存储空间中时(WebGL Player),StoresMapsInBrowser 为 true。参见 Web(WebGL)构建。 |
static bool HasReliableFunctionKeys | 在网页浏览器和移动平台上为 false。在这些环境中,用关闭按钮关闭编辑器后会显示 Open Map Editor 按钮。 |
VisualElement RootElement, UIDocument Document, RuntimeToolGizmos Gizmos | 编辑器 UI 的根元素、它的 UI 文档以及 Gizmo 渲染器。组件启用之前,它们都为 null。 |
| 命令 | |
bool SetActiveTool(string toolId) | 按 ID 激活工具,例如 "paint.layer"。如果该 ID 不存在,返回 false。 |
void SelectCategory(ToolCategory category) | 与数字键作用相同:激活该类别中上次使用的工具;如果该类别已处于激活状态,则切换到下一个工具。 |
RTSTool GetRememberedTool(ToolCategory category) | 返回某个类别中上次使用的工具;如果没有,则返回该类别的第一个工具。 |
void Undo(), void Redo() | 撤销和重做。试玩期间它们不起作用。 |
bool OpenMap(string path) | 加载 .rtsmap 文件。未保存的更改会被直接丢弃,不会事先询问。 |
bool SaveMap(string path) | 保存到指定路径,必要时补上 .rtsmap 扩展名,并写入预览图像。 |
bool SaveCurrent() | 与 Ctrl+S 作用相同。如果地图被立即写入文件,则返回 true;如果地图还没有对应的文件,则打开 Save As 对话框并返回 false。 |
void NewMap(MapCreationSettings settings) | 新建一张平坦的地图,不会询问如何处理未保存的更改。 |
bool DeleteMapFile(string path) | 删除地图文件及其预览图像。 |
ShowNewMapDialog(), ShowOpenDialog(), ShowSaveAsDialog(), ShowGenerateDialog(), ShowMapInfoDialog(), ShowResizeDialog(), ShowHelp() | 打开相应的对话框,行为与菜单命令相同。New 和 Open 会先询问如何处理未保存的更改。 |
void StartPlaytest(), void StopPlaytest() | 开始或结束试玩。 |
void TogglePathingOverlay() | 与 P 键作用相同。 |
void DeleteSelectedDecor(), void DuplicateSelectedDecor() | 删除或复制装饰物选区。每次操作都是一个撤销步骤。 |
void ShowMapsFolder(), void OpenManual(), void Quit() | File 和 Help 菜单中的命令。Quit 会询问如何处理未保存的更改。 |
bool DownloadMap(), void ImportMap() | 仅限 Web 构建:对应 File ▸ Download Map 和 File ▸ Import Map...。浏览器只会在响应单击或按键时显示文件选择器,因此要在单击事件的处理程序中调用 ImportMap。 |
void ShowToast(string message, ToastType type = ToastType.Info) | 显示一条浮动提示。ToastType 可以是 Info、Success、Warning 或 Error。 |
void ShowToast(string message, ToastType type, string key) | 如果具有相同键的浮动提示仍在显示,则更新这条提示,而不是新增一条。 |
| 事件 | |
event Action<string> MapSaved | 地图保存之后触发(参数为完整文件路径)。 |
event Action<string> MapLoaded | 从文件打开地图(参数为完整路径)或新建地图(参数为 null)之后触发。 |
event Action<bool> VisibilityChanged | EditorVisible 发生变化时触发。 |
MapLoaded 和 RTSRuntimeEditorEvents.MapOpened 会在执行 File ▸ New 和 Open 时触发。如果要响应每一次地图变化(包括 Resize 以及由你自己的代码加载的地图),需要订阅地形上的 RTSTerrain.MapLoaded。
示例:从你自己的菜单打开编辑器
许多游戏通过主菜单而不是按键打开地图编辑器。做法是:关闭切换键,通过一个按钮打开编辑器,并在编辑器的顶层添加一个“Back to Menu”按钮:
using UnityEngine;
using UnityEngine.UIElements;
using RTSTerrainStudio;
public class MapEditorLauncher : MonoBehaviour
{
[SerializeField] RTSRuntimeEditor editor; // with "Start Visible" turned off
[SerializeField] GameObject mainMenu; // your own menu UI
void Start()
{
// No F2 key: the game decides when the editor is shown (on desktop this also hides the close button).
editor.ToggleKey = RTSKey.None;
// RootElement exists once the editor component is enabled (it is created in its OnEnable).
// If you disable and enable the editor component later, its UI is rebuilt: add the button again.
var back = new Button(BackToMenu) { text = "Back to Menu" };
back.AddToClassList("rts-button"); // use the editor's button style
back.style.position = Position.Absolute;
back.style.right = 16;
back.style.bottom = 56; // just above the status bar
editor.RootElement.Add(back); // hidden together with the editor
editor.VisibilityChanged += visible => mainMenu.SetActive(!visible);
}
/// <summary>Hook this up to a "Map Editor" button in your main menu.</summary>
public void OpenEditor() => editor.EditorVisible = true;
void BackToMenu()
{
if (editor.IsPlaytesting) editor.StopPlaytest();
editor.EditorVisible = false;
editor.ShowToast("Map editor closed", ToastType.Info);
}
}
示例:编辑时隐藏游戏 HUD
using UnityEngine;
using RTSTerrainStudio;
public class HudVisibility : MonoBehaviour
{
[SerializeField] RTSRuntimeEditor editor;
[SerializeField] GameObject hud;
void OnEnable()
{
editor.VisibilityChanged += HandleVisibilityChanged;
RTSRuntimeEditorEvents.PlaytestChanged += HandlePlaytestChanged;
Refresh();
}
void OnDisable()
{
editor.VisibilityChanged -= HandleVisibilityChanged;
RTSRuntimeEditorEvents.PlaytestChanged -= HandlePlaytestChanged;
}
void HandleVisibilityChanged(bool visible) => Refresh();
void HandlePlaytestChanged(bool playtesting) => Refresh();
// The editor stays "visible" during a play test (only its panels make room),
// so both states are checked.
void Refresh() => hud.SetActive(!editor.EditorVisible || RTSRuntimeEditorEvents.IsPlaytesting);
}
自定义 UI
样式文件的位置
文件(位于 Resources/RTSTerrainStudio/UI) | 加载路径 | 用途 |
|---|---|---|
RTSRuntimeTheme.tss | RTSRuntimeEditor.ThemeResourcePath = "RTSTerrainStudio/UI/RTSRuntimeTheme" | 主题样式表。它导入了 Unity 的默认运行时主题(滑块、开关、文本框、滚动视图)以及 RTSRuntimeEditor.uss。 |
RTSRuntimeEditor.uss | RTSRuntimeEditor.StyleSheetResourcePath = "RTSTerrainStudio/UI/RTSRuntimeEditor" | 编辑器的全部样式。所有类名都以 rts- 开头,Label 等类型选择器的作用范围也限定在 .rts-root 之下,因此即使你的面板使用了此主题,编辑器也绝不会改变你游戏 UI 的外观。 |
如果你的面板设置使用了其他主题,编辑器会把 RTSRuntimeEditor.uss 添加到自己的根元素上,因此无论哪种情况,外观都保持一致。
颜色令牌
所有颜色都是定义在 .rts-root 上的变量。修改它们即可为整个编辑器换肤:
| 变量 | 默认值 | 用途 |
|---|---|---|
--rts-panel | rgba(20, 24, 30, 0.92) | 面板背景(顶栏、工具栏、工具面板、状态栏)。 |
--rts-panel-solid | rgb(22, 26, 33) | 不透明背景(对话框、弹出窗口)。 |
--rts-raised / --rts-hover / --rts-pressed | 白色,不透明度分别为 6% / 11% / 3% | 按钮状态。 |
--rts-border / --rts-border-strong | 白色,不透明度分别为 8% / 16% | 边框和分隔线。 |
--rts-input | rgba(0, 0, 0, 0.32) | 文本框和滑块的背景。 |
--rts-text / --rts-text-dim / --rts-text-faint | rgb(226, 231, 238) / rgb(150, 160, 174) / rgb(104, 114, 128) | 正文、次要文本、提示文字。 |
--rts-accent / --rts-accent-hover / --rts-accent-dim / --rts-accent-text | 蓝色 rgb(61, 169, 252) 系列 | 选中的工具、主要按钮、Play Test。 |
--rts-danger / --rts-danger-dim | rgb(239, 83, 80) | 删除按钮和错误浮动提示。 |
--rts-warning / --rts-success | rgb(255, 183, 77) / rgb(102, 187, 106) | 警告和成功浮动提示。 |
--rts-radius / --rts-radius-small | 6px / 4px | 圆角半径。 |
在不修改包文件的前提下换肤
不要直接编辑包中 Resources 文件夹里的文件,包更新时会覆盖这些文件。请改为把修改写进你自己的样式表。
- 在项目中创建一个样式表,例如
Assets/UI/MyEditorSkin.uss,并覆盖这些颜色令牌。选择器:root .rts-root的优先级高于包中的.rts-root,因此你的值会生效:/* MyEditorSkin.uss: an orange accent and warmer panels */ :root .rts-root { --rts-accent: rgb(255, 160, 50); --rts-accent-hover: rgb(255, 190, 110); --rts-accent-dim: rgba(255, 160, 50, 0.22); --rts-accent-text: rgb(255, 210, 160); --rts-panel: rgba(30, 24, 20, 0.94); } - 在启动时将该样式表添加到编辑器的 UI 文档中:
using UnityEngine; using UnityEngine.UIElements; using RTSTerrainStudio; public class EditorSkin : MonoBehaviour { [SerializeField] RTSRuntimeEditor editor; [SerializeField] StyleSheet skin; // drag MyEditorSkin.uss here void Start() { // The document root holds the editor and its toast layer, so both get the new colours. editor.Document.rootVisualElement.styleSheets.Add(skin); } }
你也可以用同样的方法单独调整某些部分的样式。主要的类有 .rts-topbar、.rts-leftbar、.rts-toolpanel、.rts-statusbar、.rts-dialog、.rts-popup、.rts-toast(包含 --success、--warning 和 --error 变体)、.rts-hint、.rts-playtest-bar、.rts-button 和 .rts-selected。游戏运行时,UI Toolkit Debugger(Window ▸ UI Toolkit ▸ Debugger)会显示每个元素的类。
面板设置、缩放与排序
- Panel Settings 为空(默认):编辑器会使用 RTS 主题创建自己的面板设置。UI 随屏幕高度缩放(参考分辨率 1920 × 1080),但不会低于 85%。Ui Scale 会作为倍率乘到这个结果上,适用于 4K 屏幕或希望文字更大的玩家。
- 使用你自己的 Panel Settings:指定一个资源,即可由你自己控制主题和缩放。 会在
Resources/RTSTerrainStudio/UI/RTSRuntimePanelSettings.asset创建一个现成的资源(RTS 主题,随屏幕尺寸缩放,1920 × 1080)。In-Game Editor Demo 场景使用的就是它。 - Enforce Minimum UI Scale(默认开启):如果你的资源随屏幕尺寸缩放,并且不渲染到纹理中,编辑器会使用该资源的运行时副本,资源本身始终保持不变。在大屏幕上,副本的缩放与该资源完全相同;在小窗口中,它永远不会小于 85%;Ui Scale 会作为倍率乘到它上面。副本的排序顺序比该资源高半级,因此即使游戏 HUD 使用同一个资源,编辑器也会显示在 HUD 之上(此时 HUD 拥有自己的面板)。如果要按原样使用该资源,例如让编辑器与你的游戏 UI 共用一个面板,请关闭此选项;此时 Ui Scale 和 85% 的下限都不再生效。
- Web 构建:85% 的下限以 CSS 像素计算,因此在高分辨率屏幕上 UI 会保持相同的尺寸。
- Sorting Order:默认值为 1000,因此编辑器会覆盖在你的 HUD 之上。如果你的某些 UI 必须显示在编辑器上方,请调低此值。
- 添加你自己的控件:将元素添加到
RootElement(参见上面的启动器示例)。这些元素会随编辑器一起隐藏;指针悬停在它们上方时,会像在其他面板上一样阻止绘制。
发布游戏内编辑器
在构建带有游戏内编辑器的游戏之前,逐项完成以下清单。
- 运行 ,并修复它报告的问题。对于构建来说,Shader references 检查最为重要(参见下一步)。
- 着色器通过
RTSShaderReferences包含到构建中。如果构建中没有任何材质使用某个着色器,Unity 就不会把它打包进构建。本包的大部分材质都在运行时创建(地形、水体、战争迷雾,以及编辑器的 Gizmo 着色器Hidden/RTS Terrain Studio/GizmoLines和Hidden/RTS Terrain Studio/GizmoGhost),因此它把这些着色器保存在Resources/RTSTerrainStudio/RTSShaderReferences资源中。脚本编译后、导入着色器时以及每次 Player 构建之前,该资源都会自动更新。如果它缺失或已过期,可使用验证器中的 Update 修复项。缺少这些着色器时,构建版本中不会显示笔刷轮廓和装饰物预览。 - 保留
Resources/RTSTerrainStudio文件夹及其中的UI/RTSRuntimeTheme.tss、UI/RTSRuntimeEditor.uss和RTSShaderReferences.asset。编辑器通过 Resources 路径加载它们。如果缺少主题,Console 窗口会显示 Controls will be unstyled。 - Panel Settings:如果你指定了 Panel Settings 资源,场景会引用它,构建时也会自动包含它。如果该字段留空,运行时面板设置会从 Resources 中加载主题(见第 3 步)。
- 输入:编辑器既支持 Input System 包,也支持旧版 Input Manager。参见输入处理。
- 确定面向玩家的选项:Manual Url(链接到你的模组制作指南;留空则隐藏该按钮)、Show Quit Command(如果游戏有自己的菜单,可将其关闭)、Toggle Key 和 Start Visible。
- 地图文件夹:默认的
Maps文件夹位于RTSMapSerializer.StorageRoot(persistentDataPath)中,在所有桌面平台上都可写入。如果要为玩家提供示例地图,可在首次启动时将它们复制到该文件夹(参见随游戏发布地图)。 - Web 构建:安装全窗口模板,并阅读 Web(WebGL)构建 一节,其中介绍了浏览器存储、下载、按键和网站托管。
- 自定义工具与代码剥离:如果 Managed Stripping Level 高于 Minimal,请用
[Preserve]标记你自己的工具(参见代码剥离与自定义工具)。 - 内容:玩家只能使用编辑器场景中指定给地形的图块集、植被库和装饰物库。由于场景引用了它们,它们会被包含在构建中。
- 测试开发版本:将绘制、保存、打开、试玩以及在有未保存的更改时退出各执行一次。
不打算发布游戏内编辑器?它的代码位于 Runtime/RuntimeEditor,菜单支持代码位于 Editor/RuntimeEditorSupport。不需要它的项目可以删除 Runtime/RuntimeEditor 文件夹。RTSRuntimeEditorEvents 保留在 Runtime/Core 中,因此监听试玩事件的游戏玩法代码仍可正常编译。
故障排除
| 问题 | 原因与解决方法 |
|---|---|
| 出现居中提示 No RTS Terrain found。 | 场景中没有已启用的 RTS Terrain,或者指定的地形已被禁用。通过 添加一个,或者将其启用。 |
| 出现居中提示 No camera found。 | 没有摄像机带有 MainCamera 标签,并且 Target Camera 为空。为你的摄像机设置该标签,或者将它指定给该字段。 |
| 单击地图没有反应。 | 指针位于面板上方、按住了 Alt(旋转摄像机)、有对话框处于打开状态,或者没有激活任何工具。没有激活工具时,状态栏会显示 Pick a tool on the left。 |
| 快捷键没有反应。 | 某个文本框拥有键盘焦点,或者有对话框处于打开状态。按 Esc 或单击地图。 |
| 按 F5 或 F2 没有反应。 | F5 仅在编辑器可见且未打开对话框时有效。试玩期间切换键无效(需先按 Esc 或 F5 结束试玩),在字段中输入文字时也无效。在 Mac 和许多笔记本电脑上,功能键默认是媒体键:按住 Fn,或者使用 Play Test 按钮;在 Web 构建中还可以使用 Open Map Editor 按钮。 |
| Esc 结束了试玩,尽管我的游戏已经处理了这次按键。 | 在该帧中调用 RTSRuntimeEditorEvents.ConsumeEscape()(参见示例)。 |
| 试玩期间什么也没有发生。 | 没有脚本监听 RTSRuntimeEditorEvents.PlaytestChanged。添加 RTSDemoGame 或你自己的组件(参见 Play Test)。 |
| 试玩结束后,浮动提示显示 undo history was reset。 | 你的游戏玩法代码在试玩期间替换、重新加载了地图,或调整了地图尺寸。地图本身仍会正确恢复。 |
| UI 太小或太大。 | 修改 Ui Scale。如果你指定了自己的 Panel Settings 且它不随屏幕尺寸缩放,或者你关闭了 Enforce Minimum UI Scale,则改为在该资源中修改缩放设置。如果 Web 构建使用的是固定尺寸的模板,请安装全窗口模板(参见全窗口模板)。 |
| 我的 HUD 遮住了编辑器,或者编辑器遮住了某个菜单。 | 调整 Sorting Order(默认 1000)。 |
| 控件没有样式(显示为 Unity 默认的白色外观)。 | 在 Resources 文件夹中找不到主题。恢复 Resources/RTSTerrainStudio/UI。 |
| 构建版本中缺少笔刷轮廓和预览。 | 构建时遗漏了 Gizmo 着色器。运行 并更新着色器引用。 |
| 欢迎提示再也不显示了。 | 玩家已关闭该提示。使用 F1 窗口中的 Open Welcome Hint,或删除 PlayerPrefs 键 RTSTerrainStudio.InGameEditor.HintDismissed。 |
| 我的地图没有出现在地图浏览器中。 | 地图浏览器只列出直接位于地图文件夹(路径显示在浏览器顶部)中的 .rtsmap 文件。将文件复制到该文件夹,然后单击 Refresh。 |
| 某张地图显示 Unreadable or damaged file。 | 该文件不是有效的 .rtsmap 文件,或者文件不完整(例如复制过程被中断)。 |
| 在 Web 构建中保存的地图不见了。 | 浏览器按网站分别保存地图,因此换一个网址或删除浏览数据后,游戏就会从没有地图的状态开始;此外,Safari 可能会删除连续七天未访问的网站的数据。使用 File ▸ Download Map 保存副本(参见浏览器中的地图)。 |
| 构建版本中的编辑器没有任何工具。 | 代码剥离移除了这些工具。保留本包的 Runtime/link.xml,并用 [Preserve] 标记你自己的工具(参见代码剥离与自定义工具)。 |
渲染与世界系统
本章介绍地图如何变成屏幕上的像素:地形网格如何构建和着色,以及植被、水体、装饰物和场景光照如何绘制。每一节都先介绍你能看到的效果和可以修改的设置,后面再讲解程序员需要的脚本 API、着色器约定和性能数据。
渲染组件
所有系统都读取 RTSTerrain 的同一份 MapData,并且只重建编辑所改动的部分。这些系统是地形 GameObject 上的独立组件。 会自动为你添加全部组件。
| 组件 | 绘制内容 | 生成的对象 | 章节 |
|---|---|---|---|
RTSTerrain | 地面、悬崖、坡道(区块网格,共用一个材质)、单元格栅格、叠加层和笔刷光标 | “RTS Chunks”根对象,每个区块对应一个子对象 | 地形渲染、地形着色器 |
RTSVegetationRenderer | 绘制的草和植物(细节)、树木 | 隐藏的“RTS Vegetation Objects”(碰撞体代理、GameObject 树木) | 植被 |
RTSWaterRenderer | 透明水面,带有深度颜色、波浪、折射和泡沫 | 隐藏的“RTS Water”根对象,每个含水的区块对应一个子对象 | 水体 |
RTSDecorManager | 道具和装饰物(doodad),以 GameObject 形式绘制或通过 GPU 实例化绘制 | 存放生成对象的“RTS Decor”容器 | 装饰物 |
RTSEnvironment(可选) | 自身不绘制任何内容,而是将地图的太阳光、环境光和雾效应用到场景中 | 无 | 环境 |
生成的对象都标记为 DontSave,永远不会写入你的场景文件:.rtsmap 文件是唯一的数据来源,打开场景时所有内容都会根据它重建。如果你在 Scene 视图中单击生成的区块、树木或水面,选中的会是它所属的地形。
如果缺少某个渲染器组件,RTSTerrain 的 Inspector 窗口会显示一条警告(例如“No Water Renderer: painted water is not shown.”),并附带 Add 按钮。
地形渲染
区块
地图划分为正方形的区块,每个区块的边长为 16、32(默认)或 64 个单元格。每个区块都是一个 GameObject,带有 MeshFilter、MeshRenderer、一个可选的 MeshCollider 以及 RTSTerrainChunk 组件。所有区块共用一个材质实例(RTSTerrain.TerrainMaterial),因此地形能够兼容 SRP Batcher。
- 区块按行存储:索引 =
z * ChunkCount.x + x。当地图尺寸不是区块大小的整数倍时,最后一行和最后一列的区块可能较小。 - 一次编辑只会重建它涉及的区块。每个发生变化的矩形区域都会向外扩展一个单元格,因为顶点的改动也会影响相邻单元格的法线和悬崖形状。因此,在靠近区块边界的位置绘制时,相邻区块也会重建。
- 绘制地面纹理不会重建网格,只会更新 Splat 纹理(参见 Splat 分辨率)。
- 绘制水体同样不会重建地形区块,只有水体渲染器会对此作出响应。
区块网格位于地图局部空间中:区块对象位于地形隐藏的 RTS Chunks 根对象之下,局部变换为单位变换;该根对象会抵消地形的缩放,因此对象空间等于地图局部空间(X/Z 从 0 到地图的世界尺寸,Y 轴朝上)。地形的变换可以包含平移和绕 Y 轴的旋转,但缩放会被忽略。
网格生成
RTSChunkMeshBuilder 使用 Burst 作业生成区块网格,并通过 Mesh.MeshDataArray 写入网格数据:
- 需要重建的区块会分成若干批,每批最多
RTSChunkMeshBuilder.MaxChunksPerBatch(256)个区块,以限制超大地图上的内存峰值。 - 如果只有少数区块需要重建(绘制时的典型情况),每个区块会按行切分成若干条带。这些条带在不同的工作线程上生成,然后再合并,因此即使只编辑一个区块,也能用上所有 CPU 核心。
BuildChunkGeometryJob(Burst,并行)负责创建三角形。平滑单元格和坡道单元格会变成由 S × S 个四边形组成的规则栅格(S = Surface Subdivisions)。悬崖单元格则在更精细的子栅格(Cliff Subdivisions)上用 Marching Squares 算法描绘轮廓,从而生成圆润的轮廓线、每个层级各自平坦的地面,以及层级之间的垂直崖壁。- 碰撞体数据通过
Physics.BakeMesh并行烘焙,然后指定给各区块的MeshCollider。 Build在返回之前会完成全部工作,因此编辑结果在同一帧内即可看到。
相邻的单元格和区块生成的边界顶点总是逐位相同,因为每个顶点的位置只取决于它在地图上的位置。这就是区块之间不会出现裂缝的原因。生成器遵循参考表面 MapGeometryView,高度查询、通行系统和水体渲染器也使用同一个参考表面。因此,渲染出来的地面与游戏逻辑所看到的地面完全一致。
顶点格式为 RTSTerrainVertex(28 字节:位置、法线和一个打包的颜色)。颜色中存放供着色器使用的数据:r = 崖壁遮罩,g = 悬崖类型,b = 烘焙的环境光遮蔽。顶点没有 UV 和切线:着色器根据顶点位置映射纹理。
崖壁造型
悬崖以程序化方式生成,因此不需要美术预先制作悬崖模块。以下四个设置项控制崖壁的外观:
| 设置项 | 默认值 | 范围 | 说明 |
|---|---|---|---|
| Cliff Noise | 0.18 | 0–0.5 | 崖壁顶点的随机水平位移,以单元格宽度的比例表示,让崖壁呈现自然的岩石外观。0 = 完全光滑的崖壁。 |
| Cliff Noise Scale | 1.6 | ≥ 0.1 | 悬崖噪声的特征尺寸,以世界单位表示。较小的值产生细碎、嶙峋的岩石;较大的值产生宽大的凸起。 |
| Cliff Slant | 0.12 | 0–0.4 | 悬崖顶边向后收进的距离,以单元格宽度的比例表示。它使崖壁略微倾斜,而不是完全垂直。 |
| Cliff Wall Segments | 3 | 1–8 | 每个悬崖层级在竖直方向上的分段数。分段越多,噪声可作用的顶点就越多(起伏更丰富),但顶点数也会相应增加。 |
噪声与倾斜叠加后,顶点的位移永远不会超过单元格宽度的 45%(RTSChunkMeshParams.MaxDisplacementFraction)。如果两个设置项之和超过这一上限,两者会按比例一起缩小,因此紧邻崖壁的地面三角形永远不会相互折叠。在坡道和地图边界附近,位移会逐渐减弱,使这些边缘保持笔直。
烘焙的环境光遮蔽(顶点颜色 b)还会让崖壁几何体的底部变暗。两面崖壁在单元格边上相接时使用相同的亮度,因此叠加和合并的悬崖看起来浑然一体,没有接缝。
碰撞体与查询
当 Collider Mode = MeshCollider(默认值)时,每个区块都会在 Chunk Layer 上获得一个 MeshCollider。每次编辑后,它会与网格一起重建。如果你的游戏依赖 Unity 物理系统(物理射线检测、刚体、NavMesh 烘焙),应使用此模式。
使用 ColliderMode.None 时不会创建任何碰撞体。即使不使用物理系统,也仍然可以查询地形:RTSTerrain.Raycast、GetHeight、GetNormal、GetCliffLevel、IsWater 和 GetPathing 都基于解析参考表面计算,在编辑模式和运行时均可使用。这样可以省去碰撞体占用的内存和烘焙(cooking)时间。
using RTSTerrainStudio;
using UnityEngine;
/// Finds the terrain point under the mouse, with or without chunk colliders.
public class TerrainPointer : MonoBehaviour
{
[SerializeField] Camera viewCamera;
void Update()
{
if (viewCamera == null) return;
Ray ray = viewCamera.ScreenPointToRay(RTSInput.PointerPosition);
// 1. Physics-free: works even when Collider Mode is None.
// RaycastAll tests every RTSTerrain in the scene and returns the nearest hit.
if (RTSTerrain.RaycastAll(ray, out RTSTerrainHit hit))
{
Debug.Log($"Cell {hit.cell} at {hit.point}, cliff level {hit.terrain.GetCliffLevel(hit.point)}");
}
// 2. Unity physics: needs Collider Mode = MeshCollider.
// RTSTerrainChunk.GetTerrain maps the collider of a chunk back to its terrain.
if (Physics.Raycast(ray, out RaycastHit physicsHit, 1000f))
{
RTSTerrain terrain = RTSTerrainChunk.GetTerrain(physicsHit.collider);
if (terrain != null) Debug.Log($"Physics hit on terrain '{terrain.name}'");
}
}
}
地形渲染设置
这些设置项位于 RTSTerrain 的 Inspector 窗口中,分为 Chunks & Geometry、Cliff Shape、Physics 和 Rendering 几组。在 Inspector 窗口中修改其中任意一项,都会自动重建地形。通过代码修改时,先更改 terrain.Settings,然后调用 terrain.RebuildAll()。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Chunk Size | 32 | 区块每条边上的单元格数(16、32 或 64)。区块越小,编辑时重建越快;区块越大,Draw Call 越少。参见选择区块大小。 |
| Surface Subdivisions | 2 | 平滑地面和坡道上,每个单元格每条边的四边形数(1–4)。1 = 经典 RTS 外观,2–4 = 更平滑的山丘。 |
| Cliff Subdivisions | 4 | 在悬崖单元格内描绘圆润悬崖轮廓线时使用的子栅格分辨率(2–8)。该值会向上取整为一个同时是 Surface Subdivisions 倍数的偶数(参见细分)。 |
| Cliff Noise, Cliff Noise Scale, Cliff Slant, Cliff Wall Segments | 参见崖壁造型 | 崖壁的外观。 |
| Collider Mode | MeshCollider | None 或 MeshCollider(每个区块一个,编辑后重建)。 |
| Chunk Layer | 0 (Default) | 区块所在的 GameObject 层。碰撞体和摄像机剔除遮罩都会用到它。绘制的细节(草、花等小型植物)也渲染在这一层上。 |
| Cast Shadows | On | 区块渲染器的阴影投射模式。 |
| Receive Shadows | 开 | 区块是否接收阴影。 |
| Material Override | 空 | 可选材质,用于替代默认材质(参见材质)。 |
| Show Grid | 关 | 始终绘制单元格栅格(主要用于编辑)。 |
| Grid Opacity | 0.35 | 开启 Show Grid 时栅格的不透明度。 |
Inspector 窗口底部的 Rebuild Terrain 按钮(RTSTerrain.RebuildAll())会销毁并重新创建所有区块、纹理和通行图。植被、水体和装饰物也会随之重建。
材质、叠加层与栅格
默认情况下,地形会使用着色器 RTS Terrain Studio/Terrain(RTSTerrain.TerrainShaderName)创建自己的材质。地形按以下顺序选择材质来源:
- 地形设置中的 Material Override(
RTSTerrainSettings.materialOverride)。 - 图块集的 Material Override(
RTSTileset.materialOverride),这样所有使用该图块集的地形都会共享同一外观。 - 一个使用
RTS Terrain Studio/Terrain的新材质。
地形始终使用源材质的副本,并向其中填入 Splat 贴图、纹理数组和地图参数。覆盖材质必须使用地形着色器,或者使用声明了相同属性的着色器(参见着色器属性约定)。
由于地形渲染的是副本,之后对覆盖材质资源所做的修改不会自动生效。编辑该材质后,请单击 Rebuild Terrain(或调用 RebuildAll())。如果要在运行时修改某个值而不重建,请直接在 terrain.TerrainMaterial 上设置。
如果找不到地形着色器(例如 Player 构建中没有包含它),地形会使用普通的灰色 URP Lit 材质绘制,同时日志中会记录一条警告:“Shader 'RTS Terrain Studio/Terrain' was not found.”此时请运行 。其中的“Shaders included in builds”检查项会创建 RTSShaderReferences 资源,确保所有包内着色器都包含在构建中(参见构建游戏)。
叠加层。SetOverlayTexture(texture, opacity) 可以将任意纹理覆盖到整张地图上,范围从第一个顶点一直到 MapData.WorldSize。地形颜色会以“纹理 Alpha × 不透明度”为混合系数,向纹理的 RGB 颜色混合。编辑器用它来显示通行视图。你可以用它制作热力图、领地显示或建筑放置预览。叠加层是表面颜色的一部分,因此会像地形一样受光照和阴影影响。
栅格。SetGridVisible(visible, opacity) 可以通过代码强制显示抗锯齿的单元格栅格,其中每 4 条线中有一条会加粗显示。在 Scene 视图中编辑时,编辑器通过 G 键切换栅格显示。无论栅格是通过代码还是通过 Show Grid 显示的,IsGridVisible 都为 true。
using RTSTerrainStudio;
using Unity.Mathematics;
using UnityEngine;
/// Shows unwalkable cells in red on top of the terrain (one overlay pixel per cell).
public class WalkabilityOverlay : MonoBehaviour
{
[SerializeField] RTSTerrain terrain;
Texture2D overlay;
public void Show()
{
MapData map = terrain != null ? terrain.Map : null;
if (map == null) return;
// One texel per cell. Point filtering keeps hard cell borders.
overlay = new Texture2D(map.Width, map.Height, TextureFormat.RGBA32, false)
{
filterMode = FilterMode.Point,
wrapMode = TextureWrapMode.Clamp
};
var pixels = new Color32[map.Width * map.Height];
for (int z = 0; z < map.Height; z++)
{
for (int x = 0; x < map.Width; x++)
{
// Texture row 0 is the map edge at z = 0, like the map itself.
Vector3 world = terrain.CellCenterToWorld(new int2(x, z));
bool walkable = terrain.IsWalkable(world);
// Alpha 0 leaves the terrain untouched, alpha 160 tints it red.
pixels[z * map.Width + x] = walkable ? new Color32(0, 0, 0, 0) : new Color32(255, 40, 40, 160);
}
}
overlay.SetPixels32(pixels);
overlay.Apply(false, false);
terrain.SetOverlayTexture(overlay, 0.8f);
}
public void Hide()
{
if (terrain != null) terrain.SetOverlayTexture(null, 0f); // null hides the overlay
if (overlay != null) Destroy(overlay);
overlay = null;
}
void OnDisable() => Hide();
}
RTSTerrain 渲染 API 一览:
| 成员 | 说明 |
|---|---|
Material TerrainMaterial | 所有区块共享的材质实例(地形启用之前为 null)。 |
Transform ChunkRoot | 容纳各个区块 GameObject 的“RTS Chunks”对象。 |
IReadOnlyList<RTSTerrainChunk> Chunks, int2 ChunkCount, int ChunkSizeInCells | 区块栅格。 |
RTSTerrainChunk GetChunk(int2 chunkCoord), GetChunkAtCell(int2 cell) | 查找区块。超出地图范围时返回 null。 |
bool IsRenderingReady | 区块、纹理和材质全部创建完毕后为 true。 |
Texture GetSplatTexture(int index) | Splat 纹理 0–3(图层 i 位于贴图 i / 4 的通道 i % 4 中)。 |
void SetOverlayTexture(Texture tex, float opacity), OverlayTexture, OverlayOpacity | 覆盖整张地图的叠加层。传入 null 或 0 即可隐藏。 |
void SetGridVisible(bool visible, float opacity), IsGridVisible | 通过代码控制单元格栅格。 |
void RebuildChunkMeshes(RectInt cellRect) | 立即重建受影响区块的网格和碰撞体。通常无需调用:NotifyChanged 会自动完成这项工作。 |
void RefreshMaterial() | 立即将图块集、纹理数组和地图参数重新应用到材质上。 |
RTSTerrainChunk: Terrain, ChunkCoord, CellRect, Mesh, MeshRenderer, MeshCollider, LocalBounds, static GetTerrain(Collider) | 每个区块的数据。GetTerrain 可将物理命中结果映射回对应的地形。 |
地形着色器
RTS Terrain Studio/Terrain(Shaders/RTSTerrain.shader)是一个手写的 URP 17.3 光照着色器。它支持 Forward、Forward+ 和 Deferred 渲染路径,以及 SRP Batcher、阴影、SSAO、贴花、光照层、光照 Cookie 和光照贴图。你无需手动编辑它的材质:所有属性都由地形根据地图及其图块集自动填充。
功能特性
| 功能 | 作用 | 控制方式 |
|---|---|---|
| 16 个地面图层 | 最多 16 个通过 Splat 混合的图层,使用 XZ 平面映射采样。只有在某个像素上具有可见权重的图层才会被采样(动态分支),因此在地图上绘制更多图层并不会让每个像素的开销都增加。 | 图块集的地面图层;Tile Size、Tint、Normal Strength、Smoothness、Metallic |
| 高度混合 | 在过渡区中,纹理高度较高的图层胜出,因此草会生长在石头之间,而不是淡入覆盖在石头上。对比度 = Height Blend² × 16;0 表示普通的线性混合。 | 每个图层的 Height Blend(0–1),外加一个高度来源:高度贴图(R 通道)或反照率 Alpha(Height From Albedo Alpha) |
| 反平铺 | 在由低频噪声选出的区域中,纹理会渐变到第二次采样,该采样使用经过旋转和缩放的 UV,从而打破肉眼可见的重复感。强度最大时,大约一半的地面使用第二次采样。这些区域会额外多出一次纹理采样的开销。 | 每个图层的 Anti Tiling(0–1) |
| 宏观变化 | 在整张地图上叠加大尺度的亮度变化和细微的冷暖色调变化,从远处看可以隐藏重复感。悬崖上的强度减半。 | 图块集的 Macro Variation(0–1,默认 0.25) |
| 三平面悬崖 | 最多 4 种悬崖类型沿三个轴向投影,因此垂直崖壁永远不会出现拉伸。法线使用 Whiteout 混合。两种悬崖类型相接处会在一条窄带内交叉淡化。 | 图块集的悬崖图层;Triplanar Sharpness(1–16,默认 6) |
| 斜坡岩石 | 坡度大于约 50° 的地面会逐渐过渡为悬崖材质(大于约 63° 时完全变为岩石),因此陡峭的山丘上永远不会出现被拉伸的草地。在岩石高度纹理较高的地方,岩石会从地面中露出来。 | 自动 |
| 遮蔽 | 悬崖底部烘焙的顶点 AO,再乘以由悬崖高度纹理得出的凹陷(cavity)项。 | 自动 |
| 单元格栅格 | 抗锯齿线条(每 4 个单元格加粗一次)。线条在远处会提前淡出,以免产生摩尔纹。 | Show Grid / SetGridVisible / 在 Scene 视图中按 G |
| 叠加层 | 覆盖整张地图的 RGBA 叠加层(通行视图、热力图)。 | SetOverlayTexture;在 Scene 视图中按 P 循环切换通行叠加层 |
| 笔刷光标 | 笔刷轮廓和衰减在着色器中精确投影到表面位置,因此你看到的位置就是绘制的位置(不受显示缩放影响)。对称绘制时最多支持 4 个位置。笔刷光标最后绘制,因此在战争迷雾中依然可见。 | RTSBrushPreview(由编辑器工具设置) |
| 战争迷雾(回退) | 未安装战争迷雾渲染器功能时,着色器会自行将未探索和不可见的区域调暗。 | RTSFogOfWarManager(参见战争迷雾) |
| 无图块集回退 | 没有图块集时,地形会渲染出中性灰色的地面和更深的灰色悬崖,而不会渲染失败。 | 自动 |
| 光照选项 | 关闭镜面高光和环境反射,以降低着色开销。 | 材质开关 Specular Highlights / Environment Reflections(在覆盖材质上设置) |
这些值大多是图块集设置:关于如何制作图层,参见图块集。
渲染通道(Pass):ForwardLit(UniversalForward)、GBuffer(UniversalGBuffer,GLES/GLCore 上不可用)、ShadowCaster、DepthOnly、DepthNormals 和 Meta。除 GBuffer 外,其余所有通道均以 Shader Model 3.5 为编译目标,因此地形也能在 WebGL 2.0 和 OpenGL ES 3.0 上渲染;GBuffer 通道则需要 Shader Model 4.5。渲染队列为 Geometry-100,因此大面积的地形会在绘制其他不透明物体之前先填充深度缓冲区。不使用 LOD 交叉淡化。
关闭镜面高光和反射
如果要适配低端设备,或者想要更平淡、更具手绘感的外观,可以使用关闭了这些光照开关的覆盖材质:
- 在 Project 窗口中选择 ,并为其命名,例如
Terrain Low End。 - 在材质的 Inspector 窗口中,将着色器设置为 。
- 在材质 Inspector 窗口底部取消勾选 Specular Highlights 和/或 Environment Reflections。其他属性保持不变即可,地形会覆盖它们。
- 将该材质指定给图块集(作用于所有使用它的地形)或单个地形(位于
RTSTerrainInspector 窗口的 Rendering 部分)的 Material Override。 - 如果地形没有立即变化,请单击 Rebuild Terrain。
这里请使用材质资源,而不是通过代码创建的材质。对于材质关键字,Unity 只会保留构建中某个材质资源实际用到的着色器变体,因此仅通过代码设置的关键字,其变体可能会在 Player 构建中被剥离。
着色器属性约定
所有属性都由 RTSTerrain 的渲染代码使用 RTSShaderIDs 中的 ID 设置。自定义着色器必须以相同的名称声明它用到的属性。标量和向量位于 UnityPerMaterial 常量缓冲区中(SRP Batcher)。
| 属性 | 类型 | 内容 |
|---|---|---|
_RTS_SplatMap0 … _RTS_SplatMap3 | Texture2D,线性 RGBA | Splat 权重。图层 i 位于贴图 i / 4 的通道 i % 4 中。所有权重之和为 1。UV = positionOS.xz * _RTS_MapSize.zw。使用双线性过滤和 Clamp 模式采样。 |
_RTS_AlbedoArray | Texture2DArray,sRGB | RGB = 反照率,A = 高度(图层没有高度时为 0.5)。切片 0–15 = 地面图层,16–19 = 悬崖图层(RTSTileset.CliffSliceOffset)。 |
_RTS_NormalArray | Texture2DArray,线性 | RGB = 切线空间法线 × 0.5 + 0.5(构建数组时已从 DXT5nm/BC5 解包)。 |
_RTS_LayerData | Texture2D 20 × 4,float,使用 Load 读取 | 每个数组切片占一列,参见下表。 |
_RTS_LayerCount | float | 图块集的地面图层数量,至少为 1(限制在 1–16)。没有图块集时,RTSTerrain 会传入 1、一组中性的图层数据和占位纹理数组(RTSTextureArrayBuilder.FallbackResolution = 128),因此地形会显示灰色地面和更深的灰色悬崖。着色器也会将 0 视为“无数据”,此时直接绘制 RTS_FALLBACK_GROUND_ALBEDO / RTS_FALLBACK_CLIFF_ALBEDO,而不读取 _RTS_LayerData;只有在 RTSTerrain 之外使用自定义材质时才会出现这种情况。 |
_RTS_MapSize | float4 | (世界尺寸 X,世界尺寸 Z,1 / X,1 / Z)。 |
_RTS_MapParams | float4 | (单元格尺寸,悬崖层级高度,宏观变化,三平面混合锐度)。包自带的着色器不使用悬崖层级高度;保留它是为了方便自定义扩展,例如基于高度的染色。 |
_RTS_OverlayTex, _RTS_OverlayOpacity | Texture2D RGBA,float | 覆盖整张地图的叠加层。不透明度 0 = 关闭。 |
_RTS_GridParams | float4 | (启用 0/1,不透明度,未使用,未使用)。 |
_SpecularHighlights, _EnvironmentReflections | float 开关 | 关键字 _SPECULARHIGHLIGHTS_OFF、_ENVIRONMENTREFLECTIONS_OFF。 |
_RTS_LayerParams[20], _RTS_LayerTints[20], _RTS_LayerParams2[20] | float4 数组 | 与上面相同的逐图层数值,以 float4 数组形式提供,供基于数组编写的自定义着色器使用。包自带的着色器只读取 _RTS_LayerData。 |
RTSShaderIDs.WorldToMap(_RTS_WorldToMap)为保留项,RTSTerrain 不会设置它。请使用对象空间(positionOS),对区块网格而言,它就是地图局部空间。
_RTS_LayerData 布局(列 = 数组切片):
| 行 | x | y | z | w |
|---|---|---|---|---|
| 0 | 1 / 平铺尺寸 | 法线强度 | 平滑度 | 高度混合 |
| 1 | 色调 RGB,以线性颜色表示(Color.linear) | 金属度 | ||
| 2 | 反平铺 | 有高度(0/1) | 图层已使用(0/1) | 0 |
| 3 | 小地图颜色(着色器不读取) | |||
“图层已使用”为 0 的悬崖槽位(16–19)会按悬崖槽位 16 渲染。如果绘制的悬崖类型超出了图块集中的悬崖数量,仍会显示为岩石。
网格输入:POSITION(地图局部空间)、NORMAL(平滑法线,对象空间)、COLOR(r = 崖壁遮罩,g = 悬崖类型 × 85 / 255,即类型 0–3 分别映射为 0、1/3、2/3 和 1,b = 环境光遮蔽,a = 未使用)。没有 UV,也没有切线。
全局变量(来自 Shaders/Include/RTSCommon.hlsl,由所有包内着色器共享):
| 全局变量 | 设置者 | 内容 |
|---|---|---|
_RTS_FogOfWarTex, _RTS_FogOfWarParams, _RTS_FogOfWarUnexplored, _RTS_FogOfWarExplored, _RTS_FogOfWarEnabled | RTSFogOfWarManager | 可见性纹理(R = 可见,G = 已探索)、映射参数和色调。HLSL:RTS_SampleFogOfWar(positionWS)、RTS_ApplyFogOfWar(color, positionWS)。 |
_RTS_BrushPositions[4], _RTS_BrushRotations[4], _RTS_BrushCount, _RTS_BrushParams, _RTS_BrushColor, _RTS_BrushMask | RTSBrushPreview | 笔刷光标。HLSL:RTS_BrushCursor(positionWS) 返回颜色(rgb)和覆盖度(a)。 |
_RTS_Wind | RTSVegetationRenderer | xy = 方向,z = 强度,w = 速度。 |
编写自定义地形着色器
地形的全部表面逻辑都位于一个 include 文件中:Shaders/Include/RTSTerrainInput.hlsl。各个 Pass 文件只负责把得到的表面数据转换为光照结果。如果要改变外观,请从包内着色器的副本入手:
- 将
Shaders/RTSTerrain.shader以及整个Shaders/Include文件夹复制到你自己项目中的某个文件夹(例如Assets/MyGame/Shaders),并保持相对目录结构不变。着色器以Include/…的形式引用其 Pass 文件,而这些 include 文件之间也通过相对路径相互引用。 - 在副本的第一行重命名着色器,例如
Shader "MyGame/Terrain",避免与原始着色器冲突。 - 按需修改代码,通常改动的是你复制的
RTSTerrainInput.hlsl中的RTS_EvaluateTerrainSurface。 - 使用你的着色器创建一个材质,并将其指定为 Material Override(图块集或地形均可)。该材质资源还能确保着色器被包含在 Player 构建中。
- 单击 Rebuild Terrain。
在副本上修改,意味着包更新永远不会覆盖你的更改。更新之后,如果你想获得最新的修复,请将你的副本与新的包文件对比。
示例:高地上的积雪。将以下代码添加到副本中 RTS_EvaluateTerrainSurface 的末尾,紧挨在 return surface; 之前:
// Snow on everything above cliff level 6. positionOS.y is the map-local ground height, and
// _RTS_MapParams.y the height of one cliff level, so their ratio is the (fractional) cliff level.
half level = (half)(positionOS.y / max(_RTS_MapParams.y, 1e-3));
// Soft border between level 5.5 and 6.5; cliff walls (cliffBlend = 1) stay rocky.
half snow = smoothstep(5.5h, 6.5h, level) * (1.0h - surface.cliffBlend);
surface.albedo = lerp(surface.albedo, half3(0.92h, 0.94h, 0.97h), snow);
surface.smoothness = lerp(surface.smoothness, 0.35h, snow);
RTSTerrainInput.hlsl 的主要组成部分:
| 函数 / 类型 | 用途 |
|---|---|
struct RTSTerrainSurface | 表面计算的结果:反照率、金属度、平滑度、遮蔽、逐像素法线和几何法线(对象空间)、切线坐标系、cliffBlend(0 = 地面,1 = 悬崖)。 |
RTS_EvaluateTerrainSurface(positionOS, normalOS, vertexColor, dPdx, dPdy) | 完整的表面计算:悬崖覆盖、地面、悬崖、基于高度的过渡、宏观变化、遮蔽。 |
RTS_EvaluateGround(positionXZ, dPdx, dPdy, detileNoise), RTS_AccumulateGroundLayer(…) | 带高度混合和反平铺的 Splat 混合地面图层(float 累加器 RTSGroundAccumulator)。 |
RTS_EvaluateCliff(cliffTypeValue, positionOS, dPdx, dPdy, normalOS), RTS_SampleCliffLayer(…), RTS_TriplanarWeights(normalOS, sharpness) | 三平面悬崖材质,以及悬崖类型之间的交叉淡化。 |
RTS_LoadLayerData(slice) → RTSLayerData | 读取 _RTS_LayerData 的一列。 |
RTS_DecodeArrayNormal(encoded, strength) | 解码法线数组中的一个纹素。 |
RTS_EvaluateNoise(positionXZ), RTS_ValueNoise, RTS_Hash21 | 程序化噪声(不使用纹理),在移动端 GPU 上表现稳定。 |
RTS_GridMask(…), RTS_ApplyEditorOverlays(…) | 栅格线和叠加层纹理。 |
RTS_CreateSurfaceData(surface) | 将表面数据转换为 URP 的 SurfaceData。 |
| 常量 | RTS_MAX_GROUND_LAYERS(16)、RTS_CLIFF_SLICE_OFFSET(16)、RTS_MAX_CLIFF_TYPES(4)、RTS_MIN_LAYER_WEIGHT(1/255)、RTS_FALLBACK_GROUND_ALBEDO、RTS_FALLBACK_CLIFF_ALBEDO |
纹理采样使用显式梯度(SampleGrad)。梯度 dPdx/dPdy 在片元着色器开头、所有分支之外计算一次。如果你在分支内添加自己的采样,请保持这种做法,否则分支边界处会出现 Mipmap 接缝。
前向 Pass 按以下顺序处理:表面计算、编辑器叠加层、URP 光照(UniversalFragmentPBR)、Unity 雾效、战争迷雾以及笔刷光标。
植被
RTSVegetationRenderer 根据地图绘制两类植被:
- 细节:以密度形式绘制的草、花和小型植物。它们由 Burst 作业按地块生成,并通过程序化 GPU 实例化绘制,永远不会以 GameObject 的形式存在。
- 树木:单独放置的实例,可以使用 GPU 实例化绘制(速度最快),也可以作为预制件 GameObject 生成(用于砍伐等玩法)。
两者都以地块(patch)为单位组织,每个地块包含 RTSVegetationRenderer.PatchCells(16)× 16 个单元格。地块是剔除和增量重建的基本单位:一次编辑只会重新生成它涉及的地块。渲染器在编辑模式和 Play 模式下均可工作,适用于 Game 视图和 Scene 视图的摄像机。
配置
- 选中地形 GameObject。如果它没有 Vegetation Renderer,请单击
RTSTerrainInspector 窗口中警告旁边的 Add(或使用 )。 - 在
RTSTerrainInspector 窗口的 Assets 部分指定一个 Vegetation Library。没有植被库时,Vegetation Renderer 的 Inspector 窗口会显示警告:“The terrain has no vegetation library, so nothing can be drawn.” - 使用 Vegetation 工具绘制细节、放置树木(编辑时在 Scene 视图中按 4 键)。
- 对于以实例化方式绘制的树木,请在其材质上勾选 Enable GPU Instancing。 会列出未开启此项的材质,并可一键修复。
植被库定义了各个原型(网格、纹理、颜色、尺寸、距离)。所有原型字段的说明参见植被库。
Vegetation Renderer 的 Inspector 窗口会显示实时统计信息(Visible Details、Total Details、Visible Trees、Tree Objects、Colliders),并提供 Rebuild 按钮(RebuildAll())和 Select Library 按钮。
渲染器设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Quality | ||
| Detail Density Multiplier | 1 | 将每个纹素上的细节实例数量乘以此值(0–2,0 = 无细节)。修改此值会重新生成所有细节。 |
| Detail Distance Multiplier | 1 | 将每个细节原型的渲染距离乘以此值(0.1–3)。 |
| Tree Distance Multiplier | 1 | 将每个实例化树木原型的渲染距离乘以此值(0.1–3)。 |
| Wind | ||
| Wind Direction | (1, 0.35) | 世界空间中的水平风向(x = 世界 X,y = 世界 Z)。 |
| Wind Strength | 0.6 | 全局风力强度(0–3)。各原型和材质会在此基础上进一步缩放。 |
| Wind Speed | 1.6 | 风力摆动动画的速度(0–10)。 |
| Rendering | ||
| Draw Details | 开 | 渲染以密度方式绘制的细节。 |
| Draw Trees | 开 | 渲染 GPU 实例化的树木。 |
| Draw In Scene View | 开 | 同时在 Scene 视图摄像机中渲染植被。 |
| Crossed Quad Count | 3 | 为 Crossed Quads 细节生成的草网格所含的面片数量(2–3)。修改此值会重建植被。 |
| Collider Layer | 0 | 为实例化树木创建的碰撞体代理所在的层。 |
细节
对于每个已绘制的细节图层和每个地块,DetailScatterJob(Burst)会根据密度图散布实例。密度为 255 时,每个细节纹素会生成原型 Max Density 所设数量的实例,再乘以 Detail Density Multiplier。坡度大于 Max Slope 的斜坡上不会生成实例;启用 Avoid Water 时,被水淹没的地面上也不会生成。实例可以随地面倾斜(Align To Ground),并根据噪声获得介于 Healthy Color 与 Dry Color 之间的颜色。
每个地块的实例会上传到一个结构化 GraphicsBuffer 中,并通过内部着色器 Hidden/RTS Terrain Studio/Detail 使用 Graphics.RenderMeshPrimitives 绘制。WebGL 2.0 不支持结构化缓冲区,OpenGL ES 3 也不保证顶点着色器能够使用结构化缓冲区,因此在这些设备上,实例会改为写入一张浮点纹理(每个实例占两个纹素)。渲染器会在运行时选择使用哪种方式,两种方式下草的外观完全相同。该着色器从缓冲区或纹理中读取每个实例,并处理风力摆动、在渲染距离最后 25% 范围内的平滑缩小消失、根部变暗、透光效果以及战争迷雾回退。每个地块都会针对每个摄像机分别进行视锥体剔除和距离剔除。
| 渲染模式 | 网格 | 材质 |
|---|---|---|
CrossedQuads | 生成的草网格,包含 2 或 3 个交叉面片(Crossed Quad Count) | 根据原型的 Texture 创建(Alpha 裁剪阈值为 0.5) |
Mesh | 原型的 Mesh(所有子网格) | 如果材质已经使用 Hidden/RTS Terrain Studio/Detail,则直接使用该材质。否则会根据其主纹理和颜色创建细节材质。只有当源材质使用 Alpha 裁剪时,才会保留其 Alpha 裁剪阈值。 |
其他着色器无法读取实例数据,因此 Mesh 模式的细节始终使用细节着色器渲染。源材质的自定义着色器功能(顶点动画、特殊光照)不会用于细节。如果你需要这些功能,请改用树木原型。
只有当原型的 Cast Shadows 开启时(默认关闭),细节才会投射阴影。细节绘制在地形的 Chunk Layer 上,不使用光照探针和反射探针。
树木:实例化还是 GameObject
每个树木原型都可以选择一种 Spawn Mode(生成模式):
Instanced(默认) | GameObject | |
|---|---|---|
| 渲染 | Graphics.RenderMeshInstanced,每次 Draw Call 最多 1023 个实例,按地块根据视锥体和 Render Distance × Tree Distance Multiplier 进行剔除 | 每棵树一个预制件实例,由 Unity 像普通 GameObject 一样渲染 |
| LOD | LODGroup 的 LOD0 和 LOD1,按地块组使用 Unity 的 LODGroup 规则选择(遵循 QualitySettings.lodBias)。没有 LODGroup 的预制件会使用其所有处于激活状态的 MeshRenderer。 | 预制件自身的 LODGroup |
| 逐实例色调 | 支持,适用于使用 RTS Terrain Studio/Vegetation 的材质(在 Tint A 和 Tint B 之间随机) | 不支持 |
| 碰撞体 | 池化的胶囊体代理(见下文) | 预制件自身的碰撞体 |
| 预制件上的脚本 | 不运行(不会实例化任何对象) | 正常运行 |
| 最适合 | 森林、场景装饰、大量树木 | 带有玩法交互的树木:砍伐、动画、自定义脚本 |
两种模式下,树木都放置在地面高度 + position.y 处,按其偏航角旋转并等比缩放。编辑地形时,树木会自动重新贴合地面。生成的树木和碰撞体代理位于地形下一个隐藏的子对象“RTS Vegetation Objects”中,并跟随地形的变换。
实例化树木需要在其材质上开启 Enable GPU Instancing。如果未开启,渲染器会通过一个隐藏的、启用了实例化的材质副本进行绘制(项目中的资源永远不会被修改)。在编辑模式下,该副本每秒约与源材质重新同步 4 次。在 Player 构建中,这类材质的实例化变体可能会被剥离,因此请勾选该复选框(项目验证器可以替你完成)。
渲染器会保留每个部件的阴影投射模式和渲染层遮罩。设置为 Shadows Only 的部件(阴影代理)可以正常工作。如果原型的 Cast Shadows 处于关闭状态,其所有部件的阴影都会关闭。
树木碰撞体
当树木原型启用了 Colliders 时:
- Instanced 模式:每棵树都会获得一个不可见的 GameObject“Tree Collider”,其上带有一个
CapsuleCollider(Collider Radius、Collider Height,随树木缩放)和一个RTSTreeInstance组件(IsColliderProxy= true)。这些代理会被池化复用,所在层为渲染器的 Collider Layer。 - GameObject 模式:使用预制件自身的碰撞体。渲染器会为每棵生成的树木添加一个
RTSTreeInstance。
无论哪种模式,都可以通过 hit.collider.GetComponentInParent<RTSTreeInstance>() 得知物理查询命中了哪棵树。通行和战争迷雾不需要碰撞体:它们使用原型的 Footprint Radius 和 Blocks Vision。
风
渲染器每帧都会把全局 _RTS_Wind 向量(方向、强度、速度)传给所有着色器。植物的总摆动幅度为:
- 细节:全局 Wind Strength × 原型的 Wind Strength(0–1)。
- 使用
RTS Terrain Studio/Vegetation着色器的树木和植物:全局 Wind Strength × 材质的 Wind Strength,再按 Wind Mode 加权。Object Height 模式下越靠近顶部弯曲越明显,以 Wind Reference Height 为参考。Vertex Color R 模式把顶点颜色的红色通道用作逐顶点权重(树干处绘制为 0,枝梢处绘制为 1)。Leaf Flutter 会在此基础上叠加快速而细微的抖动。
将渲染器上的 Wind Strength 设为 0 即可完全停止风力摆动,例如用于无风的夜晚场景。
Vegetation 着色器
RTS Terrain Studio/Vegetation 是一款带光照的植被着色器,用于树木和植物预制件的材质。它支持 SRP Batcher、带逐实例色调(_RTS_InstanceTint)的 GPU 实例化、Forward+、DBuffer 贴花、Rendering Debugger、APV 和光照贴图,以及战争迷雾回退方案。其他支持实例化的材质(例如 URP Lit)也能使用,但没有逐实例色调、透光效果和风力摆动。
| 属性 | 默认值 | 说明 |
|---|---|---|
| Albedo, Color | 白色 | 基础纹理(Alpha 通道用于裁剪)和颜色。 |
| Alpha Clipping, Alpha Cutoff | 开,0.5 | 用于树叶的 Alpha 测试。无需任何关键字,裁剪即处于开启状态;反向关键字 _ALPHATEST_OFF 会将其关闭。 |
| Use Normal Map, Normal Map, Normal Strength | 关,–,1 | 可选的法线贴图(_NORMALMAP)。 |
| Smoothness, Specular Color | 0.2,深灰色 | 镜面反射响应。 |
| Wrap Lighting | 0.35 | 柔化树叶上的光照衰减(0–1)。 |
| Translucency, Translucency Color | 0.6,浅绿色 | 朝太阳方向看时,光线透过树叶的效果。 |
| Wind Mode | Object Height | Off、Vertex Color R 或 Object Height。 |
| Wind Strength, Wind Reference Height, Leaf Flutter | 1, 6, 0.5 | 材质端的风力缩放(与渲染器的全局风力相乘)。 |
| Cull | Off | 面剔除。设为 Off 时,树叶面片的正反两面都会渲染。 |
| Receive Shadows | 开 | 关闭时使用关键字 _RECEIVE_SHADOWS_OFF。 |
通过代码创建植被材质时请注意:Alpha 裁剪默认开启。只有在你想要禁用裁剪时,才启用关键字 _ALPHATEST_OFF(RTSVegetationShaderIDs.VegetationAlphaTestOffKeyword)。
画质倍率
这三个画质字段可以在不修改植被库的情况下缩放库中的数值,非常适合用作游戏菜单中的图形选项:
using RTSTerrainStudio;
using UnityEngine;
/// Applies a vegetation quality level (0 = low, 1 = medium, 2 = high) to every terrain in the scene.
public static class VegetationQuality
{
public static void Apply(int level)
{
foreach (RTSTerrain terrain in RTSTerrain.All)
{
if (!terrain.TryGetComponent(out RTSVegetationRenderer vegetation)) continue;
switch (level)
{
case 0:
vegetation.detailDensityMultiplier = 0.4f; // fewer grass blades (regenerates details)
vegetation.detailDistanceMultiplier = 0.6f; // grass disappears earlier
vegetation.treeDistanceMultiplier = 0.7f; // instanced trees culled earlier
vegetation.crossedQuadCount = 2; // 2 quads per grass tuft instead of 3
break;
case 1:
vegetation.detailDensityMultiplier = 0.75f;
vegetation.detailDistanceMultiplier = 0.8f;
vegetation.treeDistanceMultiplier = 1f;
vegetation.crossedQuadCount = 3;
break;
default:
vegetation.detailDensityMultiplier = 1f;
vegetation.detailDistanceMultiplier = 1f;
vegetation.treeDistanceMultiplier = 1.2f;
vegetation.crossedQuadCount = 3;
break;
}
// The renderer notices the changed values on its next update and rebuilds what is needed.
}
}
}
树木 API 与采伐
| 成员 | 说明 |
|---|---|
bool RemoveTree(uint id) | 从地图中移除该树木,立即停用其 GameObject 或碰撞体代理,通知地形(MapLayers.Trees,这样通行数据会重新开放其占地范围,战争迷雾也会移除对应的视野阻挡物),并触发 TreeRemoved。此操作不会记录撤销步骤。如果没有任何树木使用该 ID,则返回 false。 |
event Action<RTSVegetationRenderer, TreeInstanceData> TreeRemoved | 由 RemoveTree 触发,并传入被移除树木的数据。 |
bool TryFindNearestTree(Vector3 worldPosition, float maxDistance, out TreeInstanceData tree) | 按地平面(XZ)上的距离查找最近的树木。传入 Mathf.Infinity 可搜索整张地图。 |
GameObject GetTreeObject(uint id) | 生成的预制件实例(GameObject 模式)或碰撞体代理(带碰撞体的实例化模式)。其他情况下为 null。 |
Vector3 GetTreeWorldPosition(in TreeInstanceData tree) | 树根的世界坐标(地面高度 + 偏移)。 |
void RebuildAll(), void ProcessChanges() | 完全重建;立即应用待处理的地图更改(通常每帧自动执行)。 |
VisibleDetailInstances, VisibleTrees, TotalDetailInstances, SpawnedTreeObjects, TreeColliderCount | 统计信息。其中“可见”类数值以最近一次渲染的 Game 摄像机或 Scene 摄像机为准。 |
RTSTreeInstance: TreeId, Owner, IsColliderProxy, TryGetData(out TreeInstanceData), Remove() | 挂载在生成的树木和碰撞体代理上的组件。Remove() 等同于 Owner.RemoveTree(TreeId)。 |
using RTSTerrainStudio;
using UnityEngine;
/// A minimal lumberjack: chops the nearest tree in reach, or the tree under the mouse.
public class Lumberjack : MonoBehaviour
{
[SerializeField] RTSVegetationRenderer vegetation;
[SerializeField] Camera viewCamera;
[SerializeField] float reach = 2f;
public int wood;
void OnEnable()
{
if (vegetation != null) vegetation.TreeRemoved += HandleTreeRemoved;
}
void OnDisable()
{
if (vegetation != null) vegetation.TreeRemoved -= HandleTreeRemoved;
}
/// Physics-free: finds the nearest tree around this unit on the ground plane.
public bool ChopNearestTree()
{
if (vegetation == null) return false;
if (!vegetation.TryFindNearestTree(transform.position, reach, out TreeInstanceData tree)) return false;
return vegetation.RemoveTree(tree.id);
}
/// Physics based: needs "Colliders" on the tree prototype.
public bool ChopTreeUnderMouse()
{
if (viewCamera == null) return false;
Ray ray = viewCamera.ScreenPointToRay(RTSInput.PointerPosition);
if (!Physics.Raycast(ray, out RaycastHit hit, 500f)) return false;
// Works for collider proxies (instanced trees) and for colliders inside spawned tree prefabs.
var tree = hit.collider.GetComponentInParent<RTSTreeInstance>();
return tree != null && tree.Remove();
}
void HandleTreeRemoved(RTSVegetationRenderer renderer, TreeInstanceData tree)
{
wood += 10;
Vector3 position = renderer.GetTreeWorldPosition(tree);
Debug.Log($"Tree {tree.id} felled at {position}. Wood: {wood}");
}
}
示例脚本中包含一个完整的采伐演示(RTSTreeHarvestDemo:工人走到树旁将其砍倒,并通过 RTSFallingTreeEffect 播放树木倒下的动画)。阅读它可以了解一个完整的游戏玩法示例,其中也包括试玩结束时如何恢复被采伐的树木。
移除树木会修改地图。在游戏内编辑器或 Unity 编辑器中,地图随后会被标记为有未保存的更改。游戏代码通常操作的是为本局对战加载的地图,并且从不将其保存回去。
水体
RTSWaterRenderer 将绘制的水体渲染为分区块的透明网格。每个标记为水体的顶点都有自己的水面高度,因此不同悬崖层级上可以有各自的湖泊。这些网格严格遵循 MapGeometryView.SampleWater,与游戏玩法查询和通行计算所用的数据完全一致。水体区块使用地形的区块大小,由 Burst 作业(WaterChunkMeshJob)生成,并且只在有水的地方存在。只有编辑涉及的区块才会重建。
配置
- 确保地形 GameObject 上有 Water Renderer 组件(通过 添加,或在
RTSTerrain组件的 Inspector 中单击 Add 按钮)。 - 打开当前使用的 URP 资源,启用 Depth Texture 和 Opaque Texture,并将 Opaque Downsampling 设为 None(见下文)。 可以检查并修复这些设置。摄像机也可以为自身单独开启这两项。
- 使用 Water 工具绘制水体(编辑时在 Scene 视图中按 6 键)。
- 在图块集 Inspector 的 Water 部分(即图块集的水体配置)中调整外观。
URP 要求
| 设置项(URP 资源或摄像机) | 用途 | 未开启时 |
|---|---|---|
| Depth Texture:开 | 立在水中的物体周围的深度颜色、岸边泡沫和柔和的岸线,以及将水面前方物体排除在外的折射(_CameraDepthTexture) | 水体改用其网格中存储的深度。深度颜色、泡沫和柔和的岸边仍然保留,但会忽略立在水中的物体。 |
| Opaque Texture:开 | 折射(_CameraOpaqueTexture) | 仅使用普通的 Alpha 混合,没有折射。 |
| Opaque Downsampling:None(URP 资源) | 清晰的湖底和水下物体 | 水下场景看起来很模糊(URP 的默认值为 2x Bilinear)。 |
URP 按摄像机分别决定:摄像机自身的 Depth Texture 和 Opaque Texture 设置(位于其 Inspector 窗口的 Rendering 部分)会覆盖 URP 资源中的设置,而 Scene 视图始终使用 URP 资源的设置。水体会在每个摄像机即将渲染时,根据该摄像机调整自身材质,因此每个视图都能得到条件允许的最佳水体效果。不渲染水体所在层的摄像机会被跳过,叠加摄像机则永远得不到不透明纹理。
Water Renderer 组件的 Inspector 会为每个缺失的设置项显示一条消息,并附带 Select URP Asset 按钮。在 Play 模式下,它还会考虑主摄像机自身的设置。缺少纹理绝不会导致水体显示异常:没有该纹理的视图会使用上文所述的回退方案。只是启用这两张纹理后,水体效果会好得多。
通过代码:RTSWaterMaterialUtility.CheckPipelineSupport(Camera camera, out bool depthTexture, out bool opaqueTexture) 会告诉你 URP 会为某个摄像机生成哪些纹理,判定方式与 URP 自身完全相同(传入 null 则只检查 URP 资源)。CheckPipelineSupport(out bool depthTexture, out bool opaqueTexture) 检查显示水体的视图:在构建版本中检查主摄像机(没有主摄像机时检查 URP 资源);在编辑器中还会检查 URP 资源,因为 Scene 视图使用的是它。URP 未激活时,这两个方法都返回 false。RTSWaterMaterialUtility.IsOpaqueTextureDownsampled() 用于检查降采样设置。
渲染器设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Material Template | 空 | 可选材质。系统会复制它的着色器和属性,然后在此基础上应用图块集的水体配置。可以用它调整水体配置未涵盖的数值:Reflection Strength、Specular Strength、Foam Tile Size、Foam Lines、Shore Fade。 |
| Cast Shadows | 关 | 水面通常只接收阴影。 |
| Receive Shadows | 开 | 在水面上显示主光源的阴影。这同样适用于 Player 构建中的运行时材质。 |
| Render Layer | 4 (Water) | 水体区块所在的 GameObject 层(RTSWaterRenderer.DefaultLayer 即 Unity 内置的 Water 层)。 |
| Wave Amplitude Multiplier | 1 | 缩放水体配置中的 Wave Height(0–4,0 = 静止水面)。 |
| Cell Subdivisions | 2 | 每个单元格每条边上的四边形数量(1–4)。数值越高,岸线和波浪越平滑,但顶点也越多。 |
| Light Probe Usage | Off | 设为 Off 时,所有区块都使用环境探针照明。如果混合光照探针,每个区块的环境色会略有不同,区块边界处会出现明显的分界。 |
| Reflection Probe Usage | Blend Probes | 在 Forward+ 中,探针逐像素计算。在 Forward 中,每个区块根据自身的包围盒选择探针,因此存在多个探针时,相邻区块可能会反射不同的探针。 |
Inspector 按钮:Rebuild(RebuildAll())和 Refresh Material(RefreshMaterial(),重新应用模板和水体配置)。Inspector 窗口还会显示区块栅格、含水的区块、总顶点数以及水体配置的来源。
水体配置(图块集)
外观和通行规则来自 RTSTileset.water(类型为 WaterProfile)。没有图块集时,会使用一份共享的默认配置。请将其视为只读;如需可编辑的副本,可使用 WaterMath.CreateDefaultProfile() 或 WaterProfile.Clone()。
| 字段 | 默认值 | 说明 |
|---|---|---|
| Shallow Color | 青色,Alpha 0.55 | 浅水的颜色(Alpha = 不透明度)。 |
| Deep Color | 深蓝色,Alpha 0.95 | 水深达到 Color Depth 时的颜色(Alpha = 不透明度)。 |
| Color Depth | 3 m | 完全呈现深水颜色时的深度。 |
| Shallow Depth Limit | 0.9 m | 深于此值的水为深水(不可行走)。较浅的水可以行走,与经典 RTS 游戏一致。 |
| Normal Map, Normal Strength, Normal Tile Size | 无,0.6,8 m | 水面涟漪。没有法线贴图时,水面只使用顶点波浪。 |
| Flow Speed | (0.03, 0.02) | 法线贴图的滚动速度。 |
| Wave Height | 0.05 m | 顶点波浪的振幅(再乘以渲染器上的倍率)。 |
| Smoothness | 0.92 | 水面平滑度。 |
| Refraction | 0.03 | 折射偏移的强度(0–0.2)。 |
| Foam Color, Foam Distance, Foam Texture | 白色 0.8,0.6 m,无 | 岸边泡沫。Foam Distance 还会让浅水区的波浪变得平缓。 |
| Minimap Color | 蓝色 | 水体在小地图上的颜色。 |
Water 着色器
RTS Terrain Studio/Water(RTSWaterMaterialUtility.ShaderName)是一款透明的 URP 着色器。区块网格提供地图局部坐标以及逐顶点的水体数据(水深、覆盖率以及到岸边的距离,最多计算到 ShoreSearchRadius = 4 个单元格)。其功能包括:
- 随水深由浅到深渐变的颜色,并在水与地面相接处呈现逐渐淡出的柔和岸线。
- 四组不同方向的顶点波浪,在浅水区会变得平缓,因此水面永远不会抬升到沙滩之上。
- 滚动的法线贴图涟漪、水下场景的折射、太阳镜面高光以及反射探针。
- 岸边泡沫(程序化线条或泡沫纹理)。
- 主光源和附加光源(包括逐顶点附加光源)、Light Cookie、光照层、接收阴影以及战争迷雾回退方案。兼容 SRP Batcher。
材质在运行时创建(WaterMaterial,由所有区块共享),并通过 RTSWaterMaterialUtility.ApplyProfile 填充数值。着色器由组件上的一个序列化字段引用,因此会被包含在构建中。
在逐顶点模式下,附加光源只提供漫反射光照(没有镜面高光,也没有阴影),与 URP 自身的顶点光照相同。
水体渲染器 API
| 成员 | 说明 |
|---|---|
Material WaterMaterial | 由所有区块共享的运行时材质(组件禁用时为 null)。 |
Material MaterialTemplate { get; set; } | 可选模板。设置后会刷新材质。 |
WaterProfile Profile | 图块集的水体配置,或共享的默认配置。 |
Transform ChunkRoot, ChunkCountX, ChunkCountZ, ActiveChunkCount, VertexCount, bool HasWater | 区块栅格和统计信息。 |
MeshRenderer GetChunkRenderer(Vector2Int chunkCoord) | 某个区块的渲染器;该区块没有水时为 null。 |
void RebuildAll(), void RebuildCells(RectInt cellRect), void RefreshMaterial() | 完全重建、局部重建、刷新材质。 |
event Action<RTSWaterRenderer> MeshesRebuilt | 在完全重建或局部重建之后触发。 |
RTSWaterChunk: Owner, Terrain, ChunkCoord, CellRect | 隐藏区块对象上的标记组件。 |
WaterMath:用于游戏玩法的水体查询
WaterMath 无需物理系统即可回答与水体相关的问题。所有位置均使用世界空间坐标。
| 方法 | 返回值 |
|---|---|
GetWaterHeight(RTSTerrain, Vector3, out float height), GetWaterHeight(Vector3, out float height) | 静止水面的世界 Y 坐标。该处无水或位于地形之外时返回 false。第二个重载通过 RTSTerrain.FindAt 查找地形。 |
GetWaterDepth(RTSTerrain, Vector3, out float depth) | 水面高度减去地面高度,以米为单位。无水时返回 false。 |
Classify(RTSTerrain, Vector3, out float depth) | WaterDepthClass.Dry、Shallow 或 Deep(依据 Shallow Depth Limit 判断)。 |
IsShallowWater(…), IsDeepWater(…), GetShallowDepthLimit(RTSTerrain) | 用于分类判断的快捷方法。 |
GetWaterSurface(RTSTerrain, Vector3, float time, out float height, out Vector3 normal) | 与渲染结果完全一致的动画水面:静止高度 + 波浪,靠近岸边时波浪减弱,并已计入渲染器的 Wave Amplitude Multiplier。 |
float ShaderTime | 着色器动画所用的时钟(在 Play 模式和 Player 构建中为 Time.time,在编辑模式下为 Time.realtimeSinceStartup)。将它作为 time 传入,可使 CPU 端和 GPU 端的波浪保持一致。 |
SampleWaveOffset(Vector3, float waveHeight, float time) | 不含岸边减弱效果的波浪偏移。 |
EvaluateWaves(float2, float time), EvaluateWaveSurface(float2, float time, float amplitude, out float offset, out float3 normal), WaveAttenuation(float depth, float foamDistance) | 兼容 Burst 的基础构件,适用于作业系统(例如用于数千个漂浮单位)。 |
GetProfile(RTSTerrain), CreateDefaultProfile() | 当前使用的水体配置;一份可以安全修改的全新默认配置。 |
using RTSTerrainStudio;
using UnityEngine;
/// Makes a boat float on the animated water surface and keeps it out of shallow water.
public class FloatingBoat : MonoBehaviour
{
[SerializeField] float draft = 0.15f; // how deep the hull sits in the water
[SerializeField] float heading; // yaw in degrees, set by your movement code
[SerializeField] float tiltSmoothing = 4f;
void LateUpdate()
{
Vector3 p = transform.position;
RTSTerrain terrain = RTSTerrain.FindAt(p);
if (terrain == null) return;
// ShaderTime is the clock the water shader uses, so the boat bobs in sync with the rendered waves.
if (!WaterMath.GetWaterSurface(terrain, p, WaterMath.ShaderTime, out float height, out Vector3 normal))
return; // dry ground: leave the boat alone
transform.position = new Vector3(p.x, height - draft, p.z);
// Tilt with the wave normal, keep our own heading.
Quaternion target = Quaternion.FromToRotation(Vector3.up, normal) * Quaternion.Euler(0f, heading, 0f);
transform.rotation = Quaternion.Slerp(transform.rotation, target, 1f - Mathf.Exp(-tiltSmoothing * Time.deltaTime));
}
/// Boats need deep water: use this before accepting a move order.
public static bool CanSailTo(Vector3 world)
{
RTSTerrain terrain = RTSTerrain.FindAt(world);
return terrain != null && WaterMath.Classify(terrain, world, out _) == WaterDepthClass.Deep;
}
}
装饰物
RTSDecorManager 负责让场景与地图中的装饰物(doodad:岩石、箱子、废墟、栅栏或任意预制件)保持同步。它监听地形的更改事件,并以增量方式工作:一次编辑只会影响被添加、移除或更改的实例。如果只有 Transform 发生变化,对象会被复用;对象被移除时则会放回对象池。编辑地形高度时,更改区域内的实例会重新贴合地面。加载地图或更改装饰物库会重建全部内容。
生成模式
装饰物库中的每个原型都可以选择一种 Spawn Mode(生成模式):
GameObject(默认) | Instanced | |
|---|---|---|
| 表现形式 | 预制件(变体)的副本,位于“RTS Decor”子对象下,并带有 RTSDecorInstance 组件 | 通过 Graphics.RenderMeshInstanced 绘制(使用预制件的第一个 LOD 级别),每个 Draw Call 最多 1023 个实例 |
| 剔除 | Unity 自身的剔除 | 按地块剔除(视锥体 + Max Draw Distance) |
| 脚本、碰撞体、动画 | 支持 | 不支持(不存在实际对象) |
| 色调 | 通过 MaterialPropertyBlock(_BaseColor 和 _Color) | 仅限支持该功能的着色器(参见逐实例色调) |
| 最适合 | 可交互或可破坏的道具,数量在几百到几千个之间 | 大量的静态景物 |
放置规则:位置 = LocalToWorld(x, ground + y + prototype yOffset, z);如果设置了 KeepHeight 标记,则使用存储的绝对高度。旋转 = 地形旋转 × 实例旋转,缩放 = 实例缩放。生成的预制件还会在此基础上保留其根对象上原有的旋转和缩放。
实例化装饰物要求其材质启用 Enable GPU Instancing。如果未启用,系统会改用一份开启了实例化的运行时副本,并显示一条警告,提醒你勾选该复选框。使用项目验证器可以一次性修复所有材质。
原型字段(变体、随机旋转和缩放、色调、占地范围、视野阻挡、阴影)详见装饰物库。
管理器设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Show In Scene View | 开 | 在 Scene 视图的摄像机中绘制实例化装饰物。生成的 GameObject 始终可见。 |
| Max Draw Distance | 0 | 与摄像机的距离超过此值的实例化装饰物不会被绘制(0 = 不限制)。 |
| Shadow Culling Margin | 12 m | 视锥体剔除时在地块周围额外留出的边距,使刚好位于视野之外的道具仍能把阴影投射到视野内。 |
| Instanced Tint | 开 | 为着色器支持该功能的实例化装饰物传递逐实例色调。 |
| Patch Size | 16 | 剔除和查询所用地块的边长,以单元格为单位(4–64)。更改此值会重建装饰物。 |
| Pool Capacity Per Prefab | 64 | 被移除的 GameObject 装饰物会以停用状态保留以便复用,每个预制件最多保留此数量的对象。 |
| Log Missing Prototypes | 开 | 对每个缺失的原型 ID、缺失的库或已销毁的预制件各记录一条警告。 |
Decor Manager 的 Inspector 会显示 Instances、GameObjects、Instanced 和 Sync(是否有待处理的更改),并提供以下按钮:Rebuild(RebuildAll():销毁并重新创建所有内容,清空对象池,重新读取预制件)、Sync Now(SyncNow())和 Select Library。
占位符
如果地图引用了库中不存在的原型 ID,或者地形根本没有装饰物库,又或者某个预制件已被删除,该实例就会显示为一个小的品红色立方体。数据不会丢失:实例仍保留在地图中,一旦原型恢复,它就会重新正常显示。IsPlaceholder(id) 可以告诉你某个实例是否为占位符。占位符按实例化对象计数。
打开他人制作的地图后出现品红色立方体,通常说明地形使用的装饰物库与制作地图时所用的不同。请在 RTSTerrain 组件的 Inspector 中指定正确的库。
在自定义着色器中使用逐实例色调
只有当着色器能够从实例化缓冲区读取色调时,实例化装饰物才会逐实例着色。URP Lit、Simple Lit 和 Unlit 会保持其材质颜色不变。着色器需要通过 SubShader 标签主动启用此功能:
| 标签 | 常量 | 着色器必须 |
|---|---|---|
"RTSInstanceTint" = "True" | RTSDecorManager.InstanceTintShaderTag | 将其反照率与实例化的 _RTS_InstanceTint(rgb = 色调,a = 1)相乘。包内的 RTS Terrain Studio/Vegetation 着色器会被自动识别。 |
"RTSInstancedColor" = "True" | RTSDecorManager.InstancedColorShaderTag | 使用 UNITY_ACCESS_INSTANCED_PROP 从 UNITY_INSTANCING_BUFFER 中读取 _BaseColor 和 _Color。写入的值为材质颜色 × 色调。 |
装饰物 API:拾取、查询与事件
拾取和查询都不需要碰撞体。检测时,每个实例都被视为一个根据预制件网格包围盒构建的有向包围盒,因此拾取和查询对两种生成模式都有效,即使是没有 GameObject 的实例化装饰物也不例外。
| 成员 | 说明 |
|---|---|
bool TryPick(Ray worldRay, float maxDistance, out uint instanceId)(另有带 out float hitDistance 的重载) | 返回被世界空间射线击中的最近实例。 |
int QueryRadius(Vector3 world, float radius, List<uint> results) | 将地平面上指定半径范围内的 ID 追加到列表中。不会先清空列表,也不产生内存分配。另有一个 IEnumerable<uint> 重载,它会分配一个新列表。 |
int QueryLocalRect(Rect localRect, List<uint> results) | 将轴心位于地图局部 XZ 矩形内的 ID 添加到列表中(用于框选)。 |
GameObject GetInstanceObject(uint id) | 返回已生成的对象;若为实例化装饰物或 ID 未知,则返回 null。 |
ContainsInstance、TryGetInstance(uint, out DecorInstanceData)、IsInstanced、IsPlaceholder | 实例查询(数据为上次同步时的状态)。 |
TryGetWorldBounds(uint, out Bounds)、TryGetObjectMatrix(uint, out Matrix4x4) | 世界空间包围盒;对象根节点矩阵,包含预制件中设定的根节点旋转和缩放。 |
Matrix4x4 GetWorldMatrix(in DecorInstanceData)、GetWorldMatrix(in DecorInstanceData, string prototypeId) | 轴心的世界矩阵,也适用于尚未加入地图的数据(放置预览)。 |
SpawnedCount、InstancedCount、InstanceCount、Container、PatchSize、HasPendingChanges | 状态与统计信息。 |
InstancesRebuilt、InstancesChanged | 前者在完全重建之后触发,后者在增量更新之后触发。 |
InstanceSpawned、InstanceDespawning | 前者在对象生成或从对象池中取出后触发,后者在对象即将放回对象池或被销毁时触发。在软重建中保留下来的对象不会再次触发 InstanceSpawned。 |
RTSDecorInstance:InstanceId(位于对象池中时为 0)、PrototypeId、Variant、Owner、Prototype、IsLive、TryGetData、static FromCollider(Collider) | 挂在每个已生成对象根节点上的组件。它的字段会在预制件自身的 Awake/OnEnable 运行之前填好,因此你的预制件脚本可以在这两个方法中读取它们。 |
using System.Collections.Generic;
using RTSTerrainStudio;
using Unity.Mathematics;
using UnityEngine;
/// Click a decor object to destroy it; Shift+click destroys everything within 5 m of the click.
public class DecorDemolisher : MonoBehaviour
{
[SerializeField] RTSDecorManager decor;
[SerializeField] Camera viewCamera;
// Reused every click: QueryRadius appends to it and never allocates.
readonly List<uint> nearby = new List<uint>();
void Update()
{
if (decor == null || viewCamera == null || !RTSInput.GetMouseButtonDown(0)) return;
Ray ray = viewCamera.ScreenPointToRay(RTSInput.PointerPosition);
// Works for GameObject and instanced decor alike, without colliders.
if (!decor.TryPick(ray, 500f, out uint id, out float distance)) return;
if (RTSInput.Shift)
{
nearby.Clear();
decor.QueryRadius(ray.GetPoint(distance), 5f, nearby);
foreach (uint other in nearby) Remove(other);
}
else
{
Remove(id);
}
}
void Remove(uint id)
{
RTSTerrain terrain = decor.Terrain;
MapData map = terrain.Map;
int index = map.FindDecorIndex(id);
if (index < 0) return;
DecorInstanceData data = map.Decor[index];
map.Decor.RemoveAt(index);
// Tell the terrain what changed. The decor manager removes (pools) the object,
// and the pathing map reopens the footprint automatically.
int2 cell = map.LocalToCell(data.position.xz);
terrain.NotifyChanged(MapLayers.Decor, new RectInt(cell.x, cell.y, 1, 1));
}
}
using RTSTerrainStudio;
using UnityEngine;
/// Gives every spawned decor object of the category "Destructible" some hit points.
public class DestructibleDecorSetup : MonoBehaviour
{
[SerializeField] RTSDecorManager decor;
void OnEnable()
{
if (decor != null) decor.InstanceSpawned += HandleSpawned;
}
void OnDisable()
{
if (decor != null) decor.InstanceSpawned -= HandleSpawned;
}
void HandleSpawned(RTSDecorManager manager, RTSDecorInstance instance)
{
// Prototype looks up the library entry, e.g. its category or footprint.
DecorPrototype prototype = instance.Prototype;
if (prototype == null || prototype.category != "Destructible") return;
// Pooled objects come back here when they are reused, so add the component only once.
if (!instance.TryGetComponent(out Health health)) health = instance.gameObject.AddComponent<Health>();
health.hitPoints = 100;
}
}
/// Example gameplay component.
public class Health : MonoBehaviour
{
public int hitPoints;
}
只有 GameObject 模式的原型会触发 InstanceSpawned,因为实例化装饰物不会创建任何对象。
环境
每张地图都在 MapData.Environment 中保存自己的光照:太阳的颜色、强度和旋转,以及环境光颜色和雾效。因此,雪地地图可以自带冷色调光照,沙漠地图则可以自带暖色调光照。RTSEnvironment 组件会在加载地图时以及环境发生变化时,将这些值应用到场景中。
配置
- 选择 。这会创建一个名为“Environment”、带有
RTSEnvironment的 GameObject;如果场景中还没有方向光,还会额外创建一个名为“Sun”的方向光。如果已打开的场景中已经有环境,你可以改为直接选中现有的那个。多个环境会互相争夺同一组光照设置。 - 或者,通过 将其添加到地形 GameObject 上(
RTSTerrain的 Inspector 中提供了 Add 按钮)。 - 在编辑器窗口()的 Map 选项卡中,找到 Environment 卡片编辑这些值:Sun Colour、Sun Intensity、Sun Rotation(x = 高于地平线的俯仰角,y = 偏航角)、Ambient Colour、Fog、Fog Colour、Fog Density。这些编辑可以撤销,并随地图一起保存。
- 也可以手动布置场景光照,然后单击 Capture From Scene(编辑器窗口)或 Use Current Lighting(组件的 Inspector),将当前的太阳、环境光和雾效存入地图,整个操作只占一个撤销步骤。
应用的光照只是预览:它不会记录到撤销历史中,也不会将场景标记为已修改。在场景即将保存之前,会先恢复原始的场景光照,保存之后再重新应用地图光照。因此,地图光照永远不会写入你的场景文件。Inspector 中的 Restore Scene Lighting 可以恢复首次应用之前的光照。
组件设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Terrain | 空 | 要应用其地图光照的地形。为空时,使用此 GameObject 上的 RTSTerrain,若没有则使用主地形(RTSTerrain.Main)。之后才启用的地形也会被自动识别。 |
| Sun | 空 | 用作太阳的方向光。为空时,使用 RenderSettings.sun,或者最亮的已启用方向光。 |
| Apply In Edit Mode | 开 | 在编辑时应用地图光照。关闭后,仅在 Play 模式下应用。 |
| Apply Sun | 开 | 应用太阳颜色、强度和旋转(世界空间)。 |
| Apply Ambient | 开 | 应用环境光。 |
| Ambient Mode | Trilight | Flat = 单一环境光颜色。Trilight = 根据地图环境光颜色推导出的天空 / 赤道 / 地面渐变。 |
| Sky Factor | 1.2 | Trilight 模式下:天空颜色 = 环境光 × 此系数(0–3)。 |
| Ground Factor | 0.55 | Trilight 模式下:地面颜色 = 环境光 × 此系数(0–3)。 |
| Apply Fog | 开 | 应用雾效的开关状态、颜色和密度。 |
| Fog Mode | Exponential Squared | 应用雾效时使用的衰减模式(地图只保存颜色和密度)。 |
| Restore On Disable | 开 | 禁用组件时,恢复首次应用之前的场景光照。 |
新地图的默认值:太阳颜色为暖白色 (1, 0.956, 0.84),强度 1.2,旋转 (50, −30),环境光 (0.45, 0.5, 0.58),雾效关闭,雾效颜色 (0.6, 0.68, 0.75),密度 0.004。
如果游戏需要昼夜循环,可以直接用你自己的脚本驱动太阳光源。RTSEnvironment 只会在加载地图或地图环境发生变化时重新应用地图中的值。另一种做法是取消勾选 Apply Sun,只保留地图中的环境光和雾效。
环境 API
| 成员 | 说明 |
|---|---|
void Apply() | 立即将地图环境应用到场景中(遵循 Apply In Edit Mode 设置)。 |
void CaptureFromScene(RTSEditSession session) | 将场景光照写入地图,作为该会话中的一个撤销步骤。 |
void CaptureFromScene(RTSToolContext ctx = null) | 将场景光照写入地图。只有在 RTSEditSession.RunOperation 内部调用时才会记录撤销。 |
void RestoreSceneState() | 恢复首次应用之前保存的光照。 |
RTSTerrain Terrain { get; set; }、Light Sun { get; set; }、bool CanApply | 绑定的地形、太阳光源,以及当前是否允许应用。 |
static Light FindSun() | 如果 RenderSettings.sun 是方向光,则返回它,否则返回最亮的已启用方向光。 |
using RTSTerrainStudio;
using UnityEngine;
/// Switches the current map to a stormy mood. RTSEnvironment applies it immediately.
public static class StormLighting
{
public static void Apply(RTSTerrain terrain)
{
MapData map = terrain != null ? terrain.Map : null;
if (map == null) return;
// Work on a copy, then assign it: this is how the editor tools change the environment too.
EnvironmentSettings env = map.Environment.Clone();
env.sunColor = new Color(0.7f, 0.75f, 0.85f);
env.sunIntensity = 0.45f;
env.ambientColor = new Color(0.3f, 0.33f, 0.4f);
env.fog = true;
env.fogColor = new Color(0.45f, 0.48f, 0.52f);
env.fogDensity = 0.012f;
map.Environment = env;
// MapLayers.Environment tells RTSEnvironment (and the in-game editor) that the lighting changed.
terrain.NotifyChanged(MapLayers.Environment, map.CellBounds);
}
}
性能指南
对于典型的 RTS 地图(每边 128–256 个单元格),默认设置在桌面平台上就能流畅运行。本节说明每个设置项的开销,便于你有针对性地调优大地图和移动平台构建版本。
快速检查清单
- 确保已安装 Burst 包,并已启用 Burst 编译()。
- 在编辑器中针对你要发布的平台构建图块集的纹理数组,并开启 Compress Arrays(图块集的 Inspector 会显示格式和预估大小)。
- 在实例化树木、细节和装饰物的每个材质上勾选 Enable GPU Instancing(项目验证器可以一键完成)。
- 纯景观用途的树木和道具使用
Instanced生成模式;带有游戏玩法逻辑的对象保留GameObject模式。 - 根据平台选择 Splat 分辨率和纹理数组分辨率(见下文)。两者主要消耗的是内存。
- 运行 Validate Map(Map 选项卡 ▸ Validation)。当树木(20 000)、GameObject 树木(1 500)、装饰物(6 000)、GameObject 装饰物(2 000)或细节图层(8)过多,或者地图过大(超过 512 个单元格)时,它会发出警告。
选择区块大小
| Chunk Size | Draw Call(256 × 256 地图) | 每次编辑的重建开销 | 适用场景 |
|---|---|---|---|
| 16 | 256 个区块 | 最小:笔刷反馈最快 | 小地图、频繁编辑(在较慢的设备上使用游戏内编辑器) |
| 32(默认) | 64 个区块 | 均衡 | 大多数地图 |
| 64 | 16 个区块 | 最大:每次编辑最多重建 64 × 64 个单元格,在区块边界处编辑时还会重建相邻区块 | 很少编辑的大地图(随游戏发布的地图,玩家只看不改) |
每个区块在每个渲染视图(主摄像机和每个阴影级联)中各占一个 Draw Call;由于所有区块共用同一个材质,SRP Batcher 能以很低的开销将它们合批。区块也是剔除的基本单位,因此当摄像机贴近地面时,较小的区块剔除得更精确。
细分
平滑地面的顶点数随 Surface Subdivisions 的平方增长:
| Surface Subdivisions | 平坦的 32 × 32 区块的顶点数 | 实际生效的 Cliff Subdivisions:默认值 4 时(设为 8 时) |
|---|---|---|
| 1 | 1 089 | 4 (8) |
| 2(默认) | 4 225 | 4 (8) |
| 3 | 9 409 | 6 (12) |
| 4 | 16 641 | 4 (8) |
Cliff Subdivisions 会向上取整为一个既是偶数、又是 Surface Subdivisions 倍数的值,这样悬崖单元格和平滑单元格就能共享所有边界顶点。表面细分为 3 时,悬崖子栅格会变成 6 的倍数,开销很大。建议使用 1、2 或 4。每个悬崖单元格大约需要 (悬崖细分数)² × 2 个三角形,外加其崖壁(每个层级的行数等于 Cliff Wall Segments),因此悬崖较多的地图最能从适中的悬崖设置中受益。
每个区块顶点占 28 字节。只要一个区块的顶点数低于 65 536,索引就使用 16 位。细分为 2 的平坦 256 × 256 地图大约有 260 000 个顶点,顶点和索引数据约 10 MB。悬崖会在此基础上继续增加数据量,区块碰撞体还需要各自的物理烘焙数据。
Splat 分辨率
Splat 分辨率(每个单元格的纹素数:1、2、4 或 8,默认 4)在创建地图时(New Map 向导)选定,它决定了地面纹理可以绘制得多精细。Splat 数据始终由 4 张 RGBA32 贴图组成(每个纹素 16 字节),并且会存在三份:地图数据中、纹理的 CPU 副本中以及 GPU 上(后两者都包含 Mipmap,+33%)。
| 地图尺寸(单元格) | Splat 分辨率 2 | Splat 分辨率 4(默认) | Splat 分辨率 8 |
|---|---|---|---|
| 128 × 128 | 1 MiB 数据 + 1.3 MiB GPU | 4 MiB + 5.3 MiB | 16 MiB + 21 MiB |
| 256 × 256 | 4 MiB + 5.3 MiB | 16 MiB + 21 MiB | 64 MiB + 85 MiB |
| 512 × 512 | 16 MiB + 21 MiB | 64 MiB + 85 MiB | 256 MiB + 341 MiB |
| 1024 × 1024 | 64 MiB + 85 MiB | 256 MiB + 341 MiB | 1 GiB + 1.3 GiB |
(纹理的 CPU 副本还要再加上一份与 GPU 列相同的数值。)绘制的开销很低:只会复制发生变化的行。对于超过 100 万纹素的纹理,如果变化区域不超过 512 × 512 纹素,则只上传该区域。该区域的 Mip 链由 CPU 上的 Burst 作业(SplatMipDownsampleJob)重建,然后在 GPU 上复制到对应位置。
分辨率 2 足以呈现硬边过渡的经典 RTS 风格图块外观(Tile Paint)。分辨率 4 可提供柔和、细腻的过渡。分辨率 8 仅适用于单元格尺寸非常小或摄像机贴近地面的情况。
纹理数组分辨率
每个图块集都会将其地面和悬崖纹理打包进两个 Texture2DArray(反照率/高度和法线),每个数组 20 个切片,分辨率取决于图块集的 Array Resolution(256、512、1024 或 2048,默认 1024)。两个数组合计的内存占用(含 Mipmap):
| Array Resolution | 未压缩 RGBA32 | BC7 / DXT5(桌面平台、主机) | ASTC 6×6(Android、iOS) |
|---|---|---|---|
| 256 | 13 MiB | 3.3 MiB | 1.5 MiB |
| 512 | 53 MiB | 13 MiB | 5.9 MiB |
| 1024(默认) | 213 MiB | 53 MiB | 24 MiB |
| 2048 | 853 MiB | 213 MiB | 95 MiB |
开启 Compress Arrays 后,图块集的 Inspector 会针对当前构建平台烘焙压缩数组:Windows、macOS、Linux 和主机平台使用 BC7,Android、iOS、tvOS 和 visionOS 使用 ASTC 6×6,Web 使用构建所设置的纹理压缩格式(DXT5、ASTC 6×6 或 ETC2 RGBA8),其他平台使用 DXT5。如果预先构建的数组缺失,或在编辑器中已过期,渲染器会改为在 GPU 上构建未压缩的 RGBA32 数组。这在快速迭代时没有问题,但内存占用是 BC7 的四倍。Player 构建会在构建开始前重建已过期的数组(参见Player 构建中的纹理数组)。使用同一图块集的所有地形共享这些数组。
如果 GPU 或浏览器无法采样烘焙所用的格式,Unity 会在加载数组时将其解压,内存占用约为原来的四倍。Player 会在首次使用预构建数组时释放它们在 CPU 端的副本,因此在 Player 中,预构建数组只占用上表所列的 GPU 内存。
要检查运行中的游戏使用的是哪种数组,可以在 Memory Profiler 中抓取一次快照,然后搜索名为“RTS Albedo Array (…)”和“RTS Normal Array (…)”的渲染纹理。如果能找到它们,说明正在使用运行时构建的未压缩数组。
细节距离与植被
- 细节最有可能成为 GPU 瓶颈,因为草由大量经过 Alpha 裁剪的小三角形组成。可见数量与 Render Distance² × 密度成正比。将细节距离减半,绘制的草大约会减少到原来的四分之一。
- 细节内存:无论摄像机位于何处,每个已绘制的地块都会把它的实例保留在 GPU 上,每个实例 32 字节。示例:在整张 256 × 256 地图上以满密度铺满一个细节图层(Max Density 6,细节分辨率 2 = 每个单元格 4 个纹素),大约会产生 160 万个实例,约 50 MB。只在真正需要的地方绘制草,或者降低 Max Density。
- 细节阴影默认关闭。开启后,每个阴影级联都会把所有草重新渲染一遍。
- 树木:实例化树木在每个地块组内,每个部件每 1 023 个实例占用一个 Draw Call。请使用带有低开销 LOD1 的 LODGroup。GameObject 树木每棵都要占用一个 GameObject(应保持在约 1 500 棵以下)。
- 装饰物:在大地图中为实例化装饰物设置 Max Draw Distance。GameObject 装饰物会由 Unity 进行剔除,但每个仍要占用一个 GameObject(应保持在约 2 000 个以下)。
- 水体:与默认值 2 相比,将 Cell Subdivisions 设为 1 可使水体顶点减少到四分之一。
Burst 与作业
所有繁重的循环都在经 Burst 编译的作业中运行:区块网格(BuildChunkGeometryJob、CopyChunkPartsJob)、Splat Mip(SplatMipDownsampleJob)、水体网格(WaterChunkMeshJob)、细节散布(DetailScatterJob)、树木矩阵(TreeMatrixJob)以及装饰物放置(DecorPlacementJob、DecorPartMatricesJob)。笔刷、通行、战争迷雾、寻路器和小地图烘焙器同样使用 Burst。区块碰撞体在工作线程上并行烘焙。
- 网格作业采用同步编译,因此域重载后的第一次构建不会退回到缓慢的托管代码实现。
- 测量性能时,应使用 Player 构建,或先关闭 Burst 安全检查()。编辑器的安全检查和 Jobs Debugger 会让作业明显变慢。
- 禁用 Burst 编译后一切仍能正常工作,但网格重建和笔触会慢上许多倍。
- Web 构建没有工作线程,所有作业都在主线程上依次运行,因此针对整张地图的操作耗时会明显更长(参见 Web 构建的渲染与性能)。
性能分析
打开 ,在主线程上查找以下标记:
| 标记 | 测量内容 |
|---|---|
RTSVegetation.Update | 将地图变更应用到植被(包含下面两个构建标记)。 |
RTSVegetation.BuildDetails | 重新生成已标记为脏的细节地块。 |
RTSVegetation.BuildTrees | 重建实例化树木组。 |
RTSVegetation.Draw | 为一个摄像机执行植被剔除并提交 Draw Call。 |
将 CPU 模块切换到 Timeline 视图,即可在工作线程上看到上面列出的作业名称。GPU 方面,可使用 Frame Debugger()统计地形区块、细节和实例化的绘制次数。Vegetation Renderer、Water Renderer 和 Decor Manager 的 Inspector 会实时显示实例数和顶点数。RTSTerrain 的 Inspector 会显示区块数量和地图内存。
内存数据
地图数据本身(即 RTSTerrain 的 Inspector 中的 Map Memory 值,也就是 MapData.GetMemoryUsage() 的结果)的构成如下:
- 每个顶点 11 字节(高度、悬崖层级、标记、悬崖类型、水面高度);
- 每个 Splat 纹素 16 字节;
- 每个通行纹素 1 字节;
- 每个细节图层的每个细节纹素 1 字节。
| 地图尺寸(默认值:Splat 4、细节 2、通行 2、1 个细节图层) | 地图数据 | Splat 纹理(GPU) | 纹理数组(1024,BC7) |
|---|---|---|---|
| 128 × 128(单元格为 2 m 时为 256 m) | ≈ 4.3 MiB | ≈ 5.3 MiB | 每个图块集 53 MiB |
| 256 × 256 | ≈ 17 MiB | ≈ 21 MiB | 每个图块集 53 MiB |
| 512 × 512 | ≈ 69 MiB | ≈ 85 MiB | 每个图块集 53 MiB |
| 1024 × 1024 | ≈ 275 MiB | ≈ 341 MiB | 每个图块集 53 MiB |
此外还有区块网格和碰撞体(参见细分)、细节实例(每个 32 字节)、通行图以及战争迷雾纹理。编辑器的撤销历史有自己独立的内存预算(参见编辑器窗口的设置)。
推荐地图尺寸
地图每边可以有 16 到 1024 个单元格,以 16 为步长。默认值为 128 × 128 个单元格,每个单元格 2 m。
| 尺寸(单元格) | 典型用途 | 备注 |
|---|---|---|
| 64–128 | 1v1 和小型遭遇战地图、移动平台 | 所有内容都能即时重建。无论在哪种设备上,都很适合使用游戏内编辑器。 |
| 128–256 | 2–4 名玩家 | 最佳区间,所有默认值都针对它调优。 |
| 256–512 | 大地图,4–8 名玩家 | 桌面平台。可考虑使用 Splat 分辨率 2–4 和 Chunk Size 32–64。 |
| 512 以上 | 超大地图 | 地图验证会显示一条信息:编辑和通行数据的重建耗时更长。请留意 Splat 内存(见上表),并考虑将世界拆分为多个 RTSTerrain。 |
可以将多个地形相邻摆放,每个地形都有自己的变换(Transform)。RTSTerrain.FindAt 和 RTSTerrain.RaycastAll 可以为给定的世界坐标找到对应的地形。
移动平台注意事项
- 纹理数组:在 Android/iOS 平台处于激活状态时构建,这样它们会被烘焙为 ASTC 6×6(构建过一次之后,Player 构建会针对当前平台让它们保持最新)。在手机上,Array Resolution 设为 512 通常就足够了。
- Splat 分辨率:与默认值 4 相比,每个单元格 2 个纹素可使 Splat 内存降至四分之一。
- 图层:在某个像素上可见的每个地面图层都会带来纹理采样开销。每个区域尽量少用图层;对于平铺重复感不明显的图层,将 Anti Tiling 设为 0(可省去第二次采样)。
- 光照:使用关闭了 Specular Highlights 和 Environment Reflections 的覆盖材质(参见前文)。
- OpenGL ES 与 WebGL 2.0:本包着色器的目标级别为 Shader Model 3.5,因此可以在 OpenGL ES 3.0 设备和浏览器中运行。这些 API 只保证每次绘制时片元着色器可使用 16 个纹理,而地形的前向渲染通道自身就要占用其中 10 个。Unity 的 URP 项目模板自带的 Mobile URP 资源(主光源阴影、反射探针混合)已经额外占用 4 个,再开启光照 Cookie 就会达到上限。需要更多纹理的着色器变体会加载失败,地形随之消失或显示为粉色。在用于 GLES 设备和 Web 构建的 URP 资源中,如果场景不混合反射探针,请关闭反射探针混合;除非确有需要,否则请保持光照 Cookie、贴花、SSAO、附加光源阴影和自适应探针体积处于关闭状态。
Shaders/Include/RTSTerrainInput.hlsl中的注释 Texture slot budget 列出了每项 URP 功能的开销。 - 水体:如果为了节省带宽而在移动平台的 URP 资源中关闭 Depth Texture 或 Opaque Texture,水体会切换到回退方案:缺少不透明纹理时没有折射,缺少深度纹理时岸边效果会忽略水中的物体。自行开启了这两项的摄像机仍能获得完整的水体效果。请使用 Cell Subdivisions 1。
- 植被:降低 Detail Density 和 Detail Distance 倍率,将 Crossed Quad Count 设为 2,并关闭细节阴影。
- 物理:如果游戏不在地形上使用 Unity 物理系统,请将 Collider Mode 设为
None,并使用RTSTerrain.Raycast/RaycastAll进行拾取。 - 几何体:将 Surface Subdivisions 设为 1(经典 RTS 外观)、Cliff Wall Segments 设为 2,可大幅减少顶点数。
通过代码应用低配预设
using RTSTerrainStudio;
using UnityEngine;
/// Applies cheap settings to a terrain and its systems, e.g. from a "Low" graphics option.
public static class LowEndPreset
{
public static void Apply(RTSTerrain terrain)
{
if (terrain == null) return;
// Terrain mesh: fewer vertices, larger chunks, no colliders (picking uses RTSTerrain.Raycast).
RTSTerrainSettings s = terrain.Settings;
s.surfaceSubdivisions = 1;
s.cliffSubdivisions = 4;
s.cliffWallSegments = 2;
s.chunkSize = ChunkSize._64;
s.colliderMode = ColliderMode.None;
// Settings changed from code are applied by a rebuild (the inspector does this automatically).
terrain.RebuildAll();
if (terrain.TryGetComponent(out RTSVegetationRenderer vegetation))
{
vegetation.detailDensityMultiplier = 0.4f;
vegetation.detailDistanceMultiplier = 0.5f;
vegetation.treeDistanceMultiplier = 0.7f;
vegetation.crossedQuadCount = 2;
}
if (terrain.TryGetComponent(out RTSWaterRenderer water))
{
water.cellSubdivisions = 1; // rebuilt automatically when the value changes
}
if (terrain.TryGetComponent(out RTSDecorManager decor))
{
decor.maxDrawDistance = 120f; // instanced props beyond 120 m are skipped
}
}
}
ColliderMode.None 会移除区块碰撞体。游戏中所有针对地形使用 Physics.Raycast 的功能、站在地形上的刚体,以及基于区块碰撞体烘焙的 NavMesh 都会失效。只有在你使用不依赖物理系统的查询时才切换到此模式。
游戏玩法系统
RTS Terrain Studio 不只是一个地图编辑器。你制作的地图自带经典 RTS 游戏所需的各种系统:会考虑悬崖层级和树木遮挡的战争迷雾、带有内置寻路功能的通行图、现成的示例玩法脚本(摄像机、单位、选择、命令、小地图、HUD),以及能生成纹理、模型、地图和可玩演示场景的演示内容生成器。
本章依次介绍每个系统。每一节先说明无需代码即可完成的操作,然后列出相关组件及其设置项,最后介绍面向程序员的脚本 API。
只想先玩一玩?运行 ,在 Project 窗口中双击随后自动选中的场景 RTS Terrain Studio - Gameplay Demo,然后单击 Play。参见演示场景。

战争迷雾
战争迷雾会隐藏玩家看不到的内容。它遵循经典 RTS 规则:
- 按队伍计算。最多可为 32 支队伍(队伍索引 0 到 31)分别跟踪可见性。每个单位或建筑都为自己所在的队伍提供视野。
- 三种状态。队伍从未见过的地面显示为黑色(未探索)。以前见过但当前看不到的地面会变暗(已探索)。当前能看到的地面正常显示(可见)。
- 探索记忆。每支队伍都会记住自己探索过的区域,直到你将其重置。
- 基于悬崖层级的视线。单位能看到自己所在的悬崖层级以及其下方的一切。从低处永远看不到更高的地面,而且更高的地面还会遮挡其后方的单元格。
- 视野阻挡物。树木(以及你标记为遮挡视野的装饰物)会遮住其后方的单元格。
- 同盟与共享视野。队伍之间可以单向或双向共享视野。
- 隐藏的敌人。当你看不到敌方单位时,它们会消失。建筑一旦被发现,就会以“残影”的形式保持可见。
可见性由 Burst 作业每秒计算数次,并存储在一张纹理中。这张纹理会随时间平滑过渡,并在 GPU 上进行模糊处理,因此迷雾边缘会柔和地移动,而不是逐格跳变。
快速入门:在场景中添加战争迷雾
- 打开一个包含 RTS Terrain 的场景。
- 选择 。这会创建一个带有
RTSFogOfWarManager组件的Fog of WarGameObject,并将其选中。 - 如果你的 URP 渲染器还没有战争迷雾渲染器功能,会弹出对话框询问是否添加。单击 Add Feature。(你也可以稍后通过 添加,或者手动添加,参见渲染。)
- 为每个需要提供视野的单位和建筑添加
RTSFogOfWarRevealer组件(),并设置其 Team 和 Radius。 - 为每个不应被敌人看到的对象添加
RTSFogOfWarHideable组件(),并设置其 Team。单位使用 Hide When Not Visible,建筑使用 Ghost When Explored。 - 单击 Play。除了 Local Team(默认为队伍 0)的视野源周围,整张地图都是黑色的。
迷雾只在 Play 模式下运行。要在编辑时查看迷雾,请在管理器上启用 Preview In Edit Mode(或在其 Inspector 窗口中单击 Enable Preview)。可隐藏对象只会在 Play 模式下被隐藏;在编辑模式下,所有对象都保持可见。
示例单位 RTSUnit 已经依赖并自动配置了视野源和可隐藏对象组件。如果你使用这些示例(或 RTSDemoGame),就无需手动添加这些组件。场景中没有管理器时,RTSDemoGame 甚至会为你自动创建一个。
可见性的计算方式
地图被划分为迷雾单元格。在 Resolution Per Cell 为默认值 2、地图单元格尺寸为默认值 2 m 时,一个迷雾单元格宽 1 m。对于每个视野源,管理器会:
- 确定视点层级:视野源确切位置处的悬崖层级,再加上它的 Height Bonus。
- 遍历视野半径(Radius)内的每个迷雾单元格。
- 根据 Line Of Sight 模式,从视野源向该单元格追踪一条直线。如果该单元格自身的悬崖层级不高于视点层级,且沿途没有任何单元格高于视点层级、也没有任何单元格(在考虑阻挡物的模式下)包含树木或遮挡视野的装饰物,则该单元格可见。阻挡视线的单元格本身是可见的,因此你能看到森林的边缘,却看不到森林后面的东西。
- 为视野源所在的队伍以及所有接收该队伍视野的队伍,将该单元格标记为可见且已探索。
| Line Of Sight 模式 | 行为 | 适用场景 |
|---|---|---|
None | 视野源能看到完整的圆形范围。悬崖和树木不会遮挡视线。这是开销最低的模式。 | 没有视线机制的游戏、单位数量极多的游戏。 |
CliffLevels | 经典 RTS 规则:视野源能看到自己所在的悬崖层级以及其下方的一切。更高的地面会遮挡视线。 | 开阔的地图,或者树木不应遮挡视野的情况。 |
CliffLevelsAndBlockers(默认) | 与 CliffLevels 相同,此外树木和遮挡视野的装饰物也会遮住其后方的单元格。 | 完整的经典 RTS 体验。 |
哪些对象会遮挡视野,需要在库中设置:
- 树木:植被库中树木原型的 Blocks Vision 选项(默认开启)。阻挡范围为树木的 Footprint Radius 乘以实例的缩放。
- 装饰物:装饰物库中装饰物原型的 Blocks Vision 选项(默认关闭)。阻挡范围为该原型的占地范围(圆形或旋转后的矩形),并随实例一同缩放。
瞭望塔、山丘和飞行侦察单位:为它们的视野源设置一个或多个层级的 Height Bonus,让它们能越过更高的悬崖观察;或者为空中单位启用 Ignore Line Of Sight,使其视线不受任何遮挡。
地图编辑会被自动跟踪(无论是在编辑器中、在游戏内编辑器中,还是来自调用 RTSTerrain.NotifyChanged 的游戏玩法代码)。悬崖层级的编辑(MapLayers.CliffLevels / VertexFlags)只会重新采样发生变化的单元格,以及向外扩展一格的边缘范围。树木和装饰物的编辑会在下一次更新时重建整张地图的视野阻挡物栅格。调用 NotifyChangedAll 会完整重建悬崖层级栅格。
RTSFogOfWarManager
每个场景使用一个管理器,为一个地形计算并渲染迷雾。你可以通过 或 创建它。如果启用了多个管理器,只会使用第一个(RTSFogOfWarManager.Active),并且 Inspector 窗口会显示警告。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Map | ||
| Terrain | 空 | 迷雾覆盖的地形。留空表示使用 RTSTerrain.Main(第一个已启用的地形)。 |
| Resolution Per Cell | 2 | 每个地图单元格在每个轴向上的迷雾单元格数:1、2 或 4。数值越高,边缘越平滑,但开销也越大。该值会被自动降低,确保迷雾栅格每个轴向不超过 2048 个单元格。修改此值会重建栅格并清除探索记录。 |
| Simulation | ||
| Update Interval | 0.1 | 两次可见性更新之间的间隔秒数。0 表示每帧更新。 |
| Local Team | 0 | 视野会被渲染出来、并供可隐藏对象使用的队伍(0 到 31)。在 Inspector 窗口中切换它,可以查看其他玩家看到的画面。 |
| Shared Vision | 全部为 0 | 每个接收方队伍各有一个位掩码,表示哪些队伍与其共享视野(第 t 位 = 队伍 t)。通常通过代码设置,参见同盟。 |
| Line Of Sight | CliffLevelsAndBlockers | 悬崖和对象如何遮挡视线。参见可见性的计算方式。 |
| Compute All Teams | 关 | 为每支队伍计算可见性,而不只是本地队伍。服务器(兴趣管理)以及需要查询其他队伍视野的 AI 代码都需要启用此项。 |
| Start Explored | 关 | 整张地图一开始就处于已探索状态:没有黑色区域,只有变暗的迷雾。在创建迷雾栅格时生效。 |
| Reveal All | 关 | 调试用:对所有队伍而言,一切都可见且已探索。 |
| Look | ||
| Unexplored Color | 黑色,alpha 1 | 从未探索区域的颜色。Alpha 表示强度。 |
| Explored Color | (0.3, 0.32, 0.38),alpha 0.6 | 叠乘到当前不可见的已探索区域上的色调。Alpha 表示强度。 |
| Fade Speed | 4 | 迷雾淡入淡出的速度(每秒完成的完整淡变次数)。0 表示立即切换。 |
| Blur Iterations | 1 | 对迷雾纹理执行的 GPU 模糊次数(0 到 4,0 = 关闭)。 |
| Blur Upscale | 2 | 模糊后纹理的分辨率倍数(1 到 4)。数值越高,边缘越圆润。该值会被自动降低,确保模糊后的纹理不超过 2048 像素。 |
| Editor | ||
| Preview In Edit Mode | 关 | 在编辑模式下运行战争迷雾,作为预览。 |
| Shaders | ||
| Blur Shader, Minimap Shader | 自动指定 | 在此处引用,以确保这些隐藏着色器会被包含在 Player 构建中。保持原样即可。 |
Inspector 窗口还会显示实时统计信息(状态、迷雾栅格尺寸、内存、活动视野源数量、更新次数)、迷雾纹理的实时预览(红色 = 当前可见,绿色 = 已探索,黑色 = 从未见过),以及以下按钮:
| 按钮 | 作用 |
|---|---|
| Explore All | 为本地队伍将整张地图标记为已探索。 |
| Reset Exploration | 清除所有队伍的全部探索记录。 |
| Update Now | 立即重新计算可见性。 |
| Rebuild Terrain Data | 从地形重新读取悬崖层级和阻挡物。 |
RTSFogOfWarRevealer
为所在队伍提供该 GameObject 周围的视野。把它添加到单位、建筑、侦察守卫以及其他任何需要“看见”的物体上。视野源在启用时会自动向管理器注册;管理器在每次更新时读取它的位置,因此该组件每帧没有任何开销。选中它,即可在 Scene 视图中看到以黄色圆圈表示的视野半径。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Team | 0 | 获得视野的队伍(0 到 31)。 |
| Radius | 12 | 视野半径,以米为单位。 |
| Height Bonus | 0 | 视野源可以额外越过的悬崖层级数(瞭望塔、山丘、飞行侦察单位)。 |
| Ignore Line Of Sight | 关 | 视线可越过悬崖和阻挡物(空中单位)。 |
| Reveal When Disabled | 关 | 在此组件或其 GameObject 被禁用期间仍继续提供视野,直到被销毁为止。 |
如果启用了 Reveal When Disabled 的视野源所在的 GameObject 在场景加载时就已处于非激活状态,管理器会在启用时统一收集一次。之后以非激活状态实例化的对象(对象池、以叠加方式加载的场景)永远不会运行 Awake,因此请使用 RTSFogOfWarManager.RegisterRevealer(revealer) 自行注册它们,或者通过代码设置 RevealWhenDisabled = true。
RTSFogOfWarHideable
当本地队伍看不到某个敌方对象时,隐藏它的渲染器(以及可选的画布和碰撞体)。本地队伍的对象,以及与本地队伍共享视野的队伍的对象,永远不会被隐藏。对象重新可见时,只会重新启用它被隐藏时原本处于启用状态的组件,因此你自己的启用/禁用逻辑仍能正常工作。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Team | 0 | 拥有该对象的队伍(0 到 31)。 |
| Mode | HideWhenNotVisible | HideWhenNotVisible:仅当该位置当前可见时才显示(单位)。GhostWhenExplored:被发现后一直保持显示,即使该区域再次被迷雾覆盖(建筑和中立道具,与经典 RTS 游戏一样)。 |
| Require Sighting | 开 | 仅适用于残影模式:对象必须至少被看到过一次。关闭时,只要所在区域已探索就会显示该对象。 |
| Hide Canvases | 开 | 同时禁用 Canvas 组件(血条、名称牌)。 |
| Hide Colliders | 关 | 隐藏时同时禁用碰撞体,使隐藏的敌人无法被单击。在权威服务器和主机上请保持关闭:它们的物理查询仍必须能命中本地玩家看不到的单位。 |
| Pivot | 空 | 可选的 Transform,用它的位置进行检测。留空表示使用此对象自身的 Transform。 |
对象被显示或隐藏时,可隐藏对象会触发 VisibilityChanged(bool)。在运行时添加或移除子渲染器后,请调用 RefreshComponents()。
渲染:渲染器功能与着色器回退
迷雾有两种绘制方式。具体使用哪种按摄像机决定,因此迷雾永远不会被重复应用。
| 绘制方式 | 使用时机 | 变暗的内容 |
|---|---|---|
| RTS Fog Of War 渲染器功能(推荐) | 摄像机使用的 Universal Renderer 包含该功能。 | 屏幕上的一切:地形、植被、水体、装饰物、单位以及任何自定义材质。天空不受影响。 |
| 着色器回退 | 摄像机的渲染器不包含该功能。 | 仅限本包自带的地形、水体和植被着色器。使用其他材质的单位和道具不会变暗(敌方单位仍会被各自的可隐藏对象组件隐藏)。 |
手动添加渲染器功能:
- 选择你的 Universal Renderer Data 资源(即游戏摄像机所使用的渲染器)。
- 在其 Inspector 窗口中单击 Add Renderer Feature,然后选择 RTS Fog Of War。
- 对游戏摄像机使用的每个渲染器重复此操作,以确保效果一致。
你也可以让 自动修复:它会把该功能添加到每个正在使用的 URP 资源的默认渲染器中。
| 功能设置项 | 默认值 | 说明 |
|---|---|---|
| Show In Scene View | 开 | 在 Scene 视图摄像机中也绘制迷雾。关闭时,使用此渲染器的 Scene 视图摄像机会显示没有迷雾的地图。 |
| Render Pass Event | AfterRenderingTransparents | 迷雾的绘制时机。默认值在后期处理之前执行,因此泛光和色调映射会叠加在迷雾之上。 |
该功能会自行请求摄像机深度纹理,并用它重建每个像素的世界位置。RTSFogOfWarRendererFeature.IsActive 会告诉你当前激活的 URP 资源的任一渲染器中是否存在已启用的该功能;它仅供参考。
自定义着色器(高级)。管理器会以全局着色器变量的形式发布迷雾数据:_RTS_FogOfWarTex(R = 可见,G = 已探索)、_RTS_FogOfWarParams(世界空间中的最小 X、最小 Z、1/X 方向尺寸、1/Z 方向尺寸)、_RTS_FogOfWarUnexplored、_RTS_FogOfWarExplored 和 _RTS_FogOfWarEnabled。本包的着色器会包含 Shaders/Include/RTSCommon.hlsl(位于 URP 的 Core.hlsl 之后),并对经过光照计算的颜色调用其中的辅助函数。自定义 URP 着色器也可以用同样的方式支持回退路径:
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// Include RTSCommon.hlsl after Core.hlsl. Adjust the path if you moved the package folder.
#include "Assets/RTSTerrainStudio/Shaders/Include/RTSCommon.hlsl"
half4 frag(Varyings input) : SV_Target
{
half3 color = /* your lit colour */ half3(1, 1, 1);
// Darkens unexplored and fogged areas. Does nothing while the renderer feature handles this camera.
color = RTS_ApplyFogOfWar(color, input.positionWS);
return half4(color, 1);
}
通过脚本控制战争迷雾
所有战争迷雾相关类型都位于 RTSTerrainStudio 命名空间中。通过 RTSFogOfWarManager.Active 获取场景中的管理器(如果管理器会动态创建和销毁,请监听静态事件 ActiveChanged)。
| 成员 | 说明 |
|---|---|
| 查询 | |
bool IsVisible(Vector3 world, int team) | 当该队伍当前能看到该位置时(包括自身视野和共享视野)返回 true。对于未计算的队伍返回 false(参见 IsTeamComputed)。 |
bool IsExplored(Vector3 world, int team) | 当该队伍至少看到过该位置一次时返回 true。 |
bool IsVisibleToLocalTeam(Vector3), bool IsExploredByLocalTeam(Vector3) | 针对本地队伍的快捷方法。 |
uint GetVisibilityMask(Vector3 world) | 当前能看到该位置的已计算队伍的位掩码(第 t 位 = 队伍 t)。非常适合服务器端的兴趣管理。 |
uint GetExploredMask(Vector3 world) | 已探索该位置的队伍的位掩码。 |
bool IsTeamComputed(int team) | 启用 Compute All Teams 时始终为 true,否则仅对本地队伍为 true。 |
bool TryGetFogCell(Vector3, out int2), Vector3 FogCellToWorld(int2) | 在世界位置与迷雾单元格之间相互转换。 |
bool IsCellVisible(int2, int team), bool IsCellExplored(int2, int team) | 上述查询的单元格版本。 |
int GetCellLevel(int2), bool IsCellBlocked(int2) | 用于视线计算的悬崖层级和阻挡状态。 |
Vector2 SampleDisplayedFog(Vector2 normalizedMapPosition) | 本地队伍在屏幕上显示的平滑值(x = 可见,y = 已探索)。没有活动的迷雾时返回 (1, 1)。 |
| 揭示与探索 | |
void RevealArea(Vector3 world, float radius, int team, float duration, bool ignoreLineOfSight = false) | 为某支队伍揭示一个圆形区域(照明弹、“揭示地图区域”类技能)。duration 在 Play 模式下按游戏时间计算,在编辑模式预览中按真实时间计算。值小于或等于 0 时,只在一次更新中揭示。 |
void ClearTemporaryReveals() | 移除所有通过 RevealArea 揭示的区域。 |
void ExploreAll(int team), void ExploreAllTeams() | 将整张地图标记为已探索。 |
void ResetExploration(), void ResetExploration(int team) | 清除探索记录(当前可见的区域会在下一次更新时再次被标记为已探索)。 |
| 同盟与共享视野 | |
void SetAlliance(int teamA, int teamB, bool allied) | 双向共享视野,或取消共享。 |
void SetSharedVision(int sourceTeam, int receiverTeam, bool shared) | 让 receiverTeam 看到 sourceTeam 所看到的内容(单向)。 |
void SetSharedVisionMask(int receiverTeam, uint sourceTeams), uint GetSharedVision(int receiverTeam) | 替换或读取某个接收方队伍的完整掩码。 |
bool HasSharedVision(int sourceTeam, int receiverTeam), uint GetViewMask(int team) | 读取共享状态;视野掩码包含该队伍自身。 |
bool IsFriendlyToLocalTeam(int team) | 对本地队伍以及与其共享视野的队伍返回 true。这些队伍的对象永远不会被隐藏。 |
| 更新控制 | |
void RequestUpdate(), void ForceUpdate() | 在下一次更新时重新计算,或立即重新计算。 |
void MarkTerrainDirty() | 完整重建悬崖层级栅格。地图编辑会被自动跟踪,因此你很少需要调用它。 |
void MarkBlockersDirty() | 当你通过代码移动了树木或装饰物、却没有调用 RTSTerrain.NotifyChanged 时,用它重建阻挡物栅格。 |
event Action Updated | 每次可见性更新完成、且可隐藏对象刷新完毕后触发。 |
| 状态 | |
IsSimulating, IsRunning, IsRenderingFog, HasGrid, HasVisibilityData | 迷雾是否正在运行、是否已有栅格,以及是否至少计算过一次。 |
FogTexture, RawFogTexture, FogWidth, FogHeight, FogCellSize, Version, ActiveRevealerCount, MemoryUsage | 栅格和纹理信息。 |
int EffectiveResolutionPerCell | 每个地图单元格实际使用的迷雾单元格数。在超大地图上,栅格会被限制在 MaxFogGridSize(2048)以内,此时该值会低于 ResolutionPerCell。 |
RTSTerrain Terrain, RTSTerrain BoundTerrain | 要覆盖的地形(null = RTSTerrain.Main),以及当前栅格所对应的地形。 |
| 设置(与 Inspector 窗口中相同) | |
ResolutionPerCell, LocalTeam, LineOfSight, ComputeAllTeams, RevealAll, FadeSpeed, BlurIterations, BlurUpscale | 可设置的属性。修改 ResolutionPerCell 会重建栅格并清除探索记录。 |
const int MaxTeams (32), const int MaxFogGridSize (2048) | 队伍数量上限,以及迷雾栅格的最大边长。 |
| 注册表 | |
static IReadOnlyList<RTSFogOfWarRevealer> Revealers, static IReadOnlyList<RTSFogOfWarHideable> Hideables | 所有管理器中全部已启用的视野源和可隐藏对象。 |
static RegisterRevealer / UnregisterRevealer, RegisterHideable / UnregisterHideable | 由组件自身调用;只有编写自定义组件时才需要使用。 |
| RTSFogOfWarHideable 与 RTSFogOfWarRevealer | |
bool IsVisible, bool HasBeenSighted(可隐藏对象) | 当前状态,以及该对象是否曾被看到过(残影模式)。 |
Vector3 TestPosition(可隐藏对象) | 用于迷雾检测的位置(Pivot 或自身的 Transform)。 |
void Reevaluate(), void ResetSighting()(可隐藏对象) | 立即重新检测;忘记该对象曾被看到过。 |
event Action<bool> VisibilityChanged(可隐藏对象) | 当对象在本地队伍的视野中出现或消失时触发。 |
bool IsRevealing(视野源) | 视野源生效期间(已启用,或开启了 Reveal When Disabled)为 true。 |
示例:照明弹技能与可见性检查。
using RTSTerrainStudio;
using UnityEngine;
// Reveals an area for a few seconds and tells whether a target can be attacked.
public class ScoutAbilities : MonoBehaviour
{
[SerializeField] int team = 0; // the team that casts the flare
[SerializeField] float flareRadius = 15f;
[SerializeField] float flareSeconds = 5f;
// Call this with the world position the player clicked.
public void CastFlare(Vector3 worldPosition)
{
var fog = RTSFogOfWarManager.Active;
if (fog == null) return; // no fog of war in this scene
// ignoreLineOfSight: true lets the flare see over cliffs and trees, like an air unit.
fog.RevealArea(worldPosition, flareRadius, team, flareSeconds, ignoreLineOfSight: true);
}
// True when our team may target something at this position.
public bool CanTarget(Vector3 targetPosition)
{
var fog = RTSFogOfWarManager.Active;
// Without a running fog, everything is visible.
if (fog == null || !fog.IsRunning) return true;
// For teams other than the local one, enable "Compute All Teams" on the manager,
// otherwise IsVisible returns false for them.
return fog.IsVisible(targetPosition, team);
}
}
示例:同盟与通过代码生成的单位。
using RTSTerrainStudio;
using UnityEngine;
public class TeamSetup : MonoBehaviour
{
[SerializeField] GameObject unitPrefab;
void Start()
{
var fog = RTSFogOfWarManager.Active;
if (fog != null)
{
fog.SetAlliance(0, 2, true); // teams 0 and 2 see what the other sees
fog.SetSharedVision(3, 0, true); // team 0 also sees what team 3 sees (one way)
}
SpawnUnit(1, new Vector3(40f, 0f, 40f));
}
// Creates an enemy unit that sees and can be hidden by the fog of war.
void SpawnUnit(int team, Vector3 position)
{
var go = Instantiate(unitPrefab, position, Quaternion.identity);
// Reuse components the prefab may already have (RTSFogOfWarHideable allows only one per object).
if (!go.TryGetComponent(out RTSFogOfWarRevealer revealer))
revealer = go.AddComponent<RTSFogOfWarRevealer>();
revealer.Team = team;
revealer.Radius = 12f; // metres
if (!go.TryGetComponent(out RTSFogOfWarHideable hideable))
hideable = go.AddComponent<RTSFogOfWarHideable>();
// Subscribe BEFORE setting Team and Mode: in Play Mode both setters re-test the object immediately,
// so a unit spawned in the fog is hidden (and the event raised) right away.
// React when the unit appears or disappears for the local player (sounds, minimap pings ...).
hideable.VisibilityChanged += visible => Debug.Log(go.name + (visible ? " spotted" : " lost"));
hideable.Team = team;
hideable.Mode = FogOfWarHideMode.HideWhenNotVisible;
// The current state after configuring.
Debug.Log(go.name + (hideable.IsVisible ? " starts visible" : " starts hidden"));
}
}
带战争迷雾的小地图
RTSFogOfWarMinimapUtility 会用本地队伍的迷雾压暗小地图图像,效果与经典 RTS 游戏的小地图一样。小地图必须恰好覆盖整个地图区域:UV (0, 0) 位于地图原点所在的角,UV (1, 1) 位于对角。用 RTSMinimapBaker 烘焙出的图像正是这种布局。示例 RTSMinimapUI 就使用了这个工具类。
| 方法 | 说明 |
|---|---|
static bool Composite(Texture minimap, RenderTexture destination, RTSFogOfWarManager manager = null) | GPU 版本,非常适合实时更新的小地图。在 Updated 事件之后或每隔几帧调用一次。没有激活的迷雾时,小地图会原样复制。着色器不可用时返回 false。 |
static bool CompositeCpu(Texture2D source, Texture2D destination, RTSFogOfWarManager manager = null) | CPU 版本(Burst 作业),无需渲染纹理,适合截图等用途。source 必须可读;destination 必须是尺寸相同的可读 RGBA32 纹理(也可以就是同一张纹理)。 |
static bool CompositeCpu(NativeArray<Color32> source, NativeArray<Color32> destination, int width, int height, RTSFogOfWarManager manager = null) | 直接处理原始像素缓冲区的 CPU 版本。 |
static void ReleaseResources() | 销毁缓存的材质和命令缓冲区。域重载前会自动调用;如果小地图彻底关闭、不再使用,需要你自行调用它。 |
using RTSTerrainStudio;
using UnityEngine;
// Bakes the map once and keeps a fogged copy in a render texture.
// Show Output with a RawImage (uGUI) or a UI Toolkit Image.
public class FoggedMinimap : MonoBehaviour
{
[SerializeField] int size = 256; // longest side in pixels
Texture2D baked;
RenderTexture output;
RTSFogOfWarManager fog;
public RenderTexture Output => output;
void OnEnable()
{
var terrain = RTSTerrain.Main;
if (terrain == null) return;
baked = RTSMinimapBaker.Bake(terrain, size); // map image with cliffs, water, trees ...
if (baked == null) return;
output = new RenderTexture(baked.width, baked.height, 0, RenderTextureFormat.ARGB32);
fog = RTSFogOfWarManager.Active;
if (fog != null) fog.Updated += Refresh; // refresh after each visibility update
Refresh();
}
void OnDisable()
{
if (fog != null) fog.Updated -= Refresh;
if (output != null) { output.Release(); Destroy(output); }
if (baked != null) Destroy(baked);
}
void Refresh()
{
if (baked != null && output != null)
RTSFogOfWarMinimapUtility.Composite(baked, output, fog);
}
}
多人联机提示
- 每个客户端都为自己的 Local Team 运行独立的战争迷雾。迷雾数据从不通过网络发送。
- 在权威服务器上,启用 Compute All Teams,并用
GetVisibilityMask(position)决定哪些客户端接收哪些单位。每个单位查询一次的开销非常低。 - 在服务器和监听服务器主机上,让可隐藏对象的 Hide Colliders 保持关闭,这样服务器端的射线检测仍能命中主机玩家看不到的单位。玩家的选择则改用
IsVisibleToLocalTeam过滤。 - 同盟关系属于本地设置。如果客户端需要显示盟友的视野,需要在服务器和客户端上都调用
SetAlliance。
Mirror 集成会替你完成以上所有工作:RTSNetworkFogOfWar 负责配置管理器(服务器计算所有队伍,客户端只计算自己的队伍),RTSFogOfWarInterestManagement 则从不发送被迷雾隐藏的敌方单位(防全图挂)。参见“Mirror 集成”一章。
性能与限制
- 可见性由 Burst 作业每隔 Update Interval 秒(默认 0.1)计算一次,而不是每帧计算。视野源和可隐藏对象保存在内部列表中,因此不会每帧搜索场景。
- 每个迷雾单元格约占 18 字节内存。一张 128 × 128 的地图在 Resolution Per Cell 为 2 时,迷雾栅格为 256 × 256,约 1.2 MB。
- 大地图:迷雾栅格每个轴最多 2048 个单元格,超出时会自动降低每个单元格的分辨率,以保持在此限制内。
- None 是开销最低的视线模式;CliffLevelsAndBlockers 开销最高,但只会为附近有更高单元格或视野阻挡物的视野源追踪视线。
- 迷雾纹理与地图轴对齐。如果地形经过旋转,会输出一条警告。
- 编辑树木或装饰物后,整张地图的视野阻挡物栅格都会重建;编辑悬崖则只更新发生变化的区域。
迷雾完全没有效果?请检查:管理器已启用,并且你处于 Play 模式(或已开启 Preview In Edit Mode);Reveal All 处于关闭状态;地形上有地图。如果只有地形被迷雾覆盖,单位和道具却没有,就需要添加 RTS Fog Of War 渲染器功能。如果画面全黑,说明本地队伍(或与其共享视野的队伍)没有任何处于激活状态的视野源。Inspector 窗口中的“No revealers exist”提示只会在场景中完全没有视野源时出现,因此还要检查你的视野源的 Team 是否与 Local Team 一致。
通行与寻路
每个 RTS Terrain 都维护一张通行图:这是一个栅格,记录地图上每个小方格内地面单位能否行走、能否放置建筑、空中单位能否飞行,以及是否有水。通行图根据地形自动生成,并在你编辑时随之更新。在此基础上,本包还提供了一个快速的内置寻路器(Burst A*),无需任何第三方包即可使用。
如果你更喜欢使用 A* Pathfinding Project 或 Unity 的 NavMesh,相应的集成会基于同一张通行图构建各自的寻路图;详见相应章节。
通行图
在通行图中,每个地图单元格在每个轴向上划分为 PathingResolution 个通行单元格(1、2 或 4,在创建地图时选择;默认值为 2)。地图单元格默认宽 2 m,此时一个通行单元格宽 1 m。每个通行单元格存储一组 PathingFlags:
| 标记 | 含义 |
|---|---|
Walkable | 地面单位可以在此行走。 |
Buildable | 可以在此放置建筑。 |
Flyable | 空中单位可以在此飞行。所有位置都会设置此标记,除非被 Unflyable 效果移除:该效果可能来自绘制的通行覆盖、图块集的地面图层规则,或装饰物原型的 Footprint Effect。 |
ShallowWater | 浅水:可行走,默认不可建造。 |
DeepWater | 深水:仅供海军单位通行。 |
Cliff | 该单元格与崖壁相接。 |
Blocked | 该单元格被树木或装饰物的占地范围阻挡。 |
Steep | 该单元格过陡,地面单位无法通行。 |
标记的生成方式。规则以 Burst 作业的形式按以下顺序应用:
- 地形。崖壁(按 Cliff Margin 加宽)标记为
Cliff。坡度超过 Max Walkable Slope 的斜坡标记为Steep。水深超过图块集浅水深度上限的水体标记为DeepWater,否则标记为ShallowWater。所有位置都设置Flyable。Buildable= 可行走、坡度低于 Max Buildable Slope 且无水(或者是浅水且开启了 Shallow Water Buildable)。然后应用主导地面图层的通行效果(例如粗糙岩石上的“Unbuildable”)。两个悬崖层级之间的有效坡道只按其精细高度检测,因此仍可行走;坡道永远不可建造。 - 占地范围。树木会阻挡一个圆形区域(半径为树木原型的 Footprint Radius × 实例缩放 × Tree Footprint Scale):它们会设置
Blocked,并移除 Walkable 和 Buildable。装饰物会在圆形或旋转后的矩形区域内应用其原型的 Footprint Effect(默认为 Unwalkable 和 Unbuildable)。带有IgnorePathing标记的装饰物实例会被跳过。 - 用 Pathing 工具(见下文)绘制的手动覆盖。Unwalkable 效果也会移除 Buildable,除非设置了 Force Buildable。
通行图采用增量重建:编辑某个区域时,只会重新推导该区域(外加一小圈边距)。
通行规则(RTSPathingSettings)
选中 RTS Terrain,在其 Inspector 窗口中展开 Pathing Rules 部分。修改任何值都会重建通行图。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Max Walkable Slope | 42 | 坡度超过此角度(单位:度)的地面不可行走。 |
| Max Buildable Slope | 18 | 坡度超过此角度(单位:度)的地面不可建造。 |
| Cliff Margin | 0.25 | 被阻挡区域从崖壁向外延伸的距离(以通行单元格宽度的比例表示)。值越大,单位离悬崖边缘越远。 |
| Shallow Water Buildable | 关 | 允许在浅水中放置建筑。 |
| Tree Footprint Scale | 1 | 应用于每棵树通行占地半径的倍数。 |
| Use Ground Layer Rules | 开 | 应用主导地面图层的通行效果(在图块集中按图层设置,例如粗糙岩石上的“Unbuildable”)。 |
在代码中,可以用 terrain.PathingSettings = settings 指定新的设置(会触发重建),也可以修改当前实例的字段,然后调用 terrain.RebuildPathing()。
手动绘制通行
有时自动规则并不符合你的需求:比如在地图边界设置一道隐形墙、设置禁建区以保持分矿点空闲,或者需要一座可行走的桥。这时可以使用 Pathing 工具(在 Scene 视图中按 7 可选择 Pathing 类别):
| 工具 / 覆盖 | 效果 |
|---|---|
| Paint Pathing: Blocker | 不可行走且不可建造:一道隐形墙(用于地图边界、装饰区域)。 |
| Paint Pathing: Unwalkable | 地面单位无法在此行走。 |
| Paint Pathing: Unbuildable | 无法在此放置建筑(用于保持道路和分矿点空闲)。 |
| Paint Pathing: Unflyable | 空中单位无法在此飞行。 |
| Paint Pathing: Force Walkable | 即使坡度、水体或占地范围会造成阻挡,地面单位也可以在此行走。 |
| Paint Pathing: Force Buildable | 即使推导出的规则禁止,也可以在此放置建筑。 |
| Clear Pathing | 移除笔刷下所有绘制的覆盖,使自动通行规则重新生效。 |
使用 Paint Pathing 时按住 Shift 可移除所选的覆盖。绘制某种模式会移除与之矛盾的覆盖(绘制 Force Walkable 会移除 Unwalkable),因此一个单元格永远不会同时存在相互冲突的规则。
通行叠加层
叠加层会按通行数据为地形着色,类似经典 RTS 地图编辑器中的通行视图。可以通过以下方式开启:
- Scene 视图叠加层中的 Pathing 下拉菜单(Pathing: Off、Walkability、Buildability、Flyability、Water、Combined);
- RTS Terrain Studio 窗口中的 Pathing Overlay 下拉菜单和不透明度滑块;
- 在 Scene 视图中按 P,按 Off → Walkability → Buildability → Combined → Off 的顺序循环切换(快捷键
RTS Terrain Studio/Cycle Pathing Overlay,可在 中重新绑定,macOS 上为 ); - 游戏内编辑器的通行视图。
| 模式 | 颜色 |
|---|---|
| Walkability | 绿色 = 可行走,红色 = 被阻挡,深红色 = 悬崖,橙色 = 过陡,蓝色 = 深水,青色 = 浅水。 |
| Buildability | 绿色 = 可建造,黄色 = 可行走但不可建造,红色 / 深红色 = 不可行走。 |
| Flyability | 浅蓝色 = 可飞行,品红色 = 不可飞行。 |
| Water | 青色 = 浅水,蓝色 = 深水。 |
| Combined | 同时显示所有信息(经典 RTS 风格的通行视图)。 |
在你自己的游戏中,可以用 PathingOverlayController 显示叠加层。通行数据变化时,它会自动更新纹理:
using RTSTerrainStudio;
using UnityEngine;
// Shows the buildability overlay on the terrain while this component is enabled (e.g. in build mode).
public class BuildModeOverlay : MonoBehaviour
{
[SerializeField] RTSTerrain terrain;
[SerializeField, Range(0f, 1f)] float opacity = 0.6f;
PathingOverlayController overlay;
void OnEnable()
{
if (terrain == null) terrain = RTSTerrain.Main;
if (terrain == null) return;
overlay = new PathingOverlayController(terrain, PathingOverlayMode.Buildability);
// The texture can be replaced (for example after a map resize): re-assign it when that happens.
overlay.TextureChanged += texture => terrain.SetOverlayTexture(texture, opacity);
terrain.SetOverlayTexture(overlay.Texture, opacity);
}
void OnDisable()
{
if (terrain != null) terrain.SetOverlayTexture(null, 0f);
overlay?.Dispose(); // destroys the overlay texture
overlay = null;
}
}
用 overlay.Mode = PathingOverlayMode.Walkability 切换显示的信息(会重绘纹理),或用 overlay.Refresh() 强制完整重绘;无需创建新的控制器。
如需一次性生成纹理,可以使用 PathingOverlay.CreateOrUpdate(map, mode, ref texture)、PathingOverlay.UpdateRegion(...) 和 PathingOverlay.DestroyTexture(ref texture);PathingOverlay.GetColor(flags, mode) 返回某个标记组合对应的图例颜色。
通过代码读取通行图
成员(位于 RTSTerrain 上) | 说明 |
|---|---|
PathingFlags GetPathing(Vector3 world) | 某个世界坐标处的标记(在地图外或通行数据尚未构建时为 None)。 |
bool IsWalkable(Vector3 world), bool IsBuildable(Vector3 world) | 两个最常用标记的快捷方法。 |
PathingMap Pathing | 通行栅格:Width、Height、CellSize、Flags(每个单元格一个字节,索引 = z × Width + x)、Get(x, z)、Version。 |
event Action<RTSTerrain, RectInt> PathingChanged | 在完整或部分重建后触发,并传入发生变化的矩形区域(以通行单元格为单位)。重新加载地图后订阅依然有效。示例单位用它来重新规划路径。 |
RTSPathingSettings PathingSettings, void RebuildPathing() | 通行规则,以及完整重建。 |
ClearanceMap GetPathingClearance() | 已更新到最新状态的净空图(参见单位尺寸)。 |
void RegisterPathingReader(JobHandle), void CompletePathingReaders() | 用于你自己编写的、读取 Flags 或净空数据的作业:地形会等这些作业完成后再写入通行数据。 |
查找路径:RTSPathfinder
RTSPathfinder 使用经 Burst 编译的 A* 算法在通行图上搜索路径:支持 8 个方向且不会斜穿拐角,为大型单位考虑净空,并通过拉绳算法(string pulling)让路径更平滑。如果起点或终点被阻挡,会改用最近的空闲单元格。它在主线程上同步运行,适合每帧只有少量请求的情况,也可以在编辑模式下使用。
using System.Collections.Generic;
using RTSTerrainStudio;
using UnityEngine;
public class PathDebug : MonoBehaviour
{
[SerializeField] Transform target;
readonly List<Vector3> path = new List<Vector3>();
void OnDrawGizmos()
{
var terrain = RTSTerrain.Main;
if (terrain == null || target == null) return;
// A unit with a radius of 0.6 m: the path keeps that distance from obstacles.
var options = PathRequestOptions.ForUnit(0.6f);
PathResult result = RTSPathfinder.FindPathDetailed(terrain, transform.position, target.position, options, path);
// Success = reached the goal, Partial = closest reachable point.
Gizmos.color = result.status == PathStatus.Success ? Color.green : Color.yellow;
for (int i = 1; i < path.Count; i++) Gizmos.DrawLine(path[i - 1], path[i]);
}
}
| 方法 | 说明 |
|---|---|
static PathStatus FindPath(RTSTerrain, Vector3 startWorld, Vector3 endWorld, List<Vector3> resultWorld) | 使用 PathRequestOptions.Default。 |
static PathStatus FindPath(RTSTerrain, Vector3, Vector3, PathRequestOptions, List<Vector3>) | resultWorld 会先被清空,然后填入路径点(包括起点和终点)。 |
static PathResult FindPathDetailed(RTSTerrain, Vector3, Vector3, PathRequestOptions, List<Vector3>) | 还会返回路径长度、统计信息,以及起点或终点是否被调整过。 |
static void DisposeSharedBuffers() | 释放搜索缓冲区(域重载和退出时也会自动执行)。 |
PathRequestOptions(以 PathRequestOptions.Default 或 PathRequestOptions.ForUnit(radius) 为基础进行修改):
| 字段 | 默认值 | 说明 |
|---|---|---|
unitRadius | 0 | 单位的半径,以世界单位表示。路径会与障碍物保持这个距离。只有当 required 包含 Walkable 时才会考虑净空:对于海军单位(DeepWater)或空中单位(Flyable)的请求,unitRadius 不起作用。净空图测量的是到不可行走单元格的距离,地图边界也算作障碍物。PathingQueries.FindNearest、LineOfSight 和 Raycast 同样如此。 |
required | Walkable | 路径上每个单元格都必须具备的标记(None 视为 Walkable)。 |
allowShallowWater | true | 允许穿过浅水(浅滩)行走。 |
maxSearchNodes | 500000 | 搜索放弃前最多展开的节点数(0 = 无限制)。 |
smoothPath | true | 利用栅格视线检测移除不必要的路径点(拉绳算法)。 |
snapToGround | true | 将每个路径点的 Y 坐标设为地面高度。 |
allowPartial | true | 终点不可达时,返回一条通往最近可达单元格的路径。 |
goalSearchRadius | 16 | 终点单元格被阻挡时,在此距离(世界单位)内搜索最近的空闲单元格。 |
| PathStatus | 含义 |
|---|---|
Success | 找到了通往终点(或被阻挡终点附近最近空闲单元格)的路径。 |
Partial | 终点不可达;路径通往离终点最近的可达单元格。 |
NoPath | 不存在路径(或达到了搜索上限),且不允许返回部分路径。 |
InvalidStart | 起点在地图之外、没有通行数据,或起点附近没有可通行的单元格。 |
PathResult 的字段:requestId、terrain、status、points(世界坐标路径点)、length(水平长度,世界单位)、searchedNodes、startAdjusted、goalAdjusted、userData,以及 HasPath(状态为 Success 或 Partial 且至少有一个路径点)。
大量单位:RTSPathfindingService
单位数量较多时,请使用 RTSPathfindingService。它会收集一帧内的所有请求,在 LateUpdate 中(地形应用完编辑之后)以并行 Burst 作业的形式执行,并在下一帧的 Update 中通过回调返回结果。可以通过 添加,也可以在 Play 模式下调用 RTSPathfindingService.GetOrCreate()(它会创建一个在场景加载后依然保留的服务对象)。该服务只在 Play 模式下运行;在编辑模式下请使用 RTSPathfinder。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Max Requests Per Frame | 64 | 每帧启动的路径搜索数量。其余请求在队列中等待。 |
| Max Parallel Jobs | 4 | 并行运行的作业数(1 到 16)。每个作业都需要自己的搜索缓冲区(每个通行单元格约 24 字节)。 |
| Memory Budget MB | 256 | 搜索缓冲区内存的上限。在非常大的栅格上会减少并行作业数;如果连一组缓冲区都超出上限,会输出一条警告。 |
| Complete In Same Frame | 关 | 在调度作业的同一帧末尾完成作业(延迟更低,但并行度也更低)。 |
| 成员 | 说明 |
|---|---|
int RequestPath(RTSTerrain, Vector3 startWorld, Vector3 endWorld, PathRequestOptions, Action<PathResult> callback, List<Vector3> resultBuffer = null, object userData = null) | 将请求加入队列并返回其 ID。传入 resultBuffer 时,路径点会写入你的列表(先清空),从而避免每次请求都分配内存。 |
int RequestPath(RTSTerrain, Vector3, Vector3, Action<PathResult>) | 同上,但使用默认选项。 |
bool Cancel(int requestId) | 取消处于排队中、运行中或已完成但结果尚未交付的请求。其回调不会执行。 |
void CancelAll(), void CompleteAll() | 取消全部请求,或立即完成全部请求(回调会在方法返回前执行)。 |
PendingCount, InFlightCount | 队列和作业的统计信息。 |
static Instance, static GetOrCreate() | 第一个已启用的服务,或新建的服务。 |
using RTSTerrainStudio;
using UnityEngine;
// Asks the service for a path and replaces the previous request when a new order arrives.
public class AsyncMover : MonoBehaviour
{
[SerializeField] float radius = 0.5f;
int requestId; // 0 = no request pending
public void OrderMove(Vector3 target)
{
var terrain = RTSTerrain.Main;
var service = RTSPathfindingService.GetOrCreate(); // null outside Play Mode
if (terrain == null || service == null) return;
if (requestId != 0) service.Cancel(requestId); // the old callback will not run
requestId = service.RequestPath(terrain, transform.position, target,
PathRequestOptions.ForUnit(radius), OnPathFound);
}
void OnPathFound(PathResult result)
{
requestId = 0;
if (!result.HasPath) { Debug.Log("Can't reach that place."); return; }
// result.points holds the waypoints. Copy them if you keep them.
Debug.Log($"{result.points.Count} waypoints, {result.length:0.0} m, status {result.status}");
}
}
单位尺寸:净空
大型单位不能挤过专为小型单位留出的缝隙。为此,地形会维护一张净空图:记录每个可行走单元格到最近不可行走单元格的距离(上限为 64 个单元格)。当请求带有 unitRadius 时,寻路器只会使用能够容纳该单位的单元格。编辑后,净空图会增量更新。
PathingQueries.GetClearance(terrain, world)返回某个位置的空闲半径,以世界单位表示(在不可行走单元格上为负值)。ClearanceMap(通过terrain.GetPathingClearance()获取)提供GetClearanceCells(x, z)、GetClearanceMetres(x, z)和ClearanceMap.RequiredValue(unitRadius, cellSize)。在斜向障碍物的拐角附近,该值最多可能偏大 0.13 个单元格左右;这样,半径为 0.3 到 0.5 个单元格的单位仍能穿过物理上足以容纳它们的单格宽通道。
游戏玩法查询:PathingQueries
PathingQueries 用来回答 RTS 游戏玩法中经常遇到的问题。所有位置均为世界空间坐标,尺寸和半径均以世界单位表示。
| 方法 | 说明 |
|---|---|
bool FindNearestWalkable(RTSTerrain, Vector3 world, float maxRadius, float unitRadius, out Vector3 result) | 离 world 最近、且能容纳半径为 unitRadius 的单位的可行走位置(用于生成点、集结点)。 |
bool FindNearest(RTSTerrain, Vector3 world, float maxRadius, float unitRadius, PathingFlags required, out Vector3 result) | 同上,但适用于任意标记,例如为船只使用 DeepWater。 |
Vector3 SnapToBuildGrid(RTSTerrain, Vector3 world, Vector2 sizeMetres) | 对齐建筑位置,使其占地范围覆盖完整的通行单元格(偶数尺寸对齐单元格角点,奇数尺寸对齐单元格中心)。结果位于地面上。 |
bool IsAreaBuildable(RTSTerrain, Vector3 worldCenter, Vector2 sizeMetres) | 当占地范围内每个通行单元格都为 Buildable 时返回 true。 |
bool IsAreaClear(RTSTerrain, Vector3 worldCenter, Vector2 sizeMetres, PathingFlags required) | 同上,但适用于任意标记。在地图外返回 false。 |
bool TryGetAreaCells(RTSTerrain, Vector3 worldCenter, Vector2 sizeMetres, out RectInt cells) | 获取占地范围覆盖的通行单元格。占地范围未完全位于地图内时返回 false。 |
float GetClearance(RTSTerrain, Vector3 world) | 某个位置周围的空闲半径。 |
bool LineOfSight(RTSTerrain, Vector3 fromWorld, Vector3 toWorld, PathingFlags required = Walkable, float unitRadius = 0) | 可以沿直线行走时返回 true(栅格视线检测,不斜穿拐角)。这里指的是移动,与战争迷雾的视野无关。 |
bool Raycast(RTSTerrain, Vector3 fromWorld, Vector3 toWorld, PathingFlags required, float unitRadius, out Vector3 lastFreeWorld) | 沿线段前进,并返回遇到障碍物前的最后一个空闲位置(用于冲锋、击退)。 |
建筑放置与运行时通行变更
经典 RTS 的建筑放置分三步:将占地范围对齐栅格、检查该范围,然后在通行图上阻挡它。阻挡的方法是向 MapData.PathingOverrides 写入手动覆盖值,并通知地形。地形会增量重建该区域并触发 PathingChanged,正在移动的单位随即绕开新建筑重新规划路径。
using RTSTerrainStudio;
using UnityEngine;
public static class BuildingFootprint
{
// Tries to place a footprint of sizeMetres (X/Z) at the cursor and blocks it for walking and building.
// Returns the covered pathing cells and their previous override bytes, so Remove can restore them
// (cells may already hold painted overrides, for example Force Walkable on a bridge).
public static bool TryPlace(RTSTerrain terrain, Vector3 cursorWorld, Vector2 sizeMetres,
out RectInt pathingCells, out byte[] previous)
{
pathingCells = default;
previous = null;
if (terrain == null || terrain.Map == null) return false;
// 1. Align the footprint with the pathing grid.
Vector3 center = PathingQueries.SnapToBuildGrid(terrain, cursorWorld, sizeMetres);
// 2. Every cell must be buildable.
if (!PathingQueries.IsAreaBuildable(terrain, center, sizeMetres)) return false;
if (!PathingQueries.TryGetAreaCells(terrain, center, sizeMetres, out pathingCells)) return false;
// 3. Keep the old bytes, then write Unwalkable | Unbuildable overrides (one byte per pathing cell).
MapData map = terrain.Map;
var overrides = map.PathingOverrides;
previous = new byte[pathingCells.width * pathingCells.height];
byte block = (byte)(PathingOverride.Unwalkable | PathingOverride.Unbuildable);
int n = 0;
for (int z = pathingCells.yMin; z < pathingCells.yMax; z++)
for (int x = pathingCells.xMin; x < pathingCells.xMax; x++, n++)
{
int i = map.PathingIndex(x, z);
previous[n] = overrides[i];
overrides[i] = (byte)(overrides[i] | block);
}
Notify(terrain, pathingCells);
return true;
}
// Restores the override bytes stored by TryPlace (call it when the building is destroyed).
public static void Remove(RTSTerrain terrain, RectInt pathingCells, byte[] previous)
{
if (terrain == null || terrain.Map == null || previous == null) return;
MapData map = terrain.Map;
var overrides = map.PathingOverrides;
int n = 0;
for (int z = pathingCells.yMin; z < pathingCells.yMax; z++)
for (int x = pathingCells.xMin; x < pathingCells.xMax; x++, n++)
overrides[map.PathingIndex(x, z)] = previous[n];
Notify(terrain, pathingCells);
}
// 4. NotifyChanged expects map cells, so convert the pathing rectangle.
static void Notify(RTSTerrain terrain, RectInt pathingCells)
{
int res = Mathf.Max(1, terrain.Map.PathingResolution);
int xMin = pathingCells.xMin / res, zMin = pathingCells.yMin / res;
int xMax = Mathf.CeilToInt(pathingCells.xMax / (float)res);
int zMax = Mathf.CeilToInt(pathingCells.yMax / (float)res);
terrain.NotifyChanged(MapLayers.PathingOverrides, new RectInt(xMin, zMin, xMax - xMin, zMax - zMin));
}
}
移除建筑时,将保存的单元格和字节传给 BuildingFootprint.Remove:它会写回之前的覆盖字节,并再次调用 NotifyChanged。示例 RTSDemoBuilding 正是这样做的,参见建筑放置演示。
通过植被渲染器移除树木也是同样的原理:RTSVegetationRenderer.RemoveTree(id) 会通知地形,因此通行图会自动将这棵树的占地范围恢复为可通行,战争迷雾也会自动移除对应的视野阻挡物。参见树木采伐演示。
进阶:在自己的作业中使用通行数据
对于自定义 Burst 作业(流场、影响力图),可以用 new PathingGrid(terrain.Pathing, terrain.GetPathingClearance()) 创建一个只读视图。它以通行单元格为单位提供 IsPassable、LineOfSight、Trace 和 FindNearestPassable,并按 PathingFilter(PathingFilter.ForUnit(required, allowShallowWater, unitRadius, cellSize))过滤结果。用 terrain.RegisterPathingReader(handle) 注册你的作业句柄,这样地形会在修改通行数据之前先完成该作业。
PathingMapBuilder 可为不由地形显示的 MapData 对象构建通行图(例如只加载地图、不做渲染的服务器):PathingMapBuilder.Build(map, pathing, settings, tileset, vegetation, decor)。
游戏玩法示例脚本
Samples/Scripts 文件夹包含一个小巧但完整的 RTS 游戏层:RTS 摄像机、贴合地形移动的单位、支持编队的选择功能、带阵型的移动命令、小地图、HUD,以及两个在运行时修改地图的演示。它们位于程序集 RTSTerrainStudio.Samples 和命名空间 RTSTerrainStudio.Samples 中。
你可以用它们立即试玩自己的地图,也可以把它们作为自己游戏的起点,或作为运行时 API 用法的参考。每个脚本都带有注释,并在合适的地方将 Unity 消息方法声明为 protected virtual,因此你可以继承它们。
所有示例组件都位于 下。输入通过 RTSInput 读取,因此示例既支持 Input System 包,也支持旧版 Input Manager。UI 使用 UI Toolkit 构建。
快速入门:让任意地图变得可玩
- 打开一个包含 RTS Terrain 的场景,并确保其地图带有出生点(用 Marker 工具放置)。
- 选择 。这会创建一个带有
RTSDemoGame组件的RTS Demo Game对象。 - 按下 Play。演示游戏会创建战争迷雾、摄像机控制器、选择系统、命令系统、小地图和 HUD,在每个出生点生成单位(你是玩家 1,其他玩家的单位会四处游荡),并将摄像机移到你的基地。
- 按 F1 显示或隐藏操作说明。
默认操作
| 输入 | 操作 |
|---|---|
| W A S D / 方向键 | 平移摄像机。选中单位时,S 会让单位停止,而不会平移摄像机(方向键始终用于平移)。 |
| 屏幕边缘、按住鼠标中键拖动 | 平移摄像机 / 抓住并拖动地面。 |
| 鼠标滚轮 | 缩放(拉得越近,视角越平)。 |
| Q / E、Alt + 右键拖动 | 旋转视角。 |
| 左键单击 / 拖动 | 选择单个单位 / 拖出选择框。 |
| Shift + 单击 | 将单位加入或移出当前选择。 |
| 双击、Ctrl + 单击 | 选择屏幕上所有同类型的单位。 |
| Esc | 取消当前模式,然后清除选择。在游戏内编辑器中试玩时,再按一次 Esc 会返回编辑器。 |
| Ctrl+1…9、0 | 保存编队(Shift+数字键可向编队中添加单位)。 |
| 1…9、0 | 调出编队(按两次可将摄像机居中到该编队)。 |
| 右键单击 | 以阵型移动(按住 Shift 可将命令排入队列,阵型会依次衔接)。 |
| S / H / P | 停止 / 原地驻守 / 巡逻(之后左键单击第二个巡逻点)。 |
| 小地图 | 左键单击或拖动:移动摄像机。右键单击:移动选中的单位。 |
| B | 放置建筑(在运行时改变通行数据)。 |
| Delete | 移除光标下的建筑。 |
| T | 砍倒光标下的树木。 |
| 右键单击树木 | 选中的工人会走过去将其砍倒。 |
| F1 | 显示或隐藏帮助面板。 |
所有按键都是各组件(摄像机、选择、命令、演示、HUD)上的 RTSKey 字段,因此你可以在 Inspector 窗口中重新绑定它们。
RTSDemoGame:游戏引导组件
该组件可将任何包含 RTS Terrain 的场景变成一个可玩的小型 RTS 游戏。游戏开始时,它会:
- 确保存在
RTSFogOfWarManager(本地队伍 = Local Team,并重置探索状态)和寻路服务; - 查找摄像机控制器、选择系统、命令系统、小地图、HUD 以及建筑和树木演示,并创建缺失的部分;
- 在每个出生点生成单位(队伍 = 玩家索引;玩家 1 为队伍 0),并为敌方单位添加
RTSWanderAI,让你可以看到它们在迷雾中出现和消失; - 将摄像机移到本地玩家的出生点。
游戏内编辑器:该组件会监听 RTSRuntimeEditorEvents.PlaytestChanged。在游戏内编辑器中开始试玩会启动游戏;结束试玩则会移除单位、删除演示建筑、恢复被采伐的树木(可选)、禁用游戏玩法系统,并交还摄像机控制权。如果开启了 Auto Start 且场景中没有游戏内编辑器,游戏会立即开始。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Start | ||
| Auto Start | 开 | 场景运行且不存在游戏内编辑器时自动开始(否则游戏会随编辑器的试玩一起开始)。 |
| Local Team | 0 | 本地玩家的队伍(0 = 玩家 1)。通过代码设置时,也会同步更新战争迷雾和选择系统。 |
| Terrain | 空 | 用于游戏的地形。留空时使用 RTSTerrain.Main。 |
| Units | ||
| Unit Prefabs | 空 | 单位预制件,依次轮流使用。留空时使用程序化生成的占位单位。 |
| Units Per Player | 8 | 为每个玩家生成的单位数量(1 到 60)。 |
| Max Players | 8 | 获得单位的最大玩家数。 |
| Enemy Wander AI | 开 | 敌方单位四处游荡。 |
| Wander Radius | 30 | 敌方 AI 的游荡半径(米)。 |
| Systems | ||
| Create Missing Systems | 开 | 缺少摄像机控制器、选择系统、命令系统、小地图、HUD 和演示时自动创建它们。单位生成器则始终会被创建。 |
| Fog Of War | 开 | 游戏进行时启用战争迷雾。 |
| Minimap | 开 | 显示小地图。 |
| Hud | 开 | 显示 HUD(信息面板、操作栏、F1 帮助)。 |
| Show Help On Start | 开 | 游戏开始时显示帮助面板。 |
| Building Demo | 开 | 启用建筑放置演示(B)。 |
| Tree Harvest Demo | 开 | 启用树木采伐演示(T、工人)。 |
| Restore Map On Stop | 开 | 游戏停止时恢复被采伐的树木,使编辑中的地图在试玩后保持不变。 |
| References | ||
| Camera Controller, Selection Manager, Command Controller, Minimap UI, Demo HUD, Spawner, Building Placement, Tree Harvest, Fog Of War Manager | 空 | 要使用的系统。留空时会在场景中查找或自动创建。 |
API:StartGame()、StopGame()、IsRunning、AutoStart、LocalTeam,事件 GameStarted 和 GameStopped,以及属性 Spawner、CameraController、SelectionManager、CommandController。如果你使用自己的游戏内编辑器,则需重写 protected virtual bool DetectInGameEditor()。
正在开发自己的游戏?把 RTSDemoGame 当作参考示例,而不是依赖项:将它的初始化步骤顺序照搬到你自己的游戏管理器中,并去掉不需要的部分。使用 Mirror 进行多人联机时,不要添加 RTSDemoGame:单位由网络管理器生成。
RTSCameraController
经典的 RTS 摄像机,支持键盘、屏幕边缘和拖动平移,带俯仰角曲线的缩放(拉得越近,视角越平)、旋转、平滑的地面高度跟随以及地图边界限制。它在 LateUpdate 中运行。RTSCameraController.Main 是第一个启用的控制器。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Target Camera | 空 | 要移动的摄像机。留空时使用此 GameObject 上的 Camera,若没有则使用 Camera.main。 |
| Terrain | 空 | 用于获取地面高度和边界的地形。留空时使用 RTSTerrain.Main。 |
| Keyboard Pan, Pan Forward/Back/Left/Right Key, Arrow Keys | 开,W/S/A/D,开 | 键盘平移。 |
| Yield Keys To Orders | 开 | 如果某个平移键同时也是命令控制器的命令键(S = Stop),则选中单位时它不会平移摄像机。 |
| Pan Speed / Fast Pan Multiplier | 35 / 2 | 默认缩放下每秒移动的米数(随缩放距离等比变化);按住 Shift 时的速度倍数。 |
| Edge Scrolling, Edge Size, Edge Speed Multiplier | 开,12 px,1 | 鼠标触及屏幕边缘时平移。当游戏窗口没有焦点、指针位于窗口之外(在 Web 构建版本中,还包括鼠标离开页面或其 iframe 时)以及在触摸屏上时,边缘滚屏会暂停。 |
| Middle Mouse Drag | 开 | 按住鼠标中键可抓住并移动地面。 |
| Min / Max Distance | 12 / 75 | 完全拉近 / 拉远时与轴心点的距离(米)。 |
| Zoom Step, Start Zoom | 0.08, 0.6 | 滚轮每格的缩放变化量;初始缩放(0 = 最近,1 = 最远)。 |
| Distance Curve, Pitch Curve | - | 将缩放值(0 到 1)映射为距离和俯仰角(单位为度,默认缩放下约 55°)。 |
| Keyboard Rotate, Rotate Left/Right Key, Rotate Speed | 开,Q/E,100 | 键盘旋转,速度单位为度/秒。 |
| Alt Right Drag Rotate, Drag Rotate Sensitivity | 开,0.3 | 使用 Alt + 右键拖动旋转。 |
| Snap Back Rotation, Snap Back Delay, Default Yaw | 关,0.25,0 | 没有旋转输入时回到默认角度(经典 RTS 的行为)。偏航角为 0 时朝向 +Z(北)。 |
| Follow Ground Height, Height Smooth Time | 开,0.3 | 让轴心点保持在地面高度,并做平滑处理,避免悬崖导致视角跳动。 |
| Clamp To Terrain, Bounds Margin | 开,4 | 将轴心点限制在地图内(负边距允许看到地图边缘以外)。 |
| Min Camera Clearance | 2 | 摄像机与其正下方地面之间的最小高度。 |
| Pan / Zoom / Rotation Smooth Time | 0.08 / 0.12 / 0.08 | 平滑时间,单位为秒(0 = 立即)。 |
| Ignore Mouse Over UI | 开 | 指针位于 UI 上方时,忽略滚轮、边缘滚屏和拖动。 |
| Use Unscaled Time | 开 | 游戏暂停时摄像机仍可移动。 |
API:JumpTo(Vector3)、FocusOn(Vector3)、FocusOn(IReadOnlyList<RTSUnit>, bool instant = false)、SetZoom(float, bool instant = false)、SetYaw(float, bool instant = false)、GetViewFootprint(Vector3[4])(屏幕四角在地面上的投影点,供小地图使用)、InitializeFromCamera()、InputEnabled,以及只读属性 Pivot、Zoom、Yaw、Pitch、Distance、IsDragging。
RTSUnit 与单位辅助类
一个简单的 RTS 单位,具备队伍归属、地形上的移动、带平滑转角的路径跟随、局部避让和战争迷雾视野。它向寻路提供器请求路径,借助前视点沿路径移动,到达时减速,与其他单位保持距离,绝不会踏上不可行走的单元格;当它被卡住,或者其路径下方的通行图发生变化(放置了建筑、砍倒了树木、在游戏内编辑器中编辑了地形)时,它会重新规划路径。它依赖并会自动配置 RTSFogOfWarRevealer(队伍、视野半径)和 RTSFogOfWarHideable(队伍)。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Team | 0 | 队伍 / 玩家索引(0 = 玩家 1)。本地队伍的单位可以被选择和指挥。 |
| Use Team Color, Player Color | 开,红色 | 每当队伍改变时,从经典玩家颜色表中取色;用于选择圈、小地图圆点和队伍颜色渲染器。 |
| Unit Type | "Unit" | 用于“双击选择所有同类型单位”的类型名称(例如 Footman、Worker)。树木演示使用“Worker”。 |
| Move Speed, Turn Speed, Acceleration | 5, 540, 25 | 米/秒、度/秒、米/秒²。 |
| Radius, Height | 0.5, 1.8 | 碰撞半径(用于寻路净空、避让、选择圈)和高度(用于拾取、选择框)。 |
| Align To Ground, Ground Align Speed | 关,8 | 随地面倾斜(适用于载具、动物)。关闭时步兵保持直立。 |
| Look Ahead, Arrival Distance, Slow Down Distance | 1.5, 0.15, 1.5 | 路径跟随参数(米)。 |
| Stuck Check Interval | 0.6 | 两次进度检查之间的秒数。没有进展的单位会重新规划路径。 |
| Path Request Timeout | 3 | 等待路径的秒数,超时后会重新发送一次请求;如果第二次也超时,则移动失败。 |
| Avoidance, Separation Strength, Personal Space | 开,8,0.1 | 简单的分离避让。空闲单位会为移动中的单位让路。 |
| Sight Radius | 14 | 战争迷雾中的视野半径(米)。 |
| Selectable | 开 | 玩家可以选择此单位。 |
| Terrain | 空 | 单位所行走的地形。留空时使用单位下方的地形(或 RTSTerrain.Main)。 |
| Path Provider | 空 | 可选组件,需实现 IRTSUnitPathProvider。留空时使用全局默认提供器(参见寻路提供器)。 |
| Team Color Renderers, Team Color Mode | 无,PropertyBlock | 显示玩家颜色的渲染器,可通过 MaterialPropertyBlock(适用于任意材质)或每种颜色共享一个材质(合批效果最佳)来实现。 |
| Draw Path Gizmos | 开 | 在 Scene 视图中绘制当前路径。 |
命令:MoveTo(Vector3, bool queue = false)、Stop()、HoldPosition()、Patrol(Vector3 a, Vector3 b)、Warp(Vector3, float? yaw = null)、FaceTowards(Vector3)、Repath()。事件:OnArrived、OnPathFailed(没有路径、受阻后放弃,或停在了最近的可到达点)、OnStateChanged。状态:State(Idle、Moving、Patrolling、Holding)、IsMoving、IsWaitingForPath、IsPathPartial、LastMoveReachedDestination、Destination、CurrentPath、QueuedDestinations、PlannedEndPosition、Velocity、Position、Yaw、IsSelected、IsVisibleToLocalTeam。
using RTSTerrainStudio;
using RTSTerrainStudio.Samples;
using UnityEngine;
// Spawns a knight for player 2 and listens to its movement events.
public class KnightSpawner : MonoBehaviour
{
[SerializeField] Transform target;
void Start()
{
var terrain = RTSTerrain.Main;
// Nearest walkable spot where a unit of radius 0.75 m fits.
Vector3 position = RTSUnitFactory.FindSpawnPosition(terrain, transform.position, 0.75f);
RTSUnit knight = RTSUnitFactory.CreatePlaceholder(1, position, 180f, RTSUnitFactory.Knight);
knight.OnArrived += unit => Debug.Log(unit.name + " arrived.");
knight.OnPathFailed += unit => Debug.Log(unit.name + " can't reach that place.");
if (target != null) knight.MoveTo(target.position);
}
}
辅助类:
| 类 | 用途 |
|---|---|
RTSUnitFactory | 程序化占位单位:样式包括 Footman、Worker、Scout、Knight 和 DefaultMix;CreatePlaceholder(team, position, yaw, style, parent)、Spawn(prefab, team, position, yaw, parent)(预制件缺少 RTSUnit 时会自动添加)、FindSpawnPosition(terrain, position, radius, searchRadius = 16)。 |
RTSUnitSpawner | 在地图的出生点为每个玩家生成单位(也可以在 Custom Spawn Points 处生成,或在地图的对角生成)。设置项:Unit Prefabs、Units Per Player(6)、Mix Placeholder Types、Terrain、Use Start Locations、Custom Spawn Points、Max Players、Spacing(2 m)、Start Offset(5 m,为主城预留空间)、Container、Spawn On Start。API:SpawnAll()、SpawnForPlayer(...)、SpawnUnit(team, position, yaw, variant)、DespawnAll()、TryGetStartLocation(player, out position, out yaw)。 |
RTSWanderAI | 让单位在其驻地周围随机走动。设置项:Wander Radius(25 m)、Min/Max Wait(2 / 7 s)、Home Is Start Position、Home Position。 |
RTSUnitRegistry | 管理所有单位并提供空间哈希:All、GetTeam(team)、QueryRadius(...)、QueryScreenRect(...)、PickAtScreen(...),事件 UnitRegistered、UnitUnregistered、UnitTeamChanged。 |
寻路提供器:IRTSUnitPathProvider
单位不绑定于某一种寻路方案。它们会向 IRTSUnitPathProvider 请求路径,并按以下顺序选择提供器:单位自身的 Path Provider(或 PathProviderOverride);若没有,则使用 RTSUnitPathProviderRegistry.Default;再没有,则使用内置提供器 RTSBuiltinPathProvider(即本包的 Burst A*,在 Play 模式下通过 RTSPathfindingService 运行)。A* Pathfinding Project 和 NavMesh 集成就是在这里接入的。
提供器分为两类:
- 寻路提供器(
HandlesMovement= false)通过RequestPath返回路径点;单位自行沿路径点移动(转向、避让、贴合地面)。 - 移动提供器(
HandlesMovement= true)自行移动单位(例如 NavMeshAgent)。单位通过OnUnitMoveCommand和OnUnitStop转发命令;提供器通过unit.NotifyArrived()或unit.NotifyPathFailed()报告结果。当此类提供器负责某个单位时,该单位绝不会写入自身的 Transform。
约定:必须在主线程上恰好调用一次 onComplete 回调,可以立即调用,也可以稍后调用;成功时传入路径点(包括起点和终点)和 true,失败时传入空列表或 null 以及 false。返回一条通往最近可到达点的路径并传入 true 也没有问题,单位会检测到路径未到达目标。接收了某次移动的提供器会一直负责到该移动结束;如果在移动过程中提供器发生更换或被销毁,旧提供器会收到 OnUnitStop,命令则转交给新提供器。在 Path Request Timeout 内未得到应答的请求会重试一次,之后判定为失败。
继承 RTSUnitPathProviderComponent 可以创建一个场景组件,它既可以将自己注册为默认提供器(Make Default,默认开启),也可以被拖入单位的 Path Provider 字段:
using System;
using System.Collections.Generic;
using RTSTerrainStudio;
using RTSTerrainStudio.Samples;
using UnityEngine;
// A path provider for heavy units that must never cross water (not even shallow fords).
// Turn off "Make Default" and drag it into the Path Provider field of the units that should use it.
public class NoWaterPathProvider : RTSUnitPathProviderComponent
{
[SerializeField] int maxSearchNodes = 200000;
readonly List<Vector3> path = new List<Vector3>(64);
// The unit follows the waypoints itself.
public override bool HandlesMovement => false;
public override void RequestPath(RTSUnit unit, Vector3 from, Vector3 to, Action<List<Vector3>, bool> onComplete)
{
RTSTerrain terrain = unit != null ? unit.Terrain : RTSTerrain.Main;
if (terrain == null) { onComplete(null, false); return; }
var options = PathRequestOptions.ForUnit(unit != null ? unit.Radius : 0.5f);
options.allowShallowWater = false;
options.maxSearchNodes = maxSearchNodes;
// Synchronous search; the list is reused for the next request, the unit copies what it keeps.
PathStatus status = RTSPathfinder.FindPath(terrain, from, to, options, path);
onComplete(path, status == PathStatus.Success || status == PathStatus.Partial);
}
}
通过代码:RTSUnitPathProviderRegistry.Default = myProvider; 会让所有没有自己提供器的单位切换到该提供器;Default = null 则恢复使用内置寻路器。RTSBuiltinPathProvider 公开了 Options(其中的 unitRadius 会被每个单位自己的半径替换)和 UseAsyncService。
RTSSelectionManager
支持单击、框选、Shift 和双击选择,以及编队、悬停圈和选择圈。只有本地队伍的单位可以被选中。RTSSelectionManager.Main 是第一个启用的管理器。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Target Camera | 空 | 用于拾取的摄像机。留空时使用 Camera.main。 |
| Use Fog Of War Local Team | 开 | 使用当前活动的战争迷雾管理器中的本地队伍。 |
| Local Team | 0 | 其单位可被选中的队伍(不从战争迷雾获取时使用)。 |
| Drag Threshold, Double Click Time | 6 px, 0.3 s | 单击变为框选的拖动阈值;双击的时间间隔。 |
| Max Selection | 0 | 最多可选中的单位数量(0 = 无限制;经典 RTS 游戏中常见的上限是 12)。 |
| Pick Tolerance | 6 px | 额外的拾取半径,让小型单位更容易点中。 |
| Keep Selection On Empty Click | 开 | 单击空地或框选范围内没有己方单位时保留当前选择,与经典 RTS 游戏一致。 |
| Clear Selection Key | Escape | 清除选择。 |
| Control Groups, Group Double Tap Time | 开,0.35 s | Ctrl+数字键保存编队,Shift+数字键添加到编队,数字键调出编队,连按两次将摄像机居中。共有十个编队(RTSSelectionManager.GroupCount = 10),对应按键 1–9 和 0;StoreGroup、RecallGroup 和 GetGroup 的编队索引 i 对应数字键 i(索引 0 即按键 0)。 |
| Show Selection Circles, Show Hover, Use Team Color For Circles, Selection Color, Circle Scale, Circle Width, Box Color | - | 各类圆环和选择框的外观。 |
API:Selection、Count、Select(unit, additive)、Deselect、ToggleSelection、SetSelection(units, additive)、ClearSelection()、SelectAllOfTypeOnScreen(unit, additive)、GetSelectionCenter()、StoreGroup(i)、AddToGroup(i)、RecallGroup(i)、GetGroup(i)、ClearGroups()、HoveredUnit,事件 SelectionChanged 和 GroupRecalled。
RTSCommandController 与阵型
将右键单击和命令键转换为对选中单位的命令。RTSCommandController.Main 是第一个启用的控制器。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Selection, Target Camera | 空 | 留空时使用 RTSSelectionManager.Main / Camera.main。 |
| Stop Key, Hold Key, Patrol Key, Cancel Key | S, H, P, Escape | 命令按键。 |
| Use Formation | 开 | 让单位群按栅格阵型移动(关闭时每个单位都走向单击的位置)。 |
| Spacing Multiplier, Extra Spacing, Width To Depth | 1.15, 0.3, 1.5 | 阵位间距(最大单位直径的倍数)、额外间距(米)、首选的列数与行数之比。 |
| Show Marker, Move Marker Color, Patrol Marker Color | 开 | 在命令目标处显示的动画圆环。 |
API:IssueMove(Vector3 target, bool queue = false)、IssueMove(IReadOnlyList<RTSUnit> units, Vector3 target, bool queue = false)、IssuePatrol(Vector3)、IssueStop()、IssueHold()、BeginPatrolTargeting()、CancelTargeting()、IsOrderKeyActive(RTSKey),事件 MoveIssued、CommandIssued、TargetingChanged。
RTSFormation.ComputeGrid(units, target, slots, spacingMultiplier, extraSpacing, widthToDepth, fromPlannedEnd) 为每个单位计算一个阵位,并把最接近该单位在群组中相对位置的阵位分配给它,因此路径很少交叉。它返回阵型的朝向。启用 fromPlannedEnd 时(用于按住 Shift 排队的命令),阵型会从单位执行完当前命令后所处的位置继续排列。
输入协调。RTSGameInput 确保每次单击只由一个系统处理:建筑放置等模式会占用指针(Acquire、Release、CanUsePointer),处理了某次单击或按键的系统会调用 Consume / ConsumeKey。示例按固定顺序运行(建筑放置、树木采伐、命令、选择、单位、摄像机、UI),该顺序由 RTSGameInput.Order… 常量定义。在你自己的模式中使用相同的机制,即可与示例协同工作。
RTSMinimapUI
运行中游戏使用的 UI Toolkit 小地图。地图图像由 RTSMinimapBaker 烘焙,地图发生变化时会重新烘焙(有频率限制);本地队伍的战争迷雾叠加在图像上方;单位以所属玩家颜色的圆点显示(敌方单位仅在可见时显示);摄像机的视野范围以轮廓框显示。左键单击或拖动可移动摄像机,右键单击则命令选中的单位移动到该处。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Terrain, Camera Controller, Command Controller | 空 | 留空时分别使用 RTSTerrain.Main、RTSCameraController.Main 和 RTSCommandController.Main。 |
| Corner | BottomLeft | 小地图所在的屏幕角落(经典 RTS 游戏使用左下角)。 |
| Size, Margin | 248, 16 | 小地图框的尺寸及其与屏幕边缘的距离,单位为 UI 像素(参考分辨率 1920 × 1080)。 |
| Resolution | 256 | 烘焙地图图像的分辨率(以最长边计)。 |
| Rebake Delay, Min Rebake Interval | 0.35 s, 1 s | 将多次编辑合并为一次烘焙。 |
| Bake Options | - | 烘焙图像的外观(地面模式、山体阴影、悬崖、水体、树木、装饰物、出生点、玩家颜色)。 |
| Show Fog, Fog Refresh Interval | 开,0.1 s | 像 3D 视图一样,将未探索区域和被迷雾覆盖的区域调暗。 |
| Show Units, Unit Dot Size, Show Camera View, Camera View Color, Overlay Rate | 开,4,开,白色,20 | 单位圆点、摄像机轮廓框及其重绘频率。 |
| Left Click Moves Camera, Right Click Moves Units | 开,开 | 交互方式。 |
API:Visible、Terrain、MapTexture、BakeOptions、Rebake()。
RTSDemoHUD 与 RTSRuntimeUI
RTSDemoHUD 显示选中对象的信息面板、操作栏(Stop、Hold、Patrol、Build)、帧率、简短的反馈消息(“Can't reach that place.”、“Can't build there.”、“Can't build in unexplored land.”)以及 F1 帮助面板。设置项:Help Key(F1)、Show Help On Start、Show Fps、Show Feedback Messages、Message Duration(2.5 s),以及对选择管理器、命令控制器和各演示组件的引用。API:Visible、HelpVisible、SetMode(string)、ShowMessage(string)。该组件启用期间会开启 RTSInput.SetBrowserShortcutGuard,因此在 Web 构建中按 F1 会打开 HUD 帮助,而不是浏览器的帮助页面。该保护机制还会拦截浏览器对直至 F10 的其他功能键(包括用于刷新页面的 F5)以及 Ctrl+S/O/D/A 的默认操作(参见浏览器中的游戏内编辑器)。在其他平台上,它不起任何作用。
RTSRuntimeUI 是各示例共用的小型 UI Toolkit 工具集:CreateDocument、CreatePanel、CreateLabel、CreateButton(text, tooltip, action)、AttachTooltip、IsScreenPointOverUI 以及示例的配色方案。用它可以添加外观一致的自定义面板。
所有示例 UI 共用同一个面板(RTSRuntimeUI.PanelSettings),该面板随屏幕尺寸缩放(参考分辨率为 1920 × 1080)。与游戏内编辑器一样,它的缩放比例不会低于 RTSRuntimeUI.MinimumScale(默认值 0.85;在 Web 构建中以 CSS 像素计算),因此 HUD、小地图和选择界面在较小的窗口和浏览器画布中依然清晰可读。大屏幕不受影响。将该值设为 0 即可取消这一下限。
RTSBuildingPlacementDemo
演示游戏代码如何在运行时修改通行图。按 B 后,占地范围的半透明预览会跟随光标并对齐通行栅格,每个单元格显示为绿色(可建造)或红色(被阻挡)。左键单击会放置一座队伍颜色的简易建筑(按住 Shift 可连续放置多座)。占地范围会写入通行覆盖,地形随即重建该区域,正在移动的单位会重新规划路线绕开建筑。在被阻挡的位置单击会显示一个红色标识和一条消息。右键单击或按 Esc 取消;按 Delete 会移除光标下的建筑并恢复通行数据。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Size In Cells | 2 × 2 | 以地图单元格为单位的占地尺寸(X × Z)。 |
| Building Height | 3 | 程序化建筑的高度,以米为单位。 |
| Building Prefab | 空 | 可选的预制件,用于替代程序化生成的建筑。其轴心放置在占地范围中心的地面上。 |
| Team | -1 | 所属队伍。-1 = 选择管理器(或战争迷雾)的本地队伍。 |
| Sight Radius | 10 | 建筑的视野半径(0 = 无视野)。建筑还会带有一个启用残影模式的可隐藏对象组件。 |
| Footprint Flags | Unwalkable, Unbuildable | 为占地范围写入的通行覆盖。 |
| Push Units Out | 开 | 站在占地范围内的单位会走开让出位置。 |
| Require Explored | 开 | 只允许在本地队伍已探索的地面上建造(仅在战争迷雾运行时生效)。 |
| Place Key, Cancel Key, Demolish Key | B, Escape, Delete | 按键。 |
| Valid Color, Invalid Color | 绿色,红色 | 占地单元格的颜色。 |
| Terrain, Target Camera | 空 | 留空时分别使用光标下的地形 / Camera.main。 |
API:BeginPlacement()、CancelPlacement()、IsPlacing、GetFootprintSize(terrain)、CanPlaceAt(terrain, worldCenter)、GetPlacementError(terrain, worldCenter)(返回给玩家看的消息,或 null)、PlaceBuilding(terrain, worldCenter, owner)、RemoveBuilding(building)、RemoveAllBuildings()、Buildings,以及事件 BuildingPlaced、BuildingRemoved、PlacementModeChanged、PlacementFailed(Vector3, string)。每座已放置的建筑都带有一个 RTSDemoBuilding 组件(Team、PathingCells、ContainsPoint、Remove()),它会保存之前的通行覆盖,并在建筑被移除或销毁时恢复这些覆盖。
要把它变成真正的建筑系统:将你的预制件指定给 Building Prefab;订阅 BuildingPlaced,在其中扣除费用并开始施工;当玩家无法建造时,调用 GetPlacementError 显示原因。
RTSTreeHarvestDemo
演示如何通过游戏逻辑移除树木。将光标悬停在一棵树上(树下会出现黄色光圈),按 T 即可立即砍倒它。也可以选中工人(单位类型为“Worker”)后右键单击一棵树:工人会走到树旁、转身面向它、砍伐片刻(一个光圈逐渐向树干收拢),然后将其砍倒。树木通过 RTSVegetationRenderer.RemoveTree 移除,因此该处会变为可通行,战争迷雾也会去掉对应的视野阻挡物。原位置会播放倒下动画(RTSFallingTreeEffect)。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Harvest Key | T | 砍倒光标下的树木。 |
| Workers Harvest On Right Click | 开 | 选中工人时右键单击一棵树,会派他们去砍伐。 |
| Worker Type | "Worker" | 可以采伐树木的单位类型。 |
| Pick Radius | 1.5 | 拾取树木时,光标射线与树木之间的最大距离。 |
| Chop Time, Work Distance | 1.5 s, 1.2 m | 工人砍伐的时长,以及砍伐时与树干的距离。 |
| Fall Duration, Fade Duration | 1.1 s, 0.5 s | 倒下动画以及最后缩小消失的时长。 |
| Highlight Color, Show Chop Progress, Chop Progress Color | - | 悬停的树木和正在砍伐的树木下方的光圈。 |
| Target Camera | 空 | 用于拾取的摄像机。 |
API:TryGetHoveredTree(out RTSVegetationRenderer, out TreeInstanceData)、HarvestTree(vegetation, treeId, fallAwayFrom)、OrderHarvest(workers, vegetation, tree)、RestoreHarvestedTrees()、CancelAllJobs()、HarvestedCount,以及事件 TreeHarvested。
using RTSTerrainStudio;
using UnityEngine;
// Cuts the tree nearest to a point, e.g. when a catapult hits a forest.
public static class TreeCutter
{
public static bool CutNearest(RTSTerrain terrain, Vector3 worldPoint, float maxDistance)
{
var vegetation = terrain != null ? terrain.GetComponent<RTSVegetationRenderer>() : null;
if (vegetation == null) return false;
if (!vegetation.TryFindNearestTree(worldPoint, maxDistance, out TreeInstanceData tree)) return false;
// Removes the tree from the map and notifies the terrain:
// pathing and fog of war blockers update automatically.
return vegetation.RemoveTree(tree.id);
}
}
小型辅助工具
| 类 | 用途 |
|---|---|
RTSMoveMarker | 在命令目标处显示的动画光圈:RTSMoveMarker.Spawn(position, color, radius, seconds)。 |
RTSGroundRing | 贴合地形的光圈(选择圈、范围指示):先调用 RTSGroundRing.Create(name, color, radius, width, parent),再调用 Place(position)。 |
RTSSampleMaterials, RTSSampleMeshes | 按颜色缓存的 URP 材质,以及用作占位符的基本体网格。 |
RTSSampleUtility | 地形查找、地面拾取、XZ 距离计算以及 PathingRectToCellRect(map, pathingRect)。 |
演示内容生成器
本包不附带任何第三方美术资源,而是由一个生成器在你的项目中以程序化方式创建全部演示内容:可平铺的地面、悬崖和水体纹理,草和花的精灵图,低多边形的树木和道具,材质、预制件、两个图块集、植被库和装饰物库、一个自动绘制配置文件、笔刷形状、三张演示地图和三个演示场景。输出结果是确定性的(使用固定种子),因此每次运行都会生成完全相同的内容。

创建演示内容
- 确保你处于编辑模式,并且已保存当前场景(Unity 只允许存在一个未命名场景;如有需要,生成器会提示你保存)。
- 选择 。进度条会显示各个步骤,整个过程需要一小会儿。
- 当生成器询问是否将演示场景添加到构建设置时,选择 Not Now,除非你想构建包含演示的 Player(之后也可以在 中添加)。
- 生成器会在 Project 窗口中选中场景
RTS Terrain Studio - Gameplay Demo。双击即可打开它。
在 Welcome 窗口()的演示内容区域单击 Create,或者在尚无图块集的项目中创建 RTS Terrain 时接受随之弹出的建议,也会运行生成器。它绝不会关闭或替换你已打开的场景:演示场景会另行创建,与它们并存。
生成器会依次执行以下步骤(每一步都可以通过代码单独调用):
| 步骤 | 方法 | 创建的内容 |
|---|---|---|
| 纹理 | CreateTextures() | 地面、悬崖、水体、树木和道具纹理(512 px),草和花的精灵图以及笔刷图章(256 px)。 |
| 材质 | CreateMaterials() | 树木和植物材质(使用 RTS Terrain Studio/Vegetation 着色器,带风力摆动)、道具材质(URP Lit)、示例使用的模板材质、天空材质以及后期处理配置文件。 |
| 网格和预制件 | CreateModels() | 低多边形的树木、灌木、岩石和道具,以及它们的预制件。 |
| 图块集、库和笔刷 | CreateDataAssets() | Summer 和 Winter 图块集,Demo Vegetation 和 Demo Decor 库(含选择面板图标),Summer Auto Paint 配置文件以及笔刷形状。 |
| 着色器引用和 UI 设置 | UpdateResources() | 确保 Player 构建中保留本包的所有着色器以及 UI Toolkit 运行时面板设置。 |
| 演示地图 | CreateMaps() | 三个 .rtsmap 文件(见下文)。 |
| 演示场景 | CreateScenes(bool interactive) | 三个场景(参见演示场景)。同时会将战争迷雾渲染器功能添加到你的 URP 渲染器中。 |
生成的内容
- Summer 图块集(ID
rts-demo-summer),包含十个地面图层:Grass、Lush Grass、Dirt、Rocky Dirt、Sand、Cobblestone、Dark Forest Floor、Rock、Snow 和 Blight。Rock 和 Blight 设置为 Unbuildable,很好地示范了地面图层的通行规则。 - Winter 图块集(ID
rts-demo-winter):使用相同的纹理并加上冷色调,图层顺序也相同,因此每张演示地图都可以在 Summer 和 Winter 之间切换。 - Summer Auto Paint:在草地上点缀茂盛草地和泥土斑块,沿悬崖边缘绘制泥土和碎石泥土,在岸边铺沙,在陡坡上铺岩石,在高处铺雪。
- Demo Vegetation 和 Demo Decor 库:草、高草、黄色和紫色的花以及蕨类;松树、阔叶树、枯树和灌木;岩石、一块巨石、一根残破的石柱、木箱、木桶、栅栏、树桩和蘑菇。
- 三张地图:
- Two Rivers (1v1):128 × 128 个单元格,中心对称。两条蜿蜒的河流在中央湖泊交汇;单位可以经由浅滩渡河。
- Four Kingdoms (4p):192 × 192 个单元格,四重旋转对称,有层叠的高地,中央是带有废墟的荒芜之地。
- Sandbox:96 × 96 个单元格,地势大体平坦,有一块高地和一个池塘,用于试用各种工具。
文件存放位置
生成器只会写入包文件夹下属于它自己的文件夹(包文件夹默认为 Assets/RTSTerrainStudio,除非你移动过它;本包放在任意文件夹中都能正常工作):
| 文件夹 | 内容 |
|---|---|
Content/Textures | 生成的纹理,分别存放在 Ground、Cliffs、Water、Details、Props 和 Brushes 子文件夹中。 |
Content/Materials, Content/Meshes, Content/Prefabs | 树木和道具的材质、网格和预制件。 |
Content/Tilesets | Summer.asset、Winter.asset 和 Summer Auto Paint.asset。 |
Content/Libraries | Demo Vegetation.asset、Demo Decor.asset 及其图标。 |
Content/Brushes | 笔刷形状。 |
Content/Maps | Two Rivers (1v1).rtsmap、Four Kingdoms (4p).rtsmap、Sandbox.rtsmap。 |
Content/Settings | 渲染设置(后期处理配置文件)。 |
Samples/Scenes | 三个演示场景。 |
Resources/RTSTerrainStudio | 着色器引用以及 UI/RTSRuntimePanelSettings.asset。 |
重新生成
再次运行 ,然后在对话框中单击 Regenerate 确认。现有的演示资源会被原地覆盖,因此引用它们的地图、场景和预制件仍能正常工作。你自己的资源绝不会被改动。内容没有变化的地图文件不会重新写入,从而避免在版本控制中产生多余的改动。
重新生成会覆盖你对生成的资源本身所做的任何修改(例如调整过的演示图块集或编辑过的演示地图)。如需自定义演示内容,请先复制该资源(Ctrl+D)或将地图另存为新名称,然后编辑副本。
通过代码(编辑器程序集,命名空间 RTSTerrainStudio.Editor):
| 成员 | 说明 |
|---|---|
RTSDemoContent.CreateAll(bool openScene) | 运行所有步骤并显示进度条。取消或任一步骤失败时返回 false。 |
RTSDemoContent.CreateAllNonInteractive() | 不显示任何对话框,也不打开场景;适用于自动化流程和批处理模式。返回一段简短的摘要。 |
RTSDemoContent.IsCreated | 主要演示资源存在时为 true。 |
RTSDemoContent.ContentFolder, ScenesFolder, SummerTilesetPath, TwoRiversMapPath, GameplayScenePath, … | 生成的文件夹和资源的路径。 |
RTSDemoContent.RenderPreview(scenePath, pngPath, width, height) | 渲染场景的预览图像。 |
// Editor script (place it in an Editor folder): create the demo content from a build pipeline or a CI job.
using RTSTerrainStudio.Editor;
using UnityEngine;
public static class BuildDemoContent
{
// Run with: Unity -batchmode -executeMethod BuildDemoContent.Run -quit
public static void Run()
{
if (RTSDemoContent.IsCreated) return; // nothing to do
Debug.Log(RTSDemoContent.CreateAllNonInteractive()); // logs where the content was written
}
}
演示场景
生成器会在 Samples/Scenes 中创建三个场景。它们都包含一个 Sun、一个 Post Processing 体积、一个 RTS Terrain(带有植被、水体、装饰物和环境组件),以及一个 Main Camera,后者会开启水体所需的深度纹理和不透明纹理。
| 场景 | 地图 | 内容 |
|---|---|---|
RTS Terrain Studio - Gameplay Demo | Two Rivers (1v1) | RTS 摄像机、战争迷雾,以及启用了 Auto Start 的 RTSDemoGame。按下 Play 即开始游戏。 |
RTS Terrain Studio - In-Game Editor Demo | Sandbox | 游戏内编辑器。编辑期间战争迷雾和 RTS 摄像机保持关闭;开始试玩时才会生成单位。 |
RTS Terrain Studio - Editor Playground | Four Kingdoms (4p) | 不含游戏玩法。一张大地图,用来试用 Unity 编辑器中的各种工具。 |
Gameplay Demo:可以尝试的操作
- 按下 Play。你的单位(玩家 1)站在你出生点的广场上;地图的其余部分一片漆黑。
- 框选几个单位,然后右键单击河对岸。观察它们如何保持阵型,选择一处浅滩穿过浅水。
- 走到悬崖旁边。注意下方的单位看不到上方的高地,森林也会遮住其后方的东西。然后沿坡道走上去,向下俯视。
- 离开某个区域:它会保持暗淡(已探索),而不会变回全黑。在那里游荡的敌方单位会在你失去视野后消失。
- 按 B,在移动中的单位的路径上放置一座建筑。它们会绕开建筑行走。将光标移到建筑上并按 Delete 即可将其移除。
- 将光标悬停在一棵树上并按 T,或者选中工人后右键单击一棵树。森林中出现的缺口会变为可行走,视线也能穿过。
- 用 Ctrl+1 编队,将视角移开后连按两次 1 跳回该编队。使用小地图移动摄像机和派遣单位。
- 退出 Play 模式,选中 Fog of War 对象并尝试其他 Line Of Sight 模式,或者修改 RTS Demo Game 上的 Local Team,以另一名玩家的身份进行游戏。
In-Game Editor Demo:可以尝试的操作
- 按下 Play。游戏内编辑器会打开 Sandbox 地图;整张地图都可见。
- 编辑地图:抬升一块高地、绘制地面、种植一片森林,并用标记点工具放置出生点。
- 单击 Play Test(F5)。
RTSDemoGame会在各出生点生成单位,并开启战争迷雾和 RTS 摄像机。你的编辑会立即生效:悬崖会遮挡视线,森林会阻挡通行。 - 按 Esc(需先取消当前模式并清除选择)或 F5,或者单击 Back to Editor (Esc or F5)。单位随即消失,地图恢复到试玩前的状态。
Editor Playground:可以尝试的操作
- 打开该场景和 RTS Terrain Studio 窗口()。
- 选中 RTS Terrain,在 Four Kingdoms 地图上试用各种工具:悬崖、坡道、绘制、植被和装饰物。
- 在 Scene 视图中按 P 循环切换通行叠加层,查看悬崖、斜坡、水体、树木和 Blight 地面如何影响可行走性和可建造性。
- 想玩一玩?通过 添加演示游戏并按下 Play:单位会在全部四名玩家的出生点生成。
各集成也有各自的演示场景,可按需创建:、 和 。它们需要演示内容以及对应的第三方包。
演示显示异常或没有纹理?运行 ,并应用建议的修复(URP 资源、渲染器功能、摄像机纹理)。如果缺少某个演示场景,再次运行生成器即可:它会重建缺失的场景,不会改动你自己的场景。
地图生成与地图工具
RTS Terrain Studio 窗口()的 Map 选项卡(Settings 之前的最后一个选项卡)包含作用于整张地图的卡片:Map、New Map、Resize、Generator、Heightmap、Minimap、Validation 和 Environment。本章介绍程序化地图生成器、高度图的导入与导出、小地图烘焙以及地图验证器。所有这些功能也都可以在你自己的代码中使用;文中的示例可以直接基于发布版本的包编译通过。
地图生成器
地图生成器(RTSMapGenerator)根据一个种子创建完整的经典 RTS 布局:由整齐的悬崖分隔开的高地、连接高地的坡道、湖泊或岛屿、可选的边界、地面涂层、草地和森林、装饰物以及玩家出生点。它作用于现有地图,并保持地图尺寸不变。相同的种子、相同的设置加上相同的地图尺寸,总会生成相同的地图,因此你可以只分享种子,而不必分享文件。
生成时总会替换高度、悬崖层级、坡道、悬崖类型、水体以及所有通行覆盖(之后只会把边界重新绘制回去,因此即使 Border 设为 None,手动绘制的通行覆盖也会被清除)。地面涂层、细节图层、树木、装饰物和出生点只有在对应选项开启时才会被替换:Paint Mode 不是 KeepExisting;Vegetation 或 Decor 处于 Enabled 状态、地形拥有植被库或装饰物库,并且 Clear Existing 已开启;Start Locations 处于 Enabled 状态(Update Max Players 开启时还会更新 Max Players)。否则会保留现有内容;Clear Existing 关闭时,新的树木和装饰物会追加到现有的树木和装饰物中。区域、标记点和环境设置永远不会被改动。整个生成过程是一个撤销步骤,因此可以放心尝试。
生成你的第一张地图(Unity 编辑器)
- 如果你的项目中还没有图块集和库,先运行一次 。生成器使用地形的图块集进行绘制,并从地形的库中放置植物和装饰物;没有库时会跳过植被和装饰物。
- 选中你的 RTS Terrain(或通过 创建一个),然后打开 。
- 单击 Map 选项卡,展开 Generator 卡片。
- 选择一个 Style(例如 Highlands),然后单击 Apply Style Preset。这会加载让该风格呈现应有效果的噪声、悬崖、坡道和水体参数。
- 选择一种 Symmetry:Rotate2 适用于公平的 1v1 地图,Rotate4 适用于正方形地图上的 4 名玩家。
- 展开 Start Locations,将 Count 设为玩家数量。
- 单击 Randomize Seed,再单击 Generate,并在对话框中确认。
- 阅读按钮下方的报告:高地层级数、坡道数、出生点数、树木数、装饰物数以及耗时。警告(例如 only 3 of 4 start locations found)会列在报告中,并同时输出到 Console 窗口。
- 不满意?按 Ctrl+Z,单击 Randomize Seed 后重新生成。对结果满意后保存地图(单击窗口标题栏或 RTS Terrain 组件 Inspector 中的 Save)。
该卡片上的其他按钮:
| 按钮 | 作用 |
|---|---|
| Randomize Seed | 选择一个新的随机种子。其他设置保持不变。 |
| Apply Style Preset | 加载所选风格的噪声、悬崖、坡道和水体参数。种子、对称、边界、绘制、植被、装饰物和出生点设置保持不变。 |
| Reset | 将所有生成器设置重置为所选风格的默认值。 |
| Generate | 请求确认后生成地图,整个过程作为一个名为 Generate Map 的撤销步骤。 |
窗口中的生成器设置保存在编辑器偏好设置中,因此重启 Unity 后依然保留。
生成全新地图(New Map Wizard)
会创建一个新的地图文件。在向导的 Terrain 步骤中选择 Generate 而不是 Flat,选择一个 Preset(六种风格,加上项目中所有的 RTSMapGeneratorPreset 资源,后者以 Preset: name 的形式列出)、一个种子和一种对称方式,然后单击 Preview,在保存任何内容之前先查看结果的小地图。向导会从它自己的玩家设置中获取玩家数量,并将其写入出生点。
在游戏内编辑器中生成
玩家也可以在发布的游戏中生成地图。在游戏内编辑器中,打开工具栏中的 Map ▸ Generate...。该对话框提供一组简化的选项:
| 选项 | 对应字段 |
|---|---|
| Style | 应用风格预设(会重置它下方的形状选项)。 |
| Seed / Random | seed |
| Symmetry | symmetry |
| Terrain: Cliffs, Plateaus, Ramps, Ramp Count, Water, Lake Coverage, Island, Border | cliffs.enabled, cliffs.plateauCount, ramps.enabled, ramps.count, water.enabled, water.lakeCoverage, water.islandMode, border.mode |
| Content: Textures, Vegetation, Forests, Decor, Decor Count, Start Locations, Players | paint.mode, vegetation.enabled, vegetation.forestCoverage, decor.enabled, decor.count, startLocations.enabled, startLocations.count |
按 Enter(或单击 Generate)开始生成,按 Esc 取消。生成结果是一个撤销步骤(Ctrl+Z),对话框会一直记住其设置,直到游戏关闭。
风格预设
风格(TerrainStylePreset)会改变基础高度场的形状,并通过 MapGeneratorSettings.ApplyPreset 加载相应的默认值。
| 风格 | 外观 | 层级 / 高地 | 坡道 | 湖泊覆盖率 |
|---|---|---|---|---|
| Plains | 以平地为主,有几块低矮的高地。经典的开阔对战地图。 | 2–3 / 2 | 3 | 0.05 |
| Hills(默认) | 起伏的丘陵(精细高度起伏较强),带有一些高地和坡道。适合作为通用的起点。 | 2–3 / 2 | 4 | 0.06 |
| Highlands | 大量层叠的高地和悬崖,之间由坡道相连。 | 2–6 / 5 | 8 | 0.04 |
| Islands | 被水体环绕的陆地(岛屿模式、更深的水、更宽的岸边)。 | 2–4 / 3 | 5 | 0.38 |
| Canyon | 利用脊状噪声在高耸的高地之间形成蜿蜒的峡谷。 | 2–5 / 3 | 6 | 0.03 |
| Arena | 低洼的中央竞技场,四周环绕一圈高地。 | 2–4 / 3 | 6 | 0.03 |
保存自定义预设
找到了满意的设置?把它们保存为资源: 会创建一个 RTSMapGeneratorPreset,其中包含 Description 和一整套 Settings(初始值为 Hills 风格的参数)。New Map Wizard 会列出项目中所有的预设资源,你的代码也可以将 preset.settings 传给生成器(参见通过代码生成)。预设就是普通的资源,因此可以随游戏一起发布,例如作为遭遇战菜单中的“2 人峡谷”或“4 人群岛”选项。
对称:用于竞技对战的公平地图
启用对称后,生成器只创建地图的一个扇区,然后复制它,因此每名玩家都会获得相同的地形、相同的坡道、相同数量的树木和装饰物,以及等价的出生点。在镜像副本中,倾斜的装饰物也会朝镜像方向倾斜。
| 对称 | 副本数 | 典型用途 | 需要正方形地图 |
|---|---|---|---|
None | 1 | 战役地图或自由布局的地图。 | 否 |
MirrorX | 2 | 左 ↔ 右镜像(2 名玩家在水平方向上相对)。 | 否 |
MirrorZ | 2 | 上 ↔ 下镜像。 | 否 |
MirrorXZ | 4 | 沿两条轴镜像(4 个角)。 | 否 |
Rotate2(默认) | 2 | 180° 中心对称,经典的 1v1 布局。 | 否 |
Rotate4 | 4 | 90° 旋转对称,经典的 4 人布局。 | 是(否则回退为 Rotate2) |
MirrorDiagonal | 2 | 沿对角线镜像(x ↔ z)。 | 是(否则回退为 MirrorX) |
Rotate2 为 2、4、6……)。否则生成器会警告该布局无法做到公平。MapGenerationReport.symmetryUsed 会告诉你实际使用的是哪种对称方式。生成器设置参考(MapGeneratorSettings)
Generator 卡片按折叠栏分组显示 MapGeneratorSettings 的所有字段。默认值列给出的是 new MapGeneratorSettings() 的值,与 Hills 风格相同;Noise、Cliffs、Ramps 和 Water 这几组设置会随所选风格而变化。生成前,Validate() 会把超出有效范围的值钳制到范围之内。
常规设置
| 设置项 | 默认值 | 说明 |
|---|---|---|
Seed (seed) | 12345 | 随机种子。相同的种子和设置总是生成相同的地图。 |
Style (style) | Hills | 地形的形态。单击 Apply Style Preset(或调用 ApplyPreset)可载入相匹配的默认值。 |
Symmetry (symmetry) | Rotate2 | 只生成一个扇区,再复制该扇区,使地图对各方公平(参见对称)。 |
Noise 折叠栏(noise,GeneratorNoiseSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Scale | 85 m | 大型地形特征的尺寸,以米为单位。值越大,高地越大。 |
| Octaves | 4 | 噪声层数(1–8)。倍频越多,轮廓上的细小细节就越多。 |
| Persistence | 0.5 | 每个倍频的振幅乘数(0.5 = 每个倍频的强度是前一个的一半)。 |
| Lacunarity | 2 | 每个倍频的频率乘数。 |
| Ridged Amount | 0 | 向脊状噪声(尖锐的山脊和峡谷)混合的程度。Canyon 风格使用 1。 |
| Warp | 0.4 | 域扭曲(domain warp)强度。让轮廓弯曲成更自然、更流畅的形状。 |
| Contrast | 1 | 值越高,越多区域会被推向最低和最高的高地。 |
| Smoothing | 2 | 将噪声场转换为悬崖层级之前执行的模糊次数(可去除细小的杂乱特征)。 |
| Fine Height Amplitude | 1.6 m | 精细高度噪声的振幅(高地上的小山丘)。 |
| Fine Height Scale | 22 m | 精细高度噪声的特征尺寸。 |
| Fine Height Octaves | 3 | 精细高度噪声的倍频数。 |
| Cliff Edge Fade | 4 个单元格 | 精细高度向悬崖边缘逐渐淡出的距离,使崖顶保持平整。 |
Cliffs 折叠栏(cliffs,GeneratorCliffSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Enabled | 开 | 生成悬崖层级。关闭时,整张地图都使用 Min Level,只有精细高度会变化。 |
| Min Level | 2 | 最低高地的悬崖层级(0–15)。层级 2 会在下方留出挖湖的空间。 |
| Max Level | 3 | 最高高地的悬崖层级。 |
| Plateau Count | 2 | Min Level 与 Max Level 之间不同高地层级的数量。 |
| Hysteresis | 0.04 | 每个层级阈值周围的缓冲带(占高度范围的比例)。位于缓冲带内的顶点会采用相邻顶点的层级,从而使悬崖线保持平滑。 |
| Min Feature Width | 8 个单元格 | 每块高地、台阶、山谷和湖泊的最小宽度。更窄的部分(两道悬崖之间的狭窄台阶、山脊、沟槽、极小的高地)会并入相邻层级,并且每块高地与其下方的悬崖之间都会保留约为该宽度四分之三的边距。湖泊可以比该宽度窄四分之一。各预设使用的值为 6–10。 |
| Cleanup Passes | 2 | 移除孤立顶点和尖刺的处理遍数。 |
| Cliff Type | 0 | 写入所有顶点的悬崖类型(图块集悬崖图层 0–3)。 |
Ramps 折叠栏(ramps,GeneratorRampSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Enabled | 开 | 在相邻高地之间放置坡道。 |
| Count | 4 | 生成扇区中的坡道数量(对称副本会再增加一些)。 |
| Width | 3 个单元格 | 坡道沿悬崖方向的宽度。 |
| Length | 2 个单元格 | 坡道垂直于悬崖方向的长度,按每个悬崖层级计算。2 可得到平缓的坡度。 |
| Ensure Connectivity | 开 | 在需要的地方添加额外坡道,使每块高地都能通往其他所有高地。 |
| Min Spacing | 12 个单元格 | 两条坡道之间的最小距离。重叠的坡道会被跳过。 |
| Min Plateau Size | 40 | 连通性处理会忽略顶点数少于该值的高地。 |
Water 折叠栏(water,GeneratorWaterSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Enabled | 开 | 生成湖泊或海洋及其水面。 |
| Sea Level Offset | −0.25 | 水面相对于最低高地的高度,以悬崖层级为单位(−0.9 到 2)。负值让最低高地保持干燥;正值会将其淹没。 |
| Lake Coverage | 0.06 | 地图中转为湖泊或海洋的区域(即最低的区域)所占的比例(0–0.7)。 |
| Max Depth | 2.5 m | 湖泊最深处位于水面以下的深度。 |
| Shore Width | 4 个单元格 | 倾斜岸边从岸线到最大水深处的宽度。 |
| Island Mode | 关 | 将地形向地图边缘逐渐压低,使地图成为一个或多个岛屿。岛屿地图始终至少有 15% 的海洋(如果覆盖率因此被提高,报告会给出警告)。 |
| Border Falloff | 0.25 | 岛屿模式:朝地图边缘衰减的过渡带宽度,以占地图尺寸的比例表示。 |
Border 折叠栏(border,GeneratorBorderSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Mode | None | None:无特殊边界。Unwalkable:沿地图边缘绘制不可行走且不可建造的通行覆盖。HighCliffs:沿地图边缘抬升一圈高悬崖(同样不可行走)。 |
| Width | 2 个单元格 | 边界宽度。 |
| Cliff Level | 0 | HighCliffs 边界的悬崖层级。0 = 自动(比最高的高地高两个层级)。 |
Paint 折叠栏(paint,GeneratorPaintSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Mode | AutoPaint | KeepExisting:保持地面涂层不变。FillBaseLayer:用 Base Layer 填充整张地图。AutoPaint:使用 Auto Paint 配置文件,按高度、坡度、悬崖层级和水体进行绘制。 |
| Profile | 无 | 一个 RTSAutoPaintProfile()。留空 = 使用内置的默认规则,按图块集的图层名称进行匹配。 |
| Base Layer | 0 | FillBaseLayer 使用的地面图层。 |
Vegetation 折叠栏(vegetation,GeneratorVegetationSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Enabled | 开 | 生成细节图层和森林(地形上需要指定植被库)。 |
| Use Whole Library When Empty | 开 | 当下面的列表为空时,使用植被库中的所有原型。 |
| Clear Existing | 开 | 生成前移除现有的细节图层和树木。 |
| Details | 空 | 要生成的细节(草)图层,参见下表。 |
| Tree Prototype Ids | 空 | 用于森林的树木原型 ID。 |
| Forest Coverage | 0.22 | 适宜地面中被森林覆盖的比例。 |
| Forest Noise Scale | 55 m | 森林噪声的特征尺寸。 |
| Tree Spacing | 2.6 m | 树木之间的距离。 |
| Tree Density | 0.85 | 森林中每个空位放置一棵树的概率(值越低,森林越稀疏)。 |
| Max Tree Slope | 26° | 坡度更陡的地方不放置树木。 |
| Start Location Clearance | 14 m | 出生点周围该距离内不放置树木和细节。 |
Details 中的每个条目(GeneratorDetailEntry):
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Prototype Id | "" | 植被库中细节原型的 ID。 |
| Density | 0.6 | 覆盖区域内的密度(1 = 该原型的最大密度)。 |
| Coverage | 0.55 | 适宜地面中被覆盖的比例。 |
| Noise Scale | 28 m | 覆盖噪声的特征尺寸。 |
| Max Slope | 32° | 坡度更陡的地方不放置细节。 |
Decor 折叠栏(decor,GeneratorDecorSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Enabled | 开 | 散布装饰物(地形上需要指定装饰物库)。 |
| Use Whole Library When Empty | 开 | 当下面的列表为空时,使用装饰物库中的所有原型。 |
| Clear Existing | 开 | 生成前移除现有的装饰物。 |
| Prototype Ids | 空 | 要散布的装饰物原型 ID。 |
| Count | 60 | 生成扇区中的装饰物数量(对称副本会再增加一些)。 |
| Min Spacing | 3 m | 装饰物之间的最小距离(如果原型自身的间距更大,则使用原型的间距)。 |
| Max Slope | 30° | 坡度更陡的地方不放置装饰物。 |
| Start Location Clearance | 12 m | 出生点周围该距离内不放置装饰物。 |
生成的装饰物遵循各装饰物原型的设置:随机偏航角与偏航步长、缩放范围、色调、变体、对齐法线以及最大随机倾斜。
Start Locations 折叠栏(startLocations,GeneratorStartLocationSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Enabled | 开 | 放置出生点。关闭时保留现有的出生点。 |
| Count | 2 | 出生点数量(1–16)。 |
| Flat Radius | 5 个单元格 | 出生点周围必须保持平坦的半径(同一层级,没有悬崖、坡道或水体)。 |
| Edge Margin | 8 个单元格 | 与地图边缘的最小距离。 |
| Update Max Players | 开 | 将 Count 写入地图的 Max Players(MapInfo.maxPlayers)。 |
通过代码生成
以下三个入口方法可覆盖所有使用场景:
| 方法 | 适用场景 |
|---|---|
RTSMapGenerator.Generate(RTSTerrain, MapGeneratorSettings, RTSToolContext ctx = null) | 游戏代码。生成到地形上的地图中,刷新地形并在日志中输出警告。只有传入 RTSEditSession.RunOperation 调用中的 ctx 时才可撤销。 |
RTSMapGenerator.Generate(RTSEditSession, MapGeneratorSettings) | 编辑器工具和游戏内编辑器扩展:整个生成过程是一个名为 Generate Map 的撤销步骤。 |
RTSMapGenerator.GenerateInto(MapData, settings, tileset, vegetation, decor) | 生成到一个不显示的 MapData 中(预览、批量生成、测试)。不发送通知,也不支持撤销。返回一个 MapGenerationReport。 |
using RTSTerrainStudio;
using UnityEngine;
// Generating maps from game code, e.g. for a "Random map" button in a skirmish menu.
public class GeneratorExample : MonoBehaviour
{
[SerializeField] RTSTerrain terrain;
[SerializeField] RTSMapGeneratorPreset preset; // optional: settings saved as an asset
// Generates a random 4-player layout into the current map (the map size is kept). No undo.
public void GenerateRandomMap(int seed)
{
// Start from a saved preset or from a style preset, then change what you need.
MapGeneratorSettings settings = preset != null
? preset.settings.Clone() // never modify the asset itself
: MapGeneratorSettings.CreatePreset(TerrainStylePreset.Highlands);
settings.seed = seed; // same seed + settings + map size = same map
settings.symmetry = SymmetryMode.Rotate4; // needs a square map, otherwise Rotate2 is used
settings.startLocations.count = 4; // a multiple of the symmetry copies is fairest
settings.water.lakeCoverage = 0.1f;
settings.Validate(); // clamps every value into its valid range
RTSMapGenerator.Generate(terrain, settings); // generates, refreshes the terrain, logs warnings
}
// Generates a map in memory without showing it (previews, batch generation, tests).
public static MapData GenerateOffscreen(RTSTerrain template, int seed)
{
MapCreationSettings creation = MapCreationSettings.Default; // 128 x 128 cells of 2 m
creation.width = 192;
creation.height = 192;
MapData map = MapData.Create(creation);
MapGeneratorSettings settings = MapGeneratorSettings.CreatePreset(TerrainStylePreset.Islands);
settings.seed = seed;
// Pass the tileset and libraries so painting, vegetation and decor can be generated.
MapGenerationReport report = RTSMapGenerator.GenerateInto(map, settings,
template.Tileset, template.VegetationLibrary, template.DecorLibrary);
foreach (string warning in report.warnings) Debug.LogWarning(warning);
Debug.Log(report); // plateaus, ramps, start locations, trees, decor and time
// You own the map: Dispose() it, or give it to a terrain with terrain.SetMap(map).
return map;
}
}
MapGenerationReport 包含 symmetryUsed、plateauLevels、ramps、startLocations、detailLayers、trees、decor、milliseconds 以及列表 warnings;ToString() 返回单行摘要。RTSMapGenerator.GeneratedLayers 列出了生成器写入的所有 MapLayers 标记;如果你自己包装 GenerateInto,传给 RunOperation 的就是这个值。
高度图导入与导出
Map 选项卡中的 Heightmap 卡片以 16-bit PNG 或 16-bit RAW 格式导入和导出地形高度。你可以用它从 World Machine、Gaea、Photoshop 或真实世界的高程数据源导入地形,也可以把地图发送到这类工具中处理后再导回。
高度图与地形的对应关系
- 高度图覆盖的是顶点栅格:256 × 256 个单元格的地图有 257 × 257 个顶点,因此最理想的高度图尺寸是 257 × 257 像素。其他任何尺寸的图像都会进行双线性重采样。
- 像素值按
height = offset + value / 65535 × scale进行映射。8 位图像同样可以使用,只是高度级数较少。 - 默认的 Height Scale 覆盖全部 16 个悬崖层级:
16 × Cliff Level Height(使用默认的 2 m 层级时为 32 m)。HeightmapIO.DefaultHeightScale(map)会返回该值。 - RAW 文件的第 0 行和 PNG 的最下方一行对应 Z = 0(第一行顶点)。如果地图看起来被镜像了,启用 Flip Vertically 即可。
- 每次导入都是一个名为 Import Heightmap 的撤销步骤。
导入
- 打开 Map 选项卡,展开 Heightmap 卡片。
- 选择导入 Mode(见下表)。对于典型的外部高度图,使用 Cliff Levels And Fine Heights;如果要在现有高地上添加山丘,使用 Fine Heights Only。
- 除非你清楚图像对应多少米,否则将 Height Scale 保持为 0(自动)。
- 单击 Import PNG…(PNG、JPG 或 EXR;8 位或 16 位灰度图,使用红色通道)或 Import RAW…(
.raw/.r16,16 位)。 - 检查结果。按 Ctrl+Z 可撤销,然后尝试其他模式或缩放值。
| 导入模式 | 写入内容 | 说明 |
|---|---|---|
| Fine Heights Only | 高度 | 仅写入精细高度(fine = value × scale + offset)。悬崖层级保持不变,图像高度叠加在层级高度之上,因此图像相当于在现有高地上添加山丘。 |
| Cliff Levels And Fine Heights | 高度、悬崖层级 | 图像表示绝对地面高度。它会被拆分为悬崖层级(四舍五入到最接近的层级,并清理孤立顶点)和精细高度,因此地面高度与图像完全一致。最适合阶梯状的高度图;跨越层级的平滑斜坡会变成小悬崖。 |
| Absolute Heights Keep Levels | 高度 | 图像表示绝对地面高度,但会保留现有的悬崖层级(fine = height − level × level height)。如果你在外部工具中编辑了 Ground Height 导出图,并希望在不改变悬崖的情况下重新导入,就使用此模式。 |
| 导入设置项 | 默认值 | 说明 |
|---|---|---|
| Mode | 见上文 | 决定如何将图像转换为层级和高度。 |
| Height Scale | 0(自动) | 完整 16 位范围所代表的米数。0 = 16 × 层级高度。 |
| Height Offset | 0 | 加到每个导入高度上的米数。 |
| Flip Vertically | 关 | 沿 Z 方向翻转图像(地图看起来被镜像时使用)。 |
| RAW Width / RAW Height | 0 | RAW 文件每行的采样数和行数。0 = 根据文件大小推测(仅限正方形文件)。 |
| RAW Little Endian | 开 | RAW 文件的字节序。Windows 和大多数工具写入的是小端序(little endian)。 |
导出
| 导出设置项 / 按钮 | 默认值 | 说明 |
|---|---|---|
| Source | Ground Height | Ground Height:悬崖层级加精细高度(绝对高度)。Fine Heights:仅包含层级之上的精细高度。 |
| Height Scale | 0(自动) | 65535 所代表的米数。 |
| Height Offset | 0 | 导出时写为 0 的高度。 |
| RAW Little Endian | 开 | 导出的 RAW 文件的字节序。 |
| Export PNG… | 16 位灰度 PNG。 | |
| Export RAW… | 16 位 RAW 文件。 | |
| Export Splat PNG… | 地面涂层的图像(主导图层颜色),可用于文档或外部工具。 |
往返流程:使用 Ground Height 导出,在外部编辑图像,然后选择 Absolute Heights Keep Levels,以相同的缩放和偏移重新导入。
通过代码使用高度图
HeightmapIO 为地形提供 ImportPng、ImportRaw16 和 ImportHeights(归一化的 float[] 或 NativeArray<float> 值,例如来自你自己的噪声),为单独的 MapData 提供 ApplyHeights,还提供 ExportPng16、ExportRaw16 和 ExportSplatPng(模式为 DominantLayerColor 和 SplatWeights),以及 TryGuessRawSize 和用于记录撤销的 GetImportLayers(mode)。每个导出方法都有一个 byte[] 重载和一个文件路径重载。
using RTSTerrainStudio;
using UnityEngine;
// Heightmap import and export from code.
public static class HeightmapExample
{
public static bool Import(RTSTerrain terrain, string pngPath)
{
// Imports need a height scale > 0. DefaultHeightScale = 16 cliff levels (all levels + headroom).
float scale = HeightmapIO.DefaultHeightScale(terrain.Map);
var options = new HeightmapImportOptions { heightOffset = 0f, flipZ = false };
// The image is resampled to the vertex grid (cells + 1 per side) and the terrain is refreshed.
// Pass an RTSToolContext (inside RTSEditSession.RunOperation) as the 5th argument to make it undoable.
return HeightmapIO.ImportPng(terrain, pngPath, HeightmapImportMode.CliffLevelsAndFineHeights, scale,
null, options);
}
public static bool Export(RTSTerrain terrain, string pngPath)
{
// 16-bit grayscale PNG of the absolute ground height. Scale 0 = DefaultHeightScale.
return HeightmapIO.ExportPng16(terrain.Map, pngPath, 0f, 0f, HeightmapExportSource.GroundHeight);
}
}
heightScale。务必传入大于 0 的值,例如 HeightmapIO.DefaultHeightScale(map)。对于导出,0 表示自动。小地图烘焙
RTSMinimapBaker 会渲染地图的俯视图像:地面颜色、山体阴影、悬崖、按深度着色的水体、树木、装饰物、出生点和区域。包内各处使用的都是同一个烘焙器:
- Map 选项卡中的 Minimap 卡片(预览和 PNG 导出);
- Project 窗口和 Inspector 窗口中
.rtsmap资源的预览,以及 New Map Wizard 中的预览; - 游戏内编辑器的地图浏览器(每张地图旁边都会保存一张小预览图,由
RTSRuntimeEditor上的 Write Preview Images 控制); RTSMinimapUI示例(带有战争迷雾、单位圆点和摄像机视野框的游戏内小地图,单击可移动摄像机,右键单击可移动单位)。地图发生变化时,它会以节流方式自动重新烘焙。
在编辑器中烘焙
- 打开 Map 选项卡,展开 Minimap 卡片。
- 选择 Size(长边为 128、256、512 或 1024 px;非正方形地图会保持其宽高比)。
- 如有需要,调整 Bake Options(见下表)。
- 单击 Bake Preview 可在卡片中查看结果,单击 Save PNG… 可将其写入文件,例如用于加载画面或地图选择菜单。
烘焙选项(MinimapBakeOptions)
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Ground Mode | BlendedLayers | BlendedLayers 混合所有已绘制图层的颜色;DominantLayer 在每个像素上使用最强的图层(更清晰,呈经典 RTS 风格)。图层颜色来自图块集(图层的小地图颜色,或图层纹理的平均颜色)。 |
| Level Brightness | 0.05 | 相对于层级 2,每个悬崖层级的亮度变化(越高的高地看起来越亮)。 |
| Hill Shading / Strength | 开 / 1 | 使背向光源的斜坡变暗。 |
| Light Direction | (−0.55, 0.75, 0.45) | 地图局部空间中指向光源的方向(x = 东,y = 上,z = 北)。 |
| Use Map Sun Direction | 关 | 使用地图环境中的太阳方向,而不是 Light Direction。 |
| Draw Cliffs / Cliff Color / Cliff Opacity | 开 / 黑色 / 0.8 | 为悬崖单元格着色。黑色 = 使用图块集的悬崖颜色。 |
| Draw Water | 开 | 按深度为水体着色(图块集的水体配置)。 |
| Draw Trees / Tree Color / Tree Dot Size | 开 / 深绿色 / 0.75 px | 以圆点表示树木(小于 1 的值会绘制单个像素)。 |
| Draw Decor / Decor Color / Decor Dot Size | 关 / 棕色 / 0.75 px | 以圆点表示装饰物。 |
| Draw Start Locations / Start Location Size | 开 / 4 px | 以玩家颜色显示的标记点。 |
| Scale Markers | 开 | 根据纹理尺寸缩放标记点大小(参考尺寸 256)。 |
| Player Colors | 16 种经典 RTS 玩家颜色 | 每个玩家索引对应的颜色。缺少的条目使用 MinimapBakeOptions.DefaultPlayerColors。 |
| Draw Regions | 关 | 以各区域的颜色绘制区域轮廓。 |
通过代码生成小地图
RTSMinimapBaker.Bake(terrain, size, options) 返回一张新的、可读的 sRGB Texture2D(需要由你自己销毁)。Update(existing, terrain, size, options) 会重新烘焙到已有的纹理中,只有在旧纹理无法复用时才创建新纹理。BakePng(terrain, size) 直接返回 PNG 字节数据;接受 MapData 的重载则可以烘焙当前未显示的地图。
using RTSTerrainStudio;
using UnityEngine;
using UnityEngine.UIElements;
// Keeps a minimap image up to date in a UI Toolkit Image named "minimap".
public class MinimapExample : MonoBehaviour
{
[SerializeField] RTSTerrain terrain;
[SerializeField] UIDocument document;
[SerializeField] int size = 256; // longer side in pixels
Texture2D minimap;
void OnEnable()
{
if (terrain == null) return;
terrain.MapLoaded += OnMapUpdated;
terrain.ChangesProcessed += OnChangesProcessed;
if (terrain.Map != null) Refresh();
}
void OnDisable()
{
if (terrain == null) return;
terrain.MapLoaded -= OnMapUpdated;
terrain.ChangesProcessed -= OnChangesProcessed;
}
void OnMapUpdated(RTSTerrain t) => Refresh();
// Called after every processed edit. While someone paints, throttle this (the RTSMinimapUI sample
// waits a moment after the last change and limits the bakes per second).
void OnChangesProcessed(RTSTerrain t, System.Collections.Generic.IReadOnlyList<DirtyRegion> regions) => Refresh();
void Refresh()
{
var options = new MinimapBakeOptions { drawDecor = true, hillShadingStrength = 1.2f };
// Update reuses the texture when it can. When it returns a new texture, destroy the old one.
Texture2D result = RTSMinimapBaker.Update(minimap, terrain, size, options);
if (minimap != null && result != minimap) Destroy(minimap);
minimap = result;
Image target = document != null ? document.rootVisualElement?.Q<Image>("minimap") : null;
if (target != null) target.image = minimap;
}
void OnDestroy()
{
if (minimap != null) Destroy(minimap);
}
}
地图验证
地图验证器(MapValidator)会检查地图中容易被忽视的问题:缺失的纹理、无法到达的出生点、损坏的对象引用以及超出性能限制的情况。在随游戏发布地图或把地图分享给玩家之前,先运行一次验证器。
- 打开 Map 选项卡,展开 Validation 卡片。
- 单击 Validate Map。摘要会显示错误、警告和信息的数量;如果没有问题,则显示 No problems found.
- 对于与地图上某个位置相关的问题(例如出生点问题),单击该问题即可在 Scene 视图中显示其位置。
- 修复问题后再次验证。
检查内容
| 类别 | 检查项 | 严重程度 |
|---|---|---|
| 图块集 | 未指定图块集(地形以中性的后备颜色渲染) | 警告 |
| 图块集中没有地面图层 | 警告 | |
| 地图中未存储图块集 ID(在指定了图块集的情况下保存时会自动设置) | 信息 | |
| 地图是为另一个图块集制作的,与当前指定的图块集不符 | 警告 | |
| 几何 | 存在高度无效(NaN 或无穷大)的顶点 | 错误 |
| 悬崖单元格使用了图块集未定义的悬崖类型 | 警告 | |
| 地图中有悬崖,但图块集没有悬崖图层 | 警告 | |
| 地面涂层 | Splat 纹素的权重之和不等于 255(涂层数据已损坏) | 警告 |
| 绘制了图块集未提供的地面图层 | 警告 | |
| 已绘制的地面图层没有反照率纹理 | 信息 | |
| 出生点 | 地图没有出生点 | 错误 |
| 出生点数量少于 Max Players | 警告 | |
| 某个玩家拥有多个出生点 | 警告 | |
| 某个出生点位于地图之外 | 错误 | |
| 某个出生点位于悬崖边缘 | 警告 | |
| 某个出生点被深水覆盖 / 位于浅水中 | 错误 / 信息 | |
| 两个出生点距离过近(小于 Min Start Distance Cells) | 警告 | |
| 某个出生点不可行走 | 错误 | |
| 可达性 | 某个玩家无法步行到达另一个玩家(基于通行图的泛洪填充,从出生点自身所在的悬崖层级开始) | 错误 |
| 地图边缘的大部分区域可行走(建议设置不可行走的边界) | 信息 | |
| 树木、装饰物、细节 | 存在树木、装饰物或细节图层,但未指定植被库 / 装饰物库 | 警告 |
| 对象引用的原型索引不在地图的原型列表中 | 错误 | |
| 对象或细节图层使用了库中缺失的原型 | 警告 | |
| 某个原型没有预制件(其对象不可见) | 警告 | |
| 对象位于地图之外、实例 ID 重复,或实例 ID 计数器小于现有 ID | 警告 | |
| 区域 | 某个区域没有面积;多个区域同名(按名称查找区域的游戏代码可能会选错) | 信息 |
| 性能 | 树木、装饰物对象或细节图层过多 | 警告 |
| GameObject 生成模式下的树木或装饰物对象过多 | 警告 | |
| 地图过大(影响内存占用和构建时间) | 信息 |
阈值(MapValidationSettings)
| 设置项 | 默认值 | 说明 |
|---|---|---|
maxTrees | 20000 | 树木数量超过此值时显示性能警告。 |
maxGameObjectTrees | 1500 | GameObject 生成模式下的树木数量超过此值时显示警告。 |
maxDecor | 6000 | 装饰物数量超过此值时显示警告。 |
maxGameObjectDecor | 2000 | GameObject 生成模式下的装饰物数量超过此值时显示警告。 |
maxDetailLayers | 8 | 细节图层数量超过此值时显示警告。 |
largeMapSize | 512 | 地图大于此值(每边单元格数)时,显示一条关于内存和构建时间的信息。 |
minStartDistanceCells | 12 | 出生点之间的距离小于此值时产生警告。 |
walkableEdgeFraction | 0.1 | 最外圈单元格中可行走单元格的比例超过此值时,显示“地图边缘可行走”的信息。 |
checkReachability | true | 检查所有出生点之间能否互相到达。 |
通过代码验证
MapValidator.Validate(terrain) 返回一个 List<MapIssue>。每个 MapIssue 包含严重程度 Severity(Info、Warning、Error)、消息 Message、可选的世界坐标 WorldPosition,以及受影响的图层 Layers。其他重载可以额外接受 MapValidationSettings,或者直接接受一个不依附于地形的 MapData,外加图块集、库和通行图。
using RTSTerrainStudio;
using UnityEngine;
// Checks a map before it is published (for example in a "Publish map" button of your game).
public static class ValidationExample
{
public static bool CanPublish(RTSTerrain terrain)
{
// Optional thresholds; null uses the defaults.
var settings = new MapValidationSettings { maxTrees = 10000, checkReachability = true };
var issues = MapValidator.Validate(terrain, settings);
foreach (MapIssue issue in issues)
{
// WorldPosition is set for issues that belong to a place on the map (e.g. a start location).
string where = issue.WorldPosition.HasValue ? " at " + issue.WorldPosition.Value : "";
switch (issue.Severity)
{
case MapIssueSeverity.Error: Debug.LogError(issue.Message + where, terrain); break;
case MapIssueSeverity.Warning: Debug.LogWarning(issue.Message + where, terrain); break;
default: Debug.Log(issue.Message + where, terrain); break;
}
}
return MapValidator.Count(issues, MapIssueSeverity.Error) == 0;
}
}
集成
RTS Terrain Studio 无需其他包即可独立使用:它内置了基于 Burst 的寻路器、自己的战争迷雾系统和示例玩法。三个可选集成可以将它与常用的包对接。这些集成包含在本包中(位于 Integrations 文件夹),一旦安装了所需的包,就会自动启用。
| 集成 | 依赖 | 提供的功能 | 演示场景菜单 |
|---|---|---|---|
| Mirror | Mirror 96 或更高版本 | 在任意地图上联机对战:向客户端流式传输地图、服务器权威的单位、按队伍计算的战争迷雾、防全图挂的兴趣管理。 | |
| A* Pathfinding Project | A* Pathfinding Project Pro 5.4.4 或更高版本(低于 6.0) | 根据通行图生成栅格图(Grid Graph)并支持增量更新,单位通过 AIPath 移动并使用 RVO 局部避让。 | |
| Unity NavMesh | AI Navigation(com.unity.ai.navigation)2.0 或更高版本 | 根据地形区块和通行图构建 NavMesh,并在编辑地图时保持同步更新;单位沿 NavMesh 路径移动,或使用 NavMeshAgent。 |
Welcome 窗口()会显示每个集成的状态:Active(已编译,可以使用)、Installed(已找到依赖项,但集成未编译,通常是因为版本不匹配)或 Not installed(未安装)。
可选程序集的工作原理
每个集成都位于各自的程序集定义中。只有当程序集的所有定义约束都满足时,Unity 才会编译它。这些符号有两个来源:
- 版本定义(
.asmdef中的versionDefines):安装了版本匹配的包时,程序集会为自己定义一个符号。A* 和 NavMesh 集成使用这种方式,因此会自动响应包的安装或移除。 - Player Settings 中的脚本定义符号:Mirror 通常以 Asset Store 文件夹的形式导入,而不是作为包导入,因此它的程序集使用 Mirror 自己添加到 Player Settings 中的
MIRROR符号。
| 程序集 | 版本定义 | 定义约束 |
|---|---|---|
RTSTerrainStudio.Integrations.Mirror 和 .Mirror.Editor | 无 | MIRROR |
RTSTerrainStudio.Integrations.Mirror.DefineCleanup.Editor | 无 | 无(始终编译;用于移除残留的 Mirror 符号,参见移除集成) |
RTSTerrainStudio.Integrations.AStar 和 .AStar.Editor | com.arongranberg.astar 版本在 [5.4.4,6.0.0) 范围内 → RTS_ASTAR | RTS_ASTAR、!RTS_ASTAR_DISABLED |
RTSTerrainStudio.Integrations.NavMesh 和 .NavMesh.Editor | com.unity.ai.navigation 2.0.0 或更高版本 → RTS_AI_NAVIGATION | RTS_AI_NAVIGATION |
这样一来,缺少依赖项时,相应集成会被直接跳过,不会导致任何错误,单位则改用内置寻路器。对于 A* 和 NavMesh,无需手动管理定义符号。
RTSUnit)通过 IRTSUnitPathProvider 获取路径。默认使用内置提供器;开启 Make Default 时,A* 和 NavMesh 提供器会将自己注册为 RTSUnitPathProviderRegistry.Default;你也可以把提供器指定给单个单位的 Path Provider 字段。你自己的寻路方案也可以通过实现 IRTSUnitPathProvider,以同样的方式接入。RTSTerrainStudio.Samples(用于 RTSUnit 以及选择控制器和命令控制器)。使用集成期间,请保留 Samples/Scripts 文件夹。Mirror 多人联机
借助 Mirror(96 或更高版本)在线游玩 RTS Terrain Studio 地图:
- 地图流式传输:服务器会把自己的地图发送给每个加入的客户端,因此即使玩家没有某张地图(包括用游戏内编辑器制作的地图),也能加入在这张地图上进行的游戏。下载过的地图会缓存下来,下次在同一张地图上开局时可以立即开始。
- 服务器权威的单位:寻路和移动只在服务器上运行。客户端发送命令,服务器检查相关单位是否属于该玩家。
- 每个客户端上都有按队伍计算的战争迷雾。
- 防全图挂的兴趣管理:被战争迷雾隐藏的单位永远不会发送给敌方客户端。
快速入门:演示场景
- 导入 Mirror(通过 Asset Store 或 GitHub)。Mirror 会添加脚本定义符号
MIRROR,集成随之完成编译。 - 运行 (每个项目只需运行一次)。
- 运行 。它会在
Integrations/Mirror/Demo中创建:场景RTS Mirror Demo.unity;预制件Prefabs/RTS Network Unit.prefab(带有RTSUnit、RTSNetworkUnit、NetworkIdentity和NetworkTransformReliable的占位胶囊体)和Prefabs/RTS Network Player.prefab;单位材质;如果项目中没有至少包含两个出生点的地图,还会生成一张 4 人地图RTS Mirror Demo Map.rtsmap。 - 出现提示时,允许生成器添加战争迷雾渲染器功能,并将该场景放在构建场景列表的第一位。
- 按下 Play,然后在 Mirror HUD 中单击 Host (Server + Client)。你将作为队伍 1(红色)进行游戏,你的单位会在你的出生点生成。
- 启动第二个实例(见下文),单击 Client,地址保持为
localhost。客户端会下载地图(显示进度条),以队伍 2(蓝色)的身份加入,并获得自己的单位。只有当你的单位能看到敌方单位时,敌方单位才会出现。
测试主机和客户端
| 方法 | 操作方式 |
|---|---|
| Player 构建(最可靠) | 打开 ,确保 RTS Mirror Demo 是第一个场景,然后构建。将构建版本启动两次,或者同时运行构建版本和编辑器。一方单击 Host,另一方单击 Client。 |
| ParrelSync(免费,可从 GitHub 获取) | 打开 ,创建一个克隆并在新的编辑器中打开它。在两个编辑器中都按 Play:原项目单击 Host,克隆单击 Client。克隆与原项目共享 Assets 文件夹,因此只在原项目中运行演示生成器。 |
Multiplayer Play Mode(com.unity.multiplayer.playmode) | 打开 ,启用一个虚拟玩家,然后按 Play。在主编辑器中单击 Host,在虚拟玩家中单击 Client。 |
| 通过网络 | 在客户端的地址字段中输入主机的 IP 地址。KCP 使用 UDP 端口 7777,需要在主机的防火墙中开放该端口。 |
| 专用服务器 | 使用 Dedicated Server 构建配置文件构建。网络管理器使用 Headless Start Mode = Auto Start Server,因此服务器会自行启动。客户端通过 Client 连接。 |
每台机器都会把下载的地图保存在 Application.persistentDataPath/MapCache 中。要测试全新下载,可以使用 或 。同一项目的编辑器和 Windows 构建版本共用此文件夹。
RTSNetworkMapSync 上关闭 Reuse Loaded Map 和 Use Cache,或者在以主机身份开始游戏之前,先在主机上修改地图(例如使用游戏内编辑器)。为你自己的场景添加多人联机
| 组件 | 添加到 | 用途 |
|---|---|---|
RTSNetworkManager | 网络管理器对象 | 分配队伍,等待客户端拿到地图,然后在该玩家的出生点生成玩家对象及其初始单位。玩家离开时移除其单位。 |
RTSNetworkMapSync | 同一对象(自动添加) | 将地图分块流式传输给客户端,并提供校验和、缓存和进度事件。 |
RTSFogOfWarInterestManagement | 同一对象 | Mirror 兴趣管理:每个客户端只接收其队伍能看到的单位。 |
RTSNetworkFogOfWar | 同一对象 | 配置 RTSFogOfWarManager:服务器计算所有队伍的迷雾,客户端只计算自己队伍的迷雾。 |
RTSNetworkStatusUI | 同一对象(可选) | 下载进度条以及“You are Player N”提示(IMGUI)。 |
RTSNetworkPlayer | 玩家预制件 | 通过 SyncVar 同步队伍。设置本地玩家(迷雾队伍、选择、摄像机),并以 Command 形式发送命令。 |
RTSNetworkUnit | 单位预制件 | 通过 SyncVar 同步队伍。移动只在服务器上运行,客户端只负责显示单位。 |
RTSNetworkCommandRelay | 在客户端上自动添加 | 将示例 RTSCommandController 发出的命令发送到服务器。 |
- 搭建一个普通的 RTS Terrain Studio 场景:
RTSTerrain(其地图需包含出生点)、RTSFogOfWarManager、RTSCameraController、RTSSelectionManager和RTSCommandController。不要添加RTSDemoGame:单位改由网络管理器生成。 - 创建一个空对象,添加
KcpTransport、RTSNetworkManager(它会添加RTSNetworkMapSync)、RTSFogOfWarInterestManagement、RTSNetworkFogOfWar,如有需要再添加NetworkManagerHUD。将传输组件指定给管理器的 Transport 字段。 - 创建一个带有
NetworkIdentity和RTSNetworkPlayer的玩家预制件,并将其指定给 Player Prefab。 - 创建一个带有
NetworkIdentity、RTSUnit、RTSNetworkUnit和NetworkTransformReliable(Sync Direction 设为 Server To Client,关闭 Sync Scale)的单位预制件。将其指定给 Unit Prefab,它会被自动加入可生成的预制件列表。你生成的其他所有联网预制件都必须位于 Registered Spawnable Prefabs 中。 - 设置 Units Per Player(默认 5),如有需要还可设置 Max Players Override(0 = 玩家数与地图的出生点数相同)。
- 将单位预制件上的 AI 或其他逻辑组件(例如
RTSWanderAI或你自己的 AI 脚本)放入RTSNetworkUnit的 Server Only Behaviours 中,这样客户端就不会运行它们。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| RTSNetworkManager | ||
| Terrain | 空 | 进行游戏的地形。留空时使用 RTSTerrain.Main。 |
| Unit Prefab | 空 | 每个玩家的初始单位(需要 NetworkIdentity、RTSUnit 和 RTSNetworkUnit)。 |
| Units Per Player | 5 | 初始单位数量(0–64)。 |
| Max Players Override | 0 | 让玩家数少于地图的玩家位数量(0 = 使用全部出生点)。 |
| Unit Spacing | 2 m | 初始单位之间的距离。 |
| Spawn Offset | 4 m | 第一排初始单位与出生点之间的距离,方向朝向地图中心。 |
| Despawn Units On Disconnect | 开 | 销毁离开的玩家的单位。 |
| Reserve Team Of Kept Units | 开 | 单位保留时:只要其中任何一个单位还存活,就继续保留该队伍位置。 |
| Reject Disconnect Delay | 0.5 s | 从发送拒绝消息到断开连接之间的秒数,以确保消息能够送达。 |
| RTSNetworkUnit | ||
| Server Only Behaviours | 空 | 客户端会禁用的行为组件,因为单位的逻辑由服务器运行。 |
| RTSNetworkPlayer | ||
| Jump Camera To Start | 开 | 游戏开始时将 RTS 摄像机移动到出生点。 |
| Create Command Relay | 开 | 纯客户端:如果场景中没有 RTSNetworkCommandRelay,则添加一个,使示例命令控制器把命令发送到服务器。 |
| Max Units Per Command, Max Commands Per Second | 256, 20 | 服务器对命令的验证(参见服务器权威的单位)。 |
| Formation (Server) | - | 服务器使用的阵型设置,应与 RTSCommandController 保持一致。 |
加入游戏与地图流式传输
client connects
server: team = first free player slot of the map (rejected when the map is full)
server -> client: map header (name, size, CRC-32)
client: same map already loaded, or in the cache? yes -> "no data needed"
no -> download the chunks
client: check the CRC-32 -> terrain.SetMap(RTSMapSerializer.Deserialize(bytes)) -> "map ready"
client: NetworkClient.Ready() + AddPlayer()
server: spawns the RTSNetworkPlayer and the starting units (owned by the connection)
在客户端报告正确的地图校验和之前,服务器会忽略该客户端发来的 Ready 和 AddPlayer,因此客户端永远不会收到它所没有的地图上的世界对象。在主机模式下,本地客户端直接使用服务器的地形,并立即加入。
- 传输的内容就是
.rtsmap文件(已经过 deflate 压缩)。一张带植被的 128 × 128 地图通常只有几百 KB。 - 如果服务器的地图从资源或文件加载后没有改动过,就直接发送原始文件字节。这些字节在每次会话中都完全相同,因此客户端缓存可以跨会话生效。编辑过或生成的地图会在需要时序列化。
- 服务器只接受对最早一个未确认数据块的确认,因此被篡改的客户端无法一次性拉取整张地图。
- 切换场景(
ServerChangeScene或 Mirror 的在线场景)时,会将新场景的地图重新发送给每个玩家;客户端只有在拿到该地图后才会加入新场景。
RTSNetworkMapSync 设置项 | 默认值 | 说明 |
|---|---|---|
| Terrain | 空 | 要同步地图的地形(服务器发送它的地图,客户端替换它的地图)。留空时使用 RTSTerrain.Main。 |
| Chunk Size | 16 KB | 每条数据块消息的字节数,会自动降低以适应传输层的最大消息大小。 |
| Chunks In Flight | 8 | 正在发往客户端、尚未确认的数据块数量。延迟较高时,值越大下载越快;值越小,为游戏保留的带宽越多。 |
| Transfer Timeout | 30 s | 传输连续多少秒没有进展即判定为失败。 |
| Load Timeout | 120 s | 服务器端:客户端在下载完成后加载场景并应用地图所允许的时间。客户端在等待期间会发送保活消息。 |
| Disconnect On Failure | 开 | 服务器端:断开传输失败的客户端(没有地图就无法游戏)。 |
| Max Map Size MB | 64 | 客户端:客户端接受的最大地图大小(防范出错或恶意的服务器)。 |
| Reuse Loaded Map | 开 | 如果客户端显示的地图与服务器的完全相同,则跳过下载。 |
| Use Cache / Max Cached Maps | 开 / 20 | 将下载的地图保存在 Application.persistentDataPath/MapCache 中(最久未玩的地图最先移除,0 = 不限数量)。 |
| Log Transfers | 开 | 在日志中记录每次传输的开始和结束。 |
可用于加载界面的事件:ClientHeaderReceived、ClientProgressChanged(float)、ClientMapReady、ClientTransferFailed(string);服务器上有 ServerClientReady 和 ServerClientFailed。SendMapToAll() 会将更改后的地图(例如在大厅中投票选出新地图后)重新发送给所有已连接的客户端。
战争迷雾与防全图挂
服务器会计算每个队伍的战争迷雾。RTSFogOfWarInterestManagement 每隔 Rebuild Interval 秒检查一次所有带 RTSNetworkUnit 的单位:
- 己方和盟友的单位(通过
RTSFogOfWarManager.SetAlliance共享视野的队伍)始终发送。 - 敌方单位只在战争迷雾对客户端的队伍显示它们时才发送。Linger Time 会让单位在离开视野后再多保留片刻,避免视野边缘的单位闪烁。
- 不带
RTSNetworkUnit的对象(玩家、管理器)始终发送。
被隐藏的敌人会在客户端上被移除,其移动数据也完全不会发送,因此被篡改的客户端无法让它现形。在客户端上,RTSFogOfWarHideable 还会隐藏那些处于网络范围内但尚不可见的敌人。
| 设置项 | 默认值 | 说明 |
|---|---|---|
| Fog Of War | 空 | 服务器端的战争迷雾。留空时使用 RTSFogOfWarManager.Active。 |
| Rebuild Interval | 0.2 s | 两次可见性检查之间的秒数。建议保持在 0.1–0.3 s:每当一个单位变为可见,客户端都会收到一条完整的生成消息。 |
| Linger Time | 0.5 s | 单位离开某个队伍的视野后,仍对该队伍保持可观察状态的秒数。 |
| Share With Allies | 开 | 共享视野的队伍始终能看到彼此的单位。 |
| Show All Without Fog | 开 | 服务器上没有运行战争迷雾时,每个连接都能看到所有单位。关闭时,每个连接只能看到自己的单位。 |
| Spectators See All | 关 | 没有队伍的连接(观战者)能看到所有单位。 |
RTSNetworkFogOfWar 提供 Compute All Teams On Server(开启,兴趣管理需要它)、服务器更新间隔(0 = 沿用管理器的值;值越小,单位出现得越早)以及 Disable Reveal All Online(开启)。每次可见性检查只需为每个单位查询一次迷雾栅格,因此数千个单位的开销也远低于 1 毫秒;服务器的迷雾开销随队伍数量增长,2–8 个队伍的开销很低。
使用 Mirror 编写脚本
using RTSTerrainStudio.Integrations.Mirror;
using RTSTerrainStudio.Samples;
using UnityEngine;
// Needs Mirror (96 or newer). Sends orders of the local player to the server and shows the map download.
public class MirrorExample : MonoBehaviour
{
[SerializeField] RTSNetworkMapSync mapSync; // on the network manager object
void OnEnable()
{
if (mapSync == null) return;
mapSync.ClientProgressChanged += OnProgress; // 0-1 while the map downloads
mapSync.ClientMapReady += OnMapReady; // the map is applied to the terrain
mapSync.ClientTransferFailed += OnTransferFailed;
}
void OnDisable()
{
if (mapSync == null) return;
mapSync.ClientProgressChanged -= OnProgress;
mapSync.ClientMapReady -= OnMapReady;
mapSync.ClientTransferFailed -= OnTransferFailed;
}
void OnProgress(float progress) => Debug.Log($"Downloading map: {progress:P0}");
void OnMapReady() => Debug.Log("Map ready");
void OnTransferFailed(string reason) => Debug.LogWarning("Map download failed: " + reason);
// For your own UI (e.g. a "Move here" button): the server validates the order (ownership,
// target, unit count, rate limit) before the units move.
public void SendSelectedTo(Vector3 target, bool queue)
{
RTSNetworkPlayer player = RTSNetworkPlayer.Local;
RTSSelectionManager selection = RTSSelectionManager.Main;
if (player == null || selection == null) return;
player.RequestMove(selection.Selection, target, queue);
}
// Host only: after changing the map (map vote, in-game editor), send it to every client again.
public void ResendMap()
{
if (mapSync != null) mapSync.SendMapToAll();
}
}
| 成员 | 说明 |
|---|---|
RTSNetworkManager(RTSNetworkManager.Instance) | |
ServerPlayerJoined(conn, team)、ServerConnectionRejected(conn) | 服务器:某个连接分配到了队伍(在地图传输之前),或因所有位置均已占满而被拒绝。 |
ServerPlayerSpawned(conn, player)、ServerUnitSpawned(conn, unit) | 服务器:玩家对象已生成(在其地图就绪之后);某个单位已生成,通过 SpawnUnit 生成的单位也会触发。可借助它们为玩家发放初始对象。 |
ServerPlayerLeft(conn, team) | 服务器:某个连接已离开,该事件在其对象被销毁之前触发。 |
ClientJoinedGame、ClientDisconnected、ClientRejected(reason) | 客户端:加载地图后已加入游戏;连接已结束;服务器拒绝了此客户端。 |
RTSNetworkUnit SpawnUnit(owner, team, position, yaw, prefab = null) | 服务器:生成一个单位(会被移到最近的可行走位置)。owner 为 null 表示无所有者,例如中立野怪或建筑。预制件必须是带有 RTSNetworkUnit 的已注册可生成预制件。 |
GetUnits(conn)、TryGetTeam(conn, out team)、IsTeamReserved(team) | 服务器:某个连接的单位、其所属队伍,以及已离开玩家的单位是否仍占用队伍位置。 |
RejectConnection(conn, reason) | 服务器:将原因发送给客户端,并在 Reject Disconnect Delay 之后断开其连接。 |
| RTSNetworkUnit 和 RTSNetworkPlayer | |
static RTSNetworkUnit.Spawned、Despawned、All | 网络单位生成(在应用其队伍之后)或销毁时,在每台机器上触发;本机已知的所有单位。 |
static RTSNetworkPlayer.LocalPlayerStarted、LocalPlayerStopped、Local | 本地玩家对象出现或消失;当前的本地玩家。 |
using Mirror;
using RTSTerrainStudio.Integrations.Mirror;
using UnityEngine;
// Server: gives every player an extra starting object (for example a town hall) next to its start location.
// Put it on the network manager object.
public class StartingObjectsExample : MonoBehaviour
{
[SerializeField] GameObject townHallPrefab; // NetworkIdentity + RTSUnit + RTSNetworkUnit, in Registered Spawnable Prefabs
RTSNetworkManager manager;
void Awake() => manager = GetComponent<RTSNetworkManager>();
void OnEnable()
{
if (manager != null) manager.ServerPlayerSpawned += OnPlayerSpawned;
}
void OnDisable()
{
if (manager != null) manager.ServerPlayerSpawned -= OnPlayerSpawned;
}
void OnPlayerSpawned(NetworkConnectionToClient conn, RTSNetworkPlayer player)
{
if (player == null || townHallPrefab == null) return;
// Start location of the player's team (a fallback position when the map has none).
RTSNetworkUtility.GetStartLocation(manager.Terrain, player.Team, manager.MaxPlayers, out Vector3 start, out float yaw);
// Owned by the connection, so the player can select and order it.
manager.SpawnUnit(conn, player.Team, start, yaw, townHallPrefab);
}
}
限制与故障排除
- 游戏过程中的地图编辑(在主机上使用游戏内编辑器)不会实时同步。调用
RTSNetworkMapSync.SendMapToAll()可重新发送地图。 RTSBuildingPlacementDemo和RTSTreeHarvestDemo只在本地运行,不支持联网。RTSDemoGame是单人游戏的启动器,不应放在网络场景中。- 在客户端上,
RTSUnit.State始终为 Idle。可改用RTSUnit.Velocity(例如用于行走动画),或添加你自己的 SyncVar。 - 同盟关系是在服务器的战争迷雾上设置的,客户端不会收到这些设置。如果希望客户端上也能看到盟友的视野,也要在客户端上调用
SetAlliance。 - 以叠加(Additive)方式加载场景时会保留当前地图。服务器和客户端必须使用相同的图块集和库(即相同的项目内容)。
| 问题 | 解决方法 |
|---|---|
| 警告“The server's fog of war computes only its local team” | 将 RTSNetworkFogOfWar 添加到网络管理器上,或在服务器的迷雾管理器上启用 Compute All Teams。 |
| 客户端一直停留在“Connecting...” | 无法连接到服务器(地址、防火墙或 UDP 端口 7777 有问题),或者服务器场景中没有 RTSNetworkManager。 |
| 显示“Could not join the game: The game is full” | 地图的空闲位置少于玩家数量。可以添加出生点、提高地图的 Max Players,或检查 Max Players Override。已离开玩家的单位存活期间,其位置会一直保留。 |
| 移除 Mirror 后出现缺少 Mirror 类型的编译错误 | MIRROR 定义符号仍未移除。参见移除集成。 |
| 单位在客户端上不移动 | 单位预制件需要 NetworkTransformReliable,场景中需要 RTSCommandController。 |
| 敌方单位始终可见 | RTSFogOfWarInterestManagement 必须位于激活的对象上,并且迷雾管理器必须在服务器上运行。 |
| 每次都会重新下载地图 | 服务器发送的是加载后又经过编辑或生成的地图。启动主机之前,先将地图保存到对应的资源或文件中。 |
A* Pathfinding Project
此集成会将地形的通行图转换为 A* 栅格图(Grid Graph),在你编辑地图时(无论在编辑器中还是在游戏中)保持其同步更新,并让示例单位借助 A* 移动:既可以沿 A* 路径、使用单位自身的转向逻辑移动,也可以使用 A* 的 AIPath 和 RVO 局部避让。
TraversalConstraint(5.4 新增)和 RVO 局部避让(RVOController、RVOSimulator),这些只有 Pro 版本才包含。免费版使用相同的包名,因此 Unity 无法区分两者:如果使用免费版,请将 RTS_ASTAR_DISABLED 添加到 (或删除该集成的文件夹)。快速入门:演示场景
- 安装 A* Pathfinding Project Pro 5.4 或更高版本。
- 运行一次 。
- 运行 。场景会保存为
Integrations/AStarPathfinding/Demo/RTS AStar Demo.unity。如果演示内容中没有地图,会在场景旁边生成RTS AStar Demo Map.rtsmap(包含高地、坡道和湖泊)。 - 单击 Play。用鼠标左键选择单位,右键单击使其移动。骑士使用 RTS Large 图,其他所有单位使用 RTS Ground 图。
- 选中 A* Pathfinding 对象,将 A* Path Provider 的 Mode 在 Agent Movement 和 Path Only 之间切换,比较两种移动方式。
将 A* 添加到你自己的场景
选中你的 RTS Terrain,然后运行 。该命令会添加一个名为 A* Pathfinding 的 GameObject,其中包含:
| 组件 | 用途 |
|---|---|
AstarPath | A* 的主组件。Scan on Startup 已关闭(由图构建器负责扫描),路径日志只记录错误。 |
RTSAstarGraphBuilder(A* Graph Builder) | 根据通行图创建栅格图,并在每次编辑地图时更新它们。 |
RVOSimulator | 为带有 RVOController 的代理提供局部避让。 |
RTSAstarPathProvider(A* Path Provider) | 让所有单位使用 A*(注册为 RTSUnitPathProviderRegistry.Default)。 |
也可以手动完成同样的设置:将这些组件添加到任意 GameObject 上(),并将地形指定给图构建器;或者把图构建器放在地形 GameObject 上,它会自动找到地形。另外还要关闭 AstarPath 的 Scan On Startup。
图的构建方式
- 精确布局。每个通行单元格对应一个节点,以地图为中心,并随地形的 Y 轴旋转而旋转。移动或旋转地形会重建图(在编辑模式下,会在地形静止 0.2 秒后重建)。
- 无需物理系统。A* 的碰撞检测和高度检测处于关闭状态。节点高度取自地形表面(与
RTSTerrain.GetHeight相同);可行走性、标签和惩罚值取自通行标记。这些数据由 Burst 作业计算。 - 标签(可在构建器中修改):0 为地面,1 为浅水,2 为深水,3 为悬崖边缘。在 Seeker 的 Tags 设置中使用它们,可以按单位禁止或惩罚特定类型的地面。
- 图配置文件。每个配置文件对应一个 A* 栅格图(例如每种单位尺寸一个)。每个配置文件包含图名称、Traversal(Land、Shallow Water、Deep Water)、Unit Radius(距离障碍物过近的节点会变为不可行走,因此大型单位永远不会穿模)、各类地面的惩罚值(1000 ≈ 每个节点多出一米路程)、用于海军图的 Use Water Surface Height,以及最大攀爬高度。单位会选择能容纳其半径的最小图。
- 海军和两栖单位。将配置文件的 Traversal 设为 Deep Water(船只)或 Land | Shallow Water | Deep Water,并启用 Use Water Surface Height。为船只添加
RTSAstarUnitSetup,并将 Required Traversal 设为 Deep Water。在代码中,可以用工厂方法RTSAstarGraphProfile.Ground()(“RTS Ground”,半径 0.5 m,Land | Shallow Water)、Naval()(“RTS Naval”,半径 1 m,Deep Water,使用水面高度)和Amphibious()(“RTS Amphibious”,半径 0.75 m,所有地面类型,使用水面高度)创建现成的配置文件。新建配置文件的默认值为 Land | Shallow Water,浅水惩罚值 600,悬崖边缘惩罚值 200。 - 更新。新地图、移动地形或更改设置都会触发完整重建。每次地图编辑(
RTSTerrain.PathingChanged)只会在一次AstarPath.AddWorkItem调用中重写受影响的节点及其净空边距和连接。 - 超大地图。A* 将栅格图限制为每个轴最多 1024 个节点(使用
ASTAR_LARGER_GRIDS优化定义时为 8192 个)。更大的通行图会自动让每个节点对应多个通行单元格;如需更快的图,也可以通过 Cells Per Node 主动这样设置。 - 启用 Build In Edit Mode 后,编辑地图时会以 A* Gizmo(辅助图标)的形式显示这些图。
RTSAstarGraphBuilder 成员 | 说明 |
|---|---|
| Cells Per Node(设置项,默认 1) | 每个图节点在每个轴上对应的通行单元格数。1 = 与通行图完全一致;在超大地图上会自动提高(EffectiveCellsPerNode)。 |
| Log Rebuilds(设置项,默认关) | 每次完整重建时在 Console 窗口中输出一行日志。 |
Profiles、Tags | 图配置文件和标签编号。通过代码修改后,需调用 RequestRebuild()。 |
RequestRebuild() | 安排在下一次更新时进行完整重建(重复调用开销很小)。 |
bool RebuildNow() | 立即重建所有图。如果没有处于激活状态的 AstarPath 或没有地图,则返回 false。 |
UpdateRegion(RectInt pathingRect) | 重写某个通行单元格矩形区域内的节点。由 RTSTerrain.PathingChanged 自动调用;如果你修改了 PathingMap.Flags 却没有触发该事件,请自行调用。 |
ProcessPendingChanges() | 立即应用待处理的更新(适用于在地图编辑的同一帧内请求的路径)。 |
GetGraph(index)、GetGraph(name)、GetProfileIndex(name) | 某个配置文件对应的 A* GridGraph。 |
FindProfileFor(radius, traversal)、FindGraphFor(radius, traversal) | 能容纳某个单位的最小配置文件(或图);如果没有配置文件允许该通行方式,则返回 -1 / null。 |
GetStats(index) | RTSAstarGraphStats:尺寸、节点尺寸、可行走节点数、重建和更新耗时、isBuilt。 |
IsBuilt、IsProfileBuilt(index)、bool TryGetProblem(out string problem) | 图是否已按当前地图构建;图无法构建的原因(没有 AstarPath、没有地形、没有地图)。 |
GraphsRebuilt、GraphsUpdated | 完整重建后和增量更新后触发的事件(后者附带通行矩形)。 |
static FindFor(terrain)、static All | 某个地形的构建器;所有已启用的构建器。 |
提供器模式与 RVO
模式(RTSAstarMovementMode) | 行为 |
|---|---|
| Agent Movement(默认) | HandlesMovement = true。移动命令会设置单位 AIPath 的目的地并开始搜索;Stop 和 Hold 会清除路径,处于 Hold 状态的单位会锁定其 RVOController。到达和失败都会反馈给单位,因此排队命令(Shift)、巡逻和单位事件的行为与使用内置寻路器时相同。RVO 局部避让会让移动中的单位彼此保持距离。 |
| Path Only | HandlesMovement = false。A* 只计算路径点(通过单位的 Seeker,因此 Funnel Modifier 等修改器会生效);单位使用自身的转向和分离逻辑沿路径点移动。没有 Funnel Modifier 的单位,其路径会被拉直(Smooth Paths)。 |
| 提供器设置 | 默认值 | 说明 |
|---|---|---|
| Mode | Agent Movement | 见上文。 |
| Graph Builder | 空 | 使用哪个构建器的图。留空 = 使用单位所在地形的构建器。 |
| Auto Configure Units | 开 | 为每个单位添加并配置 RTSAstarUnitSetup:包括 Seeker、FunnelModifier、RTSAstarAIPath(一种无需物理射线检测即可站在地形表面上的 AIPath)和 RVOController,速度、转向速率、加速度、半径和高度均从 RTSUnit 复制。 |
| Fallback To Builtin | 开 | 在没有可用的 A* 图时(编辑模式下以及首次扫描之前)使用内置寻路器。 |
| Smooth Paths | 开 | Path Only 模式:拉直没有 Funnel Modifier 的单位的路径;捷径只经过带有允许标签的节点,且绝不穿过开销更高的节点。 |
| Stuck Time | 1.5 s | Agent Movement 模式:代理在移动过程中允许静止不动的秒数;超过后会重新寻路一次,再次卡住则放弃。 |
如果要按名称选择图,或者按单位类型关闭局部避让或路径平滑,可以自行将 RTSAstarUnitSetup 添加到单位预制件上。RTSAstarAIPath 的 Inspector 窗口会显示 A* 常规的移动设置,外加一个 RTS Terrain 部分(Follow Terrain Height、Stay On Water Surface、Terrain)。
using RTSTerrainStudio.Integrations.AStar;
using RTSTerrainStudio.Samples;
using UnityEngine;
// Needs the A* Pathfinding Project Pro 5.4+. Put it into an assembly that references
// RTSTerrainStudio.Integrations.AStar (or into Assembly-CSharp, which references it automatically).
public class AstarExample : MonoBehaviour
{
[SerializeField] RTSUnit unit;
void Start()
{
// Configure one unit by hand (Seeker, FunnelModifier, RTSAstarAIPath and RVOController).
RTSAstarUnitSetup setup = RTSAstarUnitSetup.Configure(unit, agentMovement: true);
setup.Agent.maxSpeed *= 1.5f;
// The graph that fits this unit's radius.
RTSAstarGraphBuilder builder = RTSAstarGraphBuilder.FindFor(unit.Terrain);
if (builder != null)
Debug.Log("Unit uses graph " + builder.FindGraphFor(unit.Radius)?.name);
}
// After changing the pathing map from code in the middle of a frame, push the change to A* now
// instead of waiting for the next update.
public static void FlushGraphs(RTSUnit anyUnit)
{
RTSAstarGraphBuilder.FindFor(anyUnit.Terrain)?.ProcessPendingChanges();
}
}
限制
- A* 的栅格图规则(
GridGraph.rules)不起作用:每次扫描后,构建器会自行写入可行走性、标签、惩罚值和位置。请改用构建器的标签和惩罚值。 - 不支持绕 X 轴或 Z 轴旋转的地形(栅格图是水平平面)。
- 绘制的 Unwalkable 通行覆盖只会阻挡地面单位。船只会被障碍物和悬崖阻挡;可以用装饰物(doodad)或深水边界来规划海上航线。海军单位需要使用 Agent Movement 模式。
- 从构建器中移除图配置文件后,其图仍会保留在
AstarPath组件中;如果不再需要,请在该组件中将其删除。 - A* 只在 Play 模式下搜索路径。在编辑模式下,寻路请求会回退到内置寻路器。
移除集成
你永远不必移除集成:缺少依赖项时,集成根本不会被编译。当你卸载依赖项或希望项目更精简时,可以移除它。无论哪种情况,如果你创建过演示场景,都要先删除该集成的 Demo 文件夹:其中的场景和预制件使用了该集成的组件,移除集成后会显示脚本缺失。
移除 A* Pathfinding Project 或 AI Navigation
- 如果存在
Integrations/AStarPathfinding/Demo(或Integrations/UnityNavMesh/Demo),将其删除。 - 从你自己的场景和预制件中移除该集成的组件(图构建器、寻路提供器、单位设置 / NavMesh 构建器、NavMesh 寻路提供器)。
- 卸载对应的包(A* Pathfinding Project;或在 Package Manager 中卸载 AI Navigation)。版本定义符号会随包一起消失,集成程序集也会自动跳过编译。
- 如果你曾为免费版 A* 添加过
RTS_ASTAR_DISABLED,现在可以把它从 Scripting Define Symbols 中移除了。
移除 Mirror
Mirror 会向 Player Settings 添加脚本定义符号 MIRROR、MIRROR_89_OR_NEWER、MIRROR_90_OR_NEWER、MIRROR_93_OR_NEWER 和 MIRROR_96_OR_NEWER,而删除 Mirror 并不会移除这些符号。只要 MIRROR 仍被定义,Unity 就会继续针对已不存在的 Mirror 程序集编译该集成,由此产生的编译错误会让项目中的所有脚本都无法编译。
- 先删除
Integrations/Mirror/Demo。 - 在 Unity 编辑器中(Project 窗口)删除
Mirror文件夹。小型编辑器程序集RTSTerrainStudio.Integrations.Mirror.DefineCleanup.Editor会检测到这次删除,从所有构建目标中移除 Mirror 的符号,并在日志中记录移除了哪些符号。每当编辑器在未安装 Mirror 的情况下启动时,它也会运行。通过 可以手动执行同样的操作。 - 检查你要构建的每个平台的 :其中不能再有任何
MIRROR或MIRROR_..._OR_NEWER条目。如有残留,请手动删除;例如在编辑器关闭期间删除了 Mirror,之后编辑器以安全模式启动时,就可能出现残留。Mirror 的 Edgegap 插件还会添加EDGEGAP_PLUGIN_SERVERS;它本身无害,但你也可以将其移除。
集成脚本可以保留在项目中:在重新安装 Mirror 之前,它们不会被编译。
删除集成文件夹
每个集成都是自包含的。在 Project 窗口中删除 Integrations/Mirror、Integrations/AStarPathfinding 或 Integrations/UnityNavMesh,即可彻底移除对应的集成;包中的其他内容都不会引用这些文件夹。整个 Integrations 文件夹也可以删除。
Integrations/Mirror,之后又移除了 Mirror,则需要手动移除 Mirror 的定义符号(自动清理功能就位于该文件夹中)。脚本编写指南
凡是能在编辑器窗口中完成的操作,也都可以通过 C# 完成。本章介绍如何查询地形、通过代码修改地图(可撤销或不可撤销)、编写自己的工具、响应事件,以及在运行时保存和加载地图。下面的每个示例都可以直接基于发布版的包编译通过;把它复制到你的项目中,再按需修改即可。
程序集、命名空间与坐标
| 程序集 | 命名空间 | 包含内容 |
|---|---|---|
RTSTerrainStudio.Runtime | RTSTerrainStudio | 地形、地图数据、工具、笔刷、植被、水体、装饰物、战争迷雾、通行数据、地图生成器、序列化器、游戏内编辑器。 |
RTSTerrainStudio.Samples | RTSTerrainStudio.Samples | RTS 摄像机、单位、选择、命令、小地图 UI、演示游戏。 |
RTSTerrainStudio.Editor | RTSTerrainStudio.Editor | 编辑器窗口、Scene 视图工具、自定义 Inspector、导入器、配置窗口、RTSEditorContext。 |
RTSTerrainStudio.Integrations.* | RTSTerrainStudio.Integrations.Mirror / .AStar / .NavMesh | 可选集成。 |
没有程序集定义的脚本(即 Assembly-CSharp,以及 Editor 文件夹中脚本所属的 Assembly-CSharp-Editor)会自动引用这些程序集。如果你的代码有自己的 .asmdef,把 RTSTerrainStudio.Runtime(以及用到的 RTSTerrainStudio.Samples 或 RTSTerrainStudio.Editor)添加到它的引用中;如果使用了 float3 或 NativeArray 等类型,还要添加 Unity.Mathematics 和 Unity.Collections。
地图的三种栅格
- 单元格:地图由
Width × Height个单元格组成,每个单元格边长CellSize米(默认 2 m)。传给NotifyChanged的矩形以单元格为单位。 - 顶点:单元格的角点,共
(Width + 1) × (Height + 1)个;索引为z * VertexCountX + x(MapData.VertexIndex(x, z))。高度、悬崖层级、标记、悬崖类型和水体都存储在顶点上。顶点 (x, z) 位于(x * CellSize, z * CellSize)。 - 纹素:每个单元格包含 N 个纹素的栅格层,包括 Splat(
SplatResolution)、细节(DetailResolution)和通行(PathingResolution)。纹素 (x, z) 的中心位于(x + 0.5, z + 0.5) * CellSize / N。
地图局部空间以第一个顶点为起点:X/Z 以米为单位,范围从 0 到 MapData.WorldSize,Y 轴朝上。只考虑地形的位置和 Y 轴旋转(缩放会被忽略)。可使用 WorldToLocal、LocalToWorld、WorldToLocalDirection、LocalToWorldDirection、WorldToCell、CellCenterToWorld 和 VertexToWorld 进行转换。顶点的地面高度为 CliffLevels[i] × CliffLevelHeight + Heights[i]。旋转以度为单位,俯视时为顺时针方向(0 = +Z,90 = +X),与 Unity 的 Y 轴旋转一致。
查询地形
RTSTerrain 的查询 API 精确贴合你所看到的地表(包括圆润的悬崖轮廓线),而且不需要碰撞体。它在编辑模式和运行时都可以使用。
| 成员 | 返回值 |
|---|---|
RTSTerrain.Main, RTSTerrain.All | 第一个已启用的地形 / 所有已启用的地形。 |
RTSTerrain.FindAt(Vector3 world) | 区域包含该位置的地形;没有则返回 null。 |
RTSTerrain.RaycastAll(Ray, out RTSTerrainHit, maxDistance) | 所有地形中距离最近的命中结果。 |
Raycast(Ray, out RTSTerrainHit, maxDistance) | 针对当前地形的解析式射线检测。RTSTerrainHit 包含 point、normal、distance、localPoint、cell 和 terrain。 |
GetHeight(world), SnapToGround(world) | 地面的世界 Y 坐标 / 移到地面上的位置。 |
GetNormal(world) | 世界空间中的表面法线。 |
GetCliffLevel(world) | 该位置的悬崖层级(0–15)。 |
IsWater(world, out depth), TryGetWaterHeight(world, out waterY) | 该位置是否位于水下、水深以及水面的世界 Y 坐标。 |
GetPathing(world), IsWalkable(world), IsBuildable(world) | PathingFlags(Walkable、Buildable、Flyable、ShallowWater、DeepWater、Cliff、Blocked、Steep)。在地图外或通行数据尚未构建时返回 None。 |
Contains(world), WorldBounds | 该位置是否位于地图区域内 / 世界空间包围盒。 |
GetGeometry() | 适用于 Burst 的 MapGeometryView(提供基于地图局部空间的 SampleHeight、SampleNormal、SampleCliffLevel、SampleWater、Raycast 等方法),可在你自己的作业中使用。 |
RTSPathfinder.FindPath(terrain, from, to, options, list) | 由内置 Burst 寻路器计算的路径(PathStatus:Success、Partial、NoPath、InvalidStart)。 |
using System.Collections.Generic;
using RTSTerrainStudio;
using UnityEngine;
// Logs what is under the mouse cursor when you left-click: height, cliff level, water and pathing.
// Works with any number of RTS terrains in the scene and needs no colliders.
public class TerrainQueryExample : MonoBehaviour
{
[SerializeField] Camera viewCamera;
void Update()
{
// RTSInput works with the Input System and with the legacy Input Manager.
if (!RTSInput.GetMouseButtonDown(0)) return;
Camera cam = viewCamera != null ? viewCamera : Camera.main;
if (cam == null) return;
Ray ray = cam.ScreenPointToRay(RTSInput.PointerPosition);
// Analytic ray cast against every enabled RTS terrain (closest hit wins).
if (!RTSTerrain.RaycastAll(ray, out RTSTerrainHit hit)) return;
RTSTerrain terrain = hit.terrain;
Vector3 p = hit.point;
float groundY = terrain.GetHeight(p); // world Y of the ground
Vector3 normal = terrain.GetNormal(p); // world space surface normal
int level = terrain.GetCliffLevel(p); // 0-15
bool walkable = terrain.IsWalkable(p); // ground units can walk here
bool buildable = terrain.IsBuildable(p); // buildings can be placed here
PathingFlags flags = terrain.GetPathing(p); // all pathing flags of the cell
string water = "dry";
if (terrain.IsWater(p, out float depth) && terrain.TryGetWaterHeight(p, out float waterY))
water = $"water, depth {depth:0.00} m, surface at y = {waterY:0.00}";
Debug.Log($"Cell {hit.cell} on '{terrain.name}': ground y = {groundY:0.00}, level {level}, " +
$"slope {Vector3.Angle(normal, Vector3.up):0} deg, {water}, " +
$"walkable {walkable}, buildable {buildable}, flags {flags}");
}
// Keeps an object standing on the ground of whatever terrain is below it.
public static void PlaceOnGround(Transform t)
{
RTSTerrain terrain = RTSTerrain.FindAt(t.position);
if (terrain != null) t.position = terrain.SnapToGround(t.position);
}
// Asks the built-in Burst pathfinder for a path (main thread, no third-party package needed).
public static bool TryFindPath(RTSTerrain terrain, Vector3 from, Vector3 to, List<Vector3> waypoints)
{
// ForUnit sets the clearance for a unit of this radius (large units avoid narrow gaps).
PathRequestOptions options = PathRequestOptions.ForUnit(0.5f);
PathStatus status = RTSPathfinder.FindPath(terrain, from, to, options, waypoints);
// Partial: the goal is unreachable, the path ends at the closest reachable cell.
return status == PathStatus.Success || status == PathStatus.Partial;
}
}
RTSTerrain.RaycastAll 而不是 Physics.Raycast:它的结果精确,不需要碰撞体,在编辑模式下也能工作。不过,对于物理对象,以及需要同时命中其他对象的射线检测,碰撞体(地形设置中的 Collider Mode)仍然很有用。通过代码编辑地图
所有地图内容都存储在 terrain.Map(类型为 MapData)中。修改地图内容始终遵循同样的三个步骤:
- (在主线程上)修改
terrain.Map中的数据。 - 调用
terrain.NotifyChanged(layers, cellRect),传入发生变化的MapLayers以及以单元格为单位的变化矩形;如果修改了整张地图,则调用NotifyChangedAll(layers)。 - 就这么简单。地形会在下一次
LateUpdate(编辑模式下为下一次编辑器更新)中只重建标记为脏的区块、Splat 贴图、水体、植被、装饰物和通行图。如果需要在同一帧内得到结果,调用terrain.ProcessPendingChanges()(频繁调用的开销也很低)。
数据(MapData) | 栅格 / 类型 | MapLayers 标记 |
|---|---|---|
Heights | 顶点,float,叠加在层级之上的精细高度(米) | Heights |
CliffLevels | 顶点,byte,0–15 | CliffLevels |
VertexFlags | 顶点,VertexFlags(Ramp、Water、User0、User1) | VertexFlags |
CliffTypes | 顶点,byte,0–3 | CliffTypes |
WaterHeights | 顶点,水面高度(地图局部坐标,单位为米;仅在设置了 Water 标记处使用) | Water |
SplatMaps[0..3] | Splat 纹素,4 × RGBA32;图层 i 位于第 i/4 张贴图的第 i%4 个通道;权重之和为 255 | Splat |
DetailLayers | 细节纹素,每层用一个字节存储密度(DetailLayer.prototypeId、density) | Details |
Trees, Decor | TreeInstanceData / DecorInstanceData 列表,其中的原型索引指向 TreePrototypeIds / DecorPrototypeIds | Trees, Decor |
PathingOverrides | 通行纹素,绘制的通行覆盖 | PathingOverrides |
Regions, StartLocations, Markers | 列表 | Regions, StartLocations, Markers |
Info, Environment | 地图信息和光照 | Info, Environment |
TerrainToolUtility.ClampFineHeight);让每个 Splat 纹素的权重之和恰好为 255(使用 SplatRaster 和 SplatMath);用 map.AllocateInstanceId() 为新树木和装饰物分配实例 ID,用 GetOrAddTreePrototype / GetOrAddDecorPrototype 分配原型索引。切勿在地图加载或调整尺寸之后继续使用之前持有的 MapData 或其数组的引用:SetMap、LoadFromFile 和 ResizeMap 会替换整个对象并释放旧对象。运行时地形变形与绘制:弹坑与荒芜之地
典型的游戏玩法用例:爆炸炸出一个弹坑;亡灵建筑周围不断蔓延出“荒芜之地”地表。通行数据以及植被和装饰物的高度都会自动随之更新。
using RTSTerrainStudio;
using UnityEngine;
// Runtime terrain changes from gameplay code: explosion craters and spreading "blight".
// Both edit MapData directly and then call NotifyChanged. The terrain rebuilds only the
// touched chunks (and the pathing map) in its next LateUpdate.
public static class TerrainEffects
{
// Digs a bowl-shaped crater into the fine height around a world position.
public static void Crater(RTSTerrain terrain, Vector3 worldPosition, float radius, float depth)
{
MapData map = terrain != null ? terrain.Map : null;
if (map == null || radius <= 0f) return;
// Work in map-local space: X/Z in metres from the first vertex.
Vector3 local = terrain.WorldToLocal(worldPosition);
// Vertex rectangle covered by the crater (vertices sit at x * CellSize, z * CellSize).
int x0 = Mathf.Max(0, Mathf.FloorToInt((local.x - radius) / map.CellSize));
int z0 = Mathf.Max(0, Mathf.FloorToInt((local.z - radius) / map.CellSize));
int x1 = Mathf.Min(map.Width, Mathf.CeilToInt((local.x + radius) / map.CellSize));
int z1 = Mathf.Min(map.Height, Mathf.CeilToInt((local.z + radius) / map.CellSize));
if (x1 < x0 || z1 < z0) return; // completely outside the map
for (int z = z0; z <= z1; z++)
for (int x = x0; x <= x1; x++)
{
float dx = x * map.CellSize - local.x;
float dz = z * map.CellSize - local.z;
float t = Mathf.Sqrt(dx * dx + dz * dz) / radius;
if (t >= 1f) continue;
// Smooth bowl: deepest in the centre, zero at the rim.
float falloff = 1f - t * t;
int i = map.VertexIndex(x, z);
// Heights is the fine height on top of the cliff level. Clamp it to the allowed range.
map.Heights[i] = TerrainToolUtility.ClampFineHeight(map, map.Heights[i] - depth * falloff * falloff);
}
// Tell the terrain which cells changed. Meshes, colliders, water, vegetation, decor and
// pathing follow automatically.
var vertexRect = new RectInt(x0, z0, x1 - x0 + 1, z1 - z0 + 1);
terrain.NotifyChanged(MapLayers.Heights, BrushUtility.VertexRectToCellRect(map, vertexRect));
}
// Paints a ground layer (for example a "blight" texture) around a position with a soft edge.
// amount is the coverage reached in the centre (0-1).
public static void PaintLayer(RTSTerrain terrain, Vector3 worldPosition, float radius, int layer, float amount = 1f)
{
MapData map = terrain != null ? terrain.Map : null;
if (map == null || radius <= 0f) return;
layer = Mathf.Clamp(layer, 0, MapData.MaxGroundLayers - 1);
Vector3 local = terrain.WorldToLocal(worldPosition);
int res = map.SplatResolution; // splat texels per cell
float texel = map.CellSize / res; // texel size in metres
// Texel rectangle (texel centres sit at (x + 0.5) * texel).
int x0 = Mathf.Max(0, Mathf.FloorToInt((local.x - radius) / texel));
int z0 = Mathf.Max(0, Mathf.FloorToInt((local.z - radius) / texel));
int x1 = Mathf.Min(map.SplatWidth - 1, Mathf.CeilToInt((local.x + radius) / texel));
int z1 = Mathf.Min(map.SplatHeight - 1, Mathf.CeilToInt((local.z + radius) / texel));
if (x1 < x0 || z1 < z0) return;
// SplatRaster gives read/write access to all 16 layer weights of a texel.
var splat = new SplatRaster(map);
for (int z = z0; z <= z1; z++)
for (int x = x0; x <= x1; x++)
{
float dx = (x + 0.5f) * texel - local.x;
float dz = (z + 0.5f) * texel - local.z;
float t = Mathf.Sqrt(dx * dx + dz * dz) / radius;
if (t >= 1f) continue;
int index = splat.Index(x, z);
SplatTexel weights = splat.Read(index);
int target = Mathf.RoundToInt(Mathf.Clamp01(amount * (1f - t)) * SplatMath.Total);
if (weights[layer] >= target) continue; // never remove blight that is already there
// Sets the layer weight and rescales the other layers, so the texel still sums to 255.
SplatMath.SetLayerWeight(ref weights, layer, target, 0);
splat.Write(index, weights);
}
var texelRect = new RectInt(x0, z0, x1 - x0 + 1, z1 - z0 + 1);
terrain.NotifyChanged(MapLayers.Splat, BrushUtility.TexelRectToCellRect(map, texelRect, res));
}
}
TerrainEffects.Crater(RTSTerrain.FindAt(hitPoint), hitPoint, 4f, 1.5f); 或 TerrainEffects.PaintLayer(terrain, building.position, 12f, blightLayer);。对于每帧要修改数千个顶点的效果,可以像内置工具那样把循环移到 Burst 作业中。可撤销的编辑:RTSEditSession.RunOperation
直接编辑不支持撤销,对游戏玩法来说这恰恰合适。对于编辑器工具和地图编辑功能,使用 RTSEditSession:RunOperation(name, layers, action) 会记录整张地图中所列数据层的内容,运行你的操作,并把所有更改合并为一个撤销步骤。在 Unity 编辑器中,使用共享会话 RTSEditorContext.Session;它的步骤属于 Unity 撤销系统的一部分,因此 Ctrl+Z / Ctrl+Y 照常可用。
// Put this file into a folder named "Editor".
using RTSTerrainStudio;
using RTSTerrainStudio.Editor;
using UnityEditor;
using UnityEngine;
// Editor menu command that raises a plateau in the middle of the edited map.
// It runs through the shared edit session, so Ctrl+Z / Ctrl+Y undo and redo it in one step.
public static class PlateauMenu
{
[MenuItem("Tools/My Game/Raise Centre Plateau")]
static void RaiseCentrePlateau()
{
RTSTerrain terrain = RTSEditorContext.Terrain; // the terrain selected in the RTS Terrain Studio window
if (terrain == null || terrain.Map == null) return;
// The shared session of the window and the Scene View. It always edits RTSEditorContext.Terrain.
RTSEditSession session = RTSEditorContext.Session;
// RunOperation records the listed layers of the whole map BEFORE the operation runs,
// so inside it you only change data and notify the terrain.
session.RunOperation("Raise Centre Plateau", MapLayers.CliffLevels | MapLayers.Heights, ctx =>
{
MapData map = ctx.Map;
int size = Mathf.Min(map.Width, map.Height) / 4; // plateau size in cells
var vertexRect = new RectInt(map.Width / 2 - size / 2, map.Height / 2 - size / 2, size + 1, size + 1);
for (int z = vertexRect.yMin; z < vertexRect.yMax; z++)
for (int x = vertexRect.xMin; x < vertexRect.xMax; x++)
{
int i = map.VertexIndex(x, z);
// Cliff levels are bytes 0-15. Ground height = level * CliffLevelHeight + fine height.
map.CliffLevels[i] = (byte)Mathf.Min(MapData.MaxCliffLevel, map.CliffLevels[i] + 1);
map.Heights[i] = 0f; // flat top
}
ctx.MarkChanged(MapLayers.CliffLevels | MapLayers.Heights,
BrushUtility.VertexRectToCellRect(map, vertexRect));
});
}
}
在运行中的游戏里,可以使用游戏内编辑器的会话(RTSRuntimeEditor.Session),让你的操作与它共享撤销历史;也可以创建自己的会话:
using RTSTerrainStudio;
using UnityEngine;
// Undoable edits in a running game: use the session of the in-game editor, or your own session.
public class RuntimeUndoExample : MonoBehaviour
{
RTSEditSession ownSession;
public void FloodLowGround(RTSTerrain terrain, float waterLocalY)
{
// Prefer the in-game editor's session, so the edit shares its undo history (Ctrl+Z in the editor UI).
var ingameEditor = FindFirstObjectByType<RTSRuntimeEditor>();
RTSEditSession session = ingameEditor != null ? ingameEditor.Session : null;
if (session == null)
{
// Without the in-game editor: a plain session works as well (call Dispose when done).
if (ownSession == null) ownSession = new RTSEditSession();
ownSession.Terrain = terrain;
session = ownSession;
}
session.RunOperation("Flood Low Ground", MapLayers.Water | MapLayers.VertexFlags, ctx =>
{
MapData map = ctx.Map;
const byte waterFlag = (byte)VertexFlags.Water;
for (int i = 0; i < map.VertexCount; i++)
{
// Ground height of a vertex = cliff level * level height + fine height.
float ground = map.CliffLevels[i] * map.CliffLevelHeight + map.Heights[i];
if (ground >= waterLocalY) continue;
// A vertex is wet when it has the Water flag; WaterHeights holds its surface height.
map.VertexFlags[i] = (byte)(map.VertexFlags[i] | waterFlag);
map.WaterHeights[i] = waterLocalY;
}
// Water changes only need the Water layer: terrain meshes are not rebuilt.
ctx.Terrain.NotifyChangedAll(MapLayers.Water);
});
}
public void UndoLast()
{
ownSession?.Undo();
}
void OnDestroy()
{
ownSession?.Dispose();
}
}
RunOperation,而是为每个笔印调用 ctx.Record 和 ctx.MarkChanged(参见下一节)。RunOperation 会记录完整的数据层,因此在大地图上应优先指定具体的数据层,而不是 MapLayers.All。编写自定义工具
工具就是派生自 RTSTool 的普通 C# 类。本包会通过反射(RTSToolRegistry)找到所有带有公共无参构造函数的非抽象工具,因此你的工具会自动出现在 RTS Terrain Studio 窗口以及游戏内编辑器中对应的类别选项卡里,并自动具备由其 [ToolSetting] 字段生成的设置界面,以及笔刷面板、对称、撤销/重做、投影到地形上的笔刷光标和状态栏。在编辑器中,工具的设置会在不同会话之间保留。
- 在项目中的任意位置创建一个 C# 文件(不要放在
Editor文件夹中,这样该工具在构建版本中也存在,可供游戏内编辑器使用)。 - 派生自
RTSTool,并重写Id(必须唯一,例如"height.crater")、DisplayName和Category。 - 为用户可以修改的选项添加带有
[ToolSetting("Label", ...)]的公共字段。 - 重写
ApplyDab:用BrushUtility.ComputeWeights计算笔刷权重,在修改数据之前调用ctx.Record(layers, cells),然后修改数据,之后再调用ctx.MarkChanged(layers, cells)。 - 返回 Unity。脚本编译完成后,该工具就会出现在对应的选项卡中。
- 如果要在 Managed Stripping Level 高于 Minimal 的构建版本中发布该工具,用
[UnityEngine.Scripting.Preserve]标记这个类,这样代码剥离才会保留它(参见代码剥离与自定义工具)。
using RTSTerrainStudio;
using UnityEngine;
// A custom tool: every click digs a crater and paints scorched ground into it.
// No registration is needed. The class is found by reflection and appears in the Height tab
// of the RTS Terrain Studio window and of the in-game editor, with a UI for every [ToolSetting].
public sealed class CraterTool : RTSTool
{
// Unique id. The part before the dot also picks a fallback icon ("height" = height tools).
public override string Id => "height.crater";
public override string DisplayName => "Crater";
public override ToolCategory Category => ToolCategory.Height;
public override string Description => "Digs a crater with a scorched floor at the click position.";
public override string Hint => "Click: dig a crater | Shift+click: raise a mound";
public override int Order => 100; // after the built-in height tools
public override ToolInputMode InputMode => ToolInputMode.Click; // one dab per click
public override bool UsesBrush => true; // keep the brush size / shape panel
public override MapLayers ModifiedLayers => MapLayers.Heights | MapLayers.Splat;
public override Color PreviewColor => new Color(1f, 0.45f, 0.1f, 1f);
// Public fields with [ToolSetting] get a slider, toggle or palette automatically.
[ToolSetting("Depth", Min = 0.1f, Max = 6f, Order = 0, Tooltip = "Depth of the crater centre in metres.")]
public float depth = 1.5f;
[ToolSetting("Paint Floor", Order = 1, Tooltip = "Paint a ground layer into the crater.")]
public bool paintFloor = true;
[ToolSetting("Floor Layer", Kind = ToolSettingKind.GroundLayer, Order = 2, Tooltip = "Ground layer of the crater floor.")]
public int floorLayer = 0;
public override void ApplyDab(RTSToolContext ctx, in BrushDab dab)
{
MapData map = ctx.Map;
// 1. Brush weights (0-1) for every vertex under the brush. Shape, hardness, falloff and
// rotation are already applied; strength is not (use dab.strength). Dispose the result.
using BrushWeights w = BrushUtility.ComputeWeights(map, dab, ctx.BrushMask, BrushGrid.Vertices, 1);
if (w.IsEmpty) return;
// Cells touched by the dab (covers both the vertex and the splat texel grid).
RectInt cells = BrushUtility.GetCellRect(map, dab);
// 2. Record for undo BEFORE changing anything. Record every layer you are going to write.
MapLayers layers = paintFloor ? MapLayers.Heights | MapLayers.Splat : MapLayers.Heights;
ctx.Record(layers, cells);
// 3. Change the data. Shift (dab.invert) raises a mound instead.
float delta = depth * dab.strength * (dab.invert ? 1f : -1f);
for (int z = w.rect.yMin; z < w.rect.yMax; z++)
for (int x = w.rect.xMin; x < w.rect.xMax; x++)
{
float weight = w[x, z];
if (weight <= 0f) continue;
int i = map.VertexIndex(x, z);
map.Heights[i] = TerrainToolUtility.ClampFineHeight(map, map.Heights[i] + delta * weight);
}
if (paintFloor && !dab.invert)
{
// The splat maps use their own texel grid: compute weights for it as well.
int res = map.SplatResolution;
using BrushWeights sw = BrushUtility.ComputeWeights(map, dab, ctx.BrushMask, BrushGrid.Texels, res);
var splat = new SplatRaster(map);
int layer = Mathf.Clamp(floorLayer, 0, MapData.MaxGroundLayers - 1);
for (int z = sw.rect.yMin; z < sw.rect.yMax; z++)
for (int x = sw.rect.xMin; x < sw.rect.xMax; x++)
{
int target = Mathf.RoundToInt(sw[x, z] * SplatMath.Total);
int index = splat.Index(x, z);
SplatTexel t = splat.Read(index);
if (t[layer] >= target) continue;
SplatMath.SetLayerWeight(ref t, layer, target, 0);
splat.Write(index, t);
}
}
// 4. Notify the terrain AFTER the change (the same rectangle you recorded).
ctx.MarkChanged(layers, cells);
}
}
可重写的成员
| 成员 | 默认值 | 说明 |
|---|---|---|
Id、DisplayName、Category | 必需 | 唯一 ID、界面显示名称和所在选项卡(Height、Cliffs、Paint、Vegetation、Decor、Water、Pathing、Markers)。 |
Description、Hint | "" | 工具提示和帮助文本;状态栏中的用法提示。 |
IconName | Id | 图标名称。未知名称会回退到 ID 分组前缀(height.、paint. ……)对应的图标,再找不到则使用通用的扳手图标。可以通过 RTSIcons.RegisterIcon 添加自定义绘制的图标。 |
Order | 0 | 在类别中的排列位置。 |
InputMode | Brush | Brush:按笔刷间距沿指针路径落下笔印。Click:每次按下落下一个笔印。Drag:调用 OnDragBegin / OnDragUpdate / OnDragEnd(用于矩形、线条)。 |
UsesBrush | Brush 模式下为 true | 显示笔刷面板(大小、形状、强度……)。 |
RepeatRate | 0 | 按住按钮且不移动时每秒落下的笔印数(用于持续抬升或降低)。 |
ModifiedLayers | None | 仅供显示:用于叠加层和状态栏。 |
PreviewColor | 浅蓝色 | 投影笔刷光标的颜色。 |
Snap | None | 将笔刷中心吸附到顶点(悬崖工具)或单元格中心。 |
SupportsPick + OnPick | false | Ctrl+单击会拾取一个值(高度、图层),而不是绘制。 |
OnActivated、OnDeactivated、OnSettingChanged | 生命周期回调。 | |
OnStrokeBegin、OnStrokeEnd、OnStrokeCancelled | 在笔触开始和结束时调用;被取消的笔触(Esc)之后会被还原。 | |
DrawGizmos(ctx, gizmos, hasHover, hoverLocal) | 通过 IToolGizmos 绘制额外预览;在编辑器中用 Handles 实现,在游戏中用 GL 线条实现。 | |
GetStatus(ctx, hoverLocal) | null | 状态栏文本,例如“Level 3 → 4”。 |
[ToolSetting] 支持 Min/Max(滑块范围)、Order、Tooltip 和 Kind。Auto 会根据字段类型选择编辑控件(float/int 用滑块,bool 用开关,enum 用弹出菜单,Color 和 string 也有对应的控件)。特殊的 Kind:GroundLayer 和 CliffType(带缩略图的图块集选择面板)、CliffLevel、DetailPrototype / DetailPrototypeList、TreePrototype / TreePrototypeList、DecorPrototype / DecorPrototypeList(库选择面板)、Player(0–15)以及 Hidden。
通用设置界面不会显示 Hidden 设置项。只有一个例外:在 Unity 编辑器窗口中,类型为 UnityEngine.Object 的 Hidden 字段(即资源引用,例如 AutoPaintTool.profile)仍会显示为对象字段,因为通用界面没有其他办法显示资源引用。游戏内编辑器则不会显示它。隐藏的设置项仍会和其他工具设置一起保存。
须知
- 对称是自动处理的。
ApplyDab会为每个对称副本各调用一次;dab.symmetry会告诉你当前是哪个副本(包括其偏航角偏移以及是否镜像)。dab.rotation已经过变换。 - 笔印(
BrushDab)包含localPosition、worldPosition、radius、strength、rotation、invert(按住 Shift)、isFirstDab和dabIndex。 - 笔刷权重:顶点层使用
BrushGrid.Vertices,分辨率为 1;栅格层使用BrushGrid.Texels,分辨率为map.SplatResolution、DetailResolution或PathingResolution。可以用BrushUtility.VertexRectToCellRect、TexelRectToCellRect或GetCellRect(map, dab)转换矩形。权重用完后务必释放。 - 同一笔印的所有对称副本必须使用相同的随机值。
ctx.Random在每次笔触开始时重新设定种子,且每调用一次就前进一步,因此副本调用它得到的值会与原始笔印不同。只在dab.symmetry.index == 0时生成随机值,按该dab.dabIndex缓存起来,复用到其他副本时再用SymmetricScatter.TransformOffset和SymmetryPoint.TransformYaw将其镜像或旋转。公共类SymmetricScatter(GetCandidates、GetSingleSeed)可以替你完成这些工作。 - 硬选择:如需经典 RTS 风格的“全有或全无”式编辑,可以派生自
CliffTool、WaterTargetTool或PathingTool(它们提供类别、光标颜色和状态文本;WaterTargetTool还会添加 Water Height 设置项和 Ctrl+单击拾取功能),或者调用TerrainToolUtility.BeginHardVertexSelection(ctx, dab, layers, out weights, out forcedIndex, out cells)。权重不低于TerrainToolUtility.HardSelectionThreshold(0.5)的顶点,以及笔刷中心下方的顶点(forcedIndex),都会受到影响,并且只为该区域记录撤销。即使该方法返回 false,也要释放weights。 - 性能:内置工具在 Burst 作业中运行循环(参见
Runtime/Tools中的RaiseLowerTool和PaintLayerTool)。对于小笔刷,普通循环就足够了。 - 标记了
[HideTool]的类根本不会被注册:两个编辑器中都没有它,session.Tools中也没有,因此SetActiveTool、RTSEditorContext.ActivateTool和FindTool都找不到它。要在代码中使用这类工具(例如调试工具),请直接指定一个实例:session.ActiveTool = new MyDebugTool();。在 Unity 编辑器中,还需要设置RTSEditorContext.EditingEnabled = true。 - 注册表只搜索本包的运行时程序集,以及直接或经由其他此类程序集间接引用它的程序集。工具总是派生自
RTSTool,因此工具所在的程序集本来就会引用运行时程序集(没有程序集定义的脚本位于Assembly-CSharp中,该程序集会自动引用运行时程序集)。注册表会跳过名称以System、Unity.、UnityEngine、UnityEditor、Mono.、mscorlib、netstandard或nunit开头的程序集。不要给你自己的程序集起这样的名称。 - 代码剥离:工具是通过反射创建的,因此当 Managed Stripping Level 高于 Minimal 时,构建过程无法看出这些工具会被用到。本包的
Runtime/link.xml会保留内置工具;你自己的工具则需要用[UnityEngine.Scripting.Preserve]标记,或者在项目的link.xml中列出它们(参见代码剥离与自定义工具)。 - 在代码中可以用
session.SetActiveTool("height.crater")激活工具;在编辑器中则可以用RTSEditorContext.ActivateTool("height.crater")。
通过代码驱动编辑会话
两个编辑器的笔刷、对称和笔触都经由 RTSEditSession 处理。你可以用同一套 API 构建自己的前端,例如自定义绘制界面、代替玩家绘制的教程,或者关卡脚本。
| 成员 | 说明 |
|---|---|
BrushSettings Brush | 共享的笔刷:shape、customShape、radius、strength、hardness、useFalloffCurve、falloffCurve、rotation、randomRotation、spacing、jitter。在代码中修改 falloffCurve 的关键帧后,需要调用 Brush.MarkDirty():烘焙的笔刷遮罩会自动检测形状、硬度、useFalloffCurve 和 customShape 的变化,但检测不到曲线关键帧的变化。 |
SymmetrySettings Symmetry | mode(SymmetryMode)和 center(归一化坐标,0.5/0.5 = 地图中心)。GetPoints(localPos, mapWorldSize, mergeDistance, results) 返回原始点及其副本。 |
ScaleRadius(factor)、AddStrength(delta) | 即 [ ] 和 - = 快捷键执行的操作,结果会限制在有效范围内。 |
bool InvertModifier | 相当于按住 Shift:降低而非抬升,擦除而非绘制。 |
BeginStroke(worldPoint)、UpdateStroke(worldPoint, deltaTime)、EndStroke()、CancelStroke() | 从按下到松开的一次笔触。按住按钮期间每帧调用 UpdateStroke(指针不移动时也要调用,这样重复触发的工具才能持续工作)。EndStroke 会压入一个撤销步骤;CancelStroke 会还原该笔触。 |
Pick(worldPoint) | 执行当前工具的 Ctrl+单击拾取。 |
FindTool(id)、FindTool<T>()、GetTools(category)、SetActiveTool(id)、ActiveTool | 查找并激活工具。 |
ActiveToolChanged、StrokeEnded、UndoRedoPerformed、SelectionChanged | 供你的界面使用的事件:当前工具已更改、笔触已完成(被取消的笔触不会触发此事件)、执行了撤销或重做、装饰物选区已更改。 |
using RTSTerrainStudio;
using UnityEngine;
// Paints with the built-in tools from your own code, for example from a custom UI.
// Call PressAt / DragTo / Release with world positions on the terrain (for example from terrain.Raycast).
public class SessionDriverExample : MonoBehaviour
{
public RTSTerrain terrain;
RTSEditSession session;
void OnEnable()
{
// In a scene with the in-game editor you can use RTSRuntimeEditor.Session instead.
session = new RTSEditSession();
session.Terrain = terrain;
// Brush and symmetry are shared by every tool of this session.
session.Brush.radius = 8f;
session.Brush.strength = 0.6f;
session.Symmetry.mode = SymmetryMode.Rotate2; // 1v1 point symmetry
session.Symmetry.center = new Vector2(0.5f, 0.5f); // map centre
session.SetActiveTool("height.raise");
session.StrokeEnded += () => Debug.Log("Stroke finished (one undo step)");
}
void OnDisable()
{
session?.Dispose();
session = null;
}
public void PressAt(Vector3 worldPoint, bool lower)
{
session.InvertModifier = lower; // like holding Shift
session.BeginStroke(worldPoint);
}
// Call every frame while the button is held.
public void DragTo(Vector3 worldPoint) => session.UpdateStroke(worldPoint, Time.deltaTime);
public void Release() => session.EndStroke();
public void Abort() => session.CancelStroke(); // reverts the running stroke
public void UseLinearFalloff()
{
session.Brush.useFalloffCurve = true;
session.Brush.falloffCurve = AnimationCurve.Linear(0f, 1f, 1f, 0f);
// Curve keys are not detected automatically: rebuild the baked brush mask.
session.Brush.MarkDirty();
}
}
事件
| 事件 | 签名 | 触发时机 |
|---|---|---|
MapLoaded | Action<RTSTerrain> | 指定了新地图之后(加载、新建地图、调整尺寸、重建)。在此重建你从地图派生出的所有内容。 |
MapUnloading | Action<RTSTerrain> | 当前地图被替换或释放之前。在此清除对它的引用。 |
MapChanged | Action<RTSTerrain, MapChangeEventArgs> | 调用 NotifyChanged / NotifyChangedAll 时立即触发,早于网格重建。MapChangeEventArgs 包含 Layers、CellRect 和 IsFullRefresh。笔触期间会频繁触发:请保持处理函数轻量。 |
ChangesProcessed | Action<RTSTerrain, IReadOnlyList<DirtyRegion>> | 待处理的更改处理完毕之后:此时网格、纹理和通行数据均已是最新。每个 DirtyRegion 包含 layers 和 cellRect。 |
PathingChanged | Action<RTSTerrain, RectInt> | 通行图(部分)重建之后;矩形以通行单元格为单位。重新加载地图后订阅依然有效(与 PathingMap.Changed 不同)。 |
TilesetChanged | Action<RTSTerrain> | 图块集引用或其内容发生了更改。 |
LibrariesChanged | Action<RTSTerrain> | 植被库或装饰物库的引用或其内容发生了更改。 |
RTSTerrain.TerrainEnabled / TerrainDisabled | static Action<RTSTerrain> | 某个地形被启用或禁用(注册表发生变化)。 |
RTSRuntimeEditorEvents.PlaytestChanged | static Action<bool> | 游戏内编辑器开始或停止了试玩。在此生成和清理你的游戏玩法对象;试玩结束后地图会被还原。 |
RTSRuntimeEditorEvents.MapOpened | static Action<RTSTerrain> | 游戏内编辑器打开了一张地图。 |
RTSEditSession:ActiveToolChanged、TerrainChanged、StrokeEnded、UndoRedoPerformed、SelectionChanged | 会话(编辑器或游戏内编辑器)的编辑状态。 |
using System.Collections.Generic;
using RTSTerrainStudio;
using UnityEngine;
// Listens to all important terrain events. Put it on the RTS Terrain GameObject (or assign the terrain).
public class TerrainEventsExample : MonoBehaviour
{
[SerializeField] RTSTerrain terrain;
void OnEnable()
{
if (terrain == null) terrain = GetComponent<RTSTerrain>();
if (terrain == null) return;
terrain.MapLoaded += OnMapLoaded; // new map assigned (load, new map, resize)
terrain.MapChanged += OnMapChanged; // immediately when NotifyChanged is called
terrain.ChangesProcessed += OnChangesProcessed; // after meshes, textures and pathing are up to date
terrain.PathingChanged += OnPathingChanged; // after the pathing map was rebuilt
terrain.TilesetChanged += OnTilesetChanged; // tileset reference or content changed
// The map may already be loaded before this component was enabled.
if (terrain.Map != null) OnMapLoaded(terrain);
}
void OnDisable()
{
// Always unsubscribe: terrains survive scene changes in the editor and domain reloads.
if (terrain == null) return;
terrain.MapLoaded -= OnMapLoaded;
terrain.MapChanged -= OnMapChanged;
terrain.ChangesProcessed -= OnChangesProcessed;
terrain.PathingChanged -= OnPathingChanged;
terrain.TilesetChanged -= OnTilesetChanged;
}
void OnMapLoaded(RTSTerrain t)
{
// Rebuild everything your game derives from the map, e.g. spawn points from start locations.
Debug.Log($"Map '{t.Map.Info.name}' loaded: {t.Map.Width} x {t.Map.Height} cells, " +
$"{t.Map.StartLocations.Count} start locations.");
}
void OnMapChanged(RTSTerrain t, MapChangeEventArgs e)
{
// Raised often during brush strokes: keep it cheap. Check which layers changed.
if ((e.Layers & (MapLayers.Trees | MapLayers.Decor)) != 0)
Debug.Log($"Objects changed in cells {e.CellRect} (full refresh: {e.IsFullRefresh})");
}
void OnChangesProcessed(RTSTerrain t, IReadOnlyList<DirtyRegion> regions)
{
// Meshes are rebuilt now, so GetHeight and Raycast return the new surface.
foreach (DirtyRegion r in regions)
if ((r.layers & MapLayers.Geometry) != 0)
Debug.Log($"Ground changed in cells {r.cellRect}");
}
void OnPathingChanged(RTSTerrain t, RectInt pathingRect)
{
// The rectangle is in pathing cells (Map.PathingResolution per map cell). Re-plan paths here.
Debug.Log($"Pathing changed in {pathingRect}");
}
void OnTilesetChanged(RTSTerrain t)
{
Debug.Log("Tileset is now " + (t.Tileset != null ? t.Tileset.name : "none"));
}
}
运行时保存与加载、自定义文件段
地图以 .rtsmap 文件的形式保存。Unity 编辑器(导入为 RTSMapAsset)、游戏内编辑器和 Mirror 地图流式传输都使用同一种格式,因此玩家制作的地图无需任何修改即可在 Unity 中打开,反之亦然。
| 成员 | 说明 |
|---|---|
terrain.SaveToFile(path) / terrain.LoadFromFile(path) | 在运行时保存和加载。两者都会设置 CurrentFilePath;保存还会清除 HasUnsavedChanges。 |
terrain.MapAsset、terrain.LoadFromAsset() | 使用已导入项目的地图(战役地图、内置遭遇战地图)。 |
terrain.SetMap(MapData) | 显示任意 MapData。地形会接管其所有权,并释放之前的地图。 |
RTSMapSerializer.Serialize(map) / Deserialize(bytes) | 以字节形式处理地图(网络传输、云存档、模组下载)。反序列化得到的地图归调用方所有。 |
RTSMapSerializer.SaveToFile、LoadFromFile、Write(map, stream)、Read(stream) | 无需地形即可读写文件和流。 |
RTSMapSerializer.TryReadHeaderFromFile(path, out header) | 只读取很小的文件头(名称、作者、描述、尺寸、单元格尺寸、最大玩家数、图块集 ID、修改时间),适用于地图浏览器。 |
RTSMapSerializer.DefaultMapsFolder、FileExtension | StorageRoot 中的 Maps 文件夹(游戏内编辑器在未设置 Maps Folder 时使用该文件夹)以及 ".rtsmap"。 |
RTSMapSerializer.StorageRoot | 用于在不同会话之间保留文件的文件夹:Application.persistentDataPath。在 Web 构建中,它是浏览器存储空间中一个以公司名称和产品名称命名的文件夹,因为在 Web 平台上 persistentDataPath 会随页面地址而变化。 |
RTSWebFileSystem.Flush()、DownloadFile(...)、IsSupported | 用于 Web 构建:将更改过的文件写入浏览器存储空间,使其在页面重新加载后依然保留,并让浏览器下载文件。RTSMapSerializer.SaveToFile 和 terrain.SaveToFile 会自动执行刷新;你自己用 System.IO 写入或删除文件后,需要调用 Flush()。在其他平台上,这些方法不执行任何操作。参见 Web 构建的脚本 API。 |
using System.Collections.Generic;
using System.IO;
using RTSTerrainStudio;
using UnityEngine;
// Saving, listing and loading maps at runtime (for example for your own map browser or a save game).
public static class MapFilesExample
{
// The folder the in-game editor uses too: Maps in RTSMapSerializer.StorageRoot.
static string Folder => RTSMapSerializer.DefaultMapsFolder;
public static void Save(RTSTerrain terrain, string fileName)
{
// SaveToFile creates the folder when needed and remembers the path in terrain.CurrentFilePath.
terrain.SaveToFile(Path.Combine(Folder, fileName + RTSMapSerializer.FileExtension));
}
public static void Load(RTSTerrain terrain, string fileName)
{
string path = Path.Combine(Folder, fileName + RTSMapSerializer.FileExtension);
if (File.Exists(path)) terrain.LoadFromFile(path); // raises MapLoaded
}
// Lists all maps with their header (name, size, players) without loading the whole file.
public static List<RTSMapHeader> ListMaps()
{
var result = new List<RTSMapHeader>();
if (!Directory.Exists(Folder)) return result;
foreach (string path in Directory.GetFiles(Folder, "*" + RTSMapSerializer.FileExtension))
if (RTSMapSerializer.TryReadHeaderFromFile(path, out RTSMapHeader header))
result.Add(header);
return result;
}
// Maps can also travel as bytes (network, cloud save, mod download).
public static byte[] ToBytes(RTSTerrain terrain) => RTSMapSerializer.Serialize(terrain.Map);
public static void FromBytes(RTSTerrain terrain, byte[] bytes)
{
// The terrain takes ownership of the new MapData and disposes the previous map.
terrain.SetMap(RTSMapSerializer.Deserialize(bytes));
}
// Loads a .rtsmap that was imported into the project (an RTSMapAsset), e.g. for a campaign.
public static void LoadAsset(RTSTerrain terrain, RTSMapAsset asset)
{
// Assigning a different asset loads it immediately (when the terrain is enabled).
// Assigning the same asset again does nothing, so reload it explicitly.
if (terrain.MapAsset == asset) terrain.LoadFromAsset();
else terrain.MapAsset = asset;
}
}
文件布局与自定义段
"RTSM" 4 bytes magic
int32 format version (RTSMapSerializer.CurrentVersion = 1)
int32 + bytes uncompressed header (RTSMapHeader)
deflate stream sections: [int32 tag][int32 length][payload] ...
(built-in sections first, then the CustomSectionWriter sections)
int32 0 end marker
读取器会跳过未知的段,因此你可以在地图文件中存储自己的数据(触发器、初始资源、任务数据),旧版本或其他工具仍然能够读取该地图。注册 RTSMapSerializer.CustomSectionWriter 可在保存时添加段,注册 RTSMapSerializer.CustomSectionReader 可在加载时接收每一个未知段。请使用以 'X' 开头的标签,这样它们就永远不会与内置段冲突。标签 0 为保留值:它标记所有段的结尾。
每个系统都必须使用自己独有的 'X…' 标签,因为每遇到一个未知段都会调用 CustomSectionReader。如果两个写入器输出相同的标签但负载不同,读取器就会收到它无法理解的负载,导致地图加载失败。例如,自定义段中的 StartingGoldSection 使用 XGLD,因此下面的示例使用 XGMD。
using System.IO;
using System.Runtime.CompilerServices;
using RTSTerrainStudio;
using UnityEngine;
// Stores your own game data (here: starting gold and a victory text) inside the .rtsmap file.
// Older versions of the package and other games simply skip the unknown section.
public static class MapGameData
{
public sealed class Data
{
public int startingGold = 500;
public string victoryText = "";
}
// Section tag "XGMD" as a little-endian FourCC. Start your tags with 'X' so they never
// collide with the built-in sections, and use a tag no other script of your project uses.
const int Tag = 'X' | ('G' << 8) | ('M' << 16) | ('D' << 24);
// Bump this when you change the payload layout, and keep reading the old layouts.
const int PayloadVersion = 1;
// One Data object per MapData instance. The table does not keep maps alive.
static readonly ConditionalWeakTable<MapData, Data> s_Data = new ConditionalWeakTable<MapData, Data>();
// Returns the game data of a map (created with defaults on first access).
public static Data Get(MapData map) => s_Data.GetValue(map, _ => new Data());
// Registers the hooks before any scene loads (play mode and builds) and, in the editor,
// after every script reload, so maps saved from the RTS Terrain Studio window keep the data too.
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoadMethod]
#endif
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void Register()
{
// Remove first, so entering play mode without a domain reload never registers twice.
RTSMapSerializer.CustomSectionWriter -= Write;
RTSMapSerializer.CustomSectionWriter += Write;
RTSMapSerializer.CustomSectionReader -= Read;
RTSMapSerializer.CustomSectionReader += Read;
}
// Called while a map is saved: hand every section you want to write to addSection(tag, payload).
static void Write(MapData map, System.Action<int, byte[]> addSection)
{
if (!s_Data.TryGetValue(map, out Data data)) return; // nothing set: write no section
using var stream = new MemoryStream();
using (var w = new BinaryWriter(stream))
{
w.Write(PayloadVersion);
w.Write(data.startingGold);
w.Write(data.victoryText ?? "");
}
addSection(Tag, stream.ToArray());
}
// Called for every section the serializer does not know. Ignore tags that are not yours,
// other scripts may have registered readers too.
static void Read(MapData map, int tag, byte[] payload)
{
if (tag != Tag) return;
using var r = new BinaryReader(new MemoryStream(payload));
int version = r.ReadInt32();
var data = Get(map);
data.startingGold = r.ReadInt32();
if (version >= 1) data.victoryText = r.ReadString();
}
}
MapData 对象。创建新地图对象的操作(加载、调整尺寸、Clone)不会自动把这些数据带过去;你需要从文件中重新读取,或者在克隆或调整地图尺寸时,用自己的代码复制这些数据。常见问题
所有物体都显示为粉色(品红色),哪里出错了?
粉色表示某个着色器无法使用。RTS Terrain Studio 的着色器均为手写,并且只适用于 Universal Render Pipeline(通用渲染管线)。
- 打开 。检查项 Universal Render Pipeline is active 必须为绿色;单击 Fix All(或该检查项的修复按钮)会指定一个 URP 资源。
- 在同一窗口中查看检查项 Package shaders compile。如果某个着色器有错误,Console 窗口会给出它的名称;此时重新导入包中的
Shaders文件夹(右键单击 ▸ Reimport)。 - 如果只有你自己的预制件(树木、岩石、单位)显示为粉色,说明它们的材质使用的是内置渲染管线的着色器。使用 Unity 的渲染管线转换器转换这些材质,或者为它们指定 URP 材质。
我添加了 RTS Terrain,但什么都没有渲染出来。
- 选中地形并查看它的 Inspector 窗口。如果其中提示未加载地图,可以指定一个地图资源、新建一张地图(窗口 ▸ Map 选项卡 ▸ New Map),或者使用 New Map Wizard。
- 确保
RTSTerrain组件已启用。禁用的组件会卸载它的地图。 - 检查摄像机:区块位于地形设置中指定的层(Chunk Layer)上,该层必须包含在摄像机的剔除遮罩中。在 Scene 视图中按 F 聚焦地形:每边 128 个单元格、单元格尺寸为 2 m 的地图宽 256 m。
- 没有图块集的地形会以中性的后备颜色渲染,而不是变得不可见。指定一个图块集,或者运行 并使用演示图块集。
- 在地形的 Inspector 窗口中单击 Rebuild Terrain,即可重新创建所有区块、纹理和通行图。
笔刷绘制的位置与光标所在位置不一致。
编辑器使用 Unity 经过 DPI 校正的 Scene 视图射线,笔刷光标也由地形着色器绘制,因此在常规设置下,光标与绘制位置始终一致,即使显示缩放为 150% 或更高也是如此。如果两者不一致:
- 地形或其父对象经过了缩放,或者绕 X 或 Z 轴旋转过。系统只使用位置和 Y 轴旋转。地形的 Inspector 窗口会对这两种情况发出警告;请把缩放重置为 1、X/Z 旋转重置为 0,并通过修改地图的单元格尺寸来代替缩放。
- 地形使用了 Material Override,而其着色器不绘制笔刷光标。这种情况下你会在正确的位置绘制,但看不到光标。请使用包自带的地形着色器,或者在你的着色器中加入它的笔刷光标代码。
看不到战争迷雾。
- 场景中需要有且只有一个
RTSFogOfWarManager()。 - 运行 并修复 Fog of War renderer feature。该渲染器功能会让屏幕上的所有内容变暗。没有它的话,只有包自带的着色器(地形、水体、植被)会被迷雾覆盖,你自己的单位和道具则不会。
- 在编辑模式下,需要在管理器上启用 Preview In Edit Mode。
- Reveal All 必须关闭。Start Explored 会让整张地图看起来都已探索(变暗而不是全黑)。
- 你的单位需要带有
RTSFogOfWarRevealer,且其队伍为管理器的 Local Team。其他队伍的视野源不会为本地队伍揭示任何区域,因此如果没有本地队伍的视野源,地图会一直被覆盖(默认为黑色)。
植被(或水体、地形)在构建版本中缺失,但在编辑器中正常。
- 运行 并修复 Shaders included in builds。包会把它的着色器保存在
Resources下的一个着色器引用资源中,该资源会在每次 Player 构建之前自动刷新。如果你删除或移动了它,该检查项会重新创建它。 - 修复 GPU instancing on instanced prototypes:实例化的树木、细节和装饰物需要使用启用了 Enable GPU Instancing 的材质。
- 确保地图包含在构建版本中:如果地图以
RTSMapAsset形式指定给某个地形,且该地形所在的场景包含在构建中,地图会被自动包含;从磁盘加载的地图文件必须存在于玩家的电脑上(例如位于RTSMapSerializer.DefaultMapsFolder中)。 - 着色器剥离,或目标平台使用了不同的 URP 资源(Quality 设置),都可能导致某些功能被移除。检查构建所用的 URP 资源是否与编辑器中使用的资源设置相同。
- 在基于 WebGL 2.0 的 Web 构建中,开启了大量功能的 URP 资源可能会超出每次绘制 16 个纹理的上限,导致地形消失。参见移动平台注意事项中关于 OpenGL ES 与 WebGL 2.0 的说明。
- 构建版本中缺少雾效:参见 Player 构建中的雾效。
水体看起来是黑色、灰色,或者没有折射。
- 水体着色器会读取场景的深度和颜色。在 URP 资源上启用 Depth Texture 和 Opaque Texture: ▸ Depth and Opaque Texture enabled ▸ 修复。
- 把 URP 资源的 Opaque Downsampling 设为 None;降采样会使折射变模糊。
- 检查摄像机:它的 Depth Texture 和 Opaque Texture 覆盖设置不能设为 Off。水体会遵循每个摄像机自己的设置,而 Scene 视图始终使用 URP 资源中的设置。
- 对于 Web 构建,检查 Web 构建所用质量级别的 URP 资源(检查项 Web quality levels have Depth and Opaque Texture,参见 Project Validator 中的 Web 检查)。
- 水体颜色来自图块集的 Water Profile。请在那里检查浅水和深水的颜色。
如何获得最佳性能?
- 必须启用 Burst()。笔刷、通行、战争迷雾、地图生成器和小地图都以 Burst 作业的形式运行。
- Chunk size(区块大小,位于地形设置中):16、32(默认)或 64 个单元格。区块越小,编辑时重建越快;区块越大,游戏中的 Draw Call 越少。
- Surface Subdivisions:设为 1 可以用最少的三角形获得经典 RTS 外观,设为 2–4 可以得到更平滑的丘陵。
- 如果你只使用查询 API(
Raycast、GetHeight),可以把 Collider Mode 设为 None,以跳过网格碰撞体。 - 树木和装饰物优先使用 Instanced 生成模式;只有当游戏玩法需要把对象作为 GameObject 处理时(可采伐的树木、带脚本的装饰物),才使用 GameObject 模式。
- 对于性能较弱的硬件,降低植被渲染器上的 Detail Density Multiplier 和 Detail Distance Multiplier。
- 战争迷雾:把 Resolution Per Cell 设为 1 并增大 Update Interval,开销更低。
- 运行地图验证器:当树木、装饰物、GameObject 对象或细节图层过多时,它会给出性能警告。
可以在一个场景中使用多个地形吗?
可以。每个 RTSTerrain 都有自己的地图、图块集、库和 Transform。RTSTerrain.All 列出所有地形,RTSTerrain.FindAt(position) 查找某个位置下方的地形,RTSTerrain.RaycastAll 对所有地形执行射线检测。在编辑器窗口顶部的标题区域中选择要编辑的地形(在场景中选中某个地形也会自动将其设为编辑对象);在游戏内编辑器中,使用 Map ▸ Edited terrain。注意事项:
- 撤销历史属于当前编辑的地形:切换编辑的地形会清空撤销历史。
- 战争迷雾管理器只覆盖一个地形(即它的 Terrain 字段指定的地形;该字段为空时使用
RTSTerrain.Main)。 - A* 需要为每个地形配置一个图构建器,并使用各自的图名称;NavMesh 集成则需要每个地形一个构建器。
可以移动或旋转地形吗?
可以:位置和 Y 轴旋转都完全支持,在运行时也一样。所有查询、笔刷、植被、装饰物、水体、通行、战争迷雾以及各项集成都会跟随 Transform。不支持绕 X 或 Z 轴旋转,也不支持缩放(Inspector 窗口会发出警告);如需放大地图,请使用 Map 选项卡上的 Resize,或者使用更大的单元格尺寸。
它支持 Input System、旧的 Input Manager,还是两者都支持?
的三种设置都可以正常使用。运行时代码通过 RTSInput 读取输入:如果已安装并启用 Input System 包,就使用它,否则使用旧版 Input Manager。 中的 Input handling 检查项会显示你的项目使用的是哪一种。你自己的游戏玩法代码也可以使用 RTSInput(如 PointerPosition、GetMouseButtonDown、GetKey(RTSKey) 等),这样无论使用哪种设置,代码都能正常运行。字母快捷键如需跟随玩家的键盘布局,可使用 GetKeyDownByLabel(RTSKey);[ 或 + 等符号键则使用 GetCharacterDown(char)。
可以把游戏或游戏内编辑器发布到网页浏览器中吗?
可以。在桌面浏览器中,Web 构建(WebGL 2.0)能够运行地形、植被、水体、战争迷雾、游戏内编辑器和示例,并通过鼠标和键盘操作。通过 安装全窗口模板,并阅读 Web(WebGL)构建一节,了解相关设置、地图在浏览器中的存储方式以及网站托管注意事项。
可以把包移动到其他文件夹吗?
可以。没有任何内容依赖固定路径;包会根据自身所在的位置查找它的文件。
故障排除
请按从上到下的顺序逐项检查这份清单;大多数问题在前三步就能解决。
- 查看 Console 窗口。本包的消息以
[RTS Terrain Studio]开头,并链接到所属的对象(单击消息即可选中该对象)。先修复编译错误:只要有任何脚本编译失败,其他内容都不会更新。 - 验证项目。 会检查 URP、深度纹理和不透明纹理、战争迷雾渲染器功能、构建中保留的雾效模式、色彩空间、Burst/Collections/Mathematics、输入处理、着色器编译、构建中的着色器、图块集纹理数组、GPU 实例化、地形是否有图块集以及地形是否保存到地图文件;当 Web 为当前平台时,还会检查 Web 质量级别和 Web 模板。单击 Fix All(会先弹出对话框列出每项更改),然后单击 Run Again。
- 检查地形的 Inspector 窗口。是否已加载地图、指定图块集、启用组件?是否有关于缩放或旋转的警告?单击 Rebuild Terrain。
- 验证地图。Map 选项卡 ▸ Validation ▸ Validate Map 会找出缺失的原型、损坏的地面涂层、无法到达的出生点以及性能问题。
- 检查内容。纹理、植物或装饰物缺失,通常意味着没有指定图块集或库,或者地图中的某个原型 ID 在库中不存在。重新运行 可以恢复演示资源。
- 检查集成。移除 Mirror 后
Integrations中出现编译错误,说明MIRROR符号仍处于定义状态(移除 Mirror)。如果某个集成在 Welcome 窗口中标记为 Installed 但不是 Active,说明它的版本不符合要求。 - 支持禁用域重载的 Play 模式。如果某些功能在第二次 Play 之后表现不同,可以临时启用一次域重载,看看原因是否出在你自己的静态状态上。
- 仍未解决?在一个新场景中,用 创建的地形和演示内容重现问题;寻求帮助时,请附上 Unity 版本、URP 版本、Console 窗口输出以及重现步骤。
| 症状 | 可能原因 | 参见 |
|---|---|---|
| 材质显示为粉色 | URP 未启用、着色器错误、预制件使用了内置管线材质 | 常见问题 |
| 地形不可见 | 未加载地图、组件已禁用、区块所在的层被剔除 | 常见问题 |
| 笔刷落点偏离光标 | 地形被缩放或存在 X/Z 旋转 | 常见问题 |
| 没有战争迷雾 | 缺少渲染器功能、没有本地队伍的视野源、Reveal All 已开启 | 常见问题 |
| 水体发黑 | Depth / Opaque Texture 未开启 | 常见问题 |
| 构建版本中植被缺失 | 着色器引用、GPU 实例化 | 常见问题 |
| 单位不移动 | 通行图尚未构建、起点或终点不可行走、寻路提供器没有图或 NavMesh | 地图验证、集成 |
| 生成器放置的出生点少于预期 | 没有足够的平坦空间来满足 Flat Radius / Edge Margin | 生成器设置 |
| 导入的高度图过于平坦或过于极端 | 代码中的高度缩放为 0,或 RAW 尺寸、字节序错误 | 高度图 |
| 移除 Mirror 后出现与之相关的编译错误 | 残留的 MIRROR 定义 | 移除集成 |
| 出现对话框 RTS Terrain Studio requires URP | 未安装 URP 17.3,或编辑器版本低于 Unity 6.3 | Unity 与渲染管线 |
| Web 构建:UI 过小、地图丢失、按键触发浏览器功能 | 固定尺寸的模板、浏览器存储、浏览器快捷键 | Web(WebGL)构建 |
更新日志
版本 1.0.0 —— 首次发布
需要 Unity 6.3 LTS(6000.3)和通用渲染管线(URP)17.3,以及 Burst、Collections 和 Mathematics。
地图模型
- 经典 RTS 风格的单元格与角点顶点栅格,每边 16 到 1024 个单元格(16 的倍数),单元格尺寸可自由设置。
- 16 个悬崖层级,崖壁程序化生成且轮廓圆润;每个层级之上可叠加精细高度;坡道;每个图块集 4 种悬崖类型。
- 每个图块集 16 个地面图层,存储在高分辨率 Splat 贴图中;基于高度的混合,可选的三平面映射和反平铺;支持任意纹理尺寸和格式。
- 按顶点存储的水体,分为浅水和深水。
- 自动推导的通行图(可行走、可建造、可飞行、浅水和深水),以及可手动绘制的通行覆盖。
- 区域、出生点、标记点、地图信息和环境光照都存储在地图中。
编辑
- Unity 编辑器与游戏内编辑器共用同一套工具:高度、悬崖、绘制(柔和、图块、填充、自动绘制)、植被、装饰物、水体、通行和标记点。
- 笔刷支持圆形、方形、菱形和纹理图章形状,以及衰减曲线、旋转、间距和抖动;对称绘制(X/Z 镜像、四向镜像、二重和四重旋转对称)。
- 带内存预算的撤销和重做,并与 Unity 的撤销系统集成。
- 可停靠的 RTS Terrain Studio 窗口;与 Scene 视图集成,提供投影式笔刷光标和叠加层;所有资源的自定义 Inspector 和导入器;Welcome 窗口、New Map Wizard 和项目验证器。
- 基于 UI Toolkit 构建的游戏内编辑器:工具栏、工具与笔刷面板、选择面板、地图浏览器、快捷键和试玩。
地图工具
- 基于种子的程序化地图生成器,提供六种风格、对称布局、坡道、湖泊与岛屿、地图边界、自动绘制、植被、装饰物和出生点;预设可保存。
- 16 位 PNG 和 RAW 高度图的导入(三种模式)与导出,以及 Splat 图像导出。
- 小地图烘焙器、地图验证器、基于锚点调整地图尺寸。
- 编辑器与游戏共用的二进制
.rtsmap格式,支持自定义段。
运行时系统
- 在 Burst 作业中构建的分块地形网格、可选的网格碰撞体,以及不依赖物理系统的查询 API。
- 支持风力摆动和剔除的 GPU 实例化植被;树木可作为实例或 GameObject。
- 水体渲染,支持随深度变化的水色和折射;适用于任意预制件的装饰物管理器。
- 经典 RTS 战争迷雾:按队伍计算可见性、记忆已探索区域、视线会被悬崖和阻挡物遮挡、共享视野、小地图支持。
- 内置 Burst A* 寻路器,按净空区分不同的单位尺寸,并支持路径平滑。
- 示例:RTS 摄像机、选择、带阵型的移动命令、测试单位、小地图 UI、演示场景和自动生成的演示内容。
平台
- 着色器以 Shader Model 3.5 为目标,因此本包也能在 OpenGL ES 3.0 上以及 Web 构建(WebGL 2.0)中运行。
- Web 构建:地图保存在浏览器存储中;支持 Download Map、Import Map 和拖放;有未保存的更改时,离开页面前会发出警告;提供适配浏览器的键盘快捷键、全窗口 Web 模板,以及项目验证器中的 Web 检查项。
- 图块集纹理数组会在 Player 构建之前自动重建,以匹配该构建的纹理压缩格式。
- 未安装 URP 17.3 时,本包会保持未激活状态并说明如何安装 URP,而不是显示编译错误。
集成
- Mirror:带缓存的地图流式传输、服务器权威的单位、按队伍计算的战争迷雾、基于战争迷雾的兴趣管理。
- A* Pathfinding Project Pro:由通行图生成的栅格图(Grid Graph),支持增量更新,以及
AIPath和 RVO 单位。 - Unity AI Navigation:根据地形和通行图构建的 NavMesh、增量更新、寻路提供器和 NavMeshAgent 模式。