Basic physics¶
Walking through a frame¶
Caution
This section is incomplete and outdated.
In this section we will describe what the game does in each frame that are relevant to TASing. Do not skip this section if you intend to understand the rest of this document without much pain.
For our purposes we can consider
PlayerPreThink
indlls/client.cpp
to be the first relevant function that gets called in each frame. You might be wondering about theStartFrame
function. While usually it seems to get called prior toPlayerPreThink
, inexplicable things happen when one tries to outputg_ulFrameCount
andgpGlobals->time
from bothStartFrame
andPlayerPreThink
. What you will observe in 1000 fps is something like the following:startframe, g_ulFrameCount = 3, gpGlobals->time = 1.001 prethink, g_ulFrameCount = 3, gpGlobals->time = 1.003 startframe, g_ulFrameCount = 4, gpGlobals->time = 1.002 prethink, g_ulFrameCount = 4, gpGlobals->time = 1.004 ... ...
If we look only at
g_ulFrameCount
, it appears thatStartFrame
is indeed called prior toPlayerPreThink
. However, the values forgpGlobals->time
seem to tell us that the invocation ofStartFrame
is delayed by 2 frames. We have no idea how to explain this contradiction, considering the fact thatgpGlobals
is a global pointer referencing the same memory location. There could be some evil code that changes the value ofgpGlobals->time
to deceiveStartFrame
. Furthermore, when the frametime (the inverse of framerate, hereafter denoted as ) is not a multiple of 0.001, sometimesStartFrame
will be called multiple times in succession without executing the game physics at all, followed by several iterations of the game physics in sucession without callingStartFrame
. Also, while a map is loading, the entire game physics is run multiple times after eachStartFrame
call. From these observations, we conclude that one “frame” really refers to one iteration of the game physics, which can be thought to begin with the call toPlayerPreThink
as far as player physics is concerned, rather thanStartFrame
.PlayerPreThink
will callCBasePlayer::PreThink
located indlls/player.cpp
. Information such as the player health and those related to HUD will be sent over to the client in a function calledCBasePlayer::UpdateClientData
. Physics such as the using of trains, jumping animations, sounds and other cosmetic effects are run. The physics associated with other entities in the map are not run at this point.The
UpdateClientData
, which is a completely different function located indlls/client.cpp
will get called a short while later. This is where TasTools send the player velocity, position and other data in full precision to the clientside. This is important because of lines like these in thedelta.lst
file located in thevalve
directory:DEFINE_DELTA( velocity[0], DT_SIGNED | DT_FLOAT, 16, 8.0 ), DEFINE_DELTA( velocity[1], DT_SIGNED | DT_FLOAT, 16, 8.0 ), ... ... DEFINE_DELTA( velocity[2], DT_SIGNED | DT_FLOAT, 16, 8.0 ),
This means that each component of player velocity will be sent only with 16 bits of precision and rounded to a multiple of one-eighth. This is bad for optimal strafing computations done entirely at the clientside.
Assuming the string
CL_SignonReply: 2
has been printed to the console at some point (this will only be printed once). Now the client starts working. The messages sent from the server just a moment ago are now received, including one that was sent by TasTools fromUpdateClientData
. Once the messages are processed, the game callsCL_CreateMove
incl_dlls/input.cpp
. This is where the user input gets processed. The user input can come from keyboard, mouse, joystick, or just console commands such as+attack
. This is also where TasTools does all TAS-related computations such as optimal strafing. The final viewangles, button presses (represented by a 16-bit integer) and other data are sent to the server at some point afterCL_CreateMove
returns, stored asusercmd_t
defined incommon/usercmd.h
. Note that button presses do not record the actual keys pressed, but rather, they are represented by bits that are set when specific commands are issued. For example, when the+forward
command is issued, theIN_FORWARD
bit will be set. For some reasons, the game developers named the variable asbuttons
which is confusing. The definition for all “button” bits can be found incommon/in_buttons.h
.Once the server receives the user input from the clientside, the
PM_Move
inpm_shared/pm_shared.c
gets called. This is where all the fun begins, because the physics related to player movement are located here. Due to the importance of the code inpm_shared/pm_shared.c
, we will devote a couple of paragraphs to them.PM_Move
callsPM_PlayerMove
, which is an unfortunately large function.The first thing
PM_PlayerMove
does is to callPM_CheckParamters
[sic]. This is the function that scalesforwardmove
,sidemove
andupmove
(hereafter written as , and respectively) inpmove->cmd
so that the magnitude of the vector does not exceedsv_maxspeed
(denoted as ). This means if thenOtherwise, , and will remain unchanged. This is why increasing
cl_forwardspeed
,cl_sidespeed
andcl_upspeed
may not result in greater acceleration.PM_DropPunchAngle
is also invoked to decrease the magnitude of punchangles. The full equation iswhere . The punchangles are then added to the viewangles (which does not affect the viewangles at the clientside).
PM_ReduceTimers
is now called to decrement various timers such aspmove->flDuckTime
which purpose will be explained later.A very important function,
PM_CatagorizePosition
[sic], is then called. Here we will introduce a new concept: onground. A player is said to be onground if the game thinks that the player is standing some “ground” so that certain aspects of the player physics will be different from that when the player is not onground.PM_CatagorizePosition
checks whether the player is onground and also determines the player’s waterlevel (which carries a numerical value), yet another new concept. In short, the player physics will differ significantly only when the waterlevel is more than 1, which happens when the player is immersed sufficiently deeply into some water (the specific conditions will be described later).The way by which
PM_CatagorizePosition
determins the player onground status is simple. A player is onground if (vertical velocity) is at most 180 ups, and if there exists a plane at most 2 units below the player, such that the angle between the plane and the global horizontal plane is at most degrees with the plane normal pointing upward (this is another way of saying that the component of the unit normal vector is at least 0.7). If the player is determined to be onground and his waterlevel is less than 2, then the game will forcibly shift the player position downward so that the player is really standing on the plane and not continue floating in the air at most 2 units above the plane.TODO describe checkwater here
After the first onground check, the game will store in
pmove->flFallVelocity
. Although this may seem insignificant, this turned out to be how the game calculates fall damage. Next we have a call toPM_Ladder
, which determines whether the player is on some ladder.PM_Duck
, as its name suggests, is pretty important. This is the function responsible for ducking physics. Here we must introduce two new concepts: bounding box and duckstate. They are described in Ducking physics which must be read before moving on.The game will now call
PM_LadderMove
if the player is on some ladder.If
+use
and the player is onground, then will be scaled down by 0.3. This is the basis of USE braking.The game will now do different things depending on
pmove->movetype
. If the player is on ladder then the movetype isMOVETYPE_FLY
. Otherwise it will usually be the confusingly namedMOVETYPE_WALK
. We will assume the latter. If the player waterlevel is at most 1, the game makes the first gravity computation as done byPM_AddCorrectGravity
. Looking inside this function, assuming basevelocity is we see that the game performs this following computation:where is the gravitational acceleration,
ent_gravity
timespmove->movevars->gravity
. Several notes must be made here:ent_gravity
ispmove->gravity
if the latter is nonzero, otherwise the former is 1.pmove->gravity
is usually 1, which can thought as a multiplier that scalespmove->movevars->gravity
. For instance, it has a different value if we enter the Xen maps, which is how the game changes the gravitational acceleration without directly modifying thesv_gravity
cvar. Now look closer to the computation, we see that it does seem incorrect as noted in the rather unhelpful comment. However, the game always makes a call toPM_FixupGravityVelocity
towards the end ofPM_PlayerMove
which performs the exact same computation except it completely ignores basevelocity. Now the key idea is that the actual movement of player vertical position is done between these two calls. In other words, we want the final vertical position afterPM_PlayerMove
to bewhich is exactly what we know from classical mechanics. But by rewriting this equation ever so slightly, we obtain
where is the new velocity computed by
PM_AddCorrectGravity
. It is now obvious why this function does it in the seemingly incorrect way.The final velocity after
PM_PlayerMove
must be and not . This is wherePM_FixupGravityVelocity
comes into play by subtracting another . A final note: in both of these functions thePM_CheckVelocity
is called. This function ensures each component of is clamped tosv_maxvelocity
.TODO: waterjump etc
Assuming the waterlevel is less than 2. If
+jump
is active thenPM_Jump
will be called. The jumping physics will be dealt in a later section.The game calls
PM_Friction
which reduces the magnitude of player horizontal velocity if the player is onground. The friction physics is discussed much later. Note that the vertical speed is zeroed out here. AnotherPM_CheckVelocity
will be called regardless of onground status.The game will now perform the main movement physics. They are very intricate and we devoted several sections to them.
The final
PM_CatagorizePosition
will be called after the movement physics. The basevelocity will be subtracted away from the player velocity, followed by yet anotherPM_CheckVelocity
. Then thePM_FixupGravityVelocity
is called. Finally, if the player is onground the vertical velocity will be zeroed out again.After
PM_Move
returns, the game will callCBasePlayer::PostThink
shortly after. This is where fall damage is inflicted upon the player (only if the player is onground at this point), impulse commands are executed, various timers and counters are decremented, and usable objects in vicinity will be used if+use
.The game will now execute the
Think
functions for all entities. This is also where other damages and object boosting are handled.
Frame rate¶
The term “frame rate” is potentially ambiguous. If precision is desirable then
we can differentiate between three kinds of frame rate: computational frame
rate (CFR), usercmd frame rate (UFR) and rendering frame rate (RFR). The RFR
is simply the rate at which frames are drawn on the screen. (Note that it is
incorrect to define RFR as the “number of screen refreshes per second”. This
definition falls apart if r_norefresh 1
, which prevents the screen from
refreshing!)
TODO!!
Ducking physics¶
The bounding box is an imaginary cuboid, usually enclosing the player model. It is sometimes called the AABB, which stands for axis-aligned bounding box, which means the edges of the box are always parallel to the corresponding axes, regardless of player position and orientation. The bounding box is used for collision detection. If a solid entity touches this bounding box, then this entity is considered to be in contact with the player, even though it may not actually intersect the player model. The height of the player bounding box can change depending on the duckstate.
When the player is not ducking, we say the player is unducked and thus the duckstate is 0. In this state the bounding box has width 32 units and height 72 units. If the duck key has been held down for no longer than 1 second and the player has been onground all the while, then we say that the player duckstate is 1. At this point the bounding box height remains the same as that when the player has duckstate of 0. If the duck key is held (regardless of duration) but not onground, or if the duck key has been held down for more than 1 second while onground, the duckstate will be 2. Now the bounding box height will be 36 units, with the same width as before.
If the duck key is released while the player duckstate is 2, the duckstate will be changed back to 0 immediately, and the bounding box height will switch back to 72 units. However, if the key is released while the duckstate is 1, magic will happen: the player position will be shifted instantaneously 18 units above the original position, provided that there are sufficient empty space above the player. This forms the basis of ducktapping, sometimes referred to as the imprecise name “doubleduck”. Doubleduck is really a ducktap followed by another duck.
Note that the bounding box is an actual concept in the game code, while
duckstate is simply an easier abstraction used in our literature. In the code,
a duckstate of 0 means pmove->bInDuck == false
and (pmove->flags &
FL_DUCKING) == 0
. A duckstate of 1 means pmove->bInDuck == true
and
(pmove->flags & FL_DUCKING) == 0
still. Finally, a duckstate of 2 means
pmove->bInDuck == false
and (pmove->flags & FL_DUCKING) != 0
. Whereas
the type of bounding box is essentially selected by modifying
pmove->usehull
.
Now that we have described the concept of bounding box and duckstate, we will
now note that each of , and will be scaled down by
0.333 if the duckstate is 2. After the scaling down, becomes , ignoring floating point errors and
assuming original . However,
this is done before any change in duckstate happens in PM_Duck
. Suppose
the player has duckstate 0 before the call to PM_Duck
, and after
PM_Duck
is called the duckstate changes to 2. In this case, the
multiplication by 0.333 will not happen: the duckstate was not 2 before the
change. Suppose the player has duckstate 2 and the call to PM_Duck
makes
the player unducks, hence changing the duckstate back to 0. In this case the
multiplication will happen.
Jumping physics¶
Assuming the player is ongorund. Then jumping is possible only if he is
onground and the IN_JUMP
bit is unset in pmove->oldbuttons
.
Jumpbug¶
Jumpbug is one of a few exploits that can bypass fall damage when landing on any ground. The downside of jumpbug is that a jump must be made, which may be undesirable under certain circumstances. For example, when the player jumps the bunnyhop cap will be triggered.

