Help me fix my native networking game

I am a freshman student taking up Computer Science and as a project, we are making a game. I know a lot about java, so please don’t treat me as a noob. :smiley:

I have finished almost all the parts of my game except the networking, we were required to implement native Java networking on our game. I have successfully done it but the lag is so severe, that it can only accept a single client. :frowning:

My Multiplayer Client application looks like this.
init -> make 1 connection write thread and 1 connection read thread -> init nodes and objects
update -> connection read thread reads the current state of all characters from the sent output by the server -> update character and dummy characters representing the characters on the server -> accept input -> send input to server

The Server application looks like this
init -> get connections and start a write and read thread for every connection if the connections > 1
update->gets all input from write thread -> simulate -> output all data simulated(current state)

Here is my code if you need it:
GameServerApp
[java]
public class GameServerApp extends SimpleApplication{

GameServer gameServer;
BulletAppState bulletAppState;
@Override
public void simpleInitApp() {
    gameServer = new GameServer(this);
    System.out.println(gameServer.getIp());
}

@Override
public void simpleUpdate(float tpf){
    gameServer.update();
}

public static void main(String[] args){
    GameServerApp app = new GameServerApp();
    AppSettings newSettings = new AppSettings(true);
    newSettings.setFrameRate(30);
    app.setSettings(newSettings);
    //debug
    app.start();
    app.setPauseOnLostFocus(false);
    //app.start(JmeContext.Type.Headless);
}

@Override
public void destroy(){
    super.destroy();
    gameServer.stop();
}
public void initPhysics(){
    bulletAppState = new BulletAppState();
    getStateManager().attach(bulletAppState);

}

public void initStage(){ 
    Spatial stage = assetManager.loadModel("Models/Stage.j3o");
    rootNode.attachChild(stage);
    bulletAppState.getPhysicsSpace().addAll(stage);
    bulletAppState.getPhysicsSpace().setGravity(Vector3f.UNIT_Y.mult(-240f));
}

public BulletAppState getBulletAppState(){
    return bulletAppState;
}

}
[/java]
GameServer
[java]
public class GameServer {

GameServerApp gameServerApp;
ConnectionAcceptorThread cat;
ArrayList wConnections = new ArrayList();
ArrayList rConnections = new ArrayList();
ArrayList sockets = new ArrayList();
ArrayList avatars = new ArrayList();
ArrayList avatarNodes = new ArrayList();
ServerSocket ss;
boolean preGame, onGame;
String ip;

public GameServer(GameServerApp gameServerApp){
    this(gameServerApp,2048);
}
public GameServer(GameServerApp gameServerApp, int portNumber){
    this.gameServerApp = gameServerApp;
    try {
        ss = new ServerSocket(portNumber);
        ip = InetAddress.getLocalHost().getHostAddress();
        preGame = true;
    } catch (IOException ex) {
        Logger.getLogger(GameServer.class.getName()).log(Level.SEVERE, null, ex);
    }
    
}

public void update(){
    if(preGame){
        preGameUpdate();
    }
    if(onGame){
        gamePreTickUpdate();
        gameTickUpdate();
    }
}

public boolean startGame(){
    if(sockets.size() >= 1){
        gameServerApp.initPhysics();
        gameServerApp.initStage();
        for(int i = 0 ; i < sockets.size(); i++){
            wConnections.add(new ConnectionWriteThread(sockets.get(i)));
            wConnections.get(i).start();
            wConnections.get(i).send(i+"s " + sockets.size());
            rConnections.add(new ConnectionReadThread(sockets.get(i)));
            rConnections.get(i).start();
            avatars.add(new AvatarStatus());
            avatarNodes.add(new AvatarServerNode("Player" + (i+1),gameServerApp,avatars.get(i)));
            gameServerApp.getBulletAppState().getPhysicsSpace().add(avatarNodes.get(i).getNode());
            gameServerApp.getRootNode().attachChild(avatarNodes.get(i).getNode());
        }
        preGame = false;
        onGame = true;
        cat.destroy();
        cat = null;
        return true;
    }
    return false;
}

public void preGameUpdate(){
    if(cat == null){
        cat = new ConnectionAcceptorThread(ss);
        cat.start();
    }
    if(!cat.getSockets().isEmpty()){
        sockets.add(cat.getSockets().remove(0));
    }
    
    //debug
    if(!sockets.isEmpty()){
        startGame();
    }
}

private void gamePreTickUpdate() {
    for(int i = 0; i < rConnections.size() ; i++){
        ConnectionReadThread ct = rConnections.get(i);
        System.out.println("Player "+ i + " size: " +ct.getInputs().size());
        while(!ct.getInputs().isEmpty()){
            String temp = ct.getInputs().remove(0);
            if(temp.equals("") ){
                continue;
            } else {
                if(temp.charAt(0) == 'p'){
                    String[] tempArr = temp.split(" ");
                    avatars.get(i).setPosition(Float.parseFloat(tempArr[1]), 
                            Float.parseFloat(tempArr[2]), 
                            Float.parseFloat(tempArr[3]));
                }else if(temp.charAt(0) == 'w'){
                    String[] tempArr = temp.split(" ");
                    avatars.get(i).setWalkDir(new Vector3f(Float.parseFloat(tempArr[1]), 
                            Float.parseFloat(tempArr[2]), 
                            Float.parseFloat(tempArr[3])));
                }else if(temp.charAt(0) == 'v'){
                    String[] tempArr = temp.split(" ");
                    avatars.get(i).setViewDir(new Vector3f(Float.parseFloat(tempArr[1]), 
                            Float.parseFloat(tempArr[2]), 
                            Float.parseFloat(tempArr[3])));
                }else if(temp.charAt(0) == 'a'){
                    avatars.get(i).setCurrAnim((temp.split(" "))[1]);
                }
            }
        }
    }
}

private void gameTickUpdate(){
    for(int i = 0; i < wConnections.size(); i++){
        for(int j = 0; j < avatars.size(); j++){
            if(i!=j){
                Vector3f tempWalkDir = avatars.get(j).getWalkDir();
                Vector3f tempViewDir = avatars.get(j).getViewDir();
                String anim = avatars.get(j).getCurrAnim();
                wConnections.get(i).send(j + "w " + tempWalkDir.getX() + " " + tempWalkDir.getY() + " " + tempWalkDir.getZ() );
                wConnections.get(i).send(j + "v " + tempViewDir.getX() + " " + tempViewDir.getY() + " " + tempViewDir.getZ() );
                wConnections.get(i).send(j + "a " + anim);
            }
        }
    }
}

public ArrayList getAvatars(){
    return avatars;
}

public String getIp(){
    return ip;
}

public boolean isCatAlive(){
    if(cat == null) return false;
    return true;
}

public void stop(){
    for(ConnectionReadThread crt : rConnections){
        crt.destroy();
    }
     for(ConnectionWriteThread cwt : wConnections){
        cwt.destroy();
    }
}

}
[/java]

