下载 App

【聚光灯】牧羊人 开发日志#3 AI x PICO-8 完整工作流分享

修改于10/0443 浏览综合
TapTap
前篇日志:
今日关卡制作灵感续上了,目前关卡数来到20个,贴其中一个风道迷宫的关卡截图,有小时候玩宝可梦时候在山洞冰面滑来滑去的感觉。
TapTap
点击试玩:
好了人类看的日志内容结束了,如果你也想用ai做pico-8游戏的话,可以给你的机器伙伴继续阅读下面的内容~
(注:ai还是只能cover程序的部分,关卡设计不来的,我关卡都是手搓;ai 最开始自告奋勇帮我做关卡可完成验证,结果我测过的关卡他说自己枚举测试无法通关,菜……)
horizontal linehorizontal line

和 AI 一起做 PICO-8 卡带:一张卡从零到交付的完整工作流

分享我们做 PICO-8 游戏时固定走的一条流水线。核心思路:除了"好不好玩",其它问题全部交给命令回答。
每做一张卡带都按同一套顺序走:规格 → 生成素材 → 无头跑 → 断言回归 → 截图看图 → 查预算 → 一键盘带/网页包 → 人工终检。
0. 一句话总览
docs/specs/ 写下规则
      │
      ▼
<game>.p8 的 __lua__  ──── 唯一的代码来源
      │                  (边写边 pico8 -x 无头跑;语法错会挂死,所以一定套 timeout)
      ▼
assets/gen_data.py ──── 美术 + 音频,用脚本就地重写 __gfx__/__sfx__/__music__
      │
      ▼
tests/ 无头断言回归 ──→ p8count 预算 ──→ bot 试玩截图(我直接看图)
      │
      ▼
tools/export.sh  一条命令:重建数据段 → lint → 回归 → 刷封面 → 导出 .p8.png → 冒烟
      │
      ▼
package_web.ps1 / build_bin.py ── 网页版(掌机壳 + 触屏)与四平台独立 exe
      │
      ▼
人工终检:GUI 里跑、CTRL-P 看 CPU、耳朵听音色、手柄摸手感
1. 工程长在 PICO-8 的虚拟盘里
不做"源码在别处、跑的时候再拷进 PICO-8"这种往返搬运,直接把工程放在 PICO-8 的虚拟盘:
carts/<game>/
  <game>.p8            卡带本体,自包含:代码 + gfx + map + sfx + music
  tests/               test_lib.lua、test_*.p8、bot_*.p8、run_tests.sh
  assets/              生成器(像素画、音符、封面)——卡带运行时永远不读这里
  tools/               build.sh / export.sh / playtest.sh / lint_*.py / 关卡编辑器
  docs/specs/          设计规格
  docs/plans/          实现计划与验证记录
  shots/               无头截图产物(生成物)
