ローディング途中の画面を見せない工夫

2025-12-18 ・ 読了目安 4分

画面の読み込みが追いつかないうちに表示を始めてしまう問題

  • 一部のゲームオブジェクトだけない状態でゲームが始まってしまう
    • GLBモデルやテクスチャなどの読み込みが完了する前に描画を開始すると、一部のオブジェクトだけ後から突然現れる
    • 突然現れるのはカッコ悪いし、ユーザー体験も悪い
  • ロード処理に簡単にロード状態を追加できるようにしたい
    • 時間がかかる処理はローダーの詳しい実装を知らなくても、とりあえず登録できるようになっている
    • GLBモデルの読み込み、テクスチャの読み込み、物理ワールドの初期化など、様々な処理を統一的に扱える

SceneLoadTrackerによる解決

  • シーン単位でロード状態を管理
    • 各シーンごとに、ロード中のPromiseを追跡する
    • track()でPromiseを登録し、完了を待つ
  • ロード完了まで待機してから描画開始
    • waitForIdle()で、pendingが0になるまで待つ
    • 全てのロードが完了してからゲームを開始することで、突然現れるオブジェクトを防ぐ
  • 簡単にロード状態を追加できる設計
    • 時間がかかる処理のPromiseをtrack()に渡すだけで、ロード状態に追加される
    • ローダーの内部実装を知らなくても、とりあえず登録できる

SceneLoadTrackerの実装詳細

SceneLoadTrackerはシングルトンパターンで実装されており、シーンIDごとにロード状態を管理します。

export class SceneLoadTracker {
  private static instance: SceneLoadTracker | null = null;
  private stateBySceneId = new Map<string, SceneLoadState>();

  track<T>(sceneId: string, promise: Promise<T>): Promise<T> {
    const state = this.ensure(sceneId);
    state.total += 1;
    state.pendingPromises.add(promise);
    
    promise.finally(() => {
      state.completed += 1;
      state.pendingPromises.delete(promise);
      this.notify(sceneId);
    });
    
    return promise;
  }

  waitForIdle(sceneId: string): Promise<void> {
    const snap = this.snapshot(sceneId);
    if (snap.pending === 0) {
      return Promise.resolve();
    }
    
    return new Promise((resolve) => {
      const unsubscribe = this.subscribe(sceneId, (s) => {
        if (s.pending === 0) {
          unsubscribe();
          resolve();
        }
      });
    });
  }
}

使用例

// GLBモデルの読み込みを追跡
const loader = new GLTFLoader();
const loadPromise = scene.trackLoad(
  loader.loadAsync('model.glb')
);

// テクスチャの読み込みを追跡
const texturePromise = scene.trackLoad(
  new THREE.TextureLoader().loadAsync('texture.jpg')
);

// 全てのロードが完了するまで待機
await scene.waitForLoadingIdle();

// ここからゲームを開始
scene.start();