Making a networked game – Battleships

In the previous recipes, we looked at how to set up a server, and connect and handle basic messaging. In this recipe, we'll reinforce this knowledge and expand it by adding server verification and applying it to a real game.

A turn-based board game is perhaps not what you would normally develop using a 3D game SDK, but it's a very good game to learn networking. The Battleships game is a good example not only because the rules are simple and known to many but also because it has a hidden element, which will help us understand the concept of server verification.

Note

If you're unfamiliar with the Battleships game, visit http://www.wikipedia.org/wiki/Batt.

Since we're mainly interested in the networking aspects of the game, we'll skip some of the verification normally needed such as looking for overlapping ships. We also won't write any graphical interface and use the command prompt to obtain input. Again, to focus on the networking API, some of the plain Java logic for game rules won't be explained.

The game will have a client and server class. Each class will have a MessageListener implementation and share messages and game objects.

Getting ready

It is highly recommended to familiarize yourself with the content of the previous recipes in the chapter, if you haven't already.

The amount of messages will increase greatly compared to the previous recipes. Since both the server and client need to keep a track of the same messages and they need to be registered in the same order, we can create a GameUtil class. It has a static method called initialize(). For every new message type we create, we add a line like this:

Serializer.registerClass(WelcomeMessage.class);

The game revolves around a couple of objects that we'll define before getting into the networking aspect.

We need a Ship class. For this implementation, it only needs the name and segments fields. We add methods so that once a tile containing Ship is hit, we can decrease the segments. When segments reach zero, it's sunk. Likewise, Player can be a simple class, with only an ID necessary for identification with the server, and the number of ships still alive. If the number of ships reaches zero, the player loses.

Many of the message types extend a class called GameMessage. This class in turn extends AbstractMessage and needs to contain the ID of the game, and state that the message should be reliable, thus using the TCP protocol.

How to do it...

We start by setting up a Game class. This will consist of the following six steps:

  1. First of all, the Game class needs an ID. This is used by the server to keep track of which game messages to relate to (since it supports many games at the same time), and will also be used as a reference for other things.
  2. The Game class needs the two Player objects, player1 and player2, as well as the ID of the player whose turn it currently is. We can call that currentPlayerId.
  3. The Game class needs two boards; one for each player. The boards will be made of 2D Ship arrays. Each tile where there is a segment of a ship has a reference to the Ship object; the others are null.
  4. An integer status field lets us know what state the game currently is in, which is useful for message filtering. We can also add constants for the different statuses and set a default status, as follows:
    public final static int GAME_WAITING = 0;
    public final static int GAME_STARTED = 1;
    public final static int GAME_ENDED = 2;
    private int status = GAME_WAITING;
  5. Now, we add a placeShip method. The method in this implementation is simplified and only contains verification that the ship is inside the board, as follows:
    public void placeShip(int playerId, int shipId, int x, int y, boolean horizontal){
      Ship s = GameUtil.getShip(shipId);
      Ship[][] board;
      if(playerId == playerOne.getId()){
        board = boardOne;
        playerOne.increaseShips();
      } else {
        board = boardTwo;
        playerTwo.increaseShips();
      }
      for(int i = 0;i < s.getSegments(); i++){
        [verify segment is inside board bounds]
      }
    }
  6. The other method that does some work in the Game class is applyMove. This takes FireActionMessage as input, checking the supplied tile to see whether there is a ship in that spot. It then checks whether the supposed ship is sunk, and whether the player has any ships left. If a ship is hit, it returns the Ship object to the calling method, as follows:
    public Ship applyMove(FireActionMessage action){
      int x = action.getX();
      int y = action.getY();
      Ship ship = null;
      if(action.getPlayerId() == playerOne.getId()){
        ship = boardTwo[x][y];
        if(ship != null){
          ship.hit();
          if(ship.isSunk()){
            playerTwo.decreaseShips();
          }
        }
      } else {
          [replicate for playerTwo]
    }
      if(playerTwo.getShips() < 1 || playerOne.getShips() < 1){
        status = GAME_ENDED;
      }
      if(action.getPlayerId() == playerTwo.getId()){
        turn++;
      }
      return ship;
    }

