import { _decorator, Component, EventTouch, find, Node, UITransform, v3, Vec3 } from 'cc'; import { MoveDirection, SuDacePlayer } from './SuDacePlayer'; import { SuDace } from './SuDace'; const { ccclass, property } = _decorator; /** * 浮动摇杆:在固定 touchArea 内按下,以触点为摇杆中心;拖拽方向量化四向,驱动 SuDacePlayer.setMoveInput。 * 可选 stickRoot / knob 用于在按下处显示底板与拖动的摇杆头。 */ @ccclass('SuDaceJoystick') export class SuDaceJoystick extends Component { @property(SuDacePlayer) player: SuDacePlayer = null; /** 固定可触摸区域(全屏透明层或左下角区域等) */ @property(Node) touchArea: Node = null; /** 按下时显示在触点处的摇杆底板节点(可选) */ @property(Node) stickRoot: Node = null; /** 相对 stickRoot 中心拖动的摇杆头(可选) */ @property(Node) knob: Node = null; @property({ tooltip: '摇杆头最大偏移(与 UI 像素一致,用于显示)' }) maxKnobOffset = 80; @property({ tooltip: '死区半径(相对本次按下中心),低于此视为无方向输入' }) deadRadius = 24; private readonly _centerUi = v3(); private readonly _tmpLocal = v3(); private _trackingId = -1; protected onEnable(): void { this.resolvePlayerRef(); if (this.stickRoot) { this.stickRoot.active = false; } const surface = this.touchArea ?? this.node; surface.on(Node.EventType.TOUCH_START, this.onTouchStart, this); surface.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this); surface.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); surface.on(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); } protected onDisable(): void { const surface = this.touchArea ?? this.node; surface.off(Node.EventType.TOUCH_START, this.onTouchStart, this); surface.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this); surface.off(Node.EventType.TOUCH_END, this.onTouchEnd, this); surface.off(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); this.endSession(); } private onTouchStart(event: EventTouch): void { if (this._trackingId !== -1) return; if (!this.player?.isValid) return; const tid = event.getID(); this._trackingId = tid !== null && tid !== undefined ? tid : 0; const loc = event.getUILocation(); this._centerUi.set(loc.x, loc.y, 0); if (this.stickRoot) { this.stickRoot.active = true; this.placeNodeAtUi(this.stickRoot, this._centerUi); } this.updateKnob(0, 0); this.player.setMoveInput(MoveDirection.NONE); } private onTouchMove(event: EventTouch): void { if (!this.isOurTouch(event) || !this.player?.isValid) return; const loc = event.getUILocation(); const dx = loc.x - this._centerUi.x; const dy = loc.y - this._centerUi.y; this.updateKnob(dx, dy); this.player.setMoveInput(this.quantizeFourWay(dx, dy)); } private onTouchEnd(event: EventTouch): void { if (!this.isOurTouch(event)) return; this.endSession(); } private isOurTouch(event: EventTouch): boolean { if (this._trackingId === -1) return false; const id = event.getID(); const eid = id !== null && id !== undefined ? id : 0; return eid === this._trackingId; } private endSession(): void { this._trackingId = -1; if (this.stickRoot?.isValid) { this.stickRoot.active = false; } if (this.player?.isValid) { this.player.setMoveInput(MoveDirection.NONE); } } /** * 兜底回填 player,避免预制体覆盖丢失引用时摇杆失效。 */ private resolvePlayerRef(): void { if (this.player?.isValid) return; this.player = SuDace.instance?.map?.player ?? find('map/player', this.node)?.getComponent(SuDacePlayer) ?? null; } /** 将 stickRoot 等节点放到 UI 坐标对应位置(相对其父节点 UITransform) */ private placeNodeAtUi(target: Node, uiWorld: Readonly): void { const parent = target.parent; const ui = parent?.getComponent(UITransform); if (!ui) return; ui.convertToNodeSpaceAR(uiWorld, this._tmpLocal); target.setPosition(this._tmpLocal); } private updateKnob(dx: number, dy: number): void { if (!this.knob?.isValid) return; const max = this.maxKnobOffset; let kx = dx; let ky = dy; const len = Math.sqrt(dx * dx + dy * dy); if (len > max && len > 1e-6) { const s = max / len; kx *= s; ky *= s; } this.knob.setPosition(kx, ky, 0); } /** 将相对中心的偏移量量化为四方向(主轴优先) */ private quantizeFourWay(ox: number, oy: number): MoveDirection { const r = this.deadRadius; if (ox * ox + oy * oy < r * r) { return MoveDirection.NONE; } if (Math.abs(ox) >= Math.abs(oy)) { return ox > 0 ? MoveDirection.RIGHT : MoveDirection.LEFT; } return oy > 0 ? MoveDirection.UP : MoveDirection.DOWN; } }