Drowning a Player
by Michael Hall · 06/14/2012 (8:39 pm) · 15 comments
Here is a demonstration of how to simulate drowning someone after they have spent 'x' amount of time underwater.
This Resource will illustrate:
Basic operation and logic will consist of starting a new function when a player's callback for entering water is triggered. This function will check to see if the player is underwater, and if he is to increment a timer that keeps track of how long the player has held his breath. We will also display a countdown bar or meter to represent this timer on the screen. Once the check is done, it will start itself over, performing the underwater check, incrementing the timer and reducing the bar. This repeats until the player runs out of air and then begins to choke, suffering damage until either he surfaces or is dead. Once the player surfaces, the timer is reset. Once the player leaves the water, the check is cancelled, ending the loop. If the player dies, the loop is cancelled.
Add these variables to the top of your player.cs script file:
Next you'll need to add a call to start the "checkUnderwater" function.
Add this schedule to the end of your onEnterLiquid() callback:
Now you need a way of canceling a scheduled "checkUnderwater" function once you leave the water.
Add this to the end of your onLeaveLiquid() callback:
*Note that you could alternatively place the schedule start within an onStartSwim() callback, and the schedule cancel within an onStopSwim() callback. This could be an optimization to keep checkUnderwater from firing when wading in shallow water.
You'll notice that instead of simply calling checkUnderwater() that it's schedule was set as equal to the dynamic variable %obj.drowning (.drowning is a property attached to this specific object). Doing this allows us to interrupt and cancel the schedule at any time by referencing the object's drowning property.
This is the "checkUnderwater" function that does the work:
To prevent the Damage Direction indicator from flashing whenever drowning damage is applied:
Find this line within Armor::damage()
Now we need to tell the game to display a message when a death occurs due to the Drowning DamageType:
Add this near the other kill messages in scripts/server/gameCore.cs:
Inside of scripts/client/games.cs add this ShowBreathMeter Client Command:
This is how the server tells a client to display the breathmeter.
The BreathMeter is a GuiProgressCtrl with the name "BreathMeter".
You can add yours to the playGui, found in (art/gui/playGui), in whatever manner you prefer. Here is one for an example:
Start up a mission that has a water object and go swimming. Dive down and after a few seconds you'll begin to run out of air and drown.
Applicable to all ShapeBase objects, this can be used to drown little puppies and kittens as well. Recommended that you only do so in-game of course!

