el4cteo/roblox-skills

roblox-npc-ai

NPCs and enemy AI - PathfindingService (CreatePath, ComputeAsync, costs, modifiers, links, blocked paths), chase/follow loops, state machines, perception and line of sight, spawning/pooling, scaling to many NPCs.

View source
Original skill document

Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.

NPCs and AI

Architecture

  • NPC logic runs on the server (authoritative targets, damage, drops). Visual extras (idle

animations, nameplates, effects) can run on clients.

  • One NPC service drives all NPCs from a single loop (e.g. 5–10 Hz "think" rate, staggered), not one

script + while true per NPC. Tag NPC models (CollectionService) and keep per-NPC state in a table.

  • Server-owned physics: npc.PrimaryPart:SetNetworkOwner(nil) after spawning, so NPCs don't stutter

when ownership moves between nearby players (and exploiters can't push them).

  • Use Humanoid for NPCs that need walking/jumping/climbing; for simple movers (turrets, floating

enemies, crowds) use an AnimationController + Animator and move with constraints or CFrames.

Pathfinding

luau
--!strict
local PathfindingService = game:GetService("PathfindingService")

local path = PathfindingService:CreatePath({
	AgentRadius = 2,
	AgentHeight = 5,
	AgentCanJump = true,
	AgentCanClimb = false,
	WaypointSpacing = 4,
	Costs = {
		Water = 20, -- avoid water (Enum.Material name)
		DangerZone = math.huge, -- PathfindingModifier.Label: never cross
	},
})

-- Walk `humanoid` to `goal`. Returns true on arrival, false if unreachable or interrupted.
local function walkTo(humanoid: Humanoid, goal: Vector3): boolean
	local root = humanoid.RootPart
	if not root then
		return false
	end
	local ok = pcall(function()
		path:ComputeAsync(root.Position, goal)
	end)
	if not ok or path.Status ~= Enum.PathStatus.Success then
		return false -- NoPath: try a fallback (direct MoveTo, new goal, teleport if stuck long)
	end

	local waypoints = path:GetWaypoints()
	local blocked = false
	local blockedConnection = path.Blocked:Connect(function(blockedIndex)
		blocked = true -- an obstacle appeared on a waypoint ahead; caller recomputes
		print("path blocked at waypoint", blockedIndex)
	end)

	for index = 2, #waypoints do
		local waypoint = waypoints[index]
		if waypoint.Action == Enum.PathWaypointAction.Jump then
			humanoid.Jump = true
		end
		humanoid:MoveTo(waypoint.Position)
		local reached = humanoid.MoveToFinished:Wait() -- false after 8 s timeout
		if not reached or blocked or humanoid.Health <= 0 then
			blockedConnection:Disconnect()
			return false
		end
	end
	blockedConnection:Disconnect()
	return true
end

return walkTo
  • CreatePath + ComputeAsync is the current API (FindPathAsync is superseded). Reuse Path objects

per agent type; ComputeAsync yields — never call it every frame for every NPC.

  • Chasing moving targets: recompute when the target moved more than a few studs since the last

compute or every ~0.5–1 s, and skip pathfinding entirely when there's a clear line of sight (workspace:Raycast from NPC to target) — just MoveTo the target.

  • PathfindingModifier (child of a part/region, Label matching a Costs key, PassThrough for doors)

and PathfindingLink (between two attachments, e.g. ladders, teleporters, boats; the waypoint has Label and Action = Custom) shape paths.

  • Limits: start→goal straight-line distance ≤ 3,000 studs, 20,000-node budget; far goals need

intermediate targets. Visualize the navmesh with Studio's Navigation Mesh visualization option.

  • On clients, pathfinding sees only streamed-in geometry: compute paths on the server.

Behavior: state machine

luau
--!strict
type State = "Idle" | "Chase" | "Attack" | "Return"

type Npc = {
	model: Model,
	humanoid: Humanoid,
	home: Vector3,
	state: State,
	target: Model?,
	lastAttack: number,
}

local AGGRO_RANGE = 40
local ATTACK_RANGE = 5
local LEASH_RANGE = 80
local ATTACK_COOLDOWN = 1.2

local losParams = RaycastParams.new()
losParams.FilterType = Enum.RaycastFilterType.Exclude

local function hasLineOfSight(from: Vector3, target: BasePart, ignore: { Instance }): boolean
	losParams.FilterDescendantsInstances = ignore
	local result = workspace:Raycast(from, target.Position - from, losParams)
	return result == nil or result.Instance:IsDescendantOf(target.Parent :: Instance)
end

local function think(npc: Npc, findTarget: (Vector3, number) -> Model?)
	local root = npc.humanoid.RootPart
	if not root or npc.humanoid.Health <= 0 then
		return
	end
	local targetRoot = npc.target and npc.target:FindFirstChild("HumanoidRootPart") :: BasePart?
	local distance = if targetRoot then (targetRoot.Position - root.Position).Magnitude else math.huge

	if npc.state == "Idle" then
		npc.target = findTarget(root.Position, AGGRO_RANGE)
		if npc.target then
			npc.state = "Chase"
		end
	elseif npc.state == "Chase" then
		if not targetRoot or (root.Position - npc.home).Magnitude > LEASH_RANGE then
			npc.state = "Return"
		elseif distance <= ATTACK_RANGE then
			npc.state = "Attack"
		elseif hasLineOfSight(root.Position, targetRoot, { npc.model }) then
			npc.humanoid:MoveTo(targetRoot.Position)
		end -- else: request a path (throttled) via walkTo in a separate thread
	elseif npc.state == "Attack" then
		if not targetRoot or distance > ATTACK_RANGE * 1.5 then
			npc.state = "Chase"
		elseif os.clock() - npc.lastAttack >= ATTACK_COOLDOWN then
			npc.lastAttack = os.clock()
			local targetHumanoid = npc.target and npc.target:FindFirstChildOfClass("Humanoid")
			if targetHumanoid then
				targetHumanoid:TakeDamage(10)
			end
		end
	elseif npc.state == "Return" then
		npc.target = nil
		npc.humanoid:MoveTo(npc.home)
		if (root.Position - npc.home).Magnitude < 4 then
			npc.state = "Idle"
		end
	end
end

return think

For complex AI (many behaviors, priorities), use a behavior tree or utility AI; keep the same "think at a fixed rate, act through small primitives" structure. See references/scaling-npcs.md for spawning, pooling, and performance with hundreds of NPCs.

Perception tips

  • Find targets with workspace:GetPartBoundsInRadius against a collision group/filter containing only

characters, or iterate Players:GetPlayers() (cheaper for small counts) and compare squared distances.

  • Add reaction delays and field-of-view checks (root.CFrame.LookVector:Dot(direction.Unit) > 0.5)

so NPCs feel fair.

  • Stagger thinking: npcIndex % 5 == frame % 5 spreads work across frames.

Related skills

roblox-characters-animation (humanoids, animation), roblox-physics (ownership, raycasts), roblox-performance (profiling, parallel Luau), roblox-game-systems (waves, combat, drops).

from this repository

More skills

All skills
el4cteo
Community

roblox-security

Anti-exploit design - never trust the client, validate every remote argument (NaN, spoofed instances, strings), rate limiting, ProximityPrompt/ClickDetector abuse, network ownership, movement and hit validation, economy/trade duplication, code confidentiality, secure teleports, heuristics, Ban API. Use when writing server handlers for client input, combat/economy/trading, or reviewing for exploits.

installs
2
GitHub stars
29
Updated
Sep 28
el4cteo
Community

roblox-analytics-liveops

Analytics and live ops - AnalyticsService economy/funnel/onboarding/progression/custom events, player segments, experience configs via ConfigService, A/B experiments, badges, experience notifications, timed events, daily rewards, season passes. Use when instrumenting a game, tuning economy or onboarding, running A/B tests or feature flags, or planning live events and re-engagement.

installs
2
GitHub stars
29
Updated
Sep 28
el4cteo
Community

roblox-architecture

Roblox project structure and client/server model - replication, where code and assets belong (ServerScriptService, ReplicatedStorage, StarterPlayer), RunContext, service/controller bootstrapping, player/character lifecycle, CollectionService components, attributes, streaming-safe client code, library choices. Use when starting a project, deciding where code goes, or fixing replication/streaming bugs.

installs
2
GitHub stars
29
Updated
Sep 28
el4cteo
Community

roblox-audio

Sound and music - the Audio API (AudioPlayer, AudioEmitter, AudioListener, AudioDeviceOutput, Wire, effects, faders), classic Sound/SoundGroup, 2D vs 3D audio, music and SFX volume, client vs server playback, asset permissions, streaming caveats, voice routing, text-to-speech. Use when adding music, sound effects, spatial audio, mixing, or fixing sounds that don't play.

installs
2
GitHub stars
29
Updated
Sep 28