Faster alternative to a .Touched event?

I was using a .Touched event to give EXP to all players who damaged my mob. Here’s how it works:
A .Touched event is fired, then the script checks if the colliding part is called “Handle”, with its Parent being a child of a Model. However, this is not important to the issue. What I’ve noticed is that when the mob gets close to a player character or when the mob dies, there are extreme frame drops. Usually to the point of bringing my 60 FPS (as there is an FPS cap) down to around 20.

At first I thought it might be an issue with my AI, so I disabled my AI script. The frame drops continued. I then tried disabling my EXP script, the one with the .Touched event. The frame drops disappeared. I optimized all my code inside the event, yet the frame drops did not go away. Commenting all the code inside the connected function (which made the connected function empty) still left frame drops, so it was easy to assume that the event was called too often, causing the game to lag even when the connected function was empty. Is there any alternatives I could use to avoid this? Sorry if I have explained badly.

EDIT: Here’s my code, I added comments to make it easier to understand.

local playersWhoHitMe = {} --Table for all players who damaged the mob. All players, who have damaged the mob at least once will get equal amounts of EXP as everyone else.
local playerIsInTheList = false
local expAmount = math.random(45, 65)
local insert = table.insert --A little variable I'm using for syntactic sugar

script.Parent.Touched:Connect(function(hit)
	if hit.Name == "Handle" and hit.Parent.Parent:IsA("Model") then --This should be self-explanatory
		local model = hit.Parent.Parent
		local player = game.Players:GetPlayerFromCharacter(model) --Gets the player instance
		if player then --If there actually is a player then:
			for i = 1, #playersWhoHitMe do --This for loop checks if the player is already in the list.
				playerIsInTheList = false
				if playersWhoHitMe[i].Name == player.Name then
					playerIsInTheList = true
				end
			end
			if playerIsInTheList == false then --If the player is not in the list, their name is inserted to the table.
				insert(playersWhoHitMe, player)
			end
		end
	end
	if hit.Parent:FindFirstChild("Owner") then --This is the same thing as the if statement before, except it's to make it count player abilities. I should probably put the below code in a function, so that I only have to make changes once instead of in both if statements.
		local model = hit.Parent
		local player = game.Players:FindFirstChild(model.Owner)
		if player then
			for i, v in pairs(playersWhoHitMe) do
				playerIsInTheList = false
				if v.Name == player.Name then
					playerIsInTheList = true
				end
			end
			if playerIsInTheList == false then
				insert(playersWhoHitMe, player)
			end
		end
	end
end)

EDIT 2: To clarify, even running a completely empty function with my .Touched event in my mob causes these frame drops.

EDIT 3 (3/8/2019): Here’s a little test place I prepared, containing only the lag-causing script. The onTouched() function is mostly commented out, but you could uncomment it and experiment. To experience the frame-drops, try pushing the mob from the front (the side which is facing you when you press play). The amount of touches each second is printed in the output (Thanks, @IdiomicLanguage). I’ll try using Region3s and I’ll report the results.
MiniblinTestPlace.rbxl (696.7 KB)

Note: I removed most MeshIDs to prevent mesh theft.

2 Likes

If I’m understanding this correctly, you’re checking if the mob is hit by something. Instead of this you can try this:

Check if the weapon blade/handle is touched. If it is touched, see if the Hit Part belongs to a mob. If it does, do what you need to do from that point on.

You should also post the code you’re using so that we can spot any mistakes.

4 Likes

Alright, I’ll post my code in a minute.

I’ve had frame issues with NPCs with functions connected to Touched on their root part before and a debounce did not help so I’m not sure if one will help you, but you should add one anyway. Almost always you want to debounce Touched code.

Before you run any code in the Touched function, check if it’s already running. Keep a variable outside of the function and set it to true when you enter the function and false when you exit. Wrap the whole thing in a check for if this value is false.

Alternatively to what you’re currently doing, as @LordHammy has mentioned you could move the Touched connection to the weapon instead. If the same issue still occurs, since it’s on the weapon you can disconnect the connection when the weapon hits an enemy, so it only ever fires once, but with this approach you’ll need to find a way to communicate between scripts to maintain the player list, through events, or through a shared module maybe. I’m assuming this is all server code.

4 Likes

