Faster alternative to a .Touched event?

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)

I’ve tried that. Perhaps, It’s the client which can’t handle it

I found a ModuleScript for .Touched events that could be helpful as well:

I’ve also had the same problem as OP: when there were eg 5 enemies all attacking the players there were so many touch events that performence got nuked.

The solution was to create my own system where I track the position of damage zones attachments in my code and simulate a sphere in that position. So then I check if 2 spheres intersect.
It requires some extra code, but it’s instanely fast because sphere-to-sphere intersection is the fasted intersection you can do.

If your collision shape is more complex you just use more spheres on each part.

I am able to now have 20+ (and even more) mobs attack a player in melee without any performance drops, while before when using Touch events the framerate would get destroyed even on a very powerful PC.

Also by using your own sphere-intersection code you don’t have to use any Roblox physics API, which makes everything even faster.

The only optimization you might need to do is to use some quad/octree or larger zones for a broadphase pass in case you have many spheres as the algorithm is O(n^2) as every sphere has to check every other sphere.

2 Likes