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:
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)]]