上岸的鱼

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

1. 本章核心目标

本章节的目标分为显性与隐性两个层面,它们共同服务于构建一个动态、高性能的开放世界。

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

    1. 动态AI生成: 实现一个区域系统(Zone System),当玩家进入特定区域时,动态生成AI(如鹿、狼),离开时则销毁它们,使世界显得生动且不可预测。

    2. 全场景植被采伐: 使地图上所有由程序化内容生成(PCG)的静态植被(Instanced Static Meshes)变为可采伐资源。玩家可以使用斧头砍树,或直接用手采集灌木。

    3. 资源再生: 为被采伐的植被(树木、岩石、灌木)添加重生(Respawn)逻辑,使其在一段时间后能够重新出现在世界中。

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

    1. 性能优化:

      • AI生命周期管理: 通过区域化加载和卸载AI,避免在整个大型地图上持续运行所有AI的逻辑,极大地节省了CPU资源。

      • 动态导航网格 (Dynamic NavMesh): 采用Navigation Invokers技术,仅在AI周围动态生成导航网格,而不是预烘焙覆盖整个大地图的导航数据,显著降低了内存占用和加载时间。

      • 异步资源加载: 在采伐系统中,使用软引用(Soft References)和异步加载(Async Load)来加载被转换的蓝图Actor,避免了因同步加载大量资源而可能导致的瞬间卡顿。

    2. 系统解耦与可扩展性:

      • 通过数据驱动(Data-Driven)的方式设计采伐系统,使用数据表(Data Table)将静态网格体(Static Mesh)与其对应的可采伐蓝图(Blueprint)和资源信息关联起来,使添加新的可采伐物种无需修改核心代码,提高了系统的可扩展性。

      • 广泛使用接口(Interfaces)来降低系统间的耦合度,例如,采伐工具通过区域接口(BPI_Zone)来请求移除植被,而无需了解BP_Zone内部的具体实现。

    3. 多人游戏同步的鲁棒性:

      • 解决实例静态网格(ISM)在多人环境下的移除同步问题,特别是要确保后加入游戏的玩家能看到已经被其他玩家采伐的植被是消失状态,这是实现持久化世界体验的关键一步。

2. 系统与功能实现

本章主要实现和修改了以下几个核心系统:

系统/功能 (System/Function) 详细描述 与前期系统的交互/依赖
程序化区域 (BP_Zone) 一个基于Actor的蓝图,包含一个大型的碰撞盒。它负责管理其范围内的AI和植被的生命周期。 依赖: 玩家角色(用于触发重叠事件)。
交互: 主动生成和销毁AI Actor,管理植被重生计时器。
AI生成与 despawn 逻辑 玩家进入区域时,服务器在该区域内通过Line Trace寻找有效地面位置,随机生成1-3个被动AI和1-2个攻击性AI。 玩家离开后,启动一个300秒的计时器,若区域内无其他玩家,则销毁所有AI。 依赖: AI Master蓝图、玩家Pawn的Tag(“Player”)。
冲突: 初始版本AI会生成在水中,后通过BPI_Water接口或Tag进行规避。
动态采伐系统 (ISM -> Actor) 修改了Hatchet_MasterFirstPersonCharacter的交互逻辑。当与一个实例静态网格(ISM)交互时,系统会:
1. 获取ISM的显示名称。
2. 在数据表 (DT_LargeHarvestableDT_GroundHarvestables) 中查找对应的可采伐蓝图类(软引用)。
3. 异步加载并生成该蓝图Actor。
4. 通过区域接口调用,移除原始的ISM实例,并确保此操作在所有客户端同步。
依赖: 前期的采伐工具、角色交互系统、数据表结构。
交互:BP_Zone通过BPI_Zone接口通信,以处理ISM的移除和后续的重生逻辑。
植被重生系统 在植被对应的Destructible_Mesh蓝图被销毁前,它会调用BP_Zone的接口,将自身的网格体、变换信息记录到BP_Zone的一个数组中。 BP_Zone启动一个长计时器(1800秒),计时结束后,若区域内无玩家,则遍历数组,重新生成这些植被。 依赖: BP_Zone系统、Destructible Mesh系统。
交互: 通过BP_FoliageRemove这个辅助Actor的RepNotify机制,确保被移除的植被对后加入的玩家也可见,并在重生时被正确销毁。
动态导航系统 采用引擎的Navigation Invokers功能。在AI Master蓝图上添加该组件,并启用项目设置中的"Generate Navigation Only Around Invokers"。 依赖: AI系统。
交互: 引擎的导航系统会围绕带有Invoker的AI动态生成导航网格,而不是一次性为整个地图生成,极大优化了性能。

3. 关键设计思想

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

  • 设计模式 (Design Patterns):
设计模式 应用实例 目的与优势
数据驱动 (Data-Driven) 使用DT_LargeHarvestableDT_GroundHarvestables数据表,将静态网格体名称映射到其可采伐的蓝图类。 解耦与扩展性: 无需修改代码即可添加新的可采伐物种。策划和美术可直接通过编辑数据表来配置游戏内容。
接口模式 (Interface) BPI_ZoneBPI_WaterBPI_Controller等接口被广泛使用。例如,任何工具或角色都可以通过BPI_ZoneRemoveFoliage函数来移除植被,而无需知道BP_Zone的具体实现。 降低耦合度: 使得系统模块之间可以独立修改和演进,提高了代码的灵活性和可重用性。
观察者模式 (Observer) 通过RepNotify (Replicated Using Notify) 机制实现。BP_FoliageRemove Actor中的一个复制变量被修改时,所有客户端都会自动执行OnRep函数,从而触发植被实例的移除。 状态同步: 提供了一种高效、自动化的方式来同步服务器上的状态变更到所有客户端,尤其对处理后加入玩家的状态同步至关重要。
异步加载 (Async Loading) 在采伐系统中,从数据表获取到蓝图类的软引用后,使用AsyncLoad节点来加载该类,加载完成后再生成Actor。 性能优化: 避免了在游戏主线程中同步加载资源导致的卡顿,保证了流畅的游戏体验。
  • 设计原则 (Design Principles):
设计原则 体现
单一职责原则 (SRP) BP_Zone负责区域内的生命周期管理。Hatchet_Master负责斧头的采伐逻辑。BP_FoliageRemove专门负责处理复制移除的逻辑。每个类的职责都非常清晰。
开闭原则 (OCP) 采伐系统对扩展是开放的(可以通过数据表添加新内容),对修改是封闭的(核心的采伐和移除逻辑不需要改动)。
依赖倒置原则 (DIP) 高层模块(如采伐工具)不直接依赖底层模块(如BP_Zone),而是两者都依赖于抽象(如BPI_Zone接口)。

