Search code examples
javaopenglnetworkingjbox2dkryonet

Java Game Networking with Kryonet: Bare-bones packet transfering


I'm using Opengl and Jbox2d to write a real-time 2d game in Java.

I want to start coding the networking components.

Although it uses box2d, my game is very small and I want to create a bare-bones architecture using the Kryonet library.

The program itself is a 'match game' like chess. The most logical system I can think of would be to have dedicated server that stores all player data.

PlayerA and PlayerB will connect with the dedicated server which will facilitate a TCP link between their computers.

When the match is complete, both players will communicate the resulting data back to the dedicated server, which will authenticate and then save over their respective player data.

For those familiar, Diablo2 implemented a similar setup.

I want this TCP connection to simply send the shape coordinates Vector data from the host (lets say playerA) to the client (player B) which the client will then render on its own.

Then I want the client to send mouse/keyboard data back to the host. All of the processing will be run on the host's computer.

My first question: Are there any flaws in this network logic?

My second question: How does one implement barebones server/client packet transferring (as described) using Kryonet?

Note: I have done this exact type of packet transferring in C++ using a different library. The documentation/tutorials I've found for Kryonet are terrible. Suggesting another library with good support is an acceptable answer.


Solution

  • I know this is an old question, and I'm sure OP has gotten their answer one way or another, but for fun I thought I'd respond anyway. This exact question has been on my mind since I've been playing around with game development using Kryonet very recently.

    Some early network games, such as Bungie's Marathon (1994) seemed like they did exactly this: each player's events would be sent using UDP to other players. Thus, if a player moved or fired a shot, the player's movement or shot's direction, velocity, etc. would be sent to other players. There are some problem with this approach. If one of the player's actions are temporarily lost over the network, a player or players appeared to be out of sync with everyone else. There was no "truth" or "reconciliation" of game state in such situations.

    Another approach is to have the players compute their movements and actions client-side and send the updated positions to the dedicated server. With a server receiving all player state updates, there is an opportunity to reconcile them. They also do not become out of sync permanently if some data is lost on the network.

    To compare with the previous example, this would be equivalent of each player sending their positions to the server, and then having the server send each player's position to all the other players. If one of these updates gets lost for some reason, a subsequent update will correct for it. However, if only key presses are sent, a single lost keypress throws the game out of sync because all clients are computing the other clients' positions separately.

    For action games you can use a hybrid approach to minimize apparent lag. I've been using Kryonet successfully in this manner for an action game. Each player sends their state to the server at every render tick (though this is probably excessive and should be optimized). The state includes position, number of shots left, health, etc. The player also sends the shots they take (starting velocity and position.)

    The server simply echos these things back to clients. Whenever a shot is received by a client it is computed client-side, including whether or not the shot hits the receiving player. Since the receiving player only computes their own state, everything appears to stay in sync from their own point of view. When they are hit, they feel the hit. When they hit another player, they believe they've hit the other player. It is up to the player "receiving" a shot to update their health and send that info back to the server.

    This does mean that a shot could theoretically lag or "get lost" and that a player may think their shot has hit another player, while on the other player's screen no hit occurred. But in practice, I've found this approach works well.

    Here's an example (pseudocode, don't expect it to compile):

    class Client {
        final Array<Shot> shots;
        final HashMap<String, PlayerState> players; // map of player name to state
        final String playerName;
        void render() {
            // handle player input
    
            // compute shot movement
            // for shot in shot, shot.position = shot.position + shot.velociy * delta_t
    
            // if one of these shots hits another player, make it appear as though they've been hit, but wait for an update in their state before we know what really happened
    
            // if an update from another player says they died, then render their death
    
            // if one of these shots overlaps _me_, and only if it overlaps me, deduct health from my state (other players are doing their own hit detection)
    
            // only send _my own_ game state to server
            server.sendTCP(players.get(playerName));
        }
    
        void listener(Object receivedObject) {
     
            if(o instanceOf PlayerState) {
                // update everyone else's state for me
                // but ignore my own state update (since I computed it.)
                PlayerState p = (PlayerState)o;
                if(!p.name.equals(playerName) {
                    players.add(p.name, p);
                }
            } else if (o instanceof Shot) {
                // update everyone else's shots for me
                // but ignore my own shot updates (since I computed them.)
                Shot s = (Shot)o;
                if(!s.firedBy.equals(playerName) {
                    shots.add(s);
                }
            }
        }
    }
    
    class Server {
        final HashMap<String, PlayerState> players; // map of player name to 
    
        void listener(Object receivedObject) {
    
            // compute whether anybody won based on most recent player state
    
            // send any updates to all players
            for(Connection otherPlayerCon : server.getConnections()) {
                otherPlayerCon.sendTCP(o);
            }
        }
    }
    
    

    I'm sure there are pitfalls with this approach as well, and that this can be improved upon in various ways. (It would, for example, easily allow a "hacked" client to dominate, since they could always send updates that didn't factor in any damage etc. But I consider this problem out-of-scope of the question.)