Networked physics

This recipe will go into something of a final frontier in game development. The topic is extremely application-dependent, and it is difficult to get right. Hopefully, after going through this recipe, you will have a basic framework in place that can be adapted to specific projects.

Getting ready

This recipe is for those who have a fundamental understanding of both Chapter 7, Networking with SpiderMonkey, and Chapter 8, Physics with Bullet. This recipe will describe how to implement networked physics in the networked fps that was discussed previously in the book. Since this is built on top of the existing framework, an AppState pattern has been chosen to isolate as much of the physics code as possible. There will be some overlapping, though.

Physics can be expensive as it is and has its own problems and requirements. Sending translations and rotations for objects over the network with every tick will seriously affect the bandwidth load. The ground rule is this: send only what you must.

Divide physics objects into those that you're interested in sharing and those that you don't. In most games, this means separating those that affect the gameplay and those that don't.

For example, a meter-sized crate that can be climbed upon will definitely affect the gameplay. It has to be networked.

A bucket that can be kicked or small debris from an explosion do not affect the gameplay and should only have local physics. It doesn't matter if they show up in different places for different players.

The second part of the rule is this: send only when you must. There's no point in sending an update for an object that is not moving.

How to do it...

Based on the first rule, we'll start by defining a new Control class for our networked physics objects:

  1. We create a new class called PhysicsObjectControl that extends AbstractControl.
  2. It should have two fields: a Boolean field called serverControlled and an integer field called id.

We now define a network message to handle updates to objects with physics:

  1. Let's call it PhysicsObjectMessage and have it extend AbstractMessage.
  2. There are three mandatory fields for it; they are as follows:
    • The first is an integer field called objectId
    • It also needs a Vector3f field called translation
    • Finally, we add a Quaternion field called rotation
  3. Don't forget to add the @Serializable annotation, and add it to the list of messages in the GameUtil class!
  4. The last common implementation we do is for the Game class where we add a list of Spatials called physicsObjects; the following code tells us how to do this:
    private Map<Integer, Spatial> physicsObjects = new HashMap<Integer, Spatial>();

Now, we can dig into the server-side implementation by performing the following steps:

  1. We contain most of the code in a new AppState class called ServerPhysicsAppState. This AppState class will contain the reference to the BulletAppState class, and it will handle the initialization.
  2. Inside its initialize method, it should add the loaded level to physicsSpace as follows:
    bulletAppState.getPhysicsSpace().add(server.getLevelNode().getChild("terrain-TestScene").getControl(PhysicsControl.class));
  3. A strategy is needed to collect all the objects that should be affected by server physics and assign them to PhysicsObjectControl (unless this has been done in SceneComposer already). Objects that should have server physics should also have serverControlled set to true and a unique ID, which is known by both the client and the server. The resulting spatials should be stored in the physicsObject class map, as follows:
    bigBox.addControl(new PhysicsObjectControl(uniqueId));
    bigBox.getControl(PhysicsObjectControl.class).setServerControllled(true);
    physicsObjects.put(uniqueId, bigBox);
  4. In the update method of ServerPhysicsAppState, we parse through the values of the physicsObject map. If any of the item in physicsObjects has PhysicsObjectControl that isServerControlled() and their isActive() is true, a new PhysicsObjectMessage should be created as follows:
    PhysicsObjectMessage message = new PhysicsObjectMessage();
  5. It should have the ID of PhysicsObjectControl as objectId and physicsLocation and physicsRotation of RigidBodyControl; refer to the following code:
    message.setObjectId(physicsObject.getControl(PhysicsObjectControl.class).getId());
    message.setTranslation(physicsObject.getControl(RigidBodyControl.class).getPhysicsLocation());
    message.setRotation(physicsObject.getControl(RigidBodyControl.class).getPhysicsRotation());
  6. The message is then broadcasted to the clients.