好处是 load <game> 和 SPLORE 直接就能跑,导出也不用改路径。
硬约束:卡带必须自包含(内部不带 #include),否则 SPLORE 加载和导出都会出问题。只有测试卡和 bot 卡才用 #include ../<game>.p8。
我们有一个最小脚手架 carts/_template/(自带 12 条断言和一条跑通的无头试玩),新游戏就 cp -r 过去改名字——它 README 里列着的坑都已经踩过了。
2. 代码怎么写
先写规格,再写代码
新功能先落 docs/specs/<game>.md:规则、数值表、状态机、关卡格式。做完的阶段和踩的坑记到 docs/plans/。
这一步看着多余,其实最省事。Shepherd 的 M0 是手工推演策划案的关卡,先发现 3 处按规则不成立的地方、改了数据才动手写代码——省掉了"做完了发现关卡无解"的返工。
写代码时的三条铁律
  1. 状态全放在一个 g 表里,new_game() 可重复调用。 测试靠重新调 new_game() 重置世界。
  2. 所有输入只走 btn()/btnp()。 这是无头测试能驱动游戏的前提,没有第二条路:测试 stub 掉这两个函数就等于接管了玩家。任何用 stat() 读键盘的写法都会让测试失效。
  3. 不要定义与内置同名的全局变量。t(就是 time())、add、del、all、stat、count 都踩过。
PICO-8 Lua 的方言坑(动手前先知道)
坑  说明
没有 sort()、没有 pcall()  自己写插入排序;cartdata() 包不了保护
for i,v in all(t) 只给值  v 是 nil,要下标得用 pairs
del(t) 不能当弹栈  撤销栈因此吃过 nil,弹最后一个用 deli(t)
数字是 16.16 定点,范围 ±32767.99999  99999 环绕成 ≈ -31073;要在算术之前钳制,事后 min() 补不回来
>>/<</shr()/shl() 是定点数移位  13>>1 == 6.5,不是整数位运算;位测试用 ch&(1<<b)~=0
单行 if (cond) stmt 是简写形式、不带 end  它吞掉整行剩下的内容,那个多余的 end 就变成语法错
cos/sin 吃 0..1  不是 0..2π,而且 sin 方向是反的
按钮 0..3 = 左/右/上/下  与"上/右/下/左"的方向表不是同一套下标,得做映射
无头跑是常态,但必须套 timeout
bash scripts/p8run.sh cart.p8        # 内部:timeout + pico8 -x + grep 错误
Lua 语法错误会让 pico8 进程永久挂住(不退出、不报错),所以所有无头调用一律套 timeout,并把 syntax error / runtime error / program too large / could not #include / could not load 当失败信号。
注意 runtime error 只打印一行、进程仍然退出 0,光看退出码会漏。另外绝不要用 pico8 -h 之类去探参数:任何未知 flag 都会弹 GUI。
3. 素材怎么写:用生成器,不手改数据段
这是我觉得最值得分享的一条。
.p8 里的 __gfx__ / __sfx__ / __music__ 是十六进制文本,手改既难读又易错(每行长度都是硬约束:__gfx__ 128 个 hex、__sfx__ 168、__music__ 11)。所以我们的分工是:
  • <game>.p8 的 __lua__ 是代码的唯一来源,生成器从不碰它。
  • 美术和音频住在一个 Python 文件里(assets/gen_data.py):8×8 精灵就是 "33333333" 这样的字符串数组,'.' 表示 0 号色(透明);音符也是数据表。bash tools/build.sh 把它就地写回卡的三个数据段。
# assets/gen_data.py 里的样子
SPR = {
    0: [  # grass A
        "33333333", "333b3333", "33333333", "333333b3",
        "33333333", "3b333333", "33333333", "3333b333",
    ],
    ...
}
事后看这完全是赚的:
  • 可复现:删掉数据段重新生成,逐字节一样。
  • 可 diff:改一个像素是一行字符串的 diff。
  • 可程序化:风炮的四个朝向不是手画的,是"灰鼓+橙箭头"旋转生成的;两种草地平铺是同一套像素的变体。
  • 图片就是反馈:gfx_preview.py 把精灵表导成 PNG,我(AI)直接看图确认像素画对不对,而不是靠读 hex 猜。
几个具体做法:
  • 从参考图反推结构。比如做 logo 时从参考图量出"5×5 格子、四角为空",再写脚本生成,而不是手工凑。
  • 像素风比平滑渐变省一个数量级。5px 美术格 + 硬边 + NEAREST 放大后,面板图从 21/37KB 降到 1.8/2.4KB。
  • 音频:4 个声道里 BGM 只占 3 个,留 1 个给音效。这是踩过大坑的:music() 的 mask 是"保留给音乐"的声道,一旦被 sfx() 显式插进来,那个声部的音乐就断很久——听起来像掉帧,其实 CPU 很低。tools/lint_audio.py 专门守这条规则。
  • 封面 __label__ 从画面生成。PICO-8 拒绝导出没有 __label__ 段的卡带(please capture a label first)。做法:无头跑标题屏 → extcmd("screen") 截图 → make_label.py 把 128×128 截图按调色板映射回 hex 写进 __label__。同一张封面同时用在标题画面和 .p8.png 封面。
  • 关卡由编辑器产出。浏览器里的关卡编辑器(单文件 HTML,含同规则试玩)导出 JSON,tools/import_levels.py 写回 lvls={...} 那段。关卡可通关性由人负责,测试只查数据完整性。
  • 文字必须 ASCII。PICO-8 内置字体没有中文字形(自己做点阵字库会吃掉大量 token);而且卡带头两行注释会变成导出 .p8.png 的 title/byline,混进中文就是乱码——tools/lint_ascii.py 把这条钉死。
4. 做什么检查:五层,每层都能无人值守出结论
① 能加载、能跑
p8run.sh:套 timeout 跑一遍,grep 掉所有错误关键字。这一层挡掉语法错、runtime error、token 超限、include 失败。
② 行为对不对:无头断言回归
tests/test_*.p8 一组测试卡,每张的结构是:
#include ../game.p8      -- 游戏 include 在前,测试覆盖定义在后
#include test_lib.lua     -- stub 掉 btn/btnp/music/sfx,捕获游戏的 _update/_draw
function _init()
  cartdata("game_tests")           -- 一次运行只能调一次,用测试专用 key
  tcase("move right") fresh() step(5,{[1]=true}) check(g.p.x>x0,"x grew")
  ...
  printh("TESTS DONE ok="..ok.." fail="..fail)
  extcmd("shutdown")
end
harness 提供 tcase / check / step(n, keys) / fresh(seed) / finish()。测试在 _init 里一次跑完(不是在帧里),打出 PASS:/FAIL: 行和最后一行的 TESTS DONE ok=N fail=M,再 extcmd("shutdown") 干净退出。run_tests.sh 逐张跑,要求 fail=0 且必须看到干净的 TESTS DONE(没看到也算失败——避免"什么都没跑"却报绿)。
Shepherd 现在是 2359 条断言:羊的规则、交互、地图边界、关卡数据完整性、音频格式。测试卡按组拆开,因为 8192 token 上限包含 include 进来的游戏代码(一个 5000 token 的游戏只剩约 900 token 写测试)。
test_lib.lua 里三个反直觉但必须做的点:
  • fresh() 必须清空按键状态(键状态是模块级变量)。上个用例结尾按住的键会残留、吃掉下个用例的第一次 btnp();症状是"tap scores 少记一次分",而 sfx 日志看起来完全正常。
  • step() 里要同时调 _update 和 _draw,让绘制代码里的错误也能浮出来。
  • Windows 上 printh() 输出带 INFO:  前缀。要 sed 's/^INFO: //'剥掉前缀,而不是 grep -v '^INFO:'——那样过滤会把你所有测试结果一起删掉(表现:跑了但一行输出都没有)。RUNNING: 才是唯一的噪音。
③ 画面对不对:让 bot 玩给我看
写一个 bot 卡:#include 游戏、stub 掉输入、按脚本/策略跑 N 帧、在关键节点 extcmd("screen") 截图,最后 extcmd("shutdown")。跑完我直接读那些 PNG 判断画面。
bash tools/playtest.sh shepherd.p8 tests/bot_play.p8 150   # 截图落到 shots/
这个反馈环效率非常高:风格不对、元素错位、文字跑到一行、羊没进圈,都是"看一眼就知道"。而且 shots/*.png 同时是分享素材和回归证据。
注意两个坑:① helper/bot 卡 #include 只带代码,不带 gfx/map/sfx/music,所以开头要 reload(0x0000,0x0000,0x3000,"../game.p8") 把精灵表拉进来,否则渲染出来是一片沙色、没有精灵;② 截图编号会顺延,重跑前先删旧的 shots/<bot>_*.png,否则你看到的是上一轮的图。
④ 预算够不够:token / char / compressed
PICO-8 没有运行期 API 能读这三个数(info() 只能画在屏幕上),所以 p8count.sh 生成一张临时探针卡,#include 目标卡 + info() + 截图,把 TOKENS /8192、CHARS /65535、COMPRESSED /15360 拍下来读。回归脚本里固定跟一步。
⑤ 数据与格式
  • 关卡数据完整性(尺寸/字符合法/玩家起点唯一/有羊有圈);
  • 编辑器那套 JS 模拟器用 quickjs 直接跑单测(21 条),比开浏览器轻;
  • 需要 DOM/键盘/像素级断言时才上真浏览器:PowerShell 起 headless Edge + CDP。
无头测不出来的东西(很重要,别自欺)
  • CPU 用不了无头测。stat(1) 恒返回 0.0007;-x 还硬限速 30fps,空卡和满负载游戏都是 ~34ms/帧(实测 60 帧 3.3s、600 帧 21.7s,空卡和完整游戏一模一样),-foreground_sleep_ms 0 也解不开。所以只能按工作量估算,最终靠 GUI 里 CTRL-P 确认。
  • 音频听不出来。-x 不跑混音器,stat(46..49) 停在最后一次赋值的声道上不再清零。只能验格式/音高范围/长度对齐/是否指定了声道;音色、有没有掐断 BGM,只能耳朵听。
5. 交付:一条命令出成品
tools/export.sh 就是我们的"发版"按钮,五步,任何一步失败就停:
1/5  美术音频数据段   lint_audio(声道划分)→ lint_ascii(被渲染的文字)→ 重建 → 同步编辑器精灵
2/5  回归测试         tests/run_tests.sh 必须 ALL TESTS PASSED
3/5  封面             清旧截图 → 无头跑标题屏 → make_label.py 写 __label__
4/5  导出 png 卡带    pico8 <game>.p8 -export "-f <game>.p8.png"
5/5  冒烟             p8run.sh 再跑一遍本体,无输出即无错
产物:
产物  说明
<game>.p8  源卡带(自包含:代码 + 数据段)
<game>.p8.png  带封面的 PNG 卡带,SPLORE 直接能加载、可以贴 BBS
web/<game>_html/  网页版,file:// 双击就能玩
<game>_single.html  单文件网页版(手机发给别人很方便)
<game>_web.zip  分享包
网页版和独立可执行文件各有一条命令:
# 网页版 + 验证:掌机壳(像素风触屏方向键 + 按压反馈)+ 自动播放
python scripts/build_web.py path/to/game.p8
powershell -File scripts/webcheck.ps1 -Cart path/to/game.p8 -Mobile    # 手机视口 + 注入触摸
powershell -File scripts/webcheck.ps1 -Cart path/to/game.p8 -Desktop   # 桌面窗口形状
# 一步到位:构建 → 桌面校验 → 手机校验 → 单文件 → zip
powershell -File scripts/package_web.ps1 -Cart path/to/game.p8
# 四平台独立可执行文件(Windows/Linux/macOS/树莓派,自带运行时不依赖 PICO-8 安装)
python scripts/build_bin.py path/to/game.p8
网页版的验证是断言而不是眼看:#canvas 从 0×0 变成真实尺寸(说明卡带启动了)、相隔 1.5 秒的两张截图 MD5 不同(说明帧在推进)、手机模式下注入触摸按摇杆真实中心命中死区。桌面模式还有一条:页面无法 resize 自己的窗口(window.resizeTo 在普通标签页里是 no-op),所以外壳改为"发布期望的窗口形状 + 在拿到的窗口里居中",并用 1280×800 / 722×1282 / 800×1280 三种尺寸断言不溢出。
卡没变的话重建是逐字节相同的(md5 不变)——因为外壳永远从一份全新的 stock 导出重建,而不是在上一次构建的基础上打补丁。
6. 最后一道关是人
流水线能证明"这张卡没坏",证明不了"这张卡好玩"。所以每轮末尾都留出人验的位置:
  • CPU:GUI 里跑,CTRL-P。0.1 左右舒服,>0.8 在慢机器和网页播放器上就要掉帧。
  • 音色/音乐:耳朵。BGM 有没有被音效掐断,只有听。
  • 手感:手柄/键盘。键盘事件在浏览器里没法用合成事件覆盖,所以键盘手感必须人工。
  • 关卡可通关性:这是设计者的活,测试不替你做。
所以我们的循环长这样:我写代码、生成素材、跑断言、截图、查预算、打包 → 人玩游戏、听声音、看 CPU、做设计判断 → 反馈回来我再改。 AI 负责所有能自动化的部分,人只负责审美和判断。
7. 分享几个最省时间的坑
症状  原因  解法
pico8 -x 永远不返回  Lua 语法错误会挂死进程  一律套 timeout;syntax error 当失败
退出码 0 但测试什么都没跑  runtime error 只打印一行,仍退出 0  grep 输出里的 runtime error
跑了但一行输出都没有  你把 ^INFO: 过滤了(Windows 上前缀里装着全部结果)  sed 's/^INFO: //' 剥前缀,只滤 RUNNING:
用例少算一次  fresh() 没清按住的方向键,btnp() 被上个用例吃掉  fresh() 里 reset 输入 + 清日志
could not #include file 然后挂死  #include 路径相对卡带,且加载失败会挂  helper 卡放在游戏旁边;一律套 timeout
helper 卡渲染出沙色、没有精灵  #include 只带代码,不带 gfx/sfx/music  开头 reload(0x0000,0x0000,0x3000,"../game.p8")
黑色精灵全是洞、透出地板  spr() 里 0 号色是透明的  用别的颜色填底,配 palt(0,false) palt(3,true)
大坐标下距离全错、计时器瞬间触发  16.16 定点溢出  在算术之前钳制
program too large  8192 token 上限把 include 的代码也算进去  拆成多张测试卡共享一个 test_lib.lua
please capture a label first  卡带没有 __label__ 段,PICO-8 拒绝导出  无头截标题屏 → 生成 __label__
网页版一直不开始  默认模板 p8_autoplay = false 在等点击,而启动回调绑在容器上不是 canvas  构建脚本里设 autoplay 并主题化外壳
8. 想复现这套流程
需要:PICO-8 0.2.7、uv(跑 Python 工具,Pillow 走镜像装)、PowerShell(浏览器验证要真 Edge),以及一份把这些约定写下来的技能说明。
# 开新游戏
cd <PICO-8 虚拟盘>/carts
cp -r _template mygame && cd mygame
mv _template.p8 mygame.p8 && sed -i 's/_template/mygame/g' tests/*.p8 tests/run_tests.sh
bash tests/run_tests.sh            # 12 条断言应该全绿
# 日常循环
bash tools/build.sh                # 改完美术/音频后重建数据段
bash tests/run_tests.sh            # 回归(同时报 token 预算)
bash tools/playtest.sh mygame.p8 tests/bot_play.p8 150   # 无头试玩 + 截图
# 发版
bash tools/export.sh
powershell -File package_web.ps1 -Cart mygame.p8
一句话总结:把所有能自动化的东西都压到"一条命令 + 一个退出码",
把无头做不到的三件事(CPU、音色、手感)明确标出来交给人工。
这样 AI 才能真正持续地做卡带——因为每一步它都能自己看到结果,而不是猜。
合集
聚光灯2026
活动 | 聚光灯游戏创作挑战
00:39
【开发日志3】主场景与玩法无缝化衔接截图
【开发日志3】主场景与玩法无缝化衔接
大家好我是鸭。这里是墨江里的一群鸭,嘎嘎嘎~ 今天主要时间用来搭建主场景了,感觉有点浪费时间啊……理论上来说要进入到搜打撤玩法的unity场景搭建了。 不想打字了,大家看看画面表现效果吧~ #TapTap聚光灯独立游戏
11 赞
1 回复
00:10
群迹·上取其下,下取其上截图
群迹·上取其下,下取其上
#聚光灯gamejam开发者日志 向大家汇报一下今日进度,目前在和蚁群的默认移动算法进行斗智斗勇。或许是我的AI对蚂蚁有别样的理解,其实最初写出的方案更接近于围绕着恒星公转的行星。 哦我还以为文章里可以插入多支视频视频,想要展示一些其它的运动方式,可惜了。那么就简单在上方传一个比较典型的,有点类似于蟑螂的运行方式。 目前仍在解决蚁群的随机运动,和玩家可操纵性之间的平衡问题。以及如各位所见,目前的
3 赞
1 回复
00:27
7人小白组成的大一新生队会赢吗😡👊截图
7人小白组成的大一新生队会赢吗😡👊
关于“涌现”我们的理解是:1.单一组合大于整体的效果。2.玩家有探索自由度。3.游戏体验内容大于游戏本身容量。 于是我们就想出来了这个创意闯关游戏——《暗生》 简单来说就是让游戏里的植被/火焰/水流/石头沙子等等物体可被玩家控制,同时它们又具有超级自由的组合反应(类似元素反应)。 比如少量水可灭火,大量水有浮力,火可驱动水成为蒸汽运输,植被可促使火焰蔓延,而石头与蒸汽又可制造爆炸……我们赋予物体真
4 赞
5 回复
『开发者日志04』终于做好了视觉编辑截图
『开发者日志04』终于做好了视觉编辑
『开发日志04』终于!物品视觉方面做出来了一些,我使用了组件化拼装模式来制作各种物品外观和内容,也在考虑之后要不要把这个编辑器也加在游戏上?如果有时间的话,会考虑加入,那样会多出更多玩法,外观模样上也可以自行配置。 还需要添加更多的组件,以及制作更多的物品,给各个物品添加视觉模样,大工程啊大工程,因为计划了至少几百种物品。但现在看来可能做不全了,还有一些功能性物品可能也没时间做了。 项目还
3 赞
蜡忆余彩:开发日志【01】一张“灵魂涂鸦”是怎么被美术救活的?截图
蜡忆余彩:开发日志【01】一张“灵魂涂鸦”是怎么被美术救活的?
最开始,我们只是想做一张“小孩子画的画”。结果我给美术发过去的第一版,大概长这样【p1】 【p1】 嗯。太阳,大树,三个火柴人,还有一张不知道为什么像烧烤架一样的桌子。 从绘画水平上来说,确实非常符合“儿童画”这个要求哈哈哈哈哈哈。 但这张看起来非常随便的草图,其实后来变成了我们整个游戏里最重要的一件道具。 这张看起来最简单的画,后来反而成了我们整个游戏里最不能随便画的一张图。 因为它不只是场
31 赞
16 回复
《气味之城》开发日志#1|为什么"涌现"和猫是天作之合截图
《气味之城》开发日志#1|为什么"涌现"和猫是天作之合
大家好,我是一个人报名了 2026 聚光灯 21 天游戏创作挑战 的独立开发者。今年主题公布那一刻我就笑出了声——涌现。因为我已经想做这个游戏很久了: 《气味之城》:你不是猫神,你只是一只猫。 先说一个所有养猫人都懂的事实:你永远无法"教会"一只猫任何它自己不想学会的事。但你家猫可能自己学会了——听见冰箱开门就瞬移到厨房;你心情不好时过来踩奶;凌晨三点准时在你身上练马拉松。没人教过它,这些行为是它
3 赞
2 回复
【2026聚光灯日志01】什么叫你把手牌,骰子和战棋融合了?截图
【2026聚光灯日志01】什么叫你把手牌,骰子和战棋融合了?
大家好啊,我是官方小编,一位还在学校的独自开发的苦逼带学生 这次算是第二次正式参加GameJam,而这回的比赛主题是【涌现】 嗯…老实说,一开始我还真只是从中文语义上来看,理解成“大量的出现”,直到询问ai哦哦哦我这就才想起来还有英文的“整体大于部分之和”意思来着 说起这个主题的意思,立马想起的类 吸血鬼幸存者 ,或者是 文明 系列的各种爆怪,爆兵的设计,即大量的出现敌人或者单位 而从第二个
4 赞
4 回复
《地渊防线》开发日志 · 第16天:已经把30张卡组拆了截图
《地渊防线》开发日志 · 第16天:已经把30张卡组拆了
上篇换上 Codex,体验了一把鸟枪换炮。这几天继续用,发现工具升级之后,最忙的事情变成了试玩,然后推翻自己前几天的设计。 第十二天还在日志里问:固定 30 张卡,五组、每组六张,排起来是不是有点费手?要不要改成像《杀戮尖塔》那样,边打边选卡? 四天过去,先动手把这部分改了。 现在开局只有三张火球,打完一关选一张新卡,卡组跟着进度慢慢长。主角也换上了帽子、护目镜和红围巾——上次封面里的角色,这次真
3 赞
《孢岛》聚光灯游戏开发Dev log - 04 做个记忆的书册截图
《孢岛》聚光灯游戏开发Dev log - 04 做个记忆的书册
#TapTap聚光灯独立游戏 #聚光灯gamejam开发者日志 PS:我感觉我变成女生了
6 赞
2 回复
聚光灯gamejam开发日志04丨坏了坏了这下好了真种上地了截图
聚光灯gamejam开发日志04丨坏了坏了这下好了真种上地了
本台记者小鱼酱为您报道! 小鱼酱又去项目组转了一圈,发现老师们忙得不可开交,鱼鱼差点连采访的空隙都没找到。不过,来都来了,总不能空手回去。今天就先从美术老师的桌上开始吧! 哇塞,这就是美术老师设计的地块吗!之前大家聊的“在这里种点东西”的位置,总算有了具体的样子,我们的小店也越来越像那么回事了! 小鱼酱已经准备挑一块风水宝地,留给未来的三头鱼新品种。 当然,光有地还不够,再看看这次又有哪些小家伙准
3 赞