Is there a way to write whole game codebases without the use of arbitrary waits?

I have thought about creating a pseudo thread scheduler, but I am wondering if that is worth the potential overhead, inefficiency, bad practice or whatever problems there are that I’ll run into. On top of trying to get away from the use of wait(n), I’m switching over to more event-based systems over forward scopes.

2 Likes

You could write a whole game without using wait() at all. I also agree that using arbitrary wait()'s are inefficient. But to polish a game, sometimes you gotta wait() :wink:

I think using wait() is a staple for of any kind of cinematics, but not very useful or needed to exchange data and process in an event based system. For example, I used tons of them in a cinematic trailer I’m working on, and none of them in a mostly functional Tycoon(but we haven’t gotten to gui or cinematics).

To avoid them sleeping the whole script, there is spawn() Which afaik is basically like a couroutine but easier to implement.

Like this:

local function DoSomeDelayedThings()
   wait(5)
   print("This function started 5 seconds ago")
end
--script
print("hello")
spawn(function()
   DoSomeDelayedThings()
end)
print("world")

Also, Roblox has the neat PropertyChanged listeners. I’ve been preferring those when possible over wait()-until-conditions, as I assume they run at source level and not Lua. But who knows?

1 Like

Spawn isn’t a coroutine, it runs on the legacy thread scheduler and has a built-in wait statement internally. I can achieve the same functionality with spawn by running this code:

coroutine.wrap(functionBody)()

There also exists the function delay() to delay the execution of code, which in my case would work better than using the wait statement and then running code. IIRC the delay statement is still a wait internally, but it cuts out usage of the actual statement and executes the function body in a separate thread.

PropertyChanged is an event signal. I’m good with event-based signals; what I’m looking for is code without the use of waits (which I assume the proper terminology for this is thread scheduling).

3 Likes

Sometimes I need to yield for events and need to set a timeout, I usually do with delay and coroutine.resume, of course I check if it wasn’t resumed by the event before. I thought delay could be relevant here because of it’s similarity to wait.

Yeah, that’s about what I’m looking for - yielding without wait statements. coroutines would definitely be helpful but all-in-all if I can’t calculate time passage or delay the execution of threads without being able to interrupt it, it’s moot in terms of addressing my concern.

I think that in various cases I still have to use wait, so what I might try doing is using Promises and the delay function. If n of delay is internally a wait statement, it might not be a proper solution, though I’m giving that method a shot.

  • The Promise would handle the aftermath of a call
  • The delay statement would delay the execution of a function body
  • I could resolve or reject a Promise before n in delay(n, functionBody) passes
  • The functionBody of delay would therefore be irrelevant if the Promise has already been handled

Your game may be modularized enough for you to not use wait, but sometimes you need to do expensive tasks and you wouldn’t like for it to hang the player, such as generating terrain. Your goal in that example would be to yield before tasks such as rendering are skipped too much. A round-based game certainly would use wait(roundTime) somewhere, because it would be the most effective way to do it, here’s a code that would do the same but busy wait:

local function busyWait(n)
	local t = tick()
	repeat until tick() - t > n
end

Doesn’t use wait, but nobody should really do it.

You may not want to use wait but you cannot run away from the roblox thread scheduler, wait is essentially telling it: “I’m yielding, wake me up after n seconds”, which is useful for games where there isn’t necessarily an event telling when the game should end, you are responsible for that. wait is efficient in that case and allows your thread to be scheduled according to the time you called it with, and so the thread manager can resume other threads (such as rendering) and do it’s job.

Here's a few things the thread scheduler is running on my studio right now.

wait yields the same way coroutines and events do:

local thread = coroutine.running()
delay(0, function()
	coroutine.resume(thread, "Wake up!")
end)
print(wait()) --> Wake up!
print(coroutine.yield()) --> 0.041531300000088 1528.3572404038
local thread = coroutine.running()
delay(0, function()
	coroutine.resume(thread, "Wake up!")
end)
local Bindable = Instance.new("BindableEvent")
print(Bindable.Event:Wait()) --> Wake up!
Bindable:Fire() --> Here the thread scheduler tells us: cannot resume non-suspended coroutine
--It's already running; it was resumed before!

