Improvements on my Enemy Detection Code

Hi all! My code below is used as apart of a combat class that I made in a module script. The aim of it is to detect if the player is: in range of the enemy, within the FOV of the enemy and can directly be seen by the enemy (through raycasting). If all of those parameters are met, the function will return true, however if not, the function will return false.

PlayerInputHandler:

RunService.RenderStepped:Connect(function()
	CombatClass.CheckPlayersRangeFromEnemy(HRP)
end)

CombatClass:

local function IsEnemyInRange(enemy, comparisonValue, HRPPos)
	if enemy ~= nil then
		if (HRPPos - enemy).Magnitude < comparisonValue then
			return true
		end
	end
end

local function IsPlayerInRange(HRPPos)
	local EnemiesInRangeOf = {}
	local EnemiesInFOVOf = {}
	
	for _, enemy in pairs(enemiesGroup:GetChildren()) do
		if IsEnemyInRange(HRPPos, enemy:FindFirstChild("ViewDistance").Value, enemy.PrimaryPart.Position) then
			table.insert(EnemiesInRangeOf, enemy)
		end
	end
	
	for i = 1, #EnemiesInRangeOf do
		local enemyToPlayer = (HRPPos - EnemiesInRangeOf[i].PrimaryPart.Position).Unit
		local enemyLookVector = EnemiesInRangeOf[i].PrimaryPart.CFrame.LookVector
		
		local dotProduct = enemyToPlayer:Dot(enemyLookVector)
		
		if dotProduct > 0.7 then
			table.insert(EnemiesInFOVOf, EnemiesInRangeOf[i])
		end
	end
	
	return EnemiesInFOVOf
end

local function CanEnemySeePlayer(enemy, HRP)
	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {enemy}
	raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
	
	local rayDirection = (HRP.Position - enemy.PrimaryPart.Position)
	local raycast = workspace:Raycast(enemy.PrimaryPart.Position, rayDirection, raycastParams)
	
	if raycast then
		local hitPart = raycast.Instance
		
		if hitPart.Parent.PrimaryPart == HRP then
			return true
		else
			return false
		end
	else
		return false
	end
end

function CombatClass.CheckPlayersRangeFromEnemy(HRP)
	local EnemiesInFOVOf = IsPlayerInRange(HRP.Position)
	
	for i = 1, #EnemiesInFOVOf do
		local enemyDetectionResult = CanEnemySeePlayer(EnemiesInFOVOf[i], HRP)
		print(enemyDetectionResult)
	end
end

At first I had this function run from the side of the enemy, meaning that every enemy would always be checking to see if the player can be seen by them, however I found it to be extremely laggy and unoptimized, thus moving it to the client side. I currently have no idea on how to improve it or make it more efficient, since it really isn’t great to always be running a raycast from every enemy, to the player, but if you can please give me any feedback, that would be great! :smiley:

1 Like

Major thing you want to stay away from is running code in RenderStepped unless you strictly need logic to occur before a frame gets rendered. Use Heartbeat instead, this will cause your range checker to perform its work only after physics have been simulated for the current frame (sounds reasonable enough). Already a huge improvement because it won’t cause slow downs in frame execution.

Might be a microoptimisation but the one other improvement I can think of is not creating a new RaycastParams every time you call CanEnemySeePlayer since the only thing that changes in each of those instances is the enemy being passed. There is a note that since it’s one of the few/only datatypes with directly mutable properties, RaycastParams should be reused as much as possible. Create one RaycastParams at the top of your script and use that for every raycast, just change the FilterDescendantsInstances. If you want to optimise that more, blacklist every enemy and then just change from which points you’re raycasting from and to.

Looks good otherwise, can’t think of any better ways to optimise this at the moment.

2 Likes

Thank you for your help! Much appreciated

Just so you know that changing the filter descendants instances won’t work, as indexing it will return you a deep copy of it. Its necessary to create a new ray cast params for now until Roblox solves this.

image

TIL wacky FilterDescendantsInstances behaviour. Thanks for pointing that out, would be especially problematic if you hold a filter table as an upvalue (I think I do :cold_face:). Could you not hypothetically just substitute the table each time instead of creating a new RaycastParams though? The problem seems to be table deep copying, not RaycastParams itself, which creating a new object every time is sort of iffy. This looks resolvable if you hand-set the whole table rather than directly mutating.

1 Like