Implementing a network code for FPS

Networked FPS games are a genre of games that never seem to lose popularity. In this recipe, we'll look at the basics to get a server and multiple clients up and running. We will emulate a server with a persistent environment, where players can connect and disconnect at any time.

We have the benefit of using some of the code generated in earlier chapters. The code we'll use requires some changes to be adapted to a networked game, but it will again show the benefit of using jMonkeyEngine's Control and AppState classes.

Getting ready

Good recipes to read up on before this are the previous recipes in this chapter (especially Making a networked game – Battleships, on which the architecture relies heavily) and also the Creating a reusable character control recipe from Chapter 2, Cameras and Game Controls, as we will use a similar pattern here for our NetworkedPlayerControl implementations. To avoid repetition, this recipe will not show or explain all of the regular gameplay code.

How to do it...

We begin by defining a few classes that will be used commonly across both server and client:

  1. First off, we define a class called NetworkedPlayerControl extending AbstractControl. We will use this both as an identifier for a player object and as a control for the spatial representation of the player.
  2. The class will be extended in further recipes, but for now it should keep track of an integer called ID.
  3. It also needs an abstract method called onMessageReceived, taking PlayerMessage as input. This is the method that our message handlers will call to apply changes. In ServerPlayerControl, the message will contain the actual input from the player, whereas ClientPlayerControl simply replicates what has happened on the server.
  4. Now, we define a class called Game, which will be shared by both the client and server.
  5. We add a HashMap object called players, where playerId is the key and NetworkedPlayerControl is the value. It keeps track of the players.

We will need a couple of new messages for this example. All messages are assumed to be in a bean pattern with getters and setters. We define the messages with the following steps:

  1. We create a base message to be used for player-related information and call it PlayerMessage, extending AbstractMessage. This only needs an integer called playerId.
  2. We create the first message that extends PlayerMessage. It is called PlayerActionMessage and handles player input. This should be set to be reliable as we don't want to ever miss a player's input.
  3. Since player input can either be a key press or mouse click, it needs to have both a Boolean value called pressed and a float value called floatValue.
  4. In addition, we also have to add a String value called action.
  5. We extend PlayerMessage in another class called PlayerUpdateMessage. This will be used to distribute player location information from the server to the clients. This should not be reliable to avoid unnecessary delays.
  6. It has a Vector3f field called position and a Quaternion field called lookDirection.

