How to render updates from the other client with client-server messaging?

My goal is to achieve multiplayer space with a spaceship for each client. I have a server that broadcasts to everybody else when a client connects or moves, but how can I synchronize the rendering? I’ve read code from the projects jmeplanet and monkeyzone. started with a 2-player scenario and a callback in a ClientListener implements ClientStateListener, MessageListener<Client>

[java] app.enqueue(new Callable<Void>() {
public Void call() throws Exception {
app.setPlayer2update(true);
System.out.println(“UPDATING 2nd SAUCER”);
return null;
}
});[/java]

The callback could then change the state of the client to also update the rendering of the player2 object when player2 moves in the ClientStateListener:
[java]
@Override
public void simpleUpdate(float tpf) {
int speed = 25 * 80000;
Vector3f camDir = cam.getDirection().clone().multLocal(speed * tpf);
Vector3f camUp = cam.getUp().clone().mult(speed * tpf);
Quaternion roll = new Quaternion();
camDir.y = 0;
ufoDirection.set(0, 0, 0);
ufoControl.setLinearVelocity(Vector3f.ZERO);
if (left) {
roll.fromAngleAxis(-FastMath.QUARTER_PI / 3, cam.getDirection());
ufoDirection.set(cam.getLeft().multLocal(speed * tpf));
}
if (right) {
roll.fromAngleAxis(FastMath.QUARTER_PI / 3, cam.getDirection());
ufoDirection.set(cam.getLeft()).multLocal(-speed * tpf);
}
if (up) {
roll.fromAngleAxis(0, cam.getDirection());
ufoDirection.addLocal(camUp);
}
if (down) {
roll.fromAngleAxis(0, cam.getDirection());
ufoDirection.addLocal(cam.getUp().multLocal(-speed * tpf));
}
if (forward) {
roll.fromAngleAxis(0, cam.getDirection());
ufoDirection.set(camDir);
}
if (backward) {
roll.fromAngleAxis(0, cam.getDirection());
ufoDirection.set(camDir.multLocal(-1f));
}
ufoControl.setPhysicsRotation(roll);
ufoControl.setLinearVelocity(ufoDirection);

	// Calculate detection results
	CollisionResults results = new CollisionResults();
	ufoNode.collideWith((BoundingBox) jumpgateSpatial.getWorldBound(),
			results);

	CollisionResults results2 = new CollisionResults();
	// ufoNode.collideWith((BoundingSphere) moon.getWorldBound(),
	// results2);

	// Use the results
	if (results.size() &gt; 0) {
		System.out.println("Number of Collisions between"
				+ ufoNode.getName() + " and " + jumpgateSpatial.getName()
				+ ": " + results.size());

		// how to react when a collision was detected
		CollisionResult closest = results.getClosestCollision();
		System.out.println("What was hit? "
				+ closest.getGeometry().getName());
		System.out
				.println("Where was it hit? " + closest.getContactPoint());
		System.out.println("Distance? " + closest.getDistance());
		ufoControl
				.setPhysicsLocation(jumpGateControl2.getPhysicsLocation());
		System.out.println("Warped");

	} else {
		// how to react when no collision occured
	}

	if (results2.size() &gt; 0) {
		System.out.println("Number of Collisions between"
				+ ufoNode.getName() + " and " + moon.getName() + ": "
				+ results2.size());

		// how to react when a collision was detected
		CollisionResult closest2 = results2.getClosestCollision();
		System.out.println("What was hit? "
				+ closest2.getGeometry().getName());
		System.out.println("Where was it hit? "
				+ closest2.getContactPoint());
		System.out.println("Distance? " + closest2.getDistance());

	}

	Message message = new ActionMessage(1, 2, false);
	System.out.println("sending message " + message.toString());
	myClient.send(message);
	System.out.println("sent message " + message.toString());
	if (player2update == true) {
		System.out.println("simpleUpdatePlayer2 player 2 "
				+ message.toString());
		simpleUpdatePlayer2(tpf);
	}

}

public void simpleUpdatePlayer2(float tpf) {
	System.out.println("simpleUpdatePlayer2 player 2");
	int speed = 25 * 80000;
	Vector3f camDir = cam.getDirection().clone().multLocal(speed * tpf);
	Vector3f camUp = cam.getUp().clone().mult(speed * tpf);
	Quaternion roll = new Quaternion();
	camDir.y = 0;
	ufoDirection2.set(0, 0, 0);
	ufoControl2.setLinearVelocity(Vector3f.ZERO);

}

[/java]

The code is executed but it seems out of sync. Should I look more closely in the monkeyzone code for how its done or is there an obvious way to know how and for how long time to run the player2update which is invoked by the callback? My server is

