Your first AI
This builds a rig that stands still until it sees a player, chases them, and gives up when it
loses them. It uses a Humanoid rig with a PrimaryPart, the same as any NPC.
1. Write the brain
A brain is a table: an Initial state name and a States table. Each state can have Enter,
Update, Exit and Transitions, all optional.
local Stalker = {
Initial = "Idle",
States = {
Idle = {
Enter = function(agent)
agent:Stop()
end,
Transitions = {
{ To = "Chase", When = function(agent) return agent.Blackboard.SeenTarget ~= nil end },
},
},
Chase = {
Transitions = {
{ To = "Idle", When = function(agent) return agent.Blackboard.SeenPosition == nil end },
},
Update = function(agent)
agent:MoveTo(agent.Blackboard.SeenPosition)
end,
},
},
}
Every tick the runner checks the current state's transitions in order and takes the first whose
When is true. If none fires, Update runs; it may return the name of the next state, or nil
to stay. (Under --!strict with the brain annotated as VluxyAI.Brain, write return nil
explicitly: the checker wants every path of Update to return a string?.)
SeenTarget and SeenPosition are written by the Sight sense; the brain never
raycasts.
2. Stack the parts
local VluxyAI = require(ReplicatedStorage.Packages.VluxyAI)
local agent = VluxyAI.new(rig)
:UseBrain(Stalker)
:UsePathfinder(VluxyAI.Pathfinders.Navmesh.new({ AgentRadius = 2, AgentHeight = 5 }))
:UseDirect(VluxyAI.Pathfinders.Straight.new({ AgentRadius = 2, AgentHeight = 5 }))
:UseMover(VluxyAI.Movers.Humanoid.new(rig, { Speed = 14 }))
:AddSense(VluxyAI.Senses.Sight.new({ Range = 40, FieldOfView = 120, Memory = 3 }))
:Build()
:Start()
UseDirect is optional: with it, the agent walks straight at a target it has a clear line to
and only asks the navmesh when it has not, or when walking straight got it stuck. A chase then
reacts to every step the player takes instead of every re-plan.
Build() validates everything and errors with the path of the first bad field. Start() enters
the initial state and begins ticking on Heartbeat, every 0.1 seconds by default
(Builder:SetTick changes it).
The same stack, with the pathfinder, the humanoid mover, Sight and Proximity already chosen:
local agent = VluxyAI.Presets.Humanoid(rig):UseBrain(Stalker):Build():Start()
3. Move
agent:MoveTo(position) is safe to call every tick with a moving target. The Navigator
behind it plans once, re-plans when the target moves more than a couple of studs, retries when the
mover gets stuck, and never plans more often than its RepathInterval. Ask it how things are going
with agent:GetPathStatus() (Idle, Planning, Moving, Arrived, Failed) or
agent:HasArrived().
A path to a target that stays put is walked to its end. When the level changes under the agent,
the Navmesh pathfinder's Blocked signal makes the navigator re-plan on its next
cadence tick, and MaxPathAge in Builder:SetNavigation plans again
every so many seconds regardless, for the games where a fresh path is worth one ComputeAsync:
:SetNavigation({ MaxPathAge = 2 })
The brain above is written by hand. When and States shorten the
common parts, and Interrupts on the brain hold the transitions that apply from every state;
Building a horror enemy shows the whole loop in twenty lines.
4. Take it apart
agent:Destroy()
Exits the current state, stops the mover, and destroys the navigator, every sense, the mover and the pathfinder. The rig is yours; the agent never destroys it.
Where to put things
agent.Blackboardis what the AI knows. Senses write it; states read it.agent.Datais yours: the current patrol index, the chosen target. Set initial values with Builder:SetData.- Waiting is a timer:
agent:StartTimer("Attack", 1)inEnter,agent:TimerDone("Attack")in a transition. - Acting on a target goes through Perception:
VluxyAI.Perception.HumanoidOf(agent.Blackboard.SeenTarget). - Anything that needs the world (a raycast, a distance to a player) belongs in a sense, not a state. If none of the built-in senses fits, write one.