How do I detect if player is within radius without loops

I am trying to make a door-opening system where if a player gets close enough to the door (let’s say 3 studs), a floating icon with the letter E appears, and when pressed it will simply open the door.

You can see examples of this in many games because it is a common feature (mostly first-person horror games).
RobloxStudioBeta_2019-07-06_20-01-56

The problem I encountered is when it comes time to actually detect if the player is in the radius to be eligible to open the door!

I know there are other ways of doing this, but those use while loops and can significantly slow down the user experience. I’ve already searched the devforum and I’ve found this post asking a similar question.

I went ahead and used the solution to this post using the code below, but it only runs once and doesn’t seem to work properly and this is very frustrating.

Any help would be greatly appreciated!

local workSpace = game:GetService("Workspace")
local players = game:GetService("Players")
local RunService = game:GetService("RunService")

local door_radius = 3
local player = players.LocalPlayer
 
RunService.RenderStepped:Connect(function(step)
	for i,v in pairs (workSpace:WaitForChild("Doors"):GetChildren()) do
		if player:DistanceFromCharacter(Vector3.new(v.Center.Position)) < door_radius then
			print("HOT HOT HOT") -- player is within radius
		end
	end
end)
8 Likes

Instaid of using RenderStepped I would recommand using InputBegan. This only fires every time the user presses a button or clicked, which should be accurate enough for your system.
EDIT: I would also change player:DistanceFromCharacter to (character.HumanoidRootPart.Position-v.Center.Position).magnitude. Why? I just prefer it. You can change it back but make sure to type player:DistanceFromCharacter(v.Center.Position)! Not player:DistanceFromCharacter(Vector3.new(v.Center.Position)).

local workSpace = game:GetService("Workspace")
local players = game:GetService("Players")
local RunService = game:GetService("RunService")
local uiService = game:GetService("UserInputService")

local door_radius = 3
local player = players.LocalPlayer
 
uiService.InputBegan:Connect(function(key)
	for i,v in pairs (workSpace:WaitForChild("Doors"):GetChildren()) do
		if (player.Character.HumanoidRootPart.Position-v.Center.Position).magnitude < door_radius then
			print("HOT HOT HOT") -- player is within radius
		end
	end
end)

The problem with the old code is that you tried creating a vector3 with a vector3. Aka: Vector3.new(Vector3.new()). v.Center.Position Returns a vector3 not a number.

13 Likes

By God no don’t tell me you actually do that. Calling this function every time a player pushes any input is an absolutely garbage way of going about this and also a terrible reinvent of the wheel for loops. I would not advise ANYONE, including on-coming readers, to do this.

You cannot do any constant checks without loops. I don’t know what kind of bias against loops OP has against loops but loops are only taxing on performance if you have expensive functions in them. Most likely misleading or misunderstood advice.

Please just use a loop @TheGreenSuperman. You’re comparing iterating through doors and checking distance every time a player pushes input versus running something with little expense every frame (by the way, change to Stepped - RenderStepped should not be used here).

DistanceFromCharacter handles the magnitude check in-engine and accounts for edge cases as well, such as non-existent limbs. Use DistanceForCharacter and check for non-zero or less-than-max values.

10 Likes

Could you please explain why using Input is worse than a loop? Using a mix of InputBegan and InputEnded you will get accurate enough results. You won’t need frame perfect accuracy for such a small thing. Using loops is horrible because if the game uses, let’s say 1000 doors. You would be running this 60000 times a second. Sure the code isn’t expensive, but it will make a difference. Definitly when the user is afk. What will also work is something the lines of:

local rService = game:GetService("RunService")
local uiService = game:GetService("UserInputService")
local flag = false

iuService.InputBegan:Connect(function()
flag = true
end)
iuService.InputEnded:Connect(function()
flag = false
end)

rService.Heartbeat:Connect(function()
if flag then
--rest code
end
end)

This is not a loop, will only run when the player pressed/isholding an input. This way it will save on processing power while still being very accurate. Why is it accurate? The player will need to move into the radius. Thus requiring an input.

:DistanceFromCharacter() vs (character.HumanoidRootPart.Position-v.Center.Position).magnitude The first one is better and faster too which was pointed out by Halalaluyafail3. I didn’t know that by earlier.

2 Likes

What if I am only holding W and my character walks by an interactable object and I don’t notice it because my input hasn’t changed?

What if an interactable object moves in range of my character while I am standing idle?

If you are concerned with performance, you can lower the frequency of the loop. But so far there’s no reason to so suspect a performance problem. You are solving a problem that doesn’t exist yet. It is not likely the game uses 1000 doors.

4 Likes

The code I’ve written solves this. It checks if the user is holding down a button. If it does it loops until the user let go of the button.

This is something I haven’t tought about tho, I guess you got a point there! :slight_smile:

I did a test in studio, and DistanceFromCharacter() was quicker.

