上岸的鱼

心中有光,便可使整个世界升起太阳

1. 本章核心目标

本章的核心目标分为显性与隐性两个层面,它们共同构成了部落系统的基础框架,并为未来的多人协作玩法奠定了基础。

  • 显性目标 (玩家功能实现)

    • 为玩家提供创建、加入和管理部落的基础社交功能。

    • 实现部落内的权限管理,如提升/降级成员、踢出成员。

    • 建立部落内部信息共享机制,包括部落日志(Tribe Log)和每日消息(Message of the Day)。

    • 具象化部落的共享权益,例如共享建筑的拆除权、开门权限以及重生点等。

  • 隐性/战略目标 (技术与项目管理)

    • 搭建核心多人游戏状态管理框架:通过引入自定义的 GameModeGameStatePlayerState,确立了服务器权威(Server-Authoritative)的多人游戏架构。这是项目从单机功能开发向真正的网络多人游戏过渡的关键一步。

    • 建立可扩展的数据结构:设计并实现了一套完整的部落相关数据结构(Enums 和 Structs),为系统后续扩展(如部落战争、部落科技)提供了标准化的数据模型。

    • 解决唯一身份标识问题:引入“Advanced Sessions”插件,从根本上解决了之前依赖测试ID所带来的权限和所有权识别BUG,为实现可靠的多人在线功能扫清了障碍。

  • 目标关系图

    下表清晰地展示了显性与隐性目标之间的支撑与实现关系:

显性目标 (玩家功能) 支撑该功能的隐性/战略目标
创建/加入/管理部落 搭建核心多人游戏状态管理框架 (使用GameState存储所有部落信息)
权限管理 (提升/降级/踢出) 建立可扩展的数据结构 (E_TribeRank Enum);服务器权威逻辑验证
部落日志/每日消息 GameState作为中心化数据源,分发给所有部落成员
共享建筑/权限 解决唯一身份标识问题 (确保能准确识别玩家与部落归属);与建筑系统深度集成

2. 系统与功能实现

本章实现了完整的部落系统,其核心功能模块与交互流程如下:

  • 具体实现系统/功能列表

    1. UI系统

      • W_TribeWindow: 部落主界面,用于展示成员、日志和每日消息。

      • W_CreateTribe: 当玩家不属于任何部落时显示的创建界面。

      • W_TribeSwitcher: 根据玩家状态(是否在部落中)切换上述两个界面的逻辑切换器。

      • W_TribeInvite: 玩家接收到部落邀请时弹出的交互窗口。

      • W_TribeMemberSlot & W_TribeLogSlot: 用于在滚动列表中动态生成部落成员和日志条目的可复用控件。

    2. 后端逻辑系统

      • 状态管理:在 GameState 中使用 TMap<FString, FSTribeInfo> 作为核心数据容器,以部落ID为键,高效存储和检索所有部落的详细信息。

      • 数据结构:定义了 E_TribeRankS_TribeInfoS_TribeMemberInfo 等一系列结构体和枚举,标准化了系统数据。

      • 权限验证:所有敏感操作(如邀请、踢人、修改MOTD)均在服务器端(PlayerController或GameState)进行权限检查(如验证玩家是否为Owner或Admin)。

    3. 系统交互

      • 邀请与加入:通过玩家角色的服务器端射线检测(Line Trace)来识别目标玩家并发起邀请,整个流程通过接口和RPC在客户端与服务器之间传递。

      • 状态同步:当部落数据发生变更时(如新成员加入、成员被踢),GameState 会遍历部落所有在线成员,并通过RPC调用其 PlayerController 上的接口,将最新的部落信息推送给每个客户端,确保所有成员UI同步更新。

  • 系统交互流程图 (以“创建部落”为例)

    Code snippet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    graph TD
    A[玩家在 W_CreateTribe 输入部落名并点击创建] --> B{调用 PlayerController 接口};
    B --> C[PlayerController (客户端) 调用 RPC: CreateTribeOnServer];
    C --> D{PlayerController (服务器端) 执行};
    D --> E[1. 验证玩家资格(是否已在部落中)];
    E --> F[2. 构建 S_TribeInfo 结构体];
    F --> G[3. 获取玩家的 UniqueNetID 作为 TribeID];
    G --> H{4. 调用 GameState 接口: CreateNewTribe};
    H --> I[GameState (服务器端) 将新的 TribeInfo 添加到 TribeMap];
    I --> J{5. 调用 PlayerController RPC: UpdateTribeWindow (客户端)};
    J --> K[PlayerController (客户端) 获取部落UI引用];
    K --> L[6. 调用 W_TribeWindow 的更新函数,刷新UI];

3. 关键设计思想

本章的开发体现了清晰的设计模式与原则,确保了系统的可维护性和扩展性。

  • 设计模式与原则应用
设计思想 具体应用实例 带来的优势
接口隔离原则 (Interface Segregation) 创建了 BPI_SurvivalCharacterBPI_SurvivalControllerBPI_SurvivalGameState 等多个接口,用于不同蓝图类之间的通信。 解耦:蓝图之间不直接引用,而是通过接口调用,降低了耦合度。例如,任何Actor都可以通过接口与GameState通信,而无需知道其具体实现。
单一职责原则 (Single Responsibility) - W_TribeMemberSlot 只负责显示一个成员的信息。
- GameState 只负责存储和管理所有部落的状态数据。
- PlayerState 只负责存储单个玩家的部落相关状态。
高内聚,低耦合:每个类的功能都非常专注,易于理解、维护和复用。
状态模式 (State Pattern) - 简化应用 W_TribeSwitcher 根据玩家是否在部落中(一个简单的状态)来决定显示 W_CreateTribe 还是 W_TribeWindow 逻辑清晰:将UI状态的切换逻辑集中管理,避免在主UI蓝图中散布大量的if-else判断。
中心化状态管理 GameState 被用作所有部落数据的“单一事实来源”(Single Source of Truth)。所有修改都必须通过服务器在GameState中进行,然后分发给客户端。 数据一致性:确保了在多人环境下所有客户端看到的部落信息都是同步和一致的,避免了网络同步问题。

4. 核心技术点与难点

本章的实现涉及多个关键技术点,并解决了一些典型的多人游戏开发难题。

  • 核心技术点

    1. 客户端-服务器-客户端通信模型:熟练运用了 Unreal Engine 的 RPC(Remote Procedure Call)机制。例如,客户端发起操作(如邀请),通过 “Run on Server” RPC 将请求发送到服务器,服务器处理后,再通过 “Run on Owning Client” RPC 将结果或UI更新指令发回给相关客户端。

    2. 权威服务器状态管理:将部落数据 (TribeMap) 存储在仅存于服务器和客户端的 GameState 中,所有修改逻辑均在服务器上执行,客户端只能通过RPC请求修改,确保了游戏逻辑的安全和一致性。

    3. 动态UI生成:在 W_TribeWindow 中,通过循环遍历 GameState 推送过来的部落成员和日志数组,动态创建并填充 W_TribeMemberSlotW_TribeLogSlot 控件,实现了数据驱动的UI更新。

  • 技术难点与解决方案

难点 解决方案 优势/劣势
多人状态同步:当一个玩家加入/退出部落时,如何通知所有其他在线的部落成员并更新他们的UI? GameState 的修改函数(如AddTribeMember)执行完毕后,遍历更新后的部落成员列表。对每个在线的成员,获取其 PlayerController 引用,并调用一个客户端RPC (UpdateTribeWindow),将最新的完整部落数据推送给他们。 优势: 逻辑集中在GameState,保证了状态更新的原子性和一致性。劣势: 数据量大时,全量推送部落数据可能消耗更多带宽,未来可优化为增量更新。
唯一身份识别:在没有Steam等在线服务时,无法获得唯一的玩家ID,导致权限、所有权判断混乱,引发BUG(如离队功能失效)。 引入 “Advanced Sessions” 插件。在 PlayerStateBeginPlay 事件中,使用 GetUniqueNetID 节点获取并存储每个玩家的唯一网络ID,并替换项目中所有硬编码的“测试ID”。 优势: 从根本上解决了身份识别问题,为后续的Steam会话集成做好了准备,是项目走向成熟的关键一步。
已有建筑归属合并:玩家创建或加入部落后,其之前放置的个人建筑如何自动变为部落资产? 创建 MergeTribeStructures 函数,该函数使用 GetAllActorsOfClass 节点遍历关卡中所有的建筑实例。通过比对建筑的 OwnerNetID 和当前玩家的ID,筛选出属于该玩家的建筑,然后批量修改它们的部落归属信息(TribeIDOwnerName等)。 优势: 实现了核心功能需求。劣势: GetAllActorsOfClass 性能开销较大,在建筑数量庞大的服务器中可能引发卡顿,被标记为未来需要优化的点。

