我花一天做了个格斗游戏原型,判定全对,然后被「美术」劝退了
源码仓库(v1.0.0)
- GitHub:github.com/JohnnySilve…
- Gitee:gitee.com/johnny-silv…
- Release(含附件):v1.0.0 @ GitHub | v1.0.0 @ Gitee
一个 1321 行的单文件 HTML,零依赖、无构建,双击即玩。里面有关羽和赵云,能走能打能放大招,判定框可视化、帧数据面板、逐帧步进都齐了。
然后用户打开看了一眼,说:火柴人真的很丑,没办法玩下去了。
项目在这一句之后结束。
这篇不写「我多努力」,只写三件事:格斗游戏的代码到底难在哪、成本到底贵在哪、以及什么情况下应该在第三天就停手。
一、先说结论
格斗游戏的成本结构是「代码可复用、美术严格线性」。
代码那一半是一次性的:判定系统、状态机、帧数据表、命中结算,做完之后每加一个角色只是往表里填数据。美术那一半是严格线性的:一个角色的逐帧动作要 300~600 张图,加一个角色就是加三百张图,一点杠杆都没有。
而这个项目里还有一条更硬的限制:AI 出不了跨帧一致的逐帧动画。生成模型没有跨帧记忆,第 1 帧袖口三道褶、第 8 帧变成五道;视频模型能出流畅动作,但输出是带压缩的像素视频,没有透明通道,导不进游戏。
所以「用 AI 省钱做格斗美术」这条路,在当前技术下是堵死的。
二、判定跟美术一点关系都没有
这是我在动手前最想验证的一件事。结论:贴在上面的是关羽的立绘还是一块红矩形,判定代码一行都不用改。
格斗游戏的命中判定是「三框制」:
| 框 | 作用 | 谁被检测 |
|---|---|---|
| hitbox 攻击框 | 这一帧能打到哪 | 只在出招的 active 帧存在 |
| hurtbox 受击框 | 这一帧能被从哪打到 | 始终存在,随姿态变化 |
| pushbox 推挤框 | 身体占位,防止两人重叠 | 始终存在,通常比受击框窄 |
每逻辑帧跑一遍,顺序固定:读输入 → 推帧 → 各取当前帧的框列表 → 两两矩形相交 → 命中结算。
为什么用矩形不用像素:要可预测、要公平。 像素级判定会让「差一像素没打到」变成常态,玩家会认为是 bug。格斗游戏几十年都用矩形,这是刻意的选择。
三、动作是怎么实现的:帧数据表
这是整个项目最核心的结构。不是「定时切图」,而是把图片索引写进一张表,由状态机逐帧读取。
关羽的轻击,在代码里长这样:
light: {
frames: [
{ dur: 5, pose: 'lWind' },
{ dur: 3, pose: 'lHit', fx: 'slash',
hit: { ox: 30, oy: -152, w: 78, h: 62,
dmg: 8, hitstun: 15, kb: 3.0, stop: 6 } },
{ dur: 10, pose: 'lRec' }
]
}
三个字段就是全部:
dur:这一帧停几个逻辑帧。它同时决定了动作快慢和判定窗口的长度。pose:姿态名,指向姿态库里的一组骨骼数值。hit:这一帧有没有判定框。为空就是没有。
大招同理,只是数值更极端:
special: {
label: '青龙偃月斩',
frames: [
{ dur: 14, pose: 'sWind' },
{ dur: 4, pose: 'sWind2', fx: 'charge' },
{ dur: 5, pose: 'sHit', fx: 'slashBig',
hit: { ox: 24, oy: -172, w: 132, h: 124,
dmg: 24, hitstun: 34, kb: 9.5, stop: 14, heavy: true } },
{ dur: 22, pose: 'sRec' }
]
}
起手 18 帧(14+4)、判定 5 帧、收招 22 帧。判定框 132×124,比轻击的 78×62 大一倍多。stop: 14 是命中定格 14 帧——这是打击感的第一来源,比画面重要。
四、五个决定成败的结构
1. 逻辑时钟唯一。 固定 60 逻辑帧/秒,用 accumulator 累积,渲染可以插值。60Hz 和 144Hz 显示器上判定结果完全一致。不这么做,联机永远对不上,不同机器手感也不一样。
const STEP = 1 / 60;
acc += dt;
while (acc >= STEP) { step(); acc -= STEP; }
2. 帧数据表驱动,逻辑与表现分离。 加招式只加数据,不改代码。这也是换美术时判定完全不用动的原因。
3. 姿态库按角色覆盖。 公共姿态 + 每角色一套覆盖,姿态里加一个 wAng 字段直接控制武器朝向:
// 关羽:刀劈走弧线,刀从脑后抹到身前
lWind: { lean:-12, hipY: 94, armF: {x: -12, y: -78}, wAng: -128 },
lHit: { lean: 24, hipY: 88, armF: {x: 50, y: 22}, wAng: 46 },
// 赵云:枪刺走直线,收枪腰侧再平推
同一套渲染器,画出两种兵器性格。
4. 命中按帧号去重,不是布尔。 这一条我在第一版就写错了:
// ✗ 错误:一个招式只能命中一次,多段技被压成一段
if (att.hasHit) return;
// ✓ 正确:按帧号去重,不同帧可以各自命中一次
if (att.hitDone[att.frameIdx]) return;
三段连刺(6+6+11 伤害)靠的就是这个。改成帧号去重之后,实战 9 秒掉血构成是 23 + 23 + 5×5——两次完整三连刺加五次轻刺,逐段生效。
5. 特效层不参与判定,也不受 hitstop 影响。 否则打击定格的那几帧里,刀光和火花一起冻住,视觉反馈反而变弱。
五、三个真实踩到的坑
坑 1:判定窗口会跨精灵帧边界
帧数据表按「图」索引,一张图通常停 4 个逻辑帧(67ms)。但格斗游戏轻攻击的 active 只有 2~4 逻辑帧。
如果美术把 12 帧的轻拳平均分成三张图 [4,4,4],命中帧覆盖逻辑帧 58,而真实 active 是 45 帧,帧数据表就表达不出来了。玩家的体感是「有时打得中有时打不中,而且看到的帧数和手感对不上」。
修法:命中帧那张图的 duration 必须精确等于该招的 activeFrames。轻拳应该是 [3,2,7] 而不是 [4,4,4]。只有循环帧才用长 duration。
这一条美术侧零成本,但如果不写进契约,换美术那天会集体爆雷。
坑 2:后台标签页把 requestAnimationFrame 节流了
我在调判定的时候,截图看到 AI 像静止一样,12 秒只走了 20 帧,第一反应是「AI 逻辑写坏了」。
实际上是浏览器把后台标签的 rAF 压到了 1 帧/秒,按键边沿也被吞掉,游戏逻辑本身没问题。
正确做法不是加大睡眠时间,而是:把标签切到前台,再用「暂停 + 逐帧」做确定性验证。靠截图连拍去抓 active 帧是靠不住的。
坑 3:火柴人待机时武器杵到了地上
武器朝向如果纯跟手臂向量,待机手位一低,青龙偃月刀就插进地面。武器角度得单独给字段(wAng),不能靠手臂推算。
这种「一眼假」的问题,光看代码永远发现不了,必须真的渲染出来看。
六、体量账
原型做完之后,我让三个人(策划、工程、美术)分别估了一遍,数字对不上,差了四五倍,最后对齐口径重算:
| 档位 | 角色 | 招式/角色 | 代码人日 | 美术人日 | 合计 |
|---|---|---|---|---|---|
| 垂直切片(灰盒) | 1 | 1 | 18 | 0 | 18 |
| 最小可玩(纯灰盒) | 2 | 5 | 93 | 0 | 93 |
| 最小可玩 + 正式美术 | 2 | 5 | 93 | ≈60 | ≈153 |
| 标准 | 4 | 9 | 183 | 120 | ≈300 |
| 完整 | 8 | 10 | 280 | 376 | ≈656 |
换算到日历时间才有意义:
| 累计人日 | 业余 1 人日/周 | 半职 3 人日/周 | 全职 5 人日/周 |
|---|---|---|---|
| 18 | 18 周 | 6 周 | 4 周 |
| 93 | 93 周 | 31 周 | 19 周 |
| 153 | 153 周(3 年) | 51 周 | 31 周 |
| 300 | 300 周(6 年) | 100 周(2 年) | 60 周 |
关键事实:4 个角色时美术是代码的 1.9 倍,8 个角色时是 3.1 倍。 代码增长低于线性而且 AI 能提速,美术是严格线性、AI 提速为零。所以「多加一个角色」这个决定,成本几乎全在美术资产上。
七、那 KOF'97 是多少人做的
约 5070 人,1012 个月,换算下来 1.3 万人日以上。是上面 2 角色原型的 85 倍。
但人数和工期只是表面。真正的差距在三个不在同一起跑线的地方:
- SNK 没有每年重画 28 个角色。 KOF'97 里绝大多数精灵是从 '96、'95 甚至饿狼传说搬过来的,有的连色都没改。他们从 1991 年就开始积累格斗精灵,到 1997 年是六年存量做增量。而你是在空地上盖第一层。
- 他们造的是自己家的硬件。 Neo Geo 是 SNK 自研平台,工具链、烧录卡、调试机都是内部的,美术从第一天就按硬件规格画,不会出现「画完了发现塞不进去」。
- 那 50 个人是职业像素动画师。 一个人画 300~600 帧连贯动作是工种,几千小时训练,还有真人拍参考视频逐帧描的土办法。
八、现代小团队是怎么活的
没有一个成功的小团队,是靠「画 300 帧/角色」活下来的。他们全是靠换美术风格过关。
| 作品 | 团队 | 美术策略 | 结果 |
|---|---|---|---|
| Skullgirls | 十余人 | 高清手绘逐帧,一个角色上千帧 | 靠众筹 83 万美元才成,后续单角色预算约 25 万美元 |
| Divekick | 两人 | 极简几何图形 + 一根线 | 卖了几十万份 |
| Nidhogg | 两人 | 纯色剪影 | 口碑销量双赢 |
| 单人格斗(Footsies 等) | 一人 | 方块、色块、极少动作 | 被格斗圈当训练工具 |
这就是美术受限的人撞上的墙:格斗游戏的美术成本,几乎不受「你画得多简」影响。 因为它要求的不是「好看」,而是每个动作的每一帧都能被独立引用,并且对得上判定框。这个要求把「减少画量」的路堵死了一半。
九、什么时候该停
回头看,这个项目最贵的一课不是技术,是顺序:
火柴人作为「验证手感的占位」在工程上完全合理,但它是给开发者看的占位,不是给玩家看的产品。 我把这两件事混为一谈了。
正确的做法是:动手前先用半天做一个角色的完整成品效果——哪怕是免费素材套用,或者水墨剪影这种用代码就能画好的风格——让需求方先在「丑不丑」上拍板,再谈架构。
因为功能验证通过 ≠ 项目成立。
至于那 1321 行代码,没有浪费。判定系统、状态机、帧数据表、多段判定、hitstop、调试层,这套骨架换任何美术只需要换渲染层。它是一份能一直用下去的东西,只是暂时用不上了。
如果这篇文章帮你省下了一个月的试错,那它就值了。
真正想清楚「要不要做」,比闷头做三个月更重要。