Best way to dynamically switch from RenderStepped to wait()?

I am currently running some physics simulation on the client. My simulation script works alright at 30Hz, really well at 60Hz but extremely glitchy at a rate below 30Hz.

Currently to achieve these update rates, I use RenderStepped, which runs at 60Hz on good hardware.
The problem is, when someone runs the game at less than optimal hardware, say a phone, the FPS could drop to way below 30Hz, sometimes even 20Hz. Even if this is a lag spike lasting only a few seconds, this might glitch the simulation.

Ideally I’d dynamically switch to wait() when the FPS (or rather RenderStepped’s updates per second) drops below 30Hz and back to RenderStepped when FPS > 30.
Because wait() will pause the thread for approximately 1/30th of a second, even if FPS is lower than 30 and time to update is higher than 1/30th of a second, this will still result in the required 30Hz updates.
Is this currently achievable in Roblox?

If this is not possible, my only 2 options would be:
a. Run at a stable 30Hz with wait(), although not getting all of the performance out of a good machine.
b. Run at Renderstepped, whenever someone has low FPS their physics simulation might behave unstable.

1 Like

Or alternatively, a way to run a function at 60Hz (or higher), independent of FPS?
I can only think of hacky ways to do this right now, but please correct me if I’m wrong and there is a built-in function for this.

It’s possible to make code FPS independent, usually this is done through either using the delta time argument passed by RenderStepped or by using tick() to find out how much time has past and adjust your game to that value.

1 Like

I am indeed using the delta time, it’s just that the simulation is not as smooth as I’d like at a lower FPS.

The time between returns to wait() and render steps are dependant on the machine’s current frame rate. wait() resumes every other frame; at 20Hz, wait() will resume at 10Hz. Render steps occur every frame, so at 20Hz, they will occur at 20Hz. There is no way to schedule code to be executed in a shorter time that the machine’s frame rate allows.

Your simulation should not be affected by drops or raises in frame rate if you use the delta time provided to you when listening to render steps, waiting, or using any other similar yielding function. When the frame rate lowers, you will make changes in larger steps (as the delta time will be larger) and vice versa. For example:

    part.CFrame = part.CFrame *, 0, -1 * deltaTime)

That’s probably due to one of two reasons. Either your code isn’t correctly using delta time so something looks misaligned (perhaps something is non-linear with respect to time) or it’s simply just the FPS are low and so the game is perceived as less ‘smooth’. If it’s the latter it’s unfortunately going to be a case of optimisation.

^ Thank you, I was not aware of this. I will stop using wait() for simulations and keep using the more stable RenderStepped.

^ In theory it shouldn’t, but I’m simulating a dampened spring. And an update rate that is too low will result in too much ‘over-compensation’ by the spring (because the calculated force will be applied to the object for longer) and appear glitchy.

Thank you both for your help, I will try to optimize the calculations themselves to reduce my issues at a lower framerate.

This is not true – wait() tries to maintain a fixed frequency of 30Hz; it will resume every frame if your game is chugging at 20Hz.

1 Like

I’m trying to run a physics simulation on RenderStepped

Yeah–don’t do that. RenderStepped is exclusively for camera-based visual effects that can’t wait a frame to render. Tasks running in RenderStepped block the rest of the frame from running, so you want the absolute minimum of cruft in your RenderStepped loop.

Use Stepped instead. Unlike Heartbeat, the time delta from Stepped is consistent with the time delta that the Roblox physics engine is stepping on. Unlike RenderStepped, operations in Stepped don’t block the render thread.

There’s no situation where wait() is a reasonable option here in general.

I made a diagram years ago to explain how this all works.