飞机大战 Low 版 (Made in AI)

弹幕抉择 - 飞机大战 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

部署配置:

  1. 仓库 Settings → Pages → Source 选择 GitHub ActionsDeploy from a branch
  2. 分支选择 gh-pages,目录选择 /(root)
  3. 访问 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!

相关推荐
angerdream1 小时前
Android手把手编写儿童手机远程监控App之agentweb如何实现全屏
前端
星栈2 小时前
10 分钟跑起第一个 Dioxus 应用:`dx` CLI、`rsx!` 和热更新好不好用
前端·rust·前端框架
奋斗吧程序媛2 小时前
补充一个小知识点:有关@click.native
前端·vue.js
触底反弹2 小时前
🚀 手把手用 HTML5 Canvas 从零打造飞机大战游戏,代码全开源!
前端·javascript·canvas
DJ斯特拉2 小时前
axios快速使用
开发语言·前端·javascript
还有多久拿退休金2 小时前
Ant Design Tree 搜索定位避坑指南:虚拟滚动下如何实现高亮与精准定位
前端·react.js
小月土星2 小时前
CSS 3D 从入门到炫技:手把手教你写一个旋转立方体
前端·css
Hilaku3 小时前
AI 写代码越快,为什么 Code Review 越不能省?
前端·javascript·程序员
sugar__salt3 小时前
从网页小游戏到数据可视化:掌握 HTML5 Canvas 核心能力
前端·信息可视化·html5