5. 自我批判与重构

在开发过程中,遇到了一些设计缺陷和实现问题,并通过反思进行了修正与重构。

  • 遇到的关键问题 (“坑”)

    1. UI更新不完全:最初在“加入部落”逻辑中,只更新了新加入成员的UI,而部落中其他老成员的UI没有刷新,导致信息不同步。

    2. 硬编码ID依赖:过度依赖“testID”和“secondPlayerID”等硬编码字符串作为玩家标识,导致在模拟双客户端测试时,权限和所有权判断逻辑出现严重BUG,例如无法正确执行“离开部落”操作。

    3. 父类接口调用遗漏:在重写(Override)父类的接口事件(如 StructureDestroyed)时,忘记调用父类的同名函数 (Call to Parent Function),导致父类中定义的基础逻辑未能执行。

  • 反思与重构方案

原始设计/问题 反思 重构/优化方案
问题1:UI更新不完全 违背了“单一事实来源”原则。UI更新的发起者应该是状态变更的源头(GameState),而不是单个客户端。 重构:将UI更新逻辑提升至GameState。在部落数据(如成员列表)被修改后,由GameState负责向所有相关的在线客户端广播(Push)最新的数据。客户端UI只负责接收数据并渲染。
问题2:硬编码ID依赖 早期为了快速开发而使用的临时方案,缺乏对多人游戏唯一性需求的深刻理解,是项目的重大技术债务。 引入插件并重构:集成"Advanced Sessions"插件,并系统性地替换了所有使用硬编码ID的地方,改为从 PlayerState 获取动态且唯一的 NetID
问题3:父类接口调用遗漏 对UE的蓝图继承和接口覆盖机制理解不深,忽略了调用链的完整性。 代码审查与修正:对所有覆盖了父类事件的子蓝图进行检查,确保在执行完子类特有逻辑后,都调用了父函数,保证了功能的完整性。
  • 如果重来一次的优化

    • 提前引入唯一ID:会在项目初期就集成 Advanced Sessions 插件或实现一套临时的唯一ID生成机制,避免后期大规模的重构和因ID问题导致的调试困难。

    • 优化建筑合并逻辑:会考虑在玩家放置建筑时,就在一个全局管理器(如GameState中的一个TMap)中注册该建筑及其所有者ID。这样在合并部落时,可以直接查询这个管理器,而无需使用高消耗的GetAllActorsOfClass,从而大幅提升性能。

1. 本章核心目标

本章的核心任务是为游戏实装一个多样化且功能丰富的可交互物品系统,涵盖了从远程武器到采集工具的多个类别。

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

    • 丰富战斗方式: 为玩家提供包括步枪、火箭筒、弓、矛在内的多种远程武器,每种武器都具备独特的射击、瞄准和装填机制,显著提升了战斗的策略性和趣味性。

    • 建立资源循环: 引入了从基础(石器)到进阶(铁器)的采集工具(石斧、石镐、铁斧、铁镐),使玩家能够高效地与世界资源进行交互,为后续的生存和建造系统奠定基础。

    • 提供初始工具: 玩家默认会生成一个石头(Rock),作为游戏初期的基础工具,引导玩家进行最基本的资源采集。

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

    • 构建可扩展的武器框架: 最大的战略目标是建立一个可复用、可扩展的武器基类(BP_WeaponAimMaster)。通过将通用的瞄准逻辑(如第三人称越肩视角、第一/三人称ADS缩放、瞄准时减速)抽象到父类中,极大地简化了未来新增远程武器的开发流程。

    • 实现模块化的动画系统: 在动画蓝图中利用 Layered blend per bone 技术,实现了上下半身动画的分离与融合。这使得角色可以在执行跳跃、蹲伏等下半身动作的同时,保持上半身的持握与瞄准姿态,提升了动画表现的真实感和流畅度。

    • 深化伤害计算系统: 建立了一个基于物理表面的精准伤害系统,能够根据命中部位(如头部、胸部)应用不同的伤害乘数,并结合护甲进行伤害减免,为游戏后期的PVP/PVE平衡性设计打下了坚实基础。


2. 系统与功能实现

本章实现的核心系统涵盖了武器、动画、伤害及弹药管理,并与前期系统紧密耦合。

实现系统/功能 详细描述 与前期系统的交互与依赖
通用武器瞄准系统 基于BP_WeaponAimMaster实现。提供了通用的第三人称越肩镜头偏移第一/三人称瞄准(ADS)逻辑(通过Timeline平滑过渡镜头FOV和位置)及瞄准时减速 功能。 依赖角色蓝图中的Spring Arm和Camera组件。通过**接口(Interface)调用角色蓝图中的函数来设置镜头偏移和移动速度。
远程武器功能 - 步枪: 实现自动开火、弹夹式装填逻辑。
- 火箭筒: 发射物理弹道抛射物,可对建筑造成范围伤害。
- 弓: 实现了长按蓄力拉弓、松手射击的机制。
- 矛: 具备近战刺击和蓄力投掷两种攻击模式。
交互于库存系统**,在装填弹药时消耗对应的弹药物品。
交互伤害系统,对其他玩家或建筑造成伤害。
依赖动画蓝图来播放特定的攻击、装填动画。
采集工具功能 实现了石头、石斧/镐、铁斧/镐等工具。这些工具继承自统一的工具基类,具备对特定资源(树木、矿石)的采集能力和效率分级。 交互于前期搭建的资源系统 (Harvesting System),根据工具类型和等级决定采集效率和掉落物。
依赖动画蓝图来播放挥砍/敲击动画。
动画混合系统 在动画蓝图中,通过Layered blend per bone节点,以spine_01为分界,将上半身的持械/瞄准动画与下半身的移动/跳跃/蹲伏动画进行混合。 依赖角色蓝图提供的状态变量(如isAiming, isCrouching)作为混合的判断依据。
精准伤害系统 - 客户端射线检测: 从客户端镜头中心发出射线,将命中信息传递给服务器,保证“所见即所得”的射击体验。
- 物理表面判定: 利用物理资产(Physics Asset)和物理材质(Physical Material)为角色不同身体部位(头、胸、四肢)添加标签。
- 伤害计算: 服务器根据射线命中的物理材质,应用不同的伤害乘数(如爆头),并检查玩家是否装备护甲以进行伤害减免。
交互玩家属性系统,最终将计算出的伤害值施加于玩家的生命值上。
依赖于角色的物理资产配置。

3. 关键设计思想

本章的设计体现了面向对象和接口驱动的编程思想,旨在构建一个高内聚、低耦合的系统。

设计原则/模式 应用实例 目的与优势
继承与多态 - 武器层级: BP_EquippableMaster -> BP_WeaponAimMaster -> BP_RifleMaster/BP_Bow
- 工具层级: BP_HatchetMaster -> BP_Rock/BP_IronHatchet
实现了代码的最大化复用。所有可装备物品共享基础逻辑,远程武器共享瞄准逻辑,具体武器只需实现其独特功能,结构清晰,易于管理。
依赖倒置 (接口驱动) 角色蓝图不直接引用任何具体的武器类(如BP_RifleMaster),而是通过BPI_EquippableItem接口与当前装备的物品通信(如调用UseItem, ReloadItem等函数)。 解耦。角色蓝图与具体武器实现完全分离。未来添加任何新武器,只要它实现了该接口,就能被角色蓝图无缝地使用,无需修改任何角色蓝图代码。
单一职责原则 - BP_WeaponAimMaster仅负责瞄准。
- BP_RifleMaster仅负责步枪的开火与装填。
- BP_ArrowProjectile仅负责箭矢的飞行、命中和伤害逻辑。
- 角色蓝图仅负责接收输入和管理玩家状态。
每个类或蓝图的职责清晰明确,使得代码更易于理解、测试和维护。当需求变更时,只需修改相应职责的类。
开放/封闭原则 武器系统对扩展是开放的(可以随时创建BP_WeaponAimMaster的子类来增加新武器),但对修改是封闭的(添加新武器不需要修改BP_WeaponAimMaster或角色蓝图的核心逻辑)。 保证了核心系统的稳定性。在不断迭代和添加新内容的过程中,不易引入新的Bug到现有功能中。

4. 核心技术点与难点

本章涉及多个网络同步和动画融合的关键技术,并解决了一些实现过程中的难点。

