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.
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.
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.
We start by setting up a Game
class. This will consist of the following six steps:
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.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
.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.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;
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] } }
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:
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>();
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>>();
nextGameId
and nextPlayerId
.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); }
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; }
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++);
WelcomeMessage welcomeMessage = new WelcomeMessage(); welcomeMessage.setMyPlayerId(player.getId()); server.broadcast(Filters.in(conn), welcomeMessage);
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);
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:
ClientMessageHandler
, which implements MessageListener
. First, we will walk through the part handling the start of a game.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()); }
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()); }
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);
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
.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.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:
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()
.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.PlaceShipMessage
, create a new class and have it extend GameMessage
.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)};
FireActionMessage
, which also extends GameMessage
.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.
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); }
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);
TurnMessage
to the players, as follows:int startingPlayer = FastMath.nextRandomInt(1, 2); TurnMessage turnMessage = new TurnMessage(); server.broadcast(Filters.in(connsForGame), turnMessage); return game; }
TurnMessage
. It is another simple message, only containing the ID of the player whose turn it currently is and extending GameMessage
.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()){
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);
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.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); }
GameStatusMessage
with the END
status to the players and disconnect them.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.
18.119.122.82