UE 面试题库 · 整理版

第15章 设计模式与架构

本章共 31 道题,覆盖:经典设计模式(单例/Subsystem、观察者/委托、工厂、对象池、策略、命令、状态、组件、组合、装饰器、代理、适配器、迭代器、模板方法、外观、建造者、原型、抽象工厂)、游戏架构模式(ECS/Mass Entity、MVVM、事件总线、服务定位器、数据驱动、分层架构)、依赖注入、GAS 技能系统、物品系统、插件化框架、反模式与 SOLID 原则。

一、单例模式与 Subsystem

1.1 请解释 UE Subsystem 如何作为服务定位器模式,并设计一个可扩展的服务框架。

★★ 中等

考点: 服务定位器模式、依赖注入、模块化

参考答案:

  1. 服务定位器模式:提供一个全局访问点获取服务,不暴露服务的具体实现与生命周期。UE 的 Subsystem 系统天然就是服务定位器:GetGameInstance()->GetSubsystem<UMyService>(),无需关心创建时机与生命周期。
  2. Subsystem 五类生命周期:EngineSubsystem / EditorSubsystem / GameInstanceSubsystem / WorldSubsystem / LocalPlayerSubsystem,保证单实例、随宿主自动创建销毁。
  3. 可扩展服务框架的核心:定义 IGameService 接口(GetServiceName / Startup / Shutdown / IsReady),具体服务继承 UGameInstanceSubsystem 并实现接口,通过 TObjectIterator<UClass> 自动发现注册。
  4. 关键代码:
UINTERFACE(MinimalAPI)
class UGameService : public UInterface { GENERATED_BODY() };

class IGameService
{
    GENERATED_BODY()
public:
    virtual FName GetServiceName() const = 0;
    virtual void Startup() {}
    virtual void Shutdown() {}
    virtual bool IsReady() const { return true; }
};

// 具体服务:继承 Subsystem + 实现接口
UCLASS()
class USaveService : public UGameInstanceSubsystem, public IGameService
{
    GENERATED_BODY()
public:
    virtual FName GetServiceName() const override { return FName("SaveService"); }
    UFUNCTION(BlueprintCallable) bool Save(const FString& Slot, const FString& Data);
    UFUNCTION(BlueprintCallable) bool Load(const FString& Slot, FString& OutData);
};

// 服务定位器:自动发现并注册服务
UCLASS()
class UServiceLocator : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public:
    template<typename T> T* GetService() const
    {
        static_assert(TIsDerivedFrom<T, UGameInstanceSubsystem>::IsDerived, "...");
        return GetGameInstance()->GetSubsystem<T>();
    }
    void DiscoverServices()
    {
        for (TObjectIterator<UClass> It; It; ++It)
        {
            if (It->IsChildOf(UGameInstanceSubsystem::StaticClass()) &&
                It->GetClass()->ImplementsInterface(UGameService::StaticClass()))
            {
                UGameInstanceSubsystem* Sub = GetGameInstance()->GetSubsystem(*It);
                TScriptInterface<IGameService> Service;
                Service.SetObject(Sub); Service.SetInterface(Sub);
                RegisteredServices.Add(IGameService::Execute_GetServiceName(Sub), Service);
                IGameService::Execute_Startup(Sub);
            }
        }
    }
private:
    TMap<FName, TScriptInterface<IGameService>> RegisteredServices;
};
  1. 易错点/面试官提示:理解 ShouldCreateSubsystem() 条件性注册、InitializeDependency() 声明服务间依赖;不要过度封装——UE 原生 GetSubsystem<T>() 已经足够好,服务定位器框架仅在需要按名称/条件路由时才有必要。

二、观察者模式与事件通信

2.1 请详细对比 UE 中单播委托、多播委托、动态委托和事件分发器的区别与适用场景。

★★ 中等

考点: 观察者模式、UE 委托系统、性能与功能权衡

参考答案:

  1. 委托全景:单播(1对1)→ 多播(1对N)→ 动态单播(可序列化、蓝图绑定)→ 动态多播(蓝图事件分发器底层)→ Event(蓝图中暴露的动态多播)。
  2. 核心区别:
特性 单播 多播 动态单播 动态多播
绑定数量 1 N 1 N
蓝图可用 ❌ ❌ ✅ ✅
序列化 ❌ ❌ ✅ ✅
性能 最快 快 较慢 较慢
返回值 ✅ ❌ ✅ ❌
绑定 Lambda ✅ ✅ ❌ ❌
  1. 关键代码:
DECLARE_DELEGATE_OneParam(FOnHealthChanged, float);                 // 单播
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnDamageReceived, AActor*, float); // 多播
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChanged, int32, NewScore); // 动态多播

UPROPERTY(BlueprintAssignable) FOnScoreChanged OnScoreChanged;
FOnHealthChanged OnHealthChanged;
FOnDamageReceived OnDamageReceived;

// 单播:ExecuteIfBound;多播/动态多播:Broadcast
OnHealthChanged.ExecuteIfBound(Health);
OnDamageReceived.Broadcast(this, DamageAmount);
OnScoreChanged.Broadcast(NewScore);

// 绑定安全性
OnHealthChanged.BindUObject(this, &AMyGameMode::HandleHealthChanged);   // GC 安全
OnHealthChanged.BindLambda([](float H) { /* ... */ });                  // 不跟随 GC
OnDamageReceived.AddUObject(this, &AMyGameMode::HandleDamage);          // GC 安全
OnDamageReceived.AddWeakLambda(this, [this](AActor* T, float D) { });   // 悬垂安全
OnScoreChanged.AddDynamic(this, &AMyGameMode::HandleScoreChanged);      // 需 UFUNCTION
  1. 绑定方式安全性对比:BindUObject/AddUObject、AddWeakLambda、AddDynamic 自动失效;BindLambda/AddLambda、BindRaw/AddRaw 不跟随 GC,对象销毁后可能崩溃,需手动解绑。
  2. 选择指南:需要蓝图绑定 → 动态多播(BlueprintAssignable);需要返回值 → 单播;否则 → 多播。
  3. 易错点:动态委托慢的原因是基于函数名字符串查找而非函数指针调用;AddWeakLambda 是避免悬垂引用的最佳实践;FDelegateHandle 用于精确解绑;多线程下注意委托线程安全。

2.2 请设计一个基于事件总线的全局事件系统,实现模块间解耦通信。

★★★ 困难

考点: 事件总线模式、模块解耦、全局通信

参考答案:

  1. 设计目标:发布/订阅解耦(发送方不需要知道接收方);类型安全(编译期检查);生命周期安全(订阅者销毁后自动解绑);支持优先级;支持事件消费(阻止后续传播)。
  2. 核心思路:事件基类 FGameEvent(带 Timestamp 与 bConsumed);UWorldSubsystem 作为事件总线;以事件类型名(TEvent::StaticStruct()->GetFName())为键存储订阅列表;模板 Subscribe<TEvent> / Publish<TEvent> 保证类型安全。
  3. 关键代码:
USTRUCT(BlueprintType)
struct FGameEvent { GENERATED_BODY(); float Timestamp = 0.f; bool bConsumed = false;
    void Consume() { bConsumed = true; } bool IsConsumed() const { return bConsumed; } };

USTRUCT(BlueprintType)
struct FDamageEvent : public FGameEvent { GENERATED_BODY();
    UPROPERTY() AActor* DamageTarget; UPROPERTY() float DamageAmount; };

UCLASS()
class UEventBus : public UWorldSubsystem
{
    GENERATED_BODY()
public:
    template<typename TEvent>
    void Subscribe(UObject* Subscriber, TFunction<void(const TEvent&)> Callback, int32 Priority = 0)
    {
        FName EventName = TEvent::StaticStruct()->GetFName();
        FEventSubscription Sub;
        Sub.Priority = Priority; Sub.Subscriber = Subscriber;
        Sub.Handler = FEventHandler::CreateLambda([Callback](const FGameEvent& E)
        { Callback(static_cast<const TEvent&>(E)); });
        Subscriptions.FindOrAdd(EventName).Add(Sub);
        // 按优先级降序排序,高优先级先执行
    }
    template<typename TEvent>
    void Publish(const TEvent& Event)
    {
        FName EventName = TEvent::StaticStruct()->GetFName();
        // 复制订阅列表(避免回调中修改)、跳过失效订阅者、检查 IsConsumed 后执行
    }
private:
    TMap<FName, TArray<FEventSubscription>> Subscriptions;
};
  1. 使用示例:伤害系统 ApplyDamage 后 EventBus->Publish(FDamageEvent);成就系统 Subscribe<FDamageEvent>(this, lambda, 100)(高优先级)、音效系统低优先级订阅。
  2. 陷阱/面试官提示:过度使用导致逻辑不可追踪("谁在监听"难以回答);高频事件(每帧位置更新)不适合事件总线;调试需要事件日志。UE5 官方实现为 GameplayMessageSubsystem(Lyra 使用),可用 FGameplayTag 作为路由键。

三、工厂模式

3.1 请解释 UE 中工厂模式的三种实现方式,并说明各自的适用场景。

★★ 中等

考点: 工厂模式、SpawnActor、NewObject、对象初始化

参考答案:

  1. 核心意图:封装对象创建过程,让调用方只依赖基类/接口而非具体类。
  2. 三种工厂方式:
