Don't use while loops

…unless you want to wonder why the game sometimes hangs infinitely at random times.

There’s almost always a way to convert the logic into a for loop, which limit number of iterations.

I just spent a few hours figuring out why my game randomly becomes unresponsive and Godot’s CPU usage jumps to 100%. Then I realized I had 3 while loops in random places. They looked bulletproof. Still, I replace them with for loops or just different logic. Bam, problem solved.

This is just a rule of thumb. And if you ever DO use a while loop b/c seemingly nothing else can work… consider adding a counter and exiting loop when counter reaches some number that shouldn’t be possible (and log a message when that happens). This way you’re at least saying, “OK, I’m smart enough to write a while loop… but also humble enough to admit that I could be wrong.” haha

12 Likes

If you are able to reproduce this issue with a Test Project on the latest 4.2 stable, then I definitively suggest to open a new Bug Report here:
https://github.com/godotengine/godot/issues/new?assignees=&labels=&projects=&template=bug_report.yml

1 Like

Thanks man, I do use 4.2 stable but I’ve already changed the relevant code and I’m not using git so it’s too much work to go back. I’m pretty sure it was my mistake, though. Just wanted to post that while loops are inherently dangerous and it’s best to structure code to avoid them imho.

The only time the game hangs right now is if I pause the tree for a while, like 10-15 minutes (such as when I leave the game on Game Over screen). Then I can see rainbow spinner (I’m on a Mac) and have to force quit the app (but not godot editor). If I keep seeing this, I’ll dig in deeper.

4 Likes

Can I suggest you to urgently setup and use it? :wink:

Great, thanks! :slight_smile:

7 Likes

Well… it depends…in general a while loop is used to change some structure and it always should change this in an increasing or decreasing manner and also the guard has to check this apropriatly…

For example not by testing on one value (except boolean) but to check the crossing of a limit/border…
but you also can make a fatal failure with for loops… :wink:

for ( i = 9; i == 0 ; i -= 2) {    // <-- DO NOT DO THIS !!!
  someprint(i)
}

This will skip the valeu 0 !!! Even if using a for-loop… :person_shrugging:

 


So one should do this…

for ( i = 9; i <= 0 ; i -= 2) {    // check on "passing" the 0 !
  someprint(i)
}

So in general:

Computers are dump… beware what you tell them … because they will just do it… the way you told them… so better be smart… :stuck_out_tongue_winking_eye:

5 Likes

Nah, man, I’ve done that for well over a decade, let me enjoy the short period of being a solo dev when I can have zero complexity :stuck_out_tongue: (I get automatic backups to iCloud so no worries, won’t lose anything)

Very true. Also, I just added a while loop to my code so… do as I say, not as I do. I did add a counter for emergency exit this time around, though. I’m also at a bar in Vietnam having an old fashioned and coding, so quite possibly will delete this while loop tomorrow haha.

3 Likes

EDIT: I was wrong! See Dizzy_Caterpillar below.


Is this advice about GDScript or C#? Their for-loops behave very differently.

C#'s is basically a dressed-up while-loop which does some nice initialization and incrementing for you. If you write for(; someCondition;) that’s essentially exactly the same as while(someCondition), but obviously eschewing the benefit of it. In C# I totally agree: always use for-loops when you can. They’ll save your butt, they’re convenient, and there’s little reason not to.

GDScript’s for-loop is an iterator over a set. So
for i in range(0,10): creates an Array containing the numbers 0 through 9 and then iterates over the Array, providing the values through i. This is convenient, but it takes significantly more processing and often is overkill. Even if you’re literally looping over an Array, doing for element in someArray: is going to run much much slower than if you did:

var i := 0
var end := someArray.size()
while (i < end):
        var element = someArray[i]
        #do stuff here
        i += 1

Most of the time you won’t need to care about this performance benefit. But sometimes you will, especially if you have a loop somewhere in a _process function, so it’s worth knowing how GDScript for-loops behave.

2 Likes

Great addition, thank you! The way you show a while loop in your response is perfect. I was warning against using a boolean somewhere in the while loop for an exit condition, which I should have specified! But a counter that always increases until a defined point - that’s definitely the way to use them, thank you ser.

2 Likes

I always use for loops whenever possible. And if I had to use while loop the very first thing that I would wrote first is the break

	while x_is_y:
		break

To prevent forgetting it.
Also while loop inside _process / _physics_processor the like that get executed hundreds of time each second is a double danger, unless necessary.

1 Like

So for i in range(0,10): creates an Array containing the numbers 0 through 9 and then iterates over the Array, providing the values through i. This is convenient, but it takes significantly more processing and often is overkill. Even if you’re literally looping over an Array, doing for element in someArray: is going to run much much slower…

No, it doesn’t work that way. Let’s read the docs: GDScript reference — Godot Engine (stable) documentation in English

for i in range(3):
	statement # Similar to [0, 1, 2] but does not allocate an array.

Yes, you can use range to create an array of integers, but when it is used in a for loop, array is not allocated.

Let’s test which one is faster, for or while.

const LOOPS := 10000000

func test1():
	var t0 = Time.get_ticks_msec()
	for i in range(LOOPS):
		for j in range(10):
			pass
	var t1 = Time.get_ticks_msec()
	prints("time for  :",t1-t0,"ms")

func test2():
	var t0 = Time.get_ticks_msec()
	var i := 0
	var j := 0
	while i < LOOPS:
		i += 1
		j = 0
		while j < 10:
			j += 1
	var t1 = Time.get_ticks_msec()
	prints("time while:",t1-t0,"ms")

On my computer, using Godot 4.2 and release build, results are:

time for  : 979 ms
time while: 3388 ms

for takes only about 30% of the time that while takes.

Then how about iterating arrays of nodes?

var nodes: Array[Sprite2D] = []
const NODE_LOOPS := 1000000

func test3():
	var t0 = Time.get_ticks_msec()
	for i in range(NODE_LOOPS):
		for n in nodes:
			if n.frame == 1:
				pass
	var t1 = Time.get_ticks_msec()
	prints("time nodes for  :",t1-t0,"ms")

func test4():
	var t0 = Time.get_ticks_msec()
	for i in range(NODE_LOOPS):
		var j := 0
		var len := nodes.size()
		while j < len:
			if nodes[j].frame == 1:
				pass
			j += 1
	var t1 = Time.get_ticks_msec()
	prints("time nodes while:",t1-t0,"ms")

func _ready():
	for i in 20:
		nodes.push_back(Sprite2D.new())
	test3()
	test4()

Results are:

time nodes for  : 2145 ms
time nodes while: 2757 ms

In this test accessing sprite’s frame property takes a lot of time, but it is clear that for is faster, taking about 80% of the time that while takes.

So using for is faster and much less error prone than while. Of course, sometimes there are reasons for using while, but performance is not one them.

12 Likes

Nothing hotter than nerds testing stuff :hot_face: thank you for the detailed post and effort to be thorough, Dizzy!

2 Likes

That’s great! That means my info is outdated. Noted!

1 Like

@Dizzy_Caterpillar thanks a lot for all your detailed info and tests, are really appreciated! :slight_smile:

1 Like

You can start a while loop and at some point during execution await/yield to the tree’s idle_frame signal, to finish the work over multiple frames. In fact you can do this in any loop whose work may take longer than you have time for in the current frame.

3 Likes