Celebrating 12 years of Quake!
Back to Main


Disclaimer: I was a bit hesitant to post this, because there's a problem with it. For some reason, the rocket can travel through the wall a very little bit before exploding, which I believe is an engine bug. LordHavoc didn't find anything wrong with my code, but could also not figure out what might help the problem. If anyone has any ideas, feel free to poke me about it. I believe it is timing related somehow. Note that the rocket going through the wall is purely a visual thing, it still actually explodes when hitting the wall.

Now, without further ado...



Clientside Rockets by Urre

So, after the highly discouraging introduction, you dare venture into the lands unknown and unsupported! Welcome!

As we go through the certain parts of the tutorial, I will also explain what the core functions we use do, and what they can/should be used for. What I won't give you, is detailed descriptions for how to setup things for compiling, as that can easily be a tutorial in itself. If you're familiar with FTEQCC, and know how to setup compile-paths in progs.src, it shouldn't be too tough.

For this tutorial you will need, as mentioned, a copy of the FTEQCC QuakeC compiler, by Spike. You will also need either a fresh base of the progs106 sourcecode for Quake, or know what you're doing with your own codebase. As for the CSQC, we will for this tutorial use Dresk's codebase. The engine I will be using is DarkPlaces by LordHavoc, but you can also use FTEQW, although the csqc support in that engine is at the time of writing somewhat different, and things might not behave the way I expect them to in this tutorial, so beware. I will expect you to use the windows beta build of 200808 of DarkPlaces, but it shouldn't be a bad thing to use a newer beta if such exists in the DarkPlaces files directory, or even better, a newer stable build.

I've got it all setup, let's code already!

Very well, but before we get on with the CSQC side of things, we must set some things up in SSQC. For starters, copy the dpextensions.qc file from the beta build zip into both of your sourcecode directories, CSQC and SSQC. This file contains all the (official) extensions supported by DarkPlaces, most of which are very useful things, in fact so useful that I've personally grown so accustomed to them, I will blindly expect them to be there, which is why I'm including it in this tutorial.

There is however one essential thing it lacks at the time of writing. The CSQC extension. Without it, we can't do what this tutorial sets out to show you. The CSQC extension is at the time of writing incomplete, and may very well change in its design, which it already has, a few times during my time using it. Open up dpextensions.qc which you placed in the SSQC source directory. At the very end of dpextensions.qc, there is an incomplete version of the extension, which adds the builtin called addstat. This function sends stats about the player, such as health, and other things, to the CSQC, info which can be used for displaying the HUD. Another tutorial will cover the use of that extension.

For the sake of cleanness, we will now replace that definition with a more complete version of the extension to the end of dpextensions.qc. This can be a very unwise thing to do, because if you ever update dpextensions.qc, you will lose this change, especially if the full extension isn't added to the file officially in a long time. You might want to put it at the end of defs.qc instead, if you want to keep it although you update the file, but then you will have to remember to remove it if EXT_CSQC is officially added to dpextensions.qc. Still having the SSQC version of dpextensions.qc open, remove the current version of the extension (which as mentioned before only has addstat), and replace it with this, as stolen from the EXT_CSQC entry on the Quake Wiki, at the very end:

// EXT_CSQC
// AddStat
void(float index, float type, ...) AddStat = #232; // third parameter is an entity field
float AS_STRING = 1;
float AS_FLOAT_TRUNCATED = 2;
float AS_FLOAT = 8;
// Entity Sending
.float(entity viewer) SendEntity;
.float Version;
float MSG_ENTITY = 5;
// Effect bit specifies that the entity is always to be sent to CSQC (provided it has a
// SendEntity function and Version is incremented. This is the same bit as used
// in DP_EF_NODEPTHTEST, but CSQC requires it even if the extension is not present.
float EF_NOPVSCULL = 8192;

I will go through the additions as they are used.