I’ll try doing what @LordHammy suggested, as even having a .Touched event which fires an empty function generates great amounts of lag.

This section of code has a bug and a better solution. The bug is that playerIsInTheList is being set to false every time the loop runs, overwriting if playerIsInTheList was set to true in the last iteration. The result is that unless the player is the last player added to the list, then the list will add the player again. This could potentially make the list large in some circumstances. To fix this, simply set playerIsInTheList to false before the loop instead of inside it. It is also recommended to break the loop once the player is found in the list. Also, following the principle of least privilege, I’d move the declaration of playerIsInTheList to right before the loop.

To combine the two major cases (character models and abilities) I’d do this:

local player
if hit.Name == "Handle" and hit.Parent.Parent:IsA("Model") then
    player = game.Players:GetPlayerFromCharacter(hit.Parent.Parent)
elseif hit.Parent:FindFirstChild("Owner") then
    player = game.Players:FindFirstChild(hit.Parent.Owner)
end
if player then
    -- put the loop and other code in here
end

Also, a much more efficient way than a for loop would be to store the players who touched the NPC the key in the table, rather than the value. This allows Lua to use a hash able written in optimized C to almost yield O(1) constant time lookup rather than a O(n) linear search. All of the code put together looks like this:

local playersWhoHitMe = {}

local function onTouched(hit)
    local player
    if hit.Name == "Handle" and hit.Parent.Parent:IsA("Model") then
        player = game.Players:GetPlayerFromCharacter(hit.Parent.Parent)
    elseif hit.Parent:FindFirstChild("Owner") then
        player = game.Players:FindFirstChild(hit.Parent.Owner)
    end
    if player then
        playersWhoHitMe[player] = true
    end
end

script.Parent.Touched:Connect(onTouched)

This function should never cause lag. If there is lag after using this, something else is wrong.

There still is lag, even when running a completely empty function with my mob’s .Touched event.

I doubt a touched event can run so much that calling an empty function creates lag. I could see it happening with thousands of parts colliding in a ball, but the physics would create lag before the event would. I could also see networking causing lag in some circumstances.

It’d be easier to figure out what is going on if we had a minimal place file to examine, otherwise I’ll have to ask you a lot of questions. :slight_smile: Is the touched event being run in a server or local script? Does the server or client own the NPCs when they are far, and which owns them when a client is near and you see lag?

Could you run this script in your place:

_G.touchCount = 0
local lastRun = tick()
while wait(1) do
    local t = tick()
    print('Touch count this second is:',  _G.touchCount)
    print('Elapsed Time: %s', t - lastRun)
    _G.touchCount = 0
    lastRun = tick() -- Don't include time spent printing
end

And use this touch callback:

local function onTouched()
    _G.touchCount = _G.touchCount + 1
end
script.Parent.Touched:Connect(onTouched)

and report the touch count and elapsed time for a couple seconds before and after you get near a NPC?

To get an idea of roughly how many events can fire with an empty function per a second, run this script in an empty place:

local event = Instance.new 'BindableEvent'
event.Parent = workspace
event.Event:Connect(function() end)

local qty = 10 -- Start at 10 fires every 1/30th of a second (300 fires/s)
while true do
    local start = tick()
    for i = 1, 30 do
        wait()
        for j = 1, qty do
            event:Fire()
        end
    end
    local elapsed = tick() - start
    print(('Rate: %d fire/second'):format(30 * qty))
    print('Elapsed:', elapsed)
    if elapsed >= 4 then -- ( <= 15 FPS )
        break
    end
    qty = qty * 2
end

print 'Stopped test due to lower than 15 FPS'

It will print the current rate every second, along with how many seconds have actually passed. Since wait() stops your script until the next frame Lua is run, at 60 FPS the script will run 30 times a second. If it takes longer to accomplish 30 iterations, then the frame rate has dropped below 60 FPS. So, I think the Elapsed time is a good measure of lag. I’ve added a safety stop in case you stay in the game too long. It’ll stop after running four seconds with less than 15 frames a second.

You can use Region3s instead.

Never thought of that, thanks! Sorry for the late reply, I was out of the house yesterday.

I’ll try that, thanks :smiley:. Sorry for the late reply, I was out of the house yesterday.

