Cleaning Up a Hostile_NPC_Code

Hello! So, i would link an roblox file here, but this code requires all of the rest of the code to work. However, this code is a server side module script used in the Aero Game Framework. The way i designed my npc system was so that non hostile NPC’s, such as Major or Minor Story NPC’s or Shop NPC’s are local, but NPCs such as Enemy ones, Creatures, Guards… npc’s that can fight, are in the server.

When this code begins it loops through a folder in the Replicated storage and finds a type of Hostile NPC, then, it loops through a Hostile Spawn Folder in the workspace, and finds all the spawns with that Hostile NPC’s Name in it such as “Warlock Spawn”. Within that spawn is a number value for how many npc’s i want to spawn there… so it runs this code as many times as it takes to fill that up. At the end of the code i’m about to show, in the npc died function, it will call back the activate npcs function so it will find the spawn after an elated amount of time and respawn that npc.

My goal for positing here is to get some ideas on how to make these npc’s smarter cleaner, and how to make them a bit more immersive. There are many different types of npc’s so this code is very open ended for parameters depending on the npc that it is spawned so i dont have to rewrite the code for each NPC.

Some of my key issues include

*In the NPC wandering section, sometimes NPC’s tend to walk into eachother
*NPC’s, particularly the ones i’ve been testing right now which are magic based, often accidently attack eachother.
*NPC’s sometimes spawn on top of eachother.

I use the pathfinding service here… now, Hostile NPC coding is new to me… so any tips or ideas or suggestions would be awesome! Thanks! :slight_smile:

function Hostile_NPC_Service:Hostile(NPCname, spawna, spawned)
--Primary Local Indexes--

local cooldown = false

--Spawnnpc--
local npc = Replicated_Hostile_assets[NPCname]:Clone()
local cln
pcall(function()
	cln=spawna["ClassName"]
end)
if cln then
	--if part or model
	if spawna:IsA('BasePart') then
		npc:SetPrimaryPartCFrame(spawna.CFrame+Vector3.new(math.random(-100,100),npc.HumanoidRootPart.Size.Y+0.25,math.random(-100,100)))
	elseif spawna:IsA("Model") and spawna.PrimaryPart then
		npc:SetPrimaryPartCFrame(spawna.PrimaryPart.CFrame+Vector3.new(math.random(-100,100),npc.HumanoidRootPart.Size.Y+0.25,math.random(-100,100)))
	else
		warn("Failed to recieve cframe of object: "..spawna.Name)
	end
elseif spawna.p then
	npc:SetPrimaryPartCFrame(spawna+Vector3.new(math.random(-100,100),npc.HumanoidRootPart.Size.Y+0.25,math.random(-100,100)))
else
	warn("No object or CFrame given.")
end

spawned.Value = spawned.Value + 1

spawna.Transparency = 1
npc.Parent = Hostile_assets
npc.PrimaryPart = npc:WaitForChild("Head")

coroutine.resume(coroutine.create(function()
	if npc:FindFirstChild("BodyHumanoid") then
		Hostile_NPC_Service:NPC_Appearance(npc)
	end
end))

local humanoid = npc:WaitForChild("Humanoid")
local torso = npc:WaitForChild("UpperTorso")
local OriginalPosition = npc:WaitForChild("HumanoidRootPart").Position
local target = npc:WaitForChild("Target")
local location = npc:WaitForChild("Location")

local a = Vector3.new(100,5,-100)
local b = Vector3.new(75,5,-200)
local debounce = true
local debounce2 = false

local list = game.Players:GetChildren()
local loco = settings.TargetDistance

if target.Value then
	local targetTorso = target.Value:FindFirstChild("UpperTorso")
end

local Attack_Debounce = false
local Previous_Attack