方式 创建对象类型 生命周期管理 适用场景
SpawnActor AActor 派生 UWorld 管理 场景实体(角色、道具、特效)
NewObject UObject 派生 Outer/GC 管理 数据对象、子系统、组件
DataTable/DataAsset 驱动 配置定义 数据管理 批量、数据驱动创建
  1. 关键代码:
// 1. SpawnActor 工厂(带模板)
template<typename T>
T* SpawnGameActor(UWorld* World, TSubclassOf<T> Class, const FTransform& Transform)
{
    static_assert(TIsDerivedFrom<T, AActor>::IsDerived, "T must be AActor");
    FActorSpawnParameters Params;
    Params.Owner = Owner;
    Params.SpawnCollisionHandlingOverride =
        ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
    return World->SpawnActor<T>(Class, &Transform, Params);
}

// 2. NewObject 工厂
UMyObject* Obj = NewObject<UMyObject>(GetTransientPackage(), UMyObject::StaticClass());

// 3. DataTable 数据驱动工厂
FWeaponTableRow* Row = WeaponTable->FindRow<FWeaponTableRow>(WeaponID, TEXT("CreateWeapon"));
AWeapon* Weapon = World->SpawnActor<AWeapon>(Row->WeaponClass, &SpawnTransform, Params);
Weapon->SetWeaponData(*Row);
  1. 本质差异(面试官点评):
    • SpawnActor 触发完整 Actor 初始化流水线(PostSpawnInitialize → PostActorCreated → BeginPlay);
    • NewObject 只是构造+注册到 GC,没有 BeginPlay 概念;
    • DataTable 工厂是数据驱动思想——把"创建什么"与"如何创建"解耦。
  2. 延伸:FActorSpawnParameters::DeferConstruction 延迟构造;FObjectInitializer 可在构造阶段替换默认子对象;对象池是工厂模式的性能优化变体。

四、对象池模式

4.1 请设计并实现一个高性能的 Actor 对象池系统。

★★★ 困难

考点: 对象池模式、性能优化、内存管理

参考答案:

  1. 为什么需要池:频繁 SpawnActor/Destroy 带来内存分配/释放开销与堆碎片、构造/析构开销(组件注册、碰撞初始化)、GC 压力、Replicated Actor 的 Spawn 网络开销。
  2. 设计要点:IPoolable 接口(OnSpawnFromPool / OnReturnToPool / IsPoolActive);UWorldSubsystem 管理按类分池(Available 空闲列表 + Active 使用中集合);WarmPool 预热避免首次延迟;归还时完整重置(Transform、物理速度、组件激活、计时器)。
  3. 关键代码:
UINTERFACE(MinimalAPI)
class UPoolable : public UInterface { GENERATED_BODY() };
class IPoolable
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent) void OnSpawnFromPool();
    UFUNCTION(BlueprintNativeEvent) void OnReturnToPool();
    UFUNCTION(BlueprintNativeEvent) bool IsPoolActive() const;
};

UCLASS()
class UActorPoolSubsystem : public UWorldSubsystem
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable)
    void WarmPool(TSubclassOf<APoolableActor> ActorClass, int32 Count, const FVector& Loc)
    {
        for (int32 i = 0; i < Count; ++i)
            Pools.FindOrAdd(ActorClass).Available.Add(CreateNewPoolActor(ActorClass));
    }
    APoolableActor* SpawnFromPool(TSubclassOf<APoolableActor> ActorClass, const FTransform& T)
    {
        FActorPool& Pool = Pools.FindOrAdd(ActorClass);
        APoolableActor* Actor = Pool.Available.Num() > 0 ? Pool.Available.Pop()
                                                          : CreateNewPoolActor(ActorClass);
        Actor->SetActorTransform(T);
        Actor->SetPoolActive(true);
        IPoolable::Execute_OnSpawnFromPool(Actor);   // 激活
        Pool.Active.Add(Actor);
        return Actor;
    }
    void ReturnToPool(APoolableActor* Actor)
    {
        Actor->SetPoolActive(false);
        IPoolable::Execute_OnReturnToPool(Actor);    // 停用 + 重置
        Actor->SetActorLocation(PoolStorageLocation);
        Pool->Active.Remove(Actor); Pool->Available.Add(Actor);
    }
private:
    struct FActorPool { TArray<APoolableActor*> Available; TSet<APoolableActor*> Active; };
    TMap<TSubclassOf<APoolableActor>, FActorPool> Pools;
    FVector PoolStorageLocation = FVector(0.f, 0.f, -10000.f);
};
  1. 子弹池示例:子弹继承 APoolableActor,Tick 超时后 GetWorld()->GetSubsystem<UActorPoolSubsystem>()->ReturnToPool(this);武器发射时 SpawnFromPool 而非 SpawnActor。
  2. 面试官提示:归还时完整重置(Transform、物理、组件状态、计时器);池为空时降级策略(创建新的 vs 等待归还);考虑游戏线程/渲染线程安全;网络同步中池的特殊处理;Niagara、Mass Entity 中的数据导向池化。

五、策略与命令模式

5.1 请在 UE 中实现一个策略模式的例子,展示如何在运行时切换行为。

★★ 中等

考点: 策略模式、多态、接口抽象、运行时行为切换

参考答案:

  1. 核心意图:定义一系列算法并封装,使它们可互相替换,算法独立于使用它的客户端变化。
  2. UE 实现要点:UINTERFACE + BlueprintNativeEvent 让策略可在 C++ 与蓝图混合实现;用 TScriptInterface<IMovementStrategy> 持有当前策略引用;策略是 UObject,Outer 设为持有者管理生命周期。
  3. 关键代码:
UINTERFACE(MinimalAPI, Blueprintable)
class UMovementStrategy : public UInterface { GENERATED_BODY(); };
class IMovementStrategy
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
    FVector CalculateDesiredVelocity(const AActor* Owner, float DeltaTime);
};

UCLASS(Blueprintable)
class UPatrolStrategy : public UObject, public IMovementStrategy
{
    GENERATED_BODY()
public:
    virtual FVector CalculateDesiredVelocity_Implementation(const AActor* Owner, float DeltaTime) override
    { /* 巡逻:路径点来回移动 */ }
};

UCLASS()
class AMyAIController : public AAIController
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable)
    void SetMovementStrategy(TScriptInterface<IMovementStrategy> NewStrategy)
    { CurrentStrategy = NewStrategy; }
    virtual void Tick(float DeltaTime) override
    {
        if (CurrentStrategy)
        {
            FVector V = IMovementStrategy::Execute_CalculateDesiredVelocity(
                CurrentStrategy.GetObject(), GetPawn(), DeltaTime);
            GetPawn()->AddMovementInput(V.GetSafeNormal(), V.Size());
        }
    }
private:
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TScriptInterface<IMovementStrategy> CurrentStrategy;
};
  1. 运行时切换:感知到玩家 → NewObject<UChaseStrategy> 并 SetMovementStrategy;血量低 → 切逃跑策略。
  2. 面试官提示:策略模式与状态模式的区别(主动切换 vs 内部状态驱动);行为树 Selector/Sequence 本质是策略模式的树形扩展;GAS 中也有策略思想。

5.2 请分析 UE Enhanced Input System 中的命令模式设计,并实现一个简单的命令系统。

★★★ 困难

考点: 命令模式、输入系统、撤销/重做

参考答案:

  1. 核心意图:将请求封装为对象,支持参数化、排队、日志记录与可撤销操作。Enhanced Input 的映射:Input Action → 命令对象、Input Mapping → 命令绑定、Trigger → 执行条件、Modifier → 预处理。
  2. 命令系统结构:UGameCommand 抽象基类(Execute / Undo / GetDescription / IsUndoable);具体命令(移动、放置物体)保存执行前后状态以便撤销;UMacroCommand 组合命令支持事务性回滚;UCommandManager(GameInstanceSubsystem)用 Undo/Redo 双栈管理。
  3. 关键代码:
UCLASS(Abstract, Blueprintable)
class UGameCommand : public UObject
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable) virtual bool Execute() PURE_VIRTUAL(UGameCommand::Execute, return false;);
    UFUNCTION(BlueprintCallable) virtual bool Undo() PURE_VIRTUAL(UGameCommand::Undo, return false;);
    UFUNCTION(BlueprintPure) virtual bool IsUndoable() const { return true; }
};

// 移动命令:记录 PreviousLocation 以便撤销
UCLASS()
class UMoveCommand : public UGameCommand
{
    GENERATED_BODY()
public:
    virtual bool Execute() override { PreviousLocation = Executor->GetActorLocation();
        Executor->SetActorLocation(TargetLocation); return true; }
    virtual bool Undo() override { Executor->SetActorLocation(PreviousLocation); return true; }
    FVector TargetLocation; UPROPERTY() FVector PreviousLocation;
};

// 命令管理器:双栈撤销/重做
UCLASS()
class UCommandManager : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public:
    bool ExecuteCommand(UGameCommand* Cmd)
    {
        if (Cmd->Execute()) { if (Cmd->IsUndoable()) UndoStack.Push(Cmd); RedoStack.Empty(); return true; }
        return false;
    }
    bool Undo() { UGameCommand* C = UndoStack.Pop(); if (C && C->Undo()) { RedoStack.Push(C); return true; } return false; }
