Feedback on LazyValue lecture

Hi the following is just my own personal perspective and maybe others following this course haven’t had this issue with this lecture.

I’m personally only following these RPG courses to improve my programming skills so I can work with RPG plugins that I own, if I ever need to modify them outside of the scope of what they can already do.

In this lecture I think the use of the LazyValue was demonstrated in terms of how to implement it but not why the approach was used. I get that its to do with making sure values are initiated but I’d have much preferred that the code was explained thoroughly with why this approach was used or that the lazyvalue wasn’t used at all.

I’m very unlikely to ever use this approach as I’ve never seen it before and I don’t know if this is good or bad practice. Even though I understand the code it appears to be like some form of hack around unitys start and awake functions work and I personally think it should have been explained.

With all of that said, if it doesn’t cause performance issues and fits in unitys guidelines in terms of good code practices I think it’s a very interesting and useful class.

1 Like

LazyValue.cs is an extremely valuable tool for dealing with race conditions. In fact, I believe you were asking earlier about a way to see if a save file was present to avoid using a -1 as a default trigger to initialize CurrentHealth. I showed you how to see if it existed, but cautioned against it’s use. LazyValue is the correct approach. It’s a feature that appears in some other programming languages (Kotlin immediately comes to mind), but isn’t innately supported by C# or Unity, so we had to roll our own version of it. (There actually is a Lazy class in the system libraries, but it’s designed for true multi-threaded applications and is needlessly complex for using in Unity).

The core of the system is actually relatively simple. The class is generic, much like List and Dictionary. This allows us to use just about any type as a Lazy Value.

We have an initialization function. This is a recipe for how to get the correct value if the value hasn’t been established already. Whenever the LazyValue is accessed, we first check to see if it’s been initialized. If it hasn’t, then we initialize it.
If this sounds familiar, it’s because it’s exactly what we did manually in Progression.cs. The BuildLookup in that class checks to see if the Dictionary exists, and if it doesn’t, then it creates the dictionary and populates it. This is a classic form of Lazy Initialization, and we’re effectively using the same pattern here, but making it generic so that it can be used with any class… You could use the same method we used in Progression for each troublesome value, this just condenses it into one easy to use generic class.

It’s important to note that some basic rules still need to be followed. It’s imperative that at no point you access a LazyValue that references an external source in Awake(). For example, in Health.cs:

void Awake()
{
    currentHealth = new LazyValue(GetComponent<BaseStats>().GetStat(Stat.Health));
    Debug.Log($"{currentHealth.value}");
}

Why? Because BaseStats GetStat(Stat.Health) depends on the level of the character. For an enemy, the level is already set in the inspector, but for the player, the level may or may not have been initialized.
Once you’ve passed Awake() and gotten into OnEnable, the level should either be correctly set, or if it’s a LazyValue, should be accessible (because we set the LazyValue to use CalculateLevel() in Awake().

Now all of that being said, most race conditions ARE avoidable without LazyValue, following these simple rules:

  • Awake() → You may cache references to components here. You may not USE those external references in any way. You may, however, set the value to variables that do not rely on external components.
  • OnEnable()/OnDisable() → You may subscribe and unsubscribe to events here.
  • Start()/Update() → You should be able to safely access values in other components.

Note that there are still times when following these simple rules will still require some form of Lazy Evaluation to avoid race conditions. Race conditions are some of the hardest bugs to track down and eliminate, which is why Lazy Evaluation is such an important tool.

7 Likes

Thank you very much for the detailed explanation, I feel far more confident now in continuing to use this approach knowing that it’s a standard approach and not some form of hack that might not be recommended.

I was very impressed with the concept but I tend to not trust something that is radically different in any context without a detailed explanation.

Do you know if there is a technical term or name for this approach so I can research into it further? If not it’s ok as I will use this approach if I ever consistently run into race condition issues I think it’s a very good idea.

I have one question about this, Following Sams code for Health.cs:


private void Awake() {
            healthPoints = new LazyValue<float>(GetInitialHealth);
        }

        private float GetInitialHealth()
        {
            return GetComponent<BaseStats>().GetStat(Stat.Health);
        }

Isn’t there still a possibility of a race condition because even with the:

 public void ForceInit()
        {
            if (!_initialized)
            {
                _value = _initializer();
                _initialized = true;
            }
        }

What if the BaseStats isn’t intiliazed by the time it gets to the ForceInit, would it still return null or something?

As this is relevant to my above question, how does this work if the BaseStats hasn’t been initialized does it make it “jump the queue” in race conditions to be initialized sooner before it would have originally if that makes sense?

ForceInit() should be called in Start(), not Awake()… This gives everybody a chance to get their initializers created.

Essentially, yes.
So imagine in Awake(), everybody has had their initialzers assigned… So BaseStats already has an initializer that calls CalculateLevel().
Now in Start(), Health calls ForceInit(), ForceInit needs the value from BaseStats, so even though BaseStats may not have called ForceInit() yet, the very act of accessing the LazyValue automagically calls the Init if it hasn’t been initialized. Its’ not so much a jumping of the queue, it’s just that when there are cascading race conditions such as these, the very act of calling a LazyValue that hasn’t run it’s initialization causes that initialization to be run.

3 Likes

Thank you I think I fully understand how it works and it’s use cases now.

In my opinion, I still think this was worth being covered in more detail in the course especially since it’s such a useful class and approach that is unique and isn’t covered in many tutorials including C# specific.

Not that I have come across anyway, thanks again for your help.

Amazing lesson. Sorry to hear there were folks who didn’t like it.

I’m still a beginner, but I could notice all the race conditions we were accumulating over the last few lectures and how we were using Awake and Start in a way that could cause problems. I was wondering if we’d ever do anything about it.

I’m so glad we cleaned it up and did it in such a systematic way with reorganizing what’s in Awake vs Start vs set with a lazy value. 100% amazing. My only piece of feedback is I wish we at least introduced some of the more basic Awake/Start hygiene sooner and didn’t wait for things to get so messy. Cleaning the mess did make for a good lecture however.

Anyway - I just wanted to post that lectures like this where we get introduced to design patterns we can apply to our own development is one of the main reasons I signed up for this series. Great work GameDev team and I look forward to more content that looks like this.

I still have one weird race condition between the save system and the portals which I will post in a separate posting.

So here’s the ugly truth about programming… You’re going to be refactoring a lot, even if you plan well. If we would have just started with the cleanest code possible, once you’re out in the wild, if you haven’t developed the skill of refactoring, you’ll find yourself stuck in a hurry. Cleaning the mess is a great lecture. Creating the mess set it up. :slight_smile:

1 Like

Hey no arguments there! I’m actually really glad this is being taught and reinforced for people who are new to this. I can vouch for how valuable this is.

And honestly the more I think about it, I’m getting a lot of value seeing different instructors teach things differently. After going through 3+ different series each taught by a different person, I am finally developing the intuition for how to approach my own work. As of this week, I am comfortable doing some moderate pieces of functionality differently from the instructor.

Which is the ultimate goal. None of our projects are complete games ready to go (well, the closest would probably be the RPG course, but it took FOUR courses to get to something close to a complete game, and we never touched on scoring, procedurally generated equipment, and a host of other things. At a certain point, our goal is to set you free, with enough knowledge about the process to start making these projects your own.

1 Like

Privacy & Terms