About avoiding the downsides of globals

I want to further discuss some of the points raised in is there another way to access render attribute in sub object but it seemed better to make a new topic than to hijack that thread.

In summary:
What, specifically, are the downsides of globals? Why are they bad?
How do we avoid those pitfalls?

Longer:
I want to make sure I understand what makes globals bad and how to structure code so I dont have to rely on them. Especially when Panda3D might move away from them in the future.

The service locator pattern is a good read. My current project uses an ‘assetBook’, which is similar to the pattern. It’s much like a phonebook, but for asset file names. That way all the filepaths are in one place and easy to change. If I need to change a texture that’s used by several classes, I just change it’s address in the assetBook and tadaa. So at first glance, it looks like I am on the right track.

But if you google ‘why globals are bad’, it’s things like ‘anyone can change it from anywhere’, ‘complicated coupling and dependencies’. That’s still true for my assetBook, it’s quite convenient to use, but I havent really avoided any of the globals pitfalls.

Anything that can access the assetBook can fiddle with it. I need to make changes to how the access to the assetBook is managed. The service locator pattern page explains that. I can make those changes.

The code still needs references to the assetBook in lots of places, so there’s coupling and dependencies. Thaumaturge his code examples show how to better manage those. I am storing and accessing the assetBook class wrong.

So that’s kinda my concern. At a glance, I could feel like I’m cleverly avoiding globals, but when i take a step back and think about it, I havent. I just created the same downsides as globals, but I took a different route to getting to those downsides. My assetBook class isnt a global, but that doesnt mean much if it has all the same downsides.

I can improve my assetBook code, that’s not the issue. The issue is that there is a difference between avoiding globals and avoiding the downsides of globals.

(I do have that book by Nystrom as well, but when I read it my skills werent good enough yet to fully understand it all, I should reread it)

Thinking about it a little, I see two problems with globals that are addressed by the use of a “Common” file (and, I imagine, your “assetBook”).

First problem is the matter of clarity.

Let’s say that we find an interesting new Python module that you want to use, or come years from now to examine some of our old code. And let’s say that somewhere in there we see the use of “globalClock”.

Now, what module provided this thing? Is it a Panda-object? A core Python object? An object from some other module besides? If we don’t already know, the answer isn’t clear.

Conversely, if variables are stored in a “Common” file (and reasonably named), then their provenance is likely to be clearer.

Taking the example above, if that code instead held a reference to “Common.myShowBase.clock”, then it would be relatively easy (I think) to determine that the object in question came from ShowBase, and was accessed via a custom “Common” file.

The second problem is that of naming collisions.

Let’s say that we’re using both Panda and some other module–and that other module also provides a clock that it calls “globalClock”. Now, when we attempt to access “globalClock”, which one are we getting?

Conversely, accessing such objects from their source-objects, made available via a “Common” file, allows us to be specific about which object we want.

Again, taking the example above, if we access “Common.myShowBase.clock”–and presuming that we did, indeed, store a “ShowBase” object in “myShowBase”–then there is likely no significant ambiguity as to which clock we’re attempting to access.

A Common file can also give you more access control. For example, with the use of property()

The following code example allows you to set the gameBase once, and only once and forbids using del on it.

class Common():
    _gameBase = None
   
    def get_gameBase(self):
        return Common._gameBase
    
    def set_gameBase(self, new):
        assert Common._gameBase is None, 'Cannot change gameBase, is already set'
        Common._gameBase = new
        
    def del_gameBase(self):
        raise Exception('Cannot del gameBase, not allowed')
    
    gameBase = property(get_gameBase, set_gameBase, del_gameBase)

The above snippet should work, it being a class variable does make it hard to test. I havent figured out yet how to reset static variables in between tests. Especially with the ‘can change it once, and only once’.

1 Like

That’s fair. I’ll confess that I’m not inclined to go quite so far myself–if I were writing code for a large group of other developers, maybe, but not for my own work and usage.

Architecturally, putting globals in a “common” module isn’t really any different than putting them in the “builtins” module. It does solve the issue that other programmers (and IDEs) now know where the symbol is coming from, but it really doesn’t at all address the “globals are bad” issue. Really, the same applies to patterns like singletons and “asset books”, as long as they are stored on a global scope.

