【unity实战】从零使用unity手搓代码实现一个直升机物理和运动控制系统【附项目源码】
本文介绍了Unity中直升机模型的实现方法,推荐了两款优质直升机模型资源。重点讲解了直升机旋翼控制脚本(BladesController)的实现,包括可配置的旋转轴、转速限制和旋转方向控制。通过HelicopterMainEngineController脚本实现直升机动力系统,控制主旋翼和尾旋翼的转速比例,并处理油门输入。文章还涉及物理组件的添加,以及根据高度自动调节升力的算法,通过高度衰减系数使
最终效果
用unity从零手搓一个直升机控制器
文章目录
前言
在游戏开发和仿真模拟领域,真实可信的飞行器物理模拟一直是极具挑战性的开发任务之一。直升机作为一种独特的旋翼飞行器,其飞行原理和控制系统与固定翼飞机有着本质区别,这为游戏物理模拟带来了特殊的复杂性和趣味性。
本文将带领读者从零开始,完全通过代码在Unity引擎中实现一个完整的直升机物理和运动控制系统。
无论您是希望为游戏添加逼真的直升机体验,还是对飞行模拟编程有学术兴趣,亦或是单纯享受挑战复杂物理系统的乐趣,本教程都将为您提供一条清晰的实现路径。我们将从最基本的刚体物理开始,逐步构建完整的控制系统,最终实现一个响应灵敏、物理可信的直升机模拟器。
实战
1、直升机模型资源
这里我推荐两个模型,大家也可以找自己喜欢的模型
- https://assetstore.unity.com/packages/3d/vehicles/air/attack-helicopter-ii-animations-8405#reviews

- https://assetstore.unity.com/packages/3d/vehicles/air/military-attack-helicopter-hellfire-missile-4244#reviews

2、直升机旋翼旋转
直升机旋翼控制脚本,用于控制直升机旋翼的旋转行为
using UnityEngine;
/// <summary>
/// 控制直升机旋翼旋转行为的脚本
/// </summary>
public class BladesController : MonoBehaviour
{
/// <summary>
/// 可选的旋转轴枚举
/// </summary>
public enum Axis { X, Y, Z }
[Tooltip("启用时旋翼将反向旋转")]
[SerializeField] private bool inverseRotation = false;
[Tooltip("选择旋翼的旋转轴")]
[SerializeField] private Axis axis = Axis.X;
[Tooltip("旋转速度的乘数系数")]
[SerializeField] private float _speedMultiplier = 1f;
// 当前计算出的旋转轴向量
private Vector3 _rotationAxis;
private float _directionFactor; //旋转方向因素
// 当前旋翼转速(经过Clamp限制)
private float _bladeSpeed;
/// <summary>
/// 获取或设置旋翼转速(自动限制在0-3000范围内)
/// </summary>
public float BladeSpeed
{
get => _bladeSpeed;
set => _bladeSpeed = Mathf.Clamp(value, 0, 3000);
}
/// <summary>
/// 初始化时根据选择的轴更新旋转轴向量
/// </summary>
private void Awake()
{
_directionFactor = inverseRotation ? -1 : 1;
//根据当前axis枚举值更新实际的旋转轴向量
switch (axis)
{
case Axis.Y:
_rotationAxis = Vector3.up;
break;
case Axis.Z:
_rotationAxis = Vector3.forward;
break;
case Axis.X:
default:
_rotationAxis = Vector3.right;
break;
}
}
/// <summary>
/// 每帧更新旋翼旋转
/// 计算最终旋转速度(考虑反向和乘数)
/// 使用本地坐标系进行旋转
/// </summary>
void Update()
{
float currentSpeed = _directionFactor * _bladeSpeed * _speedMultiplier;
transform.Rotate(_rotationAxis, currentSpeed * Time.deltaTime, Space.Self);
}
}
在旋翼上挂载脚本,通常尾翼旋转更快

