Add minimal test level with performance metrics and extensive debug logging
Some checks failed
Build / build (push) Failing after 23s

Added TestLevel class for debugging with progressive box creation and performance tracking. Includes comprehensive debug logging throughout the explosion system to diagnose Meta Quest issues.

Key changes:
- New TestLevel with 1-1000 box spawning (doubling each 5s iteration)
- Performance metrics logging (FPS, triangle count, mesh count)
- Direct triangle computation from mesh geometry
- Extensive explosion system debug logging
- Fixed observable timing issue (initialize after listener registration)
- Material freezing optimization

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-10-30 16:33:45 -05:00
parent db72847ce6
commit 72bd25b686
8 changed files with 517 additions and 76 deletions

View File

@ -57,6 +57,10 @@
<!-- Level cards will be dynamically populated from localStorage --> <!-- Level cards will be dynamically populated from localStorage -->
</div> </div>
<div style="text-align: center; margin-top: 20px;"> <div style="text-align: center; margin-top: 20px;">
<button id="testLevelBtn" class="test-level-button">
🧪 Test Scene (Debug)
</button>
<br>
<a href="#/editor" style="color: #4CAF50; text-decoration: none; font-size: 1.1em;"> <a href="#/editor" style="color: #4CAF50; text-decoration: none; font-size: 1.1em;">
+ Create New Level + Create New Level
</a> </a>

View File

