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.
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.
Based on the first rule, we'll start by defining a new Control
class for our networked physics objects:
PhysicsObjectControl
that extends AbstractControl
.serverControlled
and an integer field called id
.We now define a network message to handle updates to objects with physics:
PhysicsObjectMessage
and have it extend AbstractMessage
.objectId
Vector3f
field called translation
Quaternion
field called rotation
@Serializable
annotation, and add it to the list of messages in the GameUtil
class!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:
AppState
class called ServerPhysicsAppState
. This AppState
class will contain the reference to the BulletAppState
class, and it will handle the initialization.initialize
method, it should add the loaded level to physicsSpace
as follows:bulletAppState.getPhysicsSpace().add(server.getLevelNode().getChild("terrain-TestScene").getControl(PhysicsControl.class));
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);
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();
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());
We'll revisit the server code in a bit, but first let's look at what is needed for the client to receive messages.
BulletAppState
set up.Game
class as on the server.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();
objectId
in the message as follows:int objectId = physicsMessage.getObjectId(); Spatial s = physicsObjects.get(objectId);
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()); }
ServerPlayerControl
, we add a BetterCharacterControl
field called physicsCharacter
and a Boolean field called usePhysics
.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); }
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)); }
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());
BetterCharacterControl
from physicsSpace
as it joins and leaves the game.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 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.
3.149.27.72