3、控制螺旋桨旋转
新增直升机主引擎控制脚本,控制直升机旋翼旋转
using UnityEngine;
// 直升机主引擎控制脚本
public class HelicopterMainEngineController : MonoBehaviour
{
public BladesController TopBlade; // 主旋翼控制器
public BladesController TailBlade; // 尾旋翼控制器
private float enginePower; // 引擎功率
// 引擎功率属性
public float EnginePower
{
get => enginePower;
set
{
// 主旋翼转速
TopBlade.BladeSpeed = value;
// 尾旋翼转速
TailBlade.BladeSpeed = value;
_enginePower = Mathf.Max( 0, value);// 确保不小于0
}
}
public float EngineLift = 0.0075f; // 引擎功率提升系数
void Update()
{
if (Input.GetKey(KeyCode.Space))
{
// 当有油门输入时,增加引擎功率
EnginePower += EngineLift;
}
}
}
效果
4、给机身添加碰撞体和刚体
注意这里我修改了刚体质量和阻力,碰撞体其实使用胶囊提更好
5、直升机上升下降
using UnityEngine;
// 直升机主引擎控制脚本
public class HelicopterMainEngineController : MonoBehaviour
{
[Header("组件引用")]
public BladesController topBlade; // 主旋翼控制器
public BladesController tailBlade; // 尾旋翼控制器
private Rigidbody _helicopterRigid; // 直升机刚体组件
[Header("输入参数")]
private bool _addEnginePowerInput;
private bool _subtractEnginePowerInput;
[Header("引擎参数")]
public float effectiveHeight = 50f; // 有效悬停高度(米)
public float _engineLift = 0.0075f; // 油门增减灵敏度
private float _enginePower; // 当前引擎动力值
// 引擎动力属性(带旋翼同步控制)
public float EnginePower
{
get => _enginePower;
set
{
topBlade.BladeSpeed = value;
tailBlade.BladeSpeed = value;
_enginePower = Mathf.Max( 0, value);// 确保不小于0
}
}
void Start() {
_helicopterRigid= GetComponent<Rigidbody>();
}
void Update() {
HandleInputs();
HelicopterPower();
}
void FixedUpdate() {
ApplyHelicopterLift();
}
/// <summary>
/// 处理玩家输入控制
/// </summary>
void HandleInputs() {
_addEnginePowerInput = Input.GetKey(KeyCode.Space);
_subtractEnginePowerInput = Input.GetKey(KeyCode.LeftControl);
}
/// <summary>
/// 直升机动力
/// </summary>
void HelicopterPower()
{
// 增加动力(Space键)
if (_addEnginePowerInput)
{
EnginePower += _engineLift;
}
else
{
// 11f是平衡力大小
if (!isGround) EnginePower = Mathf.Lerp(EnginePower, 11f, 0.003f);
}
// 降低动力(LeftControl键)
if (_subtractEnginePowerInput)
EnginePower -= _engineLift;
}
/// <summary>
/// 计算并应用直升机升力(根据高度自动调节升力效率)
/// </summary>
void ApplyHelicopterLift()
{
// 高度衰减系数(0=超过有效高度,1=地面),当前高度越高(heightFactor越小),升力效率越低
float heightFactor = 1 - Mathf.Clamp01(_helicopterRigid.transform.position.y / effectiveHeight);
// 计算实际升力,使用Lerp在0和EnginePower之间插值,高度越高可用动力越小
float upForce = Mathf.Lerp(0, EnginePower, heightFactor) * _helicopterRigid.mass;
// 在直升机局部坐标系施加升力
_helicopterRigid.AddRelativeForce(Vector3.up * upForce);
}
}
效果,LeftControl键控制降落,Space键控制上升。长按Space键会逐渐积累动力,当动力达到一定值时,直升机起飞
6、控制直升机前进和后退移动
[Header("移动参数")]
public float forwardForce = 15f; // 前进施加的力大小
public float backwardForce = 15f; // 后退施加的力大小
private Vector2 _movement = Vector2.zero;// 存储输入方向的二维向量(初始值为零)
void HandleInputs()
{
// 。。。
_movement.x = Input.GetAxis("Horizontal"); // 水平输入(左右移动)
_movement.y = Input.GetAxis("Vertical"); // 垂直输入(前后移动)
}
void FixedUpdate()
{
ApplyHelicopterLift();
HelicopterMovements();
}
// 处理直升机前后移动
void HelicopterMovements()
{
if (isGround) return;
// 如果输入方向为前(W键或上箭头)
if (_movement.y > 0)
{
// 施加向前的力,大小取决于输入值 (_movement.y)、ForwardForce 和直升机质量
// Mathf.Max(0f, ...) 确保力不小于0
_helicopterRigid.AddRelativeForce(
Vector3.forward * Mathf.Max(0f, _movement.y * forwardForce * _helicopterRigid.mass)
);
}
// 如果输入方向为后(S键或下箭头)
else if (_movement.y < 0)
{
// 施加向后的力,取输入绝对值并乘以 backwardForce 和质量
_helicopterRigid.AddRelativeForce(
Vector3.back * Mathf.Max(0f, -_movement.y * backwardForce * _helicopterRigid.mass)
);
}
}
效果
7、不在地面才可以前后移动
目前直升机在地面也可以进行前后移动,所以我们要先添加一个地面检测,来修复这个问题
[Header("地面检测")]
public LayerMask groundLayer; // 用于检测的地面层级
public float distance = 5f; // 射线检测距离
public bool isGround = true; // 是否在地面上
private RaycastHit _hit; // 存储射线检测结果
private Vector3 _direction; //检测方向
void Update()
{
HandleInputs();
HelicopterPower();
HandleGroundCheck();
}
#region 地面检测
// 地面检测方法
void HandleGroundCheck()
{
// 将物体的局部坐标系中的"向下"方向转换到世界坐标系。(确保旋转不影响检测方向,比如斜坡着陆时)
_direction = transform.TransformDirection(Vector3.down);
// 检测射线是否碰到地面层
if (Physics.Raycast(transform.position, _direction, out _hit, distance, groundLayer))
{
isGround = true;
}
// 如果未检测到地面(如悬空状态),则判定为不在地面
else
{
isGround = false;
}
}
//在场景视图显示检测,方便调试
void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
//地面检测可视化
Gizmos.DrawLine(transform.position, transform.position + transform.TransformDirection(Vector3.down) * distance);
}
#endregion
然后限制直升机在地面不能进行移动
// 处理直升机前后移动
void HelicopterMovements()
{
if (isGround) return;
//...
}
配置参数,记得修改地面层为Ground
效果
8、转向
转向力计算(复合运算)
-
- 基础转向:Movement.x(A/D键输入)
-
- 动态调整:TurnForceHelper - 垂直输入绝对值,高速时降低灵敏度,不然容易速度太快导致失控
-
- 使用Lerp平滑过渡(Mathf.Max防止负值)
[Header("转向参数")]
public float turnForce = 10f; // 转向力参数(控制直升机转向强度)
private float _turnForceHelper = 1.5f; // 转向辅助系数(用于调整转向灵敏度)
private float _turning = 0f; // 当前转向值(用于平滑过渡)
// 处理直升机转向
void HelicopterTurn()
{
if (isGround) return;
// 转向力计算(复合运算)
// 1. 基础转向:movement.x(A/D键输入)
// 2. 动态调整:TurnForceHelper - 垂直输入绝对值,高速时降低灵敏度,不然容易速度太快导致失控
// 3. 使用Lerp平滑过渡(Mathf.Max防止负值)
float turn = turnForce * Mathf.Lerp(
_movement.x,
// _movement.x * (_turnForceHelper * Mathf.Abs(_movement.y)),
_movement.x * (_turnForceHelper - Mathf.Abs(_movement.y)),
Mathf.Max(0f, _movement.y)
);
// 转向平滑过渡(避免突变)
// Time.fixedDeltaTime保证物理帧率无关
_turning = Mathf.Lerp(
_turning,
turn,
Time.fixedDeltaTime * turnForce
);
// 施加转向扭矩(只在Y轴旋转)
// 乘以质量保证物理一致性
_helicopterRigid.AddRelativeTorque(
0f,
_turning * _helicopterRigid.mass,
0f
);
}
效果
9、移动转向倾斜
[Header("倾斜参数")]
public float maxPitchAngle = 20f; //前飞时的最大俯仰角度(度)
public float maxRollAngle = 30f; //转向时的最大滚转角度(度)
private Vector2 _currentTiltAngle = Vector2.zero; // 当前实际倾斜角度
/// <summary>
/// 直升机机身倾斜效果
/// </summary>
void HelicopterTilting()
{
if (isGround) return;
// 俯仰轴(前后倾斜)
_currentTiltAngle.y = Mathf.Lerp(
_currentTiltAngle.y,
_movement.y * maxPitchAngle,
Time.deltaTime
);
// 滚转轴(左右倾斜)
_currentTiltAngle.x = Mathf.Lerp(
_currentTiltAngle.x,
_movement.x * maxRollAngle,
Time.deltaTime
);
// 应用旋转(保持原有偏航角)
_helicopterRigid.transform.localRotation = Quaternion.Euler(
_currentTiltAngle.y,
_helicopterRigid.transform.localEulerAngles.y,
-_currentTiltAngle.x
);
}
效果
10、悬停
目前如果我们的按住空格让直升机处于上升状态,这时即使我们取消输入,直升机仍然会以当前升力继续上升,显然很这不好。这里我添加悬停限制,我希望不在地面且松开空格时,直升机能迅速降低上升力,最终以比较缓慢的速度慢慢下落。
悬停力的大小很大程度上取决于你的刚体质量和阻力,可以根据你的项目做调整,我这里直升机质量是100,线性阻力是4,我觉得10是个不错的数值。
[Header("悬停参数")]
public float hoveringForce = 10f; //悬停力
public float hoverLerpSpeed = 0.003f; //动力调整平滑速度系数
/// <summary>
/// 直升机动力悬停
/// </summary>
void HelicopterHovering()
{
if (!_addEnginePowerInput && !isGround) {
EnginePower = Mathf.Lerp(EnginePower, hoveringForce, hoverLerpSpeed);
}
}
效果
11、倾斜稳定发动机功率
我们需要限制直升机不会因为前进后退倾斜而发生高度变化
同样,这里我测试觉得17.5f是个不错的数值
[Header("倾斜稳定参数")]
public float stabilizeForce = 17.5f;
public float stabilizeSpeed = 0.003f;
/// <summary>
/// 直升机动力悬停
/// </summary>
void HelicopterHovering()
{
if (!_addEnginePowerInput && !isGround && _movement.y <= 0.1f) {
EnginePower = Mathf.Lerp(EnginePower, hoveringForce, hoverLerpSpeed);
}
}
/// <summary>
/// 稳定直升机发动机功率
/// </summary>
void HelicopterStabilize()
{
if (!_addEnginePowerInput && !isGround && _movement.y > 0) {
EnginePower = Mathf.Lerp(EnginePower, stabilizeForce, stabilizeSpeed);
}
}
效果
12、快速启动关闭引擎
目前仅仅依靠Space,直升机启动太慢了,我们可以添加快速启动关闭引擎控制按钮
[Header("引擎控制")]
public float startEnginePower = 8f; // 启动最终到达动力
public float engineStartSpeed = 2f; // 过渡时间
private Coroutine _engineCoroutine; // 引擎协程
#region 快速启动关闭引擎
/// <summary>
/// 引擎控制
/// </summary>
void HandleEngine()
{
if (!isGround) return;
if (Input.GetKeyDown(KeyCode.T)) StartEngine();
if (Input.GetKeyDown(KeyCode.P)) StopEngine();
}
/// <summary>
/// 启动直升机引擎(平滑增加动力)
/// </summary>
void StartEngine()
{
if (_engineCoroutine != null)
{
StopCoroutine(_engineCoroutine);
}
_engineCoroutine = StartCoroutine(LerpEnginePower(EnginePower, startEnginePower, engineStartSpeed));
}
/// <summary>
/// 停止直升机引擎(平滑减少动力)
/// </summary>
void StopEngine()
{
// 停止当前的引擎协程(如果有的话)
if (_engineCoroutine != null)
{
StopCoroutine(_engineCoroutine);
}
_engineCoroutine = StartCoroutine(LerpEnginePower(EnginePower, 0f, engineStartSpeed));
}
/// <summary>
/// 引擎动力插值协程
/// 实现引擎动力的平滑过渡效果
/// </summary>
/// <param name="start">起始动力值</param>
/// <param name="end">目标动力值</param>
/// <param name="duration">过渡时间(秒)</param>
IEnumerator LerpEnginePower(float start, float end, float duration)
{
float elapsed = 0f;// 已过去的时间
// 循环直到达到指定持续时间
while (elapsed < duration)
{
// // Mathf.Lerp在start和end之间根据elapsed / duration比例插值
EnginePower = Mathf.Lerp(start, end, elapsed / duration);
// 累加帧时间(使用Time.deltaTime保证帧率无关)
elapsed += Time.deltaTime;
yield return null;
}
// 确保最终精确达到目标值(避免浮点数精度问题)
EnginePower = end;
}
#endregion
效果
13、控制事件,以便我们可以在直升机起飞和降落时调用方法
添加起飞和降落触发事件配置
[Header("事件控制")]
public UnityEvent OnTakeOff; // 起飞事件
public UnityEvent OnLand; // 降落事件
private bool _isTakeOff; // 是否起飞
/// <summary>
/// 处理事件触发
/// </summary>
void HandleInvokes()
{
// 当直升机起飞时
if (!isGround && !_isTakeOff)
{
OnTakeOff.Invoke(); // 触发起飞事件
_isTakeOff = true; // 标记为已起飞
}
// 当直升机降落时
if (isGround && _isTakeOff)
{
OnLand.Invoke(); // 触发降落事件
_isTakeOff = false; // 标记为已降落
}
}
新增HelicopterEvent脚本,模拟直升机事件回调
using UnityEngine;
// 直升机事件回调
public class HelicopterEvent : MonoBehaviour
{
public void TakeOff()
{
Debug.Log("起飞");
}
public void Land()
{
Debug.Log("降落");
}
}
绑定事件
效果
后续我们可以很方便的在这里添加音效和特效等等
14、悬停摆动效果
使用插件:【推荐100个unity插件】Unity 创意编程库——Klak插件的使用
这里我们给直升机新建一个父物体,需要把布朗运动和控制分开,刚体、碰撞体、控制脚本都放在最外层
将布朗运动脚本加到机身上,给机身添加Brownian Motion脚本即可,配置参数
运行游戏,发现机身已经开始很自然的摆动了
当然我们希望通过脚本来控制,修改直升机事件回调脚本
using System.Collections;
using Klak.Motion;
using UnityEngine;
// 直升机事件回调
public class HelicopterEvent : MonoBehaviour
{
private BrownianMotion _brownianMotion;
private Coroutine _motionCoroutine;
void Start()
{
_brownianMotion = GetComponent<BrownianMotion>();
//默认不进行布朗运动
_brownianMotion.positionFrequency = 0;
_brownianMotion.rotationFrequency = 0;
}
public void TakeOff()
{
Debug.Log("起飞");
StartMotion();
}
public void Land()
{
Debug.Log("降落");
StopMotion();
}
/// <summary>
/// 开始布朗运动的方法
/// </summary>
void StartMotion()
{
if (_motionCoroutine != null) StopCoroutine(_motionCoroutine);
_motionCoroutine = StartCoroutine(LerpMotion(0, 0.2f, 3f));
}
/// <summary>
/// 停止布朗运动的方法
/// </summary>
void StopMotion()
{
if (_motionCoroutine != null) StopCoroutine(_motionCoroutine);
_motionCoroutine = StartCoroutine(LerpMotion(_brownianMotion.positionFrequency, 0f, 1f));
}
/// <summary>
/// 布朗运动插值协程
/// 实现布朗运动的平滑过渡效果
/// </summary>
/// <param name="start">起始值</param>
/// <param name="end">目标值</param>
/// <param name="duration">过渡时间(秒)</param>
IEnumerator LerpMotion(float start, float end, float duration)
{
float elapsed = 0f;// 已过去的时间
// 循环直到达到指定持续时间
while (elapsed < duration)
{
// 设置位置和旋转频率为当前渐变值
_brownianMotion.positionFrequency = Mathf.Lerp(start, end, elapsed / duration);
_brownianMotion.rotationFrequency = Mathf.Lerp(start, end, elapsed / duration);
// 累加帧时间(使用Time.deltaTime保证帧率无关)
elapsed += Time.deltaTime;
yield return null;
}
// 确保最终精确达到目标值(避免浮点数精度问题)
_brownianMotion.positionFrequency = end;
_brownianMotion.rotationFrequency = end;
}
}
挂载脚本
配置事件回调
避免跟随抖动
效果
15、相机跟随
我都配置参考如下
防止抖动
效果
16、螺旋桨音效
我这里用的免费的音效资源:直升机,大家也可以自己
配置
代码控制根据引擎动力调整音量
[Header("音效")]
public float volumeMaxToEnginePower = 20f; //音效最大对应的引擎动力
private AudioSource _audioSource;
#region 效果
//音效
void HandleSound(){
// 根据引擎动力调整音量
// 将0-volumeMaxToEnginePower的动力值转换为0-1的音量范围
_audioSource.volume = Mathf.Clamp01(_enginePower / volumeMaxToEnginePower);
}
#endregion
17、添加风浪草动动效
具体参考:【推荐100个unity插件】完全程序化且动态的 性能极佳的Unity URP物理交互草地——UnityURP-InfiniteGrass的使用
粒子效果如下

