🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Rookie OOP question: How to avoid having to pass tons of inputs everywhere

Started by
15 comments, last by hplus0603 3 years, 11 months ago

Hey there, I'm new around these parts, and to the subject of game development itself

Over the summer I'm going to spend some time, trying to write a very basic game engine, using C++. My plan was to build it in such a way, that you have a application class, which contains everything about the game (such as game logic, objects and so on), and an engine class which takes a application object, does stuff with it.

However no matter how I try to organize it, I seem to keep running into the problem, that everything needs to have everything else passed to it. Being a mathematical engineering undergrad, most programming I have done, has been aimed at very narrow tasks, so I'm not sure how to have handle this problem. I won't hide, that learning some OOP along the way is also a major goal of this project. Anyway an example of the problem:

The engine class which are responsible for the main loop, is meant to use a fixed time step. It has a “time” class member object, responsible for keeping track of time, both between frames, and between updates. However even though the update uses a fixed step size, I still have to pass that step size into the update / render function, in order for it to be used by the game logic / physics / whatever. These in turn will have to pass it again, to all the components etc etc. This might seem trivial, but it to me it looks like its going to be a a recurring problem, as there will probably be more things than just time, which most things will need to know. As far as I can see, it can be handled in one of the following ways;

  • Pass the variable everywhere. This is what I don't want to have to do, as I expect this approach will quickly get out of hand, as the project expands.
  • Hard code the time step into everything, which seems obviously bad.
  • Use a namespace for “time”, rather then a class. This is bad OOP practice, as far as I can find online.
  • Use a singleton, which again the internet says is bad.
  • Pass a reference to the Time class or a shared pointer to everything, which will bloat all the classes, and still not avoid having to pass it in some capacity, which also seems bad.

So how do I handle this in a good way? Keep in mind I do not have any grand plans for this project. My hope is to make something somewhat flexible and simple, but the main goal is to learn. If you have courses to recommend, or books on C++ / game design, those would be appreciated as well. However I typically learn the most by doing, so while its probably not the most efficient way, it is the way I will actually learn something, whether then nothing.

DISCLAIMER: English is not my native language. If I'm miss using terms, or explaining something inadequately, feel free to correct me.

Advertisement

SokarEntertainment said:

  • Use a namespace for “time”, rather then a class. This is bad OOP practice, as far as I can find online.
  • Use a singleton, which again the internet says is bad.

People do things in different ways, and there are often multiple solutions to a problem. Some may be better in some respects, some in others.

Keeping stuff globally available is a smell for sure, but it's not universally bad to have access to certain things. You'd like to be able to log stuff from anywhere, right? Getting the interval in update functions is also convenient. Whether you decide to roll it out as a singleton or a namespace gives certain benefits and drawbacks.

Sorry for being so vague; my message is that if you can't think of a prettier way to do it than having it globally accessible, it probably makes sense to have it globally accessible. If you group the various global states, though, by paths and/or namespace, it makes it easier to see where different things are used rather than having one “global.h” where everything can be fetched from.

