Tag Archive for 'opengta2'

Page 2 of 10

OpenGBH basic client prediction

Added basic client prediction today, it’s very buggy right now, and it only corrects the prediction errors when you are standing. Client prediction is required to make local player see himself as instantly moving, instead of having to wait say… 200 milliseconds for information about his movement to arrive back from the server.

That is very annoying, so client prediction just makes him walk right away, later correcting for errors in his position knowing true position (it arrives from server a bit later). But right now it only synchronizes position when you stop moving (and release keys), so you sometimes jump really far.

The prediction history (all predicted positions) is stored in an array, so it can verify history of entire trail later. You can even see the trail by typing in a console command (cl_showpredict 1):

OpenGBH fixed packet flow

Whoops. Made a really silly mistake, and the game was only processing single packet per frame, which resulted in small data flow distruptions causing networking to “freeze” for few seconds (which visually was visible as a network lag). Now I have this code instead:

while (enet_host_service(Host, &enetEvent, 0)) {
  ...
}

Now instead of the long red blocks as you might have seen in the previous post, we have a large spikey instead:

OpenGBH visual interpolation

I have added a new feature to engine – visual interpolation. This allows image to look slightly smoother in a singleplayer games when the game is heavily lagging.

This has nothing to do with the networking, it simply increases visual framerate from 30 frames per second generated by the server to amount generated by rendering thread. This means that if your physics simulation (of cars and pedestrians) runs at 30 FPS, it is increased to say 500 FPS your rendering generates.

It doesn’t really make much of a difference for server FPS 60 FPS, but things look a lot smoother. And if your game lags down to 10 FPS, it still looks running smoothly.

On related note, I have added a frame history indicator, and changed net graph to look a bit nicer. The frame history bar is the first one. Blue means interpolated frames, red means extrapolated frames, and gray means reliable frames received from server:

OpenGBH Networking Protocol (part 3)

The packets sent between the server and client are delta-compressed. This means that only the revelant data is sent, although this also describes how the game treats the data – it will compress the data from the two frames every time it generates and sends a message. Two corresponding calls are:

void deltaCompress(frame_entry* old_frame, frame_entry* new_frame, Network_Message* Msg);
void deltaDecompress(frame_entry* old_frame, frame_entry* new_frame, Network_Message* Msg);

The compression is pretty simple. There is 8-bit bitfield that indicates what parts of message are sent, and also a separate 8-bit field for every pedestrian or client (only those are sent right now). The “master” bit field indicates whether pedestrian or client data update is sent in this message.

If a pedestrian data update is sent, then an integer follows which specifies total amount of pedestrians which have updated in this frame. Then, for every pedestrian there is a bit field, which has the following flags:

  • LowID – pedestrian ID is a low number (less than 65536 or 256, I forgot)
  • ControlInfo – there is update in pedestrian ID or client that controls him
  • Animation – there is an update in animation data
  • Position – position has changed
  • Velocity – velocity has changed
  • Z – position or velocity changed on Z axis
  • Heading – heading changed

The decompression function simply reads the corresponding data from an incoming message when flag is set. The compression routine looks like this:

int bLowID =
	(i <= 0xFF);
int bSendControlInfo =
	(old_frame->PedData[i]->ID != new_frame->PedData[i]->ID) ||
	(old_frame->PedData[i]->controlClient != new_frame->PedData[i]->controlClient);
int bSendAnimation =
	(old_frame->PedData[i]->animationStart != new_frame->PedData[i]->animationStart) ||
	(old_frame->PedData[i]->currentAnimation != new_frame->PedData[i]->currentAnimation);
int bSendPosition =
	(old_frame->PedData[i]->Position != new_frame->PedData[i]->Position);
int bSendVelocity =
	(old_frame->PedData[i]->Velocity != new_frame->PedData[i]->Velocity);
int bSendZ =
	(old_frame->PedData[i]->Position.z != new_frame->PedData[i]->Position.z) ||
	(old_frame->PedData[i]->Velocity.z != new_frame->PedData[i]->Velocity.z);
int bSendHeading =
	(old_frame->PedData[i]->Heading != new_frame->PedData[i]->Heading);

You can see that it just changes for change, no special hacks required. Then, depending on value of those flags, it sends the data it should. To keep things simple, it also sends pedestrian index in pedestrian array, so server and client will always have same array of pedestrians (even though they might have different indexes). This is a small point of confusion right now, because all peds are indexed using an unique key of type PedID, which increments every time a new ped is created, while an index in array might be same for the two peds in the two different timespans.

