1
\$\begingroup\$

I am currently designing a Multi User Dungeon (MUD). For those who do not know what it is, they are the ancestors of MMORPGs. There is no graphics, everything is played in a console by typing commands. Players move into rooms, fight monsters, earn xp, and so on.

I am programming in C++ and use Boost. I am inspired by several examples of MUD that I found on the internet whose source code is available.

In each of the examples I found, global variables are often used: the list of players, the list of connected players, the list of rooms, etc. Indeed, these data are used absolutely everywhere in the program. They are at the heart of the game. 80% of the code consists in manipulating these data. I understand that others have been tempted to declare these data as global variables.

But I understand the danger of global variables and the problems that their use raises.

The problem is that I cannot imagine a viable solution that would prevent me from using global data.

I will try to give you a description of my architecture to illustrate my problem:

  • a class Server (linked to a list of sessions)
  • a class Session (linked to a connection instance)
  • a class Connection Instance (linked to a Player, a Session and a Context)
  • a class Player (linked to a Room)
  • a class Area (contains Rooms)
  • a class Room
  • a class Context (used to differentiate the different contexts: normal play, or when the player types his name and password, etc)
  • a class Command (for example, “kill a mob”)
  • many others…

I am not sure to know how I can have an access to every room/player/npc/object in the game in all these classes without passing everywhere a reference or a pointer to an object containing the whole game as parameter.

For example, a player might have a "kill every player in the world" command. "KillEveryoneCommand" that would inherit "Command". But where would this list of players be stored? How could it be accessed from the "Player" class or from the "KillEveryoneCommand" class?

Theoretically, a player could also have a command to ignite all the rooms of an area, or to communicate with another player from a distance... All these things are done without any problem with global data.

Would an online game with such central and shared data be a good exception for using global data?

\$\endgroup\$
6
  • \$\begingroup\$ Why can't you pass everywhere a gigantic object containing the whole game? You won't actually be passing the object, just a pointer/reference to it. \$\endgroup\$
    – user253751
    Jun 25, 2019 at 5:05
  • \$\begingroup\$ Well, I could. But in my opinion, it's almost like having global data. From an OOP point of view, it is certainly cleaner. But the disadvantage is that we end up having to pass this pointeur/reference to this "GlobalData" object in most methods, sometimes to use the data contained in the object, but sometimes just for the purpose of passing this object to other objects/methods. \$\endgroup\$
    – Daily45
    Jun 26, 2019 at 6:14
  • \$\begingroup\$ @immibis That just hides the globals behind a layer of indirection but doesn't solve any of the problems associated with globals. \$\endgroup\$
    – Philipp
    Jun 26, 2019 at 7:29
  • \$\begingroup\$ Global data isn't that bad. Overengineering is. Keep it simple. There is nothing wrong with Singleton. But don't forget about Facade. Don't make many small singletons, encapsulate things in one game core instead. \$\endgroup\$ Jun 26, 2019 at 14:00
  • 1
    \$\begingroup\$ @trollingchar singletons are globals - unless immutable, in which case it's more like the flyweight pattern. \$\endgroup\$
    – user253751
    Jun 26, 2019 at 21:32

3 Answers 3

1
\$\begingroup\$

One option is to build an event-based architecture. In such an architecture, objects communicate via an event queue. They can post events to that queue and register themselves as receivers for specific events. But the reaction to these events is implemented in the receivers.

For example, the Kill Everyone command would be implemented by having all killable entities (or other objects which want to know about that event for some reason) register themselves as receivers for the Kill-event. The entity which performs the command then posts a Kill event for everyone to the global event queue. The other entities receive that event and do what needs to be done in order to die.

Receiving event can either happen as a push-logic (the event queue calls a method on the receivers) or as pull-logic (the receivers ask the event system for events during their update-method).

The advantage of this architecture is that the only data you need to share between entities is the event queue object itself. You will be able to avoid sharing most data about the global game state. You also have a very loose coupling between entities, because event senders and event receivers don't even need to know of each other's existence.

Usually you have at least one global event queue in such an architecture, but it is quite common to also have local event queues. In a MUD it would be quite usual to have an event queue for each room which handles events which are only relevant for entities which are in that room right now or for systems which only operate inside a specific room. When you have a component-based architecture where each entity in the game is a composite of multiple objects, you can also use entity-internal event queues to implement the communication between the components.

However, this architecture doesn't come without cost. The event dispatching logic can become pretty complex, and a complex system which is invisible to the end-user is a breeding ground for bugs and performance bottlenecks. So be careful not to overengineer. Some pitfalls you need to watch out for are:

  • Inefficient event dispatching eating too many CPU cycles. Make sure you use appropriate data structures for your event queues and receiver lists.
  • Event spam. Objects posting unnecessary events or receivers registering for too many events they don't need to know about.
  • Insufficient garbage collection. Make sure that objects always get removed from event queues when the object is deleted or no longer needs to listen to that queue for some other reason. It wastes resources in the best case and crashes your server by dereferencing a pointer to a deleted object in the worst case.
  • Relying on the order in which events arrive or in which they are processed by other entities. Avoid such dependencies whenever possible. They will lead to nothing but pain and misery in the long run.
  • Event loops. Two events from two different entities which keep triggering each other in an infinite loop.
