mirror of
https://github.com/thug1src/thug.git
synced 2024-11-30 12:06:44 +00:00
1668 lines
57 KiB
C++
1668 lines
57 KiB
C++
//****************************************************************************
|
|
//* MODULE: Gel/Components
|
|
//* FILENAME: RigidBodyComponent.cpp
|
|
//* OWNER: Dan
|
|
//* CREATION DATE: 1/22/3
|
|
//****************************************************************************
|
|
|
|
// An object component which dictates the motion of an object using an approximation of rigidbody physics.
|
|
|
|
#include <gel/components/rigidbodycomponent.h>
|
|
#include <gel/components/soundcomponent.h>
|
|
|
|
#include <core/math/matrix.h>
|
|
|
|
#include <gfx/nx.h>
|
|
|
|
#include <sk/engine/feeler.h>
|
|
#include <sk/modules/skate/skate.h>
|
|
#include <sk/objects/skater.h>
|
|
#include <sk/scripting/nodearray.h>
|
|
|
|
#include <gel/object/compositeobject.h>
|
|
#include <gel/object/compositeobjectmanager.h>
|
|
#include <gel/collision/collcache.h>
|
|
#include <gel/scripting/checksum.h>
|
|
#include <gel/scripting/script.h>
|
|
#include <gel/scripting/struct.h>
|
|
#include <gel/scripting/component.h>
|
|
#include <gel/scripting/symboltable.h>
|
|
#include <gel/scripting/utils.h>
|
|
|
|
namespace Obj
|
|
{
|
|
|
|
Nx::CCollCache CRigidBodyComponent::s_collision_cache;
|
|
bool CRigidBodyComponent::s_debug_lines_on = false;
|
|
bool CRigidBodyComponent::s_draw_skater_collision_circles = false;
|
|
float CRigidBodyComponent::s_skater_head_height;
|
|
Tmr::Time CRigidBodyComponent::s_collide_sound_allowed_time = 0;
|
|
CRigidBodyComponent::SContactState CRigidBodyComponent::sp_contact_states [ CRigidBodyComponent::vRP_MAX_NUM_CONTACTS ];
|
|
|
|
#ifdef __NOPT_DEBUG__
|
|
bool CRigidBodyComponent::s_contact_states_in_use = false;
|
|
#endif
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CBaseComponent* CRigidBodyComponent::s_create()
|
|
{
|
|
return static_cast< CBaseComponent* >( new CRigidBodyComponent );
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CRigidBodyComponent::CRigidBodyComponent() : CBaseComponent()
|
|
{
|
|
SetType( CRC_RIGIDBODY );
|
|
|
|
mp_sound_component = NULL;
|
|
|
|
// setup defaults
|
|
|
|
m_const_acc = vRP_GRAVITATIONAL_ACCELERATION;
|
|
m_mass_over_moment = vRP_DEFAULT_MASS_OVER_MOMENT;
|
|
m_coeff_restitution = vRP_DEFAULT_COEFF_RESTITUTION;
|
|
m_coeff_friction = vRP_DEFAULT_COEFF_FRICTION;
|
|
m_spring_const = vRP_DEFAULT_SPRING_CONST;
|
|
m_linear_velocity_sleep_point_sqr = vRP_DEFAULT_LINEAR_VELOCITY_SLEEP_POINT * vRP_DEFAULT_LINEAR_VELOCITY_SLEEP_POINT;
|
|
m_angular_velocity_sleep_point_sqr = vRP_DEFAULT_ANGULAR_VELOCITY_SLEEP_POINT * vRP_DEFAULT_ANGULAR_VELOCITY_SLEEP_POINT;
|
|
m_skater_collision_impulse_factor = vRP_DEFAULT_SKATER_COLLISION_IMPULSE_FACTOR;
|
|
m_skater_collision_rotation_factor = vRP_DEFAULT_SKATER_COLLISION_ROTATION_FACTOR;
|
|
m_skater_collision_radius = vRP_DEFAULT_SKATER_COLLISION_RADIUS;
|
|
m_skater_collision_application_radius = vRP_DEFAULT_SKATER_COLLISION_APPLICATION_RADIUS;
|
|
m_cos_skater_collision_assent = cosf(DEGREES_TO_RADIANS(vRP_DEFAULT_SKATER_COLLISION_ASSENT));
|
|
m_sin_skater_collision_assent = sinf(DEGREES_TO_RADIANS(vRP_DEFAULT_SKATER_COLLISION_ASSENT));
|
|
m_ignore_skater_duration = vRP_DEFAULT_IGNORE_SKATER_DURATION;
|
|
m_model_offset.Set(0.0f, 0.0f, 0.0f);
|
|
|
|
m_num_contacts = 0;
|
|
|
|
m_script_names.collide = m_script_names.bounce = m_script_names.settle = m_script_names.stuck = 0;
|
|
mp_script_params = NULL;
|
|
|
|
m_sound_setup.collide_sound = m_sound_setup.bounce_sound = NULL;
|
|
m_sound_setup.collide_mute_delay = vRP_DEFAULT_COLLIDE_MUTE_DELAY;
|
|
m_sound_setup.global_collide_mute_delay = vRP_DEFAULT_GLOBAL_COLLIDE_MUTE_DELAY;
|
|
m_sound_setup.bounce_velocity_callback_threshold = vRP_DEFAULT_BOUNCE_VELOCITY_CALLBACK_THRESHOLD;
|
|
m_sound_setup.bounce_velocity_full_speed = vRP_DEFAULT_BOUNCE_VELOCITY_FULL_SPEED;
|
|
|
|
m_collide_sound_allowed_time = 0;
|
|
|
|
m_die_countdown = -1.0f;
|
|
|
|
mp_contacts = NULL;
|
|
|
|
#ifdef __NOPT_ASSERT__
|
|
m_sound_type_id = 0;
|
|
#endif
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CRigidBodyComponent::~CRigidBodyComponent()
|
|
{
|
|
delete [] mp_contacts;
|
|
delete mp_script_params;
|
|
delete m_sound_setup.bounce_sound;
|
|
delete m_sound_setup.collide_sound;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::InitFromStructure( Script::CStruct* pParams )
|
|
{
|
|
// NOTE: right now rigidbodies ignore moving collidables; if we want to change this, we should account for the moving collidables' velocity
|
|
// in the collision resolution code
|
|
// NOTE: probably want to add some small random rotation when the skater hits the object; two reasons; one, when you hit a line of cones, right
|
|
// now it seems too orderly; two, when you push a cone ahead of you, the cone's dynamics should be more jerky and less smooth
|
|
|
|
// NOTE: don't make any contacts within a foot of the center of mass or the object is in danger of falling through the ground
|
|
|
|
// IDEA: for a skateboard rigidbody, have four of the contacts only feel friction parallel to the wheel's axis
|
|
|
|
// potential optimization; collision detect only every other frame, unless the last-ditch feeler says to do it this frame
|
|
|
|
m_state = ASLEEP;
|
|
|
|
m_vel.Set(0.0f, 0.0f, 0.0f);
|
|
m_rotvel.Set(0.0f, 0.0f, 0.0f);
|
|
|
|
m_matrix.Ident();
|
|
|
|
m_pos = GetObject()->GetPos();
|
|
|
|
m_orientation = GetObject()->GetMatrix();
|
|
m_orientation.Normalize();
|
|
m_orientation.GetMatrix(m_matrix);
|
|
|
|
pParams->GetVector(CRCD(0xc4c809e, "vel"), &m_vel);
|
|
|
|
pParams->GetVector(CRCD(0xfb1a83b2, "rotvel"), &m_rotvel);
|
|
|
|
pParams->GetVector(CRCD(0x737612c6, "center_of_mass"), &m_center_of_mass);
|
|
|
|
pParams->GetFloat(CRCD(0x6fae5c9a, "const_acc"), &m_const_acc);
|
|
|
|
pParams->GetFloat(CRCD(0x60b4860d, "coeff_restitution"), &m_coeff_restitution);
|
|
|
|
pParams->GetFloat(CRCD(0xf9d75930, "coeff_friction"), &m_coeff_friction);
|
|
|
|
pParams->GetFloat(CRCD(0xf1ecb30f, "spring_const"), &m_spring_const);
|
|
|
|
if (pParams->ContainsComponentNamed(CRCD(0xc11e4dd8, "linear_velocity_sleep_point")))
|
|
{
|
|
pParams->GetFloat(CRCD(0xc11e4dd8, "linear_velocity_sleep_point"), &m_linear_velocity_sleep_point_sqr);
|
|
m_linear_velocity_sleep_point_sqr *= m_linear_velocity_sleep_point_sqr;
|
|
}
|
|
|
|
if (pParams->ContainsComponentNamed(CRCD(0xa457ece3, "angular_velocity_sleep_point")))
|
|
{
|
|
pParams->GetFloat(CRCD(0xa457ece3, "angular_velocity_sleep_point"), &m_angular_velocity_sleep_point_sqr);
|
|
m_angular_velocity_sleep_point_sqr *= m_angular_velocity_sleep_point_sqr;
|
|
}
|
|
|
|
pParams->GetFloat(CRCD(0x3390d1e5, "skater_collision_impulse_factor"), &m_skater_collision_impulse_factor);
|
|
|
|
pParams->GetFloat(CRCD(0x5a18f61f, "skater_collision_rotation_factor"), &m_skater_collision_rotation_factor);
|
|
|
|
pParams->GetFloat(CRCD(0x85f112c1, "skater_collision_radius"), &m_skater_collision_radius);
|
|
|
|
pParams->GetFloat(CRCD(0x8ab2db68, "skater_collision_application_radius"), &m_skater_collision_application_radius);
|
|
|
|
if (pParams->ContainsComponentNamed(CRCD(0xe9156e6d, "skater_collision_assent")))
|
|
{
|
|
float skater_collision_assent;
|
|
pParams->GetFloat(CRCD(0xe9156e6d, "skater_collision_assent"), &skater_collision_assent);
|
|
m_cos_skater_collision_assent = cosf(DEGREES_TO_RADIANS(skater_collision_assent));
|
|
m_sin_skater_collision_assent = sinf(DEGREES_TO_RADIANS(skater_collision_assent));
|
|
}
|
|
|
|
pParams->GetFloat(CRCD(0x1f4d0ca5, "ignore_skater_duration"), &m_ignore_skater_duration);
|
|
|
|
pParams->GetChecksum(CRCD(0x58cdc0f0, "CollideScript"), &m_script_names.collide);
|
|
pParams->GetChecksum(CRCD(0x2d075f90, "BounceScript"), &m_script_names.bounce);
|
|
pParams->GetChecksum(CRCD(0x571917fa, "SettleScript"), &m_script_names.settle);
|
|
pParams->GetChecksum(CRCD(0xccb84047, "StuckScript"), &m_script_names.stuck);
|
|
|
|
if (pParams->ContainsComponentNamed(CRCD(0xdd85bda, "CallbackParams")))
|
|
{
|
|
Script::CStruct* p_struct;
|
|
pParams->GetStructure(CRCD(0xdd85bda, "CallbackParams"), &p_struct, Script::ASSERT);
|
|
|
|
mp_script_params = new Script::CStruct;
|
|
*mp_script_params += *p_struct;
|
|
}
|
|
|
|
// get sound setup
|
|
if (pParams->ContainsComponentNamed(CRCD(0x94956e48, "SoundType")))
|
|
{
|
|
uint32 sound_type_id;
|
|
pParams->GetChecksum(CRCD(0x94956e48, "SoundType"), &sound_type_id, Script::ASSERT);
|
|
|
|
get_sound_setup(sound_type_id);
|
|
|
|
#ifdef __NOPT_ASSERT__
|
|
m_sound_type_id = sound_type_id;
|
|
#endif
|
|
}
|
|
|
|
m_flags.Set(DIE_UPON_SLEEP, pParams->ContainsFlag(CRCD(0x5e51924f, "DieUponSettling")));
|
|
|
|
// there are currently two ways to setup the contact points; either you pass them in as an array of offsets from the center of mass
|
|
// or you specify a generic geometry and pass parameters to that geometry
|
|
|
|
EObjectGeometryType object_geometry = NO_GEOMETRY;
|
|
if (m_num_contacts == 0)
|
|
{
|
|
object_geometry = BOX;
|
|
}
|
|
|
|
if (pParams->ContainsFlag(CRCD(0xf756b7c5, "box")))
|
|
{
|
|
object_geometry = BOX;
|
|
}
|
|
else if (pParams->ContainsFlag(CRCD(0x1cb1a2b6, "pyramid")))
|
|
{
|
|
object_geometry = PYRAMID;
|
|
}
|
|
else if (pParams->ContainsFlag(CRCD(0x64fba415, "cylinder")))
|
|
{
|
|
object_geometry = CYLINDER;
|
|
}
|
|
else if (pParams->ContainsFlag(CRCD(0x20689278, "triangle")))
|
|
{
|
|
object_geometry = TRIANGLE;
|
|
}
|
|
|
|
// setup contacts using an array
|
|
if (pParams->ContainsComponentNamed(CRCD(0xccbfea8c, "contacts")))
|
|
{
|
|
Script::CArray* p_array;
|
|
pParams->GetArray(CRCD(0xccbfea8c, "contacts"), &p_array);
|
|
setup_contacts_from_array(p_array);
|
|
object_geometry = NO_GEOMETRY;
|
|
}
|
|
|
|
// setup contacts using a generic geometry
|
|
switch (object_geometry)
|
|
{
|
|
// a rectangular prism with contacts at each corner
|
|
case BOX:
|
|
{
|
|
Mth::Vector top_half_dimensions(32.0f, 32.0f, 16.0f);
|
|
Mth::Vector bottom_half_dimensions;
|
|
|
|
pParams->GetVector("dimensions", &top_half_dimensions);
|
|
bottom_half_dimensions = top_half_dimensions;
|
|
pParams->GetVector("top_dimensions", &top_half_dimensions);
|
|
pParams->GetVector("bottom_dimensions", &bottom_half_dimensions);
|
|
top_half_dimensions /= 2.0f;
|
|
bottom_half_dimensions /= 2.0f;
|
|
|
|
setup_contacts_as_box(top_half_dimensions, bottom_half_dimensions);
|
|
|
|
break;
|
|
}
|
|
|
|
// a pyramid with contacts at each corner along the bottom and one on the top
|
|
case PYRAMID:
|
|
{
|
|
float half_height = 42.0f;
|
|
float half_depth = 32.0f;
|
|
|
|
pParams->GetFloat(CRCD(0xab21af0,"height"), &half_height);
|
|
pParams->GetFloat(CRCD(0x55ce396,"depth"), &half_depth);
|
|
half_height /= 2.0f;
|
|
half_depth /= 2.0f;
|
|
|
|
setup_contacts_as_pyramid(half_height, half_depth);
|
|
|
|
break;
|
|
}
|
|
|
|
// a cylinder with six contacts around the top and bottom
|
|
case CYLINDER:
|
|
{
|
|
float top_radius = 12.0f;
|
|
float bottom_radius;
|
|
float half_height = 60.0f;
|
|
int edges = 6;
|
|
|
|
pParams->GetFloat(CRCD(0xc48391a5,"radius"), &top_radius);
|
|
bottom_radius = top_radius;
|
|
|
|
pParams->GetFloat(CRCD(0x937e3b29,"top_radius"), &top_radius);
|
|
pParams->GetFloat(CRCD(0xe2ac8c3a,"bottom_radius"), &bottom_radius);
|
|
pParams->GetFloat(CRCD(0xab21af0,"height"), &half_height);
|
|
pParams->GetInteger(CRCD(0x4055f24a,"edges"), &edges);
|
|
half_height /= 2.0f;
|
|
|
|
setup_contacts_as_cylinder(top_radius, bottom_radius, half_height, edges);
|
|
|
|
break;
|
|
}
|
|
|
|
// a triangle with three contacts on the left and right
|
|
case TRIANGLE:
|
|
{
|
|
Mth::Vector half_dimensions(32.0f, 32.0f, 16.0f);
|
|
|
|
pParams->GetVector(CRCD(0x1d82745a, "dimensions"), &half_dimensions);
|
|
half_dimensions /= 2.0f;
|
|
|
|
setup_contacts_as_triangle(half_dimensions);
|
|
|
|
break;
|
|
}
|
|
|
|
// don't use a generic geometry
|
|
case NO_GEOMETRY:
|
|
Dbg_MsgAssert(m_num_contacts, ("Contact geometry not setup for rigidbody"));
|
|
break;
|
|
}
|
|
|
|
// setup directional friction
|
|
if (pParams->ContainsComponentNamed(CRCD(0xc35cac0d, "directed_friction")))
|
|
{
|
|
Script::CArray* p_array;
|
|
pParams->GetArray(CRCD(0xc35cac0d, "directed_friction"), &p_array);
|
|
|
|
Dbg_MsgAssert(p_array->GetSize() == m_num_contacts, ("Array of directed friction specifications must equal the number of contacts"));
|
|
|
|
for (int n = p_array->GetSize(); n--; )
|
|
{
|
|
Script::CStruct* m_struct = p_array->GetStructure(n);
|
|
if (m_struct->ContainsComponentNamed(CRCD(0x806fff30, "none")))
|
|
{
|
|
mp_contacts[n].directed_friction = false;
|
|
}
|
|
else
|
|
{
|
|
mp_contacts[n].directed_friction = true;
|
|
m_struct->GetVector(NO_NAME, &mp_contacts[n].friction_direction, Script::ASSERT);
|
|
}
|
|
}
|
|
}
|
|
|
|
// adjust contacts based on extra angles
|
|
if (pParams->ContainsComponentNamed(CRCD(0x17b93077, "rotate_contacts")))
|
|
{
|
|
Mth::Vector angles;
|
|
pParams->GetVector(CRCD(0x17b93077, "rotate_contacts"), &angles, Script::ASSERT);
|
|
Mth::Matrix matrix;
|
|
matrix.SetFromAngles(angles);
|
|
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
mp_contacts[n].p = matrix.Rotate(mp_contacts[n].p);
|
|
}
|
|
}
|
|
|
|
// adjust contacts based on node array angles
|
|
if (pParams->ContainsComponentNamed(CRCD(0x9d2d0915, "Angles")))
|
|
{
|
|
Mth::Vector angles;
|
|
SkateScript::GetAngles(pParams, &angles);
|
|
Mth::Matrix matrix;
|
|
matrix.SetFromAngles(angles);
|
|
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
mp_contacts[n].p = matrix.Rotate(mp_contacts[n].p);
|
|
}
|
|
}
|
|
|
|
// setup model offset
|
|
Mth::Vector center_of_mass(0.0f, 0.0f, 0.0f);
|
|
if (pParams->ContainsComponentNamed(CRCD(0x737612c6, "center_of_mass")))
|
|
{
|
|
if (pParams->ContainsComponentNamed(CRCD(0x737612c6, "center_of_mass")))
|
|
{
|
|
pParams->GetVector(CRCD(0x737612c6, "center_of_mass"), ¢er_of_mass);
|
|
}
|
|
}
|
|
|
|
// if not set (or set to default), calculate the center of mass
|
|
if (center_of_mass[X] == 0.0f && center_of_mass[Y] == 0.0f && center_of_mass[Z] == 0.0f);
|
|
{
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
center_of_mass += mp_contacts[n].p;
|
|
}
|
|
center_of_mass *= (1.0f / m_num_contacts);
|
|
}
|
|
|
|
m_model_offset = -center_of_mass;
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
mp_contacts[n].p -= center_of_mass;
|
|
}
|
|
|
|
|
|
if (!pParams->GetFloat(CRCD(0x967cd3ae, "mass_over_moment"), &m_mass_over_moment))
|
|
{
|
|
// calculate an appropriate mass over moment adjustment; otherwise, you can get some really weird behavior
|
|
float max_dist = 0.0f;
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
max_dist = Mth::Max(max_dist, mp_contacts[n].p.Length());
|
|
}
|
|
if (max_dist > 30.0f)
|
|
{
|
|
m_mass_over_moment /= Mth::Sqr((max_dist / 30.0f));
|
|
}
|
|
}
|
|
|
|
// setup static variables
|
|
s_skater_head_height = GetPhysicsFloat(CRCD(0x542cf0c7, "Skater_default_head_height"));
|
|
|
|
// determine the largest feeler extent; used with the collision cache system
|
|
m_largest_contact_extent = 0.0f;
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
SContact& contact = mp_contacts[n];
|
|
|
|
float contact_extent_sqr = contact.p.LengthSqr();
|
|
if (contact_extent_sqr > m_largest_contact_extent)
|
|
{
|
|
m_largest_contact_extent = contact_extent_sqr;
|
|
}
|
|
}
|
|
m_largest_contact_extent = sqrtf(m_largest_contact_extent) + 1.0f;
|
|
|
|
m_ignore_skater_countdown = 0.0f;
|
|
|
|
m_pos -= m_matrix.Rotate(m_model_offset);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::Finalize()
|
|
{
|
|
mp_sound_component = GetSoundComponentFromObject(GetObject());
|
|
|
|
Dbg_Assert(mp_sound_component);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::Update()
|
|
{
|
|
#ifdef __NOPT_DEBUG__
|
|
Dbg_MsgAssert(!s_contact_states_in_use, ("Two CRigidBodyComponents updating simultaneously"));
|
|
s_contact_states_in_use = true;
|
|
#endif
|
|
|
|
float full_time_step = Tmr::FrameLength();
|
|
|
|
setup_contact_states();
|
|
|
|
// only check for skater collisions if we haven't collided with the skater recently; this insures that the stupid skater doesn't cram us
|
|
// into corners and the like
|
|
if (m_ignore_skater_countdown > 0.0f)
|
|
{
|
|
m_ignore_skater_countdown -= full_time_step;
|
|
}
|
|
else if (!m_flags.Test(PLAYER_COLLISION_DISABLED))
|
|
{
|
|
handle_skater_collisions();
|
|
}
|
|
|
|
if (m_die_countdown >= 0.0f)
|
|
{
|
|
if (m_die_countdown <= full_time_step)
|
|
{
|
|
GetObject()->MarkAsDead();
|
|
}
|
|
else
|
|
{
|
|
m_die_countdown -= full_time_step;
|
|
}
|
|
}
|
|
|
|
// if we are asleep
|
|
if (m_state == ASLEEP)
|
|
{
|
|
GetObject()->SetDisplayMatrix(m_matrix);
|
|
|
|
if (s_debug_lines_on)
|
|
{
|
|
draw_debug_lines();
|
|
}
|
|
|
|
// do just about nothing
|
|
|
|
#ifdef __NOPT_DEBUG__
|
|
s_contact_states_in_use = false;
|
|
#endif
|
|
|
|
return;
|
|
}
|
|
|
|
int main_update_loop_count = 0;
|
|
bool last_ditch_collision = false;
|
|
float remaining_time_step = full_time_step;
|
|
do {
|
|
float time_step = remaining_time_step;
|
|
|
|
// send a feeler from object's center of mass to our new center of mass; most collisions will not be caught in this manner;
|
|
// this is a last-ditch feeler to insure that we don't fly through objects at high speeds
|
|
CFeeler feeler;
|
|
feeler.m_start = m_pos;
|
|
feeler.m_end = m_pos + time_step * m_vel;
|
|
feeler.m_end[Y] += 0.5f * time_step * time_step * m_const_acc;
|
|
|
|
last_ditch_collision = feeler.GetCollision(false);
|
|
|
|
// if we have such a collision, move only to it
|
|
if (last_ditch_collision)
|
|
{
|
|
time_step = (feeler.GetDist() * feeler.Length() - 6.0f) / m_vel.Length();
|
|
if (time_step < 0.0f)
|
|
{
|
|
time_step = 0.0f;
|
|
}
|
|
}
|
|
|
|
update_dynamic_state(time_step);
|
|
|
|
// we do a very simple collision
|
|
if (last_ditch_collision)
|
|
{
|
|
m_vel -= (1.0f + m_coeff_restitution) * Mth::DotProduct(m_vel, feeler.GetNormal()) * feeler.GetNormal();
|
|
}
|
|
|
|
remaining_time_step -= time_step;
|
|
|
|
// make sure we're not in an infinite loop; a weirdly hanging box is better than a frozen game
|
|
if (++main_update_loop_count == 5)
|
|
{
|
|
MESSAGE("too many last-ditch contacts in a single frame; going to sleep");
|
|
sleep();
|
|
break;
|
|
}
|
|
} while (last_ditch_collision);
|
|
|
|
if (detect_collisions())
|
|
{
|
|
resolve_collisions();
|
|
}
|
|
|
|
consider_sleeping();
|
|
|
|
// if we've gotten here, we need to update the object
|
|
|
|
GetObject()->SetPos(m_pos + m_matrix.Rotate(m_model_offset));
|
|
|
|
GetObject()->SetMatrix(m_matrix);
|
|
GetObject()->SetDisplayMatrix(m_matrix);
|
|
|
|
GetObject()->SetVel(m_vel);
|
|
|
|
if (s_debug_lines_on)
|
|
{
|
|
draw_debug_lines();
|
|
}
|
|
|
|
#ifdef __NOPT_DEBUG__
|
|
s_contact_states_in_use = false;
|
|
#endif
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CBaseComponent::EMemberFunctionResult CRigidBodyComponent::CallMemberFunction( uint32 Checksum, Script::CStruct* pParams, Script::CScript* pScript )
|
|
{
|
|
switch ( Checksum )
|
|
{
|
|
// @script | RigidBody_IgnoreSkater | rigidbody ignores the skater for a given duration
|
|
// @uparm 0.0 | duration (defaults to seconds)
|
|
// @flag frames | time is given in frames
|
|
case CRCC(0xcc12cf87, "RigidBody_IgnoreSkater"):
|
|
{
|
|
pParams->GetFloat(NO_NAME, &m_ignore_skater_countdown, Script::ASSERT);
|
|
if (pParams->ContainsFlag(CRCD(0x19176c5, "frames")) || pParams->ContainsFlag(CRCD(0x4a07c332, "frame")))
|
|
{
|
|
m_ignore_skater_countdown *= (1.0f / 60.0f);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// @script | RigidBody_Wake | wakes up the rigidbody
|
|
case CRCC(0x9599c10f, "RigidBody_Wake"):
|
|
wake();
|
|
break;
|
|
|
|
// @script | RigidBody_Sleep | puts the rigidbody to sleep
|
|
case CRCC(0xcd5ba67b, "RigidBody_Sleep"):
|
|
sleep();
|
|
break;
|
|
|
|
// @script | RigidBody_Kick | wakes the rigidbody with a kick
|
|
case CRCC(0xae24df9f, "RigidBody_Kick"):
|
|
{
|
|
Mth::Vector v;
|
|
if (pParams->GetVector(CRCD(0xc4c809e, "vel"), &v))
|
|
{
|
|
GetObject()->SetVel(m_vel += v);
|
|
}
|
|
if (pParams->GetVector(CRCD(0xfb1a83b2, "rotvel"), &v))
|
|
{
|
|
m_rotvel += v;
|
|
}
|
|
if (pParams->GetVector(CRCD(0x7f261953, "pos"), &v))
|
|
{
|
|
GetObject()->SetPos(m_pos += v);
|
|
}
|
|
wake();
|
|
break;
|
|
}
|
|
|
|
// @script : RigidBody_Reset | reset any parameters of the rigidbody
|
|
case CRCC(0x92f5db9a, "RigidBody_Reset"):
|
|
InitFromStructure(pParams);
|
|
break;
|
|
|
|
// @script : RigidBody_DisablePlayerCollision
|
|
case CRCC(0xa2bbb3a, "RigidBody_DisablePlayerCollision"):
|
|
m_flags.Set(PLAYER_COLLISION_DISABLED);
|
|
break;
|
|
|
|
// @script : RigidBody_EnablePlayerCollision
|
|
case CRCC(0x49f44585, "RigidBody_EnablePlayerCollision"):
|
|
m_flags.Clear(PLAYER_COLLISION_DISABLED);
|
|
break;
|
|
|
|
// @script : RigidBody_EnablePlayerCollision
|
|
case CRCC(0x949be8a9, "RigidBody_MatchVelocityTo"):
|
|
{
|
|
Script::CComponent* p_component = pParams->GetNextComponent(NULL);
|
|
Dbg_MsgAssert(p_component->mType == ESYMBOLTYPE_NAME, ("RigidBody_MatchVelocityTo requires an object name as its first parameter"));
|
|
CCompositeObject* p_composite_object = static_cast< CCompositeObject* >(CCompositeObjectManager::Instance()->GetObjectByID(p_component->mChecksum));
|
|
Dbg_MsgAssert(p_composite_object, ("RigidBody_MatchVelocityTo requires a composite object name as its first parameter"));
|
|
|
|
bool apply_random_adjustment = pParams->ContainsFlag(CRCD(0x8ace8a0f, "ApplyRandomAdjustment"));
|
|
|
|
m_vel = p_composite_object->m_vel;
|
|
if (apply_random_adjustment)
|
|
{
|
|
m_vel *= 1.0f + Mth::PlusOrMinus(0.4f);
|
|
}
|
|
GetObject()->SetVel(m_vel);
|
|
|
|
if (m_state == ASLEEP && Mth::Abs(m_vel[Y]) < 1.0f)
|
|
{
|
|
m_vel[Y] = 1.0f;
|
|
}
|
|
|
|
// use the rotational velocity also, if it's a rigidbody
|
|
CRigidBodyComponent* p_rigid_body_component = GetRigidBodyComponentFromObject(p_composite_object);
|
|
if (p_rigid_body_component)
|
|
{
|
|
m_rotvel = p_rigid_body_component->m_rotvel;
|
|
if (apply_random_adjustment)
|
|
{
|
|
m_rotvel *= 1.0f * Mth::PlusOrMinus(0.4f);
|
|
}
|
|
}
|
|
|
|
wake();
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
return CBaseComponent::MF_NOT_EXECUTED;
|
|
}
|
|
return CBaseComponent::MF_TRUE;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::GetDebugInfo(Script::CStruct *p_info)
|
|
{
|
|
#ifdef __DEBUG_CODE__
|
|
Dbg_MsgAssert(p_info, ("NULL p_info sent to CRigidBodyComponent::GetDebugInfo"));
|
|
|
|
uint32 state_checksums[] =
|
|
{
|
|
CRCD(0x4484b712, "ASLEEP"), CRCD(0x99a34896, "AWAKE")
|
|
};
|
|
p_info->AddChecksum("m_state", state_checksums[m_state]);
|
|
|
|
p_info->AddFloat("m_orientation", m_orientation.GetScalar());
|
|
p_info->AddVector("m_orientation", m_orientation.GetVector());
|
|
|
|
p_info->AddVector("m_vel", m_vel);
|
|
|
|
p_info->AddVector("m_rotvel", m_rotvel);
|
|
|
|
p_info->AddFloat("m_const_acc", m_const_acc);
|
|
|
|
p_info->AddFloat("m_mass_over_moment", m_mass_over_moment);
|
|
|
|
p_info->AddFloat("m_coeff_restitution", m_coeff_restitution);
|
|
|
|
p_info->AddFloat("m_coeff_friction", m_coeff_friction);
|
|
|
|
p_info->AddFloat("m_spring_const", m_spring_const);
|
|
|
|
p_info->AddFloat("m_skater_collision_impulse_factor", m_skater_collision_impulse_factor);
|
|
|
|
p_info->AddFloat("m_skater_collision_rotation_factor", m_skater_collision_rotation_factor);
|
|
|
|
p_info->AddFloat("m_skater_collision_radius", m_skater_collision_radius);
|
|
|
|
p_info->AddFloat("m_num_collisions", m_num_collisions);
|
|
|
|
p_info->AddVector("m_model_offset", m_model_offset);
|
|
|
|
p_info->AddFloat("CollideMuteDelay", m_sound_setup.collide_mute_delay);
|
|
|
|
p_info->AddFloat("GlobalCollideMuteDelay", m_sound_setup.global_collide_mute_delay);
|
|
|
|
p_info->AddFloat("BounceVelocityCallbackThreshold", m_sound_setup.bounce_velocity_callback_threshold);
|
|
|
|
p_info->AddFloat("BounceVelocityFullSpeed", m_sound_setup.bounce_velocity_full_speed);
|
|
|
|
Script::CArray* p_array = new Script::CArray;
|
|
p_array->SetSizeAndType(m_num_contacts, ESYMBOLTYPE_VECTOR);
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
Script::CVector* p_vector = new Script::CVector;
|
|
p_vector->mX = mp_contacts[n].p[X];
|
|
p_vector->mY = mp_contacts[n].p[Y];
|
|
p_vector->mZ = mp_contacts[n].p[Z];
|
|
p_array->SetVector(n, p_vector);
|
|
}
|
|
p_info->AddArrayPointer("m_contacts", p_array);
|
|
|
|
p_info->AddChecksum("CollideScript", m_script_names.collide);
|
|
p_info->AddChecksum("BounceScript", m_script_names.bounce);
|
|
p_info->AddChecksum("SettleScript", m_script_names.settle);
|
|
p_info->AddChecksum("StuckScript", m_script_names.stuck);
|
|
|
|
if (mp_script_params)
|
|
{
|
|
p_info->AddStructure("CallbackParams", mp_script_params);
|
|
}
|
|
|
|
p_info->AddChecksum("DieUponSettling", m_flags.Test(DIE_UPON_SLEEP) ? CRCD(0x203b372, "true") : CRCD(0xd43297cf, "false"));
|
|
p_info->AddChecksum("PlayerCollisionDisabled", m_flags.Test(PLAYER_COLLISION_DISABLED) ? CRCD(0x203b372, "true") : CRCD(0xd43297cf, "false"));
|
|
|
|
if (m_sound_setup.bounce_sound)
|
|
{
|
|
Script::CStruct* p_struct = new Script::CStruct;
|
|
*p_struct += *m_sound_setup.bounce_sound;
|
|
p_info->AddStructurePointer(CRCD(0x1fb2f60c, "BounceSound"), p_struct);
|
|
}
|
|
if (m_sound_setup.collide_sound)
|
|
{
|
|
Script::CStruct* p_struct = new Script::CStruct;
|
|
*p_struct += *m_sound_setup.collide_sound;
|
|
p_info->AddStructurePointer(CRCD(0xbf8e0ace, "CollideSound"), p_struct);
|
|
}
|
|
|
|
CBaseComponent::GetDebugInfo(p_info);
|
|
#endif
|
|
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::sToggleDrawRigidBodyDebugLines ( )
|
|
{
|
|
if (s_debug_lines_on)
|
|
{
|
|
/*
|
|
if (s_draw_skater_collision_circles)
|
|
{
|
|
s_debug_lines_on = s_draw_skater_collision_circles = false;
|
|
}
|
|
else
|
|
{
|
|
s_draw_skater_collision_circles = true;
|
|
}
|
|
*/
|
|
s_debug_lines_on = false;
|
|
}
|
|
else
|
|
{
|
|
s_debug_lines_on = true;
|
|
}
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::setup_contact_states ( )
|
|
{
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
sp_contact_states[n].p_world = m_matrix.Rotate(mp_contacts[n].p);
|
|
}
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::handle_skater_collisions ( )
|
|
{
|
|
for (int n = Mdl::Skate::Instance()->GetNumSkaters(); n--; )
|
|
{
|
|
CSkater& skater = *Mdl::Skate::Instance()->GetSkater(n);
|
|
|
|
float radius_boost = skater.GetRigidBodyCollisionRadiusBoost();
|
|
|
|
// detect collisions within a cylinder of the skater; if objects end up having significant extent in some directions and not others,
|
|
// this detection code may have to become more sophisticated
|
|
|
|
// check distance to the skater in the X-Z plane
|
|
float distance_sqr = (m_pos[X] - skater.GetPos()[X]) * (m_pos[X] - skater.GetPos()[X])
|
|
+ (m_pos[Z] - skater.GetPos()[Z]) * (m_pos[Z] - skater.GetPos()[Z]);
|
|
if (distance_sqr > Mth::Sqr(m_skater_collision_radius + radius_boost)) continue;
|
|
|
|
// check Y-offset to the skater
|
|
float y_offset = skater.GetPos()[Y] - m_pos[Y];
|
|
if (y_offset > m_skater_collision_application_radius + radius_boost
|
|
|| y_offset < -(m_skater_collision_application_radius + radius_boost + s_skater_head_height)) continue;
|
|
|
|
// apply the skater collision impulse; the impulse occurs at a slight assent and in a direction halfway between of the skater's
|
|
// velocity and the line to the object; the magnitude is roughly proportional to the component of the skater's velocity along the
|
|
// line to the object; the impulse will be applied a few inches above the skater's lowest point, along the line between the object
|
|
// and the skater at distance equal to the collision radius of the object
|
|
|
|
// unit vector pointing from the skater to the object in the X-Z plane
|
|
Mth::Vector r = m_pos;
|
|
r -= skater.GetPos();
|
|
// r[Y] = 0.0f; // optimized out as r[Y] never used
|
|
float eff_length = sqrtf(r[X] * r[X] + r[Z] * r[Z]);
|
|
r[X] /= eff_length;
|
|
r[Z] /= eff_length;
|
|
|
|
// skater's relative velocity in the X-Z plane
|
|
Mth::Vector skater_vel = skater.GetVel();
|
|
skater_vel -= m_vel;
|
|
skater_vel[Y] = 0.0f;
|
|
|
|
if(( fabsf( skater_vel[X] ) < 0.01f ) && ( fabsf( skater_vel[Z] ) < 0.01f ))
|
|
{
|
|
// Small enough not to worry about - smaller can cause NaN decomposition.
|
|
return;
|
|
}
|
|
|
|
// unadjusted impulse magnitude
|
|
float magnitude = skater_vel[X] * r[X] + skater_vel[Z] * r[Z];
|
|
if (magnitude <= 0.0f) continue;
|
|
|
|
// we adjust the impulse magnitude so the object will be vaulted in front of the skater so as to give the player a nice view of the object's
|
|
// dynamics; also, at low skater velocities, we want only a very small impulse
|
|
|
|
// fast collisions
|
|
if (magnitude > (2.0f * vRP_SKATER_COLLISION_VELOCITY_THRESHOLD_INCREMENT))
|
|
{
|
|
// increase the impulse by a constant
|
|
magnitude += ((1.0f + vRP_SKATER_COLLISION_VELOCITY_THRESHOLD_INCREMENT) * vRP_SKATER_COLLISION_VELOCITY_FACTOR);
|
|
}
|
|
// medium collisions
|
|
else if (magnitude > vRP_SKATER_COLLISION_VELOCITY_THRESHOLD_INCREMENT) {
|
|
// connect the functions continuously
|
|
magnitude += (vRP_SKATER_COLLISION_VELOCITY_FACTOR * vRP_SKATER_COLLISION_VELOCITY_THRESHOLD_INCREMENT)
|
|
+ (magnitude - vRP_SKATER_COLLISION_VELOCITY_THRESHOLD_INCREMENT);
|
|
}
|
|
else
|
|
// slow collisions
|
|
{
|
|
// increase the impulse by a factor
|
|
magnitude *= (1.0f + vRP_SKATER_COLLISION_VELOCITY_FACTOR);
|
|
}
|
|
|
|
// impulse direction in the X-Z plane is the average of the skater velocity and r directions
|
|
// we know the Y component for both is zero
|
|
Mth::Vector impulse = skater_vel;
|
|
eff_length = sqrtf(impulse[X] * impulse[X] + impulse[Z] * impulse[Z]);
|
|
impulse[X] = impulse[X] / eff_length + r[X];
|
|
impulse[Z] = impulse[Z] / eff_length + r[Z];
|
|
eff_length = sqrtf(impulse[X] * impulse[X] + impulse[Z] * impulse[Z]);
|
|
impulse[X] = impulse[X] / eff_length;
|
|
impulse[Z] = impulse[Z] / eff_length;
|
|
|
|
// adjust the impulse direction upwards
|
|
impulse[X] *= m_cos_skater_collision_assent;
|
|
impulse[Y] = m_sin_skater_collision_assent;
|
|
impulse[Z] *= m_cos_skater_collision_assent;
|
|
|
|
// factor in the magnitude
|
|
impulse *= m_skater_collision_impulse_factor * magnitude;
|
|
|
|
// the impulse's point of application in the XZ-plane is along the line between the skater and the object at the object's impulse application
|
|
// radius
|
|
|
|
// we calculate the point of application with respect to the center of mass
|
|
Mth::Vector application_point = skater.GetPos();
|
|
application_point[X] -= m_pos[X];
|
|
application_point[Y] = 0.0f;
|
|
application_point[Z] -= m_pos[Z];
|
|
application_point.Normalize(m_skater_collision_application_radius);
|
|
|
|
// calculate the Y-component of the point of application
|
|
if (y_offset > 0.0f)
|
|
{
|
|
// collide with the skater's feet
|
|
application_point[Y] = skater.GetPos()[Y] - m_pos[Y];
|
|
}
|
|
else if (y_offset < -s_skater_head_height)
|
|
{
|
|
// collide with the skater's head
|
|
application_point[Y] = skater.GetPos()[Y] + s_skater_head_height - m_pos[Y];
|
|
}
|
|
else
|
|
{
|
|
// collide with the skater's body; use an offset from the center of mass to cause rotation
|
|
application_point[Y] = -12.0f;
|
|
}
|
|
|
|
// apply the impulse; artifically reduce the initial rotation; it looks nice when the objects tumbles more after their first bounce
|
|
m_vel += impulse;
|
|
m_rotvel += m_skater_collision_rotation_factor * 0.6f * m_mass_over_moment * Mth::CrossProduct(application_point, impulse);
|
|
|
|
wake();
|
|
|
|
// start a countdown; while this countdown is complete, we will ignore the skater
|
|
m_ignore_skater_countdown = m_ignore_skater_duration;
|
|
|
|
// call the bounce script callback
|
|
if (m_script_names.collide)
|
|
{
|
|
GetObject()->SpawnScriptPlease(m_script_names.collide, mp_script_params)->Update();
|
|
}
|
|
|
|
// kill the object's shadow
|
|
if (Nx::CSector *p_shadow_sector = Nx::CEngine::sGetMainScene()->GetSector(Crc::ExtendCRCWithString(GetObject()->GetID(), "_Shadow")))
|
|
{
|
|
p_shadow_sector->SetActive(false);
|
|
}
|
|
|
|
Tmr::Time time = Tmr::GetTime();
|
|
if ((int) (time - m_collide_sound_allowed_time) > 0 && (int) (time - s_collide_sound_allowed_time) > 0)
|
|
{
|
|
#ifdef __NOPT_ASSERT__
|
|
if (m_sound_type_id && Script::GetInteger(CRCD(0xd634a297, "DynamicRigidbodySounds")))
|
|
{
|
|
get_sound_setup(m_sound_type_id);
|
|
}
|
|
#endif
|
|
|
|
if (m_sound_setup.collide_sound)
|
|
{
|
|
float percent = (100.0f / 1000.0f) * impulse.Length();
|
|
m_sound_setup.collide_sound->AddFloat(CRCD(0x9e497fc6, "Percent"), percent);
|
|
mp_sound_component->PlayScriptedSound(m_sound_setup.collide_sound);
|
|
|
|
// delay the next collision sound
|
|
m_collide_sound_allowed_time = Tmr::GetTime() + (50 + Mth::Rnd(100)) * m_sound_setup.collide_mute_delay / 100;
|
|
s_collide_sound_allowed_time = Tmr::GetTime() + (50 + Mth::Rnd(100)) * m_sound_setup.global_collide_mute_delay / 100;
|
|
}
|
|
}
|
|
|
|
// collide with only a single skater
|
|
break;
|
|
} // END loop over skaters
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::update_dynamic_state ( float time_step )
|
|
{
|
|
// a massively shoe-string handling of contact forces
|
|
if (m_num_collisions < 3)
|
|
{
|
|
Mth::Vector delta_pos = m_vel;
|
|
delta_pos[Y] += 0.5f * time_step * m_const_acc;
|
|
delta_pos *= time_step;
|
|
m_pos += delta_pos;
|
|
|
|
m_vel[Y] += time_step * m_const_acc;
|
|
}
|
|
else
|
|
{
|
|
m_pos += time_step * m_vel;
|
|
}
|
|
|
|
Mth::Quat delta_orientation = m_orientation;
|
|
delta_orientation *= Mth::Quat(-0.5f * m_rotvel[X], -0.5f * m_rotvel[Y], -0.5f * m_rotvel[Z], 0.0f);
|
|
delta_orientation *= time_step;
|
|
m_orientation += delta_orientation;
|
|
|
|
m_orientation.Normalize();
|
|
|
|
m_orientation.GetMatrix(m_matrix);
|
|
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
sp_contact_states[n].p_world = m_matrix.Rotate(mp_contacts[n].p);
|
|
}
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::consider_sleeping ( )
|
|
{
|
|
// it is possible that we may want to check these conditions over a period of several frames to insure that sleeping is warranted
|
|
|
|
// only sleep if we're experiencing three or more collisions; hopefully, a surface-to-surface contact
|
|
if (m_num_collisions < 3) return;
|
|
|
|
// only sleep if we're moving slow
|
|
if (m_vel.LengthSqr() > m_linear_velocity_sleep_point_sqr) return;
|
|
|
|
// only sleep if we're moving slow
|
|
if (m_rotvel.LengthSqr() > m_angular_velocity_sleep_point_sqr) return;
|
|
|
|
sleep();
|
|
|
|
// if we're not suppose to when going to sleep
|
|
if (!m_flags.Test(DIE_UPON_SLEEP)) return;
|
|
|
|
// only die if we've moved substantially from our starting point
|
|
if ((m_pos - m_wake_pos).LengthSqr() < 48.0f * 48.0f) return;
|
|
|
|
m_die_countdown = 1.5f;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::wake ( )
|
|
{
|
|
if (m_state != AWAKE)
|
|
{
|
|
m_wake_pos = GetObject()->GetPos();
|
|
m_state = AWAKE;
|
|
}
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::sleep ( )
|
|
{
|
|
if (m_script_names.settle)
|
|
{
|
|
GetObject()->SpawnScriptPlease(m_script_names.settle, mp_script_params)->Update();
|
|
}
|
|
|
|
m_vel.Set(0.0f, 0.0f, 0.0f);
|
|
m_rotvel.Set(0.0f, 0.0f, 0.0f);
|
|
m_state = ASLEEP;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
bool CRigidBodyComponent::detect_collisions ( )
|
|
{
|
|
CFeeler feeler;
|
|
|
|
// set up a bounding box around the space within which all collision detection will occur
|
|
Mth::CBBox bounding_box(m_pos - Mth::Vector(m_largest_contact_extent, m_largest_contact_extent, m_largest_contact_extent),
|
|
m_pos + Mth::Vector(m_largest_contact_extent, m_largest_contact_extent, m_largest_contact_extent));
|
|
s_collision_cache.Update(bounding_box);
|
|
feeler.SetCache(&s_collision_cache);
|
|
|
|
// loop over the contact points
|
|
m_num_collisions = 0;
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
SContactState& contact_state = sp_contact_states[n];
|
|
|
|
// run a feeler from the object's center to the contact point
|
|
feeler.m_start = m_pos;
|
|
feeler.m_end = m_pos;
|
|
feeler.m_end += contact_state.p_world;
|
|
|
|
contact_state.collision = feeler.GetCollision(false);
|
|
|
|
if (!contact_state.collision) continue;
|
|
|
|
contact_state.normal = feeler.GetNormal();
|
|
contact_state.normal_eff_mass_set = false;
|
|
contact_state.normal_impulse_magnitude = 0.0f;
|
|
|
|
contact_state.depth = Mth::DotProduct(contact_state.normal, (1.0f - feeler.GetDist()) * (feeler.m_end - feeler.m_start));
|
|
contact_state.collision = contact_state.depth <= 0.0f;
|
|
|
|
if (contact_state.collision)
|
|
{
|
|
m_num_collisions++;
|
|
}
|
|
}
|
|
|
|
return m_num_collisions != 0;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
float CRigidBodyComponent::calculate_effective_mass ( const Mth::Vector& direction, const SContactState& contact_state ) const
|
|
{
|
|
// calculated the effective mass of the object at a contact for forces in a given direction
|
|
|
|
Mth::Vector delta_rotvel = Mth::CrossProduct(contact_state.p_world, direction);
|
|
delta_rotvel *= m_mass_over_moment;
|
|
return 1.0f / Mth::DotProduct(direction, direction + Mth::CrossProduct(delta_rotvel, contact_state.p_world));
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::resolve_collisions ( )
|
|
{
|
|
// collision resolution code of my own devise; simplified and not wholly accurate, it is good enough because we only have a single object
|
|
// colliding with a (mostly) static background; multiple collisions are not resolved simultaneously; instead, an iterative procedure is used to
|
|
// resolve the normal forces; this can cause objects which are colliding face-to-face with a surface to bounce off with odd rotation,
|
|
// as the required collision impulse will not be spread evenly across the contacts (as would be correct); also, friction is not handled in closed
|
|
// form; instead, after the collision is resolved and the normal forces are determined, we go back in and apply frictional forces (using the
|
|
// velocity of the object before the normal forces are applied; this generates friction which is incorrect, but visually reasonable; the friction
|
|
// forces could ruin the resolution of the collision, but we just ignore that possibility, and leave the problem to be fixed on the next frame
|
|
|
|
// cache the velocity and use it when determining the frictional forces
|
|
Mth::Vector cache_vel = m_vel;
|
|
Mth::Vector cache_rotvel = m_rotvel;
|
|
|
|
// maximum collision velocity; used to determine if a bounce callback is warranted
|
|
float max_collision_velocity = 0.0f;
|
|
|
|
// loop until the collisions are fully resolved
|
|
int collision_resolution_pass_count = 0;
|
|
bool clean_pass;
|
|
do {
|
|
clean_pass = true;
|
|
|
|
// loop over the collision points
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
SContactState& contact_state = sp_contact_states[n];
|
|
if (!contact_state.collision) continue;
|
|
|
|
// calculate the normal velocity at the collision point
|
|
Mth::Vector vel = m_vel;
|
|
vel += Mth::CrossProduct(m_rotvel, contact_state.p_world);
|
|
float normal_vel = Mth::DotProduct(contact_state.normal, vel);
|
|
|
|
// skip if we're not colliding
|
|
if (normal_vel > 0.0f) continue;
|
|
clean_pass = false;
|
|
|
|
if (-normal_vel > max_collision_velocity)
|
|
{
|
|
max_collision_velocity = -normal_vel;
|
|
}
|
|
|
|
// calculate the goal velocity
|
|
float goal_normal_vel = -m_coeff_restitution * normal_vel;
|
|
|
|
// calculate normal effective mass
|
|
if (!contact_state.normal_eff_mass_set)
|
|
{
|
|
contact_state.normal_eff_mass = calculate_effective_mass(contact_state.normal, contact_state);
|
|
contact_state.normal_eff_mass_set = true;
|
|
}
|
|
|
|
// calculate the normal impulse required to reach the goal velocity
|
|
float normal_impulse = (goal_normal_vel - normal_vel) * contact_state.normal_eff_mass;
|
|
|
|
// calculate normal impulse
|
|
Mth::Vector impulse = contact_state.normal;
|
|
impulse *= normal_impulse;
|
|
|
|
// accumulate the total normal impulse applied at his contact
|
|
contact_state.normal_impulse_magnitude += normal_impulse;
|
|
|
|
// apply the normal impulse
|
|
m_vel += impulse;
|
|
m_rotvel += m_mass_over_moment * Mth::CrossProduct(contact_state.p_world, impulse);
|
|
} // END loop over collision points
|
|
|
|
if (++collision_resolution_pass_count == 20)
|
|
{
|
|
Dbg_Message("CRigidBodyComponent::resolve_collisions: unresolveable collision; zeroing velocity");
|
|
// call the sleep script callback
|
|
if (m_script_names.stuck)
|
|
{
|
|
GetObject()->SpawnScriptPlease(m_script_names.stuck, mp_script_params)->Update();
|
|
}
|
|
else
|
|
{
|
|
m_vel.Set(0.0f, 0.0f, 0.0f);
|
|
m_rotvel.Set(0.0f, 0.0f, 0.0f);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// if there's only one collision point, we need to make only a single pass
|
|
if (m_num_contacts == 1) break;
|
|
} while (!clean_pass);
|
|
// END loop until collision is resolved
|
|
|
|
// we've resolved the collision; now calculate and apply the friction using the cached velocities
|
|
|
|
// loop over the collision points
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
SContact& contact = mp_contacts[n];
|
|
SContactState& contact_state = sp_contact_states[n];
|
|
if (!contact_state.collision) continue;
|
|
|
|
// calculate the tangent direction and velocity
|
|
Mth::Vector vel = cache_vel;
|
|
vel += Mth::CrossProduct(cache_rotvel, contact_state.p_world);
|
|
float normal_vel = Mth::DotProduct(contact_state.normal, vel);
|
|
Mth::Vector tangent = vel + -normal_vel * contact_state.normal;
|
|
|
|
if (contact.directed_friction)
|
|
{
|
|
Mth::Vector friction_direction_world = m_matrix.Rotate(contact.friction_direction);
|
|
friction_direction_world -= Mth::DotProduct(contact_state.normal, friction_direction_world) * contact_state.normal;
|
|
|
|
float length = friction_direction_world.Length();
|
|
if (length > 0.001f)
|
|
{
|
|
friction_direction_world *= (1.0f / length);
|
|
tangent.ProjectToNormal(friction_direction_world);
|
|
}
|
|
}
|
|
|
|
float tangent_vel = tangent.Length();
|
|
|
|
// if the tangential velocity is too small, it's direction will be meaningless anyway
|
|
if (tangent_vel < 0.001f) continue;
|
|
|
|
// normalize by hand
|
|
tangent /= tangent_vel;
|
|
|
|
float tangent_eff_mass = calculate_effective_mass(tangent, contact_state);
|
|
|
|
// calculate the tangential impulse required to stop the tangential velocity
|
|
float tangent_impulse = tangent_vel * tangent_eff_mass;
|
|
|
|
// the frictional impulse is only allowed to be so big
|
|
float max_tangent_impulse = m_coeff_friction * contact_state.normal_impulse_magnitude;
|
|
if (tangent_impulse > max_tangent_impulse)
|
|
{
|
|
tangent_impulse = max_tangent_impulse;
|
|
}
|
|
|
|
// calculate the tangent impulse
|
|
Mth::Vector impulse = tangent;
|
|
impulse *= -tangent_impulse;
|
|
|
|
if (s_draw_skater_collision_circles)
|
|
{
|
|
if (contact.directed_friction)
|
|
{
|
|
Gfx::AddDebugLine(contact_state.p_world + m_pos + Mth::Vector(0.0f, 1.0f, 0.0f), contact_state.p_world + m_pos + 6.0f * impulse + Mth::Vector(0.0f, 1.0f, 0.0f), MAKE_RGB(255, 255, 0), MAKE_RGB(255, 255, 0), 1);
|
|
}
|
|
else
|
|
{
|
|
Gfx::AddDebugLine(contact_state.p_world + m_pos + Mth::Vector(0.0f, 1.0f, 0.0f), contact_state.p_world + m_pos + 6.0f * impulse + Mth::Vector(0.0f, 1.0f, 0.0f), MAKE_RGB(255, 0, 255), MAKE_RGB(255, 0, 255), 1);
|
|
}
|
|
}
|
|
|
|
// apply the tangent impulse to both the true velocity and the velocity we are using to determine the frictional forces
|
|
|
|
Mth::Vector delta_rotvel = m_mass_over_moment * Mth::CrossProduct(contact_state.p_world, impulse);
|
|
|
|
m_vel += impulse;
|
|
m_rotvel += delta_rotvel;
|
|
|
|
cache_vel += impulse;
|
|
cache_rotvel += delta_rotvel;
|
|
} // END loop over collision points
|
|
|
|
// apply penalty forces to prevent interpenetration
|
|
|
|
float time_step = Tmr::FrameLength();
|
|
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
SContactState& contact_state = sp_contact_states[n];
|
|
if (!contact_state.collision) continue;
|
|
|
|
Mth::Vector impulse = -m_spring_const * contact_state.depth * time_step * contact_state.normal;
|
|
|
|
Mth::Vector delta_rotvel = m_mass_over_moment * Mth::CrossProduct(contact_state.p_world, impulse);
|
|
|
|
m_vel += impulse;
|
|
m_rotvel += delta_rotvel;
|
|
|
|
if (s_draw_skater_collision_circles)
|
|
{
|
|
Gfx::AddDebugLine(contact_state.p_world + m_pos, contact_state.p_world + m_pos + 60.0f * impulse, MAKE_RGB(255, 255, 0), MAKE_RGB(255, 255, 255), 1);
|
|
}
|
|
}
|
|
|
|
// script callback
|
|
if (max_collision_velocity > m_sound_setup.bounce_velocity_callback_threshold)
|
|
{
|
|
if (m_script_names.bounce)
|
|
{
|
|
GetObject()->SpawnScriptPlease(m_script_names.bounce, mp_script_params)->Update();
|
|
}
|
|
|
|
#ifdef __NOPT_ASSERT__
|
|
if (m_sound_type_id && Script::GetInteger(CRCD(0xd634a297, "DynamicRigidbodySounds")))
|
|
{
|
|
get_sound_setup(m_sound_type_id);
|
|
}
|
|
#endif
|
|
|
|
// don't use up the last vRP_NUM_UNTOUCHABLE_VOICES voices
|
|
if (m_sound_setup.bounce_sound && NUM_VOICES - Sfx::CSfxManager::Instance()->GetNumSoundsPlaying() > vRP_NUM_UNTOUCHABLE_VOICES)
|
|
{
|
|
float percent = Mth::Clamp(100.0f * max_collision_velocity / m_sound_setup.bounce_velocity_full_speed, 0.0f, 100.0f);
|
|
m_sound_setup.bounce_sound->AddFloat(CRCD(0x9e497fc6, "Percent"), percent);
|
|
mp_sound_component->PlayScriptedSound(m_sound_setup.bounce_sound);
|
|
}
|
|
}
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::get_sound_setup ( uint32 sound_type_id )
|
|
{
|
|
|
|
Script::CStruct* p_base_struct;
|
|
p_base_struct = Script::GetStructure(CRCD(0xb9738eba, "RigidBodySounds"), Script::ASSERT);
|
|
|
|
Script::CStruct* p_sound_struct = NULL;
|
|
p_base_struct->GetStructure(sound_type_id, &p_sound_struct);
|
|
Dbg_MsgAssert(p_sound_struct, ("Sound type '%s' not found in RigidBodySounds", Script::FindChecksumName(sound_type_id)));
|
|
|
|
Script::CStruct* p_struct;
|
|
|
|
if (p_sound_struct->ContainsComponentNamed(CRCD(0xbf8e0ace, "CollideSound")))
|
|
{
|
|
p_sound_struct->GetStructure(CRCD(0xbf8e0ace, "CollideSound"), &p_struct, Script::ASSERT);
|
|
if (m_sound_setup.collide_sound)
|
|
{
|
|
delete m_sound_setup.collide_sound;
|
|
}
|
|
m_sound_setup.collide_sound = new Script::CStruct;
|
|
*m_sound_setup.collide_sound += *p_struct;
|
|
}
|
|
|
|
if (p_sound_struct->ContainsComponentNamed(CRCD(0x1fb2f60c, "BounceSound")))
|
|
{
|
|
p_sound_struct->GetStructure(CRCD(0x1fb2f60c, "BounceSound"), &p_struct, Script::ASSERT);
|
|
if (m_sound_setup.bounce_sound)
|
|
{
|
|
delete m_sound_setup.bounce_sound;
|
|
}
|
|
m_sound_setup.bounce_sound = new Script::CStruct;
|
|
*m_sound_setup.bounce_sound += *p_struct;
|
|
|
|
if (p_sound_struct->ContainsComponentNamed(CRCD(0x3818ee47, "CollideMuteDelay")))
|
|
{
|
|
int time;
|
|
p_sound_struct->GetInteger(CRCD(0x3818ee47, "CollideMuteDelay"), &time);
|
|
m_sound_setup.collide_mute_delay = static_cast< Tmr::Time >(time);
|
|
}
|
|
if (p_sound_struct->ContainsComponentNamed(CRCD(0xad67a34d, "GlobalCollideMuteDelay")))
|
|
{
|
|
int time;
|
|
p_sound_struct->GetInteger(CRCD(0xad67a34d, "GlobalCollideMuteDelay"), &time);
|
|
m_sound_setup.global_collide_mute_delay = static_cast< Tmr::Time >(time);
|
|
}
|
|
p_sound_struct->GetFloat(CRCD(0x4511ca8d, "BounceVelocityCallbackThreshold"), &m_sound_setup.bounce_velocity_callback_threshold);
|
|
p_sound_struct->GetFloat(CRCD(0x8fc6519f, "BounceVelocityFullSpeed"), &m_sound_setup.bounce_velocity_full_speed);
|
|
}
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::setup_contacts_from_array ( Script::CArray* pArray )
|
|
{
|
|
m_num_contacts = pArray->GetSize();
|
|
Dbg_MsgAssert(m_num_contacts <= vRP_MAX_NUM_CONTACTS, ("Number of contacts in rigidbody exceeds limit of %i contacts", vRP_MAX_NUM_CONTACTS));
|
|
|
|
if (mp_contacts)
|
|
{
|
|
delete [] mp_contacts;
|
|
}
|
|
mp_contacts = new SContact[m_num_contacts];
|
|
|
|
for (unsigned int n = 0; n < m_num_contacts; n++)
|
|
{
|
|
Script::CVector* p_v = pArray->GetVector(n);
|
|
mp_contacts[n].p[X] = p_v->mX;
|
|
mp_contacts[n].p[Y] = p_v->mY;
|
|
mp_contacts[n].p[Z] = p_v->mZ;
|
|
mp_contacts[n].p[W] = 0.0f;
|
|
mp_contacts[n].directed_friction = false;
|
|
}
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::setup_contacts_as_box ( const Mth::Vector& top_half_dimensions, const Mth::Vector& bottom_half_dimensions )
|
|
{
|
|
m_num_contacts = 8;
|
|
|
|
if (mp_contacts)
|
|
{
|
|
delete [] mp_contacts;
|
|
}
|
|
mp_contacts = new SContact[m_num_contacts];
|
|
|
|
int n = 0;
|
|
bool x = false;
|
|
do {
|
|
bool y = false;
|
|
do {
|
|
bool z = false;
|
|
do {
|
|
mp_contacts[n].p[X] = (x ? 1.0f : -1.0f) * (y ? top_half_dimensions[X] : bottom_half_dimensions[X]);
|
|
mp_contacts[n].p[Y] = (y ? top_half_dimensions[Y] : -bottom_half_dimensions[Y]);
|
|
mp_contacts[n].p[Z] = (z ? 1.0f : -1.0f) * (y ? top_half_dimensions[Z] : bottom_half_dimensions[Z]);
|
|
mp_contacts[n].p[W] = 1.0f;
|
|
mp_contacts[n].directed_friction = false;
|
|
n++;
|
|
z = !z;
|
|
} while (z);
|
|
y = !y;
|
|
} while (y);
|
|
x = !x;
|
|
} while (x);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::setup_contacts_as_pyramid ( float half_height, float half_depth )
|
|
{
|
|
m_num_contacts = 5;
|
|
|
|
if (mp_contacts)
|
|
{
|
|
delete [] mp_contacts;
|
|
}
|
|
mp_contacts = new SContact[m_num_contacts];
|
|
|
|
mp_contacts[0].p[X] = half_depth;
|
|
mp_contacts[0].p[Y] = -0.6f * half_height;
|
|
mp_contacts[0].p[Z] = half_depth;
|
|
mp_contacts[0].p[W] = 1.0f;
|
|
mp_contacts[0].directed_friction = false;
|
|
|
|
mp_contacts[1].p[X] = -half_depth;
|
|
mp_contacts[1].p[Y] = -0.6f * half_height;
|
|
mp_contacts[1].p[Z] = half_depth;
|
|
mp_contacts[1].p[W] = 1.0f;
|
|
mp_contacts[0].directed_friction = false;
|
|
|
|
mp_contacts[2].p[X] = -half_depth;
|
|
mp_contacts[2].p[Y] = -0.6f * half_height;
|
|
mp_contacts[2].p[Z] = -half_depth;
|
|
mp_contacts[2].p[W] = 1.0f;
|
|
mp_contacts[0].directed_friction = false;
|
|
|
|
mp_contacts[3].p[X] = half_depth;
|
|
mp_contacts[3].p[Y] = -0.6f * half_height;
|
|
mp_contacts[3].p[Z] = -half_depth;
|
|
mp_contacts[3].p[W] = 1.0f;
|
|
mp_contacts[0].directed_friction = false;
|
|
|
|
mp_contacts[4].p[X] = 0.0f;
|
|
mp_contacts[4].p[Y] = 1.6f * half_height;
|
|
mp_contacts[4].p[Z] = 0.0f;
|
|
mp_contacts[4].p[W] = 1.0f;
|
|
mp_contacts[0].directed_friction = false;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::setup_contacts_as_cylinder ( float top_radius, float bottom_radius, float half_height, int edges )
|
|
{
|
|
m_num_contacts = 2 * edges;
|
|
|
|
if (mp_contacts)
|
|
{
|
|
delete [] mp_contacts;
|
|
}
|
|
mp_contacts = new SContact[m_num_contacts];
|
|
|
|
int i = 0;
|
|
bool top = false;
|
|
do {
|
|
for (int n = edges; n--; )
|
|
{
|
|
mp_contacts[i].p[X] = (top ? top_radius : bottom_radius) * cosf(n * 2.0f * 3.1415f / edges);
|
|
mp_contacts[i].p[Y] = (top ? half_height : -half_height);
|
|
mp_contacts[i].p[Z] = (top ? top_radius : bottom_radius) * sinf(n * 2.0f * 3.1415f / edges);
|
|
mp_contacts[i].p[W] = 1.0f;
|
|
mp_contacts[i].directed_friction = false;
|
|
i++;
|
|
}
|
|
top = !top;
|
|
} while (top);
|
|
}
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::setup_contacts_as_triangle ( const Mth::Vector& half_dimensions )
|
|
{
|
|
m_num_contacts = 6;
|
|
|
|
if (mp_contacts)
|
|
{
|
|
delete [] mp_contacts;
|
|
}
|
|
mp_contacts = new SContact[m_num_contacts];
|
|
|
|
int i = 0;
|
|
bool left = false;
|
|
do {
|
|
mp_contacts[i].p[X] = half_dimensions[X];
|
|
mp_contacts[i].p[Y] = -half_dimensions[Y];
|
|
mp_contacts[i].p[Z] = (left ? -1.0f : 1.0f) * half_dimensions[Z];
|
|
mp_contacts[i].p[W] = 1.0f;
|
|
mp_contacts[i].directed_friction = false;
|
|
i++;
|
|
|
|
mp_contacts[i].p[X] = -half_dimensions[X];
|
|
mp_contacts[i].p[Y] = -half_dimensions[Y];
|
|
mp_contacts[i].p[Z] = (left ? -1.0f : 1.0f) * half_dimensions[Z];
|
|
mp_contacts[i].p[W] = 1.0f;
|
|
mp_contacts[i].directed_friction = false;
|
|
i++;
|
|
|
|
mp_contacts[i].p[X] = 0.0f;
|
|
mp_contacts[i].p[Y] = half_dimensions[Y];
|
|
mp_contacts[i].p[Z] = (left ? -1.0f : 1.0f) * half_dimensions[Z];
|
|
mp_contacts[i].p[W] = 1.0f;
|
|
mp_contacts[i].directed_friction = false;
|
|
i++;
|
|
|
|
left = !left;
|
|
} while (left);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CRigidBodyComponent::draw_debug_lines ( ) const
|
|
{
|
|
int color = 0;
|
|
bool collidable = false;
|
|
switch (m_state)
|
|
{
|
|
case ASLEEP:
|
|
color = MAKE_RGB(0, 0, 200);
|
|
collidable = true;
|
|
break;
|
|
|
|
case AWAKE:
|
|
if (m_ignore_skater_countdown > 0.0f)
|
|
{
|
|
color = MAKE_RGB(0, 200, 0);
|
|
}
|
|
else
|
|
{
|
|
color = MAKE_RGB(200, 0, 0);
|
|
collidable = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (collidable && s_draw_skater_collision_circles)
|
|
{
|
|
Gfx::AddDebugCircle(m_pos + Mth::Vector(0.0f, m_skater_collision_application_radius, 0.0f), 8, m_skater_collision_radius, MAKE_RGB(200, 200, 0), 1);
|
|
Gfx::AddDebugCircle(m_pos + Mth::Vector(0.0f, -m_skater_collision_application_radius, 0.0f), 8, m_skater_collision_radius, MAKE_RGB(200, 200, 0), 1);
|
|
}
|
|
|
|
if (s_draw_skater_collision_circles)
|
|
{
|
|
Gfx::AddDebugStar(m_pos, 36.0f, MAKE_RGB(200, 0, 200), 1);
|
|
}
|
|
|
|
// draw debug lines in less pretty yet general manner
|
|
for (int n = m_num_contacts; n--; )
|
|
{
|
|
for (int m = 0; m < n; m++)
|
|
{
|
|
Gfx::AddDebugLine(sp_contact_states[n].p_world + m_pos, sp_contact_states[m].p_world + m_pos, color, color, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|