Quake DeveLS - Chase Camera v2.1 (The Initial ChaseCam) - (C) Copyright 1998. Author: James Williams - a.k.a: sATaN Description: Add a self-chasecamera for Quake 2. Difficulty: Medium / Med_Hard. (Can be quite scary... :>) Notes: This tutorial has been copyrighted. ForeWord +===================+ I've ironed out a hell of a lot of bugs, for instance, the game crashing when touching water, the camera getting stuck behind walls/sky, and the "shaking". The "shaking" still exists, but it has been drasticly reduced in volume. The biggest problem with all chasecams as such, is the re-think rate. The view will update every time the camera thinks, so that if you are playing with lag or a slow computer, it tends to move in jolts. This I'm not entirely sure how to fix, even with very small think times. I may have to change the think times to think frames which can be judged more accurately. I think there will still be a few bugs in this, and, as before, I'm leaving it up to those people who are fussy and can be bothered to fix them. Unless I stumble upon some perfect cure, then I may not get round to re-coding this much. I really don't like people that snitch other's code, and then pass it off as theirs. There is a code of conduct which all coders adhere to, and that "passing off" shits me. It contradicts everything programmers stand for. This tutorial will work with the Quake2 code release 3.14. The previous version did, to some extent, but the compiled DLL was not up-to-date. Please respect my intelligence, and my coding skills. A reminder, this tutorial is (C) Copyright 1998. You can however stick this into your mod, or use it as a basis for a mod, with permission from I, James Williams, if it is going to be a publicly available modification. If it's going to be commercial, I'd ESPECIALLY like to hear about it, (for copyright, and interest's sake.) You may distribute this as much as you want, as long as this tutorial is untouched. I would like people to distribute it, as it was a problem that frustrated me for a while until I figured it out. Other people out there are surely wanting to know how to do program this as well. My email is frolicka@hotmail.com, as I haven't been able to find a good ISP yet. Any comments or suggestions would be appreciated and you can also reach me with ICQ. My ICQ No. is 6580358. But have fun, and enjoy coding. It rocks, for sure! James Williams. Introduction +===================+ Quake I had some lovely functions... Namely, WriteEntity(), which was used for WriteByte (SVC_SETVIEWPORT) which in turn was a command that the coder could use for placing an external camera outside the player entity, by creating an entity which was used as a target for the display. Quake II has had this function removed (as such), and written in as code contained in the C files. The Quake II interface works like this : The client, (The human player controlling) is different from the player and so therefore the characteristics which are literally existent in the entity. This is a strange concept, I know. There are attributes within the client tag, like 'pmove'. The pmove tag and others similar in the same area, control the literal effects and think routines to manipulate the main entity. For instance, the "sv_gravity" cvar, effects the client gravity values in the pmove, which modifies certain effects to the players velocities, origins, and angles. These such client tags are applied to all player entities, and vary from view angles, to the offsets and origin of the view camera. The Main Coding +===================+ First open up the file G_LOCAL.H and go to the gclient_s structure. If you don't know what that is, then go to around line 835. The lines beginning with + are the lines i have added: ---------------- int weapon_sound; float pickup_msg_time; float respawn_time; // can respawn when time > this +/* ------------ + * SBOF: Chasecam variables + * ------------ */ + int chasetoggle; + edict_t *chasecam; + edict_t *oldplayer; }; ---------------- EXPLANATION: These new variables we just added are now accessible via (entity name)->client->chasetoggle, (entity name)->client->chasecam, and (entity name)->client->oldplayer. The chasetoggle is a number which we can use to determine whether the camera is on or not, while the others are entities that are easier to access via personally linked variables. The oldplayer entity is an entity which is used for display purposes to the person who is using the chasecam. This we will come to later. NEXT STEP: Still in the G_LOCAL.H file, go to the edict_s structure. Again, if you do not know what I'm talking about, go to around line 870. Go to the bottom of the structure and add: ---------------- gitem_t *item; // for bonus items // common data blocks moveinfo_t moveinfo; monsterinfo_t monsterinfo; +/* ------------ + * SBOF: Chasecam variables + * ------------ */ + int chasedist1; + int chasedist2; }; ---------------- EXPLANATION: These two integer values are for the chasecam to determine distances between the camera entity and the player. I shouldn't really say this, but they're not as important, really, as the chasecam and oldplayer entities, nor the toggle, because they are only called by one function, and are used by an entity which is not a client, which therefore disables the ability to edit the chascam's ->client variables as the camera is NOT a client. (Human player) These variables are accessed as (entity)->chasedist? and can be used for other integer functions in the same (entity)->chasedist? fields. NEXT STEP: Go to the very bottom of G_LOCAL.H and add : ---------------- extern void CheckChasecam_Viewent(edict_t *ent); ---------------- EXPLANATION: This becomes a global function which tells the compiler that it SHOULD be define later in the code and not to worry if it comes across it being mentioned before the routine is defined. NEXT STEP: Create a file called S_CAM.C and add these lines to the file: ---------------- #include "g_local.h" /*=========================================== -= C H A S E =- -= C A M E R A =- By James Williams for sATaN's BoX of FuN Edition 2 ==========================================*/ void ChasecamTrack (edict_t *ent); ---------------- EXPLANATION: Adding the #include will load up the Quake II code base which contains valuable information that other C files need to know so the C file can "talk" to Quake II. The ChasecamTrack function with no subsequent code, is for other functions that require this function before the function is defined, therefore letting the compiler know that the function is still yet to come and that the coder hasn't forgotten to put this function in. This line should technically be placed in a header file (ie, s_cam.h) and be included at the top of this file, but as it's the only line needed as a header, we don't justify creating a second file for it =) NEXT STEP: Add the creation functions for the chasecam : (to the same file) ---------------- /* The ent is the owner of the chasecam */ void ChasecamStart (edict_t *ent) { /* This creates a tempory entity we can manipulate within this * function */ edict_t *chasecam; /* Tell everything that looks at the toggle that our chasecam is on * and working */ ent->client->chasetoggle = 1; /* Make out gun model "non-existent" so it's more realistic to the * player using the chasecam */ ent->client->ps.gunindex = 0; chasecam = G_Spawn (); chasecam->owner = ent; chasecam->solid = SOLID_NOT; chasecam->movetype = MOVETYPE_FLYMISSILE; ---------------- EXPLANATION: The last four lines here will create an entity called "chasecam", make the owner of the entity, "ent" (me), make the camera not solid, and let it move around freely. NEXT STEP: Put this text into the same function at the end: ---------------- /* Now, make the angles of the player model, (!NOT THE HUMAN VIEW!) be * copied to the same angle of the chasecam entity */ VectorCopy (ent->s.angles, chasecam->s.angles); /* Clear the size of the entity, so it DOES technically have a size, * but that of '0 0 0'-'0 0 0'. (xyz, xyz). mins = Minimum size, * maxs = Maximum size */ VectorClear (chasecam->mins); VectorClear (chasecam->maxs); /* Make the chasecam's origin (position) be the same as the player * entity's because as the camera starts, it will force itself out * slowly backwards from the player model */ VectorCopy (ent->s.origin, chasecam->s.origin); chasecam->classname = "chasecam"; chasecam->nextthink = level.time + 0.100; chasecam->think = ChasecamTrack; ---------------- EXPLANATION: Make the chasecam entity's name be "chasecam" for paranoia reasons in case we have a bug of unknown origin, and wish to trace the bug via classnames. The nextthink is when the next think function is called, and it is called 0.1 seconds of level time in the future. The think function is ChasecamTrack, which is called very frequently to keep track of the position of the chasecam behind the player model. NEXT STEP: Finish the function by adding two VERY important lines: ---------------- ent->client->chasecam = chasecam; ent->client->oldplayer = G_Spawn(); } ---------------- EXPLANATION: This will copy the client's personal chasecam entity the object we just made, and create the "oldplayer" entity, which is just an object for display purposes. The "oldplayer" is updated in another function which we WILL get to, later in this tutorial. (I bet you're just ACHING to find out :>) NEXT STEP: Add in this function which becomes the think routine when the player is in water. In this case, the "ent" is the chasecam entity. ---------------- void ChasecamRestart (edict_t *ent) { /* Keep thinking this function to check all the time whether the * player is out of the water */ ent->nextthink = level.time + 0.100; /* If the player is dead, the camera is not wanted... Kill me and stop * the function. (return;) */ if (ent->owner->health <= 0) { G_FreeEdict (ent); return; } /* If the player is still underwater, break the routine */ if (ent->owner->waterlevel) return; /* If the player is NOT under water, and not dead, then he's going to * want his camera back. Create a new camera, then remove the old one * that's not doing anything. We could quite easily 're-instate' the * old camera, but I'm lazy :) */ ChasecamStart (ent->owner); G_FreeEdict (ent); } ---------------- EXPLANATION: Read the comments you ignoramus! :] NEXT STEP: Add in the function for removing the chasecam when asked to vanish, and to call the thinking function when underwater. ---------------- /* Here, the "ent" is referring to the client, the player that owns the * chasecam, and the "opt" string is telling the function whether to * totally get rid of the camera, or to put it into the background while * it checks if the player is out of the water or not. The "opt" could * have easily been a string, and might have used less memory, but it is * easier to have a string as it is clearer to the reader */ void ChasecamRemove (edict_t *ent, char *opt) { /* Stop the chasecam from moving */ VectorClear (ent->client->chasecam->velocity); /* Make the weapon model of the player appear on screen for 1st * person reality and aiming */ ent->client->ps.gunindex = gi.modelindex(ent->client->pers.weapon->view_model); /* Make our invisible appearance the same model as the display entity * that mimics us while in chasecam mode */ ent->s.modelindex = ent->client->oldplayer->s.modelindex; if (!strcmp(opt, "background")) { ent->client->chasetoggle = 3; ent->client->chasecam->nextthink = level.time + 0.100; ent->client->chasecam->think = ChasecamRestart; } ---------------- EXPLANATION: If the string sent to this function is telling us to go inactive but on, make the chasetoggle 3, as distinct from 0 which is off, because that will tell all other functions looking at the toggle variable, that the chasecam is not active, but is still there. Change the think routine to the ChasecamRestart function which checks if we are out of the water, and do it 0.1 seconds in the future. NEXT STEP: Continue with this function. ---------------- else if (!strcmp(opt, "off")) { ent->client->chasetoggle = 0; G_FreeEdict (ent->client->chasecam); } } ---------------- EXPLANATION: If the string sent to this function is NOT "background" but "off", then it is telling us to TOTALLY remove the chasecamera, toggle it off, and remove the display entity, which we won't need anymore. Do so. NEXT STEP: Gape at the amount of coding for some complicated camera manipulation, at which even I am at a loss to dictate all of. Therefore, all my comments here will be.. *ahem*.. brief :) ---------------- /* The "ent" is the chasecam */ void ChasecamTrack (edict_t *ent) { /* Create tempory vectors and trace variables */ trace_t tr; vec3_t spot1, spot2, dir; vec3_t forward, right, up; int dist; int cap; ent->nextthink = level.time + 0.100; /* if our owner is under water, run the remove routine to repeatedly * check for emergment from water */ if (ent->owner->waterlevel) { ChasecamRemove (ent->owner, "background"); return; } /* get the CLIENT's angle, and break it down into direction vectors, * of forward, right, and up. VERY useful */ AngleVectors (ent->owner->client->v_angle, forward, right, up); /* go starting at the player's origin, forward, ent->chasedist1 * distance, and save the location in vector spot2 */ VectorMA (ent->owner->s.origin, -ent->chasedist1, forward, spot2); /* make spot2 a bit higher, but adding 20 to the Z coordinate */ spot2[2] += 20.000; /* if the client is looking down, do backwards up into the air, 0.6 * to the ratio of looking down, so the crosshair is still roughly * aiming at where the player is aiming. */ if (ent->owner->client->v_angle[0] < 0.000) VectorMA (spot2, (ent->owner->client->v_angle[0] * 0.2), up, spot2); /* if the client is looking up, do the same, but do DOWN rather than * up, so the camera is behind the player aiming in a similar dir */ else if (ent->owner->client->v_angle[0] > 0.000) VectorMA (spot2, (ent->owner->client->v_angle[0] * 0.2), up, spot2); /* make the tr traceline trace from the player model's position, to spot2, * ignoring the player, with MASK_SHOT. These masks have been fixed * from the previous version. The MASK_SHOT will stop the camera from * getting stuck in walls, sky, etc. */ tr = gi.trace (ent->owner->s.origin, NULL, NULL, spot2, ent->owner, MASK_SHOT); /* subtract the endpoint from the start point for length and * direction manipulation */ VectorSubtract (tr.endpos, ent->owner->s.origin, spot1); /* in this case, length */ ent->chasedist1 = VectorLength (spot1); /* go, starting from the end of the trace, 2 points forward (client * angles) and save the location in spot2 */ VectorMA (tr.endpos, 2, forward, spot2); /* make spot1 the same for tempory vector modification and make spot1 * a bit higher than spot2 */ VectorCopy (spot2, spot1); spot1[2] += 32; /* another trace from spot2 to spot2, ignoring player, no masks */ tr = gi.trace (spot2, NULL, NULL, spot1, ent->owner, MASK_SHOT); /* if we hit something, copy the trace end to spot2 and lower spot2 */ if (tr.fraction < 1.000) { VectorCopy (tr.endpos, spot2); spot2[2] -= 32; } /* subtract endpos spot2 from startpos the camera origin, saving it to * the dir vector, and normalize dir for a direction from the camera * origin, to the spot2 */ VectorSubtract (spot2, ent->s.origin, dir); VectorNormalize (dir); /* subtract the same things, but save it in spot1 for a temporary * length calculation */ VectorSubtract (spot2, ent->s.origin, spot1); dist = VectorLength (spot1); /* another traceline */ tr = gi.trace (ent->s.origin, NULL, NULL, spot2, ent->owner, MASK_SHOT); /* if we DON'T hit anyting, do some freaky stuff */ if (tr.fraction == 1.000) { /* Make the angles of the chasecam, the same as the player, so * we are always behind the player. (angles) */ VectorCopy (ent->owner->s.angles, ent->s.angles); /* calculate the percentages of the distances, and make sure we're * not going too far, or too short, in relation to our panning * speed of the chasecam entity */ cap = (dist * 0.400); /* if we're going too fast, make us top speed */ if (cap > 5.200) { ent->velocity[0] = ((dir[0] * dist) * 5.2); ent->velocity[1] = ((dir[1] * dist) * 5.2); ent->velocity[2] = ((dir[2] * dist) * 5.2); } else { /* if we're NOT going top speed, but we're going faster than * 1, relative to the total, make us as fast as we're going */ if ( (cap > 1.000) ) { ent->velocity[0] = ((dir[0] * dist) * cap); ent->velocity[1] = ((dir[1] * dist) * cap); ent->velocity[2] = ((dir[2] * dist) * cap); } else { /* if we're not going faster than one, don't accelerate our * speed at all, make us go slow to our destination */ ent->velocity[0] = (dir[0] * dist); ent->velocity[1] = (dir[1] * dist); ent->velocity[2] = (dir[2] * dist); } } /* subtract endpos;player position, from chasecam position to get * a length to determine whether we should accelerate faster from * the player or not */ VectorSubtract (ent->owner->s.origin, ent->s.origin, spot1); if (VectorLength(spot1) < 20) { ent->velocity[0] *= 2; ent->velocity[1] *= 2; ent->velocity[2] *= 2; } } /* if we DID hit something in the tr.fraction call ages back, then * make the spot2 we created, the position for the chasecamera. */ else VectorCopy (spot2, ent->s.origin); /* If the distance is less than 90, then we haven't reached the * furthest point. If we HAVEN'T reached the furthest point, keep * going backwards. This was a fix for the "shaking". The camera was * getting forced backwards, only to be brought back, next think */ if (ent->chasedist1 < 90.00) ent->chasedist1 += 1; /* if we're too far away, give us a maximum distance */ else if (ent->chasedist1 > 90) ent->chasedist1 = 90; /* if we haven't gone anywhere since the last think routine, and we * are greater than 20 points in the distance calculated, add one to * the second chasedistance variable * The "ent->movedir" is a vector which is not used in this entity, so * we can use this a tempory vector belonging to the chasecam, which * can be carried through think routines. */ if (ent->movedir == ent->s.origin) { if (dist > 20) ent->chasedist2++; } /* if we've buggered up more than 3 times, there must be some mistake, * so restart the camera so we re-create a chasecam, destroy the old one, * slowly go outwards from the player, and keep thinking this routing in * the new camera entity */ if (ent->chasedist2 > 3) { ChasecamStart (ent->owner); G_FreeEdict(ent); return; } /* Copy the position of the chasecam now, and stick it to the movedir * variable, for position checking when we rethink this function */ VectorCopy (ent->s.origin, ent->movedir); } ---------------- EXPLANATION: Phew!... Don't ask me... Read the comments. NEXT STEP: Add in a relief step for all those people concentrating hard, a very simple function for toggling the camera on and off. ---------------- void Cmd_Chasecam_Toggle (edict_t *ent) { if (ent->client->chasetoggle) ChasecamRemove (ent, "off"); else ChasecamStart (ent); } ---------------- EXPLANATION: The "ent" here is the player, as this function is called through "cmd chasecam" as typed in from the console. We are yet to get to that, but, if the chasecam is on, inactive or not, we will remove it, by calling the ChasecamRemove function and telling it to turn OFF completely, which cleans up all the garbage variables. If the chasecam is off and we DONT have anything in the chasetoggle field, create us a camera entity with the ent being sent to the function being the player. NEXT STEP: Add in the support for the oldplayer display entity, which is still in the same file (s_cam.c), which is called from P_VIEW.C. ---------------- void CheckChasecam_Viewent (edict_t *ent) { if ((ent->client->chasetoggle == 1) && (ent->client->oldplayer)) { ent->client->oldplayer->s.frame = ent->s.frame; ---------------- EXPLANATION: The "ent" is the player. If the chasecam is on, _AND_ active, (1, not 3) and there is a display entity we can update, then update the frame of the display entity, with the entity of ourselves. Even though we are not using the player model, there are still references to the frame we should be displaying to everyone, and we are copying that frame to the vaild modelindex of the display entity, "oldplayer" NEXT STEP: Add a bit more to this routine of updating the oldplayer entity ---------------- /* Copy the origin, the speed, and the model angle, NOT * literal angle to the display entity */ VectorCopy (ent->s.origin, ent->client->oldplayer->s.origin); VectorCopy (ent->velocity, ent->client->oldplayer->velocity); VectorCopy (ent->s.angles, ent->client->oldplayer->s.angles); /* Make sure we are using the same model + skin as selected, * as well as the weapon model the player model is holding. * For customized deathmatch weapon displaying, you can * use the modelindex2 for different weapon changing, as you * can read in forthcoming tutorials */ ent->client->oldplayer->s.modelindex = ent->s.modelindex; ent->client->oldplayer->s.modelindex2 = ent->s.modelindex2; gi.linkentity (ent->client->oldplayer); } } ---------------- EXPLANATION: Read the comments, but the last line where it "links" the entity, makes sure that the game updates the state of the oldplayer entity model. The last line here is fairly important. Well, that's it for "S_CAM.C". Close "S_CAM.C" and open up G_CMDS.C for the implentation of toggling the chasecam. NEXT STEP: Scroll down to the ClientCommand() routine which handles all the... umm... commands and add in these lines: ---------------- else if (Q_stricmp (cmd, "wave") == 0) Cmd_Wave_f (ent); +/* ------------ + * SBOF: Gee... I don't know. + * ------------ */ + else if (Q_stricmp (cmd, "chasecam") == 0) + Cmd_Chasecam_Toggle (ent); ---------------- EXPLANATION: Now, this will activate the ability to type "CMD chasecam" at the console, or have this bound to a key to toggle the chasecam on and off. NEXT STEP: Close G_CMDS.C and open P_WEAPON.C. In this file you will add in the code so that when you change weapons when using the chasecam, the weapon will not appear in front of you. Scroll down to the ChangeWeapon() routine (about line 180) and go to the bottom of the function. Add in: ---------------- ent->client->ps.gunindex = 0; return; } ent->client->weaponstate = WEAPON_ACTIVATING; ent->client->ps.gunframe = 0; + if (!ent->client->chasetoggle) ent->client->ps.gunindex = gi.modelindex(ent->client->pers.weapon->view_model); } ---------------- EXPLANATION: This will check whether the chasecam is on, and it not, it will update the weapon model you see on the screen with the model associated with your current weapon. NEXT STEP: ******************************************* NB: This step is _*the*_ most important step of all. This is the step that has replaced the original Quake I WriteEntity() function that many people have been musing over. I have finally figured it out after months of "musing", and found that the LITERAL origin of the client display which is sent to the screen, each co-ordinate, MUST be multiplied by 8 to become a valid origin point in regards to the viewport. I apologize to those who did not know which file this was in, as in the beta release of the tutorial, I did not specify the file :-). This code is in P_VIEW.C. ******************************************* Change: ---------------- VectorAdd (v, ent->client->kick_origin, v); // absolutely bound offsets // so the view can never be outside the player box if (v[0] < -14) v[0] = -14; else if (v[0] > 14) v[0] = 14; if (v[1] < -14) v[1] = -14; else if (v[1] > 14) v[1] = 14; if (v[2] < -22) v[2] = -22; else if (v[2] > 30) v[2] = 30; VectorCopy (v, ent->client->ps.viewoffset); } ---------------- To: ---------------- VectorAdd (v, ent->client->kick_origin, v); // absolutely bound offsets // so the view can never be outside the player box + if (!ent->client->chasetoggle) + { if (v[0] < -14) v[0] = -14; else if (v[0] > 14) v[0] = 14; if (v[1] < -14) v[1] = -14; else if (v[1] > 14) v[1] = 14; if (v[2] < -22) v[2] = -22; else if (v[2] > 30) v[2] = 30; + } + else + { + VectorSet (v, 0, 0, 0); + if (ent->client->chasecam != NULL) + { + ent->client->ps.pmove.origin[0] = ent->client->chasecam->s.origin[0]*8; + ent->client->ps.pmove.origin[1] = ent->client->chasecam->s.origin[1]*8; + ent->client->ps.pmove.origin[2] = ent->client->chasecam->s.origin[2]*8; + VectorCopy (ent->client->chasecam->s.angles, ent->client->ps.viewangles); + } + } VectorCopy (v, ent->client->ps.viewoffset); } ---------------- EXPLANATION: This will copy the origin of the chasecam to the literal pmove.origin which is the origin of the display to the human using the computer, and multiply the chasecamera co-ordinates by 8, thus creating a valid origin point for the view camera. NEXT STEP: Go to the very bottom of the file, and in the very bottom of the function called ClientEndServerFrame(), add these lines. ---------------- + if (ent->client->chasetoggle == 1) + CheckChasecam_Viewent(ent); } ---------------- EXPLANATION: If the chasecam is ON, *AND* active, then we need to update the "oldplayer" display entity. NEXT STEP: Save and exit. You're done. I hope this fixes a lot of your problems. - Copyright (C) 1998. ----------------