1

I am making a multi-player snake game in java using sockets. All the transmission is done through a server to all the connected clients. The code for the same is yet not completely finished but it does the basic job of moving the snakes around and increasing scores if a particular client eats its food.

I generate random numbers for food coordinates from the server side and relay it to all the clients. If a client presses a key the requested movement is calculated and the direction of movement is sent to the server which then relays the movement to ALL clients (including the one who sent it) and only on receipt of the movement info do the clients make changes to the snake which moved. So every movement is tracked over the network and no movement decision is made by the client itself until it receives that, say client 'player1' has asked to move.

The problem I am facing is that even with two players there seems to be a difference in coordinates after moving about the snakes a little.

What possible remedies could I apply to my code so as to remove this apparent lag between the position of snakes?

This is the client code:

package mycode;

import java.awt.Point;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.Map;

import javax.swing.JOptionPane;

public class ConnectionManager implements Runnable {
    Socket socket;
    boolean start = false;
    DataInputStream in;
    DataOutputStream out;
    Map<String, Snake> map;

    ConnectionManager(String name, String IP, Map<String, Snake> m) {
        this.map = m;
        try {
            socket = new Socket(IP, 9977);
            in = new DataInputStream(new BufferedInputStream(
                    socket.getInputStream()));
            out = new DataOutputStream(new BufferedOutputStream(
                    socket.getOutputStream()));
            out.writeUTF(name);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
            JOptionPane.showMessageDialog(null, "Could Not Find Server",
                    "ERROR", JOptionPane.ERROR_MESSAGE);
            System.exit(0);
        }
    }

