Quake 2 Source Code Commentary
Beginning to look at someone else's source code for a large project, is a daunting task. It can be overwhelming at first. Thus, before diving into the Quake 2 source code, I wanted to see if I could get a head start based on someone else's time and effort already spent exploring the code.
Searching the Internet, I am unable to find anything, thus I decided to record my own thoughts and observations as I look at the source code, in hopes that it may be of some use to someone else. This will at first be mostly a collection of my random thoughts and observations as I proceed, and may perhaps one day be organized into an more intelligible document.
Note that at the moment, this is organized in web-log fashion, with the most recent addition at the top.
1/19/2005
The renderer, examined yesterday, does not seem to do any sorting based on texture. It does sort the entities based on cache proximity, but not the textures. As the surfaces are being drawn, though, if the texture to be drawn is already set as the current texture, then time is saved by not resetting the texture state.
1/18/2005
Question: where/how does Pmove() get called? Server: SV_InitGameProgs() in sv_game.c sets the Pmove member of a game_import_t structure to the Pmove() function. (Reminder: the game import struct is pointers in the server passed to the game. The game export struct is filled with pointers in the game, and is passed to the server.) This is the only place in the server where this function is refered to. Game: ClientThink() in p_client.c calls gi.Pmove(). Client: CL_PredictMovement() in cl_pred.c calls Pmove() which is called by CL_Frame() in cl_main.c Question: how is the stair climbing motion smoothed CL_CalcViewValues() in cl_ents.c has code commented mentioning smoothing out stair climbing. which is called by CL_AddEntities(), which sends entities, particles, and lights to the renderer. in CL_CalcViewValues(), the renderer's vieworg (I assume this is the camera position) is moved down by an amount proportional to cl.predicted_step, and (it looks like) proportional to the amount of time left until 100 time units after the predicted stair-step took place. (quantities utilized: cls.realtime, cl.predicted_step_time, and cl.predicted_step) cl (client_state_t) and cls (client_static_t) are both global variables (defined in cl_main.c) cls.realtime holds the current game time In CL_PredictMovement(), after a Pmove, if the change in height of the player is >63 but <160, and the player is on the ground, then cl.predicted_step is set to this change in height *0.125, and cl.predicted_step_time is set to the time (in milliseconds) halfway between the last frame and this one (i.e. now - frametime/2) Question: why the *0.125? This must be some scaling factor between the world coords and the renderer, since the player (predicted) position is also scaled by 0.125. I read somewhere that one game unit in Quake equals 1.5 inches; this happens to be 0.125 feet. Perhaps this is the scaling factor here. This would indicate that while the game uses units equal to 1.5 inches (1/8 of a foot), the renderer uses feet. Rendering... SCR_UpdateScreen() (cl_scrn.c) calls V_RenderView() (cl_view.c) which makes the following calls: V_ClearScene() CL_AddEntities() (build a list of entities to draw) V_TestParticles() V_TestEntities() V_TestLights() the entities are sorted, then re.RenderFrame() SCR_DrawCrosshair() apparantly the server rounds to nearest 1/8 foot (thus planes and surfaces must be on a 1/8 foot boundary. Thus the camera is nudged by 1/16 in each axis to prevent the camera from lying exactly on any surface e.g. water. re.RenderFrame() is set to R_RenderFrame in GetRefAPI in r_main.c also in GetRefAPI in gl_rmain.c (incidentally, re.RenderFrame() is called in a few other places too. the GL version of R_RenderFrame() calls: R_RenderView() R_SetLightLevel() R_SetGL2D() R_RenderView() calls: R_PushDlights() qglFinish() R_SetupFrame() sets up matrices and other things R_SetFrustum() sets up 4 frustum planes R_SetupGL() sets up GL state eg viewport, matrices, culling, blending R_MarkLeaves() determines which nodes and leaves are potentially visible R_DrawWorld() R_DrawEntitiesOnList() R_RenderDlights() R_DrawParticles() R_DrawAlphaSurfaces() R_Flash() r_worldmodel is of type model_t (gl_model.h) R_DrawWorld() calls: R_ClearSkyBox() invalidates the skybox mins and maxs R_RecursiveWorldNode() DrawTextureChains() R_BlendLightmaps() R_DrawSkyBox() draws 6 faces R_DrawTriangleOutlines() R_RecursiveWorldNode() returns immediately if: is a solid region, is not potentially visible, bounding box is outside the frustum if a leaf, mark each surface that should be drawn if not a leaf: Call R_RecursiveWorldNode() on the near side child do drawable stuff for this node, then Call R_RecursiveWorldNode() on the far side child. for the current node, call R_AddSkySurface() for each sky surface if translucent, add to texturechain (linked list of surfaces) otherwise for each surface, call GL_RenderLightmappedPoly()
1/12/2005
Pmove(pmove_t) in pmove.c appears to be the function used by both the server and the client to move the player. the pmove_t struct is in q_shared.h Among other things, it looks like it has the bounding box for the player, the groundentity for the player, the waterlevel for the player, a pointer to the trace function to use, etc. It also contains a pmove_state_t struct, which is the information necessary for client side movement prediction. It contains the player's position, velocity, Pmove() calls PM_FlyMove if flying. Otherwise calls: PM_CheckDuck() PM_CatagorizePosition() (to set groundentity, watertype, and waterlevel) PM_DeadMove() (if dead) PM_CheckSpecialMovement() For normal movement, it calls PM_CheckJump() PM_Friction() possibly PM_WaterMove() or PM_AirMove() PM_CatagorizePosition() (again) PM_SnapPosition() PM_CatagorizePosition() works by doing a trace from the current position to one unit down. if didn't collide or the collide plane is too steep, then we're not on the ground. Checks first if vertical velocity is really large upward (don't do anything) Also, if vertical velocity is large enough downward, then set a timeout to prevent a jump from occuring too soon. Also, this ground entity is saved in an array as an entitiy that the player is touching. As for water level, it can be 1, 2, or 3. 1: point one unit above feet is in water 2: point halfway between feet and eyelevel is in water 3: point at eyelevel is in water These 3 points are checked in that order, calling pointcontents() PM_CheckSpecialMovement() checks for ladder, water jump, and jump out of water. PM_CheckJump() sets a flag when the player jumps and clears it when the player releases the jump button (i.e. the upward desired move is no longer big enough) to prevent repeated jumps by holding the jump button. If a jump happens, then an amount is added onto the upward velocity, and clamps upward velocity to a lower bound. PM_Friction() handles ground and water friction if speed is less than 1 unit, then horizontal movement is clamped to zero if the player is on the ground, and the surface is not slick or a ladder, then change in speed is computed as a proportion of the speed same if in water (and not on a ladder), but friction is multiplied by the waterlevel the change in speed is subtracted from the current speed and the velocity rescaled. PM_AirMove() is for normal movement on ground and jumping. computes desired horizontal velocity. calls PM_AddCurrents() for water currents, ladder movement, and conveyor belts gets speed and normal dir for desired vel. clamps desired vel to max speed (different whether ducked or not) ladder case is handled first calling PM_Accelerate and PM_StepSlideMove otherwise, if we're on the ground (i.e., there is a groundentity) then the vertical velocity is set to zero and calls PM_Accelerate(). Then gravity is subtracted from velocity, and PM_StepSlideMove is called only if there is a horizontal movement (no need to fall since we are on the ground) Otherwise, if we're not on ground, then call PM_AirAccelerate, subtract gravity and call PM_StepSlideMove(). PM_Accelerate() takes a desired direction and speed (and acceleration constant) The component of current velocity in this direction is computed. The difference in the desired and the current component is computed, and a portion of the desired velocity is computed (based on the acceleration contstant). This portion is is clamped to make sure it's not accidentally greater than the desired increase in speed. This value is now added on to the current velocity. The result of this is (given a constant desired velocity) the current velocity is changed, linearly over time to the desired velocity. (It would be interesting to try modifying this so that the amount of change is instead proportional to the difference between the desired and current velocities.) PM_AirAccelerate appears to be exactly the same, but clamps the desired speed to a maximum speed.1/10/2005
Looking at SV_movestep() in m_move.c This is the fuction that causes an entity to step up stair steps. This is done by doing a trace straight up and down, from one STEPSIZE above a point to one stepsize below the point. If allsolid, then it's impossible, if startssolid then try just going from the point to one stepsize below it. If the trace encounters no obstacle, then the ground got pulled out from under or we walked off a cliff or something. what does the relink argument do? So, my question is, what happens when there is a wall alongside a staircase and the user walks into the corner where a stair step and the wall meet. Then this vertical trace will be allsolid, entirely inside the wall, whereas what we would like is to slide along the wall and up the stair. how does gi.pointcontents() work? where,when is SV_movestep() called? ans: only in SV_StepDirection() and M_walkmove(), both in m_move.c when a horizontal movement is attempted by the entity, a horizontal trace is not performed--only the vertical trace at the ending location, to check for the step. Wouldn't this allow moving through walls? I attempted bypassing SV_movestep() and recompiling. The only apparent result is that the monsters aren't able to move. The (client) player is still able to climb stairs, so the player stairclimbing code must be elsewhere. pmove.c looks promising (is this "player move"?) It's in qcommon\ so that both the client and server can call its functions. Two promising functions: PM_StepSlideMove_() and PM_StepSlideMove() PM_StepSlideMove_() appears to compute a sliding movement very similar to that in SV_FlyMove(). PM_StepSlideMove() uses the previous function to compute the player movement. Two movements are computed--a "down" movement for regular moving and an "up" movement for stepping. The "down" movement is just a regular sliding movement using the current velocity. The "up" movement is computed by jumping straight up STEPSIZE (checking to make sure that position is not a colliding one), then computing a sliding movement using the current velocity, then doing a trace straight down a STEPSIZE. Whichever movement ("up" or "down") resulted in the larger horizontal distance is the one we use (alternate code used the movement that had the largest distance along the direction of the velocity), unless the "up" move landed us on a plane that is too steep (i.e., it was too steep for us to have stepped up onto it.) I'm not exactly sure what the last line of this function is for.
12/22/2004
SV_RunGameFrame calls ge->RunFrame ge gets assigned in SV_InitGameProgs() in sv_game.c using Sys_GetGameAPI() Sys_GetGameAPI() is in sys_win.c Sys_GetGameAPI() loads the game dll (e.g. gamex86.dll) by name and calls "GetGameAPI" from the dll to obtain the struct that will get assigned to ge. ge stands for "game export". It looks like ge gets filled with all the game functions that the server will need. The server passes in a "game import" (gi) struct containing all the server functions that the game needs. This function can be found in g_main.c so, ge->RunFrame points to G_RunFrame apparently G_RunFrame always advances the world by 0.1 seconds, not by elapsed time. interesting. then how do we get a frame-rate of greater than 10fps? The game makes the following calls once per 0.1 seconds? // choose a client for monsters to target this frame AI_SetSightClient (); calls G_RunEntity on each entity (including the world) calls M_CheckGround(ent) to see if the ground under the entity moved // see if it is time to end a deathmatch CheckDMRules (); // see if needpass needs updated CheckNeedPass (); this is for passwords // build the playerstate_t structures for all players ClientEndServerFrames (); G_RunEntity(ent) makes the following calls ent->prethink One of: SV_Physics_Pusher (ent); (for pushable things like doors I think) SV_Physics_None (ent); SV_Physics_Noclip (ent); SV_Physics_Step (ent); (for players and monsters I think) SV_Physics_Toss (ent); (for items and corpses and stuff; basic freefall/bounce) ground is any surface normal with positive z SV_Physics_Step() calls: M_CheckGround() see if we're on the ground SV_CheckVelocity() // bound velocity SV_AddRotationalFriction() if any angular velocity SV_AddGravity() if airborn TODO: findout what hitsound is apply friction to velocity in the vertical direction for flying and swimming monsters. TODO: see what M_CheckBottom does if player is moving: apply friction to x and y directions SV_FlyMove() gi.linkentity() G_TouchTriggers() gi.sound() play sound if player wasn't on ground but now is. // regular thinking SV_RunThink(ent) SV_FlyMove handles moving according to the entity's velocity, calling collision detection (gi.trace() (which is SV_Trace)), and sliding along surfaces. We only loop a specified number of times (4), each time constraining the velocity to be parallel to the surface. Or, if we collide with 2 surfaces without moving (and constraining velocity to any of the surfaces still results in a velocity component entering one of the surfaces), then constrain velocity along the crease between the surfaces. Or, if we are in contact with >2 surfaces without moving and no valid constraining of the velocity is possible, then set velocity = 0. SV_Trace() in sv_world.c does collision detection for a moving (axis aligned) box. It is called when any entity moves. It calls CM_BoxTrace() to check collision with the world and SV_ClipMoveToEntities() to check collision with other solid entities CM_BoxTrace is in cmodel.c It has a few special cases: Stationary box: calls CM_BoxLeafnums_headnode() and CM_TestInLeaf() Moving single point (box size 0) and general case: calls CM_RecursiveHullCheck() I assume CM_RecursiveHullCheck() traverses the BSP. When a leaf is reached, CM_TraceToLeaf is called. CM_TraceToLeaf() Checks all brushes in the leaf marks each brush as it's checked to make sure we don't check the same brush twice in different leaves. Looks like each time a trace is done, a checkcount is incremented and each node in the bsp has a checkcount variable that is compared to the current check number. Calls CM_ClipBoxToBrush() for each brush if this causes the movement to goto zero, then we're done with the entire trace ClipBoxToBrush() checks if brush has no sides loops through all the sides Question: Where is the code for stair-climbing?
12/21/2004
Looking at CL_Frame() The client makes the following calls once per frame: IN_Frame() (input?) // fetch results from server CL_ReadPackets (); // send a new command message to the server CL_SendCommand (); // predict all unacknowledged movements CL_PredictMovement (); SCR_UpdateScreen (); (render the scene) // update audio S_Update ( ); (passing it the player position and orientation) CDAudio_Update(); // advance local effects for next frame CL_RunDLights (); CL_RunLightStyles (); SCR_RunCinematic (); SCR_RunConsole (); Looking at SV_Frame() The server makes the following calls once per frame: // check timeouts SV_CheckTimeouts (); (see if we lost a connection to a client) // get packets from clients SV_ReadPackets (); // update ping based on the last known frame from all clients SV_CalcPings (); // give the clients some timeslices SV_GiveMsec (); // let everything in the world think and move SV_RunGameFrame (); // send messages back to the clients that had packets read this frame SV_SendClientMessages (); // save the entire world state if recording a serverdemo SV_RecordDemoMessage (); // send a heartbeat to the master if needed Master_Heartbeat (); (I'm not sure what 'master' is) // clear teleport flags, etc for next frame SV_PrepWorldFrame ();
12/20/2004
Once per message loop, WinMain calls Qcommon_Frame, with the number of elapsed milliseconds, which in turn calls SV_Frame and CL_Frame. The server calls the 'game' (SV_Frame() calls SV_RunGameFrame() calls ge->RunFrame() ) and the client calls the 'refresh' (CL_Frame() calls SCR_UpdateScreen()) calls V_RenderView calls re.RenderFrame() which is a function pointer to R_RenderFrame() (in r_main.c) calls R_EdgeDrawing() calls R_RenderWorld() (in r_bsp.c) which apparently renders the bsp tree by calling R_RecursiveWorldNode() which renders faces (from visible leaf nodes) front to back calling R_RenderFace (in r_rast.c)
12/17/2004
Initial observations: Directory tree: // these appear self-explanatory. From Michael Abrash's "Ramblings" I recall // that the client in Quake 1 handles basic simulation and rendering, while the // server handles the authoritative simulation. Player input is received by the //client, sent to the server, and game updates are sent from server to client. client // some, not all, files with cl_* Appears to be mostly audio, input and view/display stuff. server // files easily recognizable as server.h or sv_*.c // things that aren't client and aren't server? // appears to contain the game logic and physics game // the renderer, I suppose. Called by the client? ref_gl ref_soft // Capture the flag addon ctf // wrappers for the os specifics irix linux solaris win32 null // donno yet qcommon // apparently stuff used by both client and server rhapsody // ??? We're left with many questions. Let's move on... ------------------------------------------------- Opening the VC6 project reveals 5 projects: ctf game quake2 ref_gl ref_soft Okay... why 5 projects? What's the difference between 'game' and 'quake2'? 'game' seems to have mainly files from the 'game' directory (makes sense), so just game logic. 'quake2' contains the client and server Still, why are 'game' and 'quake2' separate projects? For that matter why is the renderer 2 additional separate projects? Doesn't a project correspond to a resulting executable? Answer: 'ctf' and 'game' both build gamex86.dll 'quake2' builds quake2.exe 'ref_gl' builds ref_gl.dll 'reg_soft' builds ref_soft.dll ------------------------------------------------- Well, since at this point in time, I am most interested in the world-models/renderer, I'll start with ref_gl. The 'ref_gl' project has all the source files from the 'ref_gl' directory plus: win32\glw_imp.c win32\glw_win.h win32\q_shwin.c win32\qgl_win.c win32\winquake.h game\q_shared.c game\q_shared.h qcommon\qcommon.h qcommon\qfiles.h client\qmenu.h client\ref.h plus a nonexistant ref_gl.h gl_model.h seems to contain the definitions for structs handling brushes and polygons forming the world model qfiles.h contains data about all the quake data file formats This is just a guess, but it seems that types beginning with d (like dmodel_t) correspond to how the information is formatted in a data file, while types beginning with an m (like mmodel_t) correspond to how quake stores the information in memory. I believe ref_ stands for 'refresh'. win32\sys_win.c contains WinMain() win32\vid_dll.c contains MainWndProc() Attempting to build everything (Release) resulted many, many warnings, and 2 errors (both caused by 'ref_soft'; the others compile fine)