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.
import { AssetLoader } from "@carverjs/core/components";Quick Start #
Minimal preload #
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 #
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 #
On mount,
AssetLoaderparses the manifest and starts loading all non-lazy assets via theAssetManager.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.During loading, the
fallbackis rendered as an HTML overlay via drei's<Html fullscreen>. The progress callback receives real-timeLoadingProgressupdates.On completion, children are rendered inside a
<Suspense>boundary. The game phase transitions from"loading"to"playing".Assets are cached in the
AssetManager's memory cache with LRU eviction. Access them later viauseAssets.
Props Reference #
| Prop | Type | Default | Description |
|---|---|---|---|
manifest | AssetManifest | AssetEntry[] | string | — | Required. Assets to preload. Pass an array of entries, a full manifest object, or a URL to a JSON manifest file |
fallback | ReactNode | (progress: LoadingProgress) => ReactNode | — | UI shown while loading. Use the render-prop form for progress display |
concurrency | number | 6 | Maximum assets loading in parallel |
retries | number | 3 | Retry attempts per failed asset |
retryDelay | number | 1000 | Base delay between retries (ms). Uses exponential backoff with jitter |
timeout | number | 30000 | Timeout per asset in ms |
minLoadTime | number | 0 | Minimum time to show fallback (ms). Prevents flash for fast loads |
onComplete | () => void | — | Called when all assets finish loading |
onError | (errors: AssetLoadError[]) => void | — | Called when any asset fails after all retries |
onProgress | (progress: LoadingProgress) => void | — | Called on each progress update |
children | ReactNode | — | Required. Content to render once loading completes |
Asset Entry #
Each entry in the manifest describes a single asset:
| Field | Type | Default | Description |
|---|---|---|---|
url | string | — | Required. URL to load (relative or absolute) |
key | string | url | Unique key for referencing via useAssets() |
type | AssetType | Auto-detected | Asset type: "gltf", "texture", "audio", "json", "binary" |
priority | "critical" | "high" | "normal" | "low" | "lazy" | number | "normal" | Loading priority. Higher loads first. "lazy" assets are skipped |
group | string | — | Group name for batch loading/unloading |
sizeHint | number | — | Expected file size in bytes (for progress estimation) |
loaderOptions | Record<string, unknown> | — | Loader-specific options. GLTF: { draco: true }. Audio: { streaming: true } |
Auto-Detected Types #
| Extensions | Asset 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:
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:
<AssetLoader manifest="/assets/manifest.json" fallback={<LoadingScreen />}>
{children}
</AssetLoader>Priority Ordering #
Assets are sorted by priority before loading. Higher values load first:
| Priority | Value | Use Case |
|---|---|---|
"critical" | 100 | Splash screen assets, essential models |
"high" | 75 | Main gameplay assets |
"normal" | 50 | Standard assets (default) |
"low" | 25 | Background music, ambient textures |
"lazy" | 0 | Not loaded by AssetLoader — loaded on-demand via useAssets |
Nested AssetLoaders (Level Streaming) #
Nest AssetLoader components for level-by-level asset loading:
<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:
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:
<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:
Gamemounts →phase = "loading"(default)AssetLoaderloads assets → phase stays"loading"Loading completes →
AssetLoadersetsphase = "playing"
This integrates with useGameLoop, which only fires callbacks when phase === "playing".
Type Definitions #
See Types for AssetManifest, AssetEntry, AssetGroupConfig, AssetType, LoadingProgress, and AssetLoadError.