You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
352 lines
8.7 KiB
352 lines
8.7 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
// |
|
//=============================================================================// |
|
// c_fish.cpp |
|
// Simple fish client-side logic |
|
// Author: Michael S. Booth, April 2005 |
|
|
|
#include "cbase.h" |
|
#include <bitbuf.h> |
|
#include "engine/ivdebugoverlay.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
extern float UTIL_WaterLevel( const Vector &position, float minz, float maxz ); |
|
|
|
|
|
ConVar FishDebug( "fish_debug", "0", FCVAR_CHEAT, "Show debug info for fish" ); |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
/** |
|
* Client-side fish entity |
|
*/ |
|
class C_Fish : public C_BaseAnimating |
|
{ |
|
public: |
|
DECLARE_CLASS( C_Fish, C_BaseAnimating ); |
|
DECLARE_CLIENTCLASS(); |
|
|
|
virtual void Spawn( void ); |
|
virtual void ClientThink(); |
|
|
|
virtual void OnDataChanged( DataUpdateType_t type ); |
|
|
|
private: |
|
friend void RecvProxy_FishOriginX( const CRecvProxyData *pData, void *pStruct, void *pOut ); |
|
friend void RecvProxy_FishOriginY( const CRecvProxyData *pData, void *pStruct, void *pOut ); |
|
|
|
Vector m_pos; ///< local position |
|
Vector m_vel; ///< local velocity |
|
QAngle m_angles; ///< local angles |
|
|
|
int m_localLifeState; ///< our version of m_lifeState |
|
|
|
float m_deathDepth; ///< water depth when we died |
|
float m_deathAngle; ///< angle to float at when dead |
|
float m_buoyancy; ///< so each fish floats at a different rate when dead |
|
|
|
CountdownTimer m_wiggleTimer; ///< for simulating swimming motions |
|
float m_wigglePhase; ///< where in the wiggle sinusoid we are |
|
float m_wiggleRate; ///< the speed of our wiggling |
|
|
|
Vector m_actualPos; ///< position from server |
|
QAngle m_actualAngles; ///< angles from server |
|
|
|
Vector m_poolOrigin; |
|
float m_waterLevel; ///< Z coordinate of water surface |
|
|
|
bool m_gotUpdate; ///< true after we have received a network update |
|
|
|
enum { MAX_ERROR_HISTORY = 20 }; |
|
float m_errorHistory[ MAX_ERROR_HISTORY ]; ///< error history samples |
|
int m_errorHistoryIndex; |
|
int m_errorHistoryCount; |
|
float m_averageError; |
|
}; |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void RecvProxy_FishOriginX( const CRecvProxyData *pData, void *pStruct, void *pOut ) |
|
{ |
|
C_Fish *fish = (C_Fish *)pStruct; |
|
float *out = (float *)pOut; |
|
|
|
*out = pData->m_Value.m_Float + fish->m_poolOrigin.x; |
|
} |
|
|
|
void RecvProxy_FishOriginY( const CRecvProxyData *pData, void *pStruct, void *pOut ) |
|
{ |
|
C_Fish *fish = (C_Fish *)pStruct; |
|
float *out = (float *)pOut; |
|
|
|
*out = pData->m_Value.m_Float + fish->m_poolOrigin.y; |
|
} |
|
|
|
|
|
IMPLEMENT_CLIENTCLASS_DT_NOBASE( C_Fish, DT_CFish, CFish ) |
|
|
|
RecvPropVector( RECVINFO(m_poolOrigin) ), |
|
|
|
RecvPropFloat( RECVINFO_NAME( m_actualPos.x, m_x ), 0, RecvProxy_FishOriginX ), |
|
RecvPropFloat( RECVINFO_NAME( m_actualPos.y, m_y ), 0, RecvProxy_FishOriginY ), |
|
RecvPropFloat( RECVINFO_NAME( m_actualPos.z, m_z ) ), |
|
|
|
RecvPropFloat( RECVINFO_NAME( m_actualAngles.y, m_angle ) ), |
|
|
|
RecvPropInt( RECVINFO(m_nModelIndex) ), |
|
RecvPropInt( RECVINFO(m_lifeState) ), |
|
|
|
RecvPropFloat( RECVINFO(m_waterLevel) ), ///< get this from the server in case we die when slightly out of the water due to error correction |
|
|
|
END_RECV_TABLE() |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void C_Fish::Spawn( void ) |
|
{ |
|
BaseClass::Spawn(); |
|
|
|
m_angles = QAngle( 0, 0, 0 ); |
|
m_actualAngles = m_angles; |
|
|
|
m_vel = Vector( 0, 0, 0 ); |
|
m_gotUpdate = false; |
|
m_localLifeState = LIFE_ALIVE; |
|
m_buoyancy = RandomFloat( 0.4f, 1.0f ); |
|
|
|
m_errorHistoryIndex = 0; |
|
m_errorHistoryCount = 0; |
|
m_averageError = 0.0f; |
|
|
|
SetNextClientThink( CLIENT_THINK_ALWAYS ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void C_Fish::ClientThink() |
|
{ |
|
if (FishDebug.GetBool()) |
|
{ |
|
debugoverlay->AddLineOverlay( m_pos, m_actualPos, 255, 0, 0, true, 0.1f ); |
|
switch( m_localLifeState ) |
|
{ |
|
case LIFE_DYING: |
|
debugoverlay->AddTextOverlay( m_pos, 0.1f, "DYING" ); |
|
break; |
|
|
|
case LIFE_DEAD: |
|
debugoverlay->AddTextOverlay( m_pos, 0.1f, "DEAD" ); |
|
break; |
|
} |
|
} |
|
|
|
float deltaT = gpGlobals->frametime; |
|
|
|
|
|
// check if we just died |
|
if (m_localLifeState == LIFE_ALIVE && m_lifeState != LIFE_ALIVE) |
|
{ |
|
// we have died |
|
m_localLifeState = LIFE_DYING; |
|
|
|
m_deathDepth = m_pos.z; |
|
|
|
// determine surface float angle |
|
m_deathAngle = RandomFloat( 87.0f, 93.0f ) * ((RandomInt( 0, 100 ) < 50) ? 1.0f : -1.0f); |
|
} |
|
|
|
|
|
switch( m_localLifeState ) |
|
{ |
|
case LIFE_DYING: |
|
{ |
|
// depth parameter |
|
float t = (m_pos.z - m_deathDepth) / (m_waterLevel - m_deathDepth); |
|
t *= t; |
|
|
|
// roll onto side |
|
m_angles.z = m_deathAngle * t; |
|
|
|
// float to surface |
|
const float fudge = 2.0f; |
|
if (m_pos.z < m_waterLevel - fudge) |
|
{ |
|
m_vel.z += (1.0f - t) * m_buoyancy * deltaT; |
|
} |
|
else |
|
{ |
|
m_localLifeState = LIFE_DEAD; |
|
} |
|
|
|
break; |
|
} |
|
|
|
case LIFE_DEAD: |
|
{ |
|
// depth parameter |
|
float t = (m_pos.z - m_deathDepth) / (m_waterLevel - m_deathDepth); |
|
t *= t; |
|
|
|
// roll onto side |
|
m_angles.z = m_deathAngle * t; |
|
|
|
// keep near water surface |
|
const float sub = 0.5f; |
|
m_vel.z += 10.0f * (m_waterLevel - m_pos.z - sub) * deltaT; |
|
|
|
// bob on surface |
|
const float rollAmp = 5.0f; |
|
const float rollFreq = 2.33f; |
|
m_angles.z += rollAmp * sin( rollFreq * (gpGlobals->curtime + 10.0f * entindex()) ) * deltaT; |
|
|
|
const float rollAmp2 = 7.0f; |
|
const float rollFreq2 = 4.0f; |
|
m_angles.x += rollAmp2 * sin( rollFreq2 * (gpGlobals->curtime + 10.0f * entindex()) ) * deltaT; |
|
|
|
const float bobAmp = 0.75f; |
|
const float bobFreq = 4.0f; |
|
m_vel.z += bobAmp * sin( bobFreq * (gpGlobals->curtime + 10.0f * entindex()) ) * deltaT; |
|
|
|
const float bobAmp2 = 0.75f; |
|
const float bobFreq2 = 3.333f; |
|
m_vel.z += bobAmp2 * sin( bobFreq2 * (gpGlobals->curtime + 10.0f * entindex()) ) * deltaT; |
|
|
|
// decay movement speed to zero |
|
const float drag = 1.0f; |
|
m_vel.z -= drag * m_vel.z * deltaT; |
|
|
|
break; |
|
} |
|
|
|
case LIFE_ALIVE: |
|
{ |
|
// use server-side Z coordinate directly |
|
m_pos.z = m_actualPos.z; |
|
|
|
// use server-side angles |
|
m_angles = m_actualAngles; |
|
|
|
// fishy wiggle based on movement |
|
if (!m_wiggleTimer.IsElapsed()) |
|
{ |
|
float swimPower = 1.0f - (m_wiggleTimer.GetElapsedTime() / m_wiggleTimer.GetCountdownDuration()); |
|
const float amp = 6.0f * swimPower; |
|
float wiggle = amp * sin( m_wigglePhase ); |
|
|
|
m_wigglePhase += m_wiggleRate * deltaT; |
|
|
|
// wiggle decay |
|
const float wiggleDecay = 5.0f; |
|
m_wiggleRate -= wiggleDecay * deltaT; |
|
|
|
m_angles.y += wiggle; |
|
} |
|
|
|
break; |
|
} |
|
} |
|
|
|
// compute error between our local position and actual server position |
|
Vector error = m_actualPos - m_pos; |
|
error.z = 0.0f; |
|
float errorLen = error.Length(); |
|
|
|
if (m_localLifeState == LIFE_ALIVE) |
|
{ |
|
// if error is far above average, start swimming |
|
const float wiggleThreshold = 2.0f; |
|
if (errorLen - m_averageError > wiggleThreshold) |
|
{ |
|
// if error is large, we must have started swimming |
|
const float swimTime = 5.0f; |
|
m_wiggleTimer.Start( swimTime ); |
|
|
|
m_wiggleRate = 2.0f * errorLen; |
|
|
|
const float maxWiggleRate = 30.0f; |
|
if (m_wiggleRate > maxWiggleRate) |
|
{ |
|
m_wiggleRate = maxWiggleRate; |
|
} |
|
} |
|
|
|
// update average error |
|
m_errorHistory[ m_errorHistoryIndex++ ] = errorLen; |
|
if (m_errorHistoryIndex >= MAX_ERROR_HISTORY) |
|
{ |
|
m_errorHistoryIndex = 0; |
|
m_errorHistoryCount = MAX_ERROR_HISTORY; |
|
} |
|
else if (m_errorHistoryCount < MAX_ERROR_HISTORY) |
|
{ |
|
++m_errorHistoryCount; |
|
} |
|
|
|
m_averageError = 0.0f; |
|
if (m_errorHistoryCount) |
|
{ |
|
for( int r=0; r<m_errorHistoryCount; ++r ) |
|
{ |
|
m_averageError += m_errorHistory[r]; |
|
} |
|
m_averageError /= (float)m_errorHistoryCount; |
|
} |
|
} |
|
|
|
// keep fish motion smooth by correcting towards actual server position |
|
// NOTE: This only tracks XY motion |
|
const float maxError = 20.0f; |
|
float errorT = errorLen / maxError; |
|
if (errorT > 1.0f) |
|
{ |
|
errorT = 1.0f; |
|
} |
|
|
|
// we want a nonlinear spring force for tracking |
|
errorT *= errorT; |
|
|
|
// as fish move faster, their error increases - use a stiffer spring when fast, and a weak one when slow |
|
const float trackRate = 0.0f + errorT * 115.0f; |
|
m_vel.x += trackRate * error.x * deltaT; |
|
m_vel.y += trackRate * error.y * deltaT; |
|
|
|
const float trackDrag = 2.0f + errorT * 6.0f; |
|
m_vel.x -= trackDrag * m_vel.x * deltaT; |
|
m_vel.y -= trackDrag * m_vel.y * deltaT; |
|
|
|
|
|
// euler integration |
|
m_pos += m_vel * deltaT; |
|
|
|
SetNetworkOrigin( m_pos ); |
|
SetAbsOrigin( m_pos ); |
|
|
|
SetNetworkAngles( m_angles ); |
|
SetAbsAngles( m_angles ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void C_Fish::OnDataChanged( DataUpdateType_t type ) |
|
{ |
|
//if (!m_gotUpdate) |
|
|
|
if (type == DATA_UPDATE_CREATED) |
|
{ |
|
// initial update |
|
m_gotUpdate = true; |
|
|
|
m_pos = m_actualPos; |
|
m_vel = Vector( 0, 0, 0 ); |
|
|
|
return; |
|
} |
|
} |
|
|
|
|