This Resource will illustrate:
- how to use a built in callback to make something happen
- how to use a raycast in combination with a search filter to check for a specific object type
- how to apply damage to a given object
- how to start, repeat, and cancel a schedule
- server commandToClient functionality (for GUI manipulation)
- how to use a GuiProgressCtrl to represent a "breath meter".
Basic operation and logic will consist of starting a new function when a player's callback for entering water is triggered. This function will check to see if the player is underwater, and if he is to increment a timer that keeps track of how long the player has held his breath. We will also display a countdown bar or meter to represent this timer on the screen. Once the check is done, it will start itself over, performing the underwater check, incrementing the timer and reducing the bar. This repeats until the player runs out of air and then begins to choke, suffering damage until either he surfaces or is dead. Once the player surfaces, the timer is reset. Once the player leaves the water, the check is cancelled, ending the loop. If the player dies, the loop is cancelled.
Add these variables to the top of your player.cs script file:
// How often to check the player's underwater status. $Drowning::TickTime = 1 * 1000; // The length of the hold breath timer in number of ticks. Used in combination // with $Drowning::TickTime to calculate how long a player can hold his breath // before taking damage. $Drowning::HoldBreathTicks = 20; // Damage done per $Drowning::TickTime if the player is underwater and the // hold breath timer has expired. $Drowning::DamagePerTick = 5;
Next you'll need to add a call to start the "checkUnderwater" function.
Add this schedule to the end of your onEnterLiquid() callback:
%obj.drowning = schedule($Drowning::TickTime, 0, "checkUnderwater", %obj);
Now you need a way of canceling a scheduled "checkUnderwater" function once you leave the water.
Add this to the end of your onLeaveLiquid() callback:
cancel(%obj.drowning);
*Note that you could alternatively place the schedule start within an onStartSwim() callback, and the schedule cancel within an onStopSwim() callback. This could be an optimization to keep checkUnderwater from firing when wading in shallow water.
You'll notice that instead of simply calling checkUnderwater() that it's schedule was set as equal to the dynamic variable %obj.drowning (.drowning is a property attached to this specific object). Doing this allows us to interrupt and cancel the schedule at any time by referencing the object's drowning property.
This is the "checkUnderwater" function that does the work:
function checkUnderwater(%obj)
{
if (%obj.getState() $= "Dead")
{
// If we get here and the player is dead we should hide the breath meter
// and make sure that the next underWater check is cancelled.
commandToClient(%obj.client, 'ShowBreathMeter', false);
cancel(%obj.drowning);
}
else
{
// We'll use a "drowning" damageType so the game will know to distinguish
// between a drowning and other types of death.
%damageType = "Drowning";
// If a ray cast straight up from the eye point of the player intersects
// the surface of a waterblock then the player is obviously underwater.
%start = %obj.getEyePoint();
%end = VectorAdd(%start, "0 0 100"); // change if water is deeper than 100
%searchMasks = $TypeMasks::WaterObjectType;
if (ContainerRayCast(%start, %end, %searchMasks))
{
// If the player is underwater increment a "holding breath" counter.
%obj.holdingbreath++;
// A GuiProgressCtrl expects values 0-1. We're just calculating a
// percentage in order to generate the appropriate range of values
// that will be used to adjust the lenght of the bar shown. The
// bar shrinks over time until we're out of air.
%remainingTime = ($Drowning::HoldBreathTicks - %obj.holdingBreath) / $Drowning::HoldBreathTicks;
if(%remainingTime < 0)
%remainingTime = 0;
commandToClient(%obj.client, 'ShowBreathMeter', true, %remainingTime);
// If the holdingbreath counter is greater than $Drowning::HoldBreathTicks
// then damage the player - he's choking on water!
if (%obj.holdingbreath > $Drowning::HoldBreathTicks)
%obj.damage(0, %obj.getPosition(), $Drowning::DamagePerTick, %damageType);
}
else
{
// The player appears to have surfaced and is breathing normally.
// Reset the holdingbreath counter and hide the breathmeter.
%obj.holdingbreath = 0;
commandToClient(%obj.client, 'ShowBreathMeter', false);
}
// We're still in the water and not dead yet, so do it again.
%obj.drowning = schedule($Drowning::TickTime, 0, "checkUnderwater", %obj);
}
}To prevent the Damage Direction indicator from flashing whenever drowning damage is applied:
Find this line within Armor::damage()
// Determine damage direction if (%damageType !$= "Suicide") %obj.setDamageDirection(%sourceObject, %position);...and change it to:
// Determine damage direction if (%damageType !$= "Suicide" && %damageType !$= "Drowning") %obj.setDamageDirection(%sourceObject, %position);
Now we need to tell the game to display a message when a death occurs due to the Drowning DamageType:
Add this near the other kill messages in scripts/server/gameCore.cs:
// Customized kill message for drowning
function sendMsgClientKilled_Drowning(%msgType, %client, %sourceClient, %damLoc)
{
messageAll(%msgType, '%1 is not Aquaman!', %client.playerName);
}Inside of scripts/client/games.cs add this ShowBreathMeter Client Command:
This is how the server tells a client to display the breathmeter.
function ClientCmdShowBreathMeter(%show, %remainingTime)
{
// %show (true/false) toggles visibility of the control
// %remainingTime (0-1) is used to vary the length of the bar
BreathMeter.setVisible(%show);
BreathMeter.setValue(%remainingTime);
}The BreathMeter is a GuiProgressCtrl with the name "BreathMeter".
You can add yours to the playGui, found in (art/gui/playGui), in whatever manner you prefer. Here is one for an example:
new GuiProgressCtrl(BreathMeter) {
text = " HOLDING BREATH";
maxLength = "1024";
margin = "0 0 0 0";
padding = "0 0 0 0";
anchorTop = "1";
anchorBottom = "0";
anchorLeft = "1";
anchorRight = "0";
position = "384 88";
extent = "256 16";
minExtent = "8 2";
horizSizing = "right";
vertSizing = "bottom";
profile = "GuiProgressProfile";
visible = "0";
active = "1";
tooltipProfile = "GuiToolTipProfile";
hovertime = "1000";
isContainer = "1";
hidden = "1";
canSave = "1";
canSaveDynamicFields = "0";
};Start up a mission that has a water object and go swimming. Dive down and after a few seconds you'll begin to run out of air and drown.

