Timeout argument for Event:Wait()

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.

Let me know what you guys think.

7 Likes

The same thing was posted in December:

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.

3 Likes

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.

Edit: ninja?!

Fairly straightforward to code.

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
19 Likes

@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. :frowning:

1 Like

Edit: This post was
https://www.youtube.com/watch?v=3BE95qqkTc0

1 Like

But… it isn’t… why are you lying?

2 Likes

Coroutine: 0.0030994415283203
Spawn: 123.21496009827

Measured in milliseconds (results may vary) - spawn runs during the next step (IIRC), while coroutines runs in the current step.

local start1 = tick()
coroutine.resume(coroutine.create(function()
	print("Coroutine:", (tick() - start1) * 1000)
end))

local start2 = tick()
spawn(function()
	print("Spawn:", (tick() - start2) * 1000)
end)
2 Likes

Oh crap! Didn’t realize that. I’ll edit my post.

:stuck_out_tongue: Alrighty, we all make mistakes sometimes.