As you should know, whenever a new file is added to the source, you need to add it in progs.qc as well. Add dpextensions.qc to the SSQC progs.src just under defs.qc, and in CSQC progs.src under csqc_builtins.qc. Now we have a source which supports CSQC. Rejoice!

Making the rocket clientside

In SSQC, you're used to being able to do whatever you like, and expect it to happen when you tell it to. Generally speaking, this won't be the case in CSQC. An entity only exists for the client if he can see it (if it's in his PVS). This will mean that whenever our rocket enters a players PVS, it will be treated as an entirely new entity, no matter if it is or not. Entering the PVS could mean being being shot at by an opponent, as it will in most cases, but it can also mean appearing around a corner. This means many things, one being that you can't expect the first sight of an entity to be the initial spawning of it, so having the first sight even trigger something like a sound could be a bad idea. One idea which might strike you for example is to have the rocket firing sound event be entirely clientside, by playing it on the rockets origin as it spawns in CSQC. This could lead to the sound being played as the rocket flies past you around a corner. This isn't a limitation, it's just how things work in most networking situations and games. You just need to keep this in mind, and you'll find yourself figuring out other ways to do things in no time at all.

First we need to add a new constant, which will be used for interpreting what kind of entity is recieving an update in the CSQC. This will become important once you begin to add other shared entities than rockets. Fire up the SSQC defs.qc, and just like usual, at the bottom add this line:

float ENT_ROCKET = 1;

Now open up weapons.qc, and scroll down to W_FireRocket. Above that function, we shall make the all new function called W_SendRocket, which goes a little like this:

float() W_SendRocket =
{
WriteByte(MSG_ENTITY, ENT_ROCKET);
WriteCoord(MSG_ENTITY, self.origin_x);
WriteCoord(MSG_ENTITY, self.origin_y);
WriteCoord(MSG_ENTITY, self.origin_z);
WriteCoord(MSG_ENTITY, self.velocity_x);
WriteCoord(MSG_ENTITY, self.velocity_y);
WriteCoord(MSG_ENTITY, self.velocity_z);
return TRUE;
};

This is the function called by the engine whenever a new update is triggered for a rocket. These are the regular Write* builtins which already existed in Quake, which were used for things like cd track changes, end of episode texts and possibly more. What's been added is a new MSG_* type, namely MSG_ENTITY, as is part of the CSQC extension. It tells the Write* builtins that we're sending an update for a shared entity, so it'll know to send the information to all clients which have the entity in question in their PVS. As you can see, we're using the new constant here. Because it's a very small number, we're sending it as a byte. The origin and velocity fields are sent as coords, broken up into their components. Now how does one trigger an update, you might wonder. Wait, and you shall see.

Next up, we're going to assault the ever trusty W_FireRocket. At the very bottom of that function, add these lines:

	missile.SendEntity = W_SendRocket;
missile.Version = missile.Version + 1;

The first line tells the engine what function to use when sending updates to CSQC. In this case, the new function we just wrote, W_SendRocket. The second line is what triggers a send event at the end of the current frame. It works as such, that whenever self.Version is not the same as it was the previous frame, an update is triggered. That is why it is good practice to use + 1, instead of setting it to some fixed value, to really ensure an update will be made this frame. Something which should be noted, is that once you add these two lines, the entity is a shared entity, with all that implies. No updates are sent to the client unless you tell it to. It also becomes "invisible", unless you tell it otherwise in CSQC, which is why we are later assigning it the missile model in CSQC, as .SendEntity overrides all visual aspects of a shared entity. Note that the server still needs the entity to be somehow visible for it will be networked at all, this is why we are keeping the setmodel call in this function. Another thing I should point out, is that in order for the light to shine around corners and the explosion sound being sent to players who can't actually see the rocket, the rocket needs a form of radius to be networked with a larger scale. An easy trick is to add a light radius to the entity, and the server will know to send the entity to players who can't exactly see the rocket, but can see the light radius. But just like I mentioned, this is a visual aspect and will be overridden, thus you need to give it a light radius in CSQC as well if you want it to actually shine. In this case, we're lucky enough to have a light radius embedded into the model itself, as you might know. The Quake rocket model has a flag which gives it a fiery smoke trail, plus a light radius. The server knows about this, and thus sends it with a larger scale. If we however had a model without this flag, but still wanted it to be sent with an increased scale, you could do this by using the following lines:

	missile.light_lev = 300;
missile.pflags = PFLAGS_FULLDYNAMIC;

This is a heritage from Tenebrae, implemented to DP as an extension. missile.light_lev sets the radius, and missile.pflags... No idea, but it needs to be there. Tenebrae was weird. You could also do it with a single line:

	missile.effects = EF_DIMLIGHT;

But then you can't control the radius. I think EF_DIMLIGHT is about 500 qu (Quake Units).

Now we're going to do something which might seem odd, but will make even more sense once we head on to the CSQC side of things. Scroll up to T_MissileTouch. The bottom lines of the function which say:

	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
WriteByte (MSG_BROADCAST, TE_EXPLOSION);
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);

	BecomeExplosion ();