4. 核心技术点与难点

技术点/难点 描述与解决方案
实例静态网格的交互与移除 难点: 实例静态网格(ISM)本身不是Actor,无法直接绑定交互逻辑和进行网络复制。
解决方案: 采用“替换法”。当检测到对ISM的交互时,获取该实例的变换(Transform),在相同位置生成一个带有完整交互和销毁逻辑的蓝图Actor,然后服务器调用RemoveInstance移除原始的ISM实例。
多人同步中的“幽灵”问题 难点: 初期使用简单的多播(Multicast)RPC移除ISM实例,在网络不佳时可能导致客户端移除失败,留下一个无法交互的“幽灵”植被。
解决方案(重构): 采用了更可靠的同步策略。服务器获取所有在线玩家的控制器,然后为每个控制器调用一个RunOnClient RPC,确保RemoveInstance指令在每个客户端本地执行,从而杜绝了幽灵问题。
后加入玩家的状态同步 难点: 如何让一个新加入服务器的玩家看到在他加入之前就已经被砍掉的树?
解决方案: 设计了BP_FoliageRemove Actor。每当一个植被被移除,就在其位置生成一个该Actor。此Actor被设置为网络复制,其内部一个关键变量使用RepNotify。当新玩家加入时,服务器会向他同步这个Actor的存在,OnRep函数被自动触发,执行移除相应ISM实例的逻辑,保证了世界的持久性和一致性。
开放世界性能优化 难点: 在巨大的地图上维持高性能。
解决方案: 综合运用了多种技术:1. 区域化加载BP_Zone)管理AI和逻辑。 2. 动态导航网格Navigation Invokers)避免巨大的静态导航数据。 3. 异步加载AsyncLoad)防止资源加载卡顿。

5. 自我批判与重构

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

    1. “幽灵”实例问题: 如上所述,初期的多播同步方案不可靠,是本章遇到的最严重的技术问题。

    2. 资源命名不一致: 数据表依赖精确的静态网格体显示名称进行查找。开发过程中因命名不规范(如Tree_01 vs Tree01)导致部分植被无法被正确识别和采伐。

    3. 几何集合体(Geometry Collection)的缩放错误: 在为岩石创建可破碎模型时,直接对场景中已经缩放过的岩石创建几何集合体,导致生成的破碎蓝图默认尺寸巨大,与原始岩石不匹配。

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

    • ISM移除逻辑的重构: 核心修正是放弃了简单的多播方案,转而采用了更健壮的“服务器遍历所有客户端并逐个发送RPC”的模式,彻底解决了同步问题。这是一个关键的迭代优化。
  • 如果重来一次,如何优化:

    1. 建立严格的命名规范: 在项目早期就应强制推行统一的资源命名规则,并使用工具进行校验,以避免因手动输入错误导致的数据关联失败。

    2. 抽象采伐逻辑为组件: 可以将“检测ISM -> 查数据表 -> 异步加载 -> 生成Actor -> 请求移除ISM”这一整套逻辑封装到一个可复用的Actor组件中。这样,无论是斧头(通过重叠)还是徒手交互(通过射线检测),都可以直接调用这个组件的功能,减少在Hatchet_MasterFirstPersonCharacter中的代码重复。

    3. 规范化基础资产制作流程: 在制作可破碎模型等衍生资产时,应规定必须基于未缩放、未旋转的原始模型(T-Pose/A-Pose)进行制作,以确保衍生资产的默认状态是标准的,避免在蓝图中进行不必要的“魔法数字”式的缩放调整。

1. 本章核心目标

本章的目标分为对玩家直接呈现的“显性目标”和项目开发层面的“隐性/战略目标”。

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

    为游戏世界引入动态的生物,丰富玩家的生存体验。玩家可以与这些生物互动,无论是狩猎它们以获取资源,还是躲避它们的攻击以求生存。

  • 隐性/战略目标 (Development & Strategic Goals):

    建立一个可扩展、模块化的AI基础框架。此框架不仅要实现当前所需的基础AI行为,还要为未来引入更多、更复杂的AI类型提供支持。同时,将AI系统与游戏的核心玩法循环(探索、战斗、采集)紧密结合。


2. 系统与功能实现

本章成功实现了两大类AI的核心功能,并通过一个共享的基类进行构建,确保了代码的复用性和可维护性。

2.1 主要系统/功能列表:

  • AI基类 (BP_AI_Master):

    • 作为所有AI角色的父类,统一管理通用属性和逻辑,如 “Damageable” 标签,为子类实现具体行为提供了基础。
  • 被动型AI系统 (BP_AI_Passive - 以鹿为例):

    • 简单漫游 (SimpleRoam): AI在没有外部干扰时,会在导航网格内随机选择一个可达点进行移动,移动间隙有随机时长的停顿。

    • 玩家感知与逃跑 (Flee): 利用 PawnSensing 组件,当在视野内“看到”玩家时,触发逃跑逻辑。AI会提升移动速度,并朝随机方向奔跑。

    • 逃跑冷却机制: 逃跑状态会通过计时器在30-90秒后重置,使AI恢复到简单的漫游状态,避免无限逃跑。

  • 攻击型AI系统 (BP_AI_Aggressive - 以狼为例):

    • 玩家感知与追击 (ChaseTarget): 同样使用 PawnSensing 组件感知玩家,但触发的是追击逻辑。AI会锁定玩家为目标并提升速度进行追击。

    • 攻击逻辑 (AttackOnServer): 当AI移动到玩家附近时,通过 Sphere Overlap Actors 进行范围检测,若玩家在攻击范围内,则对其施加随机伤害。

    • 仇恨丢失机制 (LoseAggro): 追击状态设有10秒的计时器。如果AI持续追击但无法攻击到目标(或因其他条件),仇恨会丢失,AI返回漫游状态。

  • AI死亡与资源采集系统:

    • 通用死亡逻辑: AI接受伤害,生命值降至0后触发死亡事件。死亡时启用Ragdoll物理效果,并使其碰撞体不再阻挡玩家。

    • 尸体采集: 死亡后的AI尸体拥有独立的“尸体生命值” (CorpseHP)。玩家使用带有 “HarvestTool” 标签的工具攻击尸体,会调用工具的接口函数 BPI_HarvestCorpse,根据工具伤害计算并给予玩家相应的资源(如生肉)。

    • 尸体消失: 尸体资源被采集完后,会触发一个延迟销毁机制,使尸体在5秒后从世界中移除。

