IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 车辆动力学及在Unity、UE4中的实现 -> 正文阅读

[游戏开发]车辆动力学及在Unity、UE4中的实现

受力分析

直线行驶时的车轮受力如下:
在这里插入图片描述
水平方向上,所受合力为:
F = F t + F w + F f F=F_t+F_w+F_f F=Ft?+Fw?+Ff?
其中, F t F_t Ft?为牵引力, F w F_w Fw?为空气阻力, F f F_f Ff?为滚动阻力,下面我们将逐个介绍。

驱动力

先来说扭矩,扭矩是使物体发生旋转的一个特殊力矩,等于力和力臂的乘积,单位为 N ? m N?m N?m
在这里插入图片描述
设驱动轴的扭矩为 T t T_t Tt?,车轮半径为 r r r,那么牵引力:
F t = T t ? r F_t=T_t?r Ft?=Tt??r
如何求得驱动轴扭矩 T t T_t Tt?呢?设发动机扭矩为 T e T_e Te?,变速箱(Gear Ratio)和差速器(Differential Ratio)传送比分别为 i g i_g ig? i d i_d id?,传输效率为 η η η,那么发动机传送到驱动轴上的扭矩为:
T t = T e i g i d η T_t=T_e i_g i_d η Tt?=Te?ig?id?η
发动机扭矩 T e T_e Te?与发动机转速(RPM)有关:
在这里插入图片描述
那么发动机RPM如何取值呢?当司机踩下油门,气门角度变大,进气量增加,发动机输出扭矩增加,如果此时行驶阻力比发动机的输出扭矩小,则RPM会上升;如果行驶阻力比发动机的输出扭矩大,则RPM会下降。
在游戏中,我们可以设置一个变量SteerInput(0~1)代表油门的输入,让其乘以 F t F_t Ft?(用一个非零值作为发动机最小RPM求得),用根据牛顿第二定律计算出的车轮速度计算车轮RPM,再 i g i_g ig? i d i_d id?反计算发动机RPM,从而使发动机RPM曲线发挥作用。
另外,说到变速箱,就不得不提换挡了。自动换挡的规则一般如下:
在这里插入图片描述
即油门与车速同时满足一定条件,才能触发换挡。之所以升档曲线与降档曲线不重合,是为了避免处于临界区时频繁换挡。

空气阻力

空气阻力的计算公式如下:
F w = C d A ρ v 2 / 2 F_w=C_dAρv^2/2 Fw?=Cd?Aρv2/2
其中, C d C_d Cd?为空气阻力系数(参考值0.3~0.5),A为车前面积(参考值2.2 m 2 m^2 m2), ρ ρ ρ为空气密度(参考值1.29 k g ? m 3 kg?m^3 kg?m3), v v v为车辆的运动速度。

滚动阻力

轮胎滚阻的计算公式如下:
F f = G f F_f=Gf Ff?=Gf
G G G为整车重力, f f f为滚动阻力系数(参考值0.012~0.018)

在Unity中的实现

Unity给我们提供了WheelCollider组件,可以基于该组件进行动力学脚本编写:

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;

// 车轴的封装类
[System.Serializable]
public class AxleInfo
{
    // 车轮碰撞器
    public WheelCollider Left;
    public WheelCollider Right;

    // 车轮模型GameObject
    public GameObject LeftVisual;
    public GameObject RightVisual;
    public bool Motor;
    public bool Steering;
    public float MaxBrakeTorque = 1500.0f;

    // 车轮的碰撞信息
    [System.NonSerialized]
    public WheelHit HitLeft;
    [System.NonSerialized]
    public WheelHit HitRight;
    [System.NonSerialized]
    public bool GroundedLeft = false;
    [System.NonSerialized]
    public bool GroundedRight = false;
}

public class VehicleDynamics : MonoBehaviour
{
    // 车身
    [Header("车身")]
    [SerializeField] Rigidbody RB;
    [SerializeField] Vector3 CenterOfMass = new Vector3(0f, 0.35f, 0f);// 质心
    [SerializeField] float AirDragCoeff = 0.4f;// 空气阻力系数
    
    [SerializeField] float MaxMotorTorque = 450f;// 最大牵引扭矩

    // 发动机
    [Header("发动机RPM")]
    [SerializeField] AnimationCurve RPMCurve;
    [SerializeField] float MinRPM = 800f;
    [SerializeField] float MaxRPM = 8299f;
    public float CurrentRPM { get; set; } = 0f;
    [SerializeField] float RPMSmoothness = 20f;
    float WheelsRPM = 0f;// 车轮RPM

