引言
《归义军传说》虽然是一个 2D 像素风战棋游戏,但希望在画面表现上有点特色。参考《八方旅人》的做法——2D 像素 Sprite 配上法线贴图和动态光照,让平面像素产生立体感。
这篇文章记录整个实现过程:从程序化生成地形贴图和法线,到 URP 2D 灯光系统的集成,以及分期推进的策略(先做可玩的 P1,再做好看的 P2)。
一、为什么要做 HD-2D
大多数同人/独立战棋游戏的画面是纯色块加边框——功能上没问题,但视觉上比较粗糙。八方旅人证明了"2D 像素 + 动态光照"这个组合是可行的,而且成本远比做 3D 模型低。
核心思路很简单:
- 每个地形格子是一张 32×32 像素的贴图(和战棋格子一样大)
- 每张贴图附一张法线贴图(normal map),告诉引擎这个像素面朝哪个方向
- URP 的 2D Light 根据法线方向计算光照,暗面变暗、亮面打高光
- 点光源(火把、灯笼)在地图上产生动态的光影区域
结果就是:像素画风不变,但地形有"凹凸感",光源有"方向感"。
二、分期策略:先用起来,再做好看
整体分三期推进,这是从实际开发经验中学到的教训——如果一开始就堆满视觉效果,调试战斗逻辑的时候 bug 会淹没在渲染问题里。
P1:程序化色块(已实现)
最简单的实现:每种地形一个颜色,一格一个 GameObject+SpriteRenderer,颜色硬编码。不加载贴图、不接光照,纯程序化。
戈壁 = 沙黄色 (0.62, 0.55, 0.40)
胡杨林 = 墨绿色 (0.18, 0.35, 0.15)
山道 = 灰褐色 (0.45, 0.38, 0.30)
党河 = 靛蓝色 (0.15, 0.30, 0.55)
沙州城墙 = 深灰色 (0.25, 0.25, 0.28)
节度使府 = 金黄色 (0.65, 0.52, 0.20)P1 阶段不纠结美术,只要每种地形能被辨识就行。这个阶段是用来验证战斗机制、网格寻路、AI 决策的——这些才是游戏能不能玩的关键。
P2:像素贴图 + 法线光照(已实现)
P1 战斗循环跑通后,再做 P2。这阶段做了三件事:
第一,程序化生成贴图。写了一个 Python 脚本 tools/generate_tiles.py,用 numpy 生成 6 种 32×32 地形贴图:
# 胡杨林(森林)的生成逻辑
# 三丛金秋胡杨树冠(位置固定,噪声变形边缘 + 南向阴影)
for (cr, cc, rad) in [(9, 8, 6), (23, 11, 5), (15, 23, 5)]:
dist = np.sqrt((yy - cr)**2 + (xx - cc)**2)
edge = noise2d(rng, 0.8)
crown = dist < rad + edge
crown_col = np.array([146, 138, 44], float) + noise2d(rng, 10)
alb[crown] = crown_col[crown]
# 树冠下缘投影(南向暗边)
shadow = (dist < rad + 1.6) & (yy > cr) & ~crown
alb[shadow] *= 0.7每种地形有自己独特的程序化生成逻辑——戈壁是沙砾+石子随机分布、河流是周期性水波相位偏移、城墙是砖块行+砂浆线、节度使府是金底+朱红菱形纹章。
第二,自动生成法线图。同一脚本对每张贴图维护一个高度图(height map),用 Sobel 梯度算子推导法线:
def height_to_normal(h, strength):
drow, dcol = np.gradient(h) # Sobel 梯度
nx = -dcol * strength # X 方向凹凸强度
ny = drow * strength # Y 方向凹凸强度
nz = np.ones_like(h) # 默认朝上
length = np.sqrt(nx**2 + ny**2 + nz**2)
nx, ny, nz = nx/length, ny/length, nz/length
# 映射到 RGB:(-1,1) → (0,255)
rgb = stack([nx*0.5+0.5, ny*0.5+0.5, nz*0.5+0.5]) * 255法线强度(strength)每种地形不同——山道 3.2(最凹凸)、河流 0.9(最平缓)。这个参数是可调的,对照游戏内的光照效果微调。
第三,接入 URP 2D 光照。创建一个全局光(Global Light2D)作为黄昏基调环境光(暖黄色,0.8 强度),加上几个点光源放在战场的关键位置——节度使府邸灯火、党河桥头火把。
TerrainRenderer 在生成地块时尝试加载贴图 + 法线 + Sprite-Lit-Default 受光材质;如果贴图或 Shader 缺失,自动回退 P1 的纯色块方案。这个回退机制保证在任何环境(没装 URP、贴图被删等)都能运行。
P3:后处理与氛围(计划中)
计划加入 Bloom(让火把光晕扩散)、Vignette(压暗屏幕边缘、聚焦战场中央)、色彩分级(配合归义军黄昏主题的暖色调)。以及昼夜/天气切换——但这个更多是表现层,优先级低于"游戏好玩"。
三、程序化生成的取舍
有人会问为什么不用 Tilemap——Unity 自带的 Tilemap 组件比逐格 SpriteRenderer 更高效,特别是大尺寸地图。选择逐格 SpriteRenderer 的原因:
- 地图只有 24×14 = 336 格,192 个
SpriteRenderer在这个规模下性能开销可以忽略 - 法线贴图需要逐格绑定到 Sprite 的二级贴图(secondary texture),Tilemap 对每格单独贴图的二级纹理支持很麻烦
- 直接把每格设为独立 GameObject,可以方便地在编辑器中选中/调试/替换
如果以后把地图扩大到 50×50(2500 格),就必须切 Tilemap 了。但现在没必要过度优化——336 个 SpriteRenderer 在 PC 上影响是 0。
四、值得注意的细节
Sprite-Lit-Default 的 Shader 裁剪问题。Unity 的
Shader.Find在打包后的播放器中可能会返回 null——如果GraphicsSettings的Always Included Shaders列表里没有这个 Shader,构建时会裁剪掉。解决办法是手动加到Always Included Shaders列表。法线贴图的 y 轴约定。生成法线时考虑到图像行方向朝下 = 世界 -Y,所以
ny = drow * strength取正号(而不是负号)。如果用错符号,地面上凹凸会看起来"反了"。性能。全场最多 5 盏 Light2D(1 全局 + 4 点光源),在 PC / WebGL / 移动端上的开销可忽略。移动端 2D Light 的上限取决于设备,但这个项目主要还是 PC 平台的效率。
参考文献
- Octopath Traveler — The Art of HD-2D
- GitHub — 归义军传说 SRPG(个人项目,本文相关内容来自此仓库)
- 本项目设计文档 [Docs/battlefield.md]