With the messages defined, let's see what the server code looks like:

  1. We define a new class called FPSServer, which extends SimpleApplication.
  2. It needs to keep track of the following fields. Apart from the Server field, it also keeps track of the next ID to give to a connecting player, a Game, and a Map of all the currently connected players, with their connection as the key:
    private Server server;
    private int nextPlayerId = 1;
    private Game game;
    private HashMap<HostedConnection, ServerPlayerControl> playerMap = new HashMap<HostedConnection, ServerPlayerControl>();
  3. Like in the previous recipe, we use a class called GameUtil to register all our message classes. We also set frameRate to 30 fps. This might be different depending on the game type. Finally, we start the application in the headless mode, to save resources as follows:
    public static void main(String[] args ) throws Exception{
      GameUtil.initialize();
      FPSServer gameServer = new FPSServer();
      AppSettings settings = new AppSettings(true);
      settings.setFrameRate(30);
      gameServer.setSettings(settings);
      gameServer.start(JmeContext.Type.Headless);
    }
  4. We initialize the server as in the Making a networked game ‑ Battleships recipe and create a ConnectionListener instance to look for connecting and disconnecting players. This will call addPlayer and removePlayer respectively, when players connect or disconnect.
  5. In the addPlayer method, we create a new ServerPlayerControl instance, which is the server-side implementation of NetworkedPlayerControl, and assign an ID to it for easier reference, as follows:
    private void addPlayer(Game game, HostedConnection conn){
      ServerPlayerControl player = new ServerPlayerControl();
      player.setId(nextPlayerId++);
      playerMap.put(conn, player);
      game.addPlayer(player);
  6. Then, we create a spatial for it so that it has a reference in the scene graph (and thus, it will be automatically updated). This is not only for visual representation, but we are dependent on it to update our method, as follows:
      Node s = new Node("");
      s.addControl(player);
      rootNode.attachChild(s);
  7. For any future communication with the server, the client will supply its playerId in all messages, so the server sends the assigned ID back to the client in WelcomeMessage. It broadcasts the message using the client's connection as a filter, as follows:
      WelcomeMessage welcomeMessage = new WelcomeMessage();
      welcomeMessage.setMyPlayerId(player.getId());
      server.broadcast(Filters.in(conn), welcomeMessage);
  8. Then, we send information about all the other players to the player that joins, as follows:
      Collection<NetworkedPlayerControl> players = game.getPlayers().values();
      for(NetworkedPlayerControl p: players){
        PlayerJoinMessage joinMessage = new PlayerJoinMessage();
        joinMessage.setPlayerId(p.getId());
        server.broadcast(Filters.in(conn), joinMessage);
      }
  9. Lastly, the server sends a message to all the other players about the new player, as follows:
      PlayerJoinMessage joinMessage = new PlayerJoinMessage();
      joinMessage.setPlayerId(player.getId());
      server.broadcast(joinMessage);
    }
  10. The removePlayer method works similarly, but it only has to send a message to each player currently connected about the disconnected player. It also uses PlayerJoinMessage but it sets the leaving Boolean to true to indicate the player is leaving, not joining the game.
  11. Then, the server will continuously send location and rotation (direction) updates to all players. Since we set fps to 30, it will try to do this every 33 ms as follows:
    public void simpleUpdate(float tpf) {
      super.simpleUpdate(tpf);
      Collection<NetworkedPlayerControl> players = game.getPlayers().values();
      for(NetworkedPlayerControl p: players){
        p.update(tpf);
        PlayerUpdateMessage updateMessage = new PlayerUpdateMessage();
        updateMessage.setPlayerId(p.getId());
    updateMessage.setLookDirection(p.getSpatial().getLocalRotation());
    updateMessage.setPosition(p.getSpatial().getLocalTranslation());
        updateMessage.setYaw(p.getYaw());
        server.broadcast(updateMessage);
      }
    }
  12. We also create a ServerMessageHandler class that implements MessageListener. It's a short class in this case, which will only listen to messages extending PlayerMessage and pass it on to the correct NetworkedPlayerControl class to update it. In this recipe, this will mean the input coming from the player, as follows:
    public void messageReceived(HostedConnection source, Message m) {
      if(m instanceof PlayerMessage){
        PlayerMessage message = (PlayerMessage)m;
        NetworkedPlayerControl p = game.getPlayer(message.getPlayerId());
        p.onMessageReceived(message);
      }
    }
  13. For the server-side implementation of the NetworkedPlayerControl class, we extend it to a new class called ServerPlayerControl.
  14. Similar to the GameCharacterControl class from Chapter 2, Cameras and Game Controls, we will use a set of Booleans to keep track of the input, as follows:
    boolean forward = false, backward = false, leftRotate = false, rightRotate = false, leftStrafe = false, rightStrafe = false;
  15. In the implemented onMessageReceived method, listen for PlayerMessages. We don't know if it will contain Boolean or float values, so we look for both, as follows:
    public void onMessageReceived(PlayerMessage message) {
      if(message instanceof PlayerActionMessage){
        String action = ((PlayerActionMessage) message).getAction();
        boolean value = ((PlayerActionMessage) message).isPressed();
        float floatValue = ((PlayerActionMessage) message).getFloatValue();
  16. Then, we apply the values as shown in the following code snippet:
    if (action.equals("StrafeLeft")) {
      leftStrafe = value;
    } else if (action.equals("StrafeRight")) {
      rightStrafe = value;
    }
    ...
    else if (action.equals("RotateLeft")) {
      rotate(floatValue);
    } else if (action.equals("RotateRight")) {
      rotate(-floatValue);
     }
  17. In the overridden controlUpdate method, we then modify the position and rotation of the spatial based on the input, just like we did in the Creating a reusable character control recipe of Chapter 2, Cameras and Game Controls.

The client is simple in many ways, since it basically only does two things. It takes a player's input, sends it to the server, receives updates from the server, and applies them as follows:

  1. We begin by creating a new class called FPSClient extending SimpleApplication.
  2. In the constructor, we read the network properties file and connect to the server, as follows:
    Properties prop = new Properties();   prop.load(getClass().getClassLoader().getResourceAsStream("network/resources/network.properties"));
            client = Network.connectToServer(prop.getProperty("server.name"), Integer.parseInt(prop.getProperty("server.version")), prop.getProperty("server.address"), Integer.parseInt(prop.getProperty("server.port")));
  3. Just as with the server, we register all the message classes before launching the application.
  4. The application should have a reference to a Node class called playerModel, which will be the visual representation of the players in the game. There should also be a ClientPlayerControl class called thisPlayer.
  5. In the simpleInitApp method, we attach InputAppState. This has the same functionality as the one in the Creating an input AppState object recipe of Chapter 2, Cameras and Game Controls. The only difference is it will benefit from having a direct way of reaching the client to send messages:
    public void simpleInitApp() {
      InputAppState inputAppState = new InputAppState();
      inputAppState.setClient(this);
      stateManager.attach(inputAppState);
  6. Next, we create playerGeometry to be used for all the players in this example, as follows:
      Material playerMaterial  = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
      playerGeometry = new Geometry("Player", new Box(1f,1f,1f));
      playerGeometry.setMaterial(playerMaterial);
  7. We also turn off the application's flyByCamera instance and create a new game object, which we will populate when we receive information from the server, as follows:
      getFlyByCamera().setEnabled(false);
      game = new Game();
  8. Lastly, we create a new ClientMessageListener object and add it to the client, as shown in the following code snippet:
    ClientMessageHandler messageHandler = new ClientMessageHandler(this, game);
    client.addMessageListener(messageHandler);
  9. In the createPlayer method, we create a new ClientPlayerControl instance and also a Node instance, which we attach to the scene graph, as follows:
    ClientPlayerControl player = new ClientPlayerControl();
    player.setId(id);
    final Node playerNode = new Node("Player Node");
            playerNode.attachChild(assetManager.loadModel("Models/Jaime/Jaime.j3o"));//
    playerNode.addControl(player);
  10. Since we don't know when this method will be called, we make sure that we attach the spatial in a thread-safe way. This can be implemented as follows:
    enqueue(new Callable(){
      public Object call() throws Exception {
        rootNode.attachChild(playerNode);
        return null;
      }
    });
  11. Finally, we return the created ClientPlayerControl instance to the calling method.
  12. We add a new method called setThisPlayer. This method will be called when the player's WelcomeMessage is received. Inside this, we create CameraNode, which will be attached to the player, as follows:
    public void setThisPlayer(ClientPlayerControl player){
      this.thisPlayer = player;
      CameraNode camNode = new CameraNode("CamNode", cam);
      camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera);
      ((Node)player.getSpatial()).attachChild(camNode);
    }
  13. We also have to override the destroy method to make sure we close the connection to the server when the client is shutdown. This can be implemented as follows:
    public void destroy() {
      super.destroy();
      client.close();
    }
  14. Now, we need to create the client representation of NetworkedPlayerControl and extend it in a class called ClientPlayerControl.
  15. It has a Vector3f field called tempLocation and a Quaternion field called tempRotation. These are used to hold received updates from the server. It can also have a float field called yaw for head movement.
  16. In the onMessageReceived method, we only look for PlayerUpdateMessages and set tempLocation and tempRotation with the values received in the message, as follows:
    public void onMessageReceived(PlayerMessage message) {
      if(message instanceof PlayerUpdateMessage){
        PlayerUpdateMessage updateMessage = (PlayerUpdateMessage) message;
      tempRotation.set(updateMessage.getLookDirection());
      tempLocation.set(updateMessage.getPosition());
    tempYaw = updateMessage.getYaw();
      }
    }
  17. We will then apply the temp variable values in the controlUpdate method:
    spatial.setLocalTranslation(tempLocation);
    spatial.setLocalRotation(tempRotation);
    yaw = tempYaw;

Just like on the server side, we need a message handler listening for incoming messages. To do this, perform the following steps:

  1. We create a new class called ClientMessageHandler, which implements MessageListener<Client>.
  2. The ClientMessageHandler class should have a reference to FPSClient in a field called gameClient and Game itself in another field called game.
  3. In the messageReceived method, we need to handle a number of messages. The WelcomeMessage is most likely to arrive first. When this happens, we create a player object and spatial and assign it to be this client's player, as follows:
    public void messageReceived(Client source, Message m) {
      if(m instanceof WelcomeMessage){
        ClientPlayerControl p = gameClient.createPlayer(((WelcomeMessage)m).getMyPlayerId());
        gameClient.setThisPlayer(p);
        game.addPlayer(gameClient.getThisPlayer());
  4. The PlayerJoinMessage is received both when player joins and leaves a game. What sets it apart is the leaving Boolean. We call both the game and gameClient methods based on whether the player is joining or leaving, as shown in the following code snippet:
    PlayerJoinMessage joinMessage = (PlayerJoinMessage) m;
    int playerId = joinMessage.getPlayerId();
    if(joinMessage.isLeaving()){
       gameClient.removePlayer((ClientPlayerControl)   game.getPlayer(playerId));
      game.removePlayer(playerId);
    } else if(game.getPlayer(playerId) == null){
      ClientPlayerControl p = gameClient.createPlayer(joinMessage.getPlayerId());
      game.addPlayer(p);
    }
  5. When the PlayerUpdateMessage is received, we first find the corresponding ClientPlayerControl class and pass on the message to it, as follows:
      } else if (m instanceof PlayerUpdateMessage){
        PlayerUpdateMessage updateMessage = (PlayerUpdateMessage) m;
        int playerId = updateMessage.getPlayerId();
        ClientPlayerControl p = (ClientPlayerControl) game.getPlayer(playerId);
        if(p != null){
          p.onMessageReceived(updateMessage);
        }

How it works...

The server is running in the headless mode, which means it won't do any rendering and there will be no graphical output, but we still have access to the full jMonkeyEngine application. In this recipe, one server instance will only have one game active at a time.

We instantiate all network messages inside a class called GameUtil, since they have to be the same (and serialized in the same order) on the client and server.

The client will try to connect to the server as soon as it launches. Once connected, it will receive playerId from the server via WelcomeMessage, as well as PlayerJoinMessages for all other players that are already connected. Likewise, all other players will receive PlayerJoinMessage with the new player's ID.

The client sends any actions the players perform to the server using PlayerActionMessage, which applies them to its instance of the game. The server, which runs at 30 fps, will send positions and directions of each player to all the other players, using PlayerUpdateMessages.

The InputAppState class on the client is very similar to the one in Chapter 2, Cameras and Game Controls. The only difference is that instead of directly updating a Control instance, it creates a message and sends it to the server. In the onAction class, we set the Boolean value of the message, whereas in onAnalog (to look and rotate), floatValue will be used instead, as shown in the following code snippet:

public void onAction(String name, boolean isPressed, float tpf) {
  InputMapping input = InputMapping.valueOf(name);
  PlayerActionMessage action = new PlayerActionMessage();
  action.setAction(name);
  action.setPressed(isPressed);
  action.setPlayerId(client.getThisPlayer().getId());
  client.send(action);
}

In the event of a player leaving the game, PlayerJoinMessages will be sent to the other players, with leaving set to true.

The NetworkedPlayerControl class is an abstract class, and doesn't do much on its own. You might recognize the implementation of ServerPlayerControl from GameCharacterControl, and they function similarly, but rather than receiving the input directly from the user, ServerPlayerControl gets it via a networked message instead.

Both the client and server implementation of NetworkedPlayerControl use the tempRotation and tempLocation fields to which they apply any incoming changes. This is so we don't modify the actual spatial transforms outside the main loop.

We shouldn't be fooled by the relative simplicity of this recipe. It merely shows the basics of a real-time networked environment. Making a full game creates much more complexity.

See also

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

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