第十章: 存储容器

1. 本章核心目标

本章节的目标分为显性与隐性两个层面,它们共同构成了章节的开发蓝图。

  • 显性目标 (玩家体验目标):

    • 为玩家提供可建造、可放置的多种存储设施,包括储物箱、基础工作台、高级工作台、熔炉和烹饪锅。

    • 实现玩家与这些容器之间的无缝物品交互,支持拖拽、存入和取出操作。

    • 在特定容器(如工作台、熔炉)中,为玩家提供物品制作和资源精炼的功能。

    • 实现容器的可破坏性,并在破坏后生成包含内部物品的临时物品缓存包(Inventory Bag)。

  • 隐性目标 (技术/战略目标):

    • 建立可扩展的容器框架: 设计一套主从式的蓝图类(Master Class)和组件化系统,为未来所有需要库存功能的建筑(如化学台、发电机等)提供一个统一、可复用的开发基础,从而提高开发效率。

    • 实现多人同步的库存访问: 确保在多人游戏中,多个玩家同时访问同一个容器时,所有物品的操作(移动、添加、移除)都能被正确同步,保证所有玩家看到的数据状态一致。

    • 构建动态化的UI系统: 开发一个能够根据所打开容器的类型,动态调整其布局、功能按钮和内容的UI界面,以适应不同容器的特定需求(例如,简单存储、带制作列表、带“点火”功能等)。

目标关系图:

graph TD
    A[第10章: 存储容器] --> B{核心目标};
    B --> B1[显性目标: 玩家功能];
    B --> B2[隐性目标: 技术框架];

    B1 --> C1[实现储物箱/工作台/熔炉等];
    B1 --> C2[实现物品拖拽交互];
    B1 --> C3[实现容器内制作/精炼];
    B1 --> C4[实现容器破坏与物品缓存];

    B2 --> D1[构建可扩展的Master-Child结构];
    B2 --> D2[解决多人库存同步问题];
    B2 --> D3[设计动态自适应UI];

    C1 & C2 & C3 & C4 --支撑--> E[丰富的玩家物品管理体验];
    D1 & D2 & D3 --支撑--> F[健壮且高效的后台系统];

    E & F --共同实现--> A;

2. 系统与功能实现

为达成核心目标,本章实现并集成了以下关键系统与功能:

系统/功能 详细描述 与前期系统的交互/依赖
主存储容器系统 创建了 BP_StorageContainerMaster 作为所有存储类建筑的父类。它继承自 BP_BuildableMaster,并增加了管理玩家访问(AccessingPlayers 数组)、处理交互事件(Interact Event)和同步UI更新的核心逻辑。 依赖于建筑系统: 作为 BP_BuildableMaster 的子类,天然集成了建造、放置、生命值及通用破坏逻辑。
存储容器组件 BPC_StorageContainer 是一个继承自 BPC_ItemsContainer 的特殊组件,专用于建筑。它重写了 HandleSlotDropAddItemToIndex 等关键函数,以处理非玩家库存的特定逻辑,并通过其所有者(Owner)调用接口,向所有访问者广播更新。 依赖于库存系统: 复用了 BPC_ItemsContainer 的核心数据结构与物品管理功能,并对其进行了特定化扩展。
UI交互系统 扩展了主UI W_Inventory,增加了一个专用于外部容器的面板。该面板通过一个枚举 E_CraftingType 来识别容器类型,并使用 Widget Switcher 在存储网格和制作网格之间切换。按钮(如“点火”、“制作”)的可见性也由该枚举动态控制。 扩展了UI框架: 在现有的玩家库存UI基础上进行模块化添加,而不是重造。
多人同步机制 采用服务器权威模型。BP_StorageContainerMaster 在服务器上维护一个 AccessingPlayers 玩家控制器数组。任何物品操作都由服务器验证和执行,然后服务器遍历该数组,向每个客户端的控制器发送RPC(远程过程调用)来更新其UI,确保了数据的一致性。 依赖于网络框架: 广泛使用了UE的Replication系统(变量复制、RPC)来实现状态同步。
物品缓存(背包)系统 当容器被摧毁时,会触发 StructureDestroyed 事件。如果容器内有物品,系统会生成一个 BP_InventoryBag 实例(一个轻量化的存储容器),将所有物品转移至其中,然后销毁原容器。该背包在倒计时结束后或变空后会自动销毁。 与破坏系统深度耦合: 作为容器被破坏后的直接逻辑分支,确保了玩家资产的保留。
物品堆叠与拆分 实现了完整的物品堆叠逻辑。当一个物品拖到同类物品上时,会计算总数并合并,处理超出堆叠上限的余数。同时,通过右键菜单提供了“拆分堆叠”功能,可将一堆物品均分为两堆。 是对核心库存逻辑的深化:BPC_ItemsContainerMaster中实现了核心算法,所有继承者(包括玩家库存和存储容器)均可使用。

3. 关键设计思想

本章节的实现体现了多个重要的软件设计模式与原则,确保了系统的健壮性与可维护性。

