Pixi.js 游戏开发教程 / Pixi.js 完整项目:横版卷轴射击游戏
完整项目:横版卷轴射击游戏
从零构建一款经典横版卷轴射击游戏(Side-Scrolling Shooter)。
一、项目架构设计
1.1 系统架构图
┌─────────────────────────────────────────┐
│ Game │
│ ┌──────────┬──────────┬──────────────┐ │
│ │ Scene │ Input │ Audio │ │
│ │ Manager │ Manager │ Manager │ │
│ ├──────────┴──────────┴──────────────┤ │
│ │ GameScene (战斗场景) │ │
│ │ ┌──────┬───────┬───────┬────────┐ │ │
│ │ │Player│ Enemy │Bullet │ Item │ │ │
│ │ │ │ Wave │ Pool │ Spawn │ │ │
│ │ └──────┴───────┴───────┴────────┘ │ │
│ │ ┌───────────┬────────────────────┐ │ │
│ │ │ Background │ HUD │ │ │
│ │ │ Scroller │ Score/HP/Energy │ │ │
│ │ └───────────┴────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
1.2 管理器实现
// src/core/Game.ts
import { Application, Container } from 'pixi.js';
import { SceneManager } from './SceneManager';
import { InputManager } from './InputManager';
import { AudioManager } from './AudioManager';
export class Game {
public app: Application;
public scenes: SceneManager;
public input: InputManager;
public audio: AudioManager;
public width = 800;
public height = 600;
constructor() {
this.app = new Application();
this.scenes = new SceneManager(this);
this.input = new InputManager();
this.audio = new AudioManager();
}
async init() {
await this.app.init({
width: this.width,
height: this.height,
backgroundColor: 0x0a0a1a,
antialias: true,
resolution: Math.min(window.devicePixelRatio, 2),
autoDensity: true,
});
document.getElementById('app')!.appendChild(this.app.canvas);
this.app.ticker.add((ticker) => {
this.update(ticker.deltaTime);
});
await this.scenes.switchTo('menu');
}
private update(delta: number) {
this.scenes.update(delta);
}
}
// src/core/SceneManager.ts
import { Container } from 'pixi.js';
import type { Game } from './Game';
export interface Scene {
container: Container;
init(): Promise<void>;
update(delta: number): void;
destroy(): void;
}
export class SceneManager {
private current: Scene | null = null;
private scenes = new Map<string, () => Promise<Scene>>();
private game: Game;
constructor(game: Game) {
this.game = game;
}
register(name: string, factory: () => Promise<Scene>) {
this.scenes.set(name, factory);
}
async switchTo(name: string) {
if (this.current) {
this.game.app.stage.removeChild(this.current.container);
this.current.destroy();
}
const factory = this.scenes.get(name);
if (!factory) throw new Error(`Scene "${name}" not found`);
this.current = await factory();
await this.current.init();
this.game.app.stage.addChild(this.current.container);
}
update(delta: number) {
this.current?.update(delta);
}
}
// src/core/InputManager.ts
export class InputManager {
private keys = new Set<string>();
private _mouseX = 0;
private _mouseY = 0;
private _mouseDown = false;
constructor() {
window.addEventListener('keydown', (e) => this.keys.add(e.code));
window.addEventListener('keyup', (e) => this.keys.delete(e.code));
window.addEventListener('mousemove', (e) => {
this._mouseX = e.clientX;
this._mouseY = e.clientY;
});
window.addEventListener('mousedown', () => this._mouseDown = true);
window.addEventListener('mouseup', () => this._mouseDown = false);
}
isKeyDown(code: string): boolean {
return this.keys.has(code);
}
get mouseX() { return this._mouseX; }
get mouseY() { return this._mouseY; }
get mouseDown() { return this._mouseDown; }
}
// src/core/AudioManager.ts
export class AudioManager {
private sounds = new Map<string, HTMLAudioElement>();
private musicVolume = 0.5;
private sfxVolume = 0.7;
load(name: string, src: string) {
const audio = new Audio(src);
audio.preload = 'auto';
this.sounds.set(name, audio);
}
play(name: string, loop = false) {
const sound = this.sounds.get(name);
if (!sound) return;
const clone = sound.cloneNode() as HTMLAudioElement;
clone.volume = this.sfxVolume;
clone.loop = loop;
clone.play();
}
playMusic(name: string) {
const sound = this.sounds.get(name);
if (!sound) return;
sound.volume = this.musicVolume;
sound.loop = true;
sound.play();
}
stopAll() {
this.sounds.forEach(s => { s.pause(); s.currentTime = 0; });
}
}
二、玩家飞机
2.1 Player 类
// src/entities/Player.ts
import { Sprite, Texture, Container, Graphics } from 'pixi.js';
import type { InputManager } from '../core/InputManager';
export class Player extends Container {
public sprite: Sprite;
public hp = 100;
public maxHp = 100;
public lives = 3;
public speed = 5;
public fireRate = 150; // 射击间隔 (ms)
public fireLevel = 1; // 火力等级
public invincible = false;
public collisionRadius = 16;
private lastFireTime = 0;
private invincibleTimer = 0;
private blinkTimer = 0;
constructor() {
super();
this.sprite = Sprite.from('/image/player.png');
this.sprite.anchor.set(0.5);
this.addChild(this.sprite);
}
update(delta: number, input: InputManager, now: number, bounds: { w: number; h: number }) {
this.handleMovement(delta, input, bounds);
this.handleInvincibility(delta);
// 返回是否应该发射子弹
return input.isKeyDown('Space') && now - this.lastFireTime > this.fireRate;
}
fire(now: number) {
this.lastFireTime = now;
}
private handleMovement(delta: number, input: InputManager, bounds: { w: number; h: number }) {
const spd = this.speed * delta;
if (input.isKeyDown('ArrowUp') || input.isKeyDown('KeyW')) this.y -= spd;
if (input.isKeyDown('ArrowDown') || input.isKeyDown('KeyS')) this.y += spd;
if (input.isKeyDown('ArrowLeft') || input.isKeyDown('KeyA')) this.x -= spd;
if (input.isKeyDown('ArrowRight') || input.isKeyDown('KeyD')) this.x += spd;
// 限制在屏幕内
this.x = Math.max(20, Math.min(bounds.w - 20, this.x));
this.y = Math.max(20, Math.min(bounds.h - 20, this.y));
}
takeDamage(amount: number) {
if (this.invincible) return;
this.hp -= amount;
if (this.hp <= 0) {
this.lives--;
if (this.lives > 0) {
this.respawn();
}
}
}
respawn() {
this.hp = this.maxHp;
this.invincible = true;
this.invincibleTimer = 120; // 2 秒无敌 (60fps)
}
private handleInvincibility(delta: number) {
if (!this.invincible) return;
this.invincibleTimer -= delta;
this.blinkTimer += delta;
// 闪烁效果
this.sprite.alpha = Math.floor(this.blinkTimer / 4) % 2 === 0 ? 0.3 : 1;
if (this.invincibleTimer <= 0) {
this.invincible = false;
this.sprite.alpha = 1;
}
}
canFire(now: number): boolean {
return now - this.lastFireTime > this.fireRate;
}
}
三、敌人生成系统
3.1 波次管理器
// src/systems/WaveManager.ts
interface WaveConfig {
enemies: { type: string; count: number; interval: number }[];
delay: number; // 波次之间的延迟
}
const WAVE_DATA: WaveConfig[] = [
{
delay: 0,
enemies: [{ type: 'grunt', count: 5, interval: 1000 }],
},
{
delay: 3000,
enemies: [
{ type: 'grunt', count: 8, interval: 800 },
{ type: 'fast', count: 3, interval: 1500 },
],
},
{
delay: 3000,
enemies: [
{ type: 'grunt', count: 10, interval: 600 },
{ type: 'tank', count: 2, interval: 2000 },
],
},
{
delay: 5000,
enemies: [{ type: 'boss', count: 1, interval: 0 }],
},
];
export class WaveManager {
private currentWave = 0;
private spawnTimers = new Map<string, number>();
private waveDelay = 0;
private active = true;
update(
delta: number,
now: number,
spawnEnemy: (type: string) => void,
) {
if (!this.active || this.currentWave >= WAVE_DATA.length) return;
const wave = WAVE_DATA[this.currentWave];
// 波次延迟
if (this.waveDelay < wave.delay) {
this.waveDelay += delta * 16.67;
return;
}
// 生成敌人
let allSpawned = true;
for (const group of wave.enemies) {
const key = `${this.currentWave}_${group.type}`;
if (!this.spawnTimers.has(key)) {
this.spawnTimers.set(key, 0);
}
const timer = this.spawnTimers.get(key)!;
if (timer < group.count) {
allSpawned = false;
const elapsed = this.spawnTimers.get(key + '_time') ?? 0;
if (elapsed >= group.interval) {
spawnEnemy(group.type);
this.spawnTimers.set(key, timer + 1);
this.spawnTimers.set(key + '_time', 0);
} else {
this.spawnTimers.set(key + '_time', elapsed + delta * 16.67);
}
}
}
if (allSpawned) {
this.currentWave++;
this.waveDelay = 0;
}
}
get isComplete(): boolean {
return this.currentWave >= WAVE_DATA.length;
}
get waveNumber(): number {
return this.currentWave + 1;
}
}
3.2 Enemy 类型
// src/entities/Enemy.ts
import { Sprite, Container, Texture } from 'pixi.js';
export interface EnemyConfig {
texture: string;
hp: number;
speed: number;
score: number;
collisionRadius: number;
behavior: 'straight' | 'zigzag' | 'track';
}
export const ENEMY_CONFIGS: Record<string, EnemyConfig> = {
grunt: {
texture: '/image/enemy_grunt.png',
hp: 20,
speed: 2,
score: 100,
collisionRadius: 14,
behavior: 'straight',
},
fast: {
texture: '/image/enemy_fast.png',
hp: 10,
speed: 5,
score: 150,
collisionRadius: 10,
behavior: 'zigzag',
},
tank: {
texture: '/image/enemy_tank.png',
hp: 80,
speed: 1,
score: 300,
collisionRadius: 20,
behavior: 'straight',
},
boss: {
texture: '/image/enemy_boss.png',
hp: 500,
speed: 0.5,
score: 5000,
collisionRadius: 40,
behavior: 'track',
},
};
export class Enemy extends Container {
public sprite: Sprite;
public hp: number;
public maxHp: number;
public speed: number;
public score: number;
public collisionRadius: number;
public behavior: string;
public active = true;
private time = 0;
private startX = 0;
constructor(config: EnemyConfig) {
super();
this.sprite = Sprite.from(config.texture);
this.sprite.anchor.set(0.5);
this.addChild(this.sprite);
this.hp = config.hp;
this.maxHp = config.hp;
this.speed = config.speed;
this.score = config.score;
this.collisionRadius = config.collisionRadius;
this.behavior = config.behavior;
}
update(delta: number, playerX: number, playerY: number) {
this.time += delta;
this.x -= this.speed * delta;
switch (this.behavior) {
case 'zigzag':
this.y += Math.sin(this.time * 0.1) * 2;
break;
case 'track':
const dy = playerY - this.y;
this.y += Math.sign(dy) * Math.min(Math.abs(dy * 0.02), 2) * delta;
break;
}
}
takeDamage(amount: number): boolean {
this.hp -= amount;
return this.hp <= 0;
}
}
四、子弹系统(对象池)
// src/entities/BulletPool.ts
import { Sprite, Texture, Container } from 'pixi.js';
export class Bullet extends Sprite {
public vx = 0;
public vy = 0;
public active = false;
public damage = 10;
public collisionRadius = 4;
constructor() {
super(Texture.from('/image/bullet.png'));
this.anchor.set(0.5);
}
fire(x: number, y: number, vx: number, vy: number, damage: number) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.damage = damage;
this.active = true;
this.visible = true;
}
deactivate() {
this.active = false;
this.visible = false;
}
}
export class BulletPool {
private pool: Bullet[] = [];
private active: Bullet[] = [];
private container: Container;
constructor(container: Container, preallocate = 50) {
this.container = container;
for (let i = 0; i < preallocate; i++) {
const bullet = new Bullet();
bullet.deactivate();
this.pool.push(bullet);
container.addChild(bullet);
}
}
spawn(x: number, y: number, vx: number, vy: number, damage: number): Bullet {
let bullet = this.pool.pop();
if (!bullet) {
bullet = new Bullet();
this.container.addChild(bullet);
}
bullet.fire(x, y, vx, vy, damage);
this.active.push(bullet);
return bullet;
}
update(delta: number, bounds: { w: number; h: number }) {
for (let i = this.active.length - 1; i >= 0; i--) {
const bullet = this.active[i];
bullet.x += bullet.vx * delta;
bullet.y += bullet.vy * delta;
// 超出边界回收
if (bullet.x < -20 || bullet.x > bounds.w + 20 ||
bullet.y < -20 || bullet.y > bounds.h + 20) {
bullet.deactivate();
this.active.splice(i, 1);
this.pool.push(bullet);
}
}
}
getActiveBullets(): Bullet[] {
return this.active;
}
recycle(bullet: Bullet) {
const idx = this.active.indexOf(bullet);
if (idx !== -1) {
this.active.splice(idx, 1);
bullet.deactivate();
this.pool.push(bullet);
}
}
}
五、道具系统
// src/entities/Item.ts
import { Sprite, Container, Texture } from 'pixi.js';
export type ItemType = 'powerup' | 'shield' | 'life' | 'bomb';
export interface ItemConfig {
texture: string;
duration?: number;
value: number;
}
export const ITEM_CONFIGS: Record<ItemType, ItemConfig> = {
powerup: { texture: '/image/item_power.png', value: 1 },
shield: { texture: '/image/item_shield.png', duration: 5000, value: 1 },
life: { texture: '/image/item_life.png', value: 1 },
bomb: { texture: '/image/item_bomb.png', value: 1 },
};
export class Item extends Container {
public type: ItemType;
public collisionRadius = 12;
public active = true;
private speed = 2;
private time = 0;
constructor(type: ItemType) {
super();
this.type = type;
const config = ITEM_CONFIGS[type];
const sprite = Sprite.from(config.texture);
sprite.anchor.set(0.5);
this.addChild(sprite);
}
update(delta: number) {
this.time += delta;
this.x -= this.speed * delta;
this.y += Math.sin(this.time * 0.05) * 0.5; // 轻微浮动
}
apply(player: any) {
const config = ITEM_CONFIGS[this.type];
switch (this.type) {
case 'powerup':
player.fireLevel = Math.min(player.fireLevel + config.value, 5);
break;
case 'shield':
player.invincible = true;
player.invincibleTimer = (config.duration ?? 5000) / 16.67;
break;
case 'life':
player.lives = Math.min(player.lives + config.value, 5);
break;
case 'bomb':
// 清屏炸弹效果由 GameScene 处理
break;
}
this.active = false;
}
}
// 道具掉落管理
export class ItemSpawner {
private dropRates: Record<string, number> = {
grunt: 0.05,
fast: 0.08,
tank: 0.15,
boss: 1.0,
};
rollDrop(enemyType: string): ItemType | null {
const rate = this.dropRates[enemyType] ?? 0;
if (Math.random() > rate) return null;
const roll = Math.random();
if (roll < 0.4) return 'powerup';
if (roll < 0.6) return 'shield';
if (roll < 0.8) return 'life';
return 'bomb';
}
}
六、背景滚动(多层视差)
// src/systems/ParallaxBackground.ts
import { TilingSprite, Texture, Container } from 'pixi.js';
export class ParallaxBackground extends Container {
private layers: { sprite: TilingSprite; speed: number }[] = [];
constructor(width: number, height: number) {
super();
// 星空远景(移动最慢)
this.addLayer('/image/bg_stars.png', 0.2, width, height);
// 星云中景
this.addLayer('/image/bg_nebula.png', 0.5, width, height);
// 小行星近景(移动最快)
this.addLayer('/image/bg_asteroids.png', 1.0, width, height);
}
private addLayer(texturePath: string, speed: number, w: number, h: number) {
const texture = Texture.from(texturePath);
const sprite = new TilingSprite({
texture,
width: w,
height: h,
});
this.addChild(sprite);
this.layers.push({ sprite, speed });
}
update(delta: number) {
for (const layer of this.layers) {
// 向左滚动(tilePosition.x 递减)
layer.sprite.tilePosition.x -= layer.speed * delta;
}
}
}
七、Boss 战
// src/entities/Boss.ts
import { Enemy, EnemyConfig } from './Enemy';
import { Graphics, Container } from 'pixi.js';
export class Boss extends Enemy {
private phase = 1;
private attackTimer = 0;
private attackPatterns: (() => void)[] = [];
private onAttack: (pattern: number) => void;
constructor(onAttack: (pattern: number) => void) {
const config: EnemyConfig = {
texture: '/image/enemy_boss.png',
hp: 500,
speed: 0.5,
score: 5000,
collisionRadius: 40,
behavior: 'track',
};
super(config);
this.onAttack = onAttack;
}
update(delta: number, playerX: number, playerY: number) {
super.update(delta, playerX, playerY);
this.attackTimer += delta;
// 根据血量切换阶段
const hpPercent = this.hp / this.maxHp;
if (hpPercent < 0.3) this.phase = 3;
else if (hpPercent < 0.6) this.phase = 2;
// 攻击模式
this.handleAttack(delta);
}
private handleAttack(delta: number) {
const attackInterval = this.phase === 3 ? 40 : this.phase === 2 ? 60 : 90;
if (this.attackTimer >= attackInterval) {
this.attackTimer = 0;
switch (this.phase) {
case 1:
this.onAttack(0); // 散射
break;
case 2:
this.onAttack(1); // 瞄准射击
break;
case 3:
this.onAttack(2); // 弹幕
break;
}
}
}
}
八、碰撞检测
// src/systems/CollisionSystem.ts
import { Bullet } from '../entities/BulletPool';
import { Enemy } from '../entities/Enemy';
import { Item } from '../entities/Item';
import { Player } from '../entities/Player';
export interface CollisionResult {
enemyHits: { enemy: Enemy; bullet: Bullet }[];
playerHits: Enemy[];
itemPickups: Item[];
}
export function checkCollisions(
player: Player,
playerBullets: Bullet[],
enemyBullets: Bullet[],
enemies: Enemy[],
items: Item[],
): CollisionResult {
const result: CollisionResult = {
enemyHits: [],
playerHits: [],
itemPickups: [],
};
// 玩家子弹 vs 敌人
for (const bullet of playerBullets) {
for (const enemy of enemies) {
if (!enemy.active || !bullet.active) continue;
if (circleCollision(bullet, enemy)) {
result.enemyHits.push({ enemy, bullet });
}
}
}
// 敌人子弹 vs 玩家
for (const bullet of enemyBullets) {
if (!bullet.active || player.invincible) continue;
if (circleCollision(bullet, player)) {
result.playerHits.push(null as any); // 标记玩家被击中
}
}
// 敌人机体 vs 玩家
for (const enemy of enemies) {
if (!enemy.active || player.invincible) continue;
if (circleCollision(player, enemy)) {
result.playerHits.push(enemy);
}
}
// 道具拾取
for (const item of items) {
if (!item.active) continue;
if (circleCollision(player, item)) {
result.itemPickups.push(item);
}
}
return result;
}
function circleCollision(a: { x: number; y: number; collisionRadius: number },
b: { x: number; y: number; collisionRadius: number }): boolean {
const dx = a.x - b.x;
const dy = a.y - b.y;
const dist = Math.sqrt(dx * dx + dy * dy);
return dist < a.collisionRadius + b.collisionRadius;
}
九、HUD 界面
// src/ui/HUD.ts
import { Container, Text, Graphics } from 'pixi.js';
export class HUD extends Container {
private scoreText: Text;
private livesText: Text;
private hpBar: Graphics;
private energyBar: Graphics;
private waveText: Text;
constructor(width: number) {
super();
// 分数
this.scoreText = new Text({
text: 'SCORE: 0',
style: { fill: 0xffffff, fontSize: 18, fontFamily: 'monospace' },
});
this.scoreText.x = 16;
this.scoreText.y = 8;
this.addChild(this.scoreText);
// 生命
this.livesText = new Text({
text: '❤️ × 3',
style: { fill: 0xff4444, fontSize: 18 },
});
this.livesText.x = width - 120;
this.livesText.y = 8;
this.addChild(this.livesText);
// HP 条
this.hpBar = new Graphics();
this.hpBar.x = 16;
this.hpBar.y = 32;
this.addChild(this.hpBar);
// 能量条
this.energyBar = new Graphics();
this.energyBar.x = 16;
this.energyBar.y = 48;
this.addChild(this.energyBar);
// 波次提示
this.waveText = new Text({
text: '',
style: { fill: 0xffff00, fontSize: 24, fontFamily: 'monospace' },
});
this.waveText.anchor.set(0.5, 0);
this.waveText.x = width / 2;
this.waveText.y = 60;
this.addChild(this.waveText);
}
update(score: number, lives: number, hp: number, maxHp: number, energy: number, wave: number) {
this.scoreText.text = `SCORE: ${score}`;
this.livesText.text = `❤️ × ${lives}`;
// HP 条
this.hpBar.clear();
this.hpBar.rect(0, 0, 120, 10).fill({ color: 0x333333 });
const hpPercent = hp / maxHp;
const hpColor = hpPercent > 0.5 ? 0x00ff00 : hpPercent > 0.25 ? 0xffaa00 : 0xff0000;
this.hpBar.rect(0, 0, 120 * hpPercent, 10).fill({ color: hpColor });
// 能量条
this.energyBar.clear();
this.energyBar.rect(0, 0, 120, 6).fill({ color: 0x333333 });
this.energyBar.rect(0, 0, 120 * (energy / 100), 6).fill({ color: 0x4488ff });
}
showWave(number: number) {
this.waveText.text = `— WAVE ${number} —`;
// 2 秒后隐藏
setTimeout(() => { this.waveText.text = ''; }, 2000);
}
}
十、完整 GameScene
// src/scenes/GameScene.ts
import { Container } from 'pixi.js';
import { Game } from '../core/Game';
import { Player } from '../entities/Player';
import { Enemy, ENEMY_CONFIGS } from '../entities/Enemy';
import { BulletPool } from '../entities/BulletPool';
import { Item, ItemSpawner } from '../entities/Item';
import { WaveManager } from '../systems/WaveManager';
import { ParallaxBackground } from '../systems/ParallaxBackground';
import { checkCollisions } from '../systems/CollisionSystem';
import { HUD } from '../ui/HUD';
export class GameScene {
container = new Container();
private game: Game;
private player!: Player;
private enemies: Enemy[] = [];
private playerBullets!: BulletPool;
private enemyBullets!: BulletPool;
private items: Item[] = [];
private bg!: ParallaxBackground;
private hud!: HUD;
private waves!: WaveManager;
private itemSpawner = new ItemSpawner();
private score = 0;
private gameTime = 0;
constructor(game: Game) {
this.game = game;
}
async init() {
const { width, height } = this.game;
// 背景
this.bg = new ParallaxBackground(width, height);
this.container.addChild(this.bg);
// 子弹容器
const bulletContainer = new Container();
this.container.addChild(bulletContainer);
this.playerBullets = new BulletPool(bulletContainer, 100);
this.enemyBullets = new BulletPool(bulletContainer, 200);
// 敌人容器
const enemyContainer = new Container();
this.container.addChild(enemyContainer);
// 道具容器
const itemContainer = new Container();
this.container.addChild(itemContainer);
// 玩家
this.player = new Player();
this.player.x = 100;
this.player.y = height / 2;
this.container.addChild(this.player);
// HUD
this.hud = new HUD(width);
this.container.addChild(this.hud);
// 波次管理
this.waves = new WaveManager();
// 加载音效
this.game.audio.load('shoot', '/audio/shoot.mp3');
this.game.audio.load('explosion', '/audio/explosion.mp3');
this.game.audio.load('item', '/audio/item.mp3');
this.game.audio.playMusic('/audio/bgm.mp3');
}
update(delta: number) {
this.gameTime += delta;
const now = performance.now();
const bounds = { w: this.game.width, h: this.game.height };
// 背景滚动
this.bg.update(delta);
// 玩家更新
const shouldFire = this.player.update(delta, this.game.input, now, bounds);
if (shouldFire && this.player.canFire(now)) {
this.player.fire(now);
this.firePlayerBullets();
}
// 敌人生成
this.waves.update(delta, now, (type) => this.spawnEnemy(type));
// 敌人更新
for (const enemy of this.enemies) {
if (enemy.active) {
enemy.update(delta, this.player.x, this.player.y);
}
}
// 子弹更新
this.playerBullets.update(delta, bounds);
this.enemyBullets.update(delta, bounds);
// 道具更新
for (const item of this.items) {
if (item.active) item.update(delta);
}
// 碰撞检测
this.handleCollisions();
// 清理
this.cleanup();
// HUD 更新
this.hud.update(
this.score, this.player.lives,
this.player.hp, this.player.maxHp, 0,
this.waves.waveNumber,
);
// 游戏结束检查
if (this.player.lives <= 0) {
this.gameOver();
}
// 胜利检查
if (this.waves.isComplete && this.enemies.length === 0) {
this.victory();
}
}
private firePlayerBullets() {
const level = this.player.fireLevel;
const x = this.player.x + 20;
const y = this.player.y;
// 根据火力等级发射不同数量的子弹
this.playerBullets.spawn(x, y, 10, 0, 10);
if (level >= 2) {
this.playerBullets.spawn(x, y - 8, 10, -1, 10);
this.playerBullets.spawn(x, y + 8, 10, 1, 10);
}
if (level >= 3) {
this.playerBullets.spawn(x, y, 12, 0, 15);
}
if (level >= 4) {
this.playerBullets.spawn(x, y - 16, 10, -2, 10);
this.playerBullets.spawn(x, y + 16, 10, 2, 10);
}
this.game.audio.play('shoot');
}
private spawnEnemy(type: string) {
const config = ENEMY_CONFIGS[type];
if (!config) return;
const enemy = new Enemy(config);
enemy.x = this.game.width + 50;
enemy.y = 50 + Math.random() * (this.game.height - 100);
this.container.addChild(enemy);
this.enemies.push(enemy);
}
private handleCollisions() {
const result = checkCollisions(
this.player,
this.playerBullets.getActiveBullets(),
this.enemyBullets.getActiveBullets(),
this.enemies,
this.items,
);
// 子弹命中敌人
for (const { enemy, bullet } of result.enemyHits) {
this.playerBullets.recycle(bullet);
if (enemy.takeDamage(bullet.damage)) {
// 敌人死亡
this.score += enemy.score;
this.spawnExplosion(enemy.x, enemy.y);
this.tryDropItem(enemy);
enemy.active = false;
this.container.removeChild(enemy);
}
}
// 玩家被击中
if (result.playerHits.length > 0) {
this.player.takeDamage(20);
this.game.audio.play('explosion');
}
// 道具拾取
for (const item of result.itemPickups) {
item.apply(this.player);
this.game.audio.play('item');
this.container.removeChild(item);
}
}
private spawnExplosion(x: number, y: number) {
this.game.audio.play('explosion');
// 简单爆炸效果(可用粒子系统替代)
// 这里省略具体实现
}
private tryDropItem(enemy: Enemy) {
const type = this.itemSpawner.rollDrop(enemy.constructor.name === 'Boss' ? 'boss' : 'grunt');
if (type) {
const item = new Item(type);
item.x = enemy.x;
item.y = enemy.y;
this.container.addChild(item);
this.items.push(item);
}
}
private cleanup() {
this.enemies = this.enemies.filter(e => {
if (!e.active || e.x < -100) {
this.container.removeChild(e);
return false;
}
return true;
});
this.items = this.items.filter(i => {
if (!i.active || i.x < -50) {
this.container.removeChild(i);
return false;
}
return true;
});
}
private gameOver() {
console.log('Game Over! Final Score:', this.score);
this.game.scenes.switchTo('gameover');
}
private victory() {
console.log('Victory! Final Score:', this.score);
this.game.scenes.switchTo('victory');
}
destroy() {
this.container.removeChildren();
this.game.audio.stopAll();
}
}
十一、发布与优化清单
□ 游戏功能
├─ 玩家移动/射击 ✓
├─ 敌人波次系统 ✓
├─ 道具系统 ✓
├─ Boss 战 ✓
├─ 视差滚动背景 ✓
└─ HUD 界面 ✓
□ 优化
├─ 对象池 (子弹/敌人/粒子)
├─ 纹理图集
├─ Draw Call 优化
├─ 移动端适配
└─ 音效压缩
□ 发布
├─ 构建优化 (Vite)
├─ PWA 离线支持
├─ CDN 部署
└─ 错误监控 (Sentry)