Game Development Community

dev|Pro Game Development Curriculum

Swimming in one step -- standard FPS swimming physics

by Henry Todd · 11/13/2006 (4:38 pm) · 30 comments

*Updated 5/22/09: This code is already included and improved upon in stock T3D Beta1+

*Updated 1/26/07 to fix typo in code: missing ')'*
*Updated 11/6/07 to clarify a confusing element and add explaination.*
*Updated 11/12/07 to upgrade vector calculations, add moveUp/moveDown support.*

While there is one swimming resource available (Swim/Crawl/Crouch resource, it includes extra code for move states (crouch, etc.) and uses a slightly awkward (as admitted by the author) swim vector calculation routine.

This resource is intended to provide FPS-style swimming physics in a simple patch which can be done in about 30 seconds. If you also want crouching and crawling, check out the link above.

What it does: Swim in any direction you can look, using existing runForce calculations. One file change.

What it does not: Animations not included; there are many existing tutorials on adding new animations to your player object.

With that said, open player.cpp (or .cc for older versions) and go to the function:

void Player::updateMove(const Move* move)

Find the following if-else in updateMove:

if (runSurface) {
...<All the code for running (Don't delete this stuff!)>...
}
else
   mContactTimer++;

..and make it look like this (new code in bold):

if (runSurface) {
...<All the code for running (Don't delete this stuff!)>...
}
[b]
else if (inLiquid)
{
   // get the head pitch and add it to the moveVec
   // This more accurate swim vector calc comes from Matt Fairfax
   MatrixF xRot, zRot;
   xRot.set(EulerF(mHead.x, 0, 0));
   zRot.set(EulerF(0, 0, mRot.z));
   MatrixF rot;
   rot.mul(zRot, xRot);
   rot.getColumn(0,&moveVec);

   moveVec *= move->x;
   VectorF tv;
   rot.getColumn(1,&tv);
   moveVec += tv * move->y;
   rot.getColumn(2,&tv);
   moveVec += tv * move->z;

   // Force a 0 move if there is no energy, and only drain
   // move energy if we're moving.
   VectorF pv;
   if (mEnergy >= mDataBlock->minRunEnergy) {
      if (moveSpeed)
         mEnergy -= mDataBlock->runEnergyDrain;
      pv = moveVec;
   }
   else
      pv.set(0,0,0);

  F32 pvl = pv.len();

   // Convert to acceleration
   if (pvl)
      pv *= moveSpeed / pvl;
   VectorF runAcc = pv - mVelocity;
   F32 runSpeed = runAcc.len();

   // Clamp acceleration, player also accelerates faster when
   // in his hard landing recover state.
   F32 maxAcc = (mDataBlock->runForce / mMass) * TickSec;
   if (mState == RecoverState)
      maxAcc *= mDataBlock->recoverRunForceScale;
   if (runSpeed > maxAcc)
      runAcc *= maxAcc / runSpeed;
   acc += runAcc;

   mContactTimer++;
}
[/b]
else
   mContactTimer++;

Compile and jump in some water. You should be able to swim, assuming your player is not too heavy or dense. You may notice that you can still walk on the bottom of the ocean if you make contact with it.

Density: If your player has a higher density than the water, he will sink and swim very slowly. A lower density will cause the player to rise and accelerate more quickly. Equal density will make swimming feel like flying. I recommend the player's density be just slightly higher than the water's, for the sake of realism.

Waves: The waves in TGE's waterblock don't exist on the server, so they won't be part of the swimming physics.

For those interested in what this actually does, you're changing the if-else chain that usually does:
"If we're on the ground, use the running code. Else, don't do anything except count, because we're in the air."
Into:
"If we're on the ground, use the running code. Else, if we're underwater, use the swimming code. Else, we're in the air, just count."

Upgraded on 11/12/07 to use a more accurate, slightly more math-intensive swim vector. The old method worked fine, but this version is "correct" in comparison to other FPS swimming models. It also now supports move up and move down (move->z).
Page «Previous 1 2
#1
12/15/2006 (6:13 am)
Works great! (TGE 1.5)
#2
12/17/2006 (1:43 pm)
I'm not sure if I have the code in the correct spot because I haven't got it to work

// Remove acc into contact surface (should only be gravity)
      // Clear out floating point acc errors, this will allow
      // the player to "rest" on the ground.
      F32 vd = -mDot(acc,contactNormal);
      if (vd > 0) {
         VectorF dv = contactNormal * (vd + 0.002);
         acc += dv;
         if (acc.len() < 0.0001)
            acc.set(0,0,0);
      }

//
if (runSurface) {
}

else if (inLiquid)
{
   // get the head pitch and add it to the moveVec
   VectorF swimVec(0,0,-mHead.x);
   moveVec += swimVec * move->y;

   // Force a 0 move if there is no energy, and only drain
   // move energy if we're moving.
   VectorF pv;
   if (mEnergy >= mDataBlock->minRunEnergy {
      if (moveSpeed)
         mEnergy -= mDataBlock->runEnergyDrain;
      pv = moveVec;
   }
   else
      pv.set(0,0,0);

  F32 pvl = pv.len();

   // Convert to acceleration
   if (pvl)
      pv *= moveSpeed / pvl;
   VectorF runAcc = pv - mVelocity;
   F32 runSpeed = runAcc.len();

   // Clamp acceleration, player also accelerates faster when
   // in his hard landing recover state.
   F32 maxAcc = (mDataBlock->runForce / mMass) * TickSec;
   if (mState == RecoverState)
      maxAcc *= mDataBlock->recoverRunForceScale;
   if (runSpeed > maxAcc)
      runAcc *= maxAcc / runSpeed;
   acc += runAcc;

   mContactTimer++;
}

else
   mContactTimer++;




//

   // Acceleration from Jumping
   if (move->trigger[2] && !isMounted() && canJump())
   {
      // Scale the jump impulse base on maxJumpSpeed
      F32 zSpeedScale = mVelocity.z;
      if (zSpeedScale <= mDataBlock->maxJumpSpeed)
#3
12/31/2006 (7:15 pm)
@Christian
look for this line
if (mEnergy >= mDataBlock->minRunEnergy {
and put a ")" after minRunEnergy, before the "{"
that should have returned an error and told you what was wrong.

[edit]
just copy-pasted, changed that one line and presto compiles and runs perfectly :)
thanks for the great resource!
#4
01/25/2007 (2:56 pm)
Hmmm...... If I'm on the surface, it works, but if I go more then a few feet below the surface of the water, I fall and can't swim.

Any Ideas as to what I did wrong?
#5
01/25/2007 (3:20 pm)
oops :) Z scale on my water was set to 1.

Amazing resource, A must for swimming.
THANKS!!!!!
#6
01/26/2007 (6:46 am)
Sorry about the typo there. I cut this code from my heavily-modified copy of TGE, and while I did revert the code to "stock" aside from the swimming changes, I guess I clipped that parenthesis in the process and never tried to build again.
#7
02/23/2007 (2:16 pm)
Great resource! Works flawlessly!
#8
02/28/2007 (8:06 am)
This resource works, the first time. If i exit the game, restart, i cant swim again. If i delete my prefs, still cant swim, if i delete my DSO's, still cant swim. if i delete my prefs AND dso's at the same time, i get to swim again, until i shut it down. next time i start, if i havn't deleted both my prefs and DSO's, i dont swim.

I really dont get this.... it makes no sense. this problem cropped up on a build of 1.4
#9
03/28/2007 (12:14 pm)
Nice.

I got it working in TGE 1.5 and with a little modification (to the tgea engine) I was able to get it working in TGEA 1.0.

It works really well.
#10
04/11/2007 (12:37 am)
Sean: First, sorry that it took me so long to post a reply. Second, I'm afraid I can't think of any reason for this to happen; I've tested this on several different versions of TGE (though I might note that 1.4 is the only version I've never used; I went straight from 1.3.5 to 1.5).

Have you made any significant modifications to the scripts which handle basic movement on the client-side, or to the process of inputs in the code? Specifically I'm thinking of default.bind.cs, which contains the basic functions for taking movement input.

This should be pretty much error-proof; all it does is read mHead.x, the angle the player is looking up/down, and add this value to your move vector. It then carries out the standard proceedures for moving the player, minus the code which usually causes you to move parrallel to the contact surface. I would suggest you add a debug output right after "else if (inLiquid)" to output the value of mHead.x; it will likely flood your console window, but you'll be able to tell if the value of this look coordinate is somehow being set to 0, which would result in a complete failure to swim. If on the other hand you don't get any output, you'll know that your player is somehow not being detected as "inLiquid."
#11
06/14/2007 (5:38 am)
Works like a charm. Simple, fast to implement and usefull.
#12
07/17/2007 (4:25 pm)
I'm still not getting it to work. (TGE 1.5)
Do I need to delete everything; in the "lots of lines" like in Christian's example? or keep them and add them to the bottom like this: (included all the code from "if (runSurface) {" to "//Acceleration for jumping") to make sure I have it correct.

My waterblock density is set to 1, tried 0 to 10; velocity is set to 15, tried 0 to 50; Z Scale is at 50


// Acceleration on run surface


if (runSurface) {
mContactTimer = 0;

Remove acc into contact surface (should only be gravity)
Clear out floating point acc errors, this will allow
the player to "rest" on the ground.
F32 vd = -mDot(acc,contactNormal);
if (vd > 0.0f) {
VectorF dv = contactNormal * (vd + 0.002f);
acc += dv;
if (acc.len() < 0.0001f)
acc.set(0.0f, 0.0f, 0.0f);
}

// Force a 0 move if there is no energy, and only drain
// move energy if we're moving.
VectorF pv;
if (mEnergy >= mDataBlock->minRunEnergy) {
if (moveSpeed)
mEnergy -= mDataBlock->runEnergyDrain;
pv = moveVec;
}
else
pv.set(0.0f, 0.0f, 0.0f);

// Adjust the players's requested dir. to be parallel
// to the contact surface.
F32 pvl = pv.len();
if (pvl) {
VectorF nn;
mCross(pv,VectorF(0.0f, 0.0f, 1.0f),&nn);
nn *= 1.0f / pvl;
VectorF cv = contactNormal;
cv -= nn * mDot(nn,cv);
pv -= cv * mDot(pv,cv);
pvl = pv.len();
}

// Convert to acceleration
if (pvl)
pv *= moveSpeed / pvl;
VectorF runAcc = pv - (mVelocity + acc);
F32 runSpeed = runAcc.len();

// Clamp acceleratin, player also accelerates faster when
// in his hard landing recover state.
F32 maxAcc = (mDataBlock->runForce / mMass) * TickSec;
if (mState == RecoverState)
maxAcc *= mDataBlock->recoverRunForceScale;
if (runSpeed > maxAcc)
runAcc *= maxAcc / runSpeed;
acc += runAcc;

// If we are running on the ground, then we're not jumping
if (mDataBlock->isJumpAction(mActionAnimation.action))
mActionAnimation.action = PlayerData::NullAnimation;
}

//---------------------------------------------------------------------------------
//----------------------------ADDED FOR SWIMMING------------------jeremy1----------
//---------------------------------------------------------------------------------


else if (inLiquid)
{
// get the head pitch and add it to the moveVec
VectorF swimVec(0,0,-mHead.x);
moveVec += swimVec * move->y;
// Force a 0 move if there is no energy, and only drain
// move energy if we're moving.
VectorF pv;
if (mEnergy >= mDataBlock->minRunEnergy) {
if (moveSpeed)
mEnergy -= mDataBlock->runEnergyDrain;
pv = moveVec;
}
else
pv.set(0,0,0);
F32 pvl = pv.len();
// Convert to acceleration
if (pvl)
pv *= moveSpeed / pvl;
VectorF runAcc = pv - mVelocity;
F32 runSpeed = runAcc.len();
// Clamp acceleration, player also accelerates faster when
// in his hard landing recover state.
F32 maxAcc = (mDataBlock->runForce / mMass) * TickSec;
if (mState == RecoverState)
maxAcc *= mDataBlock->recoverRunForceScale;
if (runSpeed > maxAcc)
runAcc *= maxAcc / runSpeed;
acc += runAcc;
mContactTimer++;
}
else
mContactTimer++;

//------------------------------------------------------------------------------
//-------------DONE WITH SWIMMING ADDITION-----------------------
//-------------------------------------------------------------------------------

// Acceleration from Jumping
if (move->trigger[2] && !isMounted() && canJump())
#13
08/18/2007 (9:08 pm)
Hey, the "lots of lines" part was meant to say "this represents all the original code that I'm not going to type here." Probably not the clearest way to explain it, but you don't actually delete that stuff.

Basically, you should have if (runSurface) { //** All the running code **// } else if (inLiquid) { // ** All the swimming code **// } else { mContactTimer++; }

It actually looks like what you've done is correct. Since I have this working with every version of 1.5.x, I'm thinking there might be a secondary problem here. I would suggest putting a line of debug output (look for con::printf in the code if you don't know how to print to console from code) right after the "if (inLiquid)" check, to make sure that it's correctly IDing your Player as underwater.

My theories at the moment are it's either not aware that you're in liquid, thinks you're standing on the ground (note that you can't be standing on the ground underwater for this to work -- you may have to jump if you are stuck on it to start swimming), for some reason mHead.x is 0, or your gravity is too high and it's pushing you down in the water.

I'm on my way out the door right now, but when I get back I'll look at this a little closer, there might be an issue with the code you posted I didn't notice b/c of the tab-less formatting.
#14
08/27/2007 (11:18 am)
I figured I had it in correctly, but wasn't certain. I'll try it on a clean copy of the SDK and see if I get anything different. All the gravity seems to be alright. I'll mess around with it some more and see if I get anything different, and look into using the con::printf like you said. I'm a newbie and I'm not framiliar with it.

Thanks, J.
#15
09/12/2007 (9:44 am)
Just wanted to say thanks! Spent ages trying other solutions, this one took 30 seconds and worked first time - brilliant!! :) Version 1.5 in case anyone wondering.

Simon
#16
11/06/2007 (11:45 am)
Has anyone verified this working with the newer builds of TGEA? I heard they removed some code that detects when the player is in a water block. Thanks.

Mike Daly
#17
11/06/2007 (4:13 pm)
Quote:Has anyone verified this working with the newer builds of TGEA? I heard they removed some code that detects when the player is in a water block. Thanks.

Mike Daly

I know for a fact that this did not work with the old versions of TGEA, for the exact reason you stated. The new waterblock apparently resulted in them removing or breaking the "inLiquid" check, however I haven't tried it since, I believe, MS3. The fact that I never got any errors for calling inLiquid() suggests they just broke it, so it might be fixed by now.

I'll try a copy of the latest TGEA within the hour and let you know what happens. If it doesn't work, I'll post a fix; the waterblock is just a big bounding box in essence, and it's easy to check if you're inside it.
#18
11/12/2007 (4:55 pm)
Hahaha, this is a very fun resource. Thankyou. It worked perfectly the first try. I love being able to watch Kork ski on top of the water. :-D

Edit to add:

3dcentral.net/myPic/KorkSkis.jpg
#19
11/12/2007 (6:36 pm)
Doesn't appear to work with TGEA 1.0.3.
#20
12/10/2007 (5:33 am)
Mike: Just FYI, you can control how far out of the water the player can "surf" by replacing "if (inLiquid)" with "if (mWaterCoverage >= 0.5)" or whatever % you like.

Chris: I'll see if I can find the problem. I'm sure it's something missing or not linked correctly in the new waterblock. Until then, if you want to simply use a cheap hack, just replace the "if (inLiquid)" with "if (getTransform().z < (water surface height))" and use a constant water height from level to level (adjust the terrain up/down instead of the water level). This will work just as well, at least for single waterblock levels.
Page «Previous 1 2