Code
local Players = game:GetService"Players"
local plr = Players:FindFirstChildWhichIsA"Player" or Players.PlayerAdded:Wait()
local char = plr.Character or plr.CharacterAdded:Wait()
wait(1)
local vec = Vector3.new(100,100,100)
local t = tick()
for i=1,100000 do
    local dist = plr:DistanceFromCharacter(vec)
    if dist ~= 0 and dist < 3 then

    end
end
print(tick()-t)
local t2 = tick()
for i=1,100000 do
    local dist = (char.HumanoidRootPart.Position-vec).Magnitude
    if dist < 3 then

    end
end
print(tick()-t2)

For DistanceFromCharacter() I got about 0.067 seconds, and for magnitude checks I got about 0.169 seconds. (Old VM, in the new VM they were both about 3x quicker)

2 Likes

Oh well, thank you for correcting me! I’ll edit the comment.

Input is worse than a loop because:

Input doesn’t account for scenarios the same way a loop does, which is the appropriate resolution to issues like this. It’s unnecessary overhead to write code you don’t need wherein better or proper methods exist of doing something like this. Input is not constant, a loop is.

“Accurate enough”, while a loop will give you direct and perfect results provided they’re done right.

While you’re right that you don’t need perfect frame accuracy to check between doors, I don’t know how this serves as a counterpoint to loops - it doesn’t. Perfect frame accuracy isn’t being sought here, it’s proper practice for checking distances between something.

Using loops are not horrible and if you have this many doors, that’s why you use different approaches like checking for doors within a player’s region instead of iterating over every door regardless of its distance to the player.

Region check with a whitelist set to the descendants of the doors folder accomplishes that task, which then you can then check which door in the region around the player is the closest. Think differently, just because the number of doors increases doesn’t mean a loop becomes bad. Use it wisely.

That’s still a pseudoloop because you’re attaching code to be ran after physics are simulated by the engine. Input is almost always constant in certain games so you’re writing unnecessary code to active a loop for whatever reason. This can quickly lead into spaghetti code territory.

No, your solution doesn’t save processing power nor is it accurate at all. You’re still doing the same thing that shouldn’t be done as well as what OP did wrong, which is using a loop improperly.

Code you run in loops needs to be catered towards a certain situation. A small map with not many doors can do a magnitude check which is near-instantaneous. A large map with many doors should only be checking for doors near the player and then checking which of those is the closest.

7 Likes

Would the fast distance comparison be faster?

local DoorRadius = 3^2

local Vector = (Character.HumanoidRootPart.Position - v.Center.Position)
local Distance = (Vector.X*Vector.X + Vector.Y*Vector.Y + Vector.Z*Vector.Z)

local IsInRage = Distance <= DoorRadius

How exactly is this calculating the distance?

local Vector = Vector3.new(1,1,1)
local Distance = (Vector.X*Vector.X + Vector.Y*Vector.Y + Vector.Z*Vector.Z)
print(Distance)

This prints out three, but if the difference beween the positions was only 1,1,1 then the distance between them would only be about ~1.73 not 3. (i assume this is why you set the radius to 9 instead of 3?)
This seems to only become more exaggerated as the distance between them increases.

Well, i tested it, and it was even worse.

Code
local Players = game:GetService"Players"
local plr = Players:FindFirstChildWhichIsA"Player" or Players.PlayerAdded:Wait()
local char = plr.Character or plr.CharacterAdded:Wait()
wait(1)
local vec = Vector3.new(100,100,100)
local t = tick()
for i=1,100000 do
    local dist = plr:DistanceFromCharacter(vec)
    local IsInRange = dist ~= 0 and dist < 3
end
print(tick()-t)
local t2 = tick()
for i=1,100000 do
    local dist = (char.HumanoidRootPart.Position-vec).Magnitude
    local IsInRange = dist < 3
end
print(tick()-t2)
local t3 = tick()
local DoorRadius = 3^2
for i=1,100000 do
    local Vector = (char.HumanoidRootPart.Position-vec)
	local Distance = (Vector.X*Vector.X + Vector.Y*Vector.Y + Vector.Z*Vector.Z)
	local IsInRage = Distance <= DoorRadius
end
print(tick()-t3)
Results
DistanceFromCharacter() took 0.068727731704712 seconds
Magnitude checks took 0.16960620880127 seconds
Your method took 0.26777982711792 seconds
1 Like

Here is my source: https://www.youtube.com/watch?v=DxmGxkhhluU&list=PLW3Zl3wyJwWOpdhYedlD-yCB7WQoHf-My&index=4

Why bother w loops or UserInoutService when you could be using .Touched on a cylinder or a ball thats non-collide- oh right that wasn’t a thing back then was it? :^)

Honestly, the best solution would be to use a Roblox Proximity prompt with a custom UI if you want it to look like that

1 Like

Touched may not be desirable, let alone work for every case. Touched ties into the physics pipeline so you’re relying on physics to carry through your event. You’d additionally be introducing a dependency into your system which is the part. Not really necessary if you can have a pure math solution that only requires numbers. No real sense in using a part when you can hook into frame steps which would be the proper solution for any continuous task anyway.

I don’t recommend abusing proximity prompts. Just use pure math.

1 Like