You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中如何实现无需硬编码的通用Float监听触发模块?

通用Float值监听与触发方案(Unity)

针对你遇到的问题,我有几个实用的方案,既不用硬编码子类,也能避开unsafe指针的限制,而且扩展性很强,能覆盖你需要的任意GameObject组合场景:

方案1:委托+属性访问器(最灵活,推荐)

这个方案的核心是把「获取源float值」和「设置目标float值」的逻辑封装成委托,这样不用依赖具体的类,完全通用。而且Coroutine里使用委托完全没问题,不用unsafe代码。

实现代码

首先创建一个通用的ValueMonitor脚本:

using UnityEngine;
using System;
using System.Collections;

public class ValueMonitor : MonoBehaviour
{
    public enum ComparisonType
    {
        LessThan,
        GreaterThan,
        EqualTo,
        LessThanOrEqual,
        GreaterThanOrEqual
    }

    [Header("监听配置")]
    public ComparisonType comparisonRule;
    public float threshold;
    public float pollInterval = 0.1f; // 控制轮询频率,平衡性能和响应速度

    [Header("目标配置")]
    public float targetSetValue;

    // 存储获取源值和设置目标值的委托
    private Func<float> _getSourceValue;
    private Action<float> _setTargetValue;

    // 外部调用的初始化方法,传入具体的属性访问逻辑
    public void Setup(Func<float> sourceGetter, Action<float> targetSetter)
    {
        _getSourceValue = sourceGetter;
        _setTargetValue = targetSetter;
        StartCoroutine(MonitorLoop());
    }

    private IEnumerator MonitorLoop()
    {
        while (true)
        {
            if (_getSourceValue == null || _setTargetValue == null)
            {
                yield return new WaitForSeconds(pollInterval);
                continue;
            }

            float currentSource = _getSourceValue.Invoke();
            bool conditionMet = false;

            // 根据设定的规则判断条件
            switch (comparisonRule)
            {
                case ComparisonType.LessThan:
                    conditionMet = currentSource < threshold;
                    break;
                case ComparisonType.GreaterThan:
                    conditionMet = currentSource > threshold;
                    break;
                case ComparisonType.EqualTo:
                    // 浮点数比较注意精度问题
                    conditionMet = Mathf.Approximately(currentSource, threshold);
                    break;
                case ComparisonType.LessThanOrEqual:
                    conditionMet = currentSource <= threshold;
                    break;
                case ComparisonType.GreaterThanOrEqual:
                    conditionMet = currentSource >= threshold;
                    break;
            }

            if (conditionMet)
            {
                _setTargetValue.Invoke(targetSetValue);
                // 如果只需要触发一次,在这里添加停止逻辑:
                // StopCoroutine(MonitorLoop());
                // yield break;
            }

            yield return new WaitForSeconds(pollInterval);
        }
    }
}

使用示例

在初始化脚本里绑定你需要的对象和属性:

public class GameSetup : MonoBehaviour
{
    public GameObject stomachObject;
    public GameObject brainObject;

    private void Start()
    {
        Stomach stomach = stomachObject.GetComponent<Stomach>();
        Brain brain = brainObject.GetComponent<Brain>();

        // 创建并配置监听模块
        ValueMonitor hungerMonitor = gameObject.AddComponent<ValueMonitor>();
        hungerMonitor.comparisonRule = ValueMonitor.ComparisonType.LessThan;
        hungerMonitor.threshold = 0.5f;
        hungerMonitor.targetSetValue = 1f;

        // 绑定委托:获取Stomach的Fullness,设置Brain的Hunger
        hungerMonitor.Setup(() => stomach.Fullness, val => brain.Hunger = val);
    }
}

这个方案的优点是完全解耦,不管你的Stomach、Brain是什么结构,只要能通过属性访问float值,就能直接用,而且代码量很小,逻辑清晰。


方案2:反射实现可视化配置

如果你希望不用写初始化代码,直接在Inspector里拖拽对象、填写属性名,反射是个不错的选择。虽然反射有轻微性能开销,但对于游戏中的常规轮询场景完全可以忽略。

实现代码

using UnityEngine;
using System;
using System.Reflection;
using System.Collections;

public class ValueMonitorReflection : MonoBehaviour
{
    public enum ComparisonType
    {
        LessThan,
        GreaterThan,
        EqualTo,
        LessThanOrEqual,
        GreaterThanOrEqual
    }

    [Header("源对象配置")]
    public GameObject sourceObj;
    public string sourcePropertyName;

    [Header("监听条件")]
    public ComparisonType comparisonRule;
    public float threshold;
    public float pollInterval = 0.1f;

    [Header("目标对象配置")]
    public GameObject targetObj;
    public string targetPropertyName;
    public float targetSetValue;

    private PropertyInfo _sourceProp;
    private PropertyInfo _targetProp;

    private void Awake()
    {
        // 初始化反射属性
        if (sourceObj != null && !string.IsNullOrEmpty(sourcePropertyName))
        {
            _sourceProp = sourceObj.GetComponent<Component>().GetType().GetProperty(sourcePropertyName);
            if (_sourceProp == null || _sourceProp.PropertyType != typeof(float))
            {
                Debug.LogError($"源对象 {sourceObj.name} 中不存在名为 {sourcePropertyName} 的float属性!");
            }
        }

        if (targetObj != null && !string.IsNullOrEmpty(targetPropertyName))
        {
            _targetProp = targetObj.GetComponent<Component>().GetType().GetProperty(targetPropertyName);
            if (_targetProp == null || _targetProp.PropertyType != typeof(float))
            {
                Debug.LogError($"目标对象 {targetObj.name} 中不存在名为 {targetPropertyName} 的float属性!");
            }
        }
    }

