Godot双网格瓦片地图系统:16个瓦片实现无限地形平滑过渡
1. 项目概述:从“无限”到“平滑”的挑战
做2D游戏,尤其是开放世界或者大地图探索类的,最头疼的问题之一就是地图的无限扩展与视觉上的平滑衔接。你肯定不想让玩家走两步就看到一个生硬的、像补丁一样的地形边缘,那太出戏了。传统的瓦片地图(TileMap)系统,虽然高效,但在处理地形过渡时,往往需要海量的、预先绘制好的过渡瓦片(Terrain Autotile),一个简单的三阶过渡(比如草地到泥土)就可能需要47个瓦片。这不仅仅是美术工作量爆炸的问题,更关键的是,它限制了地图的动态生成和编辑的灵活性。
“用16个瓦片实现无限地形平滑过渡”,这个标题一下子就抓住了我的痛点。它背后的核心,就是Godot引擎下的双网格瓦片地图系统。这不是一个现成的插件,而是一套设计思想和实现方案。简单来说,它用两套瓦片层(一个基础地形层,一个细节过渡层)和一套巧妙的混合规则,让区区16个基础瓦片就能模拟出近乎无限的自然地形过渡效果。我最初看到这个思路时,第一反应是“这怎么可能?”,但深入研究并亲手实现后,不得不佩服其精巧。它特别适合程序化生成地图、需要大量地形类型、且对美术资源预算敏感的项目。如果你正在为你的Godot 2D项目寻找一个轻量、灵活且效果出色的地形解决方案,这套系统值得你花时间吃透。
2. 核心思路拆解:为什么是“双网格”?
要理解这套系统,我们得先抛开代码,看看它到底想解决什么问题,以及是如何从结构上破局的。
2.1 传统瓦片地图的瓶颈
Godot自带的TileMap节点配合TileSet资源,是制作2D网格地图的利器。它的地形自动拼接(Terrain)功能,能根据相邻瓦片的类型,自动选择正确的过渡瓦片。但这套系统强依赖于预设的“瓦片集”(TileSet)规则。例如,你有草地、沙地、水域三种地形,为了它们之间两两平滑过渡,你需要为每一对组合绘制大量的边缘、角落、内部填充等瓦片。地形类型越多,这个组合数量是指数级增长的(n种地形,理论最多需要 3n² - 2n 个瓦片位)。这导致了:
- 资源爆炸:美术需要绘制成百上千个几乎雷同的过渡瓦片。
- 僵化不灵活:一旦地形规则设定好,动态修改(比如在运行时用技能把一块草地烧成焦土)就非常麻烦,需要动态更换整个瓦片集或处理复杂的重计算。
- 过渡生硬:即便瓦片齐全,过渡也往往局限于固定的几种模式,缺乏更细腻的、随机的混合感。
2.2 双网格系统的破局之道
双网格系统的核心思想是解耦和分层渲染。它将“地形主体”和“地形过渡”这两个任务,分配给两个独立的网格层来处理。
基础网格层(Base Grid):
- 职责:定义地图上每个格子(Cell)的主要地形类型。比如,某个格子是“草地”,另一个是“泥土”。这一层只使用最简单的、无过渡的纯色或基础纹理瓦片。它的分辨率可以比较低,定义大块的地形区域。
- 特点:数据量小,逻辑简单。它存储的是地图的“语义”信息(这里是什么地形)。
过渡网格层(Blend Grid / Detail Grid):
- 职责:专门负责在不同地形类型的边界处进行视觉上的平滑混合。这一层网格的分辨率是基础层的两倍(即一个基础格子对应2x2个过渡格子)。它使用的是一套特殊的、数量极少的“过渡瓦片”(正是那16个)。
- 特点:不关心某个区域的主体是什么,只关心“边界在哪里”以及“边界两侧是什么”。通过高分辨率和高频的细节瓦片,来模拟平滑的渐变。
关键比喻:你可以把基础层想象成用粗蜡笔画出的色块分区图(草地用绿色块,沙地用黄色块)。而过渡层则像是一支细尖笔,在这些色块的边界线上,进行精细的描边、晕染和纹理叠加,让生硬的边界变得模糊、自然。
2.3 “16个瓦片”的奥秘
这是最精妙的部分。为什么是16个?这16个瓦片不是随意画的,它们是一套完备的、用于模拟线性插值(Blending)的图案集。
想象两个地形A和B的边界。在边界处,视觉上应该是A逐渐过渡到B。如果我们把过渡抽象成一个2x2的像素块(对应过渡层的一个最小单元),这个块内A和B的分布方式,本质上就是四种角(00, 01, 10, 11)的混合权重问题。通过精心设计这16个瓦片(可能包含全A、3/4 A + 1/4 B、半A半B的两种对角线方式、1/4 A + 3/4 B、全B等基本形态,以及一些带有噪波纹理的变体),并通过旋转(Rotate)和翻转(Flip)变换,就能组合出足够多的边界视觉效果,模拟出连续的渐变。
这16个瓦片是“程序化纹理”的思想在瓦片领域的应用。它们不是具体的草地-泥土过渡图,而是抽象的混合蒙版。实际渲染时,游戏引擎(通过自定义着色器或混合材质)会根据基础层记录的地形类型,动态地为这些蒙版瓦片着色(例如,用草地的颜色和泥土的颜色进行插值),从而实时“生成”千变万化的具体过渡效果。
注意:这16个瓦片通常是灰度图或高度图,白色代表完全显示地形A,黑色代表完全显示地形B,灰色代表混合。在Godot中,我们需要通过Shader,根据这个灰度值和当前格子记录的两个地形ID,去采样对应的两种地形纹理进行混合。
3. 系统设计与关键技术点
理解了思想,我们来搭建它。一个可用的双网格系统,需要处理好数据、渲染和逻辑三个层面。
3.1 数据结构设计
我们需要在代码中管理两个网格的数据。
# 示例:使用 Dictionary 或 Array 存储网格数据 var base_grid = {} # 键为 Vector2i 基础网格坐标,值为地形类型ID (int) var blend_grid = {} # 键为 Vector2i 高分辨率过渡网格坐标,值为过渡瓦片ID + 变换信息 # 或者,为了性能,可以使用 PoolVector2Array 和 PoolIntArray,但 Dictionary 更直观用于原型设计。基础网格:存储简单。每个位置一个terrain_id。
过渡网格:存储复杂一些。每个位置可能需要存储:
blend_tile_id:使用哪一张过渡蒙版瓦片(0-15)。terrain_a_id和terrain_b_id:这条边界混合的是哪两种地形。transform:该瓦片的旋转和翻转状态(0-7,对应Godot的TileSet变换)。
3.2 核心算法:过渡网格的生成
这是系统的“大脑”。当基础网格发生变化(编辑或生成)时,我们需要自动更新过渡网格。算法步骤如下:
- 遍历每一个基础网格单元格。
- 对于当前基础单元格,检查其四邻域(上、下、左、右)的地形类型。
- 如果邻域单元格与当前单元格类型不同,那么它们之间就存在一条边界。这条边界会影响到当前基础单元格对应的2x2过渡网格中的特定子单元格。
- 例如,如果上方邻居类型不同,则会影响当前基础格对应过渡网格的顶部一行(两个子格)。
- 如果左方邻居类型不同,则会影响左侧一列。
- 角落情况(左上、右上等)则需要同时考虑水平和垂直邻居,用于生成对角线混合或内凹角。
- 为受影响的过渡子单元格计算混合信息:
- 确定
terrain_a_id(当前基础格类型)和terrain_b_id(邻居类型)。约定一个顺序(如ID小的为A)。 - 根据边界的方向和位置,从预设的16个过渡蒙版瓦片中,选择一个最匹配的
blend_tile_id。例如,纯粹的右侧边界可能对应一个“从左到右线性渐变”的蒙版。 - 确定是否需要旋转或翻转
transform,以使蒙版的方向与边界对齐。
- 确定
- 处理重叠:一个过渡子单元格可能被多个边界影响(比如它正好在一个角落)。这就需要定义优先级或混合规则。一种简单有效的策略是**“最近原则”或“主类型优先”**:只记录最主要(如最先计算或地形ID占优)的一个边界混合。更复杂的方案可以引入第三层进行细节叠加,但初期用简单规则即可。
- 将计算好的
(blend_tile_id, terrain_a_id, terrain_b_id, transform)写入blend_grid。
这个算法可以预先为整个地图跑一遍(用于静态地图),也可以在玩家修改地形时局部运行(用于动态地图),性能开销可控。
3.3 渲染实现:Shader驱动的动态混合
数据有了,怎么画出来?这是“魔法”发生的地方。我们需要两个TileMap节点:
- BaseTileMap:使用普通的
TileSet,包含各地形的基础纹理瓦片。直接根据base_grid绘制。 - BlendTileMap:使用一个特殊的
TileSet,其中只包含那16张灰度蒙版瓦片。根据blend_grid绘制这些蒙版。
关键在于BlendTileMap的材质。我们需要为它附加一个CanvasItem Shader。
// 简化版的片段着色器 (Fragment Shader) 概念 shader_type canvas_item; uniform sampler2D terrain_textures[8]; // 假设最多8种地形纹理 uniform vec4 terrain_colors[8]; // 每种地形的颜色(可选,用于纯色地形) varying flat int terrain_a_id; // 从TileMap的custom_data中传入 varying flat int terrain_b_id; void fragment() { // 1. 采样蒙版瓦片的颜色(即灰度值) vec4 mask_color = texture(TEXTURE, UV); float blend_factor = mask_color.r; // 假设蒙版存储在r通道 // 2. 根据地形ID,采样两种地形的纹理或颜色 vec4 color_a = texture(terrain_textures[terrain_a_id], UV); vec4 color_b = texture(terrain_textures[terrain_b_id], UV); // 3. 线性混合 COLOR = mix(color_a, color_b, blend_factor); // 更高级的混合:可以加入噪波扰动blend_factor,让边界更自然 // float noise = texture(noise_texture, WORLD_VERTEX.xy * 0.1).r * 0.2 - 0.1; // COLOR = mix(color_a, color_b, clamp(blend_factor + noise, 0.0, 1.0)); }如何传递terrain_a_id和terrain_b_id给Shader?Godot的TileMap允许为每个单元格设置custom_data。我们可以在写入blend_grid时,将这两个ID打包成一个Color或两个整数,存入TileData的custom_data中。然后在Shader中通过varying或直接读取INSTANCE_CUSTOM(Godot 4.x)来获取。
这样,渲染时,引擎会先画出基础地形层,然后在上面画出过渡层。过渡层的每个像素,都由Shader根据蒙版灰度和两种地形纹理,实时计算出最终颜色,实现了动态的、平滑的、无限组合的地形过渡。
4. 在Godot中的分步实现
理论说再多不如动手。下面我以Godot 4.x为例,拆解实现步骤。
4.1 第一步:准备资源
- 创建基础地形纹理:准备几种简单的地形纹理,比如
grass.png,dirt.png,sand.png,stone.png。尺寸可以是64x64或128x128,确保它们能无缝平铺(seamless)。 - 创建16过渡蒙版瓦片:这是最需要设计感的一步。你可以用任何图像软件(Aseprite, Photoshop等)绘制一套16张的16x16或32x32的灰度图。这套图案应涵盖:
- 全白(100% A)
- 全黑(100% B)
- 左白右黑(垂直边界)
- 上白下黑(水平边界)
- 四种不同的对角线渐变(从全白到全黑的对角线,以及不同比例的三角形填充)。
- 一些带有随机噪点或有机形状的过渡图案,用于打破机械感。
- 将这套图保存为一张图集(atlas),或者单独16个文件。
4.2 第二步:设置TileSet
基础TileSet:
- 新建一个
TileSet资源。 - 添加一个
Terrain Set(地形集),然后为每种地形(草地、泥土等)创建一个Terrain。 - 将你的基础地形纹理导入为瓦片,并分配给对应的地形。注意:这里我们禁用或不用它的自动拼接功能,我们只用它来画纯色块。所以每个地形只关联一个瓦片即可。
- 新建一个
过渡蒙版TileSet:
- 新建另一个
TileSet资源。 - 不需要设置
Terrain。直接创建一个Atlas源(如果你的蒙版是图集)或多个Single Tile源。 - 导入你的16张蒙版灰度图。为每一个瓦片设置一个唯一的ID(0-15)。
- 关键:在这个
TileSet的资源属性中,为其Material槽分配一个新建的ShaderMaterial。这个材质将使用我们后面写的混合Shader。
- 新建另一个
4.3 第三步:创建场景与节点
- 新建一个2D场景。
- 添加两个
TileMap节点,分别命名为BaseTileMap和BlendTileMap。 - 将第一步创建的两个
TileSet资源,分别拖给对应的TileMap节点的Tile Set属性。 - 确保
BlendTileMap的Z Index比BaseTileMap高(例如,Base是0,Blend是1),这样过渡层会绘制在基础层之上。 - 添加一个自定义节点(比如
GridManager)作为这两个TileMap的父节点,用于存放我们的核心逻辑脚本。
4.4 第四步:编写网格管理逻辑
在GridManager.gd中,我们需要实现之前提到的数据结构和算法。
extends Node2D @onready var base_tilemap: TileMap = $BaseTileMap @onready var blend_tilemap: TileMap = $BlendTileMap # 地形类型枚举 enum TerrainType {GRASS = 0, DIRT = 1, SAND = 2, STONE = 3} # 存储网格数据(简单示例,未做优化) var base_grid: Dictionary = {} var blend_grid: Dictionary = {} # 初始化或生成基础地图 func generate_base_map(width: int, height: int): base_grid.clear() # 这里可以用噪声生成,或者简单划分区域 for x in width: for y in height: var terrain = TerrainType.GRASS # 默认草地 # 示例:中间一块是泥土 if x > width/3 and x < 2*width/3 and y > height/3 and y < 2*height/3: terrain = TerrainType.DIRT base_grid[Vector2i(x, y)] = terrain # 绘制基础层 base_tilemap.set_cell(0, Vector2i(x, y), 0, Vector2i(terrain, 0)) # 生成过渡层 update_blend_grid(width, height) # 核心:更新过渡网格 func update_blend_grid(width: int, height: int): blend_grid.clear() blend_tilemap.clear() # 清除之前绘制的过渡层 # 遍历每个基础格 for cell in base_grid.keys(): var current_terrain = base_grid[cell] var neighbors = [ {dir = Vector2i(0, -1), edges = [Vector2i(0,0), Vector2i(1,0)]}, # 上:影响当前格过渡网格的顶部两子格 {dir = Vector2i(0, 1), edges = [Vector2i(0,1), Vector2i(1,1)]}, # 下:影响底部两子格 {dir = Vector2i(-1, 0), edges = [Vector2i(0,0), Vector2i(0,1)]}, # 左:影响左侧两子格 {dir = Vector2i(1, 0), edges = [Vector2i(1,0), Vector2i(1,1)]}, # 右:影响右侧两子格 ] for neighbor_info in neighbors: var neighbor_pos = cell + neighbor_info.dir if base_grid.has(neighbor_pos): var neighbor_terrain = base_grid[neighbor_pos] if neighbor_terrain != current_terrain: # 发现边界!为受影响的过渡子格设置数据 for subcell_offset in neighbor_info.edges: # 计算过渡层高分辨率坐标 var blend_cell_pos = cell * 2 + subcell_offset # 确定地形A和B(按ID排序,保证一致性) var terrain_a = current_terrain var terrain_b = neighbor_terrain if terrain_a > terrain_b: var temp = terrain_a terrain_a = terrain_b terrain_b = temp # 注意:如果交换了,混合因子可能需要反转(1.0 - blend_factor),这取决于蒙版设计 # 根据边界方向,选择合适的蒙版瓦片ID和变换 var blend_data = calculate_blend_data(neighbor_info.dir, subcell_offset) # 存储到网格 blend_grid[blend_cell_pos] = { "tile_id": blend_data.tile_id, "terrain_a": terrain_a, "terrain_b": terrain_b, "transform": blend_data.transform } # 根据blend_grid绘制过渡层 draw_blend_layer() # 根据边界方向和子格位置,计算该用哪个蒙版瓦片以及如何变换 func calculate_blend_data(boundary_dir: Vector2i, subcell: Vector2i) -> Dictionary: # 这是一个简化的示例,实际逻辑需要映射到你那16个蒙版瓦片的设计规则 # 例如,假设我们的蒙版集里: # 瓦片0: 全A # 瓦片1: 从左到右线性渐变 (A->B) # 瓦片2: 从上到下线性渐变 # 瓦片3: 左上三角为A,其余为B # ...等等 # transform: 0-7 对应 Godot TileSet 的自动变换 var tile_id = 0 var transform = 0 if boundary_dir == Vector2i(1, 0): # 右边界 tile_id = 1 # 使用从左到右渐变的蒙版 # 如果subcell是(1,0)或(1,1),可能需要不同的变换?这里逻辑需细化 elif boundary_dir == Vector2i(0, 1): # 下边界 tile_id = 2 # 使用从上到下渐变的蒙版 transform = 0 # 可能需要旋转 # ... 其他边界和角落情况 return {"tile_id": tile_id, "transform": transform} # 将blend_grid的数据绘制到BlendTileMap上 func draw_blend_layer(): for cell_pos in blend_grid.keys(): var data = blend_grid[cell_pos] # 设置单元格,使用蒙版瓦片ID blend_tilemap.set_cell(0, cell_pos, data.tile_id, Vector2i.ZERO, data.transform) # 关键:将地形ID信息存入TileData的custom_data,供Shader读取 var tile_data = blend_tilemap.get_cell_tile_data(0, cell_pos) if tile_data: # 将两个地形ID打包到一个Color中(r通道存A,g通道存B) var custom_color = Color(data.terrain_a / 255.0, data.terrain_b / 255.0, 0, 0) tile_data.set_custom_data("terrain_ids", custom_color)4.5 第五步:编写混合Shader
为BlendTileMap的TileSet创建材质和着色器。
- 在资源面板,新建一个
ShaderMaterial。 - 为其新建一个
Shader。 - 在着色器中,写入类似前面提到的混合逻辑。这里需要根据Godot 4.x的语法调整。
// BlendShader.gdshader shader_type canvas_item; // 在外部通过Material设置这些纹理数组(Godot 4支持纹理数组) uniform sampler2DArray terrain_textures; // 从TileData的custom_data中读取地形ID uniform int terrain_a_id; uniform int terrain_b_id; void fragment() { // 获取当前像素在瓦片内的UV vec2 tile_uv = UV; // 采样蒙版颜色(我们的过渡瓦片是灰度图) vec4 mask = texture(TEXTURE, tile_uv); float blend = mask.r; // 采样两种地形纹理。注意:需要将地形ID映射到纹理数组的层索引。 // 这里假设terrain_id直接对应层索引。 vec4 color_a = texture(terrain_textures, vec3(tile_uv, float(terrain_a_id))); vec4 color_b = texture(terrain_textures, vec3(tile_uv, float(terrain_b_id))); // 线性混合 COLOR = mix(color_a, color_b, blend); }重要:在GridManager的draw_blend_layer函数中,我们通过set_custom_data设置了颜色,但Shader中如何获取?在Godot 4中,更标准的方式是在TileSet中为过渡瓦片配置Custom Data Layer,然后在Shader中使用INSTANCE_CUSTOM内置变量。步骤更复杂一些,但原理相通:将数据从代码传递到每个瓦片实例,再传入Shader。
4.6 第六步:连接与测试
- 在
GridManager的_ready()函数中调用generate_base_map(50, 50)生成一个测试地图。 - 运行场景。你应该能看到基础地形块,以及它们之间由
BlendTileMap绘制的、经过Shader混合的平滑边界。 - 尝试动态修改
base_grid中的某个单元格的地形类型,然后调用update_blend_grid局部更新,观察过渡层是否实时变化。
5. 性能优化与高级技巧
基础系统跑通后,面对大地图,性能是关键。以下是几个优化方向:
5.1 数据存储优化
使用Dictionary虽然灵活,但内存和访问效率不是最优。对于大型静态地图,可以考虑:
- 使用二维数组(Array of Arrays):访问更快,内存连续。
- 使用位压缩:如果地形类型少于256种,
terrain_id可以用uint8_t存储。两个地形ID可以打包成一个16位整数。 - 分块加载(Chunking):将无限地图划分为区块(Chunk)。只维护和更新玩家视野内及邻近区块的网格数据。这是开放世界游戏的标配。
5.2 渲染优化
- 合并绘制调用:确保
BaseTileMap和BlendTileMap使用的纹理是图集(Atlas),减少状态切换。 - Shader优化:
texture采样是耗能操作。确保terrain_textures是纹理数组(sampler2DArray),这比采样多个独立的sampler2Duniform效率高得多。避免在Shader中进行复杂的逻辑判断。 - 视口裁剪:Godot的
TileMap本身会进行视口裁剪,但确保你的GridManager在更新逻辑时也只更新视野内的区块。 - 细节层级(LOD):在远景处,可以降低过渡层网格的分辨率(比如从2倍降到1倍,甚至不显示过渡层),用更粗糙但性能开销更低的方式渲染。
5.3 效果增强
- 添加噪波:在Shader的混合因子
blend上添加一些基于世界坐标的噪波(如Simplex噪声),可以让边界呈现更自然、不规则的锯齿状,彻底摆脱网格感。float noise = texture(noise_tex, WORLD_VERTEX.xy * 0.05).r * 0.3 - 0.15; float final_blend = clamp(blend + noise, 0.0, 1.0); COLOR = mix(color_a, color_b, final_blend); - 多地形混合:当前系统只处理两两混合。对于三种地形的交界点(如草地、泥土、沙地的汇合点),效果可能不完美。一个进阶方案是引入第三层“细节层”,或者在Shader中支持三次混合(权重更复杂)。
- 高度图与法线:如果你的过渡蒙版不仅是颜色混合,还代表了高度(白色高,黑色低),你可以在Shader中同时输出到法线贴图(Normal Map),让地形边界产生视觉上的凹凸感,进一步提升立体效果。
6. 常见问题与避坑指南
在实际实现过程中,我踩过不少坑,这里总结一下:
过渡层出现“缝隙”或错位:
- 原因:最可能的原因是基础网格与过渡网格的坐标转换没对齐。记住,如果基础网格单元格大小是16x16,那么过渡网格单元格大小应该是8x8(2倍分辨率),并且在设置
BlendTileMap的cell_quadrant_size和cell_size属性时,要与BaseTileMap按比例匹配。 - 检查:分别关闭基础层和过渡层,看它们是否完美覆盖预期区域。用调试线条画出网格线。
- 原因:最可能的原因是基础网格与过渡网格的坐标转换没对齐。记住,如果基础网格单元格大小是16x16,那么过渡网格单元格大小应该是8x8(2倍分辨率),并且在设置
Shader混合颜色错误或全黑:
- 原因A:
terrain_a_id和terrain_b_id没有正确传递到Shader。在Godot 4中,确保在TileSet编辑器中为过渡瓦片所在的图集源创建了Custom Data Layer,并正确设置了类型(如Vector2用于存两个ID)。在代码中通过tile_data.set_custom_data(“terrain_ids”, Vector2(id_a, id_b))来设置。 - 原因B:Shader中采样纹理数组的索引不对。确保
terrain_textures这个纹理数组资源已正确赋值给材质,并且地形ID与数组层索引对应(从0开始)。 - 调试:在Shader中先用固定颜色输出,比如
COLOR = vec4(1.0, 0.0, 0.0, 1.0);看过渡层是否显示红色。再逐步替换为采样和混合逻辑。
- 原因A:
动态更新性能差:
- 对策:不要每次变化都全图更新
update_blend_grid。实现一个局部更新函数update_blend_cell(cell_pos),只更新受影响的基础格及其邻居(3x3或5x5区域)对应的过渡网格区域。
- 对策:不要每次变化都全图更新
16个蒙版瓦片不够自然,边界看起来重复:
- 对策:这是蒙版设计的问题。确保你的16个瓦片包含足够多的有机图案和噪波变体。此外,充分利用
TileMap的随机变换(随机旋转和翻转)功能。在calculate_blend_data函数中,可以为同一种边界模式随机从几个相似的蒙版中挑选一个,并赋予随机的变换,能极大增加视觉多样性。
- 对策:这是蒙版设计的问题。确保你的16个瓦片包含足够多的有机图案和噪波变体。此外,充分利用
与Godot内置地形系统冲突:
- 注意:我们的
BaseTileMap不要启用Godot内置的Terrain模式,或者只使用它的“匹配角落和边”中的最简单模式。我们的过渡完全由BlendTileMap和Shader控制。两个系统混用会导致不可预料的结果。
- 注意:我们的
这套双网格瓦片地图系统,初看有些复杂,但一旦搭建起来,就像拥有了一把瑞士军刀。它用可控的资源成本,换来了极大的地形表现灵活性和动态性。对于独立开发者和小团队来说,在美术资源有限的情况下,这是提升游戏世界质感的一个非常有效的技术手段。我自己的项目从传统的庞大Terrain Set切换到这套系统后,不仅包体缩小了,地图编辑和程序化生成的效率也大大提高。如果你遇到了地形艺术的瓶颈,不妨试试这个思路。