private:
    TArray<UGameCommand*> UndoStack;
    TArray<UGameCommand*> RedoStack;
};
  1. 面试官提示:宏命令的事务性(全部成功或全部回滚);命令管理器的栈结构设计;UE 编辑器的 FTransaction 就是命令模式的实现;网络游戏中命令的序列化与重放(Replay System);命令模式与事件溯源(Event Sourcing)的关系。

六、状态模式

6.1 请对比 UE 中实现状态模式的三种方式,并分析各自的优劣。

★★★ 困难

考点: 状态模式、有限状态机、行为树、动画状态机

参考答案:

  1. 三种实现方式:
维度 枚举状态机 状态对象模式 行为树
复杂度 低 中 高
可扩展性 差 好 极好
可视化 ❌ ❌ ✅(编辑器)
数据驱动 ❌ 部分 ✅(黑板)
适用场景 3-5 个简单状态 复杂角色状态 AI 系统
  1. 方式一:枚举状态机——ECharacterState + switch 实现 CanTransitionTo / EnterState / ExitState / UpdateState,适合少量状态;状态多时 switch 膨胀、内聚性差。
  2. 方式二:状态对象模式——状态基类 UCharacterStateBase(Enter/Exit/Update/HandleInput/CanTransitionTo),每个状态一个 UObject;用 UStateMachineComponent 管理,StateCache 缓存避免重复创建。关键代码:
UCLASS(Abstract, Blueprintable)
class UCharacterStateBase : public UObject
{
    GENERATED_BODY()
public:
    virtual void Enter(ACharacter* Owner) {}
    virtual void Exit(ACharacter* Owner) {}
    virtual void Update(ACharacter* Owner, float DeltaTime) {}
    virtual FName GetStateName() const { return NAME_None; }
protected:
    UPROPERTY() ACharacter* OwnerCharacter = nullptr;
};

UCLASS()
class UStateMachineComponent : public UActorComponent
{
    GENERATED_BODY()
public:
    void SwitchState(TSubclassOf<UCharacterStateBase> NewStateClass)
    {
        if (CurrentState && !CurrentState->CanTransitionTo(NewStateClass)) return;
        if (CurrentState) CurrentState->Exit(GetOwner<ACharacter>());
        UCharacterStateBase*& NewState = StateCache.FindOrAdd(NewStateClass);
        if (!NewState) NewState = NewObject<UCharacterStateBase>(this, NewStateClass);
        CurrentState = NewState;
        CurrentState->Enter(GetOwner<ACharacter>());
    }
private:
    UPROPERTY() UCharacterStateBase* CurrentState = nullptr;
    TMap<TSubclassOf<UCharacterStateBase>, UCharacterStateBase*> StateCache;
};
  1. 方式三:行为树 + 黑板——Selector 做状态选择、Sequence 做条件流程、Decorator 做状态检查、黑板存共享数据,适合需要可视化与复杂决策的 AI。
  2. 面试官提示:关键点是状态转换的合法性验证和状态数据生命周期管理;UE5 的 UStateTreeComponent(5.1+)是官方状态树方案;注意动画状态机与逻辑状态机的同步;复杂 AI 可用分层状态机(HFSM)。

七、组件与组合架构

7.1 请阐述 UE Actor-Component 架构的设计哲学,并对比继承 vs 组合两种方案。

★★ 中等

考点: 组件模式、组合优于继承、架构可扩展性

参考答案:

  1. 继承的诅咒:每增加一个能力就增加新子类,导致类爆炸(如 WalkingShootingFlyingCharacter)。
  2. 组合方案(UE 的选择):ACharacter 组合 UCharacterMovementComponent、UHealthComponent、UInventoryComponent、UAbilitySystemComponent 等,每个组件单一职责,按需拼装。
  3. 关键代码:
// 生命值组件:数据 + 逻辑 + 事件通知
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class UHealthComponent : public UActorComponent
{
    GENERATED_BODY()
public:
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnHealthChanged,
        UHealthComponent*, Comp, float, NewHealth, float, Delta);
    UPROPERTY(BlueprintAssignable) FOnHealthChanged OnHealthChanged;
    DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
    UPROPERTY(BlueprintAssignable) FOnDeath OnDeath;

    UFUNCTION(BlueprintCallable) void ApplyDamage(float Damage)
    {
        if (bIsDead) return;
        const float Old = CurrentHealth;
        CurrentHealth = FMath::Clamp(CurrentHealth - Damage, 0.f, MaxHealth);
        OnHealthChanged.Broadcast(this, CurrentHealth, CurrentHealth - Old);
        if (CurrentHealth <= 0.f) { bIsDead = true; OnDeath.Broadcast(); }
    }
protected:
    UPROPERTY(EditAnywhere, BlueprintReadOnly) float MaxHealth = 100.f;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly) float CurrentHealth;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly) bool bIsDead = false;
};

// 组合使用
AMyCharacter::AMyCharacter()
{
    MeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
    HealthComp = CreateDefaultSubobject<UHealthComponent>(TEXT("Health"));
    InventoryComp = CreateDefaultSubobject<UInventoryComponent>(TEXT("Inventory"));
    HealthComp->OnDeath.AddDynamic(this, &AMyCharacter::HandleDeath); // 组件间通过委托协作
}
  1. 继承 vs 组合对比:耦合度 高 vs 低;复用性 低 vs 高;扩展性 类爆炸 vs 加组件即可;运行时灵活 不可变 vs 可动态添加/移除;UE 推荐度 不推荐深继承 vs 官方最佳实践。
  2. 面试官提示:组件之间应通过委托解耦而非直接 Cast 引用;UActorComponent vs USceneComponent 的区别(是否需要 Transform);CreateDefaultSubobject(构造时)vs NewObject(运行时);Lyra 项目的组件化设计模式。

7.2 UE 的 Component 系统如何体现单一职责原则(SRP)?

★★ 中等

考点: 单一职责原则、组件化设计

参考答案:

  1. SRP 定义:一个类应该只有一个引起它变化的原因。
  2. 违反 SRP 的典型:ASuperCharacter 同时管理生命值、背包、技能、对话、任务,数千行难以维护。
  3. 遵循 SRP:每个 Component 只负责一个职责——UHealthComponent 只管生命值、UInventoryComponent 只管背包、UAbilitySystemComponent 只管技能、UDialogueComponent 只管对话、UQuestComponent 只管任务。
  4. 关键代码:
AMyCharacter::AMyCharacter()
{
    HealthComp    = CreateDefaultSubobject<UHealthComponent>(TEXT("Health"));   // 只管生命值
    InventoryComp = CreateDefaultSubobject<UInventoryComponent>(TEXT("Inventory")); // 只管背包
    AbilityComp   = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("Ability")); // 只管技能
}

// 组件内部也只保留单一职责,用事件通知他人,而不自己处理
UCLASS()
class UHealthComponent : public UActorComponent
{
    GENERATED_BODY()
public:
    void ApplyDamage(float Damage);
    void Heal(float Amount);
    float GetHealthPercent() const;
    bool IsDead() const;
    DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
    UPROPERTY(BlueprintAssignable) FOnDeath OnDeath;  // UI 由 UI 组件订阅,而非 Health 组件自己去刷新
private:
    float CurrentHealth;
    float MaxHealth;
};
  1. 面试官提示:UE Component 系统是 SRP 在游戏引擎中最成功的实践——单一职责、委托通信(松耦合)、自由组合、修改一个组件不影响其他组件。

7.3 如何设计一个可扩展的物品系统?

★★★ 困难

考点: 物品系统设计、数据驱动、组合模式

参考答案:

  1. 核心设计要点:数据与逻辑分离——UItemDefinition(数据资产)vs UItemComponent(行为组件);组合模式——物品通过组合不同组件获得功能;操作模式——UItemAction 独立于物品本身;堆叠逻辑——先堆叠再找空槽。
  2. 关键代码:
// 物品定义(数据层)
UCLASS(BlueprintType)
class UItemDefinition : public UPrimaryDataAsset
{
    GENERATED_BODY()
public:
    virtual FPrimaryAssetId GetPrimaryAssetId() const override
    { return FPrimaryAssetId("Item", GetFName()); }
    UPROPERTY(EditDefaultsOnly) FName ItemID;
    UPROPERTY(EditDefaultsOnly) int32 MaxStackSize = 1;
    UPROPERTY(EditDefaultsOnly) FGameplayTagContainer ItemTags;
    UPROPERTY(EditDefaultsOnly) TArray<TSubclassOf<UItemAction>> AvailableActions;
    UPROPERTY(EditDefaultsOnly) TArray<TSubclassOf<UItemComponent>> ItemComponents; // 组合
};

// 行为组件(组合模式):定义生命周期回调
UCLASS(Abstract, Blueprintable, EditInlineNew)
class UItemComponent : public UObject
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent) bool OnUse(AActor* User, const FItemInstance& Item);
    UFUNCTION(BlueprintNativeEvent) void OnEquip(AActor* User, const FItemInstance& Item);
    UFUNCTION(BlueprintNativeEvent) void OnUnequip(AActor* User, const FItemInstance& Item);
};

