Advanced Networking and Local Prediction

Written by Marco “eukara” Hladik

Attention: This guide is quite old. It may not work anymore but it still gets the jist across. If you want to see an up-to-date representation of what working prediction in CSQC looks like, look at my FreeCS sources. (licensed under the AGPL)

Anyone working closely with the FTE QuakeWorld engine knows of one hurdle they need to overcome if they want to take over networking the player entities themselves: Prediction.

If you want to use the .SendEntity field in the SSQC to define what information is networked, it will disable the prediction done via the engine.

While yes, it is easy lerping other players, getting your own to move smoothly can be a bit of a confusing adventure. To make clear how to properly apply prediction to your player, so that playing even with a 300+ ping appears smooth, please read on.

The Setup

Assuming you have not already done so, you first want to edit the SSQC and define what sort of fields you want to network.
These vary depending on what you want to do with your mod. If you don’t know what you want to network, then you most likely don’t want to network them manually anyway.

First define, inside the spawn function of the player, what your SendEntity function is (most mods spawn the player inside of PutClientInServer of Quake’s client.qc):

...
self.SendEntity = Player_SendEntity;
...

Player_SendEntity is obviously not yet defined in your mod. You might call it anything you want. Anyway, here is an example function that does its task:

/*
=================
Player_SendEntity

    Networks the player info
=================
*/
float Player_SendEntity( entity ePVEnt, float flChanged ) {
    WriteByte( MSG_ENTITY, 1 ); // Unique Identifier, I suggest you use enums to make it easy on yourself
    WriteCoord( MSG_ENTITY, self.origin_x ); // Position X
    WriteCoord( MSG_ENTITY, self.origin_y ); // Position Y
    WriteCoord( MSG_ENTITY, self.origin_z ); // Position Z
    WriteCoord( MSG_ENTITY, self.angles_x ); // Angle X
    WriteCoord( MSG_ENTITY, self.angles_y ); // Angle Y
    WriteCoord( MSG_ENTITY, self.angles_z ); // Angle Z
    WriteShort( MSG_ENTITY, self.velocity_x ); // Velocity X
    WriteShort( MSG_ENTITY, self.velocity_y ); // Velocity X
    WriteShort( MSG_ENTITY, self.velocity_z ); // Velocity X
    WriteFloat( MSG_ENTITY, self.flags ); // Flags, important for physics
    return TRUE;
}

This, compared to what Quake/QuakeWorld usually networks, is optimisticly minimalistic.
Note: You can use ePVEnt to filter out what entities will be able to receive what kind of information, if any at all. flChanged refers to what .SendFlags is set to, so you can use that to decide whether to send out all information or just parts of it.

Warning:

Make sure to use FTE QuakeWorld’s fteextensions.qc file to get all definitions, this might take some fixing of the progs106 code if you base it off of that one.
However, if you are too lazy, these defs will do:

#define MSG_ENTITY 5
.float(entity playerent, float changedflags) SendEntity;
.float SendFlags;
void(float buf, float fl) WriteFloat = #280;

Now, you can compile this without any problems, however:

  1. SendEntity will never happen, because SendFlags is never set
  2. You will get a networking error anyway because CSQC does not know how to interpret the incoming information

So let’s fix that. Let’s tackle 1) first.
Ask yourself, when does the networking happen? Usually at the end, after everything has been processed and the server has run its logic frame.
Therefor, we should go into PlayerPostThink and at the very top, to make sure it is being executed, set SendFlags.

// Network everything
self.SendFlags = 1;
...

Now, basically whenever SendFlags is set to something other than 0, it will go and call the contents of .SendEntity to all potentially visible clients.
Afterwards, it will set .SendFlags back to 0. In this example we did not care about flChanged in our .SendEntity function and therefore don’t need to give a damn about giving it a value other than 0. You could set it to 666 for all we care.

Notes on SendFlags: Keep in mind that .SendFlags is supposed to be a bitmask and can be used as such. However, you have to manually send a Write call in your .SendEntity function if you want to make use of that. It is not protected by packet-loss however so use it with caution. Don’t rely on it too much. If the entity is seen by a client for the first time, it will always be 0xffffff - which will cause all flagged sections to update.

This tackles 1), now onto 2)

Getting Your Hands Into The CSQC

If you’ve read my Beginners Guide to CSQC then you will know about a few callback functions. A new one you will learn about today is:

void CSQC_Ent_Update( float flIsNew ) {
...

Now we will finally make use of that.
May I remind you to always keep track to what sort of info we want to network. In the previous section we’ve established that we wanted to network:

  1. The entity’s identifier (in this case 1)
  2. The entity’s in-world X positon
  3. The entity’s in-world Y positon
  4. The entity’s in-world Z positon
  5. The entity’s angle pitch
  6. The entity’s angle direction
  7. The entity’s angle roll
  8. The entity’s velocity vector X value
  9. The entity’s velocity vector Y value
  10. The entity’s velocity vector Z value
  11. The entity’s flags

So we’ve got 12 bits of information in total. The first bit of information that needs to exist on ALL entity networking calls consistently is the identifier. That thing should ALWAYS be transferred first.

float flEntType = readbyte();

At the very top of CSQC_Ent_Update. Now we finally know what we will be getting info about. You can be fancy and use switch-cases or maybe just an if-case will do fine (me being more comfortable with the latter…):

...
if ( flEntType == ENT_PLAYER ) {
    if ( flIsNew == TRUE ) {
        self.classname = "player";
        self.solid = SOLID_SLIDEBOX;
        self.predraw = Player_PreDraw;
        self.drawmask = MASK_ENGINE;
        setmodel( self, "progs/player.mdl" );
    }
        
    self.origin_x = readcoord();
    self.origin_y = readcoord();
    self.origin_z = readcoord();
    self.angles_x = readcoord();
    self.angles_y = readcoord();
    self.angles_z = readcoord();
    self.velocity_x = readshort();
    self.velocity_y = readshort();
    self.velocity_z = readshort();
    self.flags = readfloat();
        
    setsize( self, '-16 -16 -24', '16 16 32' );
    setorigin( self, self.origin );
}
...

As you can see, flIsNew will be TRUE whenever the entity is first seen by this client, so it will set it up with information that otherwise doesn’t need to be networked.
Compiling the code will probably result in the error that Player_PreDraw is not yet defined.
I guess we should try to fix that.
But before you do, we need to set up the camera.

vector vPlayerOrigin;
vector vPlayerOriginOld; // We definitely need this later
vector vPlayerVelocity;

These will be needed to update the camera. If you have a definition file for your CSQC project, then please define them in there.

Inside CSQC_UpdateView we need to update the camera in order to work with the origin:

setproperty( VF_ORIGIN, vPlayerOrigin + [ 0, 0, getstatf( STAT_VIEWHEIGHT ) ] );
setproperty( VF_ANGLES, view_angles );

Put this after addentities() calls.
You don’t need to worry about view_angles, that is an internal CSQC value for the input.

Now it is time to define Player_PreDraw

float Player_PreDraw( void ) {
    if ( self.entnum == player_localentnum ) {
        addentity( self );
        vPlayerOrigin = self.origin;
        vPlayerVelocity = self.velocity;
    } else {
        addentity( self );
    }
    return PREDRAW_NEXT;
}

This will now compile and you will be able to move around in first person and it will feel somewhat like the original… but not really.
The position will essentially only ever update when the server updates your position. Setting a console variable like sv_minping to 300 will demonstrate this.

In the function, you can clearly see that we are differentiating between our own player ( self.entnum == player_localentnum ) and others. In the next chapter, we will tackle the most frustrating problem to many:

Prediction for the own, local, player.

The Tale Of Prediction

For prediction we will be using the help of a builtin called runstandardplayerphysics(). It will need to know a few things about the entity it is trying to update:

  1. Our movetype
  2. Our origin
  3. Our velocity
  4. Our movement flags

Now, unless you want to conflict with prediction - never, ever define the movetype for the player object outside of this function. Otherwise you are telling CSQC to update the physics outside of this function. Don’t do it.
So we have to define it for this function only and then roll back to MOVETYPE_NONE. That is not the only field we have to roll back to, though.
We also have to rollback our entity’s origin, velocity and movetype flags.
First, wipe the entire contents of the predraw to look like:

float Player_PreDraw( void ) {
    if ( self.entnum == player_localentnum ) {
        // WE WILL BE EDITING THIS HERE
    } else {
        addentity( self );
    }
    
    return PREDRAW_NEXT;
}

Go to the line with the obnoxious upper-case comment (after if ( self.entnum == player_localentnum ) { ) and let us define what movetype we’ll be using for calculating the physics:

self.movetype = MOVETYPE_WALK;

Now, we are about to apply prediction. This is the biggest part of the code. Really fragile, don’t mess too much with it:

// Prepare rollback
vector vOldOrigin = self.origin;
vector vOldVelocity = self.velocity;
float fOldPMoveFlags = self.pmove_flags;

// Apply physics for every single input-frame that has not yet been
// acknowledged by the server (servercommandframe = last acknowledged frame)
for ( int i = servercommandframe + 1; i <= clientcommandframe; i++ ) {
   float flSuccess = getinputstate( i );

    if ( flSuccess == FALSE ) {
        continue;
    }

    // Partial frames are the worst
    if (  input_timelength == 0 ) {
        break;
    }
    runstandardplayerphysics( self );
}
        
// Smooth stair stepping, this has to be done manually!
vPlayerOriginOld = vPlayerOrigin;
        
if ( ( self.flags & FL_ONGROUND ) && ( self.origin_z - vPlayerOriginOld_z > 0 ) ) {
    vPlayerOriginOld_z += frametime * 150;
            
    if ( vPlayerOriginOld_z > self.origin_z ) {
        vPlayerOriginOld_z = self.origin_z;
    }
    if ( self.origin_z - vPlayerOriginOld_z > 18 ) {
        vPlayerOriginOld_z = self.origin_z - 18;
    }
    vPlayerOrigin_z += vPlayerOriginOld_z - self.origin_z;
} else {
    vPlayerOriginOld_z = self.origin_z;
}
    
vPlayerOrigin = [ self.origin_x, self.origin_y, vPlayerOriginOld_z ];
vPlayerVelocity = self.velocity;
addentity( self );

// Time to roll back
self.origin = vOldOrigin;
setorigin( self, self.origin );
self.velocity = vOldVelocity;
self.pmove_flags = fOldPMoveFlags;
self.movetype = MOVETYPE_NONE;

// Set renderflag for mirrors!
self.renderflags = RF_EXTERNALMODEL;

Compiling this should be flawless and provide smooth movement, plus smooth stair-stepping.

Note: runstandardplayerphysics() can be replaced with your own function responsible for moving entities around the world. Usually those functions rely on the usage of tracebox(). If you plan on writing more advanced entities like planes or cars, you might not want to use runstandardplayerphysics(), but your very own. Make sure that both the SSQC (via .customphysics) and the CSQC (via prediction) codebase runs the same, shared logic.

The code should largely be self-documenting. However, if you have any questions, please mail me.

Download

If you want to see the source files for above, click this message. (239 KB)

License

Give credit, I suppose. Just in the hope that people will find this and my other guides. Note that the archive above contains the progs106 code as well. That is obviously meant to be GPL2.