logo

Babylon.js Market

By Lawrence

5 minutes

Last lesson the gun started working. Hold an arrow key and bullets stream out. They hit the cover walls and recycle back into the pool. But right now they fly through empty air. Nothing in the arena can take a hit. It's time to build an enemy.

But the enemy isn't a single thing you drop next to the player. You placed the ship once, and it's one capsule. The enemy works differently. The finished game has dozens of enemies. They come from a pool and spawn in waves, and each one is a fully animated robot. So the enemy is made in a different way from everything you've built so far. That changes what this lesson can do.

This lesson builds the enemy's parts. There are three: the robot model, the animator that plays its clips, and the brain that decides what it does. It does not put a live enemy in the arena yet. Spawning the waves is the pool's job (Lesson 7). By the end, every enemy system will be registered and ready, standing by for the moment a robot spawns.

The Mesh component: the enemy's model

The player is a MeshPrimitive, a built-in capsule shape. The enemy is a real model. It's the shooter robot GLB, and it has Walking, Punch, and a set of death clips baked in. Back in Lesson 1, bjs download scene TwinStickArena --all already put it in your project at public/shooter-shooter-bot-1/shooter-bot-1.glb. The Mesh component loads a file like that through the renderer. Two of its flags make a big swarm cheap:

src/components/Mesh/Mesh.core.ts
/**
 * When true, load via `loadModelTemplate` + `instantiateModel` (one shared
 * template per url, cheap per-entity clones) instead of `loadMesh`. Use for
 * pools of identical animated characters. Default false.
 */
instanced: boolean;
/**
 * When true, each frame copy the sibling `MeshPrimitiveComponent.position`
 * X/Z onto the mesh (the mesh's own Y stays `position[1]`, used as a
 * ground/feet offset) and yaw the mesh toward its movement direction.
 * Default false.
 */
follow: boolean;

instanced: true decides whether a wave runs smoothly or stutters. It loads one shared template per file, then makes a near-free clone for each enemy. Sixteen robots, but the GLB is parsed once. follow: true puts the loaded model on top of a sibling MeshPrimitive proxy. The proxy is a cheap, invisible shape. It does the real moving and colliding, and the GLB model rides on top of it. The GLB just goes where the proxy goes.

When a clone finally exists, the System announces it on the bus:

src/components/Mesh/Mesh.ts
const animationNames = this.templateAnimationNames.get(url) ?? [];
const handle = r.instantiateModel(entityId, url, {
  position: params.position,
  rotation: params.rotation,
  scale: params.scale,
});
this.getRuntime(comp).handle = handle;
// Clone starts hidden — reveal it.
r.setModelVisible(handle, true);

// Now the clone exists: complete the load + stop retrying.
inst.completeLoad(entityId, animationNames);
this.dispatched.add(entityId);
this.eventBus.emit(MeshEvents.LOADED, {
  entityId,
  meshId: entityId,
  animationNames,
});

That mesh.loaded event is a signal, and one component is waiting to hear it.

Continue reading

Unlock the Full Course

Every lesson, the runnable examples, and the finished build — yours to keep.

$9one-time

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search