GameUtil.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import { Vec2, Node, v2, PolygonCollider2D, Rect, CircleCollider2D, Vec3, UITransform, Camera } from "cc";
  2. import { AliensGlobalInstance } from "./AliensGlobalInstance";
  3. /** 游戏工具类 */
  4. export class GameUtil {
  5. /** 转换成hh:mm格式*/
  6. // static formatToTimeString(totalSeconds: number): string {
  7. // const minutes = Math.floor(totalSeconds / 60);
  8. // const seconds = totalSeconds % 60;
  9. // const formattedMinutes = String(minutes).padStart(2, '0');
  10. // const formattedSeconds = String(seconds).padStart(2, '0');
  11. // return `${formattedMinutes}:${formattedSeconds}`;
  12. // }
  13. /** 重量单位转换*/
  14. static formatWeight(weight: number): string {
  15. if (weight < 1000) {
  16. return `${weight}KG`;
  17. }
  18. // 等于或超过1000时,转换为吨(T),保留两位小数并向下取整
  19. const inTons = Math.floor((weight / 1000) * 100) / 100;
  20. return `${inTons}T`;
  21. }
  22. /**
  23. * 将 16 进制颜色转换为 RGBA 格式
  24. * @param hex - 16 进制颜色字符串 (#FFE73A 或 FFE73A)
  25. * @param alpha - 可选的透明度值(范围 0~255,默认 255)
  26. * @returns Color - 包含 r, g, b, a 的对象
  27. */
  28. static hexToRGBA(hex: string, alpha: number = 255): { r: number; g: number; b: number; a: number } {
  29. // 去掉可能存在的 '#' 前缀
  30. hex = hex.replace(/^#/, '');
  31. // 如果是简写格式 (如 #F7A),转换为完整格式 (FF77AA)
  32. if (hex.length === 3) {
  33. hex = hex.split('').map(char => char + char).join('');
  34. }
  35. // 转换为 r, g, b
  36. const r = parseInt(hex.substring(0, 2), 16);
  37. const g = parseInt(hex.substring(2, 4), 16);
  38. const b = parseInt(hex.substring(4, 6), 16);
  39. // 返回 RGBA 颜色对象
  40. return { r, g, b, a: alpha };
  41. }
  42. /** 获取节点的世界坐标并转换为 Vec2*/
  43. static getWorldPositionAsVec2(node: Node): Vec2 {
  44. const worldPosition = node.getWorldPosition().clone(); // 获取世界坐标
  45. return v2(worldPosition.x, worldPosition.y); // 转换为 Vec2
  46. }
  47. /** 射线检测
  48. * @param fromNode 起始节点
  49. * @param rayLength 射线长度
  50. * @return 射线结束点的世界坐标 (作为 Vec2)
  51. */
  52. static calculateRayEnd(fromNode: Node, rayLength: number): Vec2 {
  53. const rotation = fromNode.angle;
  54. // 根据角度计算方向向量
  55. let direction = v2(0, 0);
  56. if (rotation === -90) {
  57. direction = v2(1, 0); // 朝右
  58. } else if (rotation === 0) {
  59. direction = v2(0, 1); // 朝上
  60. } else if (rotation === 90) {
  61. direction = v2(-1, 0); // 朝左
  62. } else if (rotation === 180) {
  63. direction = v2(0, -1); // 朝下
  64. } else {
  65. const adjustedAngle = rotation - 90;
  66. direction = v2(-Math.cos(adjustedAngle * (Math.PI / 180)), Math.sin(-adjustedAngle * (Math.PI / 180)));
  67. }
  68. // 计算射线起点坐标
  69. const objs = this.getWorldPositionAsVec2(fromNode);
  70. const obje = objs.add(direction.multiplyScalar(rayLength));
  71. return obje;
  72. }
  73. /**
  74. * 判断两个 AABB 是否相交
  75. */
  76. static isAABBIntersecting(rect1: Rect, rect2: Rect): boolean {
  77. return !(
  78. rect1.xMax < rect2.xMin ||
  79. rect1.xMin > rect2.xMax ||
  80. rect1.yMax < rect2.yMin ||
  81. rect1.yMin > rect2.yMax
  82. );
  83. }
  84. /**
  85. * 检查点是否在多边形内
  86. * @param point 点的坐标
  87. * @param polygonPoints 多边形的顶点数组
  88. * @returns 是否在多边形内
  89. */
  90. static isPointInPolygon(point: Vec2, polygonPoints: Readonly<Vec2[]>): boolean {
  91. let isInside = false;
  92. for (let i = 0, j = polygonPoints.length - 1; i < polygonPoints.length; j = i++) {
  93. const xi = polygonPoints[i].x, yi = polygonPoints[i].y;
  94. const xj = polygonPoints[j].x, yj = polygonPoints[j].y;
  95. const intersect = (yi > point.y) !== (yj > point.y) &&
  96. point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
  97. if (intersect) isInside = !isInside;
  98. }
  99. return isInside;
  100. }
  101. /**
  102. * 检查圆是否与线段相交
  103. * @param circleCenter 圆心坐标
  104. * @param radius 圆的半径
  105. * @param lineStart 线段起点
  106. * @param lineEnd 线段终点
  107. * @returns 是否相交
  108. */
  109. static isCircleIntersectingLine(circleCenter: Vec2, radius: number, lineStart: Vec2, lineEnd: Vec2): boolean {
  110. const lineDir = lineEnd.subtract(lineStart);
  111. const toCircle = circleCenter.subtract(lineStart);
  112. const projection = toCircle.dot(lineDir.normalize());
  113. const closestPoint = lineStart.add(lineDir.normalize().multiplyScalar(projection));
  114. const distance = circleCenter.subtract(closestPoint).length();
  115. return distance <= radius;
  116. }
  117. /**
  118. * 获取圆形碰撞器的 AABB(轴对齐边界框)
  119. */
  120. static getCircleAABB(circleCollider: CircleCollider2D): Rect {
  121. const radius = circleCollider.radius;
  122. const center = circleCollider.worldPosition;
  123. const minX = center.x - radius;
  124. const minY = center.y - radius;
  125. const size = radius * 2;
  126. return new Rect(minX, minY, size, size);
  127. }
  128. /**
  129. * 获取多边形碰撞器的 AABB(轴对齐边界框)
  130. */
  131. static getPolygonAABB(polygonCollider: PolygonCollider2D): Rect {
  132. const points = polygonCollider.worldPoints;
  133. let minX = points[0].x;
  134. let maxX = points[0].x;
  135. let minY = points[0].y;
  136. let maxY = points[0].y;
  137. for (let i = 1; i < points.length; i++) {
  138. const p = points[i];
  139. if (p.x < minX) minX = p.x;
  140. if (p.x > maxX) maxX = p.x;
  141. if (p.y < minY) minY = p.y;
  142. if (p.y > maxY) maxY = p.y;
  143. }
  144. return new Rect(minX, minY, maxX - minX, maxY - minY);
  145. }
  146. /**
  147. * 判断多边形与圆是否相交
  148. * @param polygonCollider 多边形碰撞器
  149. * @param circleCollider 圆形碰撞器
  150. * @returns 是否相交
  151. */
  152. static isPolygonAndCircleIntersecting(
  153. polygonCollider: PolygonCollider2D,
  154. circleCollider: CircleCollider2D
  155. ): boolean {
  156. // 1. AABB 检测
  157. // const polygonAABB = this.getPolygonAABB(polygonCollider);
  158. // const circleAABB = this.getCircleAABB(circleCollider);
  159. // if (!this.isAABBIntersecting(polygonAABB, circleAABB)) {
  160. // return false;
  161. // }
  162. // 2. 精确检测
  163. const circleCenter = new Vec2(circleCollider.node.worldPosition.x, circleCollider.node.worldPosition.y); // 圆心
  164. const radius = circleCollider.radius;
  165. // 检查圆心是否在多边形内部
  166. if (this.isPointInPolygon(circleCenter, polygonCollider.worldPoints)) {
  167. return true;
  168. }
  169. // 检查圆是否与多边形的边相交
  170. const points = polygonCollider.worldPoints;
  171. for (let i = 0; i < points.length; i++) {
  172. const start = points[i];
  173. const end = points[(i + 1) % points.length];
  174. if (this.isCircleIntersectingLine(circleCenter, radius, start, end)) {
  175. return true;
  176. }
  177. }
  178. return false;
  179. }
  180. /** 3D空间坐标转屏幕坐标*/
  181. static worldToScreenLocal( target: Node, localNode: Node,carmera?:Camera,): Vec3 {
  182. if (!target || !target.worldPosition) return;
  183. const mainCamera = carmera ?? AliensGlobalInstance.instance.camera;
  184. let uiPos = new Vec3();
  185. mainCamera.convertToUINode(target.worldPosition.clone(), localNode, uiPos);
  186. return uiPos;
  187. }
  188. static delay(seconds) {
  189. return new Promise(resolve => setTimeout(resolve, seconds * 1000));
  190. }
  191. }