我正在开发一个像素艺术殖民地模拟游戏,它不需要任何PNG图像。没有精灵图集,没有纹理大图,没有资产文件夹。游戏中的每棵树、村民、风车、狼、火焰和浆果丛都是源代码中的一个小块文本。有人问我这个系统是如何工作的,所以我在这里解释整个系统。说点背景之前,我是一个软件工程师,工作了约15年,但从未在游戏行业工作过。开发游戏是一个试错过程,我每天都在学习新东西。这里我做的可能看起来疯狂,但也可能有趣。
每个精灵都是一个字符网格加上一个调色板。这里是实际的高草,正如它出现在代码中的样子:
const TALL_GRASS = [
"..t........t....",
"..g..t.....g..t.",
"..g..g.....g..g.",
".dg..gd...dg..gd",
".dgg.gd...dgg.gd",
];
const PALETTE = {
t: "#b1d67e", // 光亮尖端
g: "#6fae52", // 刀片
d: "#4a7a34", // 根
};
每个字符代表一个像素。每个字母查找调色板中的颜色,而点是透明的。一个名为gridToCanvas的函数会遍历网格一次,将像素写入ImageData中,然后在OffscreenCanvas中打印它。然后渲染器就像处理解码的PNG一样处理这个画布。这就是整个机制。
下面是为什么这个系统比我预期的好很多。
Bake一次,blit永远
网格只解析一次。在启动时或第一次需要精灵时,网格会被缓存到一个画布中。渲染循环绘制可见的地图,60fps,并且只会复制画布。将16x16精灵bake成画布只需要微秒级时间,所以没有意义的载入成本,而运行时成本与使用图像文件相同。动画也一样。精灵可以定义多个变体:第二个网格中的花朵头部在风中倾斜一个像素,或者火焰弯曲,或者村民的腿互换。每个变体都会bake成自己的画布,渲染器会选择一个帧索引。一个两帧的摆动动画大约需要十行文本。
分离形状和颜色有助于节省资源
因为形状和颜色是分离的,所以一个形状可以变成很多东西。这就是系统赚取的钱。橡树、松树、柳树和红木树都共享一个干和冠的轮廓。每种树种需要五个十六进制值。混合它们的森林看起来像一个真正的森林,而不是一个被贴在一起的树。价格是二十行调色板。
季节也一样。地形艺术在bake时根据季节更改颜色。当秋天来临时,bake缓存会被丢弃,每个网格都会重新bake通过颜色混合,所以世界会逐渐改变大约十个步骤,而渲染循环中不会进行任何帧内颜色运算。冬天会在屋顶和树冠上打雪,作为另一次bake时间的过滤。没有冬季精灵集在游戏中。地面也一样。草地、针叶林和橡树林的草地是相同的点阵图,通过不同的调色板bake出来,而草地花会有四种花瓣颜色,每种颜色都是一个两行调色板覆盖一个网格。在PNG工作流中,每一种都会是另一个导出文件,或者四十。
好吧,但它的成本是多少?
我测量了。游戏目前有173个精灵和地形纹理,约70000个像素的艺术。所有这些都以最优压缩的索引颜色PNG格式编码,然后比较:
| 表示形式 | 大小 |
|---|---|
| 源代码中的网格文本,原始大小 | 91.8 KiB |
| 与同等PNG文件的相同艺术 | 31.0 KiB |
| 源代码中的网格文本,实际大小,压缩 | 9.4 KiB |
| 原始文本大约是PNG三倍,因为字符是浪费像素的格式。但是,没有人会将原始文本发送。网格是非常重复的ASCII文本,gzip会压缩到原始大小的十分之一,而PNG内部已经使用了deflate压缩,所以gzip对它没有任何作用。结果是整个游戏的艺术在9.4KiB中发送,约等于同等PNG的三分之一大小,且没有额外的HTTP请求,因为它是内置在JavaScript包中加载的。即使在这种大小下,PNG的固定头部开销也很重要。平均来说,这里的精灵大小为543字节的文本(54字节压缩)而不是184字节的PNG,80个PNG字节只是分块头部。 |
管道:Aseprite进入,文本输出
很长一段时间,管道就是我输入字符到网格。听起来很疯狂,直到你已经尝试了一个星期,你的手指就会知道如何用“h”、“d”和“s”来绘制树冠。然而,没有理由文本系统和正常的像素艺术工具不能相互作用,所以现在这个存储库有一个转换器可以关闭循环。
- 在Aseprite(或任何像素编辑器)中绘制精灵并导出PNG。动画帧导出为水平条带,如任何精灵图集。
- 运行转换器:
node scripts/png-to-grid.mjs tallgrass.png --frames 2 - 将输出粘贴到精灵源中。脚本会发出游戏使用的完全相同的结构。
const TALL_GRASS_FRAMES: TextureGrid[] = [
[
"..s........s....",
"..h..s.....h..s.",
// ...
],
// ...
];
const TALL_GRASS_PALETTE: Record<string, string> = {
h: "#6fae52",
d: "#4a7a34",
s: "#b1d67e",
};
转换器大约有300行,没有依赖项。它包括自己的小PNG解码器(分块解析、扫描线解除滤波等)因为工具大小没有理由引入图像库。透明像素变成点,颜色变成键按最常用顺序分配,而 --frames N 会将条带分割成变体网格以便动画系统。有一个标志 --palette,它会将每个像素匹配到现有的共享调色板,而不是从图像中推导出来。这样新建的建筑就可以在同样的名称下继承所有其他建筑的颜色,并在季节更改时自动更改颜色。默认情况下,它是严格的:如果没有匹配到调色板,会抛出精确的坐标和颜色。如果需要,可以使用 --tolerance 允许一点偏差,或者 --force-nearest 强制所有像素匹配最接近的颜色。因为输出是文本,所以整个过程是可逆的。可以将网格bake成PNG,重新通过转换器,得到相同的网格。这个回路也是如何测试转换器的。
工作流程的优势
我没有计划在这里 A git diff显示了 palm树精灵的变化
艺术是可比的。精灵的变化会在git diff中显示为可读的字符网格,而不是二进制块。可以看到 palm树已经改变了。艺术也是可grep的。我的无头测试套件检查精灵是否实际上被包含在生产包中,通过查找网格行和调色板十六进制代码,因为这些字符串文字会在压缩时存活下来,而类和函数名称不会。没有资产状态会脱离同步。即使有Aseprite路径,也没有大图集打包器和忘记导出大图集的bug,因为转换器的输出会被粘贴并提交。精灵就是源代码就是资产。小的调整都是一个字符编辑。会owwillow树冠的颜色会变亮,意味着改变 #8bbf68。草地花会变大一点,意味着改变一个点到 h。
这个系统何时会停止工作
说实话,这个系统只会胜利,因为艺术很小,颜色很少。16x16像素的图块,32x32像素的建筑,调色板只有四到十种颜色。超过这个范围,两半的交易都会破裂。64x64像素的图块,渐变和点阵图,PNG的更智能的2D过滤器会在大小上胜过这个系统,更加糟糕的是,64个字符宽的网格会变得无法阅读或编辑。技术和艺术风格是匹配的。块状像素和少量可命名的颜色,如“树冠高光”和“干部阴影”正是这种情况下的最佳媒体。作为一个独立开发者制作殖民地模拟游戏,这笔交易对我来说是值得的。我可以在需要时用真实的像素编辑器绘制,或者用快速编辑网格文本,当速度更快时。无论哪种方式,艺术都生活在代码旁边,通过一个文件发送,并出现在同样的diff中。
评论 (0)