import * as THREE from "three"; export type CameraMode = "top-down-fixed" | "isometric-45" | "orbit-debug"; export interface CameraTransition { startPos: THREE.Vector3; endPos: THREE.Vector3; startLookAt: THREE.Vector3; endLookAt: THREE.Vector3; duration: number; elapsed: number; } /** * 3D 相机控制器 * 专为棋盘/拼图类游戏设计,支持自动俯瞰定位、震动、平滑过渡 */ export class CameraController { private camera: THREE.PerspectiveCamera; private mode: CameraMode; // 基础位置(未加震动偏移的原始位置) private basePosition = new THREE.Vector3(); private baseLookAt = new THREE.Vector3(); // 震动状态 private shakeIntensity = 0; private shakeDuration = 0; private shakeElapsed = 0; private shakeOffset = new THREE.Vector3(); // 过渡状态 private transition: CameraTransition | null = null; constructor(camera: THREE.PerspectiveCamera, mode: CameraMode = "top-down-fixed") { this.camera = camera; this.mode = mode; } /** * 根据棋盘尺寸自动定位相机使棋盘完整可见。 * @param rows 棋盘行数 * @param cols 棋盘列数 * @param cellSize 格子世界单位大小 */ setupForBoard(rows: number, cols: number, cellSize: number): void { const boardWidth = cols * cellSize; const boardHeight = rows * cellSize; const centerX = boardWidth / 2; const centerZ = boardHeight / 2; const maxDim = Math.max(boardWidth, boardHeight); const fovRad = (this.camera.fov * Math.PI) / 180; // 留 20% 边距确保棋盘不贴边 const margin = 1.2; switch (this.mode) { case "top-down-fixed": { // 60° 俯瞰(略微前倾,增加透视感) const distance = (maxDim / 2) / Math.tan(fovRad / 2) * margin; const tiltFactor = 0.3; this.basePosition.set( centerX, distance, centerZ + distance * tiltFactor ); this.baseLookAt.set(centerX, 0, centerZ); break; } case "isometric-45": { // 45° 等距视角 const distance = (maxDim / 2) / Math.tan(fovRad / 2) * margin; const angle45 = Math.PI / 4; this.basePosition.set( centerX + distance * Math.sin(angle45) * 0.5, distance * Math.cos(angle45), centerZ + distance * Math.sin(angle45) ); this.baseLookAt.set(centerX, 0, centerZ); break; } case "orbit-debug": { // 调试模式:保持当前位置,只更新 lookAt this.basePosition.copy(this.camera.position); this.baseLookAt.set(centerX, 0, centerZ); break; } } this.applyCamera(); } /** * 触发相机震动效果。 * @param intensity 震动强度(世界单位,推荐 0.05~0.3) * @param duration 震动持续时间(毫秒) */ shake(intensity: number = 0.1, duration: number = 200): void { this.shakeIntensity = intensity; this.shakeDuration = duration / 1000; // 转为秒 this.shakeElapsed = 0; } /** * 启动相机平滑过渡到新位置。 * @param position 目标位置 * @param lookAt 目标注视点 * @param duration 过渡时间(毫秒) */ transitionTo(position: THREE.Vector3, lookAt: THREE.Vector3, duration: number): void { this.transition = { startPos: this.basePosition.clone(), endPos: position.clone(), startLookAt: this.baseLookAt.clone(), endLookAt: lookAt.clone(), duration: duration / 1000, elapsed: 0, }; } /** * 每帧更新。处理震动衰减和过渡动画。 * @param dt 帧间隔时间(秒) */ update(dt: number): void { let dirty = false; // 更新过渡 if (this.transition) { this.transition.elapsed += dt; const t = Math.min(this.transition.elapsed / this.transition.duration, 1); // smoothstep easing const ease = t * t * (3 - 2 * t); this.basePosition.lerpVectors(this.transition.startPos, this.transition.endPos, ease); this.baseLookAt.lerpVectors(this.transition.startLookAt, this.transition.endLookAt, ease); if (t >= 1) { this.transition = null; } dirty = true; } // 更新震动 if (this.shakeElapsed < this.shakeDuration) { this.shakeElapsed += dt; const progress = this.shakeElapsed / this.shakeDuration; // 线性衰减 const decay = 1 - progress; const intensity = this.shakeIntensity * decay; this.shakeOffset.set( (Math.random() - 0.5) * 2 * intensity, (Math.random() - 0.5) * 2 * intensity * 0.5, // Y 方向震动较小 (Math.random() - 0.5) * 2 * intensity ); dirty = true; } else if (this.shakeOffset.lengthSq() > 0) { this.shakeOffset.set(0, 0, 0); dirty = true; } if (dirty) { this.applyCamera(); } } /** 获取当前模式 */ getMode(): CameraMode { return this.mode; } /** 切换相机模式 */ setMode(mode: CameraMode): void { this.mode = mode; } /** 获取当前相机注视点 */ getLookAt(): THREE.Vector3 { return this.baseLookAt.clone(); } /** 判断是否正在过渡中 */ isTransitioning(): boolean { return this.transition !== null; } /** 获取基础位置(不含震动偏移) */ getBasePosition(): THREE.Vector3 { return this.basePosition.clone(); } // ── 内部方法 ── private applyCamera(): void { this.camera.position.copy(this.basePosition).add(this.shakeOffset); this.camera.lookAt(this.baseLookAt); } }