Hello, i have made a npc monster that patrols each 20 seconds and chase near targets (players) when on sight. However, i decided to create a module script to deal with the npc states, since i thought it was hard to get with it
I have utilized a very simple module too: “Simple Path”, that just pathfinds with 1 line of code and works beauty.
Enemy AI script:
-- Services
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")
local me = script.Parent
local patrolwaypoints = workspace.Waypoints
local StatesMachine = require(script.StatesMachine)
local SimplePath = require(ServerStorage.SimplePath)
local myHuman = me:WaitForChild("Humanoid")
local myTorso = me:WaitForChild("HumanoidRootPart")
local myHead = me:WaitForChild("Head")
-- Sounds
local attackSound = myHead:WaitForChild("Attack")
-- Animations
local attack = script.Attack
local attackAnim = myHuman.Animator:LoadAnimation(attack)
-- Configurations
local config = me:WaitForChild("Configuration")
local fieldOfView = config.FieldOfView.Value
local distanceOfView = config.DistanceOfView.Value
local patrolSpeed = config.PatrolSpeed.Value
local chaseSpeed = config.ChaseSpeed.Value
local chaseRest = config.ChaseRest.Value
local timeoutSight = config.TimeoutSight.Value
-- Variables
local sm = StatesMachine.new()
local Path = SimplePath.new(script.Parent)
Path.Visualize = true
local target = nil
local targetDistance = nil
local startedChasing = nil
local lostSight = nil
-- Makes the monster to be only physics handled by the server
myTorso:SetNetworkOwner(nil)
-- Main functions
function Patrol()
local randomSpot = patrolwaypoints:GetChildren()[math.random(#patrolwaypoints:GetChildren())]
myTorso.CFrame = randomSpot.CFrame
end
function Chase()
if not target then
sm:ChangeState("Patrol")
return
end
-- Tired of chasing target
if os.clock() - startedChasing > chaseRest then
sm:ChangeState("Patrol")
return
end
-- Not on sight anymore, stay trying to chase for a few seconds
if onSight(target) then
lostSight = os.clock()
elseif os.clock() - lostSight > timeoutSight then
sm:ChangeState("Patrol")
return
end
if not behindObstacle(target) then
myHuman:MoveTo(target.Position)
if targetDistance <= 4 then
-- Kill target
target.Parent:FindFirstChild("Humanoid"):TakeDamage(100)
end
else
Path:Run(target) -- Just follow the target
end
end
function Main()
sm:BindListenner(function()
target, targetDistance = findTarget()
if target and onSight(target) and sm:GetCurrentState() == "Patrol" then
sm:ChangeState("Chase")
end
end)
-- Patrol
sm:AddState({
name = "Patrol",
released = function()
myHuman.WalkSpeed = patrolSpeed
end,
run = Patrol,
cooldown = 20,
})
-- Chase
sm:AddState({
name = "Chase",
released = function()
startedChasing = os.clock()
myHuman.WalkSpeed = chaseSpeed
end,
run = Chase,
cooldown = .1,
finished = function()
myHuman:MoveTo(myTorso.Position)
end,
})
-- Start with patrol as default
sm:ChangeState("Patrol")
sm:Resume()
end
Main()
-- Sub functions
function liesInFOV(target)
local directionToTarget = (target.Position - myTorso.Position).Unit
return myTorso.CFrame.LookVector:Dot(directionToTarget) > math.cos(math.rad(fieldOfView/2))
end
function behindObstacle(target)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Blacklist
params.FilterDescendantsInstances = { script.Parent }
local origin = myTorso.Position
local direction = (target.Position - myTorso.Position).Unit
local result = workspace:Raycast(origin, direction * distanceOfView, params)
if result then
if result.Instance:IsDescendantOf(target.Parent) then
return false
end
end
return true
end
function onSight(target)
local distToTarget = (myTorso.Position - target.Position).Magnitude
return (liesInFOV(target) and not behindObstacle(target)) or distToTarget < 10
end
function findTarget()
local dist = distanceOfView
local target = nil
for i, v in ipairs(workspace:GetChildren()) do
local human = v:FindFirstChild("Humanoid")
local torso = v:FindFirstChild("Torso") or v:FindFirstChild("HumanoidRootPart")
if human and torso and v.Name ~= script.Parent.Name then
local newDist = (myTorso.Position - torso.Position).Magnitude
if newDist < dist and human.Health > 0 then
target = torso
dist = newDist
end
end
end
return target, dist
end
StatesMachine (the modulescript i have made):
Obs:
- Released function its a function that is called with the first iteration with the state
- Run function its a function that is called every frame in the state
- Finished its a function that is called with the last iteration with the state
local StatesMachine = {}
StatesMachine.__index = StatesMachine
StatesMachine.states = {}
StatesMachine.previousState = ""
StatesMachine.currentState = ""
StatesMachine.nextState = ""
function StatesMachine.new()
local self = {}
setmetatable(self, StatesMachine)
return self
end
function StatesMachine:AddState(newstate)
newstate.cooldown = newstate.cooldown or 0
newstate._clock = os.clock()
table.insert(self.states, newstate)
self.nextState = newstate.name
self.currentState = newstate.name
end
function StatesMachine:BindListenner(listenner)
self.listenner = listenner
end
function StatesMachine:Resume()
local run = coroutine.create(_run)
coroutine.resume(run, self)
end
function StatesMachine:ChangeState(stateName)
self.nextState = stateName
self.scheduled = nil
for i, state in ipairs(self.states) do
if state.name == stateName then
if state.released then
state.released()
end
if state.run then
--coroutine.wrap(state.run)()
state.run()
end
end
end
end
function StatesMachine:ScheduleState(stateName, scheduleTime, overrideSchedule)
if overrideSchedule == false and self.scheduled then return end
self.scheduled = {
name = stateName,
_clock = os.clock(),
_scheduleTime = scheduleTime
}
end
function StatesMachine:GetCurrentState()
return self.currentState
end
function _run(self)
while task.wait() do
-- Call listenner interface
if self.listenner then self.listenner() end
-- Scheduled
if self.scheduled then
if os.clock() - self.scheduled._clock > self.scheduled._scheduleTime then
self:ChangeState(self.scheduled.name)
self.scheduled = nil
end
end
-- Update states
for i, state in ipairs(self.states) do
if state.name ~= self.currentState then continue end
if self.currentState ~= self.nextState then
self.currentState = self.nextState
if state.finished then state.finished() end
continue
end
if self.previousState ~= self.currentState then
if state.released then state.released() end
end
self.previousState = self.currentState
if state.run then
if os.clock() - state._clock > state.cooldown then
--coroutine.wrap(state.run)()
state.run()
state._clock = os.clock()
end
end
end
end
end
return StatesMachine
--[[
local sm = StatesMachine.new()
sm:AddState({
name = "Patrol",
released = function()
end,
run = function()
end,
finished = function()
end
})
]]
I just want to know if this is a messy code or potentially dangerous with bugs. Thanks.