技术点/难点 描述与解决方案
核心技术: 客户端权威的射击 为了解决网络延迟导致的射击偏差(服务器与客户端所见的玩家位置不一致),采用了客户端先行的射线检测方案。实现方法: 在客户端获取摄像机精确的旋转角度,通过RPC(远程过程调用)将该角度传递给服务器。服务器以此角度为基准进行射线检测,从而确保射击结果与玩家瞄准的位置一致。
核心技术: 上下半身动画融合 为了让角色在移动或跳跃时仍能自然地持握和瞄准武器,使用了动画蓝图中的Layered blend per bone节点。实现方法: 以脊椎骨骼(spine_01)为界,将上半身的武器姿态动画和下半身的移动动画进行分层混合,实现了流畅、自然的全身动作表现。
难点1: 第一人称瞄准位置校准 问题: 在第一人称模式下瞄准时,武器的瞄具没有精确对准屏幕中心。
解决方案: 这是一个两步校准过程。首先,直接修改武器的站立动画 (Rifle_Idle),微调spine_03骨骼的旋转,使武器在静止时就与镜头方向大致对齐。然后,在武器蓝图(BP_WeaponAimMaster)中,通过微调第一人称瞄准镜头位置变量 (FP_AimDownSights_Location),将镜头精确移动到瞄具后方,最终实现完美对齐。
难点2: 客户端抛射物生成位置不同步 问题: 在客户端视角下,武器发射的抛射物(如火箭、曳光弹)生成在玩家脚底,而非枪口位置。这是因为服务器获取的附属组件(武器)位置信息存在网络同步延迟。
解决方案: 对于这种主要影响视觉表现的问题,采用了信任客户端位置的策略。在开火时,由客户端获取武器枪口的准确世界坐标,并通过RPC将这个位置向量传递给服务器。服务器在生成抛射物时,直接使用客户端发来的这个坐标,从而保证了视觉上的准确性。
难点3: 切换视角时瞄准状态残留 问题: 如果玩家在按住右键瞄准时切换第一/第三人称视角,镜头的FOV和偏移会卡在瞄准状态,无法复位。
解决方案: 创建了一个名为ResetCameraPosition的接口函数。在角色蓝图的切换视角逻辑中调用此接口。武器的父类BP_WeaponAimMaster实现了该接口,其功能是强制将镜头的Spring Arm组件和FOV重置为默认值,从而解决了状态残留问题。

5. 自我批判与重构

本章的实现过程并非一帆风顺,暴露出一些设计上的不足和可以优化的空间。

遇到的问题 (‘坑’) 反思与修正 如果重来一次 (优化方案)
重复的抛射物逻辑 矛和箭的抛射物蓝图(BP_ArrowProjectile, BP_SpearProjectile)中存在大量重复逻辑,如命中检测、附加到目标身上、伤害计算等。这违反了DRY (Don’t Repeat Yourself) 原则。 提取基类: 创建一个BP_BaseProjectile基类,将所有通用的逻辑(如事件命中、伤害接口、附加到Actor)在基类中实现。然后让BP_ArrowProjectileBP_SpearProjectile继承自这个基类,各自只需定义独特的属性(如模型、速度、重力),从而消除代码冗余,提高可维护性。
硬编码的资源效果 采集工具(如石斧、石头)命中不同资源(树、矿)时产生的粒子特效和音效是硬编码在各自的蓝图中的。这导致每次新增资源类型或想修改效果时,都需要去修改工具蓝图。 数据驱动设计: 创建一个数据表 (Data Table),用于映射物理材质命中效果(粒子特效、音效)。工具在命中物体时,获取其物理材质,然后去数据表中查询对应的效果进行播放。这样,设计师就可以通过修改数据表来配置所有命中效果,完全无需修改蓝图代码。
不一致的属性管理 武器的部分属性(如伤害值、弹药ID)定义在蓝图中,而另一部分定义在数据资产(Data Asset)中。这种分散的管理方式增加了平衡性调整的难度和出错的风险。 数据资产中心化: 将所有与武器数值相关的属性(伤害、射速、弹药ID、后坐力、可破坏的建筑等级等)全部集中到数据资产中。蓝图本身只负责实现逻辑功能,所有数据均从数据资产中读取。这使得数值策划可以独立地进行游戏平衡调整,而无需接触复杂的蓝图逻辑。
网络变量的初始值问题 在UE 5.4版本中,新创建的布尔变量默认值为true,导致isUsingItem等逻辑判断在初次执行时失败,需要手动修正。此外,忘记勾选Actor的**“Replicates”**选项也是一个常见且致命的失误。 建立开发规范: 团队应建立严格的开发检查清单(Checklist)。对于所有新建的网络功能蓝图,必须检查:1) Actor是否勾选"Replicates";2) 所有作为状态机的布尔变量初始值是否正确设置。通过流程来避免这类低级但影响严重的问题。

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更新,极大提升用户体验。

1. 本章核心目标

本章的核心任务是为游戏构建一个功能全面、支持多人模式且高度可扩展的建造系统。这一目标分为显性与隐性两个层面:

  • 显性目标 (玩家导向):

    • 核心玩法实现: 为玩家提供一套完整的沙盒建造体验,允许他们使用不同等级(木、石、金属)的建筑部件(如地基、墙壁、门、天花板、屋顶等)自由搭建建筑。

    • 流畅的建造体验: 实现网格对齐(Snapping)功能,使建筑部件可以轻松、精确地拼接。

    • 动态交互世界: 赋予建筑物理属性,使其能够被玩家的工具或武器损坏和摧毁,并加入可交互的部件(如门和火把)。

    • 系统无缝集成: 将建造系统与现有的库存系统深度整合,玩家需从快捷栏中选择并消耗相应的建筑物品来进行建造。

  • 隐性/战略目标 (项目与技术导向):

    • 架构奠基: 搭建一个可扩展的底层框架,为后续章节的储物箱、工作台等同样需要放置和交互的系统提供基础。

    • 网络同步优化: 设计一个对多种网络环境(单人、监听服务器、专用服务器)都表现稳健的系统。核心策略是分离客户端预览与服务器实体,以解决监听服务器(Listen Server)中预览模型对其他客户端可见的渲染问题。

    • 数据驱动设计: 采用数据资产(Data Assets)来定义所有建筑部件的属性,将数据与逻辑解耦,极大地简化了新建筑部件的添加和维护流程。

    • 服务器权威: 建立一套严格的服务器验证机制,所有关键的放置逻辑(如碰撞、悬空检查)均由服务器最终裁定,以杜绝客户端作弊的可能。

目标关系图:

graph TD
    A[核心目标: 全功能建造系统] --> B{显性目标: 玩家体验};
    A --> C{隐性目标: 技术战略};

    B --> B1[自由搭建与多样化部件];
    B --> B2[网格对齐提升操作感];
    B --> B3[可破坏与交互的动态建筑];
    B --> B4[与库存系统无缝集成];

    C --> C1[为未来系统奠定架构基础];
    C --> C2[设计健壮的多人网络模型];
    C --> C3[采用数据驱动提升扩展性];
    C --> C4[确保服务器权威防止作弊];

    C2 -- 关键实现 --> B;
    C3 -- 关键实现 --> B1;
    C4 -- 关键实现 --> B2;
    C4 -- 关键实现 --> B3;

2. 系统与功能实现

本章围绕 BPC_BuildingComponent 组件,构建了客户端预览与服务器实体分离的核心架构,并通过数据资产驱动,实现了以下关键系统:

系统/功能模块 核心职责 与前期系统交互
建造组件 (BPC_BuildingComponent) 挂载于玩家角色,作为建造系统的逻辑中枢。负责管理建造模式的启停、预览更新、向服务器发送放置请求等。 - 依赖输入系统: 接收玩家的快捷栏按键和鼠标点击输入。
- 依赖库存系统:HotbarComponent 获取要建造的物品信息。
建造预览 (BP_BuildPreview) 一个 非复制 的Actor,仅在客户端本地生成。用于向玩家实时展示建筑的预览效果,并提供放置合法性(可放置/不可放置)的视觉反馈(如颜色变化)。 - 无直接交互: 与其他系统逻辑解耦,仅作为客户端的视觉呈现工具。
可建造物主类 (BP_Buildable_Master) 一个 可复制 的Actor基类。当服务器验证通过后,在世界中生成该类的实例。这是所有玩家都能看到和交互的实际建筑对象。 - 交互伤害系统: 添加了“Damageable”标签,通过 Event AnyDamage 接收伤害。
- 交互接口系统: 实现了 BPI_Interactable 接口,使门、火把等子类可以被玩家交互。
数据驱动框架 (PDA_BuildableStructure) 使用主数据资产(Primary Data Asset)定义建筑。每个资产包含了预览模型、要生成的Actor类、放置规则(如能否放地上、是否需要地基)、碰撞盒尺寸等所有信息。 - 关联库存系统: PDA_ItemInfo(物品数据资产)中新增了一个字段,用于链接到对应的 PDA_BuildableStructure,从而将物品与建筑蓝图关联起来。
放置验证系统 在客户端(用于预览)和服务器(用于权威验证)两端执行一系列检查,确保放置的有效性。 - 依赖物理引擎: 通过射线和盒子检测进行碰撞和重叠判断。
网格对齐系统 通过在静态网格上预设 Socket 来定义吸附点。系统会自动计算并吸附到最近的有效 Socket 上,实现精准拼接。 - 不直接交互: 属于建造系统内部的独立功能。
结构损坏与摧毁系统 建筑拥有生命值(HP),当HP降为0时,会触发摧毁事件。利用几何集合(Geometry Collections)实现动态的破碎效果。 - 交互伤害系统: 接收来自工具(如斧头)的伤害事件。工具本身也通过接口定义了其能对何种材质(木、石)造成伤害。
连锁摧毁(坍塌)系统 当一个承重结构(如地基、墙体)被摧毁时,系统会向上和向周围进行盒子检测,连锁摧毁所有依赖于它的建筑部件,实现物理上的坍塌效果。 - 不直接交互: 属于建造系统内部的高级逻辑。

