Code3Arena

PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 41 | Next >>

menu

  • Home/News
  • ModSource
  • Compiling
  • Help!!!
  • Submission
  • Contributors
  • Staff
  • Downloads

    Tutorials
    < Index >
    1. Mod making 101
    2. Up 'n running
    3. Hello, QWorld!
    4. Infinite Haste
    5. Armor Piercing Rails
    6. Bouncing Rockets
    7. Cloaking
    8. Ladders
    9. Favourite Server
    10. Flame Thrower
    11. Vortex Grenades
    12. Grapple
    13. Lightning Discharge
    14. Locational Damage
    15. Leg Shots
    16. Weapon Switching
    17. Scoreboard frag-rate
    18. Vortex Grenades II
    19. Vulnerable Missiles
    20. Creating Classes
    21. Scrolling Credits
    22. Weapon Dropping
    23. Anti-Gravity Boots
    24. HUD scoreboard
    25. Flashlight and laser
    26. Weapon Positioning
    27. Weapon Reloading
    28. Progressive Zooming
    29. Rotating Doors
    30. Beheading (headshot!)
    31. Alt Weapon Fire
    32. Popup Menus I
    33. Popup Menus II
    34. Cluster Grenades
    35. Homing Rockets
    36. Spreadfire Powerup
    37. Instagib gameplay
    38. Accelerating rockets
    39. Server only Instagib
    40. Advanced Grapple Hook
    41. Unlagging your mod


    Articles
    < Index >
    1. Entities
    2. Vectors
    3. Good Coding
    4. Compilers I
    5. Compilers II
    6. UI Menu Primer I
    7. UI Menu Primer II
    8. UI Menu Primer III
    9. QVM Communication, Cvars, commands
    10. Metrowerks CodeWarrior
    11. 1.27g code, bugs, batch


    Links

  • Quake3 Files
  • Quake3 Forums
  • Q3A Editing Message Board
  • Quake3 Editing


    Feedback

  • SumFuka
  • Calrathan
  • HypoThermia
  • WarZone





    Site Design by:
    ICEmosis Design


  •  
    Unlagging Your Mod
    by Neil "haste" Toronto

    This tutorial will show you how to take an existing mod and add in the lag compensation from the Unlagged mod at Alternate Fire. The compensation can't recover lost packets or reduce players' ping, but what it does, it does well. It lets HPB's use instant-hit weapons online by compensating for their lag on the server. Anyone who fires a railgun in an Unlagged mod can aim like he's in a single-player game, no matter what his ping is.

    If you are just starting on a mod, you can take a shortcut. Email me and I'll send you the source for Unlagged so you can begin from there instead of from the vanilla 1.29h source. If you do that, please read this tutorial anyway. There are concepts in here that you will need to know so you don't end up with weird bugs. Trust me: you don't want players getting stuck inside of each other because you created an execution path that skips undoing a time shift. It's bad. Fair enough?

    On with the show!

    1. LICENSING GARBAGE

    Code writers get automatic exclusive copyrights on their code. The exclusive right to copy, distribute, and make derivative works are the main rights. ("Exclusive" means that it's illegal for anyone else to do it.) Those are nearly useless to me, but I've got them anyway. I'm giving them up to you (not transferring) in exchange for two things:

    1. You credit me (Neil Toronto) somewhere visible
    2. You license this source with this license if you distribute this source or a derivative of this source to anyone else

    The first requirement is vague on purpose. I just want some recognition for the work. I'm sure you understand. And please note that the second requirement applies only to the source code. Now that that's out of the way, here are the goods.

    2. THE CONCEPTS

    Yes, yes, you want to code. I've got to explain what you'll be doing first, why, and what the issues are.

    The Reasons
    First, let's talk about inconsistency. (It's also referred to as "B.S.") Have you ever thought you dodged a rocket but been hit smack in the face anyway? Have you ever lined up a shot just perfectly and missed? Have you ever run into some other player online and been suddenly pushed to the side?

    It's all B.S., isn't it? Well, there's a hard truth here: in any online, multiplayer game, inconsistencies are inevitable. The basic problem is that packets take a while to get from one place to another, and that makes it impossible to synchronize a client with the server exactly right.

    Client-side prediction is one way to deal with the time difference issue. If you're pinging 200, it takes about 100ms (milliseconds) for the signal that something happened to get from the server to you. Does that mean that you see where everyone is 100ms late? (That's actually a long time - about the width of a player if he's running normally.) Actually, no. Id Software decided, when they made Q3A, that seeing exactly what's happening on the server is very important. So player positions are sent to the client as linear trajectories, and the client game extrapolates everyone's positions based on old data. It's pretty accurate, since players aren't really that erratic, but there are still errors involved. Those errors, and the correction of them, account for the third B.S. case I cited above.

    So what? Well, as a software designer, you have to decide what kind of inconsistencies you and your audience can deal with. As I said before, Id generally has decided in favor of what you see. Sometimes, that means that what you do suffers, introducing functional inconsistencies. For example, if you ping 200, line up a rail shot perfectly, and fire, you'll miss.

    That's where Unlagged comes in. In Unlagged, your rail shot would land, but at a small price. The algorithm described here trades some visual consistency for more functional consistency. There are many people willing to do that. Some people don't like it - but at least now we have a choice, right?

    How It Works
    An Unlagged server keeps track of 1/2 second of player position information. When an instant-hit shot is fired, every player but the attacking player is backward reconciled (or "time shifted" as I like to call it) to the positions they were in when it happened on the firing client. After the hit test, they're moved back to where they should be. The net effect is that, for the duration of the hit test, every player is pretty much where the attacking player saw them when he pulled the trigger.

    That's it. The implementation has a lot more detail, but in a nutshell, the previous paragraph describes how it works.

    The Issues
    Before you ask: there is no cheating issue. The hit tests are still done server-side, which means that we still don't trust the client to tell us when someone was hit. Doing this stuff server-side (like Half Life does) keeps us safe from those cheating idiots who like to mess it up for everyone else.

    The largest issue is this: though the hit tests are accurate for the attacking player, the hit tests will happen late. The target may have moved by then, or even be obscured by map geometry. Also, the firing player will see his rail trail late, and, if he pings high, it will most likely look like it missed. That's part of the trade-off. There are some things you can do on the client to correct that a bit, but at the expense of other things...

    Decisions, decisions. There are some very, very good things about Unlagged. First, it feels like single-player aim (with instant-hit weapons) to everyone playing, no matter what their pings are. HPB's can finally rip it up on CTF4 and DM17. (I've seen it a lot.) Second, and this is an extension of the first, every Unlagged server feels about the same, aim-wise. You can confidently practice your railing on one and your improved accuracy will transfer to every other that you join. (Heck, you can practice your railing against bots in single-player and it'll transfer.) Third, since four of the weapons are now much more useful online, the map balance is restored. All of the sudden, the lightning gun can become a point of contention in a game where everyone pings over 100.

    Like I said, you have to choose your inconsistencies. If this trade-off sounds okay to you, keep reading! (If you're starting a new mod that will likely only have a few servers running it, or a realism mod, or a mod with player classes, I strongly recommend it.) One thing I can tell you is that it was good enough for Half Life.

    3. THE TRAILS

    The first thing we need is a structure to hold all of the player position history. Open up g_local.h and find the text "clientPersistant_t". Add the new stuff shown below in red.

    } clientPersistant_t;
    
    //NT - client origin trails
    #define NUM_CLIENT_TRAILS 10
    typedef struct {
        vec3_t    mins, maxs;
        vec3_t    currentOrigin;
        int       time, leveltime;
    } clientTrail_t;
    
    // this structure is cleared on each ClientSpawn(),
    // except for 'client->pers' and 'client->sess'
    struct gclient_s {
    
    So now we've got a "clientTrail_t" structure that defines a record that can store a few vectors (bounding box extents and the current origin) and a couple of timestamps. We've got to add this to a structure...hmm... Well, which one you use depends on what you want time shifted. Just players? Have you got shootable objects that aren't players (or bots) that also move?

    For now, we'll assume that it's only players and bots and add it to gclient_t. Every player and bot has a gclient_t structure. So at the end of gclient_t (the structure just below the one we added), add the stuff in red.

        int         invulnerabilityTime;
    #endif
    
        char        *areabits;
    
        //NT - client origin trails
        int              trailHead;
        clientTrail_t    trail[NUM_CLIENT_TRAILS];
        clientTrail_t    saved;    // used to restore after time shift
    };
    
    We've got an array of 10 records to store position history. There's another record for undoing the time shift ("saved"), and an integer to store the head. One of the indexes in the array has got to be the most recent record, and we'll call it the "head" since we're implementing a queue. It's a fully initialized queue where every record has a meaningful value, so we don't have to keep track of the tail.

    In this new array of ours, every entry represents one position for one client in one server frame. (Servers usually run at 20fps, so a frame takes 50ms. With 10 positions, we've got 1/2 second of data.) One interesting thing that we'll get into in a bit is that player movement is usually calculated multiple times per server frame. This means that we'll be overwriting the head record a few times every server frame to keep with our paradigm.

    Another thing we'll get to later is the timestamp accuracy. On the head record, the timestamp has to be an accurate representation of the server's clock. (Otherwise, we severely mess up LPB's.) For all the rest, the end time for the frame they apply to (their level.time) is the right value. The problem is that the game engine has no callback to get the server's current, exact time! (Id, you nutjobs! Tsk, tsk! :) ) We'll have to estimate the server's clock using a callback that gives us the number of milliseconds since the game started (trap_Milliseconds). We'll store it in "level" like the other time stuff. So find the "level_locals_t" structure and add the code in red.

        int         framenum;
        int         time;                   // in msec
        int         previousTime;           // so movers can back up when blocked
        int         frameStartTime;         //NT - actual time frame started
    
        int         startTime;              // level.time the map was started
    
    One of the very first things the server does on a server frame is call G_RunFrame. This function advances all the non-player objects in a level (bots, movers, missiles, etc.). After G_RunFrame is finished, the server updates and advances players as it receives updates from them - and that's where all the player history storage and time shifting happens. So G_RunFrame sounds like a perfect place to store the current frame's starting time. Find G_RunFrame in g_main.c and add the code in red to the very beginning of the function.

    void G_RunFrame( int levelTime ) {
        int         i;
        gentity_t   *ent;
        int         msec;
        int         start, end;
    
        //NT - store the time the frame started
        level.frameStartTime = trap_Milliseconds();
    
    Now we've got to store player positions. We want to do it after every command packet, and we want to do it when the player is already at his updated position. Open up g_active.c and find the function ClientThink_real. This function is called for every command packet the server receives and for every bot think. I won't go into exactly all the things it does - we just need to remember to place our storage call right. Search for the words "exact origin" and you'll find them in a comment. Then make that code look like this by adding the red stuff.

        // NOTE: now copy the exact origin over...
        VectorCopy( ent->client->ps.origin, ent->r.currentOrigin );
    
        //NT - store the client's new position
        G_StoreTrail( ent );
    
    Where is G_StoreTrail? We'll get to that right now.

    4. THE LOGIC

    There are six new functions that you'll need for lag compensation:

    • void G_StoreTrail( gentity_t *ent );
    • void G_ResetTrail( gentity_t *ent );
    • void G_TimeShiftClient( gentity_t *ent, int time );
    • void G_TimeShiftAllClients( int time, gentity_t *skip );
    • void G_UnTimeShiftClient( gentity_t *ent );
    • void G_UnTimeShiftAllClients( gentity_t *skip );
    They're available for download here, as g_unlagged.c. You can include them in two ways: either paste them onto the end of g_client.c (which is what I did - don't paste in the #include if you do that) or add g_unlagged.c to your game project. (You'll have to edit the batch files that compile QVM's if you do that, too.) In either case, you'll also need g_unlagged.h, which contains the prototypes for the functions. If you're doing the pasting method, just copy the function prototypes from there into g_local.h. If you're adding g_unlagged.c to your project, add a '#include "g_unlagged.h"' to g_local.h.

    Since the code is 200-something lines long (actually, that's small, but not for a web page), I won't paste it in here. We will talk about what each function does, though.

    G_ResetTrail
    This sets up a client history with the client standing in his current position for the last 1/2 second. It's used to initialize the history when clients spawn or go through teleporters. There are two calls to it: one in ClientBegin, and another in TeleportPlayer. Open up g_client.c and find the ClientBegin function. Locate a call to ClientSpawn, and make it look like this by adding the red code.

        // locate ent at a spawn point
        ClientSpawn( ent );
    
        //NT - reset the origin trails
        G_ResetTrail( ent );
        ent->client->saved.leveltime = 0;
    
    We're initializing right after the client has been put on a spawn point. We're also telling G_TimeShiftClient that the position information in the "saved" structure in the player's gclient_t is not valid. (That's for a different issue, but here is a good place to do it.)

    Open g_misc.c and find TeleportPlayer. At the end of it, add the code in red.

        if ( player->client->sess.sessionTeam != TEAM_SPECTATOR ) {
            trap_LinkEntity (player);
        }
    
        //NT - reset the origin trail for this player to avoid lerping in a time shift
        G_ResetTrail( player );
    }
    
    The reason we need to do this is that we interpolate between two past player positions in the time shift to put players in the exact position at the time of attack. (Things are more or less smooth in the client game, but not the server game.) What if one position is on one side of a teleporter, and the other position is on the other side? The so-called "exact" position is right between them. That's pretty bogus. We can fix it by reinitializing the history.

    If you've got other places in your mod where players can noncontiguously move from one place to another (besides in ClientBegin and TeleportPlayer), you'll need to reinitialize the history there, too.

    G_StoreTrail
    This is going to start to get meaty.

    Let's put it in pseudocode. You'll want to have the actual code in front of you, too. Understand, before you read, that level.previousTime is the start time for this server frame, and level.time is the end time. (level.time is usually assumed to be "this frame's time," which is fine, but can be confusing here.) Remember that trap_Milliseconds' return value is relative to the game's start time, and level.time and level.previousTime are relative to the level's start time.

        if we're on a new frame {
        	make the current head's timestamp the level time
                at the end of the last frame
            create a new head record
        }
    
        calculate the timestamp for the head record with the following:
        if the current player is a bot {
            newtime = level.time because they only think once per server frame
        } else {
            newtime = level.previousTime + trap_Milliseconds() - level.frameStartTime
            keep newtime between level.previousTime + 1 and level.time for sanity
        }
    
        store the player's position in the head record
        timestamp the head record
    
    First, the non-bot time calculation gives us an accurate representation of the server's actual time, using the game time we stored in G_RunFrame.

    Second, you might have noticed that we update the head record's timestamp before we make a new head. This is because the time base the clients use in extrapolating other player positions is level.time. It's entirely possible for the server to get only one update from a client on a server frame, at the very beginning of it. If that happens, everyone else's representation of that player could be up to 50ms off. We're compensating for it. This is the one time that we consider what the client sees to be a little bit more important than perfect accuracy on the server.

    G_TimeShiftClient and G_TimeShiftAllClients
    G_TimeShiftAllClients simply calls G_TimeShiftClient for every player but the one attacking. So that's out of the way already.

    Let's put G_TimeShiftClient in pseudocode, too. The function is passed the player to be shifted and the time to be shifted back to.

        constrain the time to <= level.time for sanity
    
        find two entries in the origin trail whose timestamps sandwich "time"
        (assume that no two adjacent trail records have the same timestamp)
    
        (at this point, we've either sandwiched the time, or wrapped back
            around to the head record)
    
        save the player's current position
    
        if we've sandwiched {
            figure out how close the time is to the first timestamp
                (for interpolation)
            interpolate between the two origins for the exact position
            interpolate between the bounding box extents
            relink the entity to recalculate the absolute bounding box
        } else we've wrapped {
            time shift the client back to the place specified by the tail record
            relink the entity to recalculate the absolute bounding box
        }
    
    I'm not sure what I could say to clarify it more. One thing you should be aware of is this: if your mod is doing locational damage, you'll want to save each player's viewangles as well as his position and bounding box. To interpolate between angles, use a function called LerpAngle.

    G_UnTimeShiftClient and G_UnTimeShiftAllClients
    Another one out of the way quickly: G_UnTimeShiftAllClients calls G_UnTimeShiftClient for every player but the one attacking.

    A simple explanation will suffice here. G_UnTimeShiftClient simply undoes what G_TimeShiftClient did using the position information saved in G_TimeShiftClient.

    5. THE TIME SHIFT

    Now it's time to code again. Woo hoo!

    What we want to do is time shift every player but the attacker before a hit test, and then shift them all back when it's done. There are two rules for this that must be strictly observed.

    1. While the players are in a time-shifted state, no other processing should be going on that is expecting the players to be in their right positions. If you let any processing like that in, it won't work at random times.
    2. No execution path should skip undoing a time shift. This means no return statements between calls to G_TimeShiftAllClients and G_UnTimeShiftAllClients. It means that, if one of those calls is conditional, the other should be called under exactly the same conditions. If any execution path skips undoing a time shift, you'll get weird, random errors, like the one I described at the beginning of this tutorial. (Players sometimes got stuck inside of each other.)
    If you observe these rules, everything should go well. If you don't - woe, woe, woe be unto you and your mod!

    Why don't we shift the attacker? Because player movement on the server lags behind player movement on the client. For instance, if I'm pinging 200, it takes about 100ms for a movement command to get to the server. Therefore, my position on the server is 100ms behind where I see myself. What does this mean? Well, the general idea behind Unlagged is to put everyone in the same positions as the attacking player saw them - and by the time the server gets the attack message, the attacking player is already in the position he saw himself in.

    Now, I have no idea what your mod does or what weapons it's got, so I'm going to walk you through the four weapons that Unlagged compensates for: the shotgun, the railgun, the machinegun, and the lightning gun.

    The Shotgun
    This one's the easiest. Open up g_weapon.c and locate the function ShotgunPattern. After the statement that assigns oldScore, add the following red code.

        oldScore = ent->client->ps.persistant[PERS_SCORE];
    
        //NT - shift other clients back to the client's idea of the server
        // time to compensate for lag
        if ( g_delagHitscan.integer && ent->client &&
            !(ent->r.svFlags & SVF_BOT) ) {
            G_TimeShiftAllClients( ent->client->pers.cmd.serverTime, ent );
        }
    
    ent->client->pers.cmd.serverTime is the client's estimate of the server's clock at the time of attack. (usercmd_t::serverTime is sent with every packet from the client.) If someone is pinging 300, this will most likely be 150ms or so behind the server clock, since it took that long for the packet to get to the server. (Remember that the ping time is a round-trip measurement.) It's more accurate than using ping / 2, since the trip one way can take longer than a trip the other way. Besides, we want to know how long it took this packet to get to the server.

    Then, after the "for" loop that makes the shotgun pattern, add this red code.

            }
        }
    
        //NT - move the clients back to their proper positions
        if ( g_delagHitscan.integer && ent->client &&
            !(ent->r.svFlags & SVF_BOT) ) {
            G_UnTimeShiftAllClients( ent );
        }
    }
    
    G_TimeShiftAllClients and G_UnTimeShiftAllClients are called under the same conditions: g_delagHitscan is nonzero, the attacking entity has a client structure (is a bot or a player), and the attacking entity is not a bot.

    That's it! The shotgun is Unlagged. You won't be able to test it yet, since we haven't defined g_delagHitscan (that's in section 6), but it'll work.

    The Railgun
    Find the weapon_railgun_fire function. Right at the beginning, make it look like this:

        gentity_t   *unlinkedEntities[MAX_RAIL_HITS];
    
        //NT - shift other clients back to the client's idea of the server
        // time to compensate for lag
        if ( g_delagHitscan.integer && ent->client &&
            !(ent->r.svFlags & SVF_BOT) ) {
            G_TimeShiftAllClients( ent->client->pers.cmd.serverTime, ent );
        }
    
        damage = 100 * s_quadFactor;
    
    Notice that the time-shifting code here is exactly the same as the time-shifting code in ShotgunPattern. Now let's undo the time shift.

        } while ( unlinked < MAX_RAIL_HITS );
    
        //NT - move the clients back to their proper positions
        if ( g_delagHitscan.integer && ent->client &&
            !(ent->r.svFlags & SVF_BOT) ) {
            G_UnTimeShiftAllClients( ent );
        }
    
    The Machinegun
    The machinegun has one tricky part: if we did it just like the other two, there would be a return statement between the G_TimeShiftAllClients and G_UnTimeShiftAllClients. That's bad, remember? So let's not do that.

    Find Bullet_Fire. We'll start it normally, with the time shift right after the variable declarations:

        int         i, passent;
    
        //NT - shift other clients back to the client's idea of the server
        // time to compensate for lag
        if ( g_delagHitscan.integer && ent->client &&
            !(ent->r.svFlags & SVF_BOT) ) {
            G_TimeShiftAllClients( ent->client->pers.cmd.serverTime, ent );
        }
    
    Then we'll undo the time shift at the end of a function, but with a slight difference:

            }
            break;
        }
    
        //NT - move the clients back to their proper positions
    untimeshift:
        if ( g_delagHitscan.integer && ent->client &&
            !(ent->r.svFlags & SVF_BOT) ) {
            G_UnTimeShiftAllClients( ent );
        }
    }
    There's a chance you've never seen a line like "untimeshift:" before. It's a label. We're going to use it in a goto statement.

    "No! A goto! Those are eeeeevil! They told me that in my CS class!" you say. Well, they're not always "eeeeevil," they just are most of the time the way most people use them. Do a little programming language study (there are loads of programming languages), and you'll find that C lacks a construct that will be executed no matter what. (In Java, it's the "finally" construct.) We're building one here. As far as I know, this is the only justification for a goto statement in a C program.

    Find the trap_Trace call in Bullet_Fire, and make it look like this:

            trap_Trace (&tr, muzzle, NULL, NULL, end, passent, MASK_SHOT);
            if ( tr.surfaceFlags & SURF_NOIMPACT ) {
                //NT - make sure we un-time-shift the clients
                goto untimeshift;
            }
    
            traceEnt = &g_entities[ tr.entityNum ];
    
    The goto statement replaces the return statement.

    The Lightning Gun
    The lightning gun firing function has the same problem as Bullet_Fire. There's a return statement in the middle of it. Find the Weapon_LightningFire function, and, as always, add the time-shifting code to the beginning of it:

        int         damage, i, passent;
    
        //NT - shift other clients back to the client's idea of the server
        // time to compensate for lag
        if ( g_delagHitscan.integer && ent->client &&
            !(ent->r.svFlags & SVF_BOT) ) {
            G_TimeShiftAllClients( ent->client->pers.cmd.serverTime, ent );
        }
    
    Then, at the end, add the version of the time shift undo with the label:

            }
    
            break;
        }
    
        //NT - move the clients back to their proper positions
    untimeshift:
        if ( g_delagHitscan.integer && ent->client &&
            !(ent->r.svFlags & SVF_BOT) ) {
            G_UnTimeShiftAllClients( ent );
        }
    }
    Find the return statement, and replace it with the goto:

            if ( tr.entityNum == ENTITYNUM_NONE ) {
                //NT - make sure we un-time-shift the clients
                goto untimeshift;
            }
    
            traceEnt = &g_entities[ tr.entityNum ];
    
    Voila! We're done with the time shifting. All we've got left is a few miscellaneous things...

    6. MISCELLANY

    You'll need a server-side variable to turn lag compensation on and off, and another to identify the server as running Unlagged. Open up g_main.c and add this to the end of all those vmCvar_t declarations:

    //NT - new vars
    vmCvar_t    g_delagHitscan;
    vmCvar_t    g_unlaggedVersion;
    
    Right after that is a large structure full of server-side variable entries. Put a comma on the end of the last entry and add these lines:

        //NT - new vars
        { &g_delagHitscan, "g_delagHitscan", "1", 0, 0, qtrue  },
        { &g_unlaggedVersion, "g_unlaggedVersion", "1.0", CVAR_ROM | 
            CVAR_SERVERINFO, 0, qtrue }
    
    Then open g_local.h and search for "vmCvar_t" - there should be a big list of them that looks like the list in g_main.c, only with the word "extern" in front of each one. Go to the end of that list, and add these two:

    //NT - new vars
    extern  vmCvar_t    g_delagHitscan;
    extern  vmCvar_t    g_unlaggedVersion;
    
    Please add a g_unlaggedVersion. There may be a time where you can search in game browsers for games with this info variable set, and you'll want HPB's to find your mod.

    7. EXTRA THINGS YOU MIGHT WANT TO DO

    A Message on Connect
    You may want to print a nice message to your connecting clients. The place to do that is at the end of ClientConnect in g_client.c, right before the "return NULL;" statement. Here's are two commands to do it:

        trap_SendServerCommand( clientNum, "print \"This server is Unlagged!\n\"" );
        trap_SendServerCommand( clientNum, "cp \"THIS SERVER IS UNLAGGED\n\"" );
    
    Where "clientNum" is the client's number, already defined in ClientConnect. The first statement will dump the message to the console, and the second will put the message right in the center of the client's screen for a couple of seconds.

    Per-player Choice
    One thing that you should seriously, seriously consider is giving users the ability to individually turn off lag compensation. Some people don't like it and would rather lead their own shots, even HPB's. (They really would - I've met some.)

    Locational Damage
    If your mod does locational damage, you'll want to store a little more than this tutorial has you do. Every player's viewangles should be stored and then interpolated like everything else in G_TimeShiftClient. You can use the LerpAngle function to interpolate between two angles measured in degrees.

    One thing to consider is that, in regular Quake 3, player bounding boxes are made up of coaxial planes. (That means all six sides of the bounding box are all parallel with either the XY plane, the XZ plane, or the YZ plane.) If you modify a player's r.currentAngles and do a trap_LinkEntity, the bounding box will be rotated. Whether or not you want this to happen is up to you - just make sure the behavior is consistent.

    Hit-test Wrapping
    The astute reader will notice that there is a way to wrap hit tests in time shifts without gotos and with less duplicated code. If you want to implement it that way, go ahead. The reason I have it the way I do is that it's easier to see if I'm introducing a conflict with the time shifting if it's all in one function. There's much less tracing to find out. Also, keeping the time shift as short as possible could make it easier in the future to add code without creating a conflict. The time-shifted state is actually quite volatile, and I think the less time the server is in one, the better.

    Predicted Weapon Effects
    Sometimes, it really bothers people to see their rail trail late and have it apparently miss when it actually hit. It's a side-effect of the lag compensation. (Maybe one day people will just call it "lag" like they do when they miss rail shots.) One thing you might do is give users an option that they can set that will predict rail trails - make them appear immediately. As it is, the client waits for the server to say that the rail trail was created.

    The only problem with that is that it's possible to think you squeezed off a rail shot, and then have the server tell your client that you actually died before it got the message. In that case, the rail won't actually fly, but it'll look like it did. That's a little bit of B.S., but it may be acceptable for some people.

    If you want to predict rail trails regardless, you may have to store on the server whether or not a player was dead at the time he fired the rail shot. Then, you can check that to see if the rail should fly. Of course, if you do that, it's possible for apparently dead people to fire railguns. More B.S. (Geez.) Most client-side lag compensation schemes have this problem.

    cg_trueLightning is an attempt at lag compensation, and, as such, isn't quite compatible with Unlagged. The most compatible setting is "1.0" (the shaft is supposedly aimed right at the crosshair no matter what - but it's not), and even that has some problems in Quake 3's implementation. One thing you probably will want to do is make it so if the server is compensating for a player's lightning lag, the lightning shaft always points at the crosshair.

    Unlagging Movers
    It's possible to time shift movers so they're in the positions the client saw them in. There's an issue with this that I've decided is big enough to warrant not doing it myself, but you're certainly free to do so. Most players don't have much of a problem getting hit by a shot that happened 200ms ago after they've taken cover. I assumed that most players would, however, mind getting shot through a closed door.

    It's still your call, though. If you do movers, make sure you store r.currentAngles for just them. And you'll have to move the trail storage out of gclient_t, since movers don't have clients.

    Unlagging the Gauntlet
    You might have noticed that, in Unlagged, the gauntlet has no lag compensation. There are two reasons for it. First, when players get close to each other, visual inconsistencies increase. For instance, it's possible for a player to gauntlet another player, and then be behind him or facing the opposite direction by the time the hit test is calculated. You sometimes see this with close shotgun battles already.

    Second, a gauntlet hit is checked a whole lot more than even a lightning gun hit - somewhere between 5 and 10 times more. I haven't tested what kind of hit your CPU takes on this, but I assume it's a lot more than just plain Unlagged. If you'd like to try it out, though, there's nothing stopping you.


    Some of these extras are client-side. If you're making a server-side-only mod, you won't be able to do them.

    8. CREDITS

    There are tons of server admins and testers that I'd like to thank, but I don't know who they all are and this tutorial is long enough as it is. Thank you, thank you, thank you.

    There are some people I'd like to thank in particular:

    • Bryan "Apoxol" Dube, the creator of Urban Terror's server-side lag compensation. I only found out that he had implemented it after I created Unlagged beta 1, but then we put our heads together to make both of our implementations better.
    • Ted "Targhan" Vessenes, one of the creators of Art of War, for making me think harder and showing me a good few things that have really made Unlagged work. Indespensible.
    • Pat "Cornholio" Winn, for being stubborn enough to stick by his story that the first few versions didn't feel right - and also for keeping an Unlagged server up to help me test.
    • G-Man, for being patient and helping me test for hours on end in the first betas.
    • Aaron "Paradox" Storck, for the very cool Instagib implementation, Insta-Unlagged.
    Consider yourselves thanked.

    9. LINKS

    Code files: g_unlagged.c, g_unlagged.h
    Author's email: haste@planetquake.com
    Author's web site: Alternate Fire

    PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 41 | Next >>