How do you Listen for a Remote Event outside of an OOP Module/Function

Hi, I’m trying to make it so that each Portal Object created by using a Portal Class Module, listens for a remote event, and performs logic.

Now the problem is that since I have to listen for the remote event by putting the listening code inside the initialize function (or any function within the object to reference the variable self), the remote event will fire multiple times, specifically, as many times as there are portals in the game. This is because the remote event is linked to each object, so when it fires, every single object activates its own remote event listener.

I want there to be a central remote event listener for every portal, without running into any “Tables cannot be cyclic” errors.

I tried finding solutions, and also making a module holding every World object in the game, since each World object made from the World class holds their own respective Portal objects, but I ran into that cyclic table error (which was probably because of how the class functions were written, so I probably just did that wrong, maybe it still works now since I fixed that).

(If you don’t know what a cyclic table is, it’s basically a table that references itself, causing an infinite loop of referencing that table)

Here’s the Portal Class Code (The remote event listener is at the end of the Initialize function):

Script
-- SERVICES --
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- MODULES --
local ServerInfo = ServerStorage:WaitForChild("ServerInfo")
local WorldsInfo = require(ServerInfo:WaitForChild("Worlds"))
local DataManager = require(ServerStorage:WaitForChild("Data"):WaitForChild("DataManager"))

-- DATA --
local DataModules = ServerStorage:WaitForChild("Data")
local EditDataModule = require(DataModules:WaitForChild("EditData"))

-- REMOTES --
local Remotes = ReplicatedStorage:WaitForChild("Remotes")
local PortalRemotes = Remotes:WaitForChild("Portal")
local PromptPurchasePortal = PortalRemotes:WaitForChild("PromptPurchasePortal")
local PurchasePortal = PortalRemotes:WaitForChild("PurchasePortal")
local RevealWorld = PortalRemotes:WaitForChild("RevealWorld")

-- VARIABLES --
local WorkspaceWorlds = game.Workspace:WaitForChild("Worlds")

local Assets = ServerStorage:WaitForChild("Assets")
local Eggs = Assets:WaitForChild("Eggs")
local Worlds = Assets:WaitForChild("Worlds")
local Animals = Assets:WaitForChild("Animals")
local Monsters = Assets:WaitForChild("Monsters")


-- CLASSES --

-- PRIVATE FUNCTIONS --

----------------------------------------------------------------------
----------------------------------------------------------------------

--METATABLE--
local Portal = {}
Portal.__index = Portal

-- PUBLIC FUNCTIONS --

-- Constructor function
function Portal.Initialize(Model: Model, World: Object)

	-- Verify It Is An Area
	if not (Model and World and (Model.Name == "Entrance" or Model.Name == "Exit")) then warn("Portal Not Valid") return end
	
	local self = setmetatable({}, Portal)
	
	-- CONSTANTS --
	self.MODEL = Model
	self.NAME = self.MODEL.Name
	
	if self.NAME == "Entrance" then
		self.IS_EXIT = false
		self.CURRENT_AREA = World.INFO.Entrance.CurrentArea
		self.NEXT_WORLD = World.INFO.Entrance.NextWorld
	elseif self.NAME == "Exit" then
		self.IS_EXIT = true
		self.CURRENT_AREA = World.INFO.Exit.CurrentArea
		self.NEXT_WORLD = World.INFO.Exit.NextWorld
	end
	
	self.PRICE = WorldsInfo[self.NEXT_WORLD].Price
	self.CURRENCY = WorldsInfo[self.NEXT_WORLD].Currency
	
	self.DETECT_PART = self.MODEL:FindFirstChild("Detect")
	if self.MODEL:FindFirstChild("Spawn") and self.IS_EXIT == false then
		self.SPAWN = self.MODEL:FindFirstChild("Spawn")
	elseif not self.MODEL:FindFirstChild("Spawn") and self.IS_EXIT == false then
		warn("Portal: " .. self.NAME .. " Must Have Spawn")
		return
	end
	
	local ProximityPrompt = Instance.new("ProximityPrompt")
	ProximityPrompt.Name = "PurchasePrompt"
	ProximityPrompt.ActionText = self.NEXT_WORLD
	ProximityPrompt.ObjectText = "Portal"
	ProximityPrompt.RequiresLineOfSight = false
	ProximityPrompt.MaxActivationDistance = 15
	ProximityPrompt.Parent = self.DETECT_PART
	self.PROXIMITY_PROMPT = ProximityPrompt
	
	-- Properties --
	
	-- Run Time --
	
	--Hide Portal By Default
	local Highlight = Instance.new("Highlight")
	Highlight.FillColor = Color3.fromRGB(0, 0, 0)
	Highlight.FillTransparency = 0
	Highlight.OutlineColor = Color3.fromRGB(0, 0, 0)
	Highlight.OutlineTransparency = 1
	Highlight.DepthMode = Enum.HighlightDepthMode.Occluded
	Highlight.Parent = self.MODEL
	self.HIGHLIGHT = Highlight
	
	
	self.PROXIMITY_PROMPT.Triggered:Connect(function(Player: Player)
		
		local profile = DataManager.Profiles[Player]
		if not profile then return end
		
		if not table.find(profile.Data.Worlds[World.NAME], self.CURRENT_AREA) then warn(Player.Name .. " Must Purchase Area: " .. self.CURRENT_AREA .. " To Use Portal In World: " .. World.NAME) return end
		
		print(World.INFO)
		if profile.Data.Worlds[self.NEXT_WORLD] then
			self:Teleport(Player)
		else
			if Player:GetAttribute("IsBuying") == false then
				Player:SetAttribute("IsBuying", true)
				PromptPurchasePortal:FireClient(Player, self.NAME, self.NEXT_WORLD, self.PRICE, self.CURRENCY)
			end
		end
		
	end)
	
	
	-- REMOTE CONNECTIONS --
	PurchasePortal.OnServerEvent:Connect(function(Player: Player, WorldName: string, Purchased: boolean)
		warn("!")

		if self.NEXT_WORLD ~= WorldName then return end
		print("WORKED")
		
		print(World.INFO)
		print(self.NEXT_WORLD)
		if Purchased then
			self:Unlock(Player)
		else
			Player:SetAttribute("IsBuying", false)
		end
	end)
	
	
	print(self)
	
	
	return self
	
