Performance fixes and debug features
All checks were successful
Build / build (push) Successful in 1m45s

- Fix duplicate render loops causing 50% FPS drop (70→40)
  - Add stopRenderLoop() before runRenderLoop() in level1.ts and levelSelectedHandler.ts
- Add ?loglevel=debug|info|warn|error query parameter
- Add Y button to toggle inspector in XR
- Throttle scoreboard updates to every 10 frames
- Throttle game-end condition checks to every 30 frames
- Remove per-frame logging from explosion animations
- Reduce background stars from 5000 to 2500
- Freeze asteroid material after loading
- Reduce physics substeps from 5 to 2
- Disable autoClear for Quest 2 performance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-29 07:58:15 -06:00
parent cf3a74ff0b
commit b46f44e32d
10 changed files with 60 additions and 19 deletions

View File

@ -97,6 +97,7 @@ export function createLevelSelectedHandler(context: LevelSelectedContext): (e: C
if (canvas) { if (canvas) {
canvas.style.display = 'block'; canvas.style.display = 'block';
} }
engine.stopRenderLoop();
engine.runRenderLoop(() => { engine.runRenderLoop(() => {
DefaultScene.MainScene.render(); DefaultScene.MainScene.render();
}); });
@ -164,6 +165,7 @@ export function createLevelSelectedHandler(context: LevelSelectedContext): (e: C
if (canvas) { if (canvas) {
canvas.style.display = 'block'; canvas.style.display = 'block';
} }
engine.stopRenderLoop();
engine.runRenderLoop(() => { engine.runRenderLoop(() => {
DefaultScene.MainScene.render(); DefaultScene.MainScene.render();
}); });

View File

@ -1,5 +1,12 @@
import log from 'loglevel'; import log from 'loglevel';
// Check URL query parameter for log level override
const urlParams = new URLSearchParams(window.location.search);
const queryLevel = urlParams.get('loglevel');
if (queryLevel && ['debug', 'info', 'warn', 'error'].includes(queryLevel)) {
localStorage.setItem('log-level', queryLevel);
}
// Check localStorage for custom level (enables production debugging) // Check localStorage for custom level (enables production debugging)
const storedLevel = localStorage.getItem('log-level'); const storedLevel = localStorage.getItem('log-level');

View File

@ -59,6 +59,11 @@ function createMainScene(engine: Engine): void {
DefaultScene.MainScene = new Scene(engine); DefaultScene.MainScene = new Scene(engine);
DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2); DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2);
DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4(); DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4();
// Performance optimizations for Quest 2
//DefaultScene.MainScene.performancePriority = ScenePerformancePriority.Intermediate;
DefaultScene.MainScene.autoClear = false;
DefaultScene.MainScene.autoClearDepthAndStencil = false;
} }
async function setupPhysics(): Promise<void> { async function setupPhysics(): Promise<void> {
@ -66,7 +71,7 @@ async function setupPhysics(): Promise<void> {
const havokPlugin = new HavokPlugin(true, havok); const havokPlugin = new HavokPlugin(true, havok);
DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin); DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin);
DefaultScene.MainScene.getPhysicsEngine()!.setTimeStep(1/60); DefaultScene.MainScene.getPhysicsEngine()!.setTimeStep(1/60);
DefaultScene.MainScene.getPhysicsEngine()!.setSubTimeStep(5); DefaultScene.MainScene.getPhysicsEngine()!.setSubTimeStep(2);
DefaultScene.MainScene.collisionsEnabled = true; DefaultScene.MainScene.collisionsEnabled = true;
} }

View File

