AssetLoader

AssetLoader is a declarative preloader that loads all assets in a manifest before rendering its children. It shows a fallback (typically a LoadingScreen) during loading and automatically transitions the game phase from "loading" to "playing" when complete.

Place it inside Game (requires R3F context) and wrap your World or SceneManager.

tsx
import { AssetLoader } from "@carverjs/core/components";

Quick Start #

Minimal preload #

tsx
import { Game, World, Actor, AssetLoader } from "@carverjs/core/components";

const manifest = [
  { url: "/models/hero.glb" },
  { url: "/textures/grass.png" },
  { url: "/audio/bgm.mp3", priority: "low" },
];

function App() {
  return (
    <Game>
      <AssetLoader
        manifest={manifest}
        fallback={<div style={{ color: "#fff", textAlign: "center" }}>Loading...</div>}
      >
        <World>
          <Actor type="model" src="/models/hero.glb" />
        </World>
      </AssetLoader>
    </Game>
  );
}

With LoadingScreen #

tsx
import { Game, World, Actor, AssetLoader, LoadingScreen } from "@carverjs/core/components";

function App() {
  return (
    <Game>
      <AssetLoader
        manifest={[
          { url: "/models/hero.glb", priority: "critical" },
          { url: "/audio/music.mp3", priority: "low" },
        ]}
        fallback={(progress) => (
          <LoadingScreen
            progress={progress}
            theme="gaming"
            tips={["Use WASD to move", "Collect coins for points"]}
          />
        )}
      >
        <World>
          <Actor type="model" src="/models/hero.glb" />
        </World>
      </AssetLoader>
    </Game>
  );
}

How It Works #

  1. On mount, AssetLoader parses the manifest and starts loading all non-lazy assets via the AssetManager.

  2. For GLTF and texture assets, it also calls drei's useGLTF.preload() / useTexture.preload() to warm drei's internal cache. The browser's HTTP cache prevents double downloads.

  3. During loading, the fallback is rendered as an HTML overlay via drei's <Html fullscreen>. The progress callback receives real-time LoadingProgress updates.

  4. On completion, children are rendered inside a <Suspense> boundary. The game phase transitions from "loading" to "playing".

  5. Assets are cached in the AssetManager's memory cache with LRU eviction. Access them later via useAssets.


Props Reference #

PropTypeDefaultDescription
manifestAssetManifest | AssetEntry[] | stringRequired. Assets to preload. Pass an array of entries, a full manifest object, or a URL to a JSON manifest file
fallbackReactNode | (progress: LoadingProgress) => ReactNodeUI shown while loading. Use the render-prop form for progress display
concurrencynumber6Maximum assets loading in parallel
retriesnumber3Retry attempts per failed asset
retryDelaynumber1000Base delay between retries (ms). Uses exponential backoff with jitter
timeoutnumber30000Timeout per asset in ms
minLoadTimenumber0Minimum time to show fallback (ms). Prevents flash for fast loads
onComplete() => voidCalled when all assets finish loading
onError(errors: AssetLoadError[]) => voidCalled when any asset fails after all retries
onProgress(progress: LoadingProgress) => voidCalled on each progress update
childrenReactNodeRequired. Content to render once loading completes

Asset Entry #

Each entry in the manifest describes a single asset:

FieldTypeDefaultDescription
urlstringRequired. URL to load (relative or absolute)
keystringurlUnique key for referencing via useAssets()
typeAssetTypeAuto-detectedAsset type: "gltf", "texture", "audio", "json", "binary"
priority"critical" | "high" | "normal" | "low" | "lazy" | number"normal"Loading priority. Higher loads first. "lazy" assets are skipped
groupstringGroup name for batch loading/unloading
sizeHintnumberExpected file size in bytes (for progress estimation)
loaderOptionsRecord<string, unknown>Loader-specific options. GLTF: { draco: true }. Audio: { streaming: true }

Auto-Detected Types #

ExtensionsAsset Type
.gltf, .glb"gltf"
.png, .jpg, .jpeg, .webp, .svg"texture"
.mp3, .ogg, .wav, .m4a"audio"
.json"json"
.bin, .dat"binary"

Manifest Object #

For larger games, use the full manifest format with a base URL and groups:

tsx
const manifest = {
  version: 1,
  baseUrl: "/assets/",
  assets: [
    { url: "models/hero.glb", group: "level-1", priority: "critical" },
    { url: "models/tree.glb", group: "level-1" },
    { url: "maps/level-1.json", group: "level-1" },
    { url: "audio/bgm.mp3", priority: "low" },
  ],
  groups: {
    "level-1": { label: "Forest Level", preload: true },
  },
};

JSON Manifest (URL) #

Host the manifest as a JSON file and pass the URL:

tsx
<AssetLoader manifest="/assets/manifest.json" fallback={<LoadingScreen />}>
  {children}
</AssetLoader>

Priority Ordering #

Assets are sorted by priority before loading. Higher values load first:

PriorityValueUse Case
"critical"100Splash screen assets, essential models
"high"75Main gameplay assets
"normal"50Standard assets (default)
"low"25Background music, ambient textures
"lazy"0Not loaded by AssetLoader — loaded on-demand via useAssets

Nested AssetLoaders (Level Streaming) #

Nest AssetLoader components for level-by-level asset loading:

tsx
<Game>
  <AssetLoader manifest={coreAssets} fallback={<SplashScreen />}>
    <SceneManager initial="level-1">
      <Scene
        name="level-1"
        component={() => (
          <AssetLoader manifest={level1Assets} fallback={(p) => <LoadingScreen progress={p} />}>
            <Level1Scene />
          </AssetLoader>
        )}
      />
    </SceneManager>
  </AssetLoader>
</Game>

The outer loader handles core assets (UI, fonts). Each level's loader handles level-specific assets. When a level unmounts, its assets remain cached for fast revisits (evicted only under memory pressure).


GLTF Draco/Meshopt #

Pass compression options via loaderOptions:

tsx
const manifest = [
  { url: "/models/hero.glb", loaderOptions: { draco: true } },
  { url: "/models/scene.glb", loaderOptions: { meshopt: true } },
];

Error Handling #

Failed assets are retried with exponential backoff. After all retries, errors are reported:

tsx
<AssetLoader
  manifest={manifest}
  retries={3}
  retryDelay={1000}
  onError={(errors) => {
    console.error("Failed to load:", errors);
  }}
  fallback={(progress) => (
    <div>
      <p>Loading... {Math.round(progress.progress * 100)}%</p>
      {progress.errors.length > 0 && (
        <p style={{ color: "red" }}>
          {progress.errors.length} asset(s) failed
        </p>
      )}
    </div>
  )}
>
  {children}
</AssetLoader>

Game Phase Integration #

AssetLoader automatically manages the game phase:

  1. Game mounts → phase = "loading" (default)

  2. AssetLoader loads assets → phase stays "loading"

  3. Loading completes → AssetLoader sets phase = "playing"

This integrates with useGameLoop, which only fires callbacks when phase === "playing".


Type Definitions #

See Types for AssetManifest, AssetEntry, AssetGroupConfig, AssetType, LoadingProgress, and AssetLoadError.