You should use .Touched, and using Region3 will make it x4 more laggy, the problem with your code is that there is not a debounce, and the Touched event is fired 100 times per second, Just add a debounce of at least half a second.

1 Like

You’re right about the debounce, but where did you get this from?

1 Like

Because is more complicated, Touched is just a event that is fired when a part collide with another part, Region3 is a function that get all parts in Workspace inside a Box, and you sometimes need to include White lists or Black lists, so you need to get all the parts and insert them in a table.

…what? I think you’re underestimating the computational capability of Region3 methods, or overexaggerating the negligible time difference between using Touched and Region3.

Region3 is not complicated. Declaring a table and filling it with elements is not expensive. Calling a Region3 method with a whitelist or blacklist is not expensive. It’s a decent alternative and depending on the use case, has better use to OP than Touched.

3 Likes

Just got the time to try out what you said. Regularly, when the mob is away from a character, the Touch count each second is <= 50. When the mob gets close to the character, it fluctuates to around 600, making even your simple onTouched() function create lag. Though I did notice that the elapsed time does not rise above 1.03, even with the highest spike of 770 touches a second. I ran your second test in an empty place and the test stopped at 2457600 fires/second, as can be seen in the output:

 Rate: 307200 fire/second
 Elapsed: 1.510064125061
 Rate: 614400 fire/second
 Elapsed: 1.9960877895355
 Rate: 1228800 fire/second
 Elapsed: 2.9932744503021
 Rate: 2457600 fire/second
 Elapsed: 4.5036232471466
 Stopped test due to lower than 15 FPS

I am preparing a minimal test place, and will upload it soon.

Thanks for posting the test place, generally that really helps. However, I’m not able to replicate the issue using it. I have a reported elapsed time of between 1.0004 to 1.017 with 0 touch events fired while away from the NPC and always under 200 when running into it and trying to cause as many events as possible. Here are my results:

Rate: 300 fire/second
Elapsed: 1.185045003891
Rate: 600 fire/second
Elapsed: 1.0015456676483
Rate: 1200 fire/second
Elapsed: 0.99893712997437
Rate: 2400 fire/second
Elapsed: 0.99820137023926
Rate: 4800 fire/second
Elapsed: 0.99958157539368
Rate: 9600 fire/second
Elapsed: 1.0021493434906
Rate: 19200 fire/second
Elapsed: 1.0152518749237
Rate: 38400 fire/second
Elapsed: 1.0162434577942
Rate: 76800 fire/second
Elapsed: 1.1847007274628
Rate: 153600 fire/second
Elapsed: 1.4028739929199
Rate: 307200 fire/second
Elapsed: 1.5088093280792
Rate: 614400 fire/second
Elapsed: 1.7225317955017
Rate: 1228800 fire/second
Elapsed: 2.4851777553558
Rate: 2457600 fire/second
Elapsed: 3.5397212505341
Rate: 4915200 fire/second
Elapsed: 6.049498796463
Stopped test due to lower than 15 FPS

Comparing my results to yours, I suspect that you started to see lag around 76,800 events per a second like me. Running this on a server, I there was no reported slow down until an impressive 153,600 events a second. Here is the server report:

Putting all of this together – the place not replicating the event for me, your report, my report, and the server’s report, and the relatively low number of events reported to cause the lag (770), I think that more is at play here. Does this test place actually cause the issue for you? If so, does it persist when you start a server? Could one of your plugins be causing it?

I’ll check, though I started investigating the issue when multiple users had reported the issue in-game. Are you sure you don’t get any frame-drops? Have you checked the heartbeat in the “Render” panel while pushing the mob? Specs definitely aren’t an issue, as I have an AMD Ryzen 7 1700X (8 cores, 16 threads) and 16 GB of DDR4 RAM. I feel GPU is irrelevant here.

I know people are going to recommend region3 and I agree It’s fast but using it frequently will create a distortion of lag due to region3 making complicated tasks. I’m still not sure what other alternatives can be used.

Perhaps disconnecting the event?

local connection
function onTouch()
      connection:Disconnect() --Now the .Touched event is disconnected so it won't be called more.
      task.wait(1)
      connection = script.Parent.Touched:Connect(onTouch) --reconnect the event
end

connection = script.Parent.Touched:Connect(onTouch)