EasyEnemies - Enemy Module

there is a error i’m getting with this
attempt to index nil with ‘PrimaryPart’

local function GET_DISTANCE(instance: Model, target: Model)
return (instance.PrimaryPart.Position - target.PrimaryPart.Position).Magnitude  
end

When you have a model make sure you set the primary part before or in settings there will be an auto generate primary part in the next update.

Hey this looks pretty cool and really useful! will definitely try this out (since my npcs are a little stupid lol)

1 Like

Sorry if you’ve moved on from this module! but It seems the Enemies are only attacking once?


can’t seem to watch it either, would you mind uploading these videos to youtube or streamable???

2 Likes

Hey yea so I did upload to streamable but I’ll fix this asap

My apologies I’ll fix this asap

local function GET_DISTANCE(instance: Model, target: Model)
return (instance.PrimaryPart.Position - target.PrimaryPart.Position).Magnitude  
end

I have an error here I have a set PrimaryPart what is the issue?
This happens is when you get away from the NPC.

  • with my respawner script it doesn’t work anymore

A pretty cool resource! My only advice is to be sure to handle exceptions, no matter how unlikely they seem. I notice there are some edge cases that could happen (such as a loading character having no primary part) that could break pretty much the entire system.

I would recommend adding regular checks, and never hard-error if possible.

Also, as a sidenote, do not store built-in methods as variables:
image

This is actually less efficient because these methods are inline-cached by default.

3 Likes

Hey, sorry for bumping so late, but I’m having an issue. Enemies don’t wander, and are really sluggish. After a while of chasing something, they will spam this error:

ServerScriptService.EasyEnemies.Functionality:106: attempt to index nil with 'PrimaryPart'  -  Server - Functionality:106
Stack Begin  -  Studio
 Script 'ServerScriptService.EasyEnemies.Functionality', Line 106 - function GET_DISTANCE  -  Studio - Functionality:106
Script 'ServerScriptService.EasyEnemies.Functionality', Line 364  -  Studio - Functionality:364
Stack End  -  Studio

Here’s the code I’m using to start them up:

local Entities = game.Workspace:WaitForChild("Entities")
local RespawnEntities = game:GetService("ReplicatedStorage").RespawnEntities
local EnemyService = require(game:GetService("ServerScriptService"):WaitForChild("EasyEnemies"))
local AIService = require(game:GetService("ServerScriptService"):WaitForChild("AI"):WaitForChild("Main"))

local entitySettings = {
	["Zombie"] = {
		health = 100, -- Enemy Health
		damage = 10, -- Enemy Base Damage
		wander = true, -- Enemy Wandering

		attack_range = 20, -- Enemy Search Radius
		attack_radius = 5, -- Enemy Attack Radius

		attack_ally = false, -- Enemy Attacking Team Members
		attack_npcs = true, -- Enemy Attacking Random NPC's
		attack_players = true, -- Enemy Attacking Players

		default_animations = {18497275315}, -- Enemy Animations should be used for 'Light' Attacks // Example default_animations = {8972576500}
		default_functions = { -- Functions for said 'Light' Attacks ^
			function(target) -- functions pass the target as the first argument automatically
				print(target)
			end,
		},

		special_animations = {18497088737, 18497583371}, -- Enemy Animations should be used for 'Heavy' Attacks // Example special_animations = {8972576500}
		special_functions = { -- Functions for said 'Heavy' Attacks ^
			function(target) -- functions pass the target as the first argument automatically
				print('specialMove')
			end,
		},
	},
}
local entities = EnemyService.new('Zombie', entitySettings.Zombie)

wow nice.
time to make a better version of it :smiley:

1 Like

Apologies, I really don’t update this anymore, but expect a new version some time later this year, it will be more in depth npcs

1 Like

No worries! I’ll look for a temporary fix myself, and post it here. Excited to see your future module, this one already is really good!

