Advertisement

Alternatives to global variables / passing down references through deep "call-trees"?

Started by April 12, 2015 09:07 PM
16 comments, last by swiftcoder 9 years, 8 months ago

You only have a few options to get access to your "global objects"

- Global static - Ugly ugly

- Some resolution system - Not much different to global, easier to switch object for others BUT still just global state.

- Pass the references into objects as they are constructed so they are available as member variables.

- Pass the references into function calls that need them

I believe the last option is actually always the best solution. No hidden crap, the function signature is honest to its requirements and gives you better understanding as to what it does and makes it easier to make the function pure.

There are all sorts of tricks you can use to thread the values down through a call stack, my personal preference being monads (such as reader/state monads) or passing in functions that carry the required state via closures etc (C++ gained closures in C++ 11 I believe when lambdas came to the language)

If you are having to pass this data down too many levels your code structure is probably wrong :)

I just embrace parameter passing and stop fighting it. I like it. It forces me to think carefully about dependencies at every point.

If you have a bunch of objects that are always passed around together, there is nothing wrong with wrapping them up in a container object and passing that around, but if you then call a method that only needs access to one of them, you can just pass that one rather than the container.

class RenderContext
{
    GraphicsDevce graphics;
    TextureCache textures;
};

void render(RenderContext &context)
{
    Texture t = findTexture(context.textures);
}

Texture findTexture(TextureCache &textures)
{
   // ***
}
As to the "deep" issue, I generally find the deeper I go, the less I need access to, which dovetails very nicely with this kind of approach.

Even if you never write a unit test, it is often useful to think "How would I test this method in isolation? How much set up outside the method would be required to test it meaningfully?" If it is a massive amount of global state that requires being set up before the method can be tested, something is wrong.


void test_findTexture()
{
    TextureCache dc;
    Texture t = findTexture(dc);
}
Advertisement

I usually have some soft of "core" object which contains the following:

struct Core
{
  Audio audio; // Handles the sounds
  Shader shader; // Handles the shaders
  Environment environment; // Handles external stuff like paths / filesystem
  Keyboard keyboard; // handles keyboard input etc...
  // Etc..
};

Then when I add a new node to the scene, I pass a pointer to Core to it but then when I add a new node to that node, the add() function sets the core pointer within the new node to that of the parent node. So in effect, every single object in the game has a pointer to Core. So then I can do this...

void PlayerNode::onUpdate()
{
  // ...
  if(health <= 0)
  {
    core->getAudio()->play(deathSound);
    remove();
  }
  // ...
}
http://tinyurl.com/shewonyay - Thanks so much for those who voted on my GF's Competition Cosplay Entry for Cosplayzine. She won! I owe you all beers :)

Mutiny - Open-source C++ Unity re-implementation.
Defile of Eden 2 - FreeBSD and OpenBSD binaries of our latest game.

I usually have some soft of "core" object which contains the following:

struct Core
{
  Audio audio; // Handles the sounds
  Shader shader; // Handles the shaders
  Environment environment; // Handles external stuff like paths / filesystem
  Keyboard keyboard; // handles keyboard input etc...
  // Etc..
};

Then when I add a new node to the scene, I pass a pointer to Core to it but then when I add a new node to that node, the add() function sets the core pointer within the new node to that of the parent node. So in effect, every single object in the game has a pointer to Core. So then I can do this...


void PlayerNode::onUpdate()
{
  // ...
  if(health <= 0)
  {
    core->getAudio()->play(deathSound);
    remove();
  }
  // ...
}


In terms of access control, how is this different from just having globals exactly?

In terms of access control, how is this different from just having globals exactly?

Because the individual class instances that need a Core member hold pointers to it, and the classes that don't need access don't have pointers to it.

I do something similar (passing in a 'collection' struct holding the major resource systems). The only downside is if a class needs only 'Audio' and 'Textures' access, he also gets 'Maps' and 'Enemies' access; so I'm trading some unnecessary (but explicit) access for convenience, without making that access implicit and available to everything.

Logging and debugging facilities are global, though I've considered moving them to a Service Locator-type design so certain areas of logging can be enabled to give more details only when needed (but when disabled gives less verbose reporting of every tiny little thing, so I'm not flooded with info). Haven't done that yet though.

This is the exact OOP conundrum that burned me out. :/ I was trying to convert a tile engine written in C from the old OpenGL fixed function pipeline to GL 3.3 with shaders, while converting it to C++ and ran into this wall. I wanted each tile to be able to have its own shaders, textures and mesh, but realized that most tiles would use the same shader, texture and mesh. I could not figure out how to create a pool of shared resources without having deeply nested call trees and that was a nightmare in and of itself! I needed a pool of textures, one of meshes and one of shaders that were shared by the tile map. I must have gone through every online reference I could find and couldn't figure it out!

I ended up quitting programming (a hobby). I am really hopeful that this thread can shed some light on this!

Advertisement

Thus you can establish by policy a well-known set of interfaces, and with policy you guarantee to the game objects that the interfaces are valid when you call their Update() function. But also through policy you make it known that the pointers can change at any other time, and that the values may be different in between successive calls to Update(). They are valid for a limited time only. You can request a pointer to the current audio subsystem and tell it to play an audio clip, but by policy you are not to modify the pointer to the audio subsystem, nor are you are allowed to store the pointer, it is treated as invalid once the Update() function is complete.

So I should call ServiceLocator::GetRenderer() every update/render, instead of storing it once in the constructor? Isn't that kind of inefficient?
But other than that it sounds interesting, but also like a lot of extra work and complexity.
Implementing NullSound, NullRender etc, and designing the system around being able to run in a "null" state.
I can imagine it becoming kind of complicated when functions return something, like a null version of TextureCache::LoadTexture(), it would have to return a NullTexture, or some kind of default texture... hmm...

Whatever, I think I will probably give ServiceLocator a try. I have to rewrite the whole render code anyway, since I did not think ahead.


@haegarr:
How would I go about checking the state of Player inside of PlayerController?
Provide accessors for every member, like GetPosition() etc, or declare PlayerController a friend class, or something else?
Should I also seperate logic for sound playing etc into something like PlayerSoundController, or put it all into one PlayerController?
Some kind of messaging system sounds like it would make things easier, but I'm not sure if I can come up with a good one.
I coded in different scripting languages for different games (like coding Addons for World of Warcraft) and there it was always soooo easy smile.png
Register for an event, through a global variable and then just pass a function to play a sound, like event.Register(PLAYER_TAKE_DAMAGE, playMySound).
Boom, done!
I really need to learn more about decoupling and abstraction... that's one of my weaknesses, everything in my code is so "direct".


How would I go about checking the state of Player inside of PlayerController?
Provide accessors for every member, like GetPosition() etc, or declare PlayerController a friend class, or something else?

Single Responsibility Principle.

For example, why does Player have a getPosition() method? That sounds more like the job of a Transform or a MoveableObject class.

Register for an event, through a global variable and then just pass a function to play a sound, like event.Register(PLAYER_TAKE_DAMAGE, playMySound).

You can do this equally well in C++, using a signals/slots mechanism. Take a look at boost::signals2, or GameDev's own VVFEL.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

This topic is closed to new replies.

Advertisement