Feedback on my Monster AI algorithm

Hey devs,

Recently I have been working on a monster AI algorithm that will hunt players and communicate with each other (hive mind). I want some feedback on this and see if it has any issues. Thank you!

(for code that is not built-in luau, what they do should be self-explantory)

local SSS = game:GetService("ServerScriptService")
local RS = game:GetService("ReplicatedStorage")
local CS = game:GetService("CollectionService")
local Players = game:GetService("Players")

local commonModules = require(RS.Modules)
local signal = commonModules.Signal

local serverModules = require(SSS.Modules)
local SimplePath = serverModules.SimplePath
local EntityService = serverModules.EntityService

-->> Variables
local detectionFov_chase = 175
local fovForCalc = math.rad(detectionFov_chase) / 2

local random = math.random
local clamp = math.clamp
local abs = math.abs

local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Include
params.FilterDescendantsInstances = CS:GetTagged("RoomBoundingBox")

-->> Communication mechanic registry
local SharedInfo = {}

local function angleBetween(vectorA, vectorB)
	return math.deg(math.acos(math.clamp(vectorA:Dot(vectorB), -1, 1)))
end

local function offset()
	return Vector3.new(random(-60, 60), 0, random(-60, 60))
end

math.randomseed(math.random(1, 1000))

return function(entity: Model)
	-->> Instances
	local root = entity.HumanoidRootPart :: BasePart
	local head = entity.Head :: BasePart

	local subject = EntityService.Register(entity)

	-->> Variables
	local memoryTime = math.random(15,30)/10
	local patrolCooldown = math.random(2,4)
	
	local isPatrolling = false
	local nextPatrolTime = 0
	local lastPatrol = 0 

	local targetSnapshots = {}

	subject.StartedMoving:Connect(function()
		isPatrolling = true
		lastPatrol = os.clock()
	end)

	subject.FinishedMoving:Connect(function()
		nextPatrolTime = os.clock() + patrolCooldown
		isPatrolling = false
	end)

	while task.wait(0.06) do
		local hasMoved = false

		-->> CHASE IF ANY
		for i, v in subject:GetPotentialTargets() do
			local canSee = subject:CanSeeTarget(v, fovForCalc)
			if canSee or targetSnapshots[v] then
				if canSee then
					-->> Refresh memory
					SharedInfo[entity] = {v, true}
					targetSnapshots[v] = os.clock()
				else
					-->> If memory is too old, then forget about it
					SharedInfo[entity] = {v, true}
					
					if os.clock() - (targetSnapshots[v] or 0) >= memoryTime then
						SharedInfo[entity] = nil
						targetSnapshots[v] = nil
					end
				end

				hasMoved = subject:MoveTo(v) 
				if hasMoved then
					break
				end
			end
		end

		-->> SEE IF ANY OTHER INDIVIDUAL SEES THE TARGET (HIVE MIND)
		if not hasMoved then
			for otherEntity, info in pairs(SharedInfo) do
				local target = info[1]
				local isSeeing = info[2]
				
				if otherEntity == entity then
					continue
				end
				
				local ICanSee = subject:CanSeeTarget(target, fovForCalc)

				if target then
					if (otherEntity:GetPivot().Position - root.Position).Magnitude < 100 then
						warn("SHARED INFO")
						
						if os.clock() - (targetSnapshots[target] or 0) > memoryTime then
							SharedInfo[entity] = nil
							targetSnapshots[target] = nil
						else
							SharedInfo[entity] = {target, isSeeing or ICanSee or false}
							if isSeeing or ICanSee then
								targetSnapshots[target] = os.clock()
							end
						end

						hasMoved = subject:MoveTo(target) 
						if hasMoved then
							break
						end
					end
				end
			end
		end

		-->> PATROL
		if not hasMoved then
			if os.clock() - lastPatrol > patrolCooldown and os.clock() > nextPatrolTime and subject.Path.Status == "Idle" then
				patrolCooldown = random(2, 4)

				-- ALGORITHM:
				-- Prefers to move in similar directions + Farther distances
				local randomPos = {} do
					local rootDir = root.CFrame.LookVector
					local rootPos = root.Position

					for i = 1, 8 do
						-->> Variables
						local newPos = root.Position + offset()
						local dist = (rootPos - newPos).Magnitude

						local angle = angleBetween(rootDir, (newPos - rootPos).Unit)

						-->> Randomizers
						local distRandom = random() * 2.5
						local angleRandom = random() 

						-- Calculate total score (lower = better)
						-- CRITERIAS:
						local critA = -dist * distRandom
						local critB = math.abs(angle) * angleRandom
						local critC = 6 - 
							(if subject:CanSeeTarget(newPos, fovForCalc) then
								random(2,6)
								else 
								random(0, 4))
						local critD = next(workspace:GetPartBoundsInBox(CFrame.new(newPos), Vector3.zero, params)) and 0 or 999

						-->> FINAL SCORE:
						local score = critA * 0.5 + critB * 0.3 + critC * 0.2 + critD

						table.insert(randomPos, {position = newPos, score = score})
					end

					table.sort(randomPos, function(a, b)
						return a.score < b.score
					end)

					for i, data in ipairs(randomPos) do
						randomPos[i] = data.position
					end
				end

				for i, v in randomPos do
					if subject:MoveTo(v) then
						break
					end
				end
			end 
		end
	end
end

Feedback is highly appreicated!

1 Like

I feel like you could have your chase, hivemind, and patrol change a single positionToMoveTo variable at the top of your loop, then place a single :MoveTo(positionMoveTo) at the bottom of your loop.

local positionMoveTo = nil

-- chase code 

if not positionMoveTo then
-- hivemind code
end

if not positionMoveTo then
-- patrol code
end

if positionMoveTo then
-- move npc to that target
end
1 Like