几年前的一个深夜,我坐在电脑前,盯着屏幕上那个16×16的像素小人发呆。当时我刚下定决心要做一个像素风格的游戏,可真到了动手环节,第一个卡住我的不是角色怎么跳、关卡怎么设计,而是一个特别朴素却致命的问题——像素素材和关卡到底拿什么来做?市面上的绘图工具、动画工具、地图工具我都试了一圈,要么是功能割裂,导出格式互相不认,要么就是操作手感完全不符合像素创作的需求。于是那个晚上我做了一个多少有点冲动的决定:一个人,从零开始,写一套属于自己的像素编辑器。这个编辑器最后陪着我走完了整整18个月,整个项目后来叫“像素跳动”。
这篇文章不打算做那种罗列功能的说明书,我更想把从立项、选型、核心模块设计到踩坑排错这条完整链路摊开来讲。如果你也在做像素游戏、独立游戏,或者一直琢磨着给自己写一套开发工具,那这篇内容应该能给你省下不少弯路。尤其是“编辑器到底该怎么设计”“像素级坐标如何处理”“碰撞体怎样自动生成”这些具体问题,我会把当年落地的思路和实际参数都写出来。
1. 先想清楚:我要做的是一款游戏,还是一款编辑器?
很多人会把“编辑器”和“编译器”这两个词混在一起,其实游戏开发里它们完全是两码事。编译器是把源代码翻译成机器指令,编辑器则是把人的创作意图变成结构化数据。游戏里的关卡、精灵、动画、碰撞区域,本质上都是一堆数据,只是它们太抽象了,直接改文件根本没法做,所以才需要可视化编辑器。我当时的核心任务,其实是做一套能把“脑子里的画面”高效转换成“游戏可读数据”的工具链。
1.1 三个核心需求决定了编辑器长什么样
像素跳动是一款平台跳跃游戏,这类游戏的内容创作需求其实高度集中在三个地方:像素画绘制、逐帧动画制作、瓦片关卡编辑。三者不是孤立的,比如一个角色,你要先画出它的待机帧、跑步帧、跳跃帧,然后把这些帧组织成动画,再把它放进关卡里验证跳跃手感。任何一个环节缺少工具支撑,整个创作流程就会断掉。
我一开始天真地以为,游戏逻辑代码才是最难写的部分。后来才发现,如果素材工具不好用,你连一张能看的角色立绘都出不来,更别提测试游戏玩法了。所以“编辑器”在我这个项目里的分量,从一开始就超过了“游戏本体”。这也是为什么整个项目叫“像素跳动”,但大家看到的核心成果却是一套编辑器。
1.2 编辑器与游戏运行时必须彻底分离
这是我在项目第一天就定下的纪律,也是后来被证明最正确的一个决策。编辑器的代码工程和游戏的代码工程完全独立,两者通过磁盘上的标准数据文件交互:精灵导出为PNG,动画和关卡导出为JSON加二进制数据。编辑器只负责“写”,游戏运行时只负责“读”。
这个决策的意义在于:编辑器为了提高操作体验,可以堆各种复杂的界面逻辑、撤销栈、实时预览,这些代码如果塞进游戏里,必然拖累游戏的加载速度和运行时稳定性。反过来,游戏里为了性能做的各种优化,也不该反过来掣肘编辑器的功能设计。数据格式是两者之间的契约,只要契约不变,两边随便折腾。
1.3 18个月不是规划出来的,是“范围蔓延”堆出来的
如果按最初的设想,我只打算做一个能画像素画的小工具,那可能三周就做完了。但实际做过开发工具的人应该都有同感:一旦你开始用自己做的工具,就会不断发现“这里缺一个功能”“那里交互不舒服”。从像素画板开始,我做着做着发现“没有动画预览根本没法看动作是否流畅”,于是加了时间轴;动画有了之后,又发现“关卡没有编辑器就只能手写二维数组”,于是又做了瓦片地图;地图出来后,碰撞体又要自动生成……就这样一环扣一环,最后变成了一套完整的“像素游戏工作台”。回头看,18个月这个数字,就是这么一点一点被撑起来的。
2. 技术选型:为什么最终选了C# + MonoGame,而不是Web技术
技术栈的选择,基本决定了后面一年半的开发体验。当年我认真考虑过三条路线,也花时间做了对比实验,最后选了C#和MonoGame的组合。这条路线未必适合所有人,但如果你要做的是“像素级别的精确控制工具”,那这里的选型逻辑应该能给你一些参考。
2.1 三条路线的实际对比
先说我为什么没有选Web技术。当时市面上很多工具都基于Electron或纯网页实现,跨平台是方便,但像素画有一个致命需求:缩放到200%、400%甚至800%时,边缘必须保持硬朗,绝不允许出现平滑过渡的模糊感。浏览器在早期普遍会对Canvas做抗锯齿处理,虽然现代浏览器可以通过image-rendering: pixelated关掉,但在部分移动端浏览器和某些WebGL驱动上表现并不稳定。而像素编辑器恰恰是那种“一像素模糊就废了”的应用,我实在不想把命脉交到浏览器厂商手里。
然后说Python。Python做原型很快,但真要做一个需要频繁全量刷新画布、处理大尺寸图像运算的编辑器,性能会非常吃紧。另外Python的打包分发一直是个痛点,你不可能让用户去装Python环境再来用你的工具。
最终选择的是C# + MonoGame。MonoGame是XNA的继任者,2D渲染控制力非常强,可以精确指定采样状态(PointClamp),这是像素画不模糊的关键。同时C#写工具类应用效率足够高,打包发布也简单,一套代码能覆盖Windows、Linux、macOS,还能通过WebGL跑在浏览器里。
性能说实话,很多像素编辑工具对性能的要求并没有想象中那么极端。真正决定成败的反而是“对像素渲染的控制精细度”和“开发效率的平衡”,这也是我最终站在C#这边的原因。
2.2 为什么把编辑器做成独立程序,而不是IDE插件
还有一个容易被忽略的决策点:工具形态。当时流行把工具做成某款IDE的插件,这样能省去很多界面工作。但我刻意避开这条路,把编辑器做成了完全独立的窗口程序。
原因有两个。第一,IDE插件绑定在那款IDE的生态里,用户的编辑流程会被IDE自身的行为干扰,比如文件管理、快捷键冲突,这些都会影响专注度。第二,独立的编辑器才能作为“产品”分发出去——别人不需要安装那款IDE就可以直接使用。后续如果想让更多独立游戏开发者使用这套工具,独立发行几乎是唯一选择。
2.3 用VSCode替代Arduino编辑器的启发
开发过程中还有个很有意思的插曲。很多玩嵌入式的人会纠结到底用Arduino自带的编辑器还是换成VSCode,我当时也纠结过一阵。后来想明白了:替换工具不是因为原工具有多差,而是为了统一语言、统一流程、统一体验。同样的逻辑也适用于我自己写的编辑器:它的存在不是为了“取代谁”,而是为了让“画像素画—做动画—搭关卡—调手感”这条链路足够顺畅,不因为工具切换而断掉。这是工具设计里一个非常核心的思维。
3. 像素编辑器的核心:网格、坐标与“像素级”缩放
Pixel Jump Workshop这个名字听起来有点高级,但它的核心底层逻辑其实特别朴素:一块低分辨率的画布,加一套足够精准的坐标转换系统。如果你现在正准备自己写一个类似工具,我建议你从这一节开始研究——这是所有像素编辑器的地基。
3.1 像素画布的数据结构与渲染
编辑器内部,每张精灵图本质是一个二维颜色数组,比如Color[,]。画布默认分辨率按精灵尺寸来定,常见的有16×16、24×24、32×32,支持最大到128×128。所有绘制操作都发生在这个数组上,而不是直接画在屏幕上。
渲染到屏幕时,画布会通过矩阵变换放大显示。这里有一个关键参数:缩放倍数。我限定缩放倍数必须是2的幂,也就是1x、2x、4x、8x。为什么这么限定?因为整数倍缩放可以保证“原始画布的一个像素 = 屏幕上的2×2或4×4像素块”,边缘是绝对整齐的,不会出现半像素偏移。如果缩放倍数是个奇数或非整数,屏幕上就会出现有些像素格比其他像素格宽一像素的情况,整个格子视觉上就乱了。
配合缩放的是采样状态。MonoGame里渲染纹理时要强制设置SamplerState.PointClamp,这一点绝不能妥协。如果用默认的LinearClamp,GPU会在像素之间做线性插值,边缘会发虚,像素画看起来就像蒙了一层雾。
3.2 坐标转换:窗口坐标到像素坐标
鼠标操作是编辑器里最高频的动作,所以坐标转换的准确性直接决定工具的“手感”。核心公式其实只有一行:
int GetPixelX(int screenX, int offsetX, int scale) { return (int)MathF.Floor((screenX - offsetX) / (float)scale); }注意这里用的是Floor(向下取整),而不是Round(四舍五入)。理由很简单:像素格子是有明确边界的,鼠标落在格子的左半边和右半边,在用户视觉上属于不同的格子。如果用四舍五入,在放大倍数高的时候,你会觉得光标在“跳格”,明明还没移动到下一个格子,笔迹却已经画过去了。
同样的逻辑也应用到Y轴。编辑器内部统一使用整数像素坐标运算,所有涉及浮点数的地方只在“窗口坐标转像素坐标”这一步出现,后续操作全部是整数算术。这道“整数化”的约束,是整个编辑器字号对齐、格子规整的基础。
3.3 DPI缩放引发的光标偏移
这个坑我印象太深了。最开始编辑器在高分屏上出现一个诡异现象:光标在左上角时点选是准的,越往右下移动偏移越严重。排查了很久,最后定位到Windows的DPI缩放机制上。
为了兼容老程序,Windows默认会把不支持高DPI的进程的鼠标坐标“放大”后再交给应用程序。如果你的程序没有声明自己支持高DPI,系统就把你当成“老古董”,4K屏上鼠标实际移动1像素,程序收到的可能是2像素甚至3像素,于是光标和画面就错位了。
解决方式是在app.manifest里声明PerMonitorV2 DPI Awareness,窗口创建后还要手动处理DpiChanged事件,动态调整画布缩放比例。这段经验后来被我写成了一段模板,因为每次新建项目都得重新配一遍。
3.4 二维像素数组转图片的性能细节
编辑器里的数据是Color[,]二维数组,导出PNG时必须转成Bitmap。最开始我图省事用了Bitmap.SetPixel逐点写入,结果导出128×128的预览图时,能明显感觉到卡顿。后来换成LockBits配合指针直接写入像素数据,速度提升了大概几十倍。这个优化非常简单,但带来的体验提升非常明显:
using (var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb)) { var data = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); unsafe { var ptr = (byte*)data.Scan0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int idx = y * data.Stride + x * 4; ptr[idx + 0] = color.B; ptr[idx + 1] = color.G; ptr[idx + 2] = color.R; ptr[idx + 3] = color.A; } } } bitmap.UnlockBits(data); }3.5 基础工具的实现顺序
编辑器的基础绘画工具,我是按照这个顺序实现的:铅笔、橡皮、取色器、填充、选区与移动、对称画笔。前四个是必须的,第五第六个属于提升效率的利器。
填充这里有个容易踩的坑。最朴素的泛洪填充用递归实现,在稍大一点的画布上会让栈溢出。比如64×64的画布,极端情况下递归深度可能达到上万层。我换成了队列加逐行扫描的方式,速度和栈占用都稳定很多。核心思想是:从左到右扫描某一行,遇到可填充的连续区间就填掉,然后检查上一行和下一行的相邻区间,继续入队处理,直到所有连通区域都被填完。
对称画笔这个功能虽小,但像素角色很多时候是左右对称的,画左半边自动镜像到右半边,能省很多重复劳动。实现上就是在绘制回调里同时写两个坐标点。
4. 帧动画与图集打包:像素美术工序的数字化
像素画和普通插画最大的区别,在于它的“动画”通常不是骨骼驱动,而是逐帧替换的精灵图。精灵图的美术质量,很大程度取决于动画预览和帧管理的流畅程度。这一章我聊聊帧动画时间轴和图集打包这两个核心模块。
4.1 时间轴设计:30FPS基准加帧延迟
我把动画时间轴设计成了类似音频编辑器的多轨形式,每个精灵的每一帧是一张独立画布,帧按顺序排列在时间轴上,上方直接提供播放/暂停按钮,方便随时预览。帧率以30FPS为基准。
这里有一个像素游戏特有的设计决策:为什么要预设30FPS而不是60FPS?因为逐帧动画在30FPS下,每帧持续3~4个屏幕刷新周期,人眼已经能感知到流畅的动作;而60FPS意味着每秒要画60张不同的帧,工作量翻倍,对像素这种“手工逐帧”的美术风格来说性价比极低。
不过我也提供了一个“帧延迟”参数,允许每帧重复1到5个时间单位。这个参数用来做复古的“2帧走路循环”——左右脚各一帧,但在落地的瞬间故意多停一拍,就能营造出一种二次元老游戏的顿挫感。这个看似不起眼的功能,实际上对游戏风格影响很大。
4.2 图集打包:Skyline(天际线)算法实战
当游戏里精灵多起来之后,如果每个精灵帧都是一张独立纹理,GPU的绘制调用(Draw Call)数量会爆炸,直接拖垮游戏帧率。解决方案是把所有精灵帧合并到一张大纹理上,这张纹理叫图集(Sprite Atlas)。
图集打包算法我选的是实现简单但效果不错的Skyline算法。核心思路是维护一个一维高度数组,表示大图每一列当前已经被占用的高度。放置一个矩形时,从左到右扫描,找到一组连续的、宽度足够且“最高点最低”的区域,把矩形放进去,然后更新高度数组。文字描述有点抽象,伪代码会更直观:
function placeRect(width, height): bestX = -1 bestY = MAX for x from 0 to skyline.Length - width: y = max(skyline[x..x+width]) if y < bestY: bestY = y bestX = x if bestX == -1: return 需要新建图集 update skyline[bestX..bestX+width] = bestY + height return (bestX, bestY)打包完成后,每张小图在大图上的矩形区域会被记录到JSON文件里。运行时加载大图纹理,再根据矩形数据裁剪出需要的精灵帧。这套方案对几百张小图的场景完全够用。
4.3 调色板管理:像素画风格的“守门员”
像素画辨识度高的原因之一,是它通常使用有限颜色。我见过很多新手在画像素画的时候,什么颜色都往上堆,结果画面花成一片。像素跳动的编辑器内置了16色、32色、256色三档调色板,所有绘制工具都受当前调色板约束,不允许画出调色板之外的颜色。
调色板还支持从外部图片自动提取主色,以及导入ASE格式的色板文件。这个功能对美术风格统一非常有用。想象一下,你在一张宣传图里看到一套漂亮的配色,直接导入到编辑器的调色板里,后续所有精灵都用这套颜色来画,整个游戏的美术风格立刻会有一种凝聚力。
4.4 导出与扩展:从PNG到LED点阵屏
编辑器的动画可以导出为PNG序列帧,也可以直接导出为GIF。还有一个很偏门但好玩的玩法:我把动画帧导出为RGB数组后,接了一个树莓派驱动的LED点阵屏当播放器,在物理世界里播放游戏里的精灵动画。第一次看到像素小人从屏幕里“跳”到那块发光的LED板子上时,说实话挺震撼的。那一刻我觉得“像素跳动”这个名字算是起对了。
这种硬件扩展其实并不复杂,LED点阵屏通常只需要往驱动板按协议刷像素数据就行,编辑器负责把动画帧转换成对应分辨率的RGB数组。如果你手头有类似的像素屏,完全可以把它当成编辑器的“实体预览窗”。
5. 关卡编辑与碰撞体:把“跳动”手感调出来
对一个平台跳跃游戏来说,关卡编辑器的好用程度,直接决定你能做多少关、关卡质量高不高。而碰撞体的生成方式,又直接决定玩家操控时的手感。这一章是整个项目里最有“游戏开发味”的部分。
5.1 瓦片地图的多层设计
像素跳动的关卡地图最多支持8层,分为背景层、碰撞层、机关层、前景层。每一层都是一个二维字节数组。为什么是字节?因为瓦片ID范围通常是0到255,一个瓦片正好一个字节,300×200大小的地图层只有60KB,非常轻量。导出时直接用二进制格式保存,加载快,体积小。
图层还有一个关键的视觉属性:“视差滚动比例”。背景层的移动速度是主层的0.5倍,就能营造出简单的纵深感。这个参数不用写死在游戏里,而是作为图层属性存档,游戏运行时读取后自动应用。编辑器里可以一键切换显示哪些层,也可以把某一层调成半透明方便对齐。
5.2 碰撞体的自动合并算法
如果朴素地给每个实心瓦片都生成一个碰撞体,一个大关卡可能产生几百上千个矩形,物理引擎每帧检测一次全量碰撞,性能会急剧下降,而且相邻瓦片之间会产生肉眼不易察觉、但手感上能感受到的“微小的卡顿感”。
解决方式是逐行扫描合并。算法思路很简单:遍历每一行,把连续排列的实心瓦片合并成一个矩形。对于某一行上的瓦片,如果x=1到x=8都是实心的,就合并为Rectangle(1, y, 8, 1)。做完行合并后,再对纵向相邻且宽度完全相同的矩形尝试二次合并。这样300个瓦片的关卡,最终碰撞体数量通常在30到60个左右。
这个算法不到100行代码,但效果是立竿见影的。物理引擎的负担瞬间下降了一个数量级。
5.3 手动微调:自动算法永远不够
自动合并有一个典型问题:如果“玩家头顶的平台”旁边连接着一堵更高的墙,算法可能会把它们合并成一个L形的大矩形。这个合并本身没错,但会在转角处产生一个“隐藏的凹槽”,玩家跳跃时可能会被这个凹槽卡住,手感变得非常诡异。
所以编辑器里必须有碰撞体的可视化编辑模式。在这个模式下,每个碰撞矩形会以半透明色块显示在地图上,可以直接拖拽边缘来修改尺寸,或者手动拆分、合并矩形。我强烈建议所有类似的编辑器都保留这个功能,因为自动生成的碰撞体永远是“合理”的,但“玩家手感需要”的形状,只有人能判断。
5.4 手感调试面板:跳跃参数的可视化
“跳跃手感”听上去很玄,其实就是一堆参数调出来的结果。核心参数我整理成了这几项:
- 重力加速度(gravity),决定下落速度曲线的陡峭程度。
- 跳跃初速度(jumpVel),决定起跳的爆发力。
- 跳跃取消倍率(jumpCutMultiplier),松开跳跃键时,上升速度立即乘以这个系数。这个参数是让“短按轻跳、长按高跳”手感成立的关键。
- 土狼时间(coyoteTime),角色离开平台后的一小段时间内仍然允许跳跃。这个参数让玩家在边缘起跳时不会觉得“明明按了跳跃键却没反应”。
- 跳跃缓冲(jumpBuffer),按下跳跃键后的若干帧内,即使落地稍晚,落地后仍会立刻起跳。这个参数可以减少操作延迟感。
这些参数在编辑器的“手感调试面板”里可以实时修改,并立刻在预览角色身上生效。边栏会同时显示当前参数下的起跳高度和滞空时间。这个功能让我调跳跃手感时完全不需要重新编译游戏,效率翻了好几倍。
5.5 从草稿到可玩关卡的完整工作流
整个流程我最终标准化成了这样:先在网格纸上画草图,规划大致的平台位置和敌人分布。然后打开编辑器,在“结构层”快速铺瓦片,把主要地形搭出来。接着自动生成碰撞体,进入调试面板跑一遍,找出那些“跳不过去”或“卡脚”的地方。微调瓦片或手动调整碰撞矩形后,再跑一遍。循环稳定后,添加装饰层和机关层,最后导出二进制关卡文件。
这个工作流最核心的指标是“从开始铺瓦片到能跑起来”的时间。整个循环如果能控制在10分钟以内,你就会有源源不断的动力去尝试新关卡设计;如果这个循环动不动就要半小时,那你大概率会半途而废。
6. 那些差点让我放弃的坑:性能、序列化与跨平台
做工具最折磨人的不是写功能,而是遇到那种“看起来没问题但就是不对”的隐蔽坑。这一章我不打算美化什么,直接把你最可能遇到的四个问题摊开讲,每一个都标清楚了当时的排查过程。
6.1 撤销/重做内存爆炸
现象:连续绘制几百笔之后,编辑器内存飙到几百MB,64×64画布操作起来卡成PPT。
排查过程:一开始我怀疑是不是有什么资源没有释放,但反复检查逻辑都没有问题。后来排查到撤销功能时才恍然大悟:我每次绘制操作,都把整张画布的像素数组完整复制一份塞进历史栈。对32×32的小画布来说,一次操作也就几KB,但历史栈累积起来,几百次操作就是几十MB甚至更多。64×64画布更是直接失控。
解决思路:改成命令模式加差异记录。每次操作只记录“影响范围”和这个范围内的旧像素、新像素。撤销时只恢复旧像素,重做时应用新像素。历史栈深度限制为100步,差异数据用简单的行游程编码压缩。优化后,连续操作几百次,内存峰值不到20MB。
6.2 JSON序列化导致大关卡加载缓慢
现象:一个300×200的三层瓦片地图,导出成JSON后文件有700多KB,加载需要一两秒。
排查过程:JSON这种文本格式有天然冗余,逗号、大括号、字段名、转义符都是额外的字节。调试期用着方便,但游戏发布的时候这个加载时间完全不可接受。
解决思路:换成MessagePack二进制序列化。同样是那组数据,体积缩到原来的约四分之一,加载时间从一秒多降到几十毫秒。核心改动就是给数据类加特性标记,然后调两个序列化函数:
[MessagePackObject] public class TileLayerData { [Key(0)] public int Width { get; set; } [Key(1)] public int Height { get; set; } [Key(2)] public byte[] Cells { get; set; } } byte[] bytes = MessagePackSerializer.Serialize(layerData); TileLayerData loaded = MessagePackSerializer.Deserialize<TileLayerData>(bytes);6.3 DPI缩放导致的光标偏移
这个坑在上面坐标系统那节提到过一次,这里再说一下完整的排查链路。现象是光标在窗口左上角点击比较准,越往右下偏移越厉害,而且偏移量随着窗口位置不同而变化。最开始我以为是我自己坐标换算代码写错了,反复检查了好几遍都没有问题。后来在窗口拖动到另一块显示器时偏移量发生了变化,才想到可能是系统DPI缩放造成的。
Windows对没有声明DPI感知的进程,会默认做位图缩放和鼠标坐标映射。在高分屏上,这个映射是非线性的,所以左上角准、右下角偏。解决方式就是在manifest里声明PerMonitorV2 DPI Awareness,再处理一下字体和控件的缩放。这个问题耗费了我整整一天,但解决方案本身非常简单。
6.4 跨平台纹理采样状态丢失
MonoGame支持多平台,但在WebGL后端上,我发现某些平台默认的纹理采样会忽略代码里设置的PointClamp,导致像素画边缘发虚。排查后定位到原因:MonoGame在切换RenderTarget时,采样状态有时会重置为默认值。如果只在初始化时设置一次,切到渲染目标再切回来,状态就丢失了。
解决办法是每次在设置RenderTarget之后,都显式重新绑定SamplerState.PointClamp。这个教训告诉我对渲染状态的管理必须“每次渲染前显式设置”,不能依赖“初始化时设置一次生效”。
6.5 坑位复盘表格
| 坑 | 根源 | 解决方式 | 解决耗时 |
|---|---|---|---|
| Undo内存爆炸 | 全量快照 | 命令模式+差异记录 | 2天 |
| JSON体积大 | 文本格式冗余 | 换成MessagePack | 1天 |
| 光标偏移 | Windows DPI缩放 | 声明PerMonitorV2 | 1天 |
| 纹理发虚 | 采样状态丢失 | 每次渲染前显式绑定 | 半天 |
7. 18个月复盘:哪些决定是弯路,哪些值得坚持
18个月不算短,回头看,有差不多三分之一的时间在走弯路。但弯路走多了,反而让一些“对的决定”变得格外清晰。
7.1 砍掉脚本系统,是我做过最果断的减法
项目进行到大概第七八个月的时候,我觉得如果编辑器能有一个脚本系统,让玩家自定义关卡逻辑,那就无敌了。于是我花了一两个月设计脚本语言、调试器、与关卡数据的绑定层。越写越觉得不对劲——这个系统的复杂度,几乎相当于再造一个小型游戏引擎了。作为一个单人项目,这个摊子铺得实在太大了。
最终我狠下心,把整个脚本模块连根删掉,改成“事件数据”方案:编辑器内置跳板、移动平台、传送门、按钮机关这些有限的事件类型,所有逻辑都由数据驱动,不需要写代码。砍掉之后,项目稳定性立马上了一个台阶,我也把精力重新收回到游戏内容本身。
7.2 追求“通用工具”是个陷阱
早期我在设计编辑器功能时,总想着“万一别人要做其他类型的像素游戏怎么办”,于是一股脑加入了很多跟平台跳跃无关的通用功能。结果是工具越来越臃肿,每一项功能都要维护,但真正常用的只有那么几个。
后来我把定位收窄成“像素平台跳跃游戏专用编辑器”,所有工具围绕这个玩法设计,把那些无关功能全部藏起来或删掉。效率立刻就不一样了。做工具,克制比能力更重要,这个道理我是真金白银买来的。
7.3 值得坚持的三件事
第一,编辑器与游戏运行时分离。这个决策从第一天起就是对的,没有它,后面所有模块的迭代速度都会慢一半。
第二,先定数据格式,再写编辑器界面。数据格式是契约,编辑器只是契约的可视化编辑工具。只要数据是干净的,哪怕界面做得再丑,游戏本身也不会受牵连。反过来,如果数据一团糟,界面再华丽也是空中楼阁。
第三,把性能问题当成功能来做。撤销重做、序列化、碰撞体合并这一系列工作,虽然不像新功能那么有存在感,但它们才是用户体验的分水岭。一个卡顿的工具,再有创意也留不住人。
这个项目做下来,给我最大的改变是对“工具”这件事有了敬畏心。一个编辑器不只是一个画板,它决定了你每天要在工具上消耗多少耐心、能产出多少内容。像素跳动现在还在持续迭代,我下一步打算加联机协作和素材市场。如果你也在酝酿一款独立游戏,真的建议先花两周把“素材管线”想清楚,而不是急着写游戏逻辑。工具定生死,数据定未来,这句话在独立游戏开发这条路上,含金量比想象中高得多。