ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

C++游戏开发中观察者模式的五大应用场景与UE5实践

C++游戏开发中观察者模式的五大应用场景与UE5实践 1. 项目概述为什么游戏开发离不开观察者模式在游戏开发这个行当里尤其是用C这种追求极致性能的语言时我们每天都在和各种“事件”打交道。玩家按下了跳跃键、敌人被击败、任务状态更新、UI需要刷新……这些事件就像游戏世界里的神经信号需要在不同的系统之间高效、准确地传递。如果每个系统都直接去“打听”其他系统的状态代码很快就会变成一团乱麻耦合度高得吓人改一处而动全身。这就是观察者模式Observer Pattern大显身手的地方。它是一种行为设计模式定义了一种一对多的依赖关系。简单来说就是让一个对象称为“主题”或“发布者”在状态改变时自动通知所有依赖它的对象称为“观察者”或“订阅者”而无需知道这些对象具体是谁。这就像你订阅了一个新闻频道新闻一更新所有订阅者都会收到推送但新闻编辑部并不需要知道每个订阅者的具体信息。在C游戏开发中观察者模式的价值尤为突出。游戏是一个由众多复杂、动态交互的子系统构成的实时系统。使用观察者模式我们可以将事件的生产者和消费者解耦让事件系统变得清晰、可维护、易于扩展。无论是处理用户输入、管理游戏状态还是协调音频、粒子等视觉效果观察者模式都能提供一种优雅的解决方案。而UE5Unreal Engine 5作为当今顶级的游戏引擎其内部大量运用了观察者模式的变体如委托和事件分发器为我们提供了绝佳的实践范例。接下来我将结合我多年的开发经验深入剖析观察者模式在C游戏开发中的五个典型应用场景并穿插UE5中的具体实现案例希望能给你带来实实在在的启发。2. 核心需求解析游戏事件系统的痛点与解耦之道在深入具体场景之前我们必须先理解游戏开发中事件处理的核心痛点以及观察者模式是如何精准地解决这些问题的。这决定了我们为什么非用它不可。2.1 传统紧耦合事件处理的典型困境假设我们正在开发一个简单的ARPG游戏。当玩家角色击败一个怪物时需要触发一系列动作增加玩家经验值、更新任务进度、播放胜利音效、显示伤害数字、可能还会触发一个成就。一种最直观但问题重重的实现方式是让Player类直接调用所有相关模块class Player { public: void DefeatEnemy(Enemy* enemy) { // 1. 自身逻辑 GainExp(enemy-GetRewardExp()); // 2. 直接调用任务系统 QuestManager::GetInstance()-UpdateQuestProgress(enemy-GetType()); // 3. 直接调用音频系统 AudioSystem::GetInstance()-PlaySound(Victory.wav); // 4. 直接调用UI系统 UIManager::GetInstance()-ShowDamageNumber(enemy-GetLastPosition(), enemy-GetLastDamage()); // 5. 直接调用成就系统 AchievementSystem::GetInstance()-Unlock(FirstBlood); // ... 未来可能还要添加更多调用 } };这种写法的问题显而易见高耦合Player类严重依赖QuestManager、AudioSystem等具体类。一旦这些类的接口发生变化或者我们需要替换成另一个音频系统就必须修改Player类的代码。难以扩展如果后期想增加一个“怪物死亡时掉落装备”的功能就必须再次修改Player::DefeatEnemy方法违反了“开闭原则”。职责混乱Player类的职责不再是单纯的“玩家行为”它变成了一个中央调度器这违反了“单一职责原则”。性能隐患所有调用都是同步的、直接的。如果某个被调用的系统如成就系统需要联网验证处理缓慢会直接阻塞玩家击败敌人的后续逻辑影响游戏流畅度。2.2 观察者模式提供的解耦方案观察者模式通过引入一个中间层——事件或消息——来解决上述问题。我们将“怪物被击败”定义为一个事件。Player类作为事件的发布者Subject只负责在合适的时机发布这个事件而完全不关心谁会对这个事件感兴趣。另一方面任务系统、音频系统、UI系统等则作为观察者Observer它们主动向事件系统订阅“怪物被击败”事件。当事件发生时事件系统会逐个通知所有订阅者由它们各自执行相应的逻辑。这样改造后的代码结构Player类player-OnEnemyDefeated(enemy);// 仅仅发布事件QuestSystem类订阅OnEnemyDefeated事件收到后更新任务。AudioSystem类订阅OnEnemyDefeated事件收到后播放音效。AchievementSystem类订阅OnEnemyDefeated事件收到后检查成就。各个系统之间不再有直接的调用关系它们只通过事件这个抽象接口进行通信。系统的增减、修改都不会影响到事件发布者和其他订阅者。这正是观察者模式在游戏开发中最大的魅力所在它让复杂的游戏系统架构变得模块化、清晰且富有弹性。注意在C中实现观察者模式需要特别注意对象生命周期管理。一个常见的坑是观察者对象已经被销毁了但没有从主题的订阅列表中移除导致主题后续通知时访问野指针引发程序崩溃。通常的解决方案是使用智能指针如std::weak_ptr或在观察者析构时自动取消注册的RAII机制。3. 场景一游戏状态管理与全局事件通知游戏状态是游戏逻辑的基石例如游戏开始、暂停、结束、关卡切换等。这些状态的改变往往需要广播给游戏的几乎所有子系统。观察者模式是管理这类全局性状态变化的理想工具。3.1 经典实现自定义游戏状态事件总线我们可以构建一个简单的全局事件总线Event Bus或游戏状态管理器作为主题。// GameState.h - 定义状态和事件 enum class EGameState { Loading, MainMenu, Playing, Paused, GameOver }; struct GameStateChangedEvent { EGameState PreviousState; EGameState NewState; }; // IGameStateObserver.h - 观察者接口 class IGameStateObserver { public: virtual ~IGameStateObserver() default; virtual void OnGameStateChanged(const GameStateChangedEvent Event) 0; }; // GameStateManager.h - 主题单例 class GameStateManager { public: static GameStateManager GetInstance(); void ChangeState(EGameState NewState); EGameState GetCurrentState() const { return CurrentState; } void AddObserver(IGameStateObserver* Observer); void RemoveObserver(IGameStateObserver* Observer); private: GameStateManager(); EGameState CurrentState EGameState::MainMenu; std::vectorIGameStateObserver* Observers; // 简单实现生产环境需考虑线程安全 void NotifyObservers(const GameStateChangedEvent Event); };// GameStateManager.cpp - 实现 void GameStateManager::ChangeState(EGameState NewState) { if (NewState CurrentState) return; GameStateChangedEvent Event{CurrentState, NewState}; EGameState OldState CurrentState; CurrentState NewState; // 状态转换前可以进行一些验证 if (OldState EGameState::Playing NewState EGameState::Paused) { // 例如检查是否允许暂停 } NotifyObservers(Event); // 关键通知所有观察者 } void GameStateManager::NotifyObservers(const GameStateChangedEvent Event) { // 注意遍历时可能有观察者移除自身需小心处理迭代器失效 for (auto* Observer : Observers) { if (Observer) { Observer-OnGameStateChanged(Event); } } }3.2 订阅者示例UI、音频与逻辑系统现在其他系统可以轻松地订阅状态变化// UIManager.cpp - 在初始化时订阅 UIManager::UIManager() { GameStateManager::GetInstance().AddObserver(this); } UIManager::~UIManager() { GameStateManager::GetInstance().RemoveObserver(this); } void UIManager::OnGameStateChanged(const GameStateChangedEvent Event) { switch (Event.NewState) { case EGameState::Playing: HideMainMenu(); ShowHUD(); break; case EGameState::Paused: ShowPauseMenu(); break; case EGameState::GameOver: ShowGameOverScreen(CalculateScore()); break; // ... 其他状态处理 } }// AudioManager.cpp - 音频系统响应状态变化 void AudioManager::OnGameStateChanged(const GameStateChangedEvent Event) { if (Event.NewState EGameState::Playing Event.PreviousState EGameState::MainMenu) { // 从菜单进入游戏切换背景音乐 StopMusic(MenuMusic); PlayMusic(GameplayMusic, true /* loop */); } else if (Event.NewState EGameState::Paused) { // 游戏暂停降低背景音乐音量或播放暂停音效 SetMusicVolume(0.3f); } }3.3 UE5案例利用引擎内置的全局委托在UE5中我们不必自己从头实现事件总线。引擎提供了强大的委托Delegate系统可以非常方便地实现观察者模式。对于游戏状态变化我们可以使用全局的动态多播委托。// 在某个全局头文件如MyGameInstance.h中声明委托类型 DECLARE_MULTICAST_DELEGATE_TwoParams(FOnGameStateChanged, EGameState /*OldState*/, EGameState /*NewState*/); extern FOnGameStateChanged OnGameStateChangedDelegate; // 声明一个全局委托实例 // 在GameStateManager中触发事件 void AMyGameStateManager::ChangeState(EGameState NewState) { EGameState OldState CurrentState; CurrentState NewState; // 广播给所有绑定到这个委托的函数 OnGameStateChangedDelegate.Broadcast(OldState, NewState); } // 在任何需要监听的类中绑定通常在BeginPlay中 void AMyHUD::BeginPlay() { Super::BeginPlay(); // 使用AddUObject绑定UObject成员函数引擎会处理生命周期 OnGameStateChangedDelegate.AddUObject(this, AMyHUD::HandleGameStateChanged); } void AMyHUD::HandleGameStateChanged(EGameState OldState, EGameState NewState) { if (NewState EGameState::Playing) { // 更新HUD显示... } } // 注意在EndPlay或析构中最好解绑避免回调时对象已无效 void AMyHUD::EndPlay(const EEndPlayReason::Type EndPlayReason) { OnGameStateChangedDelegate.RemoveAll(this); Super::EndPlay(EndPlayReason); }实操心得使用全局委托时一定要管理好绑定的生命周期。对于UObject优先使用AddUObject它内部使用了弱引用安全性更高。对于非UObject的C类可以使用AddRaw但必须确保在对象销毁前手动调用Remove或RemoveAll否则会导致崩溃。这是一个非常高频的踩坑点。4. 场景二实体组件间通信与属性监听在基于组件Component的架构中如UE5的Actor-Component模型一个游戏实体Actor由多个组件构成。组件之间经常需要通信但又不能产生紧密耦合。观察者模式是实现组件间松散耦合通信的黄金法则。4.1 问题组件A如何通知组件B假设我们有一个HealthComponent管理生命值一个UIFloatingBarComponent负责在角色头顶显示血条。当生命值发生变化时血条需要更新。 最差的做法是让HealthComponent直接获取并调用UIFloatingBarComponent的方法。较好的做法是让血条组件每帧去查询生命值但这会产生不必要的性能开销。最佳实践就是让HealthComponent作为主题UIFloatingBarComponent作为观察者。4.2 实现一个通用的属性监听器我们可以为HealthComponent设计一个“生命值改变”事件。// HealthComponent.h class HealthComponent { public: // 委托类型参数为当前生命值、最大生命值、变化量 using FOnHealthChangedDelegate std::functionvoid(float, float, float); void TakeDamage(float Amount); void Heal(float Amount); // 供观察者订阅的方法 void AddOnHealthChangedListener(const FOnHealthChangedDelegate Delegate) { OnHealthChangedDelegates.push_back(Delegate); } // 注意实际项目中需要提供移除监听器的方法 private: float CurrentHealth; float MaxHealth; std::vectorFOnHealthChangedDelegate OnHealthChangedDelegates; void SetHealth(float NewHealth) { float OldHealth CurrentHealth; CurrentHealth FMath::Clamp(NewHealth, 0.0f, MaxHealth); float Delta CurrentHealth - OldHealth; // 通知所有监听者 for (const auto Delegate : OnHealthChangedDelegates) { if (Delegate) { Delegate(CurrentHealth, MaxHealth, Delta); } } } };// UIFloatingBarComponent.cpp UIFloatingBarComponent::UIFloatingBarComponent(HealthComponent* TargetHealthComp) { if (TargetHealthComp) { // 订阅生命值变化事件 auto Delegate [this](float CurrentHP, float MaxHP, float Delta) { this-OnHealthUpdated(CurrentHP, MaxHP); }; TargetHealthComp-AddOnHealthChangedListener(Delegate); } } void UIFloatingBarComponent::OnHealthUpdated(float CurrentHP, float MaxHP) { float Percent CurrentHP / MaxHP; UpdateBarVisual(Percent); // 更新血条UI显示 if (Delta 0) { ShowDamageEffect(); // 显示受伤特效 } }4.3 UE5案例使用UE事件分发器Event Dispatcher在UE5的蓝图和C混合编程中事件分发器是连接两者的桥梁其本质也是观察者模式。我们可以在C中定义事件分发器在蓝图中绑定事件。首先在C组件中声明和调用// MyHealthComponent.h (继承自UActorComponent) UCLASS(ClassGroup(Custom), meta(BlueprintSpawnableComponent)) class UMyHealthComponent : public UActorComponent { GENERATED_BODY() public: // 声明一个多播事件分发器蓝图可以绑定 DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnHealthChangedSignature, float, CurrentHealth, float, MaxHealth, float, Delta); UPROPERTY(BlueprintAssignable, Category Health) // BlueprintAssignable是关键 FOnHealthChangedSignature OnHealthChanged; UFUNCTION(BlueprintCallable, Category Health) void TakeDamage(float Damage); private: UPROPERTY(EditDefaultsOnly, Category Health) float MaxHealth 100.0f; float CurrentHealth; void SetHealth(float NewHealth); };// MyHealthComponent.cpp void UMyHealthComponent::BeginPlay() { Super::BeginPlay(); CurrentHealth MaxHealth; } void UMyHealthComponent::TakeDamage(float Damage) { float NewHealth CurrentHealth - Damage; SetHealth(NewHealth); } void UMyHealthComponent::SetHealth(float NewHealth) { float OldHealth CurrentHealth; CurrentHealth FMath::Clamp(NewHealth, 0.0f, MaxHealth); float Delta CurrentHealth - OldHealth; // 广播事件所有绑定到这个分发器的蓝图函数都会被调用。 OnHealthChanged.Broadcast(CurrentHealth, MaxHealth, Delta); }然后在蓝图中其他组件比如一个蓝图实现的UI_HealthBar控件可以轻松地绑定到这个事件获取拥有UMyHealthComponent的Actor。获取UMyHealthComponent组件引用。在组件细节面板找到On Health Changed事件点击“”号添加绑定。在绑定的蓝图序列中更新血条Widget的百分比。这样做的好处是游戏逻辑伤害计算在高效的C中完成而UI表现血条动画、特效在灵活的蓝图中实现两者通过事件分发器完美解耦。策划或美术人员可以在不修改C代码的情况下调整血条的表现形式。注意事项UE的多播委托DECLARE_MULTICAST_DELEGATE...和事件分发器DECLARE_DYNAMIC_MULTICAST_DELEGATE...功能强大但需注意区分。动态委托DYNAMIC支持序列化可用于蓝图非动态委托仅限C使用性能更优。根据通信范围C内部还是C-蓝图正确选择。5. 场景三用户输入与控制的响应式处理处理用户输入是游戏交互的核心。输入事件按键、鼠标点击、手柄摇杆是典型的“主题”而游戏中的各种控制器角色移动、摄像机控制、技能释放则是“观察者”。观察者模式可以让输入处理变得非常模块化。5.1 构建一个输入事件系统一个简单的输入管理器可以维护一张映射表将物理输入如键盘按键E映射到逻辑输入事件如Interact。// InputManager.h class InputManager { public: enum class EInputEvent { Pressed, Released, Held }; struct InputActionEvent { std::string ActionName; // 如 Jump, Attack, Interact EInputEvent EventType; // 可附加额外数据如按键强度、位置等 }; using FInputActionDelegate std::functionvoid(const InputActionEvent); void BindAction(const std::string ActionName, EInputEvent EventType, const FInputActionDelegate Delegate); void ProcessRawInput(int KeyCode, bool IsPressed); // 由引擎每帧调用 private: std::mapint, std::string KeyToActionMap; // 物理键到逻辑动作的映射 std::mapstd::pairstd::string, EInputEvent, std::vectorFInputActionDelegate ActionBindings; };// InputManager.cpp - 处理与通知 void InputManager::ProcessRawInput(int KeyCode, bool IsPressed) { auto it KeyToActionMap.find(KeyCode); if (it KeyToActionMap.end()) return; const std::string ActionName it-second; EInputEvent EventType IsPressed ? EInputEvent::Pressed : EInputEvent::Released; InputActionEvent Event{ActionName, EventType}; auto key std::make_pair(ActionName, EventType); auto bindingIt ActionBindings.find(key); if (bindingIt ! ActionBindings.end()) { for (const auto Delegate : bindingIt-second) { Delegate(Event); } } // 对于Held事件通常在一个独立的更新循环中处理这里简化 }5.2 控制器作为观察者进行订阅不同的控制器订阅自己关心的输入动作。// PlayerMovementComponent.cpp void PlayerMovementComponent::SetupInput(InputManager Input) { Input.BindAction(MoveForward, EInputEvent::Held, [this](const InputActionEvent Event) { this-AddMovementInput(FVector::ForwardVector, 1.0f); // 假设有GetAxisValue }); Input.BindAction(Jump, EInputEvent::Pressed, [this](const InputActionEvent Event) { if (this-IsOnGround()) { this-Jump(); } }); } // InteractionComponent.cpp void InteractionComponent::SetupInput(InputManager Input) { Input.BindAction(Interact, EInputEvent::Pressed, [this](const InputActionEvent Event) { AActor* ActorToInteract this-FindNearestInteractable(); if (ActorToInteract) { this-PerformInteraction(ActorToInteract); } }); }5.3 UE5案例增强输入系统Enhanced Input System与委托UE5推荐使用功能更强大的“增强输入系统”。它本身已经是一个高度可配置的观察者模式实现。我们定义Input Action如IA_Jump然后可以在C或蓝图中绑定回调。在C中绑定增强输入// MyPlayerController.cpp void AMyPlayerController::SetupInputComponent() { Super::SetupInputComponent(); if (UEnhancedInputComponent* EnhancedInputComponent CastUEnhancedInputComponent(InputComponent)) { // 获取输入动作上下文和资产通常在项目设置中配置 // 假设 JumpAction 是一个 UInputAction* 类型的成员变量已在BeginPlay中赋值 // 绑定“触发”事件 EnhancedInputComponent-BindAction(JumpAction, ETriggerEvent::Started, this, AMyPlayerController::OnJumpStarted); EnhancedInputComponent-BindAction(JumpAction, ETriggerEvent::Completed, this, AMyPlayerController::OnJumpCompleted); // 绑定“持续”事件如移动 EnhancedInputComponent-BindAction(MoveAction, ETriggerEvent::Triggered, this, AMyPlayerController::OnMoveTriggered); } } void AMyPlayerController::OnJumpStarted(const FInputActionValue Value) { // 处理跳跃按下逻辑 if (GetCharacter()) { GetCharacter()-Jump(); } } void AMyPlayerController::OnMoveTriggered(const FInputActionValue Value) { // Value 是一个 FVector2D包含输入轴向值 FVector2D MovementVector Value.GetFVector2D(); if (APawn* ControlledPawn GetPawn()) { // 将输入转化为移动 ControlledPawn-AddMovementInput(ControlledPawn-GetActorForwardVector(), MovementVector.Y); ControlledPawn-AddMovementInput(ControlledPawn-GetActorRightVector(), MovementVector.X); } }这里的观察者模式体现在UEnhancedInputComponent是输入事件的主题它内部维护了输入动作到一系列委托的映射。当输入系统检测到原始输入并经过处理后会触发对应的Input Action然后UEnhancedInputComponent会广播调用所有绑定到该Action的特定触发事件Started,Triggered,Completed等的委托。我们的AMyPlayerController中的成员函数就是观察者。实操心得增强输入系统比旧的输入系统更复杂但功能也更强大支持输入修饰键、上下文处理、手势等。在绑定委托时务必理解ETriggerEvent的各个枚举值的含义如Started按下瞬间、Triggered按住每帧、Completed松开瞬间、Canceled输入被取消根据不同的游戏行为需求选择正确的事件类型这是实现精准输入响应的关键。6. 场景四成就与统计系统的无侵入式集成成就系统如“击败100个敌人”、“无伤通关”和统计系统如“游戏时长”、“收集品数量”需要监听游戏中发生的各种事件但它们本身不应该侵入核心游戏逻辑。观察者模式是实现这种“无侵入式”监控的完美方案。6.1 成就系统作为全局事件的静默观察者成就系统只需要在游戏初始化时静静地订阅它关心的所有事件。之后它就像游戏的“监听者”在后台记录和分析。// AchievementSystem.h class AchievementSystem : public IGameStateObserver, public ICombatEventObserver, public IInventoryEventObserver /* 实现多个观察者接口 */ { public: void Initialize() { // 订阅各种事件 GameStateManager::GetInstance().AddObserver(this); CombatEventBus::GetInstance().AddObserver(this); InventoryEventBus::GetInstance().AddObserver(this); // ... 订阅更多 } // 实现各个接口的OnXXX方法 void OnGameStateChanged(const GameStateChangedEvent Event) override { if (Event.NewState EGameState::GameOver) { CheckForNoDamageRun(); // 检查无伤通关成就 } } void OnEnemyDefeated(const EnemyDefeatedEvent Event) override { EnemyDefeatCount; if (EnemyDefeatCount 100) { UnlockAchievement(Slayer); } if (Event.EnemyType EEnemyType::Boss) { UnlockAchievement(BossVanquisher); } } void OnItemCollected(const ItemCollectedEvent Event) override { CollectedItemIds.insert(Event.ItemId); if (CollectedItemIds.size() TotalUniqueItems) { UnlockAchievement(Completionist); } } private: int EnemyDefeatCount 0; std::setItemId CollectedItemIds; void UnlockAchievement(const std::string AchievementId); };6.2 构建专门的事件总线Event Bus对于成就系统这种需要监听多种不同来源事件的场景一个全局的、类型安全的事件总线非常有用。我们可以利用C的模板和变参模板来实现一个更通用的事件系统。// EventBus.h - 简化版类型安全事件总线 class IEvent {}; templatetypename TEvent class EventDispatcher { public: using EventHandler std::functionvoid(const TEvent); void Subscribe(EventHandler Handler) { handlers.push_back(Handler); } void Publish(const TEvent Event) { for (auto handler : handlers) { handler(Event); } } private: std::vectorEventHandler handlers; }; class EventBus { public: templatetypename TEvent static EventDispatcherTEvent GetDispatcher() { static EventDispatcherTEvent dispatcher; return dispatcher; } };使用这个事件总线// 定义具体事件类型 struct EnemyDefeatedEvent : public IEvent { EnemyType Type; Vector3 Location; }; struct ItemCollectedEvent : public IEvent { ItemId Id; PlayerId CollectorId; }; // 发布事件在游戏逻辑中 void CombatManager::OnEnemyKilled(Enemy* enemy) { EnemyDefeatedEvent event{enemy-GetType(), enemy-GetLocation()}; EventBus::GetDispatcherEnemyDefeatedEvent().Publish(event); } // 订阅事件在成就系统中 void AchievementSystem::Initialize() { auto enemyDefeatedDispatcher EventBus::GetDispatcherEnemyDefeatedEvent(); enemyDefeatedDispatcher.Subscribe([this](const EnemyDefeatedEvent e) { this-OnEnemyDefeated(e); }); auto itemCollectedDispatcher EventBus::GetDispatcherItemCollectedEvent(); itemCollectedDispatcher.Subscribe([this](const ItemCollectedEvent e) { if (e.CollectorId GetLocalPlayerId()) { this-OnItemCollected(e); } }); }6.3 UE5案例利用游戏能力系统Gameplay Ability System的标签和属性变化UE5的Gameplay Ability System (GAS) 本身就是一个基于事件驱动的复杂系统非常适合用于成就和统计。我们可以监听Attribute属性的变化或Gameplay Tag标签的添加移除。例如监听玩家生命值属性变化来达成“低血量反杀”成就// 在玩家角色或玩家状态的初始化中 void AMyCharacter::SetupAchievementListeners() { if (UAbilitySystemComponent* ASC GetAbilitySystemComponent()) { // 监听“Health”属性的变化 ASC-GetGameplayAttributeValueChangeDelegate(UMyAttributeSet::GetHealthAttribute()).AddUObject(this, AMyCharacter::OnHealthChanged_Achievement); // 监听“击杀”标签的授予 ASC-RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName(Event.Kill)), EGameplayTagEventType::NewOrRemoved).AddUObject(this, AMyCharacter::OnKillTagChanged); } } void AMyCharacter::OnHealthChanged_Achievement(const FOnAttributeChangeData Data) { float NewHealth Data.NewValue; float OldHealth Data.OldValue; if (NewHealth 0 NewHealth 20.0f OldHealth 20.0f) { // 生命值首次低于20% bWasLowHealth true; LowHealthTime GetWorld()-GetTimeSeconds(); } else if (bWasLowHealth NewHealth OldHealth NewHealth 50.0f) { // 从低血量恢复到50%以上 bWasLowHealth false; } } void AMyCharacter::OnKillTagChanged(const FGameplayTag Tag, int32 NewCount) { if (Tag FGameplayTag::RequestGameplayTag(FName(Event.Kill)) NewCount 0) { // 刚刚完成了一次击杀 if (bWasLowHealth (GetWorld()-GetTimeSeconds() - LowHealthTime 10.0f)) { // 在低血量状态10秒内完成击杀 UnlockAchievement(ComebackKing); } // 重置标记 bWasLowHealth false; } }GAS提供了一套非常强大的事件监听机制通过属性变化委托和标签事件我们可以以声明式的方式监听游戏内的各种状态变化从而驱动成就和统计逻辑完全无需修改核心的战斗或属性计算代码。常见问题在实现通用事件总线时一个棘手的问题是事件类型的生命周期和静态存储。上面的简化版使用了函数内的静态变量这在大多数情况下是安全的。但在动态模块加载/卸载如插件的复杂项目中需要更精细的管理避免在模块卸载后还去访问静态事件分发器。可以考虑使用一个全局的、由智能指针管理的映射来存储分发器实例。7. 场景五资源加载与异步任务的状态通知现代游戏资源庞大异步加载Async Loading是保证游戏流畅体验的关键。异步操作的本质是“开始一个任务等它完成后再通知我”这正是观察者模式的用武之地。我们可以将异步加载器作为主题将需要资源的系统如关卡、UI、角色作为观察者。7.1 实现一个基础的异步加载管理器// AsyncLoader.h class AsyncLoader { public: using LoadCompleteDelegate std::functionvoid(void* LoadedResource, const std::string Path, bool bSuccess); using LoadHandle uint64_t; LoadHandle LoadAssetAsync(const std::string AssetPath, const LoadCompleteDelegate Callback); void CancelLoad(LoadHandle Handle); void Update(); // 每帧调用检查加载状态并派发完成事件 private: struct LoadRequest { LoadHandle Handle; std::string Path; LoadCompleteDelegate Callback; std::futurestd::pairvoid*, bool FutureResult; // 使用std::future模拟异步 std::chrono::steady_clock::time_point StartTime; }; std::unordered_mapLoadHandle, LoadRequest PendingRequests; std::atomicLoadHandle NextHandleId{1}; };// AsyncLoader.cpp AsyncLoader::LoadHandle AsyncLoader::LoadAssetAsync(const std::string AssetPath, const LoadCompleteDelegate Callback) { LoadHandle handle NextHandleId; LoadRequest request; request.Handle handle; request.Path AssetPath; request.Callback Callback; request.StartTime std::chrono::steady_clock::now(); // 启动一个异步任务来模拟加载实际中可能调用文件IO线程池 request.FutureResult std::async(std::launch::async, [assetPath]() - std::pairvoid*, bool { // 模拟耗时加载过程 std::this_thread::sleep_for(std::chrono::milliseconds(100)); void* resource MockLoadResourceFromDisk(assetPath.c_str()); bool success (resource ! nullptr); return {resource, success}; }); PendingRequests[handle] std::move(request); return handle; } void AsyncLoader::Update() { std::vectorLoadHandle completedHandles; for (auto [handle, request] : PendingRequests) { // 检查future是否就绪 if (request.FutureResult.valid() request.FutureResult.wait_for(std::chrono::seconds(0)) std::future_status::ready) { auto [resource, success] request.FutureResult.get(); if (request.Callback) { request.Callback(resource, request.Path, success); } completedHandles.push_back(handle); } } // 清理已完成的请求 for (auto handle : completedHandles) { PendingRequests.erase(handle); } }7.2 资源使用者作为观察者// LevelStreamer.cpp void LevelStreamer::StreamInLevel(const std::string LevelName) { // 显示加载界面 ShowLoadingScreen(); // 异步加载关卡资源 std::string assetPath Levels/ LevelName .pak; LoadHandle handle AsyncLoader::GetInstance().LoadAssetAsync(assetPath, [this](void* loadedResource, const std::string path, bool success) { // 这是加载完成后的回调观察者收到通知 this-OnLevelLoaded(loadedResource, path, success); }); CurrentLoadHandle handle; } void LevelStreamer::OnLevelLoaded(void* LevelResource, const std::string Path, bool bSuccess) { if (bSuccess) { // 初始化关卡隐藏加载界面 InitializeLevel(LevelResource); HideLoadingScreen(); } else { // 加载失败显示错误信息 ShowErrorMessage(Failed to load level: Path); HideLoadingScreen(); } }7.3 UE5案例使用异步资源加载与回调委托UE5提供了完善的异步资源加载机制其核心也是通过委托回调来通知加载完成。使用FStreamableManager和TSoftObjectPtr// 在头文件中声明 TSharedPtrFStreamableHandle LevelLoadHandle; TSoftObjectPtrUWorld LevelToLoad; // 在需要加载的地方 void AMyGameMode::LoadNextLevelAsync() { FSoftObjectPath LevelPath(TEXT(/Game/Maps/NextMap.NextMap)); LevelToLoad TSoftObjectPtrUWorld(LevelPath); FStreamableManager Streamable UAssetManager::GetStreamableManager(); // 绑定加载完成后的委托 LevelLoadHandle Streamable.RequestAsyncLoad(LevelToLoad.ToSoftObjectPath(), FStreamableDelegate::CreateUObject(this, AMyGameMode::OnLevelAssetLoaded)); } void AMyGameMode::OnLevelAssetLoaded() { if (LevelToLoad.IsValid()) { UWorld* LoadedWorld LevelToLoad.Get(); // 现在可以安全地使用LoadedWorld了例如进行关卡切换 UE_LOG(LogTemp, Log, TEXT(Level asset loaded successfully!)); // 注意这只是资源加载完成实际的关卡切换可能需要调用 OpenLevel } else { UE_LOG(LogTemp, Error, TEXT(Failed to load level asset.)); } // 释放句柄 LevelLoadHandle.Reset(); }使用AsyncLoadAsset节点蓝图或LoadAssetAsyncC在蓝图中你可以直接使用Async Load Asset节点它本质上就是封装了异步加载和完成事件分发。在C中也可以使用UAssetManager的LoadAssetAsync函数它返回一个FStreamableHandle并允许你绑定一个完成委托。使用LoadPackageAsync加载整个地图包对于关卡流送UE5更推荐使用LoadPackageAsync。void AMyLevelStreamer::StreamLevelByName(const FName LevelName) { // 显示加载UI ShowLoadingWidget(); // 异步加载关卡包 LoadPackageAsync(LevelName.ToString(), FLoadPackageAsyncDelegate::CreateLambda([](const FName PackageName, UPackage* LoadedPackage, EAsyncLoadingResult::Type Result) { // 这个Lambda函数在加载完成后被调用在主线程 if (Result EAsyncLoadingResult::Succeeded) { // 关卡资源加载成功现在可以将其添加到世界中进行流送 FLatentActionInfo LatentInfo; LatentInfo.CallbackTarget this; LatentInfo.ExecutionFunction OnLevelStreamedIn; LatentInfo.Linkage 0; LatentInfo.UUID __LINE__; // 一个简单的唯一标识 UGameplayStatics::LoadStreamLevel(this, LevelName, true, true, LatentInfo); } else { // 加载失败 UE_LOG(LogTemp, Error, TEXT(Failed to async load package: %s), *LevelName.ToString()); HideLoadingWidget(); } }), 0, // 优先级 PKG_ContainsMap // 标志位表示加载的是地图包 ); } void AMyLevelStreamer::OnLevelStreamedIn() { // 关卡已成功流送并添加到世界 HideLoadingWidget(); OnLevelLoaded.Broadcast(); // 可以广播一个自定义事件通知其他系统 }注意事项异步加载的回调委托通常在游戏线程主线程中被触发但加载过程本身是在其他线程进行的。这意味着你可以在回调中安全地修改UObject属性、创建UI等。然而你必须妥善管理异步加载句柄如FStreamableHandle的生命周期通常在加载完成后重置或释放它。另外要处理游戏中途取消加载或对象被销毁的情况避免回调访问无效内存。在UE5中使用CreateUObject或CreateWeakLambda可以很好地利用引擎的垃圾回收机制来避免野指针问题但自己编写的C异步管理器则需要格外小心。
返回列表