
之前在游戏项目里接到一个交互需求玩家需要在关卡中收集“果冻”和“红宝石”两类物品当某种组合条件达成时系统要自动检测并触发隐藏奖励。比如这里说的“检测到双果冻双红宝石”翻译成开发语言就是玩家的当前数据中同时存在两颗果冻、两颗红宝石时事件系统要检测到这个状态并执行后续逻辑。初看这只是一个简单的数量判断但真正落地时你会发现它牵涉到物品数据的存储、变化通知、条件判定、重复触发控制、事件解耦等多个环节。如果直接把if写在业务逻辑里后续每增加一个组合条件就要改一片代码排查起来也很痛苦。这篇文章就围绕“检测到双果冻双红宝石”这类组合条件需求拆解一套通用的事件检测方案并用 C# 和 Unity 环境实现一个可运行的实战示例。无论你是刚接触游戏逻辑开发的新手还是需要在现有项目中加入成就、合成、隐藏条件系统的开发者这篇文章都能给你一个可以直接复用的实现思路。1. 背景与核心概念1.1 “双果冻双红宝石”本质是什么很多看起来像策划文案的需求落到底层都是一种“组合条件检测”。所谓“双果冻双红宝石”核心包含两个维度物品种类果冻、红宝石分别代表两种不同的资源。数量阈值“双”代表每个种类的数量至少为 2。所以需求本身可以描述为当 itemCount[Jelly] 2 且 itemCount[Ruby] 2 时触发事件。从技术角度只要玩家背包中的物品数量发生变化我们就要重新检查所有组合条件。满足条件时触发对应事件不满足时不做额外处理。1.2 这类需求的常见应用场景在实际游戏项目中下面这些玩法都会用到类似逻辑成就系统玩家收集到 X 个物品 A、Y 个物品 B解锁成就。合成系统当背包同时拥有若干材料解锁合成按钮或自动合成。隐藏关卡解锁满足特定组合条件后地图上出现隐藏入口。任务目标进度任务要求玩家“获得 2 个果冻、2 个红宝石”界面需要实时刷新进度。商店促销条件持有特定道具组合时解锁折扣购买权限。这些场景的共同点是条件不是单一物品的数量判断而是多个物品的组合判断而且需要响应数据变化。1.3 为什么要单独做一个检测系统也许有人会说这个需求用一个if不就行了吗if (backpack.GetCount(ItemType.Jelly) 2 backpack.GetCount(ItemType.Ruby) 2) { // 触发逻辑 }确实如果只有这一处逻辑这样写没问题。但项目一旦变大问题就会暴露出来条件散落每个需要判断的地方都写一遍相同逻辑改数量阈值要全局搜索。触发时机难控制物品变化可能来自拾取、商店购买、任务奖励、战斗掉落你很难在所有入口都补上判断代码。重复触发风险如果不记录上一次状态玩家只要一直停留在达标状态事件就会被反复执行。扩展性差今天要检测“双果冻双红宝石”明天还要检测“三宝石两水晶”每加一个条件都动业务代码。因此更好的做法是把“物品数据”和“条件检测”拆开做成一个独立的检测器由数据变更事件驱动。这样业务层只需要关心“收到通知后怎么做”而不需要关心“什么时候该检测”。2. 环境准备与实现思路本文的示例以 C# 和 Unity 作为演示环境。核心检测逻辑本身不依赖 Unity API所以你可以直接把它放到纯 C# 控制台项目中运行也可以封装成 Unity 脚本挂在场景里。开发工具Visual Studio 2022 或 Visual Studio Code运行环境.NET 6 / .NET 8或者 Unity 2021 LTS 及以上编程语言C#示例类型控制台模拟 Unity 挂载脚本版本不需要完全照搬因为核心用到的都是 C# 基础集合和事件委托在大部分现代版本中都可以直接编译运行。重点是理解设计思路版本差异影响不大。整体设计分成三个模块模块职责ItemType定义物品类型枚举BackpackManager维护物品数量数据提供增加、移除、查询能力并在数据变化时发出通知CombinationDetector接收物品变化事件检查组合条件是否满足并触发外部事件这样的分层好处很明显背包只负责数据存储检测器只负责条件判断业务逻辑写在外部监听方法里面。三者不互相耦合后期替换存储方式、增加条件都非常方便。3. 核心数据结构设计3.1 物品类型枚举首先定义物品类型。示例中先用“果冻”和“红宝石”两类但为了便于扩展可以预留几个占位类型。// 文件路径Models/ItemType.cs public enum ItemType { None 0, Jelly 1, Ruby 2, Coin 3, Crystal 4 }None作为默认空值可以避免变量未初始化的问题实际项目中如果物品很多也可以改成字符串 ID 或配置表 ID。3.2 背包数据类背包的核心职责是维护一个DictionaryItemType, int映射表记录每个物品类型的当前数量。对外提供增加、移除、查询、快照等方法。这里要注意一个设计细节背包不负责触发“双果冻双红宝石”这种具体条件。它只维护数据本身并通过事件把“数据变了”这个信号发出去。// 文件路径Managers/BackpackManager.cs using System; using System.Collections.Generic; public class BackpackManager { private readonly DictionaryItemType, int _itemCounts new DictionaryItemType, int(); /// summary /// 物品数量变化时触发参数分别为物品类型和最新数量。 /// /summary public event ActionItemType, int OnItemChanged; /// summary /// 增加物品数量。 /// /summary public void AddItem(ItemType type, int amount 1) { if (type ItemType.None || amount 0) { return; } if (!_itemCounts.ContainsKey(type)) { _itemCounts[type] 0; } _itemCounts[type] amount; OnItemChanged?.Invoke(type, _itemCounts[type]); } /// summary /// 移除物品数量。数量不足时不做扣除并返回 false。 /// /summary public bool RemoveItem(ItemType type, int amount 1) { if (type ItemType.None || amount 0) { return false; } if (!_itemCounts.ContainsKey(type) || _itemCounts[type] amount) { return false; } _itemCounts[type] - amount; OnItemChanged?.Invoke(type, _itemCounts[type]); return true; } /// summary /// 获取指定物品的当前数量。 /// /summary public int GetCount(ItemType type) { if (type ItemType.None) { return 0; } _itemCounts.TryGetValue(type, out int count); return count; } /// summary /// 获取物品数量的快照避免外部直接修改内部字典。 /// /summary public DictionaryItemType, int Snapshot() { return new DictionaryItemType, int(_itemCounts); } }几个值得注意的地方移除物品时先校验数量避免出现负数。数据变化后统一调用OnItemChanged这样检测器只需要监听这一个事件。Snapshot()返回的是副本外部无法直接改动内部数据。4. 编写组合条件检测器4.1 检测器的基础框架CombinationDetector是本文的核心。它本身不关心业务具体逻辑只负责一件事判断目标物品数量是否达标并在状态发生变化时通知外部。先定义一个条件描述类用来表达“需要哪些物品各需要多少个”。// 文件路径Core/ItemCondition.cs using System.Collections.Generic; /// summary /// 组合条件例如“双果冻双红宝石” /// /summary public class ItemCondition { /// summary /// 条件名称便于日志输出。 /// /summary public string ConditionName { get; set; } /// summary /// 需要的物品类型和对应数量。 /// /summary public DictionaryItemType, int RequiredItems { get; set; } new DictionaryItemType, int(); }然后是检测器类// 文件路径Core/CombinationDetector.cs using System; using System.Collections.Generic; public class CombinationDetector { private readonly ItemCondition _condition; private bool _wasTriggered; /// summary /// 条件从未达标变为达标时触发。 /// /summary public event ActionItemCondition OnConditionMatched; /// summary /// 条件从达标变为不达标时触发。 /// /summary public event ActionItemCondition OnConditionLost; public CombinationDetector(ItemCondition condition) { _condition condition; } /// summary /// 根据背包数据检查条件是否满足。建议在背包变化事件中调用。 /// /summary public void ForceCheck(BackpackManager backpack) { bool isSatisfied IsConditionMatched(backpack); if (isSatisfied !_wasTriggered) { // 从未达标变成达标 _wasTriggered true; OnConditionMatched?.Invoke(_condition); } else if (!isSatisfied _wasTriggered) { // 从达标变成不达标 _wasTriggered false; OnConditionLost?.Invoke(_condition); } } private bool IsConditionMatched(BackpackManager backpack) { foreach (KeyValuePairItemType, int pair in _condition.RequiredItems) { int currentCount backpack.GetCount(pair.Key); if (currentCount pair.Value) { return false; } } return true; } /// summary /// 重置状态用于玩家进入新关卡时恢复初始状态。 /// /summary public void Reset() { _wasTriggered false; } }这个实现最重要的地方是_wasTriggered状态标记。它确保了玩家一直持有双果冻双红宝石时事件只触发一次不会因为每次背包变化都触发。当玩家消耗掉部分物品、条件不再满足后状态会被重置下次再收集又不会再次触发。如果你需要“持续输出进度”而不是“只触发一次”可以在外层另加一个进度条监听不做门槛判断而是直接展示当前数量。4.2 把检测器接入背包事件检测器写好了现在要把它和背包关联起来。最简单的做法是订阅背包的OnItemChanged事件然后在回调里调用ForceCheck。这里我把整个流程封装成一个管理器用来统一配置多个条件// 文件路径Managers/ConditionManager.cs using System; using System.Collections.Generic; public class ConditionManager { private readonly BackpackManager _backpack; private readonly ListCombinationDetector _detectors new ListCombinationDetector(); public ConditionManager(BackpackManager backpack) { _backpack backpack; _backpack.OnItemChanged HandleItemChanged; } /// summary /// 注册一个组合条件。 /// /summary public void AddCondition(ItemCondition condition) { var detector new CombinationDetector(condition); detector.OnConditionMatched HandleConditionMatched; detector.OnConditionLost HandleConditionLost; _detectors.Add(detector); // 注册后先立即检测一次避免错过初始状态 detector.ForceCheck(_backpack); } /// summary /// 清空所有条件并在销毁时取消事件订阅。 /// /summary public void Clear() { foreach (CombinationDetector detector in _detectors) { detector.OnConditionMatched - HandleConditionMatched; detector.OnConditionLost - HandleConditionLost; } _detectors.Clear(); _backpack.OnItemChanged - HandleItemChanged; } private void HandleItemChanged(ItemType type, int newCount) { // 只要物品数量发生变化就重新检查所有条件 foreach (CombinationDetector detector in _detectors) { detector.ForceCheck(_backpack); } } private void HandleConditionMatched(ItemCondition condition) { // 外部可以订阅这个事件由业务层决定触发什么功能 Console.WriteLine($[条件达成] {condition.ConditionName}); } private void HandleConditionLost(ItemCondition condition) { Console.WriteLine($[条件失效] {condition.ConditionName}); } }这样设计后业务层只需要在开始时注册一次条件之后所有判断和触发都是自动的。后面要新增“三宝石两水晶”之类的条件也只需要往ConditionManager里添加新的ItemCondition不需要改动背包和检测器代码。4.3 使用一组代码模拟整个流程为了让读者可以直接看到效果我写了一个不带 Unity 依赖的控制台示例完整模拟从 0 收集到触发事件的流程。// 文件路径Program.cs using System; using System.Collections.Generic; class Program { static void Main(string[] args) { // 1. 创建背包 var backpack new BackpackManager(); // 2. 创建条件管理器 var conditionManager new ConditionManager(backpack); // 3. 注册“双果冻双红宝石”条件 var condition new ItemCondition { ConditionName 双果冻双红宝石, RequiredItems new DictionaryItemType, int { { ItemType.Jelly, 2 }, { ItemType.Ruby, 2 } } }; conditionManager.AddCondition(condition); // 4. 模拟玩家拾取道具 Console.WriteLine( 模拟拾取过程 ); backpack.AddItem(ItemType.Jelly, 1); backpack.AddItem(ItemType.Ruby, 1); backpack.AddItem(ItemType.Jelly, 1); backpack.AddItem(ItemType.Ruby, 1); // 此时果冻2红宝石2条件应当第一次达成 Console.WriteLine(\n 模拟使用道具导致条件不满足 ); backpack.RemoveItem(ItemType.Jelly, 1); // 此时果冻1红宝石2条件不满足 Console.WriteLine(\n 再次收集到双果冻双红宝石 ); backpack.AddItem(ItemType.Jelly, 1); // 此时果冻2红宝石2条件第二次达成 Console.WriteLine(\n 清理 ); conditionManager.Clear(); } }预期输出结果如下 模拟拾取过程 [条件达成] 双果冻双红宝石 模拟使用道具导致条件不满足 [条件失效] 双果冻双红宝石 再次收集到双果冻双红宝石 [条件达成] 双果冻双红宝石 清理 从输出可以看到条件在第一次达成时触发一次在中间失去条件时触发失效再次达成时又可以重新触发。这正是实际项目中比较合理的行为。5. Unity 场景中的实战改造控制台示例验证了核心逻辑接下来把它接入 Unity 项目。下面这份脚本演示了如何在场景中挂载检测组件并用 UI 文本显示检测状态。5.1 挂载脚本// 文件路径Assets/Scripts/DoubleJellyRubyDetector.cs using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; public class DoubleJellyRubyDetector : MonoBehaviour { [Header(背包)] public BackpackManager backpack; [Header(UI)] public Text statusText; private ConditionManager _conditionManager; private void Start() { if (backpack null) { backpack gameObject.AddComponentBackpackManager(); } _conditionManager new ConditionManager(backpack); var condition new ItemCondition { ConditionName 双果冻双红宝石, RequiredItems new DictionaryItemType, int { { ItemType.Jelly, 2 }, { ItemType.Ruby, 2 } } }; _conditionManager.AddCondition(condition); UpdateStatus(); } public void AddJelly() { backpack.AddItem(ItemType.Jelly, 1); UpdateStatus(); } public void AddRuby() { backpack.AddItem(ItemType.Ruby, 1); UpdateStatus(); } private void UpdateStatus() { if (statusText ! null) { statusText.text $果冻:{backpack.GetCount(ItemType.Jelly)} 红宝石:{backpack.GetCount(ItemType.Ruby)}; } } private void OnDestroy() { _conditionManager?.Clear(); } }在 Unity 编辑器中新建两个 Button分别绑定AddJelly和AddRuby方法再拖一个 UI Text 到statusText字段运行后点击按钮就能看到数量和条件触发的日志。5.2 为什么要在 OnDestroy 中清理Unity 脚本销毁时如果不取消事件订阅这个脚本持有的ConditionManager可能仍然被背包对象引用导致内存泄漏。尤其是场景切换时静态单例和跨场景对象最容易踩这个坑。在实际项目中如果有更复杂的依赖注入框架也应该在对象销毁或者模块卸载时统一回收所有事件订阅。6. 常见问题与排查思路下面整理了实现“双果冻双红宝石”检测功能时最容易遇到的几个问题以及排查方向。问题现象常见原因解决思路达到条件后事件没有触发没有订阅背包的OnItemChanged或者注册条件前已经达标但检测器没有执行初始检查注册条件后立即调用ForceCheck确认ConditionManager按时创建事件被触发了很多次没有使用_wasTriggered状态标记每次物品变化都执行触发逻辑在检测器中加入状态标记只有“未触发 - 已触发”的边界才发出事件消耗物品后条件状态没有重置移除物品逻辑里没有做数量下限校验导致数量变成负数在RemoveItem中增加判断数量不足时返回 false每个条件都要写一遍重复代码没有把条件抽象成ItemCondition数据结构使用字典驱动条件配置检测器统一遍历判断场景切换后出现重复触发事件未注销旧的检测器实例仍被引用在OnDestroy或其他销毁回调中取消订阅并清理条件列表数据不同步UI 显示数量和实际触发不一致物品数量修改绕过了BackpackManager直接改了内部字典统一通过AddItem、RemoveItem修改数据由数据层发出变更通知排查时建议先看日志中物品数量变化是否正常再确认OnItemChanged是否被触发最后检查组合检测器是否进入了“已触发”状态。7. 工程建议与扩展方向7.1 把条件配置做成可配置数据演示代码中条件写在Start方法里方便理解。真正的项目里更推荐把条件放在 ScriptableObject、Excel 表或 JSON 配置中方便策划调整不需要频繁改代码。例如 JSON 配置{ conditionName: 双果冻双红宝石, requiredItems: { Jelly: 2, Ruby: 2 } }运行时读取后转成ItemCondition对象注册到ConditionManager。这样新增玩法条件时只需要配置表加一行。7.2 事件驱动与业务解耦组合检测器只负责“判断条件是否满足”至于满足后触发什么效果应该由外部事件订阅方处理。这样可以将成就、任务、掉落、音效、动画等不同系统解耦。在本文代码中ConditionManager的回调只是打日志。实际项目里可以在这个方法中调用任务系统的CompleteTask或者把消息推送给 UI 层播放特效。7.3 性能优化建议如果条件数量很多但背包物品种类有限最简单的优化方法是每个物品类型维护一个监听该类型变化的条件列表减少无效遍历。例如DictionaryItemType, ListCombinationDetector当Jelly变化时只遍历关注Jelly的检测器。这个优化带来的效果会随着条件数量增加而变明显。7.4 日志标记与可观测性排错时最怕“为什么没触发”。建议为每个条件增加唯一 ID并在触发和失效时输出带 ID 的日志例如[Condition] 10001 - 双果冻双红宝石 statematched这样线上环境出现异常时可以快速定位是“数据没到”还是“检测器状态错误”。7.5 注意存档与初始化如果游戏支持存档初始化背包数据时需要先恢复存档中的物品数量再注册条件检测器。顺序错了可能导致初始状态被判定成“条件从无到有”引发多余的奖励发放。7.6 避免在事件回调中做耗时操作OnConditionMatched触发时不要在里面直接加载大型资源、播放过场动画、执行 UI 重建。正确做法是先把事件放入消息队列由主循环统一处理避免在物品拾取瞬间引发卡顿。8. 总结“检测到双果冻双红宝石”这个需求实际考验的是数据变更驱动的事件检测设计能力。它不只是一个if数量判断而是一套可以复用的组合条件检测模型。今天我带你从零实现了一个完整的检测流程包括背包数据管理组合条件的数据结构表示条件检测器的边界状态处理事件驱动接入方式Unity 场景挂载示例常见问题排查思路下一步你可以继续探索这些方向把条件检测和 ScriptableObject 配置打通、加入异步事件队列、在项目里实现一个更通用的“多条件组合成就系统”。动手把文章里的控制台示例跑一遍再尝试改成你想实现的任何组合条件例如“三红宝石一水晶”加深对整套流程的理解。如果你在接入项目时遇到了其他问题欢迎在评论区分享你遇到的具体数据和现象我会基于实际经验继续更新排错思路。