Page 1 of 1

When to global, and how global is a global

Posted: Fri Mar 13, 2026 7:42 pm
by celes
I'm always saying the Singleton pattern is the one that will outlive all other patterns. It is not a coincidence that I use lots of globals in my code :blobcatthink:

But you have to distinguish, because there's various levels of what one means by "global", but first let's discuss the obvious:

We like globals because they're always at reach

The nice thing about a global is that you can access it from any and everywhere with little effort. Programmers who say they never use globals are often lying, they just forgot about their beloved print. But we all tend to forget about printing unless we're pure of heart so I'm not gonna blame anyone.

Anyway, the print function is always there: It's effortless, we don't need to pass a "printer" around, we don't need to do "dependency injection" so that we get a printer to print our strings, print is just there. It is ineffable.

In gamedev, there's a bunch of things I often want to be as convenient as printing. I'm gonna list a few:
  • Get a hold of the "scene", "map", "world" or whatever it may be. This could be an ECS if you're one of those. You also want to query that scene. I don't use an ECS but I still want to get the "EnemySpawner" from the scene, or iterate all instances of Player. There's various ways to do this. My point? I need the ability to do this everywhere because it keeps coming up everywhere.
  • Play a fire-and-forget sound effect. Seriously, playing the sound effect goes next to the line of code that performs the action that produces the sound effect. You can't convince me otherwise. If you have opinions about this, don't show me your "clean code", show me your sound design.
  • Firing off tweens. Similarly, I want the ability to fire off a tween pretty much anywhere. I'll write about how I do tweens some other time but this is the kind of thing I want to do at a "print" level of convenience. I want my game to be juicy so animations are everywhere. I cannot work with the mindset that there's some "animator" that looks at the pure state of the game world and makes decisions based on that. But that might just be me.
  • Drawing debug and non-debug geometry, but especially debug geometry. If your "framework" doesn't let me call a bunch of draw_line and draw_rect to debug one of those tricky computational geometry snippets I am awful at, you've already lost me. No I don't want to spawn a "DebugRenderer" in the scene so I can get a hold of it. That's as dumb as passing a "printer" around to just print.
What color is your glob.. nah I'm just kidding. Unless...?

But there's flavors of globals. This was a lesson that took me a game and a half to figure out, so it's worth putting out there, because I've seen people make this mistake in actual engines and frameworks where they put singletons into static variables.

And you know, this sort of thing truly depends on the use case. I've found I want roughly two kinds of globals in my game:
  • Scene Globals: These live in the scene. Accessing them means getting them from the scene tree. If you change scenes, you get a different set of globals. So, every access goes through some logic to get the currently active scene, then pick the thing from the currently active scene. You can replace scene with ECS if that helps you sleep at night, but my point is these have to be accessible from anywhere: No passing around, no funny dependency injection shenanigans.
  • Game Globals: These are true globals, just one for the duration of the program. They use static variables, or whatever it is that your language calls them.
Functionally, they look exactly the same, but you gotta be careful which one you pick. For example, today I fixed a bug in my game where the Tween manager was a game global, when it should've been a scene global. This was causing crashes on stream the other day when I restarted the scene after doing some tasks (like breaking a barrel and before the barrel had finished its breaking animation). By keeping the tween manager as a scene local, all tweens are frozen in time when I switch to another scene. If I destroy the scene, all tweens will immediately stop playing too.