The trick is to yield as little as possible, and not being late, to allow all the scheduled threads to be on time. If a thread is yielding right after being resumed without really doing anything it’s being inneficient.

This diagram would help if it wasn’t unavailable…

5 Likes

Modularisation doesn’t change anything. All that says is that you’ve siphoned off your code into modules rather than following an imperative coding paradigm. That says nothing about the execution between threads or of threads at all. You still have to find a way to yield if such is necessary.

In the OP, I did mention

Which implies that while what I’m searching for is how to write a codebase that supports scheduling without the use of wait, I still have to use it in cases where I don’t want to hang up the client or server - very scarce usage of it (within for or while loops, for example). Depending on my use case, the sleeping of a thread may be absolutely necessary and I may have no way out. I can understand this for a separate thread though, but overall I’d like to use wait as little as possible.

Using wait(roundTime) for a round-based game is actually a very terrible idea, unless you’re going for a classic feel of “wait x seconds before something happens, then something else happens”. Even then, passing the entire roundTime in a wait statement is bad. This sleeps your entire thread for however long roundTime is specified and doesn’t allow you to take explicit control of what happens in between. A for or while loop is better suited for the use case of a round-based game.

-- For Loop
local FEED_FORMAT = "%d seconds until the round ends. Survive!"

for i = roundTime, 0, -1 do
    workspace.Feed.Value = FEED_FORMAT:format(i)
    wait(1)
end

-- While Loop
local FEED_FORMAT = "%d seconds until the round ends. Survive!"
local RoundTime = 300
local PlayersAlive = 15

while RoundTime > 0 do
    if PlayersAlive < 0 then break end
    RoundTime = RoundTime - 1
    workspace.Feed.Value = FEED_FORMAT:format(RoundTime)
    wait(1)
end

This is similar to the code I posted in OP, except it allows me to specify a condition to be met or wait until a certain amount of time has elapsed. That has better use to me over busyWait, which seems even worse than using the wait statement raw.

Hence an event-based system in the OP. I am trying to use wait as little as possible, or not at all. If I’m not able to and I need to schedule threads, then I’m looking for a way to schedule in such a way that I can also interrupt it or prematurely execute code if a condition is met prior to n elapsing. That’s where I step in with Promises as mentioned above and pointed out to me by buildthomas.

I’m not really sure about the relevance of the code posted near the bottom of the response. The first one still uses a wait statement and that’s not how I write my code; it also completely destroys my use case. As for the second one, RBXScriptSignal:Wait() is fine for what I want because wait and RBXScriptSignal:Wait() are different in use case but fundamentally the same. RBXScriptSignal:Wait() yields until the signal is fired, which again, hence an event-based system. This is fine. Wait alone is not. See OP:

How would you timeout an event?

I wrote something like this because it’s very useful. I never use wait() in my code.

local execution_scheduler = {}

local calendar = {}

function execution_scheduler.update()
    for callable, data in pairs(calendar) do
        if tick() >= data.time_to_execute then
            calendar[callable] = nil

            local callable_thread = coroutine.wrap(callable)
            callable_thread(unpack(data.arguments))
        end
    end
end

function execution_scheduler.schedule(execution_delay, callable, ...)
    calendar[callable] = {
        time_to_execute = tick() + execution_delay,
        arguments = {...},
    }

    return callable
end

function execution_scheduler.unschedule(callable)
    calendar[callable] = nil
end

return execution_scheduler

As you might figure, RunService.Stepped gets binded to execution_scheduler.update. You could disconnect this when there’s no more tasks to execute, and reconnect it when one is added.

Performance in this case is not something I’d be concerned about.

2 Likes

@FieryEvent When did I mention anything about timing out an event? That’s not my aim. I’m not timing out any events in my code.


@Avigant Performance is far from my mind right now, though I’ll look into this. It definitely looks like a neat scheduler and as you said, you don’t use wait in your code, so it’s very close or almost exactly what I need.

I’ll have a look into it and try it out when I have a pocket of free time.

If there are no tasks after the few next Stepped but still one that has been scheduled to a big amount of time, would your code keep resuming to check a condition?

Yes. We are talking about a single event listener being called at 60 hz, the computation penalty should be negligible.

1 Like