// 具体组件:消耗品(回血)
UCLASS(Blueprintable)
class UConsumableComponent : public UItemComponent
{
    GENERATED_BODY()
public:
    virtual bool OnUse_Implementation(AActor* User, const FItemInstance& Item) override
    { return User->FindComponentByClass<UHealthComponent>() &&
            User->FindComponentByClass<UHealthComponent>()->Heal(HealAmount) || false; }
    UPROPERTY(EditAnywhere) float HealAmount = 0.f;
};
  1. 背包容器 UInventoryComponent:TArray<FItemInstance> Items + OnItemChanged 委托;AddItem 先 StackOrAdd 堆叠、再找空槽、最后扩展;UseItem 遍历 AvailableActions 执行"使用"操作,消耗完毕清空槽位。
  2. 面试官提示:FItemInstance 需要网络序列化支持;用 Tag 而非字符串定义行为;物品操作(命令模式)支持右键菜单扩展;参考 Lyra 的 Inventory 系统。

八、装饰器与代理模式

8.1 请解释 UE 中的装饰器模式,并展示材质实例层级和动画蓝图层级的装饰器思想。

★★ 中等

考点: 装饰器模式、材质实例、动画蓝图继承

参考答案:

  1. 核心意图:动态地给对象添加额外职责,比继承更灵活。
  2. 材质实例层级:基础材质定义结构 → 父级材质实例设基础参数 → 子级实例层层添加伤痕/污渍等装饰。C++ 中 CreateDynamicMaterialInstance 创建 MID 运行时设置参数,就是运行时装饰。
UMaterialInstanceDynamic* BaseMID = Mesh->CreateDynamicMaterialInstance(0);
BaseMID->SetVectorParameterValue(FName("BaseColor"), FLinearColor::Blue);
ApplyDamageEffect(BaseMID, 0.5f);   // 装饰:伤痕
ApplyBurnEffect(BaseMID, true);     // 装饰:燃烧
  1. 动画蓝图层级:基类 ABP 定义 Locomotion/Combat 状态机,子类(Warrior/Mage)继承并覆盖/新增状态机。C++ 侧 GetMesh()->SetAnimInstanceClass() 指定子类动画实例,子类 NativeUpdateAnimation 调用 Super 后添加专属逻辑(怒气系统、连击系统)。
  2. 运行时装饰器:Buff 系统——UBuffDecorator 抽象(Apply/Remove/ModifyDamage/ModifySpeed),UBuffManagerComponent 维护装饰器链,依次修改最终伤害/速度。
  3. 面试官提示:材质实例、动画蓝图继承、Buff 系统都是装饰器在 UE 中的体现;UMaterialInstanceDynamic 支持运行时装饰;材料参数集合(MPC)实现全局装饰。

8.2 请解释 UE 中 TWeakObjectPtr 和 TSoftObjectPtr 的代理模式原理及使用场景。

★★ 中等

考点: 代理模式、弱引用、软引用、资源管理

参考答案:

  1. 核心意图:为其他对象提供代理以控制对它的访问。UE 智能指针是代理模式的实现。
  2. TWeakObjectPtr——保护代理:不持有强引用,对象销毁后自动失效,防止悬垂指针崩溃。
  3. TSoftObjectPtr——虚拟代理:只存路径,使用时才(异步)加载,避免启动时全部加载。
  4. 关键代码:
// TWeakObjectPtr:安全引用 + 使用前检查
TWeakObjectPtr<AActor> Target;
void Attack()
{
    if (Target.IsValid())
    {
        AActor* SafeTarget = Target.Get();  // 锁定强引用再使用
        UGameplayStatics::ApplyDamage(SafeTarget, 10.f, nullptr, nullptr, nullptr);
    }
    else { Target = nullptr; }  // 已销毁,清理
}

// TSoftObjectPtr:懒加载代理
UPROPERTY(EditAnywhere) TSoftObjectPtr<UTexture2D> HeavyTexture;
FStreamableManager& SM = UGameplayStatics::GetStreamableManager(GetWorld());
SM.RequestAsyncLoad(HeavyTexture.ToSoftObjectPath(),
    FStreamableDelegate::CreateUObject(this, &AMyGameMode::OnTextureLoaded));
  1. 智能指针对比:
类型 强引用 弱引用 异步加载 适用对象
TObjectPtr<T> ✅ ❌ ❌ UObject(UE5 推荐)
TWeakObjectPtr<T> ❌ ✅ ❌ UObject 安全引用
TSoftObjectPtr<T> ❌ ✅ ✅ UObject 按需加载
TSharedPtr/TWeakPtr ✅/❌ - ❌ 非 UObject
  1. 代理模式的其他 UE 实现:网络 Replication 是远程代理(客户端通过代理访问服务器对象);PlayerController 是控制代理(代理玩家对 Pawn 的控制);OnRep 回调同步客户端展示值。

九、MVC/MVVM 与 UI 架构

9.1 请在 UMG 中实现 MVVM 模式,并说明 UE5 的 ViewModel 支持。

★★★ 困难

考点: MVVM 模式、数据绑定、UMG 架构

参考答案:

  1. 传统 UMG 的问题:Widget 直接访问 PlayerState、Tick 轮询更新、业务逻辑写在 Widget 中,UI 与逻辑强耦合。
  2. MVVM 三角:Model(纯数据,如 PlayerState)→ ViewModel(数据转换 + 通知,UObject)→ View(纯 UI 展示,UserWidget)。数据流:Model → ViewModel(委托)→ View;操作流:View → ViewModel → Model。
  3. 关键代码:
// ViewModel:UObject,独立于游戏世界,负责数据缓存/转换/通知
UCLASS(BlueprintType)
class UPlayerViewModel : public UObject
{
    GENERATED_BODY()
public:
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDataChanged, const FPlayerUIData&, NewData);
    UPROPERTY(BlueprintAssignable) FOnDataChanged OnDataChanged;

    void BindToPlayerState(APlayerState* PS)
    {
        PS->OnHealthChanged.AddDynamic(this, &UPlayerViewModel::OnPlayerHealthChanged);
        NotifyDataChanged();
    }
    // 转换:百分比 → 颜色 / 格式化文本(ViewModel 核心职责)
    UFUNCTION(BlueprintPure) FLinearColor GetHealthBarColor() const
    { return CachedData.HealthPercent > 0.6f ? FLinearColor::Green
            : CachedData.HealthPercent > 0.3f ? FLinearColor::Yellow : FLinearColor::Red; }
    UFUNCTION(BlueprintCallable) void RequestUseItem(int32 Index) { /* 转发到 Model */ }
private:
    UPROPERTY() FPlayerUIData CachedData;
    void NotifyDataChanged() { OnDataChanged.Broadcast(CachedData); }
};

// View:纯 UI 更新,无业务逻辑
void UPlayerHUDWidget::BindViewModel(UPlayerViewModel* VM)
{
    if (ViewModel) ViewModel->OnDataChanged.RemoveDynamic(this, &UPlayerHUDWidget::OnViewModelDataChanged);
    ViewModel = VM;
    if (ViewModel) { ViewModel->OnDataChanged.AddDynamic(this, &UPlayerHUDWidget::OnViewModelDataChanged);
        OnViewModelDataChanged(ViewModel->GetCurrentData()); }
}
void UPlayerHUDWidget::NativeDestruct() { /* 必须解绑避免悬垂 */ }
  1. 面试官提示:ViewModel 是 UObject 而非 Actor;ViewModel 职责是数据转换(百分比→颜色、数字→格式化文本);委托实现双向数据流;Widget 的 NativeDestruct 中必须解绑。
  2. UE5 官方支持:UE5.1+ UMVVMViewModelBase(官方 MVVM 插件)、BindWidget/BindWidgetAnim 元数据、FViewModelBindingName 与蓝图数据绑定、双向绑定 UMVVMViewModelFieldWatcher。

十、游戏架构分层与模块化

10.1 请阐述 UE 中 C++ 框架层/蓝图业务层/数据配置层的分层架构设计。

★★ 中等

考点: 分层架构、职责分离、C++ 与蓝图协作

参考答案:

  1. 三层架构:数据配置层(DataTable/DataAsset/Config,只存数据不含逻辑)→ 蓝图业务层(具体游戏逻辑、关卡脚本、UI 布局)→ C++ 框架层(基类、Subsystem、组件、性能敏感代码)。
  2. 分层规则:C++ 不依赖蓝图(框架层不能引用具体蓝图类);蓝图继承 C++ 扩展;数据不包含逻辑;C++ 暴露钩子;性能敏感在 C++,美术/设计在蓝图。
  3. C++ 框架层关键代码——三种蓝图交互方式:
UCLASS(Abstract, Blueprintable)
class AWeaponBase : public AActor
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable) virtual void Fire();       // C++ 实现核心逻辑
    UFUNCTION(BlueprintImplementableEvent) void OnFireStarted();  // 蓝图实现(无默认)
    UFUNCTION(BlueprintNativeEvent) float CalculateDamage() const; // C++ 默认实现,蓝图可覆盖
};

float AWeaponBase::CalculateDamage_Implementation() const
{ return BaseDamage * GetDamageMultiplier(); }
  1. 蓝图业务层:BP_Rifle 继承 AWeaponBase 设参数、覆盖 CalculateDamage 加暴击、实现 OnFireStarted 播放特效音效。
  2. 面试官提示:BlueprintImplementableEvent(C++ 声明蓝图实现)vs BlueprintNativeEvent(C++ 提供默认实现蓝图可覆盖);EditDefaultsOnly vs EditAnywhere 控制属性可编辑位置;UInterface 是跨层通信的桥梁。

