Claude Skill

roblox-npc-ai

Use when creating Roblox NPCs or enemies with pathfinding, state machines, line-of-sight or FOV detection, spawns, or AI update loops.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tabooharmony-roblox-brain-skills_gameplay_roblox-npc-ai-6051c35.zip · 9 KB
Part of tabooharmony/roblox-brain — 29 skills

Install

skills CLI npx skills add https://github.com/TabooHarmony/roblox-brain/tree/main/skills/gameplay/roblox-npc-ai
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tabooharmony-roblox-brain@llmmart
Git git clone https://github.com/TabooHarmony/roblox-brain.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tabooharmony/roblox-brain collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Roblox NPC & AI

When to Load

Load for NPCs/enemies: pathfinding, state machines, LOS/FOV detection, spawns, AI loops, or physics ownership.

Quick Reference

PathfindingService

local path = PathfindingService:CreatePath({
    AgentRadius = 2, AgentHeight = 5, AgentCanJump = true,
    Costs = { Water = 20, DangerZone = math.huge },
})
path:ComputeAsync(npcPos, targetPos)
if path.Status ~= Enum.PathStatus.Success then return end
for _, wp in path:GetWaypoints() do
    if wp.Action == Enum.PathWaypointAction.Jump then humanoid.Jump = true end
    humanoid:MoveTo(wp.Position)
    if not humanoid.MoveToFinished:Wait() then return end
end
  • Recompute on blocked with a bounded retry or cancellation policy.
  • In Studio, test Workspace.PathfindingUseImprovedSearch on representative maps before rollout; it is not scriptable.
  • Avoid long requests and repeated recomputation. Moving collidable geometry can trigger navigation-mesh work.
  • Region modifiers: anchored part + PathfindingModifier Label → Costs
  • PassThrough = true for doors; PathfindingLink for disconnected navmesh
  • math.huge cost = non-traversable

State Machine

idle → patrol → chase → attack → flee → dead

  • idle/patrol → chase (player detected) → attack (in range) → idle (lost)
  • any → flee (health low) → idle (safe); any → dead (health ≤ 0)
  • Transitions own movement changes, target cleanup, and cancellation

Detection (distance → FOV → LOS)

  1. Distance (a-b).Magnitude: cheapest, always first
  2. FOV forward:Dot(toTarget) cosine; use a configured cone for your game, not a universal angle
  3. LOS workspace:Raycast: expensive, last
  • If the design includes hearing or proximity detection, make it a separate configured signal rather than a universal FOV bypass.

Network Ownership

  • Classic projects may use SetNetworkOwner(nil) for physics-sensitive NPCs, but it is not complete security. In Server Authority projects, configure the authority model instead of treating network ownership as the security boundary.

Update Loop

  • Throttle and stagger AI based on NPC count, path cost, and profiler evidence, not a universal tick rate. Keep NPC decisions and movement server-side; client code may handle presentation.

For spawners, lifecycle cleanup, timeout handling, and performance budgets, load references/full.md.

