下载 App

开发日志 03|不用贴图,怎样画出一个会变化的水墨世界

10/197 浏览综合

《一念万物》的水墨画面由程序绘制。树、石头、池塘、鸟和鱼的画法集中在 Paint.gd,四季使用不同配色,再叠加水墨与宣纸着色器。这样做让物件外观能够随着世界状态变化,同时也带来了一个很实际的问题:每帧重新画大量笔触,浏览器能否承受?

从形状到笔触

画面主要使用笔触 stroke、brush 和墨块 wash。它们让边缘、宽度与浓淡有变化,避免所有物件都由等宽线和正圆构成。四季调色板分别控制天空、远山、地面、水、墨色与强调色,夜间另有一套配色。
笔触负责物件的形状,水墨着色器负责画面整体的墨感,宣纸叠层提供底材质感。几层分别处理,调整某个物件的轮廓时,不必把纸纹也重做一遍。
程序绘制还要让状态可读。鱼安全与否、水位高低、花是否开放,不能只在数据里变化,画面必须跟着表达。鸟的起飞、鱼的转向和蛙的跳跃也需要自己的运动节奏。
TapTap
图 1|当前第一关收束画面。物件笔触、远山、宣纸与水墨后处理共同组成画面;这张截图展示视觉结果,不展示帧间运动。

判定位置与表现运动分开

鸟的逻辑位置用于落脚与状态判断,表现位置则加入起飞加速、落点前减速和向上拱起的飞行弧线,身体随升降改变姿态。蛙跳上荷叶时抬起一道弧,后腿蹬直,出入水时渐显渐隐。
鱼群判定使用领头位置,画面里每条鱼保留各自的位置、朝向和摆尾相位。它们追随落点、逐渐转头,转弯时身体弯曲,游得快时摆尾也更急。这样,判定可以保持清楚,运动又不会显得像一个刚性图标在平移。
这些变化仍然由世界的 step(dt) 推进,不交给独立的 Tween。自动验证推进模拟时间时,也能推进相关状态,避免游戏中能演、自检中却停住。
TapTap
图 2|2026-10-12 第 25 关收束实机截图。秋季的地面、树叶与强调色使用对应调色板,仍沿用公共绘制接口。
TapTap
图 3|同日第 37 关收束实机截图。配图用于说明季节画法,不作为性能优化前后对比或帧率证据。

性能瓶颈出现在绘制调用

2026 年 10 月 5 日的优化记录中,第 1 关每帧曾新建并销毁 106 个顶点数组,执行 213 次 bufferData,产生 137 次绘制调用。大量墨块各自提交给引擎,开销不只来自 GDScript 计算,也来自渲染提交。
修改后的 Paint 先把图元转换为三角形,累积顶点与颜色,最后通过 flush() 统一提交。单位椭圆与墨块形状按段数或种子缓存,再通过仿射变换改变位置、大小和朝向,减少逐点重复计算。
这也形成了一条必须遵守的约定:使用 Paint 的绘制回调最后调用 Paint.flush(),图元都通过同一套接口提交。混入直接绘制调用会破坏原有顺序;遗漏提交则可能让画面缺失。
当时的桌面浏览器测量里,第 1 关顶点数组新建从 106 降到 1,绘制调用从 137 降到约 8,主线程耗时从 4.9 毫秒降到 3.1 毫秒。这些是那次测量的记录,不代表所有设备的表现。

缓慢变化的背景不用每帧重画

天空、远山与地面拆到 Backdrop.gd。这一层每次生成两三百个图元,但变化主要是草的轻摆与云痕,当前每秒重画 10 次,其余帧沿用已有指令。画面范围或配色改变时立即重画,减少动态的设置下则只在必要变化时重画。
Web 导出还把渲染像素比封顶为 2,控制高像素密度屏幕上的填充量。不过,真实 3 倍屏上的文字观感与触控仍未在性能记录中验收,不能仅凭像素数量下降就宣称体验更好。

优化之后,画面有没有变?

那次绘制优化前后各生成了一遍逐关截图,共比较 152 张图。差异超过阈值的像素比例中位数为 0.006%,最大为 0.24%,主要来自按真实时钟变化的鱼尾与蝶翅。比对期间还发现并修复了屏风空心矩形横边变细的问题。
这个小问题说明,减少调用次数只是优化的一半。绘制实现改变后,线宽、透明叠加和遮挡顺序都可能变化,需要把“更快”与“仍然画对”分别验证。当前记录仍把手机帧率、发热和声音听感列为待验证内容,桌面结果只能说明这次优化在已测环境里有效。
开发日志 04|一次轻点为什么用掉了唯一机会:试玩带来的修正截图
开发日志 04|一次轻点为什么用掉了唯一机会:试玩带来的修正
2026 年 10 月 7 日的玩家视角检查里,第 12 关出现了一个很直接的问题:轻点溪中石头,没有移动,接着再拖同一块石头,石头却不走了。点击“回到清晨”以后,重新拖动又可以正常恢复水流。 当时画面没有清楚说明机会已经用完,玩家容易把它理解成触摸失效。检查代码后确认,可拖物在按下时就消耗干预,尚未判断有没有发生位移。 图 1|2026-10-07 修复前的浏览器记录。复现步骤为轻点石头后尝试拖
官方
4 赞
开发日志 02|48 关共用一个世界:把关卡写成数据截图
开发日志 02|48 关共用一个世界:把关卡写成数据
《一念万物》有 48 关,分为春夏秋冬,每关还有后续段。如果给每关单独写一套逻辑,类似的水、风、鸟和植物规则很快就会分叉:某一关改好了,另一关仍然沿用旧行为。项目因此把世界演化集中在 World.gd,关卡只描述初始条件和目标。 图 1|依据代码绘制的结构示意。关卡、终章与自在园使用同一个引擎,各自通过数据定义条件与目标。 先确定规则与数据的边界 Data/levels.json 保存关卡对象、位
官方
4 赞
开发日志 01|怎样设计“一念”引发的连锁变化截图
开发日志 01|怎样设计“一念”引发的连锁变化
《一念万物》的核心循环是“观察 → 一念 → 涌现”。玩家挪动一件东西,世界随后自己发生变化。把这个想法落成可玩的关卡,首先需要回答一个问题:玩家怎样看出,眼前的变化确实来自刚才那一下操作? 第一关“水中石”是一个具体例子。开场时,石头挡在溪里,下游池塘水浅。玩家把石头拖到岸边,溪流恢复,水逐渐进入池塘,鱼有了足够的水,周围植物也受到湿度变化的影响。这里没有一个“移走石头后播放通关动画”的专属脚本
官方
4 赞
开发日志 05|怎样确认世界真的能走通:自动自检与浏览器试玩截图
开发日志 05|怎样确认世界真的能走通:自动自检与浏览器试玩
《一念万物》的关卡依靠世界规则演化。水要往下游走,花要等环境合适,鸟要找到落脚处,天气还可能在时间线上变化。一次手动通关可以确认某条路线有效,却很难保证公共规则改动后,其他关卡与后续段都没有受到影响。 项目因此把验证与关卡数据一起维护,用自动自检检查规则,再用浏览器实操检查玩家面对的流程。 图 1|验证方法示意。自动自检、构建与实操分别提供不同证据,不能相互替代。 用已知操作路线重现结果 关卡数据
官方
4 赞