GAS学习(UE5自用)

1.项目准备

先讲自动编译和live coding关闭。

原因如下:

Live Coding 的整个前提是编辑器进程不重启,这意味着:

  • 单例、静态变量、全局状态会跨越多次编译持续存在
  • 你在某次调试里手动改过的运行时状态(比如断点里改了个变量值)可能会"污染"下一次测试
  • 内存碎片和补丁数量会随时间增长,长时间开发会话里偶尔出现莫名其妙的崩溃,很难判断是代码逻辑问题还是 Live Coding 本身的副作用

对于需要精确复现 bug 的场景(尤其是和帧同步、物理、状态机相关的),"每次都是干净的冷启动"比"热更新但状态不确定"更可靠。

"Automatically compile"(不管是保存时触发还是新建类时触发)意味着编辑器会在你没有主动要求的时候去驱动 UBT 编译。如果你同时在 IDE(VS/Rider)里也做 build(比如按 Ctrl+Shift+B),两边会同时尝试写同一批中间文件(.obj、.pdb),容易出现文件被占用导致编译失败,或者更隐蔽的------两边写入的产物不一致,导致下一次调试对不上符号。

另外自动触发编译还有个实际的烦躼:你保存一个还没写完、语法都不完整的文件时(比如中途保存),它会立刻触发一次编译,纯粹浪费时间和CPU,尤其项目大了之后一次触发可能是几十秒到几分钟。

2.知识点记录

Lazy Loading(懒加载)

指的是"资源不会在你不需要它的时候被自动加载进内存"。

对比一下硬引用和软引用的区别:

  • 硬引用(比如直接 UPROPERTY() UStaticMesh* Mesh; 或者蓝图里直接拖一个资产引用):只要持有这个引用的对象被加载,UE 就会连带把它引用的资产也一起加载进内存,哪怕你当前这一帧根本用不到这个 Mesh。这在大项目里是灾难------比如一个角色蓝图里引用了 100 个技能特效资产,只要这个角色蓝图被加载,这 100 个特效全部跟着一起进内存,不管玩家有没有真的放过这些技能。
  • 软引用(TSoftObjectPtr<UStaticMesh> Mesh;):这个变量存的只是一个"路径字符串"(FSoftObjectPath),资产本身不会 跟着一起加载。只有当你显式调用 .LoadSynchronous() 或者用 FStreamableManager 异步加载时,资产才真正被读进内存。

所以"Lazy Loading"的意思就是:加载这个动作被推迟到"真正要用的那一刻"才发生,而不是"引用它的对象一加载它就跟着加载"。这对内存占用和加载时间的优化非常关键,尤其是像 Wuwa 这种角色数量多、每个角色技能特效资产量巨大的项目。

Access Tracking(访问追踪)

指的是引擎能够记录/追踪某个资产在什么时候、被谁、以什么方式访问(加载)过

这个跟打包(Cook)流程关系很大。UE 在 Cook 的时候需要知道"最终打出来的包里到底应该包含哪些资产"------如果一个资产从来没有被任何东西真正加载访问过,理论上打包时就不该把它塞进去(否则包体会莫名其妙变大,塞进一堆用不到的资源)。

用软引用的好处是,因为加载行为是显式发生的(你手动调用 Load 才会加载),引擎就能精确追踪到"这个资产究竟在什么条件下被访问了",从而:

  • Cook 系统能更准确地判断某个资产是否应该被打进当前的包
  • 可以配合 Asset Manager 做"按需分包"(比如把不同角色的资源打进不同的 pak 文件,做 DLC 式的分发)
  • 出问题时更容易排查"为什么这个资产被意外加载了"------因为软引用的加载点是明确的代码调用位置,不像硬引用那样"只要父对象加载就自动带出来",很难定位是哪里触发的加载

放一起理解:硬引用简单粗暴,写起来方便但内存和加载行为完全不可控;软引用需要你自己写加载逻辑(麻烦一点),但换来的是"资产什么时候被加载"这件事完全在你掌控之中(Lazy Loading),同时引擎也能清楚知道"是谁在什么时候访问了这个资产"(Access Tracking),这对大型项目做内存优化、分包管理、Cook 优化是必需的机制

Check

多用check(变量,提示信息)检查是否为空指针。

类型转换

想要类型转换的C++类必须在UClass之后写"BlueprintType"

网络编程注意事项

1.绑定增强输入

获取subsystem,由于客户端有其他的Player的复制所以都会执行获取subsystem,但是这个系统只有本地客户端才有所以要进行空值判断