I don’t really understand why you feel the need to expunge the use of wait(n) from your code. There is nothing wrong with a coroutine yielding when it doesn’t need to do anything for a predetermined amount of time. A for or while loop would need to poll the time (using tick() or os.time(), etc.) to achieve a fixed-duration delay, at which point you’re just implementing your own one-off task scheduler in a system that already has one running.

Suppose you want something to execute in 5 minutes, e.g. to end a round of gameplay. Instead of using wait(300) to schedule a coroutine to resume using the built-in task manager, are you proposing to replace it with a while loop that yields with something like RunService.Heartbeat:Wait()? That’s going to resume your coroutine 60 times a second–completely unnecessarily–just to check “are we there yet?” There is nothing inherently wrong with checking timestamps, if you’re doing something like making a whole time-based game events system, but it does not make any sense to me to just go through your codebase and replace every wait(n) with a while loop polling construct just because of some irrational dislike of wait.

Not only is this irrelevant to the question I posed (it doesn’t help), but I thought I made it explicitly clear that I do need things to happen between gaps of time. Sleeping the thread entirely prevents code execution within that time, which means I need to flag another script to perform code execution within that time; or establish a wait within a separate scope so that it does not interrupt the execution of the main thread.

The difference between a for loop and a straight wait statement in my example has already been explained. If you have a straight wait, there isn’t any explicit control you can gain between time. With a for loop, you can cancel out during the loop execution if you need to.

Wait Statement:

  • Execution freezes for n seconds
  • Need to rely on separate or external threads

For Statement:

  • Identify what i is currently at between initial and final (, given increment)
  • i can be used to display various things, e.g. “time left”
  • For loop can be terminated midway if a condition is met, therefore also stopping the thread from unnecessarily running for additional time
  • Can run code parallel to for scope

I’m not proposing anything. I am asking how to write codebases without the use of wait or to use it minimally in such a way that is not intrusive or against my use cases. Details are specified in the OP.

Not only am I not doing this, but my dislike of wait statements is not “irrational” (it is in fact preferential, so I don’t see how it can be irrational). I have stated all necessary information in the OP and previous posts.

1 Like

I made an entire thread on how to avoid wait() and why. There is, and as I say in the thread, wait() does not belong in production codebases at all.

1 Like

What’s not made clear anywhere above is why you are asking how to write code without using wait. You seem to think there is something bad about “sleeping a thread” when being able to suspend and resume coroutines is actually a huge and desirable benefit of having them in the first place. You keep mentioning things like “execution freezing” and “needing things to happen in between”, and all this just suggests to me is that you’re not using coroutines effectively or correctly. Of course you don’t want to block your main thread, but no one is suggesting (or doing) that. Wait(n) statements go in your coroutines and event handlers (functions passed to event.Connect()), not your main game loop!

Just to be clear, creating a coroutine that uses wait(n) is a correct, efficient and desirable way to do something like a long fixed delay or periodic, infrequent execution of a block of code. The next best way would be a timestamp comparison inside an existing game loop that’s executing periodically on something like RunService.Heartbeat. For and while loops should generally be avoided for long-running game loops, since they can stop unexpectedly (e.g. if some error causes a script execution timeout) with no way to resume.

This discussion is only about the wait(seconds) variant. The no-argument variant of wait() is a different beast, and I don’t think anyone will contest that there is always a better option for that one.

1 Like

It’s there in the OP? My reason for writing this is scattered everywhere.

I’m not using coroutines. It’s explicitly stated in the OP that I’m using an event-based system. I’m fully aware of how to use coroutines. This thread pertains to abdicating the use of wait(n). Suggesting coroutines initially would been fine, but that assumes I have any in the first place. I’m asking how to write codebases without wait(n), given coroutines or not.

See the responses on this thread and my reply to each one of them. If it wasn’t suggested, a solution would be marked now. See above as well. Putting a wait in my event handlers doesn’t solve my issue.

I do not want to use wait(n). I don’t know how many times that can be reiterated in my responses to this thread.

I’m just going to flag this closed. I don’t feel like it’s yielding anything productive anymore.

2 Likes

This thread seems beyond redemption at this point. Rather than continue arguing about it, lets just put our keyboards down and let it close.

4 Likes

Requested by topic creator.