In a recent visit to my experience in VR, I found that I moved so slowly that I got stuck under the counter door in my still-incomplete restaurant. It’s nice that I don’t get nausea easily while I’m in VR (unless I drive a vehicle around for some reason), but some other people aren’t as lucky as I am in this regard.
I knew I needed to fix this somehow. As of today, I might have¹ done it, with a new function in “CharacterFunctions”, IsCharacterNear(). This function iterates through every player on the server, tries to find their character a couple times (not using :Wait() to avoid yielding the calling script), then checks if they’re near specific coordinates using a magnitude comparison.
Until this function returns false, the flap won’t lower back into its usual position. Check out this video to see how it looks! (Please ignore how the restaurant hasn’t changed one bit in possibly a year at this point.)
Also, yes, since my experience tries to treat NPCs and players equally, the door won’t close if a tracked NPC is standing near it.
I know some of you might be thinking of the meme, so yes… That woman is just standing there…MENANCINGLY!
Source code
Just because I think it could be useful to other developers, here’s the function, copied from my ModuleScript directly. You’ll have to remove the “CharacterStorage:” prefix on the function’s name, or change it to whatever your ModuleScript identifies itself as in itself:
-- CharacterFunctions:IsCharacterNear(takes Vector3 and number, returns boolean)
-- Compares all players (and NPC characters that have opted in)'s positions to
-- the provided reference coordinates, and returns TRUE if any of them are within
-- a specific number of studs to it. This function assumes that a player isn't
-- near the point of interest if it can't get their character reference within
-- a specific number of attempts, so this may fail and cause VR players to get
-- stuck, which could lead to nausea.
-- This function is meant to run on the server, but can run on the client. As
-- NPCs are planned to be handled by the server, any client-side scripts will
-- need to be aware of any NPCs that need to be checked for, so doors can't
-- close near them and other examples. For that, a RemoteEvent might be needed
-- so clients know when to add an NPC to their local list, which isn't synced.
local NPCScanList = {}
-- TODO: Consider changing this function to use CharacterStorage entries; All NPCs and players must generate them after spawning, and they contain
-- references to things like their HumanoidRootPart and Character, which would remove the need for the pcall.
function CharacterFunctions:IsCharacterNear(_reference : Vector3, _radius : number)
-- Longer-term variables
local IsClose = false -- This will be set to TRUE if any character is too close to this door.
-- Temporary variables
local char : Model = nil -- Temporary variable for each Player's character, nil'd between each iteration.
local HRP : BasePart = nil -- The iterated player's HumanoidRootPart, used for a distance check.
local RemainingTries = 0
local success = false -- This is set to true if a Player's character is found inside of the pcall.
-- First scan for any nearby players.
for _, player in Players:GetPlayers() do
RemainingTries = 5
while RemainingTries > 0 do
success = pcall (function()
char = player.Character
HRP = char:FindFirstChild("HumanoidRootPart")
end)
-- Break the loop early if the player's character is found without errors.
if success and char then
RemainingTries = 0
break
end
task.wait(0.075)
end
-- Is this Player within the requested number of studs of the door? They're in the way if that's the case! Break the loop here.
if char and HRP and (HRP.Position-_reference).Magnitude <= _radius then
IsClose = true
break
end
-- Reset the temporariy variables, so each character is checked, with no way to "sneak past" the scan.
char = nil
HRP = nil
success = false
end
-- No players were near the location? If there are any NPCs on the list, see if any of them are close to it.
-- TODO: This part of the function is basically copy-pasted from the player iteration above. Perhaps both could be merged into a single list,
-- then handled based on their type? (Model = NPC, Player = player, get their character)
if not IsClose and #NPCScanList > 0 then
for _, npc : Model in NPCScanList do
RemainingTries = 5
while RemainingTries > 0 do
success = pcall (function()
HRP = npc:FindFirstChild("HumanoidRootPart")
end)
-- Break the loop early if their HumanoidRootPart is found without errors.
if success and HRP then
RemainingTries = 0
break
end
task.wait(0.075)
end
-- Is this NPC within the requested number of studs of the door? They're in the way if that's the case! Break the loop here.
if HRP and (HRP.Position-_reference).Magnitude <= _radius then
IsClose = true
break
end
-- Reset the temporariy variables, so each character is checked, with no way to "sneak past" the scan.
char = nil
HRP = nil
success = false
end
end
return IsClose
end
As a bonus, you can also see my “TODO” comments, which reference an idea (NPC characters) and CharacterStorage, a system that unifies references to players and NPCs by creating data structures for both that give references to their character models and the like. Since it’s specific to my experience, I’m keeping that private…for now.
Actually, I’ve already updated this to use CharacterStorage, and it’s probably more “performant” now as a result. The older code version here will work in other experiences, though.
¹This function won’t always prevent players from getting stuck beneath the door. If it can’t get a reference to their character model in time, it’ll act as if they aren’t near it!