As a developer in multiple engines. we always use deferred functions instead of directly editing our game’s state.
For example instead of executing an immediate function like a destroy function. most of the time we use it’s deferred version to avoid any problems.
Basically we queue the task to the engine instead of executing it immediately.
A basic example from godot engine:
The difference between the function “free” and “queue_free”.
Relying on roblox previous event immediate firing. Might make the game run as fine in 99% of situations but there will be that one game where the order of firing is just not perfect, and the game breaks leaving you with no idea of what caused it.
But if I understand roblox’s new update right. this will make events more stable and confusion free for us and also for roblox’s future updates that are related to parallel lua and other advanced features.
Of course that might not be the case. but that’s my best guess!
I’m so sorry, I must have not seen it since there are so many posts on this topic. I just wanted to post a report about the problem right away. I forgot to check if there are similar ones beforehand.
This might be a dumb question because I don’t understand parts of what all goes into this, but could we have something where this feature is introduced as a second type of connect, something like :connectSynchronous(), and then not change or replace :connect() but mark it as deprecated or give it a warning? In spite of performance benefits, a few other people have pointed out there are a few cases where continuing to use non-deferred event handling seems more maneuverable, and there’s also selectively maintaining old scripts if given the choice between both methods
Let me note that he whole value of fastSpawn isn’t deferring execution. It’s saying “hey, Roblox, next time you effectively yield, please don’t block this current thread.
In other languages you have to write keywords like “await X” or declare functions as async. Lua doesn’t really care about this, and creating new threads (i.e coroutines) is reasonably cheap.
If coroutines reported their stacktrace accordingly, we wouldn’t even need other mechanisms for this sort of thing.
It’s unfortunate that wait/delay/spawn can’t be changed because effectively these already solved this problem, but are stuck operating at 30hz, on an old system.
Quick confirmation: are the handlers called/resumed after the function is done firing the bindable 5 times? Is this behaviour consistent as well? If I fire a bindable 30 times, is the handler going to be called / resumed once all the 30 firings have been queued and referred?
Yes, they’ll get run in the same invocation point, but not necessarily immediately after, because the firings get queued at the end of the queue: There may already be other stuff in the queue in-between the code firing the event and the handlers that get queued.
This feature is a lot more useful than it seems. For so many years I was stuck on problems like this without a simple solution:
Property dependency problem
You have a custom Lua-based rectangle with a position signal and a size signal / property.
A second rectangle’s size and position are dependent on the size and position of the first rectangle. When you update a rectangle, you set it’s position and size. First, the position is set and the position signal fires. Second, the size is set and the size signal fires. This results in 2 updates and causes the second rectangle to update twice within the same frame. Deep dependencies like this result in exponentially more updates than needed.
This may seem like a very specific but easy problem to solve, but it can get much worse.
Now imagine you have a BodyRectangle, which is dependent on: GuiScale, ParentRectanglePosition, ParentRectangleSize, TextBoundsResult1, and ScrollbarWidth.
GuiScale is dependent on: GlobalScreenSize
ParentRectangleSize is dependent on: GuiScale, GlobalScreenSize
TextBoundsResult1 are dependent on: GuiScale
ScrollbarWidth is dependent on: GuiScale
When the player resizes their screen, this is what happens:
BodyRectangle just updated and changed 5 times in a single frame. If this rectangle had multiple nested parent rectangles with similar behaviors, it could update hundreds of times in one frame.
With deferred handling, this is what would happen:
GlobalScreenSize runs (fires change)
GuiScale (added to queue)
ParentRectangleSize (added to queue)
GuiScale runs (fires change)
ParentRectangleSize (already added to queue)
TextBoundsResult1 (added to queue)
ScrollbarWidth (added to queue)
BodyRectangle (added to queue)
ParentRectangleSize runs (fires change)
BodyRectangle (already added to queue)
TextBoundsResult1 runs (fires change)
BodyRectangle (already added to queue)
ScrollbarWidth runs (fires change)
BodyRectangle (already added to queue)
BodyRectangle runs (fires change)
It now updates just one time in that frame. With Roblox deferred event handling, it would actually update 4 times in 1 frame, but the queue order would still prevent exponentially more updates because the custom properties wouldn’t need to change so many times.
I’m unlikely to directly use the built in method for performance reasons though. A custom invocation queue implemented as an array with non-yielding function will be much faster, especially if it doesn’t need an RBXScriptConnection (or something similar) every time an object is added. It’s also useful to be able to remove a function from this queue if it’s disconnected by a previous function in the queue.
I mean, the engine’s event handling code already does basically this. If every handler was a unique coroutine then things would be way slower than they are right now. Instead, there’s a pool of coroutines which get recycled so that if you don’t actually yield in the handler there’s not much cost, the coroutine just immediately returns to the pool.
Alright. I think I’d just want to be able to “raw add” / “raw remove” functions from the queue without a connection-related userdata needing to be allocated. Doubly linked lists are a lot faster when disconnecting from handlers with many connections, but it’s rare for a function to need to be removed from the invocation queue.
What about strategy games? I had an RTS nearing release, but I have to review all the code to get rid of undefined behavior now. A way to start would be searching for “:Fire” in all scripts, and examine the context.
I have more games affected by this update too.
Bindable events have been there for way more than a decade, and they have always worked so it’s safe to assume events fire instantly. This has never been reported in the documentation or elsewhere as bad practice. So a good portion of games that use events will be affected (not just “some developers”), and it’s not our fault.
But in the long term, as explained in an official reply, this is a good update, even if some games remain unplayable.
Unfortunately, this has usually been the trend with Roblox updates - they often improve stuff, but are occasionally destructive. I still miss some games which were broken by different Roblox updates, and the developer did not fix them because they were either inactive or tired of recoding their games over and over again. Anaminus’ Cruiser Hyperspace is the first to come to my mind, but there are many others.
And yes, it’s obvious their intention is not to break games, and this was a required change. We just have to adapt to them
The platform did well for 13+ years.
If it breaks in 5 more years, it’s mostly not gonna be because you didn’t change the order of lua event handling.
I think you should explain in detail why this is necesarry as it’s a very important update with the possible risk of breaking most, if not every single game out there (espacially because of errors caused by CoreScripts), and us developers would like to get the full message that you guys do indeed know what you’re doing.
“three quick examples” just sounds like damage control to me.
I was testing out this feature in studio just now and found out a problem that will break some of my game’s features. When a model is removed, its PrimaryPart is set to nil and its children are deleted after the ChildRemoved event is sent. So the following example will now act differently depending on if the immediate or deferred setting is being used:
local Model = game.Workspace.ContainerModel
Model.ChildRemoved:Connect(
function(Child) -- with deferred behavior, Child is now an empty model
print(Child.PrimaryPart, #Child:GetDescendants())
end
)
Model.ChildModel:Destroy()
Workspace hierarchy:
Output with immediate behavior: Part 1
Output with deferred behavior: nil 0
The same issue appears with the DescendantRemoving event, where the argument will be an empty model. I think it is safe to say that the Child variable being an empty model here is undesired behavior.
I am unsure how so many people are affected to be honest. Is there common practice among developers (aside from the issue about input handling being delayed) where the immediate invocation of the event is required? Or to disconnect a connection the first time it is fired (and why not just use signal:Wait() in a coroutine as a replacement for that)?