开发日志
Emberveil · ESP32-S3(N16R8)+ ST7789 1.54 寸 240×240 屏,PlatformIO / C++ 的像素风叙事解谜游戏。以下按日期记录踩过的坑与结论,√ 表示已完成/已解决。
2026/4/12 1.36
- 角色移动步幅太小,目前是一帧一帧移动的,walk 序列帧没有正常渲染,走路的渲染间隔和移动间隔似乎没有统一
- 镜头移动不够平滑,并且地图在刷新的时候非常卡,并且只有在松开 L 或 R 的时候才开始镜头更新
- 中景和前景仍然没有渲染成功
2026/4/12 22.55
- 可以仿照目前流程,所有资源全都存在 sd 卡,以后每一个地图都把资源统一加载到内存,进行游戏,换地图则重新加载本章节所需资源
- 看看性能还能不能再优化一下
2026/5/3
- √ first_commit:工程初始化
2026/5/4 0.17
- 主角在"3 秒不动改为 front stand"之后,主角图片不显示了
- 小白的渲染有闪动 bug,移动到主角前方这个动作会有残影,而且这个动作应该使用 walk 动作,而非 stand 的不动姿势平移过去
2026/5/4 13.57
- √ 加入特效层的薄雾和打光层的晨光
- 可以尝试加入一个交互层
2026/5/4
- √ 小白角色集成:位置、朝向、动画帧、12 套精灵资源路径、引导控制状态、缓存结构体
2026/5/5 23.40
- 把 DPNG_MAX_BUFFERED_PIXELS 调到 8192 之后,烧进去板子就会重复重启——实在不行就把 L4 薄雾的 spritesheet 拆成单独的图(暂缓)
- 询问 Parallax Layer 能否使用(远景层 Clamp 边界,不跟随摄像机超出),是不是性能跟不上;图层渲染是预合成之后统一渲染吗
- √ 改布线
- √ 修改 redraw 条件优化性能——直接去掉特效层薄雾的动画效果,改为单图片,薄雾的重绘面积太大了,以后的小特效可以用动画
2026/5/6 18.28
- 考虑去掉特效层薄雾的动画效果,改为单图片显示(与上条同一件事)
2026/5/6 23.10 — 边缘刷新优化回顾
- √ 完成边缘刷新优化的完整回顾:相机位移时的残影问题,根因是角色像素被混进了会 memmove 的共享缓存
- 先尝试了扩大清除区域和整屏重绘,确认能消除残影但会把性能拉回去,不适合作为最终方案
- √ 最终落地分层缓存方案:世界层拆成背景缓存和前景缓存,角色层独立合成,前景遮挡仍通过绘制顺序生效
- √ 相机移动时只对世界缓存做位移,角色不参与 memmove;新暴露的边缘区域与角色旧位置分别按需刷新,既保性能又保正确性
- √ 相关开关统一收到 config 里,方便后续对比不同渲染路径
2026/5/7
- 记录 Parallax 边界现象:当 kFgParallaxFactor > 1.00 时,左边界 cameraX=0,分层相机会先经过因子计算(0 * factor)结果仍为 0,再进入 clamp,因此前景与背景会一起停住;右边界则因前景/背景因子不同且各自独立 clamp,可能出现前景先停、背景后停;因子 ≤ 1 时分层效果不太明显
- 标注风险:上述边界效果在当前实现下可能导致性能降低(内存占用/带宽、CPU 合成开销、SPI flush 开销),待进一步探究
- 记录 SPI 性能观察:即使配置了硬件 SPI,实测 SPI flush 仍大致在 50-85ms 区间,没有出现预期级别的下降——瓶颈更可能在渲染/刷屏路径,而不是单纯的 SPI 时钟配置
- 记录 profiling 残影根因:开启 kEnableProfiling 后,最初的脏矩形刷新流程曾出现"先合成全部 rect、再统一 flush"的共享缓冲覆盖问题,导致相机移动时顶部错刷、静止时角色周围残影;已改为按 rect 逐个 compose 后立即 flush,profiling 只负责计时,不再改变渲染逻辑
- 后续优化方向:优先合并相机变化时的脏矩形,减少重复刷新区域,再评估是否需要更底层的显示驱动或 DMA 路径
- 考虑开源(gitignore 屏蔽 md 文件即可),但之前的 git 记录里已经提交过,先再想想
2026/5/8 — 渲染性能与内存管理
- √ 全面梳理资源预处理与渲染逻辑,确认 PSRAM 缓存、相机位移、预合成、白猫精灵预加载等优化均已落地
- √ 发现并修复
sampleImagePixel_()屏幕边界检查 bug:预合成时dstX >= 240的像素被 reject,导致composedFgLayer_右半部分全黑,相机右移后右侧场景不显示;移除dstX >= kTftWidth约束,保留正确的图像边界检查 - √ 发现并修复
sceneWidth_膨胀问题:composedWidth被含 L0/L1 的 sceneWidth 覆盖,背景层比前景层宽时合成缓冲区膨胀一倍,浪费内存和预合成时间;删除该覆盖 - √ 优化
writeForegroundRegionToBuffer()合成层路径:相机钳位移到循环外,跳过 alpha 检查,直接像素读取,消除函数调用开销 - 识别 L3 深度排序需求:L3 可能同时处于角色之前和之后(树冠遮挡、地面被走过),不适合预合成——将 L3 从预合成中剥离,在
composeCompositeRegionToBuffer()中根据depthRefY(玩家底部)做 Y-based 深度排序 - √ 发现并修复脏矩形重叠导致的严重性能问题:相机移动时 7-8 个脏矩形大量重叠,同一像素被多次合成和多次 SPI 刷新;L3 采样将每像素成本从 ~20 cycles 提升到 ~60 cycles,重叠代价放大 3 倍,表现为 compose 95ms + flush 127ms(约 4fps)。在合成循环前加入脏矩形两两重叠检测与包围盒合并,7 个矩形合并为 1 个
- √ 更新架构文档,覆盖全部 5 个修复项和当前运行时行为
2026/5/9
- 加 UI 层,已加转点阵字库脚本,板子上还没有做改动
- 加交互层
- 加第二个场景,加入错误乱码粒子特效
- 如果一个场景中有多个特效资源应该如何处理
2026/5/10
- 把能抽离到 config 的配置都抽离一下,并按不同种类划分
- √ 空白文字显示为乱码问题
- 可以考虑加一个全局地图
- 记得加音响和音频文件
- 用代码实现屏幕乱码的效果
- 对话时在屏幕上方显示两倍或三倍大小的角色
- × 把 sd 卡的传输和游戏模式写在一起,串口监视器按 T 直接传输、不用每次重新烧录——无法实现
- 目前 sd 卡 32G,游戏资源做完估计也不超过 5M,用 sd 太浪费,后续可尝试把资源放到 flash(SPIFFS)——暂时搁置,发现还是需要 msc 烧录,以后考虑 webui 方案和预编译方案(资源和代码打包在一起)
- 多动态特效处理方案和多交互对象处理方案存在很多问题
- 加入第二个地图
2026/5/10
- √ 对话 UI 实现与文本框绘制规范,优化对话交互体验
- √ 位图字体生成工具和字符脚本
2026/5/11
- 考虑把需要高效执行的代码放到 PSRAM 里
- SIMD/向量扩展加速 2D 像素运算
2026/5/13
- 场景切换时,原场景没有提前清除
- 好像没有做缓存管理(释放和加载)
- 只有角色移动的时候特效动画才播放;我想实现某些特效一直播放、某些播放固定次数后停止、某些播完即消失,可能需要状态管理
- scene2 的很多图层没有添加进来
2026/5/14
- 目前的代码优化越来越像引擎层的优化,以后可以考虑直接抽离代码,做个简单的针对 2D 横板的游戏引擎
2026/5/17
- 交互层的视差系数应该和中景一致,而非近景
2026/5/20
- 参考同类风格作品继续打磨
- L6 的 offset 没有被应用?
- 考虑修改剧情和设定进行开源,或者直接开源
2026/5/21
- 欢迎二次开发——无论是向底层游戏引擎的抽象,还是向上层游戏的开发
2026/5/21 — 重大修复:前景层/角色描边残缺与黑边问题
问题描述:
所有透明 PNG 资源(L6 前景树木、actor 角色、scene2 的 L4_1/L4_2 粒子特效)的左侧轮廓有 2-3px 宽的"残边"(内容被截断),右侧轮廓有 2-3px 宽的黑色描边。L0/L1 大背景、scene1 L4_1 薄雾/L5 打光、L3_2 感叹号不受影响。
根因:
板子配置 boards/esp32-s3-devkitc-1-n16r8.json 的 extra_flags 中定义了 -DARDUINO_ESP32S3_DEV,导致 PNGdec 库启用 ESP32-S3 汇编优化函数 s3_rgb565() 做 RGBA→RGB565 转换。该函数存在 bug:输出的 RGB565 像素数据整体水平偏移约 2 个像素(通过 DIAG 日志验证:PC 端 px[0]=0x10C3 在 ESP32 上跑到了 px[424])。而 alpha mask 由纯 C 代码 PNGMakeMask 生成,不受此偏移影响。
后果:alpha 标记的"透明/不透明"位置和实际颜色数据错位 2 像素 → 左边缘颜色缺失(残边)+ 右边缘颜色溢出到透明区(黑边)。
为什么之前没有暴露:
旧代码存在"双重渲染"路径(静态前景 sampleForegroundColor → fgRowBuffer_ → frontColor != 0,与动态前景 composite 循环内逐像素采样)都会覆盖同一区域,两条路径共用同一份 pixels[] 和 alpha[],虽都偏移但偏移量一致,自我覆盖时视觉上"自洽"。本次改动把前景渲染合并为单一路径,失去双重覆盖的"自我修正",偏移被直接暴露在最终输出上。
为什么薄雾/打光不受影响:
它们是大面积单色均匀过渡、边缘模糊的效果,1-2px 偏移在模糊边缘上不可见;L6 树枝轮廓、角色描边、小粒子边缘分明,对偏移高度敏感。
修复措施:
- √ 移除故障的汇编优化(根因修复):
boards/*.json与platformio.ini中移除-DARDUINO_ESP32S3_DEV,执行pio run --target clean彻底清缓存后重编,PNGdec 回退到纯 C 的PNGRGB565()(无偏移、略慢 ~2ms/全屏图,仅启动时一次性执行,不影响帧率) - √ 统一前景渲染路径(架构修复):
sampleForegroundColor()改为遍历动态层采样;从composeCompositeRegionToBuffer()中移除动态层逐像素采样逻辑;新增fgCoverageMask_成员,用 coverage 标记替代frontColor != 0判断,避免深色像素被误判为透明;写入前景时同步更新 coverage,相机平移时由shiftCoveragelambda 一起平移 - √ 避免双重加载(防御性修复):
validation_app.cpp检测到scene_objects.json存在时跳过静态 L4/L5/L6 的目录扫描加载
性能影响评估:纯 C PNG 解码仅在场景加载时执行,426×240 全屏图汇编约 0.5ms、纯 C 约 2-3ms(可忽略);coverage mask 240×240 = 57KB 额外内存(1 byte/pixel),对运行帧率无影响。
2026/5/21
- √ 更新项目文档与结构,同步场景资源结构、渲染链路与文档资产
2026/5/22
- 开源之前把 readme 和开发指南做好,说明各个开发步骤,包括字体、美术资源、烧录教程(改资源后的烧录、只改代码的烧录、挂载 sd 卡的烧录)、引脚说明等
2026/5/28 — SPIFFS 资源迁移完整修复
背景:从 SD 卡迁移到 SPIFFS 后,场景图层(y 层)全部无法渲染。本次完成了从问题排查到完整修复的全流程。
问题 1:SPIFFS OBJ_NAME_LEN=32 路径超长
- 现象:SPIFFS 中只有 3 个文件
- 根因:SPIFFS
OBJ_NAME_LEN=32限制,旧路径(如/spiffs/assets/layers/l0/xxx.png)超长 - √ 解决:缩短路径命名规则(
layers→y、sprites→s、文件名压缩),更新所有代码、文档和导出脚本
问题 2:spiffsgen obj-name-len 不匹配
- 现象:spiffsgen 用
obj-name-len=64生成镜像,运行时 SPIFFS 用 32 读取 - √ 解决:在
scripts/fix_spiffs_size.py中显式指定--obj-name-len 32
问题 3:PNGdec 解码失败(核心问题)
- 现象:所有 y 层 PNG
open ok但decode fail(code=2/8/1) - 根因:PNGdec v1.1.6 在
iPitch > 1024字节时解码失败(库 bug,与编码方式/滤镜/压缩级别无关) - 验证过程:CRC 校验确认设备读取数据与 PC 文件完全一致(数据无损);控制变量测试——pitch≤1024 OK,pitch>1024 FAIL;240×240 RGBA pitch=960 OK、256×64 RGBA pitch=1024 OK、426×10 RGBA pitch=1704 FAIL
- √ 解决:将 y 层 PNG 预解码为
.raw格式(RGB565 + 1-bit alpha),绕过 PNGdec——新增scripts/png_to_raw.py,PngRenderer::loadFileToCache自动检测同名.raw优先加载,sprites/title 保持 PNG(pitch≤1024)
问题 4:SPIFFS 目录遍历不支持 .raw
- 现象:
listPng通过opendir/readdir返回 0 候选 - 根因:ESP32 SPIFFS 的 POSIX
opendir实现对.raw文件不可靠 - √ 解决:在
resolveRenderAssets中加 fallback 探测,直接用storage_.exists()检查已知路径模式
问题 5:场景切换路径错误
- 现象:场景 1 正常,场景 2 的 L0/L1 不显示
- 根因:
resolveLayerPath用"l0"作目录名,实际目录是"0" - √ 解决:去掉
l前缀
问题 6:PlatformIO 增量编译不检测变化
- 现象:代码修改后固件未更新,诊断日志不出现
- 根因:SCons 构建缓存未失效
- √ 解决:创建
flash-all自定义目标(先清缓存再全量编译)
关键命令:pio run -e flash-with-resources -t flash-all(清缓存 + 全量编译 + 刷固件 + 刷 SPIFFS,推荐)
2026/5/29 — 动态层动画播放修复 + 双格式导出脚本
问题:L41/L42 粒子动画不播放
- 现象:场景 2 的 L41、L42 粒子效果只显示第一帧,不播放动画
- 验证:串口日志显示 4 帧全部加载成功,meta 文件正确
- 根因(两个):
SceneObject::periodMs未初始化——结构体字段包含垃圾值1070512144,导致dyn.periodMs = so.periodMs ? so.periodMs : metaLayer.periodMs使用垃圾值而非 meta 中的 100mslayer4Animated_标志未设置——addDynamicLayer未设置动画标志,导致渲染走快速路径跳过动态层
√ 修复:
scene_loader.h—SceneObject::periodMs和loopCount添加默认值初始化(= 0)placeholder_canvas.cpp—addDynamicLayer中根据图层名称设置layer4Animated_/layer5Animated_标志
√ 新增双格式导出脚本:art/aseprite_scripts/export_scene_raw.lua 同时导出 raw(export/,用于设备刷机)和 PNG(export_png/,用于预览调试),支持 EXPORT_EMPTY_FRAMES 配置;原 export_scene_enhanced.lua 保持不变
√ 文档更新:资源导出与加载说明、资源命名规范 v2.0→v2.1,AGENTS.md / CLAUDE.md 同步 SPIFFS 资源管道说明
2026/5/31 — 开源
- 开源仓库,整理
.gitignore,移除已跟踪的构建/IDE/暂存文件 - √ README 加入游玩截图
- √ 修正仓库徽章 URL,更新仓库链接
- 做完开源视频后,把视频加入到 readme