logo

Babylon.js Market

BallPossession

gameplay

BallPossession

1
Unlock

Install with the CLI:

bjs download BallPossession

BallPossession

Tracks which player or AI "has" the ball and broadcasts the handoffs.

What it does

Drops onto a ball entity and answers one question every frame: who owns the ball right now? The BallPossessionSystem reads the ball's world position plus every candidate's world position through the renderer adapter, then hands that snapshot to a pure core that awards ownership to the closest candidate inside possessionDistance. A candidate is any entity carrying one of the configured candidateTags (default ['player', 'ai']) and a MeshPrimitive handle. When nobody is in range the current possessor keeps the ball for possessionTimeout seconds — a grace window so a one-frame position stutter can't flap the owner — before ownership drops to null. The System writes nothing back to the scene; it reports transitions on the EventBus. It emits possession.changed the frame a new candidate takes the ball and possession.lost the frame ownership times out, so scoring, HUD, animation, and audio wire in through events instead of reading the component.

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 BallPossession

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

    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.4,
        "position": [0, 0.4, 0]
      },
      "BallPossession": {
        "possessionDistance": 1.5,
        "possessionTimeout": 0.5,
        "candidateTags": ["player", "ai"]
      }
    }
  }
}

Props

  • possessionDistance (number, default 1.5) — world-unit radius inside which a candidate can claim the ball.
  • possessionTimeout (number, default 0.5) — seconds the current possessor keeps the ball after everyone leaves range, before ownership drops to null.
  • candidateTags (string[], default ['player', 'ai']) — tags the System scans for when building the candidate list each frame; a matching entity must also carry a MeshPrimitive to be considered.

Events

  • Emits possession.changed (BallPossessionEvents.CHANGED) the frame a new candidate takes the ball — { entityId, previousEntityId, ballEntityId } (entityId is the new owner).
  • Emits possession.lost (BallPossessionEvents.LOST) the frame ownership times out to null{ entityId, ballEntityId } (entityId is the candidate that just lost it).
  • Listens for possession.reset (BallPossessionInputEvents.RESET) — { entityId? }; clears possessor and timers on every BallPossession entity, or only the matching ball when entityId is set.
  • Listens for meshprimitive.created{ entityId, handle }; caches mesh handles to read ball and candidate positions.

Dependencies

MeshPrimitive — the ball needs one so the System has a handle to sample the ball's world position, and each candidate needs one too (a tag alone is not enough to be considered).

Notes

  • The System sweeps existing entities on initialize, so it picks up MeshPrimitive handles that were emitted before its listener attached — candidates and the ball can be authored in any order.
  • Candidates are matched by tag, not by query, so any entity you tag (player, ai, or your own custom tag) joins the contest the frame it gains a mesh handle.
  • A frame with no ballPosition holds the current owner until the timeout rather than dropping it immediately — a missing ball reads as "out of range," not "gone."
  • possession.changed carries previousEntityId, so a steal (one candidate taking the ball from another) and a fresh pickup (previousEntityId is null) are distinguishable from the same event.

More like this

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

Was this page helpful?

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

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search