import { _decorator, Component, Node, Vec3, tween, Tween, UITransform, Camera } from 'cc'; const { ccclass, property } = _decorator; /** * GameStartMove * * This component is expected to be attached to the main camera node. * It provides high-level animation methods for block selection mode transitions: * 1. enterBlockSelectionMode() – complete animation for entering block selection (camera down + UI slide up) * 2. exitBlockSelectionMode() – complete animation for exiting block selection (camera up + UI slide down) * * Also provides low-level methods for specific use cases: * - moveDownInstant() – instantly move the camera down by a fixed offset * - moveUpSmooth() – move the camera back up with a smooth tween animation * - slideUpFromBottom() – slide UI up from bottom * - slideDibanDownAndHide() – slide UI down and hide */ @ccclass('GameStartMove') export class GameStartMove extends Component { /** The camera node to move. Defaults to the node the script is attached to. */ @property({ type: Node, tooltip: 'Camera node to move. Leave empty to use the current node.' }) public cameraNode: Node = null; /** Reference line node for camera positioning. When set, camera will move to show this line at the bottom. */ @property({ type: Node, tooltip: 'Reference line node. Camera will move to position this line at the bottom of the view.' }) public referenceLineNode: Node = null; /** Tween duration for the smooth move-up animation. */ @property({ tooltip: 'Duration for the smooth move-up tween (seconds).' }) public moveDuration: number = 0.4; private _originalPos: Vec3 = new Vec3(); /** 需要下滑的 diban 节点(外部拖拽赋值) */ @property({ type: Node, tooltip: 'diban 节点' }) public dibanNode: Node = null; /** Gray遮罩节点 */ @property({ type: Node, tooltip: 'Canvas/GameLevelUI/Gray节点' }) public grayNode: Node = null; private _originalDibanPos: Vec3 = new Vec3(); onLoad () { // Use self node if cameraNode not assigned via the editor. if (!this.cameraNode) { this.cameraNode = this.node; } // Save initial position so we can always restore relative to it. this._originalPos.set(this.cameraNode.position); // Save diban original position if dibanNode is assigned if (this.dibanNode) { this._originalDibanPos.set(this.dibanNode.position); } } /** * Instantly move the camera down to show the reference line at bottom. * Requires referenceLineNode to be set. */ public moveDownInstant () { if (!this.cameraNode) return; const pos = this.cameraNode.position.clone(); // Calculate movement based on reference line position const moveDistance = this.calculateMoveDistanceToShowLine(); if (moveDistance > 0) { pos.y -= moveDistance; this.cameraNode.setPosition(pos); console.log('GameStartMove.moveDownInstant 镜头下移,当前位置:', pos); } // 镜头下移时显示Gray遮罩 if (this.grayNode) { this.grayNode.active = true; } } /** * Smoothly move the camera back up using a tween to original position. * Requires referenceLineNode to be set. */ public moveUpSmooth () { if (!this.cameraNode) return; const startPos = this.cameraNode.position.clone(); // Calculate movement based on reference line position const moveDistance = this.calculateMoveDistanceToShowLine(); if (moveDistance <= 0) return; const targetPos = new Vec3(startPos.x, startPos.y + moveDistance, startPos.z); // Stop any running tweens on this node to avoid conflicting animations. console.log('GameStartMove.moveUpSmooth 镜头上移,开始执行'); Tween.stopAllByTarget(this.cameraNode); tween(this.cameraNode) .to(this.moveDuration, { position: targetPos }, { easing: 'quadOut' }) .start(); } /* ========= 私有辅助方法 ========= */ /** * Calculate the distance camera needs to move to show the reference line at the bottom of the view. * @returns The movement distance in world units */ private calculateMoveDistanceToShowLine(): number { if (!this.referenceLineNode || !this.cameraNode) { console.warn('GameStartMove.calculateMoveDistanceToShowLine: referenceLineNode or cameraNode not set'); return 0; } // Get the camera component const cameraComponent = this.cameraNode.getComponent(Camera); if (!cameraComponent) { console.warn('GameStartMove.calculateMoveDistanceToShowLine: Camera component not found'); return 0; } // Get the reference line's world position const lineWorldPos = this.referenceLineNode.getWorldPosition(); // Get camera's current world position const cameraWorldPos = this.cameraNode.getWorldPosition(); // Calculate camera's view height in world units // For orthographic camera, orthoHeight represents half of the view height const cameraViewHalfHeight = cameraComponent.orthoHeight; // Calculate the distance to move camera so that the line appears at the bottom // We want: cameraWorldPos.y - cameraViewHalfHeight = lineWorldPos.y // So: moveDistance = cameraWorldPos.y - lineWorldPos.y - cameraViewHalfHeight const moveDistance = cameraWorldPos.y - lineWorldPos.y - cameraViewHalfHeight; console.log('GameStartMove.calculateMoveDistanceToShowLine 计算移动距离:', { lineWorldPos: lineWorldPos, cameraWorldPos: cameraWorldPos, cameraViewHalfHeight: cameraViewHalfHeight, moveDistance: moveDistance }); return Math.max(0, moveDistance); // Ensure we don't move in wrong direction } /** * 获取参考线在diban父节点坐标系中的位置和diban高度 * @returns 包含referenceLineLocalPos和dibanHeight的对象,如果节点未设置则返回null */ private getReferenceLinePositionAndDibanHeight(): { referenceLineLocalPos: Vec3, dibanHeight: number } | null { if (!this.dibanNode || !this.referenceLineNode) { return null; } // 获取参考线的世界位置 const referenceLineWorldPos = this.referenceLineNode.getWorldPosition(); // 将参考线的世界位置转换为diban父节点的本地坐标 const referenceLineLocalPos = new Vec3(); this.dibanNode.parent.getComponent(UITransform).convertToNodeSpaceAR(referenceLineWorldPos, referenceLineLocalPos); // 获取diban高度 const dibanUITransform = this.dibanNode.getComponent(UITransform); const dibanHeight = dibanUITransform ? dibanUITransform.height : 0; return { referenceLineLocalPos, dibanHeight }; } /* ========= 高级动画方法 ========= */ /** * 上滑 diban节点,进入备战状态。同时镜头下移。 * @param duration 动画时长,默认 0.3s */ public slideUpFromBottom(duration: number = 0.3) { // 显示diban节点 this.dibanNode.active = true; // 获取参考线位置和diban高度 const positionData = this.getReferenceLinePositionAndDibanHeight(); if (!positionData) { return; } const { referenceLineLocalPos, dibanHeight } = positionData; // 计算diban的初始位置:diban顶部与参考线对齐 const startPos = new Vec3(this.dibanNode.position.x, referenceLineLocalPos.y - dibanHeight / 2, this.dibanNode.position.z); // 计算目标位置:diban底部与参考线对齐 const targetPos = new Vec3(this.dibanNode.position.x, referenceLineLocalPos.y + dibanHeight / 2, this.dibanNode.position.z); // 设置diban初始位置 this.dibanNode.setPosition(startPos); // 停止任何正在运行的动画 Tween.stopAllByTarget(this.dibanNode); // 执行上滑动画 tween(this.dibanNode) .to(duration, { position: targetPos }, { easing: 'quadInOut' }) .call(() => { const animationEndTime = Date.now(); }) .start(); // 同时执行camera下移动画 this.moveDownInstant(); } /** * 下滑 diban节点,结束备战进入playing 状态。 * @param duration 动画时长,默认 0.3s */ public slideDibanDownAndHide(duration: number = 0.3) { // 使用装饰器属性中的diban节点 if (!this.dibanNode) { console.warn('GameStartMove.slideDibanDownAndHide diban节点未设置'); return; } // 检查参考线节点 if (!this.referenceLineNode) { console.warn('GameStartMove.slideDibanDownAndHide 参考线节点未设置'); return; } // 获取当前位置 const currentPos = this.dibanNode.position.clone(); console.log('GameStartMove.slideDibanDownAndHide diban当前位置:', currentPos); // 获取参考线位置和diban高度 const positionData = this.getReferenceLinePositionAndDibanHeight(); if (!positionData) { return; } const { referenceLineLocalPos, dibanHeight } = positionData; // 计算目标位置:diban顶部与参考线对齐(下滑到初始位置) const targetPos = new Vec3(currentPos.x, referenceLineLocalPos.y - dibanHeight / 2, currentPos.z); console.log('GameStartMove.slideDibanDownAndHide diban目标位置:', targetPos); // 停止任何正在运行的动画 Tween.stopAllByTarget(this.dibanNode); // 执行下滑动画 tween(this.dibanNode) .to(duration, { position: targetPos }, { easing: 'quadOut' }) .call(() => { console.log('GameStartMove.slideDibanDownAndHide diban下滑动画完成'); // 动画完成时隐藏Gray遮罩 if (this.grayNode) { this.grayNode.active = false; } // 动画完成后隐藏diban节点 this.dibanNode.active = false; }) .start(); // 同时执行camera上移动画 this.moveUpSmooth(); console.log('GameStartMove.slideDibanDownAndHide diban下滑动画已启动'); } }