3.增强输入

一个客户端可能有多个 LocalPlayer,每个 LocalPlayer 恒定绑一个 Subsystem 实例;这个 Subsystem 里"生效的 IMC 集合"会随玩法频繁动态切换,而它所依附的 PlayerController 本身则很少变、变了也不影响 Subsystem 的身份。

然而Controller里面的EIC是用来专门绑定IMC里面的Action到具体回调函数的

Subsystem 相当于一本通讯录 ,你问它"现在谁有权限接电话",它翻开册子给你看名单,仅此而已;EnhancedPlayerInput 才是那个真正每帧拨号、判断有没有接通的人。

Subsystem里面的MapContext是可以随时替换的,EIC负责查看这里面的

4.鼠标控制

鼠标参数的设置是在PlayerController里面设置的

这段代码是在 PlayerController 里配置鼠标显示方式输入路由模式,直接关系到你之前问的那条流水线里"Slate 会不会先把输入吃掉"这一步。

逐行看:

bShowMouseCursor = true;

让系统鼠标指针可见。默认第三人称/FPS 游戏里这个是 false(鼠标被隐藏、用于视角转动),UI 类/策略类游戏通常要打开。

DefaultMouseCursor = EMouseCursor::Default;

指定鼠标指针的外观样式 (箭头/手型/十字线等),Default 就是普通箭头。

FInputModeGameAndUI InputModeData;

这是关键。UE 的 SetInputMode 家族一共三种:

模式 输入去向
FInputModeGameOnly 所有输入只走 Game(也就是你之前学的那条 PlayerController→PlayerInput→InputComponent 链路),Slate/UI 完全收不到
FInputModeUIOnly 所有输入只走 UMG/Slate,Game 那边收不到,典型用于纯菜单界面
FInputModeGameAndUI 两边都能收到,由 Slate 的 Hit-Test 决定某次点击具体落在 UI 上还是"漏"到 Game 层

4.常见设计模式

接口模式:

类实现接口,可以看作获得某种能力,用来判断是否有这种能力的方法就是使用类型转化,cast<Interface>如果获得的对象不为空就表明具备这个能力。

5.一些好用的命令行

show collision(显示所有的可查看的碰撞体)

show abilitySystem(看这个ASC的调试信息)

6.一些功能关于引擎的功能

post process(后处理)

影响范围的属性(勾选就是所有地方都能够覆盖)

CustomPass

区别于普通的ScenePass的一张深度图,只记录指定某种物体的距离(大概是这样,也可能不准确),这样就可以对其进行拓展达到描边的效果

选项 行为
Disabled 完全关闭 Custom Depth Pass,即使某个 Component 勾了 Render CustomDepth,也不会生效------省这部分渲染开销,项目里如果确定不用描边/轮廓效果,关掉能省一点性能
Enabled 正常开启,所有勾选了 Render CustomDepth 的物体都会渲染进这张额外深度图
Enabled on Demand 一种优化模式,Custom Depth 这张图默认不分配/不渲染,只有当某一帧真的有物体请求了 Custom Depth,才临时启用------避免场景里长期没人用这个功能时,还白白占着一张渲染目标(RT)的显存和带宽
Enabled with Stencil 在 Custom Depth 的基础上,额外附带一个 8bit 的 Stencil(模板)通道 ,可以给每个物体单独设置 CustomDepth Stencil Value(0-255 的整数标记值)。这样后处理材质不仅能判断"这个像素是不是标记物体",还能区分**"是哪一类/哪一个"**标记物体------比如敌人描边用红色(Stencil=1),友军描边用绿色(Stencil=2),同一张图里可以区分处理多种物体,而不是所有勾选的物体都混在一起变成同一种效果(所以可以在)

Collision

响应 行为
Ignore(忽略) 完全当作没发生,Trace 直接穿透,不会出现在结果里,也不会触发任何事件
Overlap(重叠) Trace 会检测到 这个物体(出现在 Trace 结果数组里),但不会阻挡 ------如果是两个物理体的运动碰撞,Overlap 意味着允许穿模、不产生物理推挤,同时会触发 OnComponentBeginOverlap/OnComponentEndOverlap 这类事件
Block(阻挡) 真正意义上的"撞到了"------Trace 会在这个物体表面停止 ,返回命中点信息;如果是物理运动,会产生实际的碰撞响应(推挤、反弹、贴着表面滑动),触发 OnComponentHit

7.GAS!!!

1.初始化

