[REPOST] Creating a player exited event

Hey! I’m trying to create a PlayerLeft event (basically when the player leaves the region/zone), which is the opposite of PlayerEntered. I’m creating my own Zone module so I can learn, but I’m trying to figure out on detecting when the PlayerLeft. I’m quite stumped.
Also, if you have any tips on how to optimize this code, please leave some!

--!nonstrict
local module = {}
module.__index = module

local Janitor = require(script.Janitor)
local FastSignal  = require(script.FastSignal)

local PlayerService = game:GetService("Players")
local RunService = game:GetService("RunService")
local HttpService = game:GetService("HttpService")

local Zones = {}

export type Zone = {
	_Janitor		: any,
	Container		: BasePart,
	Touched			: RBXScriptSignal,
	PlayerEntered	: RBXScriptSignal,
	PlayerExited	: RBXScriptSignal,
	ZoneID			: string
}

local function round(number, decimalPlaces)
	return math.round(number * 10^decimalPlaces) * 10^-decimalPlaces
end

function findPlayer(zone: Zone, player: Player)
		for _, v in pairs(workspace:GetPartsInPart(zone.Container)) do
			local Player = PlayerService:GetPlayerFromCharacter(v.Parent)

			if Player then
				return true
			else
				return nil
			end
		end
	end

function module.new(container: BasePart)
	local Zone: Zone = {}
	local Players = {}
	
	setmetatable(Zone, module)
	
	local Janitor = Janitor.new()
	
	local PlayerEntered: any = FastSignal.new()
	local PlayerExited: any = FastSignal.new()
	
	local Touched: any = FastSignal.new()
	Zone.Container = container
	Zone.Touched = Touched
	Zone.PlayerEntered = PlayerEntered
	Zone.PlayerExited = PlayerExited
	Zone.ZoneID = HttpService:GenerateGUID()
	Zone._Janitor = Janitor
	
	local updated = Janitor:Add(
		FastSignal.new()
	)
	
	local triggerTypes = {
		"Touched",
		"Player"
	}
	
	local triggerEvents = {
		"",
		"Entered",
		"Exited"
	}
	
	table.insert(Zones, Zone)
	
	RunService.Heartbeat:Connect(function()
		local TouchingParts = workspace:GetPartsInPart(Zone.Container)
		
		for _, TouchingPart in pairs(TouchingParts) do
			Touched:Fire(TouchingPart)
			
			local Player = PlayerService:GetPlayerFromCharacter(TouchingPart.Parent)
			if Player then
				Players[Player] = Player
				PlayerEntered:Fire(Player)
			end
			
			for _, plr in pairs(Players) do
				if findPlayer(Zone, plr) == nil then --[[ IF THE]]
					--print("PLAYER REALLY DID EXIT OMG")
					PlayerExited:Fire(plr)
					table.remove(Players, table.find(Players, plr))
				else
				end
			end
		end
	end)
	
	return Zone
end

function module:findPlayer(player: Player)
	for _, v in pairs(workspace:GetPartsInPart(self.Container)) do
		local Player = PlayerService:GetPlayerFromCharacter(v.Parent)
		
		if Player then
			return Player
		else
			return nil
		end
	end
end

function module.fromRegion(cframe, size)
	local container = Instance.new("Part")
	container.CFrame = cframe
	container.Size = size
	
	return module.new(container)
end

function module:GetZoneFromID(GUID: string)
	for _, zone in pairs(Zones) do
		if zone.ZoneID == GUID then
			return zone
		end
	end
end

function module:GetID(zone: Zone)
	return zone.ZoneID
end

function module:Delete()
	self._Janitor:Destroy()
end

return module
2 Likes

You will need two loops: one to find the players that have entered the region, and one to scan for players who’ve left the region. You should also go over these loops not in a nested fashion because that’s going to cause the # of iterations to exponentiate. Also, if your outer loop has 0 iterations, the inner loop won’t run at all.

I think the nested loops might be the biggest reason your script isn’t working but there could be other factors as well that I’m glossing over.

As for optimizations, you should really consider using overlap params to ascertain which parts are valid and can be used for the spatial query and which ones can’t.

I’d also use a hash table for quick lookup when querying which players are new and which are old, and an array for iterating over the old players since iterating over a standard array is faster than iterating over a dict – so you’d have two separate tables for containing the players.

Ultimately for setting up the first point, I’d do something like:

-- 1- first optimization is to create an overlapParams that will add the players' humanoid root parts to minimize the number of parts that the workspace:GetPartsInPart can query. this will save you some time
local roots = {} -- a list of HumanoidRootParts

local overlapParams = OverlapParams.new()
overlapParams.FilterDescendantsInstances = roots
overlapParams.FilterType = Enum.RaycastFilterType.Include

local info = {}
playerAdded:Connect(function(player)
    local function onCharAdded(character)
        local root = info[player].root
        if root then 
            table.remove(roots, table.find(roots, root)) 
        end -- you can probably split this up into different conditions to check if table.find(roots, root) is nil but that's up to you
            
        root = character:WaitForChild('HumanoidRootPart')
        table.insert(roots, root)
        info[player].root = root -- we need to access the root later in playerRemoving

        overlapParams.FilterDescendantsInstances = roots -- re-set filterdescendantsinstances with the old root removed and new root added as iirc overlapParams.FilterDescendantsInstances is not mutable
    end

    info[player] = {
        connection = player.CharacterAdded:Connect(onCharAdded)
    }
    
    if player.Character then onCharAdded(player.Character) end
end)

playerRemoving:Connect(function(player)
    local info = info[player]

    if not info then
        return
    end

    local root = info.root
    if root then
        table.remove(roots, table.find(roots, root)) -- again you could split this up into separate statements, or better yet make a function that handles removing the root
    end

    info.connection:Disconnect()

    info[player] = nil

    -- again re-set FilterDescendantsInstances
    overlapParams.FilterDescendantsInstances = roots
end)

Finally, for querying which players are in the region (later on in your script):

local lastLookup = {} -- hash table for quick lookup, because simply indexing a table is faster than table.find
local lastIter = {} -- for iterating over, because iterating over an array with sequential numeric indices is faster than iterating over dictionaries

heartbeat:Connect(function() -- heartbeat might be excessive here, I'd do a loop to check every .1 seconds or so since time taken by spatial queries will scale up depending on the more parts that exist, but up to you
    local parts = workspace:GetPartsInPart(zone.Container, overlapParams)
    local playersIter = {}
    local playersLookup = {}

    -- loop 1 is to check the players who are in the area and weren't in the area last time.

    for _, part in parts do
        local player = players:GetPlayerFromCharacter(part.Parent)

        if not player then
            continue -- just as a safeguard in the case that a root exists that doesn't belong to a player
        end

        if not lastLookup[player] then
            -- player entered the zone, fire the event and whatnot
        end

        table.insert(playersIter, player) -- the player is in the zone so we add it here
        playersLookup[player] = true
    end

    -- loop 2 is to check the players who were in the area last time and aren't in the area this time.

    for _, player in lastIter do
        if not playersLookup[player] then
            -- player left, so we fire the event here
        end
    end

    lastLookup, lastIter = playersLookup, playersIter
end)

I wrote this hastily and haven’t tested but if there’s anything that doesn’t work as expected after filling in the placeholder refs, or if you need a better explanation on anything lmk.

1 Like

Why not use part.Touched and part.TouchEnded ?

because it’s notoriously glitchy and roblox needs to rework it (please please i beg)

2 Likes

I’m trying to replicate ZonePlus, and you could read on why on this post.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.