end

-- Unlock Area
function Portal:Unlock(Player: Player, World: Object)
		
	Player:SetAttribute("IsBuying", false)
	
	--Pay Cost
	local success
	if self.CURRENCY == "Coins" then
		success = EditDataModule.EditCoins(Player, -(self.PRICE))
	elseif self.CURRENCY == "Gems" then
		--Gems
	end

	if not success then
		--Display That Player Cannot Purchase Area
		print("You Don't Have Enough to Purchase This Area!")
		return
	end
	
	--Unlock Area
	print(self.NEXT_WORLD)
	local unlockValid = EditDataModule.UnlockWorld(Player, self.NEXT_WORLD)
	if not unlockValid then
		--Display That Player Cannot Unlock World (Maybe add currency back to player data in the future)
		return
	end
	
	--Client
	local Next_World_Model
	if WorkspaceWorlds:FindFirstChild(self.NEXT_WORLD) then
		Next_World_Model = WorkspaceWorlds:FindFirstChild(self.NEXT_WORLD)
	elseif Worlds:FindFirstChild(self.NEXT_WORLD) then
		Next_World_Model = Worlds:FindFirstChild(self.NEXT_WORLD)
	end
	local Start_Area
	if Next_World_Model:FindFirstChild("Areas"):FindFirstChild(WorldsInfo[self.NEXT_WORLD].StartArea) then
		Start_Area = Next_World_Model:FindFirstChild("Areas"):FindFirstChild(WorldsInfo[self.NEXT_WORLD].StartArea)
	end
	
	RevealWorld:FireClient(Player, Next_World_Model, Start_Area)
	
	--Teleport Player
	self:Teleport(Player)
	
end

--Teleport
function Portal:Teleport(Player: Player)
	
	--Variables--
	local profile = DataManager.Profiles[Player]
	if not profile then return end
	
	local Character = Player.Character or Player.CharacterAdded:Wait()
	local HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart")
	if not (HumanoidRootPart and Character) then return end
	
	--Teleport
	for _, World in pairs(WorkspaceWorlds:GetChildren()) do
		if self.NEXT_WORLD == World.Name then
			if World:FindFirstChild("Portals"):FindFirstChild("Entrance") then
				HumanoidRootPart.CFrame = World:FindFirstChild("Portals"):FindFirstChild("Entrance"):FindFirstChild("Spawn").CFrame * CFrame.new(0, 3, 0)
			else
				HumanoidRootPart.CFrame = game.Workspace:FindFirstChild("SpawnLocation").CFrame * CFrame.new(0, 3, 0)
			end
		end
	end
	
	--Data--
	if profile.Data then
		profile.Data.Location = self.NEXT_WORLD
	end
	
	
end


return Portal

Thanks :happy2:

Still been trying to find a way to reference the object (basically self), outside of the object functions, not sure if that’s the only way.

I tried using an external portal counter, and adding to the variable every time a new portal is created, so it connects the portal remote event only once by checking the counter, but I think it only references that specific object (the first portal) every time the remote event is fired, since it uses self, so I don’t think that works. Any ideas?