【Cocos Creator】幽灵射手项目讲义 - 第二章:框架层核心模块

🎯 幽灵射手系列 · 全目录

文章目录

      • [🎯 幽灵射手系列 · 全目录](#🎯 幽灵射手系列 · 全目录)
    • 概述
    • [2.1 Constant(常量定义)](#2.1 Constant(常量定义))
      • [2.1.1 功能定位](#2.1.1 功能定位)
      • [2.1.2 常量分类](#2.1.2 常量分类)
        • [2.1.2.1 游戏基础配置](#2.1.2.1 游戏基础配置)
        • [2.1.2.2 本地缓存键](#2.1.2.2 本地缓存键)
        • [2.1.2.3 动画类型](#2.1.2.3 动画类型)
        • [2.1.2.4 碰撞分组](#2.1.2.4 碰撞分组)
        • [2.1.2.5 事件类型](#2.1.2.5 事件类型)
        • [2.1.2.6 玩家技能定义](#2.1.2.6 玩家技能定义)
        • [2.1.2.7 怪物技能定义](#2.1.2.7 怪物技能定义)
      • [2.1.3 使用示例](#2.1.3 使用示例)
    • [2.2 ClientEvent(事件系统)](#2.2 ClientEvent(事件系统))
      • [2.2.1 设计理念](#2.2.1 设计理念)
      • [2.2.2 核心实现](#2.2.2 核心实现)
      • [2.2.3 使用模式](#2.2.3 使用模式)
    • [2.3 PlayerData(玩家数据管理)](#2.3 PlayerData(玩家数据管理))
      • [2.3.1 数据结构](#2.3.1 数据结构)
      • [2.3.2 核心方法](#2.3.2 核心方法)
        • [2.3.2.1 数据加载](#2.3.2.1 数据加载)
        • [2.3.2.2 数据保存](#2.3.2.2 数据保存)
        • [2.3.2.3 数据更新](#2.3.2.3 数据更新)
        • [2.3.2.4 技能管理](#2.3.2.4 技能管理)
    • [2.4 UIManager(界面管理)](#2.4 UIManager(界面管理))
      • [2.4.1 设计架构](#2.4.1 设计架构)
      • [2.4.2 核心方法](#2.4.2 核心方法)
        • [2.4.2.1 显示单例面板](#2.4.2.1 显示单例面板)
        • [2.4.2.2 隐藏面板](#2.4.2.2 隐藏面板)
        • [2.4.2.3 弹窗队列管理](#2.4.2.3 弹窗队列管理)
        • [2.4.2.4 血条显示](#2.4.2.4 血条显示)
    • [2.5 ResourceUtil(资源管理)](#2.5 ResourceUtil(资源管理))
      • [2.5.1 资源路径约定](#2.5.1 资源路径约定)
      • [2.5.2 核心方法](#2.5.2 核心方法)
      • [2.5.3 资源加载策略](#2.5.3 资源加载策略)
    • [2.6 AudioManager(音频管理)](#2.6 AudioManager(音频管理))
      • [2.6.1 架构设计](#2.6.1 架构设计)
      • [2.6.2 核心方法](#2.6.2 核心方法)
        • [2.6.2.1 播放背景音乐](#2.6.2.1 播放背景音乐)
        • [2.6.2.2 播放音效](#2.6.2.2 播放音效)
        • [2.6.2.3 音量控制](#2.6.2.3 音量控制)
        • [2.6.2.4 开关控制](#2.6.2.4 开关控制)
    • [2.7 框架模块协作关系](#2.7 框架模块协作关系)
    • 总结

概述

框架层是游戏的基础设施,提供全局管理、事件通信、资源加载等核心功能。本章详细讲解各个框架模块的设计和使用方式。


2.1 Constant(常量定义)

2.1.1 功能定位

Constant 类集中管理游戏中所有常量配置,避免魔法数字和硬编码字符串,提高代码可维护性。

2.1.2 常量分类

2.1.2.1 游戏基础配置
typescript 复制代码
public static GAME_NAME = 'archero';      // 游戏英文名称
public static GAME_VERSION = '1.0.1';     // 游戏版本
public static GAME_FRAME = 60;            // 游戏帧率
public static GAME_NAME_CH = "幽灵射手";   // 游戏中文名称
2.1.2.2 本地缓存键
typescript 复制代码
public static LOCAL_CACHE = {
    PLAYER: 'player',           // 玩家基础数据
    SETTINGS: 'settings',       // 设置配置
    DATA_VERSION: 'dataVersion',// 数据版本
    ACCOUNT: 'account',         // 玩家账号
    HISTORY: "history",         // 关卡通关数据
    BAG: "bag",                 // 玩家背包
}
2.1.2.3 动画类型
typescript 复制代码
// 玩家动画
public static PLAYER_ANI_TYPE = {
    IDLE: "idle",       // 待机
    RUN: "run",         // 奔跑
    ATTACK: "attack",   // 攻击
    DIE: "die",         // 死亡
    REVIVE: "revive",   // 复活
}

// 怪物动画
public static MONSTER_ANI_TYPE = {
    IDLE: "idle",
    RUN: "run",
    ATTACK: "attack",
    DIE: "die",
    HIT: "hit",
    ATTACK_1: "attack1",  // hellFire独有
    ATTACK_2: "attack2",  // hellFire独有
}
2.1.2.4 碰撞分组
typescript 复制代码
public static PHY_GROUP = {
    DEFAULT: 1 << 0,                       // 默认分组
    PLAYER: 1 << 1,                        // 玩家
    COLLIDER_ITEM: 1 << 2,                 // 碰撞器
    MONSTER: 1 << 3,                       // 小怪
    REWARD: 1 << 4,                        // 奖品(金币、爱心)
    MONSTER_SKILL_COLLIDER: 1 << 5,        // 怪物技能
    OBSTACLE: 1 << 6,                      // 障碍物
}

位运算原理:使用位掩码实现碰撞分组,每个分组占用一个二进制位,可以高效地进行碰撞检测过滤。

2.1.2.5 事件类型
typescript 复制代码
public static EVENT_TYPE = {
    ATTACK_PLAYER: "attackPlayer",         // 攻击玩家
    ON_GAME_INIT: "onInitGame",            // 游戏初始化
    ON_GAME_OVER: "onGameOver",            // 游戏结束
    ON_GAME_PAUSE: "onGamePause",          // 游戏暂停
    ON_REVIVE: "onRevive",                 // 玩家复活
    REFRESH_GOLD: "refreshGold",           // 更新金币
    REFRESH_DIAMOND: "refreshDiamond",     // 更新钻石
    REFRESH_LEVEL: "refreshLevel",         // 刷新关卡
    INHALE_REWARD: "inhaleReward",         // 吸入奖品
    // ... 更多事件
}
2.1.2.6 玩家技能定义
typescript 复制代码
public static PLAYER_SKILL = {
    // 形态技能
    ARROW_DOUBLE: "10101",         // 双重射击
    ARROW_CONTINUOUS: "10201",     // 连续射击
    ARROW_UMBRELLA: "10301",       // 伞型射击
    ARROW_REVERSE: "10401",        // 反向射击
    ARROW_SIDE: "10501",           // 侧面射击
    ARROW_PENETRATE: "10601",      // 穿透
    
    // 数值技能
    RAISE_ATTACK_01: "20101",      // 攻击力提升1
    RAISE_ATTACK_02: "20102",      // 攻击力提升2
    RAISE_DODGE: "20201",          // 闪避率提升
    RAISE_CRITICAL_HIT_DAMAGE_01: "20301",  // 暴击提升1
    RAISE_CRITICAL_HIT_DAMAGE_02: "20302",  // 暴击提升2
    RAISE_ATTACK_SPEED_01: "20401",         // 攻速提升1
    RAISE_ATTACK_SPEED_02: "20402",         // 攻速提升2
    RAISE_HP_LIMIT: "20501",       // 生命上限提升
    RECOVERY: "20601",             // 生命回复
    MOVE_SPEED: "20701",           // 移动速度提升
    
    // Buff技能
    ARROW_ICE: "30101",            // 冰冻
    ARROW_FIRE: "30201",           // 灼烧
    
    // 触发技能
    ARROW_LIGHTNING: "40101",      // 闪电链
    BLOODTHIRSTY: "40201",         // 嗜血
    ARROW_LAUNCH: "40301",         // 弹射
}

技能ID编码规则

  • 第一位:技能类型(1-形态,2-数值,3-Buff,4-触发)
  • 第二、三位:技能组
  • 第四、五位:技能编号
2.1.2.7 怪物技能定义
typescript 复制代码
public static MONSTER_SKILL = {
    ENERGY_BALL: "101",            // 能量球
    FIRE_BALL: "102",              // 小火球
    JET_FIRES: "103",              // 直线火焰
    DISPERSION: "104",             // 180度散射
    TORNADO: "105",                // 龙卷风
    FIRE_BALL_BIG: "106",          // 大火球
    DISPERSION_SURROUND: "107",    // 360度六角散射
    LASER: "108",                  // 激光
}

2.1.3 使用示例

typescript 复制代码
// 监听事件
ClientEvent.on(Constant.EVENT_TYPE.ON_GAME_OVER, this._onGameOver, this);

// 设置碰撞分组
this.colliderCom.setGroup(Constant.PHY_GROUP.PLAYER);

// 判断技能
if (arrSkill.includes(Constant.PLAYER_SKILL.ARROW_DOUBLE)) {
    // 处理双重射击技能
}

2.2 ClientEvent(事件系统)

2.2.1 设计理念

基于观察者模式实现的事件系统,实现组件间的解耦通信。

2.2.2 核心实现

typescript 复制代码
@ccclass("ClientEvent")
export class ClientEvent {
    private static _handlers: { [key: string]: any[] } = {};  // 事件处理器映射
    
    /**
     * 监听事件
     * @param eventName 事件名称
     * @param handler 回调函数
     * @param target 回调目标(用于this绑定和取消监听)
     */
    public static on(eventName: string, handler: Function, target: any) {
        let objHandler = { handler, target };
        let handlerList = ClientEvent._handlers[eventName];
        
        if (!handlerList) {
            handlerList = [];
            ClientEvent._handlers[eventName] = handlerList;
        }
        
        handlerList.push(objHandler);
    }
    
    /**
     * 取消监听
     */
    public static off(eventName: string, handler: Function, target: any) {
        let handlerList = ClientEvent._handlers[eventName];
        if (!handlerList) return;
        
        for (let i = 0; i < handlerList.length; i++) {
            let oldObj = handlerList[i];
            if (oldObj.handler === handler && (!target || target === oldObj.target)) {
                handlerList.splice(i, 1);
                break;
            }
        }
    }
    
    /**
     * 分发事件
     */
    public static dispatchEvent(eventName: string, ...args: any) {
        let handlerList = ClientEvent._handlers[eventName];
        if (!handlerList) return;
        
        for (let i = 0; i < handlerList.length; i++) {
            let objHandler = handlerList[i];
            if (objHandler.handler) {
                objHandler.handler.apply(objHandler.target, args);
            }
        }
    }
}

2.2.3 使用模式

模式1:组件间通信

typescript 复制代码
// 组件A(发布者)
class ComponentA {
    someAction() {
        // 触发事件
        ClientEvent.dispatchEvent(Constant.EVENT_TYPE.REFRESH_GOLD);
    }
}

// 组件B(订阅者)
class ComponentB {
    onEnable() {
        // 监听事件
        ClientEvent.on(Constant.EVENT_TYPE.REFRESH_GOLD, this._onRefreshGold, this);
    }
    
    onDisable() {
        // 取消监听
        ClientEvent.off(Constant.EVENT_TYPE.REFRESH_GOLD, this._onRefreshGold, this);
    }
    
    private _onRefreshGold() {
        // 更新金币显示
        this.updateGoldDisplay();
    }
}

模式2:带参数传递

typescript 复制代码
// 发布事件(带参数)
ClientEvent.dispatchEvent(Constant.EVENT_TYPE.SHOW_BLOOD_TIPS, type, value, position);

// 监听事件
ClientEvent.on(Constant.EVENT_TYPE.SHOW_BLOOD_TIPS, this._onShowBloodTips, this);

// 处理事件
private _onShowBloodTips(type: number, value: number, position: Vec3) {
    // 根据参数显示血量提示
}

模式3:回调函数传递

typescript 复制代码
// 发布事件(传递回调)
ClientEvent.dispatchEvent(Constant.EVENT_TYPE.HIDE_LOADING_PANEL, () => {
    console.log("加载完成");
});

// 处理事件
private _onHideLoadingPanel(callback?: Function) {
    this.hidePanel();
    callback && callback();
}

2.3 PlayerData(玩家数据管理)

2.3.1 数据结构

typescript 复制代码
playerInfo = {
    diamond: 0,           // 钻石数量
    gold: 0,              // 金币数量
    key: 0,               // 钥匙数量
    level: 1,             // 当前关卡
    highestLevel: 1,      // 最高通关关卡
    arrSkill: [],         // 已解锁技能ID数组
    createDate: Date,     // 创建时间
}

settings = {
    fightTimes: 0,        // 战斗次数
    hideTime: 0,          // 离线时间
}

2.3.2 核心方法

2.3.2.1 数据加载
typescript 复制代码
/**
 * 加载全局缓存(用户ID等)
 */
public loadGlobalCache() {
    let userId = StorageManager.instance.getUserId();
    if (userId) {
        this._userId = userId;
    }
}

/**
 * 加载玩家数据
 */
public loadFromCache() {
    this._playerInfo = this._loadDataByKey(Constant.LOCAL_CACHE.PLAYER);
    this._history = this._loadDataByKey(Constant.LOCAL_CACHE.HISTORY);
    this._settings = this._loadDataByKey(Constant.LOCAL_CACHE.SETTINGS);
}

private _loadDataByKey(keyName: string) {
    let ret = {};
    let str = StorageManager.instance.getConfigData(keyName);
    if (str) {
        try {
            ret = JSON.parse(str);
        } catch (e) {
            ret = {};
        }
    }
    return ret;
}
2.3.2.2 数据保存
typescript 复制代码
/**
 * 保存玩家数据到本地缓存
 */
public savePlayerInfoToLocalCache() {
    StorageManager.instance.setConfigData(
        Constant.LOCAL_CACHE.PLAYER, 
        JSON.stringify(this._playerInfo)
    );
}

/**
 * 保存所有数据
 */
public saveAll() {
    StorageManager.instance.setConfigDataWithoutSave(
        Constant.LOCAL_CACHE.PLAYER, 
        JSON.stringify(this._playerInfo)
    );
    StorageManager.instance.setConfigDataWithoutSave(
        Constant.LOCAL_CACHE.HISTORY, 
        JSON.stringify(this._history)
    );
    StorageManager.instance.setConfigDataWithoutSave(
        Constant.LOCAL_CACHE.SETTINGS, 
        JSON.stringify(this._settings)
    );
    StorageManager.instance.setConfigData(
        Constant.LOCAL_CACHE.DATA_VERSION, 
        this._dataVersion
    );
}
2.3.2.3 数据更新
typescript 复制代码
/**
 * 更新玩家信息
 * @param key 属性名
 * @param value 增量值或新值
 */
public updatePlayerInfo(key: string, value: any) {
    let isChanged = false;
    
    if (this._playerInfo.hasOwnProperty(key)) {
        if (typeof value === 'number') {
            isChanged = true;
            this._playerInfo[key] += value;
            if (this._playerInfo[key] < 0) {
                this._playerInfo[key] = 0;
            }
        } else if (typeof value === 'boolean' || typeof value === 'string') {
            isChanged = true;
            this._playerInfo[key] = value;
        }
    }
    
    if (isChanged) {
        StorageManager.instance.setConfigData(
            Constant.LOCAL_CACHE.PLAYER, 
            JSON.stringify(this._playerInfo)
        );
    }
}

使用示例

typescript 复制代码
// 增加金币
PlayerData.instance.updatePlayerInfo('gold', 100);

// 增加钻石
PlayerData.instance.updatePlayerInfo('diamond', 10);

// 设置关卡
PlayerData.instance.updatePlayerInfo('level', 5);
2.3.2.4 技能管理
typescript 复制代码
/**
 * 添加技能
 */
public addPlayerSkill(info: any) {
    if (!this.playerInfo.arrSkill.includes(info.ID)) {
        this.playerInfo.arrSkill.push(info.ID);
        this.savePlayerInfoToLocalCache();
        // 通知技能变更
        ClientEvent.dispatchEvent(Constant.EVENT_TYPE.PARSE_PLAYER_SKILL);
    }
}

/**
 * 删除技能
 */
public reducePlayerSkill(info: any) {
    if (this.playerInfo.arrSkill.includes(info.ID)) {
        let idx = this.playerInfo.arrSkill.findIndex(item => item === info.ID);
        this.playerInfo.arrSkill.splice(idx, 1);
        this.savePlayerInfoToLocalCache();
        ClientEvent.dispatchEvent(Constant.EVENT_TYPE.PARSE_PLAYER_SKILL);
    }
}

/**
 * 获取未解锁技能
 */
public getLockPlyerSkill(): any[] {
    let arrSkill = LocalConfig.instance.getTableArr(Constant.playerSkillName());
    let arrLock = arrSkill.filter(item => 
        !this.playerInfo.arrSkill.includes(item.ID) && 
        item.ID !== Constant.PLAYER_SKILL.RECOVERY
    );
    return arrLock;
}

2.4 UIManager(界面管理)

2.4.1 设计架构

复制代码
UIManager (单例)
    │
    ├─ _dictSharedPanel: { [path]: Node }    // 单例面板缓存
    ├─ _dictLoading: { [path]: boolean }     // 加载状态
    └─ _arrPopupDialog: []                   // 弹窗队列

2.4.2 核心方法

2.4.2.1 显示单例面板
typescript 复制代码
public showDialog(panelPath: string, args?: any[], cb?: Function, panelPriority: number = Constant.PRIORITY.NORMAL) {
    // 防止重复加载
    if (this._dictLoading[panelPath]) {
        return;
    }
    
    // 获取脚本名称(路径最后一部分)
    let idxSplit = panelPath.lastIndexOf('/');
    let scriptName = panelPath.slice(idxSplit + 1);
    
    // 如果面板已存在,直接显示
    if (this._dictSharedPanel.hasOwnProperty(panelPath)) {
        let panel = this._dictSharedPanel[panelPath];
        if (isValid(panel)) {
            panel.parent = find("Canvas");
            panel.active = true;
            
            // 调用show方法
            let script = panel.getComponent(scriptName);
            if (script && script.show) {
                script.show.apply(script, args);
                cb && cb(script);
            }
            return;
        }
    }
    
    // 加载新面板
    this._dictLoading[panelPath] = true;
    ResourceUtil.createUI(panelPath).then((node: Node) => {
        let isCloseBeforeShow = !this._dictLoading[panelPath];
        this._dictLoading[panelPath] = false;
        
        // 设置层级
        node.setSiblingIndex(panelPriority);
        this._dictSharedPanel[panelPath] = node;
        
        // 调用show方法
        let script = node.getComponent(scriptName);
        if (script && script.show) {
            script.show.apply(script, args);
            cb && cb(script);
        }
        
        // 如果在显示前已被关闭
        if (isCloseBeforeShow) {
            this.hideDialog(panelPath);
        }
    });
}

使用示例

typescript 复制代码
// 显示主界面
UIManager.instance.showDialog("home/homePanel");

// 显示复活面板(带回调)
UIManager.instance.showDialog("revive/revivePanel", [() => {
    console.log("复活面板显示");
}]);

// 显示暂停面板(设置优先级)
UIManager.instance.showDialog("pause/pausePanel", [], null, Constant.PRIORITY.DIALOG);
2.4.2.2 隐藏面板
typescript 复制代码
public hideDialog(panelPath: string, callback?: Function) {
    if (this._dictSharedPanel.hasOwnProperty(panelPath)) {
        let panel = this._dictSharedPanel[panelPath];
        if (panel && isValid(panel)) {
            let ani = panel.getComponent('animationUI');
            
            if (ani) {
                // 有关闭动画
                ani.close(() => {
                    panel.parent = null;
                    callback && callback();
                });
            } else {
                // 无动画直接隐藏
                panel.parent = null;
                callback && callback();
            }
        } else if (callback) {
            callback();
        }
    }
    
    this._dictLoading[panelPath] = false;
}
2.4.2.3 弹窗队列管理
typescript 复制代码
public pushToPopupSeq(panelPath: string, scriptName: string, param: any) {
    let popupDialog = {
        panelPath,
        scriptName,
        param,
        isShow: false
    };
    
    this._arrPopupDialog.push(popupDialog);
    this._checkPopupSeq();
}

private _checkPopupSeq() {
    if (this._arrPopupDialog.length > 0) {
        let first = this._arrPopupDialog[0];
        if (!first.isShow) {
            this.showDialog(first.panelPath, first.param);
            this._arrPopupDialog[0].isShow = true;
        }
    }
}

public shiftFromPopupSeq(panelPath: string) {
    this.hideDialog(panelPath, () => {
        if (this._arrPopupDialog[0]?.panelPath === panelPath) {
            this._arrPopupDialog.shift();
            this._checkPopupSeq();
        }
    });
}
2.4.2.4 血条显示
typescript 复制代码
// 显示玩家血条
public showPlayerBloodBar(
    scriptParent: any, 
    totalBlood: number, 
    curBlood: number, 
    callback: Function = () => {}, 
    offsetPos: Vec3 = v3_playerBloodOffsetPos, 
    scale: Vec3 = v3_playerBloodScale
) {
    ResourceUtil.getUIPrefabRes('fight/playerBloodBar').then((prefab: any) => {
        let ndBloodBar = PoolManager.instance.getNode(prefab, find("Canvas")) as Node;
        ndBloodBar.setSiblingIndex(0);
        
        let scriptBloodBar = ndBloodBar.getComponent(PlayerBloodBar);
        scriptParent.scriptBloodBar = scriptBloodBar;
        scriptBloodBar.show(scriptParent, totalBlood, curBlood, offsetPos, scale, callback);
    });
}

// 显示怪物血条
public showMonsterBloodBar(
    scriptParent: any, 
    totalBlood: number, 
    hpAddition: number, 
    callback: Function = () => {}, 
    offsetPos: Vec3 = v3_monsterBloodOffsetPos
) {
    ResourceUtil.getUIPrefabRes('fight/monsterBloodBar').then((prefab: any) => {
        let ndBloodBar = PoolManager.instance.getNode(prefab, find("Canvas"));
        let scriptBloodBar = ndBloodBar.getComponent(MonsterBloodBar);
        scriptParent.scriptBloodBar = scriptBloodBar;
        scriptBloodBar.show(scriptParent, totalBlood, offsetPos, hpAddition, callback);
    });
}

2.5 ResourceUtil(资源管理)

2.5.1 资源路径约定

资源类型 路径模板 示例
特效预制体 prefab/effect/{path} prefab/effect/hit/hit
模型预制体 prefab/model/{path} prefab/model/player/player01
UI预制体 prefab/ui/{path} prefab/ui/home/homePanel
音频文件 audio/{type}/{name} audio/sound/click
配置数据 datas/{name} datas/base.csv

2.5.2 核心方法

typescript 复制代码
/**
 * 通用资源加载
 */
public static loadRes(url: string, type: any): Promise<any> {
    return new Promise((resolve, reject) => {
        resources.load(url, type, (err: any, res: any) => {
            if (err) {
                error(err.message || err);
                reject(err);
                return;
            }
            resolve(res);
        });
    });
}

/**
 * 加载特效
 */
public static async loadEffectRes(modulePath: string): Promise<Prefab> {
    return this.loadRes(
        `${Constant.RESOURCES_FILE_NAME.PREFAB}/${Constant.PREFAB_FILE_NAME.EFFECT}/${modulePath}`, 
        Prefab
    );
}

/**
 * 加载模型
 */
public static async loadModelRes(modulePath: string): Promise<Prefab> {
    return this.loadRes(
        `${Constant.RESOURCES_FILE_NAME.PREFAB}/${Constant.PREFAB_FILE_NAME.MODEL}/${modulePath}`, 
        Prefab
    );
}

/**
 * 创建UI节点
 */
public static async createUI(path: string, parent?: Node): Promise<Node> {
    let pf = await this.getUIPrefabRes(path) as Prefab;
    let node = instantiate(pf);
    node.setPosition(0, 0, 0);
    
    if (!parent) {
        parent = find("Canvas");
    }
    parent.addChild(node);
    
    return node;
}

2.5.3 资源加载策略

策略1:按需加载

typescript 复制代码
// 战斗中需要时才加载怪物技能
ResourceUtil.loadEffectRes(`${skillInfo.resName}/${skillInfo.resName}`).then((prefab) => {
    let ndSkill = PoolManager.instance.getNode(prefab, parentNode);
    // 使用技能
});

策略2:预加载

typescript 复制代码
// 关卡切换时预加载所有怪物技能
public preloadMonsterSkill(mapName: string): Promise<void> {
    return new Promise((resolve) => {
        let arrInfo = LocalConfig.instance.getTableArr(mapName);
        let arrMonster = arrInfo.filter(item => item.ID.startsWith("2"));
        
        let arrSkillIds = arrMonster.flatMap(item => 
            item.skill === "" ? [] : item.skill.split("#")
        );
        
        let uniqueSkillIds = [...new Set(arrSkillIds)];
        
        let arrPromise = uniqueSkillIds.map(id => {
            let skillInfo = LocalConfig.instance.queryByID("monsterSkill", id);
            return ResourceUtil.loadEffectRes(`${skillInfo.resName}/${skillInfo.resName}`);
        });
        
        Promise.all(arrPromise).then(() => resolve());
    });
}

策略3:批量加载

typescript 复制代码
public static loadDirRes(path: string, type: any, callback: Function) {
    resources.loadDir(path, type, (err: any, assets: any) => {
        if (err) {
            console.error(err);
        } else {
            callback(null, assets);
        }
    });
}

// 使用示例:加载所有箭矢资源
ResourceUtil.loadDirRes("prefab/model/weapon/arrow", Prefab, (err, pfs) => {
    pfs.forEach(pf => {
        PoolManager.instance.preloadPool(pf, 5);
    });
});

2.6 AudioManager(音频管理)

2.6.1 架构设计

复制代码
AudioManager (单例)
    │
    ├─ _musicSource: AudioSource      // 当前背景音乐
    ├─ _mapMusic: Map<string, AudioSource>    // 背景音乐缓存
    ├─ _mapSound: Map<string, AudioSource[]>  // 音效缓存(支持多实例)
    └─ _curSounds: AudioSource[]      // 当前播放的音效

2.6.2 核心方法

2.6.2.1 播放背景音乐
typescript 复制代码
public async playMusic(name: string, loop: boolean, cb?: Function) {
    let path = `${Constant.RESOURCES_FILE_NAME.AUDIO}/${Constant.AUDIO_FILE_NAME.MUSIC}/${name}`;
    let source = this._musicSource;
    
    // 停止当前音乐
    source && source.stop();
    
    // 复用已有音乐组件
    if (source && source.clip?.name == name) {
        // 已经是目标音乐,直接播放
    } else if (this._mapMusic.get(name)) {
        // 从缓存获取
        source = this._mapMusic.get(name)!;
    } else {
        // 加载新音乐
        let clip = await ResourceUtil.loadRes(path, AudioClip) as AudioClip;
        let musicSource = this._persistRootNode.addComponent(AudioSource);
        musicSource.clip = clip;
        this._mapMusic.set(name, musicSource);
        source = musicSource;
    }
    
    // 设置播放参数
    source.currentTime = 0;
    source.volume = this.musicVolume * this.mainVolume;
    source.loop = loop;
    source.playOnAwake = false;
    
    this._musicSource = source;
    
    // 播放
    if (this._musicSwitch) {
        source.play();
    }
    
    cb && cb();
}
2.6.2.2 播放音效
typescript 复制代码
public async playSound(name: string, loop: boolean = false, cb?: Function) {
    let path = `${Constant.RESOURCES_FILE_NAME.AUDIO}/${Constant.AUDIO_FILE_NAME.SOUND}/${name}`;
    
    // 加载音效文件
    const clip = await ResourceUtil.loadRes(path, AudioClip) as AudioClip;
    
    // 从对象池获取或创建AudioSource
    let source = this._getAudioSource(clip);
    
    // 设置参数
    source.volume = this.soundVolume * this.mainVolume;
    source.loop = loop;
    source.playOnAwake = false;
    
    // 添加到当前播放列表
    this._curSounds.push(source);
    
    // 播放
    if (this._soundSwitch) {
        source.play();
        
        // 播放完成后回收
        setTimeout(() => {
            let idx = this._curSounds.indexOf(source);
            if (idx >= 0) {
                this._curSounds.splice(idx, 1);
            }
            
            // 放回对象池
            if (!this._mapSound.get(name)) {
                this._mapSound.set(name, [source]);
            } else {
                this._mapSound.get(name)!.push(source);
            }
            
            cb?.();
        }, source.duration * 1000);
    }
}

private _getAudioSource(clip: AudioClip): AudioSource {
    let result: AudioSource | undefined;
    
    // 尝试从对象池获取
    this._mapSound.forEach((sounds, soundName) => {
        if (clip.name == name && sounds.length > 0) {
            result = sounds.pop();
        }
    });
    
    // 创建新的AudioSource
    if (!result) {
        result = this._persistRootNode.addComponent(AudioSource);
    }
    
    result.clip = clip;
    result.currentTime = 0;
    
    return result;
}
2.6.2.3 音量控制
typescript 复制代码
// 音乐音量
public get musicVolume(): number {
    return this._musicVolume * this._musicSwitch;
}

public set musicVolume(v: number) {
    this._musicVolume = v;
    this._setCurMusicVolume();
}

// 音效音量
public get soundVolume(): number {
    return this._soundVolume * this._soundSwitch;
}

public set soundVolume(v: number) {
    this._soundVolume = v;
    this._setCurSoundVolume();
}

private _setCurMusicVolume() {
    for (const source of this._mapMusic.values()) {
        source.volume = this.musicVolume;
    }
}

private _setCurSoundVolume() {
    for (const sounds of this._mapSound.values()) {
        sounds.forEach(source => {
            source.volume = this.soundVolume;
        });
    }
}
2.6.2.4 开关控制
typescript 复制代码
// 开关音乐
public switchMusic(open: boolean) {
    if (open) {
        this.resumeMusic();
    } else {
        this.stopMusic();
    }
    StorageManager.instance.setGlobalData('music', `${open}`);
}

// 开关音效
public switchSound(open: boolean) {
    if (open) {
        this.resumeSound();
    } else {
        this.pauseSound();
    }
    StorageManager.instance.setGlobalData('sound', `${open}`);
}

private stopMusic() {
    this._musicSwitch = 0;
    this._musicSource && this._musicSource.pause();
}

private resumeMusic() {
    this._musicSwitch = 1;
    if (this._musicSource) {
        this._musicSource.volume = this.musicVolume;
        this._musicSource.play();
    }
}

private pauseSound() {
    this._soundSwitch = 0;
    this._curSounds.forEach(source => {
        if (source.playing) {
            source.pause();
        }
    });
}

private resumeSound() {
    this._soundSwitch = 1;
    this._curSounds.forEach(source => {
        if (source.state == AudioSource.AudioState.PAUSED) {
            source.volume = this.soundVolume;
            source.play();
        }
    });
}

2.7 框架模块协作关系

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        游戏启动流程                            │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      main.ts (入口)                            │
│  ├─ PlayerData.instance.loadGlobalCache()                      │
│  ├─ PlayerData.instance.loadFromCache()                        │
│  ├─ LocalConfig.instance.loadConfig()                          │
│  └─ AudioManager.instance.init()                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    GameManager (游戏管理)                       │
│  ├─ 监听事件: ON_GAME_INIT, ON_GAME_OVER, ON_GAME_PAUSE       │
│  ├─ 管理游戏状态: isGameStart, isGamePause, isGameOver        │
│  ├─ 创建玩家: _createPlayer()                                 │
│  ├─ 加载地图: MapManager.buildMap()                           │
│  └─ 资源回收: _recycleAll()                                   │
└─────────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│    Player         │ │    Monster        │ │    MapManager     │
│  ├─ 移动控制      │ │  ├─ AI移动        │ │  ├─ 加载地图配置  │
│  ├─ 攻击逻辑      │ │  ├─ 技能释放      │ │  ├─ 创建怪物/NPC │
│  ├─ 技能管理      │ │  └─ 死亡处理      │ │  └─ 传送门管理   │
│  └─ 血量管理      │ └───────────────────┘ └───────────────────┘
└───────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      UIManager (界面管理)                       │
│  ├─ showDialog(): 显示面板                                     │
│  ├─ hideDialog(): 隐藏面板                                     │
│  ├─ showPlayerBloodBar(): 显示玩家血条                          │
│  ├─ showMonsterBloodBar(): 显示怪物血条                         │
│  └─ showBloodTips(): 显示血量提示                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      事件驱动                                  │
│  ClientEvent.dispatchEvent(EVENT_TYPE, ...args)                │
│  ├─ REFRESH_GOLD → 更新金币显示                                │
│  ├─ REFRESH_DIAMOND → 更新钻石显示                             │
│  ├─ ON_GAME_OVER → 显示结算界面                                │
│  └─ PARSE_PLAYER_SKILL → 重新解析玩家技能                      │
└─────────────────────────────────────────────────────────────────┘

总结

框架层是游戏的核心基础设施,主要包含:

  1. Constant:集中管理所有常量配置
  2. ClientEvent:实现组件间解耦通信
  3. PlayerData:管理玩家数据持久化
  4. UIManager:统一管理UI界面生命周期
  5. ResourceUtil:提供统一的资源加载接口
  6. AudioManager:管理音频播放和控制

这些模块共同协作,为游戏提供稳定、高效的运行环境。理解框架层的设计对于后续开发和扩展至关重要。

相关推荐
Amctwd6 小时前
【Cocos Creator】幽灵射手项目讲义 - 第一章:项目概述
cocos
LcGero11 天前
Cocos Creator 热更新地址动态化方案
cocos·动态化·热更
weixin_409383122 个月前
cocosshader像素风沙消散
shader·cocos
weixin_409383123 个月前
cocos shader消失
shader·cocos
weixin_409383123 个月前
cocos魔法阵shader
shader·cocos
weixin_409383123 个月前
cocos抛物线掉落装备 游戏中的抛物线应用x²=-2py 开口向下
游戏·cocos·抛物线
weixin_409383123 个月前
cocos 按钮光环shader
shader·cocos
weixin_409383124 个月前
a星学习记录 通过父节点从目的地格子坐标回溯起点
学习·cocos·a星
weixin_409383124 个月前
简单四方向a*寻路学习记录2 先做个数组地图 在cocos编辑器模式上运行出格子 计算角色世界坐标跟数组地图的联系
学习·编辑器·cocos