LayaAir鼠标(手指)控制相机旋转,限制角度

切换天空盒

脚本挂载到相机身上

javascript 复制代码
const { regClass, property } = Laya;

@regClass()
export class SmoothCameraController extends Laya.Script {
    declare owner: Laya.Camera;

    // 旋转灵敏度
    @property({ type: Number, name: "旋转灵敏度" })
    public rotationSensitivity: number = 0.5; // 每像素旋转角度

    // 平滑度 (0-1, 值越小越平滑)
    @property({ type: Number, name: "平滑度" })
    public smoothness: number = 0.08;

    // 角度限制
    @property({ type: Number, name: "最小俯仰角" })
    public minPitchDegrees: number = -80; // 最小俯仰角(X轴)

    @property({ type: Number, name: "最大俯仰角" })
    public maxPitchDegrees: number = 80;  // 最大俯仰角(X轴)

    // Y轴不限制,可以360度自由旋转
    private minYawDegrees: number = -Infinity;  // 最小偏航角(无限制)
    private maxYawDegrees: number = Infinity;   // 最大偏航角(无限制)

    // 目标位置
    @property({ type: Laya.Vector3, name: "目标位置" })
    public targetPosition: Laya.Vector3 = new Laya.Vector3(0, 0, 0);

    @property({ type: Laya.Sprite3D, name: "目标物体" })
    public targetTransform: Laya.Sprite3D | null = null;

    // 是否阻止在UI上操作
    @property({ type: Boolean, name: "UI上阻止操作" })
    public blockOnUI: boolean = true;

    // 是否阻止在3D物体上操作
    @property({ type: Boolean, name: "3D物体上阻止操作" })
    public blockOn3D: boolean = false;

    // 内部状态
    private isDragging: boolean = false;
    private lastPointerX: number = 0;
    private lastPointerY: number = 0;

    // 当前和目标角度
    public currentYawDeg: number = 0;
    public currentPitchDeg: number = 0;
    private targetYawDeg: number = 0;
    private targetPitchDeg: number = 0;

    // 距离目标点的距离
    private distanceToTarget: number = 5;

    // 临时变量
    private tempQuat: Laya.Quaternion = new Laya.Quaternion();
    private tempUp: Laya.Vector3 = new Laya.Vector3(0, 1, 0);
    private tempPos: Laya.Vector3 = new Laya.Vector3();
    private tempTarget: Laya.Vector3 = new Laya.Vector3();

    // UI根节点引用
    private uiRoot2D: Laya.Sprite | null = null;

    onAwake(): void {
        // 缓存2D UI根节点,避免在UI上拖拽时旋转相机
        try {
            const scene2D = Laya.stage.getChildAt(1)?.getChildByName("Scene2D");
            if (scene2D instanceof Laya.Sprite) {
                this.uiRoot2D = scene2D;
            }
        } catch (e) {
            this.uiRoot2D = null;
        }
    }

    onStart(): void {


        // 从当前相机位置初始化球面坐标
        this.initializeFromCurrentPosition();

        // 设置输入事件
        this.setupInputEvents();
        ;
    }

    private initializeFromCurrentPosition(): void {
        const eye = this.owner.transform.position.clone();
        const target = this.getTargetPosition();
        this.distanceToTarget = Laya.Vector3.distance(eye, target);

        // 计算从目标到相机的偏移向量
        const offset = new Laya.Vector3(
            eye.x - target.x,
            eye.y - target.y,
            eye.z - target.z
        );

        // 计算偏航角(绕Y轴)和俯仰角(绕X轴)
        const yawRad = Math.atan2(offset.x, offset.z);
        const horizontalLen = Math.sqrt(offset.x * offset.x + offset.z * offset.z);
        const pitchRad = Math.atan2(offset.y, horizontalLen);

        // 转换为角度并设置初始值(Z轴始终为0)
        this.currentYawDeg = this.targetYawDeg = yawRad * 180 / Math.PI;
        this.currentPitchDeg = this.targetPitchDeg = this.clampPitch(pitchRad * 180 / Math.PI);

        // 立即应用旋转,确保Z轴为0
        this.applyRotationWithZeroRoll();
    }

    private setupInputEvents(): void {


        // 鼠标和触摸事件(LayaAir统一处理)
        Laya.stage.on(Laya.Event.MOUSE_DOWN, this, this.onPointerDown);
        Laya.stage.on(Laya.Event.MOUSE_MOVE, this, this.onPointerMove);
        Laya.stage.on(Laya.Event.MOUSE_UP, this, this.onPointerUp);
        Laya.stage.on(Laya.Event.MOUSE_OUT, this, this.onPointerUp);


    }

