@chiyou/minigame-framework 1.2.69 → 1.3.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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterBilibili.ts +28 -2
  3. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterDesktopBrowser.ts +8 -1
  4. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterDouYin.ts +28 -2
  5. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterHonor.ts +31 -5
  6. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterHuaWei.ts +34 -1
  7. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterKuaiShou.ts +28 -2
  8. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterOppo.ts +17 -2
  9. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterTapTap.ts +31 -5
  10. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterVivo.ts +32 -4
  11. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterWeiXin.ts +28 -2
  12. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterXiaoMi.ts +31 -5
  13. package/src/Framework/Adapter/PlatformAdapter/PlatformAdapterZhiFuBao.ts +31 -5
  14. package/src/Framework/Definition/EventDefinition.ts +7 -0
  15. package/src/Framework/Definition/SystemDefinition.ts +12 -1
  16. package/src/Framework/Definition/TimerDefinition.ts +9 -0
  17. package/src/Framework/Definition/UIDefinition.ts +6 -0
  18. package/src/Framework/Manager/AudioMgr.ts +56 -7
  19. package/src/Framework/Manager/EventMgr.ts +31 -0
  20. package/src/Framework/Manager/InputMgr.ts +61 -0
  21. package/src/Framework/Manager/NodePoolMgr.ts +83 -4
  22. package/src/Framework/Manager/ResMgr.ts +41 -2
  23. package/src/Framework/Manager/TimerMgr.ts +53 -3
  24. package/src/Framework/Manager/UIMgr.ts +89 -9
  25. package/src/Framework/Utils/LogUtils.ts +27 -0
  26. package/src/Framework/Utils/ObjectUtils.ts +111 -0
  27. package/src/index.ts +2 -0
@@ -1,4 +1,5 @@
1
1
  import { LogUtils } from '../Utils/LogUtils';
2
+ import { EventListenerInfo } from '../Definition/EventDefinition';
2
3
  import { BaseMgr } from './BaseMgr';
3
4
 
4
5
  /** 事件项 */
@@ -73,6 +74,20 @@ export class EventMgr extends BaseMgr {
73
74
  this.eventList.set(eventName, array);
74
75
  }
75
76
 
77
+ /**
78
+ * 监听事件(只触发一次,触发后自动移除)
79
+ * @param eventName 事件名称
80
+ * @param callback 回调函数
81
+ * @param target 回调目标对象
82
+ */
83
+ public once(eventName: string, callback: Function, target: any): void {
84
+ const wrappedCallback = (...data: any[]) => {
85
+ this.off(eventName, wrappedCallback, target);
86
+ callback.apply(target, data);
87
+ };
88
+ this.on(eventName, wrappedCallback, target);
89
+ }
90
+
76
91
  /**
77
92
  * 取消监听事件
78
93
  * @param eventName 事件名称
@@ -128,6 +143,22 @@ export class EventMgr extends BaseMgr {
128
143
  }
129
144
  }
130
145
 
146
+ /**
147
+ * 获取指定事件的所有监听者(调试用)
148
+ * @param eventName 事件名称
149
+ * @return 监听者信息数组
150
+ */
151
+ public getListeners(eventName: string): EventListenerInfo[] {
152
+ const array = this.eventList.get(eventName);
153
+ if (!array || array.length === 0) {
154
+ return [];
155
+ }
156
+ return array.map(item => ({
157
+ callback: item.callback,
158
+ target: item.target
159
+ }));
160
+ }
161
+
131
162
  /**
132
163
  * 触发事件
133
164
  * @param eventName 事件名称
@@ -625,6 +625,36 @@ export class InputMgr extends BaseMgr {
625
625
  }
626
626
  }
627
627
 
628
+ /**
629
+ * 监听键盘按下(一次性)
630
+ * 触发一次后自动移除监听
631
+ * @param keyCode 按键码(KeyCode),KeyCode.NONE 表示监听所有按键
632
+ * @param callback 回调函数
633
+ * @param target 回调目标
634
+ */
635
+ public onceKeyDown(keyCode: KeyCode, callback: (event: EventKeyboard) => void, target: any): void {
636
+ let onceCallback = (event: EventKeyboard) => {
637
+ this.offKeyDown(keyCode, onceCallback, target);
638
+ callback.apply(target, [event]);
639
+ };
640
+ this.onKeyDown(keyCode, onceCallback, target);
641
+ }
642
+
643
+ /**
644
+ * 监听键盘抬起(一次性)
645
+ * 触发一次后自动移除监听
646
+ * @param keyCode 按键码(KeyCode),KeyCode.NONE 表示监听所有按键
647
+ * @param callback 回调函数
648
+ * @param target 回调目标
649
+ */
650
+ public onceKeyUp(keyCode: KeyCode, callback: (event: EventKeyboard) => void, target: any): void {
651
+ let onceCallback = (event: EventKeyboard) => {
652
+ this.offKeyUp(keyCode, onceCallback, target);
653
+ callback.apply(target, [event]);
654
+ };
655
+ this.onKeyUp(keyCode, onceCallback, target);
656
+ }
657
+
628
658
  /**
629
659
  * 判断按键是否处于按下状态
630
660
  * @param keyCode 按键码(KeyCode)
@@ -891,6 +921,22 @@ export class InputMgr extends BaseMgr {
891
921
  }
892
922
  }
893
923
 
924
+ /**
925
+ * 监听按钮点击(一次性)
926
+ * 触发一次后自动移除监听
927
+ * @param button 按钮节点
928
+ * @param callback 回调函数
929
+ * @param target 回调目标
930
+ * @param preventAccidental 是否防误触(连续点击),默认 true
931
+ */
932
+ public onceButtonClick(button: Node, callback: () => void, target: any, preventAccidental: boolean = true): void {
933
+ let onceCallback = () => {
934
+ this.offButtonClick(button, onceCallback, target);
935
+ callback.apply(target);
936
+ };
937
+ this.onButtonClick(button, onceCallback, target, preventAccidental);
938
+ }
939
+
894
940
  // ==================== 节点触摸事件(对外) ====================
