Skip to content

Build mob AI with goals

The goal system separates behavior into small stateful actions. A goal decides when it can begin, when it must stop, and what it does while active. GoalSelector schedules compatible goals and resolves priority conflicts.

Mob.Initialize sets the world and calls RegisterGoals. Override that method in a Mob subclass and register each behavior with an integer priority; smaller numbers have higher priority.

protected override void RegisterGoals()
{
GoalSelector.AddGoal(0, new FloatGoal(this));
GoalSelector.AddGoal(1, new PanicGoal(this, speedModifier: 1.35f));
GoalSelector.AddGoal(5, new WaterAvoidingRandomStrollGoal(this, speedModifier: 0.75f));
GoalSelector.AddGoal(6, new LookAtPlayerGoal(this, lookDistance: 6.0f));
}

This is the same composition pattern used by CowMob. Goals can be added to TargetSelector for target acquisition; keep target selection separate from actions that move toward or attack the selected target.

The following example shows the lifecycle and control-flag pattern used by MeleeAttackGoal. Target is selected elsewhere; this goal only steers toward it.

public sealed class FollowTargetGoal : Goal
{
private readonly Mob _mob;
public FollowTargetGoal(Mob mob)
{
_mob = mob;
SetFlags(GoalFlag.Move);
}
public override bool CanUse() =>
_mob.Target is { } target && target.IsInsideTree();
public override bool CanContinueToUse() => CanUse();
public override void Stop() => _mob.MoveControl.Stop();
public override void Tick()
{
if (_mob.Target is { } target)
_mob.MoveControl.SetWantedPosition(target.GlobalPosition, 1.0f);
}
}

Move prevents another active goal from controlling movement at the same time. Add Look only if this goal must also own look control. Stop should release any control or temporary state the goal acquired.

  • CanUse checks the initial precondition.
  • Start initializes timers or captures a target when the goal begins.
  • CanContinueToUse checks whether the active action remains valid.
  • Tick requests movement, looking, jumping, or a domain action.
  • Stop clears requests and temporary flags.

Goals normally tick on the selector’s reduced schedule. Override RequiresUpdateEveryTick only when the behavior needs a per-game-tick update. Prefer controls such as MoveControl.SetWantedPosition over writing velocity from a goal; the control and travel layers own steering and movement physics.

Not every mob is on the same path. ZombieMob creates a ZombieAi state machine and ticks it from its physics callback; it does not register its behavior through Mob.RegisterGoals. Before adding AI, follow the mob’s initialization and tick entry point. Do not install both schedulers for the same action by accident.

DolphinMob shows how a goal can request travel while a specialized MoveControl owns the steering details. The goal picks a clear water corridor and starts a jump; the control handles yaw, pitch, and swimming speed during ordinary navigation.

MoveControl = new SmoothSwimmingMoveControl(
this, IsInWater,
maxTurnX: 85,
maxTurnY: 10,
inWaterSpeedModifier: 0.02f,
outsideWaterSpeedModifier: 0.1f,
applyGravity: true);
GoalSelector.AddGoal(5, new DolphinJumpGoal(this, interval: 10));

The control turns toward the requested position and converts pitch into forward and vertical inputs. DolphinJumpGoal validates water and headroom before applying a launch impulse, then requests every-tick updates to animate the arc. Keep environment checks in the goal and repeated locomotion math in the control; this makes the control reusable and the goal’s start/stop conditions easy to inspect.

Keep sensing, navigation, and combat decisions in the AI or goal layer that owns them. Keep shared locomotion in movement controls, and shared target-selection behavior in target goals.

Related: entity simulation, adding an entity, physics validation, and build and validation.