A Better Way for ELS Community to Optimize ELS Codes for Performance

STOP USING WHILE TRUE DO LOOPS FOR PATTERN CHANGE!

folks I think this is basic optimization and coding knowledge


Foreword

Why, might you ask? "It works just like any others," you reply. Well, yes, you aren't wrong, you won't see too much of a difference in studio. But in an actual, game, you will suffer.

“What are you talking about?” Well, here it is.



What's the Problem??

First, while true do loops takes up resources. A while true do loop with nothing can crash your game and your PC due to the fact that it is running without exhaustion, and it is just dying.

So what do most ELS developers do? Simple!

  • while true do wait(0) end or
  • while wait() do end loops

What are the problems with those two now?

From a plain view, they are innocent (or so would you think) because you added a delay, you prevented the exhaustion. However, you have now further exhausted server resources by adding that wait(). Let’s take a look at the script performance from a singular car with no ELS on:

It looks innocent at first glance, however, even though activity is low, it is definitely not good to have a script running over the rate of 10.0/s for something like ELS. Once you are in an actual game taking into consideration of every other things running (ex. more cars, guns, shops, etc), it will become apparent that ELS scripts is taking up way too much script rate and in some cases, performance..


Second, it is worthy to state that wait(), delay(), and spawn() (not to be confused with wait(n) and delay(n), or RBXScriptSignal:Wait()) run on ROBLOX’s 30 Hz pipeline. The minimal time to have it waited is 1/30th of a second, but in practice, they sometimes don’t achieve that, which leads to instability, tremendous delay, or sometimes doesn’t even run at all. This practice is known as code smell, an indication that there’s a deeper problem into this practice, and for the ELS developers, the “deeper problem” means performance issues and more.


“But less code is more readable!”
“But less code runs faster!”

These statements you have heard may be true in some cases. For the way it is used in ELS, however, it is actually more resource-intensive and will impact overall game performance more than it should.


More explanations can be found here. I may be inaccurate or have worded myself incorrectly on some of my statements, and these folks who wrote the article below did a better job than I have.



How can I solve it?

Well, my dear friend, do you know that RBXScriptSignal (also known as an event) exists???

Let’s take a look at this code below:

local ELS_State = script:FindFirstChild('ELS')

while true do 	-- Initiates the loop
	wait()		-- Puts a 'delay' so that resource 'does not exhaust'
	if ELS_State.Value == 0 then
		-- Idle
	elseif ELS_State.Value == 1 then
		-- Pattern 1
	elseif ELS_State.Value == 2 then
		-- Pattern 2
	end
end

The first thing we can do is to switch out while true do with an event so that it doesn’t constantly run:

local ELS_State = script:FindFirstChild('ELS')

ELS_State:GetPropertyChangedSignal('Value'):Connect(function() 	
	--[[
	We have switched out the while true do to an event
	Now it doesn't constantly run and
	It is much better for the overall game performance
	]]
	
	-- wait() is not needed anymore here
	if ELS_State.Value == 0 then
		-- Idle
	elseif ELS_State.Value == 1 then
		-- Pattern 1
	elseif ELS_State.Value == 2 then
		-- Pattern 2
	end
end)

Now, you may say “Oh my pattern only runs once and it doesn’t repeat!” There’re two fixes to that, both are plausible depending on whichever one you like the most.

Method 1: repeat until

local ELS_State = script:FindFirstChild('ELS')

ELS_State:GetPropertyChangedSignal('Value'):Connect(function() 	
	--[[
	We have switched out the while true do to an event
	Now it doesn't constantly run
	It is much better for the overall game performance
	]]
	
	-- wait() is not needed anymore here
	if ELS_State.Value == 0 then
		-- Idle.
		-- We don't need a loop here since this is idle.
	elseif ELS_State.Value == 1 then
		repeat
			-- Pattern 1
		until ELS_State.Value ~= 1 -- Exits the loop and moves on
	        -- Function to reset all lights to zero	
	elseif ELS_State.Value == 2 then
		repeat
			-- Pattern 2
		until ELS_State.Value ~= 2 -- Exits the loop and moves on
                -- Function to reset all lights to zero
	end

end)

Method 2: while condition do

local ELS_State = script:FindFirstChild('ELS')