About the author
Been dabbling with game-programming since the age of 10 when I got my first computer, a Commodore. Got serious about game-development after modding Tribes for several years. Doesn't sleep much. Drinks rum. Teaches guitar. Plays cello.
#2
06/14/2012 (10:48 pm)
Whoops! Great suggestion, thanks Daniel! Can't believe I didn't consider that -- fixing it now.
#3
06/15/2012 (5:06 am)
@Michael: you can optimize it more by using built-in callbacks:function PlayerData::onEnterLiquid(%this, %obj, %coverage, %waterType)
{
//
}
function PlayerData::onLeaveLiquid(%this, %obj, %waterType)
{
//
}orfunction PlayerData::onStartSwim(%this, %obj)
{
//
}
function PlayerData::onStopSwim(%this, %obj)
{
//
}
#4
I had done a few tests with the swimming callbacks, thinking that the coverage calculation that kicks them off would keep me from needing the raycast, and that ended up drowning the player while visually swimming on the surface, so ended up ignoring those.
But yeah, triggering the schedule from either set of callbacks would work the same.
06/15/2012 (7:15 am)
It is using the onEnterLiquid/onLeaveLiquid callbacks to start and end things. I had done a few tests with the swimming callbacks, thinking that the coverage calculation that kicks them off would keep me from needing the raycast, and that ended up drowning the player while visually swimming on the surface, so ended up ignoring those.
But yeah, triggering the schedule from either set of callbacks would work the same.
#5
06/15/2012 (7:33 am)
Moving the schedule for checkUnderwater() to the onSwim start/stop callbacks would definitely make sense to keep it from firing when in shallow water, so I'll make note of that, thanks!
#6
06/15/2012 (9:06 am)
Oh sweet, I was looking to make something like this at some point for my FPS project. Nice resource Micheal.
#7
06/16/2012 (10:05 am)
Thanks Mike! Great stuff as usual!
#8
06/19/2012 (6:37 am)
Nice, I was just planning something like this for myself. Thanks, great job.
#9
Just added this to my game, but for some strange reason it doesn't work with a riverobject. I don't think it is in your code, the river surface doesn't get detected by the ContainerRayCast. Btw: I am using T3D 1.1 with 1.2 patch applied.
06/19/2012 (2:14 pm)
@Michael:Just added this to my game, but for some strange reason it doesn't work with a riverobject. I don't think it is in your code, the river surface doesn't get detected by the ContainerRayCast. Btw: I am using T3D 1.1 with 1.2 patch applied.
#10
06/19/2012 (3:06 pm)
Hmmm, I didn't think about the RiverObject... I'll have to check into that, thanks for me letting me know.
#11
06/19/2012 (3:19 pm)
Cool stuff, nice to see more resources around here =)
#12
www.youtube.com/watch?v=D0RkDhM9aQE&feature=related
Edit:
Actually, now that I think about it. The sound he makes would actually be a pretty good drowning sound! :)
06/19/2012 (8:32 pm)
If pressed for time just watch from 4:12. This topic just reminded me of this scene:www.youtube.com/watch?v=D0RkDhM9aQE&feature=related
Edit:
Actually, now that I think about it. The sound he makes would actually be a pretty good drowning sound! :)
#14
08/27/2012 (9:19 am)
very nice work .. is exact what i search!
#15
12/06/2013 (7:20 pm)
heres your fix works on all water entitys, most polygon normals face upfunction checkUnderwater(%obj, %coverage)
{
if (%obj.getState() $= "Dead")
{
// If we get here and the player is dead we should hide the breath meter
// and make sure that the next underWater check is cancelled.
commandToClient(%obj.client, 'ShowBreathMeter', false);
cancel(%obj.drowning);
}
else
{
// We'll use a "drowning" damageType so the game will know to distinguish
// between a drowning and other types of death.
%damageType = "Drowning";
%start = %obj.getEyePoint();
%end = VectorAdd(%start, "0 0 -100"); // fire the trace down
%searchMasks = $TypeMasks::WaterObjectType;
if (ContainerRayCast(%start, %end, %searchMasks))
{
// The player appears to have surfaced and is breathing normally.
// Reset the holdingbreath counter and hide the breathmeter.
%obj.holdingbreath = 0;
commandToClient(%obj.client, 'ShowBreathMeter', false);
}
else
{
// If the player is underwater increment a "holding breath" counter.
%obj.holdingbreath++;
// A GuiProgressCtrl expects values 0-1. We're just calculating a
// percentage in order to generate the appropriate range of values
// that will be used to adjust the lenght of the bar shown. The
// bar shrinks over time until we're out of air.
%remainingTime = ($Drowning::HoldBreathTicks - %obj.holdingBreath) / $Drowning::HoldBreathTicks;
if(%remainingTime < 0)
%remainingTime = 0;
commandToClient(%obj.client, 'ShowBreathMeter', true, %remainingTime);
// If the holdingbreath counter is greater than $Drowning::HoldBreathTicks
// then damage the player - he's choking on water!
if (%obj.holdingbreath > $Drowning::HoldBreathTicks)
%obj.damage(0, %obj.getPosition(), $Drowning::DamagePerTick, %damageType);
}
// We're still in the water and not dead yet, so do it again.
%obj.drowning = schedule($Drowning::TickTime, 0, "checkUnderwater", %obj);
}
} 
Torque Owner Daniel Buckmaster
T3D Steering Committee