    private onPointerDown(evt?: Laya.Event): void {


        // 检查是否在UI上
        if (this.blockOnUI && this.pointerOnUI(Laya.stage.mouseX, Laya.stage.mouseY)) {
            console.log("在UI上,阻止相机操作");
            return;
        }

        // 检查是否在3D物体上
        if (this.blockOn3D && this.hitAny3D(Laya.stage.mouseX, Laya.stage.mouseY)) {
            console.log("在3D物体上,阻止相机操作");
            return;
        }

        this.isDragging = true;
        this.lastPointerX = Laya.stage.mouseX;
        this.lastPointerY = Laya.stage.mouseY;

    }

    private onPointerMove(evt?: Laya.Event): void {
        if (!this.isDragging) return;

        const dx = Laya.stage.mouseX - this.lastPointerX;
        const dy = Laya.stage.mouseY - this.lastPointerY;

        // 更新目标角度
        this.targetYawDeg += dx * this.rotationSensitivity;
        this.targetPitchDeg = this.clampPitch(this.targetPitchDeg - dy * this.rotationSensitivity);

        // Y轴不限制,可以360度自由旋转
        // this.targetYawDeg = this.clampYaw(this.targetYawDeg);

        // 更新上一帧位置
        this.lastPointerX = Laya.stage.mouseX;
        this.lastPointerY = Laya.stage.mouseY;


    }

    private onPointerUp(evt?: Laya.Event): void {
        this.isDragging = false;
    }

    private clampPitch(pitchDeg: number): number {
        return Math.max(this.minPitchDegrees, Math.min(this.maxPitchDegrees, pitchDeg));
    }

    private clampYaw(yawDeg: number): number {
        // Y轴不限制,直接返回原值,允许360度自由旋转
        return yawDeg;
    }

    onUpdate(): void {
        // 平滑插值到目标角度
        const lerpFactor = 1 - Math.max(0, Math.min(1, this.smoothness));

        this.currentYawDeg = this.lerpAngle(this.currentYawDeg, this.targetYawDeg, lerpFactor);
        this.currentPitchDeg = this.lerpAngle(this.currentPitchDeg, this.targetPitchDeg, lerpFactor);

        // 从球面坐标重新计算相机位置
        this.updateCameraPosition();


    }

    private updateCameraPosition(): void {
        const yawRad = this.currentYawDeg * Math.PI / 180;
        const pitchRad = this.currentPitchDeg * Math.PI / 180;

        const cosPitch = Math.cos(pitchRad);
        const sinPitch = Math.sin(pitchRad);
        const sinYaw = Math.sin(yawRad);
        const cosYaw = Math.cos(yawRad);

        const target = this.getTargetPosition();

        // 计算新的相机位置
        this.tempPos.setValue(
            target.x + this.distanceToTarget * sinYaw * cosPitch,
            target.y + this.distanceToTarget * sinPitch,
            target.z + this.distanceToTarget * cosYaw * cosPitch
        );

        // 更新相机位置
        this.owner.transform.position = this.tempPos;

        // 应用旋转,确保Z轴旋转为0
        this.applyRotationWithZeroRoll();
    }

    // 应用旋转,确保Z轴旋转为0
    private applyRotationWithZeroRoll(): void {
        const eulerAngles = new Laya.Vector3();
        eulerAngles.setValue(
            -this.currentPitchDeg,  // X轴旋转(俯仰)
            this.currentYawDeg,     // Y轴旋转(偏航)
            0                       // Z轴旋转固定为0(禁止翻滚)
        );

        // 将欧拉角转换为四元数
        Laya.Quaternion.createFromYawPitchRoll(
            eulerAngles.y * Math.PI / 180,  // Yaw
            eulerAngles.x * Math.PI / 180,  // Pitch
            eulerAngles.z * Math.PI / 180,  // Roll (始终为0)
            this.tempQuat
        );

        this.owner.transform.rotation = this.tempQuat;

        // 调试信息:验证Z轴旋转是否为0
        if (this.isDragging) {
            const currentEuler = this.owner.transform.localRotationEuler;

        }
    }

    private lerpAngle(a: number, b: number, t: number): number {
        // 将角度差值包装到[-180,180]范围内,实现最短路径插值
        let delta = ((b - a + 540) % 360) - 180;
        return a + delta * t;
    }

