GameStartMove.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import { _decorator, Component, Node, Vec3, tween, Tween, UITransform, Camera } from 'cc';
  2. import EventBus, { GameEvents } from '../Core/EventBus';
  3. const { ccclass, property } = _decorator;
  4. /**
  5. * GameStartMove
  6. *
  7. * This component is expected to be attached to the main camera node.
  8. * It provides high-level animation methods for block selection mode transitions:
  9. * 1. enterBlockSelectionMode() – complete animation for entering block selection (camera down + UI slide up)
  10. * 2. exitBlockSelectionMode() – complete animation for exiting block selection (camera up + UI slide down)
  11. *
  12. * Also provides low-level methods for specific use cases:
  13. * - moveUpSmooth() – move the camera back up with a smooth tween animation
  14. * - slideUpFromBottom() – slide UI up from bottom
  15. * - slideDibanDownAndHide() – slide UI down and hide
  16. */
  17. @ccclass('GameStartMove')
  18. export class GameStartMove extends Component {
  19. /** The camera node to move. Defaults to the node the script is attached to. */
  20. @property({
  21. type: Node,
  22. tooltip: 'Camera node to move. Leave empty to use the current node.'
  23. })
  24. public cameraNode: Node = null;
  25. /** Reference line node for camera positioning. When set, camera will move to show this line at the bottom. */
  26. @property({
  27. type: Node,
  28. tooltip: 'Reference line node. Camera will move to position this line at the bottom of the view.'
  29. })
  30. public referenceLineNode: Node = null;
  31. /** Tween duration for the smooth move-up animation. */
  32. @property({ tooltip: 'Duration for the smooth move-up tween (seconds).' })
  33. public moveDuration: number = 0.4;
  34. private _originalPos: Vec3 = new Vec3();
  35. /** 需要下滑的 diban 节点(外部拖拽赋值) */
  36. @property({ type: Node, tooltip: 'diban 节点' })
  37. public dibanNode: Node = null;
  38. /** Gray遮罩节点 */
  39. // @property({ type: Node, tooltip: 'Canvas/GameLevelUI/Gray节点' })
  40. // public grayNode: Node = null;
  41. /** 目标线节点,diban下滑结束后相机将移动到此线的位置 */
  42. @property({
  43. type: Node,
  44. tooltip: 'Target line node. Camera will move to show this line at the bottom after diban slide down animation.'
  45. })
  46. public targetLineNode: Node = null;
  47. private _originalDibanPos: Vec3 = new Vec3();
  48. onLoad () {
  49. // Use self node if cameraNode not assigned via the editor.
  50. if (!this.cameraNode) {
  51. this.cameraNode = this.node;
  52. }
  53. // Save initial position so we can always restore relative to it.
  54. this._originalPos.set(this.cameraNode.position);
  55. // Save diban original position if dibanNode is assigned
  56. if (this.dibanNode) {
  57. this._originalDibanPos.set(this.dibanNode.position);
  58. }
  59. }
  60. /**
  61. * Smoothly move the camera back up using a tween to original position.
  62. * Requires referenceLineNode to be set.
  63. */
  64. public moveUpSmooth () {
  65. if (!this.cameraNode) return;
  66. const startPos = this.cameraNode.position.clone();
  67. // Calculate movement based on reference line position
  68. const moveDistance = this.calculateMoveDistanceToShowLine();
  69. if (moveDistance <= 0) return;
  70. const targetPos = new Vec3(startPos.x, startPos.y + moveDistance, startPos.z);
  71. // Stop any running tweens on this node to avoid conflicting animations.
  72. console.log('GameStartMove.moveUpSmooth 镜头上移,开始执行');
  73. Tween.stopAllByTarget(this.cameraNode);
  74. tween(this.cameraNode)
  75. .to(this.moveDuration, { position: targetPos }, { easing: 'quadOut' })
  76. .start();
  77. }
  78. /**
  79. * 重置镜头到原始位置(平滑动画)
  80. * 用于返回主界面时确保镜头位置正常
  81. * @param duration 动画时长,默认使用moveDuration
  82. */
  83. public resetCameraToOriginalPosition(duration?: number) {
  84. if (!this.cameraNode) return;
  85. const animationDuration = duration !== undefined ? duration : this.moveDuration;
  86. console.log('GameStartMove.resetCameraToOriginalPosition 重置镜头到原始位置:', {
  87. currentPos: this.cameraNode.position,
  88. originalPos: this._originalPos,
  89. duration: animationDuration
  90. });
  91. // 停止任何正在运行的镜头动画
  92. Tween.stopAllByTarget(this.cameraNode);
  93. // 平滑移动到原始位置
  94. tween(this.cameraNode)
  95. .to(animationDuration, { position: this._originalPos.clone() }, { easing: 'quadOut' })
  96. .call(() => {
  97. console.log('GameStartMove.resetCameraToOriginalPosition 镜头重置完成');
  98. // 隐藏Gray遮罩
  99. // if (this.grayNode) {
  100. // this.grayNode.active = false;
  101. // }
  102. })
  103. .start();
  104. }
  105. /**
  106. * 立即重置镜头到原始位置(无动画)
  107. * 用于需要立即重置镜头位置的场景
  108. */
  109. public resetCameraToOriginalPositionImmediate() {
  110. if (!this.cameraNode) return;
  111. console.log('GameStartMove.resetCameraToOriginalPositionImmediate 立即重置镜头到原始位置:', {
  112. currentPos: this.cameraNode.position,
  113. originalPos: this._originalPos
  114. });
  115. // 停止任何正在运行的镜头动画
  116. Tween.stopAllByTarget(this.cameraNode);
  117. // 立即设置到原始位置
  118. this.cameraNode.setPosition(this._originalPos.clone());
  119. // 隐藏Gray遮罩
  120. // if (this.grayNode) {
  121. // this.grayNode.active = false;
  122. // }
  123. console.log('GameStartMove.resetCameraToOriginalPositionImmediate 镜头立即重置完成');
  124. }
  125. /**
  126. * Smoothly move the camera down to show the reference line at bottom.
  127. * Requires referenceLineNode to be set.
  128. * @param duration 动画时长,默认使用moveDuration
  129. */
  130. public moveDownSmooth(duration?: number) {
  131. if (!this.cameraNode) return;
  132. const startPos = this.cameraNode.position.clone();
  133. // Calculate movement based on reference line position
  134. const moveDistance = this.calculateMoveDistanceToShowLine();
  135. if (moveDistance <= 0) return;
  136. const targetPos = new Vec3(startPos.x, startPos.y - moveDistance, startPos.z);
  137. console.log('GameStartMove.moveDownSmooth 镜头平滑下移,开始执行:', {
  138. startPos: startPos,
  139. targetPos: targetPos,
  140. moveDistance: moveDistance
  141. });
  142. // Stop any running tweens on this node to avoid conflicting animations.
  143. Tween.stopAllByTarget(this.cameraNode);
  144. // 使用传入的duration或默认的moveDuration
  145. const animationDuration = duration !== undefined ? duration : this.moveDuration;
  146. tween(this.cameraNode)
  147. .to(animationDuration, { position: targetPos }, { easing: 'quadOut' })
  148. .call(() => {
  149. console.log('GameStartMove.moveDownSmooth 镜头平滑下移完成');
  150. // 镜头下移完成时显示Gray遮罩
  151. // if (this.grayNode) {
  152. // this.grayNode.active = true;
  153. // }
  154. })
  155. .start();
  156. }
  157. /* ========= 私有辅助方法 ========= */
  158. /**
  159. * Calculate the distance camera needs to move to show the reference line at the bottom of the view.
  160. * @returns The movement distance in world units
  161. */
  162. private calculateMoveDistanceToShowLine(): number {
  163. if (!this.referenceLineNode || !this.cameraNode) {
  164. console.warn('GameStartMove.calculateMoveDistanceToShowLine: referenceLineNode or cameraNode not set');
  165. return 0;
  166. }
  167. // Get the camera component
  168. const cameraComponent = this.cameraNode.getComponent(Camera);
  169. if (!cameraComponent) {
  170. console.warn('GameStartMove.calculateMoveDistanceToShowLine: Camera component not found');
  171. return 0;
  172. }
  173. // Get the reference line's world position
  174. const lineWorldPos = this.referenceLineNode.getWorldPosition();
  175. // Get camera's current world position
  176. const cameraWorldPos = this.cameraNode.getWorldPosition();
  177. // Calculate camera's view height in world units
  178. // For orthographic camera, orthoHeight represents half of the view height
  179. const cameraViewHalfHeight = cameraComponent.orthoHeight;
  180. // Calculate the distance to move camera so that the line appears at the bottom
  181. // We want: cameraWorldPos.y - cameraViewHalfHeight = lineWorldPos.y
  182. // So: moveDistance = cameraWorldPos.y - lineWorldPos.y - cameraViewHalfHeight
  183. const moveDistance = cameraWorldPos.y - lineWorldPos.y - cameraViewHalfHeight;
  184. console.log('GameStartMove.calculateMoveDistanceToShowLine 计算移动距离:', {
  185. lineWorldPos: lineWorldPos,
  186. cameraWorldPos: cameraWorldPos,
  187. cameraViewHalfHeight: cameraViewHalfHeight,
  188. moveDistance: moveDistance
  189. });
  190. return Math.max(0, moveDistance); // Ensure we don't move in wrong direction
  191. }
  192. /**
  193. * Calculate the distance camera needs to move to show the target line at the bottom of the view.
  194. * @returns The movement distance in world units
  195. */
  196. private calculateMoveDistanceToShowTargetLine(): number {
  197. if (!this.targetLineNode || !this.cameraNode) {
  198. console.warn('GameStartMove.calculateMoveDistanceToShowTargetLine: targetLineNode or cameraNode not set');
  199. return 0;
  200. }
  201. // Get the camera component
  202. const cameraComponent = this.cameraNode.getComponent(Camera);
  203. if (!cameraComponent) {
  204. console.warn('GameStartMove.calculateMoveDistanceToShowTargetLine: Camera component not found');
  205. return 0;
  206. }
  207. // Get the target line's world position
  208. const targetLineWorldPos = this.targetLineNode.getWorldPosition();
  209. // Get camera's current world position
  210. const cameraWorldPos = this.cameraNode.getWorldPosition();
  211. // Calculate camera's view height in world units
  212. // For orthographic camera, orthoHeight represents half of the view height
  213. const cameraViewHalfHeight = cameraComponent.orthoHeight;
  214. // Calculate the distance to move camera so that the target line appears at the bottom
  215. // We want: cameraWorldPos.y - cameraViewHalfHeight = targetLineWorldPos.y
  216. // So: moveDistance = cameraWorldPos.y - targetLineWorldPos.y - cameraViewHalfHeight
  217. const moveDistance = cameraWorldPos.y - targetLineWorldPos.y - cameraViewHalfHeight;
  218. console.log('GameStartMove.calculateMoveDistanceToShowTargetLine 计算移动距离:', {
  219. targetLineWorldPos: targetLineWorldPos,
  220. cameraWorldPos: cameraWorldPos,
  221. cameraViewHalfHeight: cameraViewHalfHeight,
  222. moveDistance: moveDistance
  223. });
  224. return moveDistance; // Allow both positive and negative movement
  225. }
  226. /**
  227. * 平滑移动相机到目标线位置
  228. * @param duration 动画时长,默认 0.3s
  229. */
  230. public moveCameraToTargetLineSmooth(duration: number = 0.3) {
  231. if (!this.cameraNode || !this.targetLineNode) {
  232. console.warn('GameStartMove.moveCameraToTargetLineSmooth: cameraNode or targetLineNode not set');
  233. return;
  234. }
  235. const moveDistance = this.calculateMoveDistanceToShowTargetLine();
  236. if (moveDistance === 0) {
  237. console.log('GameStartMove.moveCameraToTargetLineSmooth: 无需移动相机');
  238. return;
  239. }
  240. const startPos = this.cameraNode.position.clone();
  241. const targetPos = new Vec3(startPos.x, startPos.y - moveDistance, startPos.z);
  242. console.log('GameStartMove.moveCameraToTargetLineSmooth 相机移动到目标线:', {
  243. startPos: startPos,
  244. targetPos: targetPos,
  245. moveDistance: moveDistance
  246. });
  247. // 停止任何正在运行的相机动画
  248. Tween.stopAllByTarget(this.cameraNode);
  249. // 执行平滑移动动画
  250. tween(this.cameraNode)
  251. .to(duration, { position: targetPos }, { easing: 'quadOut' })
  252. .call(() => {
  253. console.log('GameStartMove.moveCameraToTargetLineSmooth 相机移动到目标线完成');
  254. })
  255. .start();
  256. }
  257. /**
  258. * 获取参考线在diban父节点坐标系中的位置和diban高度
  259. * @returns 包含referenceLineLocalPos和dibanHeight的对象,如果节点未设置则返回null
  260. */
  261. private getReferenceLinePositionAndDibanHeight(): { referenceLineLocalPos: Vec3, dibanHeight: number } | null {
  262. if (!this.dibanNode || !this.referenceLineNode) {
  263. return null;
  264. }
  265. // 获取参考线的世界位置
  266. const referenceLineWorldPos = this.referenceLineNode.getWorldPosition();
  267. // 将参考线的世界位置转换为diban父节点的本地坐标
  268. const referenceLineLocalPos = new Vec3();
  269. this.dibanNode.parent.getComponent(UITransform).convertToNodeSpaceAR(referenceLineWorldPos, referenceLineLocalPos);
  270. // 获取diban高度
  271. const dibanUITransform = this.dibanNode.getComponent(UITransform);
  272. const dibanHeight = dibanUITransform ? dibanUITransform.height : 0;
  273. return { referenceLineLocalPos, dibanHeight };
  274. }
  275. /* ========= 高级动画方法 ========= */
  276. /**
  277. * 上滑 diban节点,进入备战状态。同时镜头下移。
  278. * @param duration 动画时长,默认 0.3s
  279. */
  280. public slideUpFromBottom(duration: number = 0.3) {
  281. // 显示diban节点
  282. this.dibanNode.active = true;
  283. // 获取参考线位置和diban高度
  284. const positionData = this.getReferenceLinePositionAndDibanHeight();
  285. if (!positionData) {
  286. return;
  287. }
  288. const { referenceLineLocalPos, dibanHeight } = positionData;
  289. // 计算diban的初始位置:diban顶部与参考线对齐
  290. const startPos = new Vec3(this.dibanNode.position.x, referenceLineLocalPos.y - dibanHeight / 2, this.dibanNode.position.z);
  291. // 计算目标位置:diban底部与参考线对齐
  292. const targetPos = new Vec3(this.dibanNode.position.x, referenceLineLocalPos.y + dibanHeight / 2, this.dibanNode.position.z);
  293. // 设置diban初始位置
  294. this.dibanNode.setPosition(startPos);
  295. // 停止任何正在运行的动画
  296. Tween.stopAllByTarget(this.dibanNode);
  297. // 执行上滑动画
  298. tween(this.dibanNode)
  299. .to(duration, { position: targetPos }, { easing: 'quadInOut' })
  300. .call(() => {
  301. const animationEndTime = Date.now();
  302. // 动画完成后发送进入备战状态事件,触发游戏暂停
  303. console.log('[GameStartMove] diban上滑完成,发送游戏暂停事件');
  304. EventBus.getInstance().emit(GameEvents.GAME_PAUSE);
  305. })
  306. .start();
  307. // 同时执行camera平滑下移动画
  308. this.moveDownSmooth(duration);
  309. }
  310. /**
  311. * 下滑 diban节点,结束备战进入playing 状态。
  312. * @param duration 动画时长,默认 0.3s
  313. */
  314. public slideDibanDownAndHide(duration: number = 0.3) {
  315. // 使用装饰器属性中的diban节点
  316. if (!this.dibanNode) {
  317. console.warn('GameStartMove.slideDibanDownAndHide diban节点未设置');
  318. return;
  319. }
  320. // 检查参考线节点
  321. if (!this.referenceLineNode) {
  322. console.warn('GameStartMove.slideDibanDownAndHide 参考线节点未设置');
  323. return;
  324. }
  325. // 获取当前位置
  326. const currentPos = this.dibanNode.position.clone();
  327. console.log('GameStartMove.slideDibanDownAndHide diban当前位置:', currentPos);
  328. // 获取参考线位置和diban高度
  329. const positionData = this.getReferenceLinePositionAndDibanHeight();
  330. if (!positionData) {
  331. return;
  332. }
  333. const { referenceLineLocalPos, dibanHeight } = positionData;
  334. // 计算目标位置:diban顶部与参考线对齐(下滑到初始位置)
  335. const targetPos = new Vec3(currentPos.x, referenceLineLocalPos.y - dibanHeight / 2, currentPos.z);
  336. console.log('GameStartMove.slideDibanDownAndHide diban目标位置:', targetPos);
  337. // 停止任何正在运行的动画
  338. Tween.stopAllByTarget(this.dibanNode);
  339. // 执行下滑动画
  340. tween(this.dibanNode)
  341. .to(duration, { position: targetPos }, { easing: 'quadOut' })
  342. .call(() => {
  343. console.log('GameStartMove.slideDibanDownAndHide diban下滑动画完成');
  344. // 动画完成时隐藏Gray遮罩
  345. // if (this.grayNode) {
  346. // this.grayNode.active = false;
  347. // }
  348. // 动画完成后隐藏diban节点
  349. this.dibanNode.active = false;
  350. // 动画完成后发送底板选择专用恢复事件
  351. console.log('[GameStartMove] diban下滑完成,发送底板选择恢复事件');
  352. EventBus.getInstance().emit(GameEvents.GAME_RESUME_BLOCK_SELECTION);
  353. })
  354. .start();
  355. // 在diban下滑动画开始的同时,相机平滑移动到目标线位置
  356. this.moveCameraToTargetLineSmooth(0.3);
  357. console.log('GameStartMove.slideDibanDownAndHide diban下滑动画已启动');
  358. }
  359. }