🎯 幽灵射手系列 · 全目录
文章目录
-
-
- [🎯 幽灵射手系列 · 全目录](#🎯 幽灵射手系列 · 全目录)
- 概述
- [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 → 重新解析玩家技能 │
└─────────────────────────────────────────────────────────────────┘
总结
框架层是游戏的核心基础设施,主要包含:
- Constant:集中管理所有常量配置
- ClientEvent:实现组件间解耦通信
- PlayerData:管理玩家数据持久化
- UIManager:统一管理UI界面生命周期
- ResourceUtil:提供统一的资源加载接口
- AudioManager:管理音频播放和控制
这些模块共同协作,为游戏提供稳定、高效的运行环境。理解框架层的设计对于后续开发和扩展至关重要。