Cyclic dependency issue

Alright, I’m in the process of refactoring my game in Roblox and I’m learning about Object-Oriented Programming because it’s the kind of programming I want to master and perfect to it’s maximum level because it’s a cool way to organize the code and I’m dealing with a cyclic dependency problem in which I’m creating a main module GameMode which will have all the function that other child like FallMode, ColorMode, ParkourMode will inherit of. However, for each game mode the game run its own way which makes the situation a little bit more tricky in terms of architecture design so basically here’s the issue

I need GameMode for the module child which will inherit the parents method but at the same time I need the child module to run the gamemode its own way which makes it a very annoying situation

Here’s the code of GameMode and FallMode

Edit : From my research there’s a solution called the Mediator Pattern to remedy this situation but I don’t know how I would implement it on this solution

GameMode :

-- SERVICES
local ServerStorageService = game:GetService("ServerStorage")
local ReplicatedStorageService = game:GetService("ReplicatedStorage")
local PlayerService = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")


-- FOLDERS
local MatchRemoteEvents = ReplicatedStorageService:WaitForChild("RemoteEvents"):WaitForChild("Match")
local ScriptsServerScripts = ServerScriptService:WaitForChild("Scripts")
-- EVENTS
local MatchGetScoreEvent = MatchRemoteEvents:WaitForChild("MatchGetScoreEvent")

-- VARIABLES
local Maps = ServerStorageService:WaitForChild("Assets"):WaitForChild("Maps")

local SpawnPoint = game.Workspace.SpawnLocation

-- MODULE
local FallMode = require(ScriptsServerScripts.GameMode.FallMode)

local GameMode = {}
-----------------------------------------------------------------------
--																	 --
--				FUNCTION THAT HANDLE GAME MODE START                 --
--																	 --
-----------------------------------------------------------------------