10.2 请设计一个数据驱动的游戏架构,展示 DataTable/DataAsset/配置表的综合应用。

★★ 中等

考点: 数据驱动架构、DataTable、DataAsset、配置管理

参考答案:

  1. 核心思想:将游戏逻辑与数据分离,修改内容不需要改代码。
  2. 多层数据架构:
    • DataTable:适合批量、扁平数据(物品列表、敌人属性表),FTableRowBase 定义行结构;
    • DataAsset / PrimaryDataAsset:适合复杂、嵌套数据(武器配置、关卡配置),可引用其他资产;
    • 配置管理器(UGameDataSubsystem):统一查询入口 + 缓存 + 通过 AssetManager 加载。
  3. 关键代码:
// DataTable 行定义
USTRUCT(BlueprintType)
struct FEnemyTableRow : public FTableRowBase
{
    GENERATED_BODY()
    UPROPERTY(EditAnywhere, BlueprintReadOnly) TSubclassOf<ACharacter> EnemyClass;
    UPROPERTY(EditAnywhere, BlueprintReadOnly) float BaseHealth = 100.f;
    UPROPERTY(EditAnywhere, BlueprintReadOnly) float BaseDamage = 10.f;
};

// PrimaryDataAsset:支持 AssetManager 异步加载
UCLASS(BlueprintType)
class UWeaponDataAsset : public UPrimaryDataAsset
{
    GENERATED_BODY()
public:
    virtual FPrimaryAssetId GetPrimaryAssetId() const override
    { return FPrimaryAssetId("Weapon", GetFName()); }
    UPROPERTY(EditAnywhere) TSoftObjectPtr<USkeletalMesh> WeaponMesh;
    UPROPERTY(EditAnywhere) TArray<TSubclassOf<UGameplayAbility>> GrantedAbilities;
};

// 配置管理器:统一查询 + 缓存
UCLASS()
class UGameDataSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public:
    bool GetEnemyData(FName EnemyID, FEnemyTableRow& OutData) const
    { FEnemyTableRow* Row = EnemyDataTable->FindRow<FEnemyTableRow>(EnemyID, TEXT(""));
      if (Row) { OutData = *Row; return true; } return false; }
protected:
    UPROPERTY(EditDefaultsOnly) TObjectPtr<UDataTable> EnemyDataTable;
};
  1. 使用场景:SpawnManager 读 EnemyWaves 配置批量生成敌人并用表数据初始化属性;武器 InitializeFromData 异步加载 Mesh 并授予技能。
  2. 面试官提示:DataTable 行查找 vs 全表遍历性能;配置热重载;数据验证(OnDataTableChanged);FPrimaryAssetId + AssetManager 规则。

10.3 如何设计一个插件化的游戏框架?

★★★ 困难

考点: 模块化设计、接口解耦、插件架构

参考答案:

  1. UE 模块系统:Source/MyGameCore(核心模块:接口、基类、工具)依赖关系用 Build.cs 的 PublicDependencyModuleNames 控制;Plugins/CombatSystem、InventorySystem、QuestSystem 作为插件(.uplugin)隔离。
  2. 核心原则:核心模块只定义接口不包含实现;插件通过接口通信,不直接引用其他插件类;运行时通过事件总线进一步解耦。
  3. 关键代码:
// 核心模块定义接口
UINTERFACE(MinimalAPI, Blueprintable)
class UGameModule : public UInterface { GENERATED_BODY(); };
class IGameModule
{
    GENERATED_BODY()
public:
    virtual FName GetModuleName() const = 0;
    virtual void ModuleInitialize() {}
    virtual TArray<FName> GetDependencies() const { return {}; }
};

// 模块管理器:自动发现 + 依赖校验
UCLASS()
class UModuleManager : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public:
    void AutoDiscoverModules()
    {
        for (TObjectIterator<UClass> It; It; ++It)
        {
            if (It->ImplementsInterface(UGameModule::StaticClass()) &&
                It->IsChildOf(UGameInstanceSubsystem::StaticClass()) && !It->HasAnyClassFlags(CLASS_Abstract))
            {
                UGameInstanceSubsystem* Sub = GetGameInstance()->GetSubsystem(*It);
                RegisteredModules.Add(IGameModule::Execute_GetModuleName(Sub), Sub);
                IGameModule::Execute_ModuleInitialize(Sub);
            }
        }
    }
    template<typename TInterface>
    TScriptInterface<TInterface> GetModuleByInterface() const { /* 遍历查找实现接口的模块 */ }
private:
    TMap<FName, TScriptInterface<IGameModule>> RegisteredModules;
};
  1. 插件间通信三种模式:接口通信(编译期依赖)、事件总线(运行时解耦)、Subsystem 依赖查找(GetSubsystem<T>())。
  2. 面试官提示:核心模块只定义接口;Build.cs 控制编译期依赖;ValidateDependencies 校验模块依赖完整性;接口隔离 + 依赖管理是插件化框架的核心。

10.4 请解释 UE5 Mass Entity 框架的 ECS 架构设计哲学,并对比传统面向对象方案。

★★★ 困难

考点: ECS 架构、数据驱动、性能优化、Mass Entity

参考答案:

  1. ECS vs OOP 哲学:OOP 把数据+行为封装在一起(AoS);ECS 中 Entity 仅 ID、Fragment 纯数据、Processor 纯行为,Entity 通过组合 Fragment 定义"类型",Processor 批量处理拥有特定 Fragment 组合的 Entity(SoA)。
  2. Mass 三层数据抽象:Fragment(实体级数据)、Tag(零大小标记,用于分类/状态)、ChunkFragment(Chunk 级共享数据)。
  3. Processor 声明式查询 + 批量处理关键代码:
UCLASS()
class UMovementProcessor : public UMassProcessor
{
    GENERATED_BODY()
public:
    UMovementProcessor() { ExecutionOrder.ExecuteBefore.Add(UE::Mass::ProcessorGroups::Avoidance); }
protected:
    virtual void ConfigureQueries() override
    {
        EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
        EntityQuery.AddRequirement<FVelocityFragment>(EMassFragmentAccess::ReadWrite);
        EntityQuery.AddTagRequirement<FDeadTag>(EMassFragmentPresence::None); // 排除已死亡
    }
    virtual void Execute(FMassEntityManager& EM, FMassExecutionContext& Ctx) override
    {
        EntityQuery.ForEachEntityChunk(EM, Ctx, [](FMassExecutionContext& Context)
        {
            const int32 N = Context.GetNumEntities();
            TArrayView<FTransformFragment> T = Context.GetMutableFragmentView<FTransformFragment>();
            TArrayView<FVelocityFragment> V = Context.GetMutableFragmentView<FVelocityFragment>();
            for (int32 i = 0; i < N; ++i)   // SIMD 友好的连续内存批量处理
                T[i].Transform.AddToTranslation(V[i].Velocity * Context.GetDeltaTimeSeconds());
        });
    }
private:
    FMassEntityQuery EntityQuery;
};
  1. OOP vs ECS 对比:数据布局 AoS vs SoA;缓存友好性 差 vs 极好;内存碎片 高 vs 低;批量处理 ❌ vs ✅;适用实体数 百级 vs 万级+。
  2. 面试官提示:ECS 核心优势是数据局部性(SoA 布局带来缓存友好);Context.Defer() 延迟修改避免迭代中修改集合;了解 Mass LOD(远距离降频)、Chunk 级并行、StateTree 集成、与 GAS 的协作。

十一、依赖注入

11.1 UE 中如何实现依赖注入(DI)?

★★★ 困难

考点: 依赖注入、控制反转、模块化设计

参考答案:

  1. UE 没有原生 DI 容器,但通过组件系统、Subsystem、接口和蓝图配置实现了完整的 DI 生态。
  2. 四种实现方式:
方式 时机 类型安全 UE 体现
构造函数注入 构造时 ✅ CreateDefaultSubobject 创建组件
Setter 注入 运行时 ✅ SetBehaviorTree(Asset) 配置
接口注入 运行时 ✅ TScriptInterface 依赖抽象
Subsystem 依赖查找 初始化 ✅ InitializeDependency + GetSubsystem<T>
  1. 关键代码:
// 方式一:构造函数注入(组件)
AMyCharacter::AMyCharacter()
{
    HealthComp = CreateDefaultSubobject<UHealthComponent>(TEXT("Health"));
    InventoryComp = CreateDefaultSubobject<UInventoryComponent>(TEXT("Inventory"));
    HealthComp->OnDeath.AddDynamic(this, &AMyCharacter::HandleDeath); // 依赖通过委托注入
}

// 方式三:接口注入
UINTERFACE(MinimalAPI)
class UDamageProcessor : public UInterface { GENERATED_BODY(); };
class IDamageProcessor
{
    GENERATED_BODY()
public:
    virtual float ProcessDamage(float RawDamage, const FDamageEvent& Event) = 0;
};

UCLASS()
class UArmorDamageProcessor : public UObject, public IDamageProcessor
{
    GENERATED_BODY()
public:
    virtual float ProcessDamage(float RawDamage, const FDamageEvent& Event) override
    { return RawDamage * (1.f - FMath::Clamp(Armor / (Armor + 100.f), 0.f, 0.75f)); }
    float Armor = 50.f;
};

