OpenFrontIO 地图制作全指南:用 Go 版 MapGenerator 从 PNG 生成可玩地图

原创2026-10-03 09:28:191,262 阅读
文章标签:游戏开发后端

OpenFrontIO 地图制作全指南:用 Go 版 MapGenerator 从 PNG 生成可玩地图

导读

OpenFrontIO(在线浏览器端 RTS 游戏)的全部游戏地图——从世界地图、大洲图到自定义虚构岛屿——都由仓库中的 Go 工具 MapGenerator 生成与维护。本文以 docs/Maps.md 为主线,系统讲解地图管线:如何从一张 PNG 灰度/蓝通道图片出发,配置 info.json 元数据,运行生成器产出二进制地图与缩略图,以及如何让新地图进入游戏。读完你将掌握创建一张可玩地图的完整实操流程、info.json 全部字段的语义、像素到地形的编码规则,以及生成器的命令行与日志调试技巧。

地图管线总览:PNG 像素 → 二进制地形

OpenFront 的地图制作遵循“源素材在 map-generator/assets/maps/,产物在 resources/maps/”的单向管线:

  1. 每个地图是一个文件夹 map-generator/assets/maps/<map_name>/,内含 image.png(地形图)与 info.json(元数据);
  2. go run . 读取 PNG,主要依据蓝色通道(Blue channel) 把像素映射为 Land / Water / Impassable 三种地形类型,并为陆地/水域计算 Magnitude(幅度);
  3. 生成器输出三档二进制地图数据(全尺寸、1/4、1/16)与 WebP 缩略图到 resources/maps/<map_name>/;
  4. 同时自动重写 src/core/game/Maps.gen.ts(GameMapType 枚举与 maps 列表)和 resources/lang/en.json 的 map 段——无需任何手工步骤,地图即可在游戏中启用。

由于生成只依赖蓝色通道,“灰度图和其他格式完全受支持”,map-generator/assets/maps 下大量真实地图(如 fourislands、world)本身就是灰度 PNG。下图即一张真实的源地形图:深蓝底色代表海洋,四块灰度陆地轮廓的高低明暗对应不同海拔(Magnitude)。

四岛地图源 PNG 示例:深蓝为水域,四块灰度陆地的明暗表示海拔高低

安装与环境准备

前置条件:本机安装 Go(参考官方安装文档),并从仓库根目录进入 map-generator 目录执行:

cd map-generator
go mod download   # 安装依赖
go run .          # 为所有地图运行生成器

仓库根目录下执行 npm run gen-maps(见 package.json 的 "gen-maps": "cd map-generator && go run . && npm run format")等价于对所有地图执行 go run .,并额外跑一次格式化。

创建新地图:四步走

地图是自动发现的——生成器启动时通过 discoverMaps() 扫描 assets/maps/ 与 assets/test_maps/ 下的所有子目录(见 map-generator/main.go),info.json 就包含了游戏需要的全部信息。创建流程:

  1. 新建文件夹 assets/maps/<map_name>
  2. 放入 assets/maps/<map_name>/image.png
  3. 创建 assets/maps/<map_name>/info.json(格式见下文)
  4. 运行生成器,只处理这一张地图:
go run . --maps=fourislands

默认不带 --maps 时处理全部地图;也支持逗号分隔的子集:

go run . --maps=northamerica,world

产物输出到 ../resources/maps/<map_name>(相对 map-generator 目录,即仓库根下的 resources/maps/)。

源码佐证:parseMapsFlag() 会校验传入的地图名是否真实存在(见 map-generator/main.go),未知地图名会直接报错 map "xxx" is not defined;--workers(默认 4)控制并发处理的地图数,可调低以降低峰值内存(见 map-generator/main.go)。

创作 image.png:像素 → 地形编码规则

源图建议直接裁剪世界地图(超大文件需下载后使用 Gimp 等工具裁剪)。制图者应熟读 map-generator/map_generator.go 顶部文档中的完整映射表:

输入条件 地形类型 Magnitude 说明
Alpha < 20 Water 距陆地距离 透明像素变为水域
Blue = 106 Water 距陆地距离 专用的水色关键值
#000(纯黑) Impassable 31(固定) 虚空区域,不可占领/攻击/核爆
Blue < 140 Land(平原) 0 钳制到最小幅度
Blue 140–158 Land(平原) 0–9 低海拔
Blue 159–178 Land(高地) 10–19 中海拔
Blue 179–200 Land(山地) 20–30 高海拔
Blue > 200 Land(山地) 30 钳制到最大幅度

