import { _decorator, Component, Node, Vec3, Camera, view, find } from 'cc'; const { ccclass, property } = _decorator; @ccclass('CameraFollow') export class CameraFollow extends Component { @property(Node) target: Node | null = null; // 要跟随的目标(玩家) @property({ range: [0.1, 10] }) followSpeed: number = 5.0; // 跟随速度 @property(Vec3) offset: Vec3 = new Vec3(0, 0, 10); // 相机相对目标的偏移 @property({ range: [0, 1] }) smoothness: number = 0.1; // 平滑度,0为瞬间跟随,1为最慢跟随 @property mapWidth: number = 1080; // 地图宽度 @property mapHeight: number = 1920; // 地图高度 @property initialFocusNodeName: string = 'guai_10'; @property initialFocusDuration: number = 2.0; private camera: Camera | null = null; private readonly _targetPosition: Vec3 = new Vec3(); private readonly _desiredPosition: Vec3 = new Vec3(); private readonly _newPosition: Vec3 = new Vec3(); private initialFocusNode: Node | null = null; private initialFocusTimer = 0; private isInitialFocusActive = false; onLoad() { // 获取相机组件 this.camera = this.getComponent(Camera); if (!this.camera) { console.error('CameraFollow: 未找到Camera组件'); return; } // 根据项目需要调整初始正交高度 this.camera.orthoHeight = 550; } start() { const hasInitialFocus = this.beginInitialFocus(); if (!hasInitialFocus && this.target) { this.snapToNode(this.target); } } update(deltaTime: number) { if (this.isInitialFocusActive) { if (this.initialFocusNode) { this.snapToNode(this.initialFocusNode); } this.initialFocusTimer -= deltaTime; if (this.initialFocusTimer > 0) { return; } this.isInitialFocusActive = false; } if (!this.target) return; this.target.getPosition(this._targetPosition); Vec3.add(this._desiredPosition, this._targetPosition, this.offset); // 应用地图边界限制 const clampedPosition = this.clampCameraPosition(this._desiredPosition); // 使用插值实现平滑跟随 const currentPosition = this.node.position; const lerpFactor = this.computeLerpFactor(deltaTime); if (lerpFactor >= 1) { this.node.setPosition(clampedPosition); return; } Vec3.lerp(this._newPosition, currentPosition, clampedPosition, lerpFactor); this.node.setPosition(this._newPosition); } // 限制相机位置在地图边界内 private clampCameraPosition(position: Vec3): Vec3 { if (!this.camera) return position.clone(); // 获取屏幕可见区域大小 const visibleSize = view.getVisibleSize(); const aspectRatio = visibleSize.height > 0 ? visibleSize.width / visibleSize.height : 1; // 计算相机能看到的世界区域的一半(正交相机)。缩放后需要除以 zoomRatio。 const cameraWithZoom = this.camera as Camera & { zoomRatio?: number }; const zoomRatio = cameraWithZoom.zoomRatio ?? 1; const halfCameraHeight = this.camera.orthoHeight / Math.max(zoomRatio, 0.0001); const halfCameraWidth = halfCameraHeight * aspectRatio; const mapHalfWidth = this.mapWidth * 0.5; const mapHalfHeight = this.mapHeight * 0.5; const clampedPosition = position.clone(); if (mapHalfWidth <= halfCameraWidth) { // 地图宽度不足以填满视野,水平居中 clampedPosition.x = 0; } else { const minX = -mapHalfWidth + halfCameraWidth; const maxX = mapHalfWidth - halfCameraWidth; clampedPosition.x = Math.max(minX, Math.min(maxX, clampedPosition.x)); } if (mapHalfHeight <= halfCameraHeight) { // 地图高度不足以填满视野,垂直居中 clampedPosition.y = 0; } else { const minY = -mapHalfHeight + halfCameraHeight; const maxY = mapHalfHeight - halfCameraHeight; clampedPosition.y = Math.max(minY, Math.min(maxY, clampedPosition.y)); } return clampedPosition; } private computeLerpFactor(deltaTime: number): number { if (deltaTime <= 0) { return 0; } const speed = Math.max(0, this.followSpeed); if (speed <= 0) { return 0; } if (this.smoothness <= 0) { return 1; } const smooth = Math.min(this.smoothness, 0.9999); const followRate = speed * (1 - smooth); if (followRate <= 0) { return 0; } const lerpFactor = 1 - Math.exp(-followRate * deltaTime); return Math.min(1, Math.max(0, lerpFactor)); } // 设置跟随目标 setTarget(target: Node) { this.target = target; if (target && !this.isInitialFocusActive) { this.snapToNode(target); } } // 设置偏移量 setOffset(offset: Vec3) { this.offset = offset; } // 瞬间移动到目标位置 snapToTarget() { if (!this.target) return; this.snapToNode(this.target); } private beginInitialFocus(): boolean { if (this.initialFocusDuration <= 0) { return false; } let focusNode = this.initialFocusNode; if (!focusNode) { const scene = this.node.scene; if (!scene) { return false; } if (this.initialFocusNodeName) { focusNode = find(this.initialFocusNodeName, scene) ?? this.findNodeByName(scene, this.initialFocusNodeName); } } if (!focusNode) { return false; } this.initialFocusNode = focusNode; this.initialFocusTimer = this.initialFocusDuration; this.isInitialFocusActive = true; this.snapToNode(focusNode); return true; } private snapToNode(node: Node) { node.getPosition(this._targetPosition); Vec3.add(this._desiredPosition, this._targetPosition, this.offset); const clamped = this.clampCameraPosition(this._desiredPosition); this.node.setPosition(clamped); } private findNodeByName(root: Node, name: string): Node | null { if (root.name === name) { return root; } for (let i = 0; i < root.children.length; i++) { const child = root.children[i]; const match = this.findNodeByName(child, name); if (match) { return match; } } return null; } }