    private getTargetPosition(): Laya.Vector3 {
        if (this.targetTransform) {
            return this.targetTransform.transform.position.clone();
        }
        return this.targetPosition;
    }

    private pointerOnUI(stageX: number, stageY: number): boolean {
        if (!this.uiRoot2D) return false;
        return this.uiRoot2D.hitTestPoint(stageX, stageY);
    }

    private hitAny3D(stageX: number, stageY: number): boolean {
        const scene3D = this.owner.scene as Laya.Scene3D;
        if (!scene3D) return false;

        const ray = new Laya.Ray(new Laya.Vector3(), new Laya.Vector3());
        const sp = new Laya.Vector2(stageX, stageY);
        this.owner.viewportPointToRay(sp, ray);
        const hr = new Laya.HitResult();

        return scene3D.physicsSimulation.rayCast(ray, hr, 1000);
    }

    // 公共方法:重置相机到初始位置
    public resetToInitialPosition(): void {
        this.initializeFromCurrentPosition();
    }

    // 公共方法:设置新的目标位置
    public setTargetPosition(position: Laya.Vector3): void {
        this.targetPosition = position.clone();
    }

    // 公共方法:设置新的目标变换
    public setTargetTransform(transform: Laya.Sprite3D): void {
        this.targetTransform = transform;
    }

    // 公共方法:设置距离
    public setDistance(distance: number): void {
        this.distanceToTarget = Math.max(0.1, distance);
    }

    // 公共方法:设置角度限制(仅X轴)
    public setAngleLimits(minPitch: number, maxPitch: number): void {
        this.minPitchDegrees = minPitch;
        this.maxPitchDegrees = maxPitch;

        // 立即应用限制(仅X轴)
        this.targetPitchDeg = this.clampPitch(this.targetPitchDeg);
        // Y轴不限制,保持原值
    }

    // 公共方法:设置平滑度
    public setSmoothness(smoothness: number): void {
        this.smoothness = Math.max(0, Math.min(1, smoothness));
    }

    // 公共方法:设置旋转灵敏度
    public setRotationSensitivity(sensitivity: number): void {
        this.rotationSensitivity = Math.max(0, sensitivity);
    }
    // 公共方法:设置相机旋转角度
    public setRotation(eulerAngles: Laya.Vector3): void {
        // 设置目标角度(使用欧拉角)
        this.targetYawDeg = eulerAngles.y;     // Y轴旋转(偏航)
        this.targetPitchDeg = -eulerAngles.x;  // X轴旋转(俯仰),取负值以匹配坐标系

        // 立即更新当前角度(无平滑过渡)
        this.currentYawDeg = this.targetYawDeg;
        this.currentPitchDeg = this.clampPitch(this.targetPitchDeg);

        // 立即更新相机位置
        this.updateCameraPosition();
    }
    onDisable(): void {
        this.detachEvents();
    }

    onDestroy(): void {
        this.detachEvents();
    }

    private detachEvents(): void {
        Laya.stage.off(Laya.Event.MOUSE_DOWN, this, this.onPointerDown);
        Laya.stage.off(Laya.Event.MOUSE_MOVE, this, this.onPointerMove);
        Laya.stage.off(Laya.Event.MOUSE_UP, this, this.onPointerUp);
        Laya.stage.off(Laya.Event.MOUSE_OUT, this, this.onPointerUp);
    }
}
相关推荐
大虾写代码2 小时前
vue3+TS项目配置Eslint+prettier+husky语法校验
前端·vue·eslint
wordbaby3 小时前
用 useEffectEvent 做精准埋点:React analytics pageview 场景的最佳实践与原理剖析
前端·react.js
上单带刀不带妹3 小时前
在 ES6 中如何提取深度嵌套的对象中的指定属性
前端·ecmascript·es6
excel3 小时前
使用热力贴图和高斯函数生成山峰与等高线的 WebGL Shader 解析
前端
wyzqhhhh3 小时前
组件库打包工具选型(npm/pnpm/yarn)的区别和技术考量
前端·npm·node.js
码上暴富3 小时前
vue2迁移到vite[保姆级教程]
前端·javascript·vue.js
土了个豆子的3 小时前
04.事件中心模块
开发语言·前端·visualstudio·单例模式·c#
全栈技术负责人4 小时前
Hybrid应用性能优化实战分享(本文iOS 与 H5为例,安卓同理)
前端·ios·性能优化·html5
xw54 小时前
移动端调试上篇
前端