    // 挡位
    [Header("挡位")]
    [SerializeField] AnimationCurve ShiftUpCurve;// 这里以RPM代替车速
    [SerializeField] AnimationCurve ShiftDownCurve;
    [SerializeField] float[] GearRatios = new float[] { 4.17f, 3.14f, 2.11f, 1.67f, 1.28f, 1f, 0.84f, 0.67f };
    public float CurrentGear { get; set; } = 1f;
    float GearRatio = 0f;
    [SerializeField] float FinalDriveRatio = 2.56f;// 最终传送比
    [SerializeField] float ShiftDelay = 0.7f;// 两次换挡最小时间差
    float LastShift = 0.0f;// 记录上一次换挡时刻
    [SerializeField] float ShiftTime = 0.4f; // 换挡所需时间
    private bool Shifting = false;
    private int TargetGear = 1;
    private int LastGear = 1;
    public bool Reverse { get; set; } = false;// 是否为倒挡

    // 车轮
    [Header("车轮")]
    [SerializeField] List<AxleInfo> Axles;
    [SerializeField] float MaxSteeringAngle = 39.4f;// 车轮最大转向角
    int NumberOfDrivingWheels;// 驱动轮的数量
    [SerializeField] float WheelDamping = 1f;// 车轮碰撞器的阻尼率

    // 输入
    public float AccellInput { get; set; } = 0f;
    public float SteerInput { get; set; } = 0f;
    public bool HandBrake { get; set; } = false;

    public void Awake()
    {
        RB = GetComponent<Rigidbody>();
        RB.centerOfMass = CenterOfMass;

        NumberOfDrivingWheels = Axles.Where(a => a.Motor).Count() * 2;

        foreach (var axle in Axles)
        {
            axle.Left.wheelDampingRate = WheelDamping;
            axle.Right.wheelDampingRate = WheelDamping;
        }
    }

    public void FixedUpdate()
    {
        GetInput();// 获取油门及转向输入 
        SetGearRatio();// 调整变速箱传送比
        SetRPM();// 计算发动机RPM
        ApplySteer();// 设置转向
        ApplyTorque();// 根据发动机RPM结合RPM曲线给轮子施加扭矩

        RB.AddForce(-AirDragCoeff * 2.2f * 1.29f * RB.velocity * RB.velocity.magnitude / 2); // 施加空气阻力
    }

    private void Update()
    {
        UpdateWheelVisuals();
        Debug.Log($"RPM:{CurrentRPM}, Gear:{CurrentGear}, Velocity: {RB.velocity.magnitude * 3.6}");
    }

    // 获取油门及转向输入
    void GetInput()
    {
        SteerInput = CrossPlatformInputManager.GetAxis("Horizontal");
        AccellInput = CrossPlatformInputManager.GetAxis("Vertical");

        if (HandBrake)
        {
            AccellInput = -1.0f;
        }
    }

    // 调整变速箱传送比
    void SetGearRatio()
    {
        // 根据CurrentGear获取齿轮比
        GearRatio = Mathf.Lerp(GearRatios[Mathf.FloorToInt(CurrentGear) - 1], GearRatios[Mathf.CeilToInt(CurrentGear) - 1], CurrentGear - Mathf.Floor(CurrentGear));// (因为数组的下标是从0开始的,所以要-1)
        if (Reverse)
        {
            GearRatio = -1.0f * GearRatios[0];
        }

        AutoGearBox();
    }

    // 根据发动机RPM自动换挡
    void AutoGearBox()
    {
        if (Time.time - LastShift > ShiftDelay)
        {
            // 根据升挡曲线升挡
            if (CurrentRPM / MaxRPM > ShiftUpCurve.Evaluate(AccellInput) && Mathf.RoundToInt(CurrentGear) < GearRatios.Length)
            {
                if (Mathf.RoundToInt(CurrentGear) > 1 || RB.velocity.magnitude > 15f)// 如果正处于1挡,当速度高于15时再升挡
                {
                    GearboxShiftUp();
                }
            }

            // 根据降挡曲线降档
            if (CurrentRPM / MaxRPM < ShiftDownCurve.Evaluate(AccellInput) && Mathf.RoundToInt(CurrentGear) > 1)
            {
                GearboxShiftDown();
            }

        }

        // 完成换挡
        if (Shifting)
        {
            float lerpVal = (Time.time - LastShift) / ShiftTime;
            CurrentGear = Mathf.Lerp(LastGear, TargetGear, lerpVal);
            if (lerpVal >= 1f)
                Shifting = false;
        }

        // 限制挡位范围
        if (CurrentGear >= GearRatios.Length)
        {
            CurrentGear = GearRatios.Length - 1;
        }
        else if (CurrentGear < 1)
        {
            CurrentGear = 1;
        }
    }

