28
\$\begingroup\$

I had a question about game architecture: What is the best way to have different components communicate with each other?

I do really apologize if this question has already been asked a million times, but I can't find anything with exactly the kind of information I'm looking for.

I've been trying to build a game up from scratch (C++ if it matters) and have observed some open source game software for inspiration (Super Maryo Chronicles, OpenTTD and other). I notice that a lot of these game designs use global instances and/or singletons all over the place (for things like render queues, entity managers, video managers, and so forth). I'm trying avoid global instances and singletons and build an engine that is as loosely coupled as possible, but I'm hitting some obstacles that owe to my inexperience in effective design. (Part of the motivation for this project is to address this :) )

I've built a design where I have one main GameCore object that has members that are analogous to the global instances I see in other projects (ie, it has an input manager, a video manager, a GameStage object that controls all the entities and game play for whatever stage is currently loaded, etc). The problem is that since everything is centralized in the GameCore object, I don't have an easy way for different components to communicate with each other.

Looking at Super Maryo Chronicles, for example, whenever a component of the game needs to communicate with another component (ie, an enemy object wants to add itself to the render queue to be be drawn in the render stage), it just talks to the global instance.

For me, I have to have my game objects pass relevant information back to the GameCore object, so that the GameCore object can pass that information to the other component(s) of the system that needs it (ie: for the situation above, each enemy object would pass their render information back to the GameStage object, which would collect it all and pass it back to GameCore, which would in-turn pass it to the video manager for rendering). This feels like a really horrible design as is, and I was trying to think of a resolution to this. My thoughts on possible designs:

  1. Global instances (design of Super Maryo Chronicles, OpenTTD, etc)
  2. Having the GameCore object act as a middleman through which all objects communicate (current design described above)
  3. Give components pointers to all other components they will need to talk with (ie, in the Maryo example above, the enemy class would have a pointer to the video object it needs to talk to)
  4. Break the game into subsystems - For example, have manager objects in the GameCore object that handle communication between objects in their subsystem
  5. (Other options? ....)

I imagine option 4 above to be the best solution, but I'm having some trouble designing it... perhaps because I've been thinking in terms of the designs I have seen that use globals. It feels like I'm taking the same problem that exists in my current design and replicating it in each subsystem, just at a smaller scale. For example, the GameStage object described above is somewhat of an attempt at this, but the GameCore object is still involved in the process.

Can anyone offer any design advice here?

Thanks!

\$\endgroup\$
6
  • 1
    \$\begingroup\$ I understand your instinct that singletons aren't great design. In my experience, they've been the simplest way to manage communication across systems \$\endgroup\$ Commented Apr 1, 2013 at 12:13
  • 4
    \$\begingroup\$ Adding as a comment since I don't know if it's a best practice. I have a central GameManager which is composed of subsystems such as InputSystem, GraphicsSystem, etc. Each subsystem takes the GameManager as a parameter in the constructor and stores the reference to a class private member. At that point, I can refer to any other system by accessing it through the GameManager reference. \$\endgroup\$
    – Inisheer
    Commented Apr 1, 2013 at 13:11
  • \$\begingroup\$ I changed the tags because this question is about code, not game design. \$\endgroup\$
    – Klaim
    Commented Apr 1, 2013 at 14:41
  • \$\begingroup\$ this thread is a little bit old, but I have exactly the same problem. I use OGRE and I try to use the best way, in my opinion option #4 is the best approach. I have build something like the Advanced Ogre Framework, but this is not very modular. I think I need a subsystem input handling which only get the keyboard hits and mouse movements. What I don't figure out is, how can I create such an "communication" manager between the subsystems? \$\endgroup\$ Commented Jul 11, 2013 at 19:34
  • 1
    \$\begingroup\$ Hi @Dominik2000, this is a Q&A site, not a forum. If you have a question you should post an actual question, and not an answer to an existing one. See the faq for more details. \$\endgroup\$
    – user1430
    Commented Jul 11, 2013 at 20:26

5 Answers 5

19
\$\begingroup\$

Something we use in our games to organize our global data is the ServiceLocator design pattern. The advantage of this pattern compared to the Singleton pattern is that your global data's implementation can change during the runtime of the application. Also, your global objects can be changed during the runtime too. Another advantage is that it's easier to manage the order of initialization of your global objects, which is very important especially in C++.