    private void Start()
    {
        StartCoroutine(MonitorLoop());
    }

    private IEnumerator MonitorLoop()
    {
        while (true)
        {
            if (_sourceProp == null || _targetProp == null)
            {
                yield return new WaitForSeconds(pollInterval);
                continue;
            }

            float currentSource = (float)_sourceProp.GetValue(sourceObj.GetComponent<Component>());
            bool conditionMet = false;

            switch (comparisonRule)
            {
                case ComparisonType.LessThan:
                    conditionMet = currentSource < threshold;
                    break;
                case ComparisonType.GreaterThan:
                    conditionMet = currentSource > threshold;
                    break;
                case ComparisonType.EqualTo:
                    conditionMet = Mathf.Approximately(currentSource, threshold);
                    break;
                case ComparisonType.LessThanOrEqual:
                    conditionMet = currentSource <= threshold;
                    break;
                case ComparisonType.GreaterThanOrEqual:
                    conditionMet = currentSource >= threshold;
                    break;
            }

            if (conditionMet)
            {
                _targetProp.SetValue(targetObj.GetComponent<Component>(), targetSetValue);
                // 可选:触发一次后停止
                // StopCoroutine(MonitorLoop());
            }

            yield return new WaitForSeconds(pollInterval);
        }
    }
}

使用方式

直接在Inspector里:

  1. 拖拽Stomach对象到sourceObj,填写sourcePropertyName为"Fullness"
  2. 选择比较规则为LessThan,阈值设为0.5
  3. 拖拽Brain对象到targetObj,填写targetPropertyName为"Hunger"
  4. 设置targetSetValue为1

完全不用写代码,适合快速配置多个监听规则。


方案3:ScriptableObject+事件(性能最优)

如果你能稍微重构一下现有代码,把float值改成ScriptableObject变量,就能实现无轮询的监听,只有当值变化时才触发检查,性能最好,而且可视化程度最高。

步骤1:创建Float变量ScriptableObject

using UnityEngine;
using UnityEngine.Events;

[CreateAssetMenu(fileName = "FloatVar", menuName = "Game Variables/Float Variable")]
public class FloatVariable : ScriptableObject
{
    [SerializeField] private float _value;
    public UnityEvent<float> OnValueUpdated;

    public float Value
    {
        get => _value;
        set
        {
            if (!Mathf.Approximately(_value, value))
            {
                _value = value;
                OnValueUpdated.Invoke(_value);
            }
        }
    }
}

步骤2:修改现有脚本引用变量

比如Stomach和Brain:

// Stomach.cs
public class Stomach : MonoBehaviour
{
    public FloatVariable fullness;

    // 模拟饥饿下降逻辑
    private void Update()
    {
        fullness.Value -= Time.deltaTime * 0.1f;
    }
}

// Brain.cs
public class Brain : MonoBehaviour
{
    public FloatVariable hunger;
}

步骤3:创建触发脚本

using UnityEngine;

public class ValueTrigger : MonoBehaviour
{
    public enum ComparisonType
    {
        LessThan,
        GreaterThan,
        EqualTo,
        LessThanOrEqual,
        GreaterThanOrEqual
    }

    [Header("监听变量")]
    public FloatVariable sourceVar;
    public ComparisonType comparisonRule;
    public float threshold;

    [Header("目标变量")]
    public FloatVariable targetVar;
    public float targetSetValue;

    private void OnEnable()
    {
        sourceVar.OnValueUpdated.AddListener(CheckCondition);
        // 初始化时检查一次状态
        CheckCondition(sourceVar.Value);
    }

    private void OnDisable()
    {
        sourceVar.OnValueUpdated.RemoveListener(CheckCondition);
    }

    private void CheckCondition(float currentValue)
    {
        bool conditionMet = false;

        switch (comparisonRule)
        {
            case ComparisonType.LessThan:
                conditionMet = currentValue < threshold;
                break;
            case ComparisonType.GreaterThan:
                conditionMet = currentValue > threshold;
                break;
            case ComparisonType.EqualTo:
                conditionMet = Mathf.Approximately(currentValue, threshold);
                break;
            case ComparisonType.LessThanOrEqual:
                conditionMet = currentValue <= threshold;
                break;
            case ComparisonType.GreaterThanOrEqual:
                conditionMet = currentValue >= threshold;
                break;
        }

        if (conditionMet)
        {
            targetVar.Value = targetSetValue;
        }
    }
}

使用方式

  1. 在Project窗口右键创建两个Float Variable:FullnessVar和HungerVar
  2. 给Stomach的fullness字段赋值FullnessVar,Brain的hunger字段赋值HungerVar
  3. 创建一个空物体,添加ValueTrigger组件,配置监听的sourceVar为FullnessVar,规则设为LessThan,阈值0.5;目标targetVar为HungerVar,设置值1

这个方案完全不用轮询,只有当Fullness变化时才会检查条件,性能最优,而且适合大型项目中管理多个变量的依赖关系。


总结

  • 如果不想修改现有代码结构:优先选择方案1(委托),灵活且无性能问题
  • 如果需要可视化快速配置:选择方案2(反射),不用写初始化代码
  • 如果可以重构代码追求最优性能:选择方案3(ScriptableObject+事件)

另外注意,浮点数比较时一定要用Mathf.Approximately来判断相等,避免因为浮点精度误差导致的逻辑错误。

内容的提问来源于stack exchange,提问作者Léo Caussan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:49:18