2.2 与前期系统交互:

本章实现系统 依赖的前期系统 交互方式 可能的冲突与解决方案
AI伤害与死亡 玩家伤害系统、物品系统 AI需要有 Damageable 标签才能被玩家武器伤害。伤害数值由玩家武器决定。 冲突: 无。系统按预期工作,实现了良好的解耦。
AI尸体采集 物品系统、资源数据结构 1. 采集工具需有 HarvestTool 标签。
2. AI通过接口调用工具的采集函数。
3. AI掉落的资源定义在 S_GivenResource 结构体中。
冲突: 逻辑耦合。AI需要知道工具的接口。
解决方案: 使用蓝图接口(Blueprint Interface)进行通信,降低了硬编码依赖,是目前较优的方案。
AI导航 关卡设计 AI的移动范围完全依赖于 Nav Mesh Bounds Volume 的覆盖区域。 冲突:NavMesh 未正确生成或覆盖不全,AI将无法移动。
解决方案: 确保关卡中所有AI活动区域都被 NavMesh 覆盖,并通过按 “P” 键进行可视化调试。

3. 关键设计思想

本章的设计体现了面向对象编程和游戏开发中的一些核心原则与模式。

设计思想/原则 具体应用实例 带来的优势
继承 (Inheritance) BP_AI_PassiveBP_AI_Aggressive 均继承自 BP_AI_Master 代码复用: 共享基础组件和变量。
结构清晰: 建立了清晰的 “Is-A” 关系。
单一职责原则 (SRP) - SimpleRoam 函数只负责漫游逻辑。
- FleeOnServer 函数只负责逃跑逻辑。
- AttackOnServer 函数只负责攻击逻辑。
高内聚低耦合: 每个函数功能明确,易于理解、测试和修改。
接口隔离原则 (ISP) - 创建 BPI_HarvestCorpse 接口专门用于尸体采集。
- 创建 IsTargetDead 接口专门用于查询目标死亡状态。
精确通信: AI与工具/玩家之间只通过必要的接口通信,避免了不必要的函数暴露和依赖。
服务器权威 (Server-Authoritative) 核心逻辑(如状态变更、移动决策、伤害计算)都在标记为 Run on Server 的事件中执行。 防止作弊: 客户端只负责表现,核心游戏状态由服务器控制,保证了多人游戏环境的公平性。
状态机 (State Machine) - AI通过布尔变量(如 isDead, isFleeing, isChasing)隐式地管理其状态(漫游、追击、逃跑、死亡)。
- 动画蓝图使用State Machine来管理 Idle/Walk/Run 等动画状态。
逻辑清晰: 使复杂的AI行为可以被分解为一系列独立且易于管理的状态,以及明确的转换条件。
数据驱动 (Data-Driven) AI死亡后掉落的资源类型和数量由其自身的 GivenResources 变量(一个数据结构数组)定义,而非在代码中硬编码。 高可配置性: 设计师无需修改代码,只需调整数据资产即可改变AI的掉落物,极大提高了迭代效率。

4. 核心技术点与难点

本章的实现综合运用了Unreal Engine的多个核心功能,并解决了AI开发中的一些常见难点。

  • 核心技术点:

    • AI导航系统: 使用 Nav Mesh Bounds Volume 为AI生成可行走区域,并利用 AI Move To 节点驱动AI向目标点移动。

    • AI感知系统 (PawnSensingComponent): 高效地实现了基于视野的AI感知功能,作为触发AI行为(逃跑或追击)的入口。

    • 网络复制 (Replication):

      • 通过 RepNotify 变量 isDeadMulticast RPC,确保了AI死亡时的Ragdoll效果能被所有客户端正确观察到。

      • 将所有改变游戏状态的逻辑放在服务器RPC中执行,是构建稳定多人游戏的关键。

    • 蓝图接口 (Blueprint Interface): 实现了AI与玩家工具之间的解耦通信,是不同系统间交互的推荐做法。

    • 动画蓝图与混合空间 (Animation Blueprint & Blend Space): 将AI的移动速度与动画状态(站立、行走、奔跑)动态绑定,使AI动作表现自然流畅。

  • 技术难点与解决方案:

难点描述 解决方案
AI行为状态管理 使用多个布尔变量 (isDead, isMoving, isFleeing/isChasing) 结合自定义事件,构成一个隐式的状态机来控制AI在不同行为模式(漫游、追击/逃跑、攻击、死亡)间的切换。
流畅的AI转向 初始AI转向过于生硬(“snappy”)。通过修改角色移动组件的设置:取消 Use Controller Yaw,启用 Orient Rotation to Movement 并设置合适的 Rotation Rate,实现了平滑自然的转向效果。
多人环境下的状态同步 AI的核心状态(如生命值、是否死亡、当前行为)必须在所有客户端间保持一致。解决方案是采用服务器权威模型,所有状态变更都在服务器上发起,并通过 RepNotifyMulticast 事件同步到客户端。
可复用逻辑的实现 被动型和攻击型AI有共享的逻辑(如漫游、死亡、采集)。解决方案是将大部分死亡和采集逻辑直接从被动AI复制到攻击AI中,实现了快速开发。

5. 自我批判与重构

本章的实现功能完善且结构清晰,但也存在一些可以优化和重构的地方。

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

    1. 忘记添加标签: 忘记给AI添加 Damageable 标签,导致玩家无法对其造成伤害,这是初期调试中容易忽略的问题。

    2. 逻辑连接错误: 在死亡事件中,错误地将逻辑线连接到了 Branch 的错误引脚,导致AI生命值为0后没有正确触发死亡(Ragdoll)表现。

    3. 导航缺失: 如果场景中没有放置或正确配置 Nav Mesh Bounds Volume,AI将无法移动,这在初次设置时是一个关键步骤。

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

    本章的设计是建立在前期伤害和物品系统之上的,交互设计(通过标签和接口)是成功的。这证明了前期将核心功能(如伤害判定、物品使用)与具体实现(玩家、AI)解耦的正确性。

  • 如果重来一次,如何优化?

