diff --git a/public/song1.mp3 b/public/song1.mp3 new file mode 100644 index 0000000..05b3529 Binary files /dev/null and b/public/song1.mp3 differ diff --git a/src/level1.ts b/src/level1.ts index e005d5b..f80e849 100644 --- a/src/level1.ts +++ b/src/level1.ts @@ -7,15 +7,17 @@ import { ParticleHelper, PhysicsAggregate, PhysicsMotionType, - PhysicsShapeType, PointsCloudSystem, Sound, + PhysicsShapeType, PointsCloudSystem, StandardMaterial, TransformNode, Vector3 } from "@babylonjs/core"; +import type {AudioEngineV2} from "@babylonjs/core"; import {Ship} from "./ship"; import {RockFactory} from "./starfield"; import Level from "./level"; import {Scoreboard} from "./scoreboard"; +import setLoadingMessage from "./setLoadingMessage"; export class Level1 implements Level { private _ship: Ship; @@ -25,6 +27,7 @@ export class Level1 implements Level { private _endBase: AbstractMesh; private _scoreboard: Scoreboard; private _difficulty: string; + private _audioEngine: AudioEngineV2; private _difficultyConfig: { rockCount: number; forceMultiplier: number; @@ -34,10 +37,11 @@ export class Level1 implements Level { distanceMax: number; }; - constructor(difficulty: string = 'recruit') { + constructor(difficulty: string = 'recruit', audioEngine: AudioEngineV2) { this._difficulty = difficulty; + this._audioEngine = audioEngine; this._difficultyConfig = this.getDifficultyConfig(difficulty); - this._ship = new Ship(); + this._ship = new Ship(undefined, audioEngine); this._scoreboard = new Scoreboard(); const xr = DefaultScene.XR; xr.baseExperience.onInitialXRPoseSetObservable.add(() => { @@ -47,6 +51,7 @@ export class Level1 implements Level { xr.input.onControllerAddedObservable.add((controller) => { this._ship.addController(controller); }); + this.createStartBase(); this.initialize(); @@ -116,10 +121,21 @@ export class Level1 implements Level { } private scored: Set = new Set(); - public play() { - const background = new Sound("background", "/background.mp3", DefaultScene.MainScene, () => { - }, {loop: true, autoplay: true, volume: .2}); - DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor'); + public async play() { + // Create background music using AudioEngineV2 + const background = await this._audioEngine.createSoundAsync("background", "/song1.mp3", { + loop: true, + volume: 0.2 + }); + background.play(); + + // Enter XR mode + await DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor'); + + // Check for controllers that are already connected after entering XR + DefaultScene.XR.input.controllers.forEach((controller) => { + this._ship.addController(controller); + }); } public dispose() { this._startBase.dispose(); @@ -130,13 +146,13 @@ export class Level1 implements Level { if (this._initialized) { return; } + this.createBackgroundElements(); this._initialized = true; - ParticleHelper.BaseAssetsUrl = window.location.href; this._ship.position = new Vector3(0, 1, 0); - await RockFactory.init(); const config = this._difficultyConfig; console.log(config); + setLoadingMessage("Creating Asteroids..."); for (let i = 0; i < config.rockCount; i++) { const distRange = config.distanceMax - config.distanceMin; const dist = (Math.random() * distRange) + config.distanceMin; @@ -170,6 +186,9 @@ export class Level1 implements Level { this._startBase.physicsBody.addConstraint(rock.physicsBody, constraint); rock.physicsBody.applyForce(Vector3.Random(-1, 1).scale(5000000 * config.forceMultiplier), rock.position); } + + // Notify that initialization is complete + this._onReadyObservable.notifyObservers(this); } private createStartBase() { @@ -200,6 +219,13 @@ export class Level1 implements Level { agg.body.setMotionType(PhysicsMotionType.ANIMATED); this._endBase = mesh; } + private createBackgroundElements() { + const sun = MeshBuilder.CreateSphere("sun", {diameter: 200}, DefaultScene.MainScene); + const sunMaterial = new StandardMaterial("sunMaterial", DefaultScene.MainScene); + sunMaterial.emissiveColor = new Color3(1, 1, 0); + sun.material = sunMaterial; + sun.position = new Vector3(-200, 300, 500); + } private createTarget(i: number) { const target = MeshBuilder.CreateTorus("target" + i, {diameter: 10, tessellation: 72}, DefaultScene.MainScene); diff --git a/src/main.ts b/src/main.ts index 5b87615..a01b0f8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,16 @@ import { Color3, + CreateAudioEngineAsync, Engine, HavokPlugin, + ParticleHelper, PhotoDome, Scene, StandardMaterial, Vector3, WebGPUEngine, WebXRDefaultExperience } from "@babylonjs/core"; +import type {AudioEngineV2} from "@babylonjs/core"; import '@babylonjs/loaders'; import HavokPhysics from "@babylonjs/havok"; @@ -17,6 +20,8 @@ import {Level1} from "./level1"; import {Scoreboard} from "./scoreboard"; import Demo from "./demo"; import Level from "./level"; +import setLoadingMessage from "./setLoadingMessage"; +import {RockFactory} from "./starfield"; const webGpu = false; const canvas = (document.querySelector('#gameCanvas') as HTMLCanvasElement); @@ -25,44 +30,62 @@ enum GameState { DEMO } export class Main { - private _loadingDiv: HTMLElement; private _currentLevel: Level; private _gameState: GameState = GameState.DEMO; private _selectedDifficulty: string = 'recruit'; private _engine: Engine | WebGPUEngine; + private _audioEngine: AudioEngineV2; constructor() { - this._loadingDiv = document.querySelector('#loadingDiv'); if (!navigator.xr) { - this._loadingDiv.innerText = "This browser does not support WebXR"; + setLoadingMessage("This browser does not support WebXR"); return; } this.initialize(); document.querySelectorAll('.level-button').forEach(button => { - button.addEventListener('click', (e) => { + button.addEventListener('click', async (e) => { const levelButton = e.target as HTMLButtonElement; this._selectedDifficulty = levelButton.dataset.level; - this.setLoadingMessage("Initializing Level..."); - this._currentLevel = new Level1(this._selectedDifficulty); - // Unlock audio engine if it exists - if (this._engine?.audioEngine) { - this._engine.audioEngine.unlock(); + + // Show loading UI again + const mainDiv = document.querySelector('#mainDiv'); + const levelSelect = document.querySelector('#levelSelect') as HTMLElement; + if (levelSelect) { + levelSelect.style.display = 'none'; } - this.play(); - document.querySelector('#mainDiv').remove(); + setLoadingMessage("Initializing Level..."); + + // Unlock audio engine on user interaction + if (this._audioEngine) { + await this._audioEngine.unlockAsync(); + } + + // Create and initialize level BEFORE entering XR + this._currentLevel = new Level1(this._selectedDifficulty, this._audioEngine); + + // Wait for level to be ready + this._currentLevel.getReadyObservable().add(() => { + setLoadingMessage("Level Ready! Entering VR..."); + + // Small delay to show message + setTimeout(() => { + mainDiv.remove(); + this.play(); + }, 500); + }); }); }); } private _started = false; - public play() { + public async play() { this._gameState = GameState.PLAY; - this._currentLevel.play(); + await this._currentLevel.play(); } public demo() { this._gameState = GameState.DEMO; } private async initialize() { - this._loadingDiv.innerText = "Initializing."; + setLoadingMessage("Initializing."); await this.setupScene(); DefaultScene.XR = await WebXRDefaultExperience.CreateAsync(DefaultScene.MainScene, { @@ -73,7 +96,7 @@ export class Main { disableDefaultUI: true, }); - this.setLoadingMessage("Get Ready!"); + setLoadingMessage("Get Ready!"); const photoDome1 = new PhotoDome("testdome", '/8192.webp', {size: 1000}, DefaultScene.MainScene); photoDome1.material.diffuseTexture.hasAlpha = true; @@ -86,10 +109,9 @@ export class Main { photoDome1.position = DefaultScene.MainScene.activeCamera.globalPosition; photoDome2.position = DefaultScene.MainScene.activeCamera.globalPosition; }); + setLoadingMessage("Select a difficulty to begin!"); } - private setLoadingMessage(message: string) { - this._loadingDiv.innerText = message; - } + private async setupScene() { if (webGpu) { @@ -106,14 +128,24 @@ export class Main { DefaultScene.MainScene = new Scene(this._engine); DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2); - this.setLoadingMessage("Initializing Physics Engine.."); + setLoadingMessage("Initializing Physics Engine.."); await this.setupPhysics(); - this.setLoadingMessage("Physics Engine Ready!"); + setLoadingMessage("Physics Engine Ready!"); + + setLoadingMessage("Loading Asteroids and Explosions..."); + ParticleHelper.BaseAssetsUrl = window.location.href; + await RockFactory.init(); + setLoadingMessage("Ready!"); + + // Initialize AudioEngineV2 + setLoadingMessage("Initializing Audio Engine..."); + this._audioEngine = await CreateAudioEngineAsync(); + setLoadingMessage("Ready!"); + this.setupInspector(); this._engine.runRenderLoop(() => { if (!this._started) { this._started = true; - this._loadingDiv.remove(); const levelSelect = document.querySelector('#levelSelect'); if (levelSelect) { levelSelect.classList.add('ready'); @@ -136,7 +168,7 @@ export class Main { } private setupInspector() { - this.setLoadingMessage("Initializing Inspector..."); + setLoadingMessage("Initializing Inspector..."); window.addEventListener("keydown", (ev) => { if (ev.key == 'i') { import ("@babylonjs/inspector").then((inspector) => { diff --git a/src/setLoadingMessage.ts b/src/setLoadingMessage.ts new file mode 100644 index 0000000..fbb9117 --- /dev/null +++ b/src/setLoadingMessage.ts @@ -0,0 +1,6 @@ +let loadingDiv: HTMLElement = document.querySelector('#loadingDiv') as HTMLElement; +export default function setLoadingMessage(message:string) { + if (loadingDiv) { + loadingDiv.innerText = message; + } +} \ No newline at end of file diff --git a/src/ship.ts b/src/ship.ts index 6385833..4652fc0 100644 --- a/src/ship.ts +++ b/src/ship.ts @@ -10,7 +10,6 @@ import { PhysicsMotionType, PhysicsShapeType, SceneLoader, - Sound, SpotLight, StandardMaterial, TransformNode, @@ -20,6 +19,7 @@ import { WebXRControllerComponent, WebXRInputSource } from "@babylonjs/core"; +import type {AudioEngineV2, StaticSound} from "@babylonjs/core"; import {DefaultScene} from "./defaultScene"; const MAX_FORWARD_THRUST = 40; @@ -58,16 +58,20 @@ export class Ship { private _forwardNode: TransformNode; private _rotationNode: TransformNode; private _glowLayer: GlowLayer; - private _primaryThrustVectorSound: Sound; - private _secondaryThrustVectorSound: Sound; - private _shot: Sound; + private _primaryThrustVectorSound: StaticSound; + private _secondaryThrustVectorSound: StaticSound; + private _shot: StaticSound; + private _primaryThrustPlaying: boolean = false; + private _secondaryThrustPlaying: boolean = false; private _shooting: boolean = false; private _camera: FreeCamera; private _ammoBaseMesh: AbstractMesh; private _controllerMode: ControllerStickMode; private _active = false; - constructor(mode: ControllerStickMode = ControllerStickMode.BEGINNER) { - this._controllerMode = mode + private _audioEngine: AudioEngineV2; + constructor(mode: ControllerStickMode = ControllerStickMode.BEGINNER, audioEngine?: AudioEngineV2) { + this._controllerMode = mode; + this._audioEngine = audioEngine; this.setup(); this.initialize(); } @@ -75,8 +79,25 @@ export class Ship { this._controllerMode = mode; } + private async initializeSounds() { + if (!this._audioEngine) return; + + this._primaryThrustVectorSound = await this._audioEngine.createSoundAsync("thrust", "/thrust5.mp3", { + loop: true, + volume: .2 + }); + this._secondaryThrustVectorSound = await this._audioEngine.createSoundAsync("thrust2", "/thrust5.mp3", { + loop: true, + volume: 0.5 + }); + this._shot = await this._audioEngine.createSoundAsync("shot", "/shot.mp3", { + loop: false, + volume: 0.5 + }); + } + private shoot() { - this._shot.play(); + this._shot?.play(); const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh); ammo.parent = this._ship; ammo.position.y = 2; @@ -113,17 +134,11 @@ export class Ship { this._ship = new TransformNode("ship", DefaultScene.MainScene); this._glowLayer = new GlowLayer('bullets', DefaultScene.MainScene); this._glowLayer.intensity = 1; - this._primaryThrustVectorSound = new Sound("thrust", "/thrust5.mp3", DefaultScene.MainScene, null, { - loop: true, - autoplay: false - }); - this._secondaryThrustVectorSound = new Sound("thrust2", "/thrust5.mp3", DefaultScene.MainScene, null, { - loop: true, - autoplay: false, - volume: .5 - }); - this._shot = new Sound("shot", "/shot.mp3", DefaultScene.MainScene, null, - {loop: false, autoplay: false, volume: .5}); + + // Create sounds asynchronously if audio engine is available + if (this._audioEngine) { + this.initializeSounds(); + } this._ammoMaterial = new StandardMaterial("ammoMaterial", DefaultScene.MainScene); this._ammoMaterial.emissiveColor = new Color3(1, 1, 0); this._ammoBaseMesh = MeshBuilder.CreateCapsule("bullet", {radius: .1, height: 2.5}, DefaultScene.MainScene); @@ -218,14 +233,18 @@ export class Ship { //if forward thrust is under 40 we can apply more thrust if (Math.abs(this._forwardValue) <= MAX_FORWARD_THRUST) { if (Math.abs(this._leftStickVector.y) > .1) { - if (!this._primaryThrustVectorSound.isPlaying) { + if (this._primaryThrustVectorSound && !this._primaryThrustPlaying) { this._primaryThrustVectorSound.play(); + this._primaryThrustPlaying = true; + } + if (this._primaryThrustVectorSound) { + this._primaryThrustVectorSound.volume = Math.abs(this._leftStickVector.y); } - this._primaryThrustVectorSound.setVolume(Math.abs(this._leftStickVector.y)); this._forwardValue += this._leftStickVector.y * .8; } else { - if (this._primaryThrustVectorSound.isPlaying) { - this._primaryThrustVectorSound.pause(); + if (this._primaryThrustVectorSound && this._primaryThrustPlaying) { + this._primaryThrustVectorSound.stop(); + this._primaryThrustPlaying = false; } this._forwardValue = decrementValue(this._forwardValue, .98); } @@ -245,13 +264,17 @@ export class Ship { Math.abs(this._leftStickVector.x); if (thrust2 > .01) { - if (!this._secondaryThrustVectorSound.isPlaying) { + if (this._secondaryThrustVectorSound && !this._secondaryThrustPlaying) { this._secondaryThrustVectorSound.play(); + this._secondaryThrustPlaying = true; + } + if (this._secondaryThrustVectorSound) { + this._secondaryThrustVectorSound.volume = thrust2 * .4; } - this._secondaryThrustVectorSound.setVolume(thrust2 * .4); } else { - if (this._secondaryThrustVectorSound.isPlaying) { - this._secondaryThrustVectorSound.pause(); + if (this._secondaryThrustVectorSound && this._secondaryThrustPlaying) { + this._secondaryThrustVectorSound.stop(); + this._secondaryThrustPlaying = false; } } diff --git a/src/starfield.ts b/src/starfield.ts index fb6d938..6fa6ab6 100644 --- a/src/starfield.ts +++ b/src/starfield.ts @@ -84,7 +84,7 @@ export class RockFactory { score: Observable): Promise { const rock = new InstancedMesh("asteroid-" +i, this._rockMesh as Mesh); - + console.log(rock.id); rock.scaling = size; rock.position = position; //rock.material = this._rockMaterial;