diff --git a/assets/core/Oops.ts b/assets/core/Oops.ts index 1e4de7e..d0ffa1a 100644 --- a/assets/core/Oops.ts +++ b/assets/core/Oops.ts @@ -24,7 +24,7 @@ import { GameManager } from "./game/GameManager"; import { LayerManager } from "./gui/layer/LayerManager"; /** 框架版本号 */ -export var version: string = "1.2.2"; +export var version: string = "1.3.0"; /** 框架核心模块访问入口 */ export class oops { diff --git a/assets/core/common/audio/AudioEffect.ts b/assets/core/common/audio/AudioEffect.ts index e132819..c76b430 100644 --- a/assets/core/common/audio/AudioEffect.ts +++ b/assets/core/common/audio/AudioEffect.ts @@ -4,120 +4,21 @@ * @LastEditors: dgflash * @LastEditTime: 2022-09-02 10:22:36 */ -import { AudioClip, AudioSource, _decorator, error } from 'cc'; +import { AudioClip, AudioSource, _decorator } from 'cc'; import { oops } from '../../Oops'; const { ccclass } = _decorator; -/** - * 注:用playOneShot播放的音乐效果,在播放期间暂时没办法即时关闭音乐 - */ - -/** 资源加载记录 */ -interface ResRecord { - source: boolean; - ac: AudioClip, - bundle?: string, - path?: string -} - - /** 游戏音效 */ @ccclass('AudioEffect') export class AudioEffect extends AudioSource { - private effects: Map = new Map(); + /** 背景音乐播放完成回调 */ + onComplete: Function | null = null; - private _progress: number = 0; - /** 获取音乐播放进度 */ - get progress(): number { - if (this.duration > 0) - this._progress = this.currentTime / this.duration; - return this._progress; - } - /** - * 设置音乐当前播放进度 - * @param value 进度百分比0到1之间 - */ - set progress(value: number) { - this._progress = value; - this.currentTime = value * this.duration; + start() { + this.node.on(AudioSource.EventType.ENDED, this.onAudioEnded, this); } - /** - * 加载音效并播放 - * @param url 音效资源地址 - * @param callback 资源加载完成并开始播放回调 - * @param bundleName 资源包名 - */ - load(url: string | AudioClip, callback?: Function, bundleName?: string) { - if (bundleName == null) bundleName = oops.res.defaultBundleName; - - // 资源播放音乐对象 - if (url instanceof AudioClip) { - this.effects.set(url.uuid, { source: true, ac: url }); - this.playOneShot(url, this.volume); - callback && callback(); - } - else { - const key = `${bundleName}:${url}`; - // 地址加载音乐资源后播放 - if (!this.effects.has(url)) { - oops.res.load(bundleName, url, AudioClip, (err: Error | null, data: AudioClip) => { - if (err) { - error(err); - return; - } - - this.effects.set(key, { source: false, bundle: bundleName, path: url, ac: data }); - this.playOneShot(data, this.volume); - callback && callback(); - }); - } - // 播放缓存中音效 - else { - const rr = this.effects.get(key)!; - this.playOneShot(rr.ac, this.volume); - callback && callback(); - } - } - } - - /** 释放所有已使用过的音效资源 */ - releaseAll() { - for (let key in this.effects) { - const rr = this.effects.get(key)!; - if (rr.source) { - this.release(rr.ac); - } - else { - this.release(rr.path!, rr.bundle!); - } - } - this.effects.clear(); - } - - /** - * 释放指定地址音效资源 - * @param url 音效资源地址 - * @param bundleName 资源所在包名 - */ - release(url: string | AudioClip, bundleName?: string) { - if (bundleName == null) bundleName = oops.res.defaultBundleName; - - let ac: AudioClip | undefined = undefined; - if (url instanceof AudioClip) { - ac = url; - if (this.effects.has(ac.uuid)) { - this.effects.delete(ac.uuid); - ac.decRef(); - } - } - else { - const key = `${bundleName}:${url}`; - const rr = this.effects.get(key); - if (rr) { - this.effects.delete(key); - rr.ac.decRef(); - } - } + private onAudioEnded() { + this.onComplete && this.onComplete(); } } \ No newline at end of file diff --git a/assets/core/common/audio/AudioEffectPool.ts b/assets/core/common/audio/AudioEffectPool.ts new file mode 100644 index 0000000..a5f3d54 --- /dev/null +++ b/assets/core/common/audio/AudioEffectPool.ts @@ -0,0 +1,148 @@ +import { AudioClip, Node, NodePool } from "cc"; +import { oops } from "../../Oops"; +import { resLoader } from "../loader/ResLoader"; +import { AudioEffect } from "./AudioEffect"; + +const AE_ID_MAX = 30000; + +/** 音效池 */ +export class AudioEffectPool { + /** 音效播放器对象池 */ + private pool: NodePool = new NodePool(); + /** 对象池集合 */ + private effects: Map = new Map(); + /** 用过的音效资源记录 */ + private res: Map = new Map(); + /** 音效播放器唯一编号 */ + private _aeId: number = 0; + + /** 获取请求唯一编号 */ + private getAeId() { + if (this._aeId == AE_ID_MAX) this._aeId = 1; + this._aeId++; + return this._aeId; + } + + async loadAndPlay(url: string | AudioClip, bundleName: string = resLoader.defaultBundleName, onPlayComplete?: Function): Promise { + return new Promise(async (resolve, reject) => { + let aeid = this.getAeId(); + let key: string; + if (url instanceof AudioClip) { + key = url.uuid; + } + else { + key = `${bundleName}_${url}`; + } + key += "_" + aeid; + + // 创建音效资源 + let clip: AudioClip; + if (url instanceof AudioClip) { + clip = url; + } + else { + clip = resLoader.get(url, AudioClip, bundleName)!; + if (!clip) { + this.res.set(bundleName, url); + clip = await resLoader.loadAsync(bundleName, url, AudioClip); + } + } + + // 获取音效果播放器播放音乐 + let ae: AudioEffect; + let node: Node = null!; + if (this.pool.size() == 0) { + node = new Node(); + node.name = "AudioEffect"; + node.parent = oops.audio.node; + ae = node.addComponent(AudioEffect)!; + } + else { + node = this.pool.get()!; + ae = node.getComponent(AudioEffect)!; + } + ae.onComplete = () => { + this.put(aeid, url, bundleName); // 播放完回收对象 + onPlayComplete && onPlayComplete(); + // console.log(`【音效】回收,池中剩余音效播放器【${this.pool.size()}】`); + }; + + // 记录正在播放的音效播放器 + this.effects.set(key, ae); + + ae.clip = clip; + ae.play(); + + resolve(aeid); + }); + } + + /** + * 回收音效播放器 + * @param aeid 播放器编号 + * @param url 音效路径 + * @param bundleName 资源包名 + */ + put(aeid: number, url: string | AudioClip, bundleName: string = resLoader.defaultBundleName) { + let key: string; + if (url instanceof AudioClip) { + key = url.uuid; + } + else { + key = `${bundleName}_${url}`; + } + key += "_" + aeid; + + let ae = this.effects.get(key); + if (ae && ae.clip) { + this.effects.delete(key); + ae.stop(); + this.pool.put(ae.node); + } + } + + /** 释放所有音效资源与对象池中播放器 */ + release() { + // 释放正在播放的音效 + this.effects.forEach(ae => { + ae.node.destroy(); + }); + this.effects.clear(); + + // 释放音效资源 + this.res.forEach((url: string, bundleName: string) => { + resLoader.release(bundleName, url); + }); + + // 释放池中播放器 + this.pool.clear(); + } + + /** 设置音量 */ + setVolume(volume: number) { + this.effects.forEach(ae => { + ae.volume = volume; + }); + } + + /** 停止播放所有音效 */ + stop() { + this.effects.forEach(ae => { + ae.stop(); + }); + } + + /** 恢复所有音效 */ + play() { + this.effects.forEach(ae => { + ae.play(); + }); + } + + /** 暂停所有音效 */ + pause() { + this.effects.forEach(ae => { + ae.pause(); + }); + } +} \ No newline at end of file diff --git a/assets/core/common/audio/AudioEffectPool.ts.meta b/assets/core/common/audio/AudioEffectPool.ts.meta new file mode 100644 index 0000000..a7eb4f5 --- /dev/null +++ b/assets/core/common/audio/AudioEffectPool.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "01278043-8ebb-42af-8081-a663b90d994d", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/audio/AudioManager.ts b/assets/core/common/audio/AudioManager.ts index 22cd2cb..9df341e 100644 --- a/assets/core/common/audio/AudioManager.ts +++ b/assets/core/common/audio/AudioManager.ts @@ -1,7 +1,7 @@ -import {AudioClip, Component} from "cc"; -import {oops} from "../../Oops"; -import {AudioEffect} from "./AudioEffect"; -import {AudioMusic} from "./AudioMusic"; +import { AudioClip, Component } from "cc"; +import { oops } from "../../Oops"; +import { AudioEffectPool } from "./AudioEffectPool"; +import { AudioMusic } from "./AudioMusic"; const LOCAL_STORE_KEY = "game_audio"; @@ -15,7 +15,7 @@ export class AudioManager extends Component { /** 背景音乐管理对象 */ music: AudioMusic = null!; /** 音效管理对象 */ - effect: AudioEffect = null!; + effect: AudioEffectPool = new AudioEffectPool(); /** 音乐管理状态数据 */ private local_data: any = {}; @@ -109,8 +109,7 @@ export class AudioManager extends Component { set switchMusic(value: boolean) { this._switch_music = value; - if (!value) - this.music.stop(); + if (!value) this.music.stop(); } /** @@ -119,15 +118,16 @@ export class AudioManager extends Component { * @param callback 加载完成回调 * @param bundleName 资源包名 */ - playEffect(url: string | AudioClip, callback?: Function, bundleName?: string) { + playEffect(url: string | AudioClip, bundleName?: string, onPlayComplete?: Function): Promise { if (this._switch_effect) { - this.effect.load(url, callback, bundleName); + return this.effect.loadAndPlay(url, bundleName, onPlayComplete); } + return Promise.resolve(-1); } /** 释放音效资源 */ - releaseEffect(url: string | AudioClip, bundleName?: string) { - this.effect.release(url, bundleName); + putEffect(aeid: number, url: string | AudioClip, bundleName?: string) { + this.effect.put(aeid, url, bundleName); } /** @@ -143,7 +143,7 @@ export class AudioManager extends Component { */ set volumeEffect(value: number) { this._volume_effect = value; - this.effect.volume = value; + this.effect.setVolume(value); } /** @@ -164,26 +164,20 @@ export class AudioManager extends Component { /** 恢复当前暂停的音乐与音效播放 */ resumeAll() { - if (this.music) { - if (!this.music.playing && this.music.progress > 0) this.music.play(); - if (!this.effect.playing && this.effect.progress > 0) this.effect.play(); - } + if (!this.music.playing && this.music.progress > 0) this.music.play(); + this.effect.play(); } /** 暂停当前音乐与音效的播放 */ pauseAll() { - if (this.music) { - if (this.music.playing) this.music.pause(); - if (this.effect.playing) this.effect.pause(); - } + if (this.music.playing) this.music.pause(); + this.effect.pause(); } /** 停止当前音乐与音效的播放 */ stopAll() { - if (this.music) { - this.music.stop(); - this.effect.stop(); - } + this.music.stop(); + this.effect.stop(); } /** 保存音乐音效的音量、开关配置数据到本地 */ @@ -196,25 +190,25 @@ export class AudioManager extends Component { oops.storage.set(LOCAL_STORE_KEY, this.local_data); } - /** 本地加载音乐音效的音量、开关配置数据并设置到游戏中 */ load() { this.music = this.getComponent(AudioMusic) || this.addComponent(AudioMusic)!; - this.effect = this.getComponent(AudioEffect) || this.addComponent(AudioEffect)!; this.local_data = oops.storage.getJson(LOCAL_STORE_KEY); if (this.local_data) { try { this.setState(); - } catch (e) { + } + catch { this.setStateDefault(); } - } else { + } + else { this.setStateDefault(); } if (this.music) this.music.volume = this._volume_music; - if (this.effect) this.effect.volume = this._volume_effect; + this.effect.setVolume(this._volume_effect); } private setState() { diff --git a/assets/core/common/audio/AudioMusic.ts b/assets/core/common/audio/AudioMusic.ts index d1e2d4d..ed9939d 100644 --- a/assets/core/common/audio/AudioMusic.ts +++ b/assets/core/common/audio/AudioMusic.ts @@ -5,7 +5,7 @@ * @LastEditTime: 2023-05-16 09:11:30 */ import { AudioClip, AudioSource, _decorator } from 'cc'; -import { oops } from '../../Oops'; +import { resLoader } from '../loader/ResLoader'; const { ccclass, menu } = _decorator; @@ -20,10 +20,19 @@ export class AudioMusic extends AudioSource { private _progress: number = 0; private _isLoading: boolean = false; - private _bundleName: string = null!; // 当前音乐资源包 - private _url: string = null!; // 当前播放音乐 - private _bundleName_next: string = null!; // 下一个音乐资源包 - private _url_next: string = null!; // 下一个播放音乐 + private _nextBundleName: string = null!; // 下一个音乐资源包 + private _nextUrl: string = null!; // 下一个播放音乐 + + start() { + // this.node.on(AudioSource.EventType.STARTED, this.onAudioStarted, this); + this.node.on(AudioSource.EventType.ENDED, this.onAudioEnded, this); + } + + // private onAudioStarted() { } + + private onAudioEnded() { + this.onComplete && this.onComplete(); + } /** 获取音乐播放进度 */ get progress(): number { @@ -46,75 +55,49 @@ export class AudioMusic extends AudioSource { * @param callback 加载完成回调 * @param bundleName 资源包名 */ - async load(url: string, callback?: Function, bundleName?: string) { - if (bundleName == null) bundleName = oops.res.defaultBundleName; - + async load(url: string, callback?: Function, bundleName: string = resLoader.defaultBundleName) { // 下一个加载的背景音乐资源 if (this._isLoading) { - this._bundleName_next = bundleName; - this._url_next = url; + this._nextBundleName = bundleName; + this._nextUrl = url; return; } this._isLoading = true; - var data: AudioClip = await oops.res.loadAsync(bundleName, url, AudioClip); + var data: AudioClip = await resLoader.loadAsync(bundleName, url, AudioClip); if (data) { this._isLoading = false; // 处理等待加载的背景音乐 - if (this._url_next != null) { - // 删除之前加载的音乐资源 - this.release(); - + if (this._nextUrl != null) { // 加载等待播放的背景音乐 - this.load(this._url_next, callback, this._bundleName_next); - this._bundleName_next = this._url_next = null!; + this.load(this._nextUrl, callback, this._nextBundleName); + this._nextBundleName = this._nextUrl = null!; } else { callback && callback(); - this.playPrepare(bundleName, url, data); + + // 正在播放的时候先关闭 + if (this.playing) { + this.stop(); + } + + // 删除当前正在播放的音乐 + this.release(); + + // 播放背景音乐 + this.clip = data; + this.play(); } } } - private playPrepare(bundleName: string, url: string, data: AudioClip) { - // 正在播放的时候先关闭 - if (this.playing) { - this.stop(); - } - - // 删除当前正在播放的音乐 - this.release(); - - // 播放背景音乐 - this.enabled = true; - this.clip = data; - this.play(); - - // 记录新的资源包与资源名数据 - this._bundleName = bundleName; - this._url = url; - } - - /** cc.Component 生命周期方法,验证背景音乐播放完成逻辑,建议不要主动调用 */ - update(dt: number) { - // 背景资源播放完成事件 - if (this.playing == false && this.progress == 0) { - this.enabled = false; - this.clip = null; - this._bundleName = this._url = null!; - this.onComplete && this.onComplete(); - } - } - /** 释放当前背景音乐资源 */ release() { - if (this._url) { + if (this.clip) { this.stop(); + this.clip.decRef(); this.clip = null; - oops.res.release(this._url, this._bundleName); } - - this._bundleName = this._url = null!; } } \ No newline at end of file diff --git a/assets/core/utils/ViewUtil.ts b/assets/core/utils/ViewUtil.ts index b75eb85..758e3b7 100644 --- a/assets/core/utils/ViewUtil.ts +++ b/assets/core/utils/ViewUtil.ts @@ -6,7 +6,6 @@ */ import { Animation, AnimationClip, EventTouch, instantiate, Node, Prefab, Size, UITransform, v3, Vec3 } from "cc"; import { resLoader } from "../common/loader/ResLoader"; -import { oops } from "../Oops"; /** 显示对象工具 */ export class ViewUtil { @@ -90,7 +89,7 @@ export class ViewUtil { * @param path 资源路径 * @param bundleName 资源包名 */ - static createPrefabNode(path: string, bundleName: string = oops.res.defaultBundleName): Node { + static createPrefabNode(path: string, bundleName: string = resLoader.defaultBundleName): Node { const p = resLoader.get(path, Prefab, bundleName); if (p) { return instantiate(p); @@ -103,7 +102,7 @@ export class ViewUtil { * @param path 资源路径 * @param bundleName 资源包名 */ - static createPrefabNodeAsync(path: string, bundleName: string = oops.res.defaultBundleName): Promise { + static createPrefabNodeAsync(path: string, bundleName: string = resLoader.defaultBundleName): Promise { return new Promise(async (resolve, reject) => { const p = await resLoader.loadAsync(bundleName, path, Prefab); if (p) { diff --git a/assets/libs/gui/button/ButtonEffect.ts b/assets/libs/gui/button/ButtonEffect.ts index 3958d26..e8aab13 100644 --- a/assets/libs/gui/button/ButtonEffect.ts +++ b/assets/libs/gui/button/ButtonEffect.ts @@ -59,7 +59,6 @@ export default class ButtonEffect extends ButtonSimple { super.onTouchEnd(event); } - onDestroy() { this.node.off(Node.EventType.TOUCH_START, this.onTouchtStart, this); super.onDestroy(); diff --git a/assets/libs/gui/button/ButtonSimple.ts b/assets/libs/gui/button/ButtonSimple.ts index c917fcc..b1efa7d 100644 --- a/assets/libs/gui/button/ButtonSimple.ts +++ b/assets/libs/gui/button/ButtonSimple.ts @@ -1,5 +1,6 @@ import { AudioClip, Component, EventTouch, Node, _decorator, game } from "cc"; import { oops } from "../../../core/Oops"; +import { resLoader } from "../../../core/common/loader/ResLoader"; const { ccclass, property, menu } = _decorator; @@ -22,7 +23,7 @@ export default class ButtonSimple extends Component { type: AudioClip }) private effect: AudioClip = null!; - + private effectIds: number[] = []; private touchCount = 0; private touchtEndTime = 0; @@ -54,14 +55,24 @@ export default class ButtonSimple extends Component { } /** 短按触摸音效 */ - protected playEffect() { - if (this.effect) oops.audio.playEffect(this.effect); + protected async playEffect() { + if (this.effect) { + const effectId = await oops.audio.playEffect(this.effect, resLoader.defaultBundleName, () => { + this.effectIds.remove(effectId); + }); + if (effectId > 0) this.effectIds.push(effectId); + } } onDestroy() { this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this); this.node.off(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); - if (this.effect) oops.audio.releaseEffect(this.effect); + if (this.effect) { + this.effectIds.forEach(effectId => { + console.log(effectId); + oops.audio.putEffect(effectId, this.effect); + }); + } } } diff --git a/assets/libs/gui/button/UIButton.ts b/assets/libs/gui/button/UIButton.ts index e073227..067b5f4 100644 --- a/assets/libs/gui/button/UIButton.ts +++ b/assets/libs/gui/button/UIButton.ts @@ -1,5 +1,6 @@ import { AudioClip, Button, EventTouch, _decorator, game } from "cc"; import { oops } from "../../../core/Oops"; +import { resLoader } from "../../../core/common/loader/ResLoader"; const { ccclass, property, menu } = _decorator; @@ -26,6 +27,7 @@ export default class UIButton extends Button { type: AudioClip }) private effect: AudioClip = null!; + private effectIds: number[] = []; /** 触摸次数 */ private _touchCount = 0; @@ -50,13 +52,28 @@ export default class UIButton extends Button { else { this._touchEndTime = game.totalTime; super._onTouchEnded(event); - } - // 短按触摸音效 - if (this.effect) oops.audio.playEffect(this.effect); + // 短按触摸音效 + this.playEffect(); + } + } + + /** 短按触摸音效 */ + protected async playEffect() { + if (this.effect) { + const effectId = await oops.audio.playEffect(this.effect, resLoader.defaultBundleName, () => { + this.effectIds.remove(effectId); + }); + if (effectId > 0) this.effectIds.push(effectId); + } } onDestroy() { - if (this.effect) oops.audio.releaseEffect(this.effect); + if (this.effect) { + this.effectIds.forEach(effectId => { + console.log(effectId); + oops.audio.putEffect(effectId, this.effect); + }); + } } } diff --git a/assets/module/common/GameComponent.ts b/assets/module/common/GameComponent.ts index 36ff44c..717fa95 100644 --- a/assets/module/common/GameComponent.ts +++ b/assets/module/common/GameComponent.ts @@ -22,8 +22,12 @@ enum ResType { /** 资源加载记录 */ interface ResRecord { + /** 资源包名 */ bundle: string, - path: string + /** 资源路径 */ + path: string, + /** 资源编号 */ + resId?: number } /** @@ -126,7 +130,7 @@ export class GameComponent extends Component { * @param bundleName 资源包名 * @param paths 资源路径 */ - private addPathToRecord(type: ResType, bundleName: string, paths?: string | string[] | AssetType | ProgressCallback | CompleteCallback | null) { + private addPathToRecord(type: ResType, bundleName: string, paths?: string | string[] | AssetType | ProgressCallback | CompleteCallback | null, resId?: number) { if (this.resPaths == null) this.resPaths = new Map(); var rps = this.resPaths.get(type); @@ -139,30 +143,36 @@ export class GameComponent extends Component { let realBundle = bundleName; for (let index = 0; index < paths.length; index++) { let realPath = paths[index]; - let key = `${realBundle}:${realPath}`; + let key = this.getResKey(realBundle, realPath, resId); if (!rps.has(key)) { - rps.set(key, { path: realPath, bundle: realBundle }) + rps.set(key, { path: realPath, bundle: realBundle, resId: resId }); } } } else if (typeof paths === "string") { let realBundle = bundleName; let realPath = paths; - let key = `${realBundle}:${realPath}`; + let key = this.getResKey(realBundle, realPath, resId); if (!rps.has(key)) { - rps.set(key, { path: realPath, bundle: realBundle }) + rps.set(key, { path: realPath, bundle: realBundle, resId: resId }); } } else { let realBundle = oops.res.defaultBundleName; let realPath = bundleName; - let key = `${realBundle}:${realPath}`; + let key = this.getResKey(realBundle, realPath, resId); if (!rps.has(key)) { - rps.set(key, { path: realPath, bundle: realBundle }) + rps.set(key, { path: realPath, bundle: realBundle, resId: resId }); } } } + private getResKey(realBundle: string, realPath: string, resId?: number) { + let key = `${realBundle}:${realPath}`; + if (resId != null) key += ":" + resId; + return key; + } + /** 异步加载资源 */ loadAsync(bundleName: string, paths: string | string[], type: AssetType | null): Promise; loadAsync(bundleName: string, paths: string | string[]): Promise; @@ -259,7 +269,7 @@ export class GameComponent extends Component { const rps = this.resPaths.get(ResType.Audio); if (rps) { rps.forEach((value: ResRecord) => { - oops.audio.releaseEffect(value.path, value.bundle); + oops.audio.putEffect(value.resId!, value.path, value.bundle); }); } } @@ -293,10 +303,16 @@ export class GameComponent extends Component { * @param callback 资源加载完成回调 * @param bundleName 资源包名 */ - playEffect(url: string, callback?: Function, bundleName?: string) { + async playEffect(url: string, bundleName?: string) { if (bundleName == null) bundleName = oops.res.defaultBundleName; - this.addPathToRecord(ResType.Audio, bundleName, url); - oops.audio.playEffect(url, callback, bundleName); + const id = await oops.audio.playEffect(url, bundleName, () => { + const rps = this.resPaths.get(ResType.Audio); + if (rps) { + const key = this.getResKey(bundleName, url, id); + rps.delete(key); + } + }); + this.addPathToRecord(ResType.Audio, bundleName, url, id); } //#endregion