This commit resolves several physics-related issues that were causing unexpected behavior in ship and asteroid movement: **Physics Sleep System** - Fixed abrupt stops by preventing Havok from putting bodies to sleep - Added PhysicsActivationControl.ALWAYS_ACTIVE for ship and asteroids - Made ship sleep behavior configurable via shipPhysics.alwaysActive - Sleep was causing sudden velocity zeroing at low speeds **Center of Mass Issues** - Discovered mesh-based physics calculated offset CoM: (0, -0.38, 0.37) - Override ship center of mass to (0, 0, 0) to prevent thrust torque - Applying force at offset CoM was creating unwanted pitch rotation - Added debug logging to track mass properties **Input Deadzone Improvements** - Implemented smooth deadzone scaling (0.1-0.15 range) - Replaced hard threshold cliff with linear interpolation - Prevents abrupt control cutoff during gentle inputs - Added VR mode check to disable keyboard fallback in VR **Configuration System** - Added DEFAULT_SHIP_PHYSICS constant as single source of truth - Added tunable parameters: linearDamping, angularDamping, alwaysActive - Added fuel consumption rates: linearFuelConsumptionRate, angularFuelConsumptionRate - Tuned for 1 minute linear thrust, 2 minutes angular thrust at 60Hz - All physics parameters now persist to localStorage **Other Fixes** - Changed orbit center to STATIC motion type (was ANIMATED) - Fixed linear force application point (removed offset) - Added ship initial velocity support from level config - Changed physics update from every 10 frames to every physics tick - Increased linear input threshold from 0.1 to 0.15 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
186 lines
8.0 KiB
TypeScript
186 lines
8.0 KiB
TypeScript
import {
|
|
AbstractMesh,
|
|
AudioEngineV2,
|
|
DistanceConstraint,
|
|
HavokPlugin,
|
|
InstancedMesh,
|
|
Mesh,
|
|
Observable,
|
|
PhysicsActivationControl,
|
|
PhysicsAggregate,
|
|
PhysicsBody,
|
|
PhysicsMotionType,
|
|
PhysicsShapeType,
|
|
TransformNode,
|
|
Vector3
|
|
} from "@babylonjs/core";
|
|
import {DefaultScene} from "../../core/defaultScene";
|
|
import {ScoreEvent} from "../../ui/hud/scoreboard";
|
|
import {GameConfig} from "../../core/gameConfig";
|
|
import {ExplosionManager} from "./explosionManager";
|
|
import debugLog from '../../core/debug';
|
|
import loadAsset from "../../utils/loadAsset";
|
|
|
|
export class Rock {
|
|
private _rockMesh: AbstractMesh;
|
|
constructor(mesh: AbstractMesh) {
|
|
this._rockMesh = mesh;
|
|
}
|
|
public get physicsBody(): PhysicsBody | null {
|
|
return this._rockMesh.physicsBody || null;
|
|
}
|
|
public get position(): Vector3 {
|
|
return this._rockMesh.getAbsolutePosition();
|
|
}
|
|
}
|
|
|
|
export class RockFactory {
|
|
private static _asteroidMesh: AbstractMesh;
|
|
private static _explosionManager: ExplosionManager;
|
|
private static _orbitCenter: PhysicsAggregate;
|
|
|
|
/**
|
|
* Initialize non-audio assets (meshes, explosion manager)
|
|
* Call this before audio engine is unlocked
|
|
*/
|
|
public static async init() {
|
|
// Initialize explosion manager
|
|
const node = new TransformNode('orbitCenter', DefaultScene.MainScene);
|
|
node.position = Vector3.Zero();
|
|
this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 0}, DefaultScene.MainScene );
|
|
this._orbitCenter.body.setMotionType(PhysicsMotionType.STATIC);
|
|
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
|
|
duration: 2000,
|
|
explosionForce: 150.0,
|
|
frameRate: 60
|
|
});
|
|
await this._explosionManager.initialize();
|
|
|
|
if (!this._asteroidMesh) {
|
|
await this.loadMesh();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize audio (explosion sound)
|
|
* Call this AFTER audio engine is unlocked
|
|
*/
|
|
public static async initAudio(audioEngine: AudioEngineV2) {
|
|
debugLog('[RockFactory] Initializing audio via ExplosionManager');
|
|
await this._explosionManager.initAudio(audioEngine);
|
|
debugLog('[RockFactory] Audio initialization complete');
|
|
}
|
|
private static async loadMesh() {
|
|
debugLog('loading mesh');
|
|
this._asteroidMesh = (await loadAsset("asteroid.glb")).meshes.get('Asteroid');
|
|
//this._asteroidMesh.setParent(null);
|
|
this._asteroidMesh.setEnabled(false);
|
|
debugLog(this._asteroidMesh);
|
|
}
|
|
|
|
public static async createRock(i: number, position: Vector3, scale: number,
|
|
linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable<ScoreEvent>,
|
|
useOrbitConstraint: boolean = true): Promise<Rock> {
|
|
|
|
const rock = new InstancedMesh("asteroid-" +i, this._asteroidMesh as Mesh);
|
|
debugLog(rock.id);
|
|
rock.scaling = new Vector3(scale, scale, scale);
|
|
rock.position = position;
|
|
//rock.material = this._rockMaterial;
|
|
rock.name = "asteroid-" + i;
|
|
rock.id = "asteroid-" + i;
|
|
rock.metadata = {type: 'asteroid'};
|
|
rock.setEnabled(true);
|
|
|
|
// Only create physics if enabled in config
|
|
const config = GameConfig.getInstance();
|
|
if (config.physicsEnabled) {
|
|
// PhysicsAggregate will automatically compute sphere size from mesh bounding info
|
|
// The mesh scaling is already applied, so Babylon will create correctly sized physics shape
|
|
const agg = new PhysicsAggregate(rock, PhysicsShapeType.SPHERE, {
|
|
mass: 10000,
|
|
restitution: .5
|
|
// Don't pass radius - let Babylon compute from scaled mesh bounds
|
|
}, DefaultScene.MainScene);
|
|
const body = agg.body;
|
|
|
|
// Only apply orbit constraint if enabled for this level
|
|
if (useOrbitConstraint) {
|
|
debugLog(`[RockFactory] Applying orbit constraint for ${rock.name}`);
|
|
const constraint = new DistanceConstraint(Vector3.Distance(position, this._orbitCenter.body.transformNode.position), DefaultScene.MainScene);
|
|
body.addConstraint(this._orbitCenter.body, constraint);
|
|
} else {
|
|
debugLog(`[RockFactory] Orbit constraint disabled for ${rock.name} - asteroid will move freely`);
|
|
}
|
|
|
|
body.setLinearDamping(0)
|
|
body.setMotionType(PhysicsMotionType.DYNAMIC);
|
|
body.setCollisionCallbackEnabled(true);
|
|
|
|
// Prevent asteroids from sleeping to ensure consistent physics simulation
|
|
const physicsPlugin = DefaultScene.MainScene.getPhysicsEngine()?.getPhysicsPlugin() as HavokPlugin;
|
|
if (physicsPlugin) {
|
|
physicsPlugin.setActivationControl(body, PhysicsActivationControl.ALWAYS_ACTIVE);
|
|
}
|
|
|
|
debugLog(`[RockFactory] Setting velocities for ${rock.name}:`);
|
|
debugLog(`[RockFactory] Linear velocity input: ${linearVelocitry.toString()}`);
|
|
debugLog(`[RockFactory] Angular velocity input: ${angularVelocity.toString()}`);
|
|
|
|
body.setLinearVelocity(linearVelocitry);
|
|
body.setAngularVelocity(angularVelocity);
|
|
|
|
// Verify velocities were set
|
|
const setLinear = body.getLinearVelocity();
|
|
const setAngular = body.getAngularVelocity();
|
|
debugLog(`[RockFactory] Linear velocity after set: ${setLinear.toString()}`);
|
|
debugLog(`[RockFactory] Angular velocity after set: ${setAngular.toString()}`);
|
|
body.getCollisionObservable().add((eventData) => {
|
|
if (eventData.type == 'COLLISION_STARTED') {
|
|
if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
|
|
debugLog('[RockFactory] ASTEROID HIT! Triggering explosion...');
|
|
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
|
|
|
|
// Get the asteroid mesh before disposing
|
|
const asteroidMesh = eventData.collider.transformNode as AbstractMesh;
|
|
debugLog('[RockFactory] Asteroid mesh to explode:', {
|
|
name: asteroidMesh.name,
|
|
id: asteroidMesh.id,
|
|
position: asteroidMesh.getAbsolutePosition().toString()
|
|
});
|
|
|
|
// Dispose asteroid physics objects BEFORE explosion (to prevent double-disposal)
|
|
debugLog('[RockFactory] Disposing asteroid physics objects...');
|
|
if (eventData.collider.shape) {
|
|
eventData.collider.shape.dispose();
|
|
}
|
|
if (eventData.collider) {
|
|
eventData.collider.dispose();
|
|
}
|
|
|
|
// Play explosion (visual + audio handled by ExplosionManager)
|
|
// Note: ExplosionManager will dispose the asteroid mesh after explosion
|
|
RockFactory._explosionManager.playExplosion(asteroidMesh);
|
|
|
|
// Dispose projectile physics objects
|
|
debugLog('[RockFactory] Disposing projectile physics objects...');
|
|
if (eventData.collidedAgainst.shape) {
|
|
eventData.collidedAgainst.shape.dispose();
|
|
}
|
|
if (eventData.collidedAgainst.transformNode) {
|
|
eventData.collidedAgainst.transformNode.dispose();
|
|
}
|
|
if (eventData.collidedAgainst) {
|
|
eventData.collidedAgainst.dispose();
|
|
}
|
|
debugLog('[RockFactory] Disposal complete');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return new Rock(rock);
|
|
}
|
|
}
|
|
|