But not everything should be a scene global. Some things are truly global. In my game I have a few: The editor (there's just one editor and that's enough of a headache! I don't need two), the renderer, the input system, the audio system, the scene system (not scenes themselves but the thing that has the list of scenes and knows the active one)... I think audio is the best example of a system that is *truly* global. Of course you can come up with counterexamples to all these. My point is: In my game, these are global, because I'll never need multiple instances of them.

Despite all I'm saying, I think the lesson here, though, is that often when we want a "global" something what we need is something closer to scene local. :nkoThink: At least that's been the lesson for me. Things like a tween manager, cpu particles, "arenas" to hold various forms of gameplay objects... These are all scene-global! Once the foundation for a game is set (the "engine", if you may), pretty much everything you'll need are scene globals.

Oh and there's another use case where I'll go for statics: Storing the delta time. I don't like passing it around because it's pointless busywork, but I want access to it to be as quick as possible, so I always store the delta time and a bunch other time related variables as static. That way I can conjure them from thin air anywhere in my code, e.g. Time.deltaTime, and I know the access is as quick as it gets: Just a memory read (often cached), no hashmap lookups or any weird nonsense. I still dread the days in which I had to choose between passing something around for performance or locking an RwLock for convenience...

Re: When to global, and how global is a global

Posted: Fri Mar 13, 2026 8:22 pm
by Sugui
But we all tend to forget about printing unless we're pure of heart so I'm not gonna blame anyone.
Do you think we functional weirdos here don't use an

Code: Select all

unsafePerformIO $ print "help!!"
to debug? :blobcatgiggle:

Nice article ^^ globals simplify so much the gamedev process if you know how to use them well.

I wonder if you have a variable that stores what is the current scene. Would you consider that variable global too? Or local? Because it refers to the scene itself.

:blobcatrss: Wait, what's that sound? I'm hearing from the distance to the clean coders™ shouting that Singleton is an antipattern and that you should drill every value through dependency injection /j

Re: When to global, and how global is a global

Posted: Sat Mar 14, 2026 12:40 am
by palas
Classifying globals based on their locality makes a lot of sense! Another way to distinguish them is by their usage.

1. Read-only
Basically constants, though their value doesn't have to stay immutable forever. From your post, delta time would fit this description.

2. Write-only
The opposite, but just as harmless. I guess we can consider the audio system to be write-only? Or drawing to the screen, as it can't really affect other parts of the codebase.

3. Read-write
Most spicy ones, which are read and written to all over the place. If globals ever do any harm, it's these ones, so it's important to consider which parts of the code really need such power.

Types 1 and 2 are in the safe category, so spamming them won't hurt too much. When it comes to 3, they directly affect how well can one reason about the codebase locally.
This is still mostly unsolved for me. It's not about whether or not to use them (can't really gamedev much without at least some global scene/level manager), but how to design in a way to keep as much locality as possible. For example, if I'm storing references to game objects, they can still be deleted from anywhere else. What happens if they do get deleted? Idk :akko_shrug:

Re: When to global, and how global is a global

Posted: Sat Mar 14, 2026 9:38 am
by celes
Sugui wrote: Fri Mar 13, 2026 8:22 pm I wonder if you have a variable that stores what is the current scene. Would you consider that variable global too? Or local? Because it refers to the scene itself.
I have that variable! And it's a game-global one: Even if it refers to the scene, the list of scenes and the handle to of the currently active scene must live somewhere in memory, and they can't live inside a scene :blobcatnodfast:

This is similar to how godot does it. And, half-relatedly, I suspect godot's choice of singletons, similar to my own, doesn't let them render / process input for two different scene trees at the same time, but maybe I'm wrong! Either way you're unlikely to need that for a game unless you have a very clear use case in mind.
palas wrote: Sat Mar 14, 2026 12:40 am Types 1 and 2 are in the safe category, so spamming them won't hurt too much. When it comes to 3, they directly affect how well can one reason about the codebase locally.
This is still mostly unsolved for me. It's not about whether or not to use them (can't really gamedev much without at least some global scene/level manager), but how to design in a way to keep as much locality as possible. For example, if I'm storing references to game objects, they can still be deleted from anywhere else. What happens if they do get deleted? Idk
I really like the idea behind "write-only" globals and you've made me realize a lot of the globals I use are like that. Print is definitely a write-only global, at least in spirit (I don't care if you can access the internal buffer, nobody will).

Similarly, one-off particles or one-off sfx are write only globals in the same way, you fire the thing and you forget about it!

But not all my globals are write-only, not by far :blobcatthink: There's at least a dozen systems that access the player to check various things, such as enemies or projectiles or coin drops... they all need to know where the player is. But I've realized most of the time, even if I get a read-write reference to the player I'm conceptually treating it as read-only. I'd never modify the player from enemy code because that feels intuitively wrong! Maybe it's years of borrow checker that hammered these ideas into my brain.