ELS_State:GetPropertyChangedSignal('Value'):Connect(function() 	
	--[[
	We have switched out the while true do to an event
	Now it doesn't constantly run
	It is much better for the overall game performance
	]]

	-- wait() is not needed anymore here
	if ELS_State.Value == 0 then
		-- Idle.
		-- We don't need a loop here since this is idle.
	elseif ELS_State.Value == 1 then
		while ELS_State.Value == 1 do
			game["Run Service"].Heartbeat:Wait() -- We used heartbeat:Wait() to prevent exhaution and prevented issues caused by wait()
			-- Pattern 1
		end -- Exits the loop if the condition is not met and move on
		-- Function to reset all lights to zero here
		
	elseif ELS_State.Value == 2 then
		while ELS_State.Value == 2 do
			game["Run Service"].Heartbeat:Wait() -- We used heartbeat:Wait() to prevent exhaution and prevented issues caused by wait()
			-- Pattern 2
		end -- Exits the loop if the condition is not met and move on
		-- Function to reset all lights to zero here
	end

end)

It’s totally not necessary to use a while true do break end loop for the ELS patterns as if the condition is not met at the end of the loop, it exits automatically. cmon folks this is basic scripting knowledge


You might now say "oh I’ve heard that you can use the same while true do method but instead of using wait() you can use heartbeat:wait() or I can use coroutine.wrap()". Now even though that will work better than wait(), it will not be ideal for the sake of maximizing performance, and it is totally unnecessary to use those Heartbeat:Wait() or coroutine.wrap as the ELS script does not need to run 24/7, and is only required to actually display a pattern. Furthermore, overusing RunService.AnyEvent:Wait() can sometimes backfire on you in actual games, and will barely be any good from wait() as it is almost the same thing.



Conclusion

Is it a little more coding than while true() do method that the ELS community is used to? By all means, I can’t disagree, it is, indeed, less code to type out, and less copying and pasting so on and so forth. Not only is while true do wait() end loop a design issue, but it is also a performance issue. Pick whichever one you want: Continuing using while true do wait() end or while wait() do end loop and a laggy & inaccurate experience for you game, or take a little more time to set up the listeners such as ScriptSignal:Connect(callback) or ScriptSignal:Wait(), and have your game be a much smoother ride and less inaccurate simulation & less laggy experience. The choice is yours.



Update at Sept. 19, 2022

Read the added content at your will

If you really want a comprehensive solution towards game performance with ELS, since you don’t necessarily need ELS to be extremely-sharp-accurate, what you can do is to simply have the client do all the rendering lights work. How it essentially works is client fires control, server recognizes it, then fires all client to let players render the lights themselves.

“Well shouldn’t you not trust the client?”

Even though I’m not going to have a debate on it, here’re the things you can consider:

  1. One, client-to-client interaction will regardless go through server, what’s malicious about server firing something back to client?

  2. If we can let client do the rendering, we are going to open the server up for more demanding tasks such as firearm bullet casting, other security, etc.

  3. ELS is such a small asset in your roleplay or driving game. Would you really consider putting ELS as a priority for your game and eating up a huge percentage of your task scheduler, or would you consider an asynchronous operation that has a difference of 0.1-1 second which will barely make any difference?

There’re many different ways to tackle this issue, but at the very least, stop using arbitrary waits.

23 Likes

I agree with this post entirely, I started developing an ELS Module a while back that kind of helped with this, I still update it occasionally but not often.

3 Likes

I guess I agree with what you’re saying? I’m mostly confused because I don’t know what ELS means. Apparently it has something to do with statements? I’m just really confused at the moment. Do you mind explaining that?

3 Likes

Those are all outdated for the new task.wait(), task.delay(), etc. These new versions run on Heartbeat and thus have a lower limit than 1/30th of a second. Also, they are extremely accurate compared to the outdated versions.

Doing something like RunService.Heartbeat:Wait() is the similar to task.wait().

same here lol

3 Likes

ELS is an acronym for Emergency Lighting System. It’s the system that all emergency vehicles have equipped that make the lights work.

4 Likes

The post focuses on mostly replacing while true do wait() with RBXScriptSignal:Wait(), I am aware of the new library changes though but I still don’t recommend doing while true do task.wait() end or while task.wait() do since it just defeats the purpose of optimization… But yes, it is much accurate.

me ranting about while true do loops with RunService.____:Wait

One interesting thing to note is that while true do RunService.Event:Wait() end is not only bad because it’s a infinite while loop, but also because a :Wait call to a RBXScriptSignal needs to allocate memory for a linked list node.

:Wait really just makes a connection, yields the thread it’s being called from, and disconnects it once :Fire is called and resumes the thread with the arguments passed through.

