UnityStandardAsset project, source code analysis_ 2_ Racing game [player control]_ Vehicle core control

Address of the previous chapter: UnityStandardAsset project, source code analysis_ 1_ Racing game [player control]_ input system

In the previous chapter, we learned the general process of vehicle control, and analyzed the input system, that is, those classes were called during the process from the player's handle / mobile phone tilt input to the vehicle core control logic, and how these classes work together. Then in this chapter, we will analyze the most important core control logic of the vehicle, how to use the adjusted input data to calculate the vehicle state of the next frame according to the current vehicle state.

Core control logic

In the previous chapter, we learned that there are three types of vehicle core state control: CarController, CarUserControl, and CarAudio. Among them, CarController is the core control logic, CarUserControl is the user input interface, which is responsible for calling the Move method in CarController to update the status. CarController calls CarAudio and vehicle tire effect controls to achieve various effects.

private void FixedUpdate()
{
    float h = CrossPlatformInputManager.GetAxis("Horizontal");	// Analysis content of the previous chapter
    float v = CrossPlatformInputManager.GetAxis("Vertical");

#if !MOBILE_INPUT
    float handbrake = CrossPlatformInputManager.GetAxis("Jump");
    m_Car.Move(h, v, v, handbrake);	// What this chapter focuses on
#else
    m_Car.Move(h, v, v, 0f);
#endif
}

After analyzing the content of CarUserControl, you can find that every frame of CarUserControl reads data from the input class, and then uses the data to call the Move method of CarController. In this chapter, we will analyze the most important method of CarController, Move, and see what CarController has done to update the vehicle status.

Let's see the definition of Move

public void Move(float steering, float accel, float footbrake, float handbrake);

steering is the rudder value, accel is the acceleration value, footbrake foot brake, handbrake foot brake

The function first calls the following procedure to synchronize the steering state of Mesh and collision box:

// WheelCollider and Mesh of tire are two things
// So when the step of the collision box changes, that is, when the tire turns, Mesh will not turn
// So we need to reset Mesh's steering according to the steering data of collision box here
// Makes it look like the tires are spinning
for (var i = 0; i < 4; i++)
{
    Quaternion quat;
    Vector3 position;
    m_WheelColliders[i].GetWorldPose(out position, out quat);   // Obtain the world coordinate and world turn of collision box
    m_WheelMeshes[i].transform.position = position; // Set the turn and position of Mesh
    m_WheelMeshes[i].transform.rotation = quat;
}

Call the following procedure to process the input data to make it conform to the specification:

// Steer, or steering wheel
// steering is the value on [- 1,1], which needs to be mapped to [- M] when reading on WheelCollider_ MaximumSteerAngle,m_ Maximumsteelangle]
// That is to say, the steering degree of tire is described in the form of angle, and the maximum rotation angle is m_MaximumSteerAngle

// A series of clamps here are used to adjust the input data to an acceptable range
// clamp input values
steering = Mathf.Clamp(steering, -1, 1);    // Rudder input
AccelInput = accel = Mathf.Clamp(accel, 0, 1);  // Acceleration input
BrakeInput = footbrake = -1*Mathf.Clamp(footbrake, -1, 0);  // Reverse input
handbrake = Mathf.Clamp(handbrake, 0, 1);   // Handbrake, I don't know where the input comes from

Change the rudder value of the collision box according to the input rudder value:

// Here's what we said above: map values [- 1,1] to angles
// Set the steer on the front wheels.
// Assuming that wheels 0 and 1 are the front wheels.
m_SteerAngle = steering*m_MaximumSteerAngle;
m_WheelColliders[0].steerAngle = m_SteerAngle;
m_WheelColliders[1].steerAngle = m_SteerAngle;

After the above data processing, the next step is the status update process.
The steelhelper method is called first:

SteerHelper();  // Adjust the speed direction by turning the car body
private void SteerHelper()
{
    // If a wheel is not on the ground, i.e. the normal vector of contact is (0,0,0), the car body will not turn
    for (int i = 0; i < 4; i++)
    {
        WheelHit wheelhit;
        m_WheelColliders[i].GetGroundHit(out wheelhit);
        if (wheelhit.normal == Vector3.zero)    
            return; // wheels arent on the ground so dont realign the rigidbody velocity
    }

    // If the rotation angle is greater than 10 degrees, the universal joint may be locked? I don't know what this 10 threshold means
    // this if is needed to avoid gimbal lock problems that will make the car suddenly shift direction
    if (Mathf.Abs(m_OldRotation - transform.eulerAngles.y) < 10f)
    {
        // The next few lines use the steering of the car body to adjust the direction of speed
        // Adjustment degree and M_ About steelhelper, the higher the value, the stronger the ability of speed steering
        // It can be understood that the stronger the grip is, the faster the steering is. If the grip is weak, the car body has turned, but the speed has not turned, and it has drifted
        var turnadjust = (transform.eulerAngles.y - m_OldRotation) * m_SteerHelper;
        Quaternion velRotation = Quaternion.AngleAxis(turnadjust, Vector3.up);
        m_Rigidbody.velocity = velRotation * m_Rigidbody.velocity;
    }
    m_OldRotation = transform.eulerAngles.y;
}

