Jump to content
  • Advertisement
Sign in to follow this  
Horscht

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

This topic is 1165 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

When I started programming one of the biggest mysteries to me was how to provide access to things like SoundPlayer, Renderer, ErrorLog, StateMachine etc to very deeply "nested" objects, like the player.

Let's say I create my StateMachine in main(), inside the constructor of StateMachine I initialize it's member: GameState, which in turn initializes it's member Level which in turn initializes it's member Player.

Player wants to play a sound when he jumps, so he would need to call soundPlayer.playSound(SOUND_JUMP). But Player doesn't have a reference to a SoundPlayer object. He also wants to draw himself at the center of the screen, so he needs a reference to a Renderer and the Window, to get it's dimensions. What do?

After a lot of inconclusive articles (or failure to understand them), I decided to just use a static global class, since I wanted to generate results as fast as possible and I kinda got stuck with it.

Actually, I used a mixture of dependency injection through constructors, dependecy injection through methods and global variables...

Here is an example (not actual code):

class Player
{
    private:
        SoundPlayer& _soundPlayer;
        Sprite _sprite;
    public:
    Player(SoundPlayer& soundPlayer) :_soundPlayer(soundPlayer) {} // Dependency injection, storing the reference inside the object
    void Render()
    {
        Renderer* renderer = Renderer::GetInstance(); // Global static horrible singleton!

        renderer->Render(_sprite);
    }
    void Shoot(ParticleSpawner& particleSpawner) // Dependency injection through function argument, not stored
    {
        particleSpawner.Spawn(PARTICLE_BULLET);
    }
};

What I really don't like, are the static GetInstance() function calls everywhere. It just isn't clear that this class depends on the other, it just hides somewhere inside of a random method. At least in the constructor it's clear, you can't even create an instance if you don't pass it's dependency in.

Inside the methods parameters it's already a little more awkward, because you're happy you could create an instance of the object, but then it suddenly needs an instance of another class.

 

The worst part is when I have a deep "component hierarchy" or whatever it's called. I read the term "call-tree" before and think it fits quite well.

Player needs a reference to an instance of SoundPlayer, otherwise it can't be created, so it's owner ALSO needs that reference to pass it in, but it itself DOESN'T need it, except for creating the player, so it also puts it into its constructor as a required parameter, so now THIS class ALSO needs a reference to SoundPlayer, even though it has no intention of ever using it, but one of its components needs it, to in turn construct one of its components, which in turn needs it to construct one of its components, which in turn... feels kind of wrong to me :|

 

I got so tired of typing it all out that My state machine just has a #define to pass it all in.

Kind of like this:

class StateMachine
{
    private:
        Renderer& _renderer;
        SoundPlayer& _soundPlayer;
        ErrorLog& _errorLog;
        FileSystem& _fileSystem;
        NetworkSystem& _networkSystem;
        InputDevice& _inputDevice;
        BreadMakingSystem& _breadMakingSystem;
        SystemsManager& _systemsManager;
        // ...
        State* _gameState;
    public:
        StateMachine(Renderer& renderer, SoundPlayer& soundPlayer, ErrorLog& errorLog, FileSystem& fileSystem, NetworkSystem& networkSystem, InputDevice& inputDevice, BreadMakingSystem& breadMakingSystem, SystemsManager& systemsManager):
            _renderer(renderer),
            _soundPlayer(soundPlayer),
            _errorLog(errorLog),
            _fileSystem(fileSystem),
            _networkSystem(networkSystem),
            _inputDevice(inputDevice),
            _breadMakingSystem(breadMakingSystem),
            _systemsManager(systemsManager)
        {
        }
        enum StateType
        {
            STATE_GAME,
            STATE_INTRO,
            STATE_TITLE,
            STATE_OPTIONS
        };
        void SwitchState(StateType state)
        {
#define STUFF_TO_PASS renderer, soundPlayer, errorLog, fileSystem, networkSystem, inputDevice, breadMakingSystem, systemsManager
            switch (state)
            {
                case STATE_GAME:
                    _state = new GameState(STUFF_TO_PASS);
                    break;
                case STATE_INTRO:
                    _state = new IntroState(STUFF_TO_PASS);
                    break;
                case STATE_TITLE:
                    _state = new TitleState(STUFF_TO_PASS);
                    break;
                case STATE_OPTIONS:
                    _state = new OptionsState(STUFF_TO_PASS);
                    break;
            }
        }
};

And inside of the GameStates constructor, again a #define to keep passing it on to it's children.

 

I was asking myself what the best way to do all of this would be, while refactoring Renderer::GetInstance() out and instead rewriting 100 constructors to accept a reference to Renderer instead. I just thought "What the hell am I doing? Is this really better?!"

 

Before I go and rewrite everything I want to make sure I do it correctly this time.

This is something that really irritates me about programming, there never seems to be a definitive correct way and I'm only satisfied with perfection, even though I'm nowhere good enough to even achieve anything close to it :P

Should I just leave it like this or is there a better way?

Share this post


Link to post
Share on other sites
Advertisement
I would say it depend on your goals. If you want to get a game done, leave it like this. If you want to learn new techniques you might want to look in to messaging systems (someone else probably going to give more detailed advice on that). Or in some way do it the other way around, it's not the player that wants to play a sound. It's the 'soundmanager' that wants to play a sound when someones jumping.
 