3. 关键设计思想

本章的设计体现了现代游戏开发中常见的优秀设计模式与原则,确保了系统的稳健性与可维护性。

设计思想 具体应用
数据驱动设计 核心逻辑(如放置、检测)与具体建筑数据完全分离。通过PrimaryDataAsset来定义一个建筑的所有属性,使得添加新建筑(如从木制地基到石制地基)无需修改任何核心代码,只需创建新的数据资产即可。这本质上也是一种策略模式的体现。
单一职责原则 (SRP) - BP_BuildPreview: 仅负责客户端的视觉预览和初步检测。
- BP_Buildable_Master: 仅负责已放置建筑在世界中的状态和行为。
- BPC_BuildingComponent: 仅负责协调整个建造流程,充当控制器角色。
开闭原则 (OCP) 系统对扩展开放,对修改关闭。新增任何类型的建筑部件,都只需创建新的数据资产和子类蓝图,而无需改动 BPC_BuildingComponent 的核心代码。
关注点分离 (SoC) 严格区分客户端逻辑和服务器逻辑。客户端负责即时视觉反馈和用户输入,服务器负责最终的权威验证和状态同步。这是构建安全、可靠的多人游戏功能的基石。
接口隔离原则 (ISP) 使用了多个职责明确的接口,如 BPI_Interactable(用于交互)、BPI_StructureDamage(用于定义伤害能力),而非一个庞大的通用接口,使得各个类只需实现其真正需要的功能。
组件化架构 将建造逻辑封装在 BPC_BuildingComponent 中,以组件的形式附加到角色上,遵循了组合优于继承的原则,提高了代码的灵活性和复用性。

4. 核心技术点与难点

技术点/难点 描述与解决方案
监听服务器的预览同步问题 问题: 在监听服务器模式下,服务器即玩家。若服务器为建造预览生成一个普通的复制Actor,所有客户端都会看到这个预览模型,造成不必要的网络流量和视觉混乱。
解决方案: 设计了专门的 BP_BuildPreview 类,并将其设置为 非复制 (Not Replicated)。服务器在本地生成这个Actor,因此它只对服务器玩家可见,不会同步给其他客户端。
服务器权威性验证 问题: 客户端可能发送恶意数据,请求在非法位置(如墙体内部)建造。
解决方案: 采用“客户端预测,服务器验证”模型。客户端进行实时检测以提供流畅的视觉反馈,但最终的放置请求会发送给服务器。服务器会 独立地、完整地 重新执行一遍所有的放置验证(重叠、悬空、支撑等)。只有服务器验证通过,才会生成真正的、被复制的建筑Actor。
连锁摧毁(坍塌)逻辑 问题: 如何高效地判断并摧毁所有附着在一个被毁地基上的建筑?
解决方案: 当一个承重结构(如地基)被摧毁时,它会在其上方及周围执行一次 BoxOverlapActors 检测。然后遍历所有重叠的Actor,检查它们是否带有特定标签(如 “AboveFoundation”),并对带有该标签的Actor调用Destroy接口事件,从而触发连锁反应,实现坍塌效果。
精准的网格对齐 (Snapping) 问题: 如何在众多可用的吸附点中,找到最符合玩家意图的一个?
解决方案: 系统首先获取目标建筑上所有与当前手持建筑标签匹配的 Socket。接着,遍历这些有效的 Socket,计算每个 Socket 的世界坐标与玩家视线落点的距离。最后,将预览模型吸附到距离最近的那个 Socket 上,实现了智能、精准的对齐。
射线与盒子检测的综合应用 - LineTraceByChannel: 用于从玩家摄像机向前发射射线,确定基础放置点。
- BoxTraceByChannel / BoxOverlapActors: 用于根据数据资产中定义的自定义尺寸,进行精确的重叠检测。
- 综合应用这些检测是实现所有放置规则的基础。
动态销毁效果 利用虚幻引擎的Chaos物理系统,通过创建几何集合 (Geometry Collection) 资产,为每个建筑部件预设破碎效果。当建筑的生命值降至0时,隐藏原始静态网格,同时激活几何集合并施加力,从而实现逼真的动态破碎效果。

5. 自我批判与重构

本章在开发过程中遇到了一些问题,并通过反思进行了修正与优化,体现了迭代开发的思想。

类别 问题描述与解决方案
遇到的’坑’或关键问题 - 标签继承问题: 发现子蓝图有时无法正确继承父类设置的Actor标签(如"Damageable"),导致伤害判定失效。解决方案: 手动为每个需要该功能的子蓝图重新添加标签。
- 预览模型“卡住”: 在早期版本中,当玩家的视线没有命中任何物体时,预览模型的 transform 没有被正确更新,导致其“卡”在最后一个有效位置。解决方案: 在射线检测逻辑中,为“未命中”的情况补充了更新预览模型位置的代码,使其始终跟随视线的终点。
- 命名规范冲突: 吸附逻辑因 Socket 命名问题(如"triangleFoundation"被错误地匹配到"foundation")而出错。解决方案: 采用了更独特、更严格的命名规则(如"triFound")来避免歧义。
对前期设计的反思与修正 - 重构预览系统: 明确指出当前使用的 非复制预览Actor 方案,是对旧课程设计的重大改进,专门解决了监听服务器模式下的核心痛点。
- 解耦检测逻辑: 最初,用于常规重叠检测的碰撞盒尺寸被复用到了天花板支撑检测中,导致逻辑冲突。解决方案: 在数据资产中为天花板支撑检测新增了一个独立的尺寸变量 CeilingBoxExtent,将两种检测逻辑彻底解耦,各自使用最合适的检测体积。
- 优化交互检测方式: 交互提示的触发方式从“每个建筑都有一个碰撞盒”优化为“玩家角色带有一个检测胶囊体”。这极大减少了场景中需要持续检测的碰撞体数量,对性能更为友好。
如果重来一次的优化点 - 统一的标签管理: 鉴于手动添加标签容易出错,未来可以设计一个基类函数(如在 BeginPlay 中调用),自动检查并确保所有建筑子类都拥有必需的标签,实现自动化管理。
- 数据资产验证: 可以开发编辑器工具或使用内置的验证功能,在保存数据资产时自动检查关键字段(如物品是否链接了有效的建筑资产)是否已正确填写,从而在开发阶段就避免许多潜在的运行时错误。
- 更清晰的Socket命名: 从项目一开始就应建立一套极其严格的Socket命名规范,并文档化,以避免在后期出现因命名模糊导致的逻辑判断错误。

1. 本章核心目标

本章的核心是构建一个完整且可扩展的护甲装备系统。这不仅包括玩家可见的功能实现,也包含了支撑未来开发的战略性技术决策。

1.1. 显性目标 (玩家导向)

为玩家实现了一套功能完整的护甲穿戴与交互系统,具体包括:

  • 装备穿戴:玩家可以将护甲(头盔、上衣、裤子、靴子、背包)从物品栏拖拽至专用的护甲槽位进行装备。

  • 视觉表现:装备护甲后,玩家角色模型会实时更新,显示相应的外观。

  • 属性增益:穿戴护甲会为玩家提供伤害减免效果,提升生存能力。

  • 耐久度系统:护甲拥有耐久度(生命值),在受到伤害时会损耗,最终会损坏并消失。

  • UI反馈:在物品栏UI中增加了专门的护甲槽位,并集成了一个3D角色预览窗口,让玩家可以直观地看到装备变化。

1.2. 隐性目标 (开发导向)

