Pixi.js 游戏开发教程 / Pixi.js 完整项目:塔防游戏
完整项目:塔防游戏
从零构建一款经典塔防游戏(Tower Defense)。
一、项目架构设计
1.1 系统架构图
┌──────────────────────────────────────────────┐
│ Game │
│ ┌──────────┬───────────┬──────────────────┐ │
│ │ Scene │ Input │ Audio │ │
│ │ Manager │ Manager │ Manager │ │
│ ├──────────┴───────────┴──────────────────┤ │
│ │ GameScene (战斗场景) │ │
│ │ ┌──────────┬───────────┬─────────────┐ │ │
│ │ │ TileMap │ Path │ Wave │ │ │
│ │ │ (地图) │ Manager │ Manager │ │ │
│ │ ├──────────┼───────────┼─────────────┤ │ │
│ │ │ Tower │ Enemy │ Projectile │ │ │
│ │ │ Manager │ Manager │ Manager │ │ │
│ │ ├──────────┴───────────┴─────────────┤ │ │
│ │ │ Economy (金币/升级) │ │ │
│ │ ├────────────────────────────────────┤ │ │
│ │ │ HUD (生命/金币/波次) │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
1.2 核心模块清单
| 模块 | 职责 |
|---|
| TileMap | 地图网格、地块类型、路径标记 |
| PathManager | A* 寻路、路径缓存 |
| TowerManager | 建塔、升级、出售、攻击逻辑 |
| EnemyManager | 敌人生成、移动、状态管理 |
| WaveManager | 波次配置、倒计时、生成调度 |
| Economy | 金币管理、击杀奖励 |
| HUD | 顶部信息栏、底部建造面板 |
二、地图系统
2.1 TileMap 实现
// src/map/TileMap.ts
import { Container, Graphics, Text } from 'pixi.js';
export enum TileType {
Ground = 0, // 可建造
Path = 1, // 敌人路径
Blocked = 2, // 不可建造(装饰/障碍)
Spawn = 3, // 敌人出生点
Exit = 4, // 敌人终点(基地)
}
export class TileMap extends Container {
public readonly cols: number;
public readonly rows: number;
public readonly tileSize: number;
public grid: TileType[][];
private graphics: Graphics;
constructor(data: number[][], tileSize: number = 48) {
super();
this.tileSize = tileSize;
this.rows = data.length;
this.cols = data[0].length;
this.grid = data;
this.graphics = new Graphics();
this.addChild(this.graphics);
this.drawMap();
}
private drawMap() {
const g = this.graphics;
g.clear();
const colors: Record<TileType, number> = {
[TileType.Ground]: 0x2d5a27, // 绿色草地
[TileType.Path]: 0x8b7355, // 土路
[TileType.Blocked]: 0x555555, // 灰色障碍
[TileType.Spawn]: 0xcc3333, // 红色出生点
[TileType.Exit]: 0x3333cc, // 蓝色基地
};
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
const type = this.grid[row][col];
const x = col * this.tileSize;
const y = row * this.tileSize;
g.rect(x, y, this.tileSize, this.tileSize)
.fill({ color: colors[type] ?? 0x000000 });
// 网格线
g.rect(x, y, this.tileSize, this.tileSize)
.stroke({ color: 0x000000, alpha: 0.2, width: 1 });
}
}
}
// 获取世界坐标对应的网格坐标
worldToGrid(worldX: number, worldY: number): { col: number; row: number } {
return {
col: Math.floor(worldX / this.tileSize),
row: Math.floor(worldY / this.tileSize),
};
}
// 获取网格中心的世界坐标
gridToWorld(col: number, row: number): { x: number; y: number } {
return {
x: col * this.tileSize + this.tileSize / 2,
y: row * this.tileSize + this.tileSize / 2,
};
}
// 检查某格是否可建塔
canBuild(col: number, row: number): boolean {
if (col < 0 || col >= this.cols || row < 0 || row >= this.rows) return false;
return this.grid[row][col] === TileType.Ground;
}
// 设置地块类型(建塔后标记为 Blocked)
setTile(col: number, row: number, type: TileType) {
this.grid[row][col] = type;
this.drawMap();
}
// 获取出生点和终点
getSpawnPoint(): { col: number; row: number } {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if (this.grid[r][c] === TileType.Spawn) return { col: c, row: r };
}
}
return { col: 0, row: 0 };
}
getExitPoint(): { col: number; row: number } {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if (this.grid[r][c] === TileType.Exit) return { col: c, row: r };
}
}
return { col: this.cols - 1, row: this.rows - 1 };
}
}
2.2 关卡数据
// src/levels/level1.ts
// 0=Ground, 1=Path, 2=Blocked, 3=Spawn, 4=Exit
export const LEVEL_1_MAP = [
[2, 2, 2, 3, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 4, 2],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
];
三、A* 寻路算法
3.1 PathManager 实现
// src/pathfinding/PathManager.ts
import { TileMap, TileType } from '../map/TileMap';
interface Node {
col: number;
row: number;
g: number; // 从起点到当前的实际距离
h: number; // 启发式估价(到终点的估计距离)
f: number; // g + h
parent: Node | null;
}
export class PathManager {
private tileMap: TileMap;
private pathCache = new Map<string, { col: number; row: number }[]>();
constructor(tileMap: TileMap) {
this.tileMap = tileMap;
}
findPath(startCol: number, startRow: number, endCol: number, endRow: number): { col: number; row: number }[] {
const key = `${startCol},${startRow}-${endCol},${endRow}`;
if (this.pathCache.has(key)) {
return this.pathCache.get(key)!;
}
const path = this.aStar(startCol, startRow, endCol, endRow);
this.pathCache.set(key, path);
return path;
}
clearCache() {
this.pathCache.clear();
}
private aStar(startCol: number, startRow: number, endCol: number, endRow: number): { col: number; row: number }[] {
const openList: Node[] = [];
const closedSet = new Set<string>();
const startNode: Node = {
col: startCol, row: startRow,
g: 0, h: 0, f: 0, parent: null,
};
startNode.h = this.heuristic(startCol, startRow, endCol, endRow);
startNode.f = startNode.g + startNode.h;
openList.push(startNode);
const dirs = [
{ dc: 0, dr: -1 }, // 上
{ dc: 0, dr: 1 }, // 下
{ dc: -1, dr: 0 }, // 左
{ dc: 1, dr: 0 }, // 右
];
while (openList.length > 0) {
// 找 f 值最小的节点
openList.sort((a, b) => a.f - b.f);
const current = openList.shift()!;
// 到达终点
if (current.col === endCol && current.row === endRow) {
return this.buildPath(current);
}
const currentKey = `${current.col},${current.row}`;
if (closedSet.has(currentKey)) continue;
closedSet.add(currentKey);
// 检查相邻节点
for (const dir of dirs) {
const nc = current.col + dir.dc;
const nr = current.row + dir.dr;
// 边界检查
if (nc < 0 || nc >= this.tileMap.cols || nr < 0 || nr >= this.tileMap.rows) continue;
// 可通行检查(Path / Spawn / Exit 可以走)
const tile = this.tileMap.grid[nr][nc];
if (tile === TileType.Blocked || tile === TileType.Ground) continue;
const neighborKey = `${nc},${nr}`;
if (closedSet.has(neighborKey)) continue;
const g = current.g + 1;
const h = this.heuristic(nc, nr, endCol, endRow);
const node: Node = {
col: nc, row: nr,
g, h, f: g + h,
parent: current,
};
// 检查 openList 中是否有更优路径
const existing = openList.find(n => n.col === nc && n.row === nr);
if (existing && existing.g <= g) continue;
openList.push(node);
}
}
// 未找到路径
return [];
}
private heuristic(col1: number, row1: number, col2: number, row2: number): number {
return Math.abs(col1 - col2) + Math.abs(row1 - row2); // 曼哈顿距离
}
private buildPath(node: Node): { col: number; row: number }[] {
const path: { col: number; row: number }[] = [];
let current: Node | null = node;
while (current) {
path.unshift({ col: current.col, row: current.row });
current = current.parent;
}
return path;
}
}
四、防御塔类型
4.1 塔配置数据
// src/towers/TowerConfig.ts
export type TowerType = 'single' | 'area' | 'slow' | 'laser';
export interface TowerConfig {
name: string;
texture: string;
cost: number;
damage: number;
range: number; // 像素
fireRate: number; // 毫秒
splash: number; // 溅射半径,0 = 单体
slowFactor?: number; // 减速倍率
slowDuration?: number;// 减速持续时间 (ms)
description: string;
}
export const TOWER_CONFIGS: Record<TowerType, TowerConfig> = {
single: {
name: '箭塔',
texture: '/image/tower_arrow.png',
cost: 100,
damage: 25,
range: 150,
fireRate: 800,
splash: 0,
description: '单体高伤,攻速中等',
},
area: {
name: '炮塔',
texture: '/image/tower_cannon.png',
cost: 200,
damage: 40,
range: 120,
fireRate: 1500,
splash: 60,
description: '范围伤害,攻速慢',
},
slow: {
name: '冰塔',
texture: '/image/tower_ice.png',
cost: 150,
damage: 10,
range: 130,
fireRate: 1000,
splash: 0,
slowFactor: 0.5,
slowDuration: 2000,
description: '减速敌人 50%',
},
laser: {
name: '激光塔',
texture: '/image/tower_laser.png',
cost: 300,
damage: 8, // 每帧伤害
range: 180,
fireRate: 0, // 持续发射
splash: 0,
description: '持续激光,高 DPS',
},
};
4.2 Tower 类
// src/towers/Tower.ts
import { Sprite, Container, Graphics, Texture } from 'pixi.js';
import { TowerType, TowerConfig, TOWER_CONFIGS } from './TowerConfig';
import { Enemy } from '../enemies/Enemy';
export class Tower extends Container {
public type: TowerType;
public config: TowerConfig;
public gridCol: number;
public gridRow: number;
public level = 1;
public totalDamage = 0;
private sprite: Sprite;
private rangeCircle: Graphics;
private lastFireTime = 0;
private target: Enemy | null = null;
private angle = 0;
constructor(type: TowerType, col: number, row: number, worldX: number, worldY: number) {
super();
this.type = type;
this.config = { ...TOWER_CONFIGS[type] };
this.gridCol = col;
this.gridRow = row;
this.x = worldX;
this.y = worldY;
// 塔精灵
this.sprite = Sprite.from(this.config.texture);
this.sprite.anchor.set(0.5);
this.addChild(this.sprite);
// 范围指示器(默认隐藏)
this.rangeCircle = new Graphics();
this.rangeCircle.visible = false;
this.addChild(this.rangeCircle);
this.drawRange();
}
private drawRange() {
this.rangeCircle.clear();
this.rangeCircle.circle(0, 0, this.config.range);
this.rangeCircle.stroke({ color: 0xffffff, alpha: 0.3, width: 1 });
}
showRange(visible: boolean) {
this.rangeCircle.visible = visible;
}
// 升级
upgrade(): number {
const cost = Math.floor(this.config.cost * 0.7 * this.level);
this.level++;
this.config.damage = Math.floor(this.config.damage * 1.3);
this.config.range += 10;
this.drawRange();
return cost;
}
// 出售价格
getSellPrice(): number {
return Math.floor(this.config.cost * 0.6 * this.level);
}
// 找到范围内最近的敌人
findTarget(enemies: Enemy[]): Enemy | null {
let closest: Enemy | null = null;
let closestDist = Infinity;
for (const enemy of enemies) {
if (!enemy.active || enemy.isDead) continue;
const dx = enemy.x - this.x;
const dy = enemy.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= this.config.range && dist < closestDist) {
closest = enemy;
closestDist = dist;
}
}
return closest;
}
// 更新攻击逻辑,返回是否发射了子弹
update(delta: number, enemies: Enemy[], now: number): { fired: boolean; target: Enemy | null } {
this.target = this.findTarget(enemies);
if (!this.target) {
return { fired: false, target: null };
}
// 旋转朝向目标
this.angle = Math.atan2(this.target.y - this.y, this.target.x - this.x);
this.sprite.rotation = this.angle;
// 激光塔持续输出
if (this.type === 'laser') {
return { fired: true, target: this.target };
}
// 普通塔射击间隔检查
if (now - this.lastFireTime >= this.config.fireRate) {
this.lastFireTime = now;
return { fired: true, target: this.target };
}
return { fired: false, target: null };
}
}
五、敌人路径移动
5.1 Enemy 类
// src/enemies/Enemy.ts
import { Sprite, Container, Graphics, Text } from 'pixi.js';
import { TileMap } from '../map/TileMap';
export interface EnemyConfig {
name: string;
texture: string;
hp: number;
speed: number;
reward: number;
armor: number;
}
export const ENEMY_CONFIGS: Record<string, EnemyConfig> = {
slime: { name: '史莱姆', texture: '/image/enemy_slime.png', hp: 50, speed: 1.2, reward: 10, armor: 0 },
goblin: { name: '哥布林', texture: '/image/enemy_goblin.png', hp: 80, speed: 1.8, reward: 15, armor: 2 },
orc: { name: '兽人', texture: '/image/enemy_orc.png', hp: 200, speed: 0.8, reward: 30, armor: 5 },
dragon: { name: '龙', texture: '/image/enemy_dragon.png', hp: 500, speed: 0.6, reward: 100, armor: 10 },
};
export class Enemy extends Container {
public config: EnemyConfig;
public hp: number;
public maxHp: number;
public speed: number;
public currentSpeed: number;
public armor: number;
public reward: number;
public active = true;
public isDead = false;
private sprite: Sprite;
private hpBar: Graphics;
private path: { col: number; row: number }[] = [];
private pathIndex = 0;
private slowTimer = 0;
private tileMap: TileMap;
constructor(configName: string, tileMap: TileMap) {
super();
this.config = { ...ENEMY_CONFIGS[configName] };
this.tileMap = tileMap;
this.hp = this.config.hp;
this.maxHp = this.config.hp;
this.speed = this.config.speed;
this.currentSpeed = this.speed;
this.armor = this.config.armor;
this.reward = this.config.reward;
// 精灵
this.sprite = Sprite.from(this.config.texture);
this.sprite.anchor.set(0.5);
this.addChild(this.sprite);
// 血条
this.hpBar = new Graphics();
this.hpBar.y = -24;
this.addChild(this.hpBar);
}
setPath(path: { col: number; row: number }[]) {
this.path = path;
this.pathIndex = 0;
// 起始位置
if (path.length > 0) {
const pos = tileMap.gridToWorld(path[0].col, path[0].row);
this.x = pos.x;
this.y = pos.y;
}
}
update(delta: number) {
if (this.isDead || this.path.length === 0) return;
// 减速计时
if (this.slowTimer > 0) {
this.slowTimer -= delta * 16.67;
if (this.slowTimer <= 0) {
this.currentSpeed = this.speed;
}
}
// 沿路径移动
const target = this.path[this.pathIndex];
const targetPos = this.tileMap.gridToWorld(target.col, target.row);
const dx = targetPos.x - this.x;
const dy = targetPos.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < this.currentSpeed * delta) {
// 到达当前路径点
this.pathIndex++;
if (this.pathIndex >= this.path.length) {
this.active = false; // 到达终点
return;
}
} else {
// 朝目标移动
this.x += (dx / dist) * this.currentSpeed * delta;
this.y += (dy / dist) * this.currentSpeed * delta;
}
this.updateHpBar();
}
takeDamage(damage: number): boolean {
const effectiveDamage = Math.max(1, damage - this.armor);
this.hp -= effectiveDamage;
if (this.hp <= 0) {
this.hp = 0;
this.isDead = true;
this.active = false;
return true; // 已死亡
}
return false;
}
applySlow(factor: number, duration: number) {
this.currentSpeed = this.speed * factor;
this.slowTimer = duration;
// 减速视觉效果
this.sprite.tint = 0x88ccff;
}
private updateHpBar() {
this.hpBar.clear();
const w = 30;
const h = 4;
// 背景
this.hpBar.rect(-w / 2, 0, w, h).fill({ color: 0x333333 });
// 血量
const hpPercent = this.hp / this.maxHp;
const color = hpPercent > 0.5 ? 0x00ff00 : hpPercent > 0.25 ? 0xffaa00 : 0xff0000;
this.hpBar.rect(-w / 2, 0, w * hpPercent, h).fill({ color });
}
}
六、波次系统(WaveManager)
// src/systems/WaveManager.ts
export interface WaveEntry {
type: string;
count: number;
interval: number; // 每个敌人间隔 (ms)
startDelay: number; // 本组开始延迟 (ms)
}
export interface WaveConfig {
entries: WaveEntry[];
reward: number; // 通关奖励
}
const WAVE_DATA: WaveConfig[] = [
{
reward: 50,
entries: [
{ type: 'slime', count: 5, interval: 1500, startDelay: 0 },
],
},
{
reward: 80,
entries: [
{ type: 'slime', count: 8, interval: 1200, startDelay: 0 },
{ type: 'goblin', count: 3, interval: 2000, startDelay: 5000 },
],
},
{
reward: 120,
entries: [
{ type: 'goblin', count: 10, interval: 1000, startDelay: 0 },
{ type: 'slime', count: 5, interval: 800, startDelay: 3000 },
],
},
{
reward: 200,
entries: [
{ type: 'orc', count: 3, interval: 3000, startDelay: 0 },
{ type: 'goblin', count: 8, interval: 1000, startDelay: 2000 },
{ type: 'slime', count: 10, interval: 600, startDelay: 8000 },
],
},
{
reward: 500,
entries: [
{ type: 'dragon', count: 1, interval: 0, startDelay: 0 },
{ type: 'orc', count: 5, interval: 2000, startDelay: 5000 },
],
},
];
export class WaveManager {
private currentWave = 0;
private waveStarted = false;
private waveClear = false;
private spawnTimers: { entry: WaveEntry; spawned: number; timer: number }[] = [];
private countdown = 10000; // 首波倒计时 10 秒
get waveNumber() { return this.currentWave + 1; }
get totalWaves() { return WAVE_DATA.length; }
get isComplete() { return this.currentWave >= WAVE_DATA.length; }
get isWaveClear() { return this.waveClear; }
startWave() {
if (this.currentWave >= WAVE_DATA.length) return;
const wave = WAVE_DATA[this.currentWave];
this.spawnTimers = wave.entries.map(entry => ({
entry,
spawned: 0,
timer: entry.startDelay,
}));
this.waveStarted = true;
this.waveClear = false;
}
update(delta: number, onSpawn: (type: string) => void) {
if (!this.waveStarted) {
this.countdown -= delta * 16.67;
if (this.countdown <= 0) {
this.startWave();
}
return;
}
// 检查是否所有敌人都已生成
let allDone = true;
for (const st of this.spawnTimers) {
if (st.spawned < st.entry.count) {
allDone = false;
st.timer -= delta * 16.67;
if (st.timer <= 0) {
onSpawn(st.entry.type);
st.spawned++;
st.timer = st.entry.interval;
}
}
}
if (allDone) {
this.waveClear = true;
}
}
nextWave() {
this.currentWave++;
this.waveStarted = false;
this.waveClear = false;
this.countdown = 8000;
}
get reward(): number {
return WAVE_DATA[this.currentWave]?.reward ?? 0;
}
get countdownSeconds(): number {
return Math.ceil(this.countdown / 1000);
}
}
七、金币与升级系统
// src/economy/Economy.ts
export class Economy {
private _gold: number;
private _score: number;
private listeners: ((gold: number, score: number) => void)[] = [];
constructor(startGold: number = 200) {
this._gold = startGold;
this._score = 0;
}
get gold() { return this._gold; }
get score() { return this._score; }
addGold(amount: number) {
this._gold += amount;
this.notify();
}
spendGold(amount: number): boolean {
if (this._gold < amount) return false;
this._gold -= amount;
this.notify();
return true;
}
addScore(amount: number) {
this._score += amount;
this.notify();
}
canAfford(amount: number): boolean {
return this._gold >= amount;
}
onChange(listener: (gold: number, score: number) => void) {
this.listeners.push(listener);
}
private notify() {
this.listeners.forEach(fn => fn(this._gold, this._score));
}
}
八、塔建造与出售 UI
8.1 建造面板
// src/ui/BuildPanel.ts
import { Container, Graphics, Text, Sprite } from 'pixi.js';
import { TOWER_CONFIGS, TowerType } from '../towers/TowerConfig';
export class BuildPanel extends Container {
private buttons: { type: TowerType; bg: Graphics; text: Text; cost: Text }[] = [];
private selectedType: TowerType | null = null;
private onSelect: (type: TowerType | null) => void;
constructor(panelY: number, onSelect: (type: TowerType | null) => void) {
super();
this.onSelect = onSelect;
// 背景
const bg = new Graphics();
bg.rect(0, panelY, 800, 80).fill({ color: 0x222222, alpha: 0.9 });
this.addChild(bg);
// 标题
const title = new Text({
text: '建造防御塔',
style: { fill: 0xffffff, fontSize: 14 },
});
title.x = 16;
title.y = panelY + 4;
this.addChild(title);
// 塔按钮
const types: TowerType[] = ['single', 'area', 'slow', 'laser'];
types.forEach((type, i) => {
const config = TOWER_CONFIGS[type];
const btnBg = new Graphics();
const btnX = 16 + i * 130;
const btnY = panelY + 24;
btnBg.roundRect(btnX, btnY, 120, 48, 6)
.fill({ color: 0x444444 })
.stroke({ color: 0x666666, width: 1 });
const nameText = new Text({
text: config.name,
style: { fill: 0xffffff, fontSize: 14 },
});
nameText.x = btnX + 8;
nameText.y = btnY + 4;
const costText = new Text({
text: `💰 ${config.cost}`,
style: { fill: 0xffdd44, fontSize: 12 },
});
costText.x = btnX + 8;
costText.y = btnY + 24;
btnBg.eventMode = 'static';
btnBg.cursor = 'pointer';
btnBg.on('pointertap', () => this.toggleSelect(type));
this.addChild(btnBg, nameText, costText);
this.buttons.push({ type, bg: btnBg, text: nameText, cost: costText });
});
}
private toggleSelect(type: TowerType) {
if (this.selectedType === type) {
this.selectedType = null;
} else {
this.selectedType = type;
}
this.updateHighlight();
this.onSelect(this.selectedType);
}
private updateHighlight() {
for (const btn of this.buttons) {
const selected = btn.type === this.selectedType;
btn.bg.clear();
btn.bg.roundRect(btn.bg.x ?? 0, btn.bg.y ?? 0, 120, 48, 6)
.fill({ color: selected ? 0x557755 : 0x444444 })
.stroke({ color: selected ? 0x88ff88 : 0x666666, width: 2 });
}
}
getSelectedType(): TowerType | null {
return this.selectedType;
}
}
8.2 塔操作菜单
// src/ui/TowerMenu.ts
import { Container, Graphics, Text } from 'pixi.js';
import { Tower } from '../towers/Tower';
export class TowerMenu extends Container {
private tower: Tower | null = null;
show(tower: Tower, onUpgrade: () => void, onSell: () => void) {
this.removeChildren();
this.tower = tower;
const bg = new Graphics();
bg.roundRect(0, 0, 160, 90, 8)
.fill({ color: 0x333333, alpha: 0.95 })
.stroke({ color: 0x555555, width: 1 });
this.addChild(bg);
// 塔信息
const info = new Text({
text: `${tower.config.name} Lv.${tower.level}\n伤害: ${tower.config.damage}\n范围: ${tower.config.range}`,
style: { fill: 0xffffff, fontSize: 12, lineHeight: 16 },
});
info.x = 8;
info.y = 4;
this.addChild(info);
// 升级按钮
const upgradeCost = Math.floor(tower.config.cost * 0.7 * tower.level);
const upgradeBtn = this.createButton(8, 52, `⬆ 升级 💰${upgradeCost}`, 0x447744, onUpgrade);
this.addChild(upgradeBtn);
// 出售按钮
const sellPrice = tower.getSellPrice();
const sellBtn = this.createButton(82, 52, `💰${sellPrice}`, 0x774444, onSell);
this.addChild(sellBtn);
this.x = tower.x + 30;
this.y = tower.y - 45;
}
private createButton(x: number, y: number, label: string, color: number, onClick: () => void): Graphics {
const btn = new Graphics();
btn.roundRect(x, y, 70, 24, 4).fill({ color });
btn.eventMode = 'static';
btn.cursor = 'pointer';
btn.on('pointertap', onClick);
const text = new Text({ text: label, style: { fill: 0xffffff, fontSize: 10 } });
text.x = x + 4;
text.y = y + 4;
btn.addChild(text);
return btn;
}
hide() {
this.removeChildren();
this.tower = null;
}
}
九、粒子效果
// src/effects/ParticleEffects.ts
import { Container, Graphics } from 'pixi.js';
interface Particle {
x: number; y: number;
vx: number; vy: number;
life: number; maxLife: number;
size: number; color: number;
}
export class ParticleSystem extends Container {
private particles: Particle[] = [];
private graphics: Graphics;
constructor() {
super();
this.graphics = new Graphics();
this.addChild(this.graphics);
}
// 击杀爆炸
emitExplosion(x: number, y: number, color: number = 0xff6644) {
for (let i = 0; i < 12; i++) {
const angle = (Math.PI * 2 * i) / 12 + Math.random() * 0.3;
const speed = 1 + Math.random() * 3;
this.particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1, maxLife: 1,
size: 2 + Math.random() * 3,
color,
});
}
}
// 金币飘字
emitGoldText(x: number, y: number, amount: number) {
// 简化版:向上飘的粒子
for (let i = 0; i < 5; i++) {
this.particles.push({
x: x + Math.random() * 10 - 5,
y,
vx: Math.random() - 0.5,
vy: -1 - Math.random(),
life: 1, maxLife: 1,
size: 2,
color: 0xffdd44,
});
}
}
update(delta: number) {
this.graphics.clear();
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.x += p.vx * delta;
p.y += p.vy * delta;
p.life -= 0.02 * delta;
if (p.life <= 0) {
this.particles.splice(i, 1);
continue;
}
const alpha = p.life / p.maxLife;
this.graphics.circle(p.x, p.y, p.size * alpha);
this.graphics.fill({ color: p.color, alpha });
}
}
}
十、难度平衡
10.1 难度参数
// src/difficulty/DifficultyManager.ts
export interface DifficultySettings {
enemyHpMultiplier: number;
enemySpeedMultiplier: number;
enemyCountMultiplier: number;
rewardMultiplier: number;
towerCostMultiplier: number;
}
const DIFFICULTY_PRESETS: Record<string, DifficultySettings> = {
easy: {
enemyHpMultiplier: 0.7,
enemySpeedMultiplier: 0.8,
enemyCountMultiplier: 0.8,
rewardMultiplier: 1.3,
towerCostMultiplier: 0.8,
},
normal: {
enemyHpMultiplier: 1.0,
enemySpeedMultiplier: 1.0,
enemyCountMultiplier: 1.0,
rewardMultiplier: 1.0,
towerCostMultiplier: 1.0,
},
hard: {
enemyHpMultiplier: 1.5,
enemySpeedMultiplier: 1.2,
enemyCountMultiplier: 1.3,
rewardMultiplier: 0.8,
towerCostMultiplier: 1.2,
},
};
export function getDifficultySettings(level: string): DifficultySettings {
return DIFFICULTY_PRESETS[level] ?? DIFFICULTY_PRESETS.normal;
}
10.2 平衡性参考表
| 参数 | Easy | Normal | Hard |
|---|
| 敌人 HP | ×0.7 | ×1.0 | ×1.5 |
| 敌人速度 | ×0.8 | ×1.0 | ×1.2 |
| 敌人数量 | ×0.8 | ×1.0 | ×1.3 |
| 击杀奖励 | ×1.3 | ×1.0 | ×0.8 |
| 建塔费用 | ×0.8 | ×1.0 | ×1.2 |
十一、胜利 / 失败条件
// src/GameState.ts
export class GameState {
public lives = 20; // 基地生命
public maxLives = 20;
public gameOver = false;
public victory = false;
loseLife(count: number = 1) {
this.lives = Math.max(0, this.lives - count);
if (this.lives <= 0) {
this.gameOver = true;
}
}
checkVictory(allWavesComplete: boolean, enemiesAlive: number) {
if (allWavesComplete && enemiesAlive === 0) {
this.victory = true;
}
}
get isFinished(): boolean {
return this.gameOver || this.victory;
}
}
十二、完整 GameScene 整合
// src/scenes/GameScene.ts
import { Container } from 'pixi.js';
import { Game } from '../core/Game';
import { TileMap } from '../map/TileMap';
import { PathManager } from '../pathfinding/PathManager';
import { Tower, TowerType } from '../towers/Tower';
import { Enemy } from '../enemies/Enemy';
import { WaveManager } from '../systems/WaveManager';
import { Economy } from '../economy/Economy';
import { BuildPanel } from '../ui/BuildPanel';
import { TowerMenu } from '../ui/TowerMenu';
import { ParticleSystem } from '../effects/ParticleEffects';
import { GameState } from '../GameState';
import { LEVEL_1_MAP } from '../levels/level1';
export class GameScene {
container = new Container();
private game: Game;
private tileMap!: TileMap;
private pathManager!: PathManager;
private towers: Tower[] = [];
private enemies: Enemy[] = [];
private waveManager!: WaveManager;
private economy!: Economy;
private gameState!: GameState;
private buildPanel!: BuildPanel;
private towerMenu!: TowerMenu;
private particles!: ParticleSystem;
private selectedTowerType: TowerType | null = null;
private enemyContainer = new Container();
private towerContainer = new Container();
private projectileGraphics = new Container();
constructor(game: Game) {
this.game = game;
}
async init() {
// 地图
this.tileMap = new TileMap(LEVEL_1_MAP, 48);
this.container.addChild(this.tileMap);
// 寻路
this.pathManager = new PathManager(this.tileMap);
// 容器层级
this.container.addChild(this.towerContainer);
this.container.addChild(this.enemyContainer);
this.container.addChild(this.projectileGraphics);
// 粒子
this.particles = new ParticleSystem();
this.container.addChild(this.particles);
// 系统
this.waveManager = new WaveManager();
this.economy = new Economy(200);
this.gameState = new GameState();
// UI
this.buildPanel = new BuildPanel(480, (type) => {
this.selectedTowerType = type;
});
this.container.addChild(this.buildPanel);
this.towerMenu = new TowerMenu();
this.container.addChild(this.towerMenu);
// 点击地图建造
this.tileMap.eventMode = 'static';
this.tileMap.on('pointertap', (e) => this.onMapClick(e));
// 音效
this.game.audio.load('build', '/audio/build.mp3');
this.game.audio.load('shoot', '/audio/shoot.mp3');
this.game.audio.load('enemy_die', '/audio/enemy_die.mp3');
}
update(delta: number) {
if (this.gameState.isFinished) return;
const now = performance.now();
// 波次更新
this.waveManager.update(delta, (type) => this.spawnEnemy(type));
// 敌人更新
for (const enemy of this.enemies) {
if (enemy.active) {
enemy.update(delta);
// 到达终点
if (!enemy.active && !enemy.isDead) {
this.gameState.loseLife();
this.removeEnemy(enemy);
}
}
}
// 塔攻击
for (const tower of this.towers) {
const result = tower.update(delta, this.enemies, now);
if (result.fired && result.target) {
this.handleTowerAttack(tower, result.target);
}
}
// 粒子更新
this.particles.update(delta);
// 清理死亡敌人
for (let i = this.enemies.length - 1; i >= 0; i--) {
if (this.enemies[i].isDead) {
const enemy = this.enemies[i];
this.economy.addGold(enemy.reward);
this.economy.addScore(enemy.reward);
this.particles.emitExplosion(enemy.x, enemy.y);
this.game.audio.play('enemy_die');
this.removeEnemy(enemy);
}
}
// 波次完成检查
if (this.waveManager.isWaveClear && this.enemies.length === 0) {
this.economy.addGold(this.waveManager.reward);
this.waveManager.nextWave();
}
// 胜利检查
this.gameState.checkVictory(
this.waveManager.isComplete,
this.enemies.filter(e => e.active).length,
);
if (this.gameState.gameOver) {
console.log('Game Over!');
}
if (this.gameState.victory) {
console.log('Victory!');
}
}
private onMapClick(event: any) {
const { col, row } = this.tileMap.worldToGrid(event.global.x, event.global.y);
// 如果已有塔,显示操作菜单
const existingTower = this.towers.find(t => t.gridCol === col && t.gridRow === row);
if (existingTower) {
this.towerMenu.show(existingTower,
() => this.upgradeTower(existingTower),
() => this.sellTower(existingTower),
);
return;
}
// 建造新塔
if (this.selectedTowerType && this.tileMap.canBuild(col, row)) {
this.buildTower(this.selectedTowerType, col, row);
}
}
private buildTower(type: TowerType, col: number, row: number) {
const config = TOWER_CONFIGS[type];
if (!this.economy.spendGold(config.cost)) return;
const pos = this.tileMap.gridToWorld(col, row);
const tower = new Tower(type, col, row, pos.x, pos.y);
this.towerContainer.addChild(tower);
this.towers.push(tower);
this.tileMap.setTile(col, row, 2); // 标记为不可建造
// 重新计算路径(建塔可能阻断路径)
this.pathManager.clearCache();
this.repathAllEnemies();
this.game.audio.play('build');
}
private upgradeTower(tower: Tower) {
const cost = Math.floor(tower.config.cost * 0.7 * tower.level);
if (this.economy.spendGold(cost)) {
tower.upgrade();
this.towerMenu.show(tower,
() => this.upgradeTower(tower),
() => this.sellTower(tower),
);
}
}
private sellTower(tower: Tower) {
this.economy.addGold(tower.getSellPrice());
this.tileMap.setTile(tower.gridCol, tower.gridRow, 0); // 恢复可建造
this.towerContainer.removeChild(tower);
this.towers = this.towers.filter(t => t !== tower);
this.towerMenu.hide();
this.pathManager.clearCache();
}
private handleTowerAttack(tower: Tower, target: Enemy) {
if (tower.type === 'laser') {
// 激光直接扣血
target.takeDamage(tower.config.damage);
tower.totalDamage += tower.config.damage;
// 绘制激光线
this.drawLaser(tower.x, tower.y, target.x, target.y);
} else {
// 发射弹丸
this.spawnProjectile(tower, target);
}
// 减速效果
if (tower.config.slowFactor) {
target.applySlow(tower.config.slowFactor, tower.config.slowDuration ?? 2000);
}
}
private drawLaser(x1: number, y1: number, x2: number, y2: number) {
// 简化实现:用 Graphics 画线
// 每帧清除重建,实际项目可用 RenderTexture 优化
}
private spawnProjectile(tower: Tower, target: Enemy) {
// 简化实现:直接对目标造成伤害
const killed = target.takeDamage(tower.config.damage);
tower.totalDamage += tower.config.damage;
// 溅射伤害
if (tower.config.splash > 0) {
for (const enemy of this.enemies) {
if (enemy === target || !enemy.active) continue;
const dx = enemy.x - target.x;
const dy = enemy.y - target.y;
if (Math.sqrt(dx * dx + dy * dy) < tower.config.splash) {
enemy.takeDamage(Math.floor(tower.config.damage * 0.5));
}
}
}
}
private spawnEnemy(type: string) {
const spawn = this.tileMap.getSpawnPoint();
const exit = this.tileMap.getExitPoint();
const path = this.pathManager.findPath(spawn.col, spawn.row, exit.col, exit.row);
if (path.length === 0) {
console.warn('无法生成敌人:无可用路径');
return;
}
const enemy = new Enemy(type, this.tileMap);
enemy.setPath(path);
this.enemyContainer.addChild(enemy);
this.enemies.push(enemy);
}
private removeEnemy(enemy: Enemy) {
this.enemyContainer.removeChild(enemy);
this.enemies = this.enemies.filter(e => e !== enemy);
}
private repathAllEnemies() {
const spawn = this.tileMap.getSpawnPoint();
const exit = this.tileMap.getExitPoint();
for (const enemy of this.enemies) {
if (enemy.active && !enemy.isDead) {
const currentGrid = this.tileMap.worldToGrid(enemy.x, enemy.y);
const newPath = this.pathManager.findPath(
currentGrid.col, currentGrid.row,
exit.col, exit.row,
);
if (newPath.length > 0) {
enemy.setPath(newPath);
}
}
}
}
destroy() {
this.container.removeChildren();
this.game.audio.stopAll();
}
}
十三、完整游戏流程总结
启动
│
├─ 加载资源 (Assets.load)
├─ 初始化场景 (GameScene.init)
│
├─ 游戏循环 (GameScene.update)
│ ├─ 波次调度 (WaveManager)
│ │ ├─ 倒计时 → 生成敌人
│ │ └─ 波次完成 → 发放奖励 → 下一波
│ │
│ ├─ 敌人移动 (Enemy.update)
│ │ └─ 到达终点 → 扣生命
│ │
│ ├─ 防御塔攻击 (Tower.update)
│ │ ├─ 寻找目标 (范围内最近)
│ │ ├─ 发射弹丸 / 激光
│ │ └─ 溅射 / 减速效果
│ │
│ ├─ 碰撞检测 / 伤害计算
│ ├─ 粒子效果更新
│ └─ UI 更新 (金币/生命/波次)
│
├─ 建造交互
│ ├─ 选择塔类型 (BuildPanel)
│ ├─ 点击空地建造
│ ├─ 点击已有塔 → 升级/出售菜单
│ └─ 建塔后重新寻路
│
└─ 结束
├─ 生命归零 → Game Over
└─ 所有波次清除 → Victory
扩展阅读