895
941
 
896
942
  /**
@@ -1019,6 +1065,21 @@ export class InputMgr extends BaseMgr {
1019
1065
  }
1020
1066
  }
1021
1067
 
1068
+ /**
1069
+ * 监听节点触摸结束(一次性)
1070
+ * 触发一次后自动移除监听
1071
+ * @param node 节点
1072
+ * @param callback 回调函数
1073
+ * @param target 回调目标
1074
+ */
1075
+ public onceNodeTouchEnd(node: Node, callback: (touch: Touch) => void, target: any): void {
1076
+ let onceCallback = (touch: Touch) => {
1077
+ this.offNodeTouchEnd(node, onceCallback, target);
1078
+ callback.apply(target, [touch]);
1079
+ };
1080
+ this.onNodeTouchEnd(node, onceCallback, target);
1081
+ }
1082
+
1022
1083
  /**
1023
1084
  * 移除节点触摸事件
1024
1085
  */
@@ -3,7 +3,7 @@ import { BaseMgr } from './BaseMgr';
3
3
  import { LogUtils } from '../Utils/LogUtils';
4
4
  import { FwkErrorCode } from '../Definition/FwkErrorDefinition';
5
5
  import { ServiceLocator } from '../Utils/ServiceLocator';
6
- import type { UIMgr } from './UIMgr';
6
+ import { UIMgr, UICtrl } from './UIMgr';
7
7
 
8
8
  /** 节点池配置接口 */
9
9
  export interface IPoolConfig {
@@ -25,6 +25,8 @@ export interface IPoolStatistics {
25
25
  nowActiveCount: number;
26
26
  /** 历史最大活跃节点数 */
27
27
  maxActiveCount: number;
28
+ /** 当前可用节点数 */
29
+ availableCount: number;
28
30
  /** 最后访问时间 */
29
31
  lastAccessTime: number;
30
32
  }
@@ -49,6 +51,7 @@ export class PoolInfo {
49
51
  reusedCount: 0,
50
52
  nowActiveCount: 0,
51
53
  maxActiveCount: 0,
54
+ availableCount: 0,
52
55
  lastAccessTime: Date.now(),
53
56
  };
54
57
  }