If you want to avoid globals you have to really take a step back and reevaluate the architecture of your application (and in some cases, work around the poor design of the direct package). You have to think about what the objects are really scoped to, on a conceptual level, and design your code around that.

I think you’ve (perhaps inadvertently) hit at the core of the “globals are bad” issue. Having a single ShowBase instance stored globally sounds like it will work just fine until you realise you need to run the application multiple times in, say, a unit test suite. Now you need to introduce functions to clean up the global state and recreate it.

Instead, if your game architecture were designed so that the relevant pieces were passed through without passing through some globally stored structure, with ideally the pieces in question being all limited to a particular responsibility, then you don’t have to think about these kinds of problems. All the necessary pieces are indirectly scoped to your Game instance (or what have you). If you adhere to the design principles, all the different bits of your application can be used and tested in isolation, even componentised and used in different applications.

The fact that ShowBase does so much related to so many different things makes it seem like you have to pass ShowBase to pretty much everything to get at all the useful stuff, from managing audio to scheduling tasks. I don’t think this is actually necessary; ShowBase is a massive, but actually fairly thin wrapper around the lower level Panda APIs, and you can generally work with those lower level APIs instead of accessing everything through ShowBase itself, or better yet, create your own abstractions around them. This is what I meant when I said “work around the poor design of the direct package”.

But in the end, game development is all about compromises, especially when there is a time crunch involved. I must admit that I’ve used base as a dumping ground for random variables in the final days of a game jam more than once (eg. a base.quality to store a quality setting in a game) because I did not have the time to rework the application architecture to fit it into its right place (probably in a GameSettings class that could be extended with an event mechanism later).

For most games, I think a right balance can be struck between use of globals and the “right way” based on the project’s scope. For example, relying on the global clock if you don’t expect to ever run time-manipulated simulations is probably fine, and even if you end up needing that, it’s often still possible to go back and refactor your code as needed. And if you do choose to use globals, you can still have a reasonable middle-ground where they are still separated logically into different modules/singletons by responsibility and it’s also obvious from reading the code that they are globals and where they are coming from.

My problem here tends to be that, in my experience, this quickly becomes overbearing: elements get passed around through just about every function, just in case that function or a descendant of it wants access to those elements.

I feel–for myself, at least–at some point the nuisance and maintenance of passing the same elements into so many functions overrides what issues a “common” file has, while conversely such a file deals with the issues related to globals that do still bother me.

In a sense, I suppose that I might argue that both globals and universal-passing-through are both over-extreme for my liking, with a “common” file being a reasonable compromise between the two.

This is somewhat as you go on to say, albeit that I think that we take different approaches: I don’t mind that ShowBase compiles so much stuff, as it’s handy to have them all in one place. I don’t like that they’re put into the global space, but having them accessible via a more-controlled approach (as in the case of a “common” file) seems fine to me. And I really don’t want to re-engineer things in order to avoid the use of ShowBase!

1 Like

Your experience does not match mine. I find that a minimal amount of passing-around needs to happen, if classes are given a clear scope and responsibility. You can and should create new abstractions to bundle your state; a World class might store all the state relevant to the 3D game world, such as a NodePath root, collision traverser and a manager for 3D sounds, and a player object (which is placed into the world, and therefore has a connection to it) can be passed a world to parent its models to and register its collisions and interaction sounds with. That is much better than passing the collision traverser and the 3D audio manager to the player object separately, which would be rather bad since you would need to modify the interface any time you want to change the details about how many traversers there are or how audio is managed.

I think it’s clear, though, that when people say “globals are bad” they could be talking about one of two things:

  • Having global state (incl. through common module, singleton, etc.) is a design anti-pattern, which can cause architecture problems later on, but is not a disaster
  • Storing variables in built-ins is a significantly worse problem–not only do they suffer from all of the problems of globals, but they also significantly hurt readability of code by humans and by tools.

Apropos, I do intend to replace ShowBase with a better-designed set of classes. If you’ve ever tried to design a multi-window application with it then you’ll know what a bad design choice it was to lump in window-specific and scene-specific state into the global namespace, for example. Of course you’ll still be free to put everything in a global namespace / common modules / builtins if you were so inclined, but the engine shouldn’t encourage anti-patterns by default.

