The inevitable God object

Started by
21 comments, last by 100GPing100 10 years, 1 month ago

Hello

Anyone else experiencing that most of your code end up in a God Object?

Over some time I have toyed with several different programming ways. From pure procedural to strict OOP. As I program my game I encounter problems such as how to move messages between objects. Even messages across a network. These messages has to be properly identified, dispatched and call the correct methods. All messages are derived from a base object such that they can all be stored in the same STL container.

We can take advantage of double dispatch to avoid down casting these message objects. However now a message has to be accompanied by all the possible objects it may interact with. So in order for this message to have that acces, we need an interface that holds everything, the God object, or GameManager or whatever.

C++ has so many quirks that most of my code are based on solving these quirks rather then focusing on the game itself.

Am I alone, is this a noob thing, or what?

Advertisement

This seems to mostly happen when you haven't seen enough design tricks to know how to break out of existing bad habits. Sometimes a single tweak can clean things up substantially; other times you might be looking at massive refactorings based on half a dozen techniques. In your case, it unfortunately seems more like the latter.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

Hello

Anyone else experiencing that most of your code end up in a God Object?

Over some time I have toyed with several different programming ways. From pure procedural to strict OOP. As I program my game I encounter problems such as how to move messages between objects. Even messages across a network. These messages has to be properly identified, dispatched and call the correct methods. All messages are derived from a base object such that they can all be stored in the same STL container.

We can take advantage of double dispatch to avoid down casting these message objects. However now a message has to be accompanied by all the possible objects it may interact with. So in order for this message to have that acces, we need an interface that holds everything, the God object, or GameManager or whatever.

C++ has so many quirks that most of my code are based on solving these quirks rather then focusing on the game itself.

Am I alone, is this a noob thing, or what?

Why does a message need to be derived from anything ? , a message should preferably just be data so you should be able to use the same class/struct for all messages (no need for inheritance), if your messages have methods then they're either improperly named or are doing far too much.

Personally i would have a message dispatcher object, senders can then send any message they want to the dispatcher and interested objects can implement a listener interface (or inherit from a pure virtual listener class in languages such as C++) and then just subscribe to messages based on type, source, etc.

Then the dispatcher just have to call a callback method in the listener interface/class when a matching message arrives and leave the exact details of how to deal with the message to the recieving object. (messages can then be a simple struct with a type identifier, basic information about the sender and some data)

[size="1"]I don't suffer from insanity, I'm enjoying every minute of it.
The voices in my head may not be real, but they have some good ideas!

I've noticed that some classes can become large and full of utility functions, but as ApochPiQ mentioned, that is mostly due to design choices and habits.

Large games are not so much about hacking some code together until it is a game, instead they are rather intense software architecture and design systems.

In complex games simply adding a few bytes to an object can completely destabilize everything.

Example:

The ubiquitous GameObject base class tends to become a megalithic object. Basically everything derives from it, so when a new feature is added many programmers find it quite tempting for addition. ... After all, the junior programmer may reason, adding a few virtual functions and some data members to GameObject won't hurt anything.

Unbeknownst to the junior programmers, the senior programmers have set email triggers on systems like the massive game object, and also on texture objects, mesh objects, particle systems, and all the other high-instance, high-memory, and high-performance source files. They do it for exactly this reason -- these systems are tempting targets for less disciplined programmers. They are sometimes tempting for the senior developers as well, but they know they'll need to defend their decision so they are more careful about it.

To the junior programmer's surprise, within seconds of submitting the code they receive some emails from two different senior programmers, they are told the change was reverted and a few minutes later there is a meeting scheduled in a available conference room. Why? Assuming your game engine supports several million game objects in it, some junior-level programmer comes along adding 3 floats to it (also in the wrong place so they add 16 bytes instead of 12) they just increased the memory requirements by a whopping 64 megabytes. The senior developers all have a watch flag in source control so ANY change to GameObject.h triggers a batch of emails.

End example.

There are many good solutions even though the solutions come with a cost. It is a tradeoff you must make.

A common tradeoff is to isolate features by abstract base classes. Yes, you need to pay a few cycles to check if the object is the correct cast -- usually by simply casting to the desired type and checking for null if it fails -- but that small cost is typically less than the far greater cost of adding those bytes to the GameObject class and requiring every object everywhere pay the penalty.