MPlayerGameAppState
[java]
public class MPlayerGameAppState extends AbstractAppState {

static GameApp gameApp;
Node rootNode;
Node guiNode;
CameraNode camNode;
AssetManager assetManager;
AudioRenderer audioRenderer;
InputManager inputManager;
Renderer renderer;
ViewPort viewPort;
Camera cam;
BulletAppState bulletAppState;
Node avatar;
Spatial avatar_body;
BetterCharacterControl avatar_bcc;
AvatarClientControl avatar_ctrl;
FilterPostProcessor fpp;
DepthOfFieldFilter dofFilter;
ConnectionReadThread rConnection;
ConnectionWriteThread wConnection;
AvatarStatus[] avatars;
ArrayList avatarNodes = new ArrayList();
int avatarSize;

@Override
public void initialize(AppStateManager stateManager, Application app) {
    super.initialize(stateManager, app);
    gameApp = (GameApp)app;
    
    gameApp.setupConn("127.0.0.1", 2048);
    connect();
    initGame();
    initPhysics();
    initAvatar();
    initStage();
    
   // setupFilters();
    //TODO: initialize your AppState, e.g. attach spatials to rootNode
    //this is called on the OpenGL thread after the AppState has been attached
}

@Override
public void update(float tpf) {
    
    gamePreTickUpdate();
    
    rootNode.updateLogicalState(tpf);
    rootNode.updateGeometricState();
    guiNode.updateLogicalState(tpf);
    guiNode.updateGeometricState();
    //TODO: implement behavior during runtime
}

@Override
public void cleanup() {
    super.cleanup();
    rConnection.destroy();
    wConnection.destroy();
    rootNode.detachChildNamed("Avatar");
    //TODO: clean up what you initialized in the initialize method,
    //e.g. remove all spatials from rootNode
    //this is called on the OpenGL thread after the AppState has been detached
}

public void initGame(){
    rootNode = gameApp.getRootNode();
    guiNode = gameApp.getGuiNode();
    assetManager = gameApp.getAssetManager();
    audioRenderer = gameApp.getAudioRenderer();
    inputManager = gameApp.getInputManager();
    viewPort = gameApp.getViewPort();
    renderer = gameApp.getRenderer();
    cam = gameApp.getCamera();
}

public void initPhysics(){
    bulletAppState = new BulletAppState();
    gameApp.getStateManager().attach(bulletAppState);

    
    //bulletAppState.getPhysicsSpace().enableDebug(assetManager);
}


public void initAvatar(){
    avatar = new Node("Avatar");
    avatar_body = assetManager.loadModel("Models/Char.j3o");
    TangentBinormalGenerator.generate(avatar_body);
    avatar_bcc = new BetterCharacterControl(2f, 8f, 2f);
    avatar.setLocalTranslation(new Vector3f(0f,-.5f,0f));
    
    avatar_ctrl = new AvatarClientControl(null, inputManager, cam, avatar_body, rConnection,
            wConnection);
    avatar_ctrl.setSpeed(20f);
    avatar.addControl(avatar_bcc);
    avatar.addControl(avatar_ctrl);
    
    avatar.attachChild(avatar_body);
    
    bulletAppState.getPhysicsSpace().add(avatar);

    rootNode.attachChild(avatar);
}

public void initStage(){ 
    Spatial stage = assetManager.loadModel("Models/Stage.j3o");
    rootNode.attachChild(stage);
    bulletAppState.getPhysicsSpace().addAll(stage);
    bulletAppState.getPhysicsSpace().setGravity(Vector3f.UNIT_Y.mult(-240f));
}

private void setupFilters() {
    
    fpp = new FilterPostProcessor(assetManager);
    if (renderer.getCaps().contains(Caps.GLSL120)){
            CartoonEdgeProcessor cartoonEdgeProcessor = new CartoonEdgeProcessor();
            viewPort.addProcessor(cartoonEdgeProcessor);
            CartoonEdgeFilter cartoonEdgeFilter = new CartoonEdgeFilter();
            fpp.addFilter(cartoonEdgeFilter);
            MotionBlurFilter motionBlurFilter = new MotionBlurFilter();
            fpp.addFilter(motionBlurFilter);
            dofFilter = new DepthOfFieldFilter();
            dofFilter.setFocusDistance(0);
            dofFilter.setFocusRange(40);
            dofFilter.setBlurScale(1.4f);
            fpp.addFilter(dofFilter);
    }
    viewPort.addProcessor(fpp);
    
}

private void connect() {
    try {
        Socket s = new Socket(gameApp.getHost(),gameApp.getHostPort());
        rConnection = new ConnectionReadThread(s);
        wConnection = new ConnectionWriteThread(s);
        rConnection.start();
    } catch (UnknownHostException ex) {
        Logger.getLogger(MPlayerGameAppState.class.getName()).log(Level.SEVERE, null, ex);
    } catch (IOException ex) {
        Logger.getLogger(MPlayerGameAppState.class.getName()).log(Level.SEVERE, null, ex);
    }
}

private void gamePreTickUpdate() {
    while(!rConnection.getInputs().isEmpty()){
        String temp = rConnection.getInputs().remove(0);
        if(temp.equals("") || temp.equals("null") || temp == null){
            continue;
        } else {
            int i = Integer.parseInt(temp.charAt(0)+ "");
            if(temp.charAt(1) == 'p'){
                String[] tempArr = temp.split(" ");
                avatars[i].setPosition(Float.parseFloat(tempArr[1]), 
                        Float.parseFloat(tempArr[2]), 
                        Float.parseFloat(tempArr[3]));
            }else if(temp.charAt(1) == 'w'){
                String[] tempArr = temp.split(" ");
                avatars[i].setWalkDir(new Vector3f(Float.parseFloat(tempArr[1]), 
                        Float.parseFloat(tempArr[2]), 
                        Float.parseFloat(tempArr[3])));
            }else if(temp.charAt(1) == 'v'){
                String[] tempArr = temp.split(" ");
                avatars[i].setViewDir(new Vector3f(Float.parseFloat(tempArr[1]), 
                        Float.parseFloat(tempArr[2]), 
                        Float.parseFloat(tempArr[3])));
            }else if(temp.charAt(1) == 'a'){
                avatars[i].setCurrAnim((temp.split(" "))[1]);
            }else if(temp.charAt(1) == 's'){
                
                avatarSize = Integer.parseInt(temp.split(" ")[1]);
                int counter = 0;
                avatars = new AvatarStatus[avatarSize];
                for(int j = 0; j < avatarSize; j++){
                    if(i!=j){
                        avatars[j] = new AvatarStatus();
                        avatarNodes.add(new AvatarClientNode("Player" + (j+1),gameApp,avatars[j]));
                        bulletAppState.getPhysicsSpace().add(avatarNodes.get(counter).getNode());
                        rootNode.attachChild(avatarNodes.get(counter).getNode());
                        counter++;
                    }
                }
                wConnection.clear();
                wConnection.start();
            }
        }
    }
    
}

}
[/java]