[java]
public class SpaceWorldServer extends SimpleApplication implements
ConnectionListener {
Server myServer = null;
static SpaceWorldServer app = null;
int counter = 0;
public static void main(String[] args) {
System.out.println(“main SpaceWorldServer”);
// SpaceWorldServer
app = new SpaceWorldServer();
app.start(JmeContext.Type.Headless);
}

public void simpleInitApp() {
	System.out.println("simpleInitApp");
	try {
		myServer = Network.createServer(6143);
		Serializer.registerClass(ActionMessage.class);
		myServer.addMessageListener(new ServerListener(app),
				ActionMessage.class);
		myServer.addConnectionListener(this);
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	myServer.start();
}

@Override
public void destroy() {
	// custom code
	myServer.close();
	super.destroy();
}

public void updatePlayers(Message message, HostedConnection c) {
	System.out.println("server broadcasting message " + message.toString());
	myServer.broadcast(Filters.notEqualTo(c), message);
	System.out.println("broadcast message " + message.toString());
}

public void connectionAdded(Server s, HostedConnection c) {
	System.out.println("connectionAdded:" + c.toString());
	Message message = new ActionMessage(100, 200, true);
	System.out.println("broadcasting message " + message.toString());
	myServer.broadcast(Filters.notEqualTo(c), message);
	System.out.println("broadcast message " + message.toString());
}

public void connectionRemoved(Server s, HostedConnection c) {
}


@Override
public void simpleUpdate(float tpf) {
	if (counter != myServer.getConnections().size()) {
		System.out.println("#connections:"
				+ myServer.getConnections().size());
		counter = myServer.getConnections().size();
	}
}

}
[/java]
[java]

public class ServerListener implements MessageListener<HostedConnection> {

private SpaceWorldServer sws = null;

public ServerListener(SpaceWorldServer sws) {
	this.sws = sws;
}

public void messageReceived(HostedConnection source, Message message) {
	if (message instanceof ActionMessage) {
		// do something with the message
		ActionMessage helloMessage = (ActionMessage) message;
		System.out.println("Server received '" + helloMessage.toString()
				+ " action " + helloMessage.getAction() + "' from client #"
				+ source.getId());
		if (helloMessage.getAction() != 200) {
			System.out.println("getAction() == 200");
			sws.updatePlayers(helloMessage, source);
			System.out.println("updatedPlayers");
		}
	} // else....

}

}
[/java]

It works to actually broadcast the message about one client to all the other clients via the central server, but then the rendering I don’t know how to properly render the 2nd player.

[java] public void addSaucer() {

	ufoNode = (Node) assetManager.loadModel("usaucer_v01.j3o");
	ufoNode.setLocalScale(200f);
	BoundingBox bv = (BoundingBox) ufoNode.getWorldBound();
	CylinderCollisionShape shape = new CylinderCollisionShape(new Vector3f(
			bv.getXExtent(), bv.getYExtent(), bv.getZExtent()), 1);
	ufoControl = new RigidBodyControl(shape, 500f);
	ufoNode.addControl(ufoControl);
	ufoControl.setPhysicsLocation(new Vector3f(23041f, -40770f, 940936f));

	BulletAppState bas = app.getStateManager().getState(
			BulletAppState.class);

	bas.getPhysicsSpace().addAll(ufoNode);

	rootNode.attachChild(ufoNode);

}

RigidBodyControl ufoControl2 = null;

public void addNewSaucer() {

	Node ufoNode2 = (Node) assetManager.loadModel("usaucer_v01.j3o");
	ufoNode2.setLocalScale(200f);
	BoundingBox bv = (BoundingBox) ufoNode2.getWorldBound();
	CylinderCollisionShape shape = new CylinderCollisionShape(new Vector3f(
			bv.getXExtent(), bv.getYExtent(), bv.getZExtent()), 1);
	ufoControl2 = new RigidBodyControl(shape, 500f);
	ufoNode2.addControl(ufoControl2);
	ufoControl2.setPhysicsLocation(new Vector3f(10800f, -599f, 785000f));
	ufoControl2.setGravity(Vector3f.ZERO);
	ufoControl2.setLinearVelocity(Vector3f.ZERO);
	ufoControl2.clearForces();

	BulletAppState bas = app.getStateManager().getState(
			BulletAppState.class);

	bas.getPhysicsSpace().addAll(ufoNode2);
	rootNode.attachChild(ufoNode2);

}[/java]

When I start a client, it trigger events in all the other clients, but I wonder how I can achieve updating the objects that are populated?

[java]
public class ClientListener implements ClientStateListener,
MessageListener<Client> {

private UFOSpaceWorld app;
private Client client;

public ClientListener(UFOSpaceWorld app, Client client) {
	this.app = app;
	this.client = client;
	client.addClientStateListener(this);
	client.addMessageListener(this, ActionMessage.class);
}

public void messageReceived(Client source, Message message) {
	if (message instanceof ActionMessage) {
		ActionMessage helloMessage = (ActionMessage) message;
		if (helloMessage.getAction() == 200) {
			System.out.println(" NEW OTOs AND SAUCERS");
			app.enqueue(new Callable&lt;Void&gt;() {
				public Void call() throws Exception {
					app.createNewOtoBot();
					app.addNewSaucer();
					System.out.println("CREATED NEW OTOs AND SAUCERS");
					return null;
				}
			});
		} else {
			System.out.println("getAction #" + helloMessage.getAction());
			System.out.println("UPDATING SAUCER: '" + helloMessage.toString());
			
			app.enqueue(new Callable&lt;Void&gt;() {
				public Void call() throws Exception {
					app.setPlayer2update(true);
					System.out.println("UPDATING 2nd SAUCER");
					return null;
				}
			});
			
		}
	}
}

@Override
public void clientConnected(Client arg0) {
	// TODO Auto-generated method stub
	System.out.println("clientConnected"+arg0.toString());

}

@Override
public void clientDisconnected(Client arg0, DisconnectInfo arg1) {
	// TODO Auto-generated method stub
	System.out.println("clientDisconnected");

}

}[/java]

IT exceeded my expectations but I still think I’m doing it wrong, it does sync and update the 2nd player now that player2 can move his control and update the screen at player1 using the messaging in the update method:

[java]Message message = new ActionMessage(1, myClient.getId(), right);

	if (myClient != null) {
		myClient.send(message);
	}
	if (player2update == true) {
		simpleUpdatePlayer2(tpf);
	}

[/java]
The variable player2update corresponds to the variable right for moving the object right. I used a ClientListener to update on player1 what player2 is doing, just switching a binary semaphore do move the player2 object.

[java]public class ClientListener implements ClientStateListener,
MessageListener<Client> {

private UFOSpaceWorld app;
private Client client;
private boolean doTheRoll = false;
AutoControlMessage ahelloMessage;
public ClientListener(UFOSpaceWorld app, Client client) {
	this.app = app;
	this.client = client;
	client.addClientStateListener(this);
}

public void messageReceived(Client source, Message message) {
	if (message instanceof ActionMessage) {
		ActionMessage helloMessage = (ActionMessage) message;
		if (helloMessage.getAction() == 200) {
			app.enqueue(new Callable&lt;Void&gt;() {
				public Void call() throws Exception {
					app.addNewSaucer();
					return null;
				}
			});
		} else if (helloMessage.getAction() == 1) {
			if (helloMessage.isB() == true) {
				doTheRoll = true;

			} else {
				doTheRoll = false;
			}
			app.enqueue(new Callable&lt;Void&gt;() {
				public Void call() throws Exception {
					if (doTheRoll == true) {
						app.setDoRoll(true);
					} else {
						app.setDoRoll(false);
					}
					app.setPlayer2update(true);
					return null;
				}
			});

		}
	}
	if (message instanceof AutoControlMessage) {
		ahelloMessage = (AutoControlMessage) message;
		app.enqueue(new Callable&lt;Void&gt;() {
			public Void call() throws Exception {
				System.out.println("Setting ufo direction");
				app.setUfoDirection2(ahelloMessage.getMoveTo());
				app.setPlayer2update(true);
				return null;
			}
		});
		
	}
}

@Override
public void clientConnected(Client arg0) {
}

@Override
public void clientDisconnected(Client arg0, DisconnectInfo arg1) {
}

}[/java]

In tests with 2 computers syncing via amazon over TCP, it performaned as expected. [video]- YouTube

But when I move another player on my screen, I believe that I should use the message AutoControlMessage or similar, which could transfer the Vector3f of a movement, but then I got a serialization exception.

[java]@Serializable()
public class AutoControlMessage extends PhysicsSyncMessage {

public Vector3f aimAt = new Vector3f();
public Vector3f getAimAt() {
	return aimAt;
}

public Vector3f moveTo = new Vector3f();

public Vector3f getMoveTo() {
	return moveTo;
}

public AutoControlMessage() {
}

public AutoControlMessage(long id, Vector3f aimAt, Vector3f moveTo) {

// setReliable(false);
this.syncId = id;
this.aimAt.set(aimAt);
this.moveTo.set(moveTo);
}

@Override
public void applyData(Object object) {
    NetworkedAutonomousControl netControl = ((Spatial) object).getControl(NetworkedAutonomousControl.class);
    assert (netControl != null);
    if(netControl==null){
        Logger.getLogger(AutoControlMessage.class.getName()).log(Level.SEVERE, "Entity {0} has to Autonomous Control, message not accepted", ((Spatial)object).getUserData("entity_id"));
        return;
    }
    netControl.doAimAt(aimAt);
    netControl.doMoveTo(moveTo);
}

}
[/java]

Now I’m not sure how to best implement all the degrees of freedom for multiplayer, maybe you can let me know? I’m just calling a second update method for player2 and player1 and player2 are the same class.

[java] public void simpleUpdatePlayer2(float tpf) {
System.out.println(“LOOP simpleUpdatePlayer2 player 2”);
int speed = 25 * 8000;
Vector3f camDir = cam.getDirection().clone().multLocal(speed * tpf);
Vector3f camUp = cam.getUp().clone().mult(speed * tpf);
Quaternion roll = new Quaternion();
camDir.y = 0;
ufoDirection2.set(0, 0, 0);
// ufoControl2.setLinearVelocity(Vector3f.ZERO);
if (doRoll == true) {
roll.fromAngleAxis(FastMath.QUARTER_PI / 3, cam.getDirection());
ufoDirection2.set(cam.getLeft()).multLocal(-speed * tpf);
}
if (ufoControl2 == null) {
System.out.println(“ufoControl2 == null”);
}
ufoControl2.setPhysicsRotation(roll);
ufoControl2.setLinearVelocity(ufoDirection2);
if (System.currentTimeMillis() - time > 1000) {
System.out.println(“LOOP BREAK”);
ufoDirection2.set(0, 0, 0);
ufoControl2.setLinearVelocity(Vector3f.ZERO);
time = 0;
player2update = false;
}
}[/java]

The game client is (before refactoring):

[java]public class UFOSpaceWorld extends SimpleApplication implements AnalogListener,
ActionListener {

private PlanetAppState planetAppState;
private Geometry mark;
private Node ufoNode;
private RigidBodyControl ufoControl;
private RigidBodyControl ufoControl2;
private Node ufoNode2;
private CylinderCollisionShape shape;
private BoundingBox bv;
private RigidBodyControl jumpGateControl;
private RigidBodyControl jumpGateControl2;
private AnimChannel channel;
private AnimControl control;
private Node spacemanNode;
private RigidBodyControl spacemanControl;
private Node alien;
private String serverName = "localhost";
private int portNumber = 6143;
private RigidBodyControl alienControl;
Spatial jumpgateSpatial;
CameraNode camNode;
ChaseCamera chaseCam;
Planet moon;
static UFOSpaceWorld app;
Client myClient = null;
Vector3f ufoDirection = new Vector3f();
Vector3f ufoDirection2 = new Vector3f();
Vector3f ufoCamDir2 = new Vector3f();
public Vector3f getUfoCamDir2() {
	return ufoCamDir2;
}

public void setUfoCamDir2(Vector3f ufoCamDir2) {
	this.ufoCamDir2 = ufoCamDir2;
}

public Vector3f getUfoDirection2() {
	return ufoDirection2;
}

public void setUfoDirection2(Vector3f ufoDirection2) {
	this.ufoDirection2 = ufoDirection2;
}

Vector3f gate2vector = new Vector3f(10f, 10f, 1598300f);
Vector3f gate1vector = new Vector3f(10f, 10f, 1098300f);
private BulletAppState bulletAppState;
private boolean left = false, right = false, up = false, down = false,
		forward = false, backward = false, attack = false, rotate = false;
private long starttime = 0;
private long playtime = 0;

private void setupChaseCamera() {
	flyCam.setEnabled(false);
	chaseCam = new ChaseCamera(cam, ufoNode, inputManager);
	chaseCam.setDefaultDistance(2237);
}

@Override
public void destroy() {
	// custom code
	if (myClient != null) {
		myClient.close();
	}
	super.destroy();
}

public static void main(String[] args) {

	AppSettings settings = new AppSettings(true);
	settings.setResolution(1280, 1024);
	settings.setSettingsDialogImage("Interface/spacesplash.png");
	settings.setTitle("Space World");
	app = new UFOSpaceWorld();
	if (args.length &gt; 0) {
		app.serverName = args[0];
	}
	if (args.length &gt; 1) {
		app.portNumber = new Integer(args[0]);
	}
	app.setSettings(settings);
	app.start();
}

@Override
public void simpleInitApp() {
	playtime = 0;
	starttime = System.currentTimeMillis();
	this.setDisplayStatView(false);
	setDisplayFps(false);
	java.util.logging.Logger.getLogger("com.jme3").setLevel(
			java.util.logging.Level.SEVERE);
	bulletAppState = new BulletAppState();
	bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
	stateManager.attach(bulletAppState);
	bulletAppState.setDebugEnabled(false);
	DirectionalLight sun = new DirectionalLight();
	sun.setDirection(new Vector3f(-.1f, 0f, -1f));
	sun.setColor(new ColorRGBA(0.75f, 0.75f, 0.75f, 1.0f));
	rootNode.addLight(sun);
	// Add sky
	Node sceneNode = new Node("Scene");
	sceneNode.attachChild(Utility.createSkyBox(this.getAssetManager(),
			"Textures/blue-glow-1024.dds"));
	rootNode.attachChild(sceneNode);
	// Create collision test mark
	Sphere sphere = new Sphere(30, 30, 5f);
	mark = new Geometry("mark", sphere);
	Material mark_mat = new Material(assetManager,
			"Common/MatDefs/Misc/Unshaded.j3md");
	mark_mat.setColor("Color", ColorRGBA.Red);
	mark.setMaterial(mark_mat);
	// Add planet app state
	planetAppState = new PlanetAppState(rootNode, sun);
	stateManager.attach(planetAppState);
	// Add planet
	FractalDataSource planetDataSource = new FractalDataSource(4);
	planetDataSource.setHeightScale(900f);
	Planet planet = Utility.createEarthLikePlanet(getAssetManager(),
			293710.0f, null, planetDataSource);
	planet.addControl(new RigidBodyControl(new PlanetCollisionShape(planet
			.getLocalTranslation(), planet.getRadius(), planetDataSource),
			0f));
	planetAppState.addPlanet(planet);
	rootNode.attachChild(planet);
	bulletAppState.getPhysicsSpace().add(planet);

	// Add moon
	FractalDataSource moonDataSource = new FractalDataSource(5);
	moonDataSource.setHeightScale(300f);
	moon = Utility.createMoonLikePlanet(getAssetManager(), 50000,
			moonDataSource);
	moon.setLocalTranslation(new Vector3f(10f, 10f, 1505000f));
	RigidBodyControl moonControl = new RigidBodyControl(
			new PlanetCollisionShape(moon.getLocalTranslation(),
					moon.getRadius(), moonDataSource), 0f);
	moon.addControl(moonControl);
	planetAppState.addPlanet(moon);
	// add saucer

	addSaucer();
	jumpgateSpatial = assetManager.loadModel("JumpGate.j3o");
	jumpgateSpatial.setLocalScale(10000f);
	BoundingBox jbv = (BoundingBox) jumpgateSpatial.getWorldBound();
	CylinderCollisionShape jshape = new CylinderCollisionShape(
			new Vector3f(jbv.getXExtent(), jbv.getYExtent(),
					jbv.getZExtent()), 1);
	;
	jumpGateControl = new RigidBodyControl(jshape, 0);
	jumpgateSpatial.addControl(jumpGateControl);
	jumpGateControl.setMass(0f);
	jumpGateControl.setPhysicsLocation(gate1vector);
	jumpGateControl.setPhysicsRotation(new Quaternion().fromAngleAxis(
			90 * FastMath.DEG_TO_RAD, new Vector3f(1, 0, 0)));

	Spatial jumpgateSpatial2 = assetManager.loadModel("JumpGate.j3o");
	jumpgateSpatial2.setLocalScale(10000f);

	BoundingBox jbv2 = (BoundingBox) jumpgateSpatial2.getWorldBound();
	CylinderCollisionShape jshape2 = new CylinderCollisionShape(
			new Vector3f(jbv2.getXExtent(), jbv2.getYExtent(),
					jbv2.getZExtent()), 1);
	;

	Quaternion roll180 = new Quaternion();
	roll180.fromAngleAxis(FastMath.PI / 2, new Vector3f(0, 1, 0));

	jumpGateControl2 = new RigidBodyControl(jshape2, 0);
	jumpgateSpatial2.addControl(jumpGateControl2);
	jumpGateControl2.setMass(0f);
	jumpGateControl2.setPhysicsLocation(gate2vector);
	jumpGateControl2.setPhysicsRotation(new Quaternion().fromAngleAxis(
			90 * FastMath.DEG_TO_RAD, new Vector3f(1, 0, 0)));

	Spatial spaceStationSpatial = assetManager
			.loadModel("SpaceStation.blend");
	spaceStationSpatial.setLocalScale(3500f);

	BoundingBox sbv = (BoundingBox) spaceStationSpatial.getWorldBound();

	CompoundCollisionShape shape3 = new CompoundCollisionShape();
	shape3.addChildShape(
			new CylinderCollisionShape(new Vector3f(sbv.getXExtent(), sbv
					.getYExtent(), sbv.getZExtent()), 1), new Vector3f(0,
					0, 0));
	RigidBodyControl spaceStationControl = new RigidBodyControl(shape3, 0);
	spaceStationSpatial.addControl(spaceStationControl);
	spaceStationControl.setMass(0f);
	spaceStationControl.setPhysicsLocation(new Vector3f(10000f, -10f,
			705000f));

	BlenderKey blenderKey = new BlenderKey(
			"objects/creatures/alien/alienmodel.blend");

	Spatial alien = (Spatial) assetManager.loadModel(blenderKey);
	alien.setLocalScale(20 * 2000f);
	alienControl = new RigidBodyControl(3 * 500f);
	alien.addControl(alienControl);

	alienControl.setPhysicsLocation(new Vector3f(11000f, -50f, 755000f));
	//
	BlenderKey blenderKey2 = new BlenderKey(
			"objects/creatures/spaceman/man.mesh.xml");

	Spatial man = (Spatial) assetManager.loadModel(blenderKey2);
	man.setLocalScale(200f);
	spacemanControl = new RigidBodyControl(4 * 500f);
	man.addControl(spacemanControl);

	spacemanControl
			.setPhysicsLocation(new Vector3f(10700f, -590f, 775000f));

	createNewOtoBot();
	BulletAppState bas = app.getStateManager().getState(
			BulletAppState.class);
	bas.getPhysicsSpace().addAll(spaceStationSpatial);

	bas.getPhysicsSpace().addAll(man);
	bas.getPhysicsSpace().addAll(alien);
	bas.getPhysicsSpace().addAll(jumpgateSpatial);
	bas.getPhysicsSpace().addAll(jumpgateSpatial2);
	bas.getPhysicsSpace().addAll(moon);
	bas.getPhysicsSpace().addAll(planet);
	bas.getPhysicsSpace().setGravity(new Vector3f(0, 0, 0));

	rootNode.attachChild(man);
	rootNode.attachChild(alien);
	rootNode.attachChild(spaceStationSpatial);
	rootNode.attachChild(jumpgateSpatial);
	rootNode.attachChild(jumpgateSpatial2);
	rootNode.attachChild(moon);

	setupChaseCamera();
	registerInput();

	try {
		System.out.println("connecting to " + serverName);
		myClient = Network.connectToServer(serverName, portNumber);
		System.out.println("connected to " + serverName);
		//Serializer.registerClass(AutoControlMessage.class);
		Serializer.registerClass(ActionMessage.class);
		ClientListener cl = new ClientListener(app, myClient);
		myClient.addMessageListener(cl,
				ActionMessage.class);//, AutoControlMessage.class);
		myClient.addMessageListener(cl,
				AutoControlMessage.class);
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	if (myClient != null) {
		myClient.start();
		System.out.println("myClient started");
	} else {
		System.out.println("myClient == null " + serverName);
	}
}

public void clientConnected(Client c) {
	System.out.println("clientConnected ");
}

private PhysicsSpace getPhysicsSpace() {
	return bulletAppState.getPhysicsSpace();
}

public void addSaucer() {

	ufoNode = (Node) assetManager.loadModel("usaucer_v01.j3o");
	ufoNode.setLocalScale(200f);
	BoundingBox bv = (BoundingBox) ufoNode.getWorldBound();
	CylinderCollisionShape shape = new CylinderCollisionShape(new Vector3f(
			bv.getXExtent(), bv.getYExtent(), bv.getZExtent()), 1);
	ufoControl = new RigidBodyControl(shape, 500f);
	ufoNode.addControl(ufoControl);
	System.out.println("addSaucer 200:");
	ufoControl.setPhysicsLocation(new Vector3f(10850f, -5979f, 785020f));
	BulletAppState bas = app.getStateManager().getState(
			BulletAppState.class);
	bas.getPhysicsSpace().addAll(ufoNode);
	rootNode.attachChild(ufoNode);
}

public void addNewSaucer() {
	ufoNode2 = (Node) assetManager.loadModel("usaucer_v01.j3o");
	ufoNode2.setLocalScale(700f);
	bv = (BoundingBox) ufoNode2.getWorldBound();
	shape = new CylinderCollisionShape(new Vector3f(bv.getXExtent(),
			bv.getYExtent(), bv.getZExtent()), 1);
	ufoControl2 = new RigidBodyControl(shape, 500f);
	ufoNode2.addControl(ufoControl2);
	ufoControl2.setPhysicsLocation(new Vector3f(10850f, -5979f, 785020f));
	ufoControl2.setGravity(Vector3f.ZERO);
	ufoControl2.setLinearVelocity(Vector3f.ZERO);
	ufoControl2.clearForces();
	BulletAppState bas = app.getStateManager().getState(
			BulletAppState.class);
	bas.getPhysicsSpace().addAll(ufoNode2);
	rootNode.attachChild(ufoNode2);
}

public void createNewOtoBot() {
	BlenderKey otoblenderKey = new BlenderKey("Models/Oto/Oto.mesh.xml");
	Spatial otoBot = (Spatial) assetManager.loadModel(otoblenderKey);
	otoBot.setLocalScale(600f);
	RigidBodyControl otoControl = new RigidBodyControl(30 * 500f);
	otoBot.addControl(otoControl);
	otoControl.setPhysicsLocation(new Vector3f(10800f, -579f, 785000f));
	control = otoBot.getControl(AnimControl.class);
	channel = control.createChannel();
	for (String anim : control.getAnimationNames())
		System.out.println("otoBot can:" + anim);
	channel.setAnim("pull");
	BulletAppState bas = app.getStateManager().getState(
			BulletAppState.class);
	bas.getPhysicsSpace().addAll(otoBot);
	rootNode.attachChild(otoBot);
}

public void registerInput() {
	inputManager.addMapping("moveForward", new KeyTrigger(keyInput.KEY_UP),
			new KeyTrigger(keyInput.KEY_W));
	inputManager.addMapping("moveBackward", new KeyTrigger(
			keyInput.KEY_DOWN), new KeyTrigger(keyInput.KEY_S));
	inputManager.addMapping("moveRight",
			new KeyTrigger(keyInput.KEY_RIGHT), new KeyTrigger(
					keyInput.KEY_D));
	inputManager.addMapping("moveLeft", new KeyTrigger(keyInput.KEY_LEFT),
			new KeyTrigger(keyInput.KEY_A));
	inputManager.addMapping("moveUp", new KeyTrigger(keyInput.KEY_E));
	inputManager.addMapping("startServer", new KeyTrigger(keyInput.KEY_M));
	inputManager.addMapping("moveDown", new KeyTrigger(keyInput.KEY_Q));
	inputManager.addMapping("toggleRotate", new MouseButtonTrigger(
			MouseInput.BUTTON_LEFT));
	inputManager.addMapping("rotateRight", new MouseAxisTrigger(
			MouseInput.AXIS_X, true));
	inputManager.addMapping("rotateLeft", new MouseAxisTrigger(
			MouseInput.AXIS_X, false));
	inputManager.addMapping("rotateUp", new MouseAxisTrigger(
			MouseInput.AXIS_Y, true));
	inputManager.addMapping("rotateDown", new MouseAxisTrigger(
			MouseInput.AXIS_Y, false));
	inputManager.addListener(this, "moveForward", "moveBackward",
			"moveRight", "moveLeft", "moveUp", "moveDown");
	inputManager.addListener(this, "rotateRight", "rotateLeft", "rotateUp",
			"rotateDown", "toggleRotate", "startServer");
	// Toggle mouse cursor
	inputManager.addMapping("TOGGLE_CURSOR", new MouseButtonTrigger(
			MouseInput.BUTTON_LEFT), new KeyTrigger(KeyInput.KEY_SPACE));
	inputManager.addListener(actionListener, "TOGGLE_CURSOR");
	// Toggle wireframe
	inputManager.addMapping("TOGGLE_WIREFRAME", new KeyTrigger(
			KeyInput.KEY_T));
	inputManager.addListener(actionListener, "TOGGLE_WIREFRAME");
	// Collision test
	inputManager.addMapping("COLLISION_TEST", new MouseButtonTrigger(
			MouseInput.BUTTON_RIGHT));
	inputManager.addListener(actionListener, "COLLISION_TEST");
}

public void toggleToFullscreen() {
	GraphicsDevice device = GraphicsEnvironment
			.getLocalGraphicsEnvironment().getDefaultScreenDevice();
	DisplayMode[] modes = device.getDisplayModes();
	int i = 0; // note: there are usually several, let's pick the first
	settings.setResolution(modes[i].getWidth(), modes[i].getHeight());
	settings.setFrequency(modes[i].getRefreshRate());
	settings.setBitsPerPixel(modes[i].getBitDepth());
	settings.setFullscreen(device.isFullScreenSupported());
	app.setSettings(settings);
	app.restart(); // restart the context to apply changes
}

long time = 0;

public void simpleUpdatePlayer2(float tpf) {
	System.out.println("LOOP simpleUpdatePlayer2 player 2");
	int speed = 25 * 8000;
	Vector3f camDir = cam.getDirection().clone().multLocal(speed * tpf);
	Vector3f camUp = cam.getUp().clone().mult(speed * tpf);
	Quaternion roll = new Quaternion();
	camDir.y = 0;
	ufoDirection2.set(0, 0, 0);
	// ufoControl2.setLinearVelocity(Vector3f.ZERO);
	if (doRoll == true) {
		roll.fromAngleAxis(FastMath.QUARTER_PI / 3, cam.getDirection());
		ufoDirection2.set(cam.getLeft()).multLocal(-speed * tpf);
	}
	if (ufoControl2 == null) {
		System.out.println("ufoControl2 == null");
	}
	ufoControl2.setPhysicsRotation(roll);
	ufoControl2.setLinearVelocity(ufoDirection2);
	if (System.currentTimeMillis() - time &gt; 1000) {
		System.out.println("LOOP BREAK");
		ufoDirection2.set(0, 0, 0);
		ufoControl2.setLinearVelocity(Vector3f.ZERO);
		time = 0;
		player2update = false;
	}
}

private boolean doRoll = false;

public boolean isDoRoll() {
	return doRoll;
}

public void setDoRoll(boolean doRoll) {
	this.doRoll = doRoll;
}

@Override
public void simpleUpdate(float tpf) {
	playtime = System.currentTimeMillis() - starttime;
	int speed = 25 * 80000;
	Vector3f camDir = cam.getDirection().clone().multLocal(speed * tpf);
	Vector3f camUp = cam.getUp().clone().mult(speed * tpf);
	Quaternion roll = new Quaternion();
	camDir.y = 0;
	ufoDirection.set(0, 0, 0);
	ufoControl.setLinearVelocity(Vector3f.ZERO);
	if (left) {
		roll.fromAngleAxis(-FastMath.QUARTER_PI / 3, cam.getDirection());
		ufoDirection.set(cam.getLeft().multLocal(speed * tpf));
	}
	if (right) {
		roll.fromAngleAxis(FastMath.QUARTER_PI / 3, cam.getDirection());
		ufoDirection.set(cam.getLeft()).multLocal(-speed * tpf);
	}
	if (up) {
		roll.fromAngleAxis(0, cam.getDirection());
		ufoDirection.addLocal(camUp);
	}
	if (down) {
		roll.fromAngleAxis(0, cam.getDirection());
		ufoDirection.addLocal(cam.getUp().multLocal(-speed * tpf));
	}
	if (forward) {
		roll.fromAngleAxis(0, cam.getDirection());
		ufoDirection.set(camDir);
	}
	if (backward) {
		roll.fromAngleAxis(0, cam.getDirection());
		ufoDirection.set(camDir.multLocal(-1f));
	}
	ufoControl.setPhysicsRotation(roll);
	ufoControl.setLinearVelocity(ufoDirection);

	CollisionResults results = new CollisionResults();

	System.out.println("1 #Collisions between" + ufoNode.getName()
			+ " and " + jumpgateSpatial.getName() + ": " + results.size());
	ufoNode.collideWith((BoundingBox) jumpgateSpatial.getWorldBound(),
			results);
	System.out.println("2 #Collisions between" + ufoNode.getName()
			+ " and " + jumpgateSpatial.getName() + ": " + results.size());
	CollisionResults results2 = new CollisionResults();

	// Use the results

	if (results.size() &gt; 0 &amp;&amp; playtime &gt; 50000) {
		System.out.println("playtime" + playtime);
		System.out.println("#Collisions between" + ufoNode.getName()
				+ " and " + jumpgateSpatial.getName() + ": "
				+ results.size());

		// how to react when a collision was detected
		CollisionResult closest = results.getClosestCollision();
		System.out.println("What was hit? "
				+ closest.getGeometry().getName());
		System.out
				.println("Where was it hit? " + closest.getContactPoint());
		System.out.println("Distance? " + closest.getDistance());
		ufoControl
				.setPhysicsLocation(jumpGateControl2.getPhysicsLocation());
		System.out.println("Warped");

	} else {
		// how to react when no collision occured
	}

	if (results2.size() &gt; 0) {
		System.out.println("Number of Collisions between"
				+ ufoNode.getName() + " and " + moon.getName() + ": "
				+ results2.size());

		// how to react when a collision was detected
		CollisionResult closest2 = results2.getClosestCollision();
		System.out.println("What was hit? "
				+ closest2.getGeometry().getName());
		System.out.println("Where was it hit? "
				+ closest2.getContactPoint());
		System.out.println("Distance? " + closest2.getDistance());
	}
	Message message = new ActionMessage(1, myClient.getId(), right);
	//Message amessage = new AutoControlMessage(myClient.getId(), ufoDirection, camDir);
	
	if (myClient != null) {
		myClient.send(message);
		//myClient.send(amessage);
	}
	if (player2update == true) {
		System.out.println("simpleUpdatePlayer2 player 2 "
				+ message.toString());
		time = System.currentTimeMillis();
		simpleUpdatePlayer2(tpf);
	}
}

boolean player2update = false;

public boolean isPlayer2update() {
	return player2update;
}

public void setPlayer2update(boolean player2update) {
	this.player2update = player2update;
}

private void createOto() {
	BlenderKey blenderKey = new BlenderKey("Models/Oto/Oto.mesh.xml");
	Spatial man = (Spatial) assetManager.loadModel(blenderKey);
	man.setLocalTranslation(new Vector3f(69, 15, -60));
	man.setShadowMode(ShadowMode.CastAndReceive);
	rootNode.attachChild(man);

}

public void onAnalog(String name, float value, float tpf) {

	if (name.equals("rotateRight") &amp;&amp; rotate) {
		ufoNode.rotate(0, 1 * tpf, 0);
	}
	if (name.equals("rotateLeft") &amp;&amp; rotate) {
		ufoNode.rotate(0, -1 * tpf, 0);
	}
	if (name.equals("rotateUp") &amp;&amp; rotate) {
		ufoNode.rotate(0, 0, -1 * tpf);
	}
	if (name.equals("rotateDown") &amp;&amp; rotate) {
		ufoNode.rotate(0, 0, 1 * tpf);
	}
}

public void onAction(String name, boolean keyPressed, float tpf) {

	Message message = new ActionMessage(name, keyPressed);
	if (myClient != null) {
		myClient.send(message);
	}

	if (name.equals("moveLeft")) {
		if (keyPressed) {
			left = true;
		} else {
			left = false;
		}
	} else if (name.equals("moveRight")) {
		if (keyPressed) {
			right = true;
		} else {
			right = false;
		}
	} else if (name.equals("moveUp")) {
		if (keyPressed) {
			up = true;
		} else {
			up = false;
		}
	} else if (name.equals("moveDown")) {
		if (keyPressed) {
			down = true;
		} else {
			down = false;
		}
	} else if (name.equals("moveForward")) {
		if (keyPressed) {
			forward = true;
		} else {
			forward = false;
		}
	} else if (name.equals("moveBackward")) {
		if (keyPressed) {
			backward = true;
		} else {
			backward = false;
		}
	}

	// toggling rotation on or off
	if (name.equals("toggleRotate") &amp;&amp; keyPressed) {
		rotate = true;
		inputManager.setCursorVisible(false);
	}
	if (name.equals("toggleRotate") &amp;&amp; !keyPressed) {
		rotate = false;
		inputManager.setCursorVisible(true);
	}
	if (name.equals("TOGGLE_CURSOR") &amp;&amp; !keyPressed) {
		if (inputManager.isCursorVisible()) {
			inputManager.setCursorVisible(false);
		} else {
			inputManager.setCursorVisible(true);
		}
	}
	if (name.equals("TOGGLE_WIREFRAME") &amp;&amp; !keyPressed) {
		for (Planet planet : planetAppState.getPlanets()) {
			planet.toogleWireframe();
		}
	}
	if (name.equals("COLLISION_TEST") &amp;&amp; !keyPressed) {
		CollisionResults results = new CollisionResults();
		Ray ray = new Ray(cam.getLocation(), cam.getDirection());
		// Test collision with closest planet's terrain only
		planetAppState.getNearestPlanet().getTerrainNode()
				.collideWith(ray, results);
		System.out.println("----- Collisions? " + results.size() + "-----");
		for (int i = 0; i &lt; results.size(); i++) {
			// For each hit, we know distance, impact point, name of
			// geometry.
			float dist = results.getCollision(i).getDistance();
			Vector3f pt = results.getCollision(i).getContactPoint();
			String hit = results.getCollision(i).getGeometry().getName();
			System.out.println("* Collision #" + i);
			System.out.println("  You shot " + hit + " at " + pt + ", "
					+ dist + " wu away.");
		}
		if (results.size() &gt; 0) {
			// The closest collision point is what was truly hit:
			CollisionResult closest = results.getClosestCollision();
			// Let's interact - we mark the hit with a red dot.
			mark.setLocalTranslation(closest.getContactPoint());
			rootNode.attachChild(mark);
		} else {
			// No hits? Then remove the red mark.
			rootNode.detachChild(mark);
		}
	}
}

private ActionListener actionListener = new ActionListener() {
	public void onAction(String name, boolean pressed, float tpf) {
		if (name.equals("TOGGLE_CURSOR") &amp;&amp; !pressed) {
			if (inputManager.isCursorVisible()) {
				inputManager.setCursorVisible(false);
			} else {
				inputManager.setCursorVisible(true);
			}
		}
		if (name.equals("TOGGLE_WIREFRAME") &amp;&amp; !pressed) {
			for (Planet planet : planetAppState.getPlanets()) {
				planet.toogleWireframe();
			}
		}
		if (name.equals("COLLISION_TEST") &amp;&amp; !pressed) {
			CollisionResults results = new CollisionResults();
			Ray ray = new Ray(cam.getLocation(), cam.getDirection());
			// Test collision with closest planet's terrain only
			planetAppState.getNearestPlanet().getTerrainNode()
					.collideWith(ray, results);
			System.out.println("----- Collisions? " + results.size()
					+ "-----");
			for (int i = 0; i &lt; results.size(); i++) {
				// For each hit, we know distance, impact point, name of
				// geometry.
				float dist = results.getCollision(i).getDistance();
				Vector3f pt = results.getCollision(i).getContactPoint();
				String hit = results.getCollision(i).getGeometry()
						.getName();
				System.out.println("* Collision #" + i);
				System.out.println("  You shot " + hit + " at " + pt + ", "
						+ dist + " wu away.");
			}
			if (results.size() &gt; 0) {
				// The closest collision point is what was truly hit:
				CollisionResult closest = results.getClosestCollision();
				// Let's interact - we mark the hit with a red dot.
				mark.setLocalTranslation(closest.getContactPoint());
				rootNode.attachChild(mark);
			} else {
				// No hits? Then remove the red mark.
				rootNode.detachChild(mark);
			}
		}
	}
};

}[/java]

And server

[java]public class SpaceWorldServer extends SimpleApplication implements
ConnectionListener {
Server myServer = null;
static SpaceWorldServer app = null;
int counter = 0;

public static void main(String[] args) {
	app = new SpaceWorldServer();
	app.start(JmeContext.Type.Headless);
}

public void simpleInitApp() {
	System.out.println("simpleInitApp");
	try {
		myServer = Network.createServer(6143);
		Serializer.registerClass(ActionMessage.class);
		//Serializer.registerClass(AutoControlMessage.class);
		ServerListener sl = new ServerListener(app);
		myServer.addMessageListener(sl,
				ActionMessage.class);//, AutoControlMessage.class);
		myServer.addMessageListener(sl,
				AutoControlMessage.class);
		myServer.addConnectionListener(this);
	} catch (IOException e) {
		e.printStackTrace();
	}
	myServer.start();
}

@Override
public void destroy() {
	myServer.close();
	super.destroy();
}

public void updatePlayers(Message message, HostedConnection c) {
	myServer.broadcast(Filters.notEqualTo(c), message);
}

public void connectionAdded(Server s, HostedConnection c) {
	System.out.println("connectionAdded:" + c.toString());
	Message message = new ActionMessage(100, 200, true);
	System.out.println("broadcasting message " + message.toString());
	myServer.broadcast(Filters.notEqualTo(c), message);
	System.out.println("broadcast message " + message.toString());
}

public void connectionRemoved(Server s, HostedConnection c) {
}

@Override
public void simpleUpdate(float tpf) {
	if (counter != myServer.getConnections().size()) {
		System.out.println("#connections:"
				+ myServer.getConnections().size());
		counter = myServer.getConnections().size();
	}
}

}
[/java]

General architecture for client server games:
-client sends server message “move me” of some kind.
-server keeps track of player positions and sends updates out to clients
-clients render latest position as sent from server.

There are variations on this… but in general, if the server is not keeping track of position and sending the position to the clients then something is broken. Whether you’ve run into problems yet or not is just a matter of time.

1 Like
@pspeed said: General architecture for client server games: -client sends server message "move me" of some kind. -server keeps track of player positions and sends updates out to clients -clients render latest position as sent from server.

There are variations on this… but in general, if the server is not keeping track of position and sending the position to the clients then something is broken. Whether you’ve run into problems yet or not is just a matter of time.

Thank you for the answer. I did not yet need the actual position to move the object since I only tried moving in one direction by pressing the D (move right) which doe update the other client.

[java] if (doRoll == true) {
roll.fromAngleAxis(FastMath.QUARTER_PI / 3, cam.getDirection());
ufoDirection2.set(cam.getLeft()).multLocal(-speed * tpf);
}[/java]

I expect problems when I try and sync more events, but I liked the way it worked so far. I’m thinking of using a queue with players if I try more than 2 players, but right now I’m writing on 2-player capabilities. Since it works in one direction I hav good hopes that it will succeed with more advanced movements. I’m not sure how to use the message classes though but I wil llook more into the monkeyzone code to see how it’s done.

Well, the sooner you let the server be authoritative the better. Everything kind of becomes simpler after that.

2 Likes
@pspeed said: Well, the sooner you let the server be authoritative the better. Everything kind of becomes simpler after that.

This, trust me no good comes of of clientside logic for whatever reason. But a shitbucket of problems in the long term.
I have seen to many indie games fail in real world condition because of borked network code.