For client update, every client entry has the following data flags:

  • Nickname – nickname has changed
  • Input – pressed keys have changed
  • ControlData – players ped has changed
  • NetworkData – information about players network quality has changed

OpenGBH Networking Protocol (part 2)

See extra corrections: http://brain.wireos.com/?p=2197

When game is running, all data is sent between the two sides using two network messages: FRAME and USERFRAME (or I might call it COMMAND instead..?). FRAME is delta-compressed entire state of the game world, and USERFRAME is delta-compressed entire state of the client machine.

Delta-compression means that only changes are sent. These packets are unreliable, and one or more of them may be dropped from the stream. For this case, all FRAME packets are numbered. They have a sequence number on each of them. Right now it is the same thing as frame counter in server.

Every server frame is numbered with sequence number it was created for. The USERFRAME packet carries the last received sequence number from the client. Server will always delta-compress packets against last received (acknowledged) sequence number from the client. Typical transmission looks like this:

The server keeps sending messages at rate of 15 messages per second, compressed against a last known acknowledged frame. The client sends out USERFRAMEs 15 times a second, and every time you press a key. Packet data itself isn’t compressed yet. Typical packet size for FRAME packet – anywhere between 30 and 700 bytes (depends on amount of players, and how many updates there are), and for the USERFRAME it’s about 10 bytes.

When client receives a message, it checks whether it is out of order (if so, it drops the message), and alters frame history to account for new incoming data. This means that all frames between last reliable packet from server and the new received one will be interpolated. All frames which follow reliable packet are automatically extrapolated.

Additionaly local client will keep extrapolating new frames at the same rate as server runs. This assures that even if there is a data loss, packets will keep arriving for the rendering system. I think I’ll detail frames system a bit more in next post.

The related code looks like this. I’ll coment parts of it. Here is the header, where it checks whether frame is not out of order (the frame must be useful to the game state):

if (newSequence > Frames.LastSequence) {

The game decompresses frame as the newest one based on last acknowledged frame:

  Frames.deltaDecompress(Frames.LastFrame,Frames.Temporary,Msg);
  Frames.setFrame(Frames.LastFrame,Frames.Temporary);
  Frames.LastSequence = newSequence;

This code finds index of frame that corresponds to sequence we just received:

  int frameIndex = -1;
  for (uint i = 1; i < historyLength; i++) {
    if (Frames.History[i]->Sequence == newSequence) {
      frameIndex = i;
      break;
    }
  }

If we found the frame, then extrapolate all frames that precede it (the ones which are newer, and yet unknown). Otherwise just make it newest one:

  if (frameIndex >= 0) {
    Frames.setFrame(Frames.History[frameIndex],Frames.LastFrame);
    for (int j = frameIndex; j > 0; j--) {
      Frames.extrapolateFrame(Frames.History[j-1],Frames.History[j]);
      Frames.History[j-1]->Sequence = Frames.History[j]->Sequence+1;
    }
  } else {
    frameIndex = 0;
    Frames.setFrame(Frames.History[0],Frames.LastFrame);
  }

Find reliable frame preceding this one:

  int reliableFrame = 0;
  for (uint j = frameIndex+1; j < historyLength; j++) {
    if (Frames.History[j]->reliableFrame) {
      reliableFrame = j;
      break;
    }
  }

Interpolate all frames between previous reliable one and new reliable one:

  if (reliableFrame) {
    for (int j = frameIndex+1; j < reliableFrame; j++) {
      int firstIndex = frameIndex;
      int lastIndex = reliableFrame;
      float T = ((float)(j - firstIndex)) / ((float)(lastIndex-firstIndex));
      Frames.interpolateFrame(
        Frames.History[reliableFrame],
        Frames.History[frameIndex],
        Frames.History[j],
        T);
      Frames.History[j]->Sequence =
        Frames.History[reliableFrame]->Sequence+(j-lastIndex);
    }
  }
}

The related code is in server/frame.cpp. Search for these functions:
Frame_Manager::Frame() is the server frame sending loop.
netCallback_FRAME is the client message handling callback.

OpenGBH Networking Protocol (part 1)