实现细节(见 map-generator/map_generator.go):每个像素取 8 位 RGBA,先判断 alpha < 20 || blue == 106 记为水域,再判断纯黑记为不可通行,其余按公式 Magnitude = (clamp(blue,140,200) - 140) / 2 计算。红/绿通道完全不影响地形分类。

陆地 Magnitude 表示海拔(0–30),水域 Magnitude 表示距最近陆地的曼哈顿距离——生成器用 BFS 从海岸线水格向外扩散计算(processDistToLand,见 map-generator/map_generator.go)。

不可通行地形(Impassable Terrain)

纯黑像素(#000000 / rgb(0,0,0),alpha ≥ 20)被编码为不可通行地形,它在游戏中表现为:

  • 不可被占领、攻击或核爆;
  • 核弹轨迹不能越过它(与不能飞出地图边界同理);
  • 渲染为地图背景色,使地图呈现非矩形轮廓。

可用它雕刻非矩形地图形状,或在不使用水域的前提下划分区域屏障。二进制编码为 isLand=1 + magnitude=31(即 0b10011111),且不计入陆地格数(见 packTerrain,map-generator/map_generator.go)。另外,紧邻不可通行地形的海域会被强制设置为深水幅度(setImpassableNeighborWaterDepth,同文件 L409-L429),避免边缘出现浅水渐变。

游戏内渲染与缩略图配色

  • 游戏内每格颜色由 Terrain Type + Magnitude 动态计算:海洋默认色定义在 src/client/render/gl/render-settings.json(可在设置中修改),地形颜色计算见 src/client/render/gl/utils/ColorUtils.ts;
  • 缩略图则使用独立的配色(getThumbnailColor,map-generator/map_generator.go):不可通行与水域为透明、海岸陆地 rgb(204,203,158)、平原/高地/山地为绿色到灰白色的渐变。

编写 info.json:地图的灵魂

info.json 决定了国家出生点、起始坐标、旗帜、频率、特殊规则等全部地图元数据。以下为 README 中的完整示例:

{
  "id": "MySampleMap",
  "name": "My Sample Map",
  "translation_key": "map.mysamplemap",
  "categories": ["europe", "featured"],
  "multiplayer_frequency": 4,
  "featured_rank": 5,
  "layers": [
    {
      "id": "roads",
      "placement": "land",
      "nukeable": true
    }
  ],
  "nations": [
    {
      "coordinates": [396, 364],
      "name": "United States",
      "flag": "us"
    },
    {
      "coordinates": [512, 280],
      "name": "Canada",
      "flag": "ca"
    }
  ]
}

仓库中真实地图 fourislands/info.json 展示了更丰富的用法:4 个虚构国家(Korinthal、Lunareth 等,旗帜为空串)、team_frequency: 30、special_team_count: 4、forced_modifiers: ["isWaterNukes:50"] 以及 teamGameSpawnAreas 分区域出生配置。

字段总表

字段 必填 描述
id ✅ 地图的 CamelCaseName。必须与 assets/maps/<map_name> 文件夹名匹配(忽略大小写),会成为 GameMapType 枚举键
name ✅ 规范名称——GameMapType 枚举值。地图发布后绝不能修改(属于 wire 格式,写入游戏记录)
translation_key ✅ resources/lang/en.json 中的键,必须为 map.<foldername>
categories ✅ 一个或多个分类字符串(见下),不能重复,至少一项;在选图器中分组
multiplayer_frequency 地图在公共多人播放列表中的出现权重,0(或省略)表示不进入轮换;作为各模式频率未设置时的回退值
ffa_frequency FFA 大厅轮换权重,覆盖 multiplayer_frequency;0 表示不进入 FFA;省略则回退
team_frequency 团队大厅轮换权重,覆盖 multiplayer_frequency;0 表示不进入团队;省略则回退
special_frequency 特殊大厅轮换权重,覆盖 multiplayer_frequency;0 表示不进入特殊;省略则回退
disabled_modifiers 特殊对局中永不为此图掷出的修饰器数组(如 ["isRandomSpawn"])
forced_modifiers 特殊对局中总是强制开启的修饰器数组。纯键(如 "startingGold5M")或 "key:percentage"(如 "goldMultiplier:75" = 75% 概率)。计入 3 修饰器上限
display_name 写入 resources/lang/en.json 的英文显示名,默认取 name。仅当显示名与规范名不同时设置(如 "MENA"、"Europe (Classic)")
featured_rank 精选网格中的位置(1 = 第一)。要求 categories 含 "featured";未排名的精选图按字母序排在已排名之后
special_team_count 团队/特殊对局的偏好队伍数——见 src/server/MapPlaylist.ts 中的 SPECIAL_TEAM_MAPS。必须为 0 或 ≥ 2(不允许 1)
custom_tribes 部落名数组,或 { "name": "...", "coordinates": [x, y] } 对象数组。坐标为可选(省略则随机出生)。部落名必须唯一且不得与 nations[].name 冲突,空串被拒绝
themes 用于部落名生成的主题字符串数组
layers 渲染在地形与领土之间的图层定义数组(见下)
nations 国家对象数组(见下)

校验证据:所有字段的合法性(id 大小写、translation_key 前缀、频率 ≥ 0、修饰器白名单、layer id 字符集与 placement 取值、自定义部落重复/冲突等)都由 loadMapInfos() 逐项验证,见 map-generator/codegen.go。合法修饰器键全集:isRandomSpawn、isCompact、isCrowded、isHardNations、startingGold1M/5M/25M、goldMultiplier、isAlliancesDisabled、isNukesDisabled、isSAMsDisabled、isPeaceTime、isWaterNukes、isDoomsdayClock。

Categories 合法取值

categories 的每一项必须是下列之一(对应 map-generator/codegen.go 中的 categoryOrder,同时也是选图器的分类显示顺序):

featured, new, world, continental, europe, asia, north_america, africa, south_america, oceania, antarctica, countries, cosmic, fictional, arcade, tournament

横跨多个区域的地图(如黑海、白令海峡)可以列出多个分类;加入 featured 可使地图显示在选图器的精选区。

Nations 国家对象

字段 必填 描述
name ✅ 国家显示名
coordinates [x, y] 出生点坐标。原点在左上角,x 向右延伸,y 向下延伸。省略则随机出生
flag ISO 3166 国家代码(如 "us"、"ca"、"de")。支持的完整代码列表见 resources/countries.json,对应 SVG 旗帜存放于 resources/flags

Layers 地图图层

图层是渲染在地形与领土之间的 PNG 叠加层,适合做装饰(道路、地标等)。每个图层是 assets/maps/<map_name>/ 下的独立 PNG 文件,命名为 <id>.png:

字段 必填 描述
id ✅ 图层唯一标识,也是 PNG 文件名(不含扩展名)。必须为字母数字(允许连字符),不得为 "image"(保留名)
placement ✅ "land" 或 "water"——图层位于陆地(及不可通行地形)还是水域格上
nukeable 若为 true,图层在核弹影响半径内被永久摧毁。默认 false

图层显示名存放在 resources/lang/en.json 的 map_layers 段(以图层 id 为键);玩家可在图形设置中关闭图层。

实现佐证:processMap() 会逐图层校验 placement、PNG 是否存在,以及图层 PNG 尺寸必须与地图尺寸完全一致(见 map-generator/main.go),随后把图层 PNG 复制到输出目录。

输出文件清单

每次运行(即便只指定 --maps)都会重新生成全部产物:

文件 说明
resources/maps/<map_name>/manifest.json JSON 元数据,含所有缩放下地图的宽高与陆地格数
resources/maps/<map_name>/map.bin 全尺寸二进制地图数据(地形类型 + 幅度打包)
resources/maps/<map_name>/map4x.bin 1/4 缩放(边长减半)二进制数据,用于小地图
resources/maps/<map_name>/map16x.bin 1/16 缩放(边长再减半)二进制数据,用于小地图
resources/maps/<map_name>/thumbnail.webp 地图 WebP 缩略图
src/core/game/Maps.gen.ts 生成的 TypeScript(GameMapType 枚举与 MapInfo 对象列表),由所有地图的 info.json 构建
resources/lang/en.json map 段被重写为每张地图的显示名

二进制打包格式

packTerrain() 将每个格子打包为一个字节(见 map-generator/map_generator.go):

  • Bit 7:陆地(1)/ 水域(0)
  • Bit 6:海岸线(Shoreline)
  • Bit 5:海洋(Ocean)
  • Bits 0–4:Magnitude(0–31)。水域取 距离/2

不可通行格编码为 0b10011111(isLand=1、magnitude=31),不计入陆地格数。数据按行主序(y*width+x)排列,与游戏二进制地图格式的扫描顺序一致。

多缩放层级

createMiniMap() 将 2×2 区块合并为 1 格输出,优先级 Water > Impassable > Land——水域永远优先,以保证小地图上的窄河道完整保留(寻路在小地图上运行,需要准确的水体),见 map-generator/map_generator.go。缩略图则基于 1/4 缩放数据以 0.5 比例进一步缩放,并以 WebP(质量 45、方法 4)编码(同文件 L184-L191、L235-L259)。

命令行参数与日志调试

go run . --maps=world,eastasia,big_plains   # 逗号分隔的待处理地图列表
go run . --log-level=debug                   # 显式设置日志级别
go run . -v                                  # 追加日志,并以 [mapname] 前缀标识
go run . --workers=2                         # 并发数(默认 4),调低可降低峰值内存

日志级别取值:ALL、DEBUG、INFO(默认)、WARN、ERROR。生成器基于 slog 输出标准级别的日志,另加一个 ALL 级别。

  • --verbose / -v:--log-level=DEBUG 的别名;
  • --debug-performance(代码中注册名 --log-performance):追加基于性能建议的日志,同时设置 --log-level=DEBUG;
  • --debug-removal(代码中注册名 --log-removal):追加被移除的岛屿/湖泊位置与尺寸日志,同时设置 --log-level=DEBUG。

debug-performance 与 debug-removal 是在 DEBUG 级别之上的可选输出(内容可能很长):即使日志级别为 DEBUG,也必须显式传入对应 flag 才能看到相应日志。而 --log-level=ALL 会输出所有日志,包括全部 DEBUG 层级,无论是否传了具体 flag。相关 flag 定义见 map-generator/main.go。

生成后的自动格式化与代码生成

go run . 的最后一步会用仓库的 Prettier 配置格式化每个写出的文件(调用 npx prettier --write),因此输出直接符合 npm run format 的规范,无需额外格式化。若 npx prettier 不可用(如尚未执行 npm ci),生成仍会成功但打印警告,此时需要手动在根目录运行 npm run format(见 map-generator/main.go)。

代码生成的两项关键产出(见 map-generator/codegen.go):

  1. src/core/game/Maps.gen.ts:头部声明“由 map-generator 生成,禁止手改;用 npm run gen-maps 重新生成”。内容包含 GameMapType 枚举(每个成员带注释可直接跳转到对应 info.json)、MapCategory 联合类型、SpecialModifierKey 联合类型、MapInfo/MapLayer/CustomTribe 接口,以及按 categoryOrder 排列的 maps 列表。枚举示例:FourIslands = "Four Islands"、AmazonRiver = "Amazon River";
  2. resources/lang/en.json:map 段以文件夹名(小写)为键写入每张地图的 display_name(默认 name),非地图键(如 featured、random)会被保留;map_layers 段同步收录所有图层 id。整个文件以排序键往返序列化,与 tests/EnJsonSorted.test.ts 的排序要求保持一致(见 map-generator/codegen.go)。其他语言的翻译通过 Crowdin 平台管理。

在游戏中启用:零手工步骤

一切都由 info.json 在生成器运行时推导,没有手工步骤:

地图尺寸与性能建议

生成器的自动清理与尺寸规范化规则(见 map-generator/map_generator.go 的常量定义):

  • 小于 30 格(像素)的岛屿自动移除;
  • 小于 200 格的水体自动移除;
  • 地图宽高被规范化为 4 的倍数,超出 Width - (Width % 4) / Height - (Height % 4) 的像素被裁剪(这是小地图整数降采样所需)。

水域处理还有一个重要细节:最大水体被标记为 Ocean(海洋),其余小水体若低于阈值则按相邻格多数类型填充回填(majorityNeighborType,平局优先级 Water > Impassable > Land),见 map-generator/map_generator.go。

出于性能考虑:

  • 地图面积建议在 200 万到 300 万像素之间(常量 minRecommendedPixelSize/maxRecommendedPixelSize,超出会在 DEBUG 日志中提示);
  • 陆地格数超过 300 万的图不建议使用(maxRecommendedLandTileCount);
  • 平均陆地格数约为 100–200 万。

地图在多人播放列表中的频率权重最终由 src/server/MapPlaylist.ts 消费(SPECIAL_TEAM_MAPS 读取 special_team_count,见该文件 L62、L261、L679)。

开发辅助工具

  • 格式化生成器代码:go fmt .
  • 输出生成器文档(CLI 工具需 -cmd,且不暴露 API,故加 -u -all 显示全部未导出值):go doc -cmd -u -all
  • 已知缺陷:go doc 的 -http 模式不尊重其他 flag,只渲染 README。

收尾:CREDITS 与旗帜

  • 更新 CREDITS:在地图发布前,向 CREDITS.md 添加许可证与署名信息;不确定某地图素材的许可证能否使用时,先发起 issue 或在 Discord 询问再动手;
  • 添加新旗帜:SVG 放入 resources/flags/<iso_code>.svg,并在 resources/countries.json 登记该国代码,即可在 nations[].flag 中引用。

参考资料

登录后查看全文
OpenFrontIO