UCLASS()
class AMyCharacter : public ACharacter
{
    GENERATED_BODY()
public:
    void SetDamageProcessor(TScriptInterface<IDamageProcessor> In) { DamageProcessor = In; }
private:
    UPROPERTY() TScriptInterface<IDamageProcessor> DamageProcessor; // 依赖抽象
};

// 方式四:Subsystem 依赖声明
void UMySubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    Collection.InitializeDependency<UOtherSubsystem>();  // 保证依赖先初始化
}
  1. 面试官提示:CreateDefaultSubobject = 构造函数注入;TScriptInterface = 接口注入;InitializeDependency = 依赖声明;不要为了 DI 而 DI——UE 的原生方式已经足够好。

十二、技能系统(GAS)

12.1 如何设计一个可扩展的技能系统?请分析 GAS 的架构思想。

★★★ 困难

考点: 技能系统设计、GAS 架构、Effect-Ability-Attribute 三角

参考答案:

  1. GAS 核心组件:AbilitySystemComponent(ASC,核心,管理能力/效果/标签/属性 + 网络同步)、GameplayAbility(技能逻辑)、GameplayEffect(效果配置)、AttributeSet(属性集)、GameplayTag(条件/状态标记)、GameplayCue(表现层)、GameplayTask(异步任务)。
  2. Ability-Effect-Attribute 三角:Ability 激活后通过 GameplayEffect 修改 Attribute,ASC 统一驱动整个生命周期。
  3. 关键代码:
// AttributeSet:声明属性 + Pre/Post 回调
UCLASS()
class UCombatAttributeSet : public UAttributeSet
{
    GENERATED_BODY()
public:
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health) FGameplayAttributeData Health;
    ATTRIBUTE_ACCESSORS(UCombatAttributeSet, Health)
    virtual void PreAttributeChange(const FGameplayAttribute& A, float& NewValue) override
    { if (A == GetHealthAttribute()) NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth()); }
    virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override
    { if (GetHealth() <= 0.f) { /* 广播 Event.Death */ } }
};

// Ability 基类:检查/激活钩子
UCLASS(Abstract)
class UGameplayAbilityBase : public UGameplayAbility
{
    GENERATED_BODY()
protected:
    virtual bool CanActivateAbility(...) const override;      // 检查冷却/消耗/Tag 条件
    virtual void ActivateAbility(...) override;               // 核心逻辑
    UFUNCTION(BlueprintImplementableEvent) void OnAbilityActivated();
    UFUNCTION(BlueprintImplementableEvent) void OnAbilityEnded(bool bWasCancelled);
};

// 具体技能:火球术
void UFireballAbility::ActivateAbility(...)
{
    if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) { EndAbility(..., true, true); return; }
    UAbilityTask_PlayMontageAndWait* Task = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
        this, NAME_None, CastMontage, 1.0f);
    Task->OnCompleted.AddDynamic(this, &UFireballAbility::OnMontageCompleted);
    Task->ReadyForActivation();  // 动画完成后 Spawn 火球 + 应用伤害 GE
}
  1. 面试官提示:理解 GameplayTag 条件系统(技能释放条件、效果应用条件);ASC 的 OwnerActor/AvatarActor 分离;网络同步策略(Minimal/Full Mode);UGameplayEffectExecutionCalculation 自定义伤害公式;知道 GAS 的局限性(复杂度高、学习曲线陡);Lyra 项目中的 GAS 最佳实践。

十三、SOLID 原则与反模式

13.1 UE 中的反模式(Anti-patterns)有哪些?如何规避?

★★ 中等

考点: 代码质量、架构陷阱、最佳实践

参考答案:

  1. 反模式汇总:
反模式 症状 解决方案
Tick 滥用 每帧检查条件 定时器/事件驱动/感知系统
硬引用膨胀 内存占用大 TSoftObjectPtr 按需加载
Cast 地狱 大量类型转换 接口/UInterface
God Class 单个类数千行 Subsystem/Component 职责分离
蓝图/C++ 混乱 职责不清 分层架构
裸指针泛滥 崩溃频繁 TWeakObjectPtr/TObjectPtr
字符串硬编码 难以本地化 FText/FName + DataTable
  1. Tick 滥用规避:用 FTimerManager(0.5s 定时检测)、UAIPerceptionComponent 事件驱动,或 PrimaryActorTick.bCanEverTick = false 禁用 Tick。
  2. 硬引用规避:用 TSoftObjectPtr / TSoftClassPtr 只存路径,FStreamableManager::RequestAsyncLoad 按需加载。
  3. Cast 地狱规避:用 UINTERFACE + IInteractable,OtherActor->Implements<UInteractable>() 后统一调用 IInteractable::Execute_OnInteract(...),新增类型无需改动调用方。
  4. God Class 规避:把存档、背包、成就、UI 拆分为独立 Subsystem。
  5. 面试官提示:识别反模式的能力是高级工程师的标志;回答要落到背后的性能、可维护性、可扩展性问题,而不仅是"怎么做"。

13.2 开闭原则(OCP)在 UE 中如何应用?请对比蓝图继承和组合扩展。

★★ 中等

考点: 开闭原则、扩展性设计

参考答案:

  1. OCP 定义:对扩展开放,对修改关闭。
  2. 蓝图继承:C++ 基类提供 BlueprintNativeEvent 扩展点,蓝图覆盖实现新行为,基类不修改。
UCLASS(Blueprintable)
class AWeaponBase : public AActor
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent, Category = "Weapon") float CalculateDamage() const;
    UFUNCTION(BlueprintNativeEvent, Category = "Weapon") void OnFire();
protected:
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) float BaseDamage = 10.f;
};
// 扩展:BP_Rifle 覆盖 CalculateDamage 加暴击;BP_Shotgun 覆盖 OnFire 实现散弹
  1. 组合扩展:通过添加新 Component(UExplosionComponent)扩展功能;通过 DataAsset 配置不同行为(UAmmoType 改变伤害倍率),新增数据即可扩展。
  2. 三种实现方式总结:蓝图继承(BlueprintNativeEvent 扩展点)、组合扩展(新增 Component)、数据驱动(DataAsset 配置)。
  3. 面试官提示:优先组合 + 数据驱动,仅在语义上强相关时用继承;BlueprintNativeEvent 是 C++ 侧预留扩展点的标准手段。

13.3 请解释 UE 接口(UInterface)如何实现里氏替换原则(LSP)和接口隔离原则(ISP)。

★★★ 困难

考点: 里氏替换原则、接口隔离原则、UInterface 设计

参考答案:

  1. LSP:子类型必须可替换基类型。IDamageable 接口定义契约(TakeDamage/GetHealth/IsAlive),角色与可破坏物都可实现,伤害系统只依赖接口,任意实现可替换。
UINTERFACE(MinimalAPI, Blueprintable)
class UDamageable : public UInterface { GENERATED_BODY(); };
class IDamageable
{
    GENERATED_BODY()
public:
    virtual float TakeDamage(float Amount, const FDamageEvent& Event,
        AController* Instigator, AActor* Causer) = 0;
    virtual float GetHealth() const = 0;
};

void UDamageSystem::ApplyDamageToTarget(AActor* Target, float Damage)
{
    if (Target && Target->Implements<UDamageable>())   // 不关心具体类型
        IDamageable::Execute_TakeDamage(Target, Damage, FDamageEvent(), nullptr, nullptr);
}
  1. ISP:接口应该小而专一。避免 IGameEntity 巨接口(Move/Attack/Heal/Save/PlayAnimation/UpdateUI 全塞一起);拆分为 IDamageable / IInteractable / ISaveable / IHighlightable,不同对象按需实现组合。
  2. UE 接口 vs C++ 纯虚类:UInterface 支持蓝图、Implements<T>() 运行时检查、多接口继承、蓝图实现、序列化与 GC 集成。
  3. 面试官提示:接口保证可替换性(LSP),小接口保证灵活性(ISP);Implements<T>() + Execute_* 提供类型安全的运行时接口调用;BlueprintNativeEvent 让接口方法在 C++ 与蓝图中均可实现。

13.4 依赖倒置原则(DIP)在 UE 中如何体现?请给出 Subsystem 和接口的结合案例。

★★★ 困难

考点: 依赖倒置原则、抽象依赖、解耦设计

参考答案:

  1. DIP 定义:高层模块不依赖低层模块,两者都应依赖抽象;抽象不依赖细节,细节依赖抽象。
  2. 违反 DIP:AGameMode 直接持有 UMySQLSaveSystem、USteamOnlineSystem 具体类,切换存储/平台必须改代码,无法 Mock 测试。
  3. 遵循 DIP:核心模块定义 ISaveSystem / IOnlineSystem 接口;本地/云存储、Steam/EOS 实现接口;高层模块通过 Subsystem 获取实例并只通过接口调用。
UINTERFACE(MinimalAPI)
class USaveSystem : public UInterface { GENERATED_BODY(); };
class ISaveSystem
{
    GENERATED_BODY()
public:
    virtual bool Save(const FString& Slot, const FString& Data) = 0;
    virtual bool Load(const FString& Slot, FString& OutData) = 0;
};