It’s actually not that scary if you group everything together.

from panda3d.core import NodePath
import builtins

def run():
    print('run')

render = NodePath("render")

builtins.panda3d = {'run':run, 'render':render}

# Test

panda3d['run']()

test = NodePath("test")
test.reparent_to(panda3d['render'])

In another case, you can leave only a reference to an instance of the base class. And remove the rest from the embedded objects.

from panda3d.core import NodePath
import builtins

class Base():
    def __init__(self):

        self.render = NodePath("render")

    def run(self):
        print('run')

builtins.panda3d = Base()

# Test

panda3d.run()

test = NodePath("test")
test.reparent_to(panda3d.render)
1 Like

Isn’t that what base currently already is, a builtin container for these globals? I think everything there is to be said about that has already been said in this thread.

As far as I know, in addition to the base, the render node is also added to global variables, as well as a number of panda variables. The question is why.

1 Like

The problem, I find then, is that nearly any class and nearly any method may want access to that world. And where a given class or method doesn’t, some descendant of it may yet.

As a result, the “world” class especially ends up being passed into absolutely everything (or just about), which can be a pain, I find.

Far, far more convenient to just keep a “common” file which can be imported and then accessed within any file–and that likely without the issues of placing things into the global namespace.

However, this may indeed just be a matter of our experiences in the matter differing.

That’s fair. While I’ve never had such requirements myself, and ShowBase works well for me, specifically, I can see that it might be an issue for others and that other designs might work better.

Yes, most of the classes that represent physical objects or physical interactions will need access to such a world object. Classes dealing with abstract concepts will not.

Of course I respect your design decisions as you know your own requirements best and you should do what you are most comfortable with, but I wonder, is it really such an inconvenience to add a single argument to those classes’ constructor (or something like that), especially when weighed against the advantages of having the classes no longer be dependent on global state? If there were more code involved than just a single argument and a single assignment, you could even consider having a WorldObject base class dealing with the connection to the world, with sub-classes overriding a method to handle the placement into the world.

Continuing on this specific example, it may not seem like it will make a practical difference initially, but as soon as one wants to do things like handle world destruction and recreation cleanly, do something that requires having multiple world instances (eg. if the player switches to a dream world, or plays a game-within-a-game), serializing worlds to and from disk for faster loading, simulating worlds without an active renderer (eg. in unit tests, or on a server), populating a separate world in the background during a cutscene that should transition to the next level, or that sort of thing, I believe one will quickly appreciate having avoided the use of globals and god objects.

In my experience, game codebases have a tendency of growing more and more unintelligible and complex over time as design principles are thrown aside for the sake of expedience and the game’s requirements become more complex, to the point where bugs creep in that become harder and harder for programmers to understand, so I do think it pays off to make a solid attempt to stick to them, even if they do not have to be taken to a logical extreme or be seen an an absolute law.

And similarly, in all fairness.

Easily, I find.

The thing is, I’ve done this a few times now, first for A Door to the Mists and recently (and ongoingly) for my current project. And… Quite to the contrary, I appreciate not having to pass my central ShowBase-derived object around to everything, and I appreciate having convenient universal-but-not-technically-“global” access to that ShowBase-derived object.

It may be worth noting here that my ShowBase-derived object is not my “world” object. Rather, it’s a “framework” object that handles overall maintenance and things that persist between “worlds” (such as the player, or certain menus). It then contains “world” objects (which may reference other “world” objects, as the case may be).

This means that I don’t need to clean up that central object before the end of the game, and it can manage the cleanup of “worlds” and the player-object when loading.

And yes, both A Door to the Mists and my current project had “worlds-within-worlds”–indeed, the current project has multiple levels of “world” (what might broadly be described as planet/moon, to space, to “hyperspace”). And indeed is intended to have a “dream” world at points.

In fact, the problem is not so much in global variables as in panda’s approach to creating an application framework.

  1. Panda framework is added by embedded global objects. Not a bad decision.
  2. Some panda framework variables add embedded global objects again. A dubious decision.
  3. It is recommended to use a custom application framework with the inheritance of the framework from panda. Absolutely terrible.

I think that the use of any approach should be determined by the user who writes the application.