    void populateMap() {
        try {
            String name = in.readUTF();
            System.out.println("Name received: " + name);
            if (name.equals("start_game_9977")) {
                start = true;
                System.out.println("Game Started");
                return;
            } else if (name.equals("food_coord")) {
                Game.foodx = in.readInt();
                Game.foody = in.readInt();
                return;
            }
            map.put(name, new Snake(5));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    boolean start() {
        return start;
    }

    void increaseSnakeLength(String thisname){
        Snake temp = map.get(thisname);
        Point temp1=new Point(0,0);
        temp.length++;
        switch (temp.move) {
        case DOWN:
             temp1= new Point(temp.p[temp.length - 2].x,
                    temp.p[temp.length - 2].y+6);
             break;
        case LEFT:
            temp1= new Point(temp.p[temp.length - 2].x-6,
                    temp.p[temp.length - 2].y);
            break;
        case RIGHT:
            temp1= new Point(temp.p[temp.length - 2].x+6,
                    temp.p[temp.length - 2].y);
            break;
        case UP:
            temp1= new Point(temp.p[temp.length - 2].x,
                    temp.p[temp.length - 2].y-6);
            break;
        default:
            break;
        }
        if(temp1.y>Game.max)
            temp1.y=Game.min;
        if(temp1.x>Game.max)
            temp1.x=Game.min;
        if(temp1.y<Game.min)
            temp1.y=Game.max;
        if(temp1.x<Game.min)
            temp1.x=Game.max;
        temp.p[temp.length-1]=temp1;
    }

    void readMotion() {
        try {
            while (true) {
                if (Game.changedirection) {
                    String mov = "";
                    mov = Game.move.name();
                    // System.out.println(Game.move);
                    out.writeUTF(mov);
                    out.flush();
                    Game.changedirection = false;
                }
                if (Game.foodeaten) {
                    out.writeUTF("food_eaten");
                    out.flush();
                    Game.foodeaten = false;
                }
                Thread.sleep(50);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    void otherRunMethod() {
        try {
            while (true) {
                String mname = in.readUTF();
                String mov = in.readUTF();
                if (mov.equals("Resigned")) {
                    map.remove(mname);
                } else if (mov.length() >= 10) {
                    if (mov.substring(0, 10).equals("food_eaten")) {
                        String[] s = mov.split(",");
                        Game.foodx = Integer.parseInt(s[1]);
                        Game.foody = Integer.parseInt(s[2]);
                        int score = ++map.get(mname).score;
                        increaseSnakeLength(mname);
                        System.out.println(mname + ":" + score+" Length:"+map.get(mname).length);
                    }
                } else {
                    Game.move = Direction.valueOf(mov);
                    map.get(mname).move = Game.move;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            if (!start) {
                populateMap();
            } else if (start) {
                new Thread(new Runnable() {
                    public void run() {
                        otherRunMethod();
                    }
                }).start();
                readMotion();
                break;
            }
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

The code is pretty long so I am just putting up the server side of code that manages connections.

package mycode;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.util.Map;

public class Playerhandler implements Runnable {
    Socket player;
    String thisname;
    Map<String, Socket> map;
    DataInputStream in = null;
    DataOutputStream out = null;
    ObjectInputStream ob;
    Snake snake;

    Playerhandler(Socket player, Map<String, Socket> m) {
        this.player = player;
        this.map = m;
        try {
            in = new DataInputStream(new BufferedInputStream(
                    player.getInputStream()));
            thisname = in.readUTF();
            map.put(thisname, this.player);
            populatePlayers();
            System.out.println("Connected Client " + thisname);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    void populatePlayers() {
        try {
            out = new DataOutputStream(new BufferedOutputStream(
                    player.getOutputStream()));
            for (String name : map.keySet()) {
                out.writeUTF(name);
                out.flush();
            }

            for (String name : map.keySet()) {
                out = new DataOutputStream(new BufferedOutputStream(map.get(
                        name).getOutputStream()));
                out.writeUTF(thisname);
                out.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    void relay(String move) {
        try {
            if (move.equals("food_eaten")) {
                move = move + ","
                        + (Snakeserver.randomGenerator.nextInt(100) * 6) + ","
                        + (Snakeserver.randomGenerator.nextInt(100) * 6);

            }
            for (String name : map.keySet()) {
                out = new DataOutputStream(new BufferedOutputStream(map.get(
                        name).getOutputStream()));
                out.writeUTF(thisname);
                out.flush();
                out.writeUTF(move);
                // System.out.println(Direction.valueOf(move));
                out.flush();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void run() {
        while (true) {
            try {
                relay(in.readUTF());
            } catch (IOException e) {
                // TODO Auto-generated catch block
                System.out.println("Player " + thisname + " Resigned");
                map.remove(thisname);
                relay("Resigned");
                return;
            }
        }
    }

}
emecas
  • 1,586
  • 3
  • 27
  • 43
Sohaib
  • 4,556
  • 8
  • 40
  • 68
  • you write that there is a difference in position of the snakes between the clients. The first approach to this I would take would be to log all of the movement messages that are received by each client and then compare the logs. Since the clients are not supposed to do a move until they receive a move message, I would expect that all incoming logs would have the same number of messages and the same sequence of messages. What I would be interested in knowing is whether the clients are all receiving the same messages in the same sequence as a starting point for figuring this out. – Richard Chambers Mar 31 '13 at 12:26
  • Thanks Richard, I will surely check this out and tell you. Although from what I printed on the console I guess it was the same. – Sohaib Apr 03 '13 at 09:05
  • One other question is whether the movement coordinates are relative to the current position that is held by each client or are they absolute coordinates in the shared virtual world so that the server maintains all location coordinates and tells all clients where all snakes are located. I can not tell reading over the source you provided. I ask because if each client maintains their own locations with movements specified relative to client's current position, this could introduce variability. – Richard Chambers Apr 03 '13 at 10:42
  • I guess it is the latter. Actually the client or server are not sending the coordinates at all. They are just sending say 'snake 1 moved left' so each client moves snake 1 left. I could not devise a logic to pass coordinates cause it would mean sending the whole object over network. – Sohaib Apr 03 '13 at 11:42
  • A good data point would be to compare what each client thinks is the position of the snakes versus what the server thinks is the position of all of the snakes. Also does each snake have a vector that indicates not only position but also direction of facing? From what you wrote above it is as if the position changes are relative to the current position and the current direction of facing. The direction of left will change depending on facing. While message delay may be a part of this problem, it seems best to me to make sure that the messages and message order are correct first. – Richard Chambers Apr 05 '13 at 13:01
  • U are right about that part. About change in direction happening in response to the previous direction. A vector is not maintained its based on only current direction. Ill see that log thing currently and report back in a short while. – Sohaib Apr 06 '13 at 06:44
  • I have maintained the logs as you say and have found out that they are exactly the same for any number of clients in the same order. What I deduce from that is that my lag is happening due to one snake moving more by the time the communication to turn right reaches it. – Sohaib Apr 06 '13 at 11:05
  • So should the be a turn based game in which everything is synchronized by each client taking a turn perhaps through some kind of token passing scheme or is it supposed to be real time and not turn based? The synchronization thing can be a bear to get right because you basically have to have some kind of a global clock which is used by all of the clients. So part of your design may require some kind of a time pulse that is sent out every second or half second to tell all clients, move your snakes. – Richard Chambers Apr 06 '13 at 20:10
  • Well I understand your point. I am changing the whole sending and receiving part of my program and sending objects instead of tokens. I guess since I didn't maintain a global clock it was all going awry. Now I am sending the whole object of snake. But ObjectStreams are tedious to deal with. – Sohaib Apr 07 '13 at 06:37
  • Let us know how it goes with these changes. The problem is that without some kind of a global clock synchronizing things what happens is that some clients will tend to get ahead of others or get behind others due to various kinds of lag or small delays. If there is no way to detect variances and correct them, they tend to have a kind of positive reinforcement which makes the differences between clients worse and worse. The main thing is for all clients to have a reasonably accurate shared view of what the world looks like. Relative movement within each client makes this difficult. – Richard Chambers Apr 07 '13 at 22:54
  • Sending a whole object stream has completely resolved my errors related to respective positions of snakes in different clients. But I sometimes get a stream corrupted error (Very occasionally but still a problem). Also if I run many clients and host the game on my router and play it over the net there is some lag seen which gets corrected tho since I am passing whole objects of the core snake class (This class has everything like length of snake, array of coordinates, Color of snake, score etc.. ) – Sohaib Apr 09 '13 at 20:29
  • I think this much lag would remain. I wonder how do games like COD etc run when a small game like mine is showing lag... – Sohaib Apr 09 '13 at 20:30
  • My understanding is that most games use UDP/IP rather than TCP/IP just as most video streaming also uses UDP. UDP is a simpler protocol with less overhead and when a UDP packet is sent out, it goes immediately as there is none of the sequencing, etc. of TCP. UDP is not a guaranteed protocol in the sense that many of the benefits and guarantees of TCP are not provided and UDP packets may not be delivered or may be delivered out of sequence. However it is fast. – Richard Chambers Apr 10 '13 at 12:20
  • Yeah I agree, since there's no error reporting mechanism in UDP so its generally faster. Is there a way I could use that protocol in java?? – Sohaib Apr 10 '13 at 17:04
  • Yes there is a way to use UDP in Java. take a look at http://docs.oracle.com/javase/tutorial/networking/datagrams/ followed by http://systembash.com/content/a-simple-java-udp-server-and-udp-client/ which has source and then this stackoverflow http://stackoverflow.com/questions/3997459/send-and-receive-serialize-object-on-udp-in-java – Richard Chambers Apr 11 '13 at 11:25

3 Answers3

2

This answer is to recap the dialog to arrive at a solution as well as to point some additional areas to research or try out.

The main software behavior problem was that having multiple clients resulted in the various clients showing different snake positions after several moves.

After a number of questions and responses through the comments, the poster of the question modified their software so that all of the clients are synchronized by the server sending out the objects of all of the snakes to all of the clients so that all clients are now using the same snake object. Previously each client was maintaining its own snake object data and just receiving changes or deltas in snake data. With this change, all of the clients are now synchronized through the snake object transmitted by the server however there is still a problem with clients showing slightly different positions which is corrected after a moment or two, as each client receives an update on all of the snakes, the clients become synchronized again.

The next step is to look at a different approach so that the clients will remain synchronized more closely using UDP/IP as the network transmission protocol rather than the currently used TCP/IP. The expected results of using UDP/IP is to reduce the various lags introduced by the TCP network transmission protocol in order to provide the connection oriented, sequenced byte stream provided by TCP. However using the UDP network transmission protocol requires that some of the delivery mechanisms used by TCP in order to provide the dependable sequence of bytes must be assumed by the user of UDP.

Some of the issues with UDP are: (1) packets may not be received in the same sequence in which they are sent, (2) packets may be dropped or lost so that some packets sent may not be received, and (3) data sent using UDP must be explicitly put into packets for transmission so that the sender and the receiver see packets rather than a stream of bytes.

The basic architecture for this snake game would look something like the following.

Clients would send a snake update to the server. This interaction would require an acknowledgement sent by the server back to the client. If the client does not receive such an acknowledgement, the client would resend the snake update after some time period.

The server would then update its data to reflect the change and using its list of clients, send the same data packet to all clients. Each client receiving the packet would send an acknowledgement. By sending the acknowledgement, each client notifies the server that they are still in the game. If the server is no longer receiving client acknowledgements, it will know that a client has possibly left the game or there is some kind of network problem.

Each packet would have a sequence number which is incremented after sending the packet. This sequence number gives a unique identifier so that clients and server can detect if packets have been missed or if a packet received is a duplicate of an already received packet.

With UDP it is best if packets are as small as possible. UDP packets that are larger than what can be sent in the underlying IP network protocol will be split up into multiple IP packets with the multiple IP packets sent one at a time and then reassembled into the UDP packet at the receiving network node.

Here are some resources on UDP network protocol using the Java programming language.

Lesson: All about datagrams.

A simple Java UDP server and UDP client.

Stackoverflow: Send and receive serialize object on UDP in java.

Java-Gaming.org UDP vs TCP.

Gaffer On Games: What every programmer needs to know about game networking.

Gaffer On Games: Reliability and Flow control.

Stackoverflow: What are possible ways to send Game/Simulation state with javaNIO?

Community
  • 1
  • 1
Richard Chambers
  • 16,643
  • 4
  • 81
  • 106
  • 1
    Well that really sums it up Richard. Thank you for your guidance. One more thing I would like to point out is that working with ObjectOutputStreams often prompts StreamCorrupted Exceptions. It is important to note that before an input stream is defined for an object the class (or server or client in this case) should define their OutputStreams for the class getting the data(simultaneously with the declaratin of the inputstream before data is recieved) failing which a stream corrupted exception is generated by java. – Sohaib Apr 13 '13 at 07:56
0

I've never implemented a network multiplayer game before, but I think the most widely used 'solution' here is to cheat.

I think its referred to as 'dead reckoning', although snake works exactly like this anyway.

http://www.gamasutra.com/view/feature/3230/dead_reckoning_latency_hiding_for_.php

Basically you decouple the game loop from network updates. Have each client keep its own state, and simply predict where the opponents are going to be at each frame. Then when updates from the server arrive you can adjust opponents to their true location. To hide this discrepancy, I think its common to render the state of the game as it was a few milliseconds ago, rather than the current state. That way the network updates have a more realistic chance of catching up with the game loop, so it will seem less choppy.

As I said though, I've never actually implemented this myself so YMMV. This is one of the harder problems in game development.

Zutty
  • 5,357
  • 26
  • 31
0

I would be inclined to add an explicit call of setTcpNoDelay(true). This will make sure that http://en.wikipedia.org/wiki/Nagle%27s_algorithm is turned off and so disable an optimization that increases efficiency at what is usually a small amount of increased delay.

mcdowella
  • 19,301
  • 2
  • 19
  • 25
  • Thanks mcdowella I am using testing this currently on localhost only. Would TCP delay be that much of a problem in local network also? How exactly do we implement this method? – Sohaib Apr 03 '13 at 09:08
  • It is a method on Socket - see http://docs.oracle.com/javase/1.5.0/docs/api/java/net/Socket.html and http://www.jguru.com/faq/view.jsp?EID=42242. It saves you some multiples of packet delay, which should in theory be small on local host but conceivably be noticeable if it includes some operating system overhead. – mcdowella Apr 04 '13 at 04:43
  • Thank you. Ill include the method and tell you the results. Actually my term papers are till Friday. So Ill work on the code after that only. – Sohaib Apr 04 '13 at 15:21
  • I did include that method. I tried I think it improved it a bit but the overall lag still remains unfortunately. Would be glad if I could get some other ideas. – Sohaib Apr 06 '13 at 14:06