\$\endgroup\$
0
\$\begingroup\$

The issue with global variables is that they can be hard to test and reason about as they can be modified everywhere.

To avoid the issue of having them modified everywhere, what may work well is to create an interface to manage this data in order to deal with these lists in a clean way. This could be another class which contained this data privately and did not allow anything outside the class to modify it. This way you know your lists can are only being modified in the ways you explicitly made clear within this one place. You avoid the issue of having a program with changes to the global variables sprawling throughout it.

Nonetheless, for something like a MUD where you want to keep data persistent you can mostly avoid the issue by using a database to store your global data. Still, you‘ll likely want a specified manner to access the data, this way you know where and how it’s being changed, so an interface to help work with your database would work well too!

Hope this helps and good luck with your game!

\$\endgroup\$
1
  • \$\begingroup\$ So you have a "whole game" class or a "database connection" class. Do you have a global variable holding it, or do you pass it around everywhere? \$\endgroup\$
    – user253751
    Jun 25, 2019 at 5:07
0
\$\begingroup\$

How to do Globals

Perhaps you are trying to avoid global variables out of dogma. It is common to have a class with static methods (or a global instance when that is not possible) that can be interrogated about objects. I'll give you two examples:

You can do something similar. Have an object that holds the model of the game and that has query methods. These query methods will return some sort of iterator that hides the data structures actually used, in such way that they can change independently of client code.


"Design Patterns"

Perhaps you think that what I mention above is not not satisfactory. Certainly, there must be a way to do this without globals. And a way that makes sense (and is not passing a huge model object around)...

That would be a form of Dependency Injection. If a Command needs some objects to run, and injector system must provide them.

That means that instead of having the command query the global state (as it would do if implemented as I said in the first part of this answer), it must publish a description of what it needs to run (perhaps as an interface) and then depending on that (perhaps as a template) what it needs will be injected into it as parameters when doing the call to run it.

To inject that information, it must come from somewhere... hmm... The whole injector thing is overcomplicating things. Keep it simple.


OOP

Now, should you pass this huge model object around to everything? Not necesarily. You might have encountered the argument that the solution for globals is OOP.

This is how it ends up like: On program entry, you create an object, and call a method on it. Inside that method you do your program logic. And now, instead of global variables, that object has fields, and everbody reads those fields.

OOP, whoopty doo.


A real architecture

You isolate external systems.

Standard input? That is an external system. Have a class deal with them and give you meaningful data. I suppose it gives you Commands. Whatever it is, it should have value semantics. In fact, I would argue it should not give you objects. Yes, you will have a single object of that class, created on the entry point.

Next, you have to deal with the Commands. I understand the urge to make commands smart (the commands knows how to execute itself). However, not, that is not right...

This is a MUD. There is a network. The network is an external system. We need to deal with sending these Commands to the server. You need to have an Observer pattern, so that you can have a class that deals with networking, it takes Commands, and you subscribe it to the class that deals with the console.

See, if Commands weren't values, there would be a problem to send them over the network.

Alright, the server will also have a class that deals with the network, and it will output these Commands. Here is where you need a system that knows how to execute the Commands (Note: nothing wrong with a switch based state machine to deal with Commands). This will be class that you instantiate passing the model object, and it also gets Commands. Observer pattern again. The class will update the model and generate Notifcations.

Of course, you need communication both ways. From server to client is different... since the client will - I am assuming here - have a limited view of the world, it does not have to get Notifcations of every executed command. Instead, there must be commands the client send that subscribe or unsubscribe from certain Notifcations. For example, entering a room subscribes the channel to the Notifcations from that room. You figure it out, it is your game.

Alrigh, so you got Notifications. And some clients are subscribed to some Notifications. You need another class that deals with figuring out what Notification goes to what client. And that has to talk to another class that sends reponses over the network. Finally on the client side, another one takes the Notifications from the network and renders (writes to standard output).


Why is that good? Because you can swap parts. Did you decide you will no longer have a console, but instead a GUI? You can do that. Did you decide that the network code will run on its own thread? You can do that. Did you decide that the game is persistent and you will be storing everthing to a database? You can do that.

I am, of course, ingoring handling user accounts et. al. So, you probably will not plug the system that takes commands from standard input right away, instead you will deal with connecting to the server and authenticating the user first... afterwards you instantiate that system (see, it makes sense to make it a class).

Bonus: You can deal with releasing resources in destructors. For example, the destructor of the network system will release the connection. I hear they call it RAII? Naming, whoopty doo.

\$\endgroup\$

You must log in to answer this question.

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