|
|
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.html", 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" );
}
};
|