在技术和项目层面上,本章达成了以下几个关键的战略目标:

  • 系统模块化:通过创建BP_ArmorMaster基类及各种子类,构建了一个模块化的护甲蓝图结构,极大增强了系统的可扩展性与可维护性。

  • 深化游戏玩法:引入了伤害减免和装备损耗机制,丰富了战斗和生存策略,为后续更复杂的战斗系统(如部位伤害)和经济系统(如装备修理)奠定了基础。

  • 增强用户体验 (UX):实现了实时的3D角色预览窗口,这是一种高级的UI/UX解决方案,显著提升了玩家的沉浸感和装备管理的便利性。

  • 解耦通信架构:广泛使用蓝图接口(Interface)在UI、玩家控制器和角色之间进行通信,遵循了高内聚、低耦合的设计原则,使各模块可以独立开发和修改。

1.3. 目标关系图

下表清晰地展示了显性目标与隐性目标之间的支撑关系:

显性目标 (玩家看到什么) 隐性/战略目标 (我们做了什么)
玩家可以穿上各种护甲,并看到外观变化。 系统模块化:建立了BP_ArmorMaster继承体系,便于未来添加新护甲。
穿上护甲后更“抗揍”了。 深化游戏玩法:实现了基于护甲件数的伤害减免算法。
护甲被打多了会坏掉。 深化游戏玩法:设计并实现了护甲耐久度系统,将伤害事件与装备状态关联。
物品栏里可以直接看到角色穿上装备的样子。 增强用户体验:利用SceneCaptureComponent2D实现了高级的UI内3D预览功能。
拖拽装备即可穿上/脱下,操作流畅。 解耦通信架构:通过接口驱动OnDrop事件,实现了UI层和逻辑层的清晰分离。

2. 系统与功能实现

本章引入和扩展了多个系统,以支持完整的护甲功能。

2.1. 实现的系统列表

  • 护甲数据系统

    • 使用主数据资产 DA_ItemInfo 为每件护甲(如DA_LeatherBoots)创建了实例。

    • 定义了护甲的核心数据属性,如物品ID、图标、护甲类型(EArmorType枚举)、耐久度(ItemDamage字段复用为耐久度)等。

  • 护甲Actor系统

    • 创建了BP_ArmorMaster作为所有护甲Actor的父类,其继承自BP_ItemMaster

    • BP_ArmorMaster 内含一个骨骼网格体组件和用于存储自身物品信息的S_ItemInfo变量。

    • 为每种护甲创建了对应的子蓝图(如BP_ArmorBoots),并为其分配了具体的骨骼网格体。

  • 装备/卸装逻辑系统

    • 装备:在W_InventorySlotOnDrop事件中,增加了对"是否为护甲槽"的判断。若满足条件,则通过接口调用角色蓝图中的EquipArmor事件。 该事件在服务器上执行,负责生成护甲Actor,将其附加到角色身上,并使用SetMasterPoseComponent同步动画。

    • 卸装:通过判断拖拽来源是否为护甲槽(ContainerType == Armor)来触发DequipArmor事件。 该事件在服务器上执行,负责销毁护甲Actor,并将其物品信息添加回玩家物品栏。

  • UI扩展与3D预览系统

    • 护甲槽UI:复用并扩展了 W_InventorySlot 控件,在 W_Inventory 中创建了五个专用的护甲槽位。通过公开IsArmorSlotArmorType等变量,使其具备了处理护甲装备的特殊逻辑。

    • 3D预览:创建了一个独立的BP_PlayerWindow Actor,其包含一个骨骼网格体和一个SceneCaptureComponent2D。 通过将场景捕捉组件的输出渲染到一个Render Target,再将该Render Target动态创建为UI材质实例,并应用到W_Inventory中的一个Image控件上,从而实现了3D预览。

  • 护甲属性系统

    • 伤害减免:在武器(如BP_HatchetMaster)的伤害计算流程中,增加了对目标玩家护甲件数的查询。根据护甲件数,应用一个分级的伤害减免系数。

    • 耐久度损耗:在角色蓝图的ApplyAnyDamage事件流中,增加了对护甲造成耐久度伤害的逻辑。伤害值会平均分配给所有已装备的护甲件,并更新其CurrentHP。当CurrentHP小于等于0时,触发护甲破坏逻辑。

2.2. 系统交互流程图 (以“装备护甲”为例)

sequenceDiagram
    participant Player as 玩家
    participant UI_Slot as W_InventorySlot (护甲槽)
    participant Player_Char as BP_PlayerCharacter
    participant Server

    Player->>UI_Slot: 拖拽护甲物品至护甲槽
    UI_Slot->>UI_Slot: OnDrop() 触发
    UI_Slot->>Player_Char: 调用接口 EquipArmor(ItemInfo)
    Player_Char->>Server: RPC: EquipArmor_OnServer(ItemInfo)
    Server->>Server: 检查是否可装备 (如槽位是否已占用)
    Server->>Server: SpawnActor(BP_Armor_Child)
    Server->>Server: SetOwner() & AttachToComponent()
    Server->>Server: 调用 Multicast RPC 同步动画 (SetMasterPoseComponent)
    Server->>Player_Char: 调用接口 RemoveItemFromInventory()
    Server->>Player_Char: 调用接口 UpdateArmorUI()
    Player_Char-->>UI_Slot: 更新UI显示 (图标、耐久条)

3. 关键设计思想

本章的实现体现了多种优秀的设计模式与原则,确保了系统的健壮性和可扩展性。

设计思想 具体应用实例
设计模式:观察者模式 (Observer) 通过蓝图接口和事件实现。UI(观察者)不直接修改角色(主题),而是发送EquipArmor消息。 角色状态改变后,通过UpdateArmorUI等事件通知控制器和UI进行更新。 这是一个典型的解耦通信模式。
设计模式:模板方法模式 (Template Method) BP_ArmorMaster作为抽象基类(模板),定义了所有护甲共有的组件(骨骼网格体)和变量(ItemInfo)。 子类如BP_ArmorBoots只需重写或填充具体内容(如指定特定的网格体资源),而无需重复实现通用逻辑。
设计原则:单一职责原则 (SRP) 职责划分清晰:
- 数据资产 (DA_):只负责存储静态数据。
- Actor蓝图 (BP_):负责游戏世界中的表现和交互。
- UI控件 (W_):负责显示和用户输入。
- 角色蓝图:作为核心逻辑中枢,处理装备、卸装、伤害计算等。
- BP_PlayerWindow:专门负责3D预览的渲染。
设计原则:开闭原则 (OCP) 系统对扩展开放,对修改关闭。要添加新护甲,只需创建新的数据资产和BP_ArmorMaster的子类,并在UI上添加新槽位即可。核心的装备和伤害计算逻辑几乎无需修改。虽然部分Switch节点 需要添加分支,但整体架构是高度可扩展的。
设计原则:接口隔离原则 (ISP) 使用了多个目标明确的接口,如BPI_SurvivalCharacterBPI_ArmorInterfaceBPI_PlayerWindow,而不是一个庞大的通用接口。 这使得类只需实现与其自身相关的方法,避免了实现不必要的功能。
设计原则:依赖倒置原则 (DIP) 高层模块(如BP_PlayerCharacter)不直接依赖底层模块(如BP_ArmorBoots),而是依赖于抽象(BP_ArmorMaster和蓝图接口)。例如,UI通过BPI_SurvivalCharacter接口与角色通信,实现了UI与角色逻辑的解耦。

4. 核心技术点与难点

本章涉及了多个关键技术点,并成功解决了实现过程中的一些难点。

技术/难点 描述与解决方案
核心技术:模块化角色与动画同步 描述:如何让独立的护甲模型完美地跟随角色运动和动画。
解决方案:将护甲的SkeletalMeshComponent附加到角色的主网格体上后,关键是调用Set Master Pose Component节点。 该节点强制护甲网格体(Slave)实时复制主角色网格体(Master)的骨骼姿态,从而实现无缝的动画同步。
核心技术:UI内嵌3D实时渲染 描述:如何在2D的UI界面中展示一个动态的、可交互的3D角色模型。
解决方案:在场景中一个玩家看不见的位置生成一个BP_PlayerWindow Actor。使用SceneCaptureComponent2D组件像摄像机一样“拍摄”这个Actor,并将画面实时输出到一个Render Target纹理资源上。 接着,动态创建一个使用此纹理的UI材质实例,并将其应用到UMG的Image控件上,从而在UI中“直播”3D场景。 通过在打开/关闭物品栏时开关Capture Every Frame来进行性能优化。
核心技术:客户端-服务器数据同步 描述:如何确保装备状态在所有客户端上正确同步。
解决方案:采用服务器权威模型。装备/卸装的核心逻辑通过RPC(远程过程调用)在服务器上执行(Run on Server)。 视觉表现的更新,如生成Actor和同步动画,则通过Multicast事件广播给所有客户端,确保所有玩家都能看到一致的外观变化。 UI的更新则只在所属客户端上执行(Run on Owning Client)。
难点解决:已装备物品的数据持久化 问题:护甲装备后会从物品栏数组中移除,那么它的耐久度等动态数据如何保存?
解决方案:当护甲被装备时,其完整的物品信息结构体(S_ItemInfo)被复制并存储在生成的护甲Actor(BP_ArmorMaster)的一个变量中。 当护甲受损时,直接修改这个Actor内部的变量。当护甲被卸下或损坏时,系统从该Actor中通过接口GetArmorItem取回最新的物品信息,然后才销毁Actor。

