TL;DR: What’s common practice for allowing multiple abilities to change properties like WalkSpeed or add temporary states like ‘Stun’ at the same time without interfering with one another?
In combat games its common to use tags, attributes, or single value instances to apply states such as ‘Stun’. The server binds logic to these states, ex. Stun applied → WalkSpeed = 0
If you were to try and apply a tag named ‘Stun’ from two places at once…
Ex. PlayerA and PlayerB both attack PlayerC and stun them at the same time-
The state’s logic collapses! You visibly see WalkSpeed get overridden.
If you wanted to modify WalkSpeed in multiple ability scripts you’d have to use a stack to store every value, then have the game take the average of all the modifiers whenever a modify is added or removed, THEN set the property using that evaluated average.
(For other properties you could do sum, min, max, oldest, most recent, etc.)
It makes sense to have a stack for properties like WalkSpeed, AND a stack for higher-level states like ‘Stun’.
This makes sense, especially if you want to be able to modify properties directly from multiple abilities (ex. use an ability to slow a player down, use an ability to speed yourself up) at the same time OR modify them through high-level states. Regardless, the same problem persists whether or not you’re modifying WalkSpeed or applying a temporary state like ‘Stun’- BOTH need stacks rather than tags/attributes.
My question is…
What is standard practice for wrapping properties like WalkSpeed and tags like ‘Stun’ so that you can modify/apply them from multiple abilities at the same time?
I thought about designing a module that lets me pass values into a stack created for an instance, but I don’t know if there’s a more intuitive design. What would you do?
Ex. States use various methods:
-- Methods
state:set(name: string, value: any) -- set a state's value
state:get(name: string): any? -- gets a states stored value
state:add(name: string, value: any): number -- add a value to a state's stack, returns an id (timestamp)
state:remove(name: string, timestamp: number) -- removes a value from a state's stack given a timestamp id
state:wait(name: string, value: any?, timeout: number?): boolean -- yields for a state's stored value to equal a value
-- returns true/false depending on if the value was reaches in the given (optional) timeout duration
-- Events
state.changed(name: string): RBXConnection -- listens for a state's stored value to change
state.added(name: string): (timestamp: number, value: any) -- fires when a value is added to a state's stack, takes a callback
state.removed(name: string) -- fires when a value is removed from a state's stack
Ex. Somewhere on the server:
local States = require(ServerStorage.States)
local character = workspace.PlayerA
local state = States(Character)
local function WalkSpeed() -- evaluate modifiers (take the average)
if state:has("Stun") then state:set("WalkSpeed", 0) return end -- stun takes priority
local sum, count = 0, 0
state:foreach(function(timestamp, value: any)
if typeof(value) ~= "number" then return end
sum += 1
count += value
end)
state:set("WalkSpeed", if count > 0 then sum else 16)
end
state.Added("WalkSpeed"):Connect(WalkSpeed)
state.Removed("WalkSpeed"):Connect(WalkSpeed)
--
state.changed("WalkSpeed"):Connect(function() -- set walkspeed from evaluated value
character.Humanoid.WalkSpeed = state:get("WalkSpeed") or 16
end)
Ex. In your ability scipts:
state:add("WalkSpeed", 0) -- Walkspeed -> 0
state:add("WalkSpeed", 10) -- Walkspeed -> 5
state:add("WalkSpeed", 35) -- WalkSpeed -> 15
local timestamp: number = state:add("Stun") -- Walkspeed -> 0
task.wait(5)
state:remove("Stun", timestamp) -- Walkspeed -> 15
With this logic, you can see that modifying walkspeed in your abilities now averages out between multiple abilities, it also gets overriden by a higher-level state like stun. This allows us to write our state logic in one organized script on the server for every property/state.
An approach like this works, but its doesn’t seem as memorable or intuitive as I had hoped it could be.
You also unfortunately can’t see whats going on under the hood like you can with tags/attributes.