We'll revisit the server code in a bit, but first let's look at what is needed for the client to receive messages.

  1. First of all, the client has to have BulletAppState set up.
  2. Next, it needs to have knowledge of the objects to be handled by the server physics. If the objects are gathered from the scene, a strategy is needed to make sure the IDs are the same, or they're read in the same order.
  3. They should then be stored in the Game class as on the server.
  4. The second thing is a change to ClientMessageHandler. If the message is an instance of PhysicsObjectMessage, it should get the physicsObject Map from the Game class as follows:
    Map<Integer, Spatial> physicsObjects = game.getPhysicsObjects();
  5. A spatial should then be selected based on the objectId in the message as follows:
    int objectId = physicsMessage.getObjectId();
    Spatial s = physicsObjects.get(objectId);
  6. The rotation and translation should be applied as physicsLocation and physicsRotation respectively on the spatial's RigidBodyControl:
    PhysicsObjectControl physicsControl = s.getControl(PhysicsObjectControl.class);
    if(physicsControl.getId() == objectId){
      s.getControl(RigidBodyControl.class).setPhysicsLocation(physicsMessage.getTranslation()); s.getControl(RigidBodyControl.class).setPhysicsRotation(physicsMessage.getRotation());
    }
  7. Now, the pipeline for transmitting physics updates from the server to the clients should work. If we run it, not much is happening. This is because the players in the implementation in Chapter 7, Networking with SpiderMonkey, weren't using physics. They were simply coded to stick to the surface of the terrain. We can change the player's representation to handle this.
  8. In ServerPlayerControl, we add a BetterCharacterControl field called physicsCharacter and a Boolean field called usePhysics.
  9. Next, we override the setSpatial method, and perform a check to see whether the spatial supplied has BetterCharacterControl. If it does, usePhysics should be set to true and the local physicsCharacter field should be set to spatial as follows:
    if(spatial.getControl(BetterCharacterControl.class) != null){
      usePhysics = true;
      physicsCharacter = spatial.getControl(BetterCharacterControl.class);
    }
  10. Finally, in the controlUpdate method, we check whether usePhysics is true. If it is, rather than updating the spatial like we normally do in the method, we should instead set walkDirection of physicsCharacter to the local one and set viewDirection to the forward vector of its rotation as follows:
    if(usePhysics){
    physicsCharacter.setWalkDirection(walkDirection.multLocal(50));
    physicsCharacter.setViewDirection(tempRotation.getRotationColumn(2));
    }
  11. In our server's main class, inside the addPlayer method, we should now add BetterCharacterControl to the player's spatial before we add ServerPlayerControl, as shown in the following code snippet:
    Node playerNode = new Node("Player");
    playerNode.addControl(new BetterCharacterControl(0.5f, 1.5f, 1f));
    playerNode.addControl(player);
    rootNode.attachChild(playerNode);
    stateManager.getState(ServerPhysicsAppState.class).addPlayer(player.getPhysicsCharacter());
  12. There also needs to be some logic to add and remove BetterCharacterControl from physicsSpace as it joins and leaves the game.

How it works...

The first thing we did in the recipe was to lay some ground work by defining a new control called PhysicsObjectControl to be applied to the objects that should be handled by bullet physics. This control can either be added at runtime; alternatively, if Scene Composer is used to lay out levels and scenes, it can be added to the objects beforehand. It's recommended that you define which ones should be handled by the server by setting serverControlled on the relevant objects before they're being added to the scenes. The ID should then be set in a deterministic way on both the client and the server when they parse the scene for the objects.

The architecture to handle the physics might very well look different in another implementation, but here, the AppState pattern was used so that it could be easily added as an extension to the existing framework from Chapter 7, Networking with SpiderMonkey. In this chapter, we didn't use any physics for the players but simply checked the height of the terrain to find out where the ground was. Hence, we added an optional BetterCharacterControl instance to the player—again, a change that would still make it compatible with the previous implementation. However, this was only added on the server side. For client-side physics, a similar change would have to be made there.

The server will check every update and see whether any of the objects with serverControlled enabled is active and will send any updates to the clients. Actually, you could leave out the physics all together on the client and simply update the spatial's rotation and translation, if you wanted. This would lower the requirements on the client's hardware, but this will only work if all of the physics are handled by the server of course.

There's more…

There is an opportunity here to introduce a third state on PhysicsObjectControl; a state in which the object is affected but not controlled by the server. This could be used for objects that are important in their initial state; however, once they've been moved, it's no longer important that all the clients have the same information, for example, a door that at some points get blown off its hinges. In this case, a new message type can be introduced that will apply an impulse or force to an object from the server side. Once the object has been activated, the client can take care of the calculations, lowering the network load.

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

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