Skip to content

Getting Started

Alexander Parent edited this page Oct 22, 2026 · 2 revisions

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(EntityTargets.supportedPosition(player));   // fine every tick
    controller.tick();
}

EntityTargets.supportedPosition anchors a target to the block it stands on, so a player sneaking over an edge is chased onto the edge rather than into the air beside it.

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.

targetUnreachable() is only proof when routeStop() reads FRONTIER_EXHAUSTED. Chunks that are not loaded count as solid, so a mob heading into them stops at the edge with routeStop() reading TERRAIN_UNLOADED: call moveTo again once they load.

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.controllerBuilder(mob)
        .movementPerTick(0.18)                // blocks per tick
        .build();

The builder also takes a fixed profile(...), an executionMode(...), and the mob AI's own startNodeRandom(...).

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