5. 自我批判与重构

在开发过程中遇到了一些问题,同时也对现有设计进行了反思,并提出了未来的优化方向。

类别 内容描述与优化方案
遇到的’坑’与Bug修复 Bug 1: 无法卸下护甲
原因: UI中的护甲槽没有被正确设置为Armor容器类型,导致拖拽时逻辑判断失败。
修复: 在W_InventorySlot中将ContainerType变量设置为“公开并生成时可指定”,然后在W_Inventory中为每个护甲槽手动设置为Armor类型。

Bug 2: 3D预览窗口动画不播放
原因: 动画蓝图中使用TryGetPawnOwner获取所有者,但BP_PlayerWindow是一个Actor而非Pawn。
修复: 改为使用GetOwningActor来正确获取Actor引用。

Bug 3: 忘记设置护甲蓝图的默认ItemInfo
原因: 生成的护甲Actor中没有预设物品信息,导致卸装时无法知道该往物品栏添加什么。
修复: 手动为每个护甲子蓝图的类默认值填充了正确的S_ItemInfo数据。
对前期设计的反思 W_InventorySlot控件的设计最初较为简单。本章通过增加布尔值IsArmorSlot和枚举ArmorType来扩展其功能。 这种方式虽然可行,但使基类变得臃肿。一个更优雅的重构方案是创建一个继承自W_InventorySlotW_ArmorSlot子控件,将护甲相关的特殊逻辑封装在子类中,更好地遵循了开闭原则和单一职责原则。
如果重来一次 (重构与优化) 1. 使用TMap优化Switch判断:
在角色蓝图中,装备/卸装逻辑使用了多个Switch on EArmorType节点来设置或获取对应的护甲槽位变量。 这可以重构为使用一个TMap<EArmorType, AActor*>(蓝图中的Map变量)。这样,整个Switch结构可以被一个Map.Find()Map.Add()操作替代,代码更简洁,扩展新护甲类型时也无需修改节点,完全符合开闭原则。

2. 抽象化伤害处理:
当前的伤害减免逻辑硬编码在武器(Hatchet)中。 更优的设计是为角色添加一个“可被伤害组件”(Damageable Component)。武器只负责调用该组件的“承受伤害”接口,而所有关于护甲减免、耐久度计算的逻辑都应由该组件内部处理。这能将伤害计算的职责从攻击方转移到被攻击方,是更稳健和可扩展的设计。

3. 优化Actor生成:
当前设计中,每次装备/卸装都会Spawn/Destroy Actor。 对于频繁操作,这可能会带来性能开销。未来可以引入对象池(Object Pool)模式,预先生成一定数量的护甲Actor并隐藏,装备时从池中取出一个并激活,卸装时归还池中,从而避免运行时的动态内存分配和销毁。

1. 本章核心目标

本章的核心任务是构建并集成“印痕系统”(Engram System),这是一个关键的玩家成长与内容解锁机制。其目标可分为显性与隐性两个层面。

  • 显性目标 (玩家导向):

    为玩家实现一个完整的印痕学习流程。具体表现为:

    1. 升级获得点数: 玩家角色升级时,会获得一定数量的“印痕点数”。

    2. 解锁制作配方: 玩家可以在一个专门的“印痕窗口” (Engram Window) 中,消耗点数来学习和解锁新的物品制作配方。

    3. 与制造系统联动: 成功解锁的印痕会立即在玩家的“制造界面” (Crafting Tab) 中变为可制造项目,从而将玩家的等级成长与制造能力直接挂钩。

  • 隐性目标 (技术与战略导向):

    1. 数据驱动设计实践: 核心目标是实践并巩固数据驱动的开发模式。通过使用虚幻引擎的 PrimaryDataAsset 来定义每一个印痕,将游戏内容(如解锁等级、消耗、图标等)与程序逻辑解耦,便于未来扩展和维护。

    2. 系统模块化与集成: 将新建的印痕系统无缝对接到现有的玩家属性系统(获取等级和点数) 和制造系统(控制配方可见性) 中,锻炼团队在复杂项目中的模块化设计与系统整合能力。

    3. 客户端/服务器架构深化: 在网络环境下,实现一套完整的、安全的“解锁”逻辑。所有关键验证(如点数是否足够)和数据修改(扣除点数、更新已解锁列表)均在服务器端进行,客户端仅负责发送请求和刷新UI,以此来防止作弊并保证数据一致性。

2. 系统与功能实现

本章围绕印痕系统,实现了一系列前端UI与后端逻辑紧密结合的功能模块。

  • 核心功能模块列表:
模块名称 实现细节 关键作用
印痕数据资产 (PDA_Engram) 基于 PrimaryDataAsset 创建,包含物品ID、所需等级、消耗点数、图标、制造类型等核心数据。 作为印痕系统的数据源,实现内容与代码分离,方便策划配置和扩展。
印痕主窗口 (W_EngramWindow) 一个独立的UI面板,通过 Widget Switcher 集成在主库存界面中。负责动态获取并展示所有印痕。 为玩家提供与印痕系统交互的统一入口。
印痕动态加载与排序逻辑 在窗口初始化时,通过 Asset Manager 异步获取指定目录下的所有印痕数据资产。由于引擎获取顺序是随机的,代码中实现了一套手动排序逻辑,将资产按“所需等级”分组存入一个 TMap 中,再依次生成UI。 解决了引擎底层功能无法满足特定排序需求的难题,确保UI显示条理清晰。
层级化UI生成 UI采用三层嵌套结构动态生成:W_EngramWindow -> W_EngramSection (按等级分组) -> W_EngramSlot (单个印痕)。 结构清晰,便于管理。W_EngramSection 的引入使得按等级解锁整个区域的视觉表现成为可能。
印痕解锁流程 (C/S) 1. 客户端: 点击“学习”按钮,触发接口调用,将所选印痕的软对象引用 (Soft Object Reference) 发送到服务器。
2. 服务器端: 接收请求,验证玩家等级与点数是否足够。验证通过后,扣除点数,并将该印痕ID添加至玩家角色持有的已解锁列表中。
3. UI反馈: 服务器处理完毕后,重新向客户端同步最新的印痕数据,并触发一个通知UI(如“您已学会石斧印痕”)。
构成了安全、完整的网络交互闭环,保证了游戏逻辑的严谨性。
  • 与前期系统交互关系:
交互系统 依赖/交互方式 冲突与解决方案
玩家属性系统 (Ch.6) 强依赖: 印痕系统的解锁逻辑直接读取玩家的 CurrentLevelEngramPoints。能否解锁、能否点击某个印痕,完全取决于属性系统提供的数据。 无直接冲突。本章是对属性系统产出(等级、点数)的应用和消耗,是系统间良性协作的体现。
制造系统 控制与被控制: 印痕系统成为制造系统的前置“门禁”。制造UI在生成制造列表时,不再是获取所有配方,而是必须先获取玩家已解锁的印痕ID列表,再根据此列表来筛选并显示对应的制造配方。 无直接冲突。通过在制造逻辑中增加一道“是否已解锁”的检查,实现了两个系统的无缝衔接,深化了游戏进程的层次感。
主UI框架 (HUD) 集成关系: 印痕窗口 W_EngramWindow 被作为一个子控件添加到了游戏主库存布局 W_GameInventoryLayoutWidget Switcher 中。 潜在问题: UI状态不重置。切换到印痕Tab后关闭UI,再次打开时仍然停留在印痕Tab。
解决方案: 在主库存布局的 OnActivate 事件中,强制将 Widget Switcher 的索引重置为0,确保每次打开都返回默认的库存界面。

3. 关键设计思想

本章的实现体现了现代游戏开发中若干重要的设计思想与原则。

  • 设计模式应用:
设计模式 应用实例 优势
数据驱动设计 (Data-Driven) 整个印痕系统的核心内容(解锁条件、消耗、图标等)全部定义在 PDA_Engram 数据资产中,而非硬编码在代码里。 高度解耦: 策划可独立于程序员添加或修改印痕,极大提升迭代效率。
易于扩展: 增加新印痕只需创建新资产文件,无需改动和重新编译代码。
事件驱动/观察者模式 (Event-Driven) 从UI点击到服务器逻辑处理,再到客户端UI刷新和通知,整个流程由一系列事件(OnClicked, RPCs, Custom Events)串联起来。 职责分离: 各模块只需关注自身关心的事件,无需了解触发事件的具体源头,降低了模块间的耦合度。
逻辑清晰: 复杂的交互被分解为一系列线性的事件处理流程。
组件化/组合模式 (Composition) 印痕UI由多个独立、可复用的Widget组合而成:W_EngramSlot(槽)、W_EngramSection(区域)、W_EngramWindow(窗口)。 高复用性: W_EngramSlot 这样的基础组件可以在其他需要展示物品的UI中复用。
易维护性: 修改单个组件的样式或功能,不会影响到其他部分。
  • 设计原则体现:
设计原则 体现方式
单一职责原则 (SRP) - W_EngramSlot: 只负责显示一个印痕的信息并处理其点击事件。
- FirstPersonCharacter Blueprint: 只负责存储玩家的核心数据(如已解锁的印痕ID列表)和执行服务器端的权威逻辑。
- PlayerController: 充当客户端与服务器间的通信桥梁,负责RPC的转发。
开闭原则 (OCP) 通过数据驱动设计,系统对“扩展”(增加新印痕)是开放的,对“修改”(核心解锁逻辑代码)是关闭的。新增内容不会破坏原有系统稳定性。
关注点分离 (SoC) - 数据与视图分离: 印痕数据存在于Data Assets,其视觉表现在Widget中,两者通过逻辑层关联。
- 客户端与服务器分离: 客户端(Client)负责表现和用户输入,服务器(Server)负责核心逻辑计算和数据校验,职责明确。

4. 核心技术点与难点

本章的开发涉及到了几个关键的技术点,并成功解决了一些具有代表性的难点。

  • 关键技术点:
技术点 描述
PrimaryDataAssetAssetManager 利用AssetManager在运行时根据目录路径动态加载所有PrimaryDataAsset类型的印痕资产。这是实现数据驱动而无需手动注册每一个资产的核心技术。
客户端-服务器RPC通信 熟练运用了从Widget -> PlayerController -> Character -> Server,再从Server -> PlayerController -> Widget的完整RPC(远程过程调用)链路,用于数据请求和状态同步。
TMap 数据结构应用 使用 TMap<int32, FYourStruct> 的数据结构在Widget蓝图中对获取到的无序资产进行高效的分组和存储,以等级作为Key,极大地方便了后续的UI生成。
软对象引用 (Soft Object Reference) 在需要传递资产引用但又不希望立即将其加载到内存时,使用了软对象引用。这是一种优化内存占用的良好实践,尽管在教学中为了简化使用了阻塞加载。
  • 技术难点与解决方案:
难点 描述与挑战 解决方案
数据资产的无序加载 AssetManagerGetAssetsByPath函数返回的资产列表顺序是随机的,无法直接用于按等级顺序生成UI。 手动排序与分组: 设计了一套二次处理逻辑。首先遍历所有加载的资产,读取每个资产的 RequiredLevel 属性,然后以Level为键,将资产存入一个TMap中。这样,所有相同等级的资产就被自动聚合到了一起。最后再按顺序(从1级到最大等级)遍历这个TMap来生成UI,从而实现了有序展示。
RPC无法直接传递数组 在Unreal Engine的网络框架中,像TArray<int32>这样的原始类型数组不能直接作为RPC参数进行网络复制。 结构体封装 (Struct Wrapping): 创建一个专门的结构体(S_Engram),其内部包含一个TArray<int32>成员变量。在RPC调用时,传递这个结构体的实例。引擎可以正确地复制整个结构体,从而间接地实现了数组的网络传递。这是一个在UE中处理此类问题的标准且高效的模式。

5. 自我批判与重构

复盘本章的开发过程,我们识别出一些初期设计的问题,并通过后续修正进行了优化。同时也对未来可能的重构方向进行了思考。

类别 问题/反思 解决方案/优化提议
遇到的"坑" UI状态残留: 在不同UI标签页(库存/印痕)间切换后,若不重置,关闭再打开UI时会停留在上次的标签页,不符合用户预期。
数据刷新不及时: 玩家升级后,印痕窗口不会自动刷新解锁状态,需要玩家手动切换一下Tab才能触发刷新。
统一入口重置状态: 在主库存UI的OnActivate事件(每次激活时调用)中,强制将WidgetSwitcher的激活索引设为0。这确保了每次打开UI时都从默认页开始,并间接解决了数据刷新问题,因为重新点击印痕Tab会再次触发完整的数据加载流程。
前期设计反思 最初在实现制造列表时,并未考虑未来的解锁机制,导致所有配方对玩家都是可见的。本章通过引入印痕ID列表进行前置检查,对制造系统的逻辑进行了修正和增强。 这次修正是项目迭代中的正常过程,体现了逐步完善系统功能的设计演进。
如果重来一次 1. 网络通信优化: 当前解锁印痕后,服务器会推动一次完整的UI数据刷新。更优化的方案是,服务器仅返回一个轻量级的成功回执(如:UnlockSuccess(ItemID)),由客户端根据这个回执,仅更新单个EngramSlot的视觉状态,而不是重构整个窗口,以减少网络带宽和客户端计算开销。
2. 异步加载实践: 教学中为了简化使用了Load Asset Blocking,这在实际项目中可能导致游戏线程卡顿。重构时应全面采用Async Load Asset,并在其回调函数中执行后续逻辑,保证游戏流畅性。
3. 排序逻辑改进: 当前的排序循环是基于一个硬编码的最大等级。更健壮的设计是先遍历一次所有资产,动态确定存在的最大等级,或者直接遍历已构建好的TMap的键,从而避免不必要的循环。

一、本章核心目标

本章的核心任务是构建一个全面且与游戏核心循环紧密相连的玩家属性系统。此目标可分解为对玩家可见的显性目标和项目层面的隐性战略目标。

  • 显性目标 (Player-Facing Goals):

    为玩家实现一个完整的、可视化的角色属性与成长体系。具体包括:

    • 基础生存属性: 建立并展示核心生存指标,如生命、体力、食物和水分等,让玩家能够直观地了解角色状态。

    • 动态交互: 玩家能够通过行为(如冲刺)消耗属性(体力),并通过消耗品(如浆果)恢复属性。

    • 环境影响: 引入被动消耗机制,如食物和水分会随时间自动减少,强化了生存压力和资源管理的重要性。

    • 角色成长: 建立经验与升级系统,玩家通过游戏行为(如采集)获得经验,升级后获得技能点,并可自由分配技能点以增强特定属性,实现个性化角色构建。

  • 隐性/战略目标 (Technical & Project Goals):

    在技术和项目管理层面,本章旨在为后续开发奠定坚实的数据和逻辑基础。

    • 系统解耦: 设计并实现一套基于接口(Interfaces)和事件(Events)的通信框架,将核心属性逻辑(服务器端)与UI表现(客户端)分离,确保系统的可维护性和多人游戏下的稳定性。

    • 数据驱动设计: 引入数据资产(Data Assets)和数据表(Data Tables)来管理消耗品效果和经验曲线,将策划数值与程序逻辑分离,便于后期进行游戏平衡性调整,而无需修改核心代码。

    • 模块化与可复用性: 创建独立的、功能专一的UI控件(Widgets)和功能函数,如独立的属性条、通知模块等,确保了代码的可复用性和未来扩展的便捷性。

    • 网络同步基础: 严格区分客户端与服务器逻辑,通过RPCs(远程过程调用)确保所有关键状态(如伤害计算、属性消耗)在服务器上权威执行,并同步到客户端,为稳定的多人游戏体验打下基础。


二、系统与功能实现