@@ -229,8 +232,9 @@ export class NodePoolMgr extends BaseMgr {
229
232
  reusedCount: 0,
230
233
  nowActiveCount: 0,
231
234
  maxActiveCount: 0,
235
+ availableCount: 0,
232
236
  lastAccessTime: Date.now(),
233
- }
237
+ };
234
238
  }
235
239
  }
236
240
 
@@ -334,6 +338,15 @@ export class NodePoolMgr extends BaseMgr {
334
338
  let node: Node = poolInfo.pool.get() as Node;
335
339
  poolInfo.usingNodeSet.add(node);
336
340
 
341
+ // 重置 Transform(_resetNode 只管 Transform,不管业务组件)
342
+ this._resetNode(node);
343
+
344
+ // 调用业务层钩子 onPoolGet(顺序:_resetNode → onPoolGet → addChild → active)
345
+ let ctrl: UICtrl = node.getComponent(UICtrl);
346
+ if (ctrl && typeof ctrl.onPoolGet === 'function' && node.isValid) {
347
+ ctrl.onPoolGet(node);
348
+ }
349
+
337
350
  if (parent) {
338
351
  parent.addChild(node);
339
352
  }
@@ -466,6 +479,13 @@ export class NodePoolMgr extends BaseMgr {
466
479
  }
467
480
 
468
481
  let poolInfo: PoolInfo = this._poolInfoMap.get(poolName);
482
+ if (!poolInfo) {
483
+ LogUtils.Instance.error(NodePoolMgr.TAG, FwkErrorCode.NodePool.NotFound, {
484
+ operation: "_destoryNode",
485
+ poolName: poolName,
486
+ reason: "节点池信息获取失败"
487
+ });
488
+ }
469
489
 
470
490
  if (this.uiMgr === null) {
471
491
  this.uiMgr = ServiceLocator.Instance.get<UIMgr>("UIMgr");
@@ -480,7 +500,10 @@ export class NodePoolMgr extends BaseMgr {
480
500
  }
481
501
  }
482
502
 
483
- /** 回收节点(内部方法) */
503
+ /**
504
+ * 回收节点(内部方法)
505
+ * 调用顺序:NodePoolMgr.putNode → UICtrl.onPoolPut → NodePoolMgr._resetNode → node.active = false → 入池
506
+ */
484
507
  private _recycleNode(poolName: string, node: Node, resetNode: boolean = true): void {
485
508
  if (!node || !node.isValid) {
486
509
  return;
@@ -494,7 +517,12 @@ export class NodePoolMgr extends BaseMgr {
494
517
  node.removeFromParent();
495
518
  }
496
519
 
520
+ // 调用业务层钩子 onPoolPut(顺序:onPoolPut → _resetNode → active → 入池)
497
521
  if (resetNode) {
522
+ let ctrl: UICtrl = node.getComponent(UICtrl);
523
+ if (ctrl && typeof ctrl.onPoolPut === 'function' && node.isValid) {
524
+ ctrl.onPoolPut(node);
525
+ }
498
526
  this._resetNode(node);
499
527
  }
500
528
 
@@ -513,7 +541,11 @@ export class NodePoolMgr extends BaseMgr {
513
541
  }
514
542
  }
515
543
 
516
- /** 重置节点(内部方法) */
544
+ /**
545
+ * 重置节点 Transform(内部方法)
546
+ * 注意:只重置 position/rotation/scale,不管业务组件(如 UIOpacity 等)。
547
+ * 业务组件的清理由 onPoolPut 钩子兜底。
548
+ */
517
549
  private _resetNode(node: Node): void {
518
550
  if (!node || !node.isValid) {
519
551
  return;
@@ -524,6 +556,52 @@ export class NodePoolMgr extends BaseMgr {
524
556
  node.scale = Vec3.ONE;
525
557
  }
526
558
 
559
+ /**
560
+ * 检查节点池是否存在
561
+ * @param poolName 节点池名称
562
+ */
563
+ public hasPool(poolName: string): boolean {
564
+ return this._poolInfoMap.has(poolName);
565
+ }
566
+
567
+ /**
568
+ * 获取指定节点池的统计信息
569
+ * @param poolName 节点池名称
570
+ */
571
+ public getPoolStats(poolName: string): IPoolStatistics | null {
572
+ let poolInfo: PoolInfo = this._poolInfoMap.get(poolName);
573
+ if (!poolInfo) {
574
+ return null;
575
+ }
576
+
577
+ return {
578
+ createdCount: poolInfo.statistics.createdCount,
579
+ reusedCount: poolInfo.statistics.reusedCount,
580
+ nowActiveCount: poolInfo.statistics.nowActiveCount,
581
+ maxActiveCount: poolInfo.statistics.maxActiveCount,
582
+ availableCount: poolInfo.pool.size(),
583
+ lastAccessTime: poolInfo.statistics.lastAccessTime,
584
+ };
585
+ }
586
+
587
+ /**
588
+ * 获取所有节点池的统计信息
589
+ */
590
+ public getAllPoolStats(): Map<string, IPoolStatistics> {
591
+ let result: Map<string, IPoolStatistics> = new Map<string, IPoolStatistics>();
592
+ this._poolInfoMap.forEach((poolInfo: PoolInfo, poolName: string) => {
593
+ result.set(poolName, {
594
+ createdCount: poolInfo.statistics.createdCount,
595
+ reusedCount: poolInfo.statistics.reusedCount,
596
+ nowActiveCount: poolInfo.statistics.nowActiveCount,
597
+ maxActiveCount: poolInfo.statistics.maxActiveCount,
598
+ availableCount: poolInfo.pool.size(),
599
+ lastAccessTime: poolInfo.statistics.lastAccessTime,
600
+ });
601
+ });
602
+ return result;
603
+ }
604
+
527
605
  /** 打印节点池状态(调试用) */
528
606
  private _printPoolState(poolName: string): void {
529
607
  let poolInfo: PoolInfo = this._poolInfoMap.get(poolName);
@@ -552,6 +630,7 @@ export class NodePoolMgr extends BaseMgr {
552
630
  reusedCount: poolInfo.statistics.reusedCount,
553
631
  nowActiveCount: poolInfo.statistics.nowActiveCount,
554
632
  maxActiveCount: poolInfo.statistics.maxActiveCount,
633
+ availableCount: poolInfo.pool.size(),
555
634
  lastAccessTime: poolInfo.statistics.lastAccessTime
556
635
  });
557
636
  }
@@ -575,11 +575,12 @@ export class ResMgr extends BaseMgr {
575
575
  }
576
576
 
577
577
  /**
578
- * 获取资源
578
+ * 获取资源(同步)
579
+ * @description 必须先通过 preloadResBatch 预加载或 loadRes/loadResAsync 加载目标资源后,才能使用 getAsset 获取。同一帧内不支持同步触发加载。
579
580
  * @param bundleName AssetBundle 名称
580
581
  * @param url 资源路径
581
582
  * @param assetType 资源类型
582
- * @return 资源实例
583
+ * @return 资源实例(未加载时返回 null)
583
584
  */
584
585
  public getAsset<T extends Asset>(bundleName: string, url: string, assetType?: typeof Asset): T | null {
585
586
  if (!bundleName || bundleName.trim() === "") {
@@ -739,6 +740,44 @@ export class ResMgr extends BaseMgr {
739
740
  });
740
741
  }
741
742
 
743
+ /**
744
+ * 动态加载资源(Promise 版本)
745
+ * @param bundleName AssetBundle 名称
746
+ * @param url 资源路径
747
+ * @param assetType 资源类型
748
+ * @return Promise<Asset> 资源实例
749
+ */
750
+ public loadResAsync<T extends Asset>(bundleName: string, url: string, assetType: typeof Asset): Promise<T> {
751
+ return new Promise<T>((resolve, reject) => {
752
+ this.loadRes(bundleName, url, assetType, (success: boolean) => {
753
+ if (success) {
754
+ let asset = this.getAsset<T>(bundleName, url, assetType);
755
+ if (asset) {
756
+ resolve(asset);
757
+ } else {
758
+ LogUtils.Instance.error(ResMgr.TAG, FwkErrorCode.Res.NotFound, {
759
+ operation: "loadResAsync",
760
+ reason: "loadRes 成功但 getAsset 返回 null(可能原因:图集子资源 URL 不正确,或资源类型不匹配)",
761
+ bundleName: bundleName,
762
+ url: url,
763
+ assetType: assetType?.name || "未指定"
764
+ });
765
+ reject(new Error(`loadResAsync: getAsset returned null, bundle=${bundleName}, url=${url}`));
766
+ }
767
+ } else {
768
+ LogUtils.Instance.error(ResMgr.TAG, FwkErrorCode.Res.LoadAssetFailed, {
769
+ operation: "loadResAsync",
770
+ reason: "loadRes 加载失败",
771
+ bundleName: bundleName,
772
+ url: url,
773
+ assetType: assetType?.name || "未指定"
774
+ });
775
+ reject(new Error(`loadResAsync failed: bundle=${bundleName}, url=${url}`));
776
+ }
777
+ });
778
+ });
779
+ }
780
+
742
781
  /**
743
782
  * 加载远程图片
744
783
  * @param imageUrl 图片 URL
@@ -1,7 +1,10 @@
1
1
  import { FwkErrorCode } from "../Definition/FwkErrorDefinition";
2
+ import { FrameworkBase } from "../Definition/FrameworkBase";
3
+ import { TimerRepeat } from "../Definition/TimerDefinition";
2
4
  import { LogUtils } from "../Utils/LogUtils";
3
5
  import { ServiceLocator } from "../Utils/ServiceLocator";
4
6
  import { BaseMgr } from "./BaseMgr";
7
+ import { EventMgr } from "./EventMgr";
5
8
 
6
9
  interface ITimer {
7
10
  tick: number;
@@ -18,6 +21,7 @@ export class TimerMgr extends BaseMgr {
18
21
  static TAG: string = "TimerMgr";
19
22
 
20
23
  private timers: ITimer[] = [];
24
+ private _paused: boolean = false;
21
25
 
22
26
  onLoad(): void {
23
27
  super.onLoad();
@@ -30,6 +34,10 @@ export class TimerMgr extends BaseMgr {
30
34
  }
31
35
 
32
36
  ServiceLocator.Instance.register("TimerMgr", this);
37
+
38
+ // 监听生命周期事件,切后台暂停定时器
39
+ EventMgr.Instance.on(FrameworkBase.Message.LifeCycle_onGameHide, this._onGameHide, this);
40
+ EventMgr.Instance.on(FrameworkBase.Message.LifeCycle_onGameShow, this._onGameShow, this);
33
41
  }
34
42
 
35
43
  /**
@@ -37,23 +45,63 @@ export class TimerMgr extends BaseMgr {
37
45
  */
38
46
  public init(): void {
39
47
  this.timers = [];
48
+ this._paused = false;
40
49
 
41
50
  LogUtils.Instance.info(TimerMgr.TAG, "初始化完成");
42
51
  }
43
52
 
53
+ /**
54
+ * 暂停所有定时器
55
+ */
56
+ public pauseAll(): void {
57
+ if (this._paused) return;
58
+ this._paused = true;
59
+ LogUtils.Instance.info(TimerMgr.TAG, "定时器已暂停");
60
+ }
61
+
62
+ /**
63
+ * 恢复所有定时器
64
+ */
65
+ public resumeAll(): void {
66
+ if (!this._paused) return;
67
+ this._paused = false;
68
+ LogUtils.Instance.info(TimerMgr.TAG, "定时器已恢复");
69
+ }
70
+
71
+ /**
72
+ * 定时器是否暂停中
73
+ */
74
+ public isPaused(): boolean {
75
+ return this._paused;
76
+ }
77
+
78
+ private _onGameHide(): void {
79
+ this.pauseAll();
80
+ }
81
+
82
+ private _onGameShow(): void {
83
+ this.resumeAll();
84
+ }
85
+
86
+ onDestroy(): void {
87
+ EventMgr.Instance.off(FrameworkBase.Message.LifeCycle_onGameHide, this._onGameHide, this);
88
+ EventMgr.Instance.off(FrameworkBase.Message.LifeCycle_onGameShow, this._onGameShow, this);
89
+ super.onDestroy();
90
+ }
91
+
44
92
  /**
45
93
  * 添加定时器
46
94
  * @param delay 延迟时间(秒)
47
95
  * @param interval 间隔时间(秒)
48
- * @param repeat 重复次数,0表示拒绝,负数表示无限
96
+ * @param repeat 重复次数:TimerRepeat.Once(1)= 执行一次后移除,TimerRepeat.Infinite(-1)= 无限循环,0 = 不执行(立即跳过),>0 = 执行指定次数
49
97
  * @param func 回调函数
50
98
  * @param target 回调目标对象
51
99
  */
52
- public addTimer(delay: number, interval: number, repeat: number, func: Function, target: Object) {
100
+ public addTimer(delay: number, interval: number, repeat: TimerRepeat | number, func: Function, target: Object) {
53
101
  if (repeat === 0) {
54
102
  LogUtils.Instance.error(TimerMgr.TAG, FwkErrorCode.Timer.InvalidState, {
55
103
  operation: "addTimer",
56
- reason: "重复次数为0",
104
+ reason: "repeat=0 拒绝添加(不支持),应使用 TimerRepeat.Once",
57
105
  delay: delay,
58
106
  interval: interval,
59
107
  repeat: repeat
@@ -117,6 +165,8 @@ export class TimerMgr extends BaseMgr {
117
165
  }
118
166
 
119
167
  protected update(dt: number): void {
168
+ if (this._paused) return;
169
+
120
170
  for (let index = 0; index < this.timers.length; index++) {
121
171
  const item = this.timers[index];
122
172
  item.tick += dt;
@@ -1,10 +1,12 @@
1
- import { Component, Node, Button, Prefab, instantiate, screen, UITransform, v3, view, ResolutionPolicy, math, Tween, tween, Label, Color, Sprite, SpriteFrame, BlockInputEvents, Texture2D } from 'cc';
1
+ import { Component, Node, Button, Prefab, instantiate, screen, UITransform, v3, view, ResolutionPolicy, math, Tween, tween, Label, Color, Sprite, SpriteFrame, BlockInputEvents, Texture2D, Vec3 } from 'cc';
2
2
  import { FwkErrorCode } from '../Definition/FwkErrorDefinition';
3
3
  import { LogUtils } from '../Utils/LogUtils';
4
- import { ToastDuration } from '../Definition/UIDefinition';
4
+ import { ToastDuration, ToastPosition } from '../Definition/UIDefinition';
5
5
  import { BaseMgr } from './BaseMgr';
6
6
  import { ServiceLocator } from '../Utils/ServiceLocator';
7
7
  import type { ResMgr } from './ResMgr';
8
+ import type { SystemMgr } from './SystemMgr';
9
+ import type { InputMgr } from './InputMgr';
8
10
 
9
11
  /** UI 控制器基类 */
10
12
  export class UICtrl extends Component {
@@ -16,6 +18,20 @@ export class UICtrl extends Component {
16
18
  this.load_all_object(this.node, "");
17
19
  }
18
20
 
21
+ /**
22
+ * 节点从池中取出时调用(UICtrl 子类可 override)
23
+ * 调用顺序:NodePoolMgr.getNode → NodePoolMgr._resetNode → UICtrl.onPoolGet → parent.addChild → node.active = true
24
+ * @param node 从池中取出的节点
25
+ */
26
+ public onPoolGet?(node: Node): void;
27
+
28
+ /**
29
+ * 节点归还到池中时调用(UICtrl 子类可 override)
30
+ * 调用顺序:NodePoolMgr.putNode → UICtrl.onPoolPut → NodePoolMgr._resetNode → node.active = false → 入池
31
+ * @param node 归还到池中的节点
32
+ */
33
+ public onPoolPut?(node: Node): void;
34
+
19
35
  /** 加载所有子节点到视图映射 */
20
36
  private load_all_object(root: Node, path: string): void {
21
37
  for (let i = 0; i < root.children.length; i++) {
@@ -111,6 +127,8 @@ export class UIMgr extends BaseMgr {
111
127
  private toastHistoryMaxCount: number = 10;
112
128
 
113
129
  private resMgr: ResMgr = null;
130
+ private inputMgr: InputMgr = null;
131
+ private systemMgr: SystemMgr = null;
114
132
 
115
133
  /** 打印 UI 映射(调试用) */
116
134
  public print(): void {
@@ -269,6 +287,10 @@ export class UIMgr extends BaseMgr {
269
287
  this.resMgr = ServiceLocator.Instance.get<ResMgr>("ResMgr");
270
288
  }
271
289
 
290
+ if (this.inputMgr === null) {
291
+ this.inputMgr = ServiceLocator.Instance.get<InputMgr>("InputMgr");
292
+ }
293
+
272
294
  if (!this.resMgr) {
273
295
  return null;
274
296
  }
@@ -420,12 +442,18 @@ export class UIMgr extends BaseMgr {
420
442
  * 显示 Toast
421
443
  * @param content 显示内容
422
444
  * @param toastDuration 显示时长
445
+ * @param position 显示位置(ToastPosition 枚举或世界坐标 Vec3),默认居中
423
446
  */
424
- public showToast(content: string, toastDuration: ToastDuration = ToastDuration.Duration_Short): void {
447
+ public showToast(content: string, toastDuration: ToastDuration = ToastDuration.Duration_Short, position?: ToastPosition | Vec3): void {
425
448
  if (this.node_toastRoot === null) {
426
449
  return;
427
450
  }
428
451
 
452
+ // 延迟加载 SystemMgr(避免循环依赖)
453
+ if (this.systemMgr === null) {
454
+ this.systemMgr = ServiceLocator.Instance.get<SystemMgr>("SystemMgr");
455
+ }
456
+
429
457
  // 记录 Toast 历史
430
458
  this.toastHistory.push(content);
431
459
  if (this.toastHistory.length > this.toastHistoryMaxCount) {
@@ -449,6 +477,33 @@ export class UIMgr extends BaseMgr {
449
477
 
450
478
  this.node_toastRoot.active = true;
451
479
  this.node_toastRoot.getComponent(UITransform).width = textWidth + 80;
480
+
481
+ // 设置 Toast 位置
482
+ if (position !== undefined) {
483
+ if (position instanceof Vec3) {
484
+ // Vec3 世界坐标,转换为节点坐标
485
+ let localPos: Vec3 = this.node_toastRoot.getComponent(UITransform).convertToNodeSpaceAR(position);
486
+ this.node_toastRoot.setPosition(localPos.x, localPos.y);
487
+ } else {
488
+ // ToastPosition 枚举
489
+ let x: number = 0;
490
+ let y: number = 0;
491
+ if (this.systemMgr) {
492
+ let screenInfo = this.systemMgr.getScreenInfo();
493
+ let screenH: number = screenInfo.windowHeight;
494
+ let edgeOffset: number = 150; // 距屏幕边缘的距离
495
+ if (position === ToastPosition.Top) {
496
+ y = screenH / 2 - edgeOffset;
497
+ } else if (position === ToastPosition.Bottom) {
498
+ y = -screenH / 2 + edgeOffset;
499
+ }
500
+ // Center: x=0, y=0
501
+ }
502
+ // systemMgr 为空时默认居中(x=0, y=0)
503
+ this.node_toastRoot.setPosition(x, y);
504
+ }
505
+ }
506
+
452
507
  this.tween_toast = tween(this.node_toastRoot)
453
508
  .delay(showTime)
454
509
  .call(() => {
@@ -469,13 +524,10 @@ export class UIMgr extends BaseMgr {
469
524
  /**
470
525
  * 打开 Dialog
471
526
  * @param ui_name UI 名称
472
- * @param maskColor 遮罩颜色
473
- * @return Dialog 节点
527
+ * @param maskColor 遮罩颜色(默认半透明黑)
528
+ * @param closeOnMaskClick 点击遮罩是否关闭 Dialog(默认 false)
474
529
  */
475
- public openDialog(ui_name: string, maskColor?: Color): Node {
476
- if (!maskColor) {
477
- maskColor = new Color(51, 51, 51, 230);
478
- }
530
+ public openDialog(ui_name: string, maskColor: Color = new Color(51, 51, 51, 230), closeOnMaskClick: boolean = false): Node {
479
531
 
480
532
  const whitePixelData = new Uint8Array([maskColor.r, maskColor.g, maskColor.b, maskColor.a]);
481
533
  const texture = new Texture2D();
@@ -505,6 +557,18 @@ export class UIMgr extends BaseMgr {
505
557
  if (node_dialog) {
506
558
  this.node_dialogRoot.addChild(node_agent);
507
559
  this.node_dialogRoot.active = true;
560
+
561
+ // 点击遮罩关闭 Dialog
562
+ if (closeOnMaskClick) {
563
+ let maskTouchCallback = () => {
564
+ this.closeDialog(ui_name, node_dialog);
565
+ };
566
+ // 将回调存到 node_mask 上,方便 closeDialog 时清理
567
+ (node_mask as any)._maskTouchCallback = maskTouchCallback;
568
+ if (this.inputMgr) {
569
+ this.inputMgr.onNodeTouchEnd(node_mask, maskTouchCallback, this);
570
+ }
571
+ }
508
572
  } else {
509
573
  node_agent.destroy();
510
574
  }
@@ -532,6 +596,14 @@ export class UIMgr extends BaseMgr {
532
596
 
533
597
  if (hasNode) {
534
598
  let node_agent: Node = node.parent;
599
+ // 清理点击遮罩关闭的监听器(如果存在)
600
+ let node_mask: Node = node_agent.getChildByName("Node_Mask");
601
+ if (node_mask && (node_mask as any)._maskTouchCallback) {
602
+ if (this.inputMgr) {
603
+ this.inputMgr.offNodeTouchEnd(node_mask, (node_mask as any)._maskTouchCallback, this);
604
+ }
605
+ (node_mask as any)._maskTouchCallback = null;
606
+ }
535
607
  this.destroy_ui(ui_name, node);
536
608
  node_agent.destroy();
537
609
  }
@@ -560,6 +632,14 @@ export class UIMgr extends BaseMgr {
560
632
  if (child.name !== "Node_Mask") {
561
633
  let uiName = this._nodeToUIName(child);
562
634
  if (uiName) {
635
+ // 清理点击遮罩关闭的监听器(如果存在)
636
+ let node_mask: Node = agent.getChildByName("Node_Mask");
637
+ if (node_mask && (node_mask as any)._maskTouchCallback) {
638
+ if (this.inputMgr) {
639
+ this.inputMgr.offNodeTouchEnd(node_mask, (node_mask as any)._maskTouchCallback, this);
640
+ }
641
+ (node_mask as any)._maskTouchCallback = null;
642
+ }
563
643
  this.destroy_ui(uiName, child);
564
644
  closedCount++;
565
645
  break;
@@ -9,6 +9,7 @@ export class LogUtils {
9
9
  static TAG: string = "LogUtils";
10
10
 
11
11
  private isDebug: boolean = false;
12
+ private _timers: Map<string, number> = new Map();
12
13
 
13
14
  /**
14
15
  * 初始化日志模块
@@ -133,6 +134,32 @@ export class LogUtils {
133
134
  return `[${level}][${tag}][${timestamp}]: ${content}`;
134
135
  }
135
136
 
137
+ /**
138
+ * 开始计时
139
+ * @param label 计时标签
140
+ */
141
+ public timeStart(label: string): void {
142
+ if (!this.isDebug) return;
143
+ this._timers.set(label, Date.now());
144
+ console.log(this.formatLog("TIMER", label, "计时开始"));
145
+ }
146
+
147
+ /**
148
+ * 结束计时并输出耗时
149
+ * @param label 计时标签(需与 timeStart 匹配)
150
+ */
151
+ public timeEnd(label: string): void {
152
+ if (!this.isDebug) return;
153
+ const startTime = this._timers.get(label);
154
+ if (startTime === undefined) {
155
+ console.warn(this.formatLog("TIMER", label, "未找到计时起点"));
156
+ return;
157
+ }
158
+ const elapsed = Date.now() - startTime;
159
+ this._timers.delete(label);
160
+ console.log(this.formatLog("TIMER", label, `耗时 ${elapsed}ms`));
161
+ }
162
+
136
163
  /**
137
164
  * 格式化值
138
165
  */