//**************************************************************************** //* MODULE: Gel/Components //* FILENAME: VehicleComponent.cpp //* OWNER: Dan Nelson //* CREATION DATE: 1/31/3 //**************************************************************************** #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MESSAGE(a) { printf("M:%s:%i: %s\n", __FILE__ + 15, __LINE__, a); } #define DUMPI(a) { printf("D:%s:%i: " #a " = %i\n", __FILE__ + 15, __LINE__, a); } #define DUMPF(a) { printf("D:%s:%i: " #a " = %g\n", __FILE__ + 15, __LINE__, a); } #define DUMPE(a) { printf("D:%s:%i: " #a " = %e\n", __FILE__ + 15, __LINE__, a); } #define DUMPS(a) { printf("D:%s:%i: " #a " = %s\n", __FILE__ + 15, __LINE__, a); } #define DUMPP(a) { printf("D:%s:%i: " #a " = %p\n", __FILE__ + 15, __LINE__, a); } #define DUMPV(a) { printf("D:%s:%i: " #a " = %g, %g, %g\n", __FILE__ + 15, __LINE__, (a)[X], (a)[Y], (a)[Z]); } #define DUMP4(a) { printf("D:%s:%i: " #a " = %g, %g, %g, %g\n", __FILE__ + 15, __LINE__, (a)[X], (a)[Y], (a)[Z], (a)[W]); } #define DUMPM(a) { DUMP4(a[X]); DUMP4(a[Y]); DUMP4(a[Z]); DUMP4(a[W]); } #define MARK { printf("K:%s:%i: %s\n", __FILE__ + 15, __LINE__, __PRETTY_FUNCTION__); } #define PERIODIC(n) for (static int p__ = 0; (p__ = ++p__ % (n)) == 0; ) // THOUGHTS: // // - BUG: solve negative collision depth issue for rect collisions; must project collision normal into rect plane before calculating depth (i think) // - BUG: bad wipeout behavior; IDEAS: // - use only line feelers // - turn off graivty with > 2 contacts // - sleep like a rigidbody // - compare algorithm with rigidbody // - Triggers. // - Lock motion if very slow and there's no input. // - Vehicle camera needs to reports its old position to get sounds' pitch correctly. // // - two center of masses (collision at zero and suspension) is an issue; actual rotation is done around zero; what sort of poor behavior does this cause // when the car is on two wheels? what way is there to retain stable suspension behavior but use a common center of mass? perhaps simply damp rotational // impulses due to the suspension; this may help with the "vert, linear-to-angular energy" freak-out issue as well // - body-body collisions; treat as boxes // - prehaps reduce wheel friction when very near vertical to prevent wall riding // - states: ASLEEP, AWAKE (full brake; rigidbody), DRIVEN, DRONE (?) // - vertical side rectangle feelers // - wheel camber extracted from model // - side slippage when stopped on hills namespace Obj { CVehicleComponent::SCollisionPoint CVehicleComponent::sp_collision_points[4 * (Nx::MAX_NUM_2D_COLLISIONS_REPORTED + 1)]; /******************************************************************/ /* */ /* */ /******************************************************************/ CBaseComponent* CVehicleComponent::s_create() { return static_cast< CBaseComponent* >( new CVehicleComponent ); } /******************************************************************/ /* */ /* */ /******************************************************************/ CVehicleComponent::CVehicleComponent() : CBaseComponent() { SetType( CRC_VEHICLE ); mp_input_component = NULL; m_draw_debug_lines = 0; m_update_suspension_only = false; m_steering_display = 0.0f; m_skater_pos.Set(); mp_wheels = NULL; m_sound_setup_checksum = CRCD(0x1ca1ff20, "Default"); m_num_collision_points = 0; m_artificial_collision_timer = 0.0f; m_state = AWAKE; m_consider_sleeping_count = 0; } /******************************************************************/ /* */ /* */ /******************************************************************/ CVehicleComponent::~CVehicleComponent() { delete [] mp_wheels; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::InitFromStructure( Script::CStruct* pParams ) { // extract constant parameters from struct; parameters not included in the script are left unchanged // toggles through the debug line drawing states if (pParams->ContainsFlag(CRCD(0x935ab858, "debug"))) { m_draw_debug_lines = ++m_draw_debug_lines % 3; } else if (pParams->ContainsFlag(CRCD(0xe6b5fcd9, "full_debug"))) { m_draw_debug_lines = (m_draw_debug_lines + 2) % 3; } else if (pParams->ContainsFlag(CRCD(0x751da48b, "no_debug"))) { m_draw_debug_lines = 0; } // center of mass location (in) // the point around which the car rotates; a lower center of mass reduces roll tendency; the XZ location affects the // vehicle's behavior during jumps if (pParams->ContainsComponentNamed(CRCD(0x93346b8c, "suspension_center_of_mass"))) { float suspension_center_of_mass_offset; pParams->GetFloat(CRCD(0x93346b8c, "suspension_center_of_mass"), &suspension_center_of_mass_offset); m_suspension_center_of_mass.Set(0.0f, suspension_center_of_mass_offset, 0.0f); } // mass (lbs) // affects acceleration, suspension behavior, and resistance to drag if (pParams->ContainsComponentNamed(CRCD(0x93fca499, "mass"))) { pParams->GetFloat(CRCD(0x93fca499, "mass"), &m_inv_mass); m_inv_mass = vVP_GRAVITATIONAL_ACCELERATION / m_inv_mass; } // moment of inertia (in^2 lbs) // resistance to changes in rotation along the three axes; natural value is about the weight of the car times the // square of the average radius of the car along the axes in question // X: resistance to rolling // Y: resistance to turning // Z: resistance to pitcing during acceleration, braking, and jumps if (pParams->ContainsComponentNamed(CRCD(0x8c6473ec, "moment_of_inertia"))) { pParams->GetVector(CRCD(0x8c6473ec, "moment_of_inertia"), &m_inv_moment_body); m_inv_moment_body[X] = vVP_GRAVITATIONAL_ACCELERATION / m_inv_moment_body[X]; m_inv_moment_body[Y] = vVP_GRAVITATIONAL_ACCELERATION / m_inv_moment_body[Y]; m_inv_moment_body[Z] = vVP_GRAVITATIONAL_ACCELERATION / m_inv_moment_body[Z]; } // vehicle body's coefficient of restitution pParams->GetFloat(CRCD(0x79c3b862, "body_restitution"), &m_body_restitution); // vehicle body's coefficient of friction pParams->GetFloat(CRCD(0xf476273c, "body_friction"), &m_body_friction); // vehicle body's coefficient of friction pParams->GetFloat(CRCD(0x4a23a53c, "body_wipeout_friction"), &m_body_wipeout_friction); // vehicle body's penalty-method interpenetration-prevention spring constant pParams->GetFloat(CRCD(0x1dd4890, "body_spring"), &m_body_spring); // factor of normal impulse over which you can control via steering pParams->GetFloat(CRCD(0xb9f026ed, "collision_control"), &m_collision_control); // horizontal velocity cutoff below which no in-air slerping to face velocity occurs pParams->GetFloat(CRCD(0x8c14709e, "in_air_slerp_velocity_cutoff"), &m_in_air_slerp_vel_cutoff); // time over which in-air slerping lerps to full strength after takeoff pParams->GetFloat(CRCD(0x2c91e49b, "in_air_slerp_time_delay"), &m_in_air_slerp_time_delay); // in-air slerping strength pParams->GetFloat(CRCD(0x280f0e0b, "in_air_slerp_strength"), &m_in_air_slerp_strength); // special slerping below standard velocity threshold m_vert_correction = pParams->ContainsFlag(CRCD(0x5341806a, "vert_correction")); // maximum steering angle (degrees) if (pParams->ContainsComponentNamed(CRCD(0xc1e5abdd, "max_steering_angle"))) { pParams->GetFloat(CRCD(0xc1e5abdd, "max_steering_angle"), &m_max_steering_angle); m_max_steering_angle = DEGREES_TO_RADIANS(m_max_steering_angle); } // rotational damping parameters which prevent fishtailing and drift pParams->GetFloat(CRCD(0x386837db, "constant_rotational_damping"), &m_const_rotvel_damping); pParams->GetFloat(CRCD(0x469cf79a, "quadratic_rotational_damping"), &m_quad_rotvel_damping); // if the car can be exited via triangle m_exitable = pParams->ContainsFlag(CRCD(0xc2a136cc, "exitable")); // if the car has a handbrke m_no_handbrake = pParams->ContainsFlag(CRCD(0xad6c0a46, "no_handbrake")); // the vehicle's body's interaction with the environment is expressed as an array of rectangular colliders if (pParams->ContainsComponentNamed(CRCD(0xfc8c4ac6, "colliders"))) { Script::CArray* p_colliders_array; pParams->GetArray(CRCD(0xfc8c4ac6, "colliders"), &p_colliders_array, Script::ASSERT); float average_distance = 0.0f; int num_colliders = p_colliders_array->GetSize(); // Dbg_MsgAssert(num_colliders == vVP_NUM_COLLIDERS, ("Number of colliders for CVehicleComponent is incorrect")); for (int collider_idx = num_colliders; collider_idx--; ) { SCollider& collider = mp_colliders[collider_idx]; Script::CArray* p_corner_array = p_colliders_array->GetArray(collider_idx); Dbg_MsgAssert(p_corner_array->GetSize() == 3, ("Incorrect number of corner vectors in collider array")); Script::CVector* p_corner_vector; p_corner_vector = p_corner_array->GetVector(0); collider.body.m_corner[X] = p_corner_vector->mX; collider.body.m_corner[Y] = p_corner_vector->mY; collider.body.m_corner[Z] = p_corner_vector->mZ; p_corner_vector = p_corner_array->GetVector(1); collider.body.m_first_edge[X] = p_corner_vector->mX; collider.body.m_first_edge[Y] = p_corner_vector->mY; collider.body.m_first_edge[Z] = p_corner_vector->mZ; collider.body.m_first_edge -= collider.body.m_corner; p_corner_vector = p_corner_array->GetVector(2); collider.body.m_second_edge[X] = p_corner_vector->mX; collider.body.m_second_edge[Y] = p_corner_vector->mY; collider.body.m_second_edge[Z] = p_corner_vector->mZ; collider.body.m_second_edge -= collider.body.m_corner; // update maximum_distance average_distance += collider.body.m_corner.Length(); average_distance += (collider.body.m_corner + collider.body.m_first_edge).Length(); average_distance += (collider.body.m_corner + collider.body.m_second_edge).Length(); // calculate the precomputable values collider.first_edge_length = collider.body.m_first_edge.Length(); collider.second_edge_length = collider.body.m_second_edge.Length(); Dbg_MsgAssert(collider.first_edge_length > 0.0f, ("Collider of zero area")); Dbg_MsgAssert(collider.second_edge_length > 0.0f, ("Collider of zero area")); } // set the skater's rigid body collision radius while in the car Mdl::Skate::Instance()->GetLocalSkater()->SetRigidBodyCollisionRadiusBoost(average_distance / 3.0f / num_colliders - 48.0f); } if (pParams->ContainsComponentNamed(CRCD(0x1757e572, "engine"))) { Script::CStruct* p_engine_struct; pParams->GetStructure(CRCD(0x1757e572, "engine"), &p_engine_struct, Script::ASSERT); // base drive torque of the engine; multiplied by the gear and differential ratio (ft-lbs) if (p_engine_struct->ContainsComponentNamed(CRCD(0x9aa9faee, "drive_torque"))) { p_engine_struct->GetFloat(CRCD(0x9aa9faee, "drive_torque"), &m_engine.drive_torque); m_engine.drive_torque *= 12.0f; } // base drag torque of the engine; multiplied by the square of the gear ratio (ft-lbs) if (p_engine_struct->ContainsComponentNamed(CRCD(0xb2268648, "drag_torque"))) { p_engine_struct->GetFloat(CRCD(0xb2268648, "drag_torque"), &m_engine.drag_torque); m_engine.drag_torque *= 12.0f; } // when rpm reaches this speed, the transmition upshifts (rpm) if (p_engine_struct->ContainsComponentNamed(CRCD(0x33921583, "upshift_rpm"))) { p_engine_struct->GetFloat(CRCD(0x33921583, "upshift_rpm"), &m_engine.upshift_rotvel); m_engine.upshift_rotvel = RPM_TO_RADIANS_PER_SECOND(m_engine.upshift_rotvel); } // base gear ratio; torque and rpm are multiplied by this when translated up and down the drive train p_engine_struct->GetFloat(CRCD(0x94ea6601, "differential_ratio"), &m_engine.differential_ratio); // instead of having a true reverse gear, all gears are scaled down by this ratio when in reverse p_engine_struct->GetFloat(CRCD(0x82bcbb10, "reverse_torque_ratio"), &m_engine.reverse_torque_ratio); // gear ratios; torque and rpm are multiplied by these when translated up and down the drive train if (p_engine_struct->ContainsComponentNamed(CRCD(0xc023fa75, "gear_ratios"))) { Script::CArray* p_engine_gear_ratios_array; p_engine_struct->GetArray(CRCD(0xc023fa75, "gear_ratios"), &p_engine_gear_ratios_array, Script::ASSERT); m_engine.num_gears = p_engine_gear_ratios_array->GetSize(); Dbg_MsgAssert(m_engine.num_gears <= vVP_MAX_NUM_GEARS, ("Number of gears exceeds the maximum allowed number")); for (int n = 0; n < m_engine.num_gears; n++) { m_engine.p_gear_ratios[n] = p_engine_gear_ratios_array->GetFloat(n); if (n == 0) continue; Dbg_MsgAssert(n == 0 || m_engine.p_gear_ratios[n - 1] > m_engine.p_gear_ratios[n], ("Low gear has lower gear ratio than high gear")); } } } // you can setup all wheels at once or each individually // an array of wheel structures if (pParams->ContainsComponentNamed(CRCD(0xb3f8557e, "wheels"))) { Script::CArray* p_wheels_array = NULL; pParams->GetArray(CRCD(0xb3f8557e, "wheels"), &p_wheels_array, Script::ASSERT); Dbg_MsgAssert(!mp_wheels || m_num_wheels == p_wheels_array->GetSize(), ("Changed number of wheels")); m_num_wheels = p_wheels_array->GetSize(); if (!mp_wheels) { mp_wheels = new SWheel[m_num_wheels]; } for (int n = m_num_wheels; n--; ) { Script::CStruct* p_wheel_struct = NULL; p_wheel_struct = p_wheels_array->GetStructure(n); // see update_wheel_from_structure() for documentation on wheel parameters update_wheel_from_structure(mp_wheels[n], p_wheel_struct); } // END loop over wheels } // END if wheel array specified Dbg_MsgAssert(mp_wheels, ("Wheels not created")); // a structure of wheel parameters which acts on all wheels if (pParams->ContainsComponentNamed(CRCD(0x371badff, "all_wheels"))) { Script::CStruct* p_wheel_struct; pParams->GetStructure(CRCD(0x371badff, "all_wheels"), &p_wheel_struct, Script::ASSERT); for (int n = m_num_wheels; n--; ) { // see update_wheel_from_structure() for documentation on wheel parameters update_wheel_from_structure(mp_wheels[n], p_wheel_struct); } // END loop over wheels } // END if all_wheel structure specified // calculate dependent constant characteristics // count the number of drive wheels m_num_drive_wheels = 0; for (int n = m_num_wheels; n--; ) { if (mp_wheels[n].drive) { m_num_drive_wheels++; } } // setup position and orientation based on the object's state m_orientation = GetObject()->GetMatrix(); m_orientation.Normalize(); if (pParams->ContainsComponentNamed(CRCD(0xaa99c521, "save"))) { Mth::Vector orientation_vector; orientation_vector.Set(); pParams->GetVector(CRCD(0xc97f3aa9, "orientation"), &orientation_vector); m_orientation.SetVector(orientation_vector); m_orientation.Normalize(); m_orientation.GetMatrix(m_orientation_matrix); m_orientation_matrix[W].Set(); GetObject()->SetMatrix(m_orientation_matrix); GetObject()->SetDisplayMatrix(m_orientation_matrix); } m_pos = GetObject()->GetPos(); if (pParams->ContainsComponentNamed(CRCD(0x7f261953, "pos"))) { pParams->GetVector(CRCD(0x7f261953, "pos"), &m_pos); GetObject()->SetPos(m_pos); } m_skater_visible = m_skater_visible || pParams->ContainsFlag(CRCD(0x2ed67657, "make_skater_visible")); if (pParams->ContainsFlag(CRCD(0x2ed67657, "make_skater_visible"))) { pParams->GetVector(CRCD(0xec86ef7a, "skater_pos"), &m_skater_pos, Script::ASSERT); pParams->GetChecksum(CRCD(0xda75a33e, "skater_anim"), &m_skater_anim, Script::ASSERT); } pParams->GetChecksum(CRCD(0xedcf90e, "Sounds"), &m_sound_setup_checksum); // zero velocities and accumulators m_mom.Set(0.0f, 0.0f, 0.0f); m_rotmom.Set(0.0f, 0.0f, 0.0f); m_force.Set(0.0f, 0.0f, 0.0f); m_torque.Set(0.0f, 0.0f, 0.0f); for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; wheel.state = SWheel::OUT_OF_CONTACT; wheel.orientation = 0.0f; wheel.rotvel = 0.0f; wheel.y_offset = wheel.y_offset_hang; wheel.steering_angle = 0.0f; wheel.steering_angle_display = 0.0f; wheel.rotacc = 0.0f; // set normal force history to their default values for (int i = vVP_NORMAL_FORCE_HISTORY_LENGTH; i--; ) { wheel.normal_force_history[i] = vVP_GRAVITATIONAL_ACCELERATION / m_inv_mass / m_num_wheels; } } m_next_normal_force_history_idx = 0; m_gravity_override_timer = 0.0f; m_gravity_override_fraction = 1.0f; m_in_flip = false; // grab a pointer to the vehicle's skeleton mp_skeleton_component = static_cast< CSkeletonComponent* >(GetObject()->GetComponent(CRC_SKELETON)); Dbg_MsgAssert(mp_skeleton_component, ("Vehicle component has no peer skeleton component.")); Dbg_MsgAssert(mp_skeleton_component->GetSkeleton()->GetNumBones() == static_cast< int >(2 + m_num_wheels), ("Vehicle component's peer skeleton component has an unexpected number of bones")); } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::RefreshFromStructure( Script::CStruct* pParams ) { Dbg_Assert(false); } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::Finalize ( ) { mp_input_component = GetInputComponentFromObject(GetObject()); mp_model_component = GetModelComponentFromObject(GetObject()); Dbg_Assert(mp_input_component); Dbg_Assert(mp_model_component); // extract information about the car from the model Dbg_MsgAssert(m_num_wheels == vVP_NUM_WHEELS, ("Number of wheels must equal CVehicleComponent::vVP_NUM_WHEELS")); CModelComponent* p_model_component = static_cast< CModelComponent* >(GetModelComponentFromObject(GetObject())); Dbg_Assert(p_model_component); Nx::CModel* p_model = p_model_component->GetModel(); Dbg_Assert(p_model); Nx::CHierarchyObject* p_hierarchy_objects = p_model->GetHierarchy(); Dbg_Assert(p_hierarchy_objects); for (int n = vVP_NUM_WHEELS; n--; ) { SWheel& wheel = mp_wheels[n]; Mth::Matrix wheel_matrix = (p_hierarchy_objects + 2 + n)->GetSetupMatrix(); // rotate out of max coordinate system wheel.pos[X] = -wheel_matrix[W][X]; wheel.pos[Y] = wheel_matrix[W][Z]; wheel.pos[Z] = -wheel_matrix[W][Y]; wheel.pos[W] = 1.0f; } Mth::Matrix body_matrix = (p_hierarchy_objects + 1)->GetSetupMatrix(); m_body_pos[X] = -body_matrix[W][X]; m_body_pos[Y] = body_matrix[W][Z]; m_body_pos[Z] = -body_matrix[W][Y]; m_body_pos[W] = 1.0f; // extract axle and wheelbase information int left_steering_tire = -1; int right_steering_tire = -1; int rear_tire = -1; for (int n = vVP_NUM_WHEELS; n--; ) { switch (mp_wheels[n].steering) { case SWheel::LEFT: left_steering_tire = n; break; case SWheel::RIGHT: right_steering_tire = n; break; case SWheel::FIXED: rear_tire = n; break; } } Dbg_Assert(left_steering_tire != -1); Dbg_Assert(right_steering_tire != -1); Dbg_Assert(rear_tire != -1); m_cornering_wheelbase = Mth::Abs( (p_hierarchy_objects + 2 + left_steering_tire)->GetSetupMatrix()[W][Y] - (p_hierarchy_objects + 2 + rear_tire)->GetSetupMatrix()[W][Y] ); m_cornering_axle_length = Mth::Abs( (p_hierarchy_objects + 2 + left_steering_tire)->GetSetupMatrix()[W][X] - (p_hierarchy_objects + 2 + right_steering_tire)->GetSetupMatrix()[W][X] ); // calculate the true wheel positions based on the desired wheel positions with vehicle weight applied for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; // place the wheel in the desired location based on the vehicle mass and the suspension spring rate; the suspension feeler starts at two // radii above the desired position float desired_y_pos = wheel.pos[Y]; wheel.pos[Y] = desired_y_pos + 2.0f * wheel.radius; wheel.y_offset_hang = -wheel.pos[Y] + desired_y_pos - vVP_GRAVITATIONAL_ACCELERATION / (m_inv_mass * m_num_wheels * wheel.spring); } // determine the lowest collider point float lowest_collider_height = mp_colliders[0].body.m_corner[Y]; for (int n = vVP_NUM_COLLIDERS; n--; ) { lowest_collider_height = Mth::Min(mp_colliders[n].body.m_corner[Y], lowest_collider_height); lowest_collider_height = Mth::Min(mp_colliders[n].body.m_corner[Y] + mp_colliders[n].body.m_first_edge[Y], lowest_collider_height); lowest_collider_height = Mth::Min(mp_colliders[n].body.m_corner[Y] + mp_colliders[n].body.m_second_edge[Y], lowest_collider_height); } // ready the skater for control mp_skater = Mdl::Skate::Instance()->GetLocalSkater(); mp_skater_core_physics_component = GetSkaterCorePhysicsComponentFromObject(mp_skater); mp_skater_trigger_component = GetTriggerComponentFromObject(mp_skater); Dbg_Assert(mp_skater_core_physics_component); Dbg_Assert(mp_skater_trigger_component); if (!m_skater_visible) { mp_skater->Hide(true); } else { mp_skater_animation_component = GetAnimationComponentFromObject(mp_skater); Dbg_Assert(mp_skater_animation_component); } // calculate the center of mass we will use based on the wheel locations Mth::Vector center_of_mass(0.0f, 0.0f, 0.0f, 0.0f); for (int n = m_num_wheels; n--; ) { center_of_mass += mp_wheels[n].pos; } center_of_mass /= m_num_wheels; center_of_mass[Y] = lowest_collider_height; center_of_mass[W] = 0.0f; // move wheels and colliders so that they are relative to the correct center of mass for (int n = m_num_wheels; n--; ) { mp_wheels[n].pos -= center_of_mass; } for (int n = vVP_NUM_COLLIDERS; n--; ) { mp_colliders[n].body.m_corner -= center_of_mass; } m_body_pos -= center_of_mass; m_skater_pos -= center_of_mass; update_dependent_variables(); GetObject()->SetPos(m_pos); GetObject()->SetVel(m_vel); GetObject()->SetMatrix(m_orientation_matrix); GetObject()->SetDisplayMatrix(GetObject()->GetMatrix()); update_skeleton(); } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::Update() { m_reset_this_frame = false; if (!m_update_suspension_only) { get_input(); } else { zero_input(); } if (m_state == ASLEEP) { // reduced work set if we're asleep GetObject()->SetDisplayMatrix(GetObject()->GetMatrix()); update_steering_angles(); if (m_controls.brake) { for (int n = m_num_wheels; n--; ) { mp_wheels[n].rotvel = 0.0f; } } update_wheel_dynamic_state(); update_skeleton(); draw_shadow(); control_skater(); return; } int num_time_steps; float frame_length = Tmr::FrameLength(); if (in_artificial_collision()) { m_artificial_collision_timer -= frame_length; } // count down timers if (m_gravity_override_timer != 0.0f) { m_gravity_override_timer -= frame_length; if (m_gravity_override_timer <= 0.0f || m_num_wheels_in_contact > 1) { MESSAGE("Ending vehicle gravity override"); m_gravity_override_timer = 0.0f; m_gravity_override_fraction = 1.0f; } } // the physics is unstable at low frame rates, so we take multiple physics steps during long frame; if vehicle physics is a significant fraction // of CPU time, this could exacerbate whatever frame rate problems are occuring if (frame_length >= (1.0f / 30.0f)) { num_time_steps = static_cast< int >(ceilf(frame_length / (1.0f / 60.0f))); if (num_time_steps > 6) { num_time_steps = 6; } m_time_step = frame_length / num_time_steps; Dbg_Message("CVehicleComponent::Update: using %i steps this frame", num_time_steps); } else { num_time_steps = 1; m_time_step = frame_length; } for (int step = num_time_steps; step--; ) { update_dynamic_state(); m_force.Set(0.0f, 0.0f, 0.0f); m_torque.Set(0.0f, 0.0f, 0.0f); for (int n = m_num_wheels; n--; ) { mp_wheels[n].rotacc = 0.0f; } if (!m_update_suspension_only) { apply_environment_collisions(); if (reset_this_frame()) return; } // teleport wheels to the ground, if within reach of the suspension update_wheel_heights(); if (reset_this_frame()) return; // update the steering wheels' angles update_steering_angles(); // damp out rotations to create a more drivable car damp_rotation(); // accumulate all forces and torques on the body and wheels accumulate_forces(); ///////////// // update state observers if (m_num_wheels_in_contact > 0) { m_air_time = 0.0f; } else { m_air_time += m_time_step; } if (m_num_wheels_in_contact > 0 || m_max_normal_collision_impulse > 0.0f) { m_air_time_no_collision = 0.0f; } else { m_air_time_no_collision += m_time_step; } } if (m_update_suspension_only) return; // draw debug lines draw_debug_rendering(); // update object's position and orientation GetObject()->SetPos(m_pos); GetObject()->SetVel(m_vel); GetObject()->SetMatrix(m_orientation_matrix); GetObject()->SetDisplayMatrix(GetObject()->GetMatrix()); consider_sleeping(); update_skeleton(); // Hack to draw shadow draw_shadow(); #if 0 Mth::Vector forward(0.0f, 0.0f, 1.0f); forward = m_orientation_matrix.Rotate(forward); float vel = IPS_TO_MPH(Mth::Abs(Mth::DotProduct(m_vel, forward))); PERIODIC(60) DUMPF(vel); #endif // HACK: get player proximity checks, triggers, driving animations, and the like working control_skater(); } /******************************************************************/ /* */ /* */ /******************************************************************/ CBaseComponent::EMemberFunctionResult CVehicleComponent::CallMemberFunction( uint32 Checksum, Script::CStruct* pParams, Script::CScript* pScript ) { switch ( Checksum ) { // @script | Vehicle_Kick | kicks the vehicle with a force and torque case CRCC(0x93b713a6, "Vehicle_Kick"): { Mth::Vector linear; Mth::Vector angular; m_state = AWAKE; m_consider_sleeping_count = 0; pParams->GetVector("linear", &linear); pParams->GetVector("angular", &angular); linear = m_orientation_matrix.Rotate(linear); angular = m_orientation_matrix.Rotate(angular); m_mom += linear / m_inv_mass; m_rotmom += angular / m_inv_moment_body[X]; update_velocities(); break; } // @script | Vehicle_Wake | wakes the vehicle if asleep case CRCC(0xa80a0d36, "Vehicle_Wake"): m_state = AWAKE; m_consider_sleeping_count = 0; break; // @script | Vehicle_MoveToRestart | teleport the vehicle to the restart node case CRCC(0x4b0b27dd, "Vehicle_MoveToRestart"): { uint32 node_name_checksum; if (pParams->GetChecksum(NO_NAME, &node_name_checksum)) { // a node is specifically specified MoveToNode(SkateScript::GetNode(SkateScript::FindNamedNode(node_name_checksum, Script::ASSERT))); } else { // find a linked restart node int node = pScript->mNode; Dbg_MsgAssert(node != -1,( "Vehicle_MoveToRestart called from non-node script with no target node specified")); { int num_links = SkateScript::GetNumLinks(node); for (int n = 0; n < num_links; n++) { int linked_node = SkateScript::GetLink(node, n); if (IsRestart(linked_node)) { MoveToNode(SkateScript::GetNode(linked_node)); return CBaseComponent::MF_TRUE; } } if (Ed::CParkEditor::Instance()->UsingCustomPark()) { MoveToNode(SkateScript::GetNode(Mdl::Skate::Instance()->find_restart_node(0))); } else { Dbg_MsgAssert(0, ("Vehicle_MoveToRestart called but node %d not linked to restart", node)); } } } break; } // @script | Vehicle_PlaceBeforeCamera | moves the object before the active camera case CRCC(0xc33608e4, "Vehicle_PlaceBeforeCamera"): { Gfx::Camera* camera = Nx::CViewportManager::sGetActiveCamera(0); if (camera) { Mth::Vector& cam_pos = camera->GetPos(); Mth::Matrix& cam_mat = camera->GetMatrix(); m_pos = cam_pos; m_pos += cam_mat[Y] * 12.0f * 12.0f; m_pos -= cam_mat[Z] * 12.0f * 12.0f; GetObject()->SetPos(m_pos); m_orientation_matrix[X] = -cam_mat[X]; m_orientation_matrix[Y] = cam_mat[Y]; m_orientation_matrix[Z] = -cam_mat[Z]; m_orientation = m_orientation_matrix; m_orientation.Normalize(); m_orientation.GetMatrix(m_orientation_matrix); m_orientation_matrix[W].Set(); GetObject()->SetMatrix(m_orientation_matrix); update_dependent_variables(); } break; } // @script | Vehicle_AdjustGravity | adjusts effective gravity for a given duration or until the vehicle has two or more wheels on the ground, // whichever occurs first // @parm float | Percent | Percent of standard gravity. // @parm float | Duration | Duration in seconds over which to override gravity. case CRCC(0xdb35aad8, "Vehicle_AdjustGravity"): pParams->GetFloat(CRCD(0x9e497fc6, "Percent"), &m_gravity_override_fraction, Script::ASSERT); m_gravity_override_fraction *= 1.0f / 100.0f; pParams->GetFloat(CRCD(0x79a07f3f, "Duration"), &m_gravity_override_timer, Script::ASSERT); Dbg_MsgAssert(m_gravity_override_timer > 0.0f, ("Vehicle_AdjustGravity must have positive Duration")); MESSAGE("Initiating vehicle gravity override"); break; // @script | Vehicle_ForceBrake | forces on the brake // case CRCC(0x1ad6b6bc, "Vehicle_ForceBrake"): // m_force_brake = true; // break; // @script | Vehicle_HandbrakeActive | returns true if the car has a handbrake case CRCC(0x5008b253, "Vehicle_HandbrakeActive"): return m_force_brake ? CBaseComponent::MF_TRUE : CBaseComponent::MF_FALSE; // @script | Vehicle_AllWheelsAreInContact | returns true if all of the wheels are in contact with geo case CRCC(0x279602ed, "Vehicle_AllWheelsAreInContact"): return m_num_wheels_in_contact == m_num_wheels ? CBaseComponent::MF_TRUE : CBaseComponent::MF_FALSE; // @script | Vehicle_LostCollision | lost a net collision; respond appropriately case CRCC(0x4c73526b, "Vehicle_LostCollision"): { Mth::Vector offset; pParams->GetVector(CRCD(0xa6f5352f, "Offset"), &offset, Script::ASSERT); ApplyArtificialCollision(offset); break; } // @script | Vehicle_IsSkaterVisible | true if skater should be visible while driving case CRCC(0x81faac21, "Vehicle_IsSkaterVisible"): return m_skater_visible ? CBaseComponent::MF_TRUE : CBaseComponent::MF_FALSE; default: return CBaseComponent::MF_NOT_EXECUTED; } return CBaseComponent::MF_TRUE; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::GetDebugInfo ( Script::CStruct *p_info ) { #ifdef __DEBUG_CODE__ Dbg_MsgAssert(p_info, ("NULL p_info sent to CVehicleComponent::GetDebugInfo")); p_info->AddVector("m_pos", m_pos); p_info->AddVector("m_mom", m_mom); p_info->AddVector("m_rotmom", m_rotmom); p_info->AddFloat("m_orientation", m_orientation.GetScalar()); p_info->AddVector("m_orientation", m_orientation.GetVector()); p_info->AddVector("m_force", m_force); p_info->AddVector("m_torque", m_torque); p_info->AddVector("m_vel", m_vel); p_info->AddVector("m_rotvel", m_rotvel); p_info->AddVector("m_suspension_center_of_mass", m_suspension_center_of_mass); p_info->AddFloat("mass", vVP_GRAVITATIONAL_ACCELERATION / m_inv_mass); p_info->AddVector("moment_of_inertia", vVP_GRAVITATIONAL_ACCELERATION / m_inv_moment_body[X], vVP_GRAVITATIONAL_ACCELERATION / m_inv_moment_body[Y], vVP_GRAVITATIONAL_ACCELERATION / m_inv_moment_body[Z] ); p_info->AddFloat("body_restitution", m_body_restitution); p_info->AddFloat("body_friction", m_body_friction); p_info->AddFloat("body_spring", m_body_spring); p_info->AddFloat("collision_control", m_collision_control); p_info->AddFloat("in_air_slerp_velocity_cutoff", m_in_air_slerp_vel_cutoff); p_info->AddFloat("in_air_slerp_time_delay", m_in_air_slerp_time_delay); p_info->AddFloat("in_air_slerp_strength", m_in_air_slerp_strength); p_info->AddChecksum("vert_correction", m_vert_correction ? CRCD(0x203b372, "true") : CRCD(0xd43297cf, "false")); p_info->AddFloat("wipeout_body_friction", m_body_wipeout_friction); p_info->AddFloat("max_steering_angle", RADIANS_TO_DEGREES(m_max_steering_angle)); p_info->AddFloat("cornering_wheelbase", m_cornering_wheelbase); p_info->AddFloat("cornering_axle_length", m_cornering_axle_length); p_info->AddFloat("constant_rotational_damping", m_const_rotvel_damping); p_info->AddFloat("quadratic_rotational_damping", m_quad_rotvel_damping); Script::CStruct* p_engine_info = new Script::CStruct; p_engine_info->AddFloat("drive_torque", m_engine.drive_torque / 12.0f); p_engine_info->AddFloat("drag_torque", m_engine.drag_torque / 12.0f); p_engine_info->AddFloat("upshift_rpm", RADIANS_PER_SECOND_TO_RPM(m_engine.upshift_rotvel)); p_engine_info->AddFloat("differential_ratio", m_engine.differential_ratio); p_engine_info->AddFloat("reverse_torque_ratio", m_engine.reverse_torque_ratio); Script::CArray* p_engine_gear_ratio_info = new Script::CArray; p_engine_gear_ratio_info->SetSizeAndType(m_engine.num_gears, ESYMBOLTYPE_FLOAT); for (int n = m_engine.num_gears; n--; ) { p_engine_gear_ratio_info->SetFloat(n, m_engine.p_gear_ratios[n]); } p_engine_info->AddArrayPointer("gear_ratios", p_engine_gear_ratio_info); p_info->AddStructurePointer("m_engine", p_engine_info); Script::CArray* p_wheels_info = new Script::CArray; p_wheels_info->SetSizeAndType(m_num_wheels, ESYMBOLTYPE_STRUCTURE); for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; Script::CStruct* p_local_info = new Script::CStruct; uint32 state_checksums[] = { CRCC(0xba2b31d7, "NO_STATE"), CRCC(0xcd419ee6, "OUT_OF_CONTACT"), CRCC(0x927c0fed, "UNDER_GRIPPING"), CRCC(0xe9bc148a, "GRIPPING"), CRCC(0x26acccc8, "SLIPPING"), CRCC(0xa91003cc, "SKIDDING"), CRCC(0x2e7ef449, "HANDBRAKE_THROTTLE"), CRCC(0xbf6d6529, "HANDBRAKE_LOCKED"), }; uint32 steering_checksums[] = { CRCC(0x613631cd, "FIXED"), CRCC(0x85981897, "LEFT"), CRCC(0x4b358aeb, "RIGHT") }; p_local_info->AddChecksum("state", state_checksums[wheel.state]); p_local_info->AddFloat("rotvel", wheel.rotvel); p_local_info->AddFloat("y_offset", wheel.y_offset); p_local_info->AddFloat("steering_angle", RADIANS_TO_DEGREES(wheel.steering_angle)); p_local_info->AddFloat("steering_angle_display", RADIANS_TO_DEGREES(wheel.steering_angle_display)); p_local_info->AddFloat("rotacc", wheel.rotacc); p_local_info->AddFloat("orientation", wheel.orientation); p_local_info->AddVector("pos", wheel.pos); p_local_info->AddFloat("y_offset_hang", wheel.y_offset_hang); p_local_info->AddFloat("max_draw_y_offset", wheel.max_draw_y_offset); p_local_info->AddChecksum("steering", steering_checksums[wheel.steering]); p_local_info->AddInteger("drive", wheel.drive); p_local_info->AddFloat("radius", wheel.radius); p_local_info->AddFloat("moment", vVP_GRAVITATIONAL_ACCELERATION / wheel.inv_moment); p_local_info->AddFloat("spring_rate", wheel.spring); p_local_info->AddFloat("damping_rate", wheel.damping); p_local_info->AddFloat("static_friction", wheel.static_friction); p_local_info->AddFloat("dynamic_friction", wheel.dynamic_friction); p_local_info->AddFloat("handbrake_throttle_friction", wheel.handbrake_throttle_friction); p_local_info->AddFloat("handbrake_locked_friction", wheel.handbrake_locked_friction); p_local_info->AddFloat("min_static_grip_velocity", IPS_TO_MPH(wheel.min_static_velocity)); p_local_info->AddFloat("max_static_grip_velocity", IPS_TO_MPH(wheel.max_static_velocity)); p_local_info->AddFloat("min_dynamic_grip_velocity", IPS_TO_MPH(wheel.min_dynamic_velocity)); p_local_info->AddFloat("brake_torque", wheel.brake.torque / 12.0f); p_local_info->AddFloat("handbrake_torque", wheel.brake.handbrake_torque / 12.0f); p_wheels_info->SetStructure(n, p_local_info); } p_info->AddArrayPointer("mp_wheels", p_wheels_info); Script::CStruct* p_controls_info = new Script::CStruct; p_controls_info->AddFloat("steering", m_controls.steering); p_controls_info->AddInteger("throttle", m_controls.throttle); p_controls_info->AddInteger("brake", m_controls.brake); p_controls_info->AddInteger("handbrake", m_controls.handbrake); p_controls_info->AddInteger("reverse", m_controls.reverse); p_info->AddStructurePointer("m_controls", p_controls_info); if (m_skater_visible) { p_info->AddVector(CRCD(0xec86ef7a, "skater_pos"), m_skater_pos); p_info->AddChecksum(CRCD(0xda75a33e, "skater_anim"), m_skater_anim); } // we call the base component's GetDebugInfo, so we can add info from the common base component CBaseComponent::GetDebugInfo(p_info); #endif } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::MoveToNode ( Script::CStruct* p_node ) { // move to the position relative to the given node that a ped car would be at Mth::Vector restart_pos; SkateScript::GetPosition(p_node, &restart_pos); Mth::Vector restart_angles; SkateScript::GetAngles(p_node, &restart_angles); Mth::Matrix restart_matrix; restart_matrix.SetFromAngles(restart_angles); // calculate appropriate offset from the ground based on estimated wheel offsets float avg_ground_offset = 0.0f; for (int n = vVP_NUM_WHEELS; n--; ) { avg_ground_offset += mp_wheels[n].y_offset_hang + mp_wheels[n].pos[Y] + vVP_GRAVITATIONAL_ACCELERATION / (m_inv_mass * m_num_wheels * mp_wheels[n].spring); avg_ground_offset -= mp_wheels[n].radius; } avg_ground_offset /= vVP_NUM_WHEELS; // find ground height CFeeler feeler(restart_pos + Mth::Vector(0.0f, 24.0f, 0.0f), restart_pos + Mth::Vector(0.0f, -240.0f, 0.0f)); if (feeler.GetCollision(false)) { restart_pos[Y] = feeler.GetPoint()[Y] - avg_ground_offset; } // ped cars have their origin between the rear wheels int rear_wheel_idx = -1; for (int n = vVP_NUM_WHEELS; n--; ) { if (mp_wheels[n].steering == SWheel::FIXED) { rear_wheel_idx = n; break; } } Dbg_Assert(rear_wheel_idx != -1); restart_pos -= restart_matrix[Z] * mp_wheels[rear_wheel_idx].pos[Z]; // move the car to the restart position and allow it to settle on its suspension m_pos = restart_pos; m_orientation = restart_matrix; m_mom.Set(0.0f, 0.0f, 0.0f); m_rotmom.Set(0.0f, 0.0f, 0.0f); // zero wheels m_update_suspension_only = true; for (int n = 60; n--; ) { // lock wheels each frame for (int n = m_num_wheels; n--; ) { mp_wheels[n].rotvel = 0.0f; } Update(); } m_update_suspension_only = false; m_mom.Set(0.0f, 0.0f, 0.0f); m_rotmom.Set(0.0f, 0.0f, 0.0f); m_reset_this_frame = true; m_state = ASLEEP; // update object's position and orientation GetObject()->SetPos(m_pos); GetObject()->SetVel(m_vel); GetObject()->SetMatrix(m_orientation_matrix); GetObject()->SetDisplayMatrix(GetObject()->GetMatrix()); update_skeleton(); control_skater(); GetObject()->SetTeleported(); mp_skater->SetTeleported(); } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::ApplyArtificialCollision ( const Mth::Vector& offset ) { Mth::Vector impulse_direction = offset; impulse_direction[Y] = 0.0f; impulse_direction.Normalize(); Mth::Vector sideways(-impulse_direction[Z], 0.0f, impulse_direction[X]); float impulse_forward = (1.0f + Mth::PlusOrMinus(0.5f)) * Script::GetFloat(CRCD(0x3db3fa83, "vehicle_physics_netcoll_forward_impulse")); float impulse_sideways = Mth::PlusOrMinus(1.5f) * Script::GetFloat(CRCD(0x39e99fec, "vehicle_physics_netcoll_sideways_impulse")); float impulse_upwards = (1.0f + Mth::PlusOrMinus(0.2f)) * Script::GetFloat(CRCD(0x738500fd, "vehicle_physics_netcoll_upwards_impulse")); Mth::Vector impulse = impulse_forward * impulse_direction + impulse_sideways * sideways; impulse[Y] += impulse_upwards; float rotate_spin = Mth::PlusOrMinus(1.5f) * Script::GetFloat(CRCD(0x14ddb3ef, "vehicle_physics_netcoll_spin_impulse")); float rotate_flip = (1.0f + Mth::PlusOrMinus(0.8f)) * Script::GetFloat(CRCD(0x4088e6ec, "vehicle_physics_netcoll_flip_impulse")); Mth::Vector rotate = rotate_flip * sideways; rotate[Y] += rotate_spin; m_mom += impulse / m_inv_mass; m_rotmom += rotate / m_inv_moment_body[X]; update_velocities(); m_artificial_collision_timer = Script::GetFloat(CRCD(0x771922a6, "vehicle_physics_artificial_collision_duration")); m_state = AWAKE; m_consider_sleeping_count = 0; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_flip ( ) { m_orientation.GetMatrix(m_orientation_matrix); m_orientation_matrix[W].Set(); if (m_orientation_matrix[Y][Y] > 0.999f) // || !m_controls.throttle) { m_in_flip = false; return; } bool flipping_appropriate_this_frame = m_vel.LengthSqr() < Mth::Sqr(300.0f) && m_rotvel.LengthSqr() < Mth::Sqr(Mth::PI / 4.0f) && m_max_normal_collision_impulse > 0.0f && m_orientation_matrix[Y][Y] < 0.25f && (m_num_wheels_in_contact == 0 || m_orientation_matrix[Y][Y] < 0.0f) && m_controls.throttle; if (!m_in_flip && !flipping_appropriate_this_frame) return; // starting a flip if (!m_in_flip) { m_mom.Set(); m_rotmom.Set(); m_in_flip = true; m_flip_start_time_stamp = Tmr::GetTime(); } // check for maximum flip duration if (Tmr::ElapsedTime(m_flip_start_time_stamp) > vVP_FLIP_DURATION + vVP_FLIP_DELAY) { m_in_flip = false; return; } // put a short delay before the flip actually has an effect if (Tmr::ElapsedTime(m_flip_start_time_stamp) < vVP_FLIP_DELAY) { return; } // setup this frame's slerp Mth::Matrix goal_orientation; goal_orientation[Z] = m_orientation_matrix[Z]; goal_orientation[Z][Y] = 0.0f; goal_orientation[Z].Normalize(); goal_orientation[Y].Set(0.0f, 1.0f, 0.0f); goal_orientation[X].Set(goal_orientation[Z][Z], 0.0f, -goal_orientation[Z][X]); Mth::SlerpInterpolator slerper; slerper.setMatrices(&m_orientation_matrix, &goal_orientation); // calculate the new orientation matrix slerper.getMatrix( &m_orientation_matrix, Mth::ClampMax(vVP_FLIP_RATE * Tmr::FrameLength() / slerper.getRadians(), 1.0f) ); m_orientation = m_orientation_matrix; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_wheel_from_structure ( SWheel& wheel, Script::CStruct* p_wheel_struct ) { uint32 checksum; // wheel's steering type if (p_wheel_struct->ContainsComponentNamed(CRCD(0x7560e63, "steering"))) { p_wheel_struct->GetChecksum(CRCD(0x7560e63, "steering"), &checksum); switch (checksum) { case CRCC(0x85981897, "left"): wheel.steering = SWheel::LEFT; break; case CRCC(0x4b358aeb, "right"): wheel.steering = SWheel::RIGHT; break; case CRCC(0x613631cd, "fixed"): wheel.steering = SWheel::FIXED; break; default: Dbg_MsgAssert(false, ("Bad wheel steering setting")); } } // if the wheel is a drive wheel if (p_wheel_struct->ContainsComponentNamed(CRCD(0x97e20a70, "drive"))) { p_wheel_struct->GetChecksum(CRCD(0x97e20a70, "drive"), &checksum); switch (checksum) { case CRCC(0x8a18ca56, "yes"): wheel.drive = true; break; case CRCC(0x9855d7e0, "no"): wheel.drive = false; break; default: Dbg_MsgAssert(false, ("Bad wheel drive setting")); } } // suspension spring rate (lbs / in) // strength of the suspension spring; multiplied by the compresion to get the spring's force; must be scaled // with the vehicle weight; large spring rates give stiffer suspensions; large values may cause instability in // physics model p_wheel_struct->GetFloat(CRCD(0x9e931fdf, "spring_rate"), &wheel.spring); // suspension damping; bump rate (lbs s / in) // bounce damping strength of the suspension; smaller values cause bouncier suspensions; very large values may // cause instability in the physics model p_wheel_struct->GetFloat(CRCD(0xafa960ff, "damping_rate"), &wheel.damping); // wheel radius (in) // larger radius wheels are harder for the engine to rotate and the brake and rolling resistance to stop p_wheel_struct->GetFloat(CRCD(0xc48391a5, "radius"), &wheel.radius); if (p_wheel_struct->ContainsComponentNamed(CRCD(0xca73775d, "moment"))) { p_wheel_struct->GetFloat(CRCD(0xca73775d, "moment"), &wheel.inv_moment); wheel.inv_moment = vVP_GRAVITATIONAL_ACCELERATION / wheel.inv_moment; } p_wheel_struct->GetFloat(CRCD(0xd87d0183, "max_draw_y_offset"), &wheel.max_draw_y_offset); // the following parameters affect the shape of the tire's friction curve // the friction curve gives the strength of the frictional force as a function of the tire's slip velocity; our // simple curve rises linearly from zero at zero slip velocity, reaches static_friction at a slip velocity of // min_static_velocity, remains at static_friction up to a slip velocity of max_static_velocity, drops linearly // to dynamic_friction until a slip velocity of min_dynamic_velocity, and then remains constant at dynamic_friction // normalized friction strength when the wheel is not skidding // low values will cause the vehicle to be spin-out and skid easily; high values cause the tires to feel sticky; // real world values are on the order of 1.3; fun values are much higher p_wheel_struct->GetFloat(CRCD(0xf36b97c8, "static_friction"), &wheel.static_friction); // normalized friction strength when the wheel is skidding // should be lower than static_friction; values much lower than static_friction cause the vehicle to be unforgiving; // once you lose grip, it is harder to regain control of the vehicle; values near static_friction cause the vehicle // to be very forgiving, with skidding causing little loss of handling p_wheel_struct->GetFloat(CRCD(0x674852f6, "dynamic_friction"), &wheel.dynamic_friction); // normalized friction strength when the handbrake is applied // currently specially tuned in script to force a fishtail when the handbrake and throttle are both applied and spin-outs when only handbrake is applied p_wheel_struct->GetFloat(CRCD(0x52242919, "handbrake_throttle_friction"), &wheel.handbrake_throttle_friction); p_wheel_struct->GetFloat(CRCD(0x6d615f67, "handbrake_locked_friction"), &wheel.handbrake_locked_friction); // slip speed below which tires undergrip (mph) // should be around 1 to 2 mph if (p_wheel_struct->ContainsComponentNamed(CRCD(0x60b2c097, "min_static_grip_velocity"))) { p_wheel_struct->GetFloat(CRCD(0x60b2c097, "min_static_grip_velocity"), &wheel.min_static_velocity); wheel.min_static_velocity = MPH_TO_IPS(wheel.min_static_velocity); } // maximum slip speed before tires begin to lose their grip (mph) // should be larger than min_static_velocity; with a low value the vehicle will skid more readily; a high value // causes there to be a large range of slip velocties with maximum tire grip if (p_wheel_struct->ContainsComponentNamed(CRCD(0xbb0d9370, "max_static_grip_velocity"))) { p_wheel_struct->GetFloat(CRCD(0xbb0d9370, "max_static_grip_velocity"), &wheel.max_static_velocity); wheel.max_static_velocity = MPH_TO_IPS(wheel.max_static_velocity); } // minimum slip speed before tires begin skidding (imph) // should be larger than max_static_velocity; with a low values, tires are very unforgiving and begin to skid as soon // as they begin to slip; with a larger value, the loss of control and onset of skidding will be more gradual if (p_wheel_struct->ContainsComponentNamed(CRCD(0xb71803ad, "min_dynamic_grip_velocity"))) { p_wheel_struct->GetFloat(CRCD(0xb71803ad, "min_dynamic_grip_velocity"), &wheel.min_dynamic_velocity); wheel.min_dynamic_velocity = MPH_TO_IPS(wheel.min_dynamic_velocity); } // torque which the brake exerts on the wheel (ft-lbs) if (p_wheel_struct->ContainsComponentNamed(CRCD(0x3bccbadc, "brake_torque"))) { p_wheel_struct->GetFloat(CRCD(0x3bccbadc, "brake_torque"), &wheel.brake.torque); wheel.brake.torque *= 12.0f; } // torque which the handbrake exerts on the wheel when steering is straight and the throttle is down (ft-lbs) if (p_wheel_struct->ContainsComponentNamed(CRCD(0x9439d144, "handbrake_torque"))) { p_wheel_struct->GetFloat(CRCD(0x9439d144, "handbrake_torque"), &wheel.brake.handbrake_torque); wheel.brake.handbrake_torque *= 12.0f; } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_dynamic_state ( ) { // update body state // calculate accelerations Mth::Vector acc = m_inv_mass * m_force; Mth::Vector rotacc = m_inv_moment.Rotate(m_torque); // update state variables m_mom += m_time_step * m_force; update_pos_with_uber_frig(m_time_step * (m_vel + 0.5f * m_time_step * acc)); Mth::Vector delta_orientation_vector = -0.5f * m_time_step * (m_rotvel + 0.5f * m_time_step * rotacc); Mth::Quat delta_orientation = m_orientation * Mth::Quat(delta_orientation_vector[X], delta_orientation_vector[Y], delta_orientation_vector[Z], 0.0f); m_orientation += delta_orientation; m_orientation.Normalize(); m_rotmom += m_time_step * m_torque; update_wheel_dynamic_state(); // if we're in the air and moving fast enough horizontally if (m_num_wheels_in_contact == 0 && m_max_normal_collision_impulse == 0.0f) { slerp_to_face_velocity(); } update_flip(); update_dependent_variables(); #if 0 PERIODIC(60) { float K = 0.5f * Mth::DotProduct(m_vel, m_mom) + 0.5f * Mth::DotProduct(m_rotvel, m_rotmom); float U = vVP_GRAVITATIONAL_ACCELERATION * m_pos[Y] / m_inv_mass; DUMPF(K + U); Mth::Vector a, b; a = m_rotmom; b = m_rotvel; DUMPF(Mth::DotProduct(a.Normalize(), b.Normalize())); // DUMPF(m_rotvel.Length()); // DUMPV(m_rotvel); } #endif } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_wheel_dynamic_state ( ) { for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; // wheel rotvel updates as torques are applied; thus, wheel rotvel is currently the frame's final rotvel wheel.orientation += m_time_step * (wheel.rotvel - 0.5f * m_time_step * wheel.rotacc); if (wheel.orientation > (200.0f * Mth::PI)) { wheel.orientation -= (100.0f * Mth::PI); } else if (wheel.orientation < 0.0f) { wheel.orientation += (100.0f * Mth::PI); } } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_dependent_variables ( ) { m_orientation.GetMatrix(m_orientation_matrix); m_orientation_matrix[W].Set(); m_suspension_center_of_mass_world = m_orientation_matrix.Rotate(m_suspension_center_of_mass); calculate_inverse_moment(); update_velocities(); // update wheel feeler endpoint positions for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; wheel.feeler_start_world = m_pos + m_orientation_matrix.Rotate(wheel.pos); wheel.feeler_end_world = wheel.pos; wheel.feeler_end_world[Y] += wheel.y_offset_hang - wheel.radius; wheel.feeler_end_world = m_pos + m_orientation_matrix.Rotate(wheel.feeler_end_world); } // update collider world positions for (int collider_idx = vVP_NUM_COLLIDERS; collider_idx--; ) { SCollider& collider = mp_colliders[collider_idx]; collider.world.m_corner = m_pos + m_orientation_matrix.Rotate(collider.body.m_corner); collider.world.m_first_edge = m_orientation_matrix.Rotate(collider.body.m_first_edge); collider.world.m_second_edge = m_orientation_matrix.Rotate(collider.body.m_second_edge); collider.first_edge_direction_world = collider.world.m_first_edge / collider.first_edge_length; collider.second_edge_direction_world = collider.world.m_second_edge / collider.second_edge_length; } update_collision_cache(); } /******************************************************************/ /* */ /* */ /******************************************************************/ float CVehicleComponent::calculate_friction_coefficient ( SWheel& wheel, float velocity ) const { // friction based on section 2.4 and 2.7 of Race Car Vehicle Dynamics by Milliken // the tire friction coefficient curve is very simple; just four concatenated lines if (m_controls.handbrake) { // handbrake friction is a total scam float friction; if (m_controls.throttle) { friction = wheel.handbrake_throttle_friction; wheel.state = SWheel::HANDBRAKE_THROTTLE; } else { friction = wheel.handbrake_locked_friction; wheel.state = SWheel::HANDBRAKE_LOCKED; } if (velocity < wheel.min_static_velocity) { // under gripped handbrake "skidding" wheel.state = SWheel::UNDER_GRIPPING; return friction * velocity / wheel.min_static_velocity; } else { // handbrake "skidding" return friction; } } float multiplier = m_controls.brake ? 2.0f : 1.0f; if (velocity < wheel.min_static_velocity) { // under gripped wheel.state = SWheel::UNDER_GRIPPING; return wheel.static_friction * velocity / wheel.min_static_velocity; } else if (velocity < wheel.max_static_velocity) { // maximum grip wheel.state = SWheel::GRIPPING; return multiplier * wheel.static_friction; } else if (velocity < wheel.min_dynamic_velocity) { // on the verge of skidding wheel.state = SWheel::SLIPPING; return multiplier * wheel.dynamic_friction + (wheel.static_friction - wheel.dynamic_friction) * (velocity - wheel.max_static_velocity) / (wheel.min_dynamic_velocity - wheel.max_static_velocity); } else { // skidding wheel.state = SWheel::SKIDDING; return multiplier * wheel.dynamic_friction; } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::calculate_friction_coefficients ( ) { // calculate the wheels' friction coefficients this frame for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; if (wheel.state == SWheel::OUT_OF_CONTACT) continue; // project the wheel's velocity due to the body's motion into the plane of the contact surface Mth::Vector projected_vel = wheel.vel_world; projected_vel.ProjectToPlane(wheel.contact_normal); // calculate the forward direction of the wheel Mth::Vector wheel_direction(sinf(wheel.steering_angle), 0.0f, cosf(wheel.steering_angle)); wheel_direction = m_orientation_matrix.Rotate(wheel_direction); // project that forward direction onto the contact surface wheel_direction.ProjectToPlane(wheel.contact_normal); wheel_direction.Normalize(); // NOTE: from this point and up, the calculation is repeated exactly in apply_friction_forces(); we cache the rotated and projected // wheel direction and the wheel velocity due to body velocity; because the wheel velocity will change before apply_friction_forces(), // the calculation below must be repeated wheel.cache_projected_direction = wheel_direction; wheel.cache_projected_vel = projected_vel; // subtract the contact patch velocity; positive wheel rotational velocity equals negative contact patch velocity projected_vel -= wheel.rotvel * wheel.radius * wheel_direction; // calculate the friction coefficient wheel.friction_coefficient = calculate_friction_coefficient(wheel, projected_vel.Length()); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::calculate_inverse_moment ( ) { // m_inv_moment = m_orientation_matrix * m_inv_moment_body_diag_matrix * m_orientation_matrix_transpose m_inv_moment[X][X] = m_inv_moment_body[X] * m_orientation_matrix[X][X]; m_inv_moment[X][Y] = m_inv_moment_body[Y] * m_orientation_matrix[X][Y]; m_inv_moment[X][Z] = m_inv_moment_body[Z] * m_orientation_matrix[X][Z]; m_inv_moment[Y][X] = m_inv_moment_body[X] * m_orientation_matrix[Y][X]; m_inv_moment[Y][Y] = m_inv_moment_body[Y] * m_orientation_matrix[Y][Y]; m_inv_moment[Y][Z] = m_inv_moment_body[Z] * m_orientation_matrix[Y][Z]; m_inv_moment[Z][X] = m_inv_moment_body[X] * m_orientation_matrix[Z][X]; m_inv_moment[Z][Y] = m_inv_moment_body[Y] * m_orientation_matrix[Z][Y]; m_inv_moment[Z][Z] = m_inv_moment_body[Z] * m_orientation_matrix[Z][Z]; Mth::Matrix orientation_matrix_transpose; orientation_matrix_transpose.Transpose(m_orientation_matrix); m_inv_moment = m_inv_moment * orientation_matrix_transpose; } /******************************************************************/ /* */ /* */ /******************************************************************/ int CVehicleComponent::determine_effective_gear ( float wheel_rotvel ) { int gear = 0; do { float engine_rotvel = wheel_rotvel * m_engine.differential_ratio * m_engine.p_gear_ratios[gear]; if (engine_rotvel > m_engine.upshift_rotvel) { if (gear < m_engine.num_gears - 1) { gear++; continue; } } return gear; } while (true); } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_collision_cache ( ) { Mth::CBBox collision_bbox; // add wheel location feelers for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; collision_bbox.AddPoint(wheel.feeler_start_world); collision_bbox.AddPoint(wheel.feeler_end_world); } // add body collision feelers for (int collider_idx = vVP_NUM_COLLIDERS; collider_idx--; ) { SCollider& collider = mp_colliders[collider_idx]; collision_bbox.AddPoint(collider.world.m_corner); collision_bbox.AddPoint(collider.world.m_corner + collider.world.m_first_edge); collision_bbox.AddPoint(collider.world.m_corner + collider.world.m_second_edge); collision_bbox.AddPoint(collider.world.m_corner + collider.world.m_first_edge + collider.world.m_second_edge); } m_collision_cache.Update(collision_bbox); } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_wheel_heights ( ) { // We crap out and don't do real wheel dynamics. Instead, if the ground is within a wheel's hang point, we teleport the wheel to the // ground. If not, we teleport the wheel to its hang point. CFeeler feeler; feeler.SetCache(&m_collision_cache); m_num_wheels_in_contact = 0; for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; // y_offset a feeler from the top of the wheel's y_offset to the wheel's hang point plus its radius feeler.m_start = wheel.feeler_start_world; feeler.m_end = wheel.feeler_end_world; feeler.SetIgnore(mFD_NON_COLLIDABLE, 0); if (feeler.GetCollision(false)) { // trip any triggers if (wheel.state != SWheel::OUT_OF_CONTACT) { if (feeler.GetSector() != wheel.last_ground_feeler.GetSector()) { trip_trigger(TRIGGER_SKATE_OFF, wheel.last_ground_feeler); if (reset_this_frame()) return; trip_trigger(TRIGGER_SKATE_ONTO, feeler); if (reset_this_frame()) return; } } else { trip_trigger(TRIGGER_SKATE_ONTO, feeler); if (reset_this_frame()) return; } wheel.last_ground_feeler = feeler; wheel.state = SWheel::NO_STATE; wheel.contact_normal = feeler.GetNormal(); wheel.y_offset = feeler.GetDist() * (wheel.y_offset_hang - wheel.radius) + wheel.radius; m_num_wheels_in_contact++; feeler.m_end = feeler.GetPoint(); } else { // trip any triggers if (wheel.state != SWheel::OUT_OF_CONTACT) { trip_trigger(TRIGGER_SKATE_OFF, wheel.last_ground_feeler); if (reset_this_frame()) return; } wheel.state = SWheel::OUT_OF_CONTACT; wheel.y_offset = wheel.y_offset_hang; } // Allows CAP kill planes to work on the car. if (Ed::CParkEditor::Instance()->UsingCustomPark()) { // now check for non-collidable trigger polys feeler.SetIgnore(0, mFD_NON_COLLIDABLE | mFD_TRIGGER); if (feeler.GetCollision(false)) { trip_trigger(TRIGGER_BONK, feeler); if (reset_this_frame()) return; } } } // END loop over wheels // update the wheels' dependent variables which depend of the wheel heights for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; wheel.pos_world = wheel.pos; wheel.pos_world[Y] += wheel.y_offset - wheel.radius; wheel.pos_world = m_orientation_matrix.Rotate(wheel.pos_world); wheel.vel_world = calculate_body_point_velocity(wheel.pos_world); } // END loop over wheels } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_steering_angles ( ) { // NOTE: could easily use a table here m_steering_display = Mth::Lerp(m_steering_display, m_controls.steering, 10.0f * m_time_step); float left_steering_angle; float right_steering_angle; float left_steering_angle_display; float right_steering_angle_display; if (Mth::Abs(m_controls.steering) > 0.001f) { float turning_radius = m_cornering_wheelbase / tanf(m_controls.steering * m_max_steering_angle); if (turning_radius > 0.0f) { left_steering_angle = -atan2f(m_cornering_wheelbase, turning_radius - m_cornering_axle_length / 2.0f); right_steering_angle = -atan2f(m_cornering_wheelbase, turning_radius + m_cornering_axle_length / 2.0f); } else { left_steering_angle = atan2f(m_cornering_wheelbase, -turning_radius + m_cornering_axle_length / 2.0f); right_steering_angle = atan2f(m_cornering_wheelbase, -turning_radius - m_cornering_axle_length / 2.0f); } } else { left_steering_angle = right_steering_angle = 0.0f; } if (Mth::Abs(m_steering_display) > 0.001f) { float turning_radius_display = m_cornering_wheelbase / tanf(m_steering_display * m_max_steering_angle); if (turning_radius_display > 0.0f) { left_steering_angle_display = -atan2f(m_cornering_wheelbase, turning_radius_display - m_cornering_axle_length / 2.0f); right_steering_angle_display = -atan2f(m_cornering_wheelbase, turning_radius_display + m_cornering_axle_length / 2.0f); } else { left_steering_angle_display = atan2f(m_cornering_wheelbase, -turning_radius_display + m_cornering_axle_length / 2.0f); right_steering_angle_display = atan2f(m_cornering_wheelbase, -turning_radius_display - m_cornering_axle_length / 2.0f); } } else { left_steering_angle_display = right_steering_angle_display = 0.0f; } for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; if (wheel.steering == SWheel::FIXED) continue; wheel.steering_angle = (wheel.steering == SWheel::LEFT ? left_steering_angle : right_steering_angle); wheel.steering_angle_display = (wheel.steering == SWheel::LEFT ? left_steering_angle_display : right_steering_angle_display); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::damp_rotation ( ) { // no damping when handbraking on the ground if (m_controls.handbrake && m_num_wheels_in_contact > 2) return; if (!m_controls.throttle && m_controls.steering != 0.0f && m_num_wheels_in_contact > 2) return; if (in_artificial_collision()) return; // apply quadtratic damping // quadratic damping in the air makes jump recovery much easier; the vehicle lands on its wheels much more often float quad_damping = m_time_step * m_quad_rotvel_damping * m_rotmom.LengthSqr(); // prevent reversal if (quad_damping > m_rotmom.Length()) { m_rotmom.Set(0.0f, 0.0f, 0.0f); } else { Mth::Vector direction = m_rotmom; direction.Normalize(); m_rotmom -= quad_damping * direction; } // apply constant damping; only applied in the Y direction // apply only if we're not steering, we're on the ground, and we're relatively upright if (m_controls.steering == 0.0f && m_num_wheels_in_contact > 2 && m_orientation_matrix[Y][Y] > 0.5f) { float const_damping = m_time_step * m_const_rotvel_damping;; // prevent reversal if (Mth::Abs(const_damping) > Mth::Abs(m_rotmom[Y])) { m_rotmom[Y] = 0.0f; } else { m_rotmom[Y] += (m_rotmom[Y] > 0.0f ? -const_damping : const_damping); } } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::accumulate_forces ( ) { // forces and torques on the body accumulate and are applied at the end of the frame; torques on the wheels are too large to accumulate; // they are applied to wheel velocity as they occur; thus, the order of application of wheel torques is critical apply_gravitational_forces(); apply_suspension_forces(); // we calculate the friction coefficients before applying engine torques so that friction will get the chance to counteract rotational // accelerations using static friction before the wheels spin out due to engine torque calculate_friction_coefficients(); if (!m_controls.handbrake) { apply_engine_forces(); apply_drag_forces(); // give the brakes a chance to cancel the rotation due to engine torque and to lock up the wheels before friction effects apply_brake_forces(); apply_friction_forces(); // after friction, the brakes are given the opportunity to apply additional torque and relock the wheels apply_spare_brake_forces(); } else { if (m_controls.throttle) { apply_engine_forces(); // scale the handbrake's braking effect with the steering factor; thus, you power through sharp handbrake turns; yet, the handbrake // still brakes on straight aways apply_handbrake_forces(1.0f - Mth::Abs(m_controls.steering)); apply_friction_forces(); apply_spare_brake_forces(); } else { apply_friction_forces(); // lock wheels for (int n = m_num_wheels; n--; ) { mp_wheels[n].rotvel = 0.0f; mp_wheels[n].rotacc = 0.0f; } } } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_gravitational_forces ( ) { // use reduced gravity when flipping the car if (m_in_flip && Tmr::ElapsedTime(m_flip_start_time_stamp) > vVP_FLIP_DELAY) { m_force[Y] -= vVP_FLIP_GRAVITY_FACTOR * m_gravity_override_fraction * vVP_GRAVITATIONAL_ACCELERATION / m_inv_mass; } else { m_force[Y] -= m_gravity_override_fraction * vVP_GRAVITATIONAL_ACCELERATION / m_inv_mass; } // we do our own force accumulation for this simple and constant force, having the same effect as the following code // Mth::Vector force(0.0f, -vVP_GRAVITATIONAL_ACCELERATION / m_inv_mass, 0.0f); // Mth::Vector location = m_suspension_center_of_mass; // accumulate_force(force, location, MAKE_RGB(50, 50, 0)); if (m_draw_debug_lines) { Gfx::AddDebugLine(m_pos, m_pos + 0.05f * Mth::Vector(0.0f, -vVP_GRAVITATIONAL_ACCELERATION / m_inv_mass, 0.0f), MAKE_RGB(50, 50, 0), MAKE_RGB(50, 50, 0), 1); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_suspension_forces ( ) { for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; // if not in contact with the ground, y_offset == y_offset_hang if (wheel.state == SWheel::OUT_OF_CONTACT) { // set this frame's history to the default value to smooth out landings wheel.normal_force_history[m_next_normal_force_history_idx] = vVP_GRAVITATIONAL_ACCELERATION / m_inv_mass / m_num_wheels; continue; } // direction of the suspension line; commented version is slower but explicit // Mth::Vector direction = m_orientation_matrix.Rotate(Mth::Vector(0.0f, 1.0f, 0.0f)); Mth::Vector direction = m_orientation_matrix[Y]; // magnitude of the spring force along the contact normal float magnitude = -wheel.spring * (wheel.y_offset_hang - wheel.y_offset) * Mth::DotProduct(wheel.contact_normal, direction); // velocity of the wheel projected into the contact normal direction float projected_vel = Mth::DotProduct(wheel.vel_world, wheel.contact_normal); // magnitude adjusted for damping magnitude += -wheel.damping * projected_vel; // suspension force Mth::Vector force = magnitude * wheel.contact_normal; accumulate_force(force, wheel.pos_world, MAKE_RGB(100, 100, 0)); // set the normal force magnitude; used in the friction force code wheel.normal_force_history[m_next_normal_force_history_idx] = magnitude; wheel.normal_force = 0.0f; for (int i = vVP_NORMAL_FORCE_HISTORY_LENGTH; i--; ) { wheel.normal_force += wheel.normal_force_history[i]; } wheel.normal_force /= vVP_NORMAL_FORCE_HISTORY_LENGTH; } m_next_normal_force_history_idx = ++m_next_normal_force_history_idx % vVP_NORMAL_FORCE_HISTORY_LENGTH; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_friction_forces ( ) { // NOTE: we've handled longitudinal friction reversal successfully; however, tangential friction reversal is still a significant issue for large // friction coefficients and high centers of mass // we use a simple tire model were the friction force is opposite the velocity of the tire contact and proportional to the normal force and a // friction coefficient; the friction coefficient is a function of the velocity of the contact with the following form: // 0 < v < min_static_velocity: rises linearly from zero to static_friction // min_static_velocity < v < max_static_velocity: constant at static_friction // max_static_velocity < v < min_dynamic_velocity: falls linearly from static_friction to dynamic_friction // min_dynamic_velocity < v: constant at dynamic_friction // NOTE: because friction is zero at zero slip velocity, hill slippage is an issue for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; if (wheel.state == SWheel::OUT_OF_CONTACT) continue; // grab the cached wheel direction and velocity projected into the contact plane Mth::Vector projected_wheel_direction = wheel.cache_projected_direction; Mth::Vector projected_vel = wheel.cache_projected_vel; // subtract the contact patch velocity; positive wheel rotational velocity equals negative contact patch velocity projected_vel -= wheel.rotvel * wheel.radius * projected_wheel_direction; // store the slip vel for later display wheel.slip_vel = projected_vel.Length(); // if the velocity is too small, its direction is meaningless if (wheel.slip_vel < 0.000001) continue; // propose a frictional force Mth::Vector vel_direction = projected_vel; vel_direction.Normalize(); Mth::Vector proposed_force = -wheel.friction_coefficient * wheel.normal_force * vel_direction; // now we must check that this force is not more that the force which would stop the relative longitudinal velocity // calculate the torque required to stick the wheel contact patch to the ground float longitudinal_vel = Mth::DotProduct(projected_vel, projected_wheel_direction); float stopping_rotvel = longitudinal_vel / wheel.radius; float stopping_torque = -calculate_stopping_torque(wheel, stopping_rotvel); // calculate the proposed torque float proposed_torque = -wheel.radius * Mth::DotProduct(proposed_force, projected_wheel_direction); Mth::Vector force; float torque; // if the proposed torque is less than what is required to stop the rotation if (Mth::Abs(proposed_torque) < Mth::Abs(stopping_torque)) { // we're ok force = proposed_force; torque = proposed_torque; } else { // otherwise, reduce the proposed longitudinal force to only the stopping force float proposed_longitudinal_force = Mth::DotProduct(proposed_force, projected_wheel_direction); float stopping_longitudinal_force = -stopping_torque / wheel.radius; force = proposed_force + (stopping_longitudinal_force - proposed_longitudinal_force) * projected_wheel_direction; torque = stopping_torque; } // apply the force to the body accumulate_force(force, wheel.pos_world, MAKE_RGB(0, 0, 255)); // apply the torque to the wheel apply_wheel_torque(wheel, torque); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_drag_forces ( ) { if (m_controls.throttle || m_controls.reverse || m_controls.brake) return; for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; if (!wheel.drive || wheel.state == SWheel::OUT_OF_CONTACT) continue; // determine the effective engine gear int effective_gear = determine_effective_gear(Mth::Abs(wheel.rotvel)); // drag torque scales like the square of the gear ratio float torque = m_engine.drag_torque * m_engine.p_gear_ratios[effective_gear] * m_engine.p_gear_ratios[effective_gear] / m_num_drive_wheels; // prevent reversal float maximum_torque = Mth::Abs(calculate_stopping_torque(wheel, wheel.rotvel)); if (torque > maximum_torque) { torque = maximum_torque; } apply_wheel_torque(wheel, wheel.rotvel < 0.0f ? torque : -torque); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_engine_forces ( ) { // NOTE: test float wall_climb_factor = Mth::ClampMax(m_orientation_matrix[Y][Y] * m_orientation_matrix[Y][Y] / 0.9f, 1.0f); if ((!m_controls.throttle && !m_controls.reverse) || m_controls.brake) return; for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; if (!wheel.drive) continue; // turn off power to front wheels when power handbraking if (m_controls.handbrake && wheel.steering != SWheel::FIXED) continue; // determine effective engine gear int effective_gear; if (!m_controls.reverse) { effective_gear = determine_effective_gear(wheel.rotvel > 0.0f ? wheel.rotvel : 0.0f); } else { effective_gear = determine_effective_gear(wheel.rotvel < 0.0f ? -wheel.rotvel : 0.0f); } float torque = m_engine.drive_torque * m_engine.differential_ratio * m_engine.p_gear_ratios[effective_gear] / m_num_drive_wheels; torque *= wall_climb_factor; // reverse gears are scaled down by a factor if (m_controls.reverse) { torque *= -m_engine.reverse_torque_ratio; } // account for low of power in front wheels in four-wheel drive car when power handbraking if (m_controls.handbrake && m_num_drive_wheels == 4) torque *= 2.0f; apply_wheel_torque(wheel, torque); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_brake_forces ( ) { if (!m_controls.brake) return; for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; float torque = wheel.brake.torque; float maximum_torque = Mth::Abs(calculate_stopping_torque(wheel, wheel.rotvel)); if (torque > maximum_torque) { wheel.brake.spare_torque = torque - maximum_torque; torque = maximum_torque; } else { wheel.brake.spare_torque = 0.0f; } apply_wheel_torque(wheel, wheel.rotvel < 0.0f ? torque : -torque); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_handbrake_forces ( float application_factor ) { Dbg_Assert(m_controls.handbrake); for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; float torque = application_factor * wheel.brake.handbrake_torque; float maximum_torque = Mth::Abs(calculate_stopping_torque(wheel, wheel.rotvel)); if (torque > maximum_torque) { wheel.brake.spare_torque = torque - maximum_torque; torque = maximum_torque; } else { wheel.brake.spare_torque = 0.0f; } apply_wheel_torque(wheel, wheel.rotvel < 0.0f ? torque : -torque); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_spare_brake_forces ( ) { if (!m_controls.brake) return; for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; if (wheel.brake.spare_torque == 0.0f) continue; float torque = wheel.brake.spare_torque; float maximum_torque = Mth::Abs(calculate_stopping_torque(wheel, wheel.rotvel)); if (torque > maximum_torque) { torque = maximum_torque; } apply_wheel_torque(wheel, wheel.rotvel < 0.0f ? torque : -torque); } } /******************************************************************/ /* */ /* */ /******************************************************************/ float CVehicleComponent::calculate_collision_depth ( const SCollisionPoint& collision_point, const SCollider& collider ) const { enum EDepthDirection { WITH_FIRST_EDGE, AGAINST_FIRST_EDGE, WITH_SECOND_EDGE, AGAINST_SECOND_EDGE }; // calculate displacement from base corner Mth::Vector displacement = collision_point.pos - collider.world.m_corner; // determine the minimum depth and correponding depth direction float min_depth = Mth::DotProduct(displacement, collider.first_edge_direction_world); EDepthDirection depth_direction = WITH_FIRST_EDGE; float depth = collider.first_edge_length - min_depth; if (depth < min_depth) { min_depth = depth; depth_direction = AGAINST_FIRST_EDGE; } depth = Mth::DotProduct(displacement, collider.second_edge_direction_world); if (depth < min_depth) { min_depth = depth; depth_direction = WITH_SECOND_EDGE; } depth = collider.second_edge_length - depth; if (depth < min_depth) { min_depth = depth; depth_direction = AGAINST_SECOND_EDGE; } if (min_depth < 0.01f) { return 0.0f; } switch (depth_direction) { case WITH_FIRST_EDGE: depth = min_depth / Mth::DotProduct(collider.first_edge_direction_world, collision_point.normal); break; case AGAINST_FIRST_EDGE: depth = -min_depth / Mth::DotProduct(collider.first_edge_direction_world, collision_point.normal); break; case WITH_SECOND_EDGE: depth = min_depth / Mth::DotProduct(collider.second_edge_direction_world, collision_point.normal); break; case AGAINST_SECOND_EDGE: depth = -min_depth / Mth::DotProduct(collider.second_edge_direction_world, collision_point.normal); break; } #ifdef __USER_DAN__ if (depth < 0.0f) { MESSAGE("negative depth bug"); } #endif return depth; } /******************************************************************/ /* */ /* */ /******************************************************************/ bool CVehicleComponent::check_for_capping ( SCollisionPoint& collision_point, const CRectFeeler& rect_feeler, const SCollider& collider, int collision_line_idx, int collision_point_end_idx ) const { // check to see if this culled collision is the back of a cappable collision with a 2D object for (int check_line_idx = rect_feeler.GetNumMergedCollisionSurfaces(); check_line_idx--; ) { if (check_line_idx == collision_line_idx) continue; for (int end = 2; end--; ) { if (very_close(collision_point.pos, rect_feeler.GetMergedCollisionSurface(check_line_idx).ends[end].point) && Mth::DotProduct(collision_point.normal, rect_feeler.GetMergedCollisionSurface(check_line_idx).normal) < -0.99) { // we'll use this point to cap the collision with the 2D object; the tangent cross the normal will point out of the triangle collision_point.normal = Mth::CrossProduct( rect_feeler.GetMergedCollisionSurface(collision_line_idx).ends[collision_point_end_idx].tangent, collision_point.normal ); collision_point.depth = calculate_collision_depth(collision_point, collider); if (m_draw_debug_lines) { Gfx::AddDebugLine( collision_point.pos, collision_point.pos + (72) * collision_point.normal, MAKE_RGB(255, 100, 255), MAKE_RGB(255, 100, 255), 1 ); } return true; } } // END loop over checked collision line ends } // END loop over all other collision lines return false; } /******************************************************************/ /* */ /* */ /******************************************************************/ bool CVehicleComponent::consider_culling_point ( const SCollisionPoint& collision_point ) const { // cull collision points which have a normal direction along their body offset Mth::Vector offset = collision_point.pos - m_pos; bool cull = Mth::DotProduct(offset, collision_point.normal) >= 0.0f; if (m_draw_debug_lines && cull) { uint32 c = MAKE_RGB(0, 0, 0); Gfx::AddDebugLine(collision_point.pos, collision_point.pos + 48.0f * collision_point.normal, c, c, 1); } return cull; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::apply_environment_collisions ( ) { m_max_normal_collision_impulse = 0.0f; CRectFeeler rect_feeler; CLineFeeler line_feeler; rect_feeler.SetCache(&m_collision_cache); line_feeler.SetCache(&m_collision_cache); m_num_collision_points = 0; for (int collider_idx = vVP_NUM_COLLIDERS; collider_idx--; ) { SCollider& collider = mp_colliders[collider_idx]; // first we use a rectangular feeler // setup the feeler rect_feeler.SetRectangle(collider.world); // detect collisions if (rect_feeler.GetCollision()) { trip_trigger(TRIGGER_BONK, rect_feeler); if (reset_this_frame()) return; rect_feeler.MergeCollisionSurfaces(); // copy out the collision points from the feeler output for (int collision_line_idx = rect_feeler.GetNumMergedCollisionSurfaces(); collision_line_idx--; ) { for (int end = 2; end--; ) { // add the collision line's start to the list of collision points SCollisionPoint& new_collision_point = sp_collision_points[m_num_collision_points]; new_collision_point.line = false; new_collision_point.pos = rect_feeler.GetMergedCollisionSurface(collision_line_idx).ends[end].point; new_collision_point.normal = rect_feeler.GetMergedCollisionSurface(collision_line_idx).normal; if (!consider_culling_point(new_collision_point)) { // if this is a keeper, calculate the collision depth if (rect_feeler.GetMergedCollisionSurface(collision_line_idx).ends[end].tangent_exists) { new_collision_point.depth = calculate_collision_depth(new_collision_point, collider); } else { new_collision_point.depth = 0.0f; } m_num_collision_points++; } else if (rect_feeler.GetMergedCollisionSurface(collision_line_idx).ends[end].tangent_exists) { if (check_for_capping(new_collision_point, rect_feeler, collider, collision_line_idx, end)) { m_num_collision_points++; } } } // END loop over collision line ends } // END loop over collision surfaces } // END if we had a collision this collider if (m_draw_debug_lines) { rect_feeler.DebugLines(255, 255, 255, 1); } // line feelers line_feeler.m_start = m_pos; line_feeler.m_end = rect_feeler.m_corner; if (line_feeler.GetCollision(false)) { trip_trigger(TRIGGER_BONK, line_feeler); if (reset_this_frame()) return; sp_collision_points[m_num_collision_points].pos = line_feeler.GetPoint(); sp_collision_points[m_num_collision_points].normal = line_feeler.GetNormal(); if (!consider_culling_point(sp_collision_points[m_num_collision_points])) { sp_collision_points[m_num_collision_points].depth = calculate_collision_depth(line_feeler); m_num_collision_points++; sp_collision_points[m_num_collision_points].line = true; } } if (m_draw_debug_lines) { line_feeler.DebugLine(255, 255, 255, 1); } line_feeler.m_end += rect_feeler.m_first_edge; if (line_feeler.GetCollision(false)) { trip_trigger(TRIGGER_BONK, line_feeler); if (reset_this_frame()) return; sp_collision_points[m_num_collision_points].pos = line_feeler.GetPoint(); sp_collision_points[m_num_collision_points].normal = line_feeler.GetNormal(); if (!consider_culling_point(sp_collision_points[m_num_collision_points])) { sp_collision_points[m_num_collision_points].depth = calculate_collision_depth(line_feeler); m_num_collision_points++; sp_collision_points[m_num_collision_points].line = true; } } if (m_draw_debug_lines) { line_feeler.DebugLine(255, 255, 255, 1); } line_feeler.m_end += rect_feeler.m_second_edge; if (line_feeler.GetCollision(false)) { trip_trigger(TRIGGER_BONK, line_feeler); if (reset_this_frame()) return; sp_collision_points[m_num_collision_points].pos = line_feeler.GetPoint(); sp_collision_points[m_num_collision_points].normal = line_feeler.GetNormal(); if (!consider_culling_point(sp_collision_points[m_num_collision_points])) { sp_collision_points[m_num_collision_points].depth = calculate_collision_depth(line_feeler); m_num_collision_points++; sp_collision_points[m_num_collision_points].line = true; } } if (m_draw_debug_lines) { line_feeler.DebugLine(255, 255, 255, 1); } line_feeler.m_end -= rect_feeler.m_first_edge; if (line_feeler.GetCollision(false)) { trip_trigger(TRIGGER_BONK, line_feeler); if (reset_this_frame()) return; sp_collision_points[m_num_collision_points].pos = line_feeler.GetPoint(); sp_collision_points[m_num_collision_points].normal = line_feeler.GetNormal(); if (!consider_culling_point(sp_collision_points[m_num_collision_points])) { sp_collision_points[m_num_collision_points].depth = calculate_collision_depth(line_feeler); m_num_collision_points++; sp_collision_points[m_num_collision_points].line = true; } } if (m_draw_debug_lines) { line_feeler.DebugLine(255, 255, 255, 1); } } // END loop over colliders if (m_num_collision_points == 0) return; // zero accumulators for (int collision_idx = m_num_collision_points; collision_idx--; ) { sp_collision_points[collision_idx].normal_impulse = 0.0f; } // cache the momentums and velocities before normal impulses are applied; they are used when determining the frictional forces Mth::Vector cache_mom = m_mom; Mth::Vector cache_rotmom = m_rotmom; Mth::Vector cache_vel = m_vel; Mth::Vector cache_rotvel = m_rotvel; // apply normal impulses // loop over normal impulse points until all collisions are fully resolved int pass_count = 0; bool clean_pass; do { clean_pass = true; for (int collision_idx = m_num_collision_points; collision_idx--; ) { SCollisionPoint& collision = sp_collision_points[collision_idx]; // calculate the collision position in body space Mth::Vector pos_local = collision.pos - m_pos; // calculate the velocity and normal velocity of the collision point Mth::Vector vel = calculate_body_point_velocity(pos_local); float normal_vel = Mth::DotProduct(vel, collision.normal); // ignore if we're not impacting if (normal_vel > 0.0f) continue; clean_pass = false; // calculate the goal velocity of the collision float goal_normal_velocity = -m_body_restitution * normal_vel; // calculate the effective mass of the collision point float effective_mass = calculate_body_point_effective_mass(pos_local, collision.normal); // calculate the normal impulse required to reach the body velocity float normal_impulse = (goal_normal_velocity - normal_vel) * effective_mass; // accumulate the total normal impulse applied to this collision point collision.normal_impulse += normal_impulse; // setup the required impulse vector Mth::Vector impulse = normal_impulse * collision.normal; // apply the normal impulse immediately apply_impulse(impulse, pos_local); } if (++pass_count > 5) { #ifdef __USER_DAN__ MESSAGE("environment collision resolution pass count limit exceeded"); #endif break; } } while (!clean_pass); if (m_draw_debug_lines) { for (int collision_idx = m_num_collision_points; collision_idx--; ) { // SCollisionPoint& collision = sp_collision_points[collision_idx]; // Gfx::AddDebugLine( // collision.pos + collision.normal, // collision.pos + collision.normal + collision.normal_impulse / 2.0f * collision.normal, // MAKE_RGB(0, 100, 100), MAKE_RGB(0, 100, 100), 1 // ); } } // END loop over collision points // apply penalty forces to prevent interpenetration for (int collision_idx = m_num_collision_points; collision_idx--; ) { SCollisionPoint& collision = sp_collision_points[collision_idx]; // collision normal is facing opposite the depth // BUG: too many rect feeler collisions have negative depth!!! if (collision.depth <= 0.0f) continue; // extremely deep collisions are mostly collisions with the underbody which are actually shallow in the Y direction; for now we'll cap such // collisions and perhaps allow the car to sink down into geometry which comes from below but misses the wheels if (collision.depth > 2.0f) { collision.depth = 2.0f; } Mth::Vector force = m_body_spring * collision.depth * collision.normal; accumulate_collision_force(force, collision.pos - m_pos); // Gfx::AddDebugLine( // collision.pos, // collision.pos + (2 * collision.depth) * collision.normal, // MAKE_RGB(255, 255, 0), MAKE_RGB(255, 255, 0), 1 // ); } float body_friction = m_orientation_matrix[Y][Y] > 0.707f ? m_body_friction : m_body_wipeout_friction; // apply a friction impulse at each point for (int collision_idx = m_num_collision_points; collision_idx--; ) { SCollisionPoint& collision = sp_collision_points[collision_idx]; if (collision.normal_impulse == 0.0f) continue; // calculate the local collision position Mth::Vector pos_local = collision.pos - m_pos; // calculate the tangential velocity Mth::Vector vel = cache_vel + Mth::CrossProduct(cache_rotvel, pos_local); Mth::Vector tanjent = vel - Mth::DotProduct(vel, collision.normal) * collision.normal; float tanjent_vel = tanjent.Length(); if (tanjent_vel < 0.001f) continue; tanjent /= tanjent_vel; // calculate the effective mass of the collision point float effective_mass = calculate_body_point_effective_mass(pos_local, tanjent); // calculate the tanjential impulse required to stop the tanjential velocity float tanjent_impulse = tanjent_vel * effective_mass; // cap the frictional force at the normal force times the coefficient of friction float max_tanjent_impulse = body_friction * collision.normal_impulse; if (tanjent_impulse > max_tanjent_impulse) { tanjent_impulse = max_tanjent_impulse; } // setup the tanjent impulse vector Mth::Vector impulse = -tanjent_impulse * tanjent; // apply the friction impulse to the true state apply_impulse(impulse, pos_local); if (m_draw_debug_lines) { // Gfx::AddDebugLine( // collision.pos + collision.normal, // collision.pos + collision.normal + impulse / 2.0f, // MAKE_RGB(100, 0, 100), MAKE_RGB(100, 0, 100), 1 // ); } // apply the friction impulse to the state we are using for friction calculations cache_mom += impulse; cache_rotmom += Mth::CrossProduct(pos_local, impulse); cache_vel = m_inv_mass * cache_mom; cache_rotvel = m_inv_moment.Rotate(cache_rotmom); } // END loop over collision points // determine the maximum normal impulse this frame for (int collision_idx = m_num_collision_points; collision_idx--; ) { SCollisionPoint& collision = sp_collision_points[collision_idx]; m_max_normal_collision_impulse = Mth::Max(collision.normal_impulse, m_max_normal_collision_impulse); } // no player controlled impulses in the air or with handbrake on if (m_controls.steering == 0.0f || m_num_wheels_in_contact < 3 || m_controls.handbrake) return; // extra player controled collision impulse; allows one slight control over the direction on collision impulses for (int collision_idx = m_num_collision_points; collision_idx--; ) { SCollisionPoint& collision = sp_collision_points[collision_idx]; Mth::Vector pos_local = collision.pos - m_pos; float strength = m_controls.steering * m_collision_control * Mth::DotProduct(m_orientation_matrix[Z], collision.normal) * collision.normal_impulse; Mth::Vector direction(-collision.normal[Z], 0.0f, collision.normal[X]); apply_impulse(strength * direction, pos_local); if (m_draw_debug_lines) { Gfx::AddDebugLine( collision.pos + collision.normal, collision.pos + collision.normal + strength * direction / 5.0f, MAKE_RGB(255, 0, 100), MAKE_RGB(255, 0, 100), 1 ); } } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_pos_with_uber_frig ( const Mth::Vector& movement ) { // The vehicle has two uber frigs. The first treats the c.o.m. as a point particle with respect to the geo. The seconds is a traditional uber frig. Mth::Vector frame_start_pos = m_pos; // look for collisions along the origin's movement; only do a single bounce, assuming the remaining movement is ok CFeeler feeler(m_pos, m_pos + movement); if (feeler.GetCollision(false)) { // bounce off the surface MESSAGE("less than uber frig"); m_pos = feeler.GetPoint() + feeler.GetNormal(); Mth::Vector remaining_movement = feeler.GetDist() * movement; remaining_movement -= 2.0f * Mth::DotProduct(remaining_movement, feeler.GetNormal()) * feeler.GetNormal(); m_pos += remaining_movement; // use coeff of restit of 0.5 m_mom -= 1.5f * Mth::DotProduct(m_mom, feeler.GetNormal()) * feeler.GetNormal(); } else { m_pos += movement; } // now, a traditional uber frig feeler.m_start = m_pos; feeler.m_end = feeler.m_start; feeler.m_end[Y] -= FEET(400.0f); if (!feeler.GetCollision(false)) { MESSAGE("uber frig"); m_pos = frame_start_pos; m_mom *= 1.0f; m_rotmom *= 1.0f; } else { mp_model_component->ApplyLightingFromCollision(feeler); } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::slerp_to_face_velocity ( ) { // slerp to face our velocity if (m_in_flip) return; if (in_artificial_collision()) return; // only slerp if horizontal velocity is below a given threshold bool vel_below_threshold = m_vel[X] * m_vel[X] + m_vel[Z] * m_vel[Z] < m_in_air_slerp_vel_cutoff * m_in_air_slerp_vel_cutoff; if (vel_below_threshold && !m_vert_correction) return; // extract the current orientation matrix m_orientation.GetMatrix(m_orientation_matrix); m_orientation_matrix[W].Set(); // construct our target matrix Mth::Matrix target; if (vel_below_threshold) { target[Z] = m_orientation_matrix[Z]; target[Z][Y] = 0.0f; target[Z].Normalize(); } else { target[Z] = m_vel; target[Z].Normalize(); } if (Mth::DotProduct(target[Z], m_orientation_matrix[Z]) < 0.0f) { target[Z] *= -1.0f; } target[Y].Set(0.0f, 1.0f, 0.0f); target[X] = Mth::CrossProduct(target[Y], target[Z]); target[Y] = Mth::CrossProduct(target[Z], target[X]); target[W].Set(); // setup the slerp Mth::SlerpInterpolator slerper; slerper.setMatrices(&m_orientation_matrix, &target); // lerp up to full strength over time if (m_vel[X] * m_vel[X] + m_vel[Z] * m_vel[Z] > 350.0f * 350.0f) { slerper.getMatrix(&m_orientation_matrix, Mth::LinearMap( 0.0f, m_in_air_slerp_strength * m_time_step, Mth::Min(m_air_time_no_collision, m_in_air_slerp_time_delay), 0.0f, m_in_air_slerp_time_delay )); } else { slerper.getMatrix(&m_orientation_matrix, Mth::LinearMap( 0.0f, 0.3f * m_in_air_slerp_strength * m_time_step, Mth::Min(m_air_time_no_collision, m_in_air_slerp_time_delay), 0.0f, m_in_air_slerp_time_delay )); } m_orientation = m_orientation_matrix; } /******************************************************************/ /* */ /* */ /******************************************************************/ float CVehicleComponent::calculate_body_point_effective_mass ( const Mth::Vector& pos, const Mth::Vector& direction ) const { // calculate the effect on momentums due to a unit impulse applied at pos towards direction Mth::Vector delta_mom = direction; Mth::Vector delta_rotmom = Mth::CrossProduct(pos, direction); // calculate the resulting change in velocities Mth::Vector delta_vel = m_inv_mass * delta_mom; Mth::Vector delta_rotvel = m_inv_moment.Rotate(delta_rotmom); // calculate the corresponding change in the body point's velocity Mth::Vector delta_vel_body_point = delta_vel + Mth::CrossProduct(delta_rotvel, pos); // extract the change in velocity along the direction of interest float delta_vel_direction = Mth::DotProduct(direction, delta_vel_body_point); // return the effective mass of the body point in the direction of interest Dbg_Assert(delta_vel_direction != 0.0f); return 1.0f / delta_vel_direction; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::consider_sleeping ( ) { int last_consider_sleeping_count = m_consider_sleeping_count; m_consider_sleeping_count = 0; if (m_controls.throttle) return; if (m_controls.reverse && m_num_wheels_in_contact != 0) return; // only sleep if we're experiencing three or more collisions; hopefully, a surface-to-surface contact if (m_num_wheels_in_contact + m_num_collision_points < 3) return; // only sleep if we've got four-on-the-floor or are completely flipped if (m_num_wheels_in_contact != 0 && m_num_wheels_in_contact != m_num_wheels) return; // only sleep if we're moving slow if (m_vel.LengthSqr() > Mth::Sqr(vVP_SLEEP_VEL)) return; #ifdef __USER_DAN__ DUMPF(m_vel.Length()); #endif // only sleep if we're moving slow if (m_rotvel.LengthSqr() > Mth::Sqr(vVP_SLEEP_ROTVEL)) return; #ifdef __USER_DAN__ DUMPF(m_rotvel.Length()); #endif // never sleep during an artificial collision period if (in_artificial_collision()) return; m_consider_sleeping_count = last_consider_sleeping_count + 1; if (m_consider_sleeping_count < 3) { return; } // sleep m_mom.Set(); m_rotmom.Set(); m_force.Set(0.0f, 0.0f, 0.0f); m_torque.Set(0.0f, 0.0f, 0.0f); for (int n = m_num_wheels; n--; ) { mp_wheels[n].rotacc = 0.0f; } m_state = ASLEEP; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::get_input ( ) { CControlPad& control_pad = mp_input_component->GetControlPad(); // analog control // m_controls.steering = control_pad.m_scaled_leftX; m_controls.steering = control_pad.m_scaled_leftX * Mth::Abs(control_pad.m_scaled_leftX); // dpad control if (m_controls.steering == 0.0f) { if (control_pad.m_left.GetPressed() && !control_pad.m_right.GetPressed()) { m_controls.steering = -1.0f; } else if (control_pad.m_right.GetPressed() && !control_pad.m_left.GetPressed()) { m_controls.steering = 1.0f; } } m_controls.throttle = control_pad.m_x.GetPressed(); m_controls.handbrake = control_pad.m_R1.GetPressed() && !m_no_handbrake; bool brake_pressed = control_pad.m_square.GetPressed() || (m_exitable && control_pad.m_triangle.GetPressed()); bool exit_request = brake_pressed && !control_pad.m_square.GetPressed(); // decide between brake and reverse if (brake_pressed) { if ((m_controls.reverse || (Mth::DotProduct(m_vel, m_orientation_matrix[Z]) < 300.0f && m_num_wheels_in_contact != 0)) && (m_state != ASLEEP || m_num_wheels_in_contact != 0)) { // reverse m_controls.reverse = true; m_controls.brake = false; } else { // brake m_controls.brake = true; m_controls.reverse = false; } if (exit_request) { if (Mth::DotProduct(m_vel, m_orientation_matrix[Z]) < 0.0f) { // brake m_controls.brake = true; m_controls.reverse = false; } if (Mth::Abs(Mth::DotProduct(m_vel, m_orientation_matrix[Z])) < 30.0f) { // signal an exit GetObject()->BroadcastEvent(CRCD(0xcbaa3476, "ExitVehicleRequest")); } } } else { m_controls.reverse = false; m_controls.brake = false; } if (m_force_brake) { m_controls.brake = true; m_controls.reverse = false; } if (m_state == ASLEEP && (m_controls.throttle || m_controls.reverse)) { m_state = AWAKE; m_consider_sleeping_count = 0; } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::zero_input ( ) { m_controls.brake = false; m_controls.handbrake = false; m_controls.throttle = false; m_controls.reverse = false; m_controls.steering = 0.0f; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::update_skeleton ( ) { Mth::Matrix* p_matrices = mp_skeleton_component->GetSkeleton()->GetMatrices(); for (int i = mp_skeleton_component->GetSkeleton()->GetNumBones(); i--; ) { Mth::Matrix& matrix = p_matrices[i]; // setup the matrix for each bone in the skeleton if (m_draw_debug_lines == 2) { matrix.Zero(); continue; } // shadow if (i == 0) { matrix.Zero(); } // body else if (i == 1) { matrix.Zero(); matrix[X][X] = 1.0f; matrix[Y][Z] = -1.0f; matrix[Z][Y] = 1.0f; matrix[W] = m_body_pos; } // wheel else { SWheel& wheel = mp_wheels[i - 2]; matrix.Zero(); matrix[X][X] = -1.0f; matrix[Y][Z] = 1.0f; matrix[Z][Y] = 1.0f; matrix.RotateZLocal(wheel.steering_angle_display); matrix.RotateXLocal(-wheel.orientation); matrix[W] = wheel.pos; #ifdef __NOPT_ASSERT__ if (Script::GetInteger("use_max_y_offset")) { matrix[W][Y] += wheel.max_draw_y_offset; } else { matrix[W][Y] += Mth::ClampMax(wheel.y_offset, wheel.max_draw_y_offset); } #else matrix[W][Y] += Mth::ClampMax(wheel.y_offset, wheel.max_draw_y_offset); #endif matrix[W][W] = 1.0f; } } } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::draw_shadow ( ) { //Dbg_Message("Drawing shadow for car"); #if 0 // Experiment to draw a shadow using texture splats Mth::Vector start_pos = GetObject()->GetPos(); Mth::Vector end_pos = start_pos; start_pos[Y] += 12.0f; end_pos[Y] -= 120.0f; float y_rot = 360.0f - m_orientation.GetVector()[Y]; while (y_rot < 0.0f) y_rot += 360.0f; Nx::TextureSplat( start_pos, end_pos, 120.0f, 100.0f, 2.0f / (float) Config::FPS(), "blood_01", y_rot); #endif } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::control_skater ( ) { // HACKY; setup the skater's position, orientation, and animation each frame mp_skater->SetPos(GetObject()->GetPos() + GetObject()->GetMatrix().Rotate(m_skater_pos)); mp_skater->SetMatrix(GetObject()->GetMatrix()); mp_skater_core_physics_component->ResetLerpingMatrix(); mp_skater->SetVel(GetObject()->GetVel()); if (!m_skater_visible) return; float target_time = Mth::LinearMap( 0.0f, mp_skater_animation_component->AnimDuration(m_skater_anim), mp_skater_core_physics_component->GetFlag(FLIPPED) ? m_steering_display : -m_steering_display, -1.0f, 1.0f ); if (!GameNet::Manager::Instance()->InNetGame()) { mp_skater_animation_component->PlayPrimarySequence(m_skater_anim, false, target_time, 1000.0f, Gfx::LOOPING_CYCLE, 0.0f, 0.0f); } else { // HACK to reduce number of animation events when driving cars static int anim_send_count = 0; if (++anim_send_count == 15) { anim_send_count = 0; mp_skater_animation_component->PlayPrimarySequence(m_skater_anim, true, target_time, 1000.0f, Gfx::LOOPING_CYCLE, 0.0f, 0.0f); } else { mp_skater_animation_component->PlayPrimarySequence(m_skater_anim, false, target_time, 1000.0f, Gfx::LOOPING_CYCLE, 0.0f, 0.0f); } } } /******************************************************************/ /* */ /* */ /******************************************************************/ const int num_edges = 12; Mth::Vector CVehicleComponent::wheel_point ( const SWheel& wheel, int i, bool side ) const { Mth::Vector point( (side ? 1.0f : -1.0f) * 3.85f, wheel.radius * cosf(2.0f * 3.141592f * i / num_edges + wheel.orientation), wheel.radius * sinf(2.0f * 3.141592f * i / num_edges + wheel.orientation) ); Mth::Matrix steering_rotation(Mth::Vector(0.0f, 1.0f, 0.0f), wheel.steering_angle); point = steering_rotation.Rotate(point); point[Y] += wheel.y_offset; point += wheel.pos; point = m_pos + m_orientation_matrix.Rotate(point); return point; } /******************************************************************/ /* */ /* */ /******************************************************************/ void CVehicleComponent::draw_debug_rendering ( ) const { if (m_draw_debug_lines) { for (int n = m_num_wheels; n--; ) { SWheel& wheel = mp_wheels[n]; int32 color = 0; switch (wheel.state) { case SWheel::UNDER_GRIPPING: color = MAKE_RGB(0, 255, 0); break; case SWheel::GRIPPING: color = MAKE_RGB(0, 0, 0); break; case SWheel::SLIPPING: color = MAKE_RGB(255, 200, 0); break; case SWheel::SKIDDING: color = MAKE_RGB(255, 0, 0); break; case SWheel::HANDBRAKE_THROTTLE: color = MAKE_RGB(150, 0, 150); break; case SWheel::HANDBRAKE_LOCKED: color = MAKE_RGB(0, 150, 150); break; case SWheel::NO_STATE: case SWheel::OUT_OF_CONTACT: default: color = MAKE_RGB(255, 255, 255); break; } for (int i = num_edges; i--; ) { Mth::Vector start = wheel_point(wheel, i, true); Mth::Vector end = wheel_point(wheel, i + 1, true); Gfx::AddDebugLine(start, end, color, color, 1); start = wheel_point(wheel, i, false); end = wheel_point(wheel, i + 1, false); Gfx::AddDebugLine(start, end, color, color, 1); start = wheel_point(wheel, i, true); end = wheel_point(wheel, i, false); Gfx::AddDebugLine(start, end, color, color, 1); #if 1 // draw axis of rotation lines Mth::Matrix steering_rotation(Mth::Vector(0.0f, 1.0f, 0.0f), wheel.steering_angle); start.Set(-240.0f, 0.0f, 0.0f); start = steering_rotation.Rotate(start); start += wheel.pos; start[Y] += wheel.y_offset; start = m_pos + m_orientation_matrix.Rotate(start); end.Set(240.0f, 0.0f, 0.0f); end = steering_rotation.Rotate(end); end += wheel.pos; end[Y] += wheel.y_offset; end = m_pos + m_orientation_matrix.Rotate(end); Gfx::AddDebugLine(start, end, MAKE_RGB(0, 0, 0), MAKE_RGB(0, 0, 0), 1); #endif } } // END loop over wheels } #if 1 if (m_draw_debug_lines) { Gfx::AddDebugStar(m_pos + m_suspension_center_of_mass_world, 12.0f, MAKE_RGB(100, 0, 100), 1); Gfx::AddDebugStar(m_pos, 12.0f, MAKE_RGB(0, 100, 100), 1); } #endif } }