代码
[Header("气流粒子")]
[SerializeField] private ParticleSystem airCurrentParticleSystem;
/// <summary>
/// 气流控制
/// </summary>
void HandleAirCurrent()
{
if (_enginePower > 0)
{
if(!airCurrentParticleSystem.isPlaying) airCurrentParticleSystem.Play();
// 获取粒子系统的emission模块
var emission = airCurrentParticleSystem.emission;
float emissionRate = Mathf.Lerp(1f, 2.5f, _enginePower / volumeMaxToEnginePower);
// 设置粒子发射率
emission.rateOverTime = emissionRate;
}
if (_enginePower <= 0 && airCurrentParticleSystem.isPlaying) airCurrentParticleSystem.Stop();
}
效果
18、修改天空盒、添加雾和灯光



最终效果
19、添加后处理
具体参考:【unity游戏开发入门到精通——通用篇】Post Processing 后处理插件Post-process Volume 和Volume最全基础使用说明

效果
最终代码
1、直升机旋翼控制脚本
using UnityEngine;
/// <summary>
/// 控制直升机旋翼旋转行为的脚本
/// </summary>
public class BladesController : MonoBehaviour
{
/// <summary>
/// 可选的旋转轴枚举
/// </summary>
public enum Axis { X, Y, Z }
[Tooltip("启用时旋翼将反向旋转")]
[SerializeField] private bool inverseRotation = false;
[Tooltip("选择旋翼的旋转轴")]
[SerializeField] private Axis axis = Axis.X;
[Tooltip("旋转速度的乘数系数")]
[SerializeField] private float _speedMultiplier = 1f;
// 当前计算出的旋转轴向量
private Vector3 _rotationAxis;
private float _directionFactor; //旋转方向因素
// 当前旋翼转速(经过Clamp限制)
private float _bladeSpeed;
/// <summary>
/// 获取或设置旋翼转速(自动限制在0-3000范围内)
/// </summary>
public float BladeSpeed
{
get => _bladeSpeed;
set => _bladeSpeed = Mathf.Clamp(value, 0, 3000);
}
/// <summary>
/// 初始化时根据选择的轴更新旋转轴向量
/// </summary>
private void Awake()
{
_directionFactor = inverseRotation ? -1 : 1;
//根据当前axis枚举值更新实际的旋转轴向量
switch (axis)
{
case Axis.Y:
_rotationAxis = Vector3.up;
break;
case Axis.Z:
_rotationAxis = Vector3.forward;
break;
case Axis.X:
default:
_rotationAxis = Vector3.right;
break;
}
}
/// <summary>
/// 每帧更新旋翼旋转
/// 计算最终旋转速度(考虑反向和乘数)
/// 使用本地坐标系进行旋转
/// </summary>
void Update()
{
float currentSpeed = _directionFactor * _bladeSpeed * _speedMultiplier;
transform.Rotate(_rotationAxis, currentSpeed * Time.deltaTime, Space.Self);
}
}
2、直升机主引擎控制脚本
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
// 直升机主引擎控制脚本
public class HelicopterMainEngineController : MonoBehaviour
{
[Header("组件引用")]
[SerializeField] private BladesController topBlade; // 主旋翼控制器
[SerializeField] private BladesController tailBlade; // 尾旋翼控制器
private Rigidbody _rigidbody; // 直升机刚体组件
[Header("输入参数")]
private bool _isAddingPower;
private bool _isSubtractingPower;
[Header("引擎参数")]
[SerializeField] private float effectiveHeight = 50f; // 有效悬停高度(米)
[SerializeField] private float _engineLift = 0.0075f; // 油门增减灵敏度
private float _mass; //质量
private float _enginePower; // 当前引擎动力值
// 引擎动力属性(带旋翼同步控制)
public float EnginePower
{
get => _enginePower;
set
{
// 主旋翼转速
topBlade.BladeSpeed = value * mainRotorSpeedFactor;
// 尾旋翼转速
tailBlade.BladeSpeed = value * tailRotorSpeedFactor;
_enginePower = Mathf.Max(0, value);// 确保不小于0
}
}
[Header("移动参数")]
[SerializeField] private float forwardForce = 15f; // 前进施加的力大小
[SerializeField] private float backwardForce = 15f; // 后退施加的力大小
private Vector2 _inputMovement = Vector2.zero;// 存储输入方向的二维向量(初始值为零)
[Header("地面检测")]
[SerializeField] private LayerMask groundLayer; // 用于检测的地面层级
[SerializeField] private float distance = 5f; // 射线检测距离
[SerializeField] private bool isGround = true; // 是否在地面上
private RaycastHit _hit; // 存储射线检测结果
private Vector3 _direction; //检测方向
[Header("转向参数")]
[SerializeField] private float turnForce = 10f; // 转向力参数(控制直升机转向强度)
private float _turnSensitivityFactor = 1.5f; // 转向敏感度系数
private float _turning = 0f; // 当前转向值(用于平滑过渡)
[Header("倾斜参数")]
[SerializeField] private float maxPitchAngle = 20f; //前飞时的最大俯仰角度(度)
[SerializeField] private float maxRollAngle = 30f; //转向时的最大滚转角度(度)
private Vector2 _currentTiltAngle = Vector2.zero; // 当前实际倾斜角度
[Header("悬停参数")]
[SerializeField] private float hoveringForce = 10f; //悬停力
[SerializeField] private float hoverLerpSpeed = 0.003f; //动力调整平滑速度系数
[Header("倾斜稳定参数")]
[SerializeField] private float stabilizeForce = 17.5f;//倾斜稳定力
[SerializeField] private float stabilizeSpeed = 0.003f;//倾斜稳定动力调整平滑速度系数
[Header("引擎控制")]
[SerializeField] private float startEnginePower = 8f; // 启动最终到达动力
[SerializeField] private float engineStartSpeed = 2f; // 过渡时间
private Coroutine _engineCoroutine; // 引擎协程
[Header("事件控制")]
[SerializeField] private UnityEvent OnTakeOff; // 起飞事件
[SerializeField] private UnityEvent OnLand; // 降落事件
private bool _isTakeOff; // 是否起飞
[Header("音效")]
[SerializeField] private float volumeMaxToEnginePower = 20f; //音效最大对应的引擎动力
private AudioSource _audioSource;
void Start()
{
_rigidbody = GetComponent<Rigidbody>();
_audioSource = GetComponent<AudioSource>();
_mass = _rigidbody.mass;
}
void Update()
{
ProcessInputs();
UpdateEnginePower();
HandleGroundCheck();
HelicopterTilting();
HelicopterHovering();
HelicopterStabilize();
HandleEngine();
HandleInvokes();
HandleSound();
}
void FixedUpdate()
{
ApplyLiftForce();
HelicopterMovements();
HelicopterTurn();
}
#region 玩家输入
/// <summary>
/// 处理玩家输入控制
/// </summary>
void ProcessInputs()
{
_isAddingPower = Input.GetKey(KeyCode.Space);
_isSubtractingPower = Input.GetKey(KeyCode.LeftControl);
_inputMovement.x = Input.GetAxis("Horizontal"); // 水平输入(左右移动)
_inputMovement.y = Input.GetAxis("Vertical"); // 垂直输入(前后移动)
}
#endregion
#region 直升机控制
/// <summary>
/// 直升机动力
/// </summary>
void UpdateEnginePower()
{
// 增加动力(Space键)
if (_isAddingPower) EnginePower += _engineLift;
// 降低动力(LeftControl键)
if (_isSubtractingPower) EnginePower -= _engineLift;
}
/// <summary>
/// 计算并应用直升机升力(根据高度自动调节升力效率)
/// </summary>
void ApplyLiftForce()
{
// 高度衰减系数(0=超过有效高度,1=地面),当前高度越高(heightFactor越小),升力效率越低
float heightFactor = 1 - Mathf.Clamp01(_rigidbody.transform.position.y / effectiveHeight);
// 计算实际升力,使用Lerp在0和EnginePower之间插值,高度越高可用动力越小
float upForce = Mathf.Lerp(0, EnginePower, heightFactor) * _mass;
// 在直升机局部坐标系施加升力
_rigidbody.AddRelativeForce(Vector3.up * upForce);
}
/// <summary>
/// 处理直升机前后移动
/// </summary>
void HelicopterMovements()
{
if (isGround) return;
// 如果输入方向为前(W键或上箭头)
if (_inputMovement.y > 0)
{
// 施加向前的力,大小取决于输入值 (_inputMovement.y)、ForwardForce 和直升机质量
// Mathf.Max(0f, ...) 确保力不小于0
_rigidbody.AddRelativeForce(
Vector3.forward * Mathf.Max(0f, _inputMovement.y * forwardForce * _mass)
);
}
// 如果输入方向为后(S键或下箭头)
else if (_inputMovement.y < 0)
{
// 施加向后的力,取输入绝对值并乘以 backwardForce 和质量
_rigidbody.AddRelativeForce(
Vector3.back * Mathf.Max(0f, -_inputMovement.y * backwardForce * _mass)
);
}
}
/// <summary>
/// 处理直升机转向
/// </summary>
void HelicopterTurn()
{
if (isGround) return;
// 转向力计算(复合运算)
// 1. 基础转向:movement.x(A/D键输入)
// 2. 动态调整:TurnForceHelper - 垂直输入绝对值,高速时降低灵敏度,不然容易速度太快导致失控
// 3. 使用Lerp平滑过渡(Mathf.Max防止负值)
float turn = turnForce * Mathf.Lerp(
_inputMovement.x,
// _inputMovement.x * (_turnSensitivityFactor * Mathf.Abs(_inputMovement.y)),
_inputMovement.x * (_turnSensitivityFactor - Mathf.Abs(_inputMovement.y)),
Mathf.Max(0f, _inputMovement.y)
);
// 转向平滑过渡(避免突变)
// Time.fixedDeltaTime保证物理帧率无关
_turning = Mathf.Lerp(
_turning,
turn,
Time.fixedDeltaTime * turnForce
);
// 施加转向扭矩(只在Y轴旋转)
// 乘以质量保证物理一致性
_rigidbody.AddRelativeTorque(
0f,
_turning * _mass,
0f
);
}
#endregion
#region 触发事件
/// <summary>
/// 处理事件触发
/// </summary>
void HandleInvokes()
{
// 当直升机起飞时
if (!isGround && !_isTakeOff)
{
OnTakeOff.Invoke(); // 触发起飞事件
_isTakeOff = true; // 标记为已起飞
}
// 当直升机降落时
if (isGround && _isTakeOff)
{
OnLand.Invoke(); // 触发降落事件
_isTakeOff = false; // 标记为已降落
}
}
#endregion
#region 快速启动关闭引擎
/// <summary>
/// 引擎控制
/// </summary>
void HandleEngine()
{
if (!isGround) return;
if (Input.GetKeyDown(KeyCode.T)) StartEngine();
if (Input.GetKeyDown(KeyCode.P)) StopEngine();
}
/// <summary>
/// 启动直升机引擎(平滑增加动力)
/// </summary>
void StartEngine()
{
if (_engineCoroutine != null)
{
StopCoroutine(_engineCoroutine);
}
_engineCoroutine = StartCoroutine(LerpEnginePower(EnginePower, startEnginePower, engineStartSpeed));
}
/// <summary>
/// 停止直升机引擎(平滑减少动力)
/// </summary>
void StopEngine()
{
// 停止当前的引擎协程(如果有的话)
if (_engineCoroutine != null)
{
StopCoroutine(_engineCoroutine);
}
_engineCoroutine = StartCoroutine(LerpEnginePower(EnginePower, 0f, engineStartSpeed));
}
/// <summary>
/// 引擎动力插值协程
/// 实现引擎动力的平滑过渡效果
/// </summary>
/// <param name="start">起始动力值</param>
/// <param name="end">目标动力值</param>
/// <param name="duration">过渡时间(秒)</param>
IEnumerator LerpEnginePower(float start, float end, float duration)
{
float elapsed = 0f;// 已过去的时间
// 循环直到达到指定持续时间
while (elapsed < duration)
{
// // Mathf.Lerp在start和end之间根据elapsed / duration比例插值
EnginePower = Mathf.Lerp(start, end, elapsed / duration);
// 累加帧时间(使用Time.deltaTime保证帧率无关)
elapsed += Time.deltaTime;
yield return null;
}
// 确保最终精确达到目标值(避免浮点数精度问题)
EnginePower = end;
}
#endregion
#region 效果
/// <summary>
/// 直升机动力悬停
/// </summary>
void HelicopterHovering()
{
if (!_isAddingPower && !isGround && _inputMovement.y <= 0.1f)
{
EnginePower = Mathf.Lerp(EnginePower, hoveringForce, hoverLerpSpeed);
}
}
/// <summary>
/// 倾斜稳定直升机发动机功率
/// </summary>
void HelicopterStabilize()
{
if (!_isAddingPower && !isGround && _inputMovement.y > 0)
{
EnginePower = Mathf.Lerp(EnginePower, stabilizeForce, stabilizeSpeed);
}
}
/// <summary>
/// 直升机机身倾斜效果
/// </summary>
void HelicopterTilting()
{
if (isGround) return;
// 俯仰轴(前后倾斜)
_currentTiltAngle.y = Mathf.Lerp(
_currentTiltAngle.y,
_inputMovement.y * maxPitchAngle,
Time.deltaTime
);
// 滚转轴(左右倾斜)
_currentTiltAngle.x = Mathf.Lerp(
_currentTiltAngle.x,
_inputMovement.x * maxRollAngle,
Time.deltaTime
);
// 应用旋转(保持原有偏航角)
_rigidbody.transform.localRotation = Quaternion.Euler(
_currentTiltAngle.y,
_rigidbody.transform.localEulerAngles.y,
-_currentTiltAngle.x
);
}
/// <summary>
/// 根据引擎动力调整音量
/// </summary>
void HandleSound(){
// 将0-volumeMaxToEnginePower的动力值转换为0-1的音量范围
_audioSource.volume = Mathf.Clamp01(_enginePower / volumeMaxToEnginePower);
}
#endregion
#region 地面检测
// 地面检测方法
void HandleGroundCheck()
{
// 将物体的局部坐标系中的"向下"方向转换到世界坐标系。(确保旋转不影响检测方向,比如斜坡着陆时)
_direction = transform.TransformDirection(Vector3.down);
// 检测射线是否碰到地面层
if (Physics.Raycast(transform.position, _direction, out _hit, distance, groundLayer))
{
isGround = true;
}
// 如果未检测到地面(如悬空状态),则判定为不在地面
else
{
isGround = false;
}
}
//在场景视图显示检测,方便调试
void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
//地面检测可视化
Gizmos.DrawLine(transform.position, transform.position + transform.TransformDirection(Vector3.down) * distance);
}
#endregion
}
3、直升机事件回调
using System.Collections;
using Klak.Motion;
using UnityEngine;
// 直升机事件回调
public class HelicopterEvent : MonoBehaviour
{
private BrownianMotion _brownianMotion;
private Coroutine _motionCoroutine;
void Start()
{
_brownianMotion = GetComponent<BrownianMotion>();
//默认不进行布朗运动
_brownianMotion.positionFrequency = 0;
_brownianMotion.rotationFrequency = 0;
}
public void TakeOff()
{
Debug.Log("起飞");
StartMotion();
}
public void Land()
{
Debug.Log("降落");
StopMotion();
}
#region 布朗运动
/// <summary>
/// 开始布朗运动的方法
/// </summary>
void StartMotion()
{
if (_motionCoroutine != null) StopCoroutine(_motionCoroutine);
_motionCoroutine = StartCoroutine(LerpMotion(0, 0.2f, 3f));
}
/// <summary>
/// 停止布朗运动的方法
/// </summary>
void StopMotion()
{
if (_motionCoroutine != null) StopCoroutine(_motionCoroutine);
_motionCoroutine = StartCoroutine(LerpMotion(_brownianMotion.positionFrequency, 0f, 1f));
}
/// <summary>
/// 布朗运动插值协程
/// 实现布朗运动的平滑过渡效果
/// </summary>
/// <param name="start">起始值</param>
/// <param name="end">目标值</param>
/// <param name="duration">过渡时间(秒)</param>
IEnumerator LerpMotion(float start, float end, float duration)
{
float elapsed = 0f;// 已过去的时间
// 循环直到达到指定持续时间
while (elapsed < duration)
{
// 设置位置和旋转频率为当前渐变值
_brownianMotion.positionFrequency = Mathf.Lerp(start, end, elapsed / duration);
_brownianMotion.rotationFrequency = Mathf.Lerp(start, end, elapsed / duration);
// 累加帧时间(使用Time.deltaTime保证帧率无关)
elapsed += Time.deltaTime;
yield return null;
}
// 确保最终精确达到目标值(避免浮点数精度问题)
_brownianMotion.positionFrequency = end;
_brownianMotion.rotationFrequency = end;
}
#endregion
}
源码
https://gitee.com/unity_data/unity3-dhelicopter-controller
专栏推荐
完结
好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
更多推荐
所有评论(0)