Then the ApplyDrive method, parameters to accel and footbreak self input values:

ApplyDrive(accel, footbrake);   // Adjust the torque on each tire through different operation modes and the degree of stepping on the foot brake
private void ApplyDrive(float accel, float footbrake)
{
    // Adjust the engine torque according to the driving mode (4WD / front / rear)
    float thrustTorque;
    switch (m_CarDriveType) 
    {
        case CarDriveType.FourWheelDrive:   // Four wheel drive
            thrustTorque = accel * (m_CurrentTorque / 4f);  // Current total torque divided by 4 times acceleration
            for (int i = 0; i < 4; i++)
            {
                m_WheelColliders[i].motorTorque = thrustTorque; // Torque is distributed to each tire
            }
            break;
        // Same as 4WD, only assigned to specific tires
        case CarDriveType.FrontWheelDrive:
            thrustTorque = accel * (m_CurrentTorque / 2f);
            m_WheelColliders[0].motorTorque = m_WheelColliders[1].motorTorque = thrustTorque;
            break;

        case CarDriveType.RearWheelDrive:
            thrustTorque = accel * (m_CurrentTorque / 2f);
            m_WheelColliders[2].motorTorque = m_WheelColliders[3].motorTorque = thrustTorque;
            break;

    }

    for (int i = 0; i < 4; i++)
    {
        // If the current speed is greater than 5 and the angle between the heading and the speed direction is less than 50, set the braking torque as the maximum braking torque multiplied by the foot brake factor [0,1]
        if (CurrentSpeed > 5 && Vector3.Angle(transform.forward, m_Rigidbody.velocity) < 50f)
        {
            m_WheelColliders[i].brakeTorque = m_BrakeTorque*footbrake;
        }
        else if (footbrake > 0)
        {
            // Otherwise, if the foot brake is applied, the engine torque will be - M_ Reverse torque times foot brake factor?
            m_WheelColliders[i].brakeTorque = 0f;
            m_WheelColliders[i].motorTorque = -m_ReverseTorque*footbrake;
        }
    }
}

Because the vehicle speed may exceed the maximum value, the core logic then limits it, calling CapSpeed:

CapSpeed(); // Limit maximum speed
private void CapSpeed()
{
    // Limit the maximum speed. If it is exceeded, it will be converted to the maximum speed according to different units
    float speed = m_Rigidbody.velocity.magnitude;
    switch (m_SpeedType)
    {
        case SpeedType.MPH:

            speed *= 2.23693629f;
            if (speed > m_Topspeed)
                m_Rigidbody.velocity = (m_Topspeed/2.23693629f) * m_Rigidbody.velocity.normalized;
            break;

        case SpeedType.KPH:
            speed *= 3.6f;
            if (speed > m_Topspeed)
                m_Rigidbody.velocity = (m_Topspeed/3.6f) * m_Rigidbody.velocity.normalized;
            break;
    }
}

The next step is to adjust the brake torque of the tire through the handbrake input, but I still don't know where the handbrake gets the input:

// Set handbrake
// Set the handbrake.
// Assuming that wheels 2 and 3 are the rear wheels.
if (handbrake > 0f)
{
    // Set the brake torque when the handbrake is applied
    var hbTorque = handbrake*m_MaxHandbrakeTorque;
    m_WheelColliders[2].brakeTorque = hbTorque;
    m_WheelColliders[3].brakeTorque = hbTorque;
}

After the above speed processing, the next step is to process other vehicle states. First, call CalculateRevs to calculate engine speed, which serves for sound processing:

CalculateRevs();    // Calculate engine speed
private void CalculateRevs()
{
	// Smooth calculation of engine speed, used to change sound, not force calculation
	// calculate engine revs (for display / sound)
	// (this is done in retrospect - revs are not used in force/power calculations)
	CalculateGearFactor();
	var gearNumFactor = m_GearNum/(float) NoOfGears;
	var revsRangeMin = ULerp(0f, m_RevRangeBoundary, CurveFactor(gearNumFactor));
	var revsRangeMax = ULerp(m_RevRangeBoundary, 1f, gearNumFactor);
	Revs = ULerp(revsRangeMin, revsRangeMax, m_GearFactor);
}

The vehicle is set to automatic gear, so it is necessary to calculate the upshift / downshift:

GearChanging(); // automatic catch
private void GearChanging()
{
    // Calculate the upper and lower limits of the speed of the current gear according to the total number of gears and the current gear. If it exceeds the upper limit, it will upshift; if it is lower than the lower limit, it will downshift
    float f = Mathf.Abs(CurrentSpeed/MaxSpeed);
    float upgearlimit = (1/(float) NoOfGears)*(m_GearNum + 1);
    float downgearlimit = (1/(float) NoOfGears)*m_GearNum;

    if (m_GearNum > 0 && f < downgearlimit)
    {
        m_GearNum--;
    }

    if (f > upgearlimit && (m_GearNum < (NoOfGears - 1)))
    {
        m_GearNum++;
    }
}