Edit: I need to stop making promises that I can’t keep. I’ll post the code with the changes that I was able to make. Sorry

--!strict

--// Services
local Players: Players = game:GetService('Players')
local CollectionService: CollectionService = game:GetService("CollectionService")
local TweenService: TweenService = game:GetService("TweenService")
local ServerStorage: ServerStorage = game:GetService("ServerStorage")

--// Modules
local Modules = script.Parent.Modules

local SETTINGS : any = require(script.Parent.Settings)
local Pathfinding : any = require(Modules.Pathfinding)

-- errors
local errors = {
	humanoid = 'Humanoid is nonexistent in %q',
	enemy_object = 'Enemy model does not exist',
	registered = '%q has already been registered'
}

local warnings = {
	no_tags = 'Attacking allies is not available for %q, add tag to enemy to make a team. If you wish to generate teams automatically set GENERATE_TEAMS to true in the "Settings" module',
}


--// Functions
local function CHECK_TAGS(object: Instance)
	local TAGS : any = CollectionService:GetTags(object)

	if #TAGS == 0 then 
		if SETTINGS.DEBUG_MODE then
			if not SETTINGS.GENERATE_TEAMS then
				warn(string.format(warnings.no_tags, object.Name))
			end
		end
		if SETTINGS.GENERATE_TEAMS then
			CollectionService:AddTag(object, object.Name)
		end
	end

end

local function ADD_TARGET(attack_npcs: boolean, attack_ally: boolean, enemies: any, enemy: Instance, Tag: string)
	local Tags: any = CollectionService:GetTags(enemy)

	if #Tags > 0 
	then
		local found_ally: boolean = false
		for index = 1, #Tags 
		do
			if Tags[index] == Tag 
			then
				found_ally = true
				if attack_ally then
					table.insert(enemies, enemy)
				end
			end
		end


		if attack_ally and found_ally then
			table.insert(enemies, enemy)
		else
			if attack_npcs and not found_ally then
				table.insert(enemies, enemy)
			end
		end

	else
		if attack_npcs then
			table.insert(enemies, enemy)
		end
	end
end

local function CHECK_DUPLICATES(potential_enemies: any, object: Instance)
	local add_to_table : boolean = false

	for _, enemy in pairs(potential_enemies) 
	do
		if enemy == object 
		then 
			add_to_table = true
			break;
		end
	end

	return add_to_table
end

local function GET_DISTANCE(instance: Model, target: Model)
	return (instance.PrimaryPart.Position - target.PrimaryPart.Position).Magnitude
end

local function MAKE_ANIMATION(selection, default_animations)
	local animation : any = nil
	local id : any = default_animations[selection]
	
	if typeof(selection) == 'number' or typeof(selection) == 'string' then
		animation = Instance.new('Animation')
		animation.AnimationId = 'rbxassetid://'..id
		animation.Parent = workspace
	else
		animation = id
	end
	
	
	return animation
end


local function TWEEN(part, destination: Vector3)
	local tweenBase = TweenService:Create(part, TweenInfo.new(0.07), {Position = destination + Vector3.new(0, 0.5, 0)})
	tweenBase:Play()
	tweenBase.Completed:Wait()
end

local function TWEENMODEL(model, CF)
	local CFrameValue = Instance.new("CFrameValue")
	CFrameValue.Value = model:GetPrimaryPartCFrame()

	CFrameValue:GetPropertyChangedSignal("Value"):Connect(function()
		model:SetPrimaryPartCFrame(CFrameValue.Value)
	end)

	local tween = TweenService:Create(CFrameValue, TweenInfo.new(0.07), {Value = CF})
	tween:Play()

	tween.Completed:Connect(function()
		CFrameValue:Destroy()
	end)
end

