04 英雄战斗与反馈 UI

上一章:[[03-近战连招系统]] 返回:[[00-课程索引]] 下一章:[[05-敌人AI]]

[!info] 本章重点
本章建立完整闭环:碰撞只报告命中,Gameplay Ability 构造伤害 Spec,Execution Calculation 计算数值,AttributeSet 消费元属性,Gameplay Event/Cue 负责受击和表现,UI Component 通过委托推送数据。

战斗主链

1
2
3
4
5
6
7
Montage NotifyState → 开启武器碰撞
→ Weapon BeginOverlap → CombatComponent 去重
→ Shared.Event.MeleeHit → 攻击 Ability
→ Make GameplayEffectSpec → Apply to Target ASC
→ GEExecCalc_DamageTaken → DamageTaken Meta Attribute
→ AttributeSet::PostGameplayEffectExecute
→ CurrentHealth / Dead Tag / UI Delegate

P47 Hero Combat Section Overview(02:06)

章节概览,无独立提交。路线:敌人框架 → 属性初始化 → 碰撞 → 伤害 → 受击/死亡 → UI。

P48 Set Up Enemy Character(11:56)

提交c2be3f2 SetUpEnemyCharacterDone

新增:

1
2
3
4
AWarriorEnemyCharacter : AWarriorBaseCharacter
UWarriorEnemyGameplayAbility : UWarriorGameplayAbility
UEnemyCombatComponent : UPawnCombatComponent
UDataAsset_EnemyStartUpData : UDataAsset_StartUpDataBase

AWarriorEnemyCharacter::PossessedBy 先通过基类执行 ASC.InitAbilityActorInfo,再初始化敌人 Startup Data。敌人能力基类提供 Enemy Character/Combat Component 的强类型访问。

P49 Gruntling Guardian(10:29)

提交3f03f4d BeforeGruntling + c974653 Guardian done;资产课。

资产层级:

1
2
3
4
5
6
BP_EnemyCharacter_Base
└─ BP_Gruntling_Base
└─ BP_Gruntling_Guardian

ABP_Enemy_Base
└─ ABP_Guardian → BS_Guardian_Default

配置 Mesh、AnimBP、Character Movement 和 Guardian 专属外观。骨架 Socket、材质和动画状态机需在对应资产中核对。

P50 Asynchronous Loading(14:38)

提交ed60d33 AsyncLoading Done

1
2
3
4
5
6
7
8
9
10
11
12
UAssetManager::GetStreamableManager().RequestAsyncLoad(
CharacterStartUpData.ToSoftObjectPath(),
FStreamableDelegate::CreateLambda([this, AbilityApplyLevel]()
{
if (UDataAsset_StartUpDataBase* LoadedData = CharacterStartUpData.Get())
{
LoadedData->GiveToAbilitySystemComponent(
WarriorAbilitySystemComponent,
AbilityApplyLevel);
}
})
);
  • Hero 单实例可同步加载;批量敌人生成改用异步避免卡顿。
  • 回调执行前 Owner 可能销毁,生产代码应使用弱引用或再次校验 IsValid(this)

P51 Spawn Enemy Weapon(07:51)

提交0653de7

资产:GA_Guardian_SpawnWeaponGA_Shared_SpawnWeapon_BaseBP_Guardian_Weapon。新增 Tag:Enemy.Weapon

1
2
3
4
5
DA_Guardian 授予 SpawnWeapon Ability
→ OnGiven 自动激活
→ Spawn Guardian Weapon
→ 设置 Instigator/Owner 并附着 Socket
→ RegisterSpawnedWeapon(Enemy.Weapon, Weapon, true)

P52 Attribute Set(07:08)

提交94fbaff Attribute set done

UWarriorAttributeSet 最终属性:

1
2
3
4
5
6
7
FGameplayAttributeData CurrentHealth;
FGameplayAttributeData MaxHealth;
FGameplayAttributeData CurrentRage;
FGameplayAttributeData MaxRage;
FGameplayAttributeData AttackPower;
FGameplayAttributeData DefensePower;
FGameplayAttributeData DamageTaken;

每个属性使用 ATTRIBUTE_ACCESSORS 生成 Getter、Setter、Init 和 Attribute Getter。DamageTaken 是元属性,只作为一次伤害结算中间值。

