perf: 支持相机运镜

This commit is contained in:
richarjiang
2025-09-29 08:20:59 +08:00
parent ad51ba1262
commit facdae5c5e
4 changed files with 1549 additions and 731 deletions

View File

@@ -1,4 +1,4 @@
import { _decorator, Component, Node, Vec3, Camera, view } from 'cc';
import { _decorator, Component, Node, Vec3, Camera, view, find } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('CameraFollow')
@@ -22,84 +22,145 @@ export class CameraFollow extends Component {
@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);
this.camera.orthoHeight = 680
if (!this.camera) {
console.error('CameraFollow: 未找到Camera组件');
return;
}
// 根据项目需要调整初始正交高度
this.camera.orthoHeight = 550;
}
start() {
if (this.target) {
// 初始化相机位置
const initialPos = this.target.position.clone();
initialPos.add(this.offset);
this.node.position = initialPos;
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;
// 计算目标位置
const targetPosition = this.target.position.clone();
targetPosition.add(this.offset);
this.target.getPosition(this._targetPosition);
Vec3.add(this._desiredPosition, this._targetPosition, this.offset);
// 应用地图边界限制
const clampedPosition = this.clampCameraPosition(targetPosition);
const clampedPosition = this.clampCameraPosition(this._desiredPosition);
// 使用插值实现平滑跟随
const currentPosition = this.node.position;
const newPosition = new Vec3();
const lerpFactor = this.computeLerpFactor(deltaTime);
// 根据平滑度设置插值速度
const lerpFactor = Math.min(1.0, this.followSpeed * deltaTime * (1 - this.smoothness + 0.1));
if (lerpFactor >= 1) {
this.node.setPosition(clampedPosition);
return;
}
Vec3.lerp(newPosition, currentPosition, clampedPosition, lerpFactor);
this.node.position = newPosition;
Vec3.lerp(this._newPosition, currentPosition, clampedPosition, lerpFactor);
this.node.setPosition(this._newPosition);
}
// 限制相机位置在地图边界内
private clampCameraPosition(position: Vec3): Vec3 {
if (!this.camera) return position;
if (!this.camera) return position.clone();
// 获取屏幕可见区域大小
const visibleSize = view.getVisibleSize();
const aspectRatio = visibleSize.height > 0 ? visibleSize.width / visibleSize.height : 1;
// 计算相机能看到的世界区域的一半
const halfCameraWidth = visibleSize.width * 0.5;
const halfCameraHeight = visibleSize.height * 0.5;
// 计算相机能看到的世界区域的一半(正交相机)。缩放后需要除以 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;
// 计算地图边界地图锚点为0.5,0.5,所以范围是-mapWidth/2到+mapWidth/2
const mapHalfWidth = this.mapWidth * 0.5;
const mapHalfHeight = this.mapHeight * 0.5;
// 计算相机位置的边界(确保相机边缘不超出地图边界)
const minX = -mapHalfWidth + halfCameraWidth;
const maxX = mapHalfWidth - halfCameraWidth;
const minY = -mapHalfHeight + halfCameraHeight;
const maxY = mapHalfHeight - halfCameraHeight;
// 限制相机位置
const clampedPosition = position.clone();
clampedPosition.x = Math.max(minX, Math.min(maxX, position.x));
clampedPosition.y = Math.max(minY, Math.min(maxY, position.y));
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) {
const initialPos = target.position.clone();
initialPos.add(this.offset);
this.node.position = initialPos;
if (target && !this.isInitialFocusActive) {
this.snapToNode(target);
}
}
@@ -112,9 +173,58 @@ export class CameraFollow extends Component {
snapToTarget() {
if (!this.target) return;
const targetPosition = this.target.position.clone();
targetPosition.add(this.offset);
const clampedPosition = this.clampCameraPosition(targetPosition);
this.node.position = clampedPosition;
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;
}
}