------------------------------------------------------------------------
-- Visuals
local Visuals = {
	ChaseVisual = function(Unit : any, Origin : any)
		if not SETTINGS.VISUALIZE then return end
		local Part = Instance.new('Part')
		Part.Anchored = true
		Part.CanCollide = false
		Part.Color = Color3.fromRGB(255, 0, 0)
		Part.Size = Vector3.new(.1, .1, (Unit - Origin).Magnitude)--Vector4.new(1,1,1)--
		Part.CFrame = CFrame.new(Origin, Unit) * CFrame.new(0, 0, -Part.Size.Z/2)
		Part.Parent = workspace
	end,
}




------------------------------------------------------------------------
local Functionality = {}
Functionality.__index = Functionality
Functionality.__type = "EnemyAIFunctionality"


--// Variables
Functionality.ActiveTags = {}

local ActiveTags = Functionality.ActiveTags



function Functionality:InitChecks()
	if not self.Instance then error(errors.enemy_object) end
	
	local hum: Humanoid? = self.Instance:FindFirstChild('Humanoid') 
	
	if not hum and not SETTINGS.GENERATE_ANIMATOR then
		error(string.format(errors.humanoid, self.Instance.Name))
	elseif SETTINGS.GENERATE_ANIMATOR and not hum then
		
		local AnimationController = Instance.new('AnimationController')
		AnimationController.Parent = self.Instance
		
		local Animator = Instance.new('Animator')
		Animator.Parent = AnimationController

		self.Humanoid = Animator
	else
		self.Humanoid = hum
	end
	
	
	CHECK_TAGS(self.Instance)
end

function Functionality:HumanoidCheck()
	if self.Humanoid:IsA"Humanoid" then return true end
	return false
end

function Functionality:Light_Attack()
	if self.Attacking then return end
	
	
	self.Attacking = true
	
	local Animator = self.Humanoid
	local default_animations = self.Settings.default_animations
	
	local selection : Instance | number = math.random(1, #default_animations)
	local animation = MAKE_ANIMATION(selection, default_animations)
	
	
	local _animation = Animator:LoadAnimation(animation)
	_animation.Priority = 4
	_animation:Play()

	_animation.Stopped:Connect(function()
		task.delay(.5, function()
			self.Attacking = false
		end)
	end)
	
	if self.Settings.default_functions[selection] ~= nil then
		self.Settings.default_functions[selection](self.Target)
	end
	
	_animation.Stopped:Wait()
	
	
end


function Functionality:FindNearestTarget()
	if self:Health_Check() then return end
	
	
	local attack_range = self.Settings.attack_range
	
	local Overlap : any = OverlapParams.new(); 
	Overlap.FilterDescendantsInstances = {self.Instance}; 
	Overlap.FilterType = Enum.RaycastFilterType.Exclude; 
	
	local target_elements : any = workspace:GetPartBoundsInBox(self.Instance:FindFirstChild('HumanoidRootPart').CFrame, Vector3.new(attack_range, attack_range, attack_range), Overlap)
	local potential_enemies: any = {}
	local enemies : any = {}
	
	local closest : Model
	
	for _, instance in pairs(target_elements) 
	do
		if 
			instance.Parent:FindFirstChild('Humanoid') and
			instance.Parent.Humanoid.Health ~= 0 
		then
			
			local object: Instance = instance.Parent
			
			if not CHECK_DUPLICATES(potential_enemies, object) 
			then
				table.insert(potential_enemies, object)
			end
			
		end
	end
	
	
	for _, enemy in pairs(potential_enemies) 
	do
		local IS_PLAYER: Instance | boolean = Players:GetPlayerFromCharacter(enemy) or false
		
		if IS_PLAYER then
			
			if self.Settings.attack_players 
			then
				table.insert(enemies, enemy)
				continue;
			end
			
		else
			ADD_TARGET(self.Settings.attack_npcs, self.Settings.attack_ally, enemies, enemy, self.Tag)		
		end
	end
	
	for index, target in pairs(enemies) do
		if index == 1 then closest = target; continue end
		
		if GET_DISTANCE(self.Instance, target) < GET_DISTANCE(self.Instance, closest) then
			closest = target
		end
	end
	
	self.Target = closest
end


function Functionality:DisableStates()
	if not self:HumanoidCheck() then return end
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false)
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Swimming, false) -- Disable
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Landed, false)
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Seated, false) -- Disable
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.RunningNoPhysics, false)
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Freefall, false)
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.StrafingNoPhysics, false)
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.PlatformStanding, false)
	self.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Climbing, false) -- Disable
