logo

Babylon.js Market

Install with the CLI:

bjs download TwinStickEnemy

TwinStickEnemy

A chaser that hunts the player, separates from its swarm, and deals contact damage.

Named for its subgenre. The bare Enemy is the genre-neutral hostile marker — no data, no System — which is what a space shooter or a platformer wants when it only needs to say "this entity is hostile". This one is the twin-stick swarm brain.

What it does

Chases the first entity tagged player, pushes apart from other enemies so they don't stack, and deals contact damage via health.damage when it touches the player. Bullet hits arrive as bullet.hit; HP lives on a sibling Health component, and when HealthSystem decrements it to 0 (health.died) this system emits enemy.killed and score.add. Enemies are pooled — instead of being destroyed they ask the Pool to re-park their slot (pool.release). Optional SkeletonAnimator clips drive walk/attack/death animation (with directional fall clips and a knockback slide that barrels the corpse into the swarm), and chase-vs-wander is driven by LineOfSight's acquired/lost events.

Use it in a scene

A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.

Fastest path

bjs download scene TwinStickEnemy

Pulls this exact scene into your project and runs it — no copy-paste. Add --all to grab the components and assets it needs too.

Or build it from scratch with the bjs CLI:

  1. 1Scaffold a project
    npm create @babylonjsmarket/arcade@latest my-game

    World, renderer, and dev server — ready to run.

  2. 2Install dependencies
    cd my-game && npm install
  3. 3Add TwinStickEnemy
    bjs download TwinStickEnemy

    Copies its source into src/ so the scene resolves.

    First time? Run bjs login once.

  4. 4Paste the scene into src/scenes/arcade-room.ts and run
    npm run dev

    SceneLoader builds the world from the JSON; reload to rebuild.

JSON
{
  "Player": {
    "tags": ["player"],
    "components": {
      "MeshPrimitive": { "primitive": "capsule", "radius": 0.5, "height": 1.6, "position": [0, 0.8, 0] }
    }
  },
  "Enemy_1": {
    "tags": ["enemy"],
    "components": {
      "MeshPrimitive": {
        "primitive": "sphere", "diameter": 1.2, "position": [6, 0.6, 6],
        "material": { "diffuseColor": [0.85, 0.25, 0.35] }
      },
      "TwinStickEnemy": { "speed": 2.5, "targetTag": "player", "arenaHalfWidth": 19, "arenaHalfDepth": 19 },
      "Health": { "hp": 2, "maxHp": 2 }
    }
  }
}

Props

  • speed (number, default 2.5) — chase speed in units/sec.
  • targetTag (string, default "player") — tag of the entity to chase + damage.
  • pointsOnKill (number, default 100) — score awarded on kill.
  • separationRadius (number, default 1.0) — XZ distance below which it pushes off another enemy.
  • contactRange (number, default 1.1) — XZ distance below which it damages the target.
  • contactDamage (number, default 1) — damage per contact (paced by Health i-frames).
  • arenaHalfWidth / arenaHalfDepth (number, default 0) — confine bounds; 0 = unbounded.
  • spawnerId (string, default "") — id of the owning EnemySpawner (set on spawn).
  • walkClip / attackClip (string, default "") — animation clips (enables the animated-enemy path).
  • deathClips / fallBackClips / fallForwardClips (string[], default []) — death clips; directional lists chosen by hit angle.
  • attackRangeHysteresis (number, default 1.15) — walk↔attack dead-band multiplier.
  • deathFadeSeconds (number, default 0.6) — corpse fade time before recycling.
  • knockbackSpeed / knockbackDamping / knockbackMass (number, default 12 / 6 / 5) — death-knockback slide tuning.
  • wanderSpeed / wanderInterval (number, default 1.8 / 2.0) — no-line-of-sight wander tuning.

Events

  • emits enemy.killed, score.add (on death), health.damage (contact / punch landing), pool.release (recycle slot), and SkeletonAnimator clip events for animated enemies.
  • listens health.died, bullet.hit, lineofsight.acquired / lineofsight.lost, and skeletonAnimator.completed.

Dependencies

MeshPrimitive — the body it moves. Health — HP source; health.died triggers the kill flow. Pool — re-parks the slot instead of destroying. Score — receives score.add. Bullet — hits arrive as bullet.hit. Obstacle — barriers it's pushed out of. SkeletonAnimator — optional walk/attack/death clips. LineOfSight — optional chase-vs-wander perception.

Notes

Plain (non-animated) enemies recycle immediately on death; configuring death clips switches to the animated path (tag dying, play the clip, knockback slide, fade, then re-park). The system skips an enemy's first frame after spawn ("newborn") so it doesn't write the same mesh handle as MeshPrimitiveSystem on the spawn frame. With no LineOfSight component an enemy defaults to always-chasing. Separation is O(n²) but the live count is small.

More like this

Was this page helpful?

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

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search