As for references to game objects that can be deleted, I think that is *the* question. The one that haunts every gamedev. The best way to achieve this imo is to work with two kinds of references: (1) short-lived references that you don't keep across frames and (2) weak references that you need to check before you dereference

You need to enforce that accessing game objects that may have died in a previous frame forces you to check that the object is still alive. This can take several forms. Most of my global accessors return a nullable type, so my code is filled with:

Code: Select all

if (SceneGraph.GetFirstEntityInGroup("Player") is not {} player) return;
This often is enough to protect from misuse. Though there's been cases in which I can shoot myself in the foot:

Code: Select all

if (SceneGraph.GetFirstEntityInGroup("Player") is not {} player) return;
TweenManager.Spawn(..., tick: (t) => {
    player.color = somethingSomething(t);
});
This is the #1 cause of "accessed a dead entity" kind of crashes I get in practice, because that player reference was meant to be short lived and I captured it inside a tween. I never really despawn the player except during level transitions or restarts, so these crashes are rare: Rarity makes them more dangerous, not less.

I could come up with various APIs that make this kind of error more difficult and I have some ideas. But this is the kind of thing I try to not obsess over from the get-go: I need to see the design space for what it is before I go about "designing" a solution! In this particular case, I found not stopping all running tweens before a scene transition *was* the actual issue, and fixing it that drastically reduced the amount of these errors.

For entities like enemies that regularly despawn, these issues (when they happen, which is not that often) lead to more frequent crashes so I catch them during regular development. Often the solution is just throwing an if statement checking if the entity is valid, which is only ensured by my own discipline. Something I'm sure makes the average crab very nervous :blobcatgiggle:

Oh and half-unrelatedly: The "is" operator in C# is such a powerful pattern, because it is a generalization of all those Rust specific syntaxes (if-let, let-else...).

Re: When to global, and how global is a global

Posted: Sat Mar 14, 2026 11:00 am
by palas
Oh and half-unrelatedly: The "is" operator in C# is such a powerful pattern, because it is a generalization of all those Rust specific syntaxes (if-let, let-else...).
It is indeed, though I'm really not a fan of the scope of locals introduced inside ifs being extended outside of the if { }. This combined with the absence of shadowing is so annoying! But at the time it's the only way to achieve let-else. And tbh, I'ld prefer let-else any day instead of what you have to do in C#.

As for the API to handle potentially missing entities... The first thing that comes to mind is Handle<T> with the following api:

Code: Select all

fn Entity::handle(self) -> Handle<Self>;
fn Handle<T>::get(self) -> T?;

Re: When to global, and how global is a global

Posted: Sat Mar 14, 2026 4:42 pm
by EmiSocks
Coincidentally, I've been thinking about this a bit lately!

The uses I encounter are pretty different, since I'm primarily a web dev, not a game dev. But the idea I've arrived at is that I find the current way that we do variable scopes pretty annoying. Like, the languages I know, variables are scoped by where they physically (lexically?) are, like local to a function or a script or a try/catch block or whatever. Attaching a variable to a class as a member makes a BIT more sense to vee, logically, but it's still kind of limited!

Instead the way I THINK about variable scopes is more... in terms of their lifetime, I guess! Like, I think of a value as being in the scope of "the whole application", or in the scope of "a request". In practice you have to simulate that scope with like, a dependency injection framework, or an object that you pass to every request callback one by one, etc. For a game I guess the analogous thing would be thinking of a variable as scoped to a frame, or a scene, or something. But that is different from "variable scope" at the language level!

I wonder if it would make any sense to have a language where you can declare scopes explicitly, and then declare at compile time what scopes a function is meant to access. I'm thinking of something like how if you use a State monad in Haskell, you have to declare that in the return type of the function, but... ideally more composable and less annoying than that.

Re: When to global, and how global is a global

Posted: Sat Mar 14, 2026 4:57 pm
by celes
EmiSocks wrote: Sat Mar 14, 2026 4:42 pm Instead the way I THINK about variable scopes is more... in terms of their lifetime, I guess!
Ohh!! This reminds me a lot of dynamic scoping :hammyeyes: I've only ever encountered dynamic scoping in clojure, but I think it's a popular concept in other lisps and maybe some older languages. With those, you can do something like this (excuse the parentheses, this is probably wrong but should get the point across):