@ -31,9 +31,7 @@ body {
#mainDiv { #mainDiv {
position: absolute; position: absolute;
display: block; display: block;
top: 50%; top: 48px;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000; z-index: 1000;
overflow: scroll; overflow: scroll;
} }
@ -185,6 +183,26 @@ body {
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
} }
.test-level-button {
background: linear-gradient(135deg, #FF6B6B 0%, #C92A2A 100%);
color: white;
border: 2px solid rgba(255, 107, 107, 0.5);
padding: 12px 30px;
border-radius: 8px;
cursor: pointer;
font-size: 1.1em;
font-weight: bold;
transition: all 0.3s;
margin-bottom: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.test-level-button:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.6);
border-color: rgba(255, 107, 107, 0.8);
}
.editor-link, .editor-link,
.settings-link { .settings-link {
position: absolute; position: absolute;

View File

@ -55,7 +55,12 @@ export class ExplosionManager {
* @returns Array of sphere mesh objects * @returns Array of sphere mesh objects
*/ */
private splitIntoSeparateMeshes(mesh: Mesh, pieces: number = 32): Mesh[] { private splitIntoSeparateMeshes(mesh: Mesh, pieces: number = 32): Mesh[] {
console.log(`Creating ${pieces} sphere debris pieces`); console.log(`[ExplosionManager] Creating ${pieces} sphere debris pieces`);
console.log('[ExplosionManager] Base mesh info:', {
position: mesh.position.toString(),
scaling: mesh.scaling.toString(),
hasMaterial: !!mesh.material
});
const meshPieces: Mesh[] = []; const meshPieces: Mesh[] = [];
const basePosition = mesh.position.clone(); const basePosition = mesh.position.clone();
@ -65,41 +70,62 @@ export class ExplosionManager {
const material = mesh.material?.clone('debris-material'); const material = mesh.material?.clone('debris-material');
if (material) { if (material) {
//(material as any).emissiveColor = Color3.Yellow(); //(material as any).emissiveColor = Color3.Yellow();
console.log('[ExplosionManager] Material cloned successfully');
} else {
console.warn('[ExplosionManager] WARNING: No material on base mesh');
} }
// Create sphere debris scattered around the original mesh position // Create sphere debris scattered around the original mesh position
const avgScale = (baseScale.x + baseScale.y + baseScale.z) / 3; const avgScale = (baseScale.x + baseScale.y + baseScale.z) / 3;
const debrisSize = avgScale * 0.3; // Size relative to asteroid const debrisSize = avgScale * 0.3; // Size relative to asteroid
console.log('[ExplosionManager] Debris parameters:', {
avgScale,
debrisSize,
offsetRadius: avgScale * 0.5
});
for (let i = 0; i < pieces; i++) { for (let i = 0; i < pieces; i++) {
// Create a small sphere for debris try {
const sphere = MeshBuilder.CreateIcoSphere( // Create a small sphere for debris
`${mesh.name}_debris_${i}`, const sphere = MeshBuilder.CreateIcoSphere(
{ `${mesh.name}_debris_${i}`,
radius: debrisSize, {
subdivisions: 2 radius: debrisSize,
}, DefaultScene.MainScene subdivisions: 2
); }, DefaultScene.MainScene
);
// Position spheres in a small cluster around the original position // Position spheres in a small cluster around the original position
const offsetRadius = avgScale * 0.5; const offsetRadius = avgScale * 0.5;
const angle1 = (i / pieces) * Math.PI * 2; const angle1 = (i / pieces) * Math.PI * 2;
const angle2 = Math.random() * Math.PI; const angle2 = Math.random() * Math.PI;
sphere.position = new Vector3( sphere.position = new Vector3(
basePosition.x + Math.sin(angle2) * Math.cos(angle1) * offsetRadius, basePosition.x + Math.sin(angle2) * Math.cos(angle1) * offsetRadius,
basePosition.y + Math.sin(angle2) * Math.sin(angle1) * offsetRadius, basePosition.y + Math.sin(angle2) * Math.sin(angle1) * offsetRadius,
basePosition.z + Math.cos(angle2) * offsetRadius basePosition.z + Math.cos(angle2) * offsetRadius
); );
sphere.material = material; sphere.material = material;
sphere.isVisible = true; sphere.isVisible = true;
sphere.setEnabled(true); sphere.setEnabled(true);
meshPieces.push(sphere); meshPieces.push(sphere);
} catch (error) {
console.error(`[ExplosionManager] ERROR creating debris piece ${i}:`, error);
}
} }
console.log(`Created ${meshPieces.length} sphere debris pieces`); console.log(`[ExplosionManager] Successfully created ${meshPieces.length}/${pieces} sphere debris pieces`);
if (meshPieces.length > 0) {
console.log('[ExplosionManager] First piece sample:', {
name: meshPieces[0].name,
position: meshPieces[0].position.toString(),
isVisible: meshPieces[0].isVisible,
isEnabled: meshPieces[0].isEnabled()
});
}
return meshPieces; return meshPieces;
} }
@ -108,20 +134,33 @@ export class ExplosionManager {
* @param mesh The mesh to explode (will be cloned internally) * @param mesh The mesh to explode (will be cloned internally)
*/ */
public playExplosion(mesh: AbstractMesh): void { public playExplosion(mesh: AbstractMesh): void {
console.log('[ExplosionManager] playExplosion called');
console.log('[ExplosionManager] Input mesh:', {
name: mesh.name,
id: mesh.id,
isInstancedMesh: !!(mesh as any).sourceMesh,
position: mesh.position.toString(),
scaling: mesh.scaling.toString()
});
// Get the source mesh if this is an instanced mesh // Get the source mesh if this is an instanced mesh
let sourceMesh: Mesh; let sourceMesh: Mesh;
if ((mesh as any).sourceMesh) { if ((mesh as any).sourceMesh) {
sourceMesh = (mesh as any).sourceMesh as Mesh; sourceMesh = (mesh as any).sourceMesh as Mesh;
console.log('[ExplosionManager] Using source mesh from instance:', sourceMesh.name);
} else { } else {
sourceMesh = mesh as Mesh; sourceMesh = mesh as Mesh;
console.log('[ExplosionManager] Using mesh directly (not instanced)');
} }
// Clone the source mesh so we don't affect the original // Clone the source mesh so we don't affect the original
console.log('[ExplosionManager] Cloning mesh...');
const meshToExplode = sourceMesh.clone("exploding-" + mesh.name, null, true, false); const meshToExplode = sourceMesh.clone("exploding-" + mesh.name, null, true, false);
if (!meshToExplode) { if (!meshToExplode) {
console.warn("Failed to clone mesh for explosion"); console.error('[ExplosionManager] ERROR: Failed to clone mesh for explosion');
return; return;
} }
console.log('[ExplosionManager] Mesh cloned successfully');
// Apply the instance's transformation to the cloned mesh // Apply the instance's transformation to the cloned mesh
meshToExplode.position = mesh.getAbsolutePosition().clone(); meshToExplode.position = mesh.getAbsolutePosition().clone();
@ -134,77 +173,130 @@ export class ExplosionManager {
// Check if mesh has proper geometry // Check if mesh has proper geometry
if (!meshToExplode.getTotalVertices || meshToExplode.getTotalVertices() === 0) { if (!meshToExplode.getTotalVertices || meshToExplode.getTotalVertices() === 0) {
console.warn("Mesh has no vertices, cannot explode"); console.error('[ExplosionManager] ERROR: Mesh has no vertices, cannot explode');
meshToExplode.dispose(); meshToExplode.dispose();
return; return;
} }
console.log(`Exploding mesh: ${meshToExplode.name}, vertices: ${meshToExplode.getTotalVertices()}`); console.log(`[ExplosionManager] Mesh ready for explosion:`, {
name: meshToExplode.name,
vertices: meshToExplode.getTotalVertices(),
position: meshToExplode.position.toString(),
scaling: meshToExplode.scaling.toString()
});
// Split the mesh into separate mesh objects (MeshExploder requirement) // Split the mesh into separate mesh objects (MeshExploder requirement)
console.log('[ExplosionManager] Splitting mesh into pieces...');
const meshPieces = this.splitIntoSeparateMeshes(meshToExplode, 12); const meshPieces = this.splitIntoSeparateMeshes(meshToExplode, 12);
if (meshPieces.length === 0) { if (meshPieces.length === 0) {
console.warn("Failed to split mesh into pieces"); console.error('[ExplosionManager] ERROR: Failed to split mesh into pieces');
meshToExplode.dispose(); meshToExplode.dispose();
return; return;
} }
// Original mesh is no longer needed - the pieces replace it // Original mesh is no longer needed - the pieces replace it
console.log('[ExplosionManager] Disposing original cloned mesh');
meshToExplode.dispose(); meshToExplode.dispose();
// Create the exploder with the array of separate meshes // Create the exploder with the array of separate meshes
// The second parameter is optional - it's the center mesh to explode from // The second parameter is optional - it's the center mesh to explode from
// If not provided, MeshExploder will auto-calculate the center // If not provided, MeshExploder will auto-calculate the center
const exploder = new MeshExploder(meshPieces); console.log('[ExplosionManager] Creating MeshExploder...');
try {
const exploder = new MeshExploder(meshPieces);
console.log('[ExplosionManager] MeshExploder created successfully');
console.log(`Starting explosion animation for ${meshPieces.length} mesh pieces`); console.log(`[ExplosionManager] Starting explosion animation:`, {
pieceCount: meshPieces.length,
// Animate the explosion by calling explode() each frame with increasing values duration: this.config.duration,
const startTime = Date.now(); maxForce: this.config.explosionForce
const animationDuration = this.config.duration;
const maxForce = this.config.explosionForce;
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / animationDuration, 1.0);
// Calculate current explosion value (0 to maxForce)
const currentValue = progress * maxForce;
exploder.explode(currentValue);
// Animate debris size to zero (1.0 to 0.0)
const scale = 1.0 - progress;
meshPieces.forEach(piece => {
piece.scaling.set(scale, scale, scale);
}); });
// Continue animation if not complete // Animate the explosion by calling explode() each frame with increasing values
if (progress < 1.0) { const startTime = Date.now();
requestAnimationFrame(animate); const animationDuration = this.config.duration;
} else { const maxForce = this.config.explosionForce;
// Animation complete - clean up let frameCount = 0;
console.log(`Explosion animation complete, cleaning up`);
this.cleanupExplosion(meshPieces);
}
};
// Start the animation const animate = () => {
animate(); const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / animationDuration, 1.0);
// Calculate current explosion value (0 to maxForce)
const currentValue = progress * maxForce;
try {
exploder.explode(currentValue);
} catch (error) {
console.error('[ExplosionManager] ERROR in explode():', error);
}
// Animate debris size to zero (1.0 to 0.0)
const scale = 1.0 - progress;
meshPieces.forEach(piece => {
if (piece && !piece.isDisposed()) {
piece.scaling.set(scale, scale, scale);
}
});
frameCount++;
// Log every 15 frames (approximately every 250ms at 60fps)
if (frameCount % 15 === 0 || frameCount === 1) {
console.log(`[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
if (progress < 1.0) {
requestAnimationFrame(animate);
} else {
// Animation complete - clean up
console.log(`[ExplosionManager] Animation complete after ${frameCount} frames, cleaning up`);
this.cleanupExplosion(meshPieces);
}
};
// Start the animation
console.log('[ExplosionManager] Starting animation loop...');
animate();
} catch (error) {
console.error('[ExplosionManager] ERROR creating MeshExploder:', error);
// Clean up pieces if exploder failed
meshPieces.forEach(piece => {
if (piece && !piece.isDisposed()) {
piece.dispose();
}
});
}
} }
/** /**
* Clean up explosion meshes * Clean up explosion meshes
*/ */
private cleanupExplosion(meshPieces: Mesh[]): void { private cleanupExplosion(meshPieces: Mesh[]): void {
console.log('[ExplosionManager] Starting cleanup of explosion meshes...');
let disposedCount = 0;
// Dispose all the mesh pieces // Dispose all the mesh pieces
meshPieces.forEach(mesh => { meshPieces.forEach((mesh, index) => {
if (mesh && !mesh.isDisposed()) { if (mesh && !mesh.isDisposed()) {
mesh.dispose(); try {
mesh.dispose();
disposedCount++;
} catch (error) {
console.error(`[ExplosionManager] ERROR disposing piece ${index}:`, error);
}
} }
}); });
console.log(`Explosion cleaned up - disposed ${meshPieces.length} pieces`); console.log(`[ExplosionManager] Cleanup complete - disposed ${disposedCount}/${meshPieces.length} pieces`);
} }
/** /**

View File

@ -1,28 +1,31 @@
import {AudioEngineV2, DirectionalLight} from "@babylonjs/core";
import { import {
AudioEngineV2,
Color3, Color3,
CreateAudioEngineAsync, CreateAudioEngineAsync,
DirectionalLight,
Engine, Engine,
HavokPlugin, HavokPlugin,
ParticleHelper, ParticleHelper,
Scene, Scene,
ScenePerformancePriority,
Vector3, Vector3,
WebGPUEngine, WebGPUEngine,
WebXRDefaultExperience, WebXRDefaultExperience,
WebXRFeatureName WebXRFeatureName, WebXRFeaturesManager
} from "@babylonjs/core"; } from "@babylonjs/core";
import '@babylonjs/loaders'; import '@babylonjs/loaders';
import HavokPhysics from "@babylonjs/havok"; import HavokPhysics from "@babylonjs/havok";
import {DefaultScene} from "./defaultScene"; import {DefaultScene} from "./defaultScene";
import {Level1} from "./level1"; import {Level1} from "./level1";
import {TestLevel} from "./testLevel";
import Demo from "./demo"; import Demo from "./demo";
import Level from "./level"; import Level from "./level";
import setLoadingMessage from "./setLoadingMessage"; import setLoadingMessage from "./setLoadingMessage";
import {RockFactory} from "./rockFactory"; import {RockFactory} from "./rockFactory";
import {ControllerDebug} from "./controllerDebug"; import {ControllerDebug} from "./controllerDebug";
import {router, showView} from "./router"; import {router, showView} from "./router";
import {populateLevelSelector, hasSavedLevels} from "./levelSelector"; import {hasSavedLevels, populateLevelSelector} from "./levelSelector";
import {LevelConfig} from "./levelConfig"; import {LevelConfig} from "./levelConfig";
import {generateDefaultLevels} from "./levelEditor"; import {generateDefaultLevels} from "./levelEditor";
@ -79,11 +82,85 @@ export class Main {
}, 500); }, 500);
}); });
}); });
// Listen for test level button click
window.addEventListener('DOMContentLoaded', () => {
console.log('[Main] DOMContentLoaded fired, looking for test button...');
const testLevelBtn = document.querySelector('#testLevelBtn');
console.log('[Main] Test button found:', !!testLevelBtn);
if (testLevelBtn) {
testLevelBtn.addEventListener('click', async () => {
console.log('[Main] ========== TEST LEVEL BUTTON CLICKED ==========');
// Show loading UI
const mainDiv = document.querySelector('#mainDiv');
const levelSelect = document.querySelector('#levelSelect') as HTMLElement;
console.log('[Main] mainDiv exists:', !!mainDiv);
console.log('[Main] levelSelect exists:', !!levelSelect);
if (levelSelect) {
levelSelect.style.display = 'none';
console.log('[Main] levelSelect hidden');
}
setLoadingMessage("Initializing Test Scene...");
// Unlock audio engine on user interaction
if (this._audioEngine) {
console.log('[Main] Unlocking audio engine...');
await this._audioEngine.unlockAsync();
console.log('[Main] Audio engine unlocked');
}
// Create test level
console.log('[Main] Creating TestLevel...');
this._currentLevel = new TestLevel(this._audioEngine);
console.log('[Main] TestLevel created:', !!this._currentLevel);
// Wait for level to be ready
console.log('[Main] Registering ready observable...');
this._currentLevel.getReadyObservable().add(() => {
console.log('[Main] ========== TEST LEVEL READY OBSERVABLE FIRED ==========');
setLoadingMessage("Test Scene Ready! Entering VR...");
console.log('[Main] Setting timeout to enter VR...');
// Small delay to show message
setTimeout(() => {
console.log('[Main] Timeout fired, removing mainDiv and calling play()');
if (mainDiv) {
mainDiv.remove();
console.log('[Main] mainDiv removed');
}
console.log('[Main] About to call this.play()...');
this.play();
}, 500);
});
console.log('[Main] Ready observable registered');
// Now initialize the level (after observable is registered)
console.log('[Main] Calling TestLevel.initialize()...');
await this._currentLevel.initialize();
console.log('[Main] TestLevel.initialize() completed');
});
console.log('[Main] Click listener added to test button');
} else {
console.warn('[Main] Test level button not found in DOM');
}
});
} }
private _started = false; private _started = false;
public async play() { public async play() {
console.log('[Main] play() called');
console.log('[Main] Current level exists:', !!this._currentLevel);
this._gameState = GameState.PLAY; this._gameState = GameState.PLAY;
await this._currentLevel.play();
if (this._currentLevel) {
console.log('[Main] Calling level.play()...');
await this._currentLevel.play();
console.log('[Main] level.play() completed');
} else {
console.error('[Main] ERROR: No current level to play!');
}
} }
public demo() { public demo() {
this._gameState = GameState.DEMO; this._gameState = GameState.DEMO;
@ -97,11 +174,11 @@ export class Main {
disableTeleportation: true, disableTeleportation: true,
disableNearInteraction: true, disableNearInteraction: true,
disableHandTracking: true, disableHandTracking: true,
disableDefaultUI: true, disableDefaultUI: true
}); });
DefaultScene.XR.baseExperience.featuresManager.enableFeature(WebXRFeatureName.LAYERS, "stable", console.log(WebXRFeaturesManager.GetAvailableFeatures());
{preferMultiviewOnInit: true}); //DefaultScene.XR.baseExperience.featuresManager.enableFeature(WebXRFeatureName.LAYERS, "latest", {preferMultiviewOnInit: true});
setLoadingMessage("Get Ready!"); setLoadingMessage("Get Ready!");
@ -124,16 +201,20 @@ export class Main {
if (webGpu) { if (webGpu) {
this._engine = new WebGPUEngine(canvas); this._engine = new WebGPUEngine(canvas);
console.log("Webgpu enabled");
await (this._engine as WebGPUEngine).initAsync(); await (this._engine as WebGPUEngine).initAsync();
} else { } else {
console.log("Standard WebGL enabled");
this._engine = new Engine(canvas, true); this._engine = new Engine(canvas, true);
} }
this._engine.setHardwareScalingLevel(1 / window.devicePixelRatio); this._engine.setHardwareScalingLevel(1 / window.devicePixelRatio);
window.onresize = () => { window.onresize = () => {
this._engine.resize(); this._engine.resize();
} }
DefaultScene.DemoScene = new Scene(this._engine); DefaultScene.DemoScene = new Scene(this._engine);
DefaultScene.MainScene = new Scene(this._engine); DefaultScene.MainScene = new Scene(this._engine);
DefaultScene.MainScene.ambientColor = new Color3(0,0,0); DefaultScene.MainScene.ambientColor = new Color3(0,0,0);
DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4(); DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4();
@ -175,7 +256,7 @@ export class Main {
//DefaultScene.MainScene.ambientColor = new Color3(.1, .1, .1); //DefaultScene.MainScene.ambientColor = new Color3(.1, .1, .1);
const light = new DirectionalLight("dirLight", new Vector3(-1, -2, -1), DefaultScene.MainScene); const light = new DirectionalLight("dirLight", new Vector3(-1, -2, -1), DefaultScene.MainScene);
DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin); DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin);
DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/30); DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/60);
DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(5); DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(5);
DefaultScene.MainScene.collisionsEnabled = true; DefaultScene.MainScene.collisionsEnabled = true;

View File

@ -154,6 +154,7 @@ export class MaterialFactory {
material.disableLighting = true; material.disableLighting = true;
material.roughness = 1; material.roughness = 1;
material.specularColor = Color3.Black(); material.specularColor = Color3.Black();
material.freeze();
return material; return material;
} }

View File

@ -77,6 +77,7 @@ export class RockFactory {
DefaultScene.MainScene, DefaultScene.MainScene,
this._originalMaterial this._originalMaterial
) as PBRMaterial; ) as PBRMaterial;
this._rockMaterial.freeze();
this._rockMesh.material = this._rockMaterial; this._rockMesh.material = this._rockMaterial;
importMesh.meshes[1].dispose(false, true); importMesh.meshes[1].dispose(false, true);
@ -119,22 +120,37 @@ export class RockFactory {
body.setCollisionCallbackEnabled(true); body.setCollisionCallbackEnabled(true);
body.getCollisionObservable().add((eventData) => { body.getCollisionObservable().add((eventData) => {
if (eventData.type == 'COLLISION_STARTED') { if (eventData.type == 'COLLISION_STARTED') {
console.log('[RockFactory] Collision detected:', {
collidedWith: eventData.collidedAgainst.transformNode.id,
asteroidName: eventData.collider.transformNode.name
});
if ( eventData.collidedAgainst.transformNode.id == 'ammo') { if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
console.log('[RockFactory] ASTEROID HIT! Triggering explosion...');
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"}); score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
// Get the asteroid mesh before disposing // Get the asteroid mesh before disposing
const asteroidMesh = eventData.collider.transformNode as AbstractMesh; const asteroidMesh = eventData.collider.transformNode as AbstractMesh;
console.log('[RockFactory] Asteroid mesh to explode:', {
name: asteroidMesh.name,
id: asteroidMesh.id,
position: asteroidMesh.position.toString()
});
// Play explosion using ExplosionManager (clones mesh internally) // Play explosion using ExplosionManager (clones mesh internally)
console.log('[RockFactory] Calling ExplosionManager.playExplosion()...');
RockFactory._explosionManager.playExplosion(asteroidMesh); RockFactory._explosionManager.playExplosion(asteroidMesh);
console.log('[RockFactory] Explosion call completed');
// Now dispose the physics objects and original mesh // Now dispose the physics objects and original mesh
console.log('[RockFactory] Disposing physics objects and meshes...');
eventData.collider.shape.dispose(); eventData.collider.shape.dispose();
eventData.collider.transformNode.dispose(); eventData.collider.transformNode.dispose();
eventData.collider.dispose(); eventData.collider.dispose();
eventData.collidedAgainst.shape.dispose(); eventData.collidedAgainst.shape.dispose();
eventData.collidedAgainst.transformNode.dispose(); eventData.collidedAgainst.transformNode.dispose();
eventData.collidedAgainst.dispose(); eventData.collidedAgainst.dispose();
console.log('[RockFactory] Disposal complete');
} }
} }
}); });

View File

@ -343,10 +343,7 @@ export class Ship {
private controllerCallback = (controllerEvent: ControllerEvent) => { private controllerCallback = (controllerEvent: ControllerEvent) => {
// Log first few events to verify they're firing // Log first few events to verify they're firing
if (Math.random() < 0.01) { // Only log 1% to avoid spam
console.log('Controller event:', controllerEvent.type, controllerEvent.hand,
controllerEvent.type === 'thumbstick' ? controllerEvent.axisData : controllerEvent.value);
}
if (controllerEvent.type == 'thumbstick') { if (controllerEvent.type == 'thumbstick') {
if (controllerEvent.hand == 'left') { if (controllerEvent.hand == 'left') {

232
src/testLevel.ts Normal file
View File

@ -0,0 +1,232 @@
import { DefaultScene } from "./defaultScene";
import {
Color3,
DirectionalLight,
MeshBuilder,
Observable,
StandardMaterial,
Vector3
} from "@babylonjs/core";
import type { AudioEngineV2 } from "@babylonjs/core";
import Level from "./level";
/**
* Minimal test level with just a box and a light for debugging
*/
export class TestLevel implements Level {
private _onReadyObservable: Observable<Level> = new Observable<Level>();
private _initialized: boolean = false;
private _audioEngine: AudioEngineV2;
private _boxCreationInterval: NodeJS.Timeout | null = null;
private _totalBoxesCreated: number = 0;
private _boxesPerIteration: number = 1;
constructor(audioEngine: AudioEngineV2) {
this._audioEngine = audioEngine;
console.log('[TestLevel] Constructor called');
// Don't call initialize here - let Main call it after registering the observable
}
getReadyObservable(): Observable<Level> {
return this._onReadyObservable;
}
public async play() {
console.log('[TestLevel] play() called - entering XR');
console.log('[TestLevel] XR available:', !!DefaultScene.XR);
console.log('[TestLevel] XR baseExperience:', !!DefaultScene.XR?.baseExperience);
try {
// Enter XR mode
const xr = await DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
console.log('[TestLevel] XR mode entered successfully');
console.log('[TestLevel] XR session:', xr);
console.log('[TestLevel] Camera position:', DefaultScene.XR.baseExperience.camera.position.toString());
this.startBoxCreation();
} catch (error) {
console.error('[TestLevel] ERROR entering XR:', error);
}
}
public dispose() {
console.log('[TestLevel] dispose() called');
// Stop box creation timer
if (this._boxCreationInterval) {
clearInterval(this._boxCreationInterval);
this._boxCreationInterval = null;
console.log('[TestLevel] Box creation timer stopped');
}
}
/**
* Create a box at the specified position with the specified color
*/
private createBox(position: Vector3, color: Color3, name?: string): void {
const box = MeshBuilder.CreateBox(
name || `box_${this._totalBoxesCreated}`,
{ size: 0.5 },
DefaultScene.MainScene
);
box.position = position;
const material = new StandardMaterial(`material_${this._totalBoxesCreated}`, DefaultScene.MainScene);
material.diffuseColor = color;
material.specularColor = new Color3(0.5, 0.5, 0.5);
box.material = material;
this._totalBoxesCreated++;
}
/**
* Start the box creation timer that doubles the number of boxes each iteration
*/
private startBoxCreation(): void {
console.log('[TestLevel] Starting box creation timer...');
const createBatch = () => {
const boxesToCreate = Math.min(
this._boxesPerIteration,
1000 - this._totalBoxesCreated
);
console.log(`[TestLevel] Creating ${boxesToCreate} boxes (total will be: ${this._totalBoxesCreated + boxesToCreate}/1000)`);
for (let i = 0; i < boxesToCreate; i++) {
// Random position in a 20x20x20 cube around origin
const position = new Vector3(
Math.random() * 20 - 10,
Math.random() * 20,
Math.random() * 20 - 10
);
// Random color
const color = new Color3(
Math.random(),
Math.random(),
Math.random()
);
this.createBox(position, color);
}
console.log(`[TestLevel] Created ${boxesToCreate} boxes. Total: ${this._totalBoxesCreated}/1000`);
// Log performance metrics
const fps = DefaultScene.MainScene.getEngine().getFps();
// Directly compute triangle count from all meshes
const totalIndices = DefaultScene.MainScene.meshes.reduce((sum, mesh) => {
if (mesh.isEnabled() && mesh.isVisible) {
return sum + mesh.getTotalIndices();
}
return sum;
}, 0);
const triangleCount = Math.floor(totalIndices / 3);
console.log(`[TestLevel] Performance Metrics:`, {
fps: fps.toFixed(2),
triangleCount: triangleCount,
totalIndices: totalIndices,
totalMeshes: DefaultScene.MainScene.meshes.length,
activeMeshes: DefaultScene.MainScene.meshes.filter(m => m.isEnabled() && m.isVisible).length,
totalBoxes: this._totalBoxesCreated
});
// Check if we've reached 1000 boxes
if (this._totalBoxesCreated >= 1000) {
console.log('[TestLevel] Reached 1000 boxes, stopping timer');
if (this._boxCreationInterval) {
clearInterval(this._boxCreationInterval);
this._boxCreationInterval = null;
}
return;
}
// Double the number for next iteration
this._boxesPerIteration *= 2;
};
// Create first batch immediately
createBatch();
// Set up interval for subsequent batches
this._boxCreationInterval = setInterval(createBatch, 5000);
}
public async initialize() {
console.log('[TestLevel] initialize() called');
console.log('[TestLevel] Scene info:', {
name: DefaultScene.MainScene.name,
meshCount: DefaultScene.MainScene.meshes.length,
lightCount: DefaultScene.MainScene.lights.length
});
if (this._initialized) {
console.log('[TestLevel] Already initialized, skipping');
return;
}
// Create a simple directional light
const light = new DirectionalLight(
"testLight",
new Vector3(-1, -2, 1),
DefaultScene.MainScene
);
light.intensity = 1.0;
console.log('[TestLevel] Created directional light:', {
name: light.name,
direction: light.direction.toString(),
intensity: light.intensity
});
// Create a simple colored box
const box = MeshBuilder.CreateBox(
"testBox",
{ size: 2 },
DefaultScene.MainScene
);
box.position = new Vector3(0, 1, 5); // In front of camera
// Create a simple material
const material = new StandardMaterial("testMaterial", DefaultScene.MainScene);
material.diffuseColor = new Color3(1, 0, 0); // Red
material.specularColor = new Color3(0.5, 0.5, 0.5);
box.material = material;
console.log('[TestLevel] Created test box:', {
name: box.name,
position: box.position.toString(),
size: 2,
color: 'red'
});
// Create a ground plane for reference
const ground = MeshBuilder.CreateGround(
"testGround",
{ width: 10, height: 10 },
DefaultScene.MainScene
);
ground.position.y = 0;
const groundMaterial = new StandardMaterial("groundMaterial", DefaultScene.MainScene);
groundMaterial.diffuseColor = new Color3(0.3, 0.3, 0.3); // Grey
ground.material = groundMaterial;
console.log('[TestLevel] Created ground plane:', {
name: ground.name,
dimensions: '10x10',
position: ground.position.toString()
});
console.log('[TestLevel] Final scene state:', {
totalMeshes: DefaultScene.MainScene.meshes.length,
totalLights: DefaultScene.MainScene.lights.length,
meshNames: DefaultScene.MainScene.meshes.map(m => m.name)
});
this._initialized = true;
console.log('[TestLevel] Initialization complete - scene ready for XR');
// Notify that initialization is complete
this._onReadyObservable.notifyObservers(this);
}
}