P53 Gameplay Effect(09:54)

提交566432d;资产课。

  • CT_HeroStats:按等级提供属性曲线。
  • GE_Hero_StartUp:初始化属性。
  • GE_Hero_Static:长期状态/属性效果。

Gameplay Effect 的 Instant/Duration/Infinite、Modifier 和 Granted Tag 均在资产中配置,不能只靠文件名猜测精确值。

P54 Apply Gameplay Effect To Self(05:49)

提交cb99436

Startup Data 新增:

1
TArray<TSubclassOf<UGameplayEffect>> StartUpGameplayEffects;

应用:

1
2
3
UGameplayEffect* EffectCDO = EffectClass->GetDefaultObject<UGameplayEffect>();
InASCToGive->ApplyGameplayEffectToSelf(
EffectCDO, ApplyLevel, InASCToGive->MakeEffectContext());

这使属性初始化完全由 Data Asset + GE 驱动,而不是角色构造函数硬编码。

P55 Init Enemy Attributes(09:25)

提交b094b09

资产:CT_GuardianStatsGE_Guardian_StartUpGE_Enemy_StaticDA_Guardian。敌人异步加载 Startup Data 后,以对应 Ability Level 应用启动 GE;Hero 和 Enemy 共用同一 AttributeSet 类型,数值由不同 GE/CurveTable 决定。

P56 Pawn Combat Interface(19:58)

提交076b60f

1
2
3
4
class IPawnCombatInterface
{
virtual UPawnCombatComponent* GetPawnCombatComponent() const = 0;
};
1
2
3
BaseCharacter:默认返回 nullptr
HeroCharacter:返回 HeroCombatComponent
EnemyCharacter:返回 EnemyCombatComponent

AnimNotifyState、Ability 和函数库依赖接口,不需要知道 Hero/Enemy 具体类型。

P57 Toggle Weapon Collision(08:12)

提交ab6c256

1
2
3
4
5
6
enum class EToggleDamageType : uint8
{
CurrentEquippedWeapon,
LeftHand,
RightHand
};
1
2
3
4
5
6
7
8
ANS_ToggleWeaponCollision Begin
→ ToggleWeaponCollision(true)
→ Weapon Collision = QueryOnly

ANS End
→ ToggleWeaponCollision(false)
→ Collision = NoCollision
→ OverlappedActors.Empty()

一次攻击窗口维护命中去重数组,结束时清空。

P58 On Weapon Begin Overlap(10:12)

提交babc294

AWarriorWeaponBase::OnCollisionBoxBeginOverlap

1
2
3
4
取得 Weapon Instigator Pawn
→ OtherActor 转为 Pawn
→ IsTargetPawnHostile
→ OnWeaponHitTarget.ExecuteIfBound(OtherActor)

武器只检测交互,不直接计算伤害。敌我过滤最终依赖双方 Controller 的 Generic Team ID。

P59 On Target Interacted(08:35)

提交3edc506

1
2
3
DECLARE_DELEGATE_OneParam(FOnTargetInteractedDelegate, AActor*)
FOnTargetInteractedDelegate OnWeaponHitTarget;
FOnTargetInteractedDelegate OnWeaponPulledFromTarget;

注册武器时,Combat Component 用 BindUObject 绑定命中和离开回调。

[!bug] 公开最终源码中 OnCollisionBoxEndOverlap 误调用 OnWeaponHitTarget,而不是 OnWeaponPulledFromTarget。这会影响拔出武器事件和 Hit Pause 恢复链,笔记按真实源码保留此问题。

P60 Notify Melee Hit(11:18)

提交646c6dd。新增 Shared.Event.MeleeHit

1
2
3
4
FGameplayEventData Data;
Data.Instigator = GetOwningPawn();
Data.Target = HitActor;
SendGameplayEventToActor(GetOwningPawn(), Shared_Event_MeleeHit, Data);
1
2
3
4
Weapon Overlap → HeroCombatComponent 去重
→ 事件发送给攻击者 Hero
→ 当前攻击 GA 的 Wait Gameplay Event
→ 从 EventData.Target 取得敌人

P61 Set Up Attack Montages(08:25)

提交46aab11;资产课。

