Inside3D tutorials.
Created By: Alan Kivlin
eMail: alan.kivlin@cyberiacafe.co.uk
Difficulty Scale: Hard


FLOATING QUAKE ENTITIES


Would you like to see gibs, heads, dead bodies and backpacks float in water? Now you can by following this tutorial and with a little extra programming you could make other entities float (how about a grenade that starts to surface after it reaches the floor of the water).


PART 1 - PREPARING A NEW PATCH DIRECTORY
To start, you should have an unmodified copy of the QuakeC v1.06 source code and a copy of the file FLOATER.QC that accompanies this tutorial.

Create a directory called FLOATER in the Quake directory as you would normally do for any patch and then create another directory inside it called SRC.

Now copy the QuakeC source code, as well as the new source file FLOATER.QC, into the SRC directory.


PART 2 - ADDING A NEW SOURCE FILE
As we are introducing a new file, we must add it to the patch, the file PROGS.SRC informs the compiler which source files to include in a patch.
Add FLOATER.QC to PROGS.SRC (4) as follows: (The 4 in brackets above is the line number where you will be starting the changes and will be shown for each modification throughout the tutorial)

defs.qc
floater.qc // floating entity routines
subs.qc
fight.qc
(I have included some existing lines from above and below where the modification will be made, this shows how the changed file will look)


PART 3 - MODIFYING EXISTING SOURCE CODE
Modifications start and finish with a comment line of "// FLOATER". Please include all comments when you are making the modification as otherwise the line numbers given for each change will be wrong.

Modify CLIENT.QC (904) as follows:

local float mspeed, aspeed;
local float r;

// FLOATER
// controls the floating of the floaters
floaterPreThink();
// FLOATER

if (intermission_running)
By adding a call to floaterPreThink, we are giving Quake the ability to control floating entities (floaters). This routine checks (every frame) if any floaters exist, they are processed depending on their current state which can be, falling, surfacing, floating or sinking.


PART 4 - GIVING AN ENTITY THE ABILITY TO FLOAT
A floater will sink depending on its falling velocity, it will then float to the surface where it will bob up and down for around 30 seconds and finally it will sink until it hits the ground.

Sometimes you'll find a floater will retain its avelocity, making it spin around while bobbing - I didn't code for anything like this. Also an entity will sometimes keep it’s velocity_x or velocity_y.

A maximum of 32 (configurable - see FLOATER.QC) floaters can be active at one time - should this be the case when another is enabled then the oldest will be disabled (it will drop to the ground using normal quake physics movement).

To enable an entity's floating ability we must call a new routine found in FLOATER.QC called floaterEnable.

Modify PLAYER.QC (466) as follows:

new.origin = self.origin;
setmodel (new, gibname);

// FLOATER
// give it a Z axis size so it'll show up in water 
// as well as out of water
setsize( new, '0 0 -4', '0 0 12' );
// FLOATER

new.velocity = VelocityForDamage (dm);
new.movetype = MOVETYPE_BOUNCE;

The reason for changing the size of the gib is so that it'll show up in the water as well as out of the water when its bobbing on the surface.

(Next PLAYER.QC modification starts at line 484)

new.frame = 0;
new.flags = 0;

// FLOATER
// make the gib float
floaterEnable( new, 2 );
// FLOATER


There are other modifications still to be made, but if you want to compile the patch and run quake with it to see gibs floating in water, you can do so at this point.
The call to floaterEnable sets up the gib so that it will float should it enter water.
Two parameters are passed to floaterEnable.
The first parameter is the entity being enabled.
The second parameter is an offset added to the entity's origin used when working out if the entity is in water or out of water.

You can go straight to PART 5, if you aren’t interested in making entities (with the exception of those mentioned above) float in water.