需要使用CreateDefaultSubobject来使得能够在Editor中被看到并且被调用

2.关于两种持有性的讨论

第一种是直接放在Pawn上面的,这样做通常在Pawn消亡的时候会带着他的所有的ASC和AS同时消失所以这样的做法可能适合一些NPC这种不会复活,不需要保留其能力以及属性的。

第二种做法就是在PlayerState上面,这样做的好处是能够在角色死亡的时候单独将能力和属性拖出,并且由于他是属于PlayerState的所以不会随着玩家死亡而被GC(这里可能不准确)

注意:

有个接口这个是官方定义的,就是获取当前的ASC,虽然自己也能定义一个但是官方的可能还有其他的能通过这个访问到一些东西,所以最好还是用这个,至于属性集合就可以直接用Getter来获取了

3.关于Replicated Mode(网络同步)

先记住Player用Mixed,AI敌人用Minimal

4.Init Ability Actor Info

Avatar Actor:指的是在世界中能被看见的

Owner Actor:是持有这个ASC的

这样做的原因是服务器比客户端更早拥有PlayerState,并且要等到客户端将PlayerState复制到客户端的时候才能够初始化列表,而ProccessBy是只有服务端会执行,而InitAbilityActor是在复制好了PlayerState之后,所以才能调用设置info的选项,然后如果是单机的话本地既被当作是服务器也被当做是客户端,所以函数会被执行两遍。

注意:

Owner 链的自动设置,跟着"ASC 挂在谁身上"走------挂 Pawn,PossessedBy() 自动设;挂 PlayerState,InitPlayerState() 自动设(登录时就做好,比 Possess 还早);挂在这两者之外的第三方对象上,没人管,必须自己手动 SetOwner(Controller)

为什么要在意这个 :GAS 用 Mixed 复制模式时,靠这条 Owner 链判断"该给哪个客户端发完整数据、给谁只发表现层 Cue"------Owner 没接对,Owner-only 的数据在真实多人环境下会复制失败,但单机测试大概率看不出来。

5.Attributes

一个ASC可以拥有多个Attributes

这段代码是 GAS 里 Health 这个属性做网络复制的标准写法,直接用到了你之前问的"Owner 链决定谁收到什么数据"那套机制的下游产物------这里是数据真正复制到客户端之后,客户端该怎么响应。

UPROPERTY(ReplicatedUsing = OnRep_Health)------这是什么意思

ReplicatedUsingReplicated 的升级版:普通 Replicated 只是"这个变量会自动同步到客户端",而 ReplicatedUsing = OnRep_Health 多了一层------每次这个变量在客户端上被更新时(不管是首次同步还是数值变化),自动调用你指定的这个函数 。这个函数就叫 RepNotify(复制通知)函数,是 UE 网络系统里"数据到了,顺便通知一下客户端代码"的标准机制。

为什么 Attribute 复制几乎总要配一个 RepNotify,而不是普通 Replicated 就够了

单纯把 Health 标记成 Replicated,客户端确实能拿到最新值,但没有任何代码知道这个值刚刚变了 ------你的血条 UI、受伤特效、死亡判断,都需要"知道变化发生了"这个时机,而不是自己每帧去轮询检查数值有没有变。OnRep_Health 就是这个"变化发生的那一刻"的钩子。

函数签名 void OnRep_Health(const FGameplayAttributeData& OldHealth)------为什么要传旧值

这是 GAS 特有的细节,普通 RepNotify 函数通常不需要参数(直接读当前成员变量的新值就行),但这里必须 传入旧值,原因是:客户端本地拿到新值的同时,Health 这个成员变量已经被网络系统直接覆盖了 ,如果 OnRep_Health 内部想对比"到底变化了多少"(比如算这次掉了多少血、要不要播放受击特效),不传旧值进来,函数内部根本无法知道变化前是什么------这也是为什么参数类型专门是 const FGameplayAttributeData&,而不是简单传一个 float

这个时候要调用一下下面这个函数来确保计算的公式的值是统一的,因为有一个东西拿到的是旧值,还有一些原因是这里做的是预测,所以这里同时也照顾到了可能回滚的情况,因为这里能拿到旧的数据

还有关于当前属性如何设置的一个函数需要override,下面是例子规定了某个属性要如何同步

复制代码
void UMyAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Mana, COND_OwnerOnly, REPNOTIFY_Always);
}

教程最后效果:

这样注册了之后就能够使用init和setter和getter这种函数来设置值了

8.UI框架(MVC)

Model

使用的是之前的AttributeSet充当数据

