logo

Babylon.js Market

BallPursuit

ai

BallPursuit

Talks toMovement
1
Pride-Leader SweeperBonus figurePride-Leader Sweeper
Unlock

Install with the CLI:

bjs download BallPursuit

BallPursuit

Keeps a team of AI players from dogpiling the ball: only the nearest few chase, the rest hold station.

What it does

BallPursuitSystem runs every entity carrying a BallPursuitComponent. Each frame it groups the matched entities by TEAM — the suffix of a member's first team: tag (team:RedRed); an untagged entity is its own one-member group — then reads each member's and the ball's world position through the renderer adapter (a sibling MeshPrimitive handle → getMeshWorldPosition, exactly the way AIInput and AIGoalSeek read positions). The pure core (BallPursuit.core) ranks a team's members by squared XZ distance to the ball and marks the chaseCount nearest as chasers; everyone else holds. For each member the System emits the SAME movement input a keyboard or AIInput would — input.movement.vector (MovementInputEvents.SET_MOVEMENT_VECTOR, never hardcoded) — a unit vector toward the ball for the chasers, {x:0,z:0} for the holders. Movement (physics-velocity mode) executes the intent, and AIZone — kept on each holder — tethers it back to its third of the pitch. The vector is re-emitted only when it changes (consumers latch the last value), and ballPursuit.decisionChanged fires for debuggers the frame a member's chase decision flips.

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.

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 BallPursuit
    bjs download BallPursuit

    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
{
  "Ball": {
    "tags": ["ball"],
    "components": {
      "MeshPrimitive": { "primitive": "sphere", "diameter": 0.7, "position": [0, 0.5, 0] }
    }
  },
  "RedMidL": {
    "tags": ["ai", "team:Red"],
    "components": {
      "MeshPrimitive": { "primitive": "capsule", "radius": 0.5, "height": 2, "position": [-4, 1, 4] },
      "Movement": { "speed": 5, "groundY": 0 },
      "BallPursuit": { "targetEntity": "Ball", "chaseCount": 1 },
      "AIZone": { "center": [-4, 0, 4], "radius": 9, "returnSpeed": 5 }
    }
  },
  "RedMidR": {
    "tags": ["ai", "team:Red"],
    "components": {
      "MeshPrimitive": { "primitive": "capsule", "radius": 0.5, "height": 2, "position": [-4, 1, -4] },
      "Movement": { "speed": 5, "groundY": 0 },
      "BallPursuit": { "targetEntity": "Ball", "chaseCount": 1 },
      "AIZone": { "center": [-4, 0, -4], "radius": 9, "returnSpeed": 5 }
    }
  }
}

Props

  • targetEntity (string, default "Ball") — id of the ball entity every team member chases; the System looks up its mesh world position each frame.
  • chaseCount (number, default 1) — how many of a team's nearest members may chase at once; the rest hold. Floored and clamped to >= 0; a value past the member count just means everyone chases.

Events

  • emits input.movement.vector (MovementInputEvents.SET_MOVEMENT_VECTOR) — { entityId, x, y, z } per member: a unit vector toward the ball (y always 0) for the chasers, x:0, z:0 for the holders. Re-emitted only when the vector changes. This is the exact event AIInput and the keyboard emit, so Movement can't tell them apart.
  • emits ballPursuit.decisionChanged (BallPursuitEvents.DECISION_CHANGED) — { entityId, team, chasing, moveX, moveZ } the frame a member's chase decision flips. Debug/viz only; no gameplay system needs it.

BallPursuit consumes no events — it is a pure decision producer.

Dependencies

MeshPrimitive — supplies the mesh handles the System reads world positions from (each member's and the ball's). A member whose mesh handle isn't created yet is skipped that frame.

Movement — the consumer that turns the emitted input.movement.vector into motion; run it in physics-velocity mode so the vector drives the body.

AIZone — keep it on each member so the holders (and chasers that leave their third) are tethered back to their zone instead of drifting toward the ball.

Notes

  • The team's targetEntity and chaseCount are read from the first grouped member's component each frame, so set them consistently across a team.
  • Ranking uses squared XZ distance — the pitch is treated as flat, Y is ignored. Ties break by id lexicographically, so the same member always wins a tie: the decision is deterministic and independent of entity order.
  • A chaser standing on the ball (distance under 1e-6) gets [0, 0] — there's no direction to move.
  • An untagged entity is its own one-member group keyed by its id, so at chaseCount 1 a solo AI always chases — team tags only matter when two or more members share one.
  • With chaseCount 0, no member of that team ever chases; every member is emitted {x:0,z:0} and left to its AIZone.

More like this

AIZone
MeshPrimitive
Movement
AIGoalSeek
AIKick
AiPilot
BallPossession
BallReset
Enemy
EnemySpawner
FreighterChain
Goal
LineOfSight
PlayerInputBridge
PokerAI
PokerAIBill
PokerAICali
PokerAIRandy
PokerAIShark
PokerAISurge
PokerAITilt
Scoreboard
SoccerDirector
WaveDirector

Was this page helpful?

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

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search