Now, let's have a look at the server side of things. In the previous chapters, we had a look at connecting the clients, but a full game requires a bit more communication to set things up as we will see. This section will have the following eight steps:

  1. Since the server is meant to handle several instances of a game at once, we'll define a couple of HashMaps to keep a track of the game objects. For each game we create, we put the Game object in the games map with the ID as a key:
    private HashMap<Integer, Game> games = new HashMap<Integer, Game>();
  2. We'll also use Filters to only send messages to players in a related game. To do this, we store a list of HostedConnections, with each being an address to a client, with the game ID as a key:
    private HashMap<Integer, List<HostedConnection>> connectionFilters = new HashMap<Integer, List<HostedConnection>>();
  3. Since we're continuously giving out a new player ID and increasing the value of the game ID, we'll have two fields for that as well: nextGameId and nextPlayerId.
  4. Everything starts with a connecting client. Like in the Setting up a server and client recipe, we use ConnectionListener to handle this. The method either adds the player to an existing game, or creates a new one if none are available. Regardless of whether a new game is created or not, the addPlayer method is called afterwards, as shown in the following code snippet:
    public void connectionAdded(Server server, HostedConnection conn) {
      Game game = null;
      if(games.isEmpty() || games.get(nextGameId - 1).getPlayerTwo() != null){
        game = createGame();
      } else {
        game = games.get(nextGameId - 1);
      }
      addPlayer(game, conn);
    }
  5. The createGame method creates a new game object and sets the correct ID. After placing it in the games map, it creates a new List<HostedConnection> called connsForGame and adds it to the connectionFilters map. The connsForGame list is empty for now, but will be populated as players connect:
    private Game createGame(){
      Game game = new Game();
      game.setId(nextGameId++);
      games.put(game.getId(), game);
      List<HostedConnection> connsForGame = new ArrayList<HostedConnection>();
      connectionFilters.put(game.getId(), connsForGame);
      return game;
    }
  6. The first thing the addPlayer method does is create a new Player object and then set the ID of it. We use WelcomeMessage to send the ID back to the player:
    private void addPlayer(Game game, HostedConnection conn){
      Player player = new Player();
      player.setId(nextPlayerId++);
  7. The server broadcasts this message using the client's connection as a filter, ensuring it's the only recipient of the message, as follows:
      WelcomeMessage welcomeMessage = new WelcomeMessage();
      welcomeMessage.setMyPlayerId(player.getId());
      server.broadcast(Filters.in(conn), welcomeMessage);
  8. It then decides whether the player is the first or second to connect to the game, and adds the player's HostedConnection instance to the list of connections associated with this game, as shown in the following code snippet:
      if(game.getPlayerOne() == null){
        game.setPlayerOne(player);
      } else {
        game.setPlayerTwo(player);
      }
    List<HostedConnection> connsForGame = connectionFilters.get(game.getId());
    connsForGame.add(conn);
  9. It then creates a GameStatusMessage object, letting all players in the game know the current status (which is WAITING) and any player information it might have, as shown in the following code snippet:
      GameStatusMessage waitMessage = new GameStatusMessage();
      waitMessage.setGameId(game.getId());
      waitMessage.setGameStatus(Game.GAME_WAITING);
      waitMessage.setPlayerOneId(game.getPlayerOne() != null ? game.getPlayerOne().getId() : 0);
      waitMessage.setPlayerTwoId(game.getPlayerTwo() != null ? game.getPlayerTwo().getId() : 0);
      server.broadcast(Filters.in(connsForGame), waitMessage);
    }

