Closed20

Vite を使って高速で babylon.js 用ライブラリ開発をする

やまゆやまゆ

Vite を使って急いで babylon.js 用のライブラリ babylon-fps-shooter を作ってみます。

FPS のベースとなるような Input の実装をしてみます。

$ yarn create @vitejs/app babylon-fps-shooter --template vanilla-ts
yarn create v1.22.5
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Installed "@vitejs/create-app@2.4.3" with binaries:
      - create-app
      - cva
[#######] 7/7
Scaffolding project in /home/[user]/repos/babylon-fps-shooter...

Done. Now run:

  cd babylon-fps-shooter
  yarn
  yarn dev

Done in 2.47s.
$ cd babylon-fps-shooter
$ yarn
$ yarn dev

これでもう TypeScript アプリのビルド環境が整います。極速。

やまゆやまゆ
$ yarn add @babylonjs/core
...
info Direct dependencies
└─ @babylonjs/core@4.2.0
info All dependencies
├─ @babylonjs/core@4.2.0
└─ tslib@2.3.0
Done in 2.81s.

これで現在の最新版である 4.2.0 が組み込まれました。とりあえず初期化をここをベースにして書きます。

$ yarn add pepjs
...
success Saved 1 new dependency.
info Direct dependencies
└─ pepjs@0.5.3
info All dependencies
└─ pepjs@0.5.3
Done in 2.47s.

依存パッケージである jQuery.pep.js を追加します。

src/main.ts
import { MainScene } from './MainScene'

import 'pepjs'
import './style.css'

const canvas = document.querySelector<HTMLCanvasElement>('#app')!

const mainScene = new MainScene(canvas)
await mainScene.start();
src/style.css
html, body {
  overflow: hidden;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  width: 100%;
  height: 100%;
  touch-action: none;
}
src/MainScene.ts
import { BoxBuilder } from '@babylonjs/core/Meshes/Builders/boxBuilder'
import { Camera } from '@babylonjs/core/Cameras/camera'
import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture'
import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'
import { Engine } from '@babylonjs/core/Engines/engine'
import { EngineOptions } from '@babylonjs/core/Engines/thinEngine'
import { GroundBuilder } from '@babylonjs/core/Meshes/Builders/groundBuilder'
import { Light } from '@babylonjs/core/Lights/light'
import { Mesh } from '@babylonjs/core/Meshes/mesh'
import { Scene, SceneOptions } from '@babylonjs/core/scene'
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator'
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'
import { Texture } from '@babylonjs/core/Materials/Textures/texture'
import { UniversalCamera } from '@babylonjs/core/Cameras/universalCamera'
import { Vector3 } from '@babylonjs/core/Maths/math.vector'

import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent'

/**
 * メインのインゲームシーン
 */
export class MainScene
{
    private readonly engine: Engine
    private readonly scene: Scene
    private readonly camera: Camera
    private readonly mainLight: Light
    private readonly shadowGenerator: ShadowGenerator

    constructor(
        canvas: HTMLCanvasElement,
        antialias: boolean = true,
        options: EngineOptions = {},
        adoptToDeviceRatio: boolean = true,
        sceneOptions: SceneOptions = {},
    ) {
        this.engine = new Engine(canvas, antialias, options, adoptToDeviceRatio)
        this.scene = new Scene(this.engine, sceneOptions)
        this.camera = setUpCamera(canvas, this.scene)
        this.mainLight = setUpMainLight(this.scene)
        this.shadowGenerator = new ShadowGenerator(1024, <any>this.mainLight)
        this.shadowGenerator.useContactHardeningShadow = true
        setUpSkybox(this.scene)
        setUpGround(this.scene)
    }

    public start(): void {
        this.engine.onResizeObservable.add(this.onResise)
        this.engine.runRenderLoop(() => {
            this.scene.render()
        })
    }

    private readonly onResise = (): void => {
        this.engine.resize()
    }
}

/**
 * カメラを作成
 *
 * @param canvas Target Canvas
 * @param scene Target Scene
 * @returns MainCamera
 */
function setUpCamera(canvas: HTMLCanvasElement, scene: Scene): Camera {
    const camera = new UniversalCamera(`MainCamera`, new Vector3(0, 2, 10), scene)
    camera.setTarget(Vector3.Zero())
    camera.attachControl(canvas, true)
    camera.applyGravity = true
    camera.ellipsoid = Vector3.One()
    camera.checkCollisions = true

    return camera
}

/**
 * メインライトを作成
 *
 * @param scene Target Scene
 * @returns MainLight
 */
function setUpMainLight(scene: Scene): Light {
    const light = new DirectionalLight(`MainLight`, new Vector3(0, -7, 4), scene)
    light.intensity = 0.7

    return light
}

/**
 * スカイボックス背景を生成
 *
 * @param scene Target Scene
 * @returns Mesh
 */
function setUpSkybox(scene: Scene): Mesh {
    const mesh = BoxBuilder.CreateBox(`MainSkyBox`, { size: 10000 }, scene)
    const material = new StandardMaterial(`MainSkyBoxMaterial`, scene)
    material.backFaceCulling = false
    material.reflectionTexture = new CubeTexture(`https://playground.babylonjs.com/textures/skybox`, scene)
    material.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE
    material.disableLighting = true
    mesh.material = material

    return mesh
}

/**
 * 地面を生成
 *
 * @param scene Target Scene
 * @returns Ground Mesh
 */
function setUpGround(scene: Scene): Mesh {
    const material = new StandardMaterial('ground', scene)
    const groundDiffuse = new Texture('https://playground.babylonjs.com/textures/grass.png', scene)
    groundDiffuse.uScale = 3000
    groundDiffuse.vScale = 3000
    material.diffuseTexture =  groundDiffuse
    const groundBump = new Texture('https://playground.babylonjs.com/textures/grassn.png', scene)
    groundBump.uScale = 3000
    groundBump.vScale = 3000
    material.bumpTexture = groundBump
    const ground = GroundBuilder.CreateGround("MainGround", { width: 10000, height: 10000}, scene)
    ground.material = material
    ground.checkCollisions = true
    ground.receiveShadows = true

    return ground
}
やまゆやまゆ

上記コードでスカイボックスと地面、デフォルトのカメラ(矢印キーで移動、マウスドラッグで視点回転, スマホでも動かせる)を追加しました。

やまゆやまゆ

https://playground.babylonjs.com/#YNEAUL#11 を参考にして家を生成します。

ついでに端から落ちてしまわないように壁で囲います。

src/MainScene.ts
import { BoxBuilder } from '@babylonjs/core/Meshes/Builders/boxBuilder'
import { Camera } from '@babylonjs/core/Cameras/camera'
import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture'
import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'
import { Engine } from '@babylonjs/core/Engines/engine'
import { EngineOptions } from '@babylonjs/core/Engines/thinEngine'
import { GroundBuilder } from '@babylonjs/core/Meshes/Builders/groundBuilder'
import { Light } from '@babylonjs/core/Lights/light'
import { Mesh } from '@babylonjs/core/Meshes/mesh'
import { Scene, SceneOptions } from '@babylonjs/core/scene'
import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader'
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator'
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'
import { Texture } from '@babylonjs/core/Materials/Textures/texture'
import { UniversalCamera } from '@babylonjs/core/Cameras/universalCamera'
import { Vector3 } from '@babylonjs/core/Maths/math.vector'

// side-effects
import '@babylonjs/core/Collisions/collisionCoordinator'
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent'
import '@babylonjs/core/Loading/Plugins/babylonFileLoader'

/**
 * メインのインゲームシーン
 */
export class MainScene
{
    private readonly engine: Engine
    private readonly scene: Scene
    private readonly camera: Camera
    private readonly mainLight: Light
    private readonly shadowGenerator: ShadowGenerator

    /**
     * コンストラクタ
     *
     * @param canvas レンダリングするキャンバス要素
     * @param antialias アンチエイリアスを有効にするかどうか
     * @param options エンジンオプション
     * @param adoptToDeviceRatio デバイスレシオに適応するかどうか
     * @param sceneOptions シーンオプション
     */
    constructor(
        canvas: HTMLCanvasElement,
        antialias: boolean = true,
        options: EngineOptions = {},
        adoptToDeviceRatio: boolean = true,
        sceneOptions: SceneOptions = {},
    ) {
        this.engine = new Engine(canvas, antialias, options, adoptToDeviceRatio)
        this.scene = new Scene(this.engine, sceneOptions)
        this.camera = setUpCamera(canvas, this.scene)
        this.mainLight = setUpMainLight(this.scene)
        this.shadowGenerator = new ShadowGenerator(2048, <any>this.mainLight)
        this.shadowGenerator.useContactHardeningShadow = true
        setUpSkybox(this.scene)
        setUpGround(this.scene)
        setUpWalls(this.scene)
    }

    /**
     * シーンの動作をスタートする
     */
    public async start(): Promise<void> {
        await loadHouses(this.shadowGenerator)
        document.addEventListener('resize', this.onResise)
        this.engine.runRenderLoop(() => {
            this.scene.render()
        })
    }

    /**
     * 画面リサイズ時の挙動
     */
    private readonly onResise = (): void => {
        this.engine.resize()
    }
}

/**
 * カメラを作成
 *
 * @param canvas Target Canvas
 * @param scene Target Scene
 * @returns MainCamera
 */
function setUpCamera(canvas: HTMLCanvasElement, scene: Scene): Camera {
    const camera = new UniversalCamera(`MainCamera`, new Vector3(0, 2, 10), scene)
    camera.setTarget(Vector3.Zero())
    camera.attachControl(canvas, true)
    camera.applyGravity = true
    camera.ellipsoid = new Vector3(1.2, 1.2, 1.2)
    camera.checkCollisions = true

    return camera
}

/**
 * メインライトを作成
 *
 * @param scene Target Scene
 * @returns MainLight
 */
function setUpMainLight(scene: Scene): Light {
    const light = new DirectionalLight(`MainLight`, new Vector3(0, -7, 4), scene)
    light.intensity = 0.7

    return light
}

/**
 * スカイボックス背景を生成
 *
 * @param scene Target Scene
 * @returns Mesh
 */
function setUpSkybox(scene: Scene): Mesh {
    const mesh = BoxBuilder.CreateBox(`MainSkyBox`, { size: 10000 }, scene)
    const material = new StandardMaterial(`MainSkyBoxMaterial`, scene)
    material.backFaceCulling = false
    material.reflectionTexture = new CubeTexture(`https://playground.babylonjs.com/textures/skybox`, scene)
    material.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE
    material.disableLighting = true
    mesh.material = material

    return mesh
}

/**
 * 地面を生成
 *
 * @param scene Target Scene
 * @returns Ground Mesh
 */
function setUpGround(scene: Scene): Mesh {
    const material = new StandardMaterial('ground', scene)
    const groundDiffuse = new Texture('https://playground.babylonjs.com/textures/grass.png', scene)
    groundDiffuse.uScale = 60
    groundDiffuse.vScale = 60
    material.diffuseTexture =  groundDiffuse
    const groundBump = new Texture('https://playground.babylonjs.com/textures/grassn.png', scene)
    groundBump.uScale = 60
    groundBump.vScale = 60
    material.bumpTexture = groundBump
    const ground = GroundBuilder.CreateGround("MainGround", { width: 200, height: 200}, scene)
    ground.material = material
    ground.checkCollisions = true
    ground.receiveShadows = true

    return ground
}

/**
 * サンプル用の家メッシュを読み込む
 *
 * @param scene Target Scene
 */
async function loadHouses(shadowGenerator: ShadowGenerator): Promise<void> {
    const result = await SceneLoader.ImportMeshAsync(
        `semi_house`,
        `https://assets.babylonjs.com/meshes/`,
        `both_houses_scene.babylon`,
    )
    const houseBase = result.meshes[0]
    houseBase.checkCollisions = true;

    for (let index = 0; index < 100; index++) {
        const house = houseBase.clone(`house${index}`, null)
        shadowGenerator.addShadowCaster(house!)
        house!.position = new Vector3(Math.random() * 200 - 100, 0, Math.random() * 200 - 100)
        house!.scaling = new Vector3(Math.random() + 1, Math.random() + 1, Math.random() + 1)
        house!.rotate(Vector3.Up(), Math.random() * Math.PI * 4)
    }
}

/**
 * フィールド端の壁を設置する
 *
 * @param scene Target Scene
 */
function setUpWalls(scene: Scene): void {
    const wallZMaterial = new StandardMaterial('wall', scene);
    const wallZDiffuse = new Texture('https://playground.babylonjs.com/textures/floor.png', scene);
    wallZDiffuse.uScale = 60;
    wallZDiffuse.vScale = 5;
    wallZMaterial.diffuseTexture = wallZDiffuse;
    const wallZBump = new Texture('https://playground.babylonjs.com/textures/normalmap.jpg', scene);
    wallZBump.uScale = 60;
    wallZBump.vScale = 5;
    wallZMaterial.bumpTexture = wallZBump;

    const wallXMaterial = new StandardMaterial('wall', scene);
    const wallXDiffuse = new Texture('https://playground.babylonjs.com/textures/floor.png', scene);
    wallXDiffuse.uScale = 5;
    wallXDiffuse.vScale = 60;
    wallXMaterial.diffuseTexture = wallXDiffuse;
    const wallXBump = new Texture('https://playground.babylonjs.com/textures/normalmap.jpg', scene);
    wallXBump.uScale = 5;
    wallXBump.vScale = 60;
    wallXMaterial.bumpTexture = wallXBump;

    let box = BoxBuilder.CreateBox('+z', {
        size: 1,
    }, scene);
    box.material = wallZMaterial.clone('mat+z');
    box.scaling = new Vector3(200, 10, 1);
    box.position.z = 100;
    box.checkCollisions = true;
    box.receiveShadows = true;

    box = BoxBuilder.CreateBox('-z', {
        size: 1,
    }, scene);
    box.material = wallZMaterial.clone('mat-z');
    box.scaling = new Vector3(200, 10, 1);
    box.position.z = -100;
    box.checkCollisions = true;
    box.receiveShadows = true;

    box = BoxBuilder.CreateBox('+x', {
        size: 1,
    }, scene);
    box.material = wallXMaterial.clone('mat+x');
    box.scaling = new Vector3(1, 10, 200);
    box.position.x = 100;
    box.checkCollisions = true;
    box.receiveShadows = true;

    box = BoxBuilder.CreateBox('-x', {
        size: 1,
    }, scene);
    box.material = wallXMaterial.clone('mat-x');
    box.scaling = new Vector3(1, 10, 200);
    box.position.x = -100;
    box.checkCollisions = true;
    box.receiveShadows = true;
}
やまゆやまゆ

とりあえず物がある状態になりました。一応当たり判定もついているので、簡単には壁抜け出来ません。

やまゆやまゆ
src/MainScene.ts
/**
 * 動く箱を生成する
 *
 * @param scene Target Scene
 */
function setUpMovingBox(scene: Scene): void {
    const movingBox = BoxBuilder.CreateBox(`MovingBox`, {
        size: 5,
    }, scene)
    movingBox.position.y = -2.3
    movingBox.checkCollisions = true
    movingBox.receiveShadows = true

    scene.onBeforeRenderObservable.add(() => {
        const delta = scene.getEngine().getDeltaTime()
        movingBox.position.x += delta / 60
        if (movingBox.position.x > 100) {
            movingBox.position.x = -100
        }
    })
}

動く箱を生成してみました。しかし、うまく乗れてもカメラは移動せず...。ファイナルソード状態。

やまゆやまゆ

WASD で移動

src/MainScene.ts
/**
 * カメラを作成
 *
 * @param canvas Target Canvas
 * @param scene Target Scene
 * @returns MainCamera
 */
function setUpCamera(canvas: HTMLCanvasElement, scene: Scene): Camera {
    const camera = new FreeCamera(`MainCamera`, new Vector3(0, 2, 10), scene)
    camera.setTarget(Vector3.Zero())
    camera.attachControl(canvas, true);
    // 矢印キーではなく WASD で移動
    camera.keysUp = ["W".charCodeAt(0)]
    camera.keysDown = ["S".charCodeAt(0)]
    camera.keysLeft = ["A".charCodeAt(0)]
    camera.keysRight = ["D".charCodeAt(0)]
    camera.applyGravity = true
    camera.ellipsoid = new Vector3(1.2, 1.2, 1.2)
    camera.checkCollisions = true
    camera.speed = 0.5

    return camera
}

トラッキングするキーコードを上書きしています。ついでに動きが早すぎるのでスピードを調整(default: 2.0)。

やまゆやまゆ

Pointer Lock API を使用

src/MainScene.ts
    /**
     * シーンの動作をスタートする
     */
    public async start(): Promise<void> {
        await loadHouses(this.shadowGenerator)
        window.addEventListener('resize', this.onResise)
        this.scene.activeCamera = this.camera
        document.addEventListener('click', this.onMouseClick)
        this.engine.runRenderLoop(() => {
            this.scene.render()
        })
    }

    private readonly onMouseClick = (): void => {
        if (!this.engine.isPointerLock) {
            this.engine.enterPointerlock()
        }
    }

this.engine.enterPointerLock メソッドがあるので、それを使ってマウス操作時にポインターロックをかけます。 Escape でロックを解除できます。

やまゆやまゆ

ダッシュ

src/MainScene.ts
/**
 * ダッシュを行えるようにするインプット
 */
class ShooterCameraDashInput implements ICameraInput<FreeCamera> {
    public camera: FreeCamera

    /**
     * ダッシュ速度
     */
    public dashSpeed = 0.8
    /**
     * 歩き速度
     */
    public walkSpeed = 0.5

    private _onKeyboardObservable: any

    public constructor(camera: FreeCamera) {
        this.camera = camera
    }

    public attachControl(noPreventDefault?: boolean): void {
        noPreventDefault = Tools.BackCompatCameraNoPreventDefault(arguments)

        this._onKeyboardObservable = this.camera.getScene().onKeyboardObservable.add((info) => {
            if (info.type === 1 && info.event.code === 'ShiftLeft') {
                this.camera.speed = this.dashSpeed
            } else {
                this.camera.speed = this.walkSpeed
            }
            if (!noPreventDefault) {
                info.event.preventDefault()
            }
        })
    }

    public detachControl(): void;

    public detachControl(ignored?: any): void {
        if (this._onKeyboardObservable) {
            this.camera.getScene().onActiveCameraChanged.remove(this._onKeyboardObservable)
            this._onKeyboardObservable = null
        }
    }

    public getClassName(): string {
        return 'ShooterCameraDashInput'
    }

    public getSimpleName(): string {
        return 'dash'
    }
}
    const camera = new FreeCamera(`MainCamera`, new Vector3(0, 2, 10), scene)
    camera.setTarget(Vector3.Zero())
    camera.inputs.add(new ShooterCameraDashInput(camera))
    camera.attachControl(canvas, true);

これで LeftShift を押すと少し移動速度が上がるようになりました。

やまゆやまゆ
src/MainScene.ts
/**
 * 敵キャラクターを生成する
 *
 * @param scene Target Scene
 * @param shadowGenerator Shadow Generator
 */
async function loadMobs(scene: Scene, shadowGenerator: ShadowGenerator): Promise<void> {
    const capsuleBase = CapsuleBuilder.CreateCapsule(`mobBase`, {
        height: 2,
        capSubdivisions: 6,
        radius: 1,
        subdivisions: 2,
        tessellation: 16,
    }, scene)
    capsuleBase.position.y = 1
    const material = new StandardMaterial(`MobMaterialBase`, scene)
    capsuleBase.material = material

    for (let index = 0; index < 100; index++) {
        const capsule = capsuleBase.clone(`Mob${index}`, null)
        capsule.material = material.clone(`MobMaterial${index}`);
        (capsule.material as StandardMaterial).diffuseColor = new Color3(Math.random(), Math.random(), Math.random())
        shadowGenerator.addShadowCaster(capsule)
        capsule.position = new Vector3(Math.random() * 200 - 100, 1, Math.random() * 200 - 100)
        capsule.rotate(Vector3.Up(), Math.random() * Math.PI * 4)
        scene.onBeforeRenderObservable.add(() => {
            capsule.rotate(Vector3.Up(), 0.01)
            capsule.moveWithCollisions(capsule.forward.multiplyByFloats(0.05, 0.05, 0.05))
        })
    }
}

これで色カラフルな敵キャラクターが自動で移動していきます。 mesh.moveWithCollisions を利用しているので壁にめり込みません。

やまゆやまゆ

上のスクリーンショットで気づいた方がいるかもしれませんが、最新コードでは Screen Space Ambient Occlusion 通称 SSAO を追加しています。

import { SSAORenderingPipeline } from '@babylonjs/core/PostProcesses/RenderPipeline/Pipelines/ssaoRenderingPipeline'

// side-effects
import '@babylonjs/core/Rendering/depthRendererSceneComponent'

// ...

        new SSAORenderingPipeline(`ssaoPipeline`, this.scene, 0.75, [this.camera])

一行このレンダリングパイプラインを new するだけ、それだけで SSAO が動きます。あまり精度は高くないですが、リアリティがかなり増しますね。

やまゆやまゆ

ちなみに影は ShadowGenerator というコンポーネントを利用することで簡単に影生成が出来るのですが、今回は CascadedShadowGenerator という拡張コンポーネントを利用しています。

これは、影の生成に必要なテクスチャを、カメラからの距離に応じて複数枚に分割し、近くの影は精細に、遠くの影はそれなりにという感じでよりリアルに影を感じれるものです。

import { CascadedShadowGenerator } from '@babylonjs/core/Lights/Shadows/cascadedShadowGenerator'

import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent'

// ...

        this.shadowGenerator = new CascadedShadowGenerator(2048, <any>this.mainLight)

この shadowGeneratoraddShadowCaster(mesh) していくことで、影を生成するメッシュを選択できます。 mesh.receiveShadows = true とすることで、生成された影を投影するメッシュとすることが出来ます。今回は地面のみ receiveShadows をつけています。

やまゆやまゆ

TODO

  • Mob に HP を持たせる
  • Click で Ray を飛ばして Mob に当たったらダメージを与える
    • 弾速を再現する
  • 自分に HP を持たせる
    • UI ゲージ
  • Mob に自分が当たったらダメージを受ける
やまゆやまゆ

Ray の勉強中。 origin を camera.position にするとなぜか表示されなくなる。

やまゆやまゆ
const origin = this.camera.position

これだと参照渡しになるのでカメラの移動にくっついてしまうため表示域に出てこなかった。

const origin = this.camera.globalPosition.clone()

これでちゃんとマウスクリックした時点の原点から Ray が飛ばせるようになった。

        const origin = this.camera.globalPosition.clone()
        const forward = this.camera.getDirection(Vector3.Forward())
        const ray = new Ray(origin, forward, 100)
        const rayHelper = new RayHelper(ray)
        rayHelper.show(this.scene)
やまゆやまゆ
        const hit = this.scene.pickWithRay(ray, (mesh) => {
            return mesh.name.match(/^Mob.+/) !== null
        })
        if (hit && hit.pickedMesh) {
            hit.pickedMesh.dispose()
        }

これで Mob を正面にとらえてクリックしたら、Mobが削除されるようになった。

(最初 predicate 関数を入れなかったので、クリックしたら床が抜けました。)

やまゆやまゆ

Audio も簡単に実装できる。

// こうすることで static なアセットのURLを入手することが出来る
// @see https://vitejs.dev/guide/assets.html#importing-asset-as-url
import gunfireSoundURL from './gunfire.mp3?url'

// ...

// コンストラクタで非同期読み込み
this.gunfireSound = new Sound(`Gunfire`, gunfireSoundURL, this.scene, () => this.isReadyGunfiresound = true)

// 単純に play するだけ
if (this.isReadyGunfiresound) {
    this.gunfireSound.play()
}
このスクラップは2021/08/09にクローズされました