汽车的移动和转向
我们知道,汽车的前进后退是变速运动。按w,汽车开始加速,到最大速度后保持匀速,松开w,汽车受到阻力加速。如果按s减速,则以更大的加速度减速。后退反之。
按A/D时前轮偏转。只有前进后退时,转弯会使汽车偏转。需要注意的是前轮偏转的角度也是float,但是A/D只有是否按下,是bool,比真实汽车方向盘少了转弯角度这个自由度。一般只能控制按下A/D的时间控制前轮偏转角度。要么用鼠标左右移动控制,但是大多数游戏都不是这么做的。
这和人物、坦克的移动旋转都不一样。
unity车辆的架构
车辆的物理模拟由wheelCollider配合车体的刚体实现。有车体刚体,车轮碰撞体才在scene窗口显示。车轮模型及其转动由另外的对象实现。要求车体和车轮必须z轴向前,y轴向上,否则车轮碰撞体失效。
Wheel Collider
Wheel Collider下的参数很多。只看最关键的。

motorTorque
首先,motorTorque用来驱动车轮前进后退。可以用来刹车但是官方不推荐。

WheelCollider-motorTorque - Unity 脚本 API
brakeTorque
刹车有brakeTorque。
WheelCollider-brakeTorque - Unity 脚本 API
steerAngle
steerAngle控制前轮的偏转。并不旋转wheelCollider依附的对象,只对物理系统起作用,并且会旋转Giamos显示的图标。
WheelCollider-steerAngle - Unity 脚本 API


GetWorldPose()
要让轮子模型偏转、运动中旋转,需要使用GetWorldPose()得到轮子的世界位置和旋转,写入给轮子模型。

WheelCollider-GetWorldPose - Unity 脚本 API
Quaternion q;
Vector3 p;
wheelCollider.GetWorldPose(out p, out q);
wheel.position = p;
wheel.rotation = q;
人物在步行状态和驾车状态的转换
先说进入驾车
人物
- 人物绑定到车上的正确位置;
- 关掉人物碰撞体或CharacterController
- 人物播放驾车动画;
- 对于手写重力,停止增加竖直速度,可以通过判断动画参数driving,跳过重力加速。否则开车时竖直速度一直增加,下车时会直接摔死;
输入
- 输入脚本转换到控制车的模式,可以通过SwitchCurrentActionMap转换ActionMap。输入脚本也要有个车辆引用,知道自己控制的是哪辆车;
相机
- 相机绑定到车上,受驾车状态的控制方式;
问题
车轮和地面不碰撞,不把车身撑起来

我这里的原因是车体不是z轴超前,y轴朝上,这里可以看见那个球应该是车轮和地面的接触点。如果轴向不对车轮碰撞体就失效。
前进中下车后车停不下来
下车函数里把力矩设为了0.下车后打印了一下力矩,是0,说明是在靠惯性前进,摩擦力太小。把Wheel Damping Rate改成80,滑行距离还是远,但是能停下来了。
人能把车推翻
人物用了CharacterController,使用characterController.Move()移动。推车的中部时推不动,推前后部很容易推动。

粗暴解决方法:车上没人时把刚体isKinematic=true。 单机游戏,只有玩家开车可以这么凑合,多人游戏别人开车时我还是能把车推翻,没法用。
左侧车轮模型内外反了

GetWorldPose()获得的位置旋转和车轮碰撞体对象的位置旋转无关。这里需要让GetWorldPose()得到的旋转翻过来,或者GetWorldPose()不变,把模型翻过来。
解决方法:在轮胎模型外面套一层,保证轮胎摆好时这个对象的旋转是(0,0,0)。也就是把模型翻过来。
![]()

输入类、人物类、车类的引用关系
输入类是一个单例,有一个对玩家的人物引用。为了开车,输入类要加一个车类引用吗?一开始我是这样做的,没做人和车的互相引用,然后处理下面的问题时发现需要人引用车,然后发现会形成引用蜘蛛网,还有这些引用要维护一致的问题。
然后我又想,在一个操作人物为中心,载具系统是附加功能的游戏,玩家必有人物,不一定有载具,应该以人物为中心,让人物记录他开的载具。
把车开进水里的处理
如果地图里有水域,就不得不处理一下二者的关系。我这里使用了检测到人物要游泳就强制下车的设计。
protected void CheckSwim(){
swim=Physics.Raycast(transform.position+Vector3.up*inWaterDepth,Vector3.up,out raycastHit,
Mathf.Infinity,1<<MyGameManager.waterLayer,QueryTriggerInteraction.Collide);
Swim(swim);
if(swim&&car!=null){
car.ExitCar(this);
}
}

