import { AbstractMesh, Color3, DirectionalLight, FreeCamera, GlowLayer, InstancedMesh, Mesh, MeshBuilder, Observable, PhysicsAggregate, PhysicsMotionType, PhysicsShapeType, SceneLoader, Sound, SpotLight, StandardMaterial, TransformNode, Vector2, Vector3, WebXRAbstractMotionController, WebXRControllerComponent, WebXRInputSource } from "@babylonjs/core"; import {DefaultScene} from "./defaultScene"; const MAX_FORWARD_THRUST = 40; const controllerComponents = [ 'a-button', 'b-button', 'x-button', 'y-button', 'thumbrest', 'xr-standard-squeeze', 'xr-standard-thumbstick', 'xr-standard-trigger', ] type ControllerEvent = { hand: 'right' | 'left' | 'none', type: 'thumbstick' | 'button', controller: WebXRAbstractMotionController, component: WebXRControllerComponent, value: number, axisData: { x: number, y: number }, pressed: boolean, touched: boolean } enum ControllerStickMode { BEGINNER, ARCADE, REALISTIC } export class Ship { private _ship: TransformNode; private _controllerObservable: Observable = new Observable(); private _ammoMaterial: StandardMaterial; private _forwardNode: TransformNode; private _rotationNode: TransformNode; private _glowLayer: GlowLayer; private _primaryThrustVectorSound: Sound; private _secondaryThrustVectorSound: Sound; private _shot: Sound; private _shooting: boolean = false; private _camera: FreeCamera; private _ammoBaseMesh: AbstractMesh; private _controllerMode: ControllerStickMode; private _active = false; constructor(mode: ControllerStickMode = ControllerStickMode.BEGINNER) { this._controllerMode = mode this.setup(); this.initialize(); } public set controllerMode(mode: ControllerStickMode) { this._controllerMode = mode; } private shoot() { this._shot.play(); const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh); ammo.parent = this._ship; ammo.position.y = 2; ammo.rotation.x = Math.PI / 2; ammo.setParent(null); const ammoAggregate = new PhysicsAggregate(ammo, PhysicsShapeType.CONVEX_HULL, { mass: 1000, restitution: 0 }, DefaultScene.MainScene); ammoAggregate.body.setAngularDamping(1); ammoAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC); ammoAggregate.body.setLinearVelocity(this._ship.forward.scale(10000)) //.add(this._ship.physicsBody.getLinearVelocity())); window.setTimeout(() => { ammoAggregate.dispose(); ammo.dispose() }, 1500); } public set position(newPosition: Vector3) { const body = this._ship.physicsBody; body.disablePreStep = false; body.transformNode.position.copyFrom(newPosition); DefaultScene.MainScene.onAfterRenderObservable.addOnce(() => { body.disablePreStep = true; }) } private setup() { 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}); 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); this._ammoBaseMesh.material = this._ammoMaterial; this._ammoBaseMesh.setEnabled(false); const light = new DirectionalLight("light", new Vector3(.1, -1, 0), DefaultScene.MainScene); const landingLight = new SpotLight("landingLight", new Vector3(0, 0, 0), new Vector3(0, -.5, .5), 1.5, .5, DefaultScene.MainScene); landingLight.parent = this._ship; landingLight.position.z = 5; const agg = new PhysicsAggregate(this._ship, PhysicsShapeType.BOX, { mass: 100, extents: new Vector3(4, 4, 7.4), center: new Vector3(0, 1, 1.8) }, DefaultScene.MainScene); agg.body.setMotionType(PhysicsMotionType.DYNAMIC); agg.body.setLinearDamping(.1); agg.body.setAngularDamping(.2); agg.body.setAngularVelocity(new Vector3(0, 0, 0)); agg.body.setCollisionCallbackEnabled(true); this.setupKeyboard(); this.setupMouse(); this._controllerObservable.add(this.controllerCallback); this._forwardNode = new TransformNode("forward", DefaultScene.MainScene); this._rotationNode = new TransformNode("rotation", DefaultScene.MainScene); this._forwardNode.parent = this._ship; this._rotationNode.parent = this._ship; this._camera = new FreeCamera("Flat Camera", new Vector3(0, .5, 0), DefaultScene.MainScene); this._camera.parent = this._ship; DefaultScene.MainScene.setActiveCameraByName("Flat Camera"); //const sightPos = this._forwardNode.position.scale(30); const sight = MeshBuilder.CreateSphere("sight", {diameter: 1}, DefaultScene.MainScene); sight.parent = this._ship const signtMaterial = new StandardMaterial("sightMaterial", DefaultScene.MainScene); signtMaterial.emissiveColor = Color3.Yellow(); sight.material = signtMaterial; sight.position = new Vector3(0, 2, 125); let i = Date.now(); DefaultScene.MainScene.onBeforeRenderObservable.add(() => { if (Date.now() - i > 50 && this._active == true) { this.applyForce(); i = Date.now(); } }); this._active = true; } private async initialize() { const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "cockpit2.glb", DefaultScene.MainScene); const shipMesh = importMesh.meshes[0]; shipMesh.id = "shipMesh"; shipMesh.name = "shipMesh"; shipMesh.parent = this._ship; shipMesh.rotation.y = Math.PI; shipMesh.position.y = 1; shipMesh.position.z = -1; DefaultScene.MainScene.getMaterialById('glass_mat.002').alpha = .4; } private _leftStickVector = Vector2.Zero().clone(); private _rightStickVector = Vector2.Zero().clone(); private _forwardValue = 0; private _yawValue = 0; private _rollValue = 0; private _pitchValue = 0; private _mouseDown = false; private _mousePos = new Vector2(0, 0); public get transformNode() { return this._ship; } private applyForce() { if (!this?._ship?.physicsBody) { return; } const body = this._ship.physicsBody; //If we're moving over MAX_FORWARD_THRUST, we can't add any more thrust, //just continue at MAX_FORWARD_THRUST if (Math.abs(this._forwardValue) > MAX_FORWARD_THRUST) { this._forwardValue = Math.sign(this._forwardValue) * MAX_FORWARD_THRUST; } //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) { this._primaryThrustVectorSound.play(); } this._primaryThrustVectorSound.setVolume(Math.abs(this._leftStickVector.y)); this._forwardValue += this._leftStickVector.y * .8; } else { if (this._primaryThrustVectorSound.isPlaying) { this._primaryThrustVectorSound.pause(); } this._forwardValue = decrementValue(this._forwardValue, .98); } } this._yawValue = adjustStickValue(this._leftStickVector.x, this._yawValue); this._rollValue = adjustStickValue(this._rightStickVector.x, this._rollValue); this._pitchValue = adjustStickValue(this._rightStickVector.y, this._pitchValue); this._forwardNode.position.z = this._forwardValue; this._rotationNode.position.y = this._yawValue; this._rotationNode.position.z = -this._rollValue; this._rotationNode.position.x = -this._pitchValue; const thrust2 = Math.abs(this._rightStickVector.y) + Math.abs(this._rightStickVector.x) + Math.abs(this._leftStickVector.x); if (thrust2 > .01) { if (!this._secondaryThrustVectorSound.isPlaying) { this._secondaryThrustVectorSound.play(); } this._secondaryThrustVectorSound.setVolume(thrust2 * .4); } else { if (this._secondaryThrustVectorSound.isPlaying) { this._secondaryThrustVectorSound.pause(); } } body.setAngularVelocity(this._rotationNode.absolutePosition.subtract(this._ship.absolutePosition)); body.setLinearVelocity(this._forwardNode.absolutePosition.subtract(this._ship.absolutePosition).scale(-1)); } private controllerCallback = (controllerEvent: ControllerEvent) => { if (controllerEvent.type == 'thumbstick') { if (controllerEvent.hand == 'left') { this._leftStickVector.x = controllerEvent.axisData.x; this._leftStickVector.y = controllerEvent.axisData.y; } if (controllerEvent.hand == 'right') { this._rightStickVector.x = controllerEvent.axisData.x; this._rightStickVector.y = controllerEvent.axisData.y; } this.applyForce(); } if (controllerEvent.type == 'button') { if (controllerEvent.component.type == 'trigger') { if (controllerEvent.value > .9 && !this._shooting) { this._shooting = true; this.shoot(); } if (controllerEvent.value < .1) { this._shooting = false; } } } } private setupMouse() { this._ship.getScene().onPointerDown = (evt) => { this._mousePos.x = evt.x; this._mousePos.y = evt.y; this._mouseDown = true; } this._ship.getScene().onPointerUp = () => { this._mouseDown = false; } this._ship.getScene().onPointerMove = () => { }; this._ship.getScene().onPointerMove = (ev) => { if (!this._mouseDown) { return } ; const xInc = this._rightStickVector.x = (ev.x - this._mousePos.x) / 100; const yInc = this._rightStickVector.y = (ev.y - this._mousePos.y) / 100; if (Math.abs(xInc) <= 1) { this._rightStickVector.x = xInc; } else { this._rightStickVector.x = Math.sign(xInc); } if (Math.abs(yInc) <= 1) { this._rightStickVector.y = yInc; } else { this._rightStickVector.y = Math.sign(yInc); } }; } private setupKeyboard() { document.onkeyup = () => { this._leftStickVector.y = 0; this._leftStickVector.x = 0; this._rightStickVector.y = 0; this._rightStickVector.x = 0; } document.onkeydown = (ev) => { switch (ev.key) { case '1': this._camera.position.x = 15; this._camera.rotation.y = -Math.PI / 2; break; case ' ': this.shoot(); break; case 'e': break; case 'w': this._leftStickVector.y = -1; break; case 's': this._leftStickVector.y = 1; break; case 'a': this._leftStickVector.x = -1; break; case 'd': this._leftStickVector.x = 1; break; case 'ArrowUp': this._rightStickVector.y = -1; break; case 'ArrowDown': this._rightStickVector.y = 1; break; } }; } private _leftInputSource: WebXRInputSource; private _rightInputSource: WebXRInputSource; public addController(controller: WebXRInputSource) { if (controller.inputSource.handedness == "left") { this._leftInputSource = controller; this._leftInputSource.onMotionControllerInitObservable.add((motionController) => { this.mapMotionController(motionController); }); } if (controller.inputSource.handedness == "right") { this._rightInputSource = controller; this._rightInputSource.onMotionControllerInitObservable.add((motionController) => { this.mapMotionController(motionController); }); } } private mapMotionController(controller: WebXRAbstractMotionController) { controllerComponents.forEach((component) => { const comp = controller.components[component]; const observable = this._controllerObservable; if (comp && comp.onAxisValueChangedObservable) { comp.onAxisValueChangedObservable.add((axisData) => { observable.notifyObservers({ controller: controller, hand: controller.handness, type: 'thumbstick', component: comp, value: comp.value, axisData: {x: axisData.x, y: axisData.y}, pressed: comp.pressed, touched: comp.touched }) }); } if (comp && comp.onButtonStateChangedObservable) { comp.onButtonStateChangedObservable.add((component) => { observable.notifyObservers({ controller: controller, hand: controller.handness, type: 'button', component: comp, value: component.value, axisData: {x: component.axes.x, y: component.axes.y}, pressed: component.pressed, touched: component.touched }); }); } }); } } function decrementValue(value: number, increment: number = .8): number { if (Math.abs(value) < .01) { return 0; } else { return value * increment; } } function adjustStickValue(stickVector: number, thrustValue: number): number { if (Math.abs(stickVector) > .03) { return thrustValue + (Math.pow(stickVector, 3) * .1); } else { return decrementValue(thrustValue, .85); } }