1. 项目概述为什么Unity开发者绕不开委托与事件如果你刚开始用Unity做游戏或者已经写了一些简单的脚本比如让角色移动、让UI按钮响应点击那你可能已经用到了事件驱动的雏形。但当你想要实现更复杂的交互比如一个敌人死亡时需要同时通知UI更新分数、播放音效、触发任务进度或者你想在Inspector面板里像拖拽组件一样直观地配置脚本间的调用关系那么**委托Delegate和事件Event**就是你必须掌握的核心工具。尤其是Unity在此基础上封装的UnityAction和UnityEvent它们将C#的这套强大机制与Unity编辑器的工作流无缝结合是提升开发效率和代码解耦度的关键。简单来说你可以把委托理解为一个“函数清单”或“方法指针”的容器。它允许你将方法当作参数传递、存储并在合适的时机调用。而事件则是委托的一种特殊形式它像是一个加了安全锁的委托规定了“谁能触发Invoke”和“谁能订阅Subscribe”从而更好地封装逻辑避免混乱。UnityAction是Unity定义的一种无返回值委托的“快捷方式”而UnityEvent则是一个可视化的、能在Inspector里直接配置的事件系统。我见过很多新手项目脚本之间充斥着public GameObject引用和GetComponent调用代码像蜘蛛网一样缠在一起。一个简单的改动往往需要牵动好几个脚本。而一旦你理解了委托和事件就能写出“发布-订阅”模式的优雅代码一个脚本只管“喊一嗓子”触发事件其他感兴趣的脚本自己“竖起耳朵听”订阅事件彼此不知道对方的存在但却能完美协作。这不仅是代码整洁的问题更是项目能否应对需求变化、能否高效协作的基础。接下来我就以Unity 6为背景带你从零开始彻底搞懂这套机制并分享一些实战中容易踩的坑和高效技巧。2. 核心概念深度解析从C#基础到Unity封装在直接使用Unity提供的便利工具前我们必须先理解它们背后的C#原理。这就像学开车你得先知道油门、刹车和方向盘是干嘛的而不是直接去记“自动驾驶”按钮在哪。2.1 C#委托Delegate函数指针的升级版委托的本质是一种类型它定义了方法的“签名”即参数列表和返回类型。任何符合这个签名的方法都可以被“装进”这个委托类型的变量里。定义一个委托// 声明一个委托类型它代表“所有无参数、无返回值的方法” public delegate void MyDelegate(); // 声明一个委托类型它代表“所有接收一个string参数、返回int的方法” public delegate int CalculateLengthDelegate(string text);使用委托public class DelegateExample : MonoBehaviour { // 1. 声明委托类型的变量 public MyDelegate myDelegateVariable; void Start() { // 2. 将具体的方法“赋值”给委托变量 myDelegateVariable SayHello; // 也可以使用 来添加多个方法多播委托 myDelegateVariable SayGoodbye; // 3. 通过委托变量来调用所有已添加的方法 if(myDelegateVariable ! null) // 安全调用检查避免空引用异常 { myDelegateVariable.Invoke(); // 或者简写为 myDelegateVariable(); } // 输出Hello! Goodbye! } void SayHello() { Debug.Log(Hello!); } void SayGoodbye() { Debug.Log(Goodbye!); } }关键点解析多播委托Multicast Delegate这是委托最强大的特性之一。一个委托变量可以存储多个方法引用。当调用Invoke()时这些方法会按照添加的顺序依次执行。上面的例子中myDelegateVariable就同时包含了SayHello和SayGoodbye。Action与Func泛型委托C#为我们预定义了两组非常常用的泛型委托避免了为每一种签名都去声明一个委托类型的麻烦。Action表示无返回值的方法。Action表示无参数ActionT表示一个参数ActionT1, T2表示两个参数以此类推最多支持16个参数。Func表示有返回值的方法。最后一个泛型参数指定返回值类型。例如Funcint表示无参数返回intFuncstring, int表示接收一个string参数返回int。// 使用预定义的Action和Func代码更简洁 public Action onGameStart; // 等同于 public delegate void VoidDelegate(); public Funcint, int, int onCalculateSum; // 等同于 public delegate int CalculateDelegate(int a, int b);注意委托变量是引用类型。直接使用赋值会替换掉之前所有的订阅者。如果你想添加一个订阅者务必使用操作符。这是一个非常常见的错误源头。2.2 C#事件Event受保护的委托事件在语法上很像委托但它增加了访问限制提供了更好的封装性。public class EventPublisher : MonoBehaviour { // 1. 声明一个事件基于某个委托类型 public event Action OnSomethingHappened; void Update() { if(Input.GetKeyDown(KeyCode.Space)) { // 2. 在定义事件的类内部可以触发事件 OnSomethingHappened?.Invoke(); // 使用空条件运算符?.更安全 } } } public class EventSubscriber : MonoBehaviour { public EventPublisher publisher; void Start() { // 3. 在外部类只能进行 订阅和 -取消订阅操作 publisher.OnSomethingHappened RespondToEvent; } void OnDestroy() { // 4. 务必在对象销毁时取消订阅防止内存泄漏 if(publisher ! null) { publisher.OnSomethingHappened - RespondToEvent; } } void RespondToEvent() { Debug.Log(Space key was pressed!); } }事件与委托的核心区别封装性在声明事件的类EventPublisher外部你只能对事件进行和-操作。你不能直接使用赋值publisher.OnSomethingHappened null;会编译报错也不能直接调用Invoke()。这防止了外部代码随意清空事件列表或触发事件保证了控制权在发布者手中。设计意图委托更偏向于“回调函数”或“方法列表”而事件明确体现了“发布-订阅”模式。事件用于通知“某件事发生了”而不关心谁在处理、怎么处理。2.3 UnityActionUnity的便捷ActionUnityAction位于UnityEngine.Events命名空间下。本质上它就是Unity为自己生态定义的一系列Action泛型委托的别名为了方便在Unity API中使用并与UnityEvent配合。using UnityEngine.Events; public class UnityActionExample : MonoBehaviour { // UnityAction 本质上就是预定义好的Action public UnityAction simpleAction; // 等同于 public Action simpleAction; public UnityActionint, float paramAction; // 等同于 public Actionint, float paramAction; // UnityEvent的AddListener方法要求传入UnityAction类型 public UnityEvent myUnityEvent; void Start() { simpleAction PrintMessage; paramAction PrintNumbers; // 可以像普通委托一样调用 simpleAction?.Invoke(); // 用于添加到UnityEvent myUnityEvent.AddListener(simpleAction); } void PrintMessage() { Debug.Log(Hello UnityAction!); } void PrintNumbers(int a, float b) { Debug.Log($A: {a}, B: {b}); } }为什么Unity要定义自己的Action主要是为了命名空间的统一和序列化支持。在Unity编辑器中一些特性尤其是与序列化相关的对来自UnityEngine命名空间的类型有更好的支持。但就日常使用而言你可以把它和C#的Action几乎等同看待。2.4 UnityEvent可视化的事件系统这是Unity的杀手级特性。UnityEvent是一个类它内部封装了一个委托列表并提供了在Inspector面板中可视化配置的能力。using UnityEngine; using UnityEngine.Events; public class UnityEventExample : MonoBehaviour { // 1. 声明一个UnityEvent public UnityEvent OnCustomEvent; // 2. 也可以声明带参数的UnityEventUnity预定义了多种泛型版本 public UnityEventint OnScoreChanged; void Update() { if(Input.GetMouseButtonDown(0)) { // 3. 触发事件 OnCustomEvent?.Invoke(); OnScoreChanged?.Invoke(100); } } }将上面这个脚本挂载到GameObject上在Inspector中你会看到On Custom Event下方有一个 “” 按钮。点击“”可以添加一个“调用项”。在每个调用项里你可以拖拽一个场景中的任何GameObject到对象框。然后从该对象拥有的组件中选择一个公有方法无参数或匹配事件参数进行绑定。你甚至可以动态传递参数如一个固定的int值或一个场景中的GameObject引用。UnityEvent的核心优势无需代码绑定策划、美术或其他不擅长编程的团队成员可以直接在编辑器里配置游戏对象之间的交互逻辑比如当按钮被点击时播放某个音效组件的Play()方法。解耦的极致脚本A触发UnityEvent脚本B的方法被绑定。A完全不知道B的存在。这极大地降低了模块间的依赖性。动态配置可以在运行时通过AddListener动态添加/移除处理函数同时也保留了编辑器静态配置的便利。UnityEvent与C#事件的对比特性C# 事件 (event Action)UnityEventInspector可视化不支持支持核心优势参数支持依赖委托定义灵活通过UnityEventT,UnityEventT0, T1等预定义泛型类支持常用类型已覆盖序列化委托引用无法被Unity序列化可以序列化在编辑器中的静态配置性能直接调用开销极小有额外的封装和列表遍历开销但在绝大多数情况下可忽略使用语法/-.AddListener()/.RemoveListener()也可在编辑器配置空值安全需手动检查null或使用?.Invoke()内部会检查但最好也使用?.实操心得对于纯代码逻辑、高频触发如每帧的通信优先使用C#事件性能最佳。对于需要跨模块、跨GameObject、尤其是希望非程序员也能参与配置的交互如UI、关卡机关、动画触发UnityEvent是首选。在实际项目中两者经常结合使用。3. 实战应用构建一个可扩展的玩家健康系统光说不练假把式。我们用一个完整的例子把上述概念串联起来实现一个玩家的健康Health系统。需求是当玩家生命值变化时需要更新UI血条、播放受伤音效、屏幕闪红并且如果生命值降到0触发死亡事件处理角色动画、游戏结束逻辑等。3.1 系统架构设计我们将采用“发布-订阅”模式发布者PublisherPlayerHealth组件。负责管理生命值并在值变化时触发事件。订阅者SubscribersHealthBarUI订阅生命值变化更新血条UI。DamageFeedback订阅生命值变化播放音效和屏幕特效。GameManager订阅死亡事件处理游戏结束逻辑。这样做的好处是PlayerHealth组件只关心自己的核心逻辑计算生命值完全不知道谁在关心它的变化。未来要增加新的反馈比如手柄震动只需要创建一个新的订阅者并绑定事件即可无需修改PlayerHealth的代码。3.2 发布者PlayerHealth 组件实现using UnityEngine; using UnityEngine.Events; public class PlayerHealth : MonoBehaviour { [SerializeField] private int maxHealth 100; private int currentHealth; // 使用C#事件供代码订阅高性能 public event System.Actionint, int OnHealthChanged; // 参数(当前生命值, 最大生命值) public event System.Action OnPlayerDied; // 无参数死亡事件 // 使用UnityEvent供编辑器配置例如绑定到某些特效预制体 public UnityEventint, int OnHealthChangedUnityEvent; public UnityEvent OnPlayerDiedUnityEvent; void Start() { currentHealth maxHealth; // 初始化时通知一次确保UI正确显示 OnHealthChanged?.Invoke(currentHealth, maxHealth); OnHealthChangedUnityEvent?.Invoke(currentHealth, maxHealth); } public void TakeDamage(int damageAmount) { if (currentHealth 0) return; // 已死亡不再处理伤害 int previousHealth currentHealth; currentHealth Mathf.Clamp(currentHealth - damageAmount, 0, maxHealth); // 触发生命值变化事件 OnHealthChanged?.Invoke(currentHealth, maxHealth); OnHealthChangedUnityEvent?.Invoke(currentHealth, maxHealth); // 检查是否死亡 if (previousHealth 0 currentHealth 0) { Die(); } } public void Heal(int healAmount) { if (currentHealth 0) return; // 已死亡无法治疗 int previousHealth currentHealth; currentHealth Mathf.Clamp(currentHealth healAmount, 0, maxHealth); if (currentHealth ! previousHealth) { OnHealthChanged?.Invoke(currentHealth, maxHealth); OnHealthChangedUnityEvent?.Invoke(currentHealth, maxHealth); } } private void Die() { Debug.Log(Player has died.); // 触发死亡事件 OnPlayerDied?.Invoke(); OnPlayerDiedUnityEvent?.Invoke(); // 这里可以禁用玩家控制、播放死亡动画等 // GetComponentPlayerController().enabled false; } }代码解析双事件系统我们同时暴露了C#事件OnHealthChanged和UnityEventOnHealthChangedUnityEvent。这是一种灵活的模式。高性能、纯代码的模块如UI更新可以订阅C#事件而需要在编辑器里拖拽配置的、一次性的效果如触发一个粒子特效预制体则可以配置UnityEvent。事件参数OnHealthChanged事件传递了当前生命值和最大生命值。这样订阅者如UI脚本无需再持有对PlayerHealth组件的引用去查询maxHealth实现了更彻底的解耦。空值安全调用使用?.Invoke()可以避免在没有任何订阅者时触发空引用异常。初始化触发在Start中触发一次生命值变化事件确保所有订阅的UI在游戏开始时就能获得正确的初始值。3.3 订阅者示例HealthBarUI (代码订阅)这个组件通过代码订阅C#事件适合需要频繁更新、逻辑紧密的模块。using UnityEngine; using UnityEngine.UI; public class HealthBarUI : MonoBehaviour { [SerializeField] private Slider healthSlider; [SerializeField] private PlayerHealth playerHealth; // 通过Inspector拖拽赋值 void Start() { if (playerHealth null) { playerHealth FindObjectOfTypePlayerHealth(); // 备用查找方式效率较低 } if (playerHealth ! null healthSlider ! null) { // 订阅C#事件 playerHealth.OnHealthChanged UpdateHealthBar; // 立即更新一次以显示初始值 UpdateHealthBar(playerHealth.CurrentHealth, playerHealth.MaxHealth); // 假设PlayerHealth暴露了属性 } else { Debug.LogError(HealthBarUI: PlayerHealth or Slider reference is missing!); } } void OnDestroy() { // 至关重要取消订阅防止内存泄漏 if (playerHealth ! null) { playerHealth.OnHealthChanged - UpdateHealthBar; } } private void UpdateHealthBar(int currentHealth, int maxHealth) { healthSlider.maxValue maxHealth; healthSlider.value currentHealth; // 可以在这里添加血条颜色渐变等效果 // Image fillImage healthSlider.fillRect.GetComponentImage(); // fillImage.color Color.Lerp(Color.red, Color.green, healthSlider.normalizedValue); } }3.4 订阅者示例DamageFeedback (编辑器配置)这个组件处理受伤反馈我们用它来展示UnityEvent在编辑器中的配置。using UnityEngine; public class DamageFeedback : MonoBehaviour { [Header(Visual Feedback)] [SerializeField] private Animator hurtFlashAnimator; [SerializeField] private string hurtFlashTriggerName Hurt; [Header(Audio Feedback)] [SerializeField] private AudioSource hurtAudioSource; [SerializeField] private AudioClip[] hurtClips; // 这个方法将被绑定到PlayerHealth的OnHealthChangedUnityEvent上 public void OnPlayerHurt(int currentHealth, int maxHealth) { // 注意每次生命值变化都会触发包括治疗。我们需要判断是否是受伤。 // 更合理的做法是事件传递变化量或之前的值这里简单演示。 // 实际项目中可能需要在PlayerHealth中区分OnHurt和OnHeal事件。 PlayHurtFlash(); PlayRandomHurtSound(); } private void PlayHurtFlash() { if (hurtFlashAnimator ! null !string.IsNullOrEmpty(hurtFlashTriggerName)) { hurtFlashAnimator.SetTrigger(hurtFlashTriggerName); } } private void PlayRandomHurtSound() { if (hurtAudioSource ! null hurtClips ! null hurtClips.Length 0) { AudioClip clipToPlay hurtClips[Random.Range(0, hurtClips.Length)]; hurtAudioSource.PlayOneShot(clipToPlay); } } }在编辑器中的配置步骤将PlayerHealth组件挂载到玩家GameObject上。将DamageFeedback组件也挂载到玩家或一个管理特效的GameObject上。选中玩家GameObject在Inspector中找到PlayerHealth组件。找到On Health Changed Unity Event事件列表。点击“”添加一个新条目。将挂载有DamageFeedback组件的GameObject拖入“None (Object)”框。在右侧函数选择下拉框中选择DamageFeedback-OnPlayerHurt (Int, Int)。现在每当玩家生命值变化DamageFeedback.OnPlayerHurt方法就会被自动调用。3.5 使用UnityEvent实现动态游戏逻辑UnityEvent的强大之处在于其动态性。假设我们有一个“宝箱”脚本打开时触发一个事件这个事件可以在编辑器中配置为增加金币、播放开箱动画、生成一件武器、触发一个任务进度更新等等。所有这些都不需要修改宝箱的代码。// Chest.cs using UnityEngine; using UnityEngine.Events; public class Chest : MonoBehaviour { public UnityEvent OnChestOpened; // 在Inspector中配置 void OnTriggerEnter(Collider other) { if(other.CompareTag(Player)) { OpenChest(); } } void OpenChest() { Debug.Log(Chest Opened!); // 播放自身打开动画等... OnChestOpened?.Invoke(); // 触发配置的所有逻辑 this.enabled false; // 防止重复打开 } }在编辑器中你可以为这个宝箱的OnChestOpened事件配置多个动作播放某个音效组件的Play()、激活一个隐藏的武器道具SetActive(true)、调用GameManager的AddCoin(100)方法等等。这种灵活性对于关卡设计和快速原型开发是无价的。4. 高级技巧、性能优化与常见陷阱掌握了基础用法后一些高级技巧和“坑”能让你用得更顺手、更安全。4.1 内存泄漏事件订阅的隐形杀手这是使用委托和事件时最容易犯也最严重的错误。如果一个对象订阅了另一个对象的事件但从未取消订阅那么只要事件发布者还活着即使订阅者对象已经被销毁例如一个UI面板被关闭了垃圾回收器GC也无法回收它因为发布者还持有着对它的一个引用通过委托。这会导致内存泄漏。错误示例public class LeakySubscriber : MonoBehaviour { public EventPublisher publisher; void Start() { publisher.OnEvent HandleEvent; } // 订阅 void HandleEvent() { /* ... */ } // 缺少 OnDestroy 或 OnDisable 来取消订阅 // 当这个LeakySubscriber的GameObject被Destroy后publisher.OnEvent的列表里仍然有HandleEvent的引用。 }正确做法总是配对使用和-。void OnEnable() { publisher.OnEvent HandleEvent; } void OnDisable() { publisher.OnEvent - HandleEvent; } // 使用OnDisable更通用 // 或者 void Start() { publisher.OnEvent HandleEvent; } void OnDestroy() { publisher.OnEvent - HandleEvent; }对于UnityEvent使用AddListener和RemoveListener。实操心得养成条件反射。每当写下一行或AddListener立刻补上对应的取消订阅代码。对于MonoBehaviourOnDisable是比OnDestroy更安全的取消订阅点因为对象可能被禁用而非销毁。4.2 为UnityEvent添加持久化监听器有时你希望某些监听器在编辑器配置好后就一直存在即使代码中动态添加或移除了其他监听器。这可以通过在UnityEvent上使用“持久化监听器”Persistent Listener来实现也就是我们在Inspector里拖拽配置的那些。它们会被Unity序列化与场景一起保存。代码中通过AddListener添加的叫做“运行时监听器”。两者互不干扰会按顺序被调用通常是持久化监听器先调用。4.3 自定义带参数的UnityEvent虽然Unity提供了UnityEventT,UnityEventT0, T1等常用泛型类但如果你需要传递自定义结构体或类作为参数就需要自己定义。using UnityEngine; using UnityEngine.Events; // 1. 定义自定义事件类继承自UnityEventT [System.Serializable] // 必须标记为可序列化否则无法在Inspector中显示 public class CustomDataEvent : UnityEventCustomData {} // 自定义的数据结构 [System.Serializable] public struct CustomData { public string message; public int value; public Vector3 position; } public class CustomEventExample : MonoBehaviour { // 2. 使用自定义的事件类型 public CustomDataEvent onDataReceived; void TriggerEvent() { CustomData data new CustomData { message Hello!, value 42, position transform.position }; onDataReceived?.Invoke(data); } }定义好后onDataReceived在Inspector中就可以像普通UnityEvent一样使用并且可以绑定到任何接收CustomData类型参数的方法上。4.4 性能考量C#事件 vs UnityEventC#事件的调用开销几乎等同于直接方法调用性能最优。UnityEvent由于需要管理监听器列表、处理序列化等会有额外的开销。对于每帧触发多次的事件如Update中的某些判断应优先使用C#事件。多播委托调用调用一个拥有大量订阅者的委托会按顺序遍历执行所有方法。如果某个订阅者方法执行非常耗时会阻塞后续订阅者。确保订阅到高频事件的方法都是轻量级的。避免在频繁触发的事件中进行复杂的查找操作例如在Update中触发的事件里不要频繁使用FindObjectOfType或GetComponent。4.5 调试技巧查看事件订阅者在复杂的项目中有时很难理清谁订阅了谁的事件。一个简单的调试方法是在发布者的事件触发代码处添加日志并输出当前订阅者的数量对于多播委托可以通过GetInvocationList()方法。public event Action OnMyEvent; void TriggerEvent() { Debug.Log($[{gameObject.name}] OnMyEvent is about to trigger. Subscriber count: {OnMyEvent?.GetInvocationList()?.Length ?? 0}); OnMyEvent?.Invoke(); }对于UnityEvent在运行时你可以在Inspector中展开它查看当前绑定的持久化监听器和运行时监听器列表Unity 2021版本在调试模式下支持。5. 实战问题排查与解决方案实录在实际开发中你肯定会遇到各种奇怪的问题。下面是我总结的一些典型场景和解决方法。5.1 事件触发了但订阅的方法没执行可能原因及排查订阅时机不对你在Awake中触发事件但订阅者在Start中才订阅。Unity脚本生命周期顺序是Awake-OnEnable-Start。确保订阅发生在第一次触发之前。通常建议在OnEnable中订阅在OnDisable中取消。对象已被销毁订阅者GameObject或组件已经被销毁但未取消订阅。事件触发时调用的是一个已销毁对象的方法可能导致错误或静默失败。务必做好取消订阅。方法签名不匹配订阅的方法参数数量或类型与事件委托定义的不一致。编译器可能不会报错特别是使用UnityEvent在编辑器绑定时但运行时不会调用。UnityEvent在编辑器中的绑定错误检查Inspector中的绑定确认目标对象和方法选择正确。有时预制体Prefab覆盖或场景嵌套会导致引用丢失。5.2 使用UnityEvent时Inspector中找不到想要的方法可能原因方法不是public只有公有方法才会出现在UnityEvent的绑定下拉菜单中。方法有返回值UnityEvent只能绑定返回类型为void的方法。方法参数不匹配如果你使用的是UnityEventint它只能绑定接收一个int参数的方法。无参数或参数类型不符的方法不会显示。方法属于非MonoBehaviour类UnityEvent通常绑定的是MonoBehaviour组件上的方法。静态方法或普通C#类的方法无法直接绑定。解决方案如果需要绑定一个复杂逻辑可以创建一个简单的“中转”MonoBehaviour脚本它有一个公有方法内部再去调用你真正的逻辑。5.3 多个订阅者的执行顺序问题无论是C#事件还是UnityEvent订阅者监听器的执行顺序通常与它们被添加的顺序一致。但这有时会带来依赖问题比如A必须在B之前执行。解决方案设计上解耦尽量避免监听器之间的直接依赖。如果必须有顺序考虑拆分成多个事件。例如OnBeforeHealthChange和OnAfterHealthChange。控制订阅顺序在代码中通过控制AddListener的顺序来管理。对于编辑器配置的UnityEvent持久化监听器你可以通过拖拽列表中的条目来调整顺序。使用优先级系统高级可以自己实现一个带优先级的事件管理器但这会增加复杂度。在大多数情况下清晰的逻辑设计比复杂的优先级系统更有效。5.4 在场景加载或对象销毁时的事件处理当加载新场景或销毁包含事件发布者的对象时所有订阅了该事件的订阅者都应该取消订阅。否则如果订阅者是DontDestroyOnLoad的对象它会持有一个对已销毁发布者的无效引用虽然委托会变为null但最好主动清理。最佳实践在发布者的OnDestroy方法中将事件设置为null对于C#事件或调用RemoveAllListeners()对于UnityEvent。但这需要谨慎因为可能干扰其他逻辑。更通用的做法是订阅者自身在OnDestroy或OnDisable中负责取消订阅如前所述。// 在发布者中 void OnDestroy() { // 对于C#事件设置为null会清空所有订阅谨慎使用可能影响其他对象 // OnMyEvent null; // 更常见的做法是依赖订阅者自身的清理。 }说到底委托和事件是Unity游戏开发中实现灵活、低耦合架构的基石。从简单的UI回调到复杂的游戏系统通信它们无处不在。刚开始可能会觉得概念有点绕但一旦你成功用事件系统重构了一处紧耦合的代码并体验到那种修改一处而无需牵动全身的畅快感你就会彻底爱上这种模式。我的建议是从小处着手比如先把一个按钮的点击响应从直接调用改为UnityEvent再尝试在两个完全独立的系统如背包系统和UI系统之间用C#事件进行通信。多踩几次上面提到的“坑”你就能驾轻就熟写出更专业、更易维护的Unity代码了。