// 低层实现:本地文件存储 / 云存储(可扩展)
UCLASS()
class ULocalFileSaveSystem : public UGameInstanceSubsystem, public ISaveSystem
{
    GENERATED_BODY()
public:
    virtual bool Save(const FString& Slot, const FString& Data) override
    { return FFileHelper::SaveStringToFile(Data, *GetSavePath(Slot)); }
};

// 高层依赖抽象:通过接口调用,可运行时切换实现
UCLASS()
class AMyGameMode : public AGameModeBase
{
    GENERATED_BODY()
public:
    void SaveGame()
    {
        if (ISaveSystem* Save = Cast<ISaveSystem>(SaveSystem))  // 依赖抽象
            Save->Save("Slot1", "GameData");
    }
private:
    UPROPERTY() UGameInstanceSubsystem* SaveSystem;  // 接口使用,具体实现可替换
};
  1. 依赖倒置层次图:高层模块(GameMode)→ 依赖抽象(UInterface)→ 实现抽象(具体 Subsystem)。
  2. 面试官提示:核心模块定义接口、插件模块实现接口;Subsystem 作为服务注册点通过接口暴露功能;运行时可用配置切换实现(本地 vs 云存储);单元测试可注入 Mock 实现。

十四、设计模式补充

14.1 适配器模式(Adapter)在UE中如何应用?请举例说明。

★★ 中等

考点: 适配器模式、接口转换、遗留系统集成

参考答案:

  1. 核心意图:将一个类的接口转换成客户端期望的另一个接口,使接口不兼容的类可以协同工作。
  2. UE 中的典型应用:
    • TScriptInterface 适配器:将原生 C++ 指针适配为蓝图可调用的接口引用。
    • 第三方 SDK 集成:将 Steam/EOS SDK 的回调接口适配为 UE 委托系统。
    • 数据格式转换:将外部 JSON/XML 配置适配为 UE DataTable 行结构。
  3. 关键代码:
// 适配器示例:将旧版日志接口适配为 UE 日志系统
class ILegacyLogger
{
public:
    virtual void LogMessage(const char* msg) = 0;
};

class ULegacyLogAdapter : public UObject, public ILegacyLogger
{
    GENERATED_BODY()
public:
    virtual void LogMessage(const char* msg) override
    {
        // 适配:C 风格字符串 → UE FString → UE 日志
        FString UEMsg = FString(ANSI_TO_TCHAR(msg));
        UE_LOG(LogTemp, Warning, TEXT("%s"), *UEMsg);
    }
};

// 使用:旧系统只需调用 LogMessage,内部自动走 UE 日志
void UMySubsystem::Init()
{
    TScriptInterface<ILegacyLogger> Logger;
    ULegacyLogAdapter* Adapter = NewObject<ULegacyLogger>(this);
    Logger.SetObject(Adapter); Logger.SetInterface(Adapter);
    LegacySystem->SetLogger(Logger);  // 旧系统无缝集成
}
  1. 适配器分类:类适配器(多重继承)、对象适配器(组合持有)、双向适配器(双方均可适配)。
  2. 面试官提示:适配器解决的是已有接口不匹配的问题;与外观模式区别——适配器转换接口,外观简化接口;UE 编辑器中将第三方工具集成到编辑器 UI 就是适配器思想。

14.2 迭代器模式(Iterator)的原理是什么?UE容器如何支持迭代?

★★ 中等

考点: 迭代器模式、容器遍历、统一遍历接口

参考答案:

  1. 核心意图:提供一种顺序访问聚合对象中各个元素的方法,而不暴露其内部表示。
  2. UE 容器迭代器实现:
    • TArray/TMap/TSet 均提供 begin()/end(),支持范围 for 循环。
    • TObjectIterator<T> 遍历所有 UObject 派生对象。
    • TFieldIterator<T> 遍历 UClass 的 UPROPERTY/UFUNCTION。
  3. 关键代码:
// 基础容器迭代
TArray<AActor*> Actors;
for (AActor* Actor : Actors) { /* 范围 for 自动调用迭代器 */ }

// TArray 迭代器(显式)
for (TArray<AActor*>::TIterator It(Actors); It; ++It)
{
    AActor* Actor = *It;
    UE_LOG(LogTemp, Log, TEXT("Actor: %s"), *Actor->GetName());
}

// TObjectIterator:遍历所有 UObject(慎用,全局扫描)
for (TObjectIterator<AActor> It; It; ++It)
{
    if (It->GetWorld() == GetWorld())
        UE_LOG(LogTemp, Log, TEXT("World Actor: %s"), *It->GetName());
}

// TFieldIterator:遍历类的反射信息
for (TFieldIterator<FProperty> It(AMyClass::StaticClass()); It; ++It)
{
    UE_LOG(LogTemp, Log, TEXT("Property: %s"), *It->GetName());
}
  1. 迭代器类型:前向迭代器(单向遍历)、双向迭代器(--)、随机访问迭代器([]、+n);UE 容器迭代器为随机访问。
  2. 面试官提示:迭代器使遍历逻辑与容器解耦;TObjectIterator 性能开销大,仅在编辑器工具或调试时使用;TSoftObjectPtr 的 LoadSynchronous() 是延迟加载迭代的一种体现;Mass Entity 的 ForEachEntityChunk 是数据导向迭代器。

14.3 模板方法模式(Template Method)与策略模式有何区别?

★★ 中等

考点: 模板方法模式、策略模式、算法骨架、行为替换

参考答案:

  1. 核心区别:模板方法在父类定义算法骨架,子类重写个别步骤;策略模式在运行时替换整个算法。
维度 模板方法 策略模式
算法定义 父类(编译时) 策略类(运行时)
切换粒度 步骤级(重写单个方法) 算法级(替换整个策略对象)
继承关系 强依赖继承 依赖接口/组合
UE 实现 BlueprintNativeEvent UInterface + 运行时切换
  1. 模板方法 UE 实现:
UCLASS(Abstract, Blueprintable)
class APickupBase : public AActor
{
    GENERATED_BODY()
public:
    // 模板方法:定义算法骨架
    void OnPickedUp(ACharacter* Collector)
    {
        if (CanBePickedUp(Collector))           // 步骤1:条件检查
        {
            ApplyEffect(Collector);             // 步骤2:应用效果(子类重写)
            PlayPickupEffect();                 // 步骤3:表现(固定)
            Destroy();                          // 步骤4:销毁(固定)
        }
    }
protected:
    UFUNCTION(BlueprintNativeEvent) bool CanBePickedUp(ACharacter* Collector);  // 子类可重写
    UFUNCTION(BlueprintNativeEvent) void ApplyEffect(ACharacter* Collector);    // 子类必须重写
    UFUNCTION(BlueprintCallable) void PlayPickupEffect() { /* 固定逻辑 */ }
};

// 子类:重写 ApplyEffect
void AHealthPickup::ApplyEffect_Implementation(ACharacter* Collector)
{
    UHealthComponent* HC = Collector->FindComponentByClass<UHealthComponent>();
    if (HC) HC->Heal(HealAmount);
}
  1. 策略模式 UE 实现:定义 IAttackStrategy 接口,不同武器类实现不同攻击策略,运行时通过 SetAttackStrategy() 切换。
  2. 选择指南:步骤固定、仅个别步骤变化 → 模板方法;整个行为可替换 → 策略模式;UE 中 BlueprintNativeEvent 是模板方法的标准实现手段。
  3. 面试官提示:模板方法是类级别复用,策略模式是对象级别复用;行为树的 Composite 节点用模板方法定义遍历骨架,叶子节点用策略模式定义具体行为。

14.4 外观模式(Facade)如何简化子系统接口?在UE中有哪些体现?

★★ 中等

考点: 外观模式、接口简化、子系统封装

参考答案:

  1. 核心意图:为子系统中的一组接口提供一个统一的高层接口,降低使用方的耦合度。
  2. UE 中的典型外观:
    • UGameplayStatics:统一入口封装引擎底层功能(SpawnActor、PlaySound、ApplyDamage、GetPlayerController 等),避免直接调用 UWorld/APlayerController 等复杂子系统。
    • UAssetManager:统一资源加载入口,封装 FStreamableManager、IAssetRegistry、FPrimaryAssetId 等底层细节。
    • USubsystem:每个 Subsystem 都是其对应子系统的外观。
  3. 关键代码:
// 自定义外观:简化复杂的战斗子系统调用
UCLASS()
class UCombatFacade : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public:
    // 外观方法:一次调用完成复杂流程
    UFUNCTION(BlueprintCallable)
    void ExecuteAttack(AAttacker* Attacker, AActor* Target, FName AttackID)
    {
        // 内部协调多个子系统(调用方无需知道细节)
        FAttackData* Data = AttackTable->FindRow<FAttackData>(AttackID, TEXT(""));
        float FinalDamage = DamageCalc->Calculate(Attacker, Target, Data);
        DamageSystem->ApplyDamage(Target, FinalDamage, Attacker);
        VFXSystem->SpawnHitEffect(Target->GetActorLocation(), Data->EffectID);
        AudioSystem->PlayHitSound(Data->SoundID, Target->GetActorLocation());
        if (Data->bApplyKnockback)
            PhysicsSystem->ApplyImpulse(Target, Attacker->GetActorForwardVector() * Data->KnockbackForce);
    }
