UnityStandardAsset工程、源码分析_2_赛车游戏[玩家控制]_车辆核心控制

上一章地址:UnityStandardAsset工程、源码分析_1_赛车游戏[玩家控制]_输入系统

在上一章里,我们了解了整个车辆控制的大体流程,并且分析了一下输入系统,也就是从玩家的手柄\手机倾斜输入作用到车辆核心控制逻辑的过程中,调用了那些类,这些类又是怎样协同工作的。那么这章我们来分析车辆最主要的核心控制逻辑,车辆如何使用调整好的输入数据,根据当前车辆状态,计算出下一帧的车辆状态。

核心控制逻辑

上一章里,我们了解到,车辆核心状态的控制一共使用了三个类,分别为CarControllerCarUserControlCarAudio。其中,CarController是核心控制逻辑,CarUserControl是用户输入接口,负责调用CarController中的Move方法进行状态更新。CarController调用CarAudio和车辆轮胎效果控件来实现各种效果。

private void FixedUpdate()
{
    float h = CrossPlatformInputManager.GetAxis("Horizontal");	// 上章分析的内容
    float v = CrossPlatformInputManager.GetAxis("Vertical");

#if !MOBILE_INPUT
    float handbrake = CrossPlatformInputManager.GetAxis("Jump");
    m_Car.Move(h, v, v, handbrake);	// 这章重点分析的内容
#else
    m_Car.Move(h, v, v, 0f);
#endif
}

分析完了CarUserControl的内容,可以发现它每一帧都从输入类中读取数据,再使用这些数据调用CarControllerMove方法,我们这章就来分析CarController的最主要的方法Move,看看为了更新车辆状态,CarController都做了哪些工作。

先看Move的定义

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

steering是舵值,accel是加速值,footbrake脚刹,handbrake脚刹

函数首先调用了如下过程,用于同步Mesh与碰撞盒的转向状态:

// WheelCollider和轮胎的Mesh是两个东西
// 所以碰撞盒的Steer变化,也就是轮胎转向时,Mesh是不会发生转向的
// 所以需要再这里根据碰撞盒的转向数据重新设置Mesh的转向
// 使得外观上看起来轮胎旋转了一样
for (var i = 0; i < 4; i++)
{
    Quaternion quat;
    Vector3 position;
    m_WheelColliders[i].GetWorldPose(out position, out quat);   // 获取碰撞盒的世界坐标和世界转向
    m_WheelMeshes[i].transform.position = position; // 设置Mesh的转向和位置
    m_WheelMeshes[i].transform.rotation = quat;
}

再调用如下过程处理输入数据,使其符合规范:

// Steer直译为舵,也就是方向盘
// steering为[-1,1]上的值,而在WheelCollider上读取时需要将这个值映射到[-m_MaximumSteerAngle,m_MaximumSteerAngle]上
// 也就是以角度的形式来描述轮胎的转向程度,并且旋转的角度最大值为m_MaximumSteerAngle

// 在这里的一系列Clamp是在将输入数据调整到可接受的范围内
// clamp input values
steering = Mathf.Clamp(steering, -1, 1);    // 舵输入
AccelInput = accel = Mathf.Clamp(accel, 0, 1);  // 加速输入
BrakeInput = footbrake = -1*Mathf.Clamp(footbrake, -1, 0);  // 倒车输入
handbrake = Mathf.Clamp(handbrake, 0, 1);   // 手刹,暂时不知道输入来自哪里

根据输入的舵值让碰撞盒的舵值改变:

// 这里就是上面所说的将值[-1,1]映射到角度上
// 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;

在进行了如上的数据处理后,接下来就是状态更新的流程。
首先调用了SteerHelper方法:

SteerHelper();  // 通过车体转向调整速度方向
private void SteerHelper()
{
    // 如果有一个轮子不在地上,也就是接触的法向量为(0,0,0),车体就不转向了
    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
    }

    // 若是转过的角度大于10度就可能发生万向节死锁?不知道这个10的阈值时什么意思
    // 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)
    {
        // 接下来这几行是利用车体的转向调整速度的方向
        // 调整的程度与m_SteerHelper有关,值越大,速度转向的能力越强
        // 可以理解为抓地力越强,转向越快,而抓地力很弱的话,车体已经转向,速度却没转过来,进入了漂移的状态
        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;
}

然后是ApplyDrive方法,参数来accelfootbreak自输入值:

ApplyDrive(accel, footbrake);   // 通过不同的操作模式以及踩脚刹的程度,调整每个轮胎上的扭矩
private void ApplyDrive(float accel, float footbrake)
{
    // 根据驾驶模式(四驱\前驱\后驱)调整发动机的扭矩
    float thrustTorque;
    switch (m_CarDriveType) 
    {
        case CarDriveType.FourWheelDrive:   // 四驱
            thrustTorque = accel * (m_CurrentTorque / 4f);  // 当前的总扭矩除以4再乘上加速度
            for (int i = 0; i < 4; i++)
            {
                m_WheelColliders[i].motorTorque = thrustTorque; // 扭矩分配到每个轮胎上
            }
            break;
        // 同四驱,只是分配到特定的轮胎上
        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++)
    {
        // 如果当前速度大于5,且车头的朝向与速度朝向之间的夹角小于50,则设置制动扭矩为最大制动扭矩乘以脚刹因子[0,1]
        if (CurrentSpeed > 5 && Vector3.Angle(transform.forward, m_Rigidbody.velocity) < 50f)
        {
            m_WheelColliders[i].brakeTorque = m_BrakeTorque*footbrake;
        }
        else if (footbrake > 0)
        {
            // 否则踩了脚刹的话,发动机扭矩就为-m_ReverseTorque乘以脚刹因子?
            m_WheelColliders[i].brakeTorque = 0f;
            m_WheelColliders[i].motorTorque = -m_ReverseTorque*footbrake;
        }
    }
}

