本文程序来自油管作者Gamedev Inspire的视频,地址helicopter physics and movement in unity3d | helicopter controller with weapon system - YouTube
螺旋桨的转动
直升机飞行过程中尾部和顶部两个螺旋桨需要转动,螺旋桨的转速应当和直升机发动机的功率相匹配,因此可以将螺旋桨的转动控制脚本和直升机发动机的控制脚本分开写,二者之间相互连接的桥梁就是发动机功率和螺旋桨转速之间的关系,直接给出螺旋桨转动脚本BladeController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bladescontroller : MonoBehaviour
{
//定义了枚举,该枚举是螺旋桨的转动方向
public enum Axis
{
x,
y,
z
}
//螺旋桨转动的轴
public Axis rotateAxis;
//螺旋桨转速
public float bladeSpeed;
public float BladeSpeed
{
get
{
return bladeSpeed;
}
set
{
//限制转速的范围
bladeSpeed = Mathf.Clamp(value,0,3000);
}
}
//定义一个标志位,这个标志位为真表示发动机熄火了
public bool inverseRotation = false;
private Vector3 Rotation;
private float rotateDegree;
void Start()
{
Rotation = transform.localEulerAngles;
}
void Update()
{
if(inverseRotation)
rotaionDegree -= bladeSpeed * Time.deltaTime;
else
rotationDegree += bladeSpeed * Time.deltaTime;
rotateDegree = rotateDegree % 360;
switch(rotationAxis)
{
case Axis.y:
transform.localRotation = Quaternion.Euler(Rotation.x, rotateDegree,Rotation.z);
break;
case Axis.z:
transform.localRotation = Quaternion.Euler(Rotation.x, Rotation.y, rotateDegree);
break;
default:
transform.localRotation = Quaternion.Euler(rotateDegree, Rotation.y, Rotation.z);
break;
}
}
}
代码中定义了一个枚举,根据枚举可以确定不同螺旋桨的旋转方向,在Update函数中使用switch语句根据旋转轴使用Quaternion.Euler方法旋转轴。 同时定义了一个BladeSpeed变量,在直升机的控制脚本中通过对该变量的值进行重新设置从而调整bladeSpeed变量的值,从而对螺旋桨的转动进行调整。同时在Update函数中有这样的一条语句:rotateDegree = rotateDegree % 360; 该语句对使用转速算出来的度数对360取余,我个人理解的主要目的是为了防止转速较高旋转的过快。
将该脚本附加到两个螺旋桨上并选择合适的旋转轴以及设置转速就可以看到直升机的螺旋桨转动起来。如果你的螺旋桨转动的错位了,那么请调整螺旋桨物体的轴心和中心让他们重合,具体参考这篇文章Unity重置模型物体的轴心为中心_模型的 轴心-优快云博客
直升机控制脚本
通过发动机功率调整螺旋桨的转动
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
using UnityEngine.Events;
public class HelicopterEngine : MonoBehaviour
{
public BladeController MainBlade;
public BladeController SubBlade;
private float enginePower;
public float EnginePower
{
get {return enginePower;}
set {
MainBlade.BladeSpeed = value * 200;
SubBlade.BladeSpeed = value * 500;
enginePower = value;
}
}
//发动机提供的升力
public float EngineLift = 0.0075f;
void Update()
{
if(Input.GetAxis("Throttle") > 0)
{
EnginePower += EngineLift;
}
}
}
这一部分的代码主要是用来根据发动机的功率来调整螺旋桨的转动,主要实现的效果是当启动发动机时,螺旋桨开始转动。这里定义了一个新的轴Throttle,可以在Project Settings中的Input Manager中定义一下。
直升机的上升与下降
为了方便管理代码,将处理直升机的各项功能写成函数,在Update中进行调用。为了让直升机能够上升与下降,要给直升机加上Colldier和Rigbody组件,如下
代码如下
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
using UnityEngine.Events;
public class HelicopterEngine : MonoBehaviour
{
Rigidbody helicopterRigid;
public float effectiveHeight;
//....
void Start()
{
helicopterRigid = GetComponent<Rigidbody>();
}
void Update()
{
HandleInputs();
}
void HandleInputs()
{
if(Input.GetKey(KeyCode.C))
{
EnginePower -= EngineLift;
if(EnginePower < 0)
{
EnginePower = 0;
}
}
if(Input.GetAxis("Throttle") > 0)
{
EnginePower += EngineLift;
}
}
void FixedUpdate()
{
HelicopterHover();
}
void HelicopterHover()
{
float upForce = 1 - Mathf.Clamp(helicopterRigid.transform.position.y / effectiveHeight, 0, 1);
upForce = Mathf.Lerp(0, enginePower, upForce) * helicopterRigid.mass;
helicopterRigid.AddRelativeForce(Vector3.up * upForce);
}
}
代码定义了一个函数HandleInputs,这个函数主要用来处理按键按下的事件,首先是飞机通过按下Throttle轴(这里用的是T键)来增加发动机功率,通过按下C键来减小发动机功率。在Fix Update函数中首先调用了外部定义的HelicopterHover函数,这个函数用来处理飞机上升和降落的。先定义了一个全局变量effectiveHeight,这个变量表示的是飞机的有效飞行高度,在Unity界面可以设置该值的大小,我这里设置的是100,然后在函数中去求upForce大小。然后使用插值函数Lerp来将upForce值从0插值到enginePower,这样做的目的是为了根据高度去调整升力的大小(因为enginePower更大,飞机才能抬升到更高处)。然后使用AddRelativeForce函数将这个力附加在Vector3.up的方向上。
这样直升机就可以在按下T键(在Axis的Throttle中我设置的是T键增加)后上升,按下C键后就可以下降了。
直升机前后左右移动
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
using UnityEngine.Events;
public class HelicopterEngine : MonoBehaviour
{
//...
public Vector2 movement = Vector2.zero;
public float ForwardForce;
public float BackForce;
public LayerMask groundLayer;
private float distanceToground;
private bool isOnGround = true;
public float turnForce;
private float TurnForceHelper = 1.5f;
private float turning = 0f;
void Update()
{
//...
HandleGroundCheck();
}
void HandleInputs()
{
if(!isGround)
{
movement.x = Input.GetAxis("Horizontal");
movement.y = Input.GetAxis("Vertical");
}
}
void HandleGroundCheck()
{
RaycastHit hit;
Vector3 direction = transform.TransformDirection(Vector3.down);
Ray ray = new Ray(transform.position, direction);
if(Physics.Raycast(ray, out hit, 3000, groundLayer))
{
distanceToground = hit.distance;
if(distanceToGround > 2)
{
isOnFround = false;
}
else
{
isOnGround = true;
}
}
}
void FixedUpdate()
{
//...
HelicopterMovements();
}
void HelicopterMovements()
{
if(movement.y > 0)
{
helicopterRigid.AddRelativeForce(Vector3.forward * Mathf.Max(0f,movement.y * ForwardForce * helicopterRigid.mass));
}
else if(movement.y < 0)
{
helicopterRigid.AddRelativeForce(Vector3.back * Mathf.Max(0f, -movement.y * BackwardForce * helicopterRigid.mass));
}
float turn = turnForce * Mathf.Lerp(movement.x, movement.x * (TurnForceHelper - Mathf.Abs(movement.y)), Mathf.Max(0f, movement.y));
turning = Mathf.Lerp(turning, turn, Time.fixedDeltaTime * turnForce);
helicopterRigid.AddRelativeTorque(0f, turning * helicopterRigid.mass, 0f);
}
}
首先是HandleGroundCheck函数,这个函数利用射线检测来判断直升机是否在地面,如果在地面上(isOnGround),那么就不让飞机执行移动逻辑了。
利用一个二维向量movement来让飞机执行前后左右移动的逻辑。如果飞机不在地面上,利用水平轴和垂直轴给movement的x和y分量进行赋值。
在HelicopterMovements中执行飞机移动的具体逻辑,其中if语句根据条件判断直升机前后移动,推力大小取决于movement.y的绝对值大小,并且利用Max方法及时在方向改变时截断为0,防止反向推力。
turn是旋转的力矩,这个参数的计算过程是首先获取垂直移动输入的绝对值。TurnForceHelper - Mathf.Abs(movement.y) 这部分的目的是为了由直升机前进或者后退来调整turn的大小,以便让飞机在向前飞或者向后飞的同时转弯保持平衡。后面就比较好理解了,在插值算出turning,这个值是真正加入到AddRelativeTorque以驱动飞机左后转弯的。
注意在Unity界面中Play时要在Inspector界面给ForwardForce和BackForce赋值,大小为10左右。
直升机前后左右移动时倾斜
如图为了让直升机在移动时可以倾斜,需要添加代码
//...
public Vector2 TILTING = Vector2.zero;
public float Forwardtiltforce;
public float Turntiltforce;
void FixedUpdate()
{
//...
HelicopterTilting();
}
void HelicopterTilting()
{
TILTING.y = Mathf.Lerp(TILTING.y, movement.y * Forwardtiltforce, Time.deltaTime);
TILTING.x = Mathf.Lerp(TILTING.x, movement.x * Turntiltforce, Time.deltaTime);
helicopterRigid.transform.rotation = Quaternion.Euler(TILTING.y, helicopterRigid.transform.loclalEulerAngles.y, -TILTING.x);
}
首先定义一个二维向量TILTING,该向量主要用来处理飞机的倾斜。然后添加了一个新的函数HelicopterTilting,这个函数中首先对TILTING的x和y分量进行了计算,还是利用插值函数去计算,对于y分量,其大小等于movement的y(就是按下Vertical轴时控制的大小)。算出x和y分量的大小之后,再将其附加给刚体的rotation属性上。
在Play时,设置Forwardtiltforce和Turntiltforce的大小,20左右就好。
状态修补
当前状态下,当我们按下油门键后再松开,那么飞机会一直上升,所以需要调整以保证在松开后飞机处于一个悬停的状态。
此外还需要保证飞机在移动倾斜过程中高度不会升高(如果能升高,那么只需要利用移动直升机带来的倾斜使得直升机高度上升,但是按照常识,只有在直升机增加油门飞机才会上升。)所以需要修改代码中的HandleInputs函数。
void HandleInputs()
{
if(Input.GetAxis("Throttle") > 0)
{
EnginePower += EngineLift;
}
else if ((Input.GetAxis("Vertical") != 0 || Input.GetAxis("Horizontal") != 0) && !isOnGround)
{
EnginePower = Mathf.Lerp(EnginePower,17.5f,0.001f);
}
else if(Input.GetAxis("Throttle") < 0.5f && !isGround)
{
EnginePower = Mathf.Lerp(EnginePower, 11f, 0.003f);
}
}
主要添加了两个else if语句,在第一个else if语句中当输入的Vertical轴大于0,也就是飞机处于移动状态时,将飞机的发动机功率置于当前到17.5之间,这个17.5取决于forwardforce。
第二个else if语句主要处理当油门松开后,发动机的功率将维持在当前和11之间,11也是取决飞机的RigidBody的mass和Drag属性(我这里mass是100,Drag是2)。这能保证飞机可以悬停,起始也不是完全悬停,而是缓慢下降,如果真的悬停,那么要这个就不是11,而是一个特别准确的值,不好判断。
同时两个else if中都包含是否在地面上的条件,这也是为了保证飞机是在成功起飞后才会进行判断。
启动发动机
这一部分主要实现一个功能,该功能就像是汽车插上钥匙打火的过程,当直升机在地面上时,按下一个键可以启动发送机,让发动机达到一个功率,我个人觉得有些多余,但还是记录一下,因为这一部分使用了一个很好用的插件DoTween,这个插件在接下来的文章中会好好介绍,这里先使用下。
首先去资源商店中下载DoTween资源,免费的就可以用了。导入之后,先不用修改,然后更新代码如下:
//...
using DG.Tweening;
//...
public float engineStartSpeed;
void Update()
{
//...
HandleEngine();
}
void HandleEngine()
{
if(Input.GetKeyDown(KeyCode.Q))
{
StartEngine();
}
else if(Input.GetKeyDown(KeyCode.E) && isOnGround)
{
StopEngine();
}
}
public void StartEngine()
{
DOTween.To(Starting, 0, 8.0f, engineStartSpeed);
}
void Starting(float value)
{
EnginePower = value;
}
public void StopEngine()
{
DOTween.To(Stopping, EnginePower, 0f, engineStartSpeed);
}
void Stopping(float value)
{
EnginePower = value;
}
这段代码是比较好理解的,在HandleEngine函数中,利用if语句按下Q键启动发动机,当飞机处于地面上时,按下E键即可将发动机熄火。
在启动和熄火的过程中都使用了DoTween的To方法去平滑值。具体的效果我会在之后的文章中写出,这里暂时使用这个方法去平滑发动机的启动和熄灭的过程。
直升机悬停在空中时做布朗运动
当直升机在空中悬停时,希望在增加一些细节,比如不让直升机静止不动,而是让它比较自然的晃动。这里就可以让它悬停时做布朗运动。
https://github.com/keijiro/Klak
下载好之后将其导入到项目中,然后在这个位置添加内置脚本Brownian Motion,如图
然后再创建MyBrownianMotionController脚本,脚本内容如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Klak.Motion;
using DG.Tweening;
public class MyBrownianMotionController : MonoBehaviour
{
private BrownianMotion motionController;
void Start()
{
motionController = GetComponent<BrownianMotion>();
motionController.positionFrequency = 0;
motionController.rotaionFrequency = 0;
}
public void StartMotion()
{
DOTween.To(Starting, 0, 0.2f, 3);
}
void Starting(float value)
{
motionController.positionFrequency = value;
motionController.rotationFrequency = value;
}
public void StopMotion()
{
DOTween.To(Stopping, motionController.positionFrequency, 0,1);
}
void Stopping(float value)
{
motionController.positionFrequency = value;
motionController.rotationFrequency = value;
}
}
在Brownian Motion脚本中有两个属性 positionFrequency 和rotationFrequency ,这两个属性的作用是控制物体做布朗运动的频率,一个是位置,另一个是旋转。在开始StartMotion函数中将这两个 变量置为0.2,在StopMotion函数中将其置为0。还是用到了DOTween的To方法来平滑。
这时候出现一个问题,就是即使飞机在地面上也会做布朗运动,这显然是不符合事实的,因此再次对HelicopterEngine做出更新
//...
using UnityEngine.Events
public class HelicopterEngine : MonoBehaviour
{
//...
public UnityEvent OnTakeOff;
public UnityEvent OnLand;
bool isFirstTime;
void Update()
{
//.....
HandleInvoke();
}
void HandleInvoke()
{
if(!isOnGround && isFirstTime)
{
OnTakeoff.Invoke();
isFirstTime = false;
}
else if(isOnGround && !isFirstTime)
{
OnLand.Invoke();
isFirstTime = true;
}
}
}
新定义了一个函数HanleInvoke,并且在Update中调用它,该函数中如果飞机不在地面上那么就唤醒OnTakeOff事件,反过来唤醒OnLand事件,同时多增加了一个条件isFirstTime,这个条件是为了防止事件被多次调用。具体来说,假设直升机在地面上,然后开始升空。如果没有isFirstTime变量的控制,OnTakeOff事件可能会在直升机不断升空的每一帧都被触发,而不仅仅是在直升机首次离开地面时触发一次。通过引入 isFirstTime变量,确保事件只在状态首次变化时触发一次,从而防止不必要的多次触发。
然后再Unity界面中做出这样的选择:
两个事件都是再RunTime是调用的,具体影响的是MyBrownianMotionController的函数,OnTakeOff是StartMotion,也就是开始布朗运动,OnLand则是StopMotion。
具体来说:一开始将两个运动频率置为0,然后飞机起飞→唤起OnTakeOff事件→调用MyBrownianMotionController脚本中的StartMotion函数,增加运动频率从而飞机做布朗运动。
飞机降落也是一样的道理。