Lag in TCP/IP lockstep model RTS game

Started by
8 comments, last by Kylotan 11 years, 2 months ago

Hi to everyone!

I have been dealing with this problem for weeks and I can't find the root of it.

I'm working on a RTS game, it uses lockstep to communicate.

At the moment it's just 2 players.

I'm going to show you the code of sending/receiving.

Information is an object containing a matrix of 5x6 integers.

settcpnodelay is done outside.

there is only one Thread


            SleepABit();
            ReceiveData();           
            ScheduleDutys();
            UpdateAllUnits();
            SendData();      
            DrawGraphics();
            step = StepLookAhead(step,1);
 


private void ReceiveData()
    {

        try{
                if(whoisthis == 1) //for server
               {
                    indata1 = new ObjectInputStream(sk.getInputStream());
                   
                    info_other1 = (Information) indata1.readObject();
                    ProcessInformation(info_other1); //process client info
                   
                    ProcessInformation(info); //process own info
                   
                   
                }
                else //for client
               {
                    indata1 = new ObjectInputStream(sk.getInputStream());
                    indata2 = new ObjectInputStream(sk.getInputStream());
                   
                    info_other1 = (Information) indata1.readObject();
                    info_other2 = (Information) indata2.readObject();
                   
                    ProcessInformation(info_other1);
                    ProcessInformation(info_other2);
                   
                   
                }
               }catch(IOException ioe){} catch(ClassNotFoundException css){System.out.println("nothing to do here");}
   
    }
 


private void SendData()
    {
       
         
        try{

              if(whoisthis == 2) //this is client
             {
                outdata1 = new ObjectOutputStream(sk.getOutputStream());
               
                outdata1.writeObject(info);
                info.CleanInformation();
                   
                info_other1.CleanInformation();
                info_other2.CleanInformation();
               
               
              }
              else //this is server
             {
                  outdata1 = new ObjectOutputStream(sk.getOutputStream());
                  outdata2 = new ObjectOutputStream(sk.getOutputStream());
                 
                  outdata1.writeObject(info); //send server info
               
                  info.CleanInformation();
                   
                   outdata2.writeObject(info_other1); //send client info
                 info_other1.CleanInformation();
                 
              }

             
            }catch(IOException ioe){}
    }
 

I know the problem is there, in one of those codes, because when I try it in localhost mode, it works excellent, but when it comes to playing with another player through LAN...it lags, and I really don't know why.

So, I hope you can help me here, thanks ! bye!

Advertisement

Can you describe the symptoms in more detail. Is it occasional "bursts" of latency or is it more or less constant? What kind of latency are we talking about, tens or hundreds of milliseconds or is the delay on the order of seconds or more?

Have you examined the traffic on the network? It might be enlightening to use something like Wireshark to see what is going on. Does the Information class contain any data other than this matrix? Java serialisation will by default serialise the entire object graph.

Other than that, you appear to be doing lots of unnecessary allocations. You probably can wrap the socket's input/output stream in an Object(Input|Output)Stream just once and re-use the wrapper stream over and over. You don't need to create a new one for each object you wish to write or read.

I'd worry about your apparent lack of error handling. Remember if you don't like propagating checked exceptions you can wrap them in RuntimeException:

try {
    dangerousStuff();
} catch( SomeCheckedException e ) {
    throw new RuntimeException(e);
}

At least then when an error occurs you will know about it and get a useful stack trace - though your users will probably tahnk you if your application is robust in the face of errors. Don't crash to desktop because they're connection is lost.

Well, whats happens is that when trying the game with a lan connection, the FPS get down to lie 10, with little burst of lag every 4 seconds, with a duration of half a second.

I don't know how to use Wireshark, but I'm sure it's not a traffic problem...

The Information contains two methods with 20 lines each, and the matrix of 5x6 integers.

I will have in mind your advice of just sending one object.

What's the difference in your words, between using runtime exception and normal exception??

thanks for your advice!

