-
-
Notifications
You must be signed in to change notification settings - Fork 0
API AI Goals
NpcAPI provides a powerful AI goal system that allows NPCs to perform autonomous behaviors. Goals represent specific behaviors that an NPC can execute, such as walking to locations, attacking entities, or looking around.
import de.eisi05.npc.api.ai.Goal;
import de.eisi05.npc.api.objects.NPC;
import org.jetbrains.annotations.NotNull;
public class MyCustomGoal extends Goal {
public MyCustomGoal() {
super(Priority.MEDIUM); // Priority: ALWAYS, HIGHEST, HIGH, MEDIUM, LOW
}
@Override
protected boolean canUse(@NotNull NPC npc) {
// Check if this goal can be used right now
// Called each tick to determine if goal should be considered
return npc.getLocation().distance(target) < 50;
}
@Override
protected void start(@NotNull NPC npc) {
// Called when this goal starts executing
// Initialize any state needed for the goal
startTime = System.currentTimeMillis();
}
@Override
protected void tick(@NotNull NPC npc) {
// Called each tick while this goal is active
// Update the goal's behavior
npc.lookAt(target);
}
@Override
protected void stop(@NotNull NPC npc) {
// Called when this goal stops executing
// Clean up any state or cancel ongoing tasks
target = null;
}
@Override
protected boolean canContinue(@NotNull NPC npc) {
// Check if this goal can continue executing
// If false, goal will be stopped and new goal selected
return canUse(npc) && (System.currentTimeMillis() - startTime) < 10000;
}
@Override
protected boolean canBeInterrupted(@NotNull NPC npc) {
// Check if this goal can be interrupted by higher priority goals
// Default is false (goals cannot be interrupted)
return true;
}
}Goals use a priority-based selection system:
- ALWAYS (100): Always selected over other goals
- HIGHEST (4): Very high selection chance
- HIGH (3): High selection chance
- MEDIUM (2): Medium selection chance
- LOW (1): Low selection chance
For non-ALWAYS priorities, the system uses weighted random selection.
import de.eisi05.npc.api.ai.goals.WalkToLocationGoal;
import de.eisi05.npc.api.ai.goals.LookAtGoal;
// Add goals to NPC
npc.addGoal(new WalkToLocationGoal(targetLocation, 0.3));
npc.addGoal(new LookAtGoal(player));// Remove a goal - if currently running, it will be queued for removal
// and removed once it finishes naturally to prevent weird positions
boolean removed = npc.removeGoal(goal);
// The system uses a queue-based removal:
// - If goal is not running: removed immediately
// - If goal is running: queued for removal, removed when it finishes
// - Removal happens before new goal selectionimport de.eisi05.npc.api.ai.GoalSelector;
GoalSelector selector = npc.getGoalSelector();
// Start/stop goal evaluation
selector.start();
selector.stop();
// Check if running
boolean running = selector.isRunning();
// Set tick interval (default: 1 tick)
selector.setTickInterval(5); // Evaluate every 5 ticks
// Get current goal
Goal current = selector.getCurrentGoal();
// Force a specific goal to start
selector.forceGoal(myGoal);Makes the NPC walk to a specific location using pathfinding:
import de.eisi05.npc.api.ai.goals.WalkToLocationGoal;
Location target = new Location(world, 100, 64, 100);
WalkToLocationGoal walkGoal = new WalkToLocationGoal.Builder(target)
.speed(0.4)
.build();
// With custom settings
WalkToLocationGoal walkGoal = new WalkToLocationGoal.Builder(targetLocation)
.speed(0.4) // speed (0.1-1.0)
.maxIterations(5000) // max pathfinding iterations
.allowDiagonal(true) // allow diagonal movement
.withRotation(true) // include rotation packets
.completionCallback(result -> { // completion callback
if(result == WalkingResult.SUCCESS) {
Bukkit.broadcastMessage("NPC reached destination!");
}
})
.build();Makes the NPC look at a specific entity or location:
import de.eisi05.npc.api.ai.goals.LookAtGoal;
// Look at entity
LookAtGoal goal1 = new LookAtGoal(player);
// Look at location
LookAtGoal goal2 = new LookAtGoal(targetLocation);
npc.addGoal(goal1);Makes the NPC attack nearby entities that match a predicate. Behavior varies based on held item:
- Bow/Crossbow/Trident: Long range (15 blocks)
- Sword/Axe/Other: Short range (3 blocks)
- Only activates when target is in line of sight
- Dynamically discovers targets within range
import de.eisi05.npc.api.ai.goals.AttackEntityGoal;
// Attack all players
AttackEntityGoal attackGoal = new AttackEntityGoal(entity -> entity instanceof Player);
// Attack low health players
AttackEntityGoal attackGoal = new AttackEntityGoal(
entity -> entity instanceof Player && ((Player) entity).getHealth() < 10
);
// With custom attack range
AttackEntityGoal attackGoal = new AttackEntityGoal(
entity -> entity instanceof Monster,
15.0 // custom attack range
);Makes the NPC look around randomly. Can serve as an idle/wait behavior.
import de.eisi05.npc.api.ai.goals.LookAroundGoal;
// Default settings (1-4 seconds)
LookAroundGoal lookGoal = new LookAroundGoal();
// Custom duration
LookAroundGoal lookGoal = new LookAroundGoal(40, 200); // 2-10 secondsMakes the NPC wait/idle for a specified duration.
import de.eisi05.npc.api.ai.goals.WaitGoal;
// Wait for 5 seconds (100 ticks)
WaitGoal waitGoal = new WaitGoal(100);Makes the NPC wander randomly to nearby locations.
import de.eisi05.npc.api.ai.goals.WanderGoal;
// Default settings (10 block radius)
WanderGoal wanderGoal = new WanderGoal();
// Custom radius and speed
WanderGoal wanderGoal = new WanderGoal(15, 60, 200, 0.3);Makes the NPC follow a target entity by UUID, maintaining a specified distance.
import de.eisi05.npc.api.ai.goals.FollowEntityGoal;
// Default settings (10 block follow distance, 1.5 block stop distance)
FollowEntityGoal followGoal = new FollowEntityGoal(playerToFollow.getUniqueId());
// Custom settings
FollowEntityGoal followGoal = new FollowEntityGoal(
playerToFollow.getUniqueId(),
5.0, // follow distance
2.0, // stop distance
0.5 // speed
);// Create NPC
NPC npc = new NPC(location);
// Add goals
npc.addGoal(new WanderGoal(10));
npc.addGoal(new LookAroundGoal());
npc.addGoal(new WalkToLocationGoal.Builder(target).build());
// Start goal system (goals auto-save when added)
npc.startGoals();NPC guard = new NPC(spawnLocation);
// Equip with sword
Map<EquipmentSlot, ItemStack> equipment = new HashMap<>();
equipment.put(EquipmentSlot.HAND, new ItemStack(Material.IRON_SWORD));
guard.setOption(NpcOption.EQUIPMENT, equipment);
// Add goals - attack has ALWAYS priority, others are randomizable
guard.addGoal(new AttackEntityGoal(entity -> entity instanceof Monster));
guard.addGoal(new WanderGoal(8));
guard.addGoal(new LookAroundGoal());
guard.startGoals();NPC companion = new NPC(player.getLocation());
companion.addGoal(new FollowEntityGoal(player.getUniqueId(), 5.0, 2.0, 0.4));
companion.addGoal(new LookAroundGoal());
companion.startGoals();The goal system includes a queue-based removal mechanism to prevent NPCs from ending up in weird positions when goals are removed mid-execution.
- Immediate Removal: If a goal is not currently running, it's removed immediately
- Queued Removal: If a goal is running, it's added to a removal queue
- Natural Completion: The goal continues running until it naturally finishes
- Automatic Removal: Once the goal can't continue, it's removed from the goal list
- New Selection: The system then selects a new goal
- Prevents abrupt position changes
- Ensures goals complete their cleanup logic
- Maintains smooth NPC behavior
- No manual intervention required
// NPC is currently walking to a location
WalkToLocationGoal walkGoal = new WalkToLocationGoal(target, 0.3);
npc.addGoal(walkGoal);
// Later, you want to remove it while it's still walking
npc.removeGoal(walkGoal);
// The goal will:
// 1. Continue walking until it reaches the destination or can't continue
// 2. Then be automatically removed from the goal list
// 3. A new goal will be selected (if available)-
Goals auto-save: Goals are automatically saved when added or removed via
addGoal()andremoveGoal(). No manual saving required. -
Goals auto-start: The goal system automatically starts when an NPC is loaded from disk if it has saved goals.
-
Use predicates for target filtering: AttackEntityGoal uses predicates to dynamically discover targets within range, making it more flexible than fixed target suppliers.
-
Combine goals strategically: Use a mix of ALWAYS priority reactive goals (like AttackEntityGoal) and randomizable idle goals (like WanderGoal, LookAroundGoal).
-
Set appropriate priorities: Use ALWAYS for critical behaviors that should always execute (like combat), and use HIGH/MEDIUM/LOW for behaviors that can be randomly selected.
-
Test pathfinding: The WalkToLocationGoal uses A* pathfinding which can be CPU-intensive. Adjust
maxIterationsbased on your server's performance. -
Use Appropriate Priorities: Choose priorities that match the goal's importance
-
Handle Interruption: Override
canBeInterrupted()for goals that can pause safely -
Clean Resources: Always clean up in
stop()method -
Check Conditions: Use
canUse()andcanContinue()for proper goal lifecycle -
Avoid Long Operations: Keep
tick()method lightweight for performance
- The AStarPathfinder is suitable for constant movement, but for very frequent pathfinding, consider caching paths or using simpler movement for short distances.
- Goals run on the main server thread, so avoid heavy computations in
canUse()ortick(). - Goals implement
Serializableand are saved with the NPC. Transient fields (like entity references) are handled via custom serialization. - The goal selector runs every tick by default. Adjust with
npc.getGoalSelector().setTickInterval(ticks)(internal API). -
getGoalSelector()is now private - use the public goal management methods (addGoal,removeGoal,startGoals,stopGoals).
- Check
canUse()returns true - Verify goal priority isn't being overridden
- Ensure GoalSelector is started
- Check
canContinue()implementation - Verify goal isn't marked as non-interruptable when it should be
- Look for exceptions in goal methods
- Limit expensive operations in
canUse()andtick() - Use appropriate tick intervals
- Consider goal priorities to reduce frequent switching