Memory allocation every frame is a no-no. It’s nice to point out that the garbage collector/allocator will reuse memory from stuff that has been gced, that’s how it works, but it’s still for me a big no-no for me because even then, you are still creating a linked list node each frame, just to disconnect it right after.

If you’re using task.wait / wait you basically have the same issue just with a different story behind it. You shouldn’t have an issue with bigger numbers.

while true do
    -- ...

    RunService.Heartbeat:Wait()
end

Is not that much better than:

while true do
    -- ...

    task.wait()
end

But,

RunService.Heartbeat:Connect(function()
    -- ...
end)

Is much better than all alternatives above.

This :Wait allocation thing is not even the biggest reason why those loops are bad, not even close, but I wanted to point that out since I’ve been looking into event-driven code for a long time.

while true do
    RareEvent:Wait() -- Event that fires only here and there, isn't a per-frame thing.

    -- ...
end

Is not that bad if you’re trying to yield a thread while doing something, but when you don’t want to yield the thread, then just use :Connect!!!

While probably not as useful in all this ELS business, you can always create custom ScriptSignals, which makes things a lot easier and helps you get rid of polling in overall code design.

2 Likes

Just out of curiosity since people have asked, would you think BindToRenderstep be relatively similar to the usage of Heartbeat:Connect()?

Even though, as I said, this post focuses on how to run scripts only at the necessary time (by using RBXScriptSignal), but I’m also curious on whether if it’ll behave the same as Heartbeat or Renderstepped performance wise.

Ugh I was gonna answer this sooner and forgot about it sorry!

Not really, BindToRenderStep fires slightly before RenderStepped, but thing is, if that doesn’t require being accurate down to the frame it happened in, then there’s no reason to use RenderStepped.

For example, camera uses BindToRenderStep because it needs to react to user input changes and handle them right away, but if whatever you’re doing doesn’t really depend on that, then just use Heartbeat.

Screen drawing happens right after all BindToRenderStep / RenderStepped connections initialize.

Heartbeat is for more ‘lower priority’ type of things, and so if it’s just vehicle lightning like stated above, then just use Heartbeat.

Performance-wise I would say, not really? I don’t think so, but I would put higher stress on Heartbeat because ???
It’s instinct, I don’t know.

.Stepped is cursed don’t use it

1 Like

I personally feel like I have my system set up pretty nice, What do you think?

global.Stage1 = function()
	if ELS_Handler:IsOnStage(Values.ELS.Value, 1) == false then 
		return false 
	end		

	ELS_Handler:StageRepeat(10, "Stage1_1", function()
		if ELS_Handler:IsOnStage(Values.ELS.Value, 1) == false then 
			return false 
		end		
		
		wait(0.1)
		On(AllLights["F1"], true)
	end)
end

global.DisableLights = function()
	Off(AllLights["F1"], true)
end

function RunChange()
	task.spawn(function()
		if Values.ELS.Value == 1 then
			global.DisableLights()
			repeat
				wait()
				global.Stage1()
			until Values.ELS.Value ~= 1
		else
			global.DisableLights()
		end		
	end)
end

Values.GlobalChange.Changed:Connect(function()
	RunChange()
end)

RunChange()

I love how you have the code set up. From a plain view, it looks nice (even though the programmers that run on while true do will have a hard time understanding the code and have loads of comments).

Personally, even though just for the looks and organization, I would prevent calling a function over and over again if not required and just set an instance’s transparency to 0/1 or activate/deactivate it. It’s a general practice and it’s typically the fastest since it’s just like flip a switch.

Now, let’s take a look at the function RunChange(). This is how you have it set up:

function RunChange()
	task.spawn(function() -- Here's something that I wouldn't do x1
		if Values.ELS.Value == 1 then
			global.DisableLights()
			repeat
				wait() -- Here's the problem x1
				global.Stage1() -- Here's something that I wouldn't do x2
			until Values.ELS.Value ~= 1
		else
			global.DisableLights()
		end		
	end)
end

Here’s my breakdown from code optimization experience:

Circle back to my original statement, I still love your code setup idea: clean, readable, and avoided major ELS polling, from a plain view at least.

1 Like

Firstly, Thank you :heart:

Secondly I’d like to point out a few things on why I did what I do with my code,

Reason being I run a new task inside the “RunChange” function is cause I’m going to be running several other things inside that same function, Such as TA (Traffic Advisor) will be ran in all the same script. I may be wrong but its a lot more performant to have all your ELS ran via one script instead of having 4 - 5 different scripts running at the same time, Which is why I have the “GlobalChange” signal, whenever the ELS stage change or TA change gets changed it also changes the GlobalChange value triggering all the task and stopping the ones that aren’t needed.

