最终效果

用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、转向

转向力计算(复合运算)

    1. 基础转向:Movement.x(A/D键输入)
    1. 动态调整:TurnForceHelper - 垂直输入绝对值,高速时降低灵敏度,不然容易速度太快导致失控
    1. 使用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、相机跟随

参考:【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;

/// <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
在这里插入图片描述


专栏推荐

地址
【unity游戏开发入门到精通——C#篇】
【unity游戏开发入门到精通——unity通用篇】
【unity游戏开发入门到精通——unity3D篇】
【unity游戏开发入门到精通——unity2D篇】
【unity实战】
【制作100个Unity游戏】
【推荐100个unity插件】
【实现100个unity特效】
【unity框架/工具集开发】
【unity游戏开发——模型篇】
【unity游戏开发——InputSystem】
【unity游戏开发——Animator动画】
【unity游戏开发——UGUI】
【unity游戏开发——联网篇】
【unity游戏开发——优化篇】
【unity游戏开发——shader篇】
【unity游戏开发——编辑器扩展】
【unity游戏开发——热更新】
【unity游戏开发——网络】

完结

好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述

Logo

立足具身智能前沿赛道,致力于搭建全球化、开源化、全栈式技术交流与实践共创平台。

更多推荐