Hello!
I am trying to make a game similar to Decaying Winter, and something that I cannot seem to get right are the NPCs. So many games have NPCs that follow the player around to attack them, but for some reason mine always fling the player. I’ve tried setting parts to massless, creating a custom hitbox, changing Collisions in the CollisionGroup Editor, but nothing I try seems to work.
Here is the video for a demonstration to what I am trying to avoid:
I still need the NPCs to be able to collide with each other and the player, so if anyone has any suggestions, it’d be much appreciated.
Here is my NPC code down below, if it’ll help at all:
local Lib = require(game.ReplicatedStorage.Library)
local MyHumanoid:Humanoid = script.Parent
local MyCharacterModel:Model = MyHumanoid.Parent
local PathfindingService = game:GetService("PathfindingService")
local MyEnums = require(game.ReplicatedStorage.ModuleScripts.Enums)
local RunService = game:GetService('RunService')
-- first, generate all the necessary values
local NpcConfigFolder = MyHumanoid:FindFirstChild("NpcValues") or Lib.ObjectFuncs.ShorthandNew("Configuration",MyHumanoid,"NpcValues")
local MyBehaviorState:StringValue = MyHumanoid.States:FindFirstChild("BehaviorState") or Lib.ObjectFuncs.ShorthandNew("StringValue",MyHumanoid.States,"MyBehviorState",{["Value"] = "Idle"})
local SeeDistance:NumberValue = MyHumanoid.NpcValues.SeeDistance
local FeelDistance:NumberValue = MyHumanoid.NpcValues.FeelDistance
local ArmState:StringValue = MyHumanoid.States.ArmState
local LegState:StringValue = MyHumanoid.States.LegState
local MyTargetHumanoid:ObjectValue = MyBehaviorState.TargetHumanoid
local HumanoidsOfInterest:{[Humanoid]: boolean} = {} -- if there are more than 1 enemies inside the AggroRange, they'll show up here
-- Now, do the more Runtime specific variables
local TargetLastKnownLocation:Vector3 = Vector3.zero
local CurrentTargetBodyParts:{Part|MeshPart} = {}
local CurrentMovementThreadId = 0 -- used to stop more than one pathfinding thread from running at once.
-- Aggro state has a couple values:
-- Idle (basically perfectly still)
-- Idle Wandering (Passive, not found target, just wandering to a nearby randomly chosen location)
-- Investigating (Heard gunshot, or Flare trap activated, now moving to the source of the disturbance)
-- Searching (target found, then lost, now moving towards their last known location)
-- AggressivePath (target found, now actively moving towards their location. However, complex pathing required)
-- AggressiveDirect (target found, now actively moving towards their location. Simple path, just a straight line)
function CheckForClosestVisibleEnemy():Humanoid
--if #HumanoidsOfInterest == 0 then return nil end
-- first, purge the Humanoids from the list that don't exist anymore
--remove those who don't exist, are dead, or are too far away
local OrderedHumanoidList:{Humanoid} = {}
for i,v in pairs(HumanoidsOfInterest) do
if v ~= true then HumanoidsOfInterest[i] = nil end
if i then
if i.Health <= 0 then -- is dead
HumanoidsOfInterest[i] = nil
else
if (i.Parent.PrimaryPart.Position - MyCharacterModel.PrimaryPart.Position).Magnitude > SeeDistance.Value then
-- is too far away
HumanoidsOfInterest[i] = nil
else
-- a valid humanoid to want to investigate
OrderedHumanoidList[#OrderedHumanoidList + 1] = i
end
end
else -- doesn't exist
HumanoidsOfInterest[i] = nil
end
end
-- then, sort through the remaining humanoids
-- table.sort moves smallest to first in line, so default to smaller < larger
table.sort(OrderedHumanoidList,function(a:Humanoid,b:Humanoid)
return (MyCharacterModel.PrimaryPart.Position - a.Parent.PrimaryPart.Position).Magnitude < (MyCharacterModel.PrimaryPart.Position - a.Parent.PrimaryPart.Position).Magnitude
end)
-- now, raycast to see if the target is actually visible
local MyBodyParts = Lib.ObjectFuncs.FilterTable(MyCharacterModel:GetDescendants(),{"Part","MeshPart","Union"})
for i,v in pairs(OrderedHumanoidList) do
local TargetPart:Part = v.Parent.PrimaryPart
local NewRaycast = Lib.ObjectFuncs.ShorthandRaycast(MyCharacterModel.PrimaryPart.Position,(TargetPart.Position - MyCharacterModel.PrimaryPart.Position).Unit * SeeDistance.Value,MyBodyParts,MyEnums.CollisionGroups.Eyesight)
if NewRaycast.Instance then
-- actually hit something, otherwise the enemy is too far, but.. they should have been filtered out so... idk
if NewRaycast.Instance.Parent == TargetPart.Parent then
-- first thing hit was the target, meaning they are in sight! Otherwise, loop to next item
return v
end
end
end
-- none found, so just return nothin'
return nil
end
function CreateAggroBoundary()
local NewPart:Part = Lib.ObjectFuncs.ShorthandNew("Part",MyCharacterModel,"SightAggroSphere",{
["Size"] = Vector3.new(SeeDistance.Value,SeeDistance.Value,SeeDistance.Value);
["Shape"] = Enum.PartType.Ball;
["CanCollide"] = false;
["CanTouch"] = true;
["CollisionGroup"] = Lib.Enums.CollisionGroups.Eyesight;
["Transparency"] = 0.7;
["Anchored"] = false;
["Position"] = MyCharacterModel.PrimaryPart.Position;
})
local NewWeld:Motor6D= Lib.ObjectFuncs.ShorthandNew("Motor6D",NewPart,"AggroSphereWeld",{
["Part0"] = MyCharacterModel.PrimaryPart;
["Part1"] = NewPart;
--["C1"] = CFrame.new(0,0,-SeeDistance.Value);
})
return NewPart
end
function SetupAi(AggroSphere:Part)
-- first, add all the current CharacterParts to the BlacklistedParts list
local BlacklistedParts:{Part|MeshPart} = {} --Lib.ObjectFuncs.FilterTable(MyCharacterModel:GetDescendants(),{"Part","MeshPart","Union"})
for i,v in pairs(Lib.ObjectFuncs.FilterTable(MyCharacterModel:GetDescendants(),{"Part","MeshPart","Union"})) do BlacklistedParts[v] = true end
-- setup the Touched event
AggroSphere.Touched:Connect(function(OtherPart)
-- first, see if this part is already blacklisted
if BlacklistedParts[OtherPart] then return end
-- Second, now that this is a new part, see if they are of a different faction
local OtherHum:Humanoid = OtherPart.Parent:FindFirstChild("Humanoid")
if not OtherHum then BlacklistedParts[OtherPart] = true; return end
if HumanoidsOfInterest[OtherPart] then return end -- already investigating this humanoid
if Lib.ObjectFuncs.SafelyGetPropertyFromChild(OtherHum,"Faction","Value") == MyHumanoid.Faction.Value then
BlacklistedParts[OtherPart] = true; return
end
-- now, check to see if this body part is a part of the humanoid I am already hunting
if CurrentTargetBodyParts[OtherPart] then return end
-- ok, we have determined that this is an enemy humanoid that we are NOT currently targeting
-- Later, add in a check to see if I already am Aggroed and have a target I'm going towards. If I do,
-- then ask if this new enemy is closer than I am. If not, well, aggro onto this new person, then
if OtherHum.Health <= 0 then return end
-- otherwise, this is a valid enemy. Add it to the Investigation List
HumanoidsOfInterest[OtherHum] = true
-- Ok so, we DEFINITELY have our target. Now, we need to begin moving towards them
-- first, add all parts to CurrentTargetBodyParts
CurrentTargetBodyParts = {}
for i,v in pairs(Lib.ObjectFuncs.FilterTable(OtherHum.Parent:GetDescendants(),{"Part","MeshPart","Union"})) do CurrentTargetBodyParts[v] = true end
end)
end
function StartNewMovementThread(SkipDirectMoveToEnemy:boolean)
CurrentMovementThreadId += 1
local MyThreadId = tonumber(CurrentMovementThreadId)
MyBehaviorState.Value = Lib.Enums.EnemyBehaviorState.Aggressive_Direct
-- if at any point, CurrentMovementThreadId ~= MyThreadId, stop this function thread at once
print("Starting new Thread! " ..tostring(MyThreadId))
local MyTargetPart:Part = MyTargetHumanoid.Value.Parent.PrimaryPart
local CurrPath = PathfindingService:CreatePath({})
local MyTargetPart:Part = MyTargetHumanoid.Value.Parent.PrimaryPart
while CurrentMovementThreadId == MyThreadId do
-- start moving towards them
-- start by just moving towards them using hum:MoveTo(Pos,Part)
-- and repeat that every 3 seconds, asking if the enemy can see them still. If not,
-- then move them to the enemy's last known location using a path.
-- Every time they reach a new waypoint, find out if the ClosestVisibleEnemy has changed
-- So, loop just began, so we know that the ClosestVisibleEnemy is MyTargetHumanoid
-- first, just begin walking to them, until we lose sight of them
if not SkipDirectMoveToEnemy then
local ClosestEnemy = MyTargetHumanoid.Value
repeat
-- do this every 0.1 seconds for 1.5 seconds
local MiniChaseStartTime = time()
while time() - MiniChaseStartTime <= 1.5 do
-- moves towards the enemy, backed up by 1 unit, + their velocity by 1 unit
MyHumanoid:MoveTo(MyTargetPart.Position - (MyTargetPart.Position - MyCharacterModel.PrimaryPart.Position).Unit)
--MyHumanoid:MoveTo(MyTargetPart.Position - (MyTargetPart.Position - MyCharacterModel.PrimaryPart.Position).Unit + (MyTargetPart.AssemblyLinearVelocity).Unit)
task.wait(0.02)
end
ClosestEnemy = CheckForClosestVisibleEnemy()
until ClosestEnemy ~= MyTargetHumanoid.Value
-- now, the closest visible enemy is someone else
-- first, if the Closest visible enemy is someone else, start a new thread
if ClosestEnemy ~= nil then
-- target someone else
MyTargetHumanoid.Value = ClosestEnemy
script.StartNewMovementThread:Fire()
return
end
-- current target is lost, and there's no one else, so investigate by going to their last known location
-- first, make sure their still alive. If they're not, swap to idle
if Lib.ObjectFuncs.SafelyGetProperty( Lib.ObjectFuncs.SafelyGetProperty(MyTargetHumanoid,"Value",nil),"Health",0) <= 0 then
-- target is either dead or they don't exist anymore, so swap to idle
MyHumanoid:MoveTo(MyCharacterModel.PrimaryPart.Position)
MyBehaviorState.Value = Lib.Enums.EnemyBehaviorState.Idle
return
end
end
-- if we made it this far down, we need to investigate, and our enemy is still alive
MyBehaviorState.Value = Lib.Enums.EnemyBehaviorState.Searching
TargetLastKnownLocation = MyTargetPart.Position
-- now, begin the complex path towards the last known location
CurrPath:ComputeAsync(MyCharacterModel.PrimaryPart.Position,TargetLastKnownLocation)
if CurrPath.Status == Enum.PathStatus.Success then
-- complex path found!
local Waypoints:{PathWaypoint} = CurrPath:GetWaypoints()
for i=1, #Waypoints,1 do
local Reached = false
repeat
-- in case a new humanoid enemy gets found
if CurrentMovementThreadId ~= MyThreadId then return end
MyHumanoid:MoveTo(Waypoints[i].Position)
Reached = MyHumanoid.MoveToFinished:Wait()
until Reached
-- waypoint finally reached!
-- now check to see where the nearest visible enemy is
-- if it's a different enemy, Create new thread
-- if it is the same enemy, check to see if they're new position is reachable from last waypoint
-- if yes, keep going
-- if no, recaculate path by doing StartNewThread(Yes, please skip Direct Pathing)
-- if there's no visible enemy, keep following this path
local Closest = CheckForClosestVisibleEnemy()
if Closest == nil then
-- Nobody else to switch to, so just keep looping
elseif Closest ~= MyTargetHumanoid.Value then
-- change targets
MyTargetHumanoid.Value = Closest
script.StartNewMovementThread:Fire()
return
elseif Closest == MyTargetHumanoid.Value then
local MyRay = Lib.ObjectFuncs.ShorthandRaycast(
Waypoints[#Waypoints].Position,
(MyTargetPart.Position - Waypoints[#Waypoints].Position).Unit * SeeDistance.Value
,{MyCharacterModel:GetDescendants()}
,Lib.Enums.CollisionGroups.Eyesight)
if MyRay.Instance then
if MyRay.Instance:IsDescendantOf(MyTargetHumanoid.Parent) then
-- ray hit enemy, meaning that the path is still usable and we do not need to recalculate
-- so, just move on to the next waypoint
else
-- ray hit something else, like a wall, meaning that the path needs
-- to be recalculated
-- so, start a new Thread
script.StartNewMovementThread:Fire(true)
return
end
else
-- Nothing hit, meaning Current Target is too far away somehow?
-- Anyway, I need to recaculate path
script.StartNewMovementThread:Fire(true)
return
end
end
end
else -- Enemy is unreachable
MyHumanoid:MoveTo(MyCharacterModel.PrimaryPart.Position)
MyBehaviorState.Value = Lib.Enums.EnemyBehaviorState.Idle
return
end
end
end
-- really great idea.
--[[
Every time the Enemy gets to a new Waypoint, ask
(1) Raycast from MyPos to EnemyHumanoidPos. If it's NOT blocked, update TargetLastKnownLocation
(2) if I raycast from FinalWaypoint.Pos -> TargetLastKnownLocation, is it blocked?
(3) If not, continue to the next waypoint and don't recalculate path
(4) If YES, it IS blocked, recalculate Path
]]
-- If Idle, check every 2 seconds if there is someone inside the AggroSphere. If so,
-- use Raycast from RootPart -> RootPart
-- Now, setup useful connections
local CurrEnemyCheckThread = 0
MyBehaviorState.Changed:Connect(function(newVal)
if CurrEnemyCheckThread > 1 then return end
CurrEnemyCheckThread += 1
-- check for enemies during Idle
if newVal ~= Lib.Enums.EnemyBehaviorState.Aggressive_Direct and newVal ~= Lib.Enums.EnemyBehaviorState.Aggressive_Path and newVal ~= Lib.Enums.EnemyBehaviorState.Searching then
while MyBehaviorState.Value ~= Lib.Enums.EnemyBehaviorState.Aggressive_Direct and MyBehaviorState.Value ~= Lib.Enums.EnemyBehaviorState.Aggressive_Path do
task.wait(2)
print("checking!")
if MyBehaviorState.Value ~= Lib.Enums.EnemyBehaviorState.Aggressive_Direct and MyBehaviorState.Value ~= Lib.Enums.EnemyBehaviorState.Aggressive_Path then
local Closest = CheckForClosestVisibleEnemy()
print("Closest: " ..tostring(Closest))
if Closest ~= MyTargetHumanoid.Value and Closest ~= nil then
MyTargetHumanoid.Value = Closest
StartNewMovementThread()
end
end
end
end
CurrEnemyCheckThread -= 1
end)
script.StartNewMovementThread.Event:Connect(function()
StartNewMovementThread()
end)
-- now, run the final setup Functions to get the ball rolling
local AggroSphere = CreateAggroBoundary()
SetupAi(AggroSphere)
MyBehaviorState.Value = MyEnums.EnemyBehaviorState.Idle