优化点 当前实现方式 优化方案 优势
逻辑复用 在被动型和攻击型AI之间复制粘贴了大量的死亡和采集逻辑。 将死亡、伤害处理、资源采集等通用逻辑下沉到 BP_AI_Master 基类中,或将其封装到可复用的组件 (Actor Component) 中。 减少代码冗余: 避免重复代码,降低维护成本。
更高内聚: 将相关功能(如“可被采集”)封装在一起,符合组件化设计思想。
AI行为切换 使用多个独立的布尔变量 (isFleeing, isChasing) 来管理状态。 引入**枚举(Enum)**来定义AI的状态(E_AIState: Roaming, Chasing, Fleeing, Dead),并使用一个变量来存储当前状态。 状态管理更清晰: 用一个枚举变量代替多个布尔值,可以更直观地表示AI的当前状态,并能有效防止出现(例如 isFleeingisChasing 同时为 true 的)非法状态组合。
攻击逻辑 使用 AI Move To,在其 On Success 引脚上触发攻击逻辑。 可以在 ChaseTarget 逻辑中,每帧或每隔一小段时间就检查与目标的距离。当距离小于攻击范围时,停止移动并执行攻击。 更灵敏的攻击触发: 无需等待 AI Move To 完成,AI可以在追击过程中更快速地响应并进入攻击状态,行为表现更真实。

1. 本章核心目标

本章的核心任务是利用虚幻引擎的程序化内容生成框架(PCG),为游戏世界构建一个由多个生物群落(Pine Forest, Tropical, Grasslands)组成的、富有生机且视觉上可信的动态生态系统。

  • 显性目标 (Explicit Goals):

    • 为玩家创建一个视觉丰富、层次分明的游戏世界,包含松树林、热带海滩和草原三种截然不同的地貌环境。

    • 在不同生物群落中,自动填充对应的植被(如树木、灌木)和环境道具(如岩石、浮木),提升世界的沉浸感。

    • 实现茂密的地面覆盖物,如与环境匹配的草地和落叶,使地表细节更加生动。

  • 隐性/战略目标 (Implicit/Strategic Goals):

    • 技术战略: 掌握并建立一套基于PCG框架的高效、可扩展的大世界场景填充工作流。该流程旨在自动化繁琐的植被摆放工作,同时保留美术层面的宏观控制力。

    • 性能战略: 针对不同类型的资产(如稀疏的大型物体与密集的地面植被),探索并实施最优的程序化生成方案,确保在提升视觉效果的同时,维持项目的性能稳定。

    • 系统整合: 将PCG系统与项目前期的景观材质系统(Landscape Material)深度整合,利用已有的材质图层数据(Layer)作为驱动程序化生成的“笔刷”,实现数据驱动的设计。

  • 目标关系图:

显性目标 (Explicit Goal) 隐性/战略目标 (Implicit/Strategic Goal) 关键实现技术
创建多样化的生物群落 建立可扩展、美术可控的程序化工作流 PCG Point Filter 节点,通过读取景观材质图层名称(如 “pine”, “tropical”)来区分和驱动不同群落的生成。
自动填充树木、岩石等大型资产 掌握PCG框架的核心功能 使用PCG核心节点组合:Surface Sampler, Transform Points, Density Filter, 和 Static Mesh Spawner
实现茂密的地面草地 为不同密度的资产选择最高效的工具 采用性能更优的 景观材质(Landscape Material) 内置的 Landscape Grass Output 节点,而非PCG。
确保植被分布自然、不均匀 自动化模拟环境的自然规律 运用 Normal To Density (坡度过滤), Density Noise (生成斑块), 以及 Difference (避免重叠) 等高级节点。

2. 系统与功能实现

本章主要构建了两个核心系统,它们协同工作,共同完成了多生物群落的程序化生成。

  • 程序化内容生成系统 (PCG System):

    • 描述: 一个名为 PCG_IslandMap 的主PCG图表被创建,作为整个岛屿生态生成的总控中心。 该系统以关卡中的主景观(Landscape)为输入源,通过一系列规则和过滤器,在指定区域生成静态网格体(Static Mesh)。

    • 实现流程:

      1. 数据输入: 通过 Get Landscape Data 节点获取景观信息。

      2. 区域过滤: 使用 Point Filter 节点,根据景观材质上绘制的图层名称(如 “pine”)筛选出目标生物群落的生成点。

      3. 环境规则过滤: 应用一系列过滤器,如基于坡度的 Density Filter(防止树木在悬崖上生成) 和基于噪音的 Density Filter(创造自然的林间空地)。

      4. 资产多样化: 利用 Transform Points 实现随机的缩放和旋转,并在 Static Mesh Spawner 中配置多种网格体变体,增加视觉丰富度。

      5. 碰撞规避: 使用 Difference 节点,从岩石的生成点中减去树木的生成点,有效防止资产重叠。

  • 程序化草地系统 (Procedural Grass System):

    • 描述: 一个独立于PCG、基于景观材质的轻量级程序化系统。它专门用于高效渲染大规模、高密度的地面覆盖物,如草地和落叶。

    • 实现流程:

      1. 为每个生物群落创建 Landscape Grass Type 资产,并在其中定义要生成的草地网格体、密度、剔除距离等参数。

      2. 在主景观材质中,添加 Landscape Grass Output 节点。

      3. 将各个 Landscape Layer Sample 节点的输出连接到 Landscape Grass Output 上对应的引脚。这样,草地的生成就完全由美工绘制的景观图层权重来控制。

  • 系统交互与依赖:

系统/模块 依赖于 交互方式 目的
PCG System Landscape Material PCG的 Point Filter 读取材质图层名称。 实现通过绘制地表来艺术指导(Art-direct)程序化内容生成。
Grass System Landscape Material Landscape Grass Output 节点直接由材质图层样本(Layer Sample)驱动。 将草地的分布与地表纹理紧密绑定,实现所画即所得。
PCG (Rocks) PCG (Trees) Rock生成分支使用 Difference 节点,减去Tree分支生成的点。 避免岩石与树木生成在同一位置,提升布局的合理性。

3. 关键设计思想

