编写一个单HTML文件构成的复古像素风塔
2026-01-08
编写一个单HTML文件构成的复古像素风塔防游戏,要求使用HTML5 Canvas进行所有渲染。游戏必须包含一个带有“开始游戏”按钮的像素化主菜单界面,以及游戏结束后的重玩逻辑。视觉上,请通过Canvas绘图指令实现 16x16 风格的像素瓷砖地图,包含一条清晰的预设进攻路径。核心逻辑需支持:三种具有不同属性的塔(射速快的箭塔、群体伤害的炮塔、减速效果的魔法塔),每种塔支持至少三个等级的升级,且升级后外观有明显变化;敌人波次系统需随波次增加难度(血量与移动速度提升);完整的经济系统,包括击杀奖励和造塔成本。音效部分,必须使用 Web Audio API 的 OscillatorNode 实时合成 8-bit 音效,包括放置塔的“哔”声、敌人爆炸的“嘣”声以及升级时的上升音阶。代码结构应采用模块化单文件写法,包含资源初始化、主游戏循环、状态更新(Update)和渲染绘制(Draw)等核心部分,确保玩家打开文件即可通过点击开始按钮进入游戏循环。
思考过程
**Examining Game Specs**
\n\n
I've begun dissecting the project requirements. It's all about a single HTML file, which is a nice constraint. I'm focusing intently on using the HTML5 Canvas, and nothing else, for the rendering. Furthermore, I've got the pixel-art style, with a 16x16 grid, locked firmly in my mind, as a visual anchor.
**Considering Initial Setup**
\n\n
I'm now prioritizing the game's core architecture. The state machine feels solid: MENU, PLAYING, and GAME_OVER. I'm focusing on the modular structure, with Init, Loop, Update, and Draw functions. This setup should keep things organized. Time to plan the entity structures, specifically Enemy, Tower, and Projectile.
**Mapping Out Entities**
\n\n
I'm deep in the entity design phase now. I've sketched out the Enemy, Tower, and Projectile classes, outlining their key properties. Thinking about the Tower upgrades, I'm leaning toward a simple approach: increasing damage, range, and maybe attack speed. For enemies, it's pretty clear: HP and speed should increase with each wave. Also, the particle effects from explosions should be small rectangles, to align with the overall pixel style.
**Finalizing the Structure**
\n\n
Now that I've designed the core gameplay systems, I'm focusing on the code structure. I'm writing the HTML now to set up the canvas and style, including pixelation. Also, I'm now drafting the `Map` object with pathing, the `Audio` class using the Web Audio API and creating helper functions for the oscillator, and finally, laying out the initialization for the `MENU` state.
**Implementing UI Elements**
\n\n
I'm now focusing on UI implementation, for the menu and game-over screens. I will design simple, pixelated buttons using the `drawRect` function, for the "Start Game" and "Retry" actions. The audio initialization now occurs after the user clicks "Start Game." Time to set up wave spawning with increasing enemy HP and speed, incorporating that slow effect, with the timer that reduces their speed.
**Structuring Enemy Waves**
\n\n
My current focus is on wave design and balance. I'm finalizing the enemy spawning logic, including scaling their HP and speed based on the wave number. Then, I will also incorporate the slow effect, setting the damage calculations and projectile velocity to finish.
**Coding the Audio Engine**
\n\n
My current focus is crafting the audio engine, specifically focusing on the Web Audio API. I'm building a simple class for sound generation. Then I'll focus on implementing short, distinct sounds, like the "place" and "shoot" effects, using oscillator nodes. I'm aiming for an 8-bit aesthetic to fit with the visual style.
这是一个完全由单个 HTML 文件构成的复古像素风塔防游戏。它不依赖任何外部图片或库,所有图形均通过 Canvas API 实时绘制,音效通过 Web Audio API 实时合成。
### 功能说明
1. **文件结构**:单文件 HTML,包含 CSS、HTML 和 JS。
2. **画面**:Canvas 绘制,采用类似 8-bit 的色盘。
3. **核心玩法**:
* **建造**:左键点击下方工具栏选择塔,点击地图上的草地建造。
* **升级**:点击地图上**已建造的塔**,消耗金钱进行升级(最高 3 级,外观变强)。
* **路径**:敌人沿预设土路进攻。
* **机制**:箭塔(单体快攻)、炮塔(群体溅射)、冰塔(减速)。
4. **音效**:合成器生成的复古音效(射击、爆炸、升级、UI)。
5. **流程**:主菜单 -> 游戏循环 (波次系统) -> 游戏结束 -> 重玩。
您可以直接保存代码为 `.html` 文件并在浏览器中打开。
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Retro Pixel TD (Canvas Only)</title>
<style>
body {
background-color: #222;
color: #eee;
font-family: 'Courier New', Courier, monospace;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
overflow: hidden;
user-select: none;
}
canvas {
image-rendering: pixelated; /* 关键:保持像素清晰,不模糊 */
box-shadow: 0 0 20px rgba(0,0,0,0.8);
border: 4px solid #444;
cursor: crosshair;
}
#ui-layer {
margin-top: 10px;
text-align: center;
font-size: 14px;
color: #888;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="640" height="480"></canvas>
<div id="ui-layer">点击地图建造/升级 | [1] 箭塔 [2] 炮塔 [3] 冰塔</div>
<script>
/**
* ------------------------------------------------------------------
* 1. 音频管理器 (Web Audio API)
* ------------------------------------------------------------------
*/
const AudioSys = {
ctx: null,
init: function() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
},
playTone: function(freq, type, duration, vol = 0.1) {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
},
// 音效预设
sfx: {
click: () => AudioSys.playTone(600, 'square', 0.1),
place: () => AudioSys.playTone(880, 'sine', 0.1),
shootArrow: () => AudioSys.playTone(1200, 'triangle', 0.05, 0.05),
shootCannon: () => AudioSys.playTone(150, 'square', 0.3, 0.2),
shootIce: () => AudioSys.playTone(2000, 'sine', 0.2, 0.05),
explode: () => {
// 模拟爆炸噪音
if (!AudioSys.ctx) return;
const osc = AudioSys.ctx.createOscillator();
const gain = AudioSys.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, AudioSys.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(10, AudioSys.ctx.currentTime + 0.3);
gain.gain.setValueAtTime(0.2, AudioSys.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, AudioSys.ctx.currentTime + 0.3);
osc.connect(gain);
gain.connect(AudioSys.ctx.destination);
osc.start();
osc.stop(AudioSys.ctx.currentTime + 0.3);
},
upgrade: () => {
if (!AudioSys.ctx) return;
const now = AudioSys.ctx.currentTime;
const osc = AudioSys.ctx.createOscillator();
const gain = AudioSys.ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(440, now);
osc.frequency.setValueAtTime(554, now + 0.1); // C#
osc.frequency.setValueAtTime(659, now + 0.2); // E
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0, now + 0.4);
osc.connect(gain);
gain.connect(AudioSys.ctx.destination);
osc.start();
osc.stop(now + 0.4);
}
}
};
/**
* ------------------------------------------------------------------
* 2. 游戏常量与配置
* ------------------------------------------------------------------
*/
const CANVAS_W = 640;
const CANVAS_H = 480;
const TILE_SIZE = 32; // 20x15 grid
const COLS = CANVAS_W / TILE_SIZE;
const ROWS = CANVAS_H / TILE_SIZE;
// 简单的地图设计: 0=草地(可建), 1=路径, 2=UI区域(底部)
// 生成一个S型路径
const mapGrid = [];
const pathPoints = []; // 存储路径中心点
function initMap() {
// 初始化草地
for(let y=0; y<ROWS; y++) {
let row = [];
for(let x=0; x<COLS; x++) {
row.push(0);
}
mapGrid.push(row);
}
// 绘制路径 (简单硬编码一条路)
// 起点 (0, 2) -> (18, 2) -> (18, 8) -> (2, 8) -> (2, 12) -> (19, 12) 终点
let cx = 0, cy = 2;
const addPath = (tx, ty) => {
while(cx !== tx || cy !== ty) {
mapGrid[cy][cx] = 1;
pathPoints.push({x: cx * TILE_SIZE + TILE_SIZE/2, y: cy * TILE_SIZE + TILE_SIZE/2});
if (cx < tx) cx++;
else if (cx > tx) cx--;
else if (cy < ty) cy++;
else if (cy > ty) cy--;
}
mapGrid[cy][cx] = 1; // Add turning point
};
addPath(18, 2);
addPath(18, 8);
addPath(2, 8);
addPath(2, 12);
addPath(19, 12); // Exit
}
const TOWER_TYPES = {
ARCHER: {
name: "箭塔", color: "#4CAF50", cost: 50, range: 120, speed: 30, damage: 10, type: 'single',
desc: "射速快"
},
CANNON: {
name: "炮塔", color: "#607D8B", cost: 120, range: 150, speed: 90, damage: 30, type: 'splash', splashRadius: 60,
desc: "群体伤害"
},
MAGIC: {
name: "冰塔", color: "#2196F3", cost: 80, range: 100, speed: 45, damage: 5, type: 'slow', slowFactor: 0.5,
desc: "减速敌人"
}
};
/**
* ------------------------------------------------------------------
* 3. 游戏实体类
* ------------------------------------------------------------------
*/
class Entity {
constructor(x, y) {
this.x = x;
this.y = y;
this.markedForDeletion = false;
}
}
class Enemy extends Entity {
constructor(waveLevel) {
super(pathPoints[0].x, pathPoints[0].y);
this.pathIndex = 0;
this.radius = 8;
// 难度曲线
this.maxHp = 20 + (waveLevel * 15);
this.hp = this.maxHp;
this.speed = 1.5 + (waveLevel * 0.1);
this.reward = 5 + Math.floor(waveLevel * 0.5);
this.slowTimer = 0;
this.color = `hsl(${Math.random()*50}, 70%, 50%)`; // 红色系
}
update() {
// 移动逻辑
let target = pathPoints[this.pathIndex + 1];
if (!target) {
this.markedForDeletion = true;
Game.lives--;
AudioSys.playTone(100, 'sawtooth', 0.5); // 扣血音效
return;
}
let dx = target.x - this.x;
let dy = target.y - this.y;
let dist = Math.hypot(dx, dy);
let currentSpeed = this.speed;
if (this.slowTimer > 0) {
currentSpeed *= 0.5;
this.slowTimer--;
}
if (dist < currentSpeed) {
this.x = target.x;
this.y = target.y;
this.pathIndex++;
} else {
this.x += (dx / dist) * currentSpeed;
this.y += (dy / dist) * currentSpeed;
}
}
takeDamage(amt) {
this.hp -= amt;
if (this.hp <= 0) {
this.markedForDeletion = true;
Game.money += this.reward;
AudioSys.sfx.explode();
Game.particles.push(new Particle(this.x, this.y, this.color));
}
}
draw(ctx) {
ctx.fillStyle = this.slowTimer > 0 ? "#80D8FF" : this.color;
ctx.fillRect(this.x - 6, this.y - 6, 12, 12); // Pixel enemy
// 血条
ctx.fillStyle = "red";
ctx.fillRect(this.x - 8, this.y - 10, 16, 2);
ctx.fillStyle = "#0f0";
ctx.fillRect(this.x - 8, this.y - 10, 16 * (this.hp / this.maxHp), 2);
}
}
class Tower extends Entity {
constructor(c, r, typeKey) {
super(c * TILE_SIZE + TILE_SIZE/2, r * TILE_SIZE + TILE_SIZE/2);
this.col = c;
this.row = r;
this.key = typeKey;
this.level = 1;
this.cooldown = 0;
// 复制基础属性
const base = TOWER_TYPES[typeKey];
this.range = base.range;
this.speed = base.speed;
this.damage = base.damage;
this.color = base.color;
}
upgrade() {
if (this.level >= 3) return;
const upgradeCost = Math.floor(TOWER_TYPES[this.key].cost * 1.5 * this.level);
if (Game.money >= upgradeCost) {
Game.money -= upgradeCost;
this.level++;
this.damage *= 1.5;
this.range += 20;
this.speed *= 0.9; // 射速变快
AudioSys.sfx.upgrade();
}
}
update() {
if (this.cooldown > 0) this.cooldown--;
// 寻找目标
if (this.cooldown <= 0) {
for (let e of Game.enemies) {
let dist = Math.hypot(e.x - this.x, e.y - this.y);
if (dist <= this.range) {
this.shoot(e);
this.cooldown = this.speed;
break;
}
}
}
}
shoot(target) {
const stats = TOWER_TYPES[this.key];
if (this.key === 'ARCHER') AudioSys.sfx.shootArrow();
if (this.key === 'CANNON') AudioSys.sfx.shootCannon();
if (this.key === 'MAGIC') AudioSys.sfx.shootIce();
Game.projectiles.push(new Projectile(this.x, this.y, target, this));
}
draw(ctx) {
// 底座
ctx.fillStyle = "#333";
ctx.fillRect(this.x - 14, this.y - 14, 28, 28);
// 主体颜色
ctx.fillStyle = this.color;
// 基于等级的外观变化
if (this.level === 1) {
ctx.fillRect(this.x - 10, this.y - 10, 20, 20);
} else if (this.level === 2) {
ctx.fillRect(this.x - 12, this.y - 12, 24, 24);
ctx.strokeStyle = "#FFF";
ctx.lineWidth = 2;
ctx.strokeRect(this.x - 10, this.y - 10, 20, 20);
} else {
// Level 3
ctx.fillRect(this.x - 12, this.y - 12, 24, 24);
ctx.fillStyle = "#FFD700"; // Gold trim
ctx.fillRect(this.x - 6, this.y - 6, 12, 12);
}
// 炮塔类型标识
ctx.fillStyle = "#000";
if (this.key === 'ARCHER') {
ctx.fillRect(this.x - 2, this.y - 8, 4, 16); // 竖线
} else if (this.key === 'CANNON') {
ctx.fillRect(this.x - 6, this.y - 6, 12, 12); // 黑洞
} else if (this.key === 'MAGIC') {
ctx.fillStyle = "#FFF";
ctx.fillRect(this.x - 4, this.y - 4, 8, 8); // 亮点
}
}
}
class Projectile extends Entity {
constructor(x, y, target, parentTower) {
super(x, y);
this.target = target;
this.parent = parentTower;
this.speed = 8;
this.damage = parentTower.damage;
this.active = true;
}
update() {
if (!this.target || this.target.markedForDeletion) {
this.active = false; // 目标消失,子弹消失
return;
}
let dx = this.target.x - this.x;
let dy = this.target.y - this.y;
let dist = Math.hypot(dx, dy);
if (dist < this.speed) {
// 击中
this.hit();
} else {
this.x += (dx/dist) * this.speed;
this.y += (dy/dist) * this.speed;
}
}
hit() {
this.active = false;
const stats = TOWER_TYPES[this.parent.key];
if (stats.type === 'splash') {
// 爆炸效果
Game.particles.push(new Particle(this.x, this.y, "#FFA500", 20));
// 范围伤害
Game.enemies.forEach(e => {
if (Math.hypot(e.x - this.x, e.y - this.y) < stats.splashRadius) {
e.takeDamage(this.damage);
}
});
} else {
this.target.takeDamage(this.damage);
if (stats.type === 'slow') {
this.target.slowTimer = 60; // 减速1秒 (60帧)
}
}
}
draw(ctx) {
ctx.fillStyle = this.parent.key === 'MAGIC' ? '#FFF' : '#000';
// 炮弹大一点
let size = this.parent.key === 'CANNON' ? 6 : 4;
ctx.fillRect(this.x - size/2, this.y - size/2, size, size);
}
}
class Particle extends Entity {
constructor(x, y, color, count=5) {
super(x, y);
this.life = 20;
this.color = color;
this.bits = [];
for(let i=0; i<count; i++) {
this.bits.push({
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
x: 0, y: 0
});
}
}
update() {
this.life--;
if (this.life <= 0) this.markedForDeletion = true;
this.bits.forEach(b => {
b.x += b.vx;
b.y += b.vy;
});
}
draw(ctx) {
ctx.fillStyle = this.color;
this.bits.forEach(b => {
ctx.globalAlpha = this.life / 20;
ctx.fillRect(this.x + b.x, this.y + b.y, 3, 3);
ctx.globalAlpha = 1.0;
});
}
}
/**
* ------------------------------------------------------------------
* 4. 游戏主逻辑
* ------------------------------------------------------------------
*/
const Game = {
canvas: document.getElementById('gameCanvas'),
ctx: document.getElementById('gameCanvas').getContext('2d'),
state: 'MENU', // MENU, PLAYING, GAMEOVER
money: 150,
lives: 20,
wave: 1,
waveTimer: 0,
enemiesToSpawn: 0,
spawnTimer: 0,
enemies: [],
towers: [],
projectiles: [],
particles: [],
selectedTower: 'ARCHER', // 当前选中的建造工具
init() {
initMap();
this.canvas.addEventListener('mousedown', (e) => this.handleInput(e));
// 键盘快捷键
window.addEventListener('keydown', (e) => {
if(e.key === '1') this.selectedTower = 'ARCHER';
if(e.key === '2') this.selectedTower = 'CANNON';
if(e.key === '3') this.selectedTower = 'MAGIC';
});
requestAnimationFrame(() => this.loop());
},
reset() {
this.money = 250;
this.lives = 20;
this.wave = 1;
this.enemies = [];
this.towers = [];
this.projectiles = [];
this.particles = [];
this.enemiesToSpawn = 5;
this.waveTimer = 180; // 3秒倒计时开始
this.state = 'PLAYING';
},
handleInput(e) {
const rect = this.canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
if (this.state === 'MENU' || this.state === 'GAMEOVER') {
// 简单的按钮区域检测
// 按钮大约在屏幕中间
if (mx > 220 && mx < 420 && my > 300 && my < 360) {
AudioSys.init(); // 必须用户交互才能启动AudioContext
AudioSys.sfx.click();
this.reset();
}
return;
}
if (this.state === 'PLAYING') {
// 底部 UI 点击 (简单的 Y 轴检测,实际应该更精确,但这里用键盘切换更方便)
if (my > 440) {
if (mx < 100) this.selectedTower = 'ARCHER';
else if (mx < 200) this.selectedTower = 'CANNON';
else if (mx < 300) this.selectedTower = 'MAGIC';
AudioSys.sfx.click();
return;
}
// 地图点击
const c = Math.floor(mx / TILE_SIZE);
const r = Math.floor(my / TILE_SIZE);
if (r >= ROWS || c >= COLS) return;
// 检查该位置是否有塔
const existingTower = this.towers.find(t => t.col === c && t.row === r);
if (existingTower) {
// 升级逻辑
existingTower.upgrade();
} else {
// 建造逻辑
if (mapGrid[r][c] === 0) { // 必须是草地
const cost = TOWER_TYPES[this.selectedTower].cost;
if (this.money >= cost) {
this.money -= cost;
this.towers.push(new Tower(c, r, this.selectedTower));
AudioSys.sfx.place();
}
}
}
}
},
update() {
if (this.state !== 'PLAYING') return;
// 生成逻辑
if (this.waveTimer > 0) {
this.waveTimer--;
} else {
if (this.enemiesToSpawn > 0) {
if (this.spawnTimer > 0) {
this.spawnTimer--;
} else {
this.enemies.push(new Enemy(this.wave));
this.enemiesToSpawn--;
this.spawnTimer = 40; // 间隔
}
} else if (this.enemies.length === 0) {
// 波次结束
this.wave++;
this.enemiesToSpawn = 5 + Math.floor(this.wave * 1.5);
this.waveTimer = 180; // 休息时间
// 波次奖励
this.money += 50;
}
}
// 实体更新
this.enemies.forEach(e => e.update());
this.towers.forEach(t => t.update());
this.projectiles.forEach(p => p.update());
this.particles.forEach(p => p.update());
// 清理
this.enemies = this.enemies.filter(e => !e.markedForDeletion);
this.projectiles = this.projectiles.filter(p => p.active);
this.particles = this.particles.filter(p => !p.markedForDeletion);
// 失败检测
if (this.lives <= 0) {
this.state = 'GAMEOVER';
}
},
draw() {
// 清屏
this.ctx.fillStyle = "#222";
this.ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
if (this.state === 'MENU') {
this.drawMenu("PIXEL DEFENSE", "点击开始游戏");
return;
}
// 1. 绘制地图
for(let y=0; y<ROWS; y++) {
for(let x=0; x<COLS; x++) {
if (mapGrid[y][x] === 1) {
this.ctx.fillStyle = "#C2B280"; // 土路
} else {
this.ctx.fillStyle = "#388E3C"; // 草地
// 像素噪点纹理
if ((x+y)%2===0) this.ctx.fillStyle = "#2E7D32";
}
this.ctx.fillRect(x*TILE_SIZE, y*TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
// 2. 绘制范围提示 (如果鼠标悬浮 - 简化为绘制选中的塔的范围在角落)
// (略过,保持简洁)
// 3. 绘制实体
this.towers.forEach(t => t.draw(this.ctx));
this.enemies.forEach(e => e.draw(this.ctx));
this.projectiles.forEach(p => p.draw(this.ctx));
this.particles.forEach(p => p.draw(this.ctx));
// 4. 绘制 HUD
this.drawHUD();
if (this.state === 'GAMEOVER') {
this.ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
this.ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
this.drawMenu("GAME OVER", "重试");
}
},
drawHUD() {
const ctx = this.ctx;
// 顶部信息栏
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, CANVAS_W, 30);
ctx.font = "16px 'Courier New'";
ctx.fillStyle = "#FFF";
ctx.textAlign = "left";
ctx.fillText(`生命: ${this.lives}`, 10, 20);
ctx.fillText(`金钱: $${this.money}`, 120, 20);
ctx.fillText(`波次: ${this.wave}`, 240, 20);
if (this.waveTimer > 0) {
ctx.fillStyle = "#FFEB3B";
ctx.fillText(`下一波: ${Math.ceil(this.waveTimer/60)}`, 350, 20);
}
// 底部建造栏
const barY = 440;
ctx.fillStyle = "#111";
ctx.fillRect(0, barY, CANVAS_W, 40);
const drawBtn = (idx, key, label) => {
const x = idx * 100;
const data = TOWER_TYPES[key];
const isSelected = this.selectedTower === key;
ctx.fillStyle = isSelected ? "#555" : "#333";
ctx.fillRect(x, barY, 98, 40);
ctx.fillStyle = data.color;
ctx.fillRect(x + 10, barY + 10, 20, 20);
ctx.fillStyle = "#FFF";
ctx.font = "12px sans-serif";
ctx.fillText(`${data.name} $${data.cost}`, x + 35, barY + 25);
};
drawBtn(0, 'ARCHER');
drawBtn(1, 'CANNON');
drawBtn(2, 'MAGIC');
},
drawMenu(title, btnText) {
const ctx = this.ctx;
ctx.save();
ctx.shadowColor = "#000";
ctx.shadowBlur = 0;
// 标题
ctx.fillStyle = "#FFF";
ctx.font = "bold 48px 'Courier New'";
ctx.textAlign = "center";
ctx.fillText(title, CANVAS_W/2, 200);
// 按钮
ctx.fillStyle = "#4CAF50";
ctx.fillRect(220, 300, 200, 60);
// 按钮边框 (像素风)
ctx.strokeStyle = "#FFF";
ctx.lineWidth = 4;
ctx.strokeRect(220, 300, 200, 60);
ctx.fillStyle = "#FFF";
ctx.font = "bold 24px 'Courier New'";
ctx.fillText(btnText, CANVAS_W/2, 340);
ctx.restore();
},
loop() {
this.update();
this.draw();
requestAnimationFrame(() => this.loop());
}
};
// 启动游戏
window.onload = () => Game.init();
</script>
</body>
</html>
```