1. 从零手搓 Canvas 弹幕射击为什么我建议你先跑通单文件骨架弹幕射击游戏Shootem Up简称 STG是检验一个 2D 游戏引擎是否合格的试金石。它同时压测了四个子系统玩家输入响应、敌机批量生成、子弹与实体的碰撞检测、以及高频粒子特效。任何一个环节掉帧玩家立刻能感觉到手感发飘。用 Claude Code 来做这件事的好处在于它能把一个 380 行左右的单文件 HTML 骨架一次性生成出来你不需要先纠结工程化配置直接打开浏览器就能看到三角飞船在星空里发射子弹。这篇文章面向三类人想入门 Canvas 游戏循环的前端新手、想用 AI 辅助写游戏逻辑的独立开发者、以及想找一个可复用 2D 引擎骨架的技术爱好者。核心检索词就是Claude Code 生成 Canvas 弹幕射击游戏我会把玩家移动、敌机生成、AABB 碰撞检测、粒子爆炸四个模块拆开讲每个模块都给出可直接复制的代码片段和验证方法。我试过用纯手写的方式搭这套骨架光是调requestAnimationFrame和对象池就花了小半天。后来换成让 Claude Code 先生成结构、我再逐模块微调效率高很多。下面这套流程你可以直接跟着做。先说清楚最终交付物一个index.html单文件包含 Canvas 全屏画布、深空星空背景、三角飞船、自动射击、四种敌机普通/快速/坦克/Boss、AABB 碰撞、粒子爆炸、道具掉落、波次系统和 HUD。整个文件约 380 行不依赖任何第三方库。游戏循环是整个引擎的心脏。很多人第一反应是用setInterval(gameLoop, 16)这是错的。setInterval不感知浏览器刷新率在 144Hz 屏幕上会跑得飞快在后台标签页里又会被节流到 1fps导致物理计算错乱。正确做法是requestAnimationFrame它由浏览器在每次重绘前调用自动适配刷新率切到后台时自动暂停。function gameLoop() { if (!running) return; frameCount; updateStars(); updatePlayer(); updateBullets(); updateEnemies(); updateEnemyBullets(); updateParticles(); updatePowerups(); checkCollisions(); updateWaves(); ctx.fillStyle #0a0a1a; ctx.fillRect(0, 0, canvas.width, canvas.height); renderStars(); renderBullets(); renderEnemies(); renderEnemyBullets(); renderPowerups(); renderParticles(); renderPlayer(); renderHUD(); requestAnimationFrame(gameLoop); }注意这里的顺序先清屏用深色填充而非clearRect保留拖影感再按层级渲染——星空在最底、子弹和敌机居中、玩家和粒子在最上。更新和渲染分离是保证逻辑稳定的关键。如果你把渲染写进 update 里后面加 Boss 弹幕时会很难排查。玩家移动用键盘状态表keys记录按键每帧读取并计算位移而不是在keydown事件里直接改坐标。这样按住方向键时移动是连续的松开立刻停止不会有一顿一顿的感觉。边界钳制用Math.max和Math.min一行搞定飞船永远不会飞出画布。function updatePlayer() { let dx 0, dy 0; if (keys[ArrowLeft] || keys[KeyA]) dx - PLAYER_SPEED; if (keys[ArrowRight] || keys[KeyD]) dx PLAYER_SPEED; if (keys[ArrowUp] || keys[KeyW]) dy - PLAYER_SPEED; if (keys[ArrowDown] || keys[KeyS]) dy PLAYER_SPEED; player.x Math.max(player.w / 2, Math.min(canvas.width - player.w / 2, player.x dx)); player.y Math.max(player.h / 2, Math.min(canvas.height - player.h / 2, player.y dy)); }自动射击用时间戳节流Date.now() - lastFireTime FIRE_RATE才发射FIRE_RATE默认 150ms。这样即使帧率波动射速也是稳定的。道具射速提升激活时把间隔减半效果立竿见影。2. 用 Claude Code 接入 TaoToken把模型对话能力接进你的开发流写游戏逻辑时我经常需要让模型帮我解释一段碰撞代码、或者生成一个 Boss 弹幕的数学公式。这时候如果每次都要切到网页版对话思路会被打断。把模型能力通过 API 接进本地开发流体验会顺很多。TaoToken 提供的就是这样一个统一入口官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。先说清楚它是什么TaoToken 是一个模型 API 聚合服务你用一个 Key 就能调用多种模型适合做代码补全、逻辑解释、报错排查这类开发辅助场景。适合谁正在用 Claude Code 写项目、希望把模型调用固化进工作流的开发者。它不替代你的编辑器只是给你的编辑器补上一个随时可问的通道。第一步去控制台创建 API Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 登录后在 API Keys 页面点新建复制生成的 Key形如sk-开头的一串字符。这个 Key 只显示一次务必存好。第二步如果你用的是 Claude Code 这类命令行工具需要配置 Base URL 和 Key。Claude Code 的配置文件通常在用户目录下的.claude/settings.json或者项目根目录的.claude/settings.json。写入如下内容{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key粘贴在这里 } }注意ANTHROPIC_BASE_URL后面不要带/v1也不要带任何 UTM 参数保持https://taotoken.net/api这个干净形式。Key 直接填你复制的那串。第三步如果你用的是 Cline、Roo Code 这类 VS Code 插件配置项在插件的设置面板里同样是三件套Base URL 填https://taotoken.net/apiAPI Key 填你的 KeyModel ID 填你要用的模型名比如claude-sonnet-4-20250514这类具体以控制台模型列表为准。三件套缺一不可只填 Key 不填 Base URL 会走到默认端点报 401。第四步如果你用 Codex 或类似工具配置写在~/.codex/auth.json或项目级配置里同样是 Base URL Key Model ID 三件套。Model ID 一定要和控制台里列出的名称完全一致大小写敏感。配置完成后你可以用一条 curl 命令验证连通性curl https://taotoken.net/api/v1/messages \ -H Content-Type: application/json \ -H x-api-key: sk-你的Key \ -H anthropic-version: 2023-06-01 \ -d { model: claude-sonnet-4-20250514, max_tokens: 100, messages: [{role: user, content: 用一句话解释AABB碰撞检测}] }如果返回一段 JSON 且content里有文字说明通道打通了。如果返回 401检查 Key 是否复制完整如果返回 404检查 Base URL 是否多写了路径。接入之后你在 Claude Code 里就能直接让它读你的index.html问这段碰撞检测为什么子弹穿过敌机没消失它会结合上下文给出修改建议。这比复制粘贴到网页版高效得多。需要长期做编码和 Agent 任务的可以了解 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。3. 可复制配置敌机生成、AABB 碰撞与粒子特效三件套这一节是全文的技术核心我把三个模块的完整配置和代码都摊开。你可以新建一个index.html把下面内容按顺序拼进去。先看敌机类型配置表。用配置对象驱动敌机行为新增一种敌机只需要加一行不用改生成逻辑const ENEMY_CONFIG { normal: { width: 24, height: 24, hitPoints: 1, moveSpeed: 2, scoreValue: 100, bodyColor: #f44336, pattern: straight }, fast: { width: 20, height: 20, hitPoints: 1, moveSpeed: 4, scoreValue: 150, bodyColor: #ff9800, pattern: zigzag }, tank: { width: 32, height: 32, hitPoints: 3, moveSpeed: 1.5, scoreValue: 300, bodyColor: #9c27b0, pattern: straight }, boss: { width: 60, height: 50, hitPoints: 30, moveSpeed: 1, scoreValue: 2000, bodyColor: #e91e63, pattern: boss } };pattern字段决定移动方式。straight是直线下落zigzag用正弦函数左右摆动boss是横向正弦加定时扇形弹幕。生成函数根据类型从配置表取属性function spawnEnemy(type) { const def ENEMY_CONFIG[type]; enemies.push({ x: Math.random() * (canvas.width - 60) 30, y: -def.height, w: def.width, h: def.height, hp: def.hitPoints, maxHp: def.hitPoints, speed: def.moveSpeed Math.random() * 0.5, score: def.scoreValue, color: def.bodyColor, type, shootTimer: 0 }); }Boss 的扇形弹幕用三角函数算角度。5 发子弹在 60 度扇形内均匀分布每发方向由Math.cos(angle)和Math.sin(angle)决定function bossFire(boss) { const bulletCount 5; const spreadAngle Math.PI / 3; const bulletSpeed 3; for (let i 0; i bulletCount; i) { const angle Math.PI / 2 spreadAngle * (i / (bulletCount - 1) - 0.5); enemyBullets.push({ x: boss.x, y: boss.y boss.h / 2, vx: Math.cos(angle) * bulletSpeed, vy: Math.sin(angle) * bulletSpeed, r: 4 }); } }AABB 碰撞检测是整个游戏最容易被写错的地方。核心公式是两个矩形中心点的水平距离小于两者半宽之和且垂直距离小于两者半高之和就算碰撞。用Math.abs取绝对值一行判断function aabb(a, b) { return Math.abs(a.x - b.x) (a.w b.w) / 2 Math.abs(a.y - b.y) (a.h b.h) / 2; }子弹和敌机的碰撞要注意子弹的y是顶部坐标中心点要加h/2。玩家和敌机子弹的碰撞用圆形近似判断中心距离小于半径和if (Math.abs(player.x - b.x) player.w / 2 b.r Math.abs(player.y - b.y) player.h / 2 b.r) { playerHit(); enemyBullets.splice(i, 1); }粒子爆炸系统用对象数组管理每个粒子有位置、速度、生命值、颜色、尺寸。每帧更新位置并让速度乘以摩擦系数0.96逐渐减速生命值递减到 0 就移除function createExplosion(x, y, color, count) { for (let i 0; i count; i) { const angle Math.random() * Math.PI * 2; const speed Math.random() * 5 1; particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 25 Math.random() * 25, maxLife: 50, color, size: Math.random() * 3 1, friction: 0.96 }); } }渲染时用globalAlpha控制透明度alpha life / maxLife粒子越接近消亡越透明视觉上就是淡出。Boss 爆炸生成 40 个粒子普通敌机 15 个道具拾取 12 个环形闪光。这套参数实测下来爆炸既有冲击力又不会糊满屏幕。把以上代码和第一节的循环拼起来一个完整的单文件游戏就成型了。文件结构建议style放 HUD 和覆盖层样式canvas放画布script里按常量 → 状态 → 实体容器 → 输入 → 各模块 update/render → 主循环 → 启动的顺序组织。4. 验证请求与成功结果在浏览器里逐项检查弹幕密度和帧率代码写完后验证比写代码更重要。打开终端进入项目目录用任意静态服务器启动mkdir -p shooter-game cd shooter-game # 把 index.html 放进来 python3 -m http.server 8080然后浏览器访问http://localhost:8080。如果你不想起服务器直接双击index.html也能跑因为整个游戏不依赖任何外部资源。打开后先看开始界面深空背景、标题、开始按钮。点开始逐项验证第一项玩家移动。按方向键或 WASD三角飞船应该平滑移动到边缘停住不出去。如果飞船瞬移或卡顿检查keys状态表是否在keyup里正确置 false。第二项自动射击。飞船前方每 150ms 射出一发黄色子弹子弹向上飞行飞出顶部后消失。如果子弹堆积不消失检查bullets.filter(b b.y -20)这行过滤逻辑。第三项敌机生成。每秒生成一个敌机红色普通机直线下落橙色快速机左右摆动紫色坦克机有血条。如果敌机不出现检查updateWaves里的waveTimer % 60 0条件。第四项碰撞检测。子弹打中敌机敌机消失并爆出对应颜色的粒子。如果子弹穿过敌机没反应检查aabb里子弹的y是否加了h/2。第五项帧率表现。打开浏览器开发者工具的 Performance 面板录制 10 秒看 FPS 是否稳定在 60。弹幕密集时Boss 扇形弹幕 多个敌机同时爆炸如果掉到 50 以下检查粒子数量是否失控——把createExplosion的 count 参数调小试试。第六项道具系统。击毁敌机有 25% 概率掉落道具拾取后射速提升、护盾激活或生命加一。护盾激活时飞船周围有青色光圈射速提升时子弹变成三发。第七项Boss 战。每 5 波出现 Boss30 血左右摆动并发射扇形弹幕。Boss 血条在顶部显示打完后爆出 40 个粒子。实测下来这套骨架在 Chrome 和 Safari 上都能稳定 60fps移动端触屏也能玩——触摸屏幕任意位置飞船会朝触摸点移动。触屏逻辑用touchstart和touchmove记录触摸位置计算飞船到触摸点的方向向量并归一化乘以速度就是每帧位移。如果你想验证模型接入是否正常可以在 Claude Code 里问一句帮我给这个游戏加一个追踪导弹道具看它能否正确读取你的index.html并给出可用的代码补丁。能正常返回说明 TaoToken 通道和你的开发流已经打通。想直接体验模型对话能力的可以走 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 拿 Key接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。5. 本篇常见报错排查401、local proxy failed 与 reading choices 怎么解这一节把接入和运行过程中最容易撞上的几个报错摊开讲每个都给出真实错误信息和排查路径。报错一401 Unauthorized。返回体通常是{error:{type:authentication_error,message:invalid x-api-key}}。原因有三个Key 复制时漏了尾部字符、Key 前后带了空格、或者 Base URL 写错导致请求发到了别的端点。排查方法把 Key 重新复制一遍确认ANTHROPIC_API_KEY的值没有引号外的空格确认ANTHROPIC_BASE_URL是https://taotoken.net/api不带/v1不带 UTM 参数。报错二local proxy failed / connection refused。这通常出现在你本地配了某个代理端口但代理没启动。错误信息类似Error: connect ECONNREFUSED 127.0.0.1:7890。排查方法检查你的环境变量HTTP_PROXY/HTTPS_PROXY是否指向了一个没运行的端口。如果你不需要代理把这两个变量清空再试。注意这里说的是本地开发环境的网络配置问题不是让你去搭什么通道只是排查本机端口占用。报错三reading choices of undefined。这个报错说明你用的客户端期望 OpenAI 格式的响应带choices字段但实际拿到的是 Anthropic 格式带content字段或者反过来。原因是 Base URL 和客户端协议不匹配。排查方法确认你的客户端是 Anthropic 协议还是 OpenAI 协议。Claude Code 用 Anthropic 协议Base URL 填https://taotoken.net/api如果你用的是 OpenAI 兼容客户端端点路径可能不同以接入文档为准。三件套里的 Model ID 也要和协议匹配。报错四OAuth token expired / invalid_grant。如果你之前用 OAuth 方式登录过某个工具切换成 API Key 后旧 token 还在缓存里会报这个。排查方法找到工具的凭证缓存目录Claude Code 通常在~/.claude/下删掉旧的凭证文件重新用 API Key 配置。配置时确保三件套齐全Base URL、Key、Model ID。报错五游戏能跑但敌机不生成。这不是网络报错是逻辑报错。检查updateWaves是否在gameLoop里被调用检查waveEnemies 5 wave * 2这个上限条件是否一开始就为 false检查spawnEnemy里的ENEMY_CONFIG[type]是否取到了 undefined。在控制台打console.log(waveTimer, waveEnemies)就能定位。报错六粒子爆炸后画面卡顿。粒子数组没有及时清理particles.filter(p p.life 0)这行如果漏了数组会无限增长。检查updateParticles里是否有过滤逻辑。另外renderParticles里每帧创建新对象也会导致 GC 压力尽量复用。排查顺序建议先看浏览器控制台有没有红色报错再看 Network 面板里 API 请求的状态码最后才去翻代码逻辑。大部分游戏不动的问题根源是running变量没置 true或者requestAnimationFrame没被调用。6. 把骨架变成你自己的游戏下一步可以加什么跑通这套骨架后你手里其实已经有了一个可扩展的 2D 引擎。接下来可以按兴趣加东西加一个追踪导弹道具让子弹自动锁定最近敌机加一个激光武器按住空格蓄力发射贯穿光束加一个敌机编队系统让敌机按预设队形入场而不是随机生成加一个本地最高分记录用localStorage存分数。如果你想让模型帮你加这些功能把index.html丢给 Claude Code描述清楚需求它能给出可用的补丁。接入通道配好后这个过程会很顺。需要长期做编码和 Agent 任务的Coding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 想先验证模型对话效果的走 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 拿 Key接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。最后留一个实用技巧调游戏手感时把PLAYER_SPEED、BULLET_SPEED、FIRE_RATE三个常量提到文件最顶部改一个数字刷新页面就能感受到差异。我调这套参数时把FIRE_RATE从 150 改到 100弹幕密度立刻上来了但敌机血量也得跟着调不然太简单。这种数值平衡的活让模型帮你算几组配比比手动试快得多。