here is my read and write threads:

ConnectionWriteThread
[java]
public class ConnectionWriteThread extends Thread {

Socket s;
private boolean stop, send;
PrintWriter out;
ArrayList outputCache = new ArrayList();

public ConnectionWriteThread(Socket s){
    this.s = s;
    try {
        out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(this.s.getOutputStream(),"UTF-8")),true);
    } catch (IOException ex) {
        Logger.getLogger(ConnectionWriteThread.class.getName()).log(Level.SEVERE, null, ex);
    }
}

@Override
public void run() {
    while(true){
        if(stop) break;
        if(send){
            while(!outputCache.isEmpty()){
                out.println(outputCache.remove(0));
                if(outputCache.isEmpty())send = false;
            }
        }
    }
}

public void destroy(){
    out.close();
    stop = true;
}

public void send(String s){
    outputCache.add(s);
    send = true;
}

public void clear(){
    outputCache.clear();
}

}
[/java]

ConnectionReadThread
[java]
public class ConnectionReadThread extends Thread {

Socket s;
private boolean stop, send;
BufferedReader in;
ArrayList inputCache = new ArrayList();

public ConnectionReadThread(Socket s){
    this.s = s;
    try {
        in = new BufferedReader(new InputStreamReader(new BufferedInputStream(this.s.getInputStream())));
    } catch (IOException ex) {
        Logger.getLogger(ConnectionReadThread.class.getName()).log(Level.SEVERE, null, ex);
    }
}

@Override
public void run() {
    while(true){
        if(stop) break;
        try {
            inputCache.add(in.readLine());
        } catch (IOException ex) {
            Logger.getLogger(ConnectionReadThread.class.getName()).log(Level.SEVERE, null, ex);
        }
        
    }
}

public void destroy(){
    try {
        in.close();
    } catch (IOException ex) {
        Logger.getLogger(ConnectionReadThread.class.getName()).log(Level.SEVERE, null, ex);
    }
    stop = true;
}

public ArrayList getInputs(){
    return inputCache;
}

}
[/java]

