作品简介与玩法
《末班车:站台保卫战》是一款在浏览器里直接运行的第一人称射击游戏。舞台是黄昏时分一座日本高架车站的岛式站台「星見台駅」(星见台站):武装分子占领了站台,每一班进站的列车都会从车门里送来增援。你要在末班车发车之前,把站台夺回来。
界面是中文,车站是日本的。菜单、HUD、设置、提示和结算都用中文;站内的站名牌、导向牌、灯箱广告、便利店、自动售货机、列车侧面和发车信息屏都是日文,站台广播用英语朗读,同时在屏幕上配中文字幕。
一局分三波:第一波敌人已经散布在站台上;清到只剩几个人时,2 站台的列车进站、开门,第二波从车厢里走出来;最后一波乘末班车抵达 1 站台,同时有人从地下通道顺着楼梯摸上来。中途还会有一列快速列车不停站呼啸而过。
- 三把武器:疾风步枪(全自动、红点瞄具)、暗影手枪(半自动)、雷霆霰弹枪(泵动、逐发装填,可随时开火打断换弹)。
- 爆头一击必杀,并触发约 1 秒慢动作,画面同时去色、音效压低。
- 命中反馈分材质:金属溅火花、混凝土扬尘留弹孔、塑料座椅掉碎屑;便利店的玻璃挨三四枪会出现裂纹然后整块碎掉,碎了之后可以钻进店里当掩体。
- 完整的列车流程:广播提示音与英语广播(配中文字幕)→ 进站减速、刹车啸叫、钢轨接缝的「咣当」声 → 停稳放气 → 车门与站台门同步打开、发车旋律 → 关门 → 加速离站。
- 手机、平板也能玩:左侧任意处按下即出现摇杆(推到底自动冲刺),右侧滑动转向,按住「射击」开火,按住时拖动还能继续调准星;另有瞄准、蹲、跳、换弹、切枪按键。建议横屏。
- 设置里可以调画质(ULTRA / HIGH / MEDIUM / LOW)、难度、敌人数量(6–24)、武器伤害倍率、鼠标灵敏度与视野角;按
F3 显示帧率与 Draw Call。
整个项目由 Claude Opus 5.5 在 Claude Code 里完成:从读参考文章、设计玩法、写代码、在浏览器里自测修 bug,到按本站的发布流程上线,人只提了几句简短的需求。它具体是怎么干活的,见下文「Claude Opus 5.5 是怎么工作的」。
依据:aiknow/innovation_lab/last-train/index.html(游戏全部代码)、last-train/vendor/three/(本地托管的 Three.js r160)。
制作工具与技术栈
- 编写:Claude Code(Claude Opus 5.5)。需求、架构、代码、测试与部署都在同一个对话里完成,没有使用游戏引擎编辑器。
- 渲染:Three.js r160(WebGL2)。后处理链为
EffectComposer → 场景 RenderPass → 武器 RenderPass(只清深度)→ UnrealBloomPass → OutputPass(ACES 色调映射)。 - 贴图:全部用 Canvas 2D 在加载时现画:地砖、盲道、道砟、站名牌、导向牌、灯箱广告、自动售货机、货架、车厢侧面与车头、霓虹招牌、发车信息屏。没有任何外部图片文件。
- 模型:全部由立方体、圆柱、球体在代码里拼装,再按材质合并。没有外部 3D 模型。
- 声音:Web Audio API 实时合成——枪声、换弹、脚步、弹壳落地、玻璃碎裂、列车轰鸣、刹车啸叫、广播提示音、发车旋律,外加一只傍晚的蝉。没有任何音频文件。
- 广播语音:Web Speech API,用系统自带的英语语音朗读站台广播,同时在 HUD 上显示中文字幕和英文播报原文;语音列表尚未加载时仍以 en-US 请求播报;设备需要支持英语语音合成。
- 字体:不加载任何网络字体。界面用系统中文字体(苹方、微软雅黑等);画进场景贴图的日文用系统日文字体(冬青黑体、游ゴシック、Meiryo),避免日文汉字显示成中文字形。
- 操作:电脑上用 Pointer Lock API 锁定鼠标,浏览器拒绝锁定时自动退回「自由鼠标」模式;手机上用 Pointer Events 同时追踪多根手指,摇杆、转向和射击互不干扰。
体积:游戏本体是一个约 3,400 行、200 KB 的 index.html;另附 Three.js 模块约 0.7 MB,放在同目录的 vendor/ 下,不依赖任何外部 CDN 或网络字体。
Claude Opus 5.5 是怎么工作的
Claude Opus 5.5 是 Anthropic 的大语言模型。单独使用时,它只能「读文字、写文字」。放进 Claude Code 这样的智能体环境后,它多了一组可以调用的工具,工作方式就从「一问一答」变成了一个循环:
- 想:读当前掌握的信息(需求、已有代码、上一步的结果),决定下一步做什么。
- 做:调用一个工具——读网页、写文件、运行命令、打开浏览器、执行一段脚本。
- 看:拿到工具返回的结果:命令输出、报错、网页文字,或者一张浏览器截图。
- 改:对照目标判断结果对不对,不对就修正,然后回到第一步。
这个循环一直跑到任务完成,中间不需要人逐步下指令。本项目用到的工具与各自的用途:
- 读网页:抓取参考文章,提炼出十几项功能要点,作为验收清单。
- 写文件:把游戏分成八段写出来再拼接,某一段出错只需重写那一段。
- 运行命令:用
node --check 查语法,把 Three.js 模块下载到本地,通过 SSH 查看服务器上的栏目结构,执行发布脚本。 - 浏览器:在内置浏览器里打开游戏、截图,用 JavaScript 直接读写游戏状态做自动化测试:把准星对准某个敌人头部的坐标开一枪,检查它是否倒地、慢动作是否触发;快进击杀,检查第二波是否由列车送达、第三波是否从地下通道出现、结算页是否弹出。
- 看图:模型能直接理解截图内容。「武器太大挡住半屏」「广播字幕压住了波次计数」「封面右下角被垃圾桶挡住」这类问题,都是它自己从截图里看出来的。
一次完整的「看 → 改」
第一次实测时,截图里的血量是 0:玩家刚走到站台中段,几秒钟就被打死了。模型的判断是「六个敌人同时发现玩家、同时点射」,于是做了三处修改:降低单发伤害,让前几枪更不准,再给开火加「名额」,同一时刻最多三人射击。改完再测,玩家能活下来周旋。整个过程没有人插手。
它的优势
- 上下文长,能顾全局。三千多行游戏代码、参考文章、服务器发布管线、历史部署脚本可以同时放在上下文里。界面语言前后调整了两轮,它每次都先列出全部待改字符串(第一轮一百多处),每处替换都带断言,漏一处脚本就报错,最后再用正则扫一遍确认没有漏网。
- 跨领域的编码能力。这一个项目就横跨 3D 数学(射线与包围盒求交、局部坐标变换)、着色器、音频合成、寻路算法、游戏 AI、CSS 排版、中文本地化和服务器运维,全部由同一个模型完成。
- 长时间自主执行。从读参考文章到首次上线约一个半小时,之后加手机操作、调整语言也都是一句话的需求,由它自己改、自己测、自己发布。
- 会看、会验证,而不只是会写。它不把「代码写完」当作结束,而是自己跑起来、截图、量数据,再根据结果修正。
- 尊重已有的做法。发布前它先读了站点的运维笔记和前几个项目的部署脚本,沿用「备份 → 服务器生成 → 校验 → 原子安装 → 发布 → 匿名校验」的流程,没有直接覆盖线上文件。
- 知道自己的边界。它听不见声音,所以枪声、列车轰鸣和发车旋律是按声学知识「写」出来的,没法亲耳确认;测试用的内嵌浏览器不允许锁定鼠标,它就额外做了「自由鼠标」模式兜底,并在说明里写清哪些环节没有实测。
人在这里做了什么
定方向、做取舍:要不要做、界面用什么语言、发布到哪里、介绍要讲清楚什么。另外,手感最终要靠人来玩、来判断。模型能测出「爆头会触发慢动作」,但「慢动作爽不爽」只有玩的人说了算。
制作步骤
- 先读参考文章,把功能列成清单。参考的是一篇介绍「用 AI 一次生成日本车站 FPS」的文章。能确认的要素有:列车完整进出站、真实感枪械与换弹切枪、爆头一枪倒并触发慢动作、金属 / 水泥 / 玻璃各自的命中效果、便利店玻璃打碎、胜负结算、可调敌人数量与伤害、画质档位、
F3 性能面板,以及静态网格合并、实例化、光源数固定这几条性能手法。 - 定场景坐标。站台沿 X 轴从 -40 到 40 米、宽 10.2 米,两条轨道在 z = ±6.7 米;列车三节编组,每节 19.5 米、三对车门,停车点固定,于是站台门的开口位置可以直接由车门位置算出来。站台中部挖出一个下沉楼梯井,连到地下通道。
- 分八段写代码,每段写完再拼接。界面与样式 → 音效合成 → 渲染器与程序贴图 → 车站、碰撞体与寻路网格 → 列车与特效 → 玩家与武器 → 敌人 AI → 波次调度、HUD 与主循环。拼好后先用
node --check 查语法,再进浏览器实测。 - 第一次实测暴露的问题。武器模型离镜头太近、占了小半屏;广播字幕和波次计数叠在一起;最严重的是站在明处几秒钟就被打死——六个敌人同时开火。之后做了三项调整:单发伤害从 7 降到 5;前几枪精度更低,持续瞄准才逐渐变准;同一时刻最多 3 名敌人开火(简单 2 名、困难 4 名),其余的要排队。
- 语言改了两轮。初版照参考文章做成日本车站。之后按要求把所有文字换成中文,共替换一百多处字符串。再之后又定下最终方案:界面用中文,车站场景保留日文。菜单、HUD、设置、字幕是给玩家读的,所以用中文;站名牌、导向牌、广告、列车和发车信息屏属于这座日本车站本身,所以用日文;站台广播用英语朗读、配中文字幕,既保留临场感,又听得懂。两轮修改都用正则扫描确认没有漏改的字符串。
- 为国内访问改造依赖。初版从 jsDelivr 加载 Three.js、从 Google Fonts 加载字体,这两者在国内都不稳定。改成:Three.js 模块下载到
vendor/ 本地托管;字体改用系统自带字体(界面用中文字体,场景贴图用日文字体),画布贴图里的文字因此在加载时就能直接绘制,不用等网络字体。 - 截图做封面。在真实游戏场景里摆好列车、敌人与开火瞬间,把画布临时调成 1600×900 渲染一帧导出。测试用的浏览器面板被隐藏时
requestAnimationFrame 会暂停,所以改为手动逐帧推进主循环后再截图。 - 补上手机操作。上线后又加了触屏支持:浮动摇杆、滑动转向、可拖动的射击键、七个功能键,以及手机专用的界面布局。测试在浏览器的手机模拟模式下完成:横屏 740×360、竖屏 375×812 各截图检查布局,再用脚本模拟手指事件逐项验证:摇杆前推 1 秒移动 3.1 米、推到底进入冲刺、双指同时移动和转向、按住射击半秒打出 6 发、瞄准和蹲下可切换。
- 按本站流程上线。先通过 SSH 看清「创新工场」栏目的结构和每日发布管线,再读前几个项目的部署脚本。栏目页和开发说明都由仓库里的项目注册表生成;线上目录是一个指向不可变发布版本的符号链接,任何写入都必须走「临时文件 + 原子替换」。所以新项目的上线方式与前八个完全一致:在注册表里加一项并提交代码,服务器端生成页面并校验,原子安装到工作区,切出新的发布版本,最后以匿名访客身份逐一核对线上页面。
关键实现要点
把 Draw Call 压下去
车站里所有不动的东西(站台、立柱、屋顶、护栏、长椅、导向牌、背景楼群……)在加载时按材质分组,用 mergeGeometries 合并成每种材质一个网格。重复物体用 InstancedMesh:两条轨道约 2,700 根轨枕、36 扇站台门门扇、弹孔贴花、弹壳和玻璃碎片各只占一次绘制。实测标题画面约 225 次 Draw Call、6.5 万个三角形(含阴影与泛光各趟)。
光源数量固定,避免中途卡顿
Three.js 的着色器会随场景里的光源数量重新编译。本作固定使用 1 盏半球光、1 盏带阴影的夕阳平行光、6 盏顶棚点光源和 1 盏枪口闪光点光源;枪口光平时强度为 0,开枪时才亮,从不增删光源。加载阶段还会让敌人、弹道、贴花、碎片各出现一次并预渲染一帧,把着色器提前编译好。
武器单独一趟渲染,不会插进墙里
手持武器放在独立的场景和相机里,在主场景之后渲染,渲染前只清深度缓冲。贴墙时枪也不会穿模,武器的视角也可以和游戏视角分开调。
命中判定
世界里的碰撞体都是轴对齐包围盒,每个盒子标注了表面材质(金属、混凝土、玻璃、塑料、地面、列车)。子弹是射线,对包围盒做 slab 求交,命中面的法线直接由交点所在的轴得到,用来摆放弹孔、决定火花方向。敌人的头、躯干、腿三个判定盒随下蹲姿态变化,射线先变换到敌人的局部坐标再求交。玻璃被打中后子弹穿过去继续飞,伤害乘以 0.8。
敌人 AI
- 感知:每 0.15–0.22 秒对玩家的眼睛和躯干各发一条视线射线;发现玩家后会呼叫 16 米内的同伴,枪声会惊动 60 米内的敌人。
- 寻路:站台被划成 160×20 个 0.5 米的格子,碰撞体按敌人半径膨胀后标出障碍,用 A* 求路,再做直线可达性检查、把拐点拉直。
- 选位:围绕玩家随机采样 14 个候选点,按「能否看到玩家、离玩家约 13 米、离自己近、不和同伴挤在一起」打分,约四成概率到位后蹲下。
- 射击:3–5 发点射,命中率随距离衰减,受玩家移动速度、是否下蹲、自身是否在跑动影响;没打中的子弹会落在玩家身边的墙上,擦身而过时有破空声。
列车停得准
进站时按「剩余距离」反推速度:v = √(2·a·d),匀减速刚好在停车标处归零,任何帧率下都停在同一位置,车门才能和站台门对齐。增援敌人从车门位置按脚本路点走出车厢,到站台后再切回正常 AI。
触屏操控
- 浮动摇杆:左侧 45% 的屏幕都是摇杆区,手指按在哪里摇杆就出现在哪里,不用找固定位置。摇杆输出是模拟量,推一半就走一半速度。
- 转向与射击分离:右侧滑动只负责转向;「射击」键按住开火,按住时手指移动也会转动视角,边打边跟枪,不用松手。
- 轻度辅助瞄准:准星扫过敌人时转向速度减半;按住射击或开镜时,视角会缓慢靠向敌人的胸口,不会吸到头上。实测 1.5 秒内从偏 5° 收到 0.3°,此时离头部仍差 2.2°,爆头还得自己瞄。
- 宽屏不变形:很多手机横屏是 19.5:9,直接沿用竖直视角会把画面两侧拉得很宽。代码按 16:9 屏的水平视野反推竖直视角,740×360 的屏上从 75° 调到 67°。
- 手机默认更省:首次在手机上打开时默认低画质、简单难度、12 名敌人;顶棚点光源从 6 盏减为 3 盏,在加载时一次决定,之后仍然不增删光源。
全合成的声音
一声枪响由三层叠成:高通白噪声做「啪」的枪口爆鸣,低通白噪声做枪身,音高从 150 Hz 下滑到 42 Hz 的正弦波做胸腔里的「咚」。列车轰鸣是按车速调截止频率的布朗噪声;每行驶 20 米触发一对低频冲击,就是钢轨接缝的「咣当」。慢动作时总线上的低通滤波器扫到 650 Hz,声音一下子闷下去。
局限说明
- 画面是方块风格。人物和武器都由基本几何体拼成,没有骨骼动画和布娃娃物理,敌人倒地是一段固定的倒下动画。
- 手机支持没有在真机上测过。布局和按键逻辑是在浏览器的手机模拟模式里验证的,真机上的帧率、发热和手感还需要实际试玩。低端手机可能掉帧;iPhone 的浏览器不支持网页全屏,地址栏会占一部分屏幕。竖屏可以玩,但画面较挤,建议横屏。需要支持 WebGL2 的浏览器;集成显卡的笔记本建议选 MEDIUM 或 LOW。
- AI 比较朴素。寻路只覆盖站台层,敌人不会主动下楼梯追人;没有手雷,也没有包抄之类的小队配合。
- 广播语音看系统。macOS、Windows 一般自带日语语音;系统里没有日语语音时只显示中文字幕。
- 列车只能看不能上。车厢内部是画在贴图上的,车门打开后看到的是一张「车内」图。
- 测试范围有限。上线前只在 Chromium 内核浏览器里实测过完整流程(三波敌人、列车进出站、胜利结算);Safari 与 Firefox 未逐项验证。
给 AI 的需求示例
这次真实的需求都很短:先让 AI 参考那篇文章「看一下是否可以设计一个类似的射击游戏」;中途说了「别用日文,用中文」;要求「做好之后发布到创新工场栏目」并「介绍清楚制作方法和 Claude Opus 5.5」;上线后又提了「增加手机支持操控」,以及「游戏还是中文,车站改回日本车站,场景用日文」。其余的设计都由 AI 从参考文章里拆解出来。
如果想从零开始、一次说清楚,可以按「技术约束 / 视觉要求 / 游戏机制 / 验收标准」四块来写:
【技术约束】
单个 index.html,Three.js 用 ES Module + importmap,全部资源本地托管,
不使用外部图片、模型、音频和网络字体;贴图用 Canvas 现画,声音用 Web Audio 合成。
静态网格按材质合并,重复物体用实例化,光源数量固定不变。
【视觉要求】
黄昏的高架岛式站台:屋顶灯带、立柱、站台门、盲道、站名牌、导向牌、
自动售货机、便利店、对面墙上的灯箱广告,远处是亮着窗户的城市。
界面文字用简体中文;站内标识、广告和列车用日文,站台广播用日语并配中文字幕。开启泛光与 ACES 色调映射。
【游戏机制】
第一人称:WASD 移动、Shift 冲刺、C 下蹲、空格跳、左键射击、右键瞄准、R 换弹、1/2/3 切枪。
三把武器(全自动步枪、手枪、泵动霰弹枪),带后坐力、弹壳、枪口焰和换弹动作。
爆头一击必杀并触发慢动作;金属、混凝土、玻璃命中效果不同;便利店玻璃可打碎。
列车完整进出站(广播、减速、开门、发车旋律、关门、离站),增援从车门走出来。
三波敌人,胜利 / 失败结算页,可调画质、敌人数量、武器伤害,F3 显示帧率。
【验收标准】
加载完成后标题画面能看到场景;一局能从第一波打到结算;
爆头触发慢动作;列车停车位置与站台门对齐;F3 面板里 Draw Call 保持在几百以内。
小练习
练习一 · 调难度(约 10 分钟)
打开 index.html,找到开头的 TUNE 和 DIFF 两个对象。把 enemyDamage 改成 3、把 NORMAL 的 shooters 改成 1,再玩一局,感受「同时开火人数」对难度的影响有多大——往往比改伤害更明显。
练习二 · 加一把狙击枪(约 40 分钟)
在 WDEF 数组里加第四项:半自动、伤害 90、弹匣 5、瞄准视野 20°。参照 buildRifle 写一个 buildSniper,把它加进 Weapons.init 的模型列表,再给键盘加一个 Digit4。
练习三 · 从黄昏改到深夜(约 20 分钟)
修改 SUN_DIR 让太阳落到地平线以下,把天空着色器的三个颜色调暗,再把夕阳平行光的强度降到 0.3。按 F3 对比改动前后的帧率,并观察顶棚灯光和霓虹招牌在夜里的存在感。