-
-
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;
}
@Override
protected boolean canBeRemovedNow(@NotNull NPC npc) {
// Check if this goal can be removed immediately without being queued
// Default implementation returns !canContinue(npc)
// Override this for goals that need special removal logic
return !canContinue(npc);
}
}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.
-
Removal Check: When
removeGoal()is called, the system checkscanBeRemovedNow() -
Immediate Removal: If
canBeRemovedNow()returns true, the goal is removed immediately -
Queued Removal: If
canBeRemovedNow()returns false, the goal is added to a removal queue - Continued Execution: The goal continues running until it can be safely removed
-
Automatic Removal: Once
canBeRemovedNow()returns true or the goal naturally finishes, it's removed - New Selection: The system then selects a new goal
The canBeRemovedNow(@NotNull NPC npc) method determines if a goal can be removed immediately:
@Override
protected boolean canBeRemovedNow(@NotNull NPC npc) {
// Default implementation - can be removed if goal can't continue
return !canContinue(npc);
}- AttackEntityGoal: Cannot be removed if NPC is floating during movement
- WalkToLocationGoal: Cannot be removed if NPC is floating while walking
- FollowEntityGoal: Cannot be removed if NPC is floating during movement
- Other goals: Use default behavior (can be removed when can't continue)
- Prevents NPCs from being removed while in unstable positions (like floating)
- Ensures smooth transitions between goals
- Maintains realistic NPC behavior
- Automatic handling of edge cases
// 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. Check if it can be removed immediately (canBeRemovedNow())
// 2. If NPC is floating during movement, it will be queued for removal
// 3. Continue walking until NPC has solid ground beneath it
// 4. Then be automatically removed from the goal list
// 5. 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 -
Override canBeRemovedNow: For movement-based goals, check if NPC is in stable position before allowing removal
- 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