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

I apologise if this either comes off as a broad question or an XY Problem, but I was wondering if it was possible to code a game without the use of wait(n). Essentially what I want to accomplish is coupling event-based systems with a way to perform functionality after a specified amount of time, without sleeping threads.

The closest I’ve gotten still uses wait statements but does not specify n.

repeat wait() until CONDITIONS_TO_PASS

I’m personally trying to get away from the use of wait() too frequently in my code. Sometimes it causes me grief, such as in situations where I need to wait for time to elapse before doing something. I of course will still use wait for scenarios such as waiting for a RBXScriptSignal to fire (RBXScriptSignal:Wait), but I’d like to avoid it in a general scope. For example: a dropdown topbar.

  • You scroll your mouse to the top 36 pixels of your screen for this to take effect
  • A Gui drops down. One second later, CoreGui elements are enabled
  • If you use a wait statement, then the thread is slept and 1 second later, anything below the wait runs
  • The above can cause issues, such as unintended behaviour (Gui stays dropped down and the CoreGui elements stay enabled, even if you unhover your mouse from the top 36 pixels)
5 Likes

I think the problem here (in your example) is your use case as there’s things you could be doing (eg. checking if it’s still open when you go to enable core elements) that can solve that. But I don’t see why you’d step away from wait entirely; I see it as one of the core things that you’ll always use in bigger games. There’s no (simple) way around that.

1 Like

Have you tried looking at the use of custom events?

Check out this article:

1 Like

@ThomasChabot The example is just that; an example. It’s probably not a good example to work off of because yes you can create a pseudo event to determine when the Gui reaches the down position and then set elements that way. I’d focus moreso on the content than the example.

I don’t want any of my threads to be slept unnecessarily and

The wait expression sleeps your thread. That means no code runs until the specified time n has elapsed. Perhaps knowing how wait works internally would be nice so I could replicate elapse-like behaviour in my code.


@RogueMage Ultimately that doesn’t solve my question, because I’m not looking for pseudo events, I’m looking to purge the use of arbitrary waits either in majority or completely from my codebase.

I’m aware of how to set up an event-based system; I’m not sure whether I can void the use of wait in my codebase. It’s primarily pertaining to systems where you can perform an action either after a certain amount of time passes or a condition is met (see code example above).

1 Like

You could use i.e. a tween for dropping down the UI element (or a series of tweens) and use the events on the Tween to wait for it to finish.

For general purposes:

  • Try to avoid situations where you need to yield a lot in multiple places in a single thread. Decouple your system: fire off actions in other systems and then try to complete the current thread with as little yields as possible.
  • Use events as described earlier in the thread. It is very common for something to spawn off a separate thread and that thread then fires an event that the main thread listens to. Games with an event-based architecture are much easier to manage, don’t shy away from defining tens or hundreds of custom signals in your codebase if you have to.
  • Use promises: https://github.com/LPGhatguy/roblox-lua-promise
  • Don’t reinvent the wheel: the API offers many events already and 99% of the time you won’t need to do a while wait() do loop until the condition is met and then firing off an event yourself. You can just use the existing events.
6 Likes

This is in no way actual advice, but, since we’re talking about what’s possible rather than what is advisable, it might be possible to get close to what you are talking about by using some sort of a notification/messaging system. I’ve run into the general idea a couple of times in different contexts. The gist, if I’m remembering this correctly, is to have a Dispatcher class that keeps track of messages you send to it (another class) and fires them off at the appropriate times. The messages consist of (essentially) an object ID, an action to perform (fire an event?), and a time when the action needs to happen. These are stored in a sorted list by the aforementioned time property. The manager runs in a continuous loop and dispatches messages during each iteration until one is found with an execution time that is later than the current time. Obviously, not a replacement for wait(n) where n is very small.

A usable implementation might offer something similar to what the delay() is already providing. I’ve considered doing some sort of messaging system when I get to programming NPCs in my little game (in case I want to cancel a msg or the like) , but nothing on the scale you are contemplating. I wonder if someone has already tried to write an entire game without using wait(). I guess that person would be among those you are hoping to hear from :grinning:

1 Like

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