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.
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.
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.
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...