I've noticed that some classes can become large and full of utility functions, but as ApochPiQ mentioned, that is mostly due to design choices and habits.

Large games are not so much about hacking some code together until it is a game, instead they are rather intense software architecture and design systems.

In complex games simply adding a few bytes to an object can completely destabilize everything.

Example:

The ubiquitous GameObject base class tends to become a megalithic object. Basically everything derives from it, so when a new feature is added many programmers find it quite tempting for addition. ... After all, the junior programmer may reason, adding a few virtual functions and some data members to GameObject won't hurt anything.

Unbeknownst to the junior programmers, the senior programmers have set email triggers on systems like the massive game object, and also on texture objects, mesh objects, particle systems, and all the other high-instance, high-memory, and high-performance source files. They do it for exactly this reason -- these systems are tempting targets for less disciplined programmers. They are sometimes tempting for the senior developers as well, but they know they'll need to defend their decision so they are more careful about it.

To the junior programmer's surprise, within seconds of submitting the code they receive some emails from two different senior programmers, they are told the change was reverted and a few minutes later there is a meeting scheduled in a available conference room. Why? Assuming your game engine supports several million game objects in it, some junior-level programmer comes along adding 3 floats to it (also in the wrong place so they add 16 bytes instead of 12) they just increased the memory requirements by a whopping 64 megabytes. The senior developers all have a watch flag in source control so ANY change to GameObject.h triggers a batch of emails.

End example.

There are many good solutions even though the solutions come with a cost. It is a tradeoff you must make.

A common tradeoff is to isolate features by abstract base classes. Yes, you need to pay a few cycles to check if the object is the correct cast -- usually by simply casting to the desired type and checking for null if it fails -- but that small cost is typically less than the far greater cost of adding those bytes to the GameObject class and requiring every object everywhere pay the penalty.

Only thing I don't like about your example, is that code should be code reviewed before check-in. =)

Hello

Anyone else experiencing that most of your code end up in a God Object?

Over some time I have toyed with several different programming ways. From pure procedural to strict OOP. As I program my game I encounter problems such as how to move messages between objects. Even messages across a network. These messages has to be properly identified, dispatched and call the correct methods. All messages are derived from a base object such that they can all be stored in the same STL container.

We can take advantage of double dispatch to avoid down casting these message objects. However now a message has to be accompanied by all the possible objects it may interact with. So in order for this message to have that acces, we need an interface that holds everything, the God object, or GameManager or whatever.

C++ has so many quirks that most of my code are based on solving these quirks rather then focusing on the game itself.

Am I alone, is this a noob thing, or what?

Why does a message need to be derived from anything ? , a message should preferably just be data so you should be able to use the same class/struct for all messages (no need for inheritance), if your messages have methods then they're either improperly named or are doing far too much.

Personally i would have a message dispatcher object, senders can then send any message they want to the dispatcher and interested objects can implement a listener interface (or inherit from a pure virtual listener class in languages such as C++) and then just subscribe to messages based on type, source, etc.

Then the dispatcher just have to call a callback method in the listener interface/class when a matching message arrives and leave the exact details of how to deal with the message to the recieving object. (messages can then be a simple struct with a type identifier, basic information about the sender and some data)

I agree with you and something that the Google ProcotolBuffers overdo in their design for example.

I too am all for the simple struct or list of arguments messages rather than the overly designed way of deriving them. Seems more like a futile exercise in OOP.

I also have problems with this - though usually I find it's my inexperience with code architecture engineering that's the problem.

By favoring use of composition over inheritance for many things, and keeping the God object as small as possible, it helps me reduce the problem.

Messaging is still a difficult problem for me - but less so when I realized that games are complex software, and that I shouldn't try to cram my entire game project into a single paradigm - different parts of the game code are better suited to different paradigms of architecture and different messaging systems suit different architectures better than others.

For example, I was writing messy code when I was trying to force my entire game project to use signals-and-slots. Then I was writing messy code when I switched over to forcing the entire project to use an event-based system. Now I realize that one paradigm does not fit all, architecturally-speaking, and that for parent-child hierarchies signals and slots work very well, but for other parts of the game architecture, it doesn't work very well and event systems or callbacks or plain ol' function calls work better (where better here means 'cleaner design').