由于车辆速度可能超过了最大值,核心逻辑随后进行了限制,调用CapSpeed

CapSpeed(); // 限制最大速度
private void CapSpeed()
{
    // 限制最大速度,超过的话根据不同的单位换算到最大速度
    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;
    }
}

接下来是通过手刹输入调整轮胎的制动扭矩,但我还是不懂这个handbrake从哪里获得输入:

// 设置手刹
// Set the handbrake.
// Assuming that wheels 2 and 3 are the rear wheels.
if (handbrake > 0f)
{
    // 拉了手刹就设置制动扭矩
    var hbTorque = handbrake*m_MaxHandbrakeTorque;
    m_WheelColliders[2].brakeTorque = hbTorque;
    m_WheelColliders[3].brakeTorque = hbTorque;
}

在经过上述速度处理后,接下来就是车辆其他状态的处理,首先调用CalculateRevs进行引擎转速计算,服务于声音处理:

CalculateRevs();    // 计算引擎转速
private void CalculateRevs()
{
	// 平滑计算引擎转速,用于改变声音,不用于力的计算
	// 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);
}

车辆被设定为自动挡,所以需要进行升档\降档的计算:

GearChanging(); // 自动挡
private void GearChanging()
{
    // 根据齿轮总数和当前挡位计算当前挡位的速度上下限,超过上限就升档,低于下限就降档
    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++;
    }
}

车辆在高速行驶时需要向下加一个力,使得车辆紧贴于地面,否则会让操作手感变得非常差:

AddDownForce(); // 向车身施加向下的力
// this is used to add more grip in relation to speed
private void AddDownForce()
{
    // 向车身施加向下的力,速度越快力越大
    m_WheelColliders[0].attachedRigidbody.AddForce(-transform.up * m_Downforce *
                                                 m_WheelColliders[0].attachedRigidbody.velocity.magnitude);
}

根据上面CalculateRevs计算出来的引擎转速和轮胎滑动情况,调整特效的播放:

CheckForWheelSpin();    // 检查轮胎滑动情况,并以此播放声音、粒子、轮胎印
// 检测轮胎的旋转情况
// 干三件事:1.释放粒子;2.播放轮胎滑行的声音;3.在地上留下轮胎印
// 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()
{
    // 检测每个轮子
    // loop through all wheels
    for (int i = 0; i < 4; i++)
    {
        // 获取轮子的触地情况
        WheelHit wheelHit;
        m_WheelColliders[i].GetGroundHit(out wheelHit);

        // 如果轮胎的加速滑动或是减速滑动超过给定的阈值
        // is the tire slipping above the given threshhold
        if (Mathf.Abs(wheelHit.forwardSlip) >= m_SlipLimit || Mathf.Abs(wheelHit.sidewaysSlip) >= m_SlipLimit)
        {
            // 播放轮胎烟雾,粒子效果等脚本下章分析
            m_WheelEffects[i].EmitTyreSmoke();

            // 避免有多个轮胎同时播放声音,如果有轮胎播放了,这个轮胎就不播放了
            // 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;
        }

        // 没在滑动了就停止播放声音和结束轮胎印
        // 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();
    }
}

最后的方法似乎是为了调整扭矩上限,但我不是很理解为什么要调整:

TractionControl();  // 根据滑动情况控制扭矩输出
// 车子轮胎转太快了就减少给予轮胎的能量
// 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;
    }
}

总结

至此,整个Move方法就分析完了,CarUserControl通过调用这个方法,更新了车辆的状态。
可见,这个方法的设计是十分线性的,不似上一章的控制系统那么弯弯绕绕,仅通过这一个方法就完整地更新了车辆的状态,内聚程度十分的高,同时开放给用户输入的接口也就这么一个方法,使得核心控制与用户接口之间的耦合度很低,设计得很不错。
不过这其中还是有一些问题,例如SteerHelper那里,设计者为了优化操控的手感,自己写了一些加强逻辑,这无可厚非,但是每一个例外都会成为重构时的绊脚石,没有将操控完全交给Unity的原生组件会使得重构成本提高,因为你写的这部分由你自己维护,而不是Unity,这种互相调用的关系也会让核心逻辑和Unity组件之间的耦合度提高,所以应当在实现功能的前提下,尽量将逻辑交给Unity组件,以此减低开发成本。

这一章分析了车辆的核心控制逻辑,那么下一章就分析这个核心控制逻辑是如何调用效果控件的吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值