Files (roblox-brain)
  • references
    • full.md 19.7 KB
      # Roblox NPC & AI: Full Reference
      
      
      > **Code in this reference is illustrative. Adapt to your game and verify in Studio before production use.**
      
      ## PathfindingService
      
      ### CreatePath Parameters
      
      ```luau
      local PathfindingService = game:GetService("PathfindingService")
      
      local path = PathfindingService:CreatePath({
          AgentRadius = 2,       -- half-width of the NPC (default 2)
          AgentHeight = 5,       -- height of the NPC (default 5)
          AgentCanJump = true,   -- can the agent jump over gaps?
          AgentCanClimb = true,  -- can the agent climb TrussParts?
          WaypointSpacing = 4,   -- studs between waypoints (default 4)
          Costs = {              -- material/region traversal costs
              Water = 20,        -- avoid water (20x more expensive)
              CrackedLava = math.huge, -- never traverse lava
          },
      })
      ```
      
      All materials have a default cost of 1. Set `math.huge` to make a material completely non-traversable.
      
      ### Computing and Following a Path
      
      From the official Roblox pathfinding docs:
      
      ```luau
      local PathfindingService = game:GetService("PathfindingService")
      
      local path = PathfindingService:CreatePath({
          AgentCanClimb = true,
          Costs = { Water = 20 },
      })
      
      local character = script.Parent
      local humanoid = character:WaitForChild("Humanoid")
      
      local function followPath(destination: Vector3)
          path:ComputeAsync(humanoid.RootPart.Position, destination)
      
          if path.Status ~= Enum.PathStatus.Success then
              warn("Path failed:", path.Status)
              return false
          end
      
          local waypoints = path:GetWaypoints()
      
          -- Mark the traversal stale. Let the caller retry with a bounded policy.
          local blocked = false
          local blockedConnection = path.Blocked:Connect(function()
              blocked = true
          end)
      
          local completed = true
          for _, waypoint in waypoints do
              if blocked then
                  completed = false
                  break
              end
      
              if waypoint.Action == Enum.PathWaypointAction.Jump then
                  humanoid.Jump = true
              elseif waypoint.Action == Enum.PathWaypointAction.Custom then
                  -- Custom action (e.g. open door, use ladder)
                  -- Handle based on waypoint.Label
              end
      
              humanoid:MoveTo(waypoint.Position)
              local reached = humanoid.MoveToFinished:Wait()
      
              if blocked or not reached then
                  completed = false
                  break
              end
          end
      
          blockedConnection:Disconnect()
          return completed
      end
      ```
      
      If this returns `false`, the caller should recompute from the NPC's current position with a bounded retry or cancellation policy. Do not recursively call `followPath` from `Path.Blocked`; a persistently blocked route can otherwise create overlapping traversals and unbounded work.
      
      ### Pathfinding Modifiers
      
      Control how the pathfinder treats specific regions:
      
      **Material costs**: make certain terrain materials expensive:
      ```luau
      local path = PathfindingService:CreatePath({
          Costs = {
              Water = 20,         -- 20x more expensive than default
              CrackedLava = 100,  -- nearly impassable
              Slate = 20,         -- avoid slate areas
          },
      })
      ```
      
      **Region modifiers**: mark arbitrary zones as costly or impassable:
      1. Create an Anchored, CanCollide=false Part around the region
      2. Add a `PathfindingModifier` child with a `Label` (e.g. "DangerZone")
      3. Reference the label in Costs:
      
      ```luau
      local path = PathfindingService:CreatePath({
          Costs = {
              DangerZone = math.huge, -- completely avoid this region
          },
      })
      ```
      
      **PassThrough**: pathfind through solid obstacles (e.g. doors):
      1. Create an Anchored, CanCollide=false Part around the door
      2. Add a `PathfindingModifier` with `PassThrough = true`
      3. The path will route through the door as if it's open
      
      **PathfindingLink**: connect disconnected navmesh areas:
      Use `PathfindingLink` to tell the pathfinder about custom traversal (teleporters, ziplines, boats). Set a Label and handle it in the waypoint loop via `waypoint.Action == Enum.PathWaypointAction.Custom`.
      
      ### Navigation Mesh
      
      The navigation mesh is auto-generated from geometry. Debug it in Studio:
      - View → Visualization Options → Navigation Mesh (shows walkable areas)
      - View → Visualization Options → Pathfinding Modifiers (shows labels)
      
      Colored areas = walkable. Small arrows = jump connections. Uncolored = impassable.
      
      ### Custom navmesh alternatives to PathfindingService
      
      PathfindingService auto-generates its navmesh from geometry server-side. It is robust but black-box: no control over walkable geometry or generation. For full control, community modules port real navmesh generators to Luau (see the Navcast thread as a lead, https://devforum.roblox.com/t/navcast-an-attempt-to-port-recast-to-luau/4743538). Such ports are not game-ready: voxelization is far too slow for live or changing maps, so tile generation with yields between chunks is required. Use them as a reference point when PathfindingService's constraints don't fit your geometry or agent needs, not as a drop-in replacement.
      
      <!-- temporal: 2026-07 -->
      
      ## Pathfinding performance and improved search
      
      Roblox has shipped an improved pathfinding search algorithm behind the Studio-only, non-scriptable `Workspace.PathfindingUseImprovedSearch` setting. Test it against representative maps and NPC agent parameters before enabling it in a live place. A path that succeeds is not automatically a cheap path.
      
      Keep path requests bounded and event-driven:
      
      - do not compute every NPC path every frame;
      - stagger requests across frames and cache a path until the target or world meaningfully changes;
      - avoid unnecessarily long searches and choose a destination budget appropriate for the map;
      - avoid continuously moving `CanCollide` geometry when it is not needed, because navigation-mesh regeneration can become the bottleneck;
      - capture a MicroProfiler trace when pathfinding slows down instead of assuming the state machine is at fault.
      
      If `Path.Blocked` fires, recompute from the NPC's current position with a bounded retry policy. Do not allow a blocked-path callback to recursively create unlimited overlapping `ComputeAsync` calls.
      
      ## State Machine Pattern
      
      The most reliable NPC AI architecture. Each NPC has a current state and transitions based on conditions.
      
      ```luau
      --!strict
      type State = "idle" | "patrol" | "chase" | "attack" | "flee" | "dead"
      
      type NPCConfig = {
          detectionRange: number,
          attackRange: number,
          fleeHealthPercent: number,
          patrolPoints: {Vector3},
          attackCooldown: number,
          walkSpeed: number,
          runSpeed: number,
          fovDegrees: number,
          hearingEnabled: boolean,
          hearingRange: number,
      }
      
      type NPCState = {
          current: State,
          target: Player?,
          patrolIndex: number,
          lastAttackTime: number,
          humanoid: Humanoid,
          rootPart: BasePart,
          config: NPCConfig,
      }
      
      local function transition(npc: NPCState, newState: State)
          -- Exit current state
          if npc.current == "chase" or npc.current == "patrol" then
              npc.humanoid.WalkSpeed = npc.config.walkSpeed
          end
      
          -- Enter new state
          npc.current = newState
      
          if newState == "chase" then
              npc.humanoid.WalkSpeed = npc.config.runSpeed
          elseif newState == "idle" then
              npc.target = nil
          end
      end
      ```
      
      ### State Transitions
      
      ```
      idle → patrol (has patrol points)
      idle → chase (player detected)
      patrol → chase (player detected)
      chase → attack (in attack range)
      chase → idle (target lost/died)
      chase → flee (health low)
      attack → chase (target moved out of range)
      attack → flee (health low)
      flee → idle (safe distance reached)
      any → dead (health <= 0)
      ```
      
      ## Detection
      
      ### Distance Check (cheapest, do first)
      
      ```luau
      local Players = game:GetService("Players")
      
      local function findNearestPlayer(position: Vector3, range: number): Player?
          local nearest: Player? = nil
          local nearestDist = range
      
          for _, player in Players:GetPlayers() do
              local char = player.Character
              if not char then continue end
              local root = char:FindFirstChild("HumanoidRootPart")
              if not root then continue end
              local humanoid = char:FindFirstChildOfClass("Humanoid")
              if not humanoid or humanoid.Health <= 0 then continue end
      
              local dist = (root.Position - position).Magnitude
              if dist < nearestDist then
                  nearest = player
                  nearestDist = dist
              end
          end
      
          return nearest
      end
      ```
      
      ### Line of Sight (raycast)
      
      Only check LOS after distance check passes (raycasts are expensive):
      
      ```luau
      local function hasLineOfSight(from: Vector3, to: Vector3, ignore: {Instance}, target: Instance?): boolean
          local direction = to - from
          local params = RaycastParams.new()
          params.FilterDescendantsInstances = ignore
          params.FilterType = Enum.RaycastFilterType.Exclude
      
          local result = workspace:Raycast(from, direction, params)
          -- Nothing between the endpoints: sight is clear.
          if result == nil then return true end
          -- The segment can clip the target's own queryable parts (aiming at the
          -- head grazes a hat or hand). A hit inside the target means the ray
          -- REACHED it: that is sight, not an obstruction. Only a hit on some
          -- other instance (a wall) blocks.
          if target and result.Instance:IsDescendantOf(target) then
              return true
          end
          return false
      end
      
      -- Usage: check if NPC can see player. Exclude the NPC's own character but
      -- NOT the target's: pass the target as the last argument instead.
      local canSee = hasLineOfSight(
          npc.rootPart.Position + Vector3.new(0, 2, 0), -- eye height
          targetRoot.Position + Vector3.new(0, 2, 0),   -- target eye height
          {npc.rootPart.Parent},                        -- ignore the NPC itself
          targetRoot.Parent                             -- hits within the target are visibility
      )
      ```
      
      ### Field of View (cone check)
      
      ```luau
      local function isInFOV(npcCFrame: CFrame, targetPos: Vector3, fovDegrees: number): boolean
          local toTarget = (targetPos - npcCFrame.Position).Unit
          local forward = npcCFrame.LookVector
          local dot = forward:Dot(toTarget)
          local angle = math.acos(math.clamp(dot, -1, 1))
          return angle <= math.rad(fovDegrees / 2)
      end
      ```
      
      ### Combined Detection (distance → FOV → LOS)
      
      ```luau
      local function canDetectPlayer(npc: NPCState, player: Player): boolean
          local char = player.Character
          if not char then return false end
          local root = char:FindFirstChild("HumanoidRootPart")
          if not root then return false end
      
          -- 1. Distance (cheapest check first)
          local dist = (root.Position - npc.rootPart.Position).Magnitude
          if dist > npc.config.detectionRange then return false end
      
          -- 2. Field of view (medium cost)
          if not isInFOV(npc.rootPart.CFrame, root.Position, npc.config.fovDegrees) then
              if not npc.config.hearingEnabled or dist > npc.config.hearingRange then
                  return false
              end
          end
      
          -- 3. Line of sight (expensive, do last); pass the target so hits on
          -- the player's own parts read as visibility, not obstruction
          return hasLineOfSight(
              npc.rootPart.Position + Vector3.new(0, 2, 0),
              root.Position + Vector3.new(0, 2, 0),
              {npc.rootPart.Parent},
              root.Parent
          )
      end
      ```
      
      ## Spawn Systems
      
      ### Basic Spawner with Cap
      
      ```luau
      local MAX_ENEMIES = 10
      local SPAWN_INTERVAL = 5
      local activeEnemies: {Model} = {}
      
      local function spawnEnemy(spawnPoint: BasePart): Model?
          if #activeEnemies >= MAX_ENEMIES then return nil end
      
          local enemy = ServerStorage.EnemyTemplate:Clone()
          enemy:PivotTo(spawnPoint.CFrame + Vector3.new(0, 3, 0))
          enemy.Parent = workspace.Enemies
          table.insert(activeEnemies, enemy)
      
          -- Track death
          local humanoid = enemy:FindFirstChildOfClass("Humanoid")
          if humanoid then
              humanoid.Died:Once(function()
                  local idx = table.find(activeEnemies, enemy)
                  if idx then table.remove(activeEnemies, idx) end
                  task.delay(3, function() enemy:Destroy() end) -- cleanup after death anim
              end)
          end
      
          return enemy
      end
      
      -- Spawn loop
      task.spawn(function()
          while true do
              task.wait(SPAWN_INTERVAL)
              local spawnPoints = workspace.SpawnPoints:GetChildren()
              local point = spawnPoints[math.random(1, #spawnPoints)]
              spawnEnemy(point)
          end
      end)
      ```
      
      ### Wave System
      
      ```luau
      type WaveConfig = {
          enemies: {{template: string, count: number}},
          spawnDelay: number, -- seconds between individual spawns
          waveDelay: number,  -- seconds between waves
      }
      
      local waves: {WaveConfig} = {
          {enemies = {{template = "Zombie", count = 5}}, spawnDelay = 1, waveDelay = 10},
          {enemies = {{template = "Zombie", count = 8}, {template = "FastZombie", count = 3}}, spawnDelay = 0.8, waveDelay = 15},
      }
      
      local function spawnWave(wave: WaveConfig, spawnPoints: {BasePart})
          for _, group in wave.enemies do
              for i = 1, group.count do
                  local point = spawnPoints[math.random(1, #spawnPoints)]
                  local template = ServerStorage.Enemies:FindFirstChild(group.template)
                  if template then
                      local enemy = template:Clone()
                      enemy:PivotTo(point.CFrame + Vector3.new(0, 3, 0))
                      enemy.Parent = workspace.Enemies
                  end
                  task.wait(wave.spawnDelay)
              end
          end
      end
      ```
      
      ## Network Ownership and Server Authority
      
      For classic replication, decide deliberately whether an unanchored NPC assembly should be server-owned. Server ownership can prevent client-owned physics from producing unexpected motion, but it is not a complete anti-exploit boundary and it costs server simulation work.
      
      ```luau
      local function setServerOwned(model: Model)
          for _, part in model:GetDescendants() do
              if part:IsA("BasePart") then
                  part:SetNetworkOwner(nil)
              end
          end
      end
      
      setServerOwned(enemy)
      ```
      
      In a Server Authority project, configure `Workspace.AuthorityMode = Server` and its required replication, fixed-simulation, streaming, and input settings. Server-owned gameplay physics can remain responsive through client prediction, so `SetNetworkOwner(nil)` is not the authority model and the classic secure-but-laggy trade-off does not apply in the same way.
      
      Measure CPU, pathfinding, and replication cost at the intended NPC count. If visual motion becomes too coarse in a classic project, replicate authoritative state and interpolate on the client instead of handing gameplay authority to the client.
      
      ## Update Loop
      
      Don't run AI logic every frame. Use a configured interval based on the behavior's responsiveness and profile it under the intended NPC count:
      
      ```luau
      local AI_TICK_RATE = 1/10 -- example only; tune and measure for the project
      local accumulated = 0
      
      RunService.Heartbeat:Connect(function(dt)
          accumulated += dt
          if accumulated < AI_TICK_RATE then return end
          accumulated -= AI_TICK_RATE
      
          for _, npc in activeNPCs do
              updateNPC(npc)
          end
      end)
      ```
      
      For large NPC counts, keep a rotating work cursor or queue so not all NPCs think on the same frame. Make the per-tick budget a measured project setting.
      
      ## Common Mistakes
      
      - **Client-side NPC logic**: ALL NPC behavior must run on the server. Client only handles animations/visuals.
      - **No path blocked handling**: Paths go stale when the world changes. Always connect `path.Blocked` and recompute with bounded retries.
      - **ComputeAsync with no fallback**: If path computation fails (`Status ~= Success`), don't freeze. Fall back to direct movement or idle.
      - **Tight detection loops**: Don't check every NPC against every player every frame. Distance checks are O(n*m); throttle and batch based on profiler evidence.
      - **Treating network ownership as security**: In classic projects, choose ownership deliberately. In Server Authority projects, configure the authority model; `SetNetworkOwner(nil)` alone is not the security boundary.
      - **Not cleaning up dead NPCs**: Destroy models after death animation. Corpses accumulate and kill performance.
      - **MoveTo timeout**: `Humanoid:MoveTo()` has an 8-second timeout. If the NPC gets stuck, `MoveToFinished` fires with `reached = false`. Handle it.
      - **Pathfinding on the client**: PathfindingService works on both client and server, but NPC movement must be server-authoritative. Compute paths on the server.
      - **No stagger for large NPC counts**: A large batch of path requests on one frame can spike the server. Stagger updates and measure the actual budget.
      
      ## Community ecosystem (leads, not sources)
      
      - [SimplePath](https://devforum.roblox.com/t/simplepath-pathfinding-module/1196762) (894 likes): the PathfindingService wrapper standard; [How 2 Make A* Pathfinding](https://devforum.roblox.com/t/how-2-make-a-pathfinding/2714504) for custom grids.
      - [BehaviorTrees3 + visual editor](https://devforum.roblox.com/t/behaviortrees3-btrees-visual-editor-v30/836158): the BT reference implementation.
      - [Simulating thousands of moving NPCs performantly](https://devforum.roblox.com/t/simulating-thousands-of-moving-npcs-with-humanoidsphysics-performantly/4603494) (2026): current humanoid-scale density canon.
      ## Avatar appearance: HumanoidDescription, BodyColors, Shirt, Pants
      
      ### HumanoidDescription: the canonical runtime appearance API
      
      - Change an NPC's appearance at runtime with `Humanoid:ApplyDescription(description)`; read current appearance with `Humanoid:GetDescription()`. ApplyDescription batch-applies every field in one step; prefer it over piecemeal property edits.
      - Clone-and-modify: never mutate a shared or template description in place. Clone it, edit the clone, apply. A shared instance mutates every NPC referencing it.
      - Key fields: `Shirt` and `Pants` (classic clothing asset IDs), `Face`, body-part mesh IDs (`Head`, `Torso`, `LeftArm`, `RightArm`, `LeftLeg`, `RightLeg`), per-part `*Color` (Color3), and R15 scale fields: `HeadScale`, `HeightScale`, `WidthScale`, `DepthScale`, `BodyTypeScale`, `ProportionScale`.
      - Accessories: per-bone string fields (`HatAccessory`, `HairAccessory`, `FaceAccessory`, `NeckAccessory`, `FrontAccessory`, `BackAccessory`, `WaistAccessory`, `ShouldersAccessory`) plus the Accessories table managed by `SetAccessories(accessories, includeRigidAccessories)` / `GetAccessories(includeRigidAccessories)`. Attach accessory instances with `Humanoid:AddAccessory(accessory)`.
      - R6 vs R15: scale fields and body-part mesh IDs apply to R15 only; they are ignored on R6. Classic clothing (`Shirt`/`Pants` IDs) works on both. Batch-apply with ApplyDescription so the rig rebuilds once.
      - Build descriptions from real avatars: `Players:GetHumanoidDescriptionFromUserIdAsync(userId)` and `Players:GetHumanoidDescriptionFromOutfitIdAsync(outfitId)`.
      
      ### BodyColors (legacy)
      
      - Legacy instance parented to the character with one BrickColor per body part: `HeadColor`, `TorsoColor`, `LeftArmColor`, `RightArmColor`, `LeftLegColor`, `RightLegColor`.
      - Superseded by `HumanoidDescription`'s `*Color` Color3 fields for most uses. Keep for R6-only or legacy rigs.
      
      ### Shirt / Pants
      
      - Parent a `Shirt` or `Pants` instance to the character model; set `ShirtTemplate` / `PantsTemplate` to classic clothing content IDs (ContentId).
      - Replace safely: `character:FindFirstChildOfClass("Shirt")`, create and parent a new instance if missing, then set the template.
      
      ```luau
      -- Server: clone a template description, tweak, apply
      local template = ReplicatedStorage:FindFirstChild("NPCAppearance", true):FindFirstChildOfClass("HumanoidDescription")
      local desc = template:Clone() -- never mutate the shared template
      desc.HeightScale = 1.1
      desc.HeadScale = 1.15
      desc.Shirt = 5832882895 -- classic shirt asset ID
      npcHumanoid:ApplyDescription(desc)
      ```
      
      ## Legacy tools: HopperBin (deprecated)
      
      `HopperBin` is the pre-2013 ancestor of `Tool`: drop it in `StarterPack` and the player auto-equips it. It is deprecated; do not use it for new work. Migrate to `Tool` (parent to `Backpack` / equip via `Humanoid:EquipTool`) or to a prompt-based interaction (`ProximityPrompt`) for non-held abilities. Do not wire `HopperBin`-era events (`Bin.Selected`/`Bin.Deselected` are the old API) into new systems; only tolerate HopperBins when preserving a legacy place file.
      
  • SKILL.md 2.7 KB
    ---
    name: roblox-npc-ai
    description: "Use when creating Roblox NPCs or enemies with pathfinding, state machines, line-of-sight or FOV detection, spawns, or AI update loops."
    last_reviewed: 2026-08-31
    sources:
      - https://raw.githubusercontent.com/Roblox/creator-docs/main/content/en-us/characters/pathfinding.md
      - https://create.roblox.com/docs/projects/server-authority
    ---
    
    # Roblox NPC & AI
    
    ## When to Load
    
    Load for NPCs/enemies: pathfinding, state machines, LOS/FOV detection, spawns, AI loops, or physics ownership.
    
    ## Quick Reference
    
    ### PathfindingService
    
    ```luau
    local path = PathfindingService:CreatePath({
        AgentRadius = 2, AgentHeight = 5, AgentCanJump = true,
        Costs = { Water = 20, DangerZone = math.huge },
    })
    path:ComputeAsync(npcPos, targetPos)
    if path.Status ~= Enum.PathStatus.Success then return end
    for _, wp in path:GetWaypoints() do
        if wp.Action == Enum.PathWaypointAction.Jump then humanoid.Jump = true end
        humanoid:MoveTo(wp.Position)
        if not humanoid.MoveToFinished:Wait() then return end
    end
    ```
    
    - Recompute on blocked with a bounded retry or cancellation policy.
    - In Studio, test `Workspace.PathfindingUseImprovedSearch` on representative maps before rollout; it is not scriptable.
    - Avoid long requests and repeated recomputation. Moving collidable geometry can trigger navigation-mesh work.
    - Region modifiers: anchored part + `PathfindingModifier` Label → Costs
    - `PassThrough = true` for doors; `PathfindingLink` for disconnected navmesh
    - `math.huge` cost = non-traversable
    
    ### State Machine
    
    `idle → patrol → chase → attack → flee → dead`
    - idle/patrol → chase (player detected) → attack (in range) → idle (lost)
    - any → flee (health low) → idle (safe); any → dead (health ≤ 0)
    - Transitions own movement changes, target cleanup, and cancellation
    
    ### Detection (distance → FOV → LOS)
    
    1. **Distance** `(a-b).Magnitude`: cheapest, always first
    2. **FOV** `forward:Dot(toTarget)` cosine; use a configured cone for your game, not a universal angle
    3. **LOS** `workspace:Raycast`: expensive, last
    - If the design includes hearing or proximity detection, make it a separate configured signal rather than a universal FOV bypass.
    
    ### Network Ownership
    - Classic projects may use `SetNetworkOwner(nil)` for physics-sensitive NPCs, but it is not complete security. In Server Authority projects, configure the authority model instead of treating network ownership as the security boundary.
    
    ### Update Loop
    
    - Throttle and stagger AI based on NPC count, path cost, and profiler evidence, not a universal tick rate. Keep NPC decisions and movement server-side; client code may handle presentation.
    
    For spawners, lifecycle cleanup, timeout handling, and performance budgets, load `references/full.md`.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related