Well this caused me a headache! And the answer was in the docs all the time

I cannot believe I just spent 4 harrowing hours on this piece of code, I was pulling my hair out and gnashing my teeth so I thought I would share it here. It just was not resulting in what I expected.

My code causing all sorts of strange behaviour.

for ability_ref in DataStore.available_abilities:
	if DataStore.used_abilities.has(ability_ref):
		DataStore.available_abilities[ability_ref] -= 1
		DataStore.used_abilities.erase(ability_ref)
		if DataStore.available_abilities[ability_ref] <= 0:
			DataStore.available_abilities.erase(ability_ref)
			DataStore.activated_abilities.erase(ability_ref)

The variable available_abilities is a dictionary. The other two, activated_abilities and used_abilities, are arrays.

These are used extensively in my players abilities code and I know they are all fine. The code above just simply (or so I thought) checks if the available ability was used during the previous level, and if so depletes its available quantity by 1. Then, if it has been used, checks to see if it is completely depleted, in which case it is removed from the players available abilities. Easy-peasy you may think. And so did I until all mayhem broke lose.

After at least 4 hours testing this again and again in gameplay, tracing and tracking the problems down it came to that piece of code above. It turns out the correct code is this:

Can you spot the error? Here is the corrected code:

for ability_ref in DataStore.available_abilities.keys():
	if ability_ref in DataStore.used_abilities:
		DataStore.available_abilities[ability_ref] -= 1
		DataStore.used_abilities.erase(ability_ref)
		if DataStore.available_abilities[ability_ref] <= 0:
			DataStore.available_abilities.erase(ability_ref)
			DataStore.activated_abilities.erase(ability_ref)

Can you see the difference? The all important difference that after today I shall never forget! A painful lesson I thought I would share. It all comes down to this note at the bottom of the dictionaries page:

The lesson I learned the hard way today:

Note: Erasing elements while iterating over dictionaries is not supported and will result in unpredictable behavior.
Dictionary — Godot Engine (stable) documentation in English

They were not kidding when they wrote that note! You can only iterate through dictionary keys if you are modifying the dictionary in the loop. Otherwise you truly get some very peculiar, inconsistent and downright unfathomable behaviour.

Hmph. At least I got an answer in the end. What a day.
(I really must pay more attention to those humble notes in the docs)

4 Likes

You might like to know that this isn’t specifically a GDScript or Godot thing. I do not know of a language in which removing elements from a collection even while you are iterating over its contents will not normally cause errors or strange behavior. This has to do with how iteration fundamentally works in programs, and how an iterator can lose its place or end up with inconsistent information if the collection changes unexpectedly. It would be kind of like if your friend had a pile of coins they were counting, and then you started removing coins from the pile while they were still in the process. Your friend is liable to become confused and end up miscounting.

The reason your new solution works is because the collection you’re iterating over via keys() is a copy of the list of keys in your available_abilities dictionary, made ahead of time before you start changing the dictionary, and that isn’t itself affected by changes to the dictionary.

2 Likes

@pineapple
It is strange that when I first starting using Godot I really thought Dictionaries were a new thing to me. However they are equivalent to php associative arrays or javascript maps (and objects) I suppose.

And yes, of course now I think about it, what on earth did I think I was doing. Your penny counting example is excellent! Made me smile.

But however they work, I just love Dictionaries now. Much more so than when I used to do PHP (never was a C# person although everyone seems to be these days). I think it was because almost all my data came through SQL queries which I know is a sort of dictionary equivalent, but I never used to think of it that way.

GDScript Dictionaries just seem so powerful! I just love them.

Since that infuriating half a day when I had no idea what was going wrong, I just didn’t even realise I was doing the ‘removing pennies while counting them’ thing. Like all code bugs I suppose, when you know the answer the error seems so trivial.

And thank you. I did like to know and I am genuinely pleased you mentioned it. I thought Dictionaries was a gdscript thing, but you are quite right.

But GDScript Dictionaries are, as far as I am aware, the best available in any language I have used at least! And I won’t be making that penny counting mistake again that’s for sure :slight_smile:

2 Likes

I’m glad I could help!

I was going to share how the Dictionary collection works in C#, which I personally prefer over GDScript. I was going to show how C# gives you a clearer and less confusing error when you try to make a change to a collection while iterating over it. … But then I learned, actually, C# just lets you remove items from a Dictionary while iterating over it, and it’s no problem. Works just as you’d hope.

There are other cases where C# doesn’t want you to make a change to a collection while you’re iterating over it, though. In those other cases, you can normally expect a clear error message letting you know immediately that something went wrong. Which is a huge help.

Perhaps C# is worth another look, sometime? This is only one of quite many advantages that I think it has over GDScript.

(Beware: I don’t think this would be true of Godot.Collections.Dictionary in C#, which is Godot’s own Dictionary type - same as you have with GDScript - as opposed to the standard C# Dictionary type provided by .NET, as in this below example.)

using System;
using System.Collections.Generic;
                    
public class Program
{
    public static void Main()
    {
        Dictionary<string, string> dict = new() {
            {"A", "apple"},
            {"B", "berry"},
            {"C", "citrus"},
            {"D", "durian"},
        };
		Console.WriteLine("Before:");
        foreach(var item in dict) {
			Console.WriteLine(item.Key + ": " + item.Value);
		}
		Console.WriteLine("Removing...");
        foreach(var item in dict) {
			dict.Remove(item.Key);
		}
		Console.WriteLine("After:");
        foreach(var item in dict) {
			Console.WriteLine(item.Key + ": " + item.Value);
		}
    }
}

2 Likes

@pineapple
Nice C# fiddle there. And yes it seems to run fine.

Perhaps C# is worth another look, sometime?

It definitely is, and I definitely will. It is just so hard to find the time to invest in a whole new language with it’s own peculiarities etc. It is also, lets face it, a very mature and professional level language that it will take a long time to master it. It did get me thinking though that without it my game dev experience is perhaps going to remain on a very amateur footing. And doing C# as part of a Godot project will help a lot as it is not like I am developing software from scratch after all.

So I have just spent some time thinking about this. On reflection, the time and effort might be greatly rewarded as C# is used in so many other walks of life. So much so that I am going to do it with my very next game. I will plan in additional time for prototyping and learning and see how it goes.

Now the more I dwell on it, it seems more and more exciting! Thank you Mr Pineapple, you have inspired me. Even just looking at your fiddle, I actually quite like the <> notation already. (Also love namespaces).

Thank you. This game I have just two months left to complete. And right now my homing missiles are homing but after an EMP has expired they refuse to home any more :slight_smile:

Now you have me distracted - lol. But I am serious, I am going to jump into C# on my very next project. Thank you. I should have done this before and I am really looking forward to it!

2 Likes