There are times where I have an update function that is fired whenever some event is fired. An example is a health bar, I might have something like this
local Humanoid: Humanoid = SomeHumanoid
Humanoid:GetPropertyChangedSignal("Health"):Connect(function()
UpdateHealthbar(Humanoid)
end)
The problem is if the humanoid has their health changed many times in a single frame, for example a shotgun blast with many individual projectiles, the UpdateHealthbar function will be fired more times than needed.
If I could instead use a function like task.deferOnce, the passed function will only be fired once per frame
local QueuedDefers = {}
local function FireDeferOnce(Callback)
-- Get callback data
local CallbackData = QueuedDefers[Callback]
if (CallbackData) then
-- Remove callback from queue
QueuedDefers[Callback] = nil
-- Fire callback with arguments if they exist
if (CallbackData.Arguments) then
return Callback(table.unpack(CallbackData.Arguments))
end
-- Fire callback without any arguments
return Callback()
else
-- Callback wasnt in queue for whatever reason
end
end
local function DeferOnce(Callback, ...)
-- Get arguments as array to pass to callback
local Arguments = if (select("#", ...) > 0) then {...} else nil
-- Defer if not already queued
if (not QueuedDefers[Callback]) then
task.defer(FireDeferOnce, Callback)
end
-- Add/update callback
QueuedDefers[Callback] = {Arguments = Arguments}
end
function UpdateHealthbar(Key)
print("Updating healthbar", Key)
end
while true do
-- Simulate healthbar being updated many times per frame
for Key = 1, math.random(5, 20) do
DeferOnce(UpdateHealthbar, Key)
end
-- Make it per frame
task.wait()
end
True, signals do have a Once() option, though it is not exactly a solution in the proposed situation.
The OP is using Connect() to catch all health changes which may come in small steps.
Defered or not defered signal behaviour, either way the function is going to be called for each change, for example -1, -5, -2, -4. UpdateHealthbar will be called four times. If the changes happened during the same frame, the proposed method would pile the callbacks up and only call the latest queued one (for -4).
I cannot really tell how the engineering team will decide on adding such a function, since many additions involve trade-offs (is it worth implementing for the gained benefits?). Anyhow, it does represent a solution in particular scenarios.
Update 2023-01-07; responses to replies
@Razorter the idea was not to listen for the signal once, but to queue all the signals and only respond to the latest one received in the single frame (see the OP’s example implementation).
@Rocky28447 that of course works but not as much in favour of performance. I’ve seen the term ‘polling’ used around the forum in this context, and it refers to repetitive sampling of the status of something, instead of going for event based responses, which are not as rapid. The logic of proposed behaviour would be to 1. respond to events when they occur and 2. if the events are received multiple times in the same frame, respond to the latest signal.
Good idea! If something like this was to be implemented, this really makes more sense. I suppose it would even have slightly smaller overhead.
Correct me if I’m wrong but RBXScriptSignal:Once(fn)does not call the function multiple times if it is fired multiple times. There would be no point to :Once() if it fired multiple times.
While it’s not impossible to create something like this, this is definitely a nice to have sort of thing that would definitely improve performance. I support.
See the below.
But this leads me to believe we could just have a different connection option. Why not RBXScriptSignal:OncePerFrame or something like that?
While I understand the reasoning of having this function be placed under the task library–and I do concede it seems alluringly appropriate, I would prefer an implementation as suggested by @SubtotalAnt8185. Despite being decidedly more limited in its utility, I believe this strict segmentation would work to better enforce the code’s intention. Otherwise, I fear having these wayward snippets could, in problematic contexts, quickly become a debugging nightmare, given the circumstantially unintuitive nature of their execution. Though I have in equal parts optimism and concern for this feature, I’ll refrain from commenting on its apparent implementation implications–as needless to reaffirm, from a developer’s perspective, such discussion would ultimately boil down to nothing more than nonconstructive speculation.
Unlikely that we could make a function like this because it sets up a huge trap. For example, this code does not do what you want (thanks to functions without any upvalues being treated differently as an optimization):
task.deferOnce(function()
foo(bar)
end)
task.deferOnce(function()
foo(bar)
end) --> Calls foo twice
task.deferOnce(function()
bar()
end)
task.deferOnce(function()
bar()
end) --> Calls bar once
It’s likely too easy for less experienced users to get that pattern wrong for it to be a good API. It’s also unclear how it would work with arguments:
task.deferOnce(foo, bar)
task.deferOnce(foo, baz) --> Called with bar or baz at end of frame?
A good way to handle this if you want it would be with a Lua module exposing a higher order function wrapping the target call:
local fooOncePerFrame = deferOncePerFrame(foo)
fooOncePerFrame()
fooOncePerFrame() --> calls foo once
Agreed, OncePerFrame feels more optimal. However I don’t think this development pattern is anywhere near popularized enough to warrant this engine feature. A module could easily handle this with a small LoC.
local globalid = 0
local function call()
globalid += 1
local localid = globalid
task.defer(function()
if globalid == localid then
-- code here
end
end)
end
The code will only run if it hasn’t attempted to run at least one resumption cycle later.