声音
上车时播放打火声音,加速时播放加速声音,其他时间播放怠速声音。打火和加速声音不循环,怠速声音循环。
打火声音结束后开始播放怠速。怎么知道打火声音结束了?我想到的办法是通过audioClip.length知道声音的长度,用协程延迟播放。
IEnumerator PlayeAudioLater(float delay, AudioClip audioClip,bool loop) {
yield return new WaitForSeconds(delay);
audioSource.loop = loop;
audioSource.clip = audioClip;
MyAudioManager.Instance.MyPlaySound(audioSource);
}
加速就是按下w时,怎么知道这个时机?
代码
public class Car : MonoBehaviour, Interactive {
public Transform driverAnchor;
public WheelCollider wheelFL, wheelFR, wheelBL, wheelBR;
public Transform wheelMeshFL,wheelMeshFR, wheelMeshBL, wheelMeshBR;
public float motorTorque = 10;
public float steerAngle = 45;
public Transform camAxis, exitDriverPos;
float angleX, angleY;
const float angleXMax = 45, angleXMin = -30;
const float angleYMax = 60;
const float steerLerpRate = .5f;
new Rigidbody rigidbody;
void Start () {
rigidbody = GetComponent<Rigidbody>();
rigidbody.isKinematic = true;
}
public void GetInCar(CharacterBase character){
character.GetComponent<CharacterController>().enabled = false;
character.transform.SetParent(driverAnchor);
character.transform.localPosition = Vector3.zero;
character.transform.localEulerAngles = Vector3.zero;
character.ToggleDriveMode(true);
character.car = this;
rigidbody.isKinematic = false;
if(character==MyInput.Instance.player){
InteractionManagerOverlap.Instance.Disable();
MyInput.Instance.playerInput.SwitchCurrentActionMap(MyInput.actionMapCar);
MyCamManager.Instance.TPPCam.transform.SetParent(camAxis);
MyCamManager.Instance.TPPCam.transform.localEulerAngles = Vector3.zero;
MyCamManager.Instance.ToggleDriveMode(true);
}
}
public void ExitCar(CharacterBase character) {
character.transform.SetParent(null);
character.transform.position = exitDriverPos.position;
character.transform.eulerAngles = new Vector3(0, character.transform.eulerAngles.y, 0);
character.GetComponent<CharacterController>().enabled = true;
character.ToggleDriveMode(false);
rigidbody.isKinematic = true;
character.car= null;
//把输入改回行走,断开接收输入后,把油门、转向关掉
Move(Vector2.zero);
if(character==MyInput.Instance.player){
InteractionManagerOverlap.Instance.Enable();
MyInput.Instance.playerInput.SwitchCurrentActionMap(MyInput.actionMapPlayer);
MyCamManager.Instance.TPPCam.transform.SetParent(character.aimAxis);
MyCamManager.Instance.TPPCam.transform.localEulerAngles = Vector3.zero;
MyCamManager.Instance.ToggleDriveMode(false);
}
}
public void Interact()
{
}
public void Move(Vector2 input) {
wheelBL.motorTorque = motorTorque * input.y;
wheelBR.motorTorque = motorTorque * input.y;
wheelFL.steerAngle = input.x * steerAngle;
wheelFR.steerAngle = input.x * steerAngle;
WheelsRotate();
}
public void CamRotate(Vector2 input) {
angleX -= input.y;
angleX = Mathf.Clamp(angleX, angleXMin, angleXMax);
angleY += input.x;
angleY = Mathf.Clamp(angleY, -angleYMax, angleYMax);
camAxis.localEulerAngles = new Vector3(angleX, angleY, 0);
}
void WheelsRotate(){
Quaternion q;
Vector3 p;
wheelBL.GetWorldPose(out p, out q);
wheelMeshBL.position = p;
wheelMeshBL.rotation=Quaternion.Lerp(wheelMeshBL.rotation,q,steerLerpRate);
wheelBR.GetWorldPose(out p, out q);
wheelMeshBR.position = p;
wheelMeshBR.rotation =Quaternion.Lerp(wheelMeshBR.rotation,q,steerLerpRate);
wheelFL.GetWorldPose(out p, out q);
wheelMeshFL.position = p;
wheelMeshFL.rotation =Quaternion.Lerp(wheelMeshFL.rotation,q,steerLerpRate);
wheelFR.GetWorldPose(out p, out q);
wheelMeshFR.position = p;
wheelMeshFR.rotation =Quaternion.Lerp(wheelMeshFR.rotation,q,steerLerpRate);
}
}
汽车的代码可以放在一个脚本吗?
汽车移动、车轮转动的代码只要获得了wheelCollider,放在哪个对象无所谓。主要是需要用到碰撞、触发函数的脚本必须和碰撞体、触发器放在一个对象。触发上车交互的脚本必须和触发器在一个对象,如果想做碰撞物体的声音,必须通过OnCollisionEnter()知道碰撞的时机,代码和碰撞体在一个对象。而碰撞体和触发器又不能在一个对象。
然后我感觉碰撞播放声音的功能很常见,可以写一个通用脚本
public class CollisionSoundPlayer : MonoBehaviour{
AudioSource audioSource;
public AudioClip audioCollision;
public float speedThreshold;
void Start(){
audioSource = GetComponent<AudioSource>();
audioSource.clip = audioCollision;
}
void OnCollisionEnter(Collision other){
if(other.relativeVelocity.magnitude>speedThreshold){
MyAudioManager.Instance.MyPlaySound(audioSource);
}
}
}
然后我发现这个脚本必须和刚体在一个对象才执行。总结一下组件分布的限制:
- 刚体必须是wheelCollider的父对象;
- 开车移动的是刚体所在对象,刚体必须在车的根对象;
- 碰撞体必须和刚体在一个对象才执行碰撞函数,才能做碰撞声音;
- 交互触发器不一定和刚体在一起,但是触发代码必须和它在一起;
- 交互触发器和碰撞体不能在一起,因为范围检测做不到只检测触发器;
- 为了少脚本,触发代码和车代码最好在一个脚本;
综上,一种设计可以是:刚体、碰撞体、碰撞声音脚本、碰撞声音源在根对象,脚本、触发器在子对象,


被折叠的 条评论
为什么被折叠?