We're going to take a look at message handling on the client side and see how its MessageListener interface will handle incoming WelcomeMessages and game updates:

  1. We create a class called ClientMessageHandler, which implements MessageListener. First, we will walk through the part handling the start of a game.
  2. The thisPlayer object has already been instanced in the client, so all we need to do when receiving WelcomeMessage is set the player's ID. Additionally, we can display something to the player letting it know the connection is set up:
    public void messageReceived(Client source, Message m) {
      if(m instanceof WelcomeMessage){
        WelcomeMessage welcomeMess = ((WelcomeMessage)m);
        Player p = gameClient.getThisPlayer();
        p.setId(welcomeMessage.getMyPlayerId());
    }
  3. When a GameStatusMessage is received, we need to accomplish three things. First, set the ID of the game. Knowing the ID of the game is not necessary for the client in this implementation, but can be useful for communication with the server:
    else if(m instanceof GameStatusMessage){
      int status = ((GameStatusMessage)m).getGameStatus();
      switch(status){
      case Game.GAME_WAITING:
        if(game.getId() == 0 && ((GameStatusMessage)m).getGameId() > 0){
             game.setId(((GameStatusMessage)m).getGameId());
          }
  4. Then, we set the playerOne and playerTwo fields by simply checking whether they have been set before or not. We also need to identify the player by comparing the IDs of the players in the message with the ID associated with this client. Once found, we let him or her start placing ships, as follows:
    if(game.getPlayerOne() == null && ((GameStatusMessage)m).getPlayerOneId() > 0){
      int playerOneId = ((GameStatusMessage)m).getPlayerOneId();
      if(gameClient.getThisPlayer().getId() == playerOneId){
        game.setPlayerOne(gameClient.getThisPlayer());
        gameClient.placeShips();
           } else {
             Player otherPlayer = new Player();
                 otherPlayer.setId(playerOneId);
        game.setPlayerOne(otherPlayer);
      }
    
    }
    game.setStatus(status);
  5. When TurnMessage is received, we should extract activePlayer from it and set it on the game. If activePlayer is the same as thisPlayer of gameClient, set myTurn to true on gameClient.
  6. The last message to be handled by the class is the FiringResult message. This calls applyMove on the game object. Some kind of output should be tied to this message telling the player what happened. This example game uses System.out.println to convey this.
  7. Finally, initialize our ClientMessageHandler object in the constructor of the client class, as follows:
    ClientMessageHandler messageHandler = new ClientMessageHandler(this, game);
    client.addMessageListener(messageHandler);

With the received messages handled, we can look at the logic on the client side and the messages it sends. This is very limited as most of the game functionality is handled by the server.

The following steps show how to implement the client-side game logic:

  1. The placeShip method can be written in many different ways. Normally, you will have a graphical interface. For this recipe though, we use a command prompt, which breaks down the input to x and y coordinates and whether the ship is placed horizontally or vertically. At the end, it should send five instances of PlaceShipMessages to the server. For each added ship, we also call thisPlayer.increaseShips().
  2. We also need a method called setMyTurn. This uses the command prompt to receive x and y coordinates to shoot at. After this, it populates FireActionMessage, which is sent to the server.
  3. For PlaceShipMessage, create a new class and have it extend GameMessage.
  4. The class needs to contain the ID of the player placing the ship, coordinates, and orientation of the ship. The ID of the ship refers to the position in the following array:
    private static Ship[] ships = new Ship[]{new Ship("PatrolBoat", 2), new Ship("Destroyer", 3), new Ship("Submarine", 3), new Ship("Battleship", 4), new Ship("Carrier", 5)};
  5. We create another class called FireActionMessage, which also extends GameMessage.
  6. This has a reference to the player firing and an x and y coordinate.

Message handling on the server is similar to the one on the client. We have a ServerMessageHandler class implementing the MessageListener interface. This has to handle receiving messages from the player placing ships, and firing.

  1. Inside the messageReceived method, catch all PlaceShipMessages. Using the supplied gameId, we get the game instance from the server's getGame method and call the placeShip method. Once this is done, we check to see whether both players have placed all their ships. If that is the case, it's time to start the game:
    public void messageReceived(HostedConnection conn, Message m) {
      if (m instanceof PlaceShipMessage){
        PlaceShipMessage shipMessage = (PlaceShipMessage) m;
        int gameId = shipMessage.getGameId();
        Game game = gameServer.getGame(gameId);
        game.placeShip( … );
        if(game.getPlayerOne().getShips() == 5 && game.getPlayerTwo() != null&& game.getPlayerTwo().getShips() == 5){
          gameServer.startGame(gameId);
        }
  2. In the startGame method, the first thing we need to do is send a message to let the players know the game is now started. We know what clients to send the message to by getting the list of connections from the connectionFilters map as follows:
    public Game startGame(int gameId){
      Game game = games.get(gameId);
      List<HostedConnection> connsForGame = connectionFilters.get(gameId);
      GameStatusMessage startMessage = new GameStatusMessage();
      startMessage.setGameId(game.getId());
      startMessage.setGameStatus(Game.GAME_STARTED);
      server.broadcast(Filters.in(connsForGame), startMessage);
  3. After this, we decide which player will have the first move and send TurnMessage to the players, as follows:
      int startingPlayer = FastMath.nextRandomInt(1, 2);
      TurnMessage turnMessage = new TurnMessage();
    
      server.broadcast(Filters.in(connsForGame), turnMessage);
      return game;
    }
  4. Now, we need to define TurnMessage. It is another simple message, only containing the ID of the player whose turn it currently is and extending GameMessage.
  5. Back in ServerMessageListener, we make it ready to receive FireActionMessage from a player. We begin by verifying that the playerId of the incoming message matches with the current player on the server side. It can be implemented as follows:
    if(m instanceof FireActionMessage){
      FireActionMessage fireAction = (FireActionMessage) m;
      int gameId = fireAction.getGameId();
      Game game = gameServer.getGame(gameId);
      if(game.getCurrentPlayerId() == fireAction.getPlayerId()){
  6. Then, we call applyMove on the game, letting it decide whether it's a hit or not. If it's a hit, the ship will be returned. It can be implemented by typing the following code:
        Ship hitShip = game.applyMove(fireAction);
  7. We go on and create a FiringResult message. This is an extension of FireActionMessage with additional fields for the (possible) ship being hit. It should be broadcasted to both the players letting them know whether the action was a hit or not.
  8. Finally, we switch the active player and send another TurnMessage to both the players as follows:
                    TurnMessage turnMessage = new TurnMessage();
                    turnMessage.setGameId(game.getId());
                    game.setCurrentPlayerId(game.getCurrentPlayerId() == 1 ? 2 : 1);
                    turnMessage.setActivePlayer(game.getCurrentPlayerId());
                    gameServer.sendMessage(turnMessage);
                }
  9. This flow will continue until one of the players has run out of ships. Then, we should simply send GameStatusMessage with the END status to the players and disconnect them.

How it works...

When a player launches the client, it will automatically connect to the server defined in the properties file.

The server will acknowledge this, assign a user ID to the player, and send back WelcomeMessage containing the ID. The job of WelcomeMessage is to confirm the connection to the client, and let the client know its given ID. In this implementation, it is used for future communication from the client. Another way of filtering incoming messages would be possible using the HostedConnection instance, as it holds a unique address to the client.

When the first player connects, a new game will be created. The game is put in the WAITING status until two players have connected, and both have placed their ships. For each player connecting, it creates a GameStatusMessage letting all players in the game know the current status (which is WAITING) and any player information it might have. The first player, PlayerOne, will receive the message twice (again when PlayerTwo connects), but it doesn't matter as the game will be in the WAITING status until both players have placed their ships.

The placeShip method is simplified and doesn't contain all the verification that you will normally have in a full game. Make sure that the the server checks whether a ship is outside the board, or overlapping, and make sure it's of the right type, length, and so on and send a message back if it is wrong. This method simply checks that the ship is inside bounds and skips it if it isn't. Verification can also be done on the client, but to limit exploitation, it has to be done on the server as well.

The starting player will be selected randomly and sent in a TurnMessage to both players stating who begins. The player is asked to enter a set of coordinates to fire at and FireActionMessage is sent to the server.

The server verifies the player and applies it to the board. It then broadcasts a FireResult message to all players with information about the action, and whether any ships are hit. If the attacked player still has ships left, it becomes his or her turn to fire.

Once a player has run out of ships, the game ends. The server broadcasts a message to all the clients and disconnects them.

The clients have very little information about the other player. The benefit of this is that it makes cheating much more difficult.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.122.82