3
\$\begingroup\$

In my real-time multiplayer game, there are multiple entities that are very interrelated and whose logic is very related to the player who owns them. I don't think I can decouple the classes from each other, but I would like to decouple this group of classes from the Player who owns some of these entities. The Player as I see it is a high level object that interacts with the game map and the web socket server. Here is the context:

  • Every Player is an airline that manages Airplanes that transport Passengers between Terminals.

  • A Terminal is contained in an Airport and simply represents the "portion" of the airport designated for the player.

Responsibilities of each class:

Airport: assign(Passenger) to an Airplane in one of the terminals. (Thus, must contain the collection of Terminals)

Terminal: requestFlight(Airplane). Keeps track of capacity of the terminal (each player has a certain number of airplanes allowed in the terminal which can go up throughout the game).

Airplane: Maintains its own internal state of either in PASSENGER_EXCHANGE mode or IN_FLIGHT. When in passenger exchange mode you can add passengers to the airplane but not when in flight. When in passenger exchange, it is at a particular airport and terminal, so the Terminal must contain that Airport.

Passenger: Maintains its destination and connections of airports to the destination. Does NOT keep track of what Airplane it is on.

I would like to test the Airport logic without needing to instantiate or mock a Player, which takes too many high-level objects for a unit test. But adding a terminal to an airport requires a player so that I know that that player doesn't already have a terminal.

Alternatives designs to having the airports contain terminals and terminals take in players I have considered:

  • Player contains its terminals (already does). Terminals contain airports. This doesn't work because airport logic depends on access to terminals (assigning passengers to terminals)

  • Interface like Owner that I can mock. This also doesn't seem like a good solution because the Player object is used to call the equals() method.

  • Refactor high-level Player object so it does not depend on GameMap. I would like to do this, and it would make testing possible, but the player is directly manipulating the game map, so I don't know how I can write any of the game logic without access to the game map.

\$\endgroup\$
2
  • \$\begingroup\$ Can you elaborate some more on the equals() method in the Player, is it really simple or does it involve something more? Cos it seems like you should "cut the middle man" and follow DavidT's suggestion. \$\endgroup\$
    – Kromster
    Commented May 22 at 6:50
  • \$\begingroup\$ @Kromster It’s as simple as a check on the ID of the player \$\endgroup\$
    – Marvin
    Commented May 22 at 12:40

2 Answers 2

7
\$\begingroup\$

The crux of the issue appears to be that you are tying the identity of an object to the implementation.

In it's simplest form a players ID could just be an integer (player 0,1,2,3...). Hence instead of instancing/storing the players all over your data model you can just have a PlayerID field (Integer) on any object that needs it.

But adding a terminal to an airport requires a player so that I know that that player doesn't already have a terminal.

No, all you actually need to check is that two terminals are not owned by the same player ID, the full player object is superfluous.

If the players are static (the list of players remains the same for the duration of a round) you could just store the actual player objects in an array, the ID is simply an index into the array. If the number of players is more dynamic you may want to use a Map instead - as long as you don't re-use a player ID for two different players in the same round, that should be fine.

You can use the technique of decoupling identity from implementation for any object in your game (Passengers, Airplanes, Terminals, ....) however if objects are dynamic (you create and destroy a lot of them) you have to take extra care to ensure that all references are still valid, so it's generally better to use them judiciously (where you really need them) to resolve difficult coupling problems.


For clarity using ID's like this doesn't really solve any dependency issues, in that a function that calls a method from one object to another now has an extra (array) lookup to go through in order to make the call.

What this technique really gives you is a fast way to check identities of things - many problems only require the identity - which is when this approach works well.

\$\endgroup\$
2
\$\begingroup\$

This is a job for Interfaces

An interface is basically a class which has no implementation. It is a contract which a class can implement. (The link is for C# - but this pattern exists in most object-oriented programming languages.)

In this case, you need to create a few interfaces on top of Player. Your Airport does not depend on all of the functionality of player in order to add a terminal - just one or two methods. Create an interface which exposes those methods, alter Player so that it implements that interface, and then alter Airport so that it takes an argument of that interface type instead of an exact Player object when adding a terminal.

Because Player implements the interface, the game code where you use it will not need to change at all (in most languages); you can continue to pass a Player into the AddTerminal function.

Then, for unit testing, create another class which implements the same interface, but uses dummy values when accessed.

\$\endgroup\$
4
  • \$\begingroup\$ I was considering this, but ‘equals’ is the method I’d be using (so I can call the ‘contains’ method. In Java all objects already implement this so it’d effectively be a marker interface. In effect I only need the player object as an ID for the owner of the terminal. Is it still good practice to use interfaces here? \$\endgroup\$
    – Marvin
    Commented May 22 at 2:55
  • 1
    \$\begingroup\$ @Marvin - If you depend on none of the functionality of Player, then I think the other answer is more correct. If you wanted to go full "Enterprise Quality Code," you could create an IHasPlayerID interface with a GetPlayerID method, which returns an object of type PlayerID (which presumably is just an integer or something), and then compare those. This would allow you to easily change the way that PlayerID is implemented (for example, making it a string instead of an int) without affecting any consumers. \$\endgroup\$
    – Tim C
    Commented May 22 at 18:48
  • 1
    \$\begingroup\$ However, for practical purposes, the other answer is sufficient. Rather than creating an IHasPlayerID interface, just accept a PlayerID directly. Then, rather than having a PlayerID object, just use the underlying implementation directly. If you don't mind changing all the places where you construct a terminal (to change from passing in a player to passing in a player ID) and don't plan on changing the way player ID is defined, then the interface and intermediate data type are unnecessary abstractions. \$\endgroup\$
    – Tim C
    Commented May 22 at 18:51
  • \$\begingroup\$ I would agree, I think interfaces might be what I need in some other places where I have too much coupling, but a String playerId is the best solution here (which is what I've done now). \$\endgroup\$
    – Marvin
    Commented May 23 at 21:18

You must log in to answer this question.

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