Controller

Baseed on UE5 original TObject,用有角色的一些访问Model的必要组件,创建一个Struct来初始化这些变量

并且负责广播数据,用原生的变化事件绑定我们想要触发的事件

View

拥有上面的Controller,在初始化的时候绑定,并且绑定之后立即初始化自己的其他属性

UI

WBP继承自上面的View,尽量将硬编码换成可编写的并且可继承的变量,这样在子类就方便修改

HUD

第一个是可实例化的对象,第二个是存这个类

尖括号里面的是给编译时期看的,而后面的OverlayWidgetClass是运行时看的,最后赋值给了通用的UUserWidget

负责将controller和UI串联起来,因为可以通过PlayerController得到HUD所以可以在AS info初始化的时候进行同步初始化

9.AttributeEffect

1.首先要对别的AActor产生影响必须是先获取它的ASC,而且GAS也非常贴心的提供了一个获取一个AActor的ASC的接口

其内部实现原理是通过AActor自己继承了能够获取ASC的接口,然后直接调用这个接口的函数得到的,如果没实现也没关系,这个函数会自己找使用Find

1.得到作用的Actor的实例然后通过他获得ASC组件,然后通过ASC创建上下文,这个上下文包含了很多东西,有网络同步的有效果的

2.在这个里面初始化一些东西包括AddSourceObject这种玩意

3.最后就是创建这个Effect然后所用到Target对象中去

4.如果是怪物vs角色这样写估计是不行的,因为这里的MakeEffectContext需要读取的是怪物自身的信息,比如说攻击力和buff之类的,但是因为这里是药水,药水可能会得益于自己的收治疗加成变化,所以这里干脆写成自己了

5.在蓝图中调用这个函数就可以直接使用了,如下.

小结:

1.GE的效果是通过Class 得到CDO模板然后使用模板创建GE实例

2.Context是包括了很多背景信息的"文档",比如下图所示

3.然后我们通过这两个信息创建对应的Handle,再将这个Handle的Data运用到某个Actor当中去。但是具体的效果还是在那个GE里面实现

不同类型的Effect产生的不同影响

Periodic只能在GE设置为HasDuration的时候用,其他的时候是用不了的,而且加上了Periodic之后这个效果就是永久性的了,因为是直接添加在BaseValue上面的了,之前的HasDuration是添加在了CurrentValue上面

Effect Stacking

By Source

By Target

这三个分别是如何刷新Duration(第一个,可选捡起的时候刷新时间,也可以不刷新)

Periodic是更新周期,也就是我捡起时候是需要再等一个周期还是不用等直接开始下一个周期

最后是当持续时间消失之后是直接清空所有还是清除一个然后清除所有,这个可以根据需求来实现

Coding

现在有一个中心处理函数用来ApplyEffect(target, EffectClass);

因为施加Effect的策略有三个,分别是Instant,Duration,Infinite这三种,所以为了确认我们想要的哪一种,我们需要对每一种进行一个标记,比如Instant的BeginOverlapPolicy可以是None,然后EndOverLapPolicy也是None,就表示当前这个Effect的效果是怎样的,同理Infinite也是这样的

然后我们就只需要对此进行判断就好,有些Infinite是在EndOverlap就要取消的,所以这里需要用Map记录每一个进来的和出去的,以及remove的API后面有专门为Stack准备的-1函数.

Clamp Attribute(!!!缺点是之后的修改就不能奏效了)

继承这个函数,这个函数用于真正将NewValue之前,可以对NewValue进行的一些限制,比如说生命值即将低于0或者高于Max的时候将其限制在0-Max之中。

PostGamePlayEffectExcute(下面是获取前置可能需要的信息)