本章成功实现了一系列相互关联的系统与功能模块,构成了玩家属性的核心框架。

  • 实现系统/功能列表:

    1. 核心属性结构 (S_PlayerStatStruct, S_PlayerCoreStats): 定义了玩家所有健康、生存及成长相关的数值结构体,作为数据的中心模型。

    2. 属性UI系统:

      • HUD属性条 (W_PlayerStatsWindow): 在主界面实时显示生命、体力、食物、水分等关键属性的圆形进度条。

      • 库存界面属性详情 (W_PlayerInfoWindow): 在玩家库存界面提供更详细的属性数值展示、经验条、技能点,并包含用于升级属性的交互按钮。

    3. 伤害系统:

      • 通过为玩家角色添加Damageable标签,实现了可被伤害的逻辑。

      • 实现了Event AnyDamage事件,在服务器上权威处理伤害计算并更新生命值。

    4. 被动消耗系统:

      • 通过服务器端的计时器 (Set Timer by Event),实现了食物和水分随时间被动减少的生存机制。

      • 实现了“饥饿”与“脱水”状态,当相应属性降为零时触发,并显示UI通知。当玩家处于这些负面状态时,会持续扣减生命值。

    5. 体力与冲刺系统:

      • 玩家移动(特别是冲刺)时会消耗体力。

      • 当体力低于特定阈值时,玩家移动速度会降低。

      • 停止移动后,体力会自动再生。

    6. 消耗品系统:

      • 创建了可定义消耗效果的数据资产 (PrimaryDataAsset_ConsumableEffect),允许策划轻松配置物品(如浆果)能恢复哪些属性、恢复量(即时/持续)。

      • 实现了物品消耗逻辑,玩家使用快捷栏中的消耗品会减少物品数量并触发属性恢复效果。

    7. 经验与升级系统:

      • 玩家通过特定行为(如采集)可获得经验值。

      • 通过数据表 (DT_ExperienceTable) 定义了各等级所需的经验值。

      • 经验值满后,玩家自动升级,获得技能点,并触发UI通知。

    8. 技能点分配系统:

      • 玩家可以在库存界面点击“+”按钮,消耗技能点来永久提升某个属性的最大值(如最大生命值)。
  • 与前期系统依赖交互:

    本章构建的系统与前期已有的物品和库存系统深度集成。

    • 依赖: 消耗品系统依赖于物品系统的PrimaryDataAsset_ItemInfo,通过在其内部新增一个对ConsumableEffect数据资产的引用来判定一个物品是否可被消耗及其效果。

    • 交互: 当玩家在快捷栏使用一个被标记为Consumable的物品时,会触发本章实现的F_ConsumeItem函数,该函数会与库存组件 (BPC_ItemsContainer) 交互,减少物品数量,并应用属性恢复效果。


三、关键设计思想

本章的实现体现了多种优秀的软件设计思想和原则,确保了系统的健壮性与可扩展性。

设计思想/原则 具体应用实例 优势分析
数据驱动设计 (Data-Driven) - 使用数据表 (DT_ExperienceTable) 管理角色升级所需的经验值。
- 使用数据资产 (PDA_ConsumableEffect) 定义消耗品的效果,如恢复量和持续时间。
将易变的策划数值(平衡性)与稳定的程序逻辑分离,允许在不重新编译代码的情况下调整游戏平衡,极大提升了开发和迭代效率。
观察者模式 (Observer Pattern) 通过蓝图接口 (BPI_SurvivalController)事件实现。角色(被观察者)在属性变化后,通过控制器调用接口消息(如UpdateStatBar)。 UI控件(观察者)在控制器中监听这些消息并更新自身状态。 实现了核心逻辑与UI表现的松耦合。角色不需要知道具体的UI实现,UI也可以独立于角色逻辑进行修改,符合高内聚、低耦合的原则。
单一职责原则 (SRP) - 创建了多个功能专一的UI控件,如W_PlayerStat(单个属性条)、W_StatNotification(状态通知)。
- 将不同逻辑封装在独立的函数中,如F_FoodWaterDrainF_AddExperience
每个模块或函数只负责一项任务,使得代码更易于理解、测试和维护。
关注点分离 (SoC) - 严格使用Switch Has Authority节点和RPCs(Run on Server, Run on Client)来区分服务器端权威逻辑和客户端表现逻辑。 这是构建稳定、安全的多人游戏的基础。保证了所有重要计算都在服务器上进行,防止客户端作弊,同时确保了流畅的客户端体验。
开闭原则 (OCP) - 消耗品系统:要添加一个新的消耗品,只需创建一个新的物品数据资产和对应的消耗效果数据资产即可,无需修改任何现有的消耗逻辑代码。 系统对扩展开放(可以添加新物品),对修改关闭(核心消耗逻辑稳定不变),增强了系统的灵活性和可扩展性。

四、核心技术点与难点

本章的实现涉及多个Unreal Engine的核心技术,并解决了多人游戏开发中的一些经典难题。

  • 关键技术点:

    • UMG高级应用: 深度使用UMG创建了模块化、数据驱动的UI。包括控件的动态创建(Create Widget)、数据绑定、以及使用UI动画(Animation Track)来增强视觉反馈(如经验获得时的弹出效果)。

    • 异步资源加载 (Async Load Asset): 在需要获取物品详细信息(特别是消耗品效果)时,采用异步加载的方式,避免了因同步加载磁盘资源而可能导致的瞬间卡顿,保证了游戏运行的流畅性。

    • 蓝图通信框架: 综合运用蓝图接口、事件分发器和RPCs,构建了一个高效、解耦的系统间通信网络,是本章实现多人游戏功能的核心。

    • 计时器管理 (Timer): 熟练运用计时器实现被动属性消耗、持续性效果(如消耗品随时间恢复)和冷却时间等逻辑。

  • 难点与解决方案:

难点 解决方案
多人环境下UI状态的精确同步 采用了一套严谨的服务器权威更新流程
1. 服务器执行权威操作(如应用伤害)。
2. 服务器更新权威数据(如PlayerStats结构体)。
3. 服务器通过接口调用玩家控制器上的事件。
4. 玩家控制器再调用一个Run on Owning Client的RPC事件。
5. 客户端在该事件中,安全地访问并更新自己的UI控件。
这确保了数据的一致性和UI的及时响应。
设计可扩展的经验和消耗品系统 通过数据驱动的方式解决:
- 经验系统:将复杂的等级经验曲线抽象为一个数据表 (DT_ExperienceTable),策划可直接在表格中配置每一级的数值,无需程序员介入。
- 消耗品系统:将物品效果抽象为独立的数据资产,新物品只需引用或创建新的效果资产即可,扩展性极强。
处理复杂的状态逻辑(如多级升级) F_AddExperience函数中,通过一个循环检查的逻辑来处理一次性获得大量经验可能导致的多级提升。 该函数使用局部变量来追踪剩余待处理的经验值,循环判断是否足够升下一级,直到所有经验值被正确分配。 这保证了等级计算的准确性。

五、自我批判与重构

本章在实现过程中遇到了一些问题,同时也暴露出一些可优化的设计,为未来的重构提供了方向。

  • 遇到的“坑”与关键问题:

    • 网络同步Bug: 在开发过程中,曾出现一个因蓝图节点连线错误导致的多人模式下属性更新失败的问题。这暴露出在网络编程中,微小的疏忽都可能导致严重的同步问题,强调了细心和充分测试的重要性。

    • 测试数值不当: 在测试“饥饿”状态时,初始设定的消耗速率过高,导致属性瞬间清零,无法观察到渐变过程。这说明了迭代开发中,对测试用例和平衡数值进行细致调节的必要性。

  • 对前期设计的反思与修正:

    • 本章开发中,对前期的库存UI (W_Inventory) 进行了重构。为了集成新的属性详情面板,原有的布局被拆分并替换为更灵活的垂直框和水平框组合,并删除了不再需要的占位符按钮。这是项目迭代过程中的正常修正,反映了系统设计随功能增加而演进的客观规律。
  • 重构优化建议:

待优化点 当前实现 建议优化方案
技能点升级逻辑重复 F_LevelUpSkillPoint函数中,为生命、食物、体力等每个属性升级的逻辑几乎完全相同(获取当前/最大值、最大值+10、更新结构体)。 将此段逻辑抽象成一个独立的辅助函数,如F_IncreaseMaxStat(InStatType, InAmount)。原函数只需根据传入的属性类型调用这个新函数即可,从而大幅减少代码冗余,提高可维护性。
被动消耗逻辑的扩展性 F_FoodWaterDrain函数中,减少食物和水分的逻辑是分开处理的,存在轻微的重复。 创建一个更通用的函数F_ApplyPassiveDrain(StatType, DrainPercentage)。这样,未来若要增加新的被动消耗(如寒冷环境下的体温消耗),只需调用此通用函数,而无需编写新的独立逻辑。
“魔法数字”硬编码 冲刺速度(800)、体力消耗率(0.8)、技能点提升量(10)等数值直接写在蓝图中。 将这些易于变动的平衡性数值统一提取到一个全局的数据资产游戏实例变量中进行管理。这样策划就可以在一个地方集中调整所有相关数值,而不用深入到复杂的蓝图逻辑中。
0%