Now we are ready to get into the actual dynamics classes.
Putting Wheels on Our Car
The first of these classes will be the Wheel class, which represents one wheel of a vehicle and its associated suspension. All aspects of the wheel are tracked and manipulated through this class. The attributes for the Wheel class are found in Listing 10 “4. The class includes an enumeration that declares which corner of the vehicle the wheel belongs in. As you can see from the enumeration, this class assumes a four-wheeled vehicle. If we wanted to support tractor- trailers with this class, we would need to expand the enumeration.
Listing 10.4: Wheel Class Attributes using System; namespace VehicleDynamics { public enum WhichWheel { LeftFront=0, RightFront, LeftRear, RightRear }; public struct Wheel { public Vector offset; public Vector earth_location; private double rel_heading; public double radius; public double ground_height; public double altitude; public double height_above_ground; private double weight_over_wheel; private double static_weight_over_wheel; public double suspension_offset; public double max_suspension_offset; private double upwards_force; public double friction; private double stiction; private double sliding_friction; public bool bottomed_out; public bool touching_ground; public bool squealing; public bool sliding; public bool drive_wheel; public WhichWheel position;
The offset attribute denotes the position of the wheel relative to the center of gravity of the vehicle. The earth_location attribute is the wheel location relative to the terrain frame of reference. We need this position when we want to query the terrain for the height of the ground beneath the wheel. The rel_heading attribute is the relative heading between the vehicle and the wheel itself. The Car class would adjust this if this were one of the front wheels as a function of the steering actions.
The origin for the wheel is at its point of rotation in the center. The radius attribute lets us know how far below this point we would be contacting the ground. The ground_height attribute is the height of the ground beneath the wheel. The vehicle s position plus the earth_location offset minus the radius gives us the altitude attribute. Together with the ground_height attribute, it can determine if the wheel is in contact with the ground, off of the ground, or currently pressed into the ground (which would demand additional force upwards on the suspension).
We need to figure out the various forces working on the wheel. One of the most important considerations is how much weight is pressing down on the wheel. Two attributes are used to represent this. The static_weight_over_wheel attribute represents the portion of the vehicle s weight the wheel supports when the vehicle is at rest. The weight_over_wheel attribute, on the other hand, is the amount of weight that the wheel is currently supporting. You will see later in this chapter how external forces working on the vehicle can change the distribution of weight on the wheels.
The next set of attributes to consider involves the forces that oppose the force of the weight. These are the suspension forces. The suspension_offet attribute represents the current position of the suspension referenced from its static position. This attribute can go both positive and negative, since the spring can be either stretched or compressed depending on the forces working against it. The max_suspension_off set attribute represents the limit to which the suspension can be stretched or compressed. The upwards_force attribute is the force created by the suspension. This is normally in the upward direction opposing the weight of the vehicle, unless the vehicle has lost contact with the ground. If contact has been lost, there would be a negative force until the suspension reached its static position or contact is restored.
The final forces acting on the wheel are the friction forces. Three types of friction are involved:
-
The friction of the surface that the wheel is moving over is represented by the friction attribute.
-
The static friction of the wheel is represented by the stiction attribute.
-
Finally, a reduced friction value occurs if the wheel has lost adhesion to the ground surface and is now sliding across the surface. This attribute is called sliding_friction .
All of these friction values are expressed as the number of gravities (Gs) of force that the friction supplies .
A number of status flags are also held within the structure. The bottomed_out flag is true if the suspension has been compressed to its limit and may be used by the game code to trigger an appropriate sound effect. The touching_ground flag is true if the wheel is in contact with the ground. The squealing attribute is also made available for use in triggering sound effects. If the wheel is on the verge of sliding, it will start to make a squealing sound. If it has broken loose, the sliding flag is set. There is also a flag that indicates whether this wheel is a drive wheel or not. If the car is front-wheel drive, this flag will be set for the front tires and cleared for the rear. Finally, the WhichWheel enumeration value indicates the wheel s position on the vehicle.
Several properties for the Wheel class, shown in Listing 10 “5, enforce readonly or write-only status for several of the attributes. The RelHeading property provides write-only access to the corresponding attribute. On the other hand, the UpwardsForce property is read only. The Stiction property not only sets the stiction attribute, but also sets sliding_friction to 60 percent of the static friction.
Listing 10.5: Wheel Class Properties #region Properties public double RelHeading { set { rel_heading = value; } } public double UpwardsForce { get { return upwards_force; } } public double WeightOverWheel { set { weight_over_wheel = value; } get { return weight_over_wheel; } } public double StaticWeightOverWheel { set { static_weight_over_wheel = value; } } public double Stiction { set { stiction = new_value; sliding_friction = 0.6f * stiction; } } #endregion
The constructor for the Wheel structure (shown in Listing 10 “6) is fairly straightforward. It simply initializes each of the attributes to a reasonable initial value. The vehicle class that has the wheels must set most of these values. That class (or a parent class that holds an instance of the class) will have the knowledge to set them to the proper values for that vehicle. We will assume that the vehicle begins with all of the wheels touching the ground.
Listing 10.6: Wheel Class public Wheel(WhichWheel where) { position=where; offset = new Vector (0.0, 0.0, 0.0); earth_location = new Vector(0.0, 0.0, 0.0); rel_heading = 0.0; radius = 0.5; circumference = 0.0; height_above_ground = 0.0; rpm = 0.0; ground_height = 0.0; friction = 1.0; weight_over_wheel = 0.0; static_weight_over_wheel =0.0; suspension_offset = 0.0; max_suspension_offset = 0.25; altitude = 0.0; bottomed_out = false; drive_wheel = false; sliding = false; sliding_friction = 0.0; squeeling = false; stiction = 1.0; upwards_force = 0.0; touching_ground = true; }
The method that performs that actual work for the wheels is the Process method shown in Listings 10 “7a through 10 “7e. Listing 10 “7a shows the arguments to the method as well as the calculations of the wheel s position. The first argument ( delta_t ) is the amount of time that has passed since the last time this method has been called. It will be used for integrating values as a function of time. The remaining arguments provide the status of the vehicle that the wheel belongs to. This includes the vehicle s attitude, position velocities, and accelerations.
Listing 10.7a: Wheel Class Process Method public void Process (float delta_t, Euler attitude, Vector acceleration, Vector velocity, Vector position) { double temp; double susp_delta; double squeel_force; double slide_force; double grab_force; double tire_side_force; earth_location.X = offset.X; earth_location.Y = offset.Y; earth_location.Z = offset.Z + suspension_offset; attitude.RotateAtoE(earth_location); altitude = position.Z + earth_location.Z radius; height_above_ground=altitudeground_height; touching_ground = height_above_ground <= 0.0f;
Listing 10.7b: Wheel Class Process Method if (touching_ground) { suspension_offset = height_above_ground; } else { suspension_offset = max_suspension_offset; susp_delta = (suspension_offset + height_above_ground) * delta_t; if (Math.Abs(upwards_force weight_over_wheel) < 2.0) { suspension_offset = susp_delta; } if (suspension_offset > max_suspension_offset) { suspension_offset=max_suspension_offset; { else if (suspension_offset < max_suspension_offset) { suspension_offset = max_suspension_offset; } bottomed_out = suspension_offset == max_suspension_offset;
Listing 10.7c: Wheel Class Process Method temp = (0.9f * (suspension_offset * suspension_offset) / (max_suspension_offset * max_suspension_offset)); if (suspension_offset < 0.0f) { temp *= 1.0f; } if (Math.Abs(suspension_offset) < 0.3f) { temp = 0.0f; // Suspension neutral } temp += 1.0f; if (!touching_ground) { temp = 0.0f; } upwards_force = static_weight_over_wheel * temp;
Listing 10.7d: Wheel Class Process Method if ((upwards_force weight_over_wheel) > 2. 0f) { suspension_offset = 0.5f * delta_t; } else if ((upwards_forceweight_over_wheel) < 2.0f) { suspension_offset += 0.5f * delta_t;
Listing 10.7e: Wheel Class Process Method (Conclusion) slide_force = 32.0f * stiction * friction; squeel_force=0.9f * slide_force; grab_force = 32. 0f * sliding_friction; if ((acceleration. Y > 0.0f && rel_heading > 0.0f) (acceleration.Y < 0.0f && rel_heading < 0.0f)) { tire_side_force = (float)Math.Abs (acceleration.Y * (1.0f Math.Cos (rel_heading))); } else tire_side_force = (float)Math.Abs(acceleration.Y * Math.Cos (rel_heading)); } squeeling=false; if (drive_wheel && acceleration.X >= slide_force) { sliding = true; } if ((acceleration.X < -squeel_force && acceleration.X > slide_force) (tire_side_force > squeel_force && tire_side_force < slide_force)) { squeeling = true; } if (acceleration.X <= slide_force tire_side_force >= slide_force) { sliding = true; } if (Math.Abs(acceleration.X) < grab_force && Math.Abs(acceleration.Y)< grab_force && tire_side_force < grab_force) { sliding = false; } } }; }
Taking the body-relative offsets and the current suspension offset and rotating it into earth frame using the vehicle s current attitude calculates the location of the wheel in the terrain frame of reference. The altitude at the base of the tire is this offset applied to the vehicle s height minus the radius of the wheel. The height above the ground is the difference between the ground elevation and the bottom of the tire. The tire is touching the ground if the height above the ground is less than or equal to zero.
The next section of the Process method (shown in Listing 10 “7b) deals with determining the new suspension offset. If the wheel is touching the ground, the offset will be the inverse of the height above ground. In other words, we will compress the suspension enough that the base of the wheel will be resting on the surface of the ground in the next frame. If the wheel has lost contact with the ground, the weight of the wheel will extend the suspension to its negative limit. A suspension delta is calculated as the integrated sum of the suspension offset and the height above the ground. This delta is applied if the upward force applied to the vehicle is at least 2 pounds greater than the weight of the vehicle applied to the wheel. This will give us a small increase in the suspension extension to help balance out the forces. The suspension offset is limited to the range of plus and minus the maximum suspension offset. The suspension on the wheel has bottomed out if it has reached the positive limit of its movement.
The next section of the method (shown in Listing 10 “7c) is the simplified calculation of the force that the wheel is exerting on the vehicle. The temp value represents the fractional portion of the vehicle s weight that is being countered by the wheel and its suspension. It is calculated as 90 percent of the ratio of the suspension offset squared to the maximum offset squared. Squaring the suspension offset removes the direction of the offset. If the offset is negative, the value of temp is negated. If the suspension is roughly centered, temp will be set to zero, since this is the offset from the static force required to hold up the vehicle. One is then added to the value to get the complete percentage of the base force that will be applied this time. If the wheel is not in contact with the ground, then there will be no upward force applied by the wheel. The actual force is the static weight over the wheel multiplied by the value of temp .
The next section of the method (shown in Listing 10 “7d) is a second adjustment to the suspension offset based on the new upward force. If the upward force is at least 2 pounds greater than the actual weight over the wheel, then the suspension is extended at the rate of .5 per second. Likewise, if the weight is at least 2 pounds greater, then the upward force the suspension is compressed at the same rate.
The final section of the Process method (shown in Listing 10 “7e) deals with the forward and lateral forces applied to the wheel and the interaction of the wheel with the ground. The force required to make the wheel slide is the product of the ground friction and the tire static friction converted from Gs to acceleration. The wheel will begin to squeal at 90 percent of the sliding value. The acceleration at which the wheel will stop sliding is the sliding friction converted from Gs to acceleration.
The side force applied to the tire is a function of the lateral acceleration of the vehicle and any relative heading applied to the wheel. If the wheel is turning into the direction of the acceleration, the wheel is turning somewhat with the force, and the force is reduced based on the amount of the relative heading. If the tire is steering away from the force, the tire sees much more of the acceleration as side force.
If the wheel is a drive wheel and the forward acceleration is greater than the slide force, the tire will start to slide (i.e, laying rubber, spinning, etc.). If the forward acceleration is between the squeal force level and the sliding force level or the side force is between those same extremes, then the tire will squeal. If either the forward or the lateral forces exceed the slide force level, then the wheel will be sliding rather than gripping the surface. If any of these accelerations drop below the grab force threshold, the wheel will grab the surface again and stop sliding.
Simulating the Car
That completes our look at the Wheel structure. Now we get down to the serious work of vehicle dynamics with the CarDynamics class. The attributes and enumerations for this class are shown in Listings 10 “8a and 10 “8b. The IgnitionState enumeration in Listing 10 “8a defines the current position of the key in the ignition. This allows the class to support the ability to start and stop the vehicle s engine. The second enumeration, GearState , defines the state of the transmission. Our sample game will only utilize the drive gear of the transmission, but the class will support either an automatic or three-speed manual transmission.
Listing 10.8a: CarDynamics Enumerations and Attributes |
using System; using System.Threading; using System.Diagnostics; namespace VehicleDynamics { public class CarDynamics : IDisposable { public enum IgnitionState { IgnitionOff, IgnitionOn, IgnitionStart }; public enum GearState { Park=0, Reverse=1, Neutral=2, Drive=3, FirstGear=4, SecondGear=5, ThirdGear=6 }; #region Attributes private Euler attitude = new Euler(); private Euler attitude_rate = new Euler(); private Euler ground_slope = new Euler(); private Vector position = new Vector(); private Vector velocity = new Vector(); private Vector earth_velocity = new Vector(); private Vector acceleration = new Vector(); private Vector body_gravity = new Vector(); private Wheel[] wheel = new Wheel[4]; private double weight; private double cg_height; private double wheel_base; private double wheel_track; private double wheel_angle; private double wheel _max_angle; private double weight_distribution; private double front_weight; private double back_weight; private double wheel_pos; private double throttle; private double brake; private double engine_rpm; private double wheel_rpm; private double wheel_force; private double net_force; private double drag; private double rolling_resistance; private double eng_torque; private double mph;
|
Listing 10.8b: CarDynamics Enumerations and Attributes (Conclusion) private double brake_torque; private double engine_loss; private double idle_rpm; private double max_rpm; private double target_rpm; private double engine_horsepower; private double max_engine_torque; private double air_density; private double drag_coeff; private double frontal_area; private double wheel_diameter; private double mass; // In slugs private double inverse_mass; // In slugs private float centripedal_accel; private bool running; private bool front_wheel_drive = false; private GearState gear; private GearState auto_gear = GearState.Drive; private double[] gear_ratio = new double[7]; // percent max torque index by % max RPM * 10 private LFI torque_curve = new LFI(); private IgnitionState ignition_state = IgnitionState.IgnitionOn; private bool was_sliding = false; private Thread m_process_thread; private bool thread_active = true; private Mutex mutex = new Mutex(); #endregion
The Attitude attribute holds the current heading, pitch, and roll of the vehicle. Attitude_rate is the rate of change of each of these angles. The ground_slope attribute represents the current pitch and roll of the ground under the vehicle in the vehicle s frame of reference. This will be important since the impact of gravity is a function of this slope so that the vehicle will tend to accelerate while going downhill and slow when going uphill .
Five vectors are held by the class. These vectors contain the position, velocities, and accelerations for the vehicle. We also have the gravity vector in the vehicle s body frame as well as an array of four wheels for our car. The weight attribute is the overall weight of the car. The cg_height attribute, which represents the height of the car s center of gravity above the ground, is used in calculating the offset of the wheel s positions based on the car in its static condition. The wheelbase of the vehicle is the side-to-side separation between the wheels, and the wheel track is the forward-and-back separation between the wheels. It is assumed that the wheels are evenly distributed around the center of the vehicle.
The wheel angle is the current relative heading of the front wheels due to steering inputs. The wheel_max_angle attribute represents the limits to which the front wheels may be turned. The weight_distribution attribute is the fraction of the vehicle s overall weight that is over the rear wheels when the vehicle is not moving. Accelerations will result in the actual weight distribution to shift once the vehicle starts moving. These accelerations will cause the front_weight and back_weight to change dynamically. These weights will be divided up and passed to each wheel as the weight over that wheel.
The next three attributes are the control values passed in from the parent class that control the basic driving of the vehicle. The wheel_pos attribute is the steering wheel position normalized to 1 with positive steering to the right. The throttle is the gas pedal position normalized, with 0 meaning no depression of the pedal and 1 being pedal to the metal. The brake attribute works the same way for the brake pedal, with 0 being pedal up and 1 being pedal fully depressed.
The engine_rpm is the current speed of the engine. This value is a function of the throttle position and any losses fed back from the interactions with the road. If the vehicle is going uphill, for example, the vehicle will slow and reduce engine speed until the transmission drops to a lower gear. The wheel_rpm indicates how fast the wheels are turning based on the engine RPM and the current transmission gear ratio. The wheel_force specifies the amount of the force developed by the engine that is passed to the wheels to move the vehicle. The engine torque (in the eng_torque attribute) is the force developed by the engine. The net_force attribute, calculated from the summation of all of the forces acting on the vehicle, defines the net force that changes the vehicle s acceleration. The drag attribute, which acts to slow the vehicle due to air impacting the front of the vehicle as it is moving, is one of several forces that act to limit the top speed of a vehicle. Another force acting against the movement of the vehicle is the rolling resistance. This is energy that deforms the tires as they rotate. The mph attribute is the vehicle s speed in the forward direction expressed in miles per hour .
The remaining attributes are shown in Listing 10 “8b. The brake_torque and engine_loss attributes are two forces that act in opposition to the engine torque. The normal RPM when the engine is idling is held in the idle_rpm attribute, and the maximum RPM is contained in the max_rpm attribute. The target_rpm indicates the engine speed that is currently being demanded by the throttle position. The engine_horsepower and max_engine_torque determine how much power the engine can produce.
The next three attributes calculate the aerodynamic drag against the vehicle. The air_density is the thickness of the air that the vehicle is passing through. The standard value for air density at sea level is 14.7 pounds per square inch. The air_density attribute will hold the inverse of this value (0.068). The drag coefficient ( drag_coeff ) is a factor representing the amount of drag that a given shape has when moving through the atmosphere. Without a physical model of our vehicle and a wind tunnel, it is difficult to determine a specific number for this coefficient. Instead, we will just pick a value and adjust it until everything seems realistic. The last factor in the calculation is the frontal area of the vehicle, the cross-section of the vehicle that impacts the air as it moves forward.
The diameter of the wheels is involved in the calculations for wheel torque as well as for passing the radius of the wheel to each wheel for its own calculations. The mass of the vehicle is required to change the forces working on the vehicle into accelerations. Remember that acceleration is force divided by mass. To make the calculation more efficient, we will also hold the inverse of the mass so we can multiply instead of divide. The acceleration that works laterally on the vehicle due to inertia is called centripetal acceleration. Flags will also exist that state whether or not the engine is running and if the vehicle has front-wheel or rear-wheel drive.
There are two instances of the GearState enumeration. The first attribute, gear, is the gear that has been selected by the driver. A second attribute, auto_gear , is the state of the automatic transmission. This is because an automatic transmission shifts between the forward gears as a function of engine RPM and torque. The gear ratios in each of the transmission s gears are held within an array of double-precision variables. There is also an instance of the LFI class, which holds the torque curve for the engine. The data in the curve is the percentage of maximum engine torque as a function of the engine RPM. We also have an instance of the IgnitionState enumeration, which indicates the current state of the engine s ignition. A flag indicates whether the vehicle s tires were sliding on the previous pass.
The final attributes in the class manage a separate processing thread for the class. There is a reference to the thread itself, a flag that controls if the thread should continue to cycle, and a Mutex object for thread-safe data transfers between the thread process and the remainder of the class.
The properties for the CarDynamics class (shown in Listing 10 “9) provide the public interface for the class. All of the control inputs from the containing class will come in through these properties. The SteeringWheel property sets the wheel position as a double-precision value between 1. The Throttle property specifies the gas pedal position and the Brake property does the same for the brake pedal. The Gear property is used for shifting gears and the Ignition property for turning the key. A bit of logic is involved with the Ignition property; it checks the current gear and starts the engine if the Start value is set and the gear is set to Park or Neutral . Likewise, if the Off state is requested the engine is stopped . There are also properties to retrieve the current RPM and running state.
Listing 10.9: CarDynamics Properties |
#region Properties public double SteeringWheel { get { return wheel_pos; } set { wheel_pos = value; } } public double Throttle { get { return throttle; } set { throttle = value; } } public double Brake { get { return brake; } set { brake = value; } } public GearState Gear { get { return gear; } set { gear = value; } } public IgnitionState Ignition { get { return ignition_state; } set { ignition_state=value; if (ignition_state == IgnitionState.IgnitionStart && (gear == GearState.Park gear == GearState.Neutral)) { running = true; } else if (ignition_state == IgnitionState.IgnitionOff) { running=false; } } } public double EngineRPM { get { return engine_rpm; } } public bool EngineRunning { get { return running; } } public double Roll{ get { return attitude.Phi; } set { attitude.Phi=value; } } public double Pitch { get { return attitude.Theta; } set { attitude.Theta = value; } } public double Heading { get { return attitude.Psi; } set { attitude.Psi = value; } } public double North { get { return position.X; } set { position.X = value; } } public double East { get { return position.Y; } set { position.Y = value; } } public double Height { get { return position.Z; } set { position.Z = value; } } public double NorthVelocity { get { return earth_velocity.X; } set { velocity.X = value; } } public double EastVelocity { get { return earth_velocity.Y; } set { velocity.Y = value; } } public double VerticalVelocity { get { return earth_velocity.Z; } set { velocity.Z = value; } } public double ForwardVelocity { get { return velocity.X; } } public double SidewaysVelocity { get { return velocity.Y; } } public double WheelRadius { get { return wheel[(int)WhichWheel.LeftFront].radius; } set { wheel_diameter = value * 2 .0; wheel[(int)WhichWheel.LeftFront].radius = value; wheel[(int)WhichWheel.LeftRear].radius = value; wheel[(int)WhichWheel.RightFront].radius = value; wheel[(int)WhichWheel.RightRear],radius = value;} } public double HorsePower { get { return engine_horsepower; } set { engine_horsepower = value; } } public double LFGroundHeight { set { wheel[(int)WhichWheel.LeftFront].ground_height = value; } } public double RFGroundHeight { set { wheel[(int)WhichWheel.RightFront].ground_height = value; } } public double LRGroundHeight { set { wheel[(int)WhichWheel.LeftRear].ground_height = value; } } public double RRGroundHeight { set { wheel[(int)WhichWheel.RightRear].ground_height = value; } } public double MPH { get { return mph; } } public bool Running { get { return running; } } #endregion
|
Various properties get and set the vehicle s attitude and position within the world. There are also properties to access the vehicle s velocities in both the world frame of reference as well as the body frame.
Note | Notice that we are using North, East, and Vertical instead of X, Y, and Z for the world-related values. The X, Y, and Z values have different meanings in different places. In physics equations, we tend to think of X going to the north, Y moving to the east, and Z moving up. DirectX, on the other hand, uses a different axis convention. To eliminate confusion, we use a public interface that employs world- related terms. |
The property that accesses the size of the wheels assumes that all four wheels of our vehicle are the same size . Therefore, we can return the left-front wheel s radius in the Get method and set all wheels to the same value in the Set method. We also set the diameter of the wheel with the same property. The HorsePower property changes the power of the engine for the vehicle being simulated. The height of the ground under each tire is likely to be different. Because of this, a separate property exists for each wheel. There are also properties to access the vehicle s speed in miles per hour and a flag that indicates whether or not the engine is running.
The constructor for the class (shown in Listing 10 “10) initializes the attributes that do not change over time. This includes the horsepower of the engine as well as the engine s torque curve. It also resets the members of the wheel array so that each wheel is initialized knowing its position on the vehicle. The constructor then calls the Reset method to complete the initialization. Placing the other attribute initialization in a separate method allows us to reset the vehicle to a know state when needed. The final task for the constructor is to create and start the thread that will perform the dynamics processing. The integrations involved in these calculations tend to be very rate dependent. If they are not processed quickly enough, they can become unstable and oscillate out of control. To prevent this from happening, we will run the dynamics processing in a separate thread that will cycle at a nominal 100 hertz.
Listing 10.10: CarDynamics Constructor public CarDynamics() { engine_horsepower = 70.0f; torque_curve.SetDataPoint(0, 0.0f); torque_curve.SetDataPoint(10, 0.13f); torque_curve.SetDataPoint(20, 0.32f); torque_curve.SetDataPoint(30, 0.5f); torque_curve.SetDataPoint(40, 0.72f); torque_curve.SetDataPoint(50, 0.9f); torque_curve.SetDataPoint(60, 1.0f); torque_curve.SetDataPoint(70, 0.98f); torque_curve.SetDataPoint(80, 0.89f); torque_curve.SetDataPoint(90, 0.5f); torque_curve.SetDataPoint(100, 0.13f); wheel[(int)WhichWheel.LeftFront] = new Wheel(WhichWheel.LeftFront); wheel[(int)WhichWheel.RightFront] = new Wheel(WhichWheel.RightFront); wheel[(int)WhichWheel.LeftRear] = new Wheel(WhichWheel.LeftRear); wheel[(int)WhichWheel.RightRear] = new Wheel(WhichWheel.RightRear); Reset(); m_process_thread = new Thread(new ThreadStart(Process)); m_process_thread.Start(); }
The method that will run in the processing thread is the Process method (shown in Listing 10 “11), which has no arguments. Since the thread will be running at about 100 hertz, the time delta will be the inverse of the frequency, or 0.01 seconds per pass. The method will iterate until the thread_active flag becomes cleared. Each pass, it will call the normal Process method and then sleep for 10 milliseconds . It is this idle time produced by the Sleep method that gives us the iteration rate we want.
Listing 10.11: CarDynamics Thread Process Method public void Process() { float delta_t = 0.01f; Debug.WriteLine("car physics thread started"); while (thread_active) { Process (delta_t); Thread.Sleep(10); } Debug.WriteLine("car physics thread terminated"); }
The Reset method (shown in Listing 10 “12) places the state of the dynamics into a basic starting condition. The attitude, velocities, and accelerations are all reset to zero. The maximum wheel angle defaults to 40 degrees. Since all angular work within the software is done in radians, we will set it to the radian equivalent of 40 degrees. The maximum RPM for the engine will be set to 7000 with the idle speed set to 10 percent of this maximum. The current RPM will be set to zero since it will be recalculated when the engine starts.
Listing 10.12: CarDynamics Reset Method |
public void Reset() { wheel_max_angle = 0.69813170079773183076947630739545; // 40 degrees idle_rpm = 700. 0f; max_rpm = 7000. 0f; engine_rpm = 0.0f; attitude.Theta = 0.0f; attitude.Phi = 0.0f; attitude.Psi = 0.0f; attitude_rate.Theta = 0.0; attitude_rate.Phi = 0.0; attitude_rate.Psi = 0.0; position.X = 0.0; position.Y = 0.0; position.Z = 0.0; velocity.X = 0.0; // Body velocity velocity.Y = 0.0; // Body velocity velocity.Z = 0.0; // Body velocity earth_velocity.X = 0.0; // Earth velocity earth_velocity.Y = 0.0; // Earth velocity earth_velocity.Z = 0.0; // Earth velocity acceleration.X = 0.0; // Body accelerations acceleration.Y = 0.0; // Body accelerations acceleration.Z = 0.0; // Body accelerations cg_height =2.0f wheel_base = 5.0; wheel_track = 8.0; weight = 2000.0f; wheelRadius = 1.25; wheel[(int)WhichWheel.LeftFront].offset.Set(wheel_track / 2 .0f, wheel_base / 2.0f, cg_height + wheel[(int)WhichWheel.LeftFront].radius); wheel[(int)WhichWheel.RightFront].offset.Set(wheel_track / 2 .0f, wheel_base / 2.0f, cg_height + wheel[(int)WhichWheel.LeftFront].radius); wheel[(int)WhichWheel.LeftRear ].offset.Set( wheel_track / 2 .0f, wheel_base / 2.0f, cg_height + wheel[(int)WhichWheel.LeftFront].radius); wheel[(int)WhichWheel.RightRear ].offset.Set( wheel_track / 2 .0f, wheel_Base / 2.0f, cg_height + wheel[(int)WhichWheel.LeftFront].radius); for (int i=0; i<4; i++) { wheel[i].SetStaticWeightOverWheel(weight / 4.0f); } weight_distribution = 0.5f; front_weight = weight * (1.0 weight_distribution); back_weight = weight * weight_distribution; wheel_pos = 0.0f; throttle = 0.0f; brake = 0.0f; engine_rpm = 0.0f; wheel_rpm = 0.0f; wheel_force = 0.0f; net_force = 0.0f; mph = 0.0f; drag = 0.0f; rolling_resistance = 0.0f; eng_torque = 0.0f; brake_torque = 0.0f; engine_loss = 0.0f; air_density = 0.068; drag_coeff = 0.4f; frontal_area = 20.0f; mass = weight * 0.031080950172; // In slugs inverse_mass = 1.0 / mass; running = true; front_wheel_drive = false; gear = GearState.Drive; gear_ratio[(int)GearState.Park] = 0.0f; gear_ratio[(int)GearState.Reverse] = 80.0f; gear_ratio[(int)GearState.Neutral] = 0.0f; gear_ratio[(int)GearState.Drive] = 45.0f; gear_ratio[(int)GearState.FirstGear] = 70.0f; gear_ratio[(int)GearState.SecondGear] = 50.0f; gear_ratio[(int)GearState.ThirdGear] = 30.0f; ignition_state = IgnitionState.IgnitionOn; max_engine_torque = engine_horsepower * 550.0f; if (front_wheel_drive) { wheel[(int)WhichWheel.LeftFront].drive_wheel = true; wheel[(int)WhichWheel.RightFront].drive_wheel = true; wheel[(int)WhichWheel.LeftRear].drive_wheel = false; wheel[(int)WhichWheel.RightRear].drive_wheel = false; } else { wheel[(int)WhichWheel.LeftFront ].drive_wheel = false; wheel[(int)WhichWheel.RightFront].drive_wheel = false; wheel[(int)WhichWheel.LeftRear ].drive_wheel = true; wheel[(int)WhichWheel.RightRear ].drive_wheel = true; } }
|
The height of the center of gravity defaults to 2 feet off of the ground and the wheelbase and track default to 5 feet and 8 feet, respectively. We will also default to a weight of 1 ton, which is 2000 pounds. The offset to each of the wheels is calculated from the wheelbase, wheel track, center of gravity height, and the wheel radius. Since we are assuming that the weight of the vehicle is evenly distributed over the wheels, one quarter of the weight is passed to each wheel as its static weight over the wheel. This also gives us a weight distribution of one half and the associated portions of the weight assigned to the front and rear of the vehicle.
The mass of the vehicle needs to be in slugs for the English system of measurement. The weight is converted to slugs for the mass and inverted for the inverse_mass attribute. We will assume that we reset to a rear-wheel drive vehicle with the engine running and in drive, ready to start racing. The gear ratios are the complete ratio of engine RPM to the corresponding wheel rotation speed. This includes the gearing in the rear hub with the transmission ratios. Note that the value for reverse is negative to reverse the direction of the wheel rotation. The final action in the method is to inform each of the wheels whether or not they are a drive wheel based on the front_wheel_drive flag.
The IntegratePosition method (shown in Listing 10 “13) performs the integrations that change accelerations into velocities and velocities into modifications in position. This method of integration is known as Euler s method. Since acceleration is feet per second squared and we are multiplying by seconds per pass, we get resulting units of feet per second per pass. Since feet per second represents a velocity, we can add this product to our velocity last pass to get our new velocity. The same holds true for taking our attitude rate in radians per second and integrating with our time delta to get the number of radians each attitude angle changes in a frame. Since we want to ensure that our attitude angles remain in the proper range, we will call the Attitude class s Limit method to handle any integration through zero or the maximum value for a given attitude angle.
Listing 10.13: CarDynamics IntegratePosition Method private void IntegratePosition (float delta_t) { velocity.IncrementX(delta_t * acceleration.X); velocity.IncrementY(delta_t * acceleration.Y); velocity.IncrementZ(delta_t * acceleration.Z); attitude = attitude + (attitude_rate * delta_t); attitude.Limits(); earth_velocity.X = velocity.X; earth_velocity.Y = velocity.Y; earth_velocity.Z = velocity.Z; attitude.RotateAtoE(earth_velocity); position.IncrementX(delta_t * earth_velocity.X); position.IncrementY(delta_t * earth_velocity.Y); position.IncrementZ(delta_t * earth_velocity.Z); mph = (float)velocity.X * 3600.0f / 5280.0f; }
The terms in the velocity vector are in the vehicle s frame of reference. Before we can integrate our position in the world, we must rotate this vector into the world coordinate system. The vector is copied into another vector that is rotated based on the vehicle s attitude. The earth velocity is integrated to get the change in world position. We will also calculate the miles per hour version of our velocity by taking the forward velocity and converting it from feet per second to miles per hour.
The net force that acts on the vehicle is not being applied through the vehicle s center of gravity (CG). It is actually applied at the interface between the wheels and the ground. Because the force is not being applied at the CG, there is an apparent transfer of the vehicle s weight in both the forward and lateral directions. I m sure you have experienced this while driving in a car. If the car is accelerated rapidly , the front of the car rises. If you slam on the brakes, the nose of the car dips. If you make a quick turn, the car rolls toward the outside of the turn . These are all examples of weight transfer due to the accelerations acting on the car. Remember that weight is an acceleration. The weight transferred to the front of the vehicle is the forward acceleration multiplied by the ratio of the center of gravity height and the wheelbase. This value is added to the front of the vehicle and subtracted from the rear. The same calculation holds true for the lateral acceleration. Instead of dividing by the wheelbase, though, we will use the wheel track since it is the distance between the tires in the lateral direction. Using these relationships, we can calculate the weight over each of the four tires and pass the values to the associated wheel (see Listing 10 “14).
Listing 10.14: CarDynamics CalcWeightTransfer Method private void CalcWeightTransfer() { front_weight = (1.0f weight_distribution) * weight + (float)acceleration.X * cg_height / wheel_base; back_weight = weight front_weight; wheel[(int)WhichWheel.LeftFront].weight_over_wheel = 0.5f * front_weight (float)acceleration.Y * cg_height / wheel_track; wheel[(int)WhichWheel.RightFront].weight_over_wheel = front_weight wheel [(int)WhichWheel.LeftFront].weight_over_wheel; wheel [(int)WhichWheel.LeftRear].weight_over_wheel = 0.5f * back_weight (float)acceleration.Y * cg_height / wheel_track; wheel[(int)WhichWheel.RightRear].weight_over_wheel = back_weight wheel[(int)WhichWheel.LeftRear].weight_over_wheel; }
The SetFriction method (shown in Listing 10 “15) provides the interface to set the friction of the surface under a specific wheel. The arguments to the method state which wheel is being set and what the new value should be. This value is simply passed to the requested wheel.
Listing 10.15: CarDynamics SetFriction Method public void Set Friction (WhichWheel the_wheel, float friction) { wheel[(int)the_wheel].friction = friction; }
The SetGearRatio method (shown in Listing 10 “16) provides a programmatic way to change the gears in the transmission. This is how the initialization of a class that uses the CarDynamics can tailor the transmission for a specific vehicle. The combination of different horsepower engine and different gear ratios can be used to add vehicles with varying performance.
Listing 10.16: CarDynamics SetGearRatio Method public void SetGearRatio(GearState state, float ratio) { gear_ratio[(int)state] = ratio; }
The current position of a given wheel may be queried using the WheelNorth method shown in Listing 10 “17. The enumeration value for a wheel is passed in as an argument and the position in the north axis is returned.
Listing 10.17: CarDynamics WheelNorth Method public float WheelNorth(WhichWheel the_wheel) { return (float)wheel[(int)the_wheel].earth_location.X; }
The east position of a wheel is obtained the same way using the WheelEast method and the vertical position of the wheel with the WheelHeight method. Both of these methods are shown in Listing 10 “18.
Listing 10.18: CarDynamics WheelEast Method public float WheelEast(WhichWheel the_wheel) { return (float)wheel[(int)the_wheel].earth_location.Y; } public float WheelHeight (WhichWheel the_wheel) { return (float)wheel [(int)the_wheel].earth_location.Z; }
The height of the ground beneath a wheel is set using the SetWheelAltitude method shown in Listing 10 “19. The altitude and the selection of which wheel to set are passed in as arguments. The enumeration value is used as an index into the wheel array and the associated wheel s ground height is set to the supplied altitude.
Listing 10.19: CarDynamics SetWheelAltitude Method public void SetWheelAltitude(WhichWheel the_wheel, float altitude) { wheel[(int)the_wheel].ground_height = altitude; }
The SetWheelOffset method (shown in Listing 10 “20) adjusts the position of each wheel independently. The three supplied offsets are set within the requested wheel s data structure.
Listing 10.20: CarDynamics SetWheelOffset Method public void SetWheelOffset(WhichWheel the_wheel, float forward, float right, float up) { wheel[(int)the_wheel].offset.X = forward; wheel[(int)the_wheel].offset.Y = right; wheel[(int)the_wheel].offset.Z = up; }
The Dispose method (shown in Listing 10 “21) has only one task. Its job is to terminate the processing thread. By setting the thread_active flag to false, the thread will terminate itself when it reaches the end of its while loop. A 20-millisecond sleep is included before the Dispose methods terminates. This is to ensure that the thread has time to terminate before we deallocate the class by passing out of scope.
Listing 10.21: CarDynamics Dispose Method public void Dispose() { Debug.WriteLine("car physics Dispose"); thread_active=false; Thread.Sleep(20); }
Note | If we fail to use Dispose on a class like this one, which has spawned a thread, it is possible that the thread will not get the message that the application has terminated, and the thread will continue. This will cause the application to remain partially active. |
The Process method (shown in Listings 10 “22a through 10 “22e) is where the real work of the vehicle dynamics is done. Among the local variables declared for this method is the gravity vector, which is the acceleration in the down direction that will be applied to the vehicle in addition to the other forces that affect the vehicle. The method begins, as shown in Listing 10 “22a, by processing the steering of the vehicle. The angular offset of the front wheels is a function of the current steering wheel position and the maximum turn single for the wheels. This wheel angle is passed to each of the front wheels as their new relative heading. The turn rate for the vehicle is a function of this wheel angle, the distance between the wheels, and the current forward speed of the vehicle. If both of the front wheels are sliding, then we have lost the ability to steer the vehicle, and the turn rate is set to zero. The Greek symbol used to represent heading is psi. The attitude rate for psi is set to this turn rate.
Listing 10.22a: CarDynamics Process Method |
void Process (float delta_t) { double temp; double delta_rpm; double current_gear_ratio = 0.0; double brake_force; double percent_rpm; double turn_rate; bool shift_up; bool shift_down; double delta_psi; Vector gravity = new Vector (0.0f, 0.0f, 32.0f); wheel_angle = wheel_pos * wheel_max_angle; wheel[(int)WhichWheel.LeftFront].SetRelHeading(wheel_angle); wheel[(int)WhichWheel.RightFront].SetRelHeading(wheel_angle); turn_rate = (Math.Sin(wheel_angle) * velocity.X / wheel_track) / 10.0f; if (wheel[(int)WhichWheel.LeftFront].sliding && wheel[(int)WhichWheel.RightFront].sliding) { turn_rate = 0.0f; } attitude_rate.Psi = turn_rate; delta_psi = turn_rate * delta_t; centripedal_accel = (float) (2.0 * velocity.X * Math.Sin(delta_psi)) / delta_t; wheel_rpm = 60.0f * velocity.X / (Math. PI * wheel_diameter); rolling_resistance = 0.696f * (float)Math.Abs(velocity.X); drag = 0.5f * drag_coeff * frontal_area * air_density * Math.Abs(velocity.X * velocity.X); brake_force = brake * 32.0; // Max braking 1G if (mph < 0.0) { brake_force *= 1.0; } if (wheel[(int)WhichWheel.LeftFront].sliding && wheel[(int)WhichWheel.RightFront].sliding && wheel[(int)WhichWheel.RightRear].sliding && wheel[(int)WhichWheel.RightRear].sliding) { brake_force = 0.0f; }
|
The centripetal acceleration is a function of the forward speed and the turn rate. The rotational speed of the wheels is a function of the circumference of the wheel and the forward speed of the vehicle. This assumes that the wheels are not slipping. If the wheels were slipping, there would no longer be a direct relationship between forward velocity and tire rotational speed. Instead, the relationship would be between engine RPM and tire rotational speed. The rolling resistance is also a function of the forward velocity. Since the direction of the velocity doesn t affect the resistance, only the magnitude, we can calculate the resistance as the product of a resistance coefficient and the absolute value of the velocity. Likewise, the aerodynamic drag is a function of the drag coefficient, the frontal area of the vehicle, the air density, and the square of the forward velocity. The vehicle s brakes represent another force that will affect the speed of the vehicle. The acceleration that will be applied by the brakes is a function of the brake pedal position and a maximum braking acceleration of 32 feet per second (1 G of acceleration). If the vehicle is traveling backward, the braking force is negated. If all of the wheels are sliding, there is no braking force, and the acceleration is set to zero.
The next section of code, shown in Listing 10 “22b, concerns the simulation of the vehicle s transmission. The first thing to do in this section of code is to determine the current percentage of the maximum engine RPM. The percentage of maximum RPM will come into play when deciding the proper time for the automatic transmission to shift. A switch statement based on the current gear determines what the transmission will do. If Park is selected, there will be maximum braking force until the vehicle is almost completely stopped, and then the velocity will be set to zero to complete the process. If Reverse is selected, the associated gear ratio is selected, and the automatic gear is set to the same value. The same holds true for Neutral . If the transmission is in Drive , we must be using an automatic transmission. This means that the transmission will automatically shift up and down through the forward gears as necessary. If the engine RPM is in the top 20 percent of its range, and the automatic gear is less than its top gear, the shift_up flag is set. Likewise, if the automatic gear is in Neutral and the engine is running faster them idle, it will also shift up. The transmission will downshift if the percent RPM drops below 40 percent and the automatic gear is greater than first gear.
Listing 10.22b: CarDynamics Process Method |
percent_rpm = engine_rpm / max_rpm; switch (gear) { case GearState.Park: if (mph > 1.0 mph < 1.0) { brake_force = 32.0; } else { velocity.SetX(0.0f); } auto_gear = GearState.Park; break; case GearState.Reverse: auto_gear = GearState.Reverse; break; case GearState.Neutral: auto_gear = GearState.Neutral; break; case GearState.Drive: shift_up = false; shift_down = false; if ((percent_rpm > 0.8 && auto_gear < GearState.Drive) (percent_rpm > 0.1 && auto_gear == GearState.Neutral)) { shift_up = true; } if (percent_rpm < 0.4 && auto_gear >= GearState.FirstGear) { shift_down = true; } switch (auto_gear) { case GearState.Neutral: if (shift_up) { auto_gear = GearState.FirstGear; } break; case GearState.Drive: if (shift_down) { auto_gear = GearState.ThirdGear; } break; case GearState.FirstGear: if (shift_up) { auto_gear = GearState.SecondGear; } else if (shift_down) { auto_gear = GearState.Neutral; } break; case GearState.SecondGear: if (shift_up) { auto_gear = GearState.ThirdGear; } else if (shift_down) { auto_gear = GearState.FirstGear; } break; case GearState.ThirdGear: if (shift_up) { auto_gear = GearState.Drive; } else if (shift_down) { auto_gear = GearState.SecondGear; } break; } break; case GearState.FirstGear: auto_gear = GearState.FirstGear; break; case GearState.SecondGear: auto_gear = GearState.SecondGear; break; case GearState.ThirdGear: auto_gear = GearState.ThirdGear; break; } current_gear_ratio = gear_ratio[(int)auto_gear];
|
Embedded within the Drive state is a second switch based on the automatic gear. If the transmission is currently in Neutral and we are shifting up, the automatic gear is changed to first. It is changed to third gear if we are in Drive and downshifting. If the transmission is in First gear, we can either shift up to Second or downshift to Neutral . If the transmission is in Second gear, we can either shift up to Third or downshift to First . Finally, if the transmission is in Third , we can shift up to Drive or downshift into Second . If the manually selected gear is first through third, than the automatic gear is set to match. The current gear ratio is selected from the gear ratio array based on the current automatic gear.
The next section of the Process method (shown in Listing 10 “22c) deals with the speed of the engine and the force that it creates. If the engine is running and the engine is not up to idle speed yet, then we will set idle RPM as our target. On the other hand, if the engine has been turned off, we need to target zero RPM. If neither of these conditions is met, then the target RPM is based on the gas pedal position, demanding an RPM somewhere between idle and maximum RPM. The delta RPM is the difference between the current engine speed and this target RPM. We will limit the change in RPM to 3000 RPM per second. This delta is integrated into the current engine RPM using the time delta.
Listing 10.22c: CarDynamics Process Method if (running && target_rpm < idle_rpm) { target_rpm = idle_rpm; } else if (!running) { target_rpm = 0.0f; } else { target_rpm = idle_rpm + throttle * (max_rpm idle_rpm); } delta_rpm = target_rpm engine_rpm; if (delta_rpm > 3000.0f) { delta_rpm = 3000.0f; } else if (delta_rpm < 3000.0f) { delta_rpm = 3000.0f; } if (delta_rpm < 1.0f && delta_rpm > 1.0f) { delta_rpm = 0.0f; } engine_rpm += (delta_rpm * delta_t); if (auto_gear == GearState.Neutral gear == GearState.Park) { eng_torque = 0; } else { eng_torque = torque_curve.Interpolate(percent_rpm * 100.0) * max_engine_torque; } engine_loss = Math.Max(((engine_rpm/20) * (engine_rpm/20) + 45), 0.0); brake_torque = brake_force * mass; temp = (eng_torque engine_loss brake_torque); if (temp < 0.0 && Math.Abs(mph) < 0.1) { temp = 0.0; } if (current_gear_ratio != 0.0) { wheel_force = temp; } else { wheel_force = 0.0f; }
If the transmission is in either Park or Neutral , the engine will not produce any torque. Otherwise, we will get the engine s torque from the torque curve LFI based on the engine RPM and the engine s maximum torque. Counteracting the engine torque will be the losses within the engine and the braking force. The engine loss is the sum of a constant loss as well as a portion that increases with engine speed. The braking losses are calculated from the braking acceleration we determined earlier. Since force is mass times acceleration, we need to multiply by the vehicle mass to get the braking torque. An intermediate force is calculated by subtracting these two losses from the engine torque. If this intermediate force is negative, the losses exceed the engine torque, and the speed is close to zero, we will zero this force for stability. If the gear ratio is nonzero, this will become our wheel force. Otherwise, our wheel force is zero, since no force is connecting through to the drive wheels.
The next section of the method (shown in Listing 10 “22d) covers the calculation of the net force working on the vehicle. The net force is the wheel force minus the drag and rolling resistance. If the transmission is in reverse, we will negate the force so that it acts in the opposite direction. The next step is to determine the effect of gravity on the vehicle. For this, we rotate a gravity vector from earth to vehicle reference frame. If the car is not in park, we will apply the forces to the vehicle s forward acceleration. We need to divide the net force by the vehicle mass to get the resulting acceleration. From this, we will subtract the braking acceleration and add in the acceleration due to gravity. The vertical component of the gravity is applied to the vertical acceleration. We will assume that the lateral component of gravity is not enough to make a difference and will ignore it. If this iteration s portion of the braking force is greater than the current forward velocity, then we will zero the velocities and accelerations to bring the car to a complete stop.
Listing 10.22d: CarDynamics Process Method net_force = wheel_force drag rolling_resistance; if (gear == GearState.Reverse) { net_force *= 1.0f; // Force in reverse is in opposite direction. } ground_slope.RotateEtoA(gravity); body_gravity=gravity; if (gear != GearState.Park) { acceleration. X = (net_force / mass brake_force) + body_gravity.X; } acceleration. Z -= body_gravity.Z; if (velocity.X < (delta_t * brake_force) && velocity.X > (delta_t * brake_force)) { mph = 0.0f; velocity.X = 0.0; acceleration.X = 0.0; brake_force = 0.0; }
The final portion of the method (shown in Listing 10 “22e) closes the loop on the engine speed and calls additional methods to complete the processing. The speed of the engine not only creates the power that drives the vehicle, the speed of the vehicle also feeds back to back-drive the speed of the engine. If the transmission is in park or neutral, there is no connection between the wheels and the engine, so the engine speed remains a direct relation to the throttle position. Otherwise, we calculate a value based on the vehicle speed and the current gear ratio. This value is limited between the idle and the maximum RPM, and becomes the new target RPM. To complete the processing, we will call the CalcWeightTransfer method to get the current weight over each wheel. The ProcessWheels method calls the Process method for each wheel and applies the forces from the wheels to the vehicle. The ProcessAttitude method determines the new attitude rate based on the suspension forces. The final step is to call the IntegratePosition method to integrate the new velocity and position.
Listing 10.22e: CarDynamics Process Method if (auto_gear == GearState.Neutral gear == GearState.Park) { temp = idle_rpm + (max_rpm-idle_rpm) * throttle; } else { temp = velocity.X * current_gear_ratio; } if (temp >= (idle_rpm * 0.9f)) { if (temp > max_rpm) { target_rpm = max_rpm; } else { target_rpm = temp; } } else { target_rpm = idle_rpm; } CalcWeightTransfer(); ProcessWheels(delta_t); ProcessAttitude(delta_t); IntegratePosition(delta_t); }
The SetAttitude method (shown in Listing 10 “23) provides the means of setting the attitude from another class. This would typically be used while initializing the vehicle to a new place prior to starting a new level.
Listing 10.23: CarDynamics SetAttitude Method void SetAttitude(float roll, float pitch, float heading) { attitude.Phi = roll; attitude.Theta = pitch; attitude.Psi = heading; }
The SetPosition method and SetVelocity method (shown in Listing 10 “24) handles the positioning and velocity portion of setting the vehicle to a new position and velocity.
Listing 10.24: CarDynamics SetPosition and SetVelocity Methods void SetPosition(float north, float east, float height) { position.X = north; position.Y = east; position.Z = height; } void SetVelocity(float north, float east, float vertical) { velocity.X = north; velocity.Y = east; velocity.Z = vertical; }
There are two methods (shown in Listing 10 “25) for setting the ground height under the wheels. The first is SetGroundHeight , which can be used to set the height under a specified wheel. The second method, SetAllGroundHeights , takes all four elevations as arguments in order to set the values for all four wheels in one call.
Listing 10.25: CarDynamics SetGroundHeight and SetAllGroundHeights Methods void SetGroundHeight (WhichWheel the_wheel, float height) { wheel[(int)the_wheel].ground_height = height; } void SetAllGroundHeights (float left_front, float right_front, float left_rear, float right_rear) { wheel[(int)WhichWheel.LeftFront].ground_height = left_front; wheel[(int)WhichWheel.RightFront].ground_height = right_front; wheel[(int)WhichWheel.LeftRear].ground_height = left_rear; wheel [(int)WhichWheel.RightRear].ground_height = right_rear; }
The WheelAngle method (shown in Listing 10 “26) provides the means of getting the relative angle of the front wheels in either degrees or radians. If the Boolean argument is true, the return value is converted from radians to degrees.
Listing 10.26: CarDynamics WheelAngle Method double WheelAngle(bool in_degrees) { double result; if (in_degrees) { result = (wheel_angle * 180.0 / Math.PI); } else { result = wheel_angle; } return result; }
There are similar methods for accessing pitch and roll shown in Listing 10 “27. The GetPitch method returns pitch in radians or degrees, and the GetRoll method does the same for roll.
Listing 10.27: CarDynamics GetPitch and GetRoll Methods double GetPitch(bool in_degrees) { double result; if (in_degrees) { result = attitude.ThetaAsDegrees; } else { result = attitude.Theta; } return result; } double GetRoll(bool in_degrees) { double result; if (in_degrees) { result = attitude.PhiAsDegrees; } else { result = attitude.Phi; } return result; }
The IsTireSquealing method (shown in Listing 10 “28) provides the ability to query each tire to see if it is currently in a squealing state.
Listing 10.28: CarDynamics IsTireSquealing Method bool IsTireSquealing(WhichWheel the_wheel) { return wheel[(int)the_wheel].squeeling; }
The IsTireLoose method (shown in Listing 10 “29) provides similar access to the flag that indicates an individual tire is sliding.
Listing 10.29: CarDynamics IsTireLoose Method bool IsTireLoose(WhichWheel the_wheel) { return wheel[(int)the_wheel].sliding; }
The SetTireStiction method (shown in Listing 10 “30) is the means of altering the static friction for the wheels. It is assumed that all four tires are the same and have the same friction characteristics.
Listing 10.30: CarDynamics SetTireStiction Method void SetTireStiction(float new_value) { wheel[(int)WhichWheel.LeftFront].SetStiction(new_value); wheel[(int)WhichWheel.RightFront].SetStiction(new_value); wheel[(int)WhichWheel.LeftRear].SetStiction(new_value) wheel[(int)WhichWheel.RightRear].SetStiction(new_value }
The ProcessWheels method (shown in Listing 10 “31) was mentioned in the description of the Process method. This is where we update each wheel and its suspension and the resulting forces that act on the vehicle. The lateral acceleration on the vehicle is the opposite of the centripetal force. The method loops through the four tires in the array. Each wheel s Process method is called. The upward force from the four tires is accumulated along with the suspension offsets and ground heights. If any of the wheels have bottomed out or are touching the ground, a local flag is set to reflect this fact. This method also counts the number of tires that are sliding. After all four wheels have been processed, the accumulated suspension offset and ground height are divided by four to get the average values of each. If the suspension is roughly centered, the vertical speed of the vehicle will be reduced by 80 percent to provide damping. If the force is close to zero, we will zero both the force and the velocity. The acceleration is the force divided by the mass. If the wheels are in contact with the ground, we will divide the vertical velocity by two as further damping of the vertical velocity. If any of the wheels have bottomed out, we will set the elevation of the vehicle to ensure that it remains above the ground. If it was moving down when it bottomed out, the vertical velocity will be zeroed as well. The final portion of the method deals with the lateral accelerations on the vehicle. If more than two wheels are sliding and this is the first pass that this has happened , we will break the tires loose and begin moving laterally. If we are sliding to the right, a 1 G acceleration will be applied to the left, representing the friction of the tires serving to slow the slide. If sliding to the left, the acceleration will be applied to the right. If we aren t sliding to the side, the lateral speed and acceleration will be zeroed because the tires are maintaining their grip on the ground.
Listing 10.31: CarDynamics ProcessWheels Method |
void ProcessWheels(float delta_t) { int i; double accel; double total_upwards_force = 0.0; bool bottomed_out = false; bool on_ground = false; int sliding = 0; double avg_suspension = 0.0; double delta_force; double avg_ground_height = 0.0; acceleration.SetY( centripedal_accel); for (i=0; i<4; i++) { wheel[i].Process(delta_t, attitude, acceleration, velocity, position); total_upwards_force += wheel[i].UpwardsForce(); avg_suspension += wheel[i].suspension_offset; avg_ground_height += wheel[i].ground_height; if (wheel[i].bottomed_out) { bottomed_out = true; } if (wheel[i].touching_ground) { on_ground = true; } if (wheel[i].sliding) { sliding++; } } avg_suspension /= 4.0f; avg_ground_height /= 4.0f; if (Math.Abs(avg_suspension) < 0.1f) { velocity.Z = velocity.Z * 0.2; } delta_force = total_upwards_force weight; if (Math.Abs(delta_force) < 1.0) { delta_force = 0.0; velocity.Z = 0.0; } accel = delta_force / mass; acceleration.Z = accel; if (on_ground) { velocity.Z = velocity.Z * 0.5; } if (bottomed_out) { position.Z = avg_ground_height + wheel[0].offset.X + wheel[0].radius; } if (bottomed_out && velocity.Z < 0.0f) { velocity.Z = 0.0; } if (sliding > 2 && !was_sliding) { was_sliding = true; velocity.Y = acceleration.Y; } if (sliding > 2 && velocity.Y > 0.0) { accelerations = 32.0; } else if (sliding > 2 && velocity.Y < 0.0) { accelerations = 32.0; } else { velocity.Y = 0.0; accelerations = 0.0; was_sliding = false; } }
|
The ProcessAttitude method (shown in Listing 10 “32) determines the attitude rates based on the suspension offsets and the slope of the ground under the vehicle. The method begins by determining the pitch of the ground under the vehicle. The Terrain class has a method for returning ground slope for a given point. Unfortunately, the slope at a single point on the terrain can give erroneous results compared to the average heights of the four points under the wheels. If, for example, there were a sharp crest in the terrain below the vehicle, the tires could be straddling that point. To get a correct value as experienced by the vehicle, we will take the average height below the front wheels and the average height below the rear wheels. The arcsine of the difference between these heights and the wheelbase of the vehicle is the pitch of the terrain as experienced by the vehicle. The roll angle is calculated the same way. In this case, we will use the arcsine of the difference between the average height on the left and the average height on the right, divided by the wheel track. The attitude rates will be a fraction of the difference between the current attitude angles and those that we just calculated.
Listing 10.32: CarDynamics ProcessAttitude Method |
void ProcessAttitude(float delta_t) { double avg_front; double avg_rear; double pitch; double avg_left; double avg_right; double roll; // First do ground slope. avg_front = (wheel[(int)WhichWheel.LeftFront].ground_height + wheel[(int)WhichWheel.RightFront].ground_height) / 2.0; avg_rear = (wheel[(int)WhichWheel.LeftRear].ground_height + wheel[(int)WhichWheel.RightRear].ground_height) / 2.0; pitch = Math.Asin((avg_rear avg_front) / wheel_base); ground_slope.Theta = pitch; avg_left = (wheel[(int)WhichWheel.LeftFront].ground_height + wheel[(int)WhichWheel.LeftRear].ground_height) / 2.0; avg_right = (wheel[(int)WhichWheel.RightFront].ground_height + wheel[(int)WhichWheel.RightRear].ground_height) / 2.0; roll = Math.Asin((avg_right avg_left) / wheel_track); ground_slope.Phi = roll; // Now do vehicle attitude avg_front = (wheel[(int)WhichWheel.LeftFront].suspension_offset + wheel[(int)WhichWheel.RightFront].suspension_offset) / 2.0f; avg_rear = (wheel[(int)WhichWheel.LeftRear].suspension_offset + wheel[(int)WhichWheel.RightRear].suspension_offset) / 2.0f; pitch = Math.Asin((avg_front avg_rear) / wheel_base); attitude_rate.Theta = ((ground_slope.Theta+pitch) attitude.Theta) * 0.025; avg_left = (wheel[(int)WhichWheel.LeftFront].suspension_offset + wheel[(int)WhichWheel.LeftRear].suspension_offset) / 2. 0f; avg_right = (wheel[(int)WhichWheel.RightFront].suspension_offset + wheel[(int)WhichWheel.RightRear].suspension_offset) / 2. 0f; roll = Math.Asin((avg_right avg_left) / wheel_track); attitude_rate.Phi = ((ground_slope.Phi+roll) attitude.Phi) * 0.05; }
|
The MinorCollision method (shown in Listing 10 “33) will be called whenever the vehicle hits something small that will affect the vehicle s speed only slightly ” things like bushes, shrubs, and other small objects. This reduction in speed will be accomplished by making a 10 percent reduction in the vehicle s velocity vector.
Listing 10.33: CarDynamics MinorCollision Method public void MinorCollision() { velocity = velocity * 0.9f; }
When the vehicle hits something major, a MinorCollision method won t be enough. The MajorCollision method (shown in Listing 10 “34) will be used when we hit something that we can t just drive over and continue on our way. In order to react to the object we hit, we need to have some information about the objects involved. We need to know the difference in the velocity vectors of the two objects involved. This will tell us how hard we hit the other object. The other thing we need is the distance vector between the two objects, which gives us the direction between the two objects. Since this library and the game engine use different classes for vectors, we need to pass the components of the vectors individually. The relative velocity vector is built from the velocity components , and a collision normal vector is built from the position delta. Since this needs to be a normal vector, we normalize it so that it will be a unit vector in the direction of the collision.
Listing 10.34: CarDynamics Major Collision Method public void MajorCollision(float delta_x_velocity, float delta_y_velocity, float delta_z_velocity, float delta_x_position, float delta_y_position, float delta_z_position) { Vector RelativeVelocity = new Vector(delta_x_velocity, delta_y_velocity, delta_z_velocity); Vector CollisionNormal = new Vector(delta_x_position, delta_y_position, delta_z_position); CollisionNormal.Normalize(); double collisionSpeed = RelativeVelocity * CollisionNormal; float impulse = (float)((2.50 * collisionSpeed) / ((CollisionNormal * CollisionNormal) * (inverse_mass))); velocity = (CollisionNormal * impulse) * (float) inverse_mass; engine_rpm = idle_rpm; } }; }
Normally when a car hits a solid object, we have what is referred to as an inelastic collision . The energy of the collision is dispersed by the deformation of the car. Unless we want the game to be over on impact, we need to take a step away from reality for the major collision. Instead, we will have what is called an elastic collision . In an elastic collision, the force is expended, driving the objects along the reverse of the normal of the collision. We will in fact exaggerate the force of the collision. This will cause the objects to bounce away from each other.
The first step is to determine how hard the vehicle hit the object. This is the dot product of the relative velocity and the collision normal. The impulse that will be applied to the car would normally be calculated as the impact speed divided by the dot product of the collision normal with itself and divided by the mass of the vehicle. To exaggerate the bounce, we will use two-and-a-half times the force. To convert the impulse into a new velocity for the car, we multiply the impulse by the collision normal vector to put it in the correct direction. We also divide this value by the mass of the vehicle to convert the force to an acceleration that is applied immediately as the vehicle s new velocity.