When the vehicle is driving at high speed, a downward force shall be applied to make the vehicle close to the ground, otherwise, the operation hand feeling will become very poor:

AddDownForce(); // Apply downward force to the body
// this is used to add more grip in relation to speed
private void AddDownForce()
{
    // Apply downward force to the body, the faster the speed, the greater the force
    m_WheelColliders[0].attachedRigidbody.AddForce(-transform.up * m_Downforce *
                                                 m_WheelColliders[0].attachedRigidbody.velocity.magnitude);
}

Adjust the playback of special effects according to the engine speed and tire slip calculated by CalculateRevs above:

CheckForWheelSpin();    // Check tire slip and play sounds, particles, tire prints
// Check the rotation of tires
// Do three things: 1. Release particles; 2. Play the sound of tire sliding; 3. Leave tire marks on the ground
// checks if the wheels are spinning and is so does three things
// 1) emits particles
// 2) plays tiure skidding sounds
// 3) leaves skidmarks on the ground
// these effects are controlled through the WheelEffects class
private void CheckForWheelSpin()
{
    // Check each wheel
    // loop through all wheels
    for (int i = 0; i < 4; i++)
    {
        // Get the touchdown of the wheel
        WheelHit wheelHit;
        m_WheelColliders[i].GetGroundHit(out wheelHit);

        // If the acceleration slip or deceleration slip of the tire exceeds the given threshold value
        // is the tire slipping above the given threshhold
        if (Mathf.Abs(wheelHit.forwardSlip) >= m_SlipLimit || Mathf.Abs(wheelHit.sidewaysSlip) >= m_SlipLimit)
        {
            // Play tire smoke, particle effect and other scripts next chapter analysis
            m_WheelEffects[i].EmitTyreSmoke();

            // Avoid multiple tires playing sound at the same time. If a tire plays, the tire will not play
            // avoiding all four tires screeching at the same time
            // if they do it can lead to some strange audio artefacts
            if (!AnySkidSoundPlaying())
            {
                m_WheelEffects[i].PlayAudio();
            }
            continue;
        }

        // Stop playing the sound and ending the tire mark when it's not sliding
        // if it wasnt slipping stop all the audio
        if (m_WheelEffects[i].PlayingAudio)
        {
            m_WheelEffects[i].StopAudio();
        }
        // end the trail generation
        m_WheelEffects[i].EndSkidTrail();
    }
}

The final approach seems to be to adjust the upper torque limit, but I don't quite understand why:

TractionControl();  // Control torque output according to sliding condition
// The car tires turn too fast, which reduces the energy given to the tires
// crude traction control that reduces the power to wheel if the car is wheel spinning too much
private void TractionControl()
{
    WheelHit wheelHit;
    switch (m_CarDriveType)
    {
        case CarDriveType.FourWheelDrive:
            // loop through all wheels
            for (int i = 0; i < 4; i++)
            {
                m_WheelColliders[i].GetGroundHit(out wheelHit);

                AdjustTorque(wheelHit.forwardSlip);
            }
            break;

        case CarDriveType.RearWheelDrive:
            m_WheelColliders[2].GetGroundHit(out wheelHit);
            AdjustTorque(wheelHit.forwardSlip);

            m_WheelColliders[3].GetGroundHit(out wheelHit);
            AdjustTorque(wheelHit.forwardSlip);
            break;

        case CarDriveType.FrontWheelDrive:
            m_WheelColliders[0].GetGroundHit(out wheelHit);
            AdjustTorque(wheelHit.forwardSlip);

            m_WheelColliders[1].GetGroundHit(out wheelHit);
            AdjustTorque(wheelHit.forwardSlip);
            break;
    }
}

summary

At this point, the entire Move method is analyzed, and CarUserControl updates the status of the vehicle by calling this method.
It can be seen that the design of this method is very linear, unlike the control system in the previous chapter. Only through this method, the state of the vehicle is completely updated, and the degree of cohesion is very high. At the same time, the interface open to the user input is such a method, which makes the coupling between the core control and the user interface very low, and the design is very good.
However, there are still some problems. For example, in steelhelper, in order to optimize the handle of the control, the designer wrote some strengthening logic, which is no mistake, but every exception will become a stumbling block during the reconstruction. Failure to give the control to the original components of Unity will increase the cost of the reconstruction, because you write this part of the maintenance by yourself, rather than Unity This kind of mutual calling relationship will also improve the coupling between the core logic and Unity components, so we should give the logic to Unity components as much as possible on the premise of realizing the function, so as to reduce the development cost.

This chapter analyzes the core control logic of the vehicle, so the next chapter analyzes how the core control logic calls the effect control.

Tags: Unity Mobile less

Posted on Wed, 24 Jun 2020 02:35:42 -0400 by noirsith