强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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地图网格、地块类型、路径标记
PathManagerA* 寻路、路径缓存
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 平衡性参考表

参数EasyNormalHard
敌人 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

扩展阅读