弹幕抉择 - 飞机大战 Low 版 (Made in AI)
主要框架 React + Pixi.js 包含完整的战斗系统、道具系统、程序化音频和像素美术。
项目简介
弹幕抉择 是一款竖屏弹幕射击小游戏。玩家在屏幕下方操控机体,自动向上方射击;屏幕被一条中线分为两个区域------左侧 掉落可以击碎获取强化的道具,右侧不断涌现敌人和波次 BOSS。你需要在"收集资源"和"躲避危险"之间做出取舍。
核心玩法
- 自动射击:主炮持续开火,无需手动瞄准
- 道具抉择:左侧落下可击碎的强化道具,通过子弹击碎获得临时能力
- 走位生存:右侧敌人不断下冲,需要灵活躲避并消灭
- 波次挑战:每 3 波出现 BOSS,每 12 波出现巨型 BOSS,难度逐步攀升
- 组合Build:多种强化道具自由叠加,打造专属火力
技术栈
| 层面 | 技术选型 |
|---|---|
| 框架 | React 19 |
| 语言 | TypeScript ~6.0 (ES2023 target) |
| 构建 | Vite 8 |
| 渲染 | Pixi.js 8 (Canvas 2D, 像素纹理图集) |
| 状态管理 | Zustand 4 |
| 音频 | Web Audio API (全程序化合成,零音频文件) |
| 部署 | GitHub Pages (gh-pages 分支) |
运行方式
arduino
npm install
npm run dev
构建与部署:
arduino
npm run build
npm run deploy # 推送到 gh-pages 分支
架构设计
整体分层
整个游戏采用渲染与逻辑分离的分层架构:
scss
┌─────────────────────────────────────────────┐
│ React UI 层 (Zustand) │
│ LoadingScreen / StartScreen / HUD / GameOver│
├─────────────────────────────────────────────┤
│ Pixi.js 渲染层 │
│ GameCanvas → Application → Stage Container │
├─────────────────────────────────────────────┤
│ 游戏引擎层 (Engine) │
│ GameEngine (主循环) │
│ ├── EntityManager (实体生命周期) │
│ ├── CollisionSystem (碰撞检测) │
│ ├── ParticleSystem (粒子特效) │
│ └── Camera (舞台容器 + 屏幕震动) │
├─────────────────────────────────────────────┤
│ 系统层 (Systems) │
│ SpawnSystem / MovementSystem / CombatSystem │
│ EffectSystem / DifficultySystem │
├─────────────────────────────────────────────┤
│ 配置与数据层 │
│ gameConfig (常量表) / Entity (类型定义) │
├─────────────────────────────────────────────┤
│ 表现层 (Sprites / Audio) │
│ PixelArt 图集 / 程序化 BGM + SFX │
└─────────────────────────────────────────────┘
游戏循环
游戏的核心是一个 60 FPS 的 Pixi.js Ticker 循环,每帧执行以下流程:
kotlin
// GameEngine.ts
private update(): void {
if (!this.running) return;
this.frame++;
const player = this.entities.player;
if (!player) return;
// 1. 输入处理 --- 获取玩家拖拽目标位置
const target = this.input.getTarget();
if (target) {
player.targetX = target.x;
player.targetY = target.y;
}
// 2. 生成系统 --- 安排敌怪和道具的出生时机
this.spawnSystem.update(this.difficultySystem.level, this.difficultySystem.wave);
// 3. 移动系统 --- 更新所有实体的位置
this.movementSystem.update(player, this.entities.bullets,
this.entities.enemies, this.entities.items, this.entities.enemyBullets);
// 4. 战斗系统 --- 射击、碰撞、伤害、拾取效果
this.combatSystem.update(player, this.entities, this.frame, this.difficultySystem.wave);
// 5. 效果系统 --- 倒计时强化持续时间
this.effectSystem.update(player);
// 6. 难度系统 --- 连续难度 + 波次计时
this.difficultySystem.update();
// 7. 清理 --- 移除越界/死亡的实体
this.entities.cleanup();
// 8. 摄像机 --- 屏幕震动更新
this.camera.update();
// 9. 粒子 --- 更新粒子生命周期
this.particles.update();
// 9.5 同步视觉 --- 将数据位置同步到 Pixi Sprite
this.entities.syncVisuals();
// 10. 更新 HUD (每 5 帧)
if (this.frame % 5 === 0) {
useGameStore.getState().updateHUD({ /* ... */ });
}
// 11. BGM 强度动态调整
audioEngine.bgm?.setIntensity(
Math.min(1, this.difficultySystem.wave / 10 + this.difficultySystem.level / 6)
);
// 检查游戏结束
if (player.hp <= 0) {
this.stopGame();
}
}
每一帧的职责清晰分离,每个 System 都是独立的类,便于调试和调参。
核心系统实现
1. 实体管理
游戏中共有 6 种实体类型,各自维护数据数组和对应的 Pixi Sprite 数组,通过索引一一对应:
ini
// EntityManager.ts
export class EntityManager {
// 数据层
public bullets: BulletEntity[] = [];
public enemies: EnemyEntity[] = [];
public items: ItemEntity[] = [];
public homingBullets: HomingBulletEntity[] = [];
public enemyBullets: EnemyBulletEntity[] = [];
// 表现层
public bulletSprites: Sprite[] = [];
public enemySprites: Container[] = [];
public itemSprites: Sprite[] = [];
public homingBulletSprites: Sprite[] = [];
public enemyBulletSprites: Sprite[] = [];
// 每帧同步数据到精灵
syncVisuals(): void {
for (let i = 0; i < this.bullets.length; i++) {
this.bulletSprites[i].x = this.bullets[i].x;
this.bulletSprites[i].y = this.bullets[i].y;
}
// ... 其他实体同理
}
}
这种数据与表现分离但保持索引同步的方式,避免了引入完整 ECS 框架的复杂度,同时保持了代码的可读性。
实体类型定义
css
// Entity.ts
interface PlayerEntity {
x: number; y: number;
targetX: number; targetY: number;
hp: number; maxHp: number;
shieldHp: number;
fireTimer: number;
invincible: number;
effects: { type: EffectType; timer: number }[];
armorActive: boolean;
}
interface EnemyEntity {
x: number; y: number;
hp: number; maxHp: number;
size: number; speed: number; score: number;
tier: number; // 1~4
canShoot: boolean; // 能否发射敌弹
shootCd: number;
megaLaserPhase?: 'charging' | 'beam'; // 巨型BOSS激光阶段
megaLaserFrame?: number;
wobble: number; // 动画相位
flashTimer: number; // 受击闪烁
alive: boolean;
}
2. 战斗系统
战斗系统是整个游戏最复杂的部分,负责射击、激光、追踪弹、伤害结算、道具效果等所有战斗逻辑。
自动射击与强化
ini
// 每帧检查是否达到射击间隔
player.fireTimer++;
const fireRate = this.getFireRate(fx); // 受加速道具影响
if (player.fireTimer >= fireRate) {
player.fireTimer = 0;
this.fireBullets(player, fx);
}
private getFireRate(fx: PlayerPowerupSnapshot): number {
let rate: number = GAME_CONFIG.BASE_FIRE_RATE; // 默认 5 帧
if (fx.has('fireRate')) rate = Math.floor(rate * 0.28); // 加速后约 1 帧
return Math.max(2, rate);
}
激光系统
激光是游戏中最具视觉冲击力的武器,采用三阶段状态机:
css
[charging] → [pulse] → [cooldown] → [charging] ...
42帧 1帧 40帧
kotlin
// 蓄力阶段:绘制旋转能量环
if (this.laserPhase === 'charging') {
this.particles.emitLaserChargeRing(player.x, player.y, frame, 42);
if (frame >= 42) { this.laserPhase = 'pulse'; }
}
// 脉冲阶段:一次性垂直光束伤害
else if (this.laserPhase === 'pulse') {
this.applyLaserPulseDamage(player, entities, fx);
this.particles.emitLaserVerticalPulse(beams, player.y);
audioEngine.sfx?.play('laser');
this.laserPhase = 'cooldown';
}
// 冷却阶段:静默等待
激光伤害判定是纯垂直光柱,并非扇形散射。三连弹模式下会生成多条平行光柱:
ini
private applyLaserPulseDamage(player, entities, fx) {
const hw = 17; // 半宽固定
const dmg = 32; // 每条光柱 32 伤害
const beams = this.getLaserBeamXs(player, fx); // 1条或4条
for (const enemy of entities.enemies) {
let beamHits = 0;
for (const bx of beams) {
if (Math.abs(enemy.x - bx) < hw + enemy.size * 0.55) beamHits++;
}
if (beamHits > 0) {
enemy.hp -= dmg * beamHits; // 多光柱可叠加伤害
}
}
}
浮游炮与追踪弹
浮游炮从玩家两侧派出小型追踪单位,自动寻找最近敌人:
ini
private updateDrone(player, fx) {
const n = BASE_STREAMS + (hasTriple ? TRIPLE_EXTRA : 0);
for (let i = 0; i < n; i++) {
// 找到最近的敌人
let nearest: EnemyEntity | null = null;
let nd = Infinity;
for (const e of entities.enemies) {
const d = dist(droneX, droneY, e.x, e.y);
if (d < nd) { nd = d; nearest = e; }
}
// 朝最近敌人发射
if (nearest) {
const a = Math.atan2(nearest.y - droneY, nearest.x - droneX);
entities.spawnHomingBullet(droneX, droneY,
Math.cos(a) * spd, Math.sin(a) * spd, damage, options);
}
}
}
追踪弹采用向量转向 机制,转向强度由 HOMING_TURN_RATE: 0.26 控制:
ini
private updateHomingBullets(entities) {
for (const b of entities.homingBullets) {
// 找到最近敌人
const nearest = findNearestEnemy(b);
if (nearest) {
const ux = (nearest.x - b.x) / nd;
const uy = (nearest.y - b.y) / nd;
// 渐进转向:每帧向目标方向偏移一定比例
let vx = b.vx + (ux * targetSpd - b.vx) * steer;
let vy = b.vy + (uy * targetSpd - b.vy) * steer;
// 限速:保持恒定飞行速度
const len = Math.hypot(vx, vy);
b.vx = (vx / len) * targetSpd;
b.vy = (vy / len) * targetSpd;
}
}
}
3. 生成系统
屏幕被分为两个区域,决定了游戏的核心"抉择"机制:
scss
┌──────────────────────────────────────┐
│ 左侧 45% │ 右侧 55% │
│ ─────────────┼────────────────── │
│ 道具掉落区域 │ 敌人生成区域 │
│ (安全) │ (危险) │
└──────────────────────────────────────┘
typescript
// SpawnSystem.ts
update(difficulty: number, wave: number): void {
// 敌人生成 --- 频率随难度递增
this.enemyTimer--;
if (this.enemyTimer <= 0) {
const rate = Math.max(9,
GAME_CONFIG.ENEMY_SPAWN_RATE
- Math.floor(difficulty * 9)
- Math.floor(wave * 1.2)
);
this.enemyTimer = rate;
// 根据难度决定敌怪等级
const maxTier = Math.min(2, Math.ceil(difficulty));
this.entities.spawnEnemy(randInt(1, maxTier), undefined, wave);
}
// 道具生成 --- 频率较低,避免无脑碾压
this.itemTimer--;
if (this.itemTimer <= 0) {
const rate = Math.max(58,
GAME_CONFIG.ITEM_SPAWN_RATE - Math.floor(difficulty * 8) - Math.floor(wave * 0.65)
);
this.itemTimer = rate;
// 波次越高,批量掉落越多
const bundle = 1 + Math.min(4, Math.floor(wave / 5));
for (let b = 0; b < bundle; b++) {
this.entities.spawnItem(randInt(1, maxTier));
}
}
}
波次事件调度
scss
private handleWaveEvents(wave: number): void {
for (let w = lastWave + 1; w <= wave; w++) {
// 巨型BOSS:每12波
if (w % 12 === 0) {
spawnEnemy(4, { hpMult: 4.2 });
}
// 普通BOSS:每3波(与巨型BOSS同波时优先巨型)
else if (w % 3 === 0) {
spawnEnemy(3, { hpMult: 3.2 });
}
// 精英双怪:每2波
else if (w % 2 === 0) {
spawnEnemy(2, { hpMult: 2.1, speedMult: 1.18 });
spawnEnemy(2, { hpMult: 2.1, speedMult: 1.18 });
}
}
}
4. 移动系统
不同等级的敌怪有不同的移动模式:
arduino
switch (e.tier) {
case 1:
// 小兵:正弦波蛇形下降
e.y += e.speed;
e.x += Math.sin(e.wobble * 1.5) * 1.2 + drift;
break;
case 2:
// 重甲/精英:缓慢下降 + 周期性水平冲刺
if (Math.sin(e.wobble * 0.4) > 0.7) {
const dir = e.x > WIDTH * 0.7 ? -1 : 1;
e.x += dir * e.speed * 3 + drift * 0.5;
e.y += e.speed * 0.2;
} else {
e.y += e.speed;
}
break;
case 3:
// BOSS:慢速下降 + 轨道运动 + 周期性悬停充能
e.y += e.speed;
e.x += Math.sin(e.wobble * 0.8) * 2.0;
if (Math.sin(e.wobble * 0.3) > 0.85) {
e.y -= e.speed * 0.8; // 几乎悬停
}
break;
case 4: {
// 巨型BOSS:四阶段循环
const phase = Math.floor(e.wobble / 3) % 4;
switch (phase) {
case 0: e.y += e.speed; break; // 缓降
case 1: e.x -= 0.8; e.y += e.speed * 0.3; break; // 左扫
case 2: e.x += Math.sin(e.wobble * 8) * 0.5; break; // 充能振动
case 3: e.x += 0.8; e.y += e.speed * 0.3; break; // 右扫
}
break;
}
}
玩家移动使用线性插值平滑跟随:
ini
player.x = lerp(player.x, player.targetX, 0.15);
player.y = lerp(player.y, player.targetY, 0.15);
5. 碰撞检测
采用简单的距离检测,性能足够且代码简洁:
ini
// 子弹击中敌怪
for (const bullet of entities.bullets) {
for (const enemy of entities.enemies) {
if (dist(bullet.x, bullet.y, enemy.x, enemy.y) < enemy.size + 4) {
enemy.hp -= bullet.damage;
enemy.flashTimer = 4;
if (!bullet.pierce) bullet.alive = false; // 穿透弹不消失
// 粒子特效 + 音效
this.particles.emit(bullet.x, bullet.y, 0xffaa00, 4, 3);
this.combo++;
}
}
}
6. 难度系统
难度通过两条曲线递增:
kotlin
// 连续难度:每帧 +0.00135
this.difficulty += 0.00135;
// 离散波次:每 2520 帧 (+1 波,约 42 秒)
this.waveTimer++;
if (this.waveTimer >= 2520) {
this.waveTimer = 0;
this.wave++;
}
敌人生成频率、BOSS 血量、敌弹射率都随难度和波次动态变化。
视觉系统
像素艺术管线
游戏没有使用任何外部图片资源,所有像素美术都通过 Canvas 2D 在运行时生成:
ini
// 像素网格定义 (16x15 的飞机)
const playerPixels: PixelGrid = [
[null,null,null, 1,1,1, null,null, 1,1,1, null,null,null,null],
[null,null, 1,1,1,1,1, 1,1,1,1,1, null,null,null],
// ...
];
// 转换为 Pixi.js Texture
function createPixelTexture(grid: PixelGrid): Texture {
const canvas = document.createElement('canvas');
canvas.width = grid[0].length;
canvas.height = grid.length;
const ctx = canvas.getContext('2d')!;
for (let y = 0; y < grid.length; y++) {
for (let x = 0; x < grid[y].length; x++) {
const colorIdx = grid[y][x];
if (colorIdx !== null) {
ctx.fillStyle = PALETTE[colorIdx];
ctx.fillRect(x, y, 1, 1);
}
}
}
const image = new ImageSource(new ImageBitmap(canvas));
return Texture.makeTextureFromSource(image);
}
所有纹理被组织到一个图集 (Atlas) 中,SpriteFactory 作为单例管理:
css
export function getAtlas(): SpriteAtlas {
return {
player: createPixelTexture(playerPixels),
playerArmored: createPixelTexture(playerArmorPixels),
// ... 4 种玩家外观变体
bullets: { normal, pierce, enemy, homingCrescent },
enemies: { 1: tier1, 2: tier2, 3: tier3, 4: tier4 },
items: { fireRate, tripleShot, laser, nuke, ... },
particle: createSolidTexture(4, 4, 0xffffff),
};
}
玩家外观切换
根据装备的强化,玩家机体外观会在 4 种变体间切换:
kotlin
syncVisuals() {
const hasDrones = player.armorActive || player.effects.some(e => e.type === 'drone');
if (player.armorActive && hasDrones) {
this.playerSprite.texture = this.atlas.playerArmoredWithDrones;
} else if (player.armorActive) {
this.playerSprite.texture = this.atlas.playerArmored;
} else if (hasDrones) {
this.playerSprite.texture = this.atlas.playerWithDrones;
} else {
this.playerSprite.texture = this.atlas.player;
}
}
粒子系统
粒子系统支持多种特效类型:
javascript
class ParticleSystem {
// 普通爆炸粒子
emit(x, y, color, count, speed) { }
// 激光蓄力环:螺旋向外扩散
emitLaserChargeRing(x, y, frame, totalFrames) { }
// 激光脉冲:垂直向上的定向粒子
emitLaserVerticalPulse(beams, playerY) { }
// 巨型BOSS激光:向下扫过的红色光柱
emitMegaBossDownLaser(xs, startY, frame, halfWidth, maxHalfWidth) { }
}
镜头震动
屏幕震动强度随敌怪等级动态调整:
kotlin
// 普通敌人死亡
this.camera.shake(1.1 + enemy.tier * 0.9);
// BOSS 死亡
this.camera.shake(1.1 + 3 * 0.9); // 3.8
// 核弹爆发
this.camera.shake(9); // 最大震动
音频系统
游戏没有任何音频文件,所有 BGM 和音效都是通过 Web Audio API 实时合成的。
音频架构
scss
AudioContext
├── masterGain (总音量 0.8)
│ ├── bgmGain (背景音乐 0.4) → BGMManager
│ │ ├── LoadingTrack (环境音)
│ │ ├── MenuTrack (110 BPM 芯片音乐)
│ │ ├── BattleTrack (172 BPM 战斗音乐,带强度缩放)
│ │ └── GameOverTrack (下行琶音)
│ └── sfxGain (音效 0.7) → SFXLibrary (20+ 种音效)
程序化音效
每种音效都用不同的振荡器或噪声缓冲区实现:
ini
class SFXLibrary {
// 射击声:高频正弦波快速下滑
play('shoot') {
const osc = this.ctx.createOscillator();
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(200, now + 0.08);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
osc.connect(gain).connect(this.sfxGain);
osc.start(now); osc.stop(now + 0.08);
}
// 爆炸声:白噪声 + 低通滤波
play('kill') {
const bufferSize = this.ctx.sampleRate * 0.3;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * (1 - i / bufferSize);
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
// ... 滤波 + 包络
}
// 受伤、拾取、组合连击、BOSS命中、护盾破碎... 共 20+ 种
}
程序化 BGM --- 战斗曲
战斗音乐是整首歌精华,采用 172 BPM 快节奏芯片音乐,并带有动态强度缩放:
typescript
// BattleTrack.ts
class BattleTrack {
private kickPhrases: number[][] = [
// 基础节拍:每小节 4 拍
[0, 0.25, 0.5, 0.75],
];
private bassNotes: number[] = [220, 261.63, 293.66, 246.94];
// 根据强度逐步添加音轨层
schedule(intensity: number) {
// intensity 0.22 → 加入贝斯层
// intensity 0.42 → 加入主旋律层
// intensity 0.62 → 加入琶音层
// intensity 0.78 → 加入额外鼓点
}
}
BGM 强度由游戏难度驱动:
ini
audioEngine.bgm?.setIntensity(
Math.min(1, wave / 10 + level / 6)
);
道具系统
游戏设计了 10 种强化道具,每种都有独立的像素图标、HP 值、持续时间和颜色:
| 道具 | 效果 | 持续时间 | 等级 |
|---|---|---|---|
| 加速射击 | 射速提升至约 1 帧 | 350 帧 (~5.8s) | 1 |
| 小回血 | 瞬间恢复 10% 最大生命 | 瞬时 | 1 |
| 三连弹 | 每次射击额外 +3 条弹道 | 450 帧 (~7.5s) | 2 |
| 护盾 | 获得 60 点护盾值 | 500 帧 (~8.3s) | 2 |
| 反应装甲 | 常驻浮游炮,首次碰撞碎裂 | 瞬时 | 2 |
| 激光炮 | 蓄力垂直光束 (32 伤害/条) | 300 帧 (~5s) | 3 |
| 穿透弹 | 子弹无视碰撞继续飞行 | 400 帧 (~6.7s) | 3 |
| 浮游炮 | 两侧派出自动追踪单位 | 500 帧 (~8.3s) | 3 |
| 跟踪弹 | 新月形追踪弹,转向强度 0.26 | 400 帧 (~6.7s) | 3 |
| 全屏爆破 | 清除所有敌弹,对 BOSS 削血 | 瞬时 | 4 |
道具获取机制
道具不是直接拾取,而是需要用子弹击碎:
ini
private resolveBulletItemHits(entities) {
for (const bullet of entities.bullets) {
for (const item of entities.items) {
if (this.collision.checkBulletItem(bullet, item)) {
item.hp -= bullet.damage;
item.flashTimer = 4; // 受击闪烁
if (!bullet.pierce) bullet.alive = false;
}
}
}
}
private resolveItemLifecycle(player, entities) {
for (const item of entities.items) {
if (item.hp <= 0) {
item.alive = false;
this.applyEffect(player, item.effect, item.duration, entities);
this.score += item.value; // 同时获得分数
}
if (item.y > LOGICAL_HEIGHT + 30) item.alive = false; // 漏掉也消失
}
}
道具的 HP 值随等级递增,高等级道具更耐打,但也给更多分数和更强的效果。
效果叠加策略
部分效果是瞬时的(回血、装甲、核弹、护盾),部分效果是持续性的(射速、三连、激光、浮游炮、跟踪弹、穿透)。同种效果不会重复叠加,拾取时会刷新计时器:
ini
private applyEffect(player, effect, duration) {
// 瞬时效果单独处理
if (effect === 'heal') { restoreHP(10%); return; }
if (effect === 'nuke') { destroyAllEnemies(); return; }
if (effect === 'shield') { player.shieldHp = 60; return; }
// 持续性效果:过滤旧的同类型,替换为新计时器
player.effects = player.effects.filter(e => e.type !== effect);
player.effects.push({ type: effect, timer: duration });
}
敌怪设计
敌怪等级
| 等级 | 名称 | HP | 大小 | 速度 | 分数 | 行为 |
|---|---|---|---|---|---|---|
| 1 | 小兵 | 22 | 12 | 1.05 | 10 | 正弦波蛇形下降 |
| 1 | 疾风 | 14 | 10 | 1.95 | 20 | 快速直线下降 |
| 2 | 重甲 | 62 | 16 | 0.52 | 30 | 缓慢下降 + 水平冲刺 |
| 2 | 精英 | 135 | 20 | 0.38 | 60 | 大范围水平横扫 |
| 3 | BOSS | 4200 | 38 | 0.14 | 200 | 轨道运动 + 悬停充能 + 射击 |
| 4 | 巨型BOSS | 16000 | 54 | 0.08 | 500 | 四阶段巡航 + 向下激光 |
巨型 BOSS 激光攻击
巨型 BOSS 拥有独特的激光攻击模式,分两个阶段:
css
[蓄力环] → [向下光束] → [蓄力环] → ...
78帧 62帧
ini
private updateMegaBossLasers(player, entities, frame) {
for (const e of entities.enemies) {
if (e.tier !== 4) continue;
if (e.megaLaserPhase === 'charging') {
// 绘制旋转红色能量环
this.particles.emitMegaBossLaserRing(e.x, e.y, frame, 78);
if (frame >= 78) { e.megaLaserPhase = 'beam'; }
} else {
// 向下扫过的激光束,宽度逐渐收缩
const t = frame / 62;
const halfWidth = MAX_HALF_WIDTH * (1 - t);
// 检测是否击中玩家核心
if (Math.abs(player.x - e.x) < halfWidth + PLAYER_CORE_RADIUS) {
player.hp -= 2.4; // 配合短无敌帧避免秒杀
player.invincible = 7;
}
}
}
}
玩家弱点设计
玩家机体有一个核心弱点(半径 4px),只有敌怪子弹命中这里才会受伤:
scss
checkEnemyBulletPlayerCore(bullet, player) {
return dist(bullet.x, bullet.y, player.x, player.y) <= PLAYER_CORE_RADIUS;
}
这鼓励玩家精确走位,而非单纯依赖护盾硬扛。
UI 与状态管理
屏幕状态
使用 Zustand 管理四个离散屏幕状态:
java
type GameScreen = 'loading' | 'start' | 'playing' | 'gameOver';
// React 组件中条件渲染
if (screen !== 'playing') return null;
HUD 更新
游戏内的分数、连击、血量等信息每 5 帧同步一次到 Zustand store:
yaml
if (this.frame % 5 === 0) {
useGameStore.getState().updateHUD({
score: this.combatSystem.score,
combo: this.combatSystem.combo,
hp: player.hp,
maxHp: player.maxHp,
shieldHp: player.shieldHp,
armorActive: player.armorActive,
activeEffects: player.effects.map(e => ({
type: e.type, remaining: e.timer
})),
wave: this.difficultySystem.wave,
});
}
得分与连击
kotlin
// 击杀得分 = 基础分数 × 连击倍率
const mult = 1 + Math.floor(this.combo / 5);
this.score += enemy.score * mult;
// 连击计时器:90 帧无击杀则重置
this.comboTimer = 90;
if (this.comboTimer <= 0) this.combo = 0;
部署
项目通过 gh-pages 部署到 GitHub Pages:
arduino
# 构建
npm run build
# 部署(自动推断仓库名设置 base 路径并推送到 gh-pages 分支)
npm run deploy
部署配置:
- 仓库 Settings → Pages → Source 选择
GitHub Actions或Deploy from a branch - 分支选择
gh-pages,目录选择/(root) - 访问
https://<username>.github.io/<repo-name>/
总结
这个项目展示了如何用现代前端技术栈构建一款完整的浏览器游戏:
-
React 负责菜单和 HUD 等 UI 层
-
Pixi.js 8 提供高性能的 2D 渲染和像素美术管线
-
Zustand 以极简的 API 管理游戏状态
-
Web Audio API 实现了零素材的程序化音频
-
TypeScript 的类型系统让复杂的实体关系和系统交互清晰可维护
项目源码:github.com/SceneryCN/s...
欢迎体验、Star 和 Fork!