Please drop any comments regarding my code. :smiley:

Initial thoughts:

Why not use an ObjectStream and Serializable objects for your communication layer? You can at least do away with all the command parsing and make it a little easier to add new commands.

Assuming this is only supposed to handle a handful of clients, that is.

Aside from this, it’s really hard to go through the code without the code highlighter. I’ll have another look when that is reenabled.

It is hard to read without the code highlighter working but I picked up enough to know this code has a lot of problems.

The biggest general problem is that the threads are all running crazy on top of each other. There are no properly thread safe data structures, etc. and so things will be stomping all over each other. That’s not a performance problem really but it could look like one as messages are lost, etc…

In what I could read, the only performance problem I saw was the conversion of everything to string and back. ObjectOuputStream/ObjectInputStream will have its own baggage. If (like threading) you’ve never really used it before then it will end up being less performant over time than even your current solution. DataInputStream and DataOutputStream would at least let you write floats and stuff natively.

If you are trying to learn networking and threading then maybe write a separate small example program that just receives inputs from clients and blasts them back out at regular intervals. Concentrate on proper threading (ConcurrentLinkedQueue may be your best friend), proper message passing, etc… Get the throughput of that nice and tight. Measure it, etc… Then worry about the game integration.

I do wonder a little about a computer science project that would have you create a networked game without having already provided the network/threading skills to complete the task. Was networking it a feature you decided to add to the project? If so then it make the project about 100x harder and I’d recommend doing a different kind of game.

