Dynamically Loading Game Assets with Babylon.js AssetManager
We're diving deeper into the data-driven wonderland of Babylon.js, focusing on dynamically loading game assets using components and systems. Our trusty SceneManager
will orchestrate the show, reading our scene blueprints from JSON and magically bringing our game world to life.
A Refresher: Components and Systems
Before we embark on our asset-loading quest, let's recap the dynamic duo powering our data-driven game:
- Components: Think of components as Lego bricks, each encapsulating a specific aspect of a game object. A "Position" component might hold x, y, z coordinates, while an "Asset" component could store the path to a 3D model.
- Systems: Systems are the master builders, responsible for processing entities with specific components and implementing game logic. For example, a "RenderingSystem" might draw all entities with "Mesh" and "Position" components.
The SceneManager
: Our Game's Stage Manager
Here's a condensed version of our SceneManager
, which we'll be using to load our scene data and kickstart the asset loading process:
import { Scene } from "@babylonjs/core";
import { EntityManager } from "./Entity";
export class SceneManager {
public currentScene: Scene;
entityManager: EntityManager;
scenes: Map<Scene>;
sceneCode = { systems: [], componentTypes: [], entities: [] };
// ... (Other methods like switchToScene, unloadScene, etc.)
async loadSceneData(sceneName: string, gameName: string, onSceneLoaded = null) {
// ... (Check if scene is already loaded)
const response = await fetch(`/gamedata/${gameName}/scenes/${sceneName}.json`);
const sceneData = await response.json();
if (sceneData) {
this.sceneCode = await this.loadSceneCode(this.entityManager, sceneData);
}
// ... (Scene setup and onSceneLoaded logic)
}
async loadSceneCode(entityManager: EntityManager, data: any) {
// ... (Process entities, components, and systems)
}
async importComponent(componentType) {
try {
const module = await import(`../components/${componentType}Component.ts`);
return module[componentType + `Component`];
} catch (error) {
console.error(`Failed to import component: ${componentType}`, error);
return null;
}
}
// ... (Other methods like updateSystems, deriveRequiredSystems)
}
The Key: The loadSceneData
method fetches our scene data from a JSON file. Then hands off to loadSceneCode
, which dynamically imports components and systems based on the data.
The scene.json
File: Blueprinting Our Game World
Let's define our "World" and "Player" entities, each with an Asset
component:
We can call the file anything we want. level1.json
, scene1.json
, rpg-game.json
, etc. We'll stick with scene.json
for now.
{
"entities": {
"World": {
"components": {
"Asset": {
"name": "World",
"path": "glb/world.glb"
}
}
},
"Player": {
"components": {
"Asset": {
"name": "Player",
"path": "glb/player.glb"
}
}
}
}
}
The AssetComponent
: Storing Our 3D Goodies
Just like before, we'll create a simple AssetComponent
to hold our loaded assets:
// AssetComponent.ts
import { Animation, Mesh, ShadowGenerator } from "@babylonjs/core";
import { Component } from "../lib";
export interface AssetComponentInput {
name: string;
path: string;
meshes: Map<String, Mesh>;
animations: Map<String, Mesh>;
}
export class AssetComponent extends Component {
mainMesh: Mesh;
colliderMesh: Mesh;
loaded: boolean;
loading: boolean;
constructor(data: any) {
super(data);
this.name = data.name;
this.meshes = new Map();
this.animations = new Map();
}
}
The AssetSystem
: The Asset-Loading Maestro
Now for the real workhorse – our AssetSystem
:
// AssetSystem.ts
import { Scene, AssetsManager } from "@babylonjs/core";
import "@babylonjs/loaders";
import { Entity, EntityManager, System } from "../lib";
import { AssetComponent } from "../components/AssetComponent";
export class AssetSystem extends System {
constructor(
entityManager: EntityManager,
componentClasses = [AssetComponent],
) {
super(entityManager, componentClasses);
console.log("AssetSystem initialized");
}
processEntity(entity: Entity, deltaTime: number): void {
const assetComponent = entity.getComponent(AssetComponent);
if (!assetComponent.loading && !assetComponent.loaded) {
assetComponent.loading = true;
this.loadAssets(this.scene, assetComponent);
}
}
loadAssets(scene: Scene, assetComponent: AssetComponent): void {
const assetsManager = new AssetsManager(scene);
let { path } = assetComponent;
if (!Array.isArray(path)) path = [path];
let i = 0;
for (const p of path) {
i++;
this.newTask("assetTask" + i, p, assetsManager, assetComponent);
}
assetsManager.load();
assetComponent.loaded = true;
assetComponent.loading = false;
}
newTask(name, path, assetsManager, assetComponent) {
const meshTask = assetsManager.addMeshTask(name, "", "", path);
meshTask.onSuccess = (task) => {
task.loadedMeshes.forEach((mesh) => {
if (mesh.name == "__root__") {
assetComponent.mainMesh = mesh;
}
if (mesh.name.includes("__collider__")) {
mesh.isVisible = false;
mesh.parent = assetComponent.mainMesh;
assetComponent.colliderMesh = mesh;
}
assetComponent.meshes.set(mesh.name, mesh);
console.log("Added", mesh.name);
});
task.loadedAnimationGroups.forEach((a) => {
a.stop();
assetComponent.animations.set(a.name, a);
});
};
meshTask.onError = (task, message, exception) => {
console.error(message, exception);
};
}
}
Explanation:
- The
AssetSystem
gets instantiated by theSceneManager
based on ourscene.json
data. - The
processEntity
method is called by theEntityManager
(not shown here) for each entity with anAssetComponent
. - Inside
processEntity
, we initiate the asset loading process if the asset isn't already loaded or being loaded. - The
loadAssets
method handles the actual loading using Babylon.js'sAssetsManager
.
The Data-Driven Powerhouse in Action
With this setup, here's how the magic unfolds:
- We call
loadSceneData
on ourSceneManager
. - The
SceneManager
reads ourscene.json
, dynamically imports theAssetComponent
andAssetSystem
. - The
EntityManager
processes entities, calling theAssetSystem
'sprocessEntity
for entities withAsset
components. - The
AssetSystem
loads the assets in the background, populating theAssetComponents
with our 3D models.
In Sum
You've come so far – we've built a flexible, data-driven asset loading system with Babylon.js. Next we're going to dive into the heart of our game – the player's movement and interaction with player input.