设计思想 具体体现
设计模式
模板方法模式 (Template Method) BPC_ItemsContainerMaster 定义了库存操作的骨架(如 AddItemToIndex),而其子类 BPC_StorageContainer 对其中的特定步骤(如成功添加后如何更新UI)进行了重写,以适应多人同步的需求,这是典型的模板方法应用。
观察者模式 (Observer) BP_StorageContainerMaster 扮演了“主题”(Subject)的角色,维护着一个 AccessingPlayers(“观察者”列表)。当其状态(库存内容)改变时,它会通知所有观察者(调用其控制器上的更新函数),实现了状态变更的自动广播。
继承与组合 继承: BP_StorageBox, BP_Forge 等都继承自 BP_StorageContainerMaster,复用了通用的交互和网络逻辑。
组合: 存储容器Actor通过包含一个 BPC_StorageContainer 组件来获得库存功能,而不是从一个“库存Actor”基类继承。这体现了“组合优于继承”的思想,更为灵活。
设计原则
单一职责原则 (SRP) 各个类的职责划分明确:BP_StorageContainerMaster 负责Actor层面的交互与玩家管理;BPC_StorageContainer 负责数据层的物品管理;W_Inventory 负责表现层的UI展示。
开闭原则 (OCP) 系统对扩展开放,对修改关闭。添加一个新的存储容器类型(如“冰箱”)时,只需创建一个新的 BP_StorageContainerMaster 子类并配置其特有属性即可,无需修改任何现有核心代码。
接口隔离原则 (ISP) 使用了多个小而专的接口,如 BPI_Interactable (处理交互)、BPI_StorageBuildable (处理容器特定操作如开盖、点火),避免了创建臃肿的“万能”接口。

4. 核心技术点与难点

技术点/难点 描述 解决方案
多人库存同步 难点: 确保多个客户端同时与一个容器交互时,数据不出现失步或冲突,所有玩家看到的物品状态实时一致。 方案: 严格遵循服务器权威模型。客户端的所有操作请求(如移动物品)都作为RPC发送至服务器。服务器是唯一有权修改容器库存状态的实体。修改成功后,服务器再通过RPC将结果广播给所有正在查看该容器的客户端,强制其UI刷新。
动态UI适配 难点: UI需要根据不同容器(储物箱、工作台、熔炉)的功能差异,显示不同的界面元素和布局。 方案:BP_StorageContainerMaster 中定义一个 StorageType (E_CraftingType) 枚举变量。当玩家与容器交互时,此枚举值被传递给UI。UI内部使用一个 Switch on Enum 节点来控制不同子控件(如制作按钮、点火按钮、制作列表)的可见性、文本内容和行为。
被动式制作/精炼 难点: 实现如熔炉、烹饪锅这类需要持续消耗燃料并定时产出物品的“被动式”工作逻辑。 方案:BP_ForgeBP_CookingPot 中,当“点火”成功后,启动一个 Set Timer by Event。该计时器以固定间隔(如15秒)在服务器上触发一个自定义事件(如 SmeltItems)。该事件负责检查配方、消耗原料和燃料、添加产物,形成一个自动循环,直到燃料耗尽或被手动停止。
容器破坏后的物品处理 难点: 容器被破坏时,内部存储的物品不能凭空消失,需要一个符合逻辑且对性能影响小的处理方式。 方案:StructureDestroyed 事件中,首先检查容器库存是否为空。若不为空,则在服务器上动态生成一个新的、轻量级的Actor BP_InventoryBag,将原容器的所有物品数据一次性转移给它,然后安全地销毁原容器。这个物品包有独立的生命周期,会在一段时间后或变空后自动销毁。

5. 自我批判与重构

问题分类 识别出的问题/“坑” 反思与修正/优化方案
关键问题 1. UI绑定错误: 在Lecture 140中,直接对UI文本框调用 SetText 无效,因为其Text属性已在编辑器中绑定到变量。
2. 制作列表不刷新: 在Lecture 143中,向工作台添加物品后,其制作列表的可制作状态不会实时更新。
3. 物品丢失风险: 在Lecture 150的“拆分堆叠”功能中,未检查库存是否有空位,可能导致在满背包情况下拆分物品时,新生成的一堆物品丢失。
修正:
1. 修改为更新与UI绑定的变量,而不是直接操作UI控件。
2. 这个问题在后续被修正,但更好的方案见下文优化。
优化方案:
3. 增加前置检查: 在执行拆分逻辑前,先调用 FindEmptySlot 函数检查是否存在至少一个空槽位。如果不存在,则操作失败,并向玩家发出提示,从而避免物品丢失。
前期设计反思 1. 函数功能过于专一: 角色蓝图中的 F_SlotDrop(拖拽)和 F_CraftItem(制作)函数最初只考虑了玩家自身的背包和快捷栏,缺乏对外部容器的兼容性。
2. 基类设计不够抽象: BPC_ItemsContainerMaster 中的 UpdateUI 函数最初的实现方式与玩家控制器耦合较深,导致在用于存储容器时需要额外添加逻辑分支。
修正/重构思路:
1. 函数通用化: 对这些函数进行重构,使其能够接受一个通用的“容器组件引用”作为参数,而不是硬编码地访问玩家的特定组件。这样,无论是玩家背包还是外部储物箱,都可以通过同一套逻辑进行操作。
2. 提升抽象层级:UpdateUI 的具体实现下放到子类中,或者让基类通过接口调用其Owner的更新方法。这样基类就不需要知道具体的容器类型,使其更加通用,符合“依赖倒置原则”。
重构优化 被动UI更新: 当前,工作台的制作列表需要玩家切换标签页才能刷新,体验不佳。 采用事件驱动模型:BPC_StorageContainer 的物品发生变化时(增、删、改),广播一个事件调度器(Event Dispatcher)。UI(如 W_Inventory)在被创建时绑定到这个事件。当事件触发时,UI会自动调用刷新函数(如 UpdateCraftWidgets),实现实时、自动的UI更新,极大提升用户体验。