Tips.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {
  2. _decorator,
  3. Component,
  4. Node,
  5. Prefab,
  6. instantiate,
  7. Canvas,
  8. Vec3,
  9. find,
  10. director,
  11. UITransform,
  12. resources,
  13. tween,
  14. Tween,
  15. } from "cc";
  16. import { TipItem } from "./TipItem";
  17. const { ccclass, property } = _decorator;
  18. @ccclass("Tips")
  19. export class Tips extends Component {
  20. public static show(content: string, worldPos?: Vec3) {
  21. Tips.ins.showTip(content, worldPos);
  22. }
  23. private static _ins: Tips = null!;
  24. // TipItem预制体
  25. @property(Prefab)
  26. public tipPrefab: Prefab = null!;
  27. private tipContainer: Node = null!;
  28. // 对象池
  29. private tipPool: Node[] = [];
  30. private activeTips: Node[] = [];
  31. // 配置参数
  32. private readonly MAX_POOL_SIZE = 20; // 对象池最大数量
  33. private readonly MAX_ACTIVE_TIPS = 10; // 同时显示的最大提示数量
  34. private offsetY = 0; // Y轴偏移,用于错开显示位置
  35. public static get ins(): Tips {
  36. return this._ins ??= new Tips();
  37. }
  38. protected onLoad(): void {
  39. Tips._ins = this;
  40. director.addPersistRootNode(this.node);
  41. this.createContainer(this.node);
  42. this.preCreateTipItems();
  43. }
  44. /**
  45. * 创建提示容器
  46. */
  47. private createContainer(parent: Node): void {
  48. // 创建TipContainer
  49. this.tipContainer = new Node("TipContainer");
  50. this.tipContainer.addComponent(UITransform);
  51. this.tipContainer.parent = parent;
  52. // 设置容器层级较高,确保在最前面显示
  53. this.tipContainer.setSiblingIndex(999);
  54. }
  55. /**
  56. * 预创建TipItem到对象池
  57. */
  58. private preCreateTipItems(): void {
  59. for (let i = 0; i < 5; i++) {
  60. const tipNode = this.createTipItem();
  61. tipNode.active = false;
  62. this.tipPool.push(tipNode);
  63. }
  64. }
  65. /**
  66. * 创建TipItem节点
  67. */
  68. private createTipItem(): Node {
  69. if (!this.tipPrefab) {
  70. console.error("TipItem预制体未加载");
  71. return null!;
  72. }
  73. const tipNode = instantiate(this.tipPrefab);
  74. tipNode.parent = this.tipContainer;
  75. // 确保有TipItem组件
  76. let tipComponent = tipNode.getComponent(TipItem);
  77. if (!tipComponent) {
  78. tipComponent = tipNode.addComponent(TipItem);
  79. }
  80. return tipNode;
  81. }
  82. /**
  83. * 从对象池获取TipItem
  84. */
  85. private getTipItemFromPool(): Node {
  86. // 先检查是否有可用的池对象
  87. for (let i = 0; i < this.tipPool.length; i++) {
  88. const tipNode = this.tipPool[i];
  89. const tipComponent = tipNode.getComponent(TipItem);
  90. if (tipComponent && !tipComponent.getIsInUse()) {
  91. this.tipPool.splice(i, 1);
  92. return tipNode;
  93. }
  94. }
  95. // 如果池中没有可用对象,且总数未达到上限,创建新的
  96. const totalCount = this.tipPool.length + this.activeTips.length;
  97. if (totalCount < this.MAX_POOL_SIZE) {
  98. return this.createTipItem();
  99. }
  100. // 如果达到最大数量,强制回收最老的提示
  101. if (this.activeTips.length > 0) {
  102. const oldestTip = this.activeTips[0];
  103. const tipComponent = oldestTip.getComponent(TipItem);
  104. if (tipComponent) {
  105. tipComponent.forceRecycle();
  106. }
  107. // 回收后应该有可用的对象了,再次检查池
  108. for (let i = 0; i < this.tipPool.length; i++) {
  109. const tipNode = this.tipPool[i];
  110. const tipComponent = tipNode.getComponent(TipItem);
  111. if (tipComponent && !tipComponent.getIsInUse()) {
  112. this.tipPool.splice(i, 1);
  113. return tipNode;
  114. }
  115. }
  116. }
  117. // 最后的备选方案,创建新的(但这种情况应该很少发生)
  118. console.warn("对象池逻辑异常,创建新的TipItem");
  119. return this.createTipItem();
  120. }
  121. /**
  122. * 回收TipItem到对象池
  123. */
  124. public recycleTipItem(tipNode: Node): void {
  125. // 从激活列表中移除
  126. const index = this.activeTips.indexOf(tipNode);
  127. if (index >= 0) {
  128. this.activeTips.splice(index, 1);
  129. }
  130. // 添加到池中(节点已经在TipItem.recycle()中设置为inactive)
  131. if (this.tipPool.length < this.MAX_POOL_SIZE) {
  132. this.tipPool.push(tipNode);
  133. } else {
  134. // 池已满,销毁节点
  135. tipNode.destroy();
  136. }
  137. }
  138. /**
  139. * 调整Y轴偏移
  140. */
  141. private adjustOffsetY(): void {
  142. this.offsetY = Math.max(0, this.offsetY - 80);
  143. }
  144. /**
  145. * 将所有现有的提示向上推移
  146. */
  147. private pushExistingTipsUp(): void {
  148. for (const tipNode of this.activeTips) {
  149. // 先停止该节点上所有的tween动画,避免动画冲突
  150. Tween.stopAllByTarget(tipNode);
  151. const currentPos = tipNode.getPosition();
  152. const newPos = new Vec3(currentPos.x, currentPos.y + 80, currentPos.z);
  153. // 使用缓动动画让推移更平滑
  154. tween(tipNode)
  155. .to(0.2, { position: newPos }, { easing: "quadOut" })
  156. .start();
  157. }
  158. }
  159. /**
  160. * 显示提示
  161. * @param content 提示内容
  162. * @param worldPos 世界坐标位置,如果不传则显示在屏幕中心上方
  163. */
  164. public showTip(content: string, worldPos?: Vec3): void {
  165. if (!this.tipContainer) {
  166. console.error("TipManager未初始化");
  167. return;
  168. }
  169. // 检查是否超过最大显示数量,如果超过则强制回收最老的提示
  170. if (this.activeTips.length >= this.MAX_ACTIVE_TIPS) {
  171. const oldestTip = this.activeTips[0];
  172. const tipComponent = oldestTip.getComponent(TipItem);
  173. if (tipComponent) {
  174. tipComponent.forceRecycle();
  175. }
  176. // 注意:forceRecycle会调用recycle,recycle会调用recycleTipItem,
  177. // recycleTipItem会从activeTips中移除,所以这里不需要手动移除
  178. }
  179. // 将所有现有的提示向上推移
  180. //this.pushExistingTipsUp();
  181. // 获取TipItem
  182. const tipNode = this.getTipItemFromPool();
  183. if (!tipNode) {
  184. console.error("无法获取TipItem");
  185. return;
  186. }
  187. // 计算显示位置 - 新提示始终在基础位置显示
  188. let displayPos: Vec3;
  189. if (worldPos) {
  190. // 转换世界坐标到UI坐标
  191. displayPos = this.worldToUIPos(worldPos);
  192. } else {
  193. // 新提示始终在固定位置显示
  194. displayPos = new Vec3(0, 200, 0);
  195. }
  196. // 添加到激活列表
  197. this.activeTips.push(tipNode);
  198. // 初始化并显示
  199. const tipComponent = tipNode.getComponent(TipItem);
  200. if (tipComponent) {
  201. tipComponent.init(content, displayPos);
  202. }
  203. }
  204. /**
  205. * 将世界坐标转换为UI坐标
  206. */
  207. private worldToUIPos(worldPos: Vec3): Vec3 {
  208. // 这里需要根据实际的坐标系统来转换
  209. // 简单实现,可以根据需要调整
  210. return new Vec3(worldPos.x, worldPos.y + 100, 0);
  211. }
  212. /**
  213. * 清除所有提示
  214. */
  215. public clearAllTips(): void {
  216. // 强制回收所有激活的提示
  217. for (const tipNode of this.activeTips) {
  218. const tipComponent = tipNode.getComponent(TipItem);
  219. if (tipComponent) {
  220. tipComponent.forceRecycle();
  221. }
  222. }
  223. }
  224. /**
  225. * 销毁管理器
  226. */
  227. public onDestroy(): void {
  228. if (2 > 1) {
  229. return;
  230. }
  231. this.clearAllTips();
  232. // 销毁对象池中的所有节点
  233. for (const tipNode of this.tipPool) {
  234. tipNode.destroy();
  235. }
  236. this.tipPool = [];
  237. this.activeTips = [];
  238. // 销毁容器
  239. if (this.tipContainer) {
  240. this.tipContainer.destroy();
  241. this.tipContainer = null!;
  242. }
  243. Tips._ins = null!;
  244. }
  245. }