Skip to content

Getting Started

Alexander Parent edited this page Jul 31, 2026 · 1 revision

Getting Started

One NavigationSystem per server, one controller per mob, and you call tick().

NavigationSystem navigation = NavigationSystem.create();   // owns the workers

EntityNavigationController controller = navigation.controller(mob);
controller.moveTo(destination);

controller.tick();      // every tick, from the entity/instance tick thread

controller.close();     // when the mob despawns
navigation.close();     // at shutdown

A mob that never moves is almost always a controller that is never ticked. Nothing ticks it for you.

moveTo is cheap enough to call every tick. The mob keeps walking its current route until the replacement lands, so a chased target never stands still.

public void update(long time) {
    controller.moveTo(player.getPosition());   // fine every tick
    controller.tick();
}

Knowing what it is doing

switch (controller.state()) {
    case COMPUTING, FOLLOWING -> {}          // working on it
    case COMPLETED -> onArrived(mob);
    case PARTIAL -> {                        // ran out short of the goal
        if (controller.targetUnreachable()) chooseAnotherGoal(mob);
    }
    case STUCK -> unwedge(mob);              // cannot walk the route it has
    case IDLE, FAILED, CANCELLED -> {}
}

PARTIAL and STUCK are different problems:

  • PARTIAL + targetUnreachable() — no further search reaches that goal. Pick a new one.
  • STUCK — the follower could not walk a route it was given. Unwedge the mob.

A follower that reported targetUnreachable() is never later relabelled STUCK.

Polling like this works, but you have to remember what you saw last tick, because COMPLETED persists. Reacting to Navigation does it for you.

Choosing a speed

navigation.controller(mob, 0.18);            // blocks per tick

Or set the default for every controller the system makes:

NavigationSystem navigation = NavigationSystem.builder()
        .movementPerTick(0.2)
        .parallelism(8)
        .queueCapacity(256)
        .build();

NavigationOptions options = navigation.options();   // reads it all back

Defaults are sized for a normal server: one controller search per worker, with newest-per-mob intents held in a bounded scheduler so a crowd never becomes an executor queue. See Running at Scale before changing them.

Next

Clone this wiki locally