for both my On/Off and all my color functions they do more than just make the light go on and off, Since I run TA, Cruise and a few other things off the same light parts I make sure the sections handling those lights have priority instead of the Stage 1 - 3 segments.

function On(Object, RespectTA)
	pcall(function()
		if Object ~= nil and IsTrafficEnabled(Object, RespectTA) == false and IsCruiseEnabled(Object) == false then
			local LightSource = FindLight(Object)
			--Object.Transparency = 0.02
			TweenService:Create(Object, TweenInfo.new(0.1), {Transparency = 0.02}):Play()
			
			if LightSource then
				LightSource.Color = Object.Color
				LightSource.Enabled = true
			end
		end
	end)

	return Object
end

function Off(Object, RespectTA)
	pcall(function()
		if Object ~= nil and IsTrafficEnabled(Object, RespectTA) == false and IsCruiseEnabled(Object) == false then
			local LightSource = FindLight(Object)
			--Object.Transparency = 1
			TweenService:Create(Object, TweenInfo.new(0.1), {Transparency = 1}):Play()
			
			if LightSource then
				LightSource.Color = Object.Color
				LightSource.Enabled = false
			end
		end
	end)

	return Object
end

I do agree some of the things I did throughout the function are a bit of bad practices such as constantly running TweenService throughout the module and running pcalls as I do think they do cause a slight delay in the runtime, Other than that the On/Off functions do a bit more than just making sure the lights get turned on and off.

The reason for that being there is I usually work in stages when I do my ELS (I actually just started like a month ago on it :eyes:) but I’ve been scripting since the beginning of 2020

function RunChange()
	task.spawn(function()
		if Values.ELS.Value == 1 and not ActiveFunctions["Stage1"] then
			global.DisableLights()
			repeat
				task.wait()
				global.Stage1()
			until Values.ELS.Value ~= 1
		elseif Values.ELS == 2 and not ActiveFunctions["Stage2"] then
			global.DisableLights()
			repeat
				task.wait()
				global.Stage2()
			until Values.ELS.Value ~= 2
		elseif Values.ELS == 3 and not ActiveFunctions["Stage3"] then
			global.DisableLights()
			repeat
				task.wait()
				global.Stage3()
			until Values.ELS.Value ~= 3
		else
			global.DisableLights()
		end		
	end)
end

As you can see I’ve done some minor adjustments in this since I last sent this function but basically, when it changes through its stages it disables all the prior lights making sure it all reset and all the Disable Lights run through its Off function still allowing the other segments of the code to take priority if it’s active. After it does all that it’ll run through a repeat constantly running that code but while the stage is active that section of the code yields waiting to run again, I did this for my instant breaks I run periodically throughout the stage to make sure it has little to no delay during stage switching or when you fully cut off the ELS I try to make sure it ends the script no matter where it is in the thread.

I may have forgot a few things but for the most part that’s why I did what I did in some sections of the script.

2 Likes

Update, I’m almost finished with the ELS on one of my vehicles, and It’s done really well in terms of performance. I’ve gotten it pretty slimmed down to where it only runs maybe 8 activity with everything enabled.

The last thing I have to finish is the TA and that’ll pretty much be my final result for the entire vehicle.

My vehicles ELS

Random model I got from the toolbox

2 Likes

ELS Code example of how I do it:

while true do
wait (0)
if script.Parent.on.Value == 1 then
script.Parent.Red.Transparency = 0
script.Parent.Blue.Transparency = 1
wait (0.1)
script.Parent.Red.Transparency = 1
script.Parent.Blue.Transparency = 0
wait (0.1)
else
script.Parent.Red.Transparency = 1
script.Parent.Blue.Transparency = 1
      end
end

That is literally the point of this topic, to avoid how you are setting up your code and pooling

It doesn’t seem like this was mentioned but indexing Instances especially is kind of expensive.
Well, anything Instance related is expensive.

I always recommend caching Instances in locals, not only it helps in performance but also it just looks better if you do it right, if you’re using a proper style choice.

2 years later & people still script ELS exactly like the way you are showing to not to
Although I’ve went in a completely different direction, I’ve still found this useful to adjust to my system, in particular the repeat until part, although my difference is that I use attributes in a single value with GetAttributeChangedSignal()

But I’ve yet to see how the script performs on their own