本章的实现体现了现代化游戏开发中数据驱动和模块化设计的核心思想。

  • 设计模式 (Design Patterns):

    • 策略模式 (Strategy Pattern): 针对“放置植被”这一目标,根据对象的特性(大小、密度)选择了不同的实现策略。对树木、岩石等大型稀疏物体使用PCG系统;对草地等小型密集物体使用景观草地系统。这体现了为不同场景选择最合适算法的思想。

    • 观察者模式 (Observer Pattern) (隐式): PCG系统实时“观察”着景观材质的变化。当美术师在编辑器中修改地表图层时,PCG系统会自动响应这一“通知”,实时更新植被的生成与销毁。这是一种高效的响应式、数据驱动设计。

  • 设计原则 (Design Principles):

    • 单一职责原则 (Single Responsibility Principle):

      • PCG系统的职责是“放置程序化定义的、相对稀疏的大型资产”。

      • 景观草地系统的职责是“渲染高密度的地面覆盖物”。

      • PCG图表内部,处理树木和处理岩石的节点链也各自负责不同的逻辑,分工明确。

    • 组合优于继承 (Composition over Inheritance): PCG图表的构建过程完美诠释了此原则。复杂的行为是通过将许多功能单一、可复用的小节点(如Sampler, Filter, Transform)组合(Composition)而成,而非通过继承一个庞大的“生物群落基类”来实现。这种方式提供了极高的灵活性和可扩展性。

4. 核心技术点与难点

类别 技术点/难点描述 解决方案与实现
核心技术 景观图层驱动的过滤 (Landscape Layer Filtering) 使用PCG中的 Point Filter 节点,将其操作目标设置为 Landscape Layer,并填入与景观材质中完全一致的图层名称(如"tropical"),从而将生成范围精确限定在特定区域。
核心技术 基于坡度的真实感过滤 (Slope-based Filtering) 通过 ProjectionNormal To DensityDensity Filter 的节点链,将地表的法线(代表坡度)信息转化为密度值,然后过滤掉密度值过高(即坡度过陡)的点,确保植被生长在合理的地形上。
核心技术 高性能草地渲染 (Performant Grass Rendering) 认识到PCG处理超高密度对象的性能瓶颈,果断放弃使用PCG生成草地,转而采用引擎内置的、经过高度优化的 Landscape Grass Output 方案。
解决难点 资产方向错误 (Incorrect Asset Orientation) 默认生成的树木会沿山坡法线倾斜生长。 通过在 Transform Points 节点中勾选 Absolute Rotation,强制资产的Z轴与世界Z轴对齐,使其垂直向上生长,同时仅在Z轴上施加随机旋转。
解决难点 资产重叠与不自然聚集 (Asset Overlap & Clustering) 不同类型的资产(树和岩石)会生成在同一位置。 解决方案是使用 Difference 节点从一个点集中减去另一个点集;对于同类资产,使用 Self Pruning 节点来移除过于靠近的点,保证间距。
解决难点 草地生成后不显示 (Grass Not Appearing) 在正确配置了所有草地系统后,关卡中依然看不到草。 这是一个引擎工作流问题,通过 Build > Build Grass Maps Only 手动重建草地数据,并重启编辑器来解决。

5. 自我批判与重构

本章的实现虽已功能完备,但仍有宝贵的优化空间和值得反思之处。

  • 遇到的"坑"与关键问题:

    • 性能陷阱: 险些用PCG处理草地,这暴露了在选择技术方案时,必须对工具的适用场景和性能限制有清晰的认识。

    • 工作流冗余: 为不同生物群落创建PCG逻辑时,大量使用了“复制粘贴”节点图。 这种方式效率低下,且后续维护成本高,容易出错。

    • 隐性依赖: 草地系统需要手动“构建”并“重启”才能生效,这种隐藏的工作流步骤可能会在团队协作中造成困惑。

  • 如果重来一次,可以如何优化?

优化点 当前实现方式 推荐的重构方案
图表逻辑重复 为每个生物群落复制粘贴一套相似的节点逻辑。 使用PCG子图 (Subgraphs): 将通用的生成逻辑(如过滤 → 变换 → 生成)封装成一个可复用的“生物群落生成器”子图。主图表将变得极为简洁,只需调用几个子图节点,并为其传入不同的参数(如图层名、资产列表),极大提升了可维护性和可读性,并遵循了DRY(Don’t Repeat Yourself)原则。
资产与逻辑紧耦合 静态网格体资产被硬编码在各个 Static Mesh Spawner 节点中。 采用数据驱动设计: 创建一个数据资产(Data Asset)或数据表(Data Table)来定义每个生物群落。表中包含图层名、树木列表、岩石列表、密度参数等。PCG图表仅负责读取这份数据来执行生成逻辑。这样,策划或美术师无需打开复杂的PCG图表,只需修改数据表即可轻松添加或调整生物群落,实现逻辑与数据的完全解耦。
手动材质变更 为实现颜色变化,手动创建了多个重复的材质实例。 程序化材质变种: 利用PCG为每个生成点赋予一个随机数,并将此数值写入自定义的“逐实例数据(Per-instance Custom Data)”。在资产的主材质中,读取该数据来程序化地驱动颜色、亮度或其他参数的变化。这样仅需一个材质实例即可创造出无限的视觉变种。
全局参数调整不便 调整一个全局性参数(如整体密度)可能需要进入图表修改多个节点。 暴露参数到组件: 将PCG图表中的关键参数(如全局密度乘数树木最大缩放)暴露到放置在关卡中的PCG Volume Actor的细节面板上。这使得关卡设计师可以直接在编辑器中快速迭代和调整效果,而无需深入图表内部,极大地改善了易用性。

1. 本章核心目标

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

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

    为玩家创建一个广阔、多样化且视觉上可信的开放世界岛屿地图。这包括:

    • 具有山脉、平原、海岸线等多种地貌的复杂地形。

    • 包含松林、草原、热带、河床、沙滩等多种生物群落的无缝混合地表。

    • 拥有动态海洋、河流和湖泊的完整水体系统。

    • 一个可供玩家角色进入并进行基本交互(如建造)的关卡环境。

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

    • 技术掌握: 熟练运用Unreal Engine的景观(Landscape)和水体(Water)系统,掌握创建复杂主材质(Master Material)的技术流程。

    • 资产整合: 建立一套高效的工作流,用于整合外部资源,如Quixel Megascans的纹理和第三方的地形高度图(Heightmap)。

    • 性能与效率: 通过使用材质实例(Material Instance)、程序化混合(如坡度遮罩)和预绘制图层导入,在保证视觉效果的同时,提升开发效率和运行性能。

    • 奠定基础: 为后续的游戏系统(如植被生成、AI寻路、任务部署等)提供一个稳定且功能完善的场景基础。


2. 系统与功能实现

