Add minimal test level with performance metrics and extensive debug logging
Some checks failed
Build / build (push) Failing after 23s
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:
parent
db72847ce6
commit
72bd25b686
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,13 +70,23 @@ 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++) {
|
||||||
|
try {
|
||||||
// Create a small sphere for debris
|
// Create a small sphere for debris
|
||||||
const sphere = MeshBuilder.CreateIcoSphere(
|
const sphere = MeshBuilder.CreateIcoSphere(
|
||||||
`${mesh.name}_debris_${i}`,
|
`${mesh.name}_debris_${i}`,
|
||||||
@ -97,9 +112,20 @@ export class ExplosionManager {
|
|||||||
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,36 +173,51 @@ 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
|
||||||
|
console.log('[ExplosionManager] Creating MeshExploder...');
|
||||||
|
try {
|
||||||
const exploder = new MeshExploder(meshPieces);
|
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,
|
||||||
|
duration: this.config.duration,
|
||||||
|
maxForce: this.config.explosionForce
|
||||||
|
});
|
||||||
|
|
||||||
// Animate the explosion by calling explode() each frame with increasing values
|
// Animate the explosion by calling explode() each frame with increasing values
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const animationDuration = this.config.duration;
|
const animationDuration = this.config.duration;
|
||||||
const maxForce = this.config.explosionForce;
|
const maxForce = this.config.explosionForce;
|
||||||
|
let frameCount = 0;
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
const elapsed = Date.now() - startTime;
|
const elapsed = Date.now() - startTime;
|
||||||
@ -171,40 +225,78 @@ export class ExplosionManager {
|
|||||||
|
|
||||||
// Calculate current explosion value (0 to maxForce)
|
// Calculate current explosion value (0 to maxForce)
|
||||||
const currentValue = progress * maxForce;
|
const currentValue = progress * maxForce;
|
||||||
|
|
||||||
|
try {
|
||||||
exploder.explode(currentValue);
|
exploder.explode(currentValue);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ExplosionManager] ERROR in explode():', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Animate debris size to zero (1.0 to 0.0)
|
// Animate debris size to zero (1.0 to 0.0)
|
||||||
const scale = 1.0 - progress;
|
const scale = 1.0 - progress;
|
||||||
meshPieces.forEach(piece => {
|
meshPieces.forEach(piece => {
|
||||||
|
if (piece && !piece.isDisposed()) {
|
||||||
piece.scaling.set(scale, scale, scale);
|
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
|
// Continue animation if not complete
|
||||||
if (progress < 1.0) {
|
if (progress < 1.0) {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
} else {
|
} else {
|
||||||
// Animation complete - clean up
|
// Animation complete - clean up
|
||||||
console.log(`Explosion animation complete, cleaning up`);
|
console.log(`[ExplosionManager] Animation complete after ${frameCount} frames, cleaning up`);
|
||||||
this.cleanupExplosion(meshPieces);
|
this.cleanupExplosion(meshPieces);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start the animation
|
// Start the animation
|
||||||
|
console.log('[ExplosionManager] Starting animation loop...');
|
||||||
animate();
|
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()) {
|
||||||
|
try {
|
||||||
mesh.dispose();
|
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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
95
src/main.ts
95
src/main.ts
@ -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;
|
||||||
|
|
||||||
|
if (this._currentLevel) {
|
||||||
|
console.log('[Main] Calling level.play()...');
|
||||||
await this._currentLevel.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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
232
src/testLevel.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user