How should important data be passed around a program?

Started by
10 comments, last by Khatharr 6 years, 11 months ago

In my particular case I have a "________App" class which is basically a wrapper for all the logic in the program. This is the class which calls the run(), update(), draw(), etc, functions. I'm also using a StateMachine class which is used to handle the different "states" of the program (e.g. the "main" state or the "pause" state). The update() and draw() methods are really just wrapper functions for the state machine's update() and draw(). It look a little bit like this...


class MyApp
{
private:
    //...
    Graphics gfx;
    StateMachine<MyApp> state_machine;
    //...
public:
    //...
    void update() {state_machine.draw();}
    void draw() {state_machine.draw();}
    //...
}

However, in order for my state_machine to properly update() and draw(), it needs access to some of MyApp's private members, such as gfx. What I'm really looking for here is a way that the StateMachine can modify the members of the MyApp class that owns it. Making the StateMachine a friend class is tempting, although I believe that it's a poor design decision. Can someone give me some advice or point me in a direction where I could read up on this?

Here's a look at my actual code:


//Sol::Application is the class I made which implements run() as well as other lower level operations.
class LogicGatesApp : public Sol::Application
{
public:
    using WireGraph = SparseGraph<GraphNode, Wire>;

private:
    StateMachine<LogicGatesApp> m_state_machine;
    //This is the graphical component of the grid that the user will
    //be interacting with
    Grid m_grid;

    //this is used to represent the wire circuitry logic of the program.
    //each cell in the grid corresponds to a node in the wire graph.
    //see the file description at the top for a visual.
    WireGraph m_wire_graph;

public:
    LogicGatesApp();

    void update();
    void draw();

    //...
};

In this case, the StateMachine needs to be able to access and modify Grid and WireGraph. Particularly, in one of the states, it needs to highlight the block in the grid that the mouse is hovering over (hence the need to modify the Graphics object, and access to the Mouse object both found in Sol::Application)

Advertisement

If gfx is the only instance ever, I'd just make a global pointer called GFX or GRAPHICS. When the app is created, the GFX point is set to &this->gfx. Upon destruction, GFX is set to 0.

However, in order for my state_machine to properly update() and draw(), it needs access to some of MyApp's private members, such as gfx. Making the StateMachine a friend class is tempting, although I believe that it's a poor design decision. Can someone give me some advice or point me in a direction where I could read up on this?

Friendship isn't a dirty word in general, but it can be used as a way to sledgehammer your way through a design problem where a more delicate tool would be better. This is probably one of those cases.

If you state machine needs the Graphics class, have the state machine take a reference to a Graphics object in its constructor (or in the specific functions where it would need it, if applicable):


StateMachine::StateMachine(Graphics& graphics) : m_graphics(graphics) {
}

In general, pass your dependencies to things that need them at the most-discrete level possible (that is, pass the Graphics if that's all you need, rather than passing the Game just to get at the Graphics).

That said, I'd suggest that the very idea of your state machine object needing the graphics (to "draw" things) suggests that the state machine itself is perhaps poorly designed. It's just just managing logical state in some fashion, it's also providing the path to draw a representation of that state. It is also possible to approach this problem by keeping only state update in the state machine, and having something external (like the application itself, perhaps) push whatever objects were updated by the state machine or the current state of everything to the renderer. (I realize this sounds vague, but it's hard to go into more detail without knowing more about your code).

If gfx is the only instance ever, I'd just make a global pointer called GFX or GRAPHICS. When the app is created, the GFX point is set to &this->gfx. Upon destruction, GFX is set to 0.

At that point we may as well make Graphics a singleton class don't you think--since I was the creator of it.

@Josh Petrie I would agree with what you said about passing around Graphics; I've done that often before. However, my StateMachine needs access to MyApps private members in order to perform its logical operations as well. My code is not that complicated nor are my requirements for the program, so if you would like to see it I can supply it.

However, my StateMachine needs access to MyApps private members in order to perform its logical operations as well.
Maybe the state machine should own that data instead?

If gfx is the only instance ever, I'd just make a global pointer called GFX or GRAPHICS. When the app is created, the GFX point is set to &this->gfx. Upon destruction, GFX is set to 0.

At that point we may as well make Graphics a singleton class don't you think--since I was the creator of it.

Sure if you want to. Personally I don't find value in singletons though, but to each their own.

I usually collect the things that states need shared access to into a struct and have the "app" class hold an instance of that struct. When a state is created it gets passed a pointer to the struct, which it stores and thereby has access to the stuff it may need.

This is a moderately hamfisted approach in some ways (it allows all states to access all the shared stuff instead of only what they actually need), but the alternatives are equally manky and I've sort of gravitated more and more to this for the sake of its simplicity. In practice I've never had any trouble with it.

The shared object package tends to easy to work with as long as I keep it conceptually organized (which is not hard to do). At the root level it contains the modular parts like the "graphics" object, then it also includes a utility struct for holding things like a randomizer instance, the game timer, the message dispatcher, etc. Finally it can include a struct containing the persistent game state. For example, in an RPG you may have a map scene where you're running around exploring, and a menu scene where you sort out your inventory and check your stats. The persistent state data can hold the stats and inventory as well as your current location on the map. Both states (and probably others) want access to that information for different reasons.

The end result is something like:


shared->gameData.inventory["bacon"] += 4;

or


shared->util.timer.getDT();

I know a lot of people aren't fans of this approach (Demeter slaps my face once a day.), but honestly after having experimented with a lot of ways of working this issue out this is the one that has presented the least problems to me. Compile times can be an issue if gameData gets complicated (there are ways to deal with this), and sometimes I wonder about passing 'system', 'util', and 'gameData' instead of just 'shared', but that's about the extent of my difficulty with this pattern for now.

At that point we may as well make Graphics a singleton class don't you think--since I was the creator of it.

That's a terrible habit to be in, and I'd advise you to steer clear of it.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

@Khatharr

At that point we may as well make Graphics a singleton class don't you think--since I was the creator of it.

That's a terrible habit to be in, and I'd advise you to steer clear of it.

Good thing it's not a habit then. The only thing that I have used singletons for thus far are the various program State classes (the "main" state or "pause" state; going off my previous example).

The only thing that I have used singletons for thus far are the various program State classes

I don't think that's really worthwhile either, though I suppose the idea of states that are never discarded could be made to work, I don't typically keep them around when they're not a part of the active stack. Either way there's no reason to impose the single instance rule on them.

I came back to clarify something about the use of the gameData object in my approach. I only use it to hold "things that belong in a save file" and make everything else the responsibility of the individual states. This creates a problem for transient state (such as AI state) in some games, but that's neatly solved by using a state stack. When I load a game it involves deserializing the save file into gameData and then switching from the load state to one of the gameplay states, and the instantiation of said state generates all the transient data based on the contents of gameData.

The idea of the state stack - if you're not familiar - is that the state base class contains a pointer to a state, which is nullptr by default. If the pointer is null then nothing special happens. Otherwise the state logic and drawing delegate to the sub-state indicated there. This means that the map scene of aforementioned RPG can create the menu state as a sub-state rather than switching the primary state machine to it. For each logical frame you can update the parts of the state that you want, skip the parts you don't want, and then delegate to the sub-state's logic. This allows you to painlessly retain the transient data of the map state, including AI state, physics state, etc.

Likewise for drawing, you can draw the primary state normally and then let the sub-state draw "on top" of it, easily allowing for effects like transparent menu backgrounds.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

This topic is closed to new replies.

Advertisement