That said, since i stopped worrying about things that didn't 'feel right' i actually started finishing projects :) I use globals for almost all subsystems as long as they dont carry state (and even then sometimes). I've had no code-reuse problems, i reuse my globals class and do a bit of copy pasteing :) If i actually find something to be a problem, i try to fix it then, or if the game's almost complete i leave it be and fix it in my next project instead.
 
 

Share this post


Link to post
Share on other sites
As MartinMM said, if you want to make a game, make a game. If it's not broken, don't fix it. You'll waste a lot of valuable time trying to perfect these things (I know this personally.)

That being said, your thought process seems backwards to me. A player wouldn't tell the audio system to play a sound, the engine should do that when it tells the player to jump.

Digressions aside, store your subsystems in a variable and pass that instead of passing each one separately.

Share this post


Link to post
Share on other sites

That being said, your thought process seems backwards to me. A player wouldn't tell the audio system to play a sound, the engine should do that when it tells the player to jump.

I agree and I have a hard time thinking outside the box.

My current approach is to have all the logic related to the player inside one class: Player.

In Player::Update() I check if the jump button has been pressed, using a reference to InputDevice that got passed into the constructor and saved as a member, then play the sound the same way, but instead with a SoundPlayer. If I had more things for whoknowswhat I would also put it there smile.png

I don't really know how to really do it from "outside" since I would have to expose a million different things like position, velocity, state etc (which would be the hardest since the Player class itself uses kind of a state machine pattern instead of a variable).

 

There is an in-between zone between the extremes.

It goes by various names, including "service finder" and "well known objects".

Reminds me of this one:

http://gameprogrammingpatterns.com/service-locator.html

But I don't really see how it is much different from a static singleton. It just goes through one more class to actually fetch the service. The only upside I see is, it can dynamically return different derived classes. (Like the NullSound example).

 

Now if enough sub-systems are available, the problem of disorganization appears again. This can be lowered by using a layered architecture. Sub-systems collaborate with sub-systems in the same layer, they utilize sub-systems of the next lower layer, but they MUST NOT call sub-systems of upper layers (although they MAY respond to them), and they SHOULD NOT utilize sub-systems from the layer after next. Of course, in reality this is more a rule thumb than a strict law.

Let's make sure we are talking about the same things here, could you explain what exactly you mean by layers and levels, up and down?

The way I see it is, a layer is a scope, like the function body of main() or the scope of a class like GameState. Up is main() and down is something like statemanager.gamestate.level.player, right?

So if the Player wants to be drawn, it should not interact with the Renderer but instead the Renderer should "collect" all Renderables "below" it, or the class that owns Renderer should feed the renderer the Sprites/Renderables?

So something like:

void GameState::Render()
{
    _renderer.Render(_level.GetPlayer().GetSprite());
    _renderer.Render(_level.GetEnemy().GetSprite());
    _renderer.Render(_level.GetPickup().GetSprite());
}

Of course that would need to be made more general, maybe through an Interface with a recursive function like GetAllRenderables() or whatever :)

I don't know it's just an idea.

The way I do it is kind of the opposite, I call stateManager.Render() which calls gameState.Render() which calls level.Render() which calls player.Render() which then gets a pointer to the DirectX device directly and does all its drawing directly in low level code hahaha :) But once I wanted different graphic-layers, for UI and debug output, it became clear that this is not a very good way to do it.

Share this post


Link to post
Share on other sites
I think you might be violating the principle of a class having 1 sole purpose.
Some food for thought, how about:

Class player
Class input
Class scene/ level or world
Class audio

Class game, with update/ render/ init or something

Now, game::update
{
if(player does something and scene something is state abc) audio.playfx(id)
Etc.

That way your game class (what you might call main), doesn't have to go to deeper trees.
Depending on the size of the game you might get a big game class, but there you can create helper/ subclasses probably.

Share this post


Link to post
Share on other sites

But I don't really see how it is much different from a static singleton. It just goes through one more class to actually fetch the service.

It makes all the difference in the world.

 

A static singleton is initialized at a time that is difficult to control. It may be initialized on first use, which may even happen before main() is called.  A static singleton is destroyed when static objects are destroyed, after main() has already completed. A singleton class has a strict rule that there can only be one of them, ever, and it is not permitted to ever create another instance.

 

 

Under the service locator pattern there is a global pointer, and you ensure through policy that the pointer is valid between certain times.  It is initialized at the time you specify, and it can be cleaned up at the time you specify.  While that well-known pointer only has a single instance, and that single instance has pointers to other objects, nothing prevents you from having other instances of the objects, you can have any number of them created for any other purpose.

 

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.

 

 

Thus, a system of ::simulator->audio->play() is very different from ::audio.play(), because lifetimes are controlled, access is controlled, and the interfaces can be replaced as needed with different objects, including mock objects and test objects, and they can be swapped out if needed during execution.  

 

It still suffers from the problem of a hidden dependency (you didn't explicitly pass the bundle it belongs to) but since the whole purpose was to introduce a hidden dependency so you don't need to pass the parameters everywhere, that's a known concern that can be accepted and understood.  You could resolve that by passing the bundle pointer through the objects, but that's a trade off.  Adding a parameter everywhere has a tiny cost of passing the parameter but it gets multiplied thousands or even millions of times per second. Thus you take a penalty in the form of needing to know and understand the policy of the hidden dependency, done in order to save the cost of thousands or millions of parameters per second.  

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!