e.g (C# code that can be easily translated to C++ or Java)

Lets say that you have a rendering backend interface which has some common operations for rendering stuff.

public interface IRenderBackend
{
    void Draw();
}

And that you have the default render backend implementation

public class DefaultRenderBackend : IRenderBackend
{
    public void Draw()
    {
        //do default rendering stuff.
    }
}

In some designs it seems legit to be able to access the render backend globally. In the Singleton pattern that means that each IRenderBackend implementation should be implemented as unique global instance. But using the ServiceLocator pattern doesn't require this.

Here's how:

public class ServiceLocator<T>
{
    private static T currGlobalInstance;

    public static T Service
    {
        get { return currGlobalInstance; }
        set { currGlobalInstance = value; }
    }
}

In order to be able to access your global object you need to intialize it first.

//somewhere during program initialization
ServiceLocator<IRenderBackend>.Service = new DefaultRenderBackend();

//somewhere else in the code
IRenderBackend currentRenderBackend = ServiceLocator<IRenderBackend>.Service;

Just to demonstrate how implementations can vary during the runtime, let's say that your game has a minigame where rendering is isometric and you implement an IsometricRenderBackend.

public class IsometricRenderBackend : IRenderBackend
{
    void draw()
    {
        //do rendering using an isometric view
    }
}

When you transition from the current state to the minigame state you just need to change the global render backend provided by the service locator.

ServiceLocator<IRenderBackend>.Service = new IsometricRenderBackend();

Another advantage is that you can use null services too. For example, if we had an ISoundManager service and the user wanted to turn the sound off, we could just implement a NullSoundManager that does nothing when its methods are called, so by setting the ServiceLocator's service object to a NullSoundManager object we could achieve this result with hardly any amount of work.

To summarize, sometimes it may be impossible to eliminate global data but that doesn't mean that you can't organize them properly and in an object oriented way.

\$\endgroup\$
3
  • \$\begingroup\$ I've looked into this before but not actually implemented it into any of my designs. This time, I plan to. Thank you :) \$\endgroup\$
    – Awesomania
    Commented Apr 6, 2013 at 19:09
  • 3
    \$\begingroup\$ @Erevis So, basically, you are describing global reference to polymorphic object. In turn, this is just double indirection (pointer -> interface -> implementation). In C++ it may be implemented easily as std::unique_ptr<ISomeService>. \$\endgroup\$ Commented Apr 7, 2013 at 13:49
  • 1
    \$\begingroup\$ You could change the initialization strategy to "initialize on first access" and avoid needing to have some external code sequence allocate and push services to the locator. You could add a "depends on" list to services so that when one is initialized it will automatically set up other services it needs and not pray that someone remembered to do that in main.cpp. A good answer with flexibility for future tweaking. \$\endgroup\$ Commented Oct 12, 2015 at 23:33
4
\$\begingroup\$

There are many ways to design a game engine and it really all boils down to preference.

To get the basics out of the way, some developers prefer to design it like much like a pyramid where there is some top core class often referred to as a kernel, core or framework class that creates, owns, and initializes a series of subsystems such as audio, graphics, network, physics, AI, and task, entity, and resource management. Generally, these subsystems are exposed to you by this framework class and usually you would pass this framework class to your own classes as a constructor argument where appropriate.

I believe you're on the right track with your thinking of option #4.

Keep in mind when it comes to communication itself, that doesn't always have to imply a direct function call itself. There are many indirect ways communication can occur, whether it be through some indirect method using Signal and Slots or using Messages.

Sometimes in games, it's important to allow actions to occur asynchronously to keep our game loop moving as fast as possible so that frame rates are fluid to the naked eye. Players don't like slow and choppy scenes and so we have to find ways to keep things flowing for them but keep logic flowing but in check and ordered too. While asynchronous operations have their place, they're not the answer for every operation either.

Just know that you'll have a mix-in of both synchronous & asynchronous communications. Pick what is appropriate, but know you'll need to support both styles among your subsystems. Designing support for both will serve you well into the future.

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

You just have to make sure that there are no reverse or cyclical dependencies. For example, if you have a class Core, and this Core has a Level, and the Level has a list of Entity, then the dependency tree should look like:

Core --> Level --> Entity

So, given this initial dependency tree, you should never have Entity depend on Level or Core, and Level should never depend on Core. If either Level or Entity need to have access to data that's higher up in the dependency tree, it should be passed as a parameter by reference.

Consider the following code (C++):

class Core;
class Entity;
class Level;

class Level
{
    public:
        Level(Core& coreIn) : core(coreIn) {}

        Core& core;
}

class Entity
{
    public:
        Entity(Level& levelIn) : level(levelIn) {}

        Level& level;
}

Using this technique, you can see that each Entity has access to the Level, and the Level has access to the Core. Notice that each Entity stores a reference to the same Level, wasting memory. Upon noticing this, you should question whether or not each Entity really needs access to the Level.

In my experience, there is either A) A really obvious solution to avoid reverse dependencies, or B) There is no possible way to avoid global instances and singletons.

\$\endgroup\$
2
  • \$\begingroup\$ Am I missing something? You mention 'you should never have Entity depend on Level' but then you describe it's ctor as 'Entity(Level& levelIn)'. I understand that the dependency is passed by ref but it's still a dependency. \$\endgroup\$ Commented May 29, 2014 at 14:59
  • \$\begingroup\$ @AdamNaylor The point is that sometimes you really need reverse dependencies, and you can avoid globals by passing references. In general, though, it is best to avoid these dependencies entirely, and it is not always clear how to do this. \$\endgroup\$
    – Apples
    Commented May 29, 2014 at 17:04
0
\$\begingroup\$

So, basically, you want to avoid global mutable state? You can make it either local, immutable, or not a state at all. Latter is most efficient and flexible, imo. It is known as iplementation hiding.

class ISomeComponent // abstract base class
{
    //...
};

extern ISomeComponent & g_SomeComponent; // will be defined somewhere else;
\$\endgroup\$
0
\$\begingroup\$

The question is actually seems to be about how to reduce coupling without sacrificing performance. All global objects (services) usually form a sort of context which is mutable during the run-time of the game. In this sense, the service locator pattern disperses different parts of the context into different parts of the application, which may or may not be what you want. Another real world approach would be to declare a structure like this:

struct sEnvironment
{
    owning<iAudio*> m_Audio;
    owning<iRenderer*> m_Renderer;
    owning<iGameLevel*> m_GameLevel;
    ...
}

And pass it around as a non-owning raw pointer sEnvironment*. Here pointers point to interfaces so coupling is reduced in a similar way compared to service locator. However, all services are in one place (which might or might not be good). This is just another approach.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .