
1. 项目概述碰撞事件驱动的属性交互在UE5里做RPG角色的属性系统是核心骨架。传统的做法可能是写一堆蓝图事件分发器或者在角色类里硬编码一堆变量和函数一旦系统复杂起来维护就成了噩梦。这也是为什么Epic的Gameplay Ability SystemGAS框架越来越受青睐它提供了一套标准化的、网络复制的、可预测的属性与技能管理方案。我们这个项目的核心就是利用GAS实现一个非常经典且高频的RPG交互场景通过碰撞事件来动态修改角色的属性值。听起来简单比如角色碰到一个治疗药水生命值回复踩到一个火焰陷阱持续掉血。但要把这套逻辑在GAS的框架下做得干净、可扩展、网络同步无误里面有不少门道。我最近在做一个类似《暗黑破坏神》风格的ARPG原型就大量用到了这个模式。怪物攻击命中玩家一次碰撞事件触发一个“造成伤害”的Gameplay EffectGE这个GE会去修改玩家的“生命值”属性。反过来玩家拾取一个“力量圣坛”也是一次碰撞会施加一个持续30秒、增加“力量”属性的BUFF GE。整个过程属性值的计算、网络同步、UI更新都由GAS自动处理我们只需要关心“什么时候”触发“什么效果”。这不仅仅是把Actor.OnComponentBeginOverlap事件和AttributeSet.SetHealth简单连起来。我们需要考虑属性定义在哪里碰撞事件如何与GAS交互是直接修改属性还是通过Gameplay AbilityGA或Gameplay EffectGE来驱动网络环境下如何保证客户端和服务器表现一致UI如何响应属性变化这些都是实战中必须解决的问题。接下来我会带你从零开始拆解如何用UE5 GAS实现这套碰撞驱动属性修改的系统。无论你是刚接触GAS还是已经有所了解但在实战中遇到瓶颈相信这些从项目里踩坑总结出的经验都能帮到你。2. 核心架构设计与GAS组件配置在动手写碰撞逻辑之前我们必须先把GAS的基础架子搭好。这一步就像盖房子打地基地基不稳后面加再酷炫的功能都容易崩。2.1 创建自定义AttributeSet属性集AttributeSet是GAS中属性的容器和管家。所有需要被GAS管理的角色属性比如生命值Health、魔法值Mana、力量Strength等都应该定义在这里。首先在C中创建一个继承自UAttributeSet的类比如URPGAttributeSet。在这个类的头文件里我们使用UPROPERTY(BlueprintReadOnly, Category Attributes, ReplicatedUsing OnRep_Health)这样的宏来定义属性。ReplicatedUsing指定了当这个属性在网络上复制时用于回调的函数这对于同步UI至关重要。// RPGAttributeSet.h UCLASS() class YOURPROJECT_API URPGAttributeSet : public UAttributeSet { GENERATED_BODY() public: URPGAttributeSet(); // 生命值属性 UPROPERTY(BlueprintReadOnly, Category Attributes|Vital, ReplicatedUsing OnRep_Health) FGameplayAttributeData Health; ATTRIBUTE_ACCESSORS(URPGAttributeSet, Health) // 这个宏会自动生成GetHealth、SetHealth函数 // 最大生命值属性 UPROPERTY(BlueprintReadOnly, Category Attributes|Vital, ReplicatedUsing OnRep_MaxHealth) FGameplayAttributeData MaxHealth; ATTRIBUTE_ACCESSORS(URPGAttributeSet, MaxHealth) // 魔法值、力量等属性定义类似... // 网络复制回调函数 UFUNCTION() virtual void OnRep_Health(const FGameplayAttributeData OldHealth); UFUNCTION() virtual void OnRep_MaxHealth(const FGameplayAttributeData OldMaxHealth); // 重写PostGameplayEffectExecute用于处理属性修改后的逻辑如角色死亡 virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData Data) override; };在源文件中你需要初始化属性的默认值通常在构造函数里实现复制回调函数并在GetLifetimeReplicatedProps中注册需要复制的属性。注意属性值本身FGameplayAttributeData的复制是GAS内部处理的。ReplicatedUsing回调的主要目的是在客户端属性值更新后触发一些本地逻辑比如更新血条UI、播放受伤音效等。不要把复杂的游戏逻辑放在这里它只是一个通知机制。2.2 为角色装配AbilitySystemComponentASCAbilitySystemComponent是GAS的大脑它挂在拥有属性的实体通常是Character或Pawn上负责管理所有的AttributeSet、GameplayAbility和GameplayEffect。在你的角色基类比如ARPGCharacterBase中需要做以下几件事声明并创建ASC添加一个UAbilitySystemComponent类型的组件。// RPGCharacterBase.h UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category GAS) class UAbilitySystemComponent* AbilitySystemComponent;在构造函数或BeginPlay中创建它。初始化ASC并设置AttributeSet在服务器端或单机游戏的角色控制器端你需要将创建好的AttributeSet实例注册到ASC中。// 通常在角色的PossessedBy服务器或OnRep_PlayerState客户端中调用 if (AbilitySystemComponent) { AbilitySystemComponent-InitAbilityActorInfo(this, this); // 创建并初始化AttributeSet HealthAttributeSet CreateDefaultSubobjectURPGAttributeSet(TEXT(HealthAttributeSet)); // AttributeSet会自动注册到ASC因为ASC会扫描Actor上的所有AttributeSet组件。 }这里有个关键点AttributeSet应该作为角色Actor的一个子组件UObject存在而不是ASC的子组件。ASC通过InitAbilityActorInfo调用会自动收集Actor身上所有的AttributeSet并建立关联。实现IAbilitySystemInterface接口这个接口要求实现一个GetAbilitySystemComponent函数。GAS内部的很多功能如效果施加、属性查询都需要通过这个接口来获取ASC。在你的角色基类中实现它直接返回AbilitySystemComponent指针即可。完成这些你的角色就具备了运行GAS的基本能力。你可以在编辑器中看到角色身上有了ASC组件并且可以访问到AttributeSet中定义的属性尽管现在还没有任何修改它们的逻辑。3. 碰撞检测的蓝图与C实现策略有了GAS的基础我们现在来搭建碰撞检测的桥梁。碰撞是触发属性修改的“扳机”我们需要决定在哪里、以及如何检测碰撞。3.1 碰撞体设置与事件绑定通常我们会使用UCapsuleComponent作为角色的根碰撞体用于基本的物理碰撞和移动。但对于技能、子弹、拾取物等特定交互我们往往需要更精确的碰撞体比如USphereComponent或UBoxComponent。以创建一个治疗药水拾取物为例创建Actor蓝图新建一个Blueprint Class父类选择Actor命名为BP_HealthPotion。添加碰撞组件在组件面板中添加一个Sphere Component将其作为根组件。调整球体大小以匹配药水模型。配置碰撞预设选中Sphere Component在细节面板的“Collision”栏目下设置Collision Presets: 推荐使用Custom...。Collision Enabled:Query Only我们只需要检测重叠事件不需要物理模拟。Object Type: 可以设置为WorldDynamic或自定义一个Pickup类型。Generate Overlap Events:必须勾选否则不会触发重叠事件。Collision Responses: 设置对Pawn或你角色使用的碰撞通道如Pawn为Overlap。绑定重叠事件有两种主流方式蓝图事件图表在蓝图中右键搜索“Add Event”选择“Add On Component Begin Overlap”事件并指定为你的Sphere Component。这种方式快速直观。C 函数重写在药水的C类中重写NotifyActorBeginOverlap函数。这种方式更灵活便于集中处理逻辑和代码复用。// HealthPotion.h virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;3.2 在碰撞事件中获取目标ASC无论采用蓝图还是C碰撞事件触发后我们首先需要获取与药水发生重叠的“其他Actor”OtherActor的AbilitySystemComponent。关键步骤类型检查与转换首先检查OtherActor是否是你的目标角色类型例如是否实现了IAbilitySystemInterface接口或者是你的ARPGCharacterBase类。// C 示例 AYourProjectCharacterBase* OverlappedCharacter CastAYourProjectCharacterBase(OtherActor); if (!OverlappedCharacter) { return; // 不是目标角色直接返回 }获取ASC通过接口或直接访问角色身上的组件来获取ASC。UAbilitySystemComponent* TargetASC OverlappedCharacter-GetAbilitySystemComponent(); if (!TargetASC) { return; // 目标没有ASC无法施加效果 }这里有一个非常重要的细节对于玩家控制的角色PlayerControllerPossess的PawnASC通常挂在Pawn本身上。但在一些架构中ASC也可能挂在PlayerState上以实现角色死亡重生后属性状态的持久化。你需要在你的游戏架构中保持一致并在碰撞检测逻辑中正确地获取到ASC。权限判断网络游戏必备在多人游戏中施加GameplayEffect修改属性的操作必须在服务器端执行。因此在碰撞事件处理函数中你需要判断当前代码是否运行在服务器上。if (!HasAuthority()) { // HasAuthority() 返回true表示在服务器端 return; }如果你在客户端检测到碰撞并尝试施加效果这个效果不会被复制到服务器和其他客户端会导致严重的数据不一致。通常的做法是在服务器端执行效果施加逻辑然后由GAS的网络同步机制将属性变化和效果同步到所有客户端。实操心得我强烈建议将“从Actor获取ASC”和“权限检查”封装成一个工具函数如GetTargetASCAndValidateAuthority这样可以在所有需要交互的地方技能、陷阱、道具复用避免重复代码和遗漏权限检查。4. 通过GameplayEffect动态修改属性值拿到了目标的ASC我们终于可以修改属性了。在GAS中直接调用AttributeSet.SetHealth()是错误的方式。正确的、也是唯一推荐的方式是使用GameplayEffectGE。你可以把GameplayEffect理解为一个“属性修改器”或“状态效果”的配方。它定义了要修改哪个属性、如何修改加、减、乘、覆盖、持续多久、附带什么视觉/音频效果等。4.1 创建GameplayEffect蓝图在内容浏览器中右键选择“Gameplay Effect” - “Gameplay Effect Blueprint”。我们创建两种常见的GE即时效果Instant用于一次性的属性修改如拾取药水瞬间回复生命。新建一个GE蓝图命名为GE_HealthPotion_Instant。在类默认值中将Duration Policy设置为Instant。在Modifiers数组中添加一项Attribute: 选择你的RPGAttributeSet.Health。Modifier Op:Add相加。Modifier Magnitude:Scalable Float。可以设置一个固定值如50或者一个基于等级Level的曲线表Curve Table实现高级药水回复更多。Granted Tags和Application Required Tags等可以先留空。持续效果Duration用于持续一段时间的属性修改或伤害如火焰陷阱的持续灼烧。新建GE蓝图命名为GE_FireTrap_Duration。Duration Policy设置为Has Duration。Duration Magnitude: 设置持续时间如5秒。在Modifiers中添加一项Attribute:RPGAttributeSet.Health。Modifier Op:Add注意对于伤害是负值相加例如-10。Modifier Magnitude:Scalable Float设为负值如-5。Modifier Op还有一个Period选项勾选后可以设置周期时间如1秒实现周期性伤害每1秒触发一次Modifier。4.2 在碰撞事件中施加GameplayEffect在药水的碰撞事件处理函数中确保在服务器端我们创建这个GE的实例并施加给目标。C 示例// 假设我们已经获取到了 TargetASC if (TargetASC) { // 定义GE的类引用这需要在头文件中用TSubclassOf声明并在蓝图中赋值 // TSubclassOfUGameplayEffect HealthPotionEffectClass; if (HealthPotionEffectClass) { // 创建GE的上下文Context包含来源Source和目标Target等信息 FGameplayEffectContextHandle EffectContext TargetASC-MakeEffectContext(); EffectContext.AddSourceObject(this); // 来源是药水本身 // 创建GE规格Spec FGameplayEffectSpecHandle SpecHandle TargetASC-MakeOutgoingSpec(HealthPotionEffectClass, 1.0f, EffectContext); // 1.0f是等级Level if (SpecHandle.IsValid()) { // 可以在这里动态修改Spec的参数比如根据角色等级调整回复量 // FGameplayEffectSpec* Spec SpecHandle.Data.Get(); // Spec-SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName(Data.HealthRestore)), CustomHealAmount); // 施加效果 TargetASC-ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get()); // 药水被拾取后可以销毁自身或播放消失动画 Destroy(); } } }蓝图示例在药水蓝图的OnComponentBeginOverlap事件后类型转换OtherActor到你的角色类。获取角色的ASC组件可能需要通过接口转换。使用“Apply Gameplay Effect to Self”或“Apply Gameplay Effect To Target”节点。“Gameplay Effect Class”参数选择你创建的GE_HealthPotion_Instant。“Level”可以设置为1.0或从数据表中读取。连接“Destroy Actor”节点。通过GE来修改属性GAS会自动处理所有繁重的工作网络复制、属性预测如果开启、与GameplayCue视觉/音频效果的关联、GameplayTag的添加与移除等。这保证了整个属性修改流程是数据驱动的、可预测的。5. 属性修改的响应与UI同步属性值被修改后我们通常需要做两件事一是在游戏逻辑上做出反应比如生命值0时死亡二是更新UI比如血条、数字飘字。5.1 监听属性变化使用Attribute Change Delegate最优雅的方式是订阅属性变化的委托Delegate。我们可以在角色的初始化阶段例如BeginPlay中在ASC初始化之后进行订阅。在角色C类中void ARPGCharacterBase::BeginPlay() { Super::BeginPlay(); if (AbilitySystemComponent) { // 订阅Health属性变化委托 HealthChangedDelegateHandle AbilitySystemComponent-GetGameplayAttributeValueChangeDelegate( URPGAttributeSet::GetHealthAttribute() ).AddUObject(this, ARPGCharacterBase::OnHealthChanged); // 同样可以订阅MaxHealth, Mana等 // MaxHealthChangedDelegateHandle ... } } void ARPGCharacterBase::OnHealthChanged(const FOnAttributeChangeData Data) { // Data.NewValue 是新的生命值 // Data.OldValue 是旧的生命值 float CurrentHealth Data.NewValue; float OldHealth Data.OldValue; // 1. 更新UI OnHealthUpdated(CurrentHealth, GetMaxHealth()); // 调用一个更新UI的蓝图可调用事件或函数 // 2. 处理游戏逻辑例如死亡 if (CurrentHealth 0.0f OldHealth 0.0f) { OnDeath(); // 触发死亡逻辑 } // 3. 可以在这里播放受击或治疗反馈通过GameplayCue更规范 if (CurrentHealth OldHealth) { // 受到伤害播放受击特效/音效 } else if (CurrentHealth OldHealth) { // 受到治疗播放治疗特效/音效 } } void ARPGCharacterBase::EndPlay(const EEndPlayReason::Type EndPlayReason) { // 记得在角色销毁时移除委托防止内存泄漏 if (AbilitySystemComponent HealthChangedDelegateHandle.IsValid()) { AbilitySystemComponent-GetGameplayAttributeValueChangeDelegate( URPGAttributeSet::GetHealthAttribute() ).Remove(HealthChangedDelegateHandle); } Super::EndPlay(EndPlayReason); }在AttributeSet的PostGameplayEffectExecute中处理另一种方式是在AttributeSet的PostGameplayEffectExecute函数中处理。这个函数在GE对属性修改计算完成后、应用前被调用。这里适合处理一些基于最终计算结果的逻辑比如确保生命值不超过最大值、不低于0以及触发死亡。void URPGAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData Data) { Super::PostGameplayEffectExecute(Data); if (Data.EvaluatedData.Attribute GetHealthAttribute()) { // 确保生命值在0到MaxHealth之间 SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth())); // 在这里也可以触发死亡但更推荐在角色类中通过委托处理以保持逻辑分离。 // if (GetHealth() 0.0f) { ... } } // 处理其他属性... }5.2 更新UMG用户界面UI更新通常通过将属性值绑定到UMG Widget的变量上实现。我们创建一个UW_HealthBar控件。在Widget蓝图中创建变量比如CurrentHealth和MaxHealth类型为float。在角色类中创建UI更新事件如上文C代码中的OnHealthUpdated函数将其标记为BlueprintImplementableEvent或BlueprintNativeEvent以便在蓝图中实现。// RPGCharacterBase.h UFUNCTION(BlueprintImplementableEvent, Category UI) void OnHealthUpdated(float NewHealth, float NewMaxHealth);在角色蓝图事件图表中实现该事件获取玩家控制器。从玩家控制器获取HUD或直接获取你创建的UW_HealthBarWidget实例。调用Widget上的一个自定义函数如UpdateHealthBar将NewHealth和NewMaxHealth传递进去。在Widget蓝图中实现更新逻辑在UpdateHealthBar函数中设置CurrentHealth和MaxHealth变量的值。使用这些变量来驱动进度条Progress Bar的PercentCurrentHealth / MaxHealth或者更新文本块Text Block显示具体数值。这样每当Health属性通过GAS被修改委托就会触发调用角色蓝图中的事件来更新UI实现完美的同步。6. 高级应用与性能优化实战当系统跑通后我们会面临更复杂的需求和性能考量。这里分享几个实战中总结的高级技巧和优化点。6.1 利用GameplayTag进行复杂条件判断GameplayTag是一种层次化的标签系统非常适合用来管理状态和进行条件判断。在碰撞修改属性的场景中它可以大显身手。场景示例角色处于“无敌”状态时不受陷阱伤害。定义GameplayTag在项目设置中定义标签例如State.Invincible无敌状态。为无敌效果创建GE创建一个Duration类型的GEGE_Invincibility在Granted Tags中添加State.Invincible标签。当施加这个GE时角色就拥有了“无敌”标签。为伤害效果GE添加条件修改火焰陷阱的伤害GEGE_FireTrap_Duration。在Application Required Tags中不填。在Asset Tags中添加一个标签比如Effect.Damage.Fire。关键在Granted Tags中添加一个State.Immune.Damage不正确做法是利用Gameplay Effect的Application Tag Requirements。更常见的做法是在施加效果的地方如陷阱的碰撞逻辑或GameplayAbility中进行判断。或者使用GameplayEffect的Application Tag Requirements中的Ongoing Tag Requirements。一个更清晰的设计是在陷阱施加伤害效果前先检查目标身上是否有State.Invincible标签。// 在施加伤害GE之前 if (TargetASC-HasMatchingGameplayTag(FGameplayTag::RequestGameplayTag(FName(State.Invincible)))) { return; // 目标无敌不施加伤害效果 } // ... 否则正常施加伤害GE基于标签修改数值SetByCaller你还可以在GE的Modifier中将Modifier Magnitude的类型选为Set By Caller并指定一个Tag如Data.Damage。这样在施加GE时MakeOutgoingSpec之后你可以动态地设置伤害值这个值可以来源于角色的攻击力属性、技能的等级或者一个复杂的伤害计算公式。FGameplayEffectSpecHandle SpecHandle TargetASC-MakeOutgoingSpec(DamageEffectClass, 1.0f, EffectContext); if (SpecHandle.IsValid()) { // 动态设置伤害值 float FinalDamage CalculateDamage(SourceCharacter, TargetCharacter); SpecHandle.Data.Get()-SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName(Data.Damage)), FinalDamage); TargetASC-ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get()); }6.2 性能优化与注意事项避免每帧查询属性绝对不要在Tick函数里通过GetGameplayAttributeValue去读取属性值来更新UI。正确的做法就是上面提到的使用属性变化委托。委托只在值真正改变时触发效率极高。GE池化GameplayEffect Spec Pooling对于高频施加的GE比如每秒触发多次的持续伤害效果频繁创建和销毁FGameplayEffectSpec会产生垃圾回收GC开销。UE5的GAS内部已经有了一定程度的Spec缓存但对于极端情况可以考虑自己实现一个简单的对象池来复用Spec。网络优化确保GE的Replication Policy设置正确。对于只影响本地视觉的GE如某些客户端特效可以设置为Replicate No。对于重要的状态效果设置为Replicate Yes。合理使用Gameplay Cue来处理纯客户端的音效和粒子效果而不是通过GE复制。碰撞检测优化使用查询通道Query Channels而非物理模拟对于只需要检测重叠的事件将碰撞组件的Collision Enabled设为Query Only这比Physics Only或Physics and Query性能更好。合理设置碰撞响应精确设置你的碰撞组件对哪些通道Channel做出Overlap响应避免不必要的碰撞检测计算。对于大量拾取物可以考虑使用UBoxComponent或USphereComponent的SetGenerateOverlapEvents在必要时动态开启/关闭重叠事件生成。例如当玩家离得很远时关闭靠近时再开启。调试与可视化在控制台输入showdebug abilitysystem可以显示当前选中Actor的GAS状态包括所有Attribute、Active Gameplay Effects和Granted Tags是调试神器。使用AbilitySystemComponent-PrintDebug()函数或在蓝图中使用“Print Debug String”节点输出ASC信息到屏幕。7. 常见问题排查与解决方案实录在实际开发中你肯定会遇到各种奇怪的问题。下面是我踩过的一些坑和解决办法。7.1 属性修改没有反应UI不更新逻辑不触发问题描述碰撞发生了GE也施加了但角色的血条没变化OnHealthChanged委托也没触发。排查步骤检查网络权限这是最常见的问题确保施加GE的代码运行在服务器端HasAuthority()返回true。在客户端的碰撞事件里施加的GE是无效的。在碰撞事件处理函数开头加个打印或断点。检查ASC是否有效确保从OtherActor获取到的AbilitySystemComponent指针不是nullptr。确认你的角色类正确初始化了ASC并实现了IAbilitySystemInterface。检查GE类引用确保你传递给MakeOutgoingSpec或蓝图节点的Gameplay Effect Class是正确的资产引用没有为None。检查AttributeSet属性名在GE的Modifier中Attribute下拉菜单选择的是否是你自定义的RPGAttributeSet.Health而不是引擎自带的或其他AttributeSet的属性。属性名必须完全匹配。检查委托绑定时机确保你在角色的BeginPlay或InitAbilityActorInfo之后才绑定了属性变化委托。如果绑定太早ASC可能还没准备好。使用调试命令选中目标角色在游戏运行中输入showdebug abilitysystem查看Active Gameplay Effects列表里是否有你施加的GE。查看Attributes部分Health的BaseValue和CurrentValue是否改变了。7.2 客户端看不到属性变化或效果问题描述服务器上属性正常变化但客户端角色血条不动或者伤害数字不显示。排查步骤确认属性已复制在AttributeSet的属性声明中必须包含ReplicatedUsing宏并在cpp文件中实现GetLifetimeReplicatedProps使用DOREPLIFETIME_CONDITION_NOTIFY宏复制该属性。CONDITION_NOTIFY可以确保属性变化时通知委托。void URPGAttributeSet::GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME_CONDITION_NOTIFY(URPGAttributeSet, Health, COND_None, REPNOTIFY_Always); DOREPLIFETIME_CONDITION_NOTIFY(URPGAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always); }检查GE的复制设置在GE蓝图的类默认值中Replication Policy是否设置为Replicate对于需要同步的效果。检查GameplayCue如果用了确保GameplayCue的Notify事件在客户端被正确触发。有时需要检查Cue的Tag是否与GE中Gameplay Cues数组里添加的Tag匹配。7.3 碰撞事件被多次触发问题描述角色碰到一个药水瞬间回复了多次生命值或者一个陷阱对角色造成了多次伤害。解决方案使用“已处理”标记在药水Actor中添加一个布尔变量bIsConsumed。在NotifyActorBeginOverlap中先检查这个变量如果为true则直接返回。在成功施加GE后将其设为true。if (bIsConsumed) return; // ... 施加GE的逻辑 ... bIsConsumed true;立即禁用碰撞施加效果后立即调用碰撞组件的SetGenerateOverlapEvents(false)来禁用该碰撞体防止同一帧内再次触发。使用定时器延迟销毁如果你希望播放一个拾取动画再消失可以在施加效果后禁用碰撞然后设置一个定时器在动画播放完毕后销毁Actor而不是立即Destroy()。注意碰撞体层级检查角色身上是否有多个碰撞体同时与药水重叠。确保你只在你想要的那个碰撞体如胶囊体上处理事件或者做好去重判断。7.4 数值计算不符合预期如伤害减免未生效问题描述设计了一个增加防御力减少伤害的GE但实际受到的伤害还是原值。排查步骤理解执行顺序GAS中多个GE对同一属性的修改其Modifier Op加、减、乘、覆盖和Modifier Magnitude的计算顺序由GE的Modifier中的Modifier Op和GameplayEffectExecutionCalculation如果用了决定。复杂的公式建议使用GameplayEffectExecutionCalculation简称Execution Calculation类来集中计算。检查GE的周期对于持续伤害Periodic修改的是每次周期触发时的Modifier Magnitude。如果你希望防御力减免的是每次跳的伤害那么减免计算需要在Execution Calculation中或者在施加伤害时动态计算SetByCaller的值。使用预览Prediction对于客户端预测确保你的Execution Calculation支持预测。在Execution Calculation的Execute函数中使用FGameplayEffectCustomExecutionOutput来输出最终结果。这套通过碰撞事件驱动GAS修改属性的模式一旦跑通就会成为你RPG战斗、交互系统的强大基石。它清晰地将数据AttributeSet、逻辑GameplayEffect/Ability和触发条件碰撞事件分离让系统易于扩展和维护。下次当你需要实现一个“毒雾区域减速”、“圣光区域回血”或者“攻击弹反”的功能时你只需要考虑定义什么属性创建什么GE由什么事件触发剩下的就交给GAS这个可靠的框架来处理吧。