Files
gta6/three-player-controller/src/systems/PlayerDebug.ts
T
knsoandClaude Opus 5 fb38652e7f chore(controller): three-player-controller als Ordner statt Submodul
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>
2026-09-15 21:27:03 +02:00

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);
}
}