本文根据油管作者EYEmaginary原视频创作,视频地址是Car AI Tutorial #1 (Unity 5 ) - Make the Path - YouTube
本文主要做的是对视频中的内容进行分析和讲解,如果各位有时间请去看原视频。以下内容如有错误请留言评论,欢迎理性讨论。
本文详细介绍视频中的内容,具体实现的效果可以看我录的这个视频
为了让汽车沿着指定路径行驶,首先要创造出一条路径,该路径由各个路径点组成,汽车会在相邻的路径点之间完成转弯。首先创造出一个脚本Path,内容如下
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Path : MonoBehaviour { public Color lineColor; //创建一个数组来保存路径点 private Listnodes = new List (); private void OnDrawGizmosSelected() { Gizmos.color = lineColor; Transform[] pathTransforms = GetComponentsInChildren (); for(int i = 0; i < pathTransforms.Length; i++) { if(pathTransforms[i] != transform) { nodes.Add(pathTransforms[i]); } } for(int i = 0; i < nodes.Count; i++) { Vector3 currentNode = nodes[i].position; Vector3 previousNode = Vector3.zero; if(i > 0) { previousNode = nodes[i-1].position; } else if(i == 0 && nodes.Count > 0 ) { previousNode = nodes[nodes.Count - 1].position; } } Gizmos.DrawLine(currentNode,previousNode); Gizmos.DrawWireSphere(currentNode,1f); } }
将该脚本挂载在一个空的游戏物体(路径)上并且为该游戏物体创建几个子物体作为路径点,得到的效果如下图所示:
如果按照步骤并没有显示出路线,那么请你打开Line Color将颜色值的A值(这个值应该是不透明度)调到最大
下面来介绍这个脚本。脚本的内容比较简单,主要是创建一个Transform类型的数组nodes,然后利用OnDrawGizmosSelected函数(使用这个函数是为了当我们点选Path时,才会去绘制线条,这是为了方便观察)绘制出相邻两个路径点之间的连线并且在每个路径点上画了一个线条球。
首先通过GetComponentsInChildren方法将Path下的子物体(每个路径点)的Transform保存在一个新创建的数组pathsTransform中,然后将利用第一个for循环将pathsTransform中的每个元素一次添加到nodes这个数组中去。该for语句中有一个if判断,原因是GetComponentsInChildren会获取当前物体以及其所有子物体的Transform组件,如果不加if判断直接将该函数返回的结果放在pathTransform数组中,那么Path父物体的Transform会被放在该数组中的第一个位置。
第二个for循环主要是在获取路径点中相邻的两个,定义了两个Vector3变量,currentNode表示当前节点,previousNode表示当前节点的上一个节点。当索引值i大于0时,previousNode就等于nodes[i-1],如果i等于0并且路径点大于1个,previousNode就是路径点中的最后一个节点。这样做是为了将所有的路径点围成一个圈。
最后利用DrawLine在currentNode和previousNode中画线,用DrawWireSphere在每个节点上画一个线条球。
在添加之前请先为汽车添加RigidBody组件,否则是不会有wheel Collider出现的,其次请将RigidBody组件的Mass调大,否则车会飞起来,这个值为1000最好,最后请为车身添加一个碰撞器,注意碰撞器不要把车轮完全包含在内了,可以点击汽车模型中的车身部分,添加一个Mesh Collider,将Convex勾选上,这样系统会自动为车身添加合适大小的碰撞器,用Box Collider也是可以的,总之注意碰撞器不要把车全部包裹起来了。可以参照下图
为了让车动起来,需要给每个车轮添加上车轮碰撞器wheel Collider,如下图所示,将汽车模型的车轮放在wheel组中,在创建一个wheelColliders存放四个轮胎的wheel collider,具体就是在wheelCollider这个组中创建四个空物体,在这四个空物体上添加wheel Collider组件,调整这四个空物体的位置让他们与轮胎重合,如下图所示(我这里隐藏了车身方便观察)
上图中绿色线条就是wheel Collider的位置。Wheel Collider的参数请根据自己的车辆适当的调整,各个参数的意思请自行查阅Unity文档。
PS:调整的时候可以将场景调成2D。
这里主要实现让汽车可以在不在同一条直线上的两个路径点直接转弯。
注意横轴是X轴,纵轴是Z轴(因为车不可能在天上飞,所以忽略y轴)。假设(3,0,2)是汽车的位置点A,(4,0,4)下一个路径点B,车要想行驶到B点,他的前进方向应该是向量(3,0,2),接下来要让汽车的前进方向转变成(4,0,4)减去(3,0,2)也就是向量AB=(1,0,2)。通过这个向量可以发现如果这个向量的X值大于0,那么汽车应该向右转,小于应该向左转,等于0就不转。据此创建一个CarEngine脚本挂载到汽车上,内容如下
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class CarEngine : MonoBehaviour { //汽车的最大转向角度 public float maxSteerAngle; //获取路径 public Transform path; private Listnodes; //路径点索引值 private int currentIndex = 0; //车轮碰撞器 public WheelCollider LF; //左前轮 public WheelCollider RF; //右前轮 private float targetSteerAngle; //汽车轮胎实际的转角 private void Start() { Transform[] pathTransfroms = path.GetComponentsInChildren (); nodes = new List (); for(int i = 0; i < pathTranforms.Length; i++ ) { if(pathTransfroms[i] != path.transform) { nodes.Add(pathTransforms[i]); } } } private void FixUpdate() { ApplySteer(); } private void ApplySteer() { //根据车的位置和路径点的位置坐标计算出相对向量 Vector3 relativeVector=transform.InverseTransformPoint(nodes[currentIndex].position); //计算轮胎的实际转角 float newSteer = (relativeVector.x / relativeVector.magnitude) * maxSteerAngle; targetSteerAngle = newSteer; //将实际转角运用到左前轮和右前轮的转角 LF.steerAngle = targetSteerAngle; RF.steerAngle = targetSteerAngle; } }
在Start函数中重新让汽车获取了路径点,这部分代码和Path部分是一样的,在Path中是为了绘制路线,在此脚本中是为了让汽车获取路径点。注意需要在脚本写完之后在Unity界面将Path这个游戏物体拖到该脚本的path上,同时请将上一部分调整好的wheel Collider放在LF和RF上。然后声明了一个函数ApplySteer(),该函数主要负责车轮的转向。在Unity中Vector3有一个方法InverseTransform,该方法可以获取两个点之间的相对向量,transform.InverseTransform(nodes[currentIndex].position);相当于求车目前的位置和下一个要到达的路径点之间的方向向量。得到该向量之后计算实际的转角,具体的逻辑是利用该相对向量的x值除以相对向量的模依次来获得一个值,该值在-1到1之间,然后用这个值乘以最大转向角得出车轮实际转向角,将其赋给targetSteer,再赋给左右轮的steerAngle(这里多用了一个targetSteer是为了后面的平滑)。
为了方便理解这里举个例子:假设车在某一帧的位置是(1,0,2),下一个路径点的位置是(4,0,4),那么此时relativeVector就是(3,0,2),newSteer就是1除以根号下9加4等于根号13分之1,在将这个值乘以最大转向角得出车轮的转向角,注意这只是其中一帧的情况,随着车的位置的不断变化,车轮的实际转角也在不断变化。
看一下实际的图片:
可以看到车轮是指向路径点的(到这一步你的车并没有转动,但是当你点击查看车轮碰撞器时,你可以发现车轮碰撞器是转向的,车轮没转向是因为还没有同步车轮碰撞器和车轮的Transform)。
这一步将使汽车移动起来。
继续编写CarEngine脚本,如下所示
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class CarEngine : MonoBehaviour { //.... //轮胎碰撞器 public WheelCollider LB; //左后 public WheelCollider RB; //右后 //车轮动力部分 public float maxMotorTorque = 600f; //车轮最大动力 public float maxSpeed = 60f; //最大车速 public float currentSpeed; //汽车当前车速 private void FixedUpdate() { //... Drive(); CheckNextWayPointDistance(); } private void Drive() { //计算车速 currentSpeed = 2 * Mathf.PI * LF.radius * LF.rpm * 60 / 1000; //如果车速小于最大速度,那么给予车轮动力 if(currentSpeed < maxSpeed) { LF.motorTorque = maxMotorTorque; RF.motorTorque = maxMotorTorque; } else { LF.motorTorque = 0; Rf.motorTorque = 0; } } private void CheckNextWaypointDistance() { //判断汽车当前的距离和路径点之间的距离 ifVector3.Distance(new Vector3(transform.position.x,0,transform.position.z), new Vector3(nodes[currentIndex].position.x,0, nodes[currentIndex].position.z))<0.5f) { //如果已经到达了最后一个路径点,那么将索引值置0,绕圈 if(currentIndex == nodes.Count-1) { currentIndex = 0; } else { currentIndex++; } } } }
Drive函数中先根据汽车的轮胎的rpm来计算汽车的速度,这里有个公式:
车速 = 2 * Π * 轮胎半径 * 车轮的rpm * 60 / 1000
当汽车的速度小于最大速度时给予汽车车轮最大动力,这里是给了前面两个轮胎,也就是所谓的前驱。这样汽车就可以不断加速,当汽车到达最大速度,就将动力置0,这样汽车会减速,然后又被判定小于最大速度,无限套娃,从而最终让汽车以最大速度行驶。
CheckNextWaypointDistance函数中,刚开始路径点索引值currentIndex为0,那么会计算汽车距离第一个路径点的距离,注意这里是将汽车位置和路径点的位置点的y值都置0再计算距离,因为不同的汽车他的中心点位置可能偏高也有可能偏低,路径点可能会在不同的高度,如果直接判断,那么无法确定距离小于某个值才是真正合理的值,这里直接将y值置0,就省去了判断正确的值。如果距离小于0.5f,那么就将索引值加一以指导汽车前往下一个路径点,如果已经到达最后的路径点,那么将索引值置0,让汽车返回第一个路径点以完成跑圈。
如果你将车速设置的较大那么车辆在急转弯时会发生侧翻的情况(很符合显示(😓)),为了解决这一问题,可以改变车辆的重心。
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class CarEngine : MonoBehaviour { //..... private Rigidbody rb; public Vector3 centorOfMass; void Start() { rb = GetComponent(); rb.centorOfMass = centerOfMass; } //..... }
如代码所示,在Unity界面调整centorOfMass的值,将这个位置放在车的底部位置,我设置的是(0,-1,0),不要太离谱就行。设置得太低重心直接在地上就很幽默了。
汽车应该具有刹车功能。代码如下:
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class CarEngine : MonoBehaviour { //.... //汽车动力 public float maxBrakeTorque = 600f; //刹车的制动力 //刹车部分的设置 public bool isBraking; public Texture2D textureNormal; //不刹车时车灯的贴图 public Texture2D textureBreaking; //刹车时车灯的贴图 public Renderer carRender; //.... void FixedUpdate() { //... Brakking(); } //... private void Drive() { //.... //修改一下汽车行驶的条件 if(currentSpeed < maxSpeed && !isBraking) //... } private void Braking() { if(isBraking) { carRender.material.mainTexture = textureBraking; //后轮刹车 RB.brakeTorque = maxBrakeTorque; LB.brakeTorque = maxBrakeTorque; } else { carRender.material.mainTexture = textureNormal; RB.brakeTorque = 0; LB.brakeTorque = 0; } } }
声明一个标志位isBraking,当该标志位为真时,让后轮采用最大制动力,同时修改了Drive函数中的if判断语句,当标志位为真时,汽车的前轮制动力也会被置为0,这样汽车就会停下来。
同时为了做出汽车刹车时后面的车灯亮起来的效果, 在刹车和不刹车的情况下更新车灯处的贴图。找出自己车模型中的车灯的贴图位置,有可能在整个贴图中,一般在模型的Texture文件夹下,找到之后用PS等作图软件打开它,然后更改车灯处贴图的颜色,让它更亮,比如我这里原来是暗红,更改之后让其变得更红,更改之后将其保存在相同的文件夹下,注意不要覆盖原来的图片,将其保存为一张新的图片。(PS:本人PS能力较差,看个效果吧,理解一下)
然后在Unity界面更改变量,将textureNormal设置为没改的图片,将textureBraking设置为修改后的图片。将carRender设置为车灯部分。
刹车部分的更改图片似乎没什么必要,但是这么一个小的例子将PS等绘图软件和Unity结合起来了,让我们当了一次美术。起始也可以在刹车时添加例子特效,让车灯更亮一些。
做到这车轮并不会转动,下面来同步车轮和车轮碰撞器的状态
添加脚本CarWheel
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CarWheel : MonoBehaviour { public WheelCollider targetWheel; private Vector3 wheelPosition = new Vector3(); private Quaternion wheelRotation; void Update() { targetWheel.GetWorldPose(out wheelPosition,out wheelRotation); transform.position = wheelPosition; transform.rotation = wheelRotation; } }
将该脚本挂载到每个轮胎上,注意不是车轮碰撞器上,然后在Unity界面将每个轮胎对应的车轮碰撞器拖到每个车轮负载脚本的targetWheel上。如下图所示
该脚本就是通过GetWorldPose方法去获取车轮碰撞器的位置和转动情况,然后把对应的值直接赋给相应的轮胎。
完成到这汽车已经可以沿着指定路径行驶了,原视频还做了一个传感器来避障,这一部分放在下一篇文章中讲解。