3

I am working on my final year project and making a multi player game. It's a tank battle game.

In the client basically we will have 2 types of Tank. One will be controllable by the keyboard and other will be the one's which are enemy tank's and not controllable. Enemy tank's position will change depending on when it gets data from the server about change in position.

Every tank will have a Turret which will be used to fire Bullets and missile. It will also store ammunition details. Now for the user's tank we will need turret which can be rotated by the keyboard independently as well as it will move when the tank moves. Enemy tank's turret can't be controlled by us obviously and will only rotate and move when we get update from the server to do so.

Now this brings me to a lot of confusion about how to go on designing this. My current model design is as follows.

UML

I have done a UML Diagram after a long time so there may be some errors.

One of the problem I am having is with the turret. A turret on enemy tank should effectively now have all those firePrimary(), fireSecondary() methods as we will never invoke them. If an opponent fires a bullet server will send a message and I will just spawn a missile.

A turret on user tank should have all these methods as we should be able to fire bullets from it. Now my confusion is if I implement shootable on Turret base class then I will get those methods like firePrimary() and fireSecondary() on EnemyTank which will be useless. If I rather than implementing the Shootable interface compose the UserTurret with Shootable then it will break my architecture of having an abstract class Turret in Tank and passing in either UserTurret or EnemyTurret.

BasicShootable and HeavyShootable implementations are mainly for spawning different kinds of bullets.

If this is not correct what can be the best way to do it so that I don't have useless methods in various classes and I can expand the game entities further if I want adding more behaviour to my tanks or turrets on the fly.

Note: this is a follow-on to my previous question: Should I use strategy design pattern or something else?

Sneh
  • 157
  • 6

1 Answers1

5

This is a follow-on to your previous question, and this is a case for using a strategy. Your tanks should all expose the same functionality, but use a different driver or input. Player tanks would use user input, while enemy tanks would use AI.

Turrets can have different capabilities as well, and I believe your design captures this correctly. You have a common interface or parent class, with the player subclass offering the extra complexity of having additional child components.

There are several ways to implement this, but the general idea looks something like this:

class TankDriver {
  abstract void move(Tank t);
}

class PlayerTankDriver {
  ...
}

class AiTankDriver {
  ...
}

class Tank {
  TankDriver driver;

  Tank(TankDriver d) {
    driver = d;
  }

  void move() {
    d.move(this);
  }
}

The general use case is you specify the driver strategy when you construct a tank, then keep a reference around for the tank. When it is the tank's turn to move, you tell it to move: it delegates to its strategy, which is implementation-defined. It is flexible because it can further query the user or use any AI you want: one enemy tank could be very aggressive, another could be more risk-adverse. One might use a lookahead algorithm with a very deep tree, another might just look ahead one move (i.e. the difficulty is different).

The point is, you still only need a single tank class because the pluggable logic is in in other classes that have a laser focus on one aspect of what it means to be a tank.

Finally, there is the issue that the turret on a player tank may have different methods exposed than on an AI-controlled turret. This can get into a weird three-way dependency that is a bit tightly coupled, and there are multiple ways to address it.

This is a tough nut to crack in a way that keeps your design reasonable while also satisfying your professor's goal of having a "proper" OO design.

I think the best way to accomplish this in a true OO fashion is to keep the design you have with the turret belonging to the tank, and to expose a subset of turret functionality on the tank itself which it then forwards to the actual turret. This is not how I would design it in a real-world scenario most likely, but it has advantages:

  • It satisfies the Law of Demeter which basically states you have driver → tank → turret, and driver should not "reach through" tank to get at turret.

  • It avoids a messy triangle issue where the driver, tank and turret have a nonlinear dependency which can complicate construction, usage, and referential integrity. For example, you might have a driver have a tank and turret, and the tank has a different turret than the driver. This would be a bug.

  • It avoids or simplifies generics/templates which may be employed to allow the tank to tell its driver what type of turret to expect, given that tank is a general-purpose class.

But wait, now the tank has to know the turret's interface and expose only those methods that make sense! That goes against the point of using composition like this!

I recommend exposing all turret methods and have nonsensical ones simply do nothing. Again, not quite ideal, but it will make your professor happy because of the Law of Demeter.


Pseudocode for forwarding turret requests through the tank:

interface Turret {
  void firePrimary();
  void fireSecondary();
  // and the other methods in your diagram.
}

class Tank {
  Turret turret;

  Tank(Turret t) {
    turret= t;
  }

  void firePrimary() {
    t.firePrimary();
  }

  void fireSecondary() {
    t.fireSecondary();
  }
}

You could have Tank implement the Turret interface, that is up to you. It can be convenient, but can potentially allow Tank to be misused:

  • Tank has-a turret, so it makes sense from an OO perspective that you can tell a tank to do "turret stuff" and it will pass those commands through.

  • One could misuse tank by adding another tank as its turret: tank → tank → turret, which is nonsensical. Having compile-time protection against this misuse by using the static type system is a good thing.

There are arguments for and against tank implementing turret, or having the tank return its turret for the driver to use. What you will learn is there is no one correct way to design and implement this: there are only tradeoffs. On one end of the spectrum you have "pure" OO which is what your professor is likely trying to teach you. On the other end of the spectrum are you "mostly" OO designs that have little fudges here and there to make life easier. This is a bit of a tangent, but I bring this up because this is an assignment and you will see real-world projects design more toward the "practical" end of the spectrum not the "theoretical" end.


I wrote up an answer to a different question which you might find helpful. The AI and user input approaches will necessarily require different inputs, making the example above a bit more complex (it is still a good start). If you look at the question/answer I linked you might learn a bit more about this approach.

  • 1
    Thank you for the long answer, I love you. All this makes sense now. By the way since it is a final year project I can do whatever is best for me as the project will stretch for 8 months so using strict OO is not important. My professor wants me to use the architecture and pattern which is best suited rather than which follows OOP. – Sneh Oct 13 '15 at 19:24
  • Just came back to thank you again. I can make much more sense of your solution now and see how much that earlier design decision is helping me in making changes. – Sneh Jan 01 '16 at 03:35