本章成功实现并集成了以下关键系统:

  • 模块化主材质系统 (Master Material System):

    • 描述: 创建了一个名为 M_Landscape 的主材质,其核心特点是使用了“材质属性”(Use Material Attributes)。该材质整合了松林、草原、热带、河流和沙滩五种地表层。每一层都是一个独立的材质逻辑集合,并通过 LandscapeLayerBlend 节点进行混合。

    • 交互与依赖: 此系统依赖于通过 Fab/Quixel Bridge 插件下载的PBR纹理。所有可调参数(如颜色、粗糙度、纹理缩放等)均被设置为参数,以便在材质实例 MI_Landscape 中进行非破坏性调整,这极大地提升了美术迭代效率。

  • 高度图地形生成系统 (Heightmap-based Landscape Generation):

    • 描述: 绕过了耗时的手动雕刻,直接从一个免费的外部高度图文件 (.png) 导入生成了整个岛屿的基础地形结构。导入时对Z轴向的高度进行了调整(缩放至45)以获得更合适的山体陡峭度。

    • 交互与依赖: 地形的最终外观完全依赖于主材质系统的应用。同时,本章提供了预先绘制好的各生物群落的图层权重图(Layer Info),通过导入这些权重图,快速完成了地表的绘制工作,实现了地形数据与地表外观的分离管理。

  • UE原生水体系统 (UE Native Water System):

    • 描述: 启用了引擎的实验性 “Water” 插件,并使用其提供的工具创建了三种水体:

      1. Water Body Ocean: 使用样条线勾勒出整个岛屿的海岸线,形成海洋。

      2. Water Body River: 使用样条线工具在山谷中绘制出多条与海洋连接的河流。

      3. Water Body Lake & Island: 在内陆创建湖泊,并使用 Water Body Island 工具在湖泊和海洋中生成岛屿或排除水体区域。

    • 交互与依赖: 水体系统与地形系统紧密耦合。水体能自动与地形进行交互,生成海岸线的泡沫和湿润效果。水体的材质是独立的,但为了风格统一,我们对其进行了深度定制。

  • 系统实现流程图:

    graph TD
        A[启用插件: Fab & Water] --> B(下载纹理与高度图);
        B --> C{创建 M_Landscape 主材质};
        C -- 使用材质属性 --> D[为每个生物群落构建材质逻辑];
        D -- LandscapeLayerBlend --> E[混合所有图层];
        F[创建新关卡 Island_Map] --> G{进入Landscape模式};
        B & G --> H[导入高度图生成地形];
        E --> I{创建 MI_Landscape 材质实例};
        H & I --> J[将材质实例应用到地形];
        J --> K[创建并导入各图层权重信息];
        K --> L{进入Water模式};
        L --> M[添加并编辑海洋、河流、湖泊];
        M --> N[定制水体材质];
        N --> O[设置GameMode与PlayerStart];
        O --> P[打包测试: 验证功能与修复Bug];

3. 关键设计思想

本章的实现过程体现了多个核心设计模式与原则,确保了系统的可扩展性和可维护性。

  • 设计模式应用:

    • 原型模式 (Prototype Pattern) / 实例模式: 主材质 M_Landscape 作为“原型”,MI_Landscape 作为其实例。通过修改实例的参数来创建多样的地表效果,而无需重新编译复杂的父材质。这是UE材质工作流的核心模式,极大地提升了迭代速度和性能。

    • 策略模式 (Strategy Pattern): LandscapeLayerBlend 节点可以被看作是策略模式的实现。每个图层输入(如草原、沙滩)都是一个独立的“渲染策略”。地形绘制的权重图则决定了在地形的哪个部分应用哪种策略。这使得添加新的地表类型(新策略)变得非常简单,只需添加一个新的图层输入即可。

    • 模板方法模式 (Template Method Pattern): 主材质 M_Landscape 定义了整个材质的渲染框架(如使用材质属性,连接到最终输出),而每个具体的材质层(如松林、热带)则填充了这个框架中的具体实现细节。

  • 设计原则体现:

    • 开闭原则 (Open/Closed Principle): 材质系统对修改是关闭的(不轻易改动核心 LandscapeLayerBlend 结构),但对扩展是开放的(可以轻松添加新的生物群落图层)。

    • 单一职责原则 (Single Responsibility Principle): 在材质图表中,每一组节点(如“热带层”的所有节点)都只负责一个生物群落的外观,使得图表结构清晰,易于调试。

    • 不要重复自己 (Don’t Repeat Yourself - DRY): 通过创建可复用的节点组(如纹理坐标的缩放设置被应用于多个纹理)和使用材质实例,避免了代码和设置的重复。

    • 依赖倒置原则 (Dependency Inversion Principle): 通过将材质参数化,高层模块(美术师调整的材质实例)不直接依赖于底层模块(复杂的材质着色器逻辑),而是两者都依赖于抽象(参数接口)。


4. 核心技术点与难点

本章的开发涉及了多个关键技术,并解决了若干实现难点。

  • 关键技术点:

    • 程序化地貌混合:

      • 高度混合: 利用 Absolute World Position 节点的Z值(高度),结合 SmoothStep,实现了沙滩干湿分离的自动过渡效果。在特定高度以下为湿沙,以上为干沙。

      • 坡度混合: 利用 SlopeMask 节点,根据地形的陡峭程度自动混合草地和岩石材质。平坦区域显示草地,而陡峭的悬崖侧壁则显示岩石,实现了自动化绘制,效果自然。

    • 材质属性系统: 使用 Use Material Attributes 选项配合 Make/Set Material Attributes 节点,将复杂的PBR材质输出引脚(BaseColor, Roughness, Normal等)打包成单一数据线,极大地简化了材质图表的视觉复杂性,使其更易于管理。

    • 水体系统定制: 深入到水体插件的内容文件夹,通过复制而非直接修改的方式,对默认的水体材质 M_Water_Ocean 等进行参数调整,以匹配项目所需的暗色、更真实的视觉风格。

  • 难点与解决方案:

难点 解决方案 优势/劣势
大规模地表纹理绘制耗时 导入预先在外部绘制好的图层权重图 (.png)。 优势: 极大地节省了手动绘制时间,确保了不同开发者之间结果的一致性。
劣势: 灵活性较低,重大修改需要重新导出权重图。
远景水面与近景水面不匹配 识别出远景水面由独立的 WaterZone Actor及其材质 M_Water_FarMesh 控制。
创建该材质的修改版实例,并应用与近景水面相同的参数调整。
优势: 解决了视觉上的接缝问题,保证了场景的整体性和沉浸感。
水体插件与项目配置冲突 启动时引擎提示缺少 WaterBodyCollision 配置文件条目。
根据引擎提示自动修复 DefaultEngine.ini 文件。
优势: 问题解决直接明了。
劣势: 暴露了新插件集成时可能存在的配置风险,需要注意。
手动勾勒海岸线效率低下 沿着地形的海岸线,使用样条线工具手动添加和拖动点来塑造海洋的边界。这是一个纯手工作业。 优势: 控制精确,可以实现任何想要的海岸线形状。
劣势: 极其耗时,对于大型或复杂的海岸线来说是主要的瓶颈。