修改轻/重攻击 Montage、Master Ability 和 ANS_ToggleWeaponCollision。每段 Montage 的伤害帧用 Notify State 控制,避免动画未挥到目标就开始检测。

P62 Make Gameplay Effect Spec Handle(10:16)

提交ae18278

1
2
3
4
5
FGameplayEffectSpecHandle MakeHeroDamageEffectSpecHandle(
TSubclassOf<UGameplayEffect> EffectClass,
float WeaponBaseDamage,
FGameplayTag CurrentAttackTypeTag,
int32 UsedComboCount);

Effect Context 写入当前 Ability、SourceObject、Instigator;MakeOutgoingSpec 使用当前 Ability Level。Spec 是“本次攻击”的数据容器。

P63 Hero Damage Info(12:04)

提交4091cff

武器数据:

1
FScalableFloat WeaponBaseDamage;
1
2
3
4
float UHeroCombatComponent::GetHeroCurrentEquippWeaponDamageAtLevel(float Level) const
{
return CurrentWeapon->HeroWeaponData.WeaponBaseDamage.GetValueAtLevel(Level);
}

新增:GE_Shared_DealDamageCT_HeroWeaponStatsUGEExecCalc_DamageTaken。SetByCaller 协议:

1
2
3
Shared.SetByCaller.BaseDamage
Player.SetByCaller.AttackType.Light
Player.SetByCaller.AttackType.Heavy

P64 Apply Effect Spec Handle To Target(09:17)

提交c8150a5

1
2
3
FActiveGameplayEffectHandle NativeApplyEffectSpecHandleToTarget(
AActor* TargetActor,
const FGameplayEffectSpecHandle& SpecHandle);
1
2
3
Target Actor → AbilitySystemBlueprintLibrary 获取 Target ASC
→ Source ASC.ApplyGameplayEffectSpecToTarget
→ 返回 ActiveEffectHandle

蓝图包装通过 EWarriorSuccessTypeExpandEnumAsExecs 生成成功/失败执行引脚。

P65 Capture Relevant Attributes(09:15)

提交a023587

1
2
3
DEFINE_ATTRIBUTE_CAPTUREDEF(UWarriorAttributeSet, AttackPower, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UWarriorAttributeSet, DefensePower, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UWarriorAttributeSet, DamageTaken, Target, false);

false 表示执行时捕获,不在 Spec 创建时快照。三个定义加入 RelevantAttributesToCapture

P66 Retrieve Hero Damage Info(11:38)

提交3657cba

Execution 从 Owning Spec 读取:

  • Source/Target Aggregated Tags。
  • Source AttackPower
  • Target DefensePower
  • SetByCallerTagMagnitudes 中的 BaseDamage 和轻/重攻击 ComboCount。

运行时攻击段数据使用 SetByCaller,稳定基础属性使用 Attribute Capture。

P67 Calculate Final Damage Done(15:06)

提交a71d189

1
2
3
LightMultiplier = 1 + (ComboCount - 1) × 0.05
HeavyMultiplier = 1 + ComboCount × 0.15
FinalDamage = ScaledBaseDamage × SourceAttackPower ÷ TargetDefensePower

输出 Modifier:

1
2
3
4
OutExecutionOutput.AddOutputModifier(
FGameplayModifierEvaluatedData(DamageTakenProperty,
EGameplayModOp::Override,
FinalDamage));

[!warning] 最终源码没有保护 TargetDefensePower == 0;配置必须保证防御力大于零,或代码中做下限约束。

P68 Set Up Heavy Attacks For Damage(06:00)

提交680203f;修改 Heavy Attack Master Ability。

重攻击复用同一伤害 GE/Execution,只把攻击类型和 ComboCount 写入 Player.SetByCaller.AttackType.Heavy,从而应用 15% 的重击段数倍率。

P69 Modify Health Attribute(10:00)

提交eedc765

1
2
void UWarriorAttributeSet::PostGameplayEffectExecute(
const FGameplayEffectModCallbackData& Data)
1
2
3
4
DamageTaken → GetDamageTaken
→ CurrentHealth - Damage
→ Clamp(0, MaxHealth)
→ SetCurrentHealth

将“算伤害”和“如何消费伤害”分开,便于后续加入 UI、护盾、死亡等逻辑。

P70 Hit React Ability(12:50)

