资讯动态

UE4设计模式实战:从SOLID原则到组件化架构的引擎内功

发布时间:2026/10/14 18:16:43 来源:尧图企业网站定制
1. 从UE4的构造谈面向对象原则设计模式不是面试题是引擎的“内功”做UE4开发的朋友十有八九都经历过这样一段困惑基础C语法都懂了Actor、Component也套得出来可一遇到复杂玩法系统、多人同步、大型架构重构代码就会乱成一锅粥。这时候回头看设计模式和面向对象原则才发现它不是八股文而是引擎设计者一开始就在用的底层思维。UE4本身就是一个设计模式的大型展览馆。你写一个类继承自Actor本质上就是在遵守里氏替换规则你挂一个UActorComponent上去就是在实践组合与单一职责。甚至UObject的反射系统、UCLASS的宏体系背后都是依赖倒置和开闭原则的体现。这篇文章不打算把23种设计模式逐个念一遍而是结合引擎的实际机制聊聊几个真正与UE4强相关的原则和模式SOLID怎么投射到引擎API上、创建型模式怎么处理UObject的new与生命周期、行为型模式怎么解决AI与UI的解耦。我会尽量用自己在实际项目里的做法和踩过的坑来展开看完至少能让你下次重构某块玩法系统时多出几条清晰的路径。1.1 开闭与依赖倒置UCLASS和接口的微妙关系开闭原则讲的是“对扩展开放对修改关闭”。这话听着像废话但UE4里有无处不在的体现。最典型的就是UCLASS的Blueprintable标记——父类写好逻辑子类或者蓝图只负责扩展细节父类模块不用跟着改。你在C里定义好一个基类技能标记BlueprintNativeEvent或BlueprintImplementableEvent蓝图去实现具体的表现层逻辑C只保留规则框架。这就是一种开闭。依赖倒置的关系则隐蔽一些。它说的是上层模块不应该依赖下层模块的具体实现而是依赖抽象。UE4里常见到很多人直接在一个GameMode里引用具体类型的角色类比如“AWarriorCharacter”然后在构造函数里硬编码。一旦要支持法师、刺客、弓手GameMode就得改好几个地方。正确做法是把具体类型抛到BP里配置C只保存一个TSubclassOf 的引用。引擎在运行时通过反射把配置好的子类实例化出来C代码永远不知道你到底是哪一个职业。这就是依赖倒置在UObject生态里的标准形态。这里要补充一个重要认知UE4的继承体系本身允许派生类覆盖虚函数、BlueprintNativeEvent、BlueprintImplementableEvent这套机制就是在引导你遵守开闭原则。如果你新加一个功能要大面积改动基类、改动已有子类的虚函数实现那就说明设计没到位扩展性差。我在项目里定过一个基础建议凡是逻辑可能会被蓝图覆盖的入口先想清楚是不是该声明成BlueprintNativeEvent而不是闷头写一个纯C的虚函数。虽然这对C和蓝图的调用权限有影响但本质上就是在设计“扩展点”。1.2 单一职责Component化思维是UE4给开发者的一条安全绳单一职责原则意思是“一个类只负责一件事”。放到UE4里最明显的语言就是别在一个Actor里把所有事情干完。很多人刚上手时喜欢做一个巨大的“主角”类动作、属性、背包、对话、音效、技能全部塞进一个类里。前两周开发速度飞快第三周开始改动作的时候不小心碰坏了技能加背包功能时发现音效系统又崩了。原因再简单不过——职责混杂耦合太深。UE4原生给出的解法就是ActorComponent。引擎把自己的组件体系设计得极其完善移动要靠UCharacterMovementComponent渲染靠UPrimitiveComponent碰撞靠UCapsuleComponent音效靠UAudioComponent。你的主角类本质上就变成了一个容器真正干活的是挂在上面的各个组件。顺着这个思路往下走你会发现自定义玩法逻辑也应该拆成组件。我个人的实践是一个角色类里不到万不得已不写超过300行的业务逻辑。属性面板拆成自定义组件技能逻辑拆成技能仓库组件连摄像机手感也可以拆成专用的ControllerComponent。这样做的好处有三个一是代码复用变强任何Actor想有这组能力挂上组件就行二是调试定位变快某个模块出问题直接看对应的组件类三是更容易在层与层之间设边界比如客户端显示逻辑与服务器规则逻辑可以被强制分开。1.3 里氏替换与接口隔离继承党和接口党都绕不过的坑里氏替换原则说得数学一点就是“子类必须能够替换父类并且系统行为一致”。在UE4里最常见的问题是你写了一个ATrap陷阱基类派生了一个AFlameTrap火焰陷阱然后为了让火焰陷阱有额外功能你重写了父类某个虚函数但重写后的行为会让原先调用方预期失效。比如父类有个状态变量表示“是否激活”子类重写激活函数时把“冷却”“销毁”也包了进去结果外部逻辑只判断“激活”状态时判断错乱。这就是里氏替换被违背它往往不是编译期报错而是运行时表现异常。接口隔离原则在UE4里对应的是UInterface。很多新手习惯把所有通用能力塞进一个大接口里比如IInteraction就放Interact、GetPromptText、CanInteract、GetHighlightTag。看着方便实际上当你只需要一个“能被高亮”的物体时它被迫实现一堆用不上的方法。更好的做法是把接口拆细IInteractable只负责“能交互”IHighlightable只负责“能被高亮”IUseable负责“能使用”。注意UInterface本身是不带状态的它是纯虚的声明集合所以很适合做能力标记和行为入口。关于接口的有个经验一定要记住UInterface的默认实现是托底但它并不能像纯C接口那样方便地提供默认行为如果某个函数几乎每个实现类都要重复写那就不适合进接口而是应该放到基类里。接口应该保持“又小又专用”否则就是一种隐性耦合。这一节最后想强调一句继承表达“是什么”接口表达“能做什么”组件表达“有什么”。三者各有用处不要一根筋。2. 创建型模式UE4里new的哲学与对象生产的门道创建型模式关注的是“怎么把对象造出来”这件事。听起来没什么技术含量但在UE4里由于UObject必须走反射和GC系统不能直接new创建对象的方式就被引擎硬性规定好几件套NewObject、SpawnActor、ConstructObject、工厂类。理解这些API的本质比背设计模式的定义有意义得多。2.1 工厂模式的身姿从SpawnActor到UFactory游戏开发里最廉价也最常见的工厂函数就是SpawnActor。UWorld::SpawnActor接收一个TSubclassOf 和参数它内部会根据类的ClassDefaultObject去分配内存、触发构造函数、移交生命周期。你写代码时不用关心Actor是怎么分配内存的、怎么注册进场景的只需要告诉引擎“你要什么类型的Actor”。这本身就是工厂模式。但SpawnActor只是最底层的一个生产动作。当你有一套复杂的生成策略时比如“某个波次会混合生成三种怪物各自还有权重”直接在BP里让逻辑节点去一个个SpawnActor就非常凌乱。这时候手动写一个工厂类更合理。比如我做过一套“敌人配置工厂”给一个抽象的FEnemySpawnData里面包含怪物类型、等级倍率、数量权重、出生点位集合工厂类的Build接口拿这个Data回去内部用随机权重挑出具体的TSubclassOf、计算好等级与属性再调用SpawnActor返回生成的数组。好处是整个生成逻辑集中一处后续加新敌人只需要改配置不用动生成逻辑。还要提一下UFactory。它是编辑器扩展里常见的类用于自定义资源的导入与创建。当我们用C扩展编辑器菜单让美术可以一键“创建某个复合资源”时UFactory的FactoryCreateNew就是正儿八经的工厂方法。不过普通玩家项目用到它的机会不多知道这个工具类的位置即可不用过度研究。2.2 单例的诱惑与USubsystem的救赎单例模式是游戏开发里的双刃剑。全局唯一对象确实方便“随便调、哪里都能喊”但它的后果是隐藏依赖你根本看不出某个功能依赖了谁代码执行顺序一旦悄悄改掉一切就崩。UE4早期版本里常见的一种做法是“把某个全局管理器Actor放进Level然后谁要数据都去找它”还有用StaticClass()-GetDefaultObject这种事但那是防君子不防小人的做法蓝图或代码里任一处设置数据都可能污染全局。UE4的现代官方方案是USubsystem。它按照生命周期分成UGameInstanceSubsystem、UWorldSubsystem、UEditorSubsystem既具备单例“全局可访问唯一存在”的能力又让引擎负责初始化与释放。比如你在UGameInstanceSubsystem里保存跨关卡的玩家存档数据UWorldSubsystem里管理当前关卡每日刷新列表用起来比传统单例安全得多。使用USubsystem有个细节必须记得不要在构造函数里初始化需要场景上下文的数据。子系统的初始化依赖引擎阶段有些数据在构造函数时根本还没准备好。我见过有同学把SaveGame的加载放在UGameInstanceSubsystem构造函数里结果SaveGame对象都还没有有效World指针读档直接失败。正确做法是重写Initialize函数把加载动作放到里面而且要考虑容错初始化时如果本地没有存档就返回默认值。话说回来传统的C单例模式也不是完全没用。极少情况下我们做一些纯算法库对象、工具类不需要继承UObject这时用一个静态局部变量实现单例非常简单稳妥。我自己写随机数池、网格路径计算辅助类时就偶尔用到这种静态单例。核心区别在于涉及UObject生命周期的逻辑永远要交给引擎管纯计算逻辑才适合自己管。2.3 对象池子弹打着打着就反复分配帧率能被拖垮创建型模式里还有一类应用极广的对象池模式。游戏里最典型的痛点是子弹、粒子特效、飘字伤害这些高频生成和销毁的东西。你用SpawnActor和DestroyActor反复创建、销毁几百个对象引擎的GC压力、内存碎片、初始化成本全都会被放大。对象池的基本思路是一开始创建好一批对象取用时从空闲池里拿用完放回空闲池而不是销毁。UE4里实现对象池有几种路线一个是Actor池化把一组AOnHitEffectActor预先Spawn好SetActive(false)后隐藏在场景里另一个是UObject池化用NewObject创建一批UObject缓冲用完Reset后回收到池中。我个人更喜欢后者因为Actor的Spawn、销毁成本太高而且放进场景里还有Tick开销。在做对象池时有个必须小心的技术点池里对象的状态重置。如果Get接口返回一个被上一次使用污染的对象没清干净就交给下一个请求很容易出现“子弹带着前一个玩家的颜色”这种bug。所以对象池内需要一个彻底Reset的接口把Transform、速度、材质、碰撞响应、归属Actor全部复位。宁可Reset慢一点也不能返回脏对象。还有就是池的大小不是越大越好每次扩容都有性能开销配合观察场景里同屏子弹的上限来决定容量更科学。3. 结构型模式UE4组件的拼装艺术结构型模式关注类与对象的组装方式。UE4里最经典的结构型范式就是Actor与Component的组合关系此外还有装饰器模式在外观Buff、能力增强方面的应用以及外观模式对复杂子系统封装的妙用。3.1 组合模式整个场景就是一个巨型组合树组合模式的定义是“将对象组合成树形结构以表示部分-整体的层次关系让客户端统一调用”。UE4里的世界就是这样一个树Level里有ActorActor里有ComponentComponent里还能嵌套SceneComponent挂载子节点。渲染、物理、碰撞、变换层级全都在这个树上完成。开发者最容易利用到组合模式的地方是“复合物体”的设计。比如做一个“载具”Actor它由车身、四轮、炮塔、灯组成其中炮塔上又挂了一个观察相机组件。从外部逻辑看你只需要操作载具这个Actor的移动与转向它内部的所有子组件会自己跟随、旋转。这就是组合模式带来的封装——外部不需要知道复杂的细节只管调用顶层对象子节点的行为由对象内部自我管理。这套设计一旦尝到甜头你就不会再痴迷于巨大继承树了。以前做过一个项目把近战怪、远程怪、Boss都继承自一个ABaseEnemy结果Boss多出一个二阶段变身、会召唤小怪、还会放全屏技能这些逻辑越加越痛苦基类越来越肥。后来我按组件化重构把“Boss变身”“召唤”“范围技能”都拆成独立的LogicComponent敌人Actor按需挂载基类只保留共性属性与挂载容器。代码立刻清爽扩展新模式也变得简单只要装配不同组件即可。3.2 装饰器模式叠加Buff与GAS的底层逻辑装饰器模式允许你在不改变原有类代码的情况下动态给对象添加职责。放到游戏里就是Buff叠加、装备词条加成、时装特效等。你不想把每种Buff分别写进一个巨大角色类而是希望有一个可堆叠的修饰层。UE4里最正统的落地是GAS的GameplayEffect与GameplayEffectSpec尤其是延迟修饰、百分比加成的实现。不过GAS这套系统太重很多小项目并不想引一整套框架。这时手写一个轻量装饰体系也常见。我给出一个简易的“属性修饰器”思路角色身上挂一个UAttributeComponent里面维护基础属性攻击力、移速、防御。又建一个UModifierComponent管理一组FModifier实例每个实例有类型加值/百分比/覆盖、数值、持续时间、优先级。当UModifierComponent发生变化时它会告诉UAttributeComponent重新聚合计算结果。聚合函数是把所有修饰器按优先级排序依次计算。这样角色的基类代码完全不需要知道“中毒加攻击力”还是“穿装备移速提升”每种修饰都以装饰器形式独立存在。这里要提醒一个常见错误直接在角色类上SetFinalValue。比如攻击力被Buff暂时改成200等Buff一消失你还是需要回到100。因此基类里永远要保存“未修饰基础值”和“修饰后计算值”两个维度每次重新计算都从基础值开始。我在实际项目里因为图省事直接改最终值导致Buff消失后角色属性变不回原样查了很多天才发现这个逻辑错误现在写属性系统一定会保留基础层修饰层只做动态计算。3.3 外观模式GameMode这个“前台接待”外观模式的核心是给一堆复杂类提供统一的高层接口。最直观的UE4例子就是GameMode与GameState。某关卡里涉及库存系统、任务系统、天气系统、NPC状态如果UI和玩家控制器直接挨个调用各自接口依赖就会爆炸。GameMode在服务器权威模式下作为一个行为协调调度层统一暴露玩法规则的入口比如StartMatch、PlayRound、OnPlayerDied。对外的具体来讲UI想显示倒计时只需要从GameState拿一个同步过来的TimeSeconds而不需要知道倒计时后端是怎么计算的。做项目时我习惯把“玩法规则”的故事都浓缩在GameMode里。它不是什么都自己算而是协调子系统GameMode在RoundBegin的时候问一下关卡配置管理器要刷怪表再让敌人工厂去生成敌人顺便给GameState一个开始播报的信号。这样一来新同事接手项目时只要看GameMode这一层就能大概知道一局游戏怎么运转。这比翻遍十几个类才猜到流程要高效得多。外观模式也告诉我们接口不是越细越散越好要给“高频场景”提供一站式入口。但外观层不要做太重它不应该承载具体实现而是调度与委派。过度把UI的刷新逻辑塞给GameMode它很快也会变成一个巨型“上帝类”。4. 行为型模式让AI、UI和玩家操作都走向解耦行为型模式处理的是对象之间的职责分配和通信方式。这恰好是UE4里最容易写得乱的一块UI要刷新角色属性、技能要通知成就系统、AI要感知玩家状态并做决策。只要通信方式一乱项目后期就变成彼此你调用我、我调用你的蜘蛛网。4.1 观察者模式Dynamic Multicast Delegate是UE4的黄金事件总线观察者模式就是要解决“一对多通知”的问题。UE4把这个能力直接内置成Dynamic Multicast Delegate也就是动态多播委托。它比普通的C委托多了反射支持和蓝图绑定的能力因此成为跨C和蓝图之间通信最常用的工具。常见的场景是角色血量变化时UI血条、伤害飘字、成就系统、AI感知模块都要知道。如果你的主角色在SetHealth里直接调用四五个其他类的成员函数耦合度立刻飙升。正确做法是角色只声明一个OnHealthChanged动态多播委托在属性变化时Broadcast各方系统各自BindEvent去响应。角色自己完全不知道有谁在听新增一个系统时也不需要改角色代码。用Dynamic Multicast Delegate有几个坑不得不提。第一个是试图把Lambda作为动态委托的回调动态委托的签名要求在C里面搭配相应的函数指针或显式绑定函数Lambda需要存成静态函数或使用BaseDelegate相关机制才能支持直接用很容易踩坑。第二个更关键绑定与解绑的成对性问题。我见过很多项目里在重叠事件、角色初始化时Bind结果角色被销毁了某个Manager还持有那个失效对象的函数引用引擎在Broadcast时发现访问违规线上版本直接就闪退了。用动态委托时强烈建议在BeginDestroy、EndPlay里同样做Unbind。如果你的Delegate是挂在Actor自身的动态多播属性上Actor销毁时委托通常也会随对象一起释放但如果你拿到的是另一个Manager的Delegate去Bind自己的函数则必须在EndPlay里主动Unbind。另外一个实用建议是动态多播委托在蓝图里显示为Event Dispatcher你可以直接在蓝图细节面板里绑定也可以用Bind Event节点。但如果你在C里声明了一个BlueprintAssignable的委托蓝图绑定后就自动拥有了可视化的接线能力。这个特性做机制系统非常顺手尤其要多利用“携带参数”的多播让UI一次性拿到新旧值。4.2 策略模式行为树节点与武器切换写法的通用套路策略模式定义了一系列可互换的算法让客户端可以在运行时替换策略而不影响调用方。游戏开发里最常见的案例就是AI决策同样是追击动作A怪是直线跑B怪是绕后包抄C怪是潜行追踪。你不希望把追击算法用一大堆if else写在同一个“追击”节点里而是想动态切换追击策略。UE4行为树提供的就是一种天然的策略容器Behavior Tree Task。你可以把“追击”定义成一个TaskTask内部读取AI的Blackboard上的某个策略枚举再调用对应的追击函数。或者更面向对象一点把策略封装成UObject子类用TInstancedObject存储。策略模式在玩家角色的应用就更常见了。我在一个射击项目里做武器切换角色持有一把主武器、一把副武器、若干投掷物攻击输入按下时不直接去执行某一种武器的射击代码而是先问“当前激活的武器策略”是什么再由策略去执行射击。每把武器实际上就是某个IWeaponStrategy接口的实现比如狙击枪有一个蓄力模块冲锋枪有连射模块霰弹枪有散射模块。换枪只是把激活策略指针换掉角色本身的输入映射不用改动画层的切换由策略内部的事件通知来完成。这样扩加新武器的时候就不需要动角色基类和输入代码非常舒服。要注意的是策略模式不要做成“单独一个大策略类管所有事情”。策略粒度太小会导致策略数量爆炸策略粒度太大又会导致策略内部再次出现大量分支。我的经验是每个策略只解决“一类条件下的一种行为细节”比如武器策略解决射击反馈不需要同时管理换弹动画和弹药数这些可以放在另外的独立系统里。4.3 状态模式角色手感优化的隐形神器角色控制是UE4开发绕不开的领域从跳跃、下落、跑动、翻滚、受伤到死亡再到各种技能释放状态。状态模式在这种情况下特别合适把每个可明确定义的状态做成分离的类角色只保存一个“当前状态”由状态类决定做什么、何时切换。最常见的一种做法是在角色类里放一堆bool是否跳跃、是否翻滚、是否技能中。当bool一多逻辑判断就变成一坨if(跳跃 !翻滚 技能NONE)。这种代码不光难维护改一个bool就可能引发冲突。状态模式则让你把行为限制在状态内部。比如“地面移动状态下禁止释放需要锁定的技能”“受击状态下只允许受击打断优先级最高的技能释放”。每个状态都清楚自己是不是合法入口和出口再配合一个状态机管理器统一负责切换。在UE4里落地状态模式我推荐两种方案。轻量方案是用enum加switch配合一个Tick更新函数适合状态少、逻辑简单的功能。重度方案是让每个状态继承自UObject基类比如BStateBase里面有BeginState/UpdateState/EndState虚函数角色身上挂一个BFsmComponent来管理状态栈这种方式更适合复杂的战斗动作和AI层级。关于状态机有个很重要的性能教训别让状态Update逻辑直接去轮询一大堆外部条件。正确做法是让外部系统通过事件或交互结果来触发切换判断。一个具体的例子是“倒地状态”你不该每帧检测角色是否被炸弹炸到而是炸弹爆炸后调用受力判定如果判定击中且力度超过阈值就让FSM尝试切换ToKnockDownState。状态机内部是反应式而非轮询式的状态切换开销才会低逻辑也才清晰。5. 实操搭建一个“模式友好”的敌人系统讲了这么多原则和模式下面用一个具体的例子把前面抽象的东西串起来。假设我们要做一关卡式射击游戏里的敌人系统要求敌人包含普通兵、精英兵、BossBoss能召唤小怪、能换阶段、还会在不同阶段使用不同技能。如果不用任何模式直接写代码很容易膨胀。现在我按模式友好的思路拆解一个可落地的最简设计。5.1 从需求到类图想清楚每个类该负责什么先明确目标敌人系统既要扩展方便又不能让“想加一个新敌人”变成一场灾难。需求拆出来是四个维度敌人的通用属性血量、移速、攻击力、敌人行为差异巡逻、追击、放技能、敌人触发的事件死亡通知、掉宝、成就、敌人的外观与动画表现。按照单一职责去分配就会得到这样几个基础类AEnemyBase继承自ACharacter保存通用属性、挂载通用组件碰撞、网格、生命组件。UEnemyAttributeComponent血量和属性容器负责数值计算对外发OnHealthChanged、OnDied委托。UEnemyAIControllerAI控制器管理感知、Blackboard和BehaviorTree引用。UEnemySkillSet存放该敌人可用的技能生产工厂或技能策略列表。行为树Task与Decorator负责AI具体行为策略。外观与动画用AnimationBlueprint尽量不写进C。这样一分加新敌人时大多数情况不需要改AEnemyBase只需要调整数据资产、挂载不同技能集合、替换BehaviorTree。5.2 关键类实现工厂、装饰器、观察者三合一生成侧我做了一个简单的怪物流水线。配置层用DataAsset里面是数组模板每个模板Id、敌人类型TSubclassOf 、等级倍率、攻击力、技能组Tag。敌人工厂类UEnemyFactory提供静态生成接口SpawnEnemyByTemplate它的内部职责读取配置、动态生成技能集合并挂载、SpawnActor、初始化属性组件。这里的工厂模式好处是生成流程的复杂度从BP里转移到了C侧蓝图只需把DataAsset拖进引用所有生成逻辑统一可见可调试。属性侧用装饰器思想做一个简易Buff组件。每个Buff是一个UObject派生类有Duration、StackStrategy、ModifierType和CoolDown。属性组件维护一个BaseAttribute每次Buff变化后重算FinalAttribute。这样Boss进入二阶段时只要挂一个“阶段强化Buff”攻击力和体型等级就能被统一处理不需要为二阶段单独写一套属性改动。通信侧统一用动态多播委托。AEnemyBase的AttributeComponent一旦OnDied触发不只是播放死亡动画还会通过一个局部的EventBus可以用UGameInstanceSubsystem做管理向外广播。成就系统、掉宝管理器、击杀计数器都订阅这个事件。新增一个“挑战任务系统”完全不需要改敌人代码只需要也订阅事件即可。这就是观察者模式的实际好处。5.3 踩坑与思考这套设计带来哪些得失这套架构在一开始会显得比直接码业务逻辑更费事因为你得多写几个类、多定义几个委托。但项目一旦过了一个月的增长速度优势就很明显想要新增敌人类型只需要做一个新蓝图、配一个DataAsset、挂上不同的技能集和行为树不需要动原有C基类想要加新Buff类型只需要新写一个UBuff_XXX注册到属性组件的创建工厂即可也不需要改角色的每一处逻辑。踩过的坑也不能略过。最大的一个坑是AEnemyBase的组件拆分之后外观和动画想拿到最新属性反而麻烦了。原本一个巨型基类可以直接读到所有成员变量拆完组件后动画蓝图需要跨组件获取数据。解决办法是把最常用的有限属性写进Character类的虚接口比如GetAttributeComponent()、GetSkillSet()动画代码只通过这两个接口获取引用。这实际上就是封装与外观模式的回归——拆分的另一边需要提供一个统一的聚合入口而不是让每个子系统都裸奔。6. 常见设计与实现问题速查最后把我在UE4项目里反反复复看到的、自己踩过的问题整理成一份速查都是跟设计模式在UE4里的落地强相关的看完能帮你少走弯路。6.1 动态委托绑定膨胀与生命周期崩溃这是UE4中使用观察者模式时最频繁的bug来源。常见症状游戏运行一段时间后控制台报“Function is not bound”或“Accessed None trying to call function”甚至崩溃。原因往往是A对象Bind了B对象的委托B销毁了A没有合适时机解绑。复盘几乎每次都能归结为“在Init阶段有Bind但在EndPlay阶段没有对应的Unbind”。解决方案也简单总结成两条铁律。一凡是绑定到别的对象委托上的一律在本对象的EndPlay里Unbind。二如果只监听本对象的委托比如角色自己的AttributeComponent事件因为委托对象和监听对象生命周期一致可以省略Unbind但你仍然要在BeginDestroy里对子组件做析构清理。此外强烈推荐利用WeakObjectPtr或者TWeakObjectPtr做委托绑定的持有模式至少在C侧、Mind the binding顺序上给自己留一层兜底。6.2 过度设计与性能焦虑很多接触设计模式不久的同学容易反向误伤把每个小功能都拆成一堆策略类和装饰器。结果本来一个50行就能写明白的函数被拆成6个类代码跳转链极其复杂新人根本不敢改。设计模式的核心是解决“变化点”没有明显变化需求的功能不必硬套模式。另一个误区是觉得“模式一定会带来性能开销”。其实UE4里绝大部分设计模式落地的成本都被引擎优化得比较低。动态多播Delegate的一次广播通常只有微秒级别行为树节点的一个Task查询也比常规的函数调用多不了多少。反倒是对象池和GC处理真的需要考虑性能所以我的建议是把性能分析放在Hot Path上比如每帧都执行的Tick里不要频繁创建UObject不要在Update里反复绑定委托。架构上优先考虑可读性和扩展性性能问题定位后再优化。6.3 继承爆炸接口、组件、继承如何搭配最后聊一个经典问题该用继承还是该用接口还是该用组件。其实每一层表达的东西不一样。继承表达的是“是什么”的关系适合稳定的类型树。比如“所有敌人都是ACharacterBase派生的”这很自然。接口表达的是“能力标记”适合跨类型的行为统一。比如UI交互系统希望“所有可以被射线检测并显示交互提示的物体”都实现IInteractable但它不关心这个物体是敌人、箱子还是门。组件表达的是“能力组合”适合以能力单位做逻辑复用比如“可受伤”“可被击飞”“可掉落物品”。我个人给一个建议优先试着用组件处理行为复用用接口处理跨类型调用继承只用于“不会轻易变化”的抽象层级。如果某个功能既不适合接口标记又不好做成组件那就多考虑考虑是不是设计思路本身有问题。这样能最大程度避免类的数量爆炸也能让系统的扩展保持在可控水平。做UE4开发的这些年我的体会是新手阶段容易迷恋写法层面的技巧老手阶段反而更关注对象之间的职责边界。设计模式和面向对象原则本质上不是在教你怎么写代码而是在教你怎么划分边界。UE4引擎本身已经把很多边界处理得非常好你需要做的是顺着它的机制走而不是逆着它自己硬造一套类体系。每次接手新模块的时候先问三个问题谁负责生成谁负责数据谁负责通知想清楚这三个问题你的代码结构就基本稳了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