This article covers the collision physics of Tribes 1, i.e. attempting to actually move the player and what to do when the player runs in to something. This is the most convoluted part of the physics and requires a lot of little touches to get right. Tribes movement and collision handling actually gets a little too low level, so I won’t be able to show exactly how it works (it gets down to dealing with the raw triangle lists which I don’t think all collision detection systems will let you get at), but it will be detailed enough so that there will be no major differences.
Warning!
Before I get in to the details, a little explanation is required. When Tribes attempts to move an object, it takes the maximum distance the object will cover in the remaining time (X), then divides the remaining time by ceil(X) and does ceil(X) collision detection loops. This is ostensibly to ensure that an object only moves 1m at a time, but for what reason, I don’t know. The engine is obviously capable of fairly arbitrary translations (or else Gambase::GetLosInfo would not work) so I can only imagine many short translations proved to be more efficient than a single large translation.
I am going to be using a single translation, and normally this would not make a difference, but Tribes does something a little weird on collisions which necessitates a rather odd fix-up when you are using a single translation versus slicing them up. Instead of adjusting the position based on velocity when there is no collision and adjusting the velocity & setting the position to the collision point on a collision, Tribes adjusts the position based on the velocity no matter what. This means Tribes will attempt to move the player, hit a surface, adjust the velocity based on the collision, and then move the player anyway from their original position based on the new velocity, usually resulting in the player bouncing ever so slightly away from the contacted surface. There is actually a noticeable difference between the correct way and Tribes way of handling collisions, as the correct way feels slightly velcroish while Tribes feels more fluid.
Things get more complicated when you move the player in a single translation instead of sliced up translations as the weird adjustment on collisions is only done for the last fraction of the translation and not the entire thing. I worked around this by calculating some values which let me figure out how many “non-collision” slices have occurred and only do the weird handling on the last slice.
Collision Code
Player.UpdatePosition( float tickLen ) {
float decayFriction = currentFriction * Physics.FRICTIONDECAY
float lastSurfDirection = lastJumpableNormal.Dot( Gravity.upNormal )
float timeLeft = tickLen
int maxBumps = 4, bumps
currentFriction = 0
collisionLastTick = false
for ( bumps = 0; ( bumps < maxBumps ) && ( timeLeft > 0 ); bumps++ ) {
// slice fixup values
Vector3 maxDistance = ( velocity * timeLeft )
int iterations = ceil( UnitsToMeters( maxDistance.Length ) )
float sliceTime = timeLeft / iterations
// attempt to move through the world
Vector3 originalPos = position, endPos = position + maxDistance
moveFraction, finalPos, contactNormal = Physics.Translate( originalPos, endPos )
// figure out how long we moved for and adjust the remaining time
float duration = timeLeft * moveFraction
timeLeft -= duration
position = finalPos
// did we move the entire distance safely?
if ( !timeLeft )
break
// collisionLastFrame gets set even if we step up and don't have an actual collision
collisionLastFrame = true
float surfDirection = contactNormal.Dot( Gravity.upNormal )
if ( surfDirection < armor.JUMPSURFACE_MINDOT ) {
// code to handle potentially stepping up sheer surfaces
if ( steppedUp )
continue
}
float impactDot = -velocity.Dot( contactNormal )
// take damage if needed
if ( UnitsToMeters( impactDot ) > armor.MINDAMAGESPEED )
OnDamage( ( UnitsToMeters( impactDot ) - armor.MINDAMAGESPEED ) * armor.DAMAGESCALE )
// if we hit a jumpable surface, update the jumpable normal and reset the timestamp
if ( surfDirection >= armor.JUMPSURFACE_MINDOT ) {
if ( ( lastJumpableNormalTimestamp > ( Physics.TICKBASE * 1000 ) ) ||
( surfDirection < lastSurfDirection ) ) {
lastSurfDirection = surfDirection
lastJumpableNormalTimestamp = 0
lastJumpableNormal = contactNormal
}
}
// do some voodoo for tribes collision adjustments and timeslices
int impactIterations = ceil( duration / sliceTime )
float fullMotionTime = sliceTime * ( impactIterations - 1 )
float fixupTime = duration - fullMotionTime
position = originalPosition + ( velocity * fullMotionTime )
// bounce
Vector3 bounce = ( contactNormal * ( impactDot + MetersToUnits( Physics.ELASTICITY ) ) )
velocity += bounce
// readjust position based on bounced velocity
position = Physics.Translate( position, position + ( velocity * fixupTime ) )
// only update friction on upward facing surfaces
if ( surfDirection > 0 ) {
currentFriction = surfDirection
if ( crawledToStop && ( velocity < MetersToUnits( Physics.MINSPEED ) ) ) {
velocity = Vector3( 0, 0, 0 )
position = originalPosition
break
}
}
}
if ( bumps >= maxBumps ) {
// Tribes sets the velocity to 0 here, this is where skibugs happen
}
if ( collisionLastFrame )
currentFriction = Min( Max( currentFriction, decayFriction ), 1 )
return ( collisionLastFrame )
}
Notes
Ski bugs are caused when the translation loop exceeds the maximum number of collisions. When this happens, Tribes zeros the player’s velocity as it is having trouble successfully moving. I think there is something in the velocity bounce that occasionally causes the translation loop to get stuck running in to the surface over and over with no change in velocity. When this happens, there is obviously no right answer as to what to do, but zeroing the velocity is fairly annoying answer. I’ve found that just ignoring the situation and hoping the next tick results in the player getting dislodged appears to work much better.
Also note that I keep forgetting to add constants (MINDAMAGESPEED and DAMAGESCALE in this post) and need to update the original post to include them. Since nobody is reading this and I am taking a while in getting it together, I do not think anyone will mind.
Tribes 1 Physics Series
- Part One: Overview
- Part Two: Movement
- Part Three: Collision
- Part Four: Explosions