5. 自我批判与重构

本章工作虽已达成目标,但仍有值得反思和优化的空间。

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

    1. 水体插件的破坏性修改风险: 直接修改插件目录中的核心资产会导致所有项目受影响且在引擎升级时可能丢失。

    2. 隐藏的系统依赖: 在新地图上测试旧的建造系统时,出现了一个空指针访问的Bug。这表明新环境可能会暴露先前系统中未发现的潜在问题。

    3. 视觉接缝问题: 远近水体材质参数不一致导致了明显的视觉边界,影响了场景一体性。

    4. 工作流的“体力活”: 绘制海岸线和河流的过程是重复且枯燥的体力劳动,是效率的瓶颈。

  • 反思与修正:

    • 问题1的修正: 严格遵循了“复制到项目,再修改”的最佳实践,将水体材质复制到项目 /Content 目录下再进行定制,保证了原插件的完整性和项目的独立性。

    • 问题2的修正: 快速定位了建造蓝图中的空指针问题,并通过添加 Is Valid 节点进行了防御性编程修复,增强了代码的健壮性。

    • 问题3的修正: 通过细致排查,定位到 WaterZone 并同步修改了其材质参数,体现了良好的问题排查和解决能力。

  • 如果重来一次,如何优化:

    • 材质结构优化: 对于 M_Landscape,可以将每个生物群落的复杂逻辑(如松林层、热带层)封装成独立的材质函数(Material Function)。主材质图表将只包含几个函数调用节点和 LandscapeLayerBlend 节点,这将使结构更加模块化,可读性和复用性达到极致。

    • 海岸线生成自动化: 探索程序化生成海岸线样条线的可能性。例如,编写一个蓝图工具,读取地形高度数据,在指定的海拔高度(如Z=0)自动生成样条线点,从而将数小时的手动工作缩短为几秒钟的计算。

    • 参数命名规范: 建立更严格的材质参数命名规范,例如 Layer_[BiomeName]_[ParameterType]_[SpecificName](如 Layer_Grass_Scalar_Tiling),在大团队协作中能有效避免混淆。

    • 前期风险评估: 在集成任何新插件(尤其是实验性插件)前,进行更充分的文档阅读和社区问题检索,预先检查和修改项目配置,避免开发过程中的中断。


1. 本章核心目标

本章节的核心目标是构建游戏内玩家之间进行交互的基础框架,增强社区感和多人协作体验。这些目标可分为显性和隐性两个层面。

  • 显性目标 (玩家体验层面)

    1. 文本聊天系统: 为玩家提供一个可以实时交流的渠道,包括面向服务器所有人的全局聊天和仅限部落成员可见的部落聊天

    2. 玩家名牌系统: 在玩家角色头部上方显示其名称部落名,方便身份识别,并根据玩家关系(是否为同部落成员)以不同颜色高亮。

    3. 近距离语音聊天: 实现一种基于地理位置的语音沟通方式,玩家按下特定按键后,只有附近的玩家才能听到其语音。

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

    1. 网络架构验证: 建立并验证一套稳健的客户端-服务器(Client-Server)通信模型,用于处理实时的、非 gameplay 核心的数据交换,如聊天消息和状态更新。

    2. 系统解耦与模块化: 通过接口(Interfaces)和事件驱动,确保社交系统(聊天、语音)与玩家角色、游戏模式(Game Mode)等核心模块低耦合,易于扩展和维护。

    3. 第三方插件集成: 成功集成并使用 Advanced SessionsSteam Sessions 插件,为语音聊天等高级网络功能提供底层支持,为后续的在线服务打下基础。

    4. UI与游戏状态管理: 实现游戏输入模式的无缝切换(例如,从“仅游戏”模式切换到“仅UI”模式以进行聊天输入),确保玩家在进行社交互动时不会影响游戏主循环的稳定性。

2. 系统与功能实现

本章主要实现了三大核心功能系统,它们与前期系统紧密交互。

系统/功能 核心实现描述 与前期系统的交互/依赖
文本聊天系统 1. UI层: W_ChatWindow作为主聊天窗口,包含用于显示消息的ScrollBox和用于输入的EditableTextBoxW_ChatMessage作为单个消息的UI模板。
2. 逻辑层: 客户端通过PlayerController将消息RPC发送至服务器。服务器(GameMode)处理消息,判断是全局还是部落消息(通过/tribe前缀区分),然后将消息RPC分发给所有相关客户端。
3. 流程: 输入(Client) -> RPC(Server) -> GameMode处理 -> RPC(Target Clients) -> UI更新(Client)
依赖: 严重依赖玩家状态(PlayerState)来获取玩家名和部落信息。部落聊天功能直接依赖于前期构建的部落系统
交互: 新的聊天窗口W_ChatWindow被集成到主HUD布局DefaultHUDLayout中。
玩家名牌系统 1. UI层: W_PlayerName作为名牌UI模板,包含显示名称和部落的Text控件,以及一个用于语音提示的Image控件。
2. 组件: 在角色蓝图中添加一个Widget Component,设置其UI类为W_PlayerName,并将其附加到角色模型的头部骨骼上,空间模式设为Screen Space
3. 触发机制: 使用一个Sphere Collision球体碰撞器。当其他玩家进入范围时触发OnBeginOverlap显示名牌,离开时触发OnEndOverlap隐藏名牌。
4. 状态显示: 名牌颜色会根据是否与本地玩家同属一个部落而改变(友好为绿色,否则为黄色)。
依赖: 同样依赖玩家状态(PlayerState)来获取显示数据(玩家名、部落名)。颜色逻辑依赖部落系统进行关系判断。
交互: 作为Widget Component直接集成在玩家角色蓝图中。
近距离语音聊天 1. 输入: 通过新的输入动作IA_VoiceChat绑定到键盘按键(B)。
2. 核心逻辑: 按下按键时,在PlayerController中执行控制台命令Toggle Speaking 1开启语音发送;松开时执行Toggle Speaking 0关闭。
3. 技术实现: 依赖Advanced Sessions插件提供的VoipTalker。需在角色蓝图中初始化VoipTalker,将其注册到PlayerState,并设置衰减(Attenuation)来实现近距离效果。
4. UI反馈: 玩家说话时,本地HUD和其头顶名牌上都会显示一个麦克风图标。
5. 配置: 需要修改项目配置文件DefaultEngine.iniDefaultGame.ini以启用语音和按键说话功能。
依赖: 功能上完全依赖Advanced SessionsSteam Sessions插件。
交互: 语音状态的UI反馈与玩家名牌系统主HUD进行了集成。