Comment those out, and add a remove(self); at the bottom. This is because we're going to handle the explosion event entirely in CSQC. Calling remove() will trigger the CSQC remove event for the specified entity, and we will be able to play the explosion animation before actually removing the entity in CSQC. Nifty, huh? Save your work, and close the file.

The CSQC side of things

Finally, we're at the awesome place of everything. This is where things get both interesting and strange, if you're used to the hackyness of SSQC. Brace yourself.

First, we need to add the constant ENT_ROCKET to our CSQC source as well. Open up Defs.qc in your csqc directory, and at the very bottom add:

float ENT_ROCKET = 1;

The fact that the values match between server and client is very important, as you will see later on. Under that, we'll add the field that value is read into:

.float enttype;

This field will store our new constant, which I will show very soon.

CSQC also doesn't automaticly do trails for models like SSQC does, so we'll have to create a field to store the effect number. At the bottom, add:

.float traileffect;

Save that file and close it. Now, very much like SSQC, CSQC likes models precached, so we shall add a precache call for the rocket model and explosion sprite. In Main.qc, there is a function called CSQC_Init, which just like Dresk's comment says, is called at initialization. So this makes the perfect spot to precache things in, and call other kinds of initial settings and whatnot. Inside that function, make lines called:

	precache_model("progs/missile.mdl");
precache_model("progs/s_explod.spr");

Before we continue, we need to change something in the source, which confused me a bit when I was first experimenting with these things, as I wasn't familiar with the preprocessor. Note the comment which says:

// BEGIN OPTIONAL CSQC FUNCTIONS

Under that, there's an #ifdef. This is a preprocessor directive, which tells the compiler to skip the following piece of code unless the options are met. In our case, we always want the following code to be read by the compiler, so remove that line, and at the very bottom remove the #endif. Anyone who has done C or C++ coding should know what these things are, and how they're used.

Next up, the CSQC_Ent_Update function. This is the function which is called in CSQC whenever the SSQC calls a Write* function with the first parameter set to MSG_ENTITY. This function both spawns an entity and updates it if already present. When the entity is new, the parameter bIsNewEntity will be TRUE. This is useful when you want to set things up, like what model the entity will be represented as, and so forth. Dresk has included some placeholder code in it, which quite effectively displays how it's used. You simply assign the values of Read* calls to variables. For the sake of cleanness, we're going to replace the contents of that function with the following:

	self.enttype = ReadByte();
if (self.enttype == ENT_ROCKET)
W_UpdateRocket(bIsNewEntity);