function GameMode:StartGame(modeChoosen: Instance)	
	local mapChoosen = Maps:FindFirstChild(modeChoosen)
	
	if not mapChoosen then
		warn("Map not found: ", modeChoosen)
		return
	end
	
	self.mapChoosen = Maps:WaitForChild(modeChoosen):Clone()
	self.mapChoosen.Parent = game.Workspace

	local function selectRandomSpawn()
		local spawnPoints = self.mapChoosen:FindFirstChild("Spawns"):GetChildren()
		local spawnPart

		repeat
			local randomIndex = math.random(1, #spawnPoints)
			spawnPart = spawnPoints[randomIndex]
		until not spawnPart:GetAttribute("isOccupied")

		spawnPart:SetAttribute("isOccupied", true) 
		return spawnPart
	end

	for _, player in ipairs(PlayerService:GetPlayers()) do
		if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
			local spawnPart = selectRandomSpawn()

			player.Character.HumanoidRootPart.CFrame = spawnPart.CFrame
			player.Character.HumanoidRootPart.Anchored = true
		end
	end

	task.wait(3)

	self.participants = {}
	self.timeSinceGameStarted = os.clock()

	for _, player in ipairs(PlayerService:GetPlayers()) do
		if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
			player.Character.HumanoidRootPart.Anchored = false
			self.participants[player] = {isAlive = true, survivalTime = 0}

		end
	end
	
	print(self)
end

-----------------------------------------------------------------------
--																	 --
--				FUNCTION THAT HANDLE GAME MODE ENDING                --
--																	 --
-----------------------------------------------------------------------

function GameMode:EndGame()
	self:GetFinalScores()
	
	self.mapChoosen:Destroy()
end

-----------------------------------------------------------------------
--																	 --
--		FUNCTION THAT HANDLE THE SCORES AT THE END OF THE GAME       --
--																	 --
-----------------------------------------------------------------------

function GameMode:GetFinalScores()
	self.scores = {}

	for player, data in pairs(self.participants) do
		table.insert(self.scores, {player = player, survivalTime = data.survivalTime})
	end
	
	table.sort(self.scores, function(a, b)
		return a.survivalTime > b.survivalTime 
	end)
	
	if #self.scores > 0 then
		for rank, entry in ipairs(self.scores) do			
			MatchGetScoreEvent:FireClient(entry.player, rank, entry.survivalTime, self.mapChoosen.Name)

		end
	else
	end
end

function GameMode:ManageEliminations(player)
	if self.participants[player] then
		self.participants[player].isAlive = false
		self.participants[player].survivalTime = os.clock() - self.timeSinceGameStarted
	end

	local aliveCount = 0
	local lastParticipant = nil

	for participant, data in pairs(self.participants) do
		if data.isAlive then
			aliveCount = aliveCount + 1
			lastParticipant = participant
		end
	end

	if aliveCount == 1 then
		if lastParticipant and lastParticipant.Character and lastParticipant.Character:FindFirstChild("HumanoidRootPart") then
			self.participants[lastParticipant].isAlive = false
			self.participants[lastParticipant].survivalTime = os.clock() - self.timeSinceGameStarted

			lastParticipant.Character.HumanoidRootPart.CFrame = SpawnPoint.CFrame
			
		end

		self:EndGame()
	elseif aliveCount == 0 then
		self:EndGame()
	end
end

return GameMode

FallMode :

-- SERVICES 
local ServerScriptService = game:GetService("ServerScriptService")
local TweenService = game:GetService("TweenService")
local PlayerService = game:GetService("Players")
-- FOLDERS
local ScriptsServerScript = ServerScriptService:WaitForChild("Scripts")

-- MODULE
local GameMode = require(ScriptsServerScript.GameMode)

local FallMode = {}
FallMode.__index = FallMode

setmetatable(FallMode, GameMode)

function FallMode:runGame()
	local magma = self.mapChoosen:FindFirstChild("Magma")

	if magma then
		magma.Touched:Connect(function(hit)			
			local humanoid = hit.Parent:FindFirstChild("Humanoid")
			local Player = PlayerService:GetPlayerFromCharacter(hit.Parent)

			if humanoid and not self.debouncePlayers[Player] then
				self.debouncePlayers[Player] = true

				humanoid.Health = 0

				self:removeParticipants(Player)
			end
		end)
	end
	
	local debounce = {}

	for i, v in ipairs(self.mapChoosen:GetDescendants()) do
		if v:IsA("MeshPart") and not v:IsA("Part") then
			v.Touched:Connect(function(hit)				
				local humanoid = hit.Parent:FindFirstChild("Humanoid")

				if humanoid and not debounce[v] then
					debounce[v] = true 

					local tween
					if v.Parent ~= nil then
						if v.Parent.Name == "Stage1" then
							tween = TweenService:Create(v, TweenInfo.new(1.28), { Transparency = 1 })
						elseif v.Parent.Name == "Stage2" then
							tween = TweenService:Create(v, TweenInfo.new(0.98), { Transparency = 1 })
						elseif v.Parent.Name == "Stage3" then
							tween = TweenService:Create(v, TweenInfo.new(0.58), { Transparency = 1 })
						elseif v.Parent.Name == "Stage4" then
							tween = TweenService:Create(v, TweenInfo.new(0.58), { Transparency = 1 })
						elseif v.Parent.Name == "Stage5" then
							tween = TweenService:Create(v, TweenInfo.new(0.58), { Transparency = 1 })
						else
							tween = TweenService:Create(v, TweenInfo.new(1.28), { Transparency = 1 })
						end
					end

					if tween ~= nil then
						tween:Play()

						tween.Completed:Connect(function()
							v:Destroy()
							debounce[v] = nil 
						end)
					end 
				end
			end)
		end
	end
end

return FallMode

3 Likes

You can do setmetatable(require(directory.to.FallMode), GameMode) inside of the GameMode module.

Also, you don’t have any metamethods attached to your GameMode, so why set it as the metatable? Instead you’re doing that with the FallMode which is the parent table of the metatable…

2 Likes

Yeah what about the ColorMode, ParkourMode etc. all the other specifics mode ??

I’m not using .new constructor since I don’t need it and I feel like it would cause massive issue

Wasn’t suggesting a constructor since it doesn’t look like a class but for other modes you can do the same can’t you?

If you want a non-cyclic workaround that must be the way you’re doing it, you can have a child module of GameMode called something like “Methods” (which contains all the methods and few variables that the GameMode module has), and have other files require that instead. This allows for the shared metamethods table while removing it’s cyclic behavior.


This looks like an incorrect usage of class inheritance.

Also, ideally, this shouldn’t be done with OOP, but let’s walk over it anyway:

If FallMode inherits from GameMode, then the latter should be able to handle all modes that inherit from it. You should probably structure your classes so that
A. All game modes run using the same method under GameMode class and
B. Each mode still has their own logical callback that we can set using a constructor.

2 Likes

They’re referring to the fact your GameMode class does not have a __index metamethod, thus attempting to call any members from it will fail.

I don’t think I need a constructor for this specific scenario since I don’t have specific properties like self.timerCountdown, self.alivePlayers etc. it would be hard for my scenario I’ve watched this specific thread on OOP

Well yeah, that’s just kind of how __index works, you’re setting it to itself. So that other tables which have it as the metatable can use that __index metamethod, which would allow access to that metatable without directly referencing it.

There’s also a problem I have a Main script which the script runs. At the very start of the game when the first player arrives it triggers the vote ui but after that the voteUI will run at each endGame of the GameMode parent and repeat infinitely without using like while loop equivalence

-- SERVICES

local ReplicatedStorageService = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")

-- FOLDERS

local ScriptsReplicatedStorage = ReplicatedStorageService:WaitForChild("Scripts")
local ScriptsServerScripts = ServerScriptService:WaitForChild("Scripts")

-- MODULE
local VoteLogic = require(ScriptsReplicatedStorage.VoteLogic)
local GameMode = require(ScriptsServerScripts.GameMode)

VoteLogic:initVote()

local MapChoosen = VoteLogic:ChooseMode()
GameMode:StartGame(MapChoosen)
















I see 0 usage of game mode inside of the fall mode module. Remove it.

Two modules cannot require each other, you could work around by doing an importer module and then requiring and getting from there pretty sure that works. If not then I am not too sure.

Game mode should handle everything to do with the game anyway.

The problem is I need them for inheritance wait hold on I actually don’t need GameMode anyway since I call the method of FallMode.

Edit : Actually no I need to reference GameMode since it will cause this issue

or I could just put it in parameter at this point

Why are you using FindFirstChild and not WaitForChild?

Edit:
saw the issue, you didn’t say it was the print. Just merge the two or use a one way import.

Because :FindFirstChild make sure that the instance actually exist before running the code with WaitForChild if the instance doesn’t exist it will display nil

And if it doesn’t exist then handle its nil return. This is also the same for FFC not just WFC.

Actually I don’t know the difference by looking at the name WaitForChild wait for the instance to be completely load and FindFirstChild will try to find the instance but both likely give the same output in a certain way so

I tried the if condition and neither gave them a red line so my error is I forgot a if statement to check if the instance exist or not

This was so confusing to read, please proof read.


local x = game:WFC("name")

if x == nil then
   print("x returned nil.")
end

Ok nevermind FindFirstChild is completely useless

“self.mapChoosen” is nil, :FindFirstChild() is not useless. You’re not even using it though