3. 关键设计思想

本章的实现体现了多种优秀的设计思想和模式,确保了系统的可维护性和扩展性。

设计思想 具体应用体现
接口驱动开发 (Interface-Driven Development) - 广泛使用蓝图接口: 创建了BPI_SurvivalGameMode, BPI_SurvivalCharacter等多个接口,用于解耦不同模块间的通信。
- 示例: 聊天窗口W_ChatWindow不需要知道PlayerController的具体实现,只需调用SendChatMessage接口消息即可。这使得更换或扩展Controller变得容易,而无需修改UI代码。
关注点分离 (Separation of Concerns) - 客户端/服务器职责分离: 严格划分了客户端和服务器的职责。客户端负责UI呈现和输入捕捉(如按下聊天键),服务器负责核心逻辑处理和状态同步(如广播聊天消息)。
- UI与逻辑分离: Widget蓝图(如W_ChatWindow)主要处理UI布局和用户交互事件,而具体的网络通信和逻辑判断则委托给PlayerController和GameMode,遵循了MVVM或类似模式的思想。
单一职责原则 (Single Responsibility Principle) - W_ChatWindow负责管理聊天窗口的整体结构。
- W_ChatMessage仅负责展示一条消息。
- PlayerController负责处理玩家输入和网络消息的路由。
- GameMode负责服务器端游戏规则的执行,如谁应该接收聊天消息。
组件化设计 (Component-Based Architecture) - 玩家名牌系统被实现为一个可复用的Widget Component
- 近距离检测逻辑被封装在一个Sphere Collision Component中。
- 这种设计使得可以轻松地将这些功能附加到任何Actor上,而不仅仅是玩家角色。

4. 核心技术点与难点

本章的实现涉及多个关键技术点,并成功解决了一些开发中的常见难题。

技术类别 关键技术点/难点描述 解决方案
网络通信 不可靠的RPC调用:如何在客户端、服务器间建立一套清晰、解耦的通信流程。 使用**蓝图接口(Blueprint Interface)**作为消息契约,结合Run on Server, Run on Client, Multicast等不同类型的RPC事件,构建了从客户端到服务器再到目标客户端的完整通信链路。
UI 动态UI生成与管理:如何在运行时根据收到的消息动态创建并添加聊天条目到滚动列表中。 W_ChatWindow中,当接收到新消息时,调用Create Widget节点创建W_ChatMessage实例,然后使用Add Child函数将其添加到ScrollBox中。
UI 聊天框自动滚动:如何确保当新消息填满聊天框时,视图能自动滚动到最底部。 ScrollBoxScroll when focus changes属性设为Instant Scroll,并在添加新消息Widget后,对该新Widget调用Set Focus函数来触发自动滚动。
输入系统 输入模式冲突:玩家在打字时,如何阻止角色移动等游戏输入,并在打完字后恢复。 通过Set Input Mode Game OnlySet Input Mode UI Only节点来动态切换输入模式。当玩家聚焦到聊天输入框时,切换为UI模式;当发送消息或失去焦点时,切换回游戏模式。
集成与配置 第三方插件和引擎配置:如何正确配置和使用Advanced Sessions插件及虚幻引擎底层语音功能。 1. 通过蓝图节点(如Create Voip Talker)调用插件功能。
2. 通过修改DefaultEngine.iniDefaultGame.ini文件,添加如[Voice], bRequiresPushToTalk等配置项,来启用引擎级的语音聊天支持。

5. 自我批判与重构

在开发过程中遇到了一些问题,同时也识别出一些可优化的设计。

类别 问题/反思 当前解决方案/修正 若重构,可采用的优化方案
Bug修复 Widget焦点问题: 新创建的聊天消息Widget无法接收焦点,导致自动滚动失效。 W_ChatMessage的类默认设置中,勾选Is Focusable属性,使其可以被聚焦。 无需重构,此为正确修复。
Bug修复 文本不换行: 过长的聊天消息会超出UI边界,影响可读性。 W_ChatMessageText控件细节面板中,启用Auto Wrap Text属性。 无需重构,此为正确修复。
设计局限 名牌更新不及时: 玩家加入或退出部落后,其头顶名牌信息(部落名、颜色)不会立即更新,需要离开再进入对方的视野范围才能刷新。 当前设计依赖Overlap事件来触发名牌的刷新,没有实现主动推送更新。 采用事件驱动更新
1. 在部落系统中,当玩家部落状态变更时(加入、退出),应在服务器上广播一个事件。
2. 关心此事件的系统(如名牌系统)监听该事件。
3. 收到事件后,服务器可以主动找到相关玩家,通过RPC强制其客户端刷新名牌UI,实现数据的实时同步。
代码结构 逻辑分散: 社交系统的相关逻辑分散在PlayerController, Character, GameMode以及多个Widget中,当功能复杂化后,追溯和维护可能变得困难。 当前结构虽然分散,但职责相对清晰。 引入社交管理器(SocialManager)
创建一个独立的子系统或Actor(如SocialManagerSubsystem),专门负责处理所有社交相关的逻辑。无论是聊天消息、部落状态更新还是语音状态,都由这个管理器统一接收、处理和分发。这将极大地提高内聚性,降低模块间的耦合度。
性能 不必要的RPC: 名牌显示逻辑链条较长(Overlap -> RPC -> Client RPC -> Get Widget -> Call Interface),可能存在优化空间。 当前实现确保了逻辑在正确的客户端上执行。 利用数据复制优化: 由于PlayerState本身就是被复制到所有客户端的,因此触发重叠的客户端可以直接从其本地的、对端的PlayerState副本中读取所需信息(名字、部落ID),并在本地直接更新名牌UI。这可以减少为了获取数据显示信息而发起的RPC调用,降低网络开销。

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) 所有作为状态机的布尔变量初始值是否正确设置。通过流程来避免这类低级但影响严重的问题。
0%