Now this is important. All those Write* calls you made earlier in the rocket's send function in SSQC, those are going to be sent to the CSQC in that specific order. This means that you need to do Read* calls of the same type in the specific order as they were sent from the server. This is why we want to know the type of entity at the first call, so we know which kind of Read* calls to do next. In CSQC_Ent_Update, self is the entity being updated. As you can see, first we call ReadByte and assign the value of that to self.enttype. This will be useful later on, when we want to spawn the explosion effect. Next we check what kind of entity we've been dealt, and as we only have the rocket as a shared entity thus far, we're left with the W_UpdateRocket function, which we will write shortly. First we'll finish our work in this file, namely handling the removal of the rocket.

Scroll down to the CSQC_Ent_Remove function. This function is called whenever the server removes a shared entity. In other words, this happens when you do remove(self); on the rocket in SSQC. In this function, self is once again the entity being updated (removed). Replace the code in that function with the following:

	if (self.enttype == ENT_ROCKET)
W_RemoveRocket();
else
remove(self);

You should now see the usefulness of the .enttype field. This lets us call a special removal function for the rocket, which will be an explosion effect! Save and close up.

For some reason, Dresk's codebase never included a setorigin builtin, so we need to add it. Open up csqc_builtins.qc, and just above the setmodel line (second line) add this:

void(entity e, vector o) setorigin = #2;

Save and close that file. Fire up the CSQC progs.src, and just above Main.qc add weapons.qc and save + close the file. Create a file called just that, weapons.qc, in the csqc directory and open it up. First, paste in the following function:

void(float newent) W_UpdateRocket =
{
self.origin_x = ReadCoord();
self.origin_y = ReadCoord();
self.origin_z = ReadCoord();
self.velocity_x = ReadCoord();
self.velocity_y = ReadCoord();
self.velocity_z = ReadCoord();
self.angles = vectoangles(self.velocity);
if (newent)
{
self.classname = "missile";
self.drawmask = MASK_NORMAL; // makes the entity visible
setmodel(self, "progs/missile.mdl");
self.traileffect = particleeffectnum("TR_ROCKET"); // assigns the rocket fire/smoke trail and light glow to the entity
self.think = RocketThink;
self.nextthink = time;
}
};

This is our lovely update function for the rocket, as mentioned earlier. If you remember, in the function called W_SendRocket in SSQC, we first wrote a byte to indicate the entity type. Then we had a bunch of coord's, indicating origin and velocity. In CSQC_Ent_Update, we first called ReadByte, to capture the entity type, to be able to call the correct update function. Now that we have called that function, we need to read the coord's being sent by the server. Hence we have ReadCoord, six times in a row, assigning the values of origin and velocity sent from the server, to the clientside variant of the entity. We set up the angles according to velocity, nothing strange there. Then we have the newent case. This means if it's an altogether new entity for the client, so it could need some things set up. self.drawmask = MASK_NORMAL tells the engine to draw the entity, and we assign the rocket particle trail number (with accompanying light glow) to the new .traileffect field we created. In our case, newent might be considered a bit silly, because the rocket only gets updated once (if disregarding the remove event), but for the sake of learning things, I'm showing how this is done. Now, above that function, yes you guessed it:

void() RocketThink =
{
local vector v;

self.nextthink = time;
v = self.origin + self.velocity*frametime; // magic?
trailparticles(self, self.traileffect, self.origin, v);
setorigin(self, v);
};

Here comes the odd part. CSQC doesn't support movetypes. Therefore, we need to do our own handling of the .velocity field. For anyone who never has given any thought to how the Quake engine moves things about, this could easily be mind boggling, but I'll give it my best shot, limiting myself to the MOVETYPE_FLYMISSILE movetype. In quake, a single frame of the game goes through all of the gamecode, and executes it accordingly. This should be a familiar concept to you. Now, each frame, the engine also loops through all of the entities, checking if they have a movetype and a velocity. At the end of the frame, the entity will gain a new origin, when the server adds the velocity vector multiplied with the length of that frame, to the origin. The longer the frame, the longer the move during that frame (lower framerates cause larger steps in time). In other words, the .velocity field is a speed parameter of qu/s (Quake Units per Second, a single quake unit being about an inch). The missile moves at 1000 qu/s, so at 50 fps, which translates to 0.02 seconds, it will move 1000*0.02 quake units, which is 20 quake units, in a single frame. For collision, Quake does either a traceline or a tracebox between the starting and end point of that move. That's the basics of movement in most game engines. Very many even skip the trace part, and just pop things out of solids at the end of the move. This leads to being able to move through thin walls if moving fast enough, as it never noticed the entity being inside that wall. We are skipping tracing in this tutorial, and just trust the server to remove the rocket as it hits a wall.