I’ve decided to sum up all what I’ve learned and researched when working on the OpenGBH networking protocol. This is far from final, I’m still working on things, so stuff might change!

Right now the protocol is pretty simple. I based it off Quake3 networking, and with some extra ideas from Valve’s wiki page on Source engine networking. It is based on UDP, a fast protocol, but without any packet loss checks. As a low-level transport I am using the ENet library, it provides connection management and related stuff. It can also send reliable packet streams (reliable means they will be delievered to target computer, even if packet is lost, another one is sent in its place).

The connection to server is pretty simple. To connect to server, client must send a CHALLENGE packet. Currently this packet contains protocol version, and later it might contain server password. This means that client requests to open data channel between server and client, it must supply information required for server to verify his identity.

Server might reject client at this point, in case password is invalid, or the clients IP is banned. On success to will respond with CHALLENGERESPONSE message, which contains unique 4-byte identifier. Every client has an unique identifier, which can be used as key. Right now it serves no other purpose than connecting players to the game, and eventually it will also help checking against automated connection attempts.

After client was successfully accepted by server, it can send a CONNECT packet. At this moment it only contains the 32-byte nickname client wants to use (it is not guranteed that this nickname will be available). Upon successfully spawning player server will instantly start sending him data.

All related sourcecode available here: server/protocol.cpp.

After connection is established, server is sending world state to the client every second server frame (if it is running at 30 FPS, it sends 15 updates per second). The client will send a reply 15 times a second, irregardless of whether it receives server updates or not. These two packets are called FRAME and USERFRAME.

They are both delta-compressed, see more information in the next post.

OpenGBH networking update

One more networking update. I have improved delta compression a lot, now it sends a special bitmask for every client and pedestrian, which decreases amount of data sent. It doesn’t even send Z coordinate of pedestrians, unless it is changing (because usually you remain standing on same elevation). The clients send control input and last sequence acknowledgment to server many times per second.

I have tested it under load of 32 players, with different network connections. The latter one is provided by DummyNet, which can install as a network service for Windows, or for any other OS, and allows to emulate specific internet connection (with various packet loss, bandwidth limits, delays).

The results are fairly good, the data traffic when all players are on screen (so they all are synchronized) is 10 – 16 kb/sec. There are no lags, unless a packet loss occurs (which might result in game skipping over some data, it’s visually noticable). The delta compression is actually pretty noticable, for example the difference between 10kb/sec and 16kb/sec is only in transferring the Z coordinate of position and velocity (when all players walk on slope, there’s more data sent).

Later, when clients off-screen are properly cut off, the data rates will be even less. And that’s for non-AI players, AI players can be synchronized by running simulation at the same time on both machines – and only rarely resynchronized!

Some images, you may notice new layout of perfomance displays, and it now tracks all packets sent and received (including service packets):

There’s a command “net_reset” which resets client state, and requests user to resend all the data from scratch. But don’t worry, later I’ll add a rate limit, so calling that function too often might result in server rejecting your connection (which disconnects you from server).

OpenGBH frames update

I updated frames system and added basic delta compression to networking. This means that only stuff that changed will be sent over the network. This allows to reduce traffic a lot. See one of next posts for exact description of the networking protocol and all concepts.

Anyway, the frames system is much nicer now, it’s no longer stored in one array, but now there are to arrays of frames – history (which stores last several frames), and storage (which stores last frame for every user). The purpose of storage is to allow more efficient delta compression, it stores last server frame for every connected user.

There are following functions to work with frames now (including delta compression):

//Set frame to contents of different frame
void setFrame(frame_entry* dest, frame_entry* src);
//Clear frame contents
void resetFrame(frame_entry* dest);
//Allocate frame
void allocateFrame(frame_entry* dest);
//Release frame
void releaseFrame(frame_entry* dest);

//Delta-compress two frames into net message
void deltaCompress(frame_entry* old_frame, frame_entry* new_frame, Network_Message* Msg);
//Decompress frame based on old one from net message
void deltaDecompress(frame_entry* old_frame, frame_entry* new_frame, Network_Message* Msg)

They should be more or less descriptive, if not – you will have to wait until the protocol description. Oh, and here’s a picture, I added a network graph. Red is pedestrian data, blue is client data (received), it works kind of like in Source engine:

My primary goal now is to complete networked demo, which would include ability to walk around, chat, and possibly basic weapons (infinite pistol).