@ -299,17 +299,6 @@ export class ExplosionManager {
frameCount++; frameCount++;
// Log every 15 frames (approximately every 250ms at 60fps)
if (frameCount % 15 === 0 || frameCount === 1) {
log.debug(`[ExplosionManager] Animation frame ${frameCount}:`, {
elapsed: `${elapsed}ms`,
progress: progress.toFixed(3),
currentValue: currentValue.toFixed(2),
scale: scale.toFixed(3),
piecesAlive: meshPieces.filter(p => !p.isDisposed()).length
});
}
// Continue animation if not complete // Continue animation if not complete
if (progress >= 1.0) { if (progress >= 1.0) {
// Animation complete - remove observer and clean up // Animation complete - remove observer and clean up

View File

@ -93,6 +93,8 @@ export class RockFactory {
log.debug('loading mesh'); log.debug('loading mesh');
const asset = await loadAsset("asteroid.glb"); const asset = await loadAsset("asteroid.glb");
this._asteroidMesh = asset.meshes.get('Asteroid') || null; this._asteroidMesh = asset.meshes.get('Asteroid') || null;
this._asteroidMesh.material.backFaceCulling = true;
this._asteroidMesh.material.freeze();
if (this._asteroidMesh) { if (this._asteroidMesh) {
this._asteroidMesh.setEnabled(false); this._asteroidMesh.setEnabled(false);
} }

View File

@ -27,9 +27,9 @@ export class BackgroundStars {
private scene: Scene; private scene: Scene;
private config: Required<BackgroundStarsConfig>; private config: Required<BackgroundStarsConfig>;
// Default configuration // Default configuration (reduced from 5000 for Quest 2 performance)
private static readonly DEFAULT_CONFIG: Required<BackgroundStarsConfig> = { private static readonly DEFAULT_CONFIG: Required<BackgroundStarsConfig> = {
count: 5000, count: 2500,
radius: 5000, radius: 5000,
minBrightness: 0.3, minBrightness: 0.3,
maxBrightness: 1.0, maxBrightness: 1.0,

View File

@ -112,8 +112,9 @@ export class Level1 implements Level {
canvas.style.display = 'block'; canvas.style.display = 'block';
} }
// Ensure render loop is running // Ensure render loop is running (stop first to prevent duplicates)
const engine = DefaultScene.MainScene.getEngine(); const engine = DefaultScene.MainScene.getEngine();
engine.stopRenderLoop();
engine.runRenderLoop(() => { engine.runRenderLoop(() => {
DefaultScene.MainScene.render(); DefaultScene.MainScene.render();
}); });

View File

@ -58,6 +58,7 @@ export class ControllerInput {
private _onCameraAdjustObservable: Observable<CameraAdjustment> = private _onCameraAdjustObservable: Observable<CameraAdjustment> =
new Observable<CameraAdjustment>(); new Observable<CameraAdjustment>();
private _onStatusScreenToggleObservable: Observable<void> = new Observable<void>(); private _onStatusScreenToggleObservable: Observable<void> = new Observable<void>();
private _onInspectorToggleObservable: Observable<void> = new Observable<void>();
private _enabled: boolean = true; private _enabled: boolean = true;
private _mappingConfig: ControllerMappingConfig; private _mappingConfig: ControllerMappingConfig;
@ -87,6 +88,13 @@ export class ControllerInput {
return this._onStatusScreenToggleObservable; return this._onStatusScreenToggleObservable;
} }
/**
* Get observable that fires when Y button is pressed on left controller
*/
public get onInspectorToggleObservable(): Observable<void> {
return this._onInspectorToggleObservable;
}
/** /**
* Get current input state (stick positions) * Get current input state (stick positions)
* Applies controller mapping configuration to translate raw input to actions * Applies controller mapping configuration to translate raw input to actions
@ -329,6 +337,12 @@ export class ControllerInput {
this._onStatusScreenToggleObservable.notifyObservers(); this._onStatusScreenToggleObservable.notifyObservers();
} }
} }
if (controllerEvent.component.id === "y-button" && controllerEvent.hand === "left") {
// Only trigger on button press, not release
if (controllerEvent.pressed) {
this._onInspectorToggleObservable.notifyObservers();
}
}
log.info(controllerEvent); log.info(controllerEvent);
} }
} }
@ -341,5 +355,6 @@ export class ControllerInput {
this._controllerObservable.clear(); this._controllerObservable.clear();
this._onShootObservable.clear(); this._onShootObservable.clear();
this._onCameraAdjustObservable.clear(); this._onCameraAdjustObservable.clear();
this._onInspectorToggleObservable.clear();
} }
} }

View File

@ -309,6 +309,18 @@ export class Ship {
} }
}); });
// Wire up inspector toggle event (Y button)
this._controllerInput.onInspectorToggleObservable.add(() => {
import('@babylonjs/inspector').then(() => {
const scene = DefaultScene.MainScene;
if (scene.debugLayer.isVisible()) {
scene.debugLayer.hide();
} else {
scene.debugLayer.show({ overlay: true, showExplorer: true });
}
});
});
// Wire up camera adjustment events // Wire up camera adjustment events
this._keyboardInput.onCameraChangeObservable.add((cameraKey) => { this._keyboardInput.onCameraChangeObservable.add((cameraKey) => {
if (cameraKey === 1) { if (cameraKey === 1) {
@ -335,16 +347,22 @@ export class Ship {
this._physics.setGameStats(this._gameStats); this._physics.setGameStats(this._gameStats);
// Setup physics update loop (every 10 frames) // Setup physics update loop (every 10 frames)
let p = 0;
this._physicsObserver = DefaultScene.MainScene.onAfterPhysicsObservable.add(() => { this._physicsObserver = DefaultScene.MainScene.onAfterPhysicsObservable.add(() => {
this.updatePhysics();
this.updatePhysics();
}); });
let renderFrameCount = 0;
this._renderObserver = DefaultScene.MainScene.onAfterRenderObservable.add(() => { this._renderObserver = DefaultScene.MainScene.onAfterRenderObservable.add(() => {
// Update voice audio system (checks for completed sounds and plays next in queue) // Update voice audio system (checks for completed sounds and plays next in queue)
if (this._voiceAudio) { if (this._voiceAudio) {
this._voiceAudio.update(); this._voiceAudio.update();
} }
// Check game end conditions every frame (but only acts once) // Check game end conditions every 30 frames (~0.5 sec at 60fps)
this.checkGameEndConditions(); if (renderFrameCount++ % 30 === 0) {
this.checkGameEndConditions();
}
}); });
// Setup camera // Setup camera

View File

@ -189,6 +189,8 @@ export class Scoreboard {
advancedTexture.addControl(panel); advancedTexture.addControl(panel);
let i = 0; let i = 0;
const _afterRender = scene.onAfterRenderObservable.add(() => { const _afterRender = scene.onAfterRenderObservable.add(() => {
if (i++ % 10 !== 0) return;
scoreText.text = `Score: ${this.calculateScore()}`; scoreText.text = `Score: ${this.calculateScore()}`;
remainingText.text = `Remaining: ${this._remaining}`; remainingText.text = `Remaining: ${this._remaining}`;
@ -201,7 +203,7 @@ export class Scoreboard {
} }
const elapsed = Date.now() - this._startTime; const elapsed = Date.now() - this._startTime;
if (this._active && i++%30 == 0) { if (this._active) {
timeRemainingText.text = `Time: ${Math.floor(elapsed/60000).toString().padStart(2,"0")}:${(Math.floor(elapsed/1000)%60).toString().padStart(2,"0")}`; timeRemainingText.text = `Time: ${Math.floor(elapsed/60000).toString().padStart(2,"0")}:${(Math.floor(elapsed/1000)%60).toString().padStart(2,"0")}`;
fpsText.text = `FPS: ${Math.floor(scene.getEngine().getFps())}`; fpsText.text = `FPS: ${Math.floor(scene.getEngine().getFps())}`;
} }