So, the line which assigns v, is what Quake does for velocity. Take the current origin, apply the velocity vector multiplied by the current framerate. Simple as that. Next we call trailparticles, which spawns the particles we assigned to the .traileffect field earlier, namely rocket smoke and fire (with a glow). After that, we give the rocket its new position. Thanks to the magic of self.nextthink = time, we'll have this happen every frame, in a smooth motion. Let's scroll down, and below the W_UpdateRocket function, paste the following:

void() SUB_Remove = {remove(self);};

void()	s_explode1	=	[0,		s_explode2] {};
void() s_explode2 = [1, s_explode3] {};
void() s_explode3 = [2, s_explode4] {};
void() s_explode4 = [3, s_explode5] {};
void() s_explode5 = [4, s_explode6] {};
void() s_explode6 = [5, SUB_Remove] {};

void() W_RemoveRocket =
{
te_explosion(self.origin);
setmodel (self, "progs/s_explod.spr");
s_explode1();
};

As you can see, I've blatantly ripped the animation code for the explosion sprite from the server code. I thought, why not, it's something that works, and it's something you recognize. You can also see that I've added the SUB_Remove function, because Dresk's codebase didn't have one. You can move it somewhere more suitable if you like, if you're building a proper mod. A good place would be a subs.qc file. Below the famous explosion macro, we have the code for the remove handling of the rocket we called earlier in CSQC_Remove_Ent. First we use one of the builtins in dpextensions.qc to create the explosion glow and particles, then we change the model of the rocket into the explosion sprite we precached earlier. After that, when we call the first frame of the animation, the .think field of the rocket is overwritten, because Quakes animation macros assign a new .think and .nextthink field, so we don't need to worry about the rocket flying any further, as it does in it's previous .think function.

Although minor, there is one problem with doing the explosion this way. Seeing as the entity was already removed on the server, when a client connects to a server during the playing of the explosion sprite, he will have no sprite there at all, as the server has nothing to send, since it considers it entirely gone. This also means a savegame which had a sprite playing in mid-explosion, would have no sprite or sound or effect when the game is loaded. Seeing as the sprite is a six frame animation, meaning it takes 0.6 seconds to play, I found it a problem so minor that I ignored it. If you think it's a grave enough problem, you will need to add a new ENT_* constant for the explosion, change the .SendEntity to one which sends the origin and frame of the explosion, read the first incoming byte into a local variable before assigning it, to know wether the .enttype changed, in order to handle wether it's the first time a client recieves the explosion or not, and add a new update function which starts/updates the explosion animation. I've probably forgotten something there too. But generally speaking, it's a bunch of work for little gain. You'd still not get the particles, as the state of those can't be saved in either case, nor the sound.

Ladies and gentlemen, that, is it! Save your new file, and quickly fire up your mod to witness... seemingly no difference at all! If that's the case, you've succeeded at your task. Although you may not see it, your mod is now a step closer to being superior for multiplayer use, because the rockets now use two small updates during their entire lifespan, instead of an update each server frame. That's a significant difference. Imagine the impact this would do for the nailguns. That's enough "ooh's" and "aah's". After the nailguns, try grenades, they're a bit trickier, but entirely doable, ofcourse.

- Urre

Back to Main