提交4f035a6;资产课。

新增 Shared.Ability.HitReactShared.Event.HitReact,以及 GA_Enemy_HitReact_BaseGA_Guardian_HitReact、Guardian 受击 Montage。HitReact 作为 Reactive Ability 由 DA_Guardian 授予。

P71 Trigger Hit React Ability(05:51)

提交5c5fa9b;蓝图课。

1
2
3
伤害成功应用 → Send Shared.Event.HitReact 给 Target
→ Reactive HitReact Ability 响应
→ 播放 Guardian 受击 Montage

P71 本身没有 C++ diff,因此事件具体发送节点以 Ability 蓝图为准。

P72 Material Hit FX(10:32)

提交fd88cfa;材质/蓝图课。

MI_Guardian 与 HitReact Ability 实现受击闪白/颜色变化。每个敌人应操作 Dynamic Material Instance,避免修改共享材质影响所有实例。

P73 Hit Pause(07:36)

提交0344d2e

新增:Player.Ability.HitPausePlayer.Event.HitPauseGA_Hero_HitPause

1
2
3
HeroCombatComponent::OnHitTargetActor
→ Send Player.Event.HitPause 给 Hero
→ GA_Hero_HitPause 执行短暂停顿

设计上 EndOverlap 可再次通知恢复;但当前公开源码存在 P59 所述 EndOverlap 委托错误。

P74 Camera Shake(07:16)

提交d1d93f3;资产课。

CameraShake_HeroMelee 接入 GA_Hero_HitPause。镜头反馈由本地 PlayerController 播放,数值伤害逻辑不依赖镜头表现。

P75 Hit React Sound(05:47)

提交8714625;音频/动画资产课。

Guardian 受击 Montage 加入声音,Concurrency_OneAtATime 限制同类吼叫并发,避免连续受击造成声音叠加。

P76 Gameplay Cues(10:03)

提交281d901

Config:

1
2
3
GameplayCueNotifyPaths="/Game/GameplayCues"
GameplayCue.Sounds.MeleeHit.Axe
GameplayCue.Sounds.MeleeHit.Stick

新增 GC_Hero_AxeHit。Gameplay Cue 只负责命中声音/特效,伤害仍由 EffectSpec/Execution 结算。

P77 Enemy Death Ability(16:31)

提交a89b6e3

新增:Shared.Ability.DeathShared.Status.DeadGA_Enemy_Death_BaseGA_Guardian_Death

1
2
3
4
if (GetCurrentHealth() == 0.f)
{
AddGameplayTagToActorIfNone(TargetActor, Shared_Status_Dead);
}

Dead Tag 驱动响应式 Death Ability;具体 Trigger 配置位于能力蓝图。

P78 BP Death Interface(07:11)

提交组合8381e98 Enemy death asset done + bd72f09 DeathInterfaceDone

资产:Guardian 多个 Death Montage、Death Sound Cue、BPI_EnemyDeath。接口把通用死亡 Ability 与具体敌人表现解耦:

1
2
Death Ability → BPI_EnemyDeath → Enemy Blueprint
→ 停止移动/碰撞/血条 → 播放具体死亡表现

P79 Dissolve Material FX(11:53)

提交104da4f Material FX done

修改角色和武器材质,建立死亡溶解参数。蓝图接口同时驱动角色与武器动态材质,Timeline/曲线控制溶解进度。

P80 Dissolve Niagara FX(11:01)

提交3c4042f DissolveNiagaraDone

死亡链加入 Niagara 消散表现。应确保 Niagara 有独立生命周期,Actor 销毁时不会过早截断必要特效。

P81 Pawn UI Component(12:42)

提交c11c7dd PawnUIComponentDone

1
2
3
4
UPawnExtensionComponentBase
└─ UPawnUIComponent
├─ UHeroUIComponent
└─ UEnemyUIComponent

IPawnUIInterface 让 AttributeSet 取得角色 UI Component,而不引用任何具体 Widget。

P82 Broadcast Value Change(13:10)

提交ed50146

1
2
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
FOnPercentChangedDelegate, float, NewPercent);
  • UPawnUIComponent::OnCurrentHealthChanged
  • UHeroUIComponent::OnCurrentRageChanged
