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