Files
climb/assets/scripts/CameraFollow.ts
2025-09-29 08:20:59 +08:00

231 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}