@ives_xxz/framework 1.4.16 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,54 @@
1
1
  import { FWManager } from './FWManager';
2
- import Framework from '../Framework';
3
2
  import FWLog from '../log/FWLog';
3
+ import { FWSystemDefine } from '../define/FWSystemDefine';
4
+
5
+ class FWAudioData {
6
+ clip: cc.AudioClip;
7
+ volume: number = 1;
8
+ loop: boolean = false;
9
+ cb?: (id: number) => void;
10
+ tag?: string;
11
+ }
12
+
13
+ class FWAudioPoolItem {
14
+ constructor(public audioId: number, public startTime: number, public tag?: string) {}
15
+ }
4
16
 
5
17
  export default class FWAudioManager extends FWManager implements FW.AudioManager {
6
- /**背景音量 */
7
18
  musicVolume: number = 1;
8
- /**音效音量 */
9
19
  effectsVolume: number = 1;
10
- /**播放音效的音量 */
11
20
  soundVolume: number = 1;
12
21
 
13
- public initialize(): void {}
22
+ private audioPool: Map<number, FWAudioPoolItem> = new Map();
23
+ private maxConcurrentAudio: number = 10;
24
+ private cleanupInterval: number = 5;
14
25
 
15
- playMusic(path: string, volume?: number, loop?: boolean);
16
- playMusic(music: cc.AudioClip, volume?: number, loop?: boolean);
17
- playMusic(assetProperty: FW.AssetProperty, volume?: number, loop?: boolean);
18
- async playMusic() {
26
+ public initialize(): void {
27
+ this.autoClear();
28
+ }
29
+
30
+ /**
31
+ * 播放背景音乐
32
+ */
33
+ async playMusic(path: string, volume?: number, loop?: boolean): Promise<void>;
34
+ async playMusic(music: cc.AudioClip, volume?: number, loop?: boolean): Promise<void>;
35
+ async playMusic(assetProperty: FW.AssetProperty, volume?: number, loop?: boolean): Promise<void>;
36
+ async playMusic(): Promise<void> {
19
37
  try {
20
- const data = await this.dataProcessing(arguments[0], arguments[1], arguments[2]);
21
- if (!data) return;
22
- const clip = data.clip;
23
- const volume = data.volume === undefined ? this.musicVolume : arguments[1];
24
- const loop = data.loop === undefined ? true : arguments[2];
25
- const id = cc.audioEngine.playMusic(clip, loop);
38
+ const audioData = await this.processAudioArguments(
39
+ arguments,
40
+ FWSystemDefine.FWAudioType.MUSIC,
41
+ );
42
+ if (!audioData.clip) return;
43
+
44
+ const volume = audioData.volume ?? this.musicVolume;
45
+ const loop = audioData.loop ?? true;
46
+
47
+ cc.audioEngine.stopMusic();
48
+ const id = cc.audioEngine.playMusic(audioData.clip, loop);
26
49
  cc.audioEngine.setVolume(id, volume);
50
+
51
+ FWLog.debug(`播放背景音乐: ${this.getAudioName(audioData.clip)}`);
27
52
  } catch (e) {
28
53
  FWLog.error('播放音乐失败:', e);
29
54
  }
@@ -32,255 +57,275 @@ export default class FWAudioManager extends FWManager implements FW.AudioManager
32
57
  stopMusic(): void {
33
58
  cc.audioEngine.stopMusic();
34
59
  }
60
+
35
61
  pauseMusic(): void {
36
62
  cc.audioEngine.pauseMusic();
37
63
  }
64
+
38
65
  resumeMusic(): void {
39
66
  cc.audioEngine.resumeMusic();
40
67
  }
41
- /**设置背景音效是否静音 */
68
+
69
+ /** 设置背景音效是否静音 */
42
70
  setMusicMute(mute: boolean): void {
43
- if (mute) {
44
- this.musicVolume = 1;
45
- } else {
46
- this.musicVolume = 0;
47
- }
71
+ this.musicVolume = mute ? 0 : 1;
48
72
  cc.audioEngine.setMusicVolume(this.musicVolume);
49
73
  }
50
74
 
75
+ /**
76
+ * 播放音效
77
+ */
51
78
  async play(
52
79
  path: string,
53
80
  cb?: (id: number) => void,
54
81
  volume?: number,
55
82
  loop?: boolean,
83
+ tag?: string,
56
84
  ): Promise<number>;
57
85
  async play(
58
86
  audio: cc.AudioClip,
59
87
  cb?: (id: number) => void,
60
88
  volume?: number,
61
89
  loop?: boolean,
90
+ tag?: string,
62
91
  ): Promise<number>;
63
92
  async play(
64
93
  assetProperty: FW.AssetProperty,
65
94
  cb?: (id: number) => void,
66
95
  volume?: number,
67
96
  loop?: boolean,
97
+ tag?: string,
68
98
  ): Promise<number>;
69
99
  async play(): Promise<number> {
70
100
  try {
71
- const data = await this.dataProcessing(
72
- arguments[0],
73
- arguments[1],
74
- arguments[2],
75
- arguments[3],
101
+ if (this.audioPool.size >= this.maxConcurrentAudio) {
102
+ this.clearFinishedAudio();
103
+ if (this.audioPool.size >= this.maxConcurrentAudio) {
104
+ FWLog.warn('音频池已满,无法播放新音频');
105
+ return -1;
106
+ }
107
+ }
108
+
109
+ const audioData = await this.processAudioArguments(
110
+ arguments,
111
+ FWSystemDefine.FWAudioType.SOUND,
76
112
  );
77
- if (!data) return;
78
- const clip = data.clip;
79
- const volume = data.volume === undefined ? this.soundVolume : data.volume;
80
- const loop = data.loop === undefined ? false : data.loop;
81
- const id = cc.audioEngine.play(clip, loop, volume);
113
+ if (!audioData.clip) return -1;
114
+
115
+ const volume = (audioData.volume ?? this.soundVolume) * this.effectsVolume;
116
+ const loop = audioData.loop ?? false;
117
+ const tag = audioData.tag;
118
+
119
+ const id = cc.audioEngine.play(audioData.clip, loop, volume);
120
+
121
+ this.audioPool.set(id, new FWAudioPoolItem(id, Date.now(), tag));
122
+
82
123
  cc.audioEngine.setFinishCallback(id, () => {
83
- data.cb?.(id);
124
+ audioData.cb?.(id);
125
+ this.audioPool.delete(id);
84
126
  });
127
+
85
128
  return id;
86
129
  } catch (e) {
87
130
  FWLog.error('播放音效失败:', e);
131
+ return -1;
88
132
  }
89
133
  }
134
+
90
135
  /**
91
- *
92
- * @param id 停止
136
+ * 停止指定音效
93
137
  */
94
- public stop(id: number) {
95
- cc.audioEngine.stop(id);
138
+ stop(id: number): void {
139
+ if (this.audioPool.has(id)) {
140
+ cc.audioEngine.stop(id);
141
+ this.audioPool.delete(id);
142
+ FWLog.debug(`停止音效: ID ${id}`);
143
+ }
96
144
  }
145
+
97
146
  pauseAll(): void {
98
147
  cc.audioEngine.pauseAll();
148
+ FWLog.debug('暂停所有音效');
99
149
  }
150
+
100
151
  resumeAll(): void {
101
152
  cc.audioEngine.resumeAll();
153
+ FWLog.debug('恢复所有音效');
102
154
  }
155
+
103
156
  stopAll(): void {
104
157
  cc.audioEngine.stopAll();
158
+ this.audioPool.clear();
159
+ FWLog.debug('停止所有音效');
105
160
  }
106
- /**设置播放音效是否静音 */
161
+
162
+ /** 设置播放音效是否静音 */
107
163
  setSoundMute(mute: boolean): void {
108
- if (mute) {
109
- this.soundVolume = 1;
110
- } else {
111
- this.soundVolume = 0;
112
- }
164
+ this.soundVolume = mute ? 0 : 1;
165
+ FWLog.debug(`音效${mute ? '静音' : '取消静音'}`);
113
166
  }
114
167
 
115
- /**播放音效 */
116
- async playEffect(path: string, loop?: boolean): Promise<number>;
117
- async playEffect(audio: cc.AudioClip, loop?: boolean): Promise<number>;
118
- async playEffect(assetProperty: FW.AssetProperty, loop?: boolean): Promise<number>;
119
- async playEffect(): Promise<number> {
168
+ /**
169
+ * 预加载音频资源
170
+ */
171
+ async preloadAudio(assetProperty: FW.AssetProperty): Promise<cc.AudioClip> {
120
172
  try {
121
- const data = await this.dataProcessing(arguments[0], arguments[1]);
122
- if (!data) return;
123
- const clip = data.clip;
124
- const loop = data.loop === undefined ? false : arguments[1];
125
- const id = cc.audioEngine.playEffect(clip, loop);
126
- return id;
173
+ let clip: cc.AudioClip;
174
+
175
+ if (typeof assetProperty === 'string') {
176
+ clip = await FW.Entry.resMgr.loadAsset<cc.AudioClip>({
177
+ path: assetProperty,
178
+ bundle: FW.Entry.bundleName,
179
+ });
180
+ } else {
181
+ clip = await FW.Entry.resMgr.loadAsset<cc.AudioClip>(assetProperty);
182
+ }
183
+
184
+ return clip;
127
185
  } catch (e) {
128
- FWLog.error('播放音效失败:', e);
186
+ return null;
129
187
  }
130
188
  }
131
189
 
132
190
  /**
133
- * 停止音效
134
- * @param effectName 声音文件名
135
- * @returns
191
+ * 释放音频资源
136
192
  */
137
- stopEffect(audioID: number): void {
138
- if (!audioID) return;
139
- return cc.audioEngine.stopEffect(audioID);
193
+ releaseAudio(assetProperty: FW.AssetProperty): void {
194
+ FW.Entry.resMgr.releaseAsset(assetProperty);
140
195
  }
196
+
141
197
  /**
142
- * 设置音效音量
143
- * @param volume 音量
198
+ * 获取音频池信息
144
199
  */
145
- setEffectsVolume(volume: number): void {
146
- cc.audioEngine.setEffectsVolume(volume);
200
+ getAudioPoolInfo(): { playing: number; maxConcurrent: number } {
201
+ return {
202
+ playing: this.audioPool.size,
203
+ maxConcurrent: this.maxConcurrentAudio,
204
+ };
147
205
  }
206
+
148
207
  /**
149
- * 停止播放所有音效
208
+ * 设置最大并发音频数
150
209
  */
151
- stopAllEffects(): void {
152
- cc.audioEngine.stopAllEffects();
210
+ setMaxConcurrentAudio(max: number): void {
211
+ this.maxConcurrentAudio = Math.max(1, max);
153
212
  }
154
- /**设置音效是否静音 */
155
- setEffectsMute(mute: boolean): void {
156
- if (mute) {
157
- this.effectsVolume = 1;
158
- } else {
159
- this.effectsVolume = 0;
160
- }
161
- cc.audioEngine.setEffectsVolume(this.effectsVolume);
213
+
214
+ /**
215
+ * 根据标签停止音效
216
+ */
217
+ stopByTag(tag: string): void {
218
+ const arr: number[] = [];
219
+
220
+ this.audioPool.forEach((item, id) => {
221
+ if (item.tag === tag) {
222
+ arr.push(id);
223
+ }
224
+ });
225
+
226
+ arr.forEach((id) => {
227
+ this.stop(id);
228
+ });
162
229
  }
163
230
 
164
- private async dataProcessing(
165
- path: string,
166
- cb?: (id: number) => void,
167
- volume?: number,
168
- loop?: boolean,
169
- ): Promise<{
170
- clip: cc.AudioClip;
171
- volume: number;
172
- loop: boolean;
173
- cb: (id: number) => void;
174
- }>;
175
-
176
- private async dataProcessing(
177
- audio: cc.AudioClip,
178
- cb?: (id: number) => void,
179
- volume?: number,
180
- loop?: boolean,
181
- ): Promise<{
182
- clip: cc.AudioClip;
183
- volume: number;
184
- loop: boolean;
185
- cb: (id: number) => void;
186
- }>;
187
-
188
- private async dataProcessing(
189
- assetProperty: FW.AssetProperty,
190
- cb?: (id: number) => void,
191
- volume?: number,
192
- loop?: boolean,
193
- ): Promise<{
194
- clip: cc.AudioClip;
195
- volume: number;
196
- loop: boolean;
197
- cb: (id: number) => void;
198
- }>;
199
-
200
- private async dataProcessing(
201
- path: string,
202
- volume?: number,
203
- loop?: boolean,
204
- ): Promise<{
205
- clip: cc.AudioClip;
206
- volume: number;
207
- loop: boolean;
208
- cb: (id: number) => void;
209
- }>;
210
- private async dataProcessing(
211
- music: cc.AudioClip,
212
- volume?: number,
213
- loop?: boolean,
214
- ): Promise<{
215
- clip: cc.AudioClip;
216
- volume: number;
217
- loop: boolean;
218
- cb: (id: number) => void;
219
- }>;
220
- private async dataProcessing(
221
- assetProperty: FW.AssetProperty,
222
- volume?: number,
223
- loop?: boolean,
224
- ): Promise<{
225
- clip: cc.AudioClip;
226
- volume: number;
227
- loop: boolean;
228
- cb: (id: number) => void;
229
- }>;
230
- private async dataProcessing(): Promise<{
231
- clip: cc.AudioClip;
232
- volume: number;
233
- loop: boolean;
234
- cb: (id: number) => void;
235
- }> {
236
- let clip: cc.AudioClip;
237
- if (typeof arguments[0] === 'string') {
238
- const bundle = FW.Entry.bundleName;
239
- const path = arguments[0];
240
- clip = await FW.Entry.resMgr.loadAsset<cc.AudioClip>(<FW.AssetProperty>{
241
- path: path,
242
- bundle: bundle,
243
- });
244
- } else if (typeof arguments[0] === 'object') {
245
- const bundle = (arguments[0] as FW.AssetProperty).bundle || FW.Entry.bundleName;
246
- const path = (arguments[0] as FW.AssetProperty).path;
247
- clip = await FW.Entry.resMgr.loadAsset<cc.AudioClip>(<FW.AssetProperty>{
248
- path: path,
249
- bundle: bundle,
250
- });
251
- } else if (arguments[0] instanceof cc.AudioClip) {
252
- clip = arguments[0];
231
+ /**
232
+ * 处理音频参数
233
+ */
234
+ private async processAudioArguments(
235
+ args: IArguments,
236
+ type: FWSystemDefine.FWAudioType,
237
+ ): Promise<FWAudioData> {
238
+ const audioData = new FWAudioData();
239
+ const argsArray = Array.from(args);
240
+
241
+ if (argsArray[0] instanceof cc.AudioClip) {
242
+ audioData.clip = argsArray[0];
243
+ } else {
244
+ const assetProperty = this.normalizeAssetProperty(argsArray[0]);
245
+ audioData.clip = await this.loadAudioClip(assetProperty);
253
246
  }
254
247
 
255
- let volume: number;
248
+ if (type === FWSystemDefine.FWAudioType.MUSIC) {
249
+ audioData.volume = argsArray[1];
250
+ audioData.loop = argsArray[2];
251
+ } else {
252
+ audioData.cb = argsArray[1];
253
+ audioData.volume = argsArray[2];
254
+ audioData.loop = argsArray[3];
255
+ audioData.tag = argsArray[4];
256
+ }
256
257
 
257
- let loop: boolean;
258
+ return audioData;
259
+ }
258
260
 
259
- let cb: (id: number) => void;
260
- if (typeof arguments[1] === 'function') {
261
- cb = arguments[1];
262
- } else if (typeof arguments[1] === 'number') {
263
- volume = arguments[1];
264
- } else if (typeof arguments[1] === 'boolean') {
265
- loop = arguments[1];
261
+ /**
262
+ * 标准化资源属性
263
+ */
264
+ private normalizeAssetProperty(input: any): FW.AssetProperty {
265
+ if (typeof input === 'string') {
266
+ return {
267
+ path: input,
268
+ bundle: FW.Entry.bundleName,
269
+ };
266
270
  }
271
+ return input;
272
+ }
267
273
 
268
- if (typeof arguments[2] === 'boolean') {
269
- loop = arguments[2];
270
- } else if (typeof arguments[2] === 'number') {
271
- volume = arguments[2];
274
+ /**
275
+ * 加载音频片段
276
+ */
277
+ private async loadAudioClip(assetProperty: FW.AssetProperty): Promise<cc.AudioClip> {
278
+ try {
279
+ return await FW.Entry.resMgr.loadAsset<cc.AudioClip>(assetProperty);
280
+ } catch (e) {
281
+ return null;
272
282
  }
283
+ }
273
284
 
274
- if (arguments.length == 4) {
275
- loop = arguments[3];
285
+ /**
286
+ * 生成缓存键
287
+ */
288
+ private getCacheKey(assetProperty: FW.AssetProperty): string {
289
+ if (typeof assetProperty === 'string') {
290
+ return `${FW.Entry.bundleName}_${assetProperty}`;
276
291
  }
292
+ return `${assetProperty.bundle || FW.Entry.bundleName}_${assetProperty.path}`;
293
+ }
277
294
 
278
- return {
279
- clip: clip,
280
- volume: volume,
281
- loop: loop,
282
- cb: cb,
283
- };
295
+ /**
296
+ * 获取音频名称
297
+ */
298
+ private getAudioName(clip: cc.AudioClip): string {
299
+ return clip.name || '未知音频';
300
+ }
301
+
302
+ /**
303
+ * 清理已完成的音频
304
+ */
305
+ private clearFinishedAudio(): void {
306
+ const currentTime = Date.now();
307
+ const maxAge = 30000;
308
+
309
+ this.audioPool.forEach((item, id) => {
310
+ if (!cc.audioEngine.getState(id)) {
311
+ this.audioPool.delete(id);
312
+ } else if (currentTime - item.startTime > maxAge) {
313
+ FWLog.warn(`强制停止超时音频: ID ${id}`);
314
+ this.stop(id);
315
+ }
316
+ });
317
+ }
318
+
319
+ /**
320
+ * 启动自动清理
321
+ */
322
+ private autoClear(): void {
323
+ FW.Entry.timeMgr.schedule(
324
+ this.clearFinishedAudio,
325
+ this.cleanupInterval,
326
+ cc.macro.REPEAT_FOREVER,
327
+ this,
328
+ );
284
329
  }
285
330
 
286
331
  public onDestroy(): void {
@@ -1,6 +1,7 @@
1
1
  import { FWManager } from './FWManager';
2
2
  import FWLog from '../log/FWLog';
3
3
  import Framework from '../Framework';
4
+ import { FWSystemConfig } from '../config/FWSystemConfig';
4
5
 
5
6
  export class FWBundleManager extends FWManager implements FW.BundleManager {
6
7
  bundleMap: Map<string, cc.AssetManager.Bundle>;
@@ -31,11 +32,8 @@ export class FWBundleManager extends FWManager implements FW.BundleManager {
31
32
  if (!bundleName) return;
32
33
  if (this.bundleMap.has(bundleName)) return this.bundleMap.get(bundleName);
33
34
 
34
- return new Promise(
35
- (
36
- resolve: (value: cc.AssetManager.Bundle | PromiseLike<cc.AssetManager.Bundle>) => void,
37
- reject: (reason?: any) => void,
38
- ) => {
35
+ return await this.invoke(
36
+ FW.Entry.promiseMgr.execute((resolve, reject, signal) => {
39
37
  const remote = this.remoteBundleConfigMap.get(bundleName);
40
38
  const url = remote ? remote.url : '';
41
39
  const path = `${url}${bundleName}`;
@@ -54,7 +52,8 @@ export class FWBundleManager extends FWManager implements FW.BundleManager {
54
52
  resolve(bundle);
55
53
  },
56
54
  );
57
- },
55
+ }, FWSystemConfig.PromiseConfig.loadBundle).promise,
56
+ `loadBundle -> ${bundleName}`,
58
57
  );
59
58
  }
60
59
 
@@ -18,6 +18,7 @@ class FWObserver implements FW.Observer {
18
18
  private readonly target: FW.TargetType,
19
19
  public readonly priority: number,
20
20
  public readonly intercept: boolean,
21
+ public readonly once: boolean = false,
21
22
  ) {
22
23
  this.cb = cb;
23
24
  this.target = target;
@@ -55,6 +56,7 @@ class FWObserver implements FW.Observer {
55
56
  args10,
56
57
  );
57
58
  }
59
+
58
60
  /**
59
61
  * 目标比较
60
62
  * @param target 目标对象
@@ -115,14 +117,31 @@ export default class FWEventManager implements FW.EventManager {
115
117
  ) {
116
118
  const observers = this.listeners[eventName];
117
119
  if (!observers) return;
120
+
118
121
  let shouldStopPropagation = false;
119
- for (const observer of observers) {
122
+ const observersToRemove: FW.Observer[] = [];
123
+
124
+ const observersCopy = [...observers];
125
+
126
+ for (const observer of observersCopy) {
120
127
  if (shouldStopPropagation) break;
128
+
129
+ if (observers.indexOf(observer) === -1) continue;
130
+
121
131
  observer.notify(args1, args2, args3, args4, args5, args6, args7, args8, args9, args10);
132
+
133
+ if (observer.once) {
134
+ observersToRemove.push(observer);
135
+ }
136
+
122
137
  if (observer.intercept) {
123
138
  shouldStopPropagation = true;
124
139
  }
125
140
  }
141
+
142
+ if (observersToRemove.length > 0) {
143
+ this.removeObservers(eventName, observersToRemove);
144
+ }
126
145
  }
127
146
 
128
147
  /**
@@ -149,20 +168,26 @@ export default class FWEventManager implements FW.EventManager {
149
168
  options?: {
150
169
  priority?: FWSystemDefine.FWPriorityOrder;
151
170
  intercept?: boolean;
171
+ once?: boolean;
152
172
  },
153
173
  ) {
154
174
  const observers = this.listeners[eventName];
155
175
  const define = FWSystemDefine.FWPriorityOrder;
156
176
  const priority = options?.priority === undefined ? define.NORMAL : options.priority;
157
177
  const intercept = options?.intercept === undefined ? false : options.intercept;
158
- const observer = new FWObserver(eventName, cb, target, priority, intercept);
178
+ const once = options?.once === undefined ? false : options.once;
179
+
180
+ const observer = new FWObserver(eventName, cb, target, priority, intercept, once);
181
+
159
182
  if (!observers) {
160
183
  this.listeners[eventName] = [];
161
184
  }
185
+
162
186
  const order = Object.values(define);
163
187
  const insertIndex = this.listeners[eventName].findIndex(
164
188
  (o) => order[o.priority] > order[priority],
165
189
  );
190
+
166
191
  if (insertIndex === -1) {
167
192
  this.listeners[eventName].push(observer);
168
193
  } else {
@@ -170,6 +195,39 @@ export default class FWEventManager implements FW.EventManager {
170
195
  }
171
196
  }
172
197
 
198
+ /**
199
+ * 注册一次性事件
200
+ * @param eventName 事件名
201
+ * @param cb 回调函数
202
+ * @param target 事件回调绑定目标
203
+ * @param options 选项
204
+ */
205
+ public registerOnce(
206
+ eventName: string | number,
207
+ cb: (
208
+ args1?: FW.EventManagerArgs,
209
+ args2?: FW.EventManagerArgs,
210
+ args3?: FW.EventManagerArgs,
211
+ args4?: FW.EventManagerArgs,
212
+ args5?: FW.EventManagerArgs,
213
+ args6?: FW.EventManagerArgs,
214
+ args7?: FW.EventManagerArgs,
215
+ args8?: FW.EventManagerArgs,
216
+ args9?: FW.EventManagerArgs,
217
+ args10?: FW.EventManagerArgs,
218
+ ) => void,
219
+ target: FW.TargetType,
220
+ options?: {
221
+ priority?: FWSystemDefine.FWPriorityOrder;
222
+ intercept?: boolean;
223
+ },
224
+ ) {
225
+ this.register(eventName, cb, target, {
226
+ ...options,
227
+ once: true, // 强制设置为一次性事件
228
+ });
229
+ }
230
+
173
231
  /**
174
232
  * 注销事件
175
233
  * @param eventName
@@ -268,6 +326,37 @@ export default class FWEventManager implements FW.EventManager {
268
326
  });
269
327
  }
270
328
 
329
+ /**
330
+ * 移除指定的观察者列表
331
+ * @param eventName 事件名
332
+ * @param observersToRemove 待移除的观察者列表
333
+ */
334
+ private removeObservers(eventName: string | number, observersToRemove: FW.Observer[]): void {
335
+ const observers = this.listeners[eventName];
336
+ if (!observers) return;
337
+
338
+ for (const observerToRemove of observersToRemove) {
339
+ const index = observers.indexOf(observerToRemove);
340
+ if (index !== -1) {
341
+ observers.splice(index, 1);
342
+ }
343
+ }
344
+
345
+ if (observers.length === 0) {
346
+ delete this.listeners[eventName];
347
+ }
348
+ }
349
+
350
+ /**
351
+ * 获取事件监听器数量
352
+ * @param eventName 事件名
353
+ * @returns 监听器数量
354
+ */
355
+ public getListenerCount(eventName: string | number): number {
356
+ const observers = this.listeners[eventName];
357
+ return observers ? observers.length : 0;
358
+ }
359
+
271
360
  /** 销毁 */
272
361
  public onDestroy(): void {
273
362
  this.listeners = cc.js.createMap();