Can My Status Effect System Be Improved?

I have created my own status effect system, because most of the ones I’ve seen were either way too basic for what I wanted, or extremely complicated, large, and used several separate modules. I feel like this could be better optimized somehow, but I do not know how specifically. I want it designed to be flexible and easy to understand, which I really hope I’ve achieved.

Please let me know of any ways I could improve my system, if possible.

file (drag into studio): Status Effect System.rbxm (7.6 KB)

or copy+paste the scripts below in the correct services:

image

image

Manager Module (“EffectManager”)

-- recommended to be placed in replicated storage --

local EffectManager = {}
EffectManager.__index = EffectManager

local managers = {}

function EffectManager.new(target) -- creates a new "Effect Manager"
	
	print("new manager")
	
	local self = setmetatable({}, EffectManager)
	self.Target = target
	
	self.Effects = {}
	
	self._Connections = {}
	if self.Target and typeof(self.Target) == "Instance" then
		self._Connections["ParentChanged"] = self.Target:GetPropertyChangedSignal("Parent"):Connect(function(parent)
			if parent == nil then -- player was removed
				self:Destroy()
			end
		end)
	end
	
	self._Connections["Run"] = game:GetService("RunService").Heartbeat:Connect(function(delta)
		self:Update(delta)
	end)
	
	managers[target] = self
	return self
end

function EffectManager.Find(key) -- returns an "Effect Manager" (if one exists)
	if key == nil then
		return
	end
	return managers[key]
end

function EffectManager:ApplyEffect(EffectName, ID) -- applies an effect based on name, optional ID to help separate effects of the same type
	print("apply effect " .. '"' .. tostring(EffectName) .. '"')
	
	local EffectModule = script.Effects:FindFirstChild(EffectName)
	if not (EffectModule and EffectModule:IsA("ModuleScript")) then
		warn("unable to find effect " .. tostring(EffectName) .. ": invalid effect")
	end
	
	--print(ID)
	local name = EffectName
	if ID then
		EffectName += tostring(ID)
	end

	if self.Effects[name] then
		warn(tostring(EffectName) .. "effect was already applied")
		if self.Effects[name].Duration then
			self.Effects[name].TimeLeft = self.Effects[name].Duration
		end
		
		return
	end
	
	--print("apply")
	
	local effect = setmetatable({}, require(EffectModule))
	
	effect.Target = self.Target
	
	--print(effect.Target, self.Target)
	
	if effect.OnApply then
		effect:OnApply()
	end
	
	self.Effects[name] = effect
end

function EffectManager:RemoveEffect(effect, ID) -- removes an effect base on name, and an optional ID
	print("remove effect")
	
	local name = effect
	if ID then
		effect += tostring(ID)
	end
	
	local effectName = name
	
	if typeof(effect) == "string" then
		effect = self.Effects[effectName]
	else
		effectName = nil
	end
	
	if not effect then
		warn('effect "'.. effectName .. '"' .. " could not be removed: effect was not applied", effectName, effect)
		return
	end
	
	--print(name)
	
	effect:OnRemove()

	setmetatable(effect, nil)
	table.clear(effect)
	if effectName then
		self.Effects[effectName] = nil
	else
		table.remove(self.Effects, table.find(self.Effects, effect))
	end
end

function EffectManager:Update(delta) -- not intended to be called externally, "updates" every effect in the "Effect Manager"
	for effectID, effect in pairs(self.Effects) do
		if effect.UpdatesPerSecond then
			effect.TimeSinceLastUpdate = (effect.TimeSinceLastUpdate or 0) + delta
			
			--print(effect.TimeSinceLastUpdate, effect.UpdatesPerSecond)
			if ((effect.TimeSinceLastUpdate) > (1/effect.UpdatesPerSecond)) or effect.UpdatesPerSecond == 0 then
				--print("update")
				
				if effect.OnUpdate then
					effect:OnUpdate(delta)
				end

				effect.TimeSinceLastUpdate = 0
			end
		--else
			--print("missing UpdatesPerSecond")
		end

		effect.TimeLeft = (effect.TimeLeft or effect.Duration or 0) - delta
		if effect.TimeLeft <= 0 then
			self:RemoveEffect(effectID)
		end
	end
end

function EffectManager:Destroy() -- it go bye bye
	--print("bye")
	
	for i, v in pairs(self._Connections) do
		if typeof(v) == "RBXScriptConnection" and v.Connected then
			v:Disconnect()
		end
	end
	
	setmetatable(self, nil)
	table.clear(self)
end

return EffectManager

Effect Template (Module, parent to the “Effects” folder in the Manager Module)

-- belongs in the "Effects" folder in the "EffectManager" module --

