So there’s this issue I’ve ran into a few times by now; I regularly write code which waits for a ‘response’. Often times the response is given by my framework’s font- or back-end so a response it pretty much always guaranteed to be given. Sometimes however the response is given by a player. Players can be really unexpected sometimes and might not give a response at all which could complicate some stuff.
Let’s say for example that I’m making a dialog system. Some parts of the dialog allow you to select one or more responses and to spice things up I can put a timeout on some parts of the dialog. For example, the person you’re talking to might ask “Hey, do you like fries?” and the player would react with “Yes” or “No”. If the player doesn’t react for more than 10 seconds though I want the dialog to be cut short and transition to another text which says “Hey, are you even listening?”. Normally I would write my code in such a way that a RemoteEvent is triggered when a player selects a response and the code which is responsible for the dialog would yield until a response is given. Given that in this scenario I want to add a timeout and given that the ‘Event:Wait()’ construction doesn’t support timeouts like say WaitForChild does, the required logic to make this work suddenly becomes a ton more complex. This is an example of how I would have to write my code right now:
local ResponseTime = 10
local GivenAnswer = ""
ExampleDialog.Text = "Hey, do you like fries?"
coroutine.resume(
coroutine.create(
function()
local TriggerMoment = game.workspace.DistributedGameTime
-- substitute variable in case the timeout is triggered and the next dialog overrides the answer
local AnswerSubstitute = PlayerGaveAnswerEvent:Wait()
if game.workspace.DistributedGameTime-TriggerMoment > ResponseTime then
return -- the dialog system already moved on so let's return
else
GivenAnswer = AnswerSubstitute
end
end
end
)
coroutine.resume(
coroutine.create(
function()
wait(ResponseTime)
end
)
)
if GivenAnswer~="" then
local SubAnswer = GivenAnswer
GivenAnswer = "" -- reset answer just in case
GoToDialog(SubAnswer)
else -- woops! player didn't respond in time
GoToDialog("No response to fries")
end
Excuse my ugly usage of coroutines by the way.
Now if we could pass a number in Event:Wait() which defines the maximum time the code will yield before it drops the wait, the required logic for my case suddenly becomes a lot more simple:
local ResponseTime = 10
ExampleDialog.Text = "Hey, do you like fries?"
local GivenAnswer = PlayerGaveAnswerEvent:Wait(ResponseTime)
if GivenAnswer~=nil then
GoToDialog(GivenAnswer)
else -- woops! player didn't respond in time
GoToDialog("No response to fries")
end
Quite a big difference right?
This is only one of many scenarios where a timeout for the Event:Wait() method would be an awesome feature. It could for example also be used for turn-based games where players only have a certain amount of time to make their moves. It might even be used for round-based games where a round only starts if the game receives more than X responses from different clients within Y seconds. And as always, there are many more situations than listed in which this feature would be awesome.
I said this about the suggestion of making it return nil:
Making it throw an error when it times out is much more consistent API-wise than having it return nil, because some events don’t return parameters. So I would suggest that over returning nil.
Some events gives the result of nil (such as ObjectValue.Changed, Tool.Activated, Tool.Unequipped etc.)
In that case, how would we be able to differ between a timeout and an actual event change?
As to how WaitForChild is handled with timeout, it throws an error if the timeout is reached, and the developer needs to wrap it in pcall to detect if the call was successful or not. Perhaps something like this would be good? I don’t think the engineers would like a sudden change in what data a method returns depending on an argument.
local RbxUtility = LoadLibrary("RbxUtility")
local function waitUntilTimeout(event, timeout)
local signal = RbxUtility.CreateSignal()
local conn = nil
conn = event:Connect(function(...)
conn:Disconnect()
signal:fire(...)
end)
delay(timeout, function()
if (conn ~= nil) then
conn:Disconnect()
conn = nil
signal:fire(nil)
end
end)
return signal:wait()
end
-- Example usage
local child = waitUntilTimeout(game.Workspace.ChildAdded, 5)
if (child == nil) then
print("ChildAdded did not fire before timeout")
else
print("New child " .. tostring(child) .. " added")
end
@buildthomas Thanks for letting me know. Looks like I completely missed that topic. I’ll be more careful next time when making a feature request.
@Merely Awesome, that’s exactly what I needed. Although, I consider myself to be a decent programmer but that doesn’t look ‘fairly straightforward’ to me.