I still write messy code, but just slightly less messy once I understood that.

Good architecture is something I'm still struggling to learn, so don't take my statement as an experienced view. smile.png


For example, I was writing messy code when I was trying to force my entire game project to use signals-and-slots. Then I was writing messy code when I switched over to forcing the entire project to use an event-based system. Now I realize that one paradigm does not fit all, architecturally-speaking, and that for parent-child hierarchies signals and slots work very well, but for other parts of the game architecture, it doesn't work very well and event systems or callbacks or plain ol' function calls work better (where better here means 'cleaner design').

What's an example of a situation where callbacks work better than signals-and-slots for you? My simple-minded understanding of signals is that they are collections of callbacks...


Why does a message need to be derived from anything ? , a message should preferably just be data so you should be able to use the same class/struct for all messages (no need for inheritance), if your messages have methods then they're either improperly named or are doing far too much.

Deriving messages can offer some advantages. In my Entity/Component-Implementation, this allows me to do neat stuff like:


struct MyMessage : public BaseMessage<MyMessage> // implementation generates an unique ID for this message class
{
    int data;
    float data2;
}

// a system using the message:

void System::Register(MessageManager& messages)
{
    messages.Subscribe<MyMessage>(*this);
}

void System::Receive(BasMessage& message)
{
    // eigther by using a convenience static-casting method
    if(auto pMyMessage = message.Convert<MyMessage>();
    {
        int i = pMyMessage->data;
    }
    // or potentially faster, though currently not working completely typesafe in MSVC due to lack of constexpr:
    switch(message.GetClassID())
    {
    case MessageType::MY_MESSAGE: // should really be MyMessage::ID();
        pMyMessage->data;
    }
}
    
// in game code:

m_pMessages->Send<MyMessage>(1, 2.0f);

I can't think of a way how this would work that easily without deriving (and template magic, too).

Oh, since we are on the topic, entity/component is a great architecture preventing god objects in gameplay/overall game design code.

What's an example of a situation where callbacks work better than signals-and-slots for you? My simple-minded understanding of signals is that they are collections of callbacks...

I kinda view signals as a more generalized and powerful form of callbacks, but sometimes overkill.

If I just want to run an function on every element of a container, passing in a callback (or functor) works perfectly. With signals and slots (in C++ specifically), both the container and the caller would have to support signals and slots, I'd have to attach a slot "DoSomethingToElement()" to the container's signal - which would be a weird signal, not really even an event - maybe called "OnElementAccessed" or something oddly forced sounding like that - and then call the container's "Crawl()" function so the signals get sent. It'd make much more sense to just pass a callback into the Crawl() function directly (and to rename the Crawl() function to something better suited like ForEvery()).

Callbacks are useful because they can be temporary or permanent (like signals and slots) but if they are temporary they don't require as much setup, and they don't require the caller to support slots.

Callback:


object.doSomething(onCallback);

Signals and slots: (when used for a temporary purpose)


object.attach(this, onCallback);
object.doSomethingThatSignals();
object.dettach(this, onCallback);

Signals and slots are more powerful and more flexible, but aren't always the best choice for me.

One of their pros is having multiple slots from multiple objects attached to the same signal, and another is the ease of use when an object has many signals that it emits.

It depends on the needs of the code. My logging system just takes a callback pointer that it permanently holds onto, to process log messages when they arrive. However, it might be good to expand it to take any number of functor objects, so I can have the logs outputted to multiple locations. That's a step inbetween callbacks and signals and slots.

I think my game objects will end up using alot of signals and slots (but that's still unknown to me), because they have alot of different types of interactions between each other and are in a parent-child hierarchy.

And my GUI stuff (since I'm using Qt) uses loads of signals and slots to great benefit - with a variety of different types of interactions and data being communicated. The signals going up to the parents and siblings, and regular function calls going down to the children.

Both the GUI (currently) and my game objects (in the future) also use regular event message queues - these are messages from the overall system rather than communication between two specific objects.

</inexperienced> </still-discovering-what-works>

This topic is closed to new replies.

Advertisement