Calculate the origin offset as follows:
Firstly, this is how I see an entity's origin in quake:
(I have limited knowledge so this is probably not how I should be thinking about it but until I find the time to read up on it, it'll have to do ;)

Quake encases a bounding box around an entity in the format 'X Y Z' (below origin, called mins), 'X Y Z' (above origin, called maxs) to give it a particular size (maxs - mins) and an origin (size - maxs).

The following diagram shows a head (heh use your imagination ;) which has a bounding box of '-16 -16 0', '16 16 56':

======   --- top of bounding box (56)


------
[head]
------   --- bottom of bounding box (0), origin (0)
~~~~~~   --- surface of water
(diagram not actual size ;)

When we check if the head is in water or not, we use it’s origin. In the case of our head above, it would hardly ever be in the water as it's origin is too close to the bottom of the bounding box.

Only the origin's Z axis is important when making the head float properly. We use (56 - 0) - 56 to calculate it, the result is 0 and is relative to the bottom of the bounding box.

To get the head bobbing properly we would pass as the second parameter to floaterEnable, a value of 5 (I use 5 because the appearance (model) of the head is in most cases only about 20% of its bounding box size) this would make the head's water checking origin as follows:

======   --- top of bounding box (56)


------
[head]   --- water checking origin (5)
------   --- bottom of bounding box (0), origin (0)
~~~~~~   --- surface of water
Here is another example, this time its a dead body, it’s bounding box extents are: '-16 -16 -24', '16 16 32'

======   --- top of bounding box (56)

         --- origin (24)
------
[body]   --- water checking origin (6)
------

======   --- bottom of bounding box
~~~~~~   --- surface of the water

In the case of the dead body, we need an offset of -18 (6 - 24) to get it to float in water properly.

I realise the above explanation isn't all that easy to get to grips with (I found it quite difficult to explain) but if you try different offset values depending on the bounding box of the entity I'm sure you'll be able to see how it works.


PART 5 - COMPLETING THE PATCH


Modify CLIENT.QC (483) as follows:

void() PutClientInServer =
{
   local entity spot;

   // FLOATER
   // only dead players float
   floaterDisable( self );
   // FLOATER

   spot = SelectSpawnPoint ();
(Next CLIENT.QC modification starts at line 791)

if (self.movetype == MOVETYPE_NOCLIP)
   return;

// FLOATER
// this was a check for < 0 but a player is also dead 
// when his health == 0, the reason for this minor change
// is that if the player dies with a health of zero, then this
// routine actually goes through the motions which causes
// the player not to float properly!
if( self.health <= 0 )
   return;
// FLOATER

if (self.waterlevel != 3)

Modify PLAYER.QC (504) as follows:

self.flags = self.flags - (self.flags & FL_ONGROUND);
self.avelocity = crandom() * '0 600 0';

// FLOATER
// make the head float
floaterEnable( self, 5 );
// FLOATER
(Next PLAYER.QC modification starts at line 573)

self.angles_x = 0;
self.angles_z = 0;

// FLOATER
// make the player's dead body float
floaterEnable( self, -18 );
// FLOATER

if (self.weapon == IT_AXE)
Modify COMBAT.QC (78) as follows:

if (self.flags & FL_MONSTER)
{
   // FLOATER
   // make the monster's dead body float
   floaterEnable( self, -18 );
   // FLOATER

   killed_monsters = killed_monsters + 1;
   WriteByte (MSG_ALL, SVC_KILLEDMONSTER);
}
Modify WORLD.QC (390) as follows:

setorigin (bodyque_head, ent.origin);
setsize (bodyque_head, ent.mins, ent.maxs);

// FLOATER
bodyque_head.sFloating = ent.sFloating;
bodyque_head.state = ent.state;
bodyque_head.speed = ent.speed;

bodyque_head.fOriginOffset = ent.fOriginOffset;

bodyque_head.ltime = ent.ltime;

if( ent.flags & FL_INWATER )
   bodyque_head.flags = bodyque_head.flags | FL_INWATER;
// FLOATER

bodyque_head = bodyque_head.owner;
The above modification makes the copy of a dead player have the same state as the player before he respawns. Modify ITEMS.QC (1380) as follows:

item.nextthink = time + 120;  // remove after 2 minutes
item.think = SUB_Remove;

// FLOATER
// make the backpack float
floaterEnable( item, 6 );
// FLOATER



That’s all there is to it, compile the patch then run quake with it.
Now that you've got it working with an unmodified version, you can easily do it to another patch out there. How about playing Dissolution of Eternity (this pack is great, it has so much atmosphere! - the QuakeC source code is available on Rogue's web page) with floating entities.

I gave the example of grenades floating in water, I think the easiest way is to make it a floater as soon as it is created. That way if it does enter water, it'll act accordingly, probably if it does hit water you'll want to add time to it's nextthink so it explodes a little later.

Have fun!

One final thing, I'd like to say a big thank you to id Software for QuakeC.



/*
==============================================================================
FLOATER.QC by Alan Kivlin <e-mail: alan.kivin@cyberiacafe.co.uk>

12 / MAY / 1997

Description of the Modification
-------------------------------

Routines for making an entity float to the surface of water.

A floater will sink depending on its falling velocity, it will then float
to the surface where it will bob up and down for around 30 seconds and 
finally it will sink until it hits the ground.

Sometimes you'll find a floater will retain its avelocity, making it 
spin around while bobbing - I didn't code for anything like this. Also an 
entity will sometimes keep it's velocity_x or velocity_y.

Copyright and Distribution Permissions
--------------------------------------

This modification is Copyright (C) 1997 by Alan Kivlin.

Authors MAY use this modification as a basis for other
publicly available work.
^^^^^^^^

Use this in a commercial endeavour and become a friend of Satan. Talk to me
first, ok?

DISCLAIMER: Alan Kivlin (aka Virtuoso) is not responsible for any harm or
psychological affects, loss of sleep, fatigue or general irresponsibility
from using this modification.

If you put this on a CD, you owe me one free copy of the CD. You also owe
everyone in the world a Quake related competition, with the prize being a 
free copy of the CD to the first 10 competition winners. You pay postage 
too. Don't like this? Don't put it on a CD!

If you take my work and rip my name off, you will burn in hell.

Thanks to Dave 'Zoid' Kirsch for his Copyright and Distribution Permissions
that I based the above on.
==============================================================================
*/

// last frame processed
float fFloaterLastFrame;

// "floating" when the entity is a floater
.string sFloating;

// origin offset when checking for water
.float fOriginOffset;

// floater state flags
float FS_FALLING   = 1;
float FS_SURFACING = 2;
float FS_FLOATING  = 3;
float FS_SINKING   = 4;

// maximum number of active floaters
float FLOATER_MAXIMUM = 32;

//----------------------------------------------------------------------------

// returns TRUE if in WATER, SLIME or LAVA
float( entity ent ) floaterInWater;

// makes the entity float
void( entity ent, float offset ) floaterEnable;

// stops the entity from floating
void( entity ent ) floaterDisable;

// controls the floating of the floaters
void() floaterPreThink;

//----------------------------------------------------------------------------

/*
==============================================================================
floaterInWater

returns TRUE if in WATER, SLIME or LAVA
==============================================================================
*/

float( entity ent ) floaterInWater =
{
   local vector where;
   local float contents;

   where = ent.origin;
   where_z = where_z + ent.fOriginOffset;

   contents = pointcontents( where );

   if( contents >= -5 && contents <= -3 )
      // is in WATER (-3), SLIME (-4) or LAVA (-5)
      return TRUE;

   return FALSE;
};

/*
==============================================================================
floaterEnable

makes the entity float
==============================================================================
*/

void( entity ent, float offset ) floaterEnable =
{
   local float floatercount;
   local entity floater, oldest;

   oldest = floater = find( world, sFloating, "floating" );

   while( floater )
   {
      floatercount = floatercount + 1;

      if( floater.ltime <= oldest.ltime )
         oldest = floater;

      floater = find( floater, sFloating, "floating" );
   }

   if( floatercount == FLOATER_MAXIMUM )
      floaterDisable( oldest );

   ent.sFloating = "floating";
   ent.state = FS_FALLING;
   ent.speed = 0;

   // save origin offset, used when checking for water
   ent.fOriginOffset = offset;

   // time to start sinking
   ent.ltime = time + 30 + random() * 5;

   if( floaterInWater( ent ) )
   {
      // MOVETYPE_TOSS in water
      ent.movetype = MOVETYPE_TOSS;

      // set inwater flag
      ent.flags = ent.flags | FL_INWATER;
   }
   else
   {
      // MOVETYPE_BOUNCE out of water
      ent.movetype = MOVETYPE_BOUNCE;

      // reset inwater flag
      ent.flags = ent.flags - ( ent.flags & FL_INWATER );
   }
};

/*
==============================================================================
floaterDisable

stops the entity from floating
==============================================================================
*/

void( entity ent ) floaterDisable =
{
   // stop floating
   ent.sFloating = string_null;
};

/*
==============================================================================
floaterPreThink

controls the floating of the floaters
==============================================================================
*/

void() floaterPreThink =
{
   local entity ent;

   if( fFloaterLastFrame == framecount )
      // already processed this frame
      return;

   // set last frame so we don't process a frame more than once
   fFloaterLastFrame = framecount;

   ent = find( world, sFloating, "floating" );

   while( ent )
   {
      if( ( ent.state == FS_FLOATING ) && ( ent.flags & FL_ONGROUND ) )
      {
         // if we are on the ground then we should be falling, this occurs
         // when the floater is on a moving platform that has left the water
         ent.state = FS_FALLING;
         ent.speed = 0;
      }

      if( ent.state == FS_FLOATING )
      {
         if( ent.speed > 0 )
            // floating up
            ent.velocity_z = ent.speed * ( 1 + frametime * 8 );
         else
            // floating down
            ent.velocity_z = 0;
      }
      else if( ent.state == FS_SURFACING )
      {
         if( ent.velocity_z > 0 )
            // keep surfacing to a constant speed
            ent.velocity_z = ent.speed;
         else if( floaterInWater( ent ) )
            // can't reach the surface so make it sink
            ent.state = FS_SINKING;
      }
      else if( ent.state == FS_SINKING )
         if( ent.flags & FL_ONGROUND )
            // sunk to the bottom
            floaterDisable( ent );
         else
            // sink slowly
            ent.velocity_z = 0;

      if( floaterInWater( ent ) )
      {
         if( ! ( ent.flags & FL_INWATER ) )
         {
            // MOVETYPE_TOSS in water
            ent.movetype = MOVETYPE_TOSS;

            // set inwater flag
            ent.flags = ent.flags | FL_INWATER;

            if( ent.state == FS_FLOATING )
               // start floating up
               ent.speed = 72 + random() * 16;
            else if( ent.state == FS_FALLING )
               // play enter water sound
               sound( ent, CHAN_BODY, "player/h2ojump.wav", 1, ATTN_NORM );
         }

         if( ent.state == FS_FALLING )
         {
            if( ! ent.speed )
            {
               if( ent.velocity_z < 0 )
                  // set maximum falling speed before floater should surface
                  ent.speed = ent.velocity_z * 8;
            }

            if( ( ent.velocity_z <= ent.speed ) || ( ent.flags & FL_ONGROUND ) )
            {
               // start surfacing
               ent.state = FS_SURFACING;
               ent.speed = 128 + random() * 32;

               ent.velocity_z = ent.speed;
               ent.velocity_x = 0;
               ent.velocity_y = 0;
            }
         }
      }
      else
      {
         if( ( ent.flags & FL_INWATER ) )
         {
            // MOVETYPE_BOUNCE out of water
            ent.movetype = MOVETYPE_BOUNCE;

            // reset inwater flag
            ent.flags = ent.flags - FL_INWATER;

            if( ent.state == FS_FLOATING )
               // start floating down
               ent.speed = 0;
            else if( ent.state == FS_SINKING )
            {
               // floater has sunk out of the water
               ent.state = FS_FALLING;
               ent.speed = 0;
            }

            if( ent.state == FS_FALLING )
               // play leave water sound
               sound( ent, CHAN_BODY, "player/h2ojump.wav", 1, ATTN_NORM );
         }

         if( ent.state == FS_SURFACING )
         {
            // once its surfaced, make it jump out
            ent.velocity_z = ent.speed * 1.5;

            // start floating down
            ent.state = FS_FLOATING;
            ent.speed = 0;
         }
      }

      if( ent.ltime <= time )
      {
         if( ent.flags & FL_INWATER )
         {
            // floater has taken in too much water so sink to the bottom
            ent.state = FS_SINKING;
            ent.ltime = time + 10 + random() * 5;
         }
      }

      // stop quake from making a splash sound
      ent.watertype = 0;

      // physics movement won't happen otherwise
      ent.flags = ent.flags - ( ent.flags & FL_ONGROUND );

      ent = find( ent, sFloating, "floating" );
   }
};