local Effect = {
	Duration = 5, -- 5 seconds
	
	-- other data could also be included, but not all are required nor would be used by the effect manager
	
	UpdatesPerSecond = 1, -- higher number means it calls "OnUpdate" more rapidly
	Target = nil, -- placeholder, refers to its manager's target
	
	-- TimeLeft = nil -- the effect manager creates a variable called "TimeLeft", which could be used. not recommended to modify this variable
}
Effect.__index = Effect

-- the functions below are used by the effect manager - but are not required --

function Effect:OnApply() -- this function is ran while the effect is being applied 
	print("effect was applied")
end

function Effect:OnUpdate(delta) -- this function is ran every time the effect is updated
	print("effect was updated")
end

function Effect:OnRemove() -- this function is ran as the effect is about to be removed
	print("effect was removed")
end

return Effect

Example “Fire” Effect (same as the Effect Template, add a particle emitter named “FireParticle”)

-- belongs in the "Effects" folder in the "EffectManager" module --

local Effect = {
	Duration = 5, -- 5 seconds
	
	-- other data could also be included, but not required nor would be used by the effect manager
	
	Strength = 5, -- in this case, this means it does five damage
	UpdatesPerSecond = 1, -- higher number means it damages faster
	Target = nil, -- placeholder, refers to its manager's target
	
	TimeLeft = nil -- the effect manager creates a variable called "TimeLeft", which could be used. not recommended to modify this variable
}
Effect.__index = Effect

-- the functions below are used by the effect manager - but are not required --

function burn(self) -- except for this one, this is only to make it easier to reuse the same bit of code and is not used by the effect manager.
	print("burn")

	--local self = self or setmetatable({}, Effect) -- this was to make the auto prediction thing actually work - it is not necessary
	
	--self.Hits = (self.Hits or 0) + 1 -- debug stuff
	--print(self.Hits)

	local target = self.Target
	if target and typeof(target) == "Instance" then
		if target:IsA("Player") then
			--print("is a player")
			target = target.Character
		end
		local hum = target:FindFirstChild("Humanoid")

		if hum then
			--print("do damage")
			hum:TakeDamage(self.Strength)
		end
	else
		print(tostring(target) .. " is not a valid target - " .. typeof(target))
	end
end

function Effect:OnApply() -- this function is ran while the effect is being applied 
	print("effect was applied")
	burn(self)
	
	local target = self.Target
	if target and typeof(target) == "Instance" then
		if target:IsA("Player") then -- if the target is a player, target their character
			--print("is a player")
			target = target.Character
		end
		
		local hum = target:FindFirstChild("Humanoid") :: Humanoid?
		if hum then
			self.died = hum.Died:Once(function()
				self.TimeLeft = 0
			end)
		end
		
		if not script:FindFirstChild("FireParticle") then return end -- if there's no fire particles, just ignore the rest of this
		
		if target:IsA("Model") then -- if its a player's character, it should be a model
			self._Fire = script.FireParticle:Clone()
			
			self._Fire.Parent = target.PrimaryPart
		elseif target:IsA("BasePart") then -- in the case that, for whatever reason, the target is a part and not a model
			self._Fire = script.FireParticle:Clone()
			
			self._Fire.Parent = target
		end
	else
		print(tostring(target) .. " is not a valid target - " .. typeof(target))
	end
end

function Effect:OnUpdate(delta) -- this function is ran every time the effect is updated
	print("effect was updated")
	burn(self)
end

function Effect:OnRemove() -- this function is ran as the effect is about to be removed
	print("effect was removed")
	
	if self.died and typeof(self.died) == "RBXScriptConnection" and self.died.Connected then
		self.died:Disconnect()
	end
	
	local fire = self._Fire
	if not fire then return end -- no particle? just ignore
	
	task.spawn(function()
		fire.Enabled = false
		task.wait(3)
		fire:Destroy()
	end)
end

return Effect

Example Use (Regular Script)

-- example usage --
-- belongs in server script service --

local PLRS = game:GetService("Players")

local EffectModule = game:GetService("ReplicatedStorage"):FindFirstChild("EffectManager")
local EffectManager = require(EffectModule)

PLRS.PlayerAdded:Connect(function(plr) -- burn players that join hehehe >:)
	print("new player")

	local manager = EffectManager.new(plr)

	task.wait(5)

	--print("effect apply")

	manager:ApplyEffect("Fire")

	-- task.wait(5)

	-- manager:Destroy() -- destroy after 5 seconds
end)

--[[PLRS.PlayerRemoving:Connect(function(plr) -- not necessary since the effect manager self destructs after the player leaves
	EffectManager.Destroy(EffectManager.Find(plr))
end)]]
2 Likes