Well, whats happens is that when trying the game with a lan connection, the FPS get down to lie 10...

What is the frame rate when you are not using a LAN? Also consider measuring fractions of seconds per frame, rather than frames per second.

The network should not affect the frame rate. If you are waiting on the network before drawing, you will get some latency. If you decouple direct network access, game logic and rendering, you can present a responsive user interface regardless of the speed of the network.

... with little burst of lag every 4 seconds, with a duration of half a second.

These bursts could be caused by garbage building up. It would be interesting to watch your program in a memory profiler. This problem can be reduced by being more conservative with allocations. If you cannot eliminate runtime allocation (which is difficult to do in Java), then you might consider periodically hinting that the runtime call the garbage collector when it suits you, for example as when the time per frame is low.

I don't know how to use Wireshark, but I'm sure it's not a traffic problem...

You'll probably have to use a tool like Wireshark eventually, if you are developing a networked game. That you do not currently know how to use a tool is not sufficient justification for not using it.

I will have in mind your advice of just sending one object.

You misunderstand. I don't mean send one object - you need to send exactly the number of objects you need to send, no more and no less. What I was saying is that you can send or receive multiple objects using the same ObjectInputStream instance - there is no need to create many of these.

What's the difference in your words, between using runtime exception and normal exception??

RuntimeException is a normal exception. However, any exceptions that derive from RuntimeException are not required to be spelled out as part of the method signature.

Let us take a simple example program (warning: code not compiled or tested):

public class Example {
 
    public static void foo() throws IOException {
        if(Math.random() > 0.5) {
            throw new IOException("Whoops!");
        }
        System.out.println("Hello, world");
    }
 
    public static void bar() throws IOException {
        foo();
    }
 
    public static void main(String [] args) {
        try {
            bar();
        } catch( IOException e ) {
            System.err.println("An unexpected I/O failure occurred: " + e.getMessage());
        }
    }
 
}

Note how the intermediate method, "bar", also needs to declare that it throws an IOException. Some people find this kind of explicit propagation very tedious, and in extreme cases can causes certain high level functions to throw multiple types of unrelated exceptions.

Now let us example what happens when we use a RuntimeException instead:

public class Example {
 
    public static void foo() {
        if(Math.random() > 0.5) {
            throw new RuntimeException("Whoops!");
        }
        System.out.println("Hello, world");
    }
 
    public static void bar() {
        foo();
    }
 
    public static void main(String [] args) {
        try {
            bar();
        } catch( RuntimeException e ) {
            System.err.println("An unexpected error occurred: " + e.getMessage());
        }
    }
 
}

Notice how "bar" no longer has to say that it also throws an exception? But also notice that we've lost some information, before we knew in main when an I/O error occurred, now it could be any kind of error.

Of course, we can create a RuntimeIOException class, if this information is important:

public class RuntimeIOException extends RuntimeException {
    // Constructors, etc, etc
}
 
 
public class Example {
 
    public static void foo() {
        if(Math.random() > 0.5) {
            throw new RuntimeIOException("Whoops!");
        }
        System.out.println("Hello, world");
    }
 
    public static void bar() {
        foo();
    }
 
    public static void main(String [] args) {
        try {
            bar();
        } catch( RuntimeIOException e ) {
            System.err.println("An unexpected I/O failure occurred: " + e.getMessage());
        }
    }
 
}

With this hybrid approach, we avoid having to declare exceptions while retaining information about the nature of the error.

Of course, this only matters if you care about the nature of the error. In your case, what you might want to do is have somewhere reasonably high level which can catch network errors and retry or gracefully terminate the game session, explaining to the user that the network caused the problem. So you only need to introduce a class such as RuntimeIOException if this allows you to reasonably differentiate between different error causes.

Whatever you decide to do, the worst option is silently swallowing such errors. The second worst thing is swallowing them but printing them in some oft-neglected log. Someday you're going to get a bug report, and you'll have to figure out what is happening. If you eat exceptions without telling anyone, then don't be surprised if "impossible" situations start occurring, because your program bravely continues in the face of adversity.

