2
\$\begingroup\$

I am currently creating a game in C++ using SDL. Many game objects and sub-systems require the use of variables such as an SDL_Renderer for drawing to the screen and delta time for updating positions and there may be more added in the future (such as a TextureManager object). In this case the code for a game object might look something like this:

void Update(float deltaTime)
{
    // ...
}

void Render(SDL_Renderer* renderer)
{
    // ...
}

However, if another variable was added as a parameter, any subclasses would have to be updated and other systems made consistent with the new parameter.

Another way of passing data around would be to have one "storage class/structure" to store these variables and pass that instead. For example:

struct Data
{
    SDL_Renderer* renderer;
    float deltaTime;
    TextureManager textureManager;
};

My question is: what a good way of passing around data in a game engine? The first example would be bothersome to update if any changes were made to the parameters and would make adding more classes difficult. What about the second example? Would it be good design to have objects depend on a single class? Are there any other design patterns for passing around data without causing dependency issues?

\$\endgroup\$
1
\$\begingroup\$

Having built out my first game engine for the first time (in 20 years), and completely ignored alot of good literature (I should of read more but oh well :)). I can offer a few quick insights into my issues and what dawned on me. That question is why do I need to pass objects locally around, there are good reasons in many cases, but what I found was to limit that and at the least make it a pattern of use.

I won't go into actual architecture, because there are a number of ways to do this. I've used your model of update and render. So it triggered some recollections.

Firstly I thought more about how to loosely couple systems together so I can swap out and enhance components as I went. One such method that worked for me was I found that by using an event manager (in my experience) where objects subscribed and unsubscribed from events removed about 80% of my coupling across the system. All components had the right to post events to the system and really became the piping for alot in my game engine. This event system was VERY basic, passing around an event type, subtype and a payload (it could be anything else, this is just my example). I still have coupling afterwards but now only for creating certain entities in game. The model itself allows you to have completely different classes and systems convey simple loosely coupled messaging between systems. Having said that, my Eventmanager was a Singleton, which probably contradicts point 2 to a degree.

Secondly, Globals most of the time won't help, in fact, it just makes it worse in many cases (YES, they do help in some spots, so its actually cleaner way although they are a bit of a crutch) . So I used container classes for the more major manager objects and don't be afraid to group managers into specific containers, my grouping was coarse, graphics and game logic managers for example.

In respects to globals, I found that it meant I had flattened the dependency tree of the various system components. Removing the globals meant I was forced to address the hierarchy of my critical objects and order them more logically. The globals were a reflection of my lack of planning in the architecture of my engine. It has a number of roll improvements once I had fixed this, controlling the order of destruction and creation of objects was natural. Rather than having to understand explicitly dependence of objects, it was implied and also enforced a standard.

Thirdly (and a little off topic), you will continue to learn and that will mean you will see patterns where you didn't before. So don't be hard on yourself, there is code you will look at and say "What the hell was I thinking.". Always revise, and improve. You can read a book, but if you shoe horn yourself into a discipline/design and don't end up liking it, it will kill your enjoyment. So diving in on your project can be a good and bad experience, but just keep learning.

Finally, all is a disclaimer, because of my experience with developing my own game. I've learned alot and would redo some things from the start, but I have had some surprising results around design and execution just through learning. good luck. Everyone's experience is different, I'm sure you will get some good tips from others on here.

\$\endgroup\$
  • \$\begingroup\$ added more info on the globals, for me globals had demonstrated to me a a lack of planning and architecture in my game engine. It also enforced more rigid and repeatable implementation patterns. \$\endgroup\$ – ErnieDingo Jun 7 '18 at 4:47
0
\$\begingroup\$

There is no ideal solution, it's just a matter of understanding Pros/Cons of each choices and endorse them.

void Update(float deltaTime)
{
    // ...
}

As you said, this will be a pain to refactor but it's not an impossible task. Moreover you can expect to have an API that become more and more stable over the time.

I would discourage to 'hide' this into a Struct, because 'Data' makes no sense. What if you have different function with different 'Data' struct. It will be a pain to understand the project when it grows. This is the inverse of the first solution: Quick now but hard latter. However you can use struct when it make sense (just a dummy example):

struct FrameTime
{
    Millisecond  deltaTime;
    int          frameNumber;
};

Statics (or globals) make thing easier to use (but not easier int case of refactoring). For instance a paradigm I like, because it's not really a Singleton but provide me a Global 'Main' access:

// Time.h
class Time
{
  public: // Public class API
    Millisecond getDeltaTime();

  public: // Static Main access (Better if in another class)
    static Milliseconds GetDeltaTime();
};

// Time.cpp
Millisecond Time::getDeltaTime()
{
  return ...;
}

static Time s_time;
Millisecond Time::GetDeltaTime()
{
return s_time.getDeltaTime();
}


// ...

void Update()
{
    Millisecond deltaTime = Time::GetDeltaTime();
    // ...
}

Then question is not, is it possible that you need something else than the 'Global delta time', because there is always a possibility. But the question is, what happens if suddenly you need to provide an independent delta time. How much energy you will need to refactor it versus which is the risk. This rarely happens, but it happens.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the answer. Using some sort of static function setup like you have shown is a method that I will definitely consider using. Are there any immediate or future problems with using this method? \$\endgroup\$ – Ioan Thomas Jun 8 '18 at 23:02
  • \$\begingroup\$ I use this solution for a while now and did not noticed any specific issue. What can happen also is, if you need to change an API used by a lot a static methods, you will have more refactor than if you just used the getInstance() \$\endgroup\$ – FloFu Jun 18 '18 at 11:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.