Unity汽车笔记

汽车的移动和转向

我们知道,汽车的前进后退是变速运动。按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;

人物在步行状态和驾车状态的转换

先说进入驾车

人物

  1. 人物绑定到车上的正确位置;
  2. 关掉人物碰撞体或CharacterController
  3. 人物播放驾车动画;
  4. 对于手写重力,停止增加竖直速度,可以通过判断动画参数driving,跳过重力加速。否则开车时竖直速度一直增加,下车时会直接摔死;

输入

  1. 输入脚本转换到控制车的模式,可以通过SwitchCurrentActionMap转换ActionMap。输入脚本也要有个车辆引用,知道自己控制的是哪辆车;

相机

  1. 相机绑定到车上,受驾车状态的控制方式;

问题

车轮和地面不碰撞,不把车身撑起来

我这里的原因是车体不是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);
            }
        }
    }

然后我发现这个脚本必须和刚体在一个对象才执行。总结一下组件分布的限制:

  1. 刚体必须是wheelCollider的父对象;
  2. 开车移动的是刚体所在对象,刚体必须在车的根对象;
  3. 碰撞体必须和刚体在一个对象才执行碰撞函数,才能做碰撞声音;
  4. 交互触发器不一定和刚体在一起,但是触发代码必须和它在一起;
  5. 交互触发器和碰撞体不能在一起,因为范围检测做不到只检测触发器;
  6. 为了少脚本,触发代码和车代码最好在一个脚本;

综上,一种设计可以是:刚体、碰撞体、碰撞声音脚本、碰撞声音源在根对象,脚本、触发器在子对象,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值