    public bool GearboxShiftUp()
    {
        if (Reverse)
        {
            Reverse = false;
        }
        else
        {
            LastGear = Mathf.RoundToInt(CurrentGear);
            TargetGear = LastGear + 1;
            LastShift = Time.time;
            Shifting = true;
        }
        return true;
    }

    public bool GearboxShiftDown()
    {
        if (Mathf.RoundToInt(CurrentGear) == 1)
        {
            Reverse = true;
        }
        else
        {
            LastGear = Mathf.RoundToInt(CurrentGear);
            TargetGear = LastGear - 1;
            LastShift = Time.time;
            Shifting = true;
        }
        return true;
    }

    private void ApplyLocalPositionToVisuals(WheelCollider collider, GameObject visual)
    {
        if (visual == null || collider == null)
        {
            return;
        }

        Vector3 position;
        Quaternion rotation;
        collider.GetWorldPose(out position, out rotation);

        visual.transform.position = position;
        visual.transform.rotation = rotation;
    }

    private void SetRPM()
    {
        // 获取车轮的RPM
        WheelsRPM = (Axles[1].Right.rpm + Axles[1].Left.rpm) / 2f;
        if (WheelsRPM < 0)
        {
            WheelsRPM = 0;
        }

        // 根据车轮的RPM增加发动机的RPM
        CurrentRPM = Mathf.Lerp(CurrentRPM, MinRPM + (WheelsRPM / GearRatio / FinalDriveRatio), Time.fixedDeltaTime * RPMSmoothness);
        if (CurrentRPM < 0.02f)
        {
            CurrentRPM = 0.0f;
        }
    }

    void ApplySteer()
    {
        float steer = MaxSteeringAngle * SteerInput;
        foreach (var axle in Axles)
        {
            if (axle.Steering)
            {
                axle.Left.steerAngle = steer;
                axle.Right.steerAngle = steer;
            }
        }
    }

    void ApplyTorque()
    {
        // 根据发动机RPM获取轮子的扭矩
        var currentTorque = (float.IsNaN(CurrentRPM / MaxRPM)) ? 0.0f : RPMCurve.Evaluate(CurrentRPM / MaxRPM) * MaxMotorTorque * GearRatio * FinalDriveRatio;

        // 给轮子施加扭矩
        if (AccellInput >= 0)// 加速
        {
            float torquePerWheel = AccellInput * (currentTorque / NumberOfDrivingWheels);

            foreach (var axle in Axles)
            {
                if (axle.Motor)
                {
                    axle.Left.motorTorque = torquePerWheel;
                    axle.Right.motorTorque = torquePerWheel;
                }

                axle.Left.brakeTorque = 0f;
                axle.Right.brakeTorque = 0f;
            }

        }
        else// 制动
        {
            foreach (var axle in Axles)
            {
                var brakeTorque = AccellInput * -1 * axle.MaxBrakeTorque;
                axle.Left.brakeTorque = brakeTorque;
                axle.Right.brakeTorque = brakeTorque;
                axle.Left.motorTorque = 0f;
                axle.Right.motorTorque = 0f;
            }
        }
    }

    // 更新车轮模型的Pose
    private void UpdateWheelVisuals()
    {
        foreach (var axle in Axles)
        {
            ApplyLocalPositionToVisuals(axle.Left, axle.LeftVisual);
            ApplyLocalPositionToVisuals(axle.Right, axle.RightVisual);
        }
    }
}

对于WheelCollider的sidewaysFriction与forwardFriction,官方给的图表如下:
在这里插入图片描述
在急速起步、急速刹车或者急速转向时,轮胎可能会与地面产生滑动。Slip为车轮运动中滑动速度与车轮中心速度的比值,代表滑动成分所占的比例,取值范围为(0~1)。Force为制动力系数。
在低打滑条件下,轮胎可能会施加很大的力,因为橡胶会通过拉伸来补偿打滑。随后,当打滑变得非常高时,随着轮胎开始滑动或旋转,力会减小。

在UE4中的实现

UE4中的“轮子”比较完整,在WheeledVehicleMovementComponent4W组件的机械设置中,可以直接设置车子的机械属性。
在这里插入图片描述

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-07-31 16:57:47  更:2021-07-31 16:58:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/21 20:15:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码