You could also consider inheriting (or aggregating) classes that have static members which you can assign centrally. This could also be rather pretty (but won't be so unconditionally)

And also, if you insist on implementing the entire project strictly on OOP principles, you'll run into stuff like this sooner or later. It's not alpha and omega in development, just a lot of good practices.

OOP is about data with intelligence, or perhaps, intelligence with data.

So while you could have a step-size object, it feels ‘too small’ to be an object to me. I mean, it's just a real value, right? The only thing you can do with it is ‘get’, which is technically not even something you can do with a real number, although that is a bit nit-picky.

A ‘better’ object is likely bigger, a significant unit of behavior, for example, a ‘main loop’ object. Then you can attach all kinds of stuff into that. Passing around that bigger object means you don't have to pass around everything that is already inside it.

In other words, make bigger ‘smart/intelligent’ blobs of data as object, so you have less of them to pass around. (With things like ‘timestep’ likely disappearing completely, as nobody has an interest in it besides some logic near the main loop.)

@SuperVGA Alright I assumed that there would be a standard way of handling something like this, but I guess I can always rewrite whatever is written later. I have a tendency of getting stuck on details, which really hinders project progression. As for the rest. What do you mean by:

“You could also consider inheriting (or aggregating) classes that have static members which you can assign centrally. This could also be rather pretty (but won't be so unconditionally)”

Can you give a quick example?

Also I don't necessarily insist on implementing everything using OOP. I can imagine that would be rather limiting. However I would like to learn to recognize the difference, between when it makes sense, and when it does not ?

@Alberth Well its not really that simple an objective. Here is the code as it as amount

class Time {
private:
	// System time
	double m_currentTime, m_lastTime, m_deltaTime;
	// Engine time
	double m_engineTime, m_engineTimeBuffer;
public:
	Time()
	{
		m_currentTime = 0;
		m_lastTime = 0;
		m_deltaTime = 0;
		m_engineTime = 0;
		m_engineTimeBuffer = 0;
	};
	~Time() {};

	inline void UpdateTime() {
		// Update system time
		m_currentTime = glfwGetTime();
		m_deltaTime = m_currentTime - m_lastTime;
		m_lastTime = m_currentTime;
		// Insert deltaTime into engine time buffer
		m_engineTimeBuffer += m_deltaTime;
	}
	inline double GetCurrentTime() const { return m_currentTime; }
	inline double GetDeltaTime() const { return m_deltaTime; }

	inline void UpdateEngineTime(double timeStep) {
		// Iterate engine time
		m_engineTime += timeStep;
		m_engineTimeBuffer -= timeStep;
	}
	inline double GetEngineTime() const { return m_engineTime; }
	inline double GetTimeBuffer() const { return m_engineTimeBuffer; }
};

Its mainly there to keep the main loop neat. The timebuffer, is the time used by the inner update loop btw. Also not making it an object, won't prevent the problem. The time step and time in general, still needs to go down into both the game logic and the and the rendering (at least as I have planned it. Again perhaps my structure is wrong or bad, so any and all suggestions are welcome)

The idea is to use an ECS model, though I'm far off trying to plan or implement this atm.

Code doesn't seem to do a lot, about ½ the code is just getter boilerplate, which I usually omit, as I don't believe much in being very protective in an environment where all objects are supposed to co-operate.

The “Time” name seems a bit wrong, this is not time, it's timing or time-management or something, the give-away is that the Time class has functions that return time ?

I don't know the right answer either, but my current ideas is as follows.

First, there is a thing called “game mode” (it needs a better name). Basically it's “a thing you do in the game” at very high level. Eg, intro, level selection, play a level, look at higscore, etc. Each with their own screen.

class GameMode {
public:
	GameMode(const std::string &name);
	virtual ~GameMode();

	virtual void InitialSetup();
	virtual void PrepareRun();

	/**
	 * Perform the game mode.
	 * Called after #PrepareRun. #EndRun will be called after the method returns to the caller.
	 * @return Name of the next game mode to perform, or \c code "exit" to end the program.
	 */
	virtual std::string Run() = 0;
	virtual void EndRun();

	const std::string name; ///< Name of the game mode.
};

It is a base class, you're supposed to derive a class with your code doing “the thing you do” here. Obviously you should override “Run” here and implement something useful for the user. You can make derived base classes from this which act as base class aimed at a more specific type of activity if you wish.

While this class is very bare, you can add fields with data or hooks here which are then directly available in the derived class. Alternatively you can supply such objects during Setup() or Prepare().

Inside Run, in a derived class, there is a game loop. I want the game mode to be in control, and I ended with with an idea like

GameDisplay::GameDisplay(video::Video &video)
		: GameMode("play", video), tiled_background(), ship(), gl_buffer()
{
}
GameDisplay::~GameDisplay() { }

void GameDisplay::InitialSetup() {
}
void GameDisplay::PrepareRun() {
	gl_buffer.Clear();
}

std::string GameDisplay::Run() {
	for (;;) {
		if (video.Poll() != video::PollResult::ok) break;
	}

	return "done";
}

void GameDisplay::EndRun() {
	gl_buffer.Clear();
}

GameDisplay is a derived class from GameMode (as you can see, I am not sure where ‘video’ belongs, here I pass it down into the base class, while above that parameter doesn't even exist).

In the loop I call Poll() to perform IO (the name “video” is misleading, video is just one form of IO, but there are more). Not included here, but also needed is to draw something to the screen for the user to see, and something for checking keyboard or mouse etc. Currently the former has its own set of classes, although it's not fleshed out good enough.

Obviously you can do all kinds of things inside Poll besides polling IO. You could handle timing there, and your Time class would be fully buried in the internals of Video here, although Video would likely have an API to talk to it to get timing information.

SokarEntertainment said:

@SuperVGA Alright I assumed that there would be a standard way of handling something like this, but I guess I can always rewrite whatever is written later. I have a tendency of getting stuck on details, which really hinders project progression.

I know what you mean. I think all developers struggle or have struggled with that at some point. For your first few projects, I think you should be given a "even the clutter serves a purpose" pass. It's difficult to not learn anything, and you'll become used to adopting design patterns that fit your needs.

SokarEntertainment said:

What do you mean by:

“You could also consider inheriting (or aggregating) classes that have static members which you can assign centrally. This could also be rather pretty (but won't be so unconditionally)”

Can you give a quick example?

Sure! see the following, or try it here (alright, GD isn't very fond of links; it's this one cpp.sh/7gg3q​ on C++ shell)
Note a few things. I've tried demonstrating the use of

  • Putting classes with the same base class into a single collection
  • A pure virtual function (these can be used to define interfaces) defined in UpdatingEveryTick.
  • A lambda function
  • Initializer lists
  • Keyword auto (sometimes it's nice to be explicit about stuff, sometime less so) and range-based for loop.
  • This thing with accessing a static member that I described in my previous comment.

There's lot's of bad practice here also;

  • I've defined classes here instead of in headers, and implemented their functions right away instead of putting them in separate cpp files when possible.
  • I also haven't used any namespaces - you seem to know what those are. Remember you can nest them, and remember that they can reflect the folder structure also, which I prefer.
  • The static time_since_last_tick_ms member is public - everyone can use it! You could mark it protected to sort that out…
  • For demonstration purposes, I've repeated setting the time interval inside the pretend game loop.

If you want to make it clear to another consumer of UpdatingEveryTick that it's only one place that sets this, you could run the main loop within a class, and make that a friend of UpdatingEveryTick, while making the interval private, and providing a public getter.

#include <memory> // std::shared_ptr, std::make_shared
#include <vector> // std::vector
#include <string> // std::string
#include <iostream> // std::cout, std::endl

class UpdatingEveryTick {
  public:
    static unsigned time_since_last_tick_ms;
    virtual void update() = 0;
};
unsigned UpdatingEveryTick::time_since_last_tick_ms = 0;

class Projectile : public UpdatingEveryTick {
  public:
    Projectile(const std::string &label) : label(label) {}
    void update() { std::cout << "Projectile \"" << label << "\" updating with period of " << UpdatingEveryTick::time_since_last_tick_ms << " ms." << std::endl; }
  private:
    std::string label;
};

class Character : public UpdatingEveryTick {
  public:
    Character(const std::string &label) : label(label) {}
    void update() { std::cout << "Character \"" << label << "\" updating with period of " << UpdatingEveryTick::time_since_last_tick_ms << " ms." << std::endl; }
  private:
    std::string label;
};

int main ()
{
  // This demonstrates a way you can insert several items of varying class into a single collection. (It's really just full of glorified UpdatingEveryTick* -pointers)
  std::vector< std::shared_ptr< UpdatingEveryTick > > items_that_update_every_tick;
  
  // Let's add some items
  items_that_update_every_tick.emplace_back( std::make_shared< Projectile >("Plasma blob") );
  items_that_update_every_tick.emplace_back( std::make_shared< Projectile >("RPG") );
  items_that_update_every_tick.emplace_back( std::make_shared< Projectile >("9mm bullet") );
  items_that_update_every_tick.emplace_back( std::make_shared< Character >("John") );
  items_that_update_every_tick.emplace_back( std::make_shared< Character >("Marsha") );
  
  // This is a lambda function that just iterates over items_that_update_every_tick and runs update on every item. It's only here to make the game loop more compact
  const auto update_all_items = [&]() {
    for(auto &amp;amp;amp;item_to_update : items_that_update_every_tick) {
      item_to_update.get()->update();
    }
  };
  
  // Let's pretend the following is your game loop, and you get the interval somehow - from std::chrono perhaps...
  //while(true) {
      UpdatingEveryTick::time_since_last_tick_ms = 22;
      update_all_items();
      UpdatingEveryTick::time_since_last_tick_ms = 20;
      update_all_items();
      UpdatingEveryTick::time_since_last_tick_ms = 19;
      update_all_items();
  //}

  return 0;
}

SokarEntertainment said:

Also I don't necessarily insist on implementing everything using OOP. I can imagine that would be rather limiting. However I would like to learn to recognize the difference, between when it makes sense, and when it does not ?

Great. You will, eventually. See, I just spent 10 minutes writing this little example, and another 15 minutes criticizing it. It's often something worthy of a discussion with a colleague, but gradually you'll get this gut feeling for it. It's never black and white, but of as I said, and as you reckon, of course there's always solutions that look nicer, perform faster or are easier to maintain. It's always a balance.

@undefined @SuperVGA That is one hell of an answer! :D I'll be looking over that piece of code greater detail tomorrow, as its getting late here. My big take away thus far though, is that its better to just “do something”, and refractor code as I learn better ways of doing things. I greatly appreciate both of your answers. Its given me quite something to think about, and motivate me to focus more on just getting things done, and not try to learn “the perfect way" ahead of time. Thanks for all the help thus far ?

SokarEntertainment said:
… not try to learn “the perfect way" ..

This is one of the big things. People generally think there is 1 perfect solution, while in fact there are an infinite number of perfect solutions, each one subtly aiming at a different optimal goal, a different notion of "perfect". For example, optimal for experimenting or optimal for performance are both valid achievable goals, but it is not realistically possible to get them both in one implementation.

Anything beyond a typical school exercise is way too complicated to get anywhere near optimal on the first try.

If you thoroughly understand the problem, the programming language, and the (in this case OOP) programming model, then you can achieve a solid near optimal implementation around the 3th implementation. Note that the pre-condition requires that you write many more than 3 implementations here, especially the first part, “understand the problem” tends to be very tricky.

Until that time, aiming for “something that sort-of works”, and learn from your mistakes is the best way to make progress indeed.

class Time {
private:
	// System time
	double m_currentTime, m_lastTime, m_deltaTime;
	// Engine time
	double m_engineTime, m_engineTimeBuffer;
public:
	Time()
	{
		m_currentTime = 0;
		m_lastTime = 0;
		m_deltaTime = 0;
		m_engineTime = 0;
		m_engineTimeBuffer = 0;
	};
	~Time() {};

	inline void UpdateTime() {
		// Update system time
		m_currentTime = glfwGetTime();
		m_deltaTime = m_currentTime - m_lastTime;
		m_lastTime = m_currentTime;
		// Insert deltaTime into engine time buffer
		m_engineTimeBuffer += m_deltaTime;
	}
	inline double GetCurrentTime() const { return m_currentTime; }
	inline double GetDeltaTime() const { return m_deltaTime; }

	inline void UpdateEngineTime(double timeStep) {
		// Iterate engine time
		m_engineTime += timeStep;
		m_engineTimeBuffer -= timeStep;
	}
	inline double GetEngineTime() const { return m_engineTime; }
	inline double GetTimeBuffer() const { return m_engineTimeBuffer; }
}

Not a good class.

  • The name of “time” is traditionally used for classes that represent time of day (timezone, hours, minutes, seconds and usually fractions of second), and it usually goes hand in hand with a “date” class. This class, on the other hand, is clearly intended to represent the step-by step advancement of the state of your game engine and rendering, not time.
  • Field m_lastTime is redundant.
  • The class is incomplete, bordering on meaningless: what decisions are based on m_engineTimeBuffer? who (and when) calls the update methods? Who decides the timestep? What do you actually do with the various times? Clearly this logic is artificially and inconveniently ripped out from the game loop at the heart of your engine: collecting into a class some variables for the frivolous reason that they all represent times is not useful.
  • UpdateTime has an ugly dependency on GLFW, for the purpose of reducing flexibility by querying the system clock instead of receiving a current time value as a parameter.

Omae Wa Mou Shindeiru

This topic is closed to new replies.

Advertisement