end


function Functionality:EnemySearch()
	if self:HumanoidCheck() then
		if self.Dead then return false; end
		if self:Health_Check() then return false; end
	end
	
	self:FindNearestTarget()

	if self.Target then
		
		print('test')

		local Origin : any = self.Instance.PrimaryPart.Position
		local Target : any = self.Target.PrimaryPart.Position
		local Unit = Origin + (Target - Origin).Unit * ((Target - Origin).Magnitude - self.Size.Z/2)

		Visuals.ChaseVisual(Unit, Origin)

		self.pathUnit = Unit

		self.path:Run(Unit)
		local s, e = pcall(function()
		end)
	end
	
	return true
end



function Functionality:Calibrate()
	
	if self:HumanoidCheck() then 
		self.Instance.Humanoid.Died:Connect(function()
			self.Dead = true
			self:Remove()
		end) 
	end
	
	
	self.Size = self.Instance:GetExtentsSize()
	self.Settings.attack_radius = math.ceil(self.Size.Z/2)
	
	self.path = Pathfinding.new(self.Instance)
	self.Visualize = true
	
	self.path.Reached:Connect(function()
		print(self.Instance)
		print(self.Target)
		if self.Target and math.floor(GET_DISTANCE(self.Instance, self.Target)) <= self.Settings.attack_radius then
			self:Light_Attack()
		end
	end)
	
	if not self:HumanoidCheck() then
		self.path.WaypointReached:Connect(function(model, lastWaypoint, nextWaypoint)
			TWEEN(model, CFrame.new(nextWaypoint.Position))
			self.path:Run()
		end)
	end
	
	while task.wait(SETTINGS.TICK_TIMER) do
		if not self:EnemySearch() then
			break;
		end
	end
end


function Functionality:_Init()
	self:InitChecks()
	
	if not ActiveTags[self.Tag] 
	then
		ActiveTags[self.Tag] = {self}
	else
		table.insert(ActiveTags[self.Tag], self)
	end
	

	local tagConnection: RBXScriptConnection

	local function onTagRemoved(instance: Instance)
		if instance == self.Instance 
		then
			tagConnection:Disconnect()
			self:Destroy()
		end
	end
	
	self:DisableStates()
	self:Calibrate()
	tagConnection = CollectionService:GetInstanceRemovedSignal(self.Tag):Connect(onTagRemoved)
end


function Functionality:Health_Check()
	if not self:HumanoidCheck() then return false end
	if not self.Instance:FindFirstChild('Humanoid') then
		return true	
	else
		if self.Instance.Humanoid.Health < 1 then
			self:Remove()
		end
	end
	
	return false
end

function Functionality:Remove()
	--self.Instance:Destroy()
end

return Functionality

Only real changes I made:

  • Removed the predefined section, replaced the variable/function calls associated with both.
  • Changed Overlap.FilterType = Enum.RaycastFilterType.Blacklist to Overlap.FilterType = Enum.RaycastFilterType.Exclude;
  • Changed the timer for the EnemySearch thing to a new variable in SETTINGS called SETTINGS.TICK_TIMER. I had personally set it to SETTINGS.TICK_TIMER = 0.01, but that’s likely super expensive. It seems to be the main bottleneck.
2 Likes

This would be killer! 3 months left in the year! Please DM me when you want someone to test it out! A lot of new stuff over the years with pathfinding, waypoints and such has happened!

2 Likes