private:
    UPROPERTY() UDamageCalcSubsystem* DamageCalc;
    UPROPERTY() UDamageSystemSubsystem* DamageSystem;
    UPROPERTY() UVFXSubsystem* VFXSystem;
    UPROPERTY() UAudioSubsystem* AudioSystem;
    UPROPERTY() UPhysicsSubsystem* PhysicsSystem;
};

// 使用:调用方只需一行
CombatFacade->ExecuteAttack(Attacker, Target, FName("Fireball"));
  1. 外观模式 vs 适配器:外观简化接口(不转换接口),适配器转换接口(保持功能不变)。
  2. 面试官提示:UGameplayStatics 是 UE 中最大的外观类;外观不增加新功能,只是简化访问路径;外观可随子系统演化而演进,但不应包含业务逻辑;面试中提到外观模式说明理解子系统封装思想。

14.5 建造者模式(Builder)适用于什么场景?请用UE举例。

★★★ 困难

考点: 建造者模式、分步构建、复杂对象创建

参考答案:

  1. 核心意图:将复杂对象的构建与表示分离,使同样的构建过程可以创建不同的表示。适用于对象有多个可选参数、构建步骤固定但细节不同的场景。
  2. UE 中的体现:
    • FActorSpawnParameters:分步设置 Owner/Transform/SpawnCollisionHandling 等参数后一次性 Spawn。
    • FStreamableManager::RequestAsyncLoad:链式配置加载请求(回调、优先级、批量)。
    • 编辑器向导(Wizard):分步配置资产属性后生成资产。
  3. 关键代码:
// 建造者模式:分步构建复杂武器
UCLASS()
class UWeaponBuilder
{
    GENERATED_BODY()
public:
    UWeaponBuilder& SetBaseDamage(float Damage) { BaseDamage = Damage; return *this; }
    UWeaponBuilder& SetFireRate(float Rate) { FireRate = Rate; return *this; }
    UWeaponBuilder& SetAmmoType(TSubclassOf<UAmmoType> Type) { AmmoType = Type; return *this; }
    UWeaponBuilder& AddModifier(TSubclassOf<UWeaponModifier> Mod) { Modifiers.Add(Mod); return *this; }
    UWeaponBuilder& SetMesh(TSoftObjectPtr<USkeletalMesh> InMesh) { MeshPath = InMesh; return *this; }

    AWeaponBase* Build(UWorld* World, const FTransform& Transform)
    {
        AWeaponBase* Weapon = World->SpawnActor<AWeaponBase>(WeaponClass, &Transform);
        Weapon->SetBaseDamage(BaseDamage);
        Weapon->SetFireRate(FireRate);
        Weapon->SetAmmoType(AmmoType);
        Weapon->SetMesh(MeshPath);
        for (auto& Mod : Modifiers) Weapon->AddModifier(Mod);
        return Weapon;
    }
private:
    TSubclassOf<AWeaponBase> WeaponClass = AWeaponBase::StaticClass();
    float BaseDamage = 10.f; float FireRate = 0.5f;
    TSubclassOf<UAmmoType> AmmoType = nullptr;
    TSoftObjectPtr<USkeletalMesh> MeshPath;
    TArray<TSubclassOf<UWeaponModifier>> Modifiers;
};

// 使用:链式调用清晰表达构建意图
AWeaponBase* Weapon = UWeaponBuilder()
    .SetBaseDamage(25.f)
    .SetFireRate(0.3f)
    .SetAmmoType(UHeavyAmmo::StaticClass())
    .AddModifier(UFireModifier::StaticClass())
    .AddModifier(UExplosiveModifier::StaticClass())
    .SetMesh(WeaponMeshAsset)
    .Build(GetWorld(), SpawnTransform);
  1. 建造者 vs 工厂:工厂关注"创建哪个对象",建造者关注"如何分步构建同一个复杂对象"。
  2. 面试官提示:Builder 适用于参数多且可选的对象创建;UE 编辑器属性面板本身就是建造者思想的可视化体现(分步设置参数);流式接口(Fluent Interface)让构建代码可读性极高;面试官可能追问 Builder vs Abstract Factory 的区别。

14.6 原型模式(Prototype)如何通过克隆创建对象?UE中DataTable/数据资产如何体现?

★★★ 困难

考点: 原型模式、克隆、数据资产复用

参考答案:

  1. 核心意图:通过复制已有实例来创建新对象,避免重新执行复杂初始化过程。
  2. UE 中的原型体现:
    • UObject::DuplicateObject:复制 UObject 及其子对象。
    • DataTable 行:每行数据是一个"原型",运行时通过 FindRow 获取副本并修改。
    • DataAsset:UPrimaryDataAsset 可作为模板,运行时 DuplicateObject 创建实例副本。
    • CDO(Class Default Object):每个 UClass 的默认对象就是原型,GetDefaultObject() 获取。
  3. 关键代码:
// DataTable 作为原型库
FWeaponTableRow* TemplateRow = WeaponTable->FindRow<FWeaponTableRow>(WeaponID, TEXT("Clone"));
if (TemplateRow)
{
    // 原型克隆:复制模板数据,修改特定属性
    FWeaponTableRow WeaponData = *TemplateRow;
    WeaponData.Damage *= PlayerDamageMultiplier;  // 基于原型修改
    WeaponData.FireRate *= PlayerSpeedBonus;
    SpawnWeapon(WeaponData);
}

// DuplicateObject 原型克隆
UWeaponModBase* ModTemplate = GetDefault<UFireMod>();
UWeaponModBase* ModInstance = DuplicateObject(ModTemplate, OwnerActor);
ModInstance->SetIntensity(0.5f);  // 基于原型修改

// 运行时创建原型副本(适合配置热重载)
UCLASS()
class UPrototypeManager : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public:
    template<typename T>
    T* ClonePrototype(FName PrototypeName)
    {
        T* Template = Prototypes.FindRef(PrototypeName);
        if (Template)
            return DuplicateObject<T>(Template, GetTransientPackage());
        return nullptr;
    }
private:
    TMap<FName, UObject*> Prototypes;
};
  1. 原型模式 vs 工厂模式:工厂创建全新对象,原型基于现有对象克隆;原型避免重复初始化开销,适合对象创建代价高的场景。
  2. 面试官提示:DuplicateObject 是 UE 中原型模式的核心 API;DataTable 的行数据就是原型数据,FindRow 返回副本而非引用,天然支持原型修改;CDO 用于构造时默认值原型;原型模式在配置驱动系统中非常常见。

14.7 抽象工厂模式(Abstract Factory)与工厂模式的区别是什么?

★★★ 困难

考点: 抽象工厂、工厂模式、产品族、可扩展性

参考答案:

  1. 核心区别:工厂模式创建单个产品,抽象工厂创建一族相关产品。
维度 工厂模式 抽象工厂
产品数量 单一产品 一族相关产品
扩展方式 新增工厂类 新增工厂类 + 产品族
适用场景 创建一种对象 创建一组配套对象
  1. UE 中的体现:
    • 编辑器工具:UFactory 是 UE 内置工厂模式,每种资产类型一个 Factory。
    • 抽象工厂:不同平台/风格的 UI 工厂——UMobileUIFactory / UConsoleUIFactory,各自创建一套 UI 组件(按钮、列表、弹窗)。
  2. 关键代码:
// 抽象工厂:为不同风格创建一套 UI 组件
UINTERFACE(MinimalAPI)
class UUIStyleFactory : public UInterface { GENERATED_BODY(); };
class IUIStyleFactory
{
    GENERATED_BODY()
public:
    virtual UButtonWidget* CreateButton() = 0;
    virtual UTextWidget* CreateText() = 0;
    virtual UPanelWidget* CreatePanel() = 0;
};

// 科幻风格工厂
UCLASS()
class USciFiUIFactory : public UObject, public IUIStyleFactory
{
    GENERATED_BODY()
public:
    virtual UButtonWidget* CreateButton() override
    { return NewObject<USciFiButtonWidget>(this); }
    virtual UTextWidget* CreateText() override
    { return NewObject<USciFiTextWidget>(this); }
    virtual UPanelWidget* CreatePanel() override
    { return NewObject<USciFiPanelWidget>(this); }
};

// 奇幻风格工厂
UCLASS()
class UFantasyUIFactory : public UObject, public IUIStyleFactory
{
    GENERATED_BODY()
public:
    virtual UButtonWidget* CreateButton() override
    { return NewObject<UFantasyButtonWidget>(this); }
    virtual UTextWidget* CreateText() override
    { return NewObject<UFantasyTextWidget>(this); }
    virtual UPanelWidget* CreatePanel() override
    { return NewObject<UFantasyPanelWidget>(this); }
};

// 使用:运行时切换风格,整套 UI 自动变化
void UGameUIManager::SetUIStyle(TScriptInterface<IUIStyleFactory> Factory)
{
    Button = Factory->CreateButton();
    Text = Factory->CreateText();
    Panel = Factory->CreatePanel();
}
  1. 工厂模式 UE 体现:UFactory(编辑器资产创建)、SpawnActor(Actor 创建)、NewObject(UObject 创建),每种都是单一产品工厂。
  2. 面试官提示:抽象工厂解决的是产品族一致性问题——同一风格的 UI 必须配套使用;新增产品族只需新增工厂类,符合开闭原则;工厂模式新增产品需修改工厂(违反 OCP),抽象工厂通过接口隔离解决;面试中可对比工厂方法→抽象工厂的演进路径。