To begin a jumpbug sequence, suppose that the player is initially not onground
(as determined by the first onground check) and that the duckstate is 2, as
illustrated by the +duck
bounding box in the figure above. Some time later
the player unducks, hence PM_UnDuck
will be called to change the duckstate
back to 0 and the second onground check will be triggered. If there exists a
ground 2 units below the player, then the player will now be onground (as shown
by the -duck
box above), and if +jump
happens to be active the player
will jump when PM_Jump
is called within the same frame (shown by the
+jump
box). But recall that PM_Jump
will always make the player to be
not onground. Also, as the upward velocity is now greater than 180 ups, when
the third onground check is made the player will again be determined to be not
onground. As a result, when the control passes to CBasePlayer::PostThink
,
the game will not inflict fall damage to the player.
Jumpbug can fail if the player was not able to unduck to make himself onground after the second groundcheck. The chances of this happening is greater at lower frame rates and higher vertical speeds.
Edgebug¶
TODO
Basevelocity and pushfields¶
In pm_shared.c
the basevelocity is stored in pmove->basevelocity
. This
is nonzero usually when being inside a trigger_push
or conveyor belt, which
are called pushfields. The way the player physics incorporates basevelocity
is vastly different for its horizontal and vertical components.
The basevelocity is always added to the player velocity after acceleration.
This means the new player position is computed by where is the
basevelocity. Shortly after that the basevelocity will be subtracted away from
before PM_PlayerMove
returns.
Interestingly, whenever the player leaves a pushfield the basevelocity of the pushfield will be added to the player’s velocity somewhere in the game engine. The added components will not be subtracted away. This is the basis of the famous push trigger boost, whereby a player ducks and unducks in rapid succession so that the bounding box enters and leaves the pushfield repeatedly.
The is handled differently. It is incorperated into in
PM_AddCorrectGravity
without being subtracted away later. Instead,
is set to zero in the function. Let us write to mean
for now. The vertical velocity at the -th frame would be
. But bear in mind that the position is
computed using instead.
Therefore, to find the position at an arbitrary -th frame we must
compute
These formulae can be useful in planning.
Water physics¶
Water movement is unfortunately not optimisable in Half-Life. However, we will still include a description of its physics here.
If the point 1 unit above the bottom of bounding box is immersed in water, then the waterlevel is 1. If the player origin (centre of bounding box) is additionally in water, then the waterlevel will be increased to 2. If the player’s view (origin plus view offset) is also in water, then the waterlevel will be 3. Depending on the existence of water current and the waterlevel, the magnitude of basevelocity may be modified.
In water physics the acceleration vector is provided at least one of , , is nonzero. Otherwise . Note that is an vector. In the context of water physics we denote , where it can be shown that, if not all , , are zero, then
Thus the water movement equation can be written as
with
The first thing we should notice is that is independent of , which means as the speed increases will inevitably decrease until it is negative. The speed can be written as
If and is sufficiently high so that , then we see that . This means the maximum possible swimming speed is simply . Moreover, assuming then the acceleration is independent of frame rate:
Also observe that the player always experience geometric friction while in the water.