Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json. Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser Stand als normaler Ordner im Repo, ohne Verweis auf GitHub. dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher lokal gebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
273 lines
10 KiB
TypeScript
273 lines
10 KiB
TypeScript
import * as THREE from "three";
|
|
|
|
import type { playerController } from "../PlayerController";
|
|
|
|
const HIT_COLOR = 0x66ff66;
|
|
const FAR_HIT_COLOR = 0xffcc33;
|
|
const MISS_COLOR = 0xff5555;
|
|
const SENSOR_IDLE_COLOR = 0x45d7ff;
|
|
|
|
/** 绘制玩家胶囊与地面探测结果。 */
|
|
export class PlayerDebug {
|
|
private visible = false;
|
|
private hasGroundProbe = false;
|
|
private groundProbeHasHit = false;
|
|
private groundProbeMaxDistance = 0;
|
|
private groundProbeNear = 0;
|
|
private groundRay: THREE.Line | null = null;
|
|
private groundHit: THREE.Mesh | null = null;
|
|
private groundProbePoint = new THREE.Vector3();
|
|
private hasGroundSensor = false;
|
|
private groundSensorActive = false;
|
|
private groundSensorHasHit = false;
|
|
private groundSensorMaxDistance = 0;
|
|
private groundSensorRadius = 0;
|
|
private groundSensor: THREE.Mesh | null = null;
|
|
private readonly probeLocalPoint = new THREE.Vector3();
|
|
private readonly capsuleLocalDown = new THREE.Vector3(0, -1, 0);
|
|
private debugRoot: THREE.Group | null = null;
|
|
|
|
constructor(private readonly ctrl: playerController) { }
|
|
|
|
/** 返回人物调试开关。 */
|
|
isVisible(): boolean {
|
|
return this.visible;
|
|
}
|
|
|
|
/** 切换人物胶囊与地面探测调试。 */
|
|
setVisible(visible: boolean): void {
|
|
this.visible = visible;
|
|
this.syncVisibility();
|
|
}
|
|
|
|
/** 在玩家模型或控制模式变化后重新同步显隐。 */
|
|
syncVisibility(): void {
|
|
const capsule = this.ctrl.playerCapsule;
|
|
if (capsule) {
|
|
const materials = Array.isArray(capsule.material) ? capsule.material : [capsule.material];
|
|
for (const material of materials) material.visible = this.visible;
|
|
}
|
|
|
|
const showProbe = this.visible && this.hasGroundProbe && this.ctrl.controllerMode === 0;
|
|
const showSensor = this.visible && this.hasGroundSensor && this.ctrl.controllerMode === 0;
|
|
if (this.groundRay) this.groundRay.visible = showProbe;
|
|
if (this.groundHit) this.groundHit.visible = showProbe && this.groundProbeHasHit;
|
|
if (this.groundSensor) this.groundSensor.visible = showSensor;
|
|
if (showProbe) this.renderGroundProbe();
|
|
if (showSensor) this.renderGroundSensor();
|
|
}
|
|
|
|
/**
|
|
* 移动结束后刷新调试体姿态,使射线/体积传感区跟随胶囊实时位置。
|
|
*/
|
|
refreshTransforms(): void {
|
|
if (!this.visible || this.ctrl.controllerMode !== 0) return;
|
|
if (this.hasGroundProbe) this.renderGroundProbe();
|
|
if (this.hasGroundSensor) this.renderGroundSensor();
|
|
}
|
|
|
|
/** 写入移动系统本帧实际使用的中心地面探测结果。 */
|
|
updateGroundProbe(
|
|
origin: THREE.Vector3,
|
|
hitPoint: THREE.Vector3 | null,
|
|
maxDistance: number,
|
|
near = 0,
|
|
): void {
|
|
this.hasGroundProbe = true;
|
|
this.groundProbeHasHit = hitPoint !== null;
|
|
this.groundProbeMaxDistance = Math.max(0, maxDistance);
|
|
this.groundProbeNear = Math.max(0, near);
|
|
if (hitPoint) this.groundProbePoint.copy(hitPoint);
|
|
else this.groundProbePoint.copy(origin).y -= this.groundProbeMaxDistance;
|
|
}
|
|
|
|
/** 写入胶囊下方体积地面传感区的当前状态。 */
|
|
updateGroundSensor(
|
|
_start: THREE.Vector3,
|
|
_down: THREE.Vector3,
|
|
maxDistance: number,
|
|
radius: number,
|
|
active: boolean,
|
|
hasHit: boolean,
|
|
): void {
|
|
this.hasGroundSensor = maxDistance > 0 && radius > 0;
|
|
this.groundSensorActive = active;
|
|
this.groundSensorHasHit = hasHit;
|
|
this.groundSensorMaxDistance = Math.max(0, maxDistance);
|
|
this.groundSensorRadius = Math.max(0, radius);
|
|
}
|
|
|
|
/** 清除当前地面探测显示。 */
|
|
clearGroundProbe(): void {
|
|
this.hasGroundProbe = false;
|
|
this.hasGroundSensor = false;
|
|
if (this.groundRay) this.groundRay.visible = false;
|
|
if (this.groundHit) this.groundHit.visible = false;
|
|
if (this.groundSensor) this.groundSensor.visible = false;
|
|
}
|
|
|
|
/** 释放人物调试几何与材质。 */
|
|
dispose(): void {
|
|
if (this.debugRoot) {
|
|
this.debugRoot.removeFromParent();
|
|
this.debugRoot.traverse((obj) => {
|
|
const mesh = obj as THREE.Mesh;
|
|
mesh.geometry?.dispose();
|
|
const mat = mesh.material;
|
|
if (mat) {
|
|
if (Array.isArray(mat)) mat.forEach(m => m.dispose());
|
|
else mat.dispose();
|
|
}
|
|
});
|
|
this.debugRoot = null;
|
|
}
|
|
this.groundRay = null;
|
|
this.groundHit = null;
|
|
this.groundSensor = null;
|
|
this.hasGroundProbe = false;
|
|
this.hasGroundSensor = false;
|
|
}
|
|
|
|
private ensureDebugRoot(): THREE.Group | null {
|
|
const capsule = this.ctrl.playerCapsule;
|
|
if (!capsule) return null;
|
|
|
|
if (!this.debugRoot) {
|
|
this.debugRoot = new THREE.Group();
|
|
this.debugRoot.name = "playerDebugRoot";
|
|
capsule.add(this.debugRoot);
|
|
} else if (this.debugRoot.parent !== capsule) {
|
|
capsule.add(this.debugRoot);
|
|
}
|
|
return this.debugRoot;
|
|
}
|
|
|
|
/** 按当前探测结果更新线段、命中点和颜色。 */
|
|
private renderGroundProbe(): void {
|
|
const capsule = this.ctrl.playerCapsule;
|
|
const root = this.ensureDebugRoot();
|
|
if (!capsule || !root) return;
|
|
|
|
this.ensureGroundProbeObjects(root);
|
|
if (!this.groundRay || !this.groundHit) return;
|
|
|
|
this.worldToCapsuleLocal(this.groundProbePoint, this.probeLocalPoint);
|
|
const positions = this.groundRay.geometry.getAttribute("position") as THREE.BufferAttribute;
|
|
positions.setXYZ(0, 0, 0, 0);
|
|
positions.setXYZ(1, this.probeLocalPoint.x, this.probeLocalPoint.y, this.probeLocalPoint.z);
|
|
positions.needsUpdate = true;
|
|
|
|
const distance = capsule.position.y - this.groundProbePoint.y;
|
|
const hitInRange = this.groundProbeHasHit
|
|
&& distance >= this.groundProbeNear
|
|
&& distance <= this.groundProbeMaxDistance;
|
|
const color = hitInRange ? HIT_COLOR : this.groundProbeHasHit ? FAR_HIT_COLOR : MISS_COLOR;
|
|
(this.groundRay.material as THREE.LineBasicMaterial).color.setHex(color);
|
|
this.groundRay.visible = true;
|
|
|
|
this.groundHit.visible = this.groundProbeHasHit;
|
|
if (this.groundProbeHasHit) {
|
|
this.groundHit.position.copy(this.probeLocalPoint);
|
|
const sy = Math.max(1e-6, capsule.scale.y || 1);
|
|
const localRadius = (capsule.capsuleInfo?.radius ?? 0) / sy;
|
|
this.groundHit.scale.setScalar(Math.max(1e-6, localRadius * 0.05));
|
|
(this.groundHit.material as THREE.MeshBasicMaterial).color.setHex(color);
|
|
}
|
|
}
|
|
|
|
/** 按当前状态更新体积地面传感区(锚定在胶囊 segment.end)。 */
|
|
private renderGroundSensor(): void {
|
|
const capsule = this.ctrl.playerCapsule;
|
|
const root = this.ensureDebugRoot();
|
|
const segEnd = capsule?.capsuleInfo?.segment.end;
|
|
if (!capsule || !root || !segEnd) return;
|
|
|
|
this.ensureGroundSensorObject(root);
|
|
if (!this.groundSensor) return;
|
|
|
|
const distance = Math.max(this.groundSensorMaxDistance, 1e-6);
|
|
this.groundSensor.position.copy(segEnd).addScaledVector(this.capsuleLocalDown, distance * 0.5);
|
|
this.groundSensor.quaternion.identity();
|
|
this.groundSensor.scale.set(this.groundSensorRadius, distance, this.groundSensorRadius);
|
|
|
|
const material = this.groundSensor.material as THREE.MeshBasicMaterial;
|
|
material.color.setHex(
|
|
this.groundSensorActive
|
|
? this.groundSensorHasHit ? HIT_COLOR : MISS_COLOR
|
|
: SENSOR_IDLE_COLOR,
|
|
);
|
|
material.opacity = this.groundSensorActive ? 0.75 : 0.4;
|
|
this.groundSensor.visible = this.hasGroundSensor;
|
|
}
|
|
|
|
private worldToCapsuleLocal(world: THREE.Vector3, target: THREE.Vector3): THREE.Vector3 {
|
|
target.copy(world);
|
|
this.ctrl.playerCapsule.worldToLocal(target);
|
|
return target;
|
|
}
|
|
|
|
/** 延迟创建地面射线和命中标记。 */
|
|
private ensureGroundProbeObjects(root: THREE.Group): void {
|
|
if (!this.groundRay) {
|
|
this.groundRay = new THREE.Line(
|
|
new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3()]),
|
|
new THREE.LineBasicMaterial({
|
|
color: MISS_COLOR,
|
|
depthTest: true,
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
}),
|
|
);
|
|
this.groundRay.name = "playerGroundRayDebug";
|
|
this.groundRay.renderOrder = 30;
|
|
this.groundRay.frustumCulled = false;
|
|
root.add(this.groundRay);
|
|
} else if (this.groundRay.parent !== root) {
|
|
root.add(this.groundRay);
|
|
}
|
|
|
|
if (!this.groundHit) {
|
|
this.groundHit = new THREE.Mesh(
|
|
new THREE.SphereGeometry(2, 10, 8),
|
|
new THREE.MeshBasicMaterial({
|
|
color: HIT_COLOR,
|
|
wireframe: true,
|
|
depthTest: true,
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
}),
|
|
);
|
|
this.groundHit.name = "playerGroundRayHitDebug";
|
|
this.groundHit.renderOrder = 31;
|
|
this.groundHit.frustumCulled = false;
|
|
root.add(this.groundHit);
|
|
} else if (this.groundHit.parent !== root) {
|
|
root.add(this.groundHit);
|
|
}
|
|
}
|
|
|
|
/** 延迟创建体积地面传感区线框。 */
|
|
private ensureGroundSensorObject(root: THREE.Group): void {
|
|
if (this.groundSensor) {
|
|
if (this.groundSensor.parent !== root) root.add(this.groundSensor);
|
|
return;
|
|
}
|
|
this.groundSensor = new THREE.Mesh(
|
|
new THREE.CylinderGeometry(1, 1, 1, 16, 1, true),
|
|
new THREE.MeshBasicMaterial({
|
|
color: SENSOR_IDLE_COLOR,
|
|
wireframe: true,
|
|
depthTest: true,
|
|
depthWrite: true,
|
|
transparent: true,
|
|
opacity: 0.4,
|
|
side: THREE.DoubleSide,
|
|
}),
|
|
);
|
|
this.groundSensor.name = "playerGroundVolumeDebug";
|
|
this.groundSensor.renderOrder = 29;
|
|
this.groundSensor.frustumCulled = false;
|
|
root.add(this.groundSensor);
|
|
}
|
|
}
|