Code: Select all

(def ^:dynamic *user-session* nil)

(def auth-middleware [request, next-fn]
  (if-let [session (validate-user-session (-> request :cookies :user-session-cookie))]
     (binding [*user-session* session]
       (next-fn))
     #_else 
     (throw "somethingsomething unauthenticated")))
Here, we define a dynamic var *user-session*[1] and it is set in a middleware so that code reading the variable inside a request handlers will get the user session for that request, but code outside an authenticated handler reading the variable will see null. This is done by setting a value for the variable inside a "binding" block: Anything that happens within that block (not lexically in that function but, dynamically at runtime) will see the bound value, but code that is outside the block, even code that may be running at the same time (such as other threads spawned by other requests) will not see this binding and may instead see other bindings or none at all.

The system is very powerful and you can even have an arbitrary level of nested bindings, which to some people is too much power, but to me is the right amount of power!

Another great use case for dynamic variables is testing. It's customary in clojure to put things like your database connection in a dynamic variable. That way you can bind it to the actual connection in real requests handlers and bind it to something else in tests. So in some sense, it's kind of dependency-injection-like, but requiring far less magic.

And now I wonder why I don't use these more often in game development... :blobcatthink:

[1] For the curious, the *s surrounding the variable name are actually a lisp/clojure naming convention for this and mean nothing, it's just part of the name. In lisp, you can use most characters inside a symbol, including the common math operators like +, -, * and more often than not also /, tho / is special in some lisps.

Re: When to global, and how global is a global

Posted: Sat Mar 14, 2026 7:25 pm
by celes
palas wrote: Sat Mar 14, 2026 11:00 am It is indeed, though I'm really not a fan of the scope of locals introduced inside ifs being extended outside of the if { }. This combined with the absence of shadowing is so annoying! But at the time it's the only way to achieve let-else. And tbh, I'ld prefer let-else any day instead of what you have to do in C#.
Hmmmm you know what, you're right. Having if-let and let-else as separate constructs lets you differentiate when the bound object will be scoped inside the if statement only and when it will live outside. I had not considered this but the pain point you bring up here is very real.

Then there's the fact you can combine multiple if statements using "is" with boolean operators like && to access a chain of possibly null things, kinda like if-let-chains in Rust, which again makes me think the design of is is very elegant and versatile...
Palas wrote: As for the API to handle potentially missing entities... The first thing that comes to mind is Handle<T> with the following api:

Code: Select all

fn Entity::handle(self) -> Handle<Self>;
fn Handle<T>::get(self) -> T?;
Oh yup! I have things like this. Not for entities (maybe I should?), but for components I have a ComponentRef thing that's basically this. Maybe I should add a similar thing for entities, though nothing would enforce using an entity handle over just passing an entity around, so I'm not fully convinced there :nkoThink:

Re: When to global, and how global is a global

Posted: Sat Mar 14, 2026 8:37 pm
by palas
Then there's the fact you can combine multiple if statements using "is" with boolean operators like && to access a chain of possibly null things, kinda like if-let-chains in Rust, which again makes me think the design of is is very elegant and versatile...
On, you mean something like this?

Code: Select all

if value.inner is Inner inner 
  && inner.inner is Inner inner
  && inner.inner is Inner inner {...}
 
This can also be very elegantly expressed in combination with potential-null-access operator.

Code: Select all

if value?.inner?.inner is Inner inner {...}

Re: When to global, and how global is a global

Posted: Sat Mar 14, 2026 10:31 pm
by celes
palas wrote: Sat Mar 14, 2026 8:37 pm This can also be very elegantly expressed in combination with potential-null-access operator.

Code: Select all

if value?.inner?.inner is Inner inner {...}
Yup! I meant this, tho null-access is only one ingredient, I like "is" doubling up as pattern matching, especially for a hypothetical language with discriminated unions, you could do something along the lines of:

Code: Select all

if value is Str(s) && s.trim().to_lower() == "potato" { ... }
So while null-access is nice syntactic sugar for the most common case, having the full thing is still quite useful imo!