cpp 复制代码
void UAuraAttributeSet::SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const
{
	// Source = causer of the effect, Target = target of the effect (owner of this AS)

	Props.EffectContextHandle = Data.EffectSpec.GetContext();
	Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();

	if (IsValid(Props.SourceASC) && Props.SourceASC->AbilityActorInfo.IsValid() && Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
	{
		Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
		Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
		if (Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
		{
			if (const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
			{
				Props.SourceController = Pawn->GetController();
			}
		}
		if (Props.SourceController)
		{
			Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
		}
	}

	if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
	{
		Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
		Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
		Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
		Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
	}
}

这里关于为什么能得到Controller是因为在初始化Info之后会对俩进行查找,尽可能找到Controller,然后我们通过controller就可以得到真正的控制的角色了,下面是当时看视频时思考的问题

流程图

最后就是用Curve修改Effect值,唯一需要注意的是传入是在ApplyEffect之中的.

关键的API

复制代码
OnGameplayEffectAppliedDelegateToSelf

用于当ASC检测到有GE影响到自己触发的函数(仅在服务端执行)

复制代码
PreAttributeChange

在设置curValue之前在Modifier之后做的操作,不会修改BaseValue,如果要修改的话就直接用和GetHealt得到Curvalue然后再SetHealth设置BaseValue,这就就能保证效果,因为药水这种Instant修改的都是BaseValue所以之前那个Pre不起作用

关于GE的修改

可以按一句话记:

GE 修改 BaseValue 还是 CurrentValue,主要由 Duration Policy 和是否设置 Period 决定,与 Add、Multiply、Override 等 Modifier Operation 无关。

GE 类型总结

GE 配置 修改对象 GE 结束后是否恢复 常见用途
Instant BaseValue 不恢复 伤害、治疗、消耗 Mana
Has Duration,无 Period CurrentValue 恢复 临时加攻速、临时减移速
Infinite,无 Period CurrentValue 移除 GE 后恢复 装备、光环、永久存在的 Buff
Has Duration,有 Period 每次周期修改 BaseValue 已发生的修改不恢复 中毒、持续治疗
Infinite,有 Period 每次周期修改 BaseValue 已发生的修改不恢复 无限回血、持续掉血

Epic 官方也将其概括为:Instant GE 修改 BaseValue;有持续时间的 GE 通常作用于 CurrentValue,而周期性 GE 每次周期都会执行。Epic GAS 文档

1. Instant GE:修改 BaseValue

例如:

复制代码
Health Base    = 100
Health Current = 100

应用一个 Instant GE:

复制代码
Health Add -20

结果:

复制代码
Health Base    = 80
Health Current = 80

GE 执行完立即消失,但是扣除的生命值不会恢复。

适合:

  • 普通伤害;
  • 立即治疗;
  • 消耗 Mana;
  • 增加经验;
  • 修改永久属性。

它会调用:

复制代码
PreAttributeBaseChange()
PreAttributeChange()
PostGameplayEffectExecute()

其中 PostGameplayEffectExecute() 专门用于处理这种已经执行并修改 BaseValue 的 GE。


2. Duration GE,无 Period:修改 CurrentValue

例如:

复制代码
MoveSpeed Base    = 500
MoveSpeed Current = 500

应用一个持续 5 秒的 GE:

复制代码
MoveSpeed Add +200

生效期间:

复制代码
MoveSpeed Base    = 500
MoveSpeed Current = 700

5 秒结束:

复制代码
MoveSpeed Base    = 500
MoveSpeed Current = 500

它本质上是在 ASC 的属性聚合器中保存了一个临时 Modifier:

复制代码
CurrentValue = BaseValue + Active Modifier
             = 500 + 200
             = 700

这种 GE 不会因为应用或移除而调用:

复制代码
PostGameplayEffectExecute()

因为它没有"执行并修改 BaseValue",而是作为 Active Gameplay Effect 参与 CurrentValue 计算。

但是 CurrentValue 发生变化时会经过:

复制代码
PreAttributeChange()

3. Infinite GE,无 Period:修改 CurrentValue

它与普通 Duration GE 原理相同,只是不会自动到期。

例如装备提供 50 点护甲:

复制代码
Armor Base     = 100
装备 Modifier  = +50
Armor Current  = 150

移除装备对应的 Infinite GE 后:

复制代码
Armor Base     = 100
Armor Current  = 100

适合:

  • 装备属性;
  • 被动技能;
  • 常驻光环;
  • 姿态加成。

虽然 UI 可能长期显示 150,但 BaseValue 仍然是 100。


4. 周期性 GE:每次 Period 修改 BaseValue

这是最容易误解的类型。

例如创建:

复制代码
Duration = 5秒
Period   = 1秒
Health Modifier = Add -10

它不是让 CurrentValue 临时变成:

复制代码
BaseValue - 10

而是每隔一秒执行一次,永久修改 BaseValue:

复制代码
开始:BaseHealth = 100
1秒:BaseHealth = 90
2秒:BaseHealth = 80
3秒:BaseHealth = 70
4秒:BaseHealth = 60
5秒:BaseHealth = 50

GE 到期以后:

复制代码
BaseHealth = 50

不会恢复成 100。

所以周期性中毒、持续治疗实际上是:

每个 Tick 都像执行了一次 Instant GE。

每次周期执行都会调用:

复制代码
PostGameplayEffectExecute()

因此你可以在这里进行 Health Clamp、伤害响应、死亡判断等处理。

Execute Periodic Effect on Application 只决定第一次周期效果是否在 GE 应用时立即执行,不改变它修改 BaseValue 的性质。

Modifier Operation 不决定 Base/Current

下面这些选项:

复制代码
Add
Multiply
Divide
Override

只决定如何计算数值,不决定修改 BaseValue 还是 CurrentValue。

例如:

复制代码
Instant + Add          → 修改 BaseValue
Instant + Override     → 修改 BaseValue

Duration + Add         → 修改 CurrentValue
Duration + Multiply    → 修改 CurrentValue

Periodic + Add         → 每个周期修改 BaseValue
Periodic + Override    → 每个周期修改 BaseValue

同样,以下 Magnitude 类型也不决定 Base/Current:

复制代码
Scalable Float
Attribute Based
Custom Calculation Class / MMC
Set By Caller

它们只负责算出 Modifier 的大小。

与几个回调的对应关系

回调/接口 主要处理的值
PreAttributeBaseChange 即将写入的 BaseValue
PreAttributeChange 即将生效的最终 CurrentValue
PostGameplayEffectExecute Instant/Periodic GE 修改 BaseValue 后的处理
GetHealth() CurrentValue
SetHealth() 设置 BaseValue,并重新计算 CurrentValue
属性变化 Delegate 的 Data.NewValue CurrentValue
ShowDebug AbilitySystem 主数值 CurrentValue

最实用的记忆例子

复制代码
普通伤害:
Instant GE → BaseValue

一次治疗:
Instant GE → BaseValue

5秒增加20%攻击力:
Duration,无Period → CurrentValue

装备增加50护甲:
Infinite,无Period → CurrentValue

每秒受到10点毒伤:
Duration,有Period → 每次修改BaseValue

每秒恢复5点生命:
Infinite/Duration,有Period → 每次修改BaseValue

对于你的 Health,如果所有伤害和治疗都是 Instant GE,那么通常使用:

复制代码
void UAuraAttributeSet::PostGameplayEffectExecute(
    const FGameplayEffectModCallbackData& Data)
{
    Super::PostGameplayEffectExecute(Data);

    if (Data.EvaluatedData.Attribute == GetHealthAttribute())
    {
        SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
    }
}

这样每次 Instant 或 Periodic GE 修改完 Health 的 BaseValue 后,都会把 BaseValue 和重新计算的 CurrentValue限制在合法范围内。

关于AttributeSet的初始化

1.硬编码初始化

直接在Inti里面做Init*AttributeName*(yourvalue)来进行初始化,非常垃圾的一种方法。

2.使用DT表格初始化

ASC系统提供的原生初始化方法如下,只要表格中的内容和AT的名字一样然后属性字段的名称一样就行,but 在源代码中显示的是to be down(5.7版本也一样)所以这个目前位置只适用于初始化。

3.使用Effect初始化

首先回顾一下ASC和AttributeSet分配在哪里,由于角色是可以死亡然后复活的,所以需要将ASC和AttributeSet挂在到角色身的PlayerState上面,然后PlayerState的初始化又必须在PossessedBy之后,所以我们初始化也是随着ASC的初始化做初始化,而PossessedBy一般都是在Character当中的所以,因为在PossessedBy这个函数之后才有PlayerState,所以帮助ASC->setInfo()

然后就是Effect的初始化,所以初始化我们可以直接定义一个和EffectActor一样的ApplyEffect函数然后在初始化的过程中调用它达到初始化的目的。

相关推荐
她说..44 分钟前
MySQL JSON 处理学习文档
学习·mysql·json
资讯综合1 小时前
一年级数学机构怎么选?与学习阶段相匹配的线上思维课程与线下机构差异分析
学习
M78佐菲1 小时前
51单片机学习笔记(2)
linux·笔记·嵌入式硬件·学习·51单片机
子不语1802 小时前
LabVIEW和PLC通过OPC通信
学习·自动化·plc
萧行之2 小时前
Observable Plot 源码深度解析——4 核心抽象逐项学习
前端·学习·数据可视化
浔溺2 小时前
al+大数据每日学习笔记37
大数据·笔记·学习
TheBestRucy2 小时前
PyTorch 基本使用学习笔记
pytorch·笔记·学习
ouynagda2 小时前
51 单片机 LED 流水灯 + 数码管动态扫描学习笔记(STC89C52)
笔记·单片机·学习