1
OnCurrentHealthChanged.Broadcast(GetCurrentHealth() / GetMaxHealth());

[!warning] 需要保证 MaxHealth/MaxRage 非零,或代码做除零保护。

P83 Listen For Broadcasting(12:57)

提交e9a9a4b

UWarriorWidgetBase::NativeOnInitialized

1
2
3
4
5
GetOwningPlayerPawn
→ Cast<IPawnUIInterface>
→ GetHeroUIComponent
→ BP_OnOwningHeroUIComponentInitialized
→ Widget 蓝图绑定 Health/Rage 委托

Widget 被动接收更新,不在 Tick 中轮询 AttributeSet。

P84 Enemy Init Created Widget(09:00)

提交1005675

敌人世界空间 Widget 没有 Owning Player Pawn,因此:

1
2
3
4
5
EnemyCharacter::BeginPlay
→ WidgetComponent.GetUserWidgetObject
→ UWarriorWidgetBase::InitEnemyCreatedWidget(this)
→ IPawnUIInterface.GetEnemyUIComponent
→ BP_OnOwningEnemyUIComponentInitialized

P85 Template Widgets(11:56)

提交9c22683;UMG 资产课。

新增 TPWBP_StatusBarWarriorSizeBoxMI_HeroHealthBar,删除临时 WBP_Test。模板负责通用显示,业务数据仍来自 UI Component 委托。

P86 Set Status Bar Fill Color(09:19)

提交25230f2;仅修改 TPWBP_StatusBar。通过暴露 Fill Color/材质参数,让 Health、Rage、Enemy Health 复用同一控件。

P87 Template Icon Slot Widget(09:37)

提交be2ce77。新增 TPWBP_IconSlot,统一图标容器布局;后续武器图标、技能图标和冷却遮罩都会复用它。

P88 Hero Overlay Widget(13:10)

提交00938cf

资产:WBP_HeroOverlayGA_Hero_DrawOverlayWidget。启动数据授予 OnGiven UI Ability,创建 Overlay 后通过 UWarriorWidgetBase 注入 HeroUIComponent。

P89 Enemy Health Widget Component(12:43)

提交553b3ac

1
UWidgetComponent* EnemyHealthWidgetComponent;

构造时附着到 Mesh;BeginPlay 后初始化 WBP_DefaultEnemyHealthBar。血条订阅 EnemyUIComponent::OnCurrentHealthChanged

P90 Hide Enemy Health Bar(06:49)

提交cbabfae;仅修改敌人血条 Widget。显示/隐藏规则完全位于 UMG 资产中,可能根据满血、受伤、死亡或延时控制,精确条件需打开资产确认。

P91 Update Weapon Icon(08:46)

提交0a97bdf

武器数据:

1
TSoftObjectPtr<UTexture2D> SoftWeaponIconTexture;

UI 委托:

1
FOnEquippedWeaponChangedDelegate OnEquippedWeaponChanged;

装备/卸下 Ability 蓝图广播软图标,WBP_HeroOverlay 更新 TPWBP_IconSlot。广播端在蓝图中,C++ 只定义数据与委托。

P92 Final Tweaking(14:11)

提交1435e17

新增/调整 MI_EnemyHealthBarMI_HeroRageBar、Hero Health Material、Overlay、Enemy Health Bar、Icon Slot 和 HeroUIComponent 暴露方式。本节是视觉与参数收敛,不新增战斗算法。

P93 Section Wrap Up(00:56)

总结课。完整闭环:

1
2
3
4
5
6
7
8
9
flowchart LR
A[Montage碰撞窗口] --> B[Weapon Overlap]
B --> C[Shared.Event.MeleeHit]
C --> D[Damage EffectSpec]
D --> E[Execution Calculation]
E --> F[DamageTaken]
F --> G[Health/Dead Tag]
G --> H[HitReact/Death]
G --> I[UI Delegate]

关键源码审计

  • OnCollisionBoxEndOverlap 当前委托疑似错误。
  • 伤害公式需防御 DefensePower == 0
  • UI 百分比需防御 Max 属性为 0。
  • CurrentHealth == 0.f 因前置 Clamp 通常成立,但 <= 0.f 更健壮。
  • 蓝图表现不得反向承担权威伤害计算。

本文从 LearnByCompany 原始文档 自动同步。