使用 HTML5 Canvas + 面向对象 ES6 JavaScript 架构,根据用户自然语言描述生成完整可玩的 NES/FC 风格复古游戏。输出单个 HTML 文件,可在任何现代浏览器中直接运行。
当用户满足以下任意条件时激活本技能:
nes-game-creator/
├── SKILL.md # 技能说明(本文件)
├── assets/
│ └── template.html # 完整游戏模板(引擎 + 占位符,生成时内联为单文件)
├── scripts/ # 引擎源码模块(按职责拆分,供参考和维护)
│ ├── nes-constants.js # NES 常量、调色板、Vec2 向量工具
│ ├── input-manager.js # InputManager 输入管理器
│ ├── renderer.js # Renderer 像素渲染器
│ ├── audio-manager.js # AudioManager 音效管理器
│ ├── music-player.js # MusicPlayer BGM 播放器 + NOTE/SONGS 预设乐曲库
│ ├── game-systems.js # ObjectPool、SpriteAnimator、ParticleSystem、Camera、Tilemap、Physics、TimerManager、TransitionManager、StateMachine
│ └── engine.js # NESEngine 引擎核心 + 全局快捷引用
└── references/ # 参考文档
├── engine_api.md # 引擎完整 API 文档
└── nes_design_guide.md # NES 游戏设计规范
scripts/ 与 template.html 的关系:
scripts/ 下的 7 个 JS 文件是引擎源码的模块化拆分,按职责分离,便于阅读、维护和理解各子系统template.html 是这些模块内联合并后的完整模板,用于最终游戏生成template.html 作为基础,输出单个自包含 HTML 文件scripts/ 下对应模块和 template.htmlnes-constants.js → input-manager.js → renderer.js → audio-manager.js → music-player.js → game-systems.js → engine.js模板文件 assets/template.html 提供了一套完整的面向对象游戏引擎(源码模块化拆分于 scripts/ 目录),包含以下核心子系统:
| 子系统 | 类名 | 职责 |
|---|---|---|
| -------- | ------ | ------ |
| 数学工具 | Vec2 | 2D 向量运算(加减、缩放、归一化、距离) |
| 输入 | InputManager | 键盘 + 移动端触控,支持按下/松开/刚按下检测 |
| 渲染 | Renderer | 像素绘制、精灵渲染(支持翻转/描边/闪白/旋转)、像素字体(含描边字体)、扫描线(预渲染缓冲)、HP条/分段HP条/心形图标、圆形/线段绘制、场景过渡效果(幕帘/百叶窗/iris)、视差背景层、星空背景、相机偏移渲染 |
| 音频 | AudioManager | 方波/三角波/噪声合成、LFSR噪声、频率扫描、主音量混合器、20+预设音效 |
| BGM | MusicPlayer | 多声道芯片音乐播放器,BPM驱动,循环播放,声道独立音量,精确循环衔接 |
| 乐曲库 | SONGS / NOTE / n() | 15首预设乐曲(title/stage/boss/gameOver/victory/dungeon/shop/fight/sports/town/intense/heroic/training/festival/chase)+ 完整音符表 |
| 对象池 | ObjectPool | 子弹/粒子等高频创建对象的零 GC 复用(优化swap-remove) |
| 动画 | SpriteAnimator | 多动画状态切换、按时间自动播放帧、支持一次性动画(loop:false) |
| 粒子 | ParticleSystem | 爆炸/火花/碎片效果,支持重力和相机,粒子池复用+透明度渐变,定向发射 |
| 相机 | Camera | 平滑跟随、死区跟随、边界约束、衰减震动 |
| 地图 | Tilemap | 2D 瓦片地图加载/渲染/碰撞查询(仅绘制视口内区域,使用缓存精灵) |
| 物理 | Physics | AABB/圆/矩形-圆碰撞、Tilemap 碰撞解析、clamp/lerp/smoothStep/distance/angleBetween |
| 计时器 | TimerManager | 延时/循环回调,支持手动取消 |
| 过渡 | TransitionManager | 场景过渡管理(fade/wipe/blinds/iris),自动渐出→回调→渐入 |
| 状态机 | StateMachine | 游戏状态管理(enter/update/render/exit 生命周期) |
| 引擎核心 | NESEngine | 主循环、帧计数、FPS 统计、子系统聚合、自动过渡渲染 |
全局快捷引用(直接使用,无需 engine. 前缀):
engine — 引擎实例input — 输入管理器renderer — 渲染器audio — 音频管理器(音效 SFX)music — 芯片音乐播放器(BGM)camera — 相机particles — 粒子系统timers — 计时器管理器transition — 场景过渡管理器SONGS — 预设乐曲库(title/stage/boss/gameOver/victory/dungeon/shop/fight/sports/town/intense/heroic/training/festival/chase)NOTE — MIDI 音高常量表(含完整半音 + 等音别名)n(note, duration, volume?) — 音符创建快捷函数从用户描述中提取:
如果描述模糊,以经典 NES 游戏为灵感做出合理的创意决策,偏向有趣和可玩性。不要过度询问。
参阅 references/nes_design_guide.md 获取 NES 技术规格、各类型游戏设计模式、视觉风格规范、精灵设计模板库、配色方案速查表、音效设计和代码架构指导。
参阅 references/engine_api.md 获取引擎所有类的完整 API 文档。
关键设计决策:
StateMachine 管理)ObjectPoolSpriteAnimator)Camera + TilemapTransitionManagerNES_PALETTE 数组索引设计精灵使用 assets/template.html 作为基础,替换 {{GAME_TITLE}} 和 {{GAME_CODE_PLACEHOLDER}}。
所有生成的游戏必须使用面向对象架构。以下为必须遵守的代码结构模式:
// ─── 游戏常量 ───────────────────────
const GRAVITY = 400; // 像素/秒²
const PLAYER_SPEED = 120; // 像素/秒
// ─── 精灵数据 ───────────────────────
// 【重要】精灵必须有:黑色轮廓(13) + 3层明暗 + 清晰的眼睛(2×2)
const SPRITES = {
playerIdle: [ /* 2D palette index array */ ],
playerRun1: [ /* ... */ ],
playerRun2: [ /* ... */ ],
// ...
};
// ─── 实体基类 ───────────────────────
class Entity {
constructor(x, y, w, h) {
this.x = x; this.y = y;
this.w = w; this.h = h;
this.vx = 0; this.vy = 0;
this.active = true;
}
update(dt) {}
render() {}
getBounds() { return { x: this.x, y: this.y, w: this.w, h: this.h }; }
}
// ─── 玩家类 ─────────────────────────
class Player extends Entity {
constructor(x, y) {
super(x, y, 14, 16);
this.hp = 3;
this.score = 0;
this.facing = 1; // 1=右, -1=左
this.onGround = false;
this.invincible = 0;
this.animator = new SpriteAnimator({
idle: { frames: [SPRITES.playerIdle], speed: 0.5 },
run: { frames: [SPRITES.playerRun1, SPRITES.playerRun2], speed: 0.15 },
jump: { frames: [SPRITES.playerJump], speed: 1 },
});
}
update(dt) {
// 输入处理
this.vx = input.dirX * PLAYER_SPEED;
if (this.vx !== 0) this.facing = this.vx > 0 ? 1 : -1;
// 跳跃
if (input.pressedA && this.onGround) {
this.vy = -250;
this.onGround = false;
audio.sfxJump();
}
// 重力
this.vy += GRAVITY * dt;
// 移动 + 碰撞
this.x += this.vx * dt;
this.y += this.vy * dt;
// 动画
if (!this.onGround) this.animator.play('jump');
else if (this.vx !== 0) this.animator.play('run');
else this.animator.play('idle');
this.animator.update(dt);
// 无敌帧
if (this.invincible > 0) this.invincible -= dt;
}
render() {
const flash = this.invincible > 0 && Math.floor(this.invincible * 8) % 2;
renderer.drawSpriteEx(this.animator.frame, this.x, this.y, 1, this.facing < 0, {
alpha: flash ? 0.5 : 1,
whiteFlash: flash,
outlineColor: '#000000',
});
}
takeDamage() {
if (this.invincible > 0) return;
this.hp--;
this.invincible = 1.5;
audio.sfxHit();
camera.startShake(0.3, 6);
}
}
// ─── 敌人类 ─────────────────────────
class Enemy extends Entity {
constructor(x, y, type) {
super(x, y, 16, 16);
this.type = type;
this.hp = 1;
// 状态机驱动AI...
}
update(dt) { /* 巡逻/追踪逻辑 */ }
render() { /* 渲染精灵 */ }
}
// ─── 使用 StateMachine 管理游戏状态 ──
let player, enemies, score;
let blinkTimer = 0;
const game = new StateMachine({
TITLE: {
enter() { blinkTimer = 0; music.play(SONGS.title); },
update(dt) {
blinkTimer += dt;
if (input.pressedStart) {
audio.sfxStart();
// 使用场景过渡效果切换
transition.start(() => game.transition('PLAYING'), 'fade', 0.4);
}
},
render() {
renderer.clear('#000');
renderer.drawPixelTextCenteredShadow('GAME TITLE', 80, '#F8F8F8', '#000000', 2);
if (Math.floor(blinkTimer * 2) % 2 === 0) {
renderer.drawPixelTextCentered('PRESS START', 160, '#F8B800');
}
renderer.drawScanlines();
}
},
PLAYING: {
enter() {
player = new Player(128, 200);
enemies = [];
score = 0;
music.play(SONGS.stage);
},
update(dt) {
player.update(dt);
enemies.forEach(e => e.update(dt));
particles.update(dt);
camera.follow(player, 8, 8);
camera.update(dt);
// 碰撞检测...
if (player.hp <= 0) {
transition.start(() => game.transition('GAME_OVER'), 'iris', 0.6, { cx: player.x, cy: player.y });
}
},
render() {
renderer.clear('#000');
// 背景视差层
renderer.renderBgLayers(camera);
player.render();
enemies.forEach(e => e.render());
particles.render(renderer);
// HUD — 使用像素字体
renderer.drawPixelTextShadow(`SCORE ${score}`, 8, 8);
renderer.drawHpBar(8, 20, 40, 4, player.hp, 3);
renderer.drawScanlines();
}
},
GAME_OVER: {
enter() { blinkTimer = 0; music.play(SONGS.gameOver, false); },
update(dt) {
blinkTimer += dt;
if (input.pressedStart) {
transition.start(() => game.transition('TITLE'), 'fade', 0.5);
}
},
render() {
renderer.clear('#000');
renderer.drawPixelTextCenteredShadow('GAME OVER', 100, '#F83800', '#000000', 2);
renderer.drawPixelTextCentered(`SCORE: ${score}`, 130);
if (Math.floor(blinkTimer * 2) % 2 === 0) {
renderer.drawPixelTextCentered('PRESS START TO RETRY', 170, '#F8B800');
}
renderer.drawScanlines();
}
}
}, 'TITLE');
// ─── 覆写引擎主循环 ─────────────────
engine.update = (dt) => game.update(dt);
engine.render = () => game.render();
Entity 基类SpriteAnimator、StateMachine 等组件组合行为renderer、音效用 audio 等)ObjectPooldt(秒),速度单位为 像素/秒NES_PALETTE,不得使用任意 hex 值StateMachine 管理Camera,配合 renderer.drawSpriteCam() / renderer.fillRectCam()Tilemap 加载,配合 Physics.resolveMapCollisionX/Y() 做碰撞解析music.play(SONGS.xxx) 播放 BGM,状态切换时切换对应乐曲transition.start() 实现场景切换时的渐变效果,不要直接切换每个角色/敌人/道具精灵必须满足以下全部条件:
精灵模板示例(16×16 高质量角色):
// 红色帽子角色 — 注意轮廓线(13)、明暗层次、大头比例
const heroIdle = [
[-1,-1,-1,-1,-1,13,13,13,13,13,-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,13,22,22,22,22,22,13,-1,-1,-1,-1,-1],
[-1,-1,-1,13,22,22, 5, 5, 5,22,22,13,-1,-1,-1,-1],
[-1,-1,-1,13,54,54,54,54,54,54,54,13,-1,-1,-1,-1],
[-1,-1,13,54,54,13,48,32,54,13,48,32,13,-1,-1,-1], // 眼睛:白(48)+灰(32)
[-1,-1,13,38,54,13,13,54,54,13,13,38,13,-1,-1,-1],
[-1,-1,-1,13,54,54,38,38,38,54,54,13,-1,-1,-1,-1],
[-1,-1,-1,-1,13,13,13,13,13,13,13,-1,-1,-1,-1,-1],
[-1,-1,-1,13,17,17,22,22,22,17,17,13,-1,-1,-1,-1],
[-1,-1,13,17,17,22,40,22,40,22,17,17,13,-1,-1,-1],
[-1,-1,13,17,17,22,22,22,22,22,17,17,13,-1,-1,-1],
[-1,-1,-1,13,17,17,17,17,17,17,17,13,-1,-1,-1,-1],
[-1,-1,-1,13,17, 2,17,-1,17, 2,17,13,-1,-1,-1,-1],
[-1,-1,-1,13, 6, 6, 6,-1, 6, 6, 6,13,-1,-1,-1,-1],
[-1,-1,13, 6,24, 6, 6,-1, 6, 6,24, 6,13,-1,-1,-1],
[-1,-1,-1,13,13,13,-1,-1,-1,13,13,13,-1,-1,-1,-1],
];
// 走路帧1 — 右脚前迈
const heroRun1 = [
[-1,-1,-1,-1,-1,13,13,13,13,13,-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,13,22,22,22,22,22,13,-1,-1,-1,-1,-1],
[-1,-1,-1,13,22,22, 5, 5, 5,22,22,13,-1,-1,-1,-1],
[-1,-1,-1,13,54,54,54,54,54,54,54,13,-1,-1,-1,-1],
[-1,-1,13,54,54,13,48,32,54,13,48,32,13,-1,-1,-1],
[-1,-1,13,38,54,13,13,54,54,13,13,38,13,-1,-1,-1],
[-1,-1,-1,13,54,54,38,38,38,54,54,13,-1,-1,-1,-1],
[-1,-1,-1,-1,13,13,13,13,13,13,13,-1,-1,-1,-1,-1],
[-1,-1,-1,13,17,17,22,22,22,17,17,13,-1,-1,-1,-1],
[-1,-1,13,17,17,22,40,22,40,22,17,17,13,-1,-1,-1],
[-1,-1,13,17,17,22,22,22,22,22,17,17,13,-1,-1,-1],
[-1,-1,-1,13,17,17,17,17,17,17,17,13,-1,-1,-1,-1],
[-1,-1,13, 6, 6,13,-1,-1,-1,13,17,17,13,-1,-1,-1], // 腿部交叉
[-1,13, 6,24, 6,13,-1,-1,-1,-1,13, 2,17,13,-1,-1],
[-1,-1,13,13,13,-1,-1,-1,-1,-1,-1,13, 6,13,-1,-1],
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,13,13,-1,-1],
];
8×8 小型精灵模板(道具/子弹/UI元素):
// 金币 — 旋转动画2帧
const coinFrame1 = [
[-1,13,40,40,40,13,-1,-1],
[13,40,56,56,40,40,13,-1], // 56=#F8D878 亮金, 40=#F8B800 金
[13,40,56,40,40,40,13,-1],
[13,40,40,40,40,40,13,-1],
[13,40,40,40,40,24,13,-1], // 24=#AC7C00 暗金
[13,40,40,40,24,24,13,-1],
[-1,13,40,24,24,13,-1,-1],
[-1,-1,13,13,13,-1,-1,-1],
];
// 心形(生命值图标)
const heartSprite = [
[-1,22,22,-1,22,22,-1,-1],
[22, 5,22,22,22, 5,22,-1], // 亮暗层次
[22,22,22,22,22,22,22,-1],
[22,22,22,22,22,22,22,-1],
[-1,22,22,22,22,22,-1,-1],
[-1,-1,22,22,22,-1,-1,-1],
[-1,-1,-1, 5,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1,-1,-1],
];
角色-红系: 亮红(22)→中红(5)→暗红(6) 轮廓(13)
角色-蓝系: 亮蓝(33)→中蓝(17)→暗蓝(2) 轮廓(13)
角色-绿系: 亮绿(41)→中绿(42)→暗绿(10) 轮廓(13)
角色-肤色: 亮肤(54)→中肤(38)→暗肤(7) 轮廓(13)
金币/道具: 亮金(56)→金(40)→暗金(24) 轮廓(13)
天空/水面: 亮天(49)→中天(33)→暗天(17)
草地/树木: 亮绿(41)→中绿(42)→暗绿(25)
砖块/地面: 亮棕(38)→中棕(7)→暗棕(8)
岩石/金属: 亮灰(32)→中灰(16)→暗灰(0)
粉色系: 亮粉(52)→中粉(36)→暗粉(4)
紫色系: 亮紫(35)→中紫(19)→暗紫(3)
橙色系: 亮橙(39)→中橙(23)→暗橙(7)
文字-白: 纯白(48) 或 亮白(32)
文字-黄: 金黄(40)
背景-黑: 纯黑(13)
renderer.drawPixelTextCenteredShadow() 大号像素字体居中(带阴影更立体)renderer.drawPixelTextCentered() 以 0.5 秒间隔闪烁input.pressedStart)进入游戏,使用 transition.start() 过渡music.play(SONGS.title) 播放标题曲dtrenderer.drawSpriteEx() 支持描边/闪白/透明度效果renderer.drawSpriteEx() 的 whiteFlash + alpha 参数实现闪烁renderer.drawPixelText() 像素字体显示分数/生命值renderer.drawHpBar() 或 renderer.drawHearts() 绘制sfxJump)、得分(sfxCoin)、受伤(sfxHit)、死亡(sfxDeath)、开始(sfxStart)music.play(SONGS.stage) 播放关卡曲;Boss 战切换 music.play(SONGS.boss)particles.emit() 发射粒子,敌人死亡使用多色爆炸renderer.drawScanlines() 添加扫描线效果renderer.addBgLayer() 注册视差背景层transition.start(),不要直接transitionmusic.play(SONGS.gameOver, false) 播放结束曲(不循环)space-shooter.html、pixel-adventure.htmlcd <project_directory> && python3 -m http.server 8080
提供 URL: http://localhost:8080/
交付前必须验证:
music.play() + SONGS 预设或自定义乐曲)transition.start() 过渡效果renderer.drawScanlines() 添加扫描线效果drawPixelText 系列像素字体(非系统字体)NES_PALETTE,不得使用任意 hex 颜色SpriteAnimator 管理AudioManager 的预设方法 + playTone()/playSweep()music.play(SONGS.xxx) 播放预设乐曲,或自定义乐曲数据传入 music.play()drawPixelText/drawPixelTextCentered 系列方法drawPixelTextCenteredShadow() 增加立体感renderer.drawScanlines() 添加CRT扫描线效果renderer.addBgLayer() 视差层transition.start() 实现 fade/wipe/blinds/iris共 1 个版本