231 lines
6.1 KiB
TypeScript
231 lines
6.1 KiB
TypeScript
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;
|
||
}
|
||
}
|