you can send or receive multiple objects using the same ObjectInputStream instance

You can also poke out multiple eyes with a single fork, and gag multiple people with a single spoon (although not all at the same time.)

I've been trying to not comment on this, but I think that the object serialization support in any of the large runtimes (JVM, CLR, etc) is not suitable for game networking. The overhead on the wire is typically large, the guarantees are typically a poor match for real-time updates, and it's much harder to figure out what's going on on the wire.

Most games define a low-level protocol in the form of byte layouts, and write custom marshaling code that converts between game-level messages, and wire-level messages. Then you frame each of these messages in a small bit of header (say, a byte for message type, and a byte for length) and in turn frame multiple of those messages into a single "tick" packet with a header containing things like "the tick this message is for," "the number of messages" etc.

For an RTS, you will then send tick messages with commands for some tick in the future. If you're running physical simulation at 10 ticks per second, you may send commands for tick 20 when you're displaying tick 15. This gives you 500 milliseconds of de-jitter latency that lets everybody get the same data for the same tick. The fact that the user gave a command at tick 15, and the command doesn't take effect until tick 20, is hidden with the "yes, sir!" animation on the issuing user's machine.

Other than that, I second rip-offs comment: You'll need to use Wireshark to complete a networked game. Might as well start now!
enum Bool { True, False, FileNotFound };

well, Within a localhost connection, the speed is 30 fps, and with a LAN, it
drops to 10...
I downloaded Wireshark, and shit, it's a mess! I didn't understand anything! looks like
I'm going to need a tutorial...


I understood your explanation of runtimeexception, thanks! it was very detailed!

I'm considering the option hplus0603 gives me, I hope that works! I must redesign
the send/receive system...oh well....

I'm going to post news as long as I finish the new protocol.

thanks for helping!

Typically, you will want to filter TCP connections using the particular port that your protocol uses. Otherwise you'll see all kinds of requests -- computers are pretty spammy, what with file sharing, DNS look-ups, domain controllers, ARPs, DHCP keep-alives, iTunes / Windows Media Center, and what else is going on on a "quiet" network.

Within a localhost connection, the speed is 30 fps, and with a LAN, it drops to 10

First things first: Have you turned on the TCP_NODELAY option on the socket? (Called something like NoDelay in .NET)
enum Bool { True, False, FileNotFound };

I think your problem is probably such a common problem than well, everyone tends to forget it. Did you by chance forget to turn off Nagle?

The OP did say:

settcpnodelay is done outside.

However, there doesn't appear to be an explicit flush() anywhere, I don't know if there is any buffering done by some of the intermediate classes.

I've been trying to not comment on this, but I think that the object serialization support in any of the large runtimes (JVM, CLR, etc) is not suitable for game networking. The overhead on the wire is typically large, the guarantees are typically a poor match for real-time updates, and it's much harder to figure out what's going on on the wire.

While very true, and pretty much a necessity for production games, I think that the default serialisation is an acceptable way to bootstrap such a game.

My gut instinct is that the overhead on the wire is probably not the root cause of the dramatic performance jump, provided the OP is not mistaken about the scope of the object being sent. On my system a simple object with a 5 x 6 naive matrix of native "int" (i.e. not packed into a single dimensional array, not Integer objects stored in a Collection) appears to be 247 bytes long, which is approximately twice as large as necessary.

I think the OP might better spend their time decoupling the game logic from the rendering first, and leave optimising the data transfer for later.

@OP: Does the Information class have any super classes, and if so do any of them have fields of non-primitive type? What is the type of your matrix?

I don't know these Java serialisation and streaming classes but it looks like each side will wait for the other side to send its data, but the other side could be busy rendering. If that's the case, you're going to waste a lot of time while data's in transit.

This topic is closed to new replies.

Advertisement