1 Like

Thanks for the input. We were taught how to thread and network seperately, we were not taught how to implement thread-safe networking though. :smiley: I see the problem now is that the data is not synchronized. So how do make a client and server program so that the client sends user input -> the server accepts and simulate it -> send current state to others? :smiley: Do i just convert my arraylists into ConcurrentLinkedQueue and that’s all? :S D: Thanks a lot for the help.

As for making the communication layer use ObjectStreams and Serializable, how do i do that? :S Do i just implement a message abstract then serialize an object using it then send it over and check if it is an instanceof the object type? :smiley:

Really, I wouldn’t use object streams. You will end up sending twice the data and having to constantly clear the stream’s internal cache. But yeah, basically just have your objects implement Serializable. I don’t think Vector3f does so you probably have to still write it manually… in which case DataOuputStream starts to look pretty attractive again.

General architecture: each connection has two threads, one for reading and one for writing. Writer thread has a concurrent linked queue of things to send. Things that want to write add them to the queue. Reader thread posts read message to a central shared server queue.

Server pulls from this queue and does stuff. That stuff may be putting messages on all of the connections output queues.

That’s kind of the way SpiderMonkey does it internally.

1 Like

I’m using Hessian for serialization. It doesn’t require objects to implement Serializable and avoid quite a few issues which default java serialization has.

I have used mina for non-blocking IO. You can take a look at
http://subversion.assembla.com/svn/vmat/trunk/src/main/java/net/virtualmat/client/VmatNetManager.java
connect method (putting HessianCodecFactory in the chain)
http://subversion.assembla.com/svn/vmat/trunk/src/main/java/net/virtualmat/util/HessianCodecFactory.java
for implementaton of codec (quite straightforward)
http://subversion.assembla.com/svn/vmat/trunk/src/main/java/net/virtualmat/util/HessianEncoder.java
for encoding and
http://subversion.assembla.com/svn/vmat/trunk/src/main/java/net/virtualmat/util/HessianDecoder.java
for decoding.

Serialization part is just

ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output ho = new Hessian2Output(baos);
ho.getSerializerFactory().setAllowNonSerializable(true);
ho.writeObject(message);
ho.flush();
ho.close();

    byte[] byteArray = baos.toByteArray();
and then sending these byte arrays prefixed with length over the wire.

Mina might be slightly overhelming at start, but it is a good framework if you want to be scalable.

@abies said: Mina might be slightly overhelming at start, but it is a good framework if you want to be scalable.

I had assumed he had to write his own (since it’s for school) or I’d have just suggested SpiderMonkey which would take care of all of this for him also… and is already in his SDK.

@pspeed said:

General architecture: each connection has two threads, one for reading and one for writing. Writer thread has a concurrent linked queue of things to send. Things that want to write add them to the queue. Reader thread posts read message to a central shared server queue.

Server pulls from this queue and does stuff. That stuff may be putting messages on all of the connections output queues.

That’s kind of the way SpiderMonkey does it internally.

Woah, my implementation is almost the same, with the exception of the reader threads having a seperate queue. I’ll try to make them add it to a single queue. Big Thanks to you man for giving me a clear answer. :slight_smile: I’ll reply with the output I get. :smiley:

EDIT: BOO YEA, the networking is workin’! :smiley: I just need to implement a message object that is serializable. Thanks alot. btw my project’s submission is tomorrow, lol.