local function attack(target)
	if npc.Attack_Type.Value == "Magic" then
		if Attack_Debounce == false then
			Attack_Debounce = true
			local RandomSpell
			repeat wait()
			RandomSpell = Spells[math.random(#Spells)]
			until npc.MagInfo.Power.Value >= SpellLevels[RandomSpell] and RandomSpell ~= Previous_Attack
			Previous_Attack = RandomSpell
			NPC_Attack_Service:NPC_PowerValues(npc, RandomSpell, false, math.random(1,2), target)
			Attack_Debounce = false
		end
	end
end

--Create Sound--
local Passive_Sound
if not Passive_Sound then
	Passive_Sound = Instance.new("Sound")
	Passive_Sound.Name = "NPC_Passive_Sound"
	Passive_Sound.Parent = npc:WaitForChild("Head")
else
	Passive_Sound = npc:WaitForChild("Head"):FindFirstChild("NPC_Passive_Sound")
end
--Check for the NPC's Line of sight.


local function findNearestTorso(pos)
	local list = game.Workspace:children()
	local torso = nil
	local dist = settings.TargetEnemyDistance
	local temp = nil
	local human = nil
	local temp2 = nil
	for x = 1, #list do
		temp2 = list[x]
		if (temp2.className == "Model") and (temp2 ~= npc) and game.Players:FindFirstChild(temp2.Name) then
			temp = temp2:findFirstChild("HumanoidRootPart")
			human = temp2:findFirstChild("Humanoid")
			if (temp ~= nil) and (human ~= nil) and (human.Health > 0) then
				if (temp.Position - pos).magnitude < dist then
					torso = temp
					dist = (temp.Position - pos).magnitude
				end
			end
		end
	end
	return torso
end

local raus,vaus,haus,saus
local WanderCoolDown
local PassiveSoundCoolDown

local render = game:GetService("RunService").Heartbeat:connect(function(dt)
	
	local target = findNearestTorso(npc.HumanoidRootPart.Position)
	TargetLocked = false
	--local targetPos = target.Position
	
	if not haus and not vaus and not PassiveSoundCoolDown and not TargetLocked then
		PassiveSoundCoolDown = true
		local PassiveSounds = Hostile_NPC_Dialogue[NPCname]["Passive_Sounds"]["Inactive"]
		local RandomSoundId = Hostile_NPC_Dialogue[NPCname]["Passive_Sounds"]["Inactive"][math.random(#PassiveSounds)]
		Passive_Sound.SoundId = RandomSoundId
		Passive_Sound:Play()
		wait(math.random(8,30))
		PassiveSoundCoolDown = false
	end
	
	if target ~= nil and target.Name == "HumanoidRootPart" then
		local NpcHRP = npc:WaitForChild("HumanoidRootPart")
		local CharHRP = target
		
		local dist = (NpcHRP.CFrame.p-CharHRP.CFrame.p).magnitude
		
		if not cooldown and not vaus then
			if dist <= settings.animationOffset then
				local ray = Ray.new(npc.HumanoidRootPart.CFrame.p,npc.HumanoidRootPart.CFrame.LookVector*(settings.TargetEnemyDistance*2))
				local object,surfaceposition = workspace:FindPartOnRay(ray,npc)
				if object then
						local target = object.Parent
						if target:FindFirstChild("Humanoid") and game.Players:FindFirstChild(target.Name) then
							cooldown=true
							haus = true
							saus = false
							--local track = Hostile_NPC_Service:animate(507770239,npc.Humanoid)
							local PassiveSounds = Hostile_NPC_Dialogue[NPCname]["Passive_Sounds"]["Active"]
							local RandomSoundId = Hostile_NPC_Dialogue[NPCname]["Passive_Sounds"]["Active"][math.random(#PassiveSounds)]
							print(RandomSoundId)
							Passive_Sound.SoundId = RandomSoundId
							Passive_Sound:Play()
							wait(1.5)
							--track:Stop()
							spawn(function()
								wait(settings.animationCooldownTime)
								cooldown=false
							end)
						else
							haus = false
					end		
				end
			else
				local ray = Ray.new(npc.HumanoidRootPart.CFrame.p,npc.HumanoidRootPart.CFrame.LookVector*(settings.TargetEnemyDistance*2))
				local object,surfaceposition = workspace:FindPartOnRay(ray,npc)
				if object then
					local target = object.Parent
					if target:FindFirstChild("Humanoid") and game.Players:FindFirstChild(target.Name) then
						haus = true
						saus = false
					else
						haus = false
					end		
				end
			end
		end
		
		if npc.MagInfo.Energy.Value <= 0 then
			wait(4)
			npc.MagInfo.Energy.Value = npc.MagInfo.MaxEnergy.Value
		end
		
		if not vaus then
			if dist <= settings.TargetEnemyDistance or haus == true then
				--Raise the gui				
				if target ~= nil and not TargetLocked  then -- found player
					TargetLocked = true
					npc.Humanoid.WalkSpeed = Hostile_NPC_Settings["HostileChaseSpeeds"][npc.Name]
					
					local function ChasePlayer()
						local path = PathfindingService:FindPathAsync(npc.UpperTorso.Position, target.Position)
						local points = path:GetWaypoints()
						if path.Status == Enum.PathStatus.Success then
							for i,v in pairs(points) do
								npc.Humanoid:MoveTo(v.Position)
								npc.Humanoid.MoveToFinished:Wait()
								if v.Action == Enum.PathWaypointAction.Jump then
									npc.Humanoid.Jump = true
								end
							end
						end	
					end		
					
					if dist >= Hostile_NPC_Settings["HostileHaltDistances"][npc.Name] then
						ChasePlayer()
					elseif dist >= Hostile_NPC_Settings["HostileHaltDistances"][npc.Name] and TargetLocked and target:FindFirstChild("HumanoidRootPart") then
						npc.HumanoidRootPart.CFrame = CFrame.new(target.HumanoidRootPart.CFrame.p,npc.HumanoidRootPart.CFrame.p)
					end	
					
					if not saus and haus then
						attack(target)	
					end			
				end
				
				raus=true
			else -- No longer Target Locked
				if target == nil and  TargetLocked  then
					TargetLocked = false
					haus = false
					npc.Humanoid.WalkSpeed = 8
					
					local path = PathfindingService:FindPathAsync(npc.UpperTorso.Position, OriginalPosition)
					local points = path:GetWaypoints()
					
					if path.Status == Enum.PathStatus.Success then
						for i,v in pairs(points) do
							npc.Humanoid:MoveTo(v.Position)
							npc.Humanoid.MoveToFinished:Wait()
							if v.Action == Enum.PathWaypointAction.Jump then
								npc.Humanoid.Jump = true
							end
						end
					end
					
					--npc.Humanoid:MoveTo(OriginalPosition)
				end
				raus=false
			end
		end
	else --Doesnt Deteect Anything
		TargetLocked = false
		haus = false
		
		npc.Humanoid.WalkSpeed = 8
					
		local path = PathfindingService:FindPathAsync(npc.UpperTorso.Position, OriginalPosition)
		local points = path:GetWaypoints()
					
		if path.Status == Enum.PathStatus.Success then
			for i,v in pairs(points) do
				npc.Humanoid:MoveTo(v.Position)
				npc.Humanoid.MoveToFinished:Wait()
				if v.Action == Enum.PathWaypointAction.Jump then
					npc.Humanoid.Jump = true
				end
			end
		end
	end
	
	--NPC Wandering Around
	coroutine.resume(coroutine.create(function()
			if not vaus and not haus and not TargetLocked and not WanderCoolDown then
				WanderCoolDown = true

				local RandomDestination = npc.HumanoidRootPart.Position + Vector3.new(math.random(-50,50),0,math.random(-50,50))
				npc.Humanoid.WalkSpeed = 8
							
				local path = PathfindingService:FindPathAsync(npc.UpperTorso.Position, RandomDestination)
				local points = path:GetWaypoints()
							
				if path.Status == Enum.PathStatus.Success then
					for i,v in pairs(points) do
						npc.Humanoid:MoveTo(v.Position)
						npc.Humanoid.MoveToFinished:Wait()
						if v.Action == Enum.PathWaypointAction.Jump then
							npc.Humanoid.Jump = true
						end
					end
				end	
					
				wait(8)			
				WanderCoolDown = false	
			end
		end))
end)

npc.Humanoid.Died:Connect(function()
	render:disconnect()
	spawned.Value = spawned.Value - 1
	wait(5)
	for i,v in pairs(npc:GetDescendants()) do
		coroutine.resume(coroutine.create(function()
			if v:IsA'BasePart' then
				local Smoke = Instance.new("ParticleEmitter")
				Smoke.Name = "SpellSmoke"
				Smoke.Color = ColorSequence.new(ColorSettings[npc.MagInfo.Architype.Value])
				Smoke.Texture = "http://www.roblox.com/asset/?id=445231898"
				Smoke.LightEmission = 0.5
				Smoke.Size = NumberSequence.new({NumberSequenceKeypoint.new(0, .2, 0), NumberSequenceKeypoint.new(1, .7, 0)})
				Smoke.Transparency = NumberSequence.new({NumberSequenceKeypoint.new(0, 0, 0), NumberSequenceKeypoint.new(0.75, 0.25, 0), NumberSequenceKeypoint.new(1, 1, 0)})
				Smoke.ZOffset = 1
				Smoke.Acceleration = Vector3.new(4,4,0)
				Smoke.Drag = 10
				Smoke.VelocityInheritance = 1
				Smoke.Lifetime = NumberRange.new(4,6)
				Smoke.Rate = 2000
				Smoke.Rotation = NumberRange.new(0,360)
				Smoke.RotSpeed = NumberRange.new(-20,20)
				Smoke.Speed = NumberRange.new(0,0)
				Smoke.SpreadAngle = Vector2.new(100,100)
				Smoke.Parent = v
				Smoke.Enabled = true
				
				if v.Name == "Head" then
					v.face.Transparency = 1
				end
				wait(2)
				v.Transparency = 1
				Smoke.Enabled = false
			end
		end))
	end
	wait(10)
	npc:Destroy()
	wait(settings.NPCRegenTime)
	Hostile_NPC_Service:Activate()
end)

end

6 Likes

I few quick questions.
Do the NPC’s check before moving that the target location is not occupied?
Do they belong in a Don’t collide group?
Do they check the target of an attack is not another NPC before attacking?
Are your spawn points randomised?
Hope these help.

3 Likes

In your code you’re checking whether your cln is a BasePart or a Model - this isn’t a bad thing in and of itself, but I do worry that you have set up your environment such that you don’t know what Instance type you’re cloning. Could you explain this?

2 Likes

@RamJoT I’m not checking currently if the space is occupied, could you elaborate on that? And honestly i’ve never experimented with collide groups. And there are certain peramaters for NPC’s not to chase other NPC’s, the problem is that for this particular npc type in question is magic based with projectiles, and the npc’s keep accidentally hitting their fellow npc’s

@plasmascreen so, its cloning the npc from a folder called Hostile_NPC_Assets, in a folder called NPC_Assets, in the replicated storage. This is just checking to make sure its not a value of some kind or anything but that. I suppose i don’t need the BasePart part, but someone suggested once that i should do that.

1 Like

Note: I’m not a expert on enemy AI yet but I’ve recently made my own enemy script, and I’m using OOP

Code
local ServerScriptService = game:GetService("ServerScriptService")
local enemyTypes = ServerScriptService.EnemyTypes
local Enemy = require(enemyTypes.Enemy)

local EnemySwordType = setmetatable({}, Enemy)
EnemySwordType.__index = EnemySwordType

function EnemySwordType.new(enemyInfo, startPosition, zone)
	local self = setmetatable({}, EnemySwordType)
	self:Init(enemyInfo, startPosition, zone)
	local instance = self:CreateInstance()
	return instance, self
end

function EnemySwordType:Start()
	if not self.dead then
		self:ShowInstance()
		self:MoveLoop()
	end
end

function EnemySwordType:Stop()
	self:HideInstance()
end

return EnemySwordType

local ServerScriptService = game:GetService("ServerScriptService")
local info = ServerScriptService.Info
local AliveCharacters = require(info.AliveCharacters)

local updateTick = 0

local Enemy = {}
Enemy.__index = Enemy



function Enemy:Init(enemyInfo, startPosition, zone)
	self.name = enemyInfo.name
	-- stats
	self.health = enemyInfo.MaxHealth
	self.maxHealth = enemyInfo.MaxHealth
	self.respawnTime = enemyInfo.RespawnTime
	--self.moveSpeed = enemyInfo.moveSpeed
	self.spawnRange = enemyInfo.SpawnRange
	self.chaseRange = enemyInfo.ChaseRange
	-- physical parts
	self.model = enemyInfo.enemyModel
	self.instance = nil
	self.humanoid = nil
	self.primaryPart = nil
	-- states
	self.dead = false
	self.retreating = false
	-- combat
	self.target = nil
	self.attackers = {}
	--misc
	self.players = zone.players
	
	self.startPosition = startPosition
	self.hideParent = game.ReplicatedStorage
	self.showParent = game.Workspace
	self.enabled = false
end

function Enemy:SetEnabled(bool)
	if bool and not self.enabled then
		print("enemy Enable")
		self.enabled = bool
		self:Start()
	elseif self.enabled then
		print("enemy disable")
		self.enabled = bool
		self:Stop()	
	end	
end



function Enemy:isInSpawnRange()
	return (self.primaryPart.Position - self.startPosition).magnitude < self.spawnRange
end

function Enemy:isInChaseRange(character)
	return (character.HumanoidRootPart.Position - self.primaryPart.Position).magnitude < self.chaseRange
end

function Enemy:MoveToTarget()
	self.humanoid:MoveTo(AliveCharacters[self.target].PrimaryPart.Position)
end

function Enemy:MoveLoop()
	while self.enabled do
		local target = self.target
		
		if not self:isInSpawnRange() then
			self.target = nil
			self:Retreat()
		end	
		
		if not self.dead and not self.retreating then
			if target then
				if AliveCharacters[target] then
					self:MoveToTarget(target) 
				else
					self:Retreat()
				end
			else
				self:LookForTarget()
			end
		end
		wait()
	end
end

function Enemy:LookForTarget()
	for player, _ in pairs(self.players) do
		local character = AliveCharacters[player]
		if character then
			if self:isInChaseRange(character) then
				self.target = player
			end
		end
	end		
end

function Enemy:Retreat()
	self.retreating = true
	self.target = nil
	local connection
	self.humanoid:MoveTo(self.startPosition)	
	connection = self.humanoid.MoveToFinished:Connect(function()
		self.retreating = false
		connection:Disconnect()
	end)
end

function Enemy:Die()
	if self.dead then return end
	self.dead = true
	self:HideInstance()
	-- respawn
	-- reward and clear attackers
	delay(self.respawnTime, function()
		self.dead = false
		self.health = self.maxHealth
		self:ShowInstance()
	end)
end

function Enemy:SetTarget(player)
	local character = AliveCharacters[player]
	if character then
		self.target = player
	end	
end

function Enemy:TakeDamage(num, player)
	if self.retreating or self.dead or not self.enabled then return end
	self.health = self.health - num
	print(self.health, self.maxHealth)
	
	self:SetTarget(player)
	
	if self.health <= 0 then
		self:Die()
	end
end

function Enemy:ShowInstance()
	if not self.enabled then return end
	self.instance.Parent = self.showParent
	self.instance:MoveTo(self.startPosition)
end

function Enemy:HideInstance()
	self.instance.Parent = self.hideParent
end

function Enemy:CreateInstance()
	local newInstance = self.model:Clone()
	newInstance.Parent = self.hideParent
	self.instance = newInstance
	self.humanoid = newInstance.Humanoid
	self.primaryPart = newInstance.primaryPart
	return newInstance
end

return Enemy
My Code Logic

I’m using OOP because I want some enemies to have different Classes e.g a Sword enemy and a Magic enemy

while sword enemies and magic enemies are vastly different, they still have shared methods, so I can have both a sword enemy class and a magic enemy class that both inherit from a enemy class which has methods they will both need to use like finding a target, taking damage, etc etc

You can greatly increase readability by breaking your functions down into smaller ones, and also can reduce nesting by using guard clauses

local x = false

if not x then return end
-- instead of
if x then

end

my code isn’t perfect and I’m planning on redoing it but thought I’d just give a couple of tips

2 Likes

If Hostile_NPC_Assets only contains models then that part of the code is unnecessary, oui

1 Like