I need help fixing a cyclic dependency

I am pretty amateur at scripting and only recently, I’ve tried to really utilize OOP and single script architecture in my projects. I’ve been stumped by this cyclic dependency issue and have spent some time thinking of ways to resolve it:

  • Create a helper script for each class (to do functions like GetPlayer, etc) and have the script that needs to access the playerClass access this helper class instead, although this helper class also requires access to the class itself, so that doesn’t help

  • Add an attribute to each player that joined, that contains their player class info

If someone can tell me of a way to resolve this, and explains their thoughts, I would really appreciate it! Thanks.

-- player
local PlayerClass = {}
PlayerClass.__index = PlayerClass

local replicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local playersService = game:GetService("Players")
local DebrisService = game:GetService("Debris")

local ragdollService = require(replicatedStorage.RagdollService)
local GunClass = require(ServerScriptService.Server.GunServer)
local RemoteEvents = replicatedStorage.RemoteEvents

local InitializedPlayers = {}

playersService.PlayerAdded:Connect(function(plr)
	PlayerClass.new(plr)
end)

function PlayerClass.GetAllGuns()
	local tbl = {}
	for _,v in InitializedPlayers do
		table.insert(tbl, v.GetGun())
	end
	
	return tbl
end

function PlayerClass.GetPlayer(playerName)
	return InitializedPlayers[playerName]
end

function PlayerClass.new(player: Player) 
	local self = setmetatable({}, PlayerClass)
	self.Player = player
	self.IsGunEquipped = false
	self.Gun = GunClass.new(self, "TROY DEFENSE AR")
	
	player.CharacterAdded:Connect(function(char)
		self:OnCharacterAdded(char)
	end)
	
	RemoteEvents.EquipGun.OnServerEvent:Connect(function(plr, IsEquipped)
		if plr ~= self.Player then return end
		self.IsGunEquipped = IsEquipped
	end)
	
	RemoteEvents.Fire.OnServerEvent:Connect(function(plr, startPos, dir)
		if plr ~= self.Player then return end
		self.Gun:Fire(PlayerClass.GetAllGuns(), startPos, dir)
	end)

	InitializedPlayers[player.Name] = self
	return self
end

function PlayerClass:OnCharacterAdded(character)
	self.Gun:WeldToCharacter(character)
	self.IsGunEquipped = false
	local humanoid = character:WaitForChild("Humanoid")
	humanoid.BreakJointsOnDeath = false

	humanoid.Died:Connect(function()
		self:Death()
	end)
	
end

function PlayerClass:FireGun(startPos, dir)
	
end

function PlayerClass:GetGun()
	return self.Player.Character:FindFirstChildOfClass("Model")
end

function PlayerClass:Death()
	local character = self.Player.Character
	character.Archivable = true
	local bulletDirection = character:GetAttribute("bulletDirection")
	ragdollService.Ragdoll(character, self.IsGunEquipped, bulletDirection)
end

function PlayerClass:TakeDamage(damageAmount, bulletDirection)
	local character = self.Player.Character
	character:SetAttribute("bulletDirection", bulletDirection)
	
	task.delay(2,function()
		character:SetAttribute("bulletDirection", nil)
	end)
	
	local humanoid = character.Humanoid
	
	humanoid.Health -= damageAmount
end

export type PlayerClass = {
	Player: Player,
	IsGunEquipped: boolean
}


return PlayerClass


-- gun class
local GunClass = {}
GunClass.__index = GunClass

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

local PlayerClass = require(ServerScriptService.Server.PlayerClass)

function GunClass.new(initializedPlayer, gunName)
	local configs = require(ReplicatedStorage:FindFirstChild(gunName))
	if not configs then error("Configs not found for " .. gunName) end
	local self = setmetatable({}, GunClass)
	
	self.Player = initializedPlayer
	self.CurrentAmmo = configs.MaxAmmo
	self.MaxAmmo = configs.MaxAmmo
	self.Configs = configs
	
	return self
end

function GunClass:Fire(filters, startPos, dir)
	if self.CurrentAmmo <= 0 then return end
	self.CurrentAmmo -= 1
	
	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Exclude

	params:AddToFilter(filters)

	local raycast = workspace:Raycast(startPos, dir * 1000, params)

	if raycast and raycast.Instance then
		if not raycast.Instance.Parent:FindFirstChild("Humanoid") then return end
		PlayerClass.GetPlayer(raycast.Instance.Parent.Name):TakeDamage(101, dir.Unit)
	end
end

function GunClass:Reload()
	self.CurrentAmmo = self.MaxAmmo
end

function GunClass:WeldToCharacter(character)
	local weld = self.Configs.Weld:Clone()
	local model = self.Configs.Model:Clone()
	
	model.Parent = character
	weld.Parent = character.Torso
	weld.Part0 = character.Torso
	weld.Part1 = model.Holder
end


return GunClass

1 Like

What you always could do is creating a a service wrapper:

local playerService = {}
playerService.controllers = {}

local players = game:GetService("Players")

local function playerAdded(player)
return playerService:createController(player)
end

function playerService:initialize()
players.PlayerAdded:Connect(playerAdded)
end

function playerService:createController(player)
--
local controller = playerController.new(player)
controller:initialize()
--
playerService.controllers[player] = controller
return controller
--
end

function playerService:getController(player)
return playerService.controllers[player]
end

return playerService

same thing for the gun

But really, why don’t you just use :GetPlayerFromCharacter() ?

1 Like

I’m thinking of trying different behaviors for getting shot at (bleeding, etc…) that is different for players or npcs, but you just gave me the idea that I can use Humanoid.HealthChanged() to implement that better, instead of having a takedamage wrapper, thanks a bunch!

1 Like

As for the service wrappers, do I have to create this for every single class in my project? I feel like that would be, I don’t know, hacky? repetitive? I wouldn’t be too lazy to implement the service itself, but if I have to create this service for every class, I think it wouldn’t feel good (programmatically, I think).

Technically, that’s how I do it. But I never had a problem with it, since I only have 5 - 10. How many classes are you talking about?

I don’t really know haha. I have a lot of grand ideas and not a lot of skills to implement them, but I could say it’s a lot

You also dont need OOP for everything, example, for a rebirth system, its better tl use a generic module and not a rebirth controller

1 Like

Alright, thanks for the help! I’ll try implementing this as I go, and see if I personally have any problems with it along the way. In the mean time though, I think this is the best (and only) solution I might have

No problem, hope it turns out well

1 Like

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