230 lines
6.5 KiB
JavaScript
230 lines
6.5 KiB
JavaScript
|
|
// movementManager.js
|
||
|
|
import * as THREE from 'three';
|
||
|
|
|
||
|
|
export class MovementManager {
|
||
|
|
constructor(vrm, animationMgr, scene) {
|
||
|
|
this.vrm = vrm;
|
||
|
|
this.animationMgr = animationMgr;
|
||
|
|
this.scene = scene;
|
||
|
|
|
||
|
|
// Movement state
|
||
|
|
this.isMoving = false;
|
||
|
|
this.targetPosition = null;
|
||
|
|
this.moveSpeed = 1.5; // units per second
|
||
|
|
this.rotationSpeed = 5; // radians per second
|
||
|
|
this.stopDistance = 0.1; // how close to get before stopping
|
||
|
|
|
||
|
|
// Animation states
|
||
|
|
this.idleAction = null;
|
||
|
|
this.walkAction = null;
|
||
|
|
this.runAction = null;
|
||
|
|
this.currentAction = null;
|
||
|
|
|
||
|
|
// Movement mixer (separate from VRMA mixer)
|
||
|
|
this.movementMixer = new THREE.AnimationMixer(vrm.scene);
|
||
|
|
|
||
|
|
// Debug helper (optional)
|
||
|
|
this.targetMarker = null;
|
||
|
|
this.showDebugMarker = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load locomotion animations (idle, walk, run)
|
||
|
|
async loadLocomotionAnimations(idleUrl, walkUrl, runUrl = null) {
|
||
|
|
const loader = new THREE.GLTFLoader();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Load idle animation
|
||
|
|
if (idleUrl) {
|
||
|
|
const idleGltf = await loader.loadAsync(idleUrl);
|
||
|
|
const idleClip = idleGltf.animations[0];
|
||
|
|
this.idleAction = this.movementMixer.clipAction(idleClip);
|
||
|
|
this.idleAction.setLoop(THREE.LoopRepeat);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load walk animation
|
||
|
|
if (walkUrl) {
|
||
|
|
const walkGltf = await loader.loadAsync(walkUrl);
|
||
|
|
const walkClip = walkGltf.animations[0];
|
||
|
|
this.walkAction = this.movementMixer.clipAction(walkClip);
|
||
|
|
this.walkAction.setLoop(THREE.LoopRepeat);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load run animation (optional)
|
||
|
|
if (runUrl) {
|
||
|
|
const runGltf = await loader.loadAsync(runUrl);
|
||
|
|
const runClip = runGltf.animations[0];
|
||
|
|
this.runAction = this.movementMixer.clipAction(runClip);
|
||
|
|
this.runAction.setLoop(THREE.LoopRepeat);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Start with idle
|
||
|
|
if (this.idleAction) {
|
||
|
|
this.currentAction = this.idleAction;
|
||
|
|
this.idleAction.play();
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log("✅ Locomotion animations loaded");
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load locomotion animations:", error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Transition between animations with crossfade
|
||
|
|
crossFadeTo(targetAction, duration = 0.3) {
|
||
|
|
if (!targetAction || targetAction === this.currentAction) return;
|
||
|
|
|
||
|
|
targetAction.reset();
|
||
|
|
targetAction.setEffectiveTimeScale(1);
|
||
|
|
targetAction.setEffectiveWeight(1);
|
||
|
|
targetAction.play();
|
||
|
|
|
||
|
|
if (this.currentAction) {
|
||
|
|
this.currentAction.crossFadeTo(targetAction, duration, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.currentAction = targetAction;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Move to a specific position
|
||
|
|
moveTo(x, y, z, speed = null) {
|
||
|
|
this.targetPosition = new THREE.Vector3(x, y, z);
|
||
|
|
this.isMoving = true;
|
||
|
|
|
||
|
|
if (speed) {
|
||
|
|
this.moveSpeed = speed;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Switch to walk animation
|
||
|
|
if (this.walkAction) {
|
||
|
|
this.crossFadeTo(this.walkAction, 0.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Show debug marker
|
||
|
|
if (this.showDebugMarker) {
|
||
|
|
this.createTargetMarker(this.targetPosition);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`🚶 Moving to position: (${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Stop movement
|
||
|
|
stop() {
|
||
|
|
this.isMoving = false;
|
||
|
|
this.targetPosition = null;
|
||
|
|
|
||
|
|
// Switch back to idle
|
||
|
|
if (this.idleAction) {
|
||
|
|
this.crossFadeTo(this.idleAction, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove debug marker
|
||
|
|
if (this.targetMarker) {
|
||
|
|
this.scene.remove(this.targetMarker);
|
||
|
|
this.targetMarker = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update loop (call this in your main animation loop)
|
||
|
|
update(deltaTime) {
|
||
|
|
// Update movement mixer
|
||
|
|
if (this.movementMixer) {
|
||
|
|
this.movementMixer.update(deltaTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle movement
|
||
|
|
if (this.isMoving && this.targetPosition) {
|
||
|
|
const currentPos = this.vrm.scene.position;
|
||
|
|
const direction = new THREE.Vector3()
|
||
|
|
.subVectors(this.targetPosition, currentPos);
|
||
|
|
|
||
|
|
const distance = direction.length();
|
||
|
|
|
||
|
|
// Check if we've arrived
|
||
|
|
if (distance < this.stopDistance) {
|
||
|
|
this.stop();
|
||
|
|
console.log("✅ Arrived at destination");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Normalize direction
|
||
|
|
direction.normalize();
|
||
|
|
|
||
|
|
// Move towards target
|
||
|
|
const moveDistance = this.moveSpeed * deltaTime;
|
||
|
|
const actualMove = Math.min(moveDistance, distance);
|
||
|
|
|
||
|
|
currentPos.x += direction.x * actualMove;
|
||
|
|
currentPos.z += direction.z * actualMove;
|
||
|
|
// Keep Y position (don't move vertically unless specified)
|
||
|
|
if (this.targetPosition.y !== currentPos.y) {
|
||
|
|
currentPos.y = this.targetPosition.y;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Rotate to face direction of movement
|
||
|
|
const targetRotation = Math.atan2(direction.x, direction.z);
|
||
|
|
const currentRotation = this.vrm.scene.rotation.y;
|
||
|
|
|
||
|
|
// Smooth rotation interpolation
|
||
|
|
let rotationDiff = targetRotation - currentRotation;
|
||
|
|
|
||
|
|
// Normalize rotation difference to [-PI, PI]
|
||
|
|
while (rotationDiff > Math.PI) rotationDiff -= Math.PI * 2;
|
||
|
|
while (rotationDiff < -Math.PI) rotationDiff += Math.PI * 2;
|
||
|
|
|
||
|
|
const maxRotation = this.rotationSpeed * deltaTime;
|
||
|
|
const actualRotation = Math.max(-maxRotation, Math.min(maxRotation, rotationDiff));
|
||
|
|
|
||
|
|
this.vrm.scene.rotation.y += actualRotation;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create a visual marker at target position (for debugging)
|
||
|
|
createTargetMarker(position) {
|
||
|
|
// Remove old marker
|
||
|
|
if (this.targetMarker) {
|
||
|
|
this.scene.remove(this.targetMarker);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create new marker
|
||
|
|
const geometry = new THREE.SphereGeometry(0.1, 16, 16);
|
||
|
|
const material = new THREE.MeshBasicMaterial({
|
||
|
|
color: 0xff0000,
|
||
|
|
transparent: true,
|
||
|
|
opacity: 0.7
|
||
|
|
});
|
||
|
|
this.targetMarker = new THREE.Mesh(geometry, material);
|
||
|
|
this.targetMarker.position.copy(position);
|
||
|
|
this.scene.add(this.targetMarker);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Enable/disable debug marker
|
||
|
|
setDebugMarker(enabled) {
|
||
|
|
this.showDebugMarker = enabled;
|
||
|
|
if (!enabled && this.targetMarker) {
|
||
|
|
this.scene.remove(this.targetMarker);
|
||
|
|
this.targetMarker = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set movement speed
|
||
|
|
setSpeed(speed) {
|
||
|
|
this.moveSpeed = speed;
|
||
|
|
|
||
|
|
// Optionally switch to run animation for higher speeds
|
||
|
|
if (this.runAction && speed > 2.5 && this.isMoving) {
|
||
|
|
this.crossFadeTo(this.runAction, 0.2);
|
||
|
|
} else if (this.walkAction && speed <= 2.5 && this.isMoving) {
|
||
|
|
this.crossFadeTo(this.walkAction, 0.2);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get current position
|
||
|
|
getPosition() {
|
||
|
|
return this.vrm.scene.position.clone();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if currently moving
|
||
|
|
isCurrentlyMoving() {
|
||
|
|
return this.isMoving;
|
||
|
|
}
|
||
|
|
}
|