【unity实战】从零使用unity手搓代码实现一个直升机物理和运动控制系统【附项目源码】_unity 仿真物理运动
最终效果
用unity从零手搓一个直升机控制器
文章目录
- 最终效果
- 前言
- 实战
-
- 1、直升机模型资源
- 2、直升机旋翼旋转
- 3、控制螺旋桨旋转
- 4、给机身添加碰撞体和刚体
- 5、直升机上升下降
- 6、控制直升机前进和后退移动
- 7、不在地面才可以前后移动
- 8、转向
- 9、移动转向倾斜
- 10、悬停
- 11、倾斜稳定发动机功率
- 12、快速启动关闭引擎
- 13、控制事件,以便我们可以在直升机起飞和降落时调用方法
- 14、悬停摆动效果
- 15、相机跟随
- 16、螺旋桨音效
- 17、添加风浪草动动效
- 18、修改天空盒、添加雾和灯光
- 19、添加后处理
- 最终代码
-
- 1、直升机旋翼控制脚本
- 2、直升机主引擎控制脚本
- 3、直升机事件回调
- 源码
- 专栏推荐
- 完结
前言
在游戏开发和仿真模拟领域,真实可信的飞行器物理模拟一直是极具挑战性的开发任务之一。直升机作为一种独特的旋翼飞行器,其飞行原理和控制系统与固定翼飞机有着本质区别,这为游戏物理模拟带来了特殊的复杂性和趣味性。
本文将带领读者从零开始,完全通过代码在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;/// /// 控制直升机旋翼旋转行为的脚本/// public class BladesController : MonoBehaviour{ /// /// 可选的旋转轴枚举 /// 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; /// /// 获取或设置旋翼转速(自动限制在0-3000范围内) /// public float BladeSpeed { get => _bladeSpeed; set => _bladeSpeed = Mathf.Clamp(value, 0, 3000); } /// /// 初始化时根据选择的轴更新旋转轴向量 /// 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; } } /// /// 每帧更新旋翼旋转 /// 计算最终旋转速度(考虑反向和乘数) /// 使用本地坐标系进行旋转 /// 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(); } /// /// 处理玩家输入控制 /// void HandleInputs() { _addEnginePowerInput = Input.GetKey(KeyCode.Space); _subtractEnginePowerInput = Input.GetKey(KeyCode.LeftControl); } /// /// 直升机动力 /// void HelicopterPower() { // 增加动力(Space键) if (_addEnginePowerInput) { EnginePower += _engineLift; } else { // 11f是平衡力大小 if (!isGround) EnginePower = Mathf.Lerp(EnginePower, 11f, 0.003f); } // 降低动力(LeftControl键) if (_subtractEnginePowerInput) EnginePower -= _engineLift; } /// /// 计算并应用直升机升力(根据高度自动调节升力效率) /// 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; // 当前实际倾斜角度/// /// 直升机机身倾斜效果/// 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; //动力调整平滑速度系数/// /// 直升机动力悬停/// 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; /// /// 直升机动力悬停/// void HelicopterHovering(){ if (!_addEnginePowerInput && !isGround && _movement.y <= 0.1f) { EnginePower = Mathf.Lerp(EnginePower, hoveringForce, hoverLerpSpeed); }}/// /// 稳定直升机发动机功率/// 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 快速启动关闭引擎/// /// 引擎控制/// void HandleEngine(){ if (!isGround) return; if (Input.GetKeyDown(KeyCode.T)) StartEngine(); if (Input.GetKeyDown(KeyCode.P)) StopEngine();}/// /// 启动直升机引擎(平滑增加动力)/// void StartEngine(){ if (_engineCoroutine != null) { StopCoroutine(_engineCoroutine); } _engineCoroutine = StartCoroutine(LerpEnginePower(EnginePower, startEnginePower, engineStartSpeed));}/// /// 停止直升机引擎(平滑减少动力)/// void StopEngine(){ // 停止当前的引擎协程(如果有的话) if (_engineCoroutine != null) { StopCoroutine(_engineCoroutine); } _engineCoroutine = StartCoroutine(LerpEnginePower(EnginePower, 0f, engineStartSpeed));}/// /// 引擎动力插值协程/// 实现引擎动力的平滑过渡效果/// /// 起始动力值/// 目标动力值/// 过渡时间(秒)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; // 是否起飞/// /// 处理事件触发/// 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(); } /// /// 开始布朗运动的方法 /// void StartMotion() { if (_motionCoroutine != null) StopCoroutine(_motionCoroutine); _motionCoroutine = StartCoroutine(LerpMotion(0, 0.2f, 3f)); } /// /// 停止布朗运动的方法 /// void StopMotion() { if (_motionCoroutine != null) StopCoroutine(_motionCoroutine); _motionCoroutine = StartCoroutine(LerpMotion(_brownianMotion.positionFrequency, 0f, 1f)); } /// /// 布朗运动插值协程 /// 实现布朗运动的平滑过渡效果 /// /// 起始值 /// 目标值 /// 过渡时间(秒) 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、相机跟随
参考:【unity知识】最新的Cinemachine3简单使用介绍
我都配置参考如下
防止抖动
效果
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;/// /// 气流控制/// 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;/// /// 控制直升机旋翼旋转行为的脚本/// public class BladesController : MonoBehaviour{ /// /// 可选的旋转轴枚举 /// 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; /// /// 获取或设置旋翼转速(自动限制在0-3000范围内) /// public float BladeSpeed { get => _bladeSpeed; set => _bladeSpeed = Mathf.Clamp(value, 0, 3000); } /// /// 初始化时根据选择的轴更新旋转轴向量 /// 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; } } /// /// 每帧更新旋翼旋转 /// 计算最终旋转速度(考虑反向和乘数) /// 使用本地坐标系进行旋转 /// 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 玩家输入 /// /// 处理玩家输入控制 /// void ProcessInputs() { _isAddingPower = Input.GetKey(KeyCode.Space); _isSubtractingPower = Input.GetKey(KeyCode.LeftControl); _inputMovement.x = Input.GetAxis(\"Horizontal\"); // 水平输入(左右移动) _inputMovement.y = Input.GetAxis(\"Vertical\"); // 垂直输入(前后移动) } #endregion #region 直升机控制 /// /// 直升机动力 /// void UpdateEnginePower() { // 增加动力(Space键) if (_isAddingPower) EnginePower += _engineLift; // 降低动力(LeftControl键) if (_isSubtractingPower) EnginePower -= _engineLift; } /// /// 计算并应用直升机升力(根据高度自动调节升力效率) /// 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); } /// /// 处理直升机前后移动 /// 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) ); } } /// /// 处理直升机转向 /// 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 触发事件 /// /// 处理事件触发 /// void HandleInvokes() { // 当直升机起飞时 if (!isGround && !_isTakeOff) { OnTakeOff.Invoke(); // 触发起飞事件 _isTakeOff = true; // 标记为已起飞 } // 当直升机降落时 if (isGround && _isTakeOff) { OnLand.Invoke(); // 触发降落事件 _isTakeOff = false; // 标记为已降落 } } #endregion #region 快速启动关闭引擎 /// /// 引擎控制 /// void HandleEngine() { if (!isGround) return; if (Input.GetKeyDown(KeyCode.T)) StartEngine(); if (Input.GetKeyDown(KeyCode.P)) StopEngine(); } /// /// 启动直升机引擎(平滑增加动力) /// void StartEngine() { if (_engineCoroutine != null) { StopCoroutine(_engineCoroutine); } _engineCoroutine = StartCoroutine(LerpEnginePower(EnginePower, startEnginePower, engineStartSpeed)); } /// /// 停止直升机引擎(平滑减少动力) /// void StopEngine() { // 停止当前的引擎协程(如果有的话) if (_engineCoroutine != null) { StopCoroutine(_engineCoroutine); } _engineCoroutine = StartCoroutine(LerpEnginePower(EnginePower, 0f, engineStartSpeed)); } /// /// 引擎动力插值协程 /// 实现引擎动力的平滑过渡效果 /// /// 起始动力值 /// 目标动力值 /// 过渡时间(秒) 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 效果 /// /// 直升机动力悬停 /// void HelicopterHovering() { if (!_isAddingPower && !isGround && _inputMovement.y <= 0.1f) { EnginePower = Mathf.Lerp(EnginePower, hoveringForce, hoverLerpSpeed); } } /// /// 倾斜稳定直升机发动机功率 /// void HelicopterStabilize() { if (!_isAddingPower && !isGround && _inputMovement.y > 0) { EnginePower = Mathf.Lerp(EnginePower, stabilizeForce, stabilizeSpeed); } } /// /// 直升机机身倾斜效果 /// 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 ); } /// /// 根据引擎动力调整音量 /// 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 布朗运动 /// /// 开始布朗运动的方法 /// void StartMotion() { if (_motionCoroutine != null) StopCoroutine(_motionCoroutine); _motionCoroutine = StartCoroutine(LerpMotion(0, 0.2f, 3f)); } /// /// 停止布朗运动的方法 /// void StopMotion() { if (_motionCoroutine != null) StopCoroutine(_motionCoroutine); _motionCoroutine = StartCoroutine(LerpMotion(_brownianMotion.positionFrequency, 0f, 1f)); } /// /// 布朗运动插值协程 /// 实现布朗运动的平滑过渡效果 /// /// 起始值 /// 目标值 /// 过渡时间(秒) 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,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!