TGE/Player Physics
From TDN
Contents |
Introduction
This document is intended to be a blow-by-blow breakdown of the player physics found in stock TGE. It should give the reader a good idea as to how to make modifications to fine tune their player to their own liking, and not necessarily the remains of a Tribes 2 player.
Overview
Two methods called in player's ProcessTick are the meat of the player's physical behavior. These methods are updateMove and updatePos. The updateMove method focuses more on updating the player's velocity based on the Move structure passed in (which is filled in based on player input). The updatePos method focuses more on updating player's position based on collisions and the current velocity of the player as calculated from updateMove. To be perfectly clear, updateMove is called before updatePos. Generally, player velocities are calculated first (updateMove), then collisions/position are adjusted based on calculated velocities (updatePos).
updateMove
The beginning of the function deals with the current orientation of the player so this section will be ignored. The real code starts at line 1499:
Calculating moveVec
The following chunks of code are responsible for calculating a vector called moveVec which is requested unit vector to move in the xy plane.
MatrixF zRot; zRot.set(EulerF(0, 0, mRot.z));
This sets up a transform matrix based on the rotation of the player along it's z-axis. It's only used directly in the following if statement (and probably should be, but isn't, with it).
// Desired move direction & speed
VectorF moveVec;
F32 moveSpeed;
if (mState == MoveState && mDamageState == Enabled)
{
zRot.getColumn(0,&moveVec);
moveVec *= move->x;
VectorF tv;
zRot.getColumn(1,&tv);
moveVec += tv * move->y;
The moveVec is used throughout updateMove and is calculated to contain the unit direction of the requested movement from the Move object passed in. To understand this calculation move->x and move->y are either 0.0f, 1.0f, -1.0f depending on the direction of movement. Column 0 of the zRot matrix is the resulted transform of the object's x-axis based on the z Rotation, Likewise Column 1 of the zRot matrix is the resulted transform of the object's y-axis based on the z Rotation. The moveVec is calculated as the sum of the contributions of the requested local x / y movement according to the move object. The moveVec itself now contains a unit vector in world coordinates of the direction of movement.
The MoveState is usually only MoveState or RecoverState (which occurs when recovering from a large fall). mDamageState basically checks to see whether or not the player is alive (Enabled).
Calculating moveSpeed
The following chunks of code are responsible for calculating a scalar called moveSpeed which is the movement speed of player.
// Clamp water movement
if (move->y > 0)
{
if( mWaterCoverage >= 0.9 )
moveSpeed = getMax(mDataBlock->maxUnderwaterForwardSpeed * move->y,
mDataBlock->maxUnderwaterSideSpeed * mFabs(move->x));
else
moveSpeed = getMax(mDataBlock->maxForwardSpeed * move->y,
mDataBlock->maxSideSpeed * mFabs(move->x));
}
else
{
if( mWaterCoverage >= 0.9 )
moveSpeed = getMax(mDataBlock->maxUnderwaterBackwardSpeed * mFabs(move->y),
mDataBlock->maxUnderwaterSideSpeed * mFabs(move->x));
else
moveSpeed = getMax(mDataBlock->maxBackwardSpeed * mFabs(move->y),
mDataBlock->maxSideSpeed * mFabs(move->x));
}
// Cancel any script driven animations if we are going to move.
if (moveVec.x + moveVec.y + moveVec.z != 0 &&
(mActionAnimation.action >= PlayerData::NumTableActionAnims
|| mActionAnimation.action == PlayerData::LandAnim))
mActionAnimation.action = PlayerData::NullAnimation;
}
To understand the above code you must realize an object's local y-axis is its forward vector. This code basically checks to see if the player is moving either forward (move->y > 0) or backwards (move->y < 0) and then calculating moveSpeed based on whether or not the player is underwater ( mWaterCoverage >= 0.9 ) or not.
else
{
moveVec.set(0,0,0);
moveSpeed = 0;
}
This is the end of the first if (mState == MoveState && mDamageState == Enabled). So if the player is dead, or recovering, the moveSpeed and the moveVec is zeroed out for calculations further on.
// Acceleration due to gravity
VectorF acc(0,0,mGravity * mGravityMod * TickSec);
This is another vector that is used throughout updateMove. This particular vector is eventually added to the player's velocity near the end of of the method. The vector is initialized to amount of velocity to be added based on standard gravity, and gravity modifiers (mGravityMod) from physical zones.
// Determine ground contact normal. Only look for contacts if
// we can move.
VectorF contactNormal;
bool jumpSurface = false, runSurface = false;
if (!isMounted())
findContact(&runSurface,&jumpSurface,&contactNormal);
if (jumpSurface)
mJumpSurfaceNormal = contactNormal;
To understand the above code, findContact() finds a contact with the ground (if any) and calculates whether or not you can run or jump on the surface based on the max surface/jump angles set in the player's datablock. It also stores the resulting normal to the contact in contactNormal.
The following code deals with physics while moving along a "runnable" surface as checked by findContact().
// 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) {
VectorF dv = contactNormal * (vd + 0.002);
acc += dv;
if (acc.len() < 0.0001)
acc.set(0,0,0);
}
The mContactTimer is used for picking action animations and if mContactTimer is greater than the static constant sContactTickTime, it sets to the root anim. As long as we've contacted with a run surface the mContactTimer stays at 0.
After setting mContactTimer to 0, the following of code dealswith subtracting out the added velocity due to gravity on the current tick based on the normal the player has contacted with. Simply put, if the surface is perpendicular to the direction of our fall - it should stop the player completely, otherwise, the player should slide off the surface it has made contact with, but at a rate slower than falling down directly.
The contactNormal is of unit length, and therefore vd will be a scalar of length <= acc since the dot product is equal to the product of the magnitudes of the vectors and the cosine of the angle between them. Between the angles of 90 and 270 (the angles we're interested in), the dot product will result in a negative number - hence the negative sign on the mDot. Recall, that the acc and the contactNormal vectors are in the opposite directions, the code following the dot product calculates a vector multplied by a the scalar based on the dot product, and is added to diminish effects of gravity (and if close enough to zero - it's just simply set to 0).
// 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);
In this piece of code, vector pv is set to moveVec only if the player's current energy is greater than the minRunEnergy as defined by the datablock. If the player is actually moving, the runEnergyDrain from the datablock is subtracted from the energy. If there is not enough energy, the pv vector is zeroed out. The pv vector is used in the following calculations, so it's important to remember it's equal to the moveVec vector if there's enough energy, zero otherwise.
// 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,0,1),&nn);
nn *= 1 / pvl;
VectorF cv = contactNormal;
cv -= nn * mDot(nn,cv);
pv -= cv * mDot(pv,cv);
pvl = pv.len();
}
If you recall vector pv is a copy of moveVec which is (AFAIK) a unit vector. The length of pv (1.0 or 0.0) is stored in pvl is used in later checks as well.
If pv wasn't set zeroed by the previous if statement, then pv is adjusted to be aligned along the surface of the contacted surface. The cross product produces another vector called nn which is (again AFIAK) a unit vector. The nn vector lies in the xy plane along with pv. The next statement is AFAIK redundant since as far as I can tell moveVec/pv are both unit vectors - hence it would be a multiplication of 1/1 or 1 which are both equally redundant. The next few statements with the two dot products basically do the adjusting based on the angles between the the contact normal and the two perpendicular vectors in xy plane. The two statements are additive with the end result being stored in pv. Now pv has been adjusted to be aligned along the surface of the contacted surface.
// Convert to acceleration
if (pvl)
pv *= moveSpeed / pvl;
VectorF runAcc = pv - (mVelocity + acc);
F32 runSpeed = runAcc.len();
The first if statement checks to see if the the length of pv is not zero. If not, it is multplied by moveSpeed (divided by pvl, which would be 1.0, which is again redundant AFAIK). The next few lines calculates runAcc, or running acceleration. This value is the amount of velocity to be added based on the requested run direction. If you recall, pv, at this point is a vector pointing in the direction of the requested movement parallel to the surface contacted with, with the magnitude of moveSpeed. The amount of velocity to add based solely on the requested movement of the player is this requested movement with the previous velocity and the acceleration due to gravity (currently stored in acc) subtracted out. Subtracting out the previous velocity gives the effect where if you take too sharp a turn (say 180 degrees), you'll start to slow down and accelerate in the direction you've started to move in. Subtracting out acc (which currently holds the speed to be added based on gravity) ensures that running up (or down) hills affects the acceleration due to running as well.
runSpeed is stored for later checks.
// 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;
In this segment of code runAcc is checked against the maximum possible acceleration as defined by runForce from the player datablock. If it's greated than largest allowed running acceleration, the acceleration is capped. Afterwards the running acceleration is added to acc. Now acc contains acceleration due to gravity and acceleration from running.
}
else
mContactTimer++;
This part concludes the if statement if(runSurface). Therefore if we're not on a runnable surface, the mContactTimer is incremented which (as mentioned earlier) is used solely for animation purposes.
Acceleration from Jumping
// Acceleration from Jumping
if (move->trigger[2] && !isMounted() && canJump())
{
The move->trigger[2] is the button used for jumping. The function canJump() is basically a list of checks to see if the player can jump, and !isMounted() is included in that list of checks which makes the call inside the if statement redundant.
// Scale the jump impulse base on maxJumpSpeed
F32 zSpeedScale = mVelocity.z;
if (zSpeedScale <= mDataBlock->maxJumpSpeed)
{
zSpeedScale = (zSpeedScale <= mDataBlock->minJumpSpeed)? 1:
1 - (zSpeedScale - mDataBlock->minJumpSpeed) /
(mDataBlock->maxJumpSpeed - mDataBlock->minJumpSpeed);
The zSpeedScale is a scalar based on the previous z-velocity (say from climbing a slope) and the max and min jump speeds defined by the player datablock. The jump will only occur if the player isn't already moving upwards faster than the maximum allowed jump speed. The following line code basically determines the zSpeedScale and is checked against the previous z velocity.
// Desired jump direction
VectorF pv = moveVec;
F32 len = pv.len();
if (len > 0)
pv *= 1 / len;
This chunk of code copies moveVec (discussed above) into a vector called pv. The next 3 lines is AFAIK redundant since moveVec is already a unit vector, even though the code explicity sets it to one.
// We want to scale the jump size by the player size, somewhat
// in reduced ratio so a smaller player can jump higher in
// proportion to his size, than a larger player.
F32 scaleZ = (getScale().z * 0.25) + 0.75;
// If we are facing into the surface jump up, otherwise
// jump away from surface.
F32 dot = mDot(pv,mJumpSurfaceNormal);
F32 impulse = mDataBlock->jumpForce / mMass;
if (dot <= 0)
acc.z += mJumpSurfaceNormal.z * scaleZ * impulse * zSpeedScale;
else
{
acc.x += pv.x * impulse * dot;
acc.y += pv.y * impulse * dot;
acc.z += mJumpSurfaceNormal.z * scaleZ * impulse * zSpeedScale;
}
The player's jump force/amount is scaled by the object scaling factor applied to the player object. This amount of scaling is calculated by the variable scaleZ.
The next chunk of code deals with how the player will jump based on positioning and moving along a slope. As the comments correctly suggest, if the player is moving into an upwards slope, a jump will result in a straight upwards jump (if (dot <= 0)), otherwise the player jumps in the requested direction (the else statement). Recall that mJumpSurfaceNormal is a copy of contactNormal. The z component of the unit vector of this normal is used in the calculations on how high the player can jump. The flatter the surface (larger z component), the higher the jump.
All of these calculations are summed into the vector acc (used throughout) which is the running sum of all components of velocity to be added on this current tick.
mJumpDelay = mDataBlock->jumpDelay;
mEnergy -= mDataBlock->jumpEnergyDrain;
setActionThread((mVelocity.len() < 0.5)?
PlayerData::StandJumpAnim: PlayerData::JumpAnim, true, false, true);
mJumpSurfaceLastContact = JumpSkipContactsMax;
}
}
Not much in terms of physics here. There's an update to the internal variable of the player's jump delay (the forced delay in ticks between sucessive jumps), and the removal of energy based on the jumpEnergyDrain paramter in the datablock. The rest deals with animation.
else
if (jumpSurface) {
if (mJumpDelay > 0)
mJumpDelay--;
mJumpSurfaceLastContact = 0;
}
else
mJumpSurfaceLastContact++;
At this point we're at the other end of the if clause checking if the player had decided to jump. On the ocassion that the player decided not to jump, updates need to be made based on whether or not the player in on a "jumpable" surface. If the player is, the delay is decremented. I'm not certain what the purpose of mJumpSurfaceLastContact is, but it does count the number of ticks the player has not been in contact with a "jumpable" surface.
Putting it All Together
In this last section of code, the acc vector is finally added to the velocity, and our velocity is capped by datablock settings such as drag, and maximum speed.
// Add in force from physical zones... acc += (mAppliedForce / mMass) * TickSec;
Right before the addition of the acc we add in any applied forces from physical zones. The mAppliedForce is updated with a call to updateContainer() defined in shapeBase.cc. It's called in updatePos which is covered in the next section.
// Adjust velocity with all the move & gravity acceleration // TG: I forgot why doesn't the TickSec multiply happen here... mVelocity += acc;
Finally the acc vector is added to our current velocity. Don't let the comment above confuse you. The acc vector contains the amount of velocity to be added to our mVelocity paramater on this current tick, that's the reason why the TickSec multiply doesn't happen - the acc is actually a vector that already contains a velocity, not an acceleration, to be added on the current tick.
// apply horizontal air resistance
F32 hvel = mSqrt(mVelocity.x * mVelocity.x + mVelocity.y * mVelocity.y);
if(hvel > mDataBlock->horizResistSpeed)
{
F32 speedCap = hvel;
if(speedCap > mDataBlock->horizMaxSpeed)
speedCap = mDataBlock->horizMaxSpeed;
speedCap -= mDataBlock->horizResistFactor * TickSec * (speedCap - mDataBlock->horizResistSpeed);
F32 scale = speedCap / hvel;
mVelocity.x *= scale;
mVelocity.y *= scale;
}
if(mVelocity.z > mDataBlock->upResistSpeed)
{
if(mVelocity.z > mDataBlock->upMaxSpeed)
mVelocity.z = mDataBlock->upMaxSpeed;
mVelocity.z -= mDataBlock->upResistFactor * TickSec * (mVelocity.z - mDataBlock->upResistSpeed);
}
In this section of code the horizontal (x/y plane) velocity and the z velocity are capped with the datablock parameters of horizResistSpeed and upResistSpeed.
// Container buoyancy & drag
if (mBuoyancy != 0)
{ // Applying buoyancy when standing still causing some jitters-
if (mBuoyancy > 1.0 || !mVelocity.isZero() || !runSurface)
mVelocity.z -= mBuoyancy * mGravity * mGravityMod * TickSec;
}
Underwater, mBuoyancy is not zero. It's updated with every call to updateContainer() which is called in updatePos, covered in the next section.
mVelocity -= mVelocity * mDrag * TickSec;
As the last modification to our velocity (mVelocity), drag is subtracted out. What's interesting to note from this line is that drag is calculated both underwater and above water.
// If we are not touching anything and have sufficient -z vel,
// we are falling.
if (runSurface)
mFalling = false;
else {
VectorF vel;
mWorldToObj.mulV(mVelocity,&vel);
mFalling = vel.z < sFallingThreshold;
}
The past section of code, although nothing to do with physics, I'm mentioning since it might be helpful for other physics. Currently I can find no use of the mFalling parameter. But it does track whether or not the player is currently falling. Any physics/animation dealing strictly with falling might find this parameter useful.
updatePos
Less physics calculations happen in this section of code, but since collisions and some physical calculations take place here, it's included in the article. Beware of the delta structure used throughout the code, this structure is used for interpolation on the client, so this part of the code will not be explained. Also the majority of the code deals with collision, which will not be covered either.
We'll start a little farther down in the method at line 2536.
// Take into account any physical zones...
for (U32 j = 0; j < physZoneCollisionList.count; j++) {
AssertFatal(dynamic_cast<PhysicalZone*>(physZoneCollisionList.collision[j].object), "Bad phys zone!");
PhysicalZone* pZone = (PhysicalZone*)physZoneCollisionList.collision[j].object;
if (pZone->isActive())
mVelocity *= pZone->getVelocityMod();
}
In this chunk of code any velocity modifications are made to the player if the player has collided with a physical zone. The physZoneCollisionList at this point contains a list of physical zone objects the player has collided with.
if (collisionList.count != 0 && collisionList.t < 1.0) {
In this if statement comes all the physics dealing with colliding with objects other than physical zones. The collisionList at this point contains a list of objects that the player has collided with that aren't physical zone objects, and are included in the list of flags defined in sCollisionMoveMask.
Skipping the next little chunk of code, we arrive at player stepping at line 2562.
// Try stepping if there is a vertical surface
if (collisionList.maxHeight < start.z + mDataBlock->maxStepHeight * scale.z) {
bool stepped = false;
for (U32 c = 0; c < collisionList.count; c++) {
Collision& cp = collisionList.collision[c];
// if (mFabs(mDot(cp.normal,VectorF(0,0,1))) < sVerticalStepDot)
// Dot with (0,0,1) just extracts Z component [lh]-
if (mFabs(cp.normal.z) < sVerticalStepDot)
{
stepped = step(&start,&maxStep,time);
break;
}
}
if (stepped)
{
continue;
}
}
This chunk of code checks or whether or not the player should step. The first if statements checks whether or not the player should take a step up on what the player is colliding with, which is also based on the scale of player. It then walks through the list of objects collided with on that object and then performs a step up with the player if the the surface is vertical enough based on sVerticalStepDot (which by default is based on 80 degrees from the xy plane). If the player sucessfully steps up, the loop breaks and the step is performed.
The next if statement (if (stepped)) forces the loop over that checks collisions after a step since we're potentially moving while taking a step, we need to refresh our checking after a step is performed.
// Pick the surface most parallel to the face that was hit.
Collision* collision = &collisionList.collision[0];
Collision* cp = collision + 1;
Collision *ep = collision + collisionList.count;
for (; cp != ep; cp++)
{
if (cp->faceDot > collision->faceDot)
collision = cp;
}
F32 bd = -mDot(mVelocity,collision->normal);
In this code chunk, collision is first calculated to contain the surface who's normal is closest aligned with the velocity vector the player has hitting the surface. After the loop, bd contains a scalar that's in between 0 and the length of mVelocity. This value is used to calculate how hard the player has hit a surface.
// shake camera on ground impact
if( bd > mDataBlock->groundImpactMinSpeed && isControlObject() )
{
F32 ampScale = (bd - mDataBlock->groundImpactMinSpeed) / mDataBlock->minImpactSpeed;
CameraShake *groundImpactShake = new CameraShake;
groundImpactShake->setDuration( mDataBlock->groundImpactShakeDuration );
groundImpactShake->setFrequency( mDataBlock->groundImpactShakeFreq );
VectorF shakeAmp = mDataBlock->groundImpactShakeAmp * ampScale;
groundImpactShake->setAmplitude( shakeAmp );
groundImpactShake->setFalloff( mDataBlock->groundImpactShakeFalloff );
groundImpactShake->init();
gCamFXMgr.addFX( groundImpactShake );
}
In this if statement, the camera shakes if the player has hit the surface hard enough using the bd scalar calculated earlier.
if (bd > mDataBlock->minImpactSpeed && !mMountPending) {
if (!isGhost())
onImpact(collision->object, collision->normal*bd);
if (mDamageState == Enabled && mState != RecoverState) {
// Scale how long we're down for
F32 value = (bd - mDataBlock->minImpactSpeed);
F32 range = (mDataBlock->minImpactSpeed * 0.9);
U32 recover = mDataBlock->recoverDelay;
if (value < range)
recover = 1 + S32(mFloor( F32(recover) * value / range) );
// Con::printf("Used %d recover ticks", recover);
// Con::printf(" minImpact = %g, this one = %g", mDataBlock->minImpactSpeed, bd);
setState(RecoverState, recover);
}
}
If the player hit the surface hard enough, the onImpact is called into script on the server, and if the player isn't already recovering, is set to start recovering.
if (isServerObject() && bd > (mDataBlock->minImpactSpeed / 3.0f)) {
mImpactSound = PlayerData::ImpactNormal;
setMaskBits(ImpactMask);
}
Although not having to do with player physics, this portion can be a little confusing since this past piece of code AFAIK is completely redundant. As far as I can tell mImpactSound is not used for anything.
// Subtract out velocity
VectorF dv = collision->normal * (bd + sNormalElasticity);
mVelocity += dv;
This is the last significant piece of code dealing with the player physics. After the new velocity was calculated in updateMove, and all the collisions have been processed based on that new velocity, the velocity is again subtracted out based on how well the player collided with the surface in a very similar method to one seen in updatePos.
Categories: TGE | Code



