Unreal GAS系统及网络使用学习 前置知识 UE网络架构
简介 Gameplay Ability System,简称(GAS)是一个健壮的、高度可扩展的、gameplay框架,通常用于构建RPG、MOBA等游戏的完整战斗逻辑框架。通过GAS,可以快速地制作游戏中的主动/被动技能、各种效果buff、计算属性伤害、处理玩家各种战斗状态逻辑。
GAS试图将机制提取为通用的游戏设计模式 ,并提供框架来解决常见的Gameplay实现问题,同时使上下文因项目而异。
GAS提供了哪些功能?
实现了带有消耗和冷却功能的角色技能
处理数值属性(生命、魔法、攻击力、防御力)
应用状态效果(击飞、着火、眩晕)
应用游戏标签(GameplayTags)
生成特效和音效
完整的网络复制、预测功能
适合使用GAS的项目
C++项目,开发人员有充足的C++开发经验
使用Dedicated Server专用服务器的联机游戏(单纯的游戏逻辑服务端,与Dedicated Server相对的是Listen Server,其中一位玩家充当服务器,容易出现“主机优势”)
项目有大量且复杂的技能逻辑设计需求
GAS的主要优势
网络复制(Network Replication): 不必担心你的属性或减益效果得不到妥善地应用或复制。GAS会为你处理内部逻辑。
模块化(Modularity): 添加或更改游戏机制通常与实现和赋予新技能一样简单。通过将Gameplay功能分解为单独的资产,技能系统可以在完全不同的游戏对象或机制之间提供通用的通信层。例如,生命值(Health)可以划分到自己的属性集,并通过来自各种系统的Gameplay效果进行交互。
快速迭代(Fast iteration): GAS可以轻松更改单个游戏规则,而无需修改整个系统。用于计算游戏的数据源可以轻松交换,并且可以从相应的Gameplay效果中修改动作效果。
我为什么学习GAS 编写样板代码通常易出错且耗时,尤其是对于多人游戏而言。例如,你不希望花费大量时间来确保你的生命(Health)值正确复制,或者在你决定具有相同行为的能量(Energy)值时,复制相同的代码行。
GAS通过提供尽可能实现常见Gameplay功能的基础来解决这些问题,同时保持机制中立。GAS并非强制使用诸如生命值(Health)、弹药(Ammo)、近战攻击(Melee Attack)或毒药减益(Poison Debuff)之类的概念,而是提供了能够定义、复制和使用 属性(Attributes) 、 技能(Abilities) 和 效果(Effects) 的工具,然后你可以将这些工具专用于满足给定Gameplay机制的需求。
于我而言 :公司的项目需要使用到联网的游戏服务。所以你会在我的笔记中看到一些斜体的备注,这些备注意味着对我自己项目的备注。可以忽略。
GAS的组件 ASC:who谁能放技能? Ability System Component(ASC)是整个 GAS 的基础组件。ASC 本质上是一个 UActorComponent,用于处理整个框架下的交互逻辑,包括使用技能(GameplayAbility)、包含属性(AttributeSet)、处理各种效果(GameplayEffect)。所有需要应用 GAS 的对象(Actor),都必须拥有 GAS 组件。(即所有有技能交互的角色都是ASC)。
ASC是一种角色组件,负责和GA、GE、AS打交道。
一般只放在Character or PlayerState上。
拥有 ASC 的 Actor 被称为 ASC 的 OwnerActor,ASC 实际作用的 Actor 叫做 AvatarActor。
GA :How:技能的逻辑?Gameplay Ability(GA)标识了游戏中一个对象(Actor)可以做的行为或技能 。它代表角色可以执行的任何技能或行为。 能力(Ability)可以是普通攻击或者吟唱技能,可以是角色被击飞倒地,还可以是使用某种道具,交互某个物件,甚至跳跃、飞行等角色行为也可以是Ability。 Ability可以被赋予对象或从对象的ASC中移除,对象同时可以激活多个GameplayAbility。
基本的移动输入、UI交互行为则不能或不建议通过GA来实现。
通过蓝图继承GamePlay Ability来实现
GA的执行流程
graph TD
A[TryActivateAbility] --> B{CanActivateAbility}
B -->|Yes| C[ActivateAbility]
B -->|No| D[EndAbility]
C --> E[Start AbilityTask]
E -->|Some Time Later| F[CommitAbility]
F -->|Can't Afford| D
F --> G[Start AbilityTask]
G -->|Some Time Later| H[Apply GameplayEffects]
G --> I[Start AbilityTask]
I -->|Some Time Later| J[Apply GameplayEffects]
I -->|Some Time Later| K[End Ability]
D --> L[OnEndAbility]
K --> L
L --> M[Cleanup]
TryActivateAbility :首先判断一下能不能执行。
ActivateAbility :执行一套技能(有技能动画,比如发射一个火焰)。
CommitAbility :技能释放了成功了,要扣蓝。
Start AbilityTask * 2:技能执行中…技能执行结束。然后应用对应效果。
其中ActivateAbility和End Ability是类似于BeginPlay和EndPlay这样的函数,在蓝图中调用。
蓝图中的设置 主要选项解释 常见蓝图
高级设置
Replication Policy (复制策略): 决定该Ability的激活、执行等信息是否在网络上传播。常见选项有:
Not Replicated:仅本地执行,不同步到其他客户端/服务器。
Replicated:在服务器和客户端之间同步。
Instancing Policy (实例化策略): 决定Ability的实例化方式。
Non-Instanced:所有激活者共享同一个Ability对象,适合无状态逻辑。
Instanced Per Actor:每个拥有者有独立实例,适合有状态逻辑。
Instanced Per Execution:每次激活都新建实例,适合需要隔离状态的复杂技能。
Server Respects Remote Ability Cancellation (服务器尊重远端取消):勾选后,服务器会响应客户端请求取消Ability的操作。适合需要客户端主动取消技能的场景。
Retrigger Instanced Ability (可重复触发实例化Ability):允许Ability在已激活时再次被激活(会重新实例化),适合可叠加或可重复释放的技能。
Net Execution Policy (网络执行策略):决定Ability的激活和执行在哪端进行。
Local Predicted:客户端预测执行,服务器校验。
Local Only:仅本地执行。
Server Only:仅服务器执行。
Server Initiated:服务器发起,客户端可参与。
Net Security Policy (网络安全策略):控制Ability的安全等级,防止被恶意客户端滥用。
Client Or Server:客户端和服务器都可激活。
Server Only Execution:只有服务器可激活。
Server Only Execution and Data:只有服务器可激活且数据仅在服务器。
GA要做的事情
一般GA要做的事有:
设置GA的Tag、CD、Cost等属性。
获取必要信息,主要通过Get Actor Info。如果是通过Event调用的GA(使用Activate Ability From Event节点作为输入),还可以通过Gameplay Event Data获取。
编写逻辑,如播放动画、应用GE、应用冲量等。
一定不要忘了EndAbility。
GA的调用 GA的调用分成了主动调用(释放技能)和被动调用(挨打)两类,下面依次介绍不同的调用方法。
主动调用 在蓝图中主要有by Class和by Tag 两种调用方法。
byClass一次只能Activate一个GA,byTag可以Activate任意多个GA,配合Tag容器使用。
如果使用EnhancedInputAction插件来管理输入,要注意在某些设置下trigger会每帧都进行输出。
只要能获取ASC,就可以在任何地方调用GA,比如行为树Task蓝图,甚至在GA蓝图中调用其他GA。
被动调用 Trigger可以理解为一个Tag,当ASC组件收到一个Trigger时,就会自动调用所有拥有该Trigger的GA。
Trigger的Tag在GA的details面板中设置。
Trigger的触发方式有三种,分别是:
Gameplay Event: 当Owner收到一个带有Tag的Gameplay Event(不是Gameplay Effect的GE! )时调用一次 GA,此时Owner不会拥有对应的Tag 。
Owner Tag Added: 当Owner获取对应Tag 的时候调用一次 GA。
Owner Tag Present: 当Owner拥有Tag 时调用GA,失去Tag时移除。
一般使用第一种方法,并配合SendGameplayEventToActor节点使用,如下图所示。(这张图是很久以前截的,Tag建议以Event开头)
受击效果的例子,发送一个Tag为Hit的Event给碰撞检测到的Actor
使用Gameplay Event调用的好处是,可以传入数据(Payload),是除了Get Actor Info外的另一种信息传递方法。
此时应该删除ActiveAbility节点,转而使用ActivateAbilityFromEvent事件。(不要通过在左上角重载函数的方式,右键空白处搜索才是对的 )
GE:What技能改变的属性? Gameplay Effect(GE)是Ability对自己或他人产生影响的途径 。 GE通常可以被理解为我们游戏中的buff。比如增益/减益效果(修改属性)。 但是GAS中的GE也更加广义,释放技能时候的伤害结算,施加特殊效果的控制、霸体效果(修改GameplayTag)都是通过GE来实现的。
GE只是一个可配置的*数据表 * ,不可以添加逻辑。开发者创建一个UGameplayEffect的派生蓝图,就可以根据需求制作想要的效果。
GE是纯蓝图
GT:if技能改变的条件? FGameplayTags是一种层级标签,如Parent.Child.GrandChild。 通过GameplayTagManager进行注册。 替代了原来的Bool,或Enum的结构,可以在玩法设计中更高效的标记对象的行为或状态 (比如一个灼烧效果,GE完成之后,便可以取消标签)。 我理解是一个在Actor下的Json。
Tag的层级关系 也需要合理设计,到了后期修改成本比较大。
Attribute Set 负责定义和持有属性,并且管理属性的变化 ,包括网络同步 。 需要在Actor中被添加为成员变量,并注册到ASC(C++)。 一个ASC可以拥有一个或多个(不同的)AttributeSet,因此可以角色共享一个很大的Attribute Set,也可以每个角色按需添加Attribute Set。 可以在属性变化前(PreAttributeChange)后(PostGameplayEffectExecute)处理相关逻辑,可以通过委托的方式绑定属性变化。
Player State
PlayerState 是 Unreal Engine 中用于存储玩家相关信息的特殊 Actor 类。在多人游戏中,PlayerState 会自动在服务器和所有客户端之间复制,非常适合存储需要跨网络同步的玩家数据。
在 GAS 系统中,ASC 可以选择挂载在 PlayerState 上而不是 Character 上 。这样做的好处是:
持久性 :当角色死亡或重生时,PlayerState 依然存在,技能和属性数据不会丢失
网络复制 :PlayerState 天然支持网络复制,适合多人游戏场景
UI 访问 :UI 可以更方便地访问玩家的技能和属性信息,无需依赖具体的 Character 实例
观察者模式 :即使玩家处于观察者状态(没有 Character),也能保持技能系统的状态
常见的做法是:
玩家控制的角色 :ASC 放在 PlayerState 上
AI 控制的角色 :ASC 直接放在 Character 上
组件总结 Visual:技能的视觉效果?GameplayCue Async:技能的长时行动?(异步)GameplayTask Send:技能的消息事件?GamePplayEvent 这俩是同一个东西
其他背景概念 RPC RPC = Remote Procedure Call,远程过程调用。
在 UE 网络里,它的意思是:
你在客户端调用一个函数,但这个函数的实现会在服务器执行(Server RPC);
或者在服务器调用一个函数,但它会在某些/所有客户端执行(Client/NetMulticast RPC)。
它本质是:
把“函数名 + 参数”打包成一段网络消息(通过 actor channel)
发送到网络对端
对端收到后,再去调用对应的 _Implementation 函数
RPC 成功需要两个条件:
你调用的对象在网络上“有通道/可发送”(通常是 Actor 或挂在 Actor 上的东西)
引擎知道这条网络消息应该发给谁(服务器 or 某个客户端)
“路由”(routing) 你可以把“路由”理解成一句话:这条 RPC 消息到底通过哪个网络连接发出去?发给谁?引擎怎么知道? 在 UE 里,RPC 最终是靠 Actor 的网络连接/通道发送的(actor channel + owning connection)。 而 UTCGGameplayAbility这种 UObject(不是 Actor)它自己没有网络连接,它的 RPC 需要“借道”
借谁的道? 答案:借它所属的/可追溯到的 Actor 通道(通常是 ASC 的 OwnerActor / AvatarActor / PlayerController 这条链)。
最典型的归属链(客户端玩家)应该是: PlayerController(拥有网络连接 owning connection) 👆owns Pawn/Character 👆Pawn 上有 ASC(或者 ASC 的 OwnerActor 是 Pawn/PlayerState) 👆Ability 从 ASC 拿 ActorInfo
所以当你在 Ability(UObject)里调用 Server RPC 时,UE 会尝试沿着 Ability 的 ActorInfo 找到: 哪个 Actor 有 owning connection(通常是 PlayerController 或被它拥有的 Pawn)再通过那条连接把 RPC 发到服务器。
基础使用 开启插件
C++项目 ,安装Gameplay Ability插件。
在Tools菜单下面有GameplayCue Editor有了,就说明开启成功了。
在新建的蓝图的页面可以看到GamePlay相关的东西
使用Rider快速添加Unreal类
通过这里创建一个Character。可以快速拥有一些头文件。
ASC组件架构
classDiagram
class ACharacter
class ARPGCharacterBase {
+AbilitySystemComponent
}
class BP_Character {
+公共逻辑
}
class BP_PlayerCharacter {
-组装Camera等组件
-输入绑定
}
class BP_NPCCharacter
ACharacter <|-- ARPGCharacterBase
ARPGCharacterBase <|-- BP_Character
BP_Character <|-- BP_PlayerCharacter
BP_Character <|-- BP_NPCCharacter
你需要自己创建一个继承自ACharacter的C++类。
之后想用蓝图,就从这个自定义的C++Character类派生就可以了。
写一个Base GAS ACharacter文件 1 2 3 4 5 PublicDependencyModuleNames.AddRange(new string [] { "Core" , "CoreUObject" , "Engine" , "InputCore" , "EnhancedInput" , "GameplayAbilities" ,"GameplayTags" ,"GameplayTasks" });
在Build文件里添加如下的内容GameplayAbilities、GameplayTags、GameplayTasks。
在头文件里面添加一个AbilitySystemComponent,直接在角色上挂载 AbilitySystemComponent(ASC),可以适配多人游戏。
1 TObjectPtr<UTCGAbilitySystemComponent> AbilitySystemComponent;
在.cpp中构造函数部分实例化AbilitySystemComponent(ASC)。
1 2 AbilitySystemComponent = CreateDefaultSubobject <UAbilitySystemComponent>(TEXT ("AbilitySystem" ));
角色类继承IAbilitySystemInterface接口,并实现GetASC函数
1 2 3 4 5 #include "AbilitySystemInterface.h" class ARPG_UNREAL_API ACharacterBase : public ACharacter, public IAbilitySystemInterfacepublic :UAbilitySystemComponent* GetAbilitySystemComponent () const override ;
在CPP中实现
1 2 3 4 UAbilitySystemComponent* ACharacterBase::GetAbilitySystemComponent () const { return AbilitySystemComponent; }
👆这里的return即使写为null,也可能可能调用
这个在项目中的路径在:Source/TCG_AwesomeLive/CharacterSystem/TCGCharacterBase.h
把GA数组加入到Actor中 可以将UE原生的UGameplayAbility直接加入到需要Ability的Actor中(也是加到这个Base GAS ACharacter文件中)
1 2 UPROPERTY (BlueprintReadOnly, EditAnywhere, Category = "Test|Abilities" )TArray<TSubclassOf<UGameplayAbility>> CharacterAbilities;
在C++中赋予GA :我们可以将我们所有的技能储存在一张DA表之中,然后从DA表里读取数据,并使用AbilitySystemComponent->GiveAbility将数据赋予ASC。
1 2 3 4 5 6 7 8 9 10 11 12 13 for (FTCGAblityInfo& Info : AllAbilitiesInfo){ if (Info.AbilityClass == nullptr ) continue ; CharacterAbilities.Add (Info.AbilityClass); } for (auto StartupAbility : CharacterAbilities){ AbilitySystemComponent->GiveAbility ( FGameplayAbilitySpec (StartupAbility, 1 , INDEX_NONE, this ) ); }
这样我们就可以在继承了Base Character的子类中,看到Ability的选项了。
赋予GA的方式有很多,比如在需要的蓝图中调用
Give Ability 节点。还可以使用GE赋予GA。
在官方的Lyra案例里面使用AbilitySet来储存一类Ability,通过加载来实现赋予。参看下文Unreal官方案例“赋予GA”部分。
Unreal官方案例 UE中有一个非常不错的官方示例Lyra 里面展示了一个完整的GAS系统。
项目里的设置
这个项目里开发者设置了一些调整选项,可以设定一些debug的参数。
比如子弹射击停留时间,生成的bot的数量。
项目蓝图常见前缀标识 WID_ 全称可理解为 Weapon Item Definition。 用途:武器类的“物品定义”蓝图资产,继承自 ULyraInventoryItemDefinition(你在代码里看到的基类)。ID_ 可理解为(Generic)Item Definition。 用途:非武器的一般物品定义(比如消耗品、弹药、包裹、可堆叠资源等)也继承 ULyraInventoryItemDefinition,用 ID_ 前缀区分与 WID_(武器专用)资产。B _ B_ 前缀在 Lyra 中通常代表一个 Blueprint 类资产(等同很多团队常用的 BP_,Lyra 项目里取了更短的 B_)。
源码解析
这一部分我主要参考的是这里 加上了一些我自己的补充。整体结构采用了原本的结构,但是有我自己的删减,可以对照地看。
初始化Ability System 创建ASC ASC用于总体控制GAS功能,可以挂在Character上或者PlayerState上,对于角色会死亡重生的场景,挂在PlayerState上更合适,不会因Character销毁而丢失数据。因此Lyra选择把ASC挂在了PlayerState上,这样还有额外作用,就是按Tab键显示计分板这种战斗无关的功能,也可以用GA实现,以往的习惯都是硬编码。
ULyraAbilitySystemComponent本身是一个Manager,与数据无关,所以PlayerState直接New了一个对象即可:
Source/LyraGame/GameModes/LyraGameState.h/cpp
1 2 3 4 5 6 7 8 UPROPERTY (VisibleAnywhere, Category = "Lyra|PlayerState" )TObjectPtr<ULyraAbilitySystemComponent> AbilitySystemComponent; AbilitySystemComponent = ObjectInitializer.CreateDefaultSubobject <ULyraAbilitySystemComponent>(this , TEXT ("AbilitySystemComponent" )); AbilitySystemComponent->SetIsReplicated (true ); AbilitySystemComponent->SetReplicationMode (EGameplayEffectReplicationMode::Mixed);
ASC里面也会记录Actor
Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemComponent.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private : UPROPERTY (ReplicatedUsing = OnRep_OwningActor) TObjectPtr<AActor> OwnerActor; UPROPERTY (ReplicatedUsing = OnRep_OwningActor) TObjectPtr<AActor> AvatarActor; public : TSharedPtr<FGameplayAbilityActorInfo> AbilityActorInfo;
ASC也会记录关联Actor的信息:
OwnerActor 挂在哪个Actor上,这里是PlayerState;
AvatarActor 是使用Ability实体,这里是Character。
这两个属性是ASC经常要获取的变量,除此之外,还有多个与Actor关联的属性会被高频读取,为了提高读取性能,ASC索性把它们复制了一份,集中存储在AbilityActorInfo 变量中,包括了PlayerController,SkeletalMeshComponent,AnimInstance,MovementComponent,AffectedAnimInstanceTag。
赋予GA(前建立Experience做数据衔接) 创建完ASC后,要往这个容器里赋予GA了,表示Actor有这些能力。
Lyra工程高度模块化,配置比较分散,使用AbilitySet来储存一类Ability,通过加载来实现自动化配置和赋予GA。
首先创建一个Experience,首先什么是Experience?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 UCLASS (BlueprintType, Const)class ULyraExperienceDefinition : public UPrimaryDataAsset{ ... public : UPROPERTY (EditDefaultsOnly, Category = Gameplay) TArray<FString> GameFeaturesToEnable; UPROPERTY (EditDefaultsOnly, Category=Gameplay) TObjectPtr<const ULyraPawnData> DefaultPawnData; UPROPERTY (EditDefaultsOnly, Instanced, Category="Actions" ) TArray<TObjectPtr<UGameFeatureAction>> Actions; UPROPERTY (EditDefaultsOnly, Category=Gameplay) TArray<TObjectPtr<ULyraExperienceActionSet>> ActionSets; };
如图Lyra的结构。
继承自ULyraExperienceDefinition的:B_BasicShooterTestB_BasicShooterTest这个数据类里的Action分组里面,有需要授予的GA的DA表。
比如:**AbilitySet_Elimination(下面那个) 和 HeroData_ShooterGame里面的 AbilitySet_**ShooterHero。
首先看AbilitySet_Elimination DA表(它继承自LyraAbilitySet)。
打开这个AbilitySet_Elimination,里面包含两个GA。 GA_ShowLeaderboard_TDM用于按Tab显示计分板。 GA_AutoRespawn用于死亡复活,这两个GA与Character战斗无关,在没用Character时也能执行。
然后看AbilitySet_ShooterPistol DA表(储存在总表的HeroData_ShooterGame里面 它继承自ULyraPawnData)
包含Character相关的GA,数量较多。比如GA_Hero_Jump实现了跳跃,GA_Emote实现了跳舞。
然后还有一个AbilitySet_ShooterPisto
它不是定义在B_BasicShooterTestB_BasicShooterTest这张大表里面的。
而是通过WID_Pistol被具体的蓝图调用。
然后对每个GA,调用UAbilitySystemComponent::GiveAbility函数进行赋予。
会在很多地方调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility (const FGameplayAbilitySpec& Spec) { ABILITYLIST_SCOPE_LOCK (); FGameplayAbilitySpec& OwnedSpec = ActivatableAbilities.Items[ActivatableAbilities.Items.Add (Spec)]; if (OwnedSpec.Ability->GetInstancingPolicy () == EGameplayAbilityInstancingPolicy::InstancedPerActor) { CreateNewInstanceOfAbility (OwnedSpec, Spec.Ability); } OnGiveAbility (OwnedSpec); MarkAbilitySpecDirty (OwnedSpec, true ); UE_LOG (LogAbilitySystem, Log, TEXT ("%s: GiveAbility %s [%s] Level: %d Source: %s" ), *GetNameSafe (GetOwner ()), *GetNameSafe (Spec.Ability), *Spec.Handle.ToString (), Spec.Level, *GetNameSafe (Spec.SourceObject.Get ())); UE_VLOG (GetOwner (), VLogAbilitySystem, Log, TEXT ("GiveAbility %s [%s] Level: %d Source: %s" ), *GetNameSafe (Spec.Ability), *Spec.Handle.ToString (), Spec.Level, *GetNameSafe (Spec.SourceObject.Get ())); return OwnedSpec.Handle; }
函数参数为FGameplayAbilitySpec,表示“这次赋予”的描述信息。
不仅包含这次赋予的描述信息,比如GA的Class,指定GA的等级,GA绑定的输入Tag。而且也是GA在ASC上的runtime表示,有很多runtime信息。每个Spec都有唯一的ID,称为Handle,通过自增赋予,各系统间Spec的传递都通过Handle实现。
值得注意的是Spec.SourceObject属性,表示这个GA所关联的最“密切”的Object,在创建时由我们指定。 比如GA_Weapon_Fire_Pistol,是枪开火的GA,SourceObject就是武器B_WeaponInstance_Pistol; GA_Hero_Jump是角色跳跃的GA,SourceObejct就是Character; GA_ShowLeaderboard_TDM则是与战斗无关的打开计分板的GA,OurceObject就是PlayerStat。
1 2 3 4 5 6 7 8 9 10 11 12 13 USTRUCT(BlueprintType) struct GAMEPLAYABILITIES_API FGameplayAbilitySpec : public FFastArraySerializerItem { GENERATED_USTRUCT_BODY() ... UPROPERTY(NotReplicated) TArray<TObjectPtr<UGameplayAbility>> NonReplicatedInstances; UPROPERTY() TArray<TObjectPtr<UGameplayAbility>> ReplicatedInstances; }
添加时,首先把Spec加入到ActivatableAbilities数组中,然后根据Policy创建一个GA的实例,默认的InstancedPerActor是要创建的。创建完成后,如果GA设置为网络同步,就加入到Spec.ReplicatedInstances中,否则加入Spec.NonReplicatedInstances。
注册Trigger GA可以绑定一些Trigger,实际是Tag,当发出GamePlayEvent,并指定对应Tag后,就能激活GA了。
所以这里的Tag为了更方便做判断具体的GA。
首先是InputTag,它本身非GAS框架内的,是Lyra自己做的一套输入和GA的映射。InputTag在AbilitySet中指定
AbilitySet_ShooterPisto
AbilitySet_ShooterPistol
AbilitySet_ShooterPisto
创建Spec时会把InputTag添加到其DynamicSpecSourceTags,DynamicSpecSourceTags是一个通用Tag容器。 该Tags在结构体FGameplayAbilitySpec里面,也就是说,一个FGameplayAbilitySpec代表的是传递的一个Ability的能力,然后里面有很多DynamicAbilityTags 。
注册的路径如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void ULyraAbilitySet::GiveToAbilitySystem (ULyraAbilitySystemComponent* LyraASC, FLyraAbilitySet_GrantedHandles* OutGrantedHandles, UObject* SourceObject) const { ... for (int32 SetIndex = 0 ; SetIndex < GrantedAttributes.Num (); ++SetIndex) { ... } for (int32 AbilityIndex = 0 ; AbilityIndex < GrantedGameplayAbilities.Num (); ++AbilityIndex) { const FLyraAbilitySet_GameplayAbility& AbilityToGrant = GrantedGameplayAbilities[AbilityIndex]; if (!IsValid (AbilityToGrant.Ability)) { UE_LOG (LogLyraAbilitySystem, Error, TEXT ("GrantedGameplayAbilities[%d] on ability set [%s] is not valid." ), AbilityIndex, *GetNameSafe (this )); continue ; } ULyraGameplayAbility* AbilityCDO = AbilityToGrant.Ability->GetDefaultObject <ULyraGameplayAbility>(); FGameplayAbilitySpec AbilitySpec (AbilityCDO, AbilityToGrant.AbilityLevel) ; AbilitySpec.SourceObject = SourceObject; AbilitySpec.GetDynamicSpecSourceTags ().AddTag (AbilityToGrant.InputTag); const FGameplayAbilitySpecHandle AbilitySpecHandle = LyraASC->GiveAbility (AbilitySpec); if (OutGrantedHandles) { OutGrantedHandles->AddAbilitySpecHandle (AbilitySpecHandle); } } for (int32 EffectIndex = 0 ; EffectIndex < GrantedGameplayEffects.Num (); ++EffectIndex) { ... } }
在授予ASC Ability的时候,将Tags Effect等信息一并传入。
授予GA的逻辑,以上就是服务器初始化ASC并赋予GA的全部过程。
同步客户端 ASC 首先,ASC作为一个Component挂在PlayerState上,是可以同步的。
由于AvatarActor是Character,因此OwnerActor和AvatarActor的最终设置,需要客户端的Character创建后才能执行。
具体为Character创建后,触发LyraHeroComponent的加载资源流程,当加载完成并且客户端获取到PlayerState后,就把当前阶段设置为DataAvailabe,并最终设置OwnerActor和AvatarActor。
GASpec Spec存于ActivatableAbilities容器中,本身是FastArray,标记了Replicated,可正常网络同步
1 2 UPROPERTY (ReplicatedUsing = OnRep_ActivateAbilities, BlueprintReadOnly, Transient, Category = "Abilities" )FGameplayAbilitySpecContainer ActivatableAbilities;
Spec同步到客户端后,FastArray会触发FGameplayAbilitySpec::PostReplicatedAdd()方法,里面再和服务器同样的UAbilitySystemComponent::OnGiveAbility()方法来注册Spec。
1 2 3 4 5 6 7 8 void FGameplayAbilitySpec::PostReplicatedAdd (const struct FGameplayAbilitySpecContainer& InArraySerializer) { if (InArraySerializer.Owner) { InArraySerializer.Owner->OnGiveAbility (*this ); } }
对于不同步但InstancedPerActor的GA,会在此时创建实例,加入到NonReplicatedInstances数组,然后再注册AbilityTriggers。
Spec的GA实例 首先,GA实例只同步给Autonomous客户端,不会同步给SimulateProxy。然后,会把GA实例作为ASC的SubObject进行同步,这是SubObject同步是UE自身支持的机制。
主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void UAbilitySystemComponent::AddReplicatedInstancedAbility (UGameplayAbility* GameplayAbility) { const ELifetimeCondition LifetimeCondition = bReplicateAbilitiesToSimulatedProxies ? COND_None : COND_ReplayOrOwner; AddReplicatedSubObject (GameplayAbility, LifetimeCondition); } void UActorComponent::AddReplicatedSubObject (UObject* SubObject, ELifetimeCondition NetCondition) { if (AActor* MyOwner=GetOwner ()) { MyOwner->AddActorComponentReplicatedSubObject (this , SubObject, NetCondition); } }
同步到客户端后表现如下:
常用调整命令行 ShowDebug AbilitySystem 展示Ability相关的内容
自己实现方案 通过自定义GA实现复杂参数GAS传递 模块与分层(结构图) 它可以直接从UI层调用。且一个GA可以做多个事情。有回调回UI。
主要关键字:GAS的逻辑 通过创建子GA蓝图 通过蓝图被experience管理 然后被GAS触发 通过RPC传递复杂的设置参数 ServerOnly创建Actor 然后通过Replicated赋值属性回客户端)
通用层(可复用)
SubGameAbility/RequestBridge/UTCGRequestCallbackSubsystem:客户端请求回调桥接器(RequestId 精确匹配)
3DText 特有层
UTCG3DTextModifierLibrary:对外接口(蓝图/UMG 调用入口)
UTCGAbility_Create3DText:服务器创建/修改入口(GAS Ability)
ATCGReplicatedText3DActor:复制“显示参数”并在客户端应用
lang: “zh-CN” 核心数据与关键字段 2.1 FTCG3DTextParams 用于 Create/Modify 的统一参数结构:
Header.RequestId:一次请求的唯一标识(客户端发起时生成)
Header.RequestingActor:请求来源(通常是 PC)
bIsModify:false=Create,true=Modify
TargetActor:Modify 时目标 Actor
文本显示参数:bVisible / SpawnLocation / SpawnRotation / InitialText / TextSize / bOutLine / ...
2.2 ATCGReplicatedText3DActor::ReplicatedParams
ReplicatedUsing=OnRep_TextParams
服务器写入 → 客户端收到后触发 OnRep_TextParams()
Create:完整网络时序(时序图) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Client Server Client | | | | UI 调用 `UTCG3DTextModifierLibrary` | | | 生成 Params.Header.RequestId | | | 在 `UTCGRequestCallbackSubsystem` 注册回调 | | | TryActivateAbility | | |-------------------------------------------------->| (GAS 触发 Ability 激活) | | | `UTCGAbility_Create3DText::ActivateAbility()` | | | 1) CommitAbility | | | 2) ServerOnly: 服务器侧执行 Spawn | | | 3) Spawn `ATCGReplicatedText3DActor` | | | 4) `InitializeFromParams_Server(Params)` | | | - ReplicatedParams = Params | | | - ApplyParamsToComponents(服务器本地) | | | - ForceNetUpdate() | | |-------------------------- Replicate ----------->| | | | `OnRep_TextParams()` | | | ApplyParamsToComponents(客户端) | | | `NotifyClientCompleted(RequestId, Actor)` | | | 回调触发:把 Actor 传给 UI
关键点:
服务器端负责 Spawn Actor 。
客户端看到“完整显示效果”主要依赖:
ReplicatedParams 复制 + OnRep_TextParams() 应用
ForceNetUpdate() 用于加速把新参数推送出去(不是“应用参数”本身)
Modify:完整网络时序(简图) 1 2 3 4 5 6 7 8 9 10 Client Server Client | | | | 组 Params(bIsModify=true)| | | TargetActor=要修改的Actor| | |------------------------->| `ServerText3DActor(Params)` | | | `UpdateFromParams_Server(Params)` | | | -> ReplicatedParams = Params | | | -> ForceNetUpdate() | | |-------------------- Replicate -------->| | | | `OnRep_TextParams()` Apply
相关代码 FTcg3dTextParams.h 文件路径 : 3DText\Struct\FTcg3dTextParams.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 #pragma once #include "GameplayAbilitySystem/SubGameAbility/RequestBridge/TCGRequestBridgeTypes.h" #include "Text3DActor.h" #include "Text3DComponent.h" #include "Materials/MaterialInterface.h" #include "Extensions/Text3DDefaultMaterialExtension.h" #include "FTcg3dTextParams.generated.h" USTRUCT(BlueprintType) struct FTcg3dTextParams { GENERATED_BODY() UPROPERTY(BlueprintReadWrite, EditAnywhere) FTCGRequestHeaderBase Header; UPROPERTY(BlueprintReadWrite, EditAnywhere) TObjectPtr<AActor> TargetActor = nullptr; UPROPERTY(BlueprintReadWrite, EditAnywhere) bool bIsModify = false ; UPROPERTY(BlueprintReadWrite, EditAnywhere) bool bVisible = true ; UPROPERTY(BlueprintReadWrite, EditAnywhere) FVector SpawnLocation = FVector::ZeroVector; UPROPERTY(BlueprintReadWrite, EditAnywhere) FRotator SpawnRotation = FRotator::ZeroRotator; UPROPERTY(BlueprintReadWrite, EditAnywhere) FText InitialText = FText::FromString(TEXT("New Text" )); UPROPERTY(BlueprintReadWrite, EditAnywhere) float TextSize = 50.0f ; UPROPERTY(BlueprintReadWrite, EditAnywhere) bool bOutLine = false ; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "TCG|3DText|Material" ) TObjectPtr<UMaterialInterface> FrontMaterial = nullptr; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "TCG|3DText|Material" ) TObjectPtr<UMaterialInterface> BevelMaterial = nullptr; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "TCG|3DText|Material" ) TObjectPtr<UMaterialInterface> ExtrudeMaterial = nullptr; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "TCG|3DText|Material" ) TObjectPtr<UMaterialInterface> BackMaterial = nullptr; UPROPERTY(BlueprintReadWrite, EditAnywhere) TSubclassOf<AActor> Text3DActorClass = nullptr; UPROPERTY(BlueprintReadWrite, EditAnywhere) TObjectPtr<AActor> OwnerActor = nullptr; UPROPERTY(BlueprintReadWrite, EditAnywhere) TObjectPtr<APawn> InstigatorPawn = nullptr; static void ApplyToComponent (UText3DComponent* Text3DComponent, const FTcg3dTextParams& InParams) { if (!Text3DComponent) { return ; } Text3DComponent->SetVisibility(InParams.bVisible); Text3DComponent->SetText(InParams.InitialText); Text3DComponent->SetWorldScale3D(FVector(InParams.TextSize, InParams.TextSize, InParams.TextSize)); Text3DComponent->SetHasOutline(InParams.bOutLine); Text3DComponent->SetWorldLocation(InParams.SpawnLocation); Text3DComponent->SetWorldRotation(InParams.SpawnRotation); if (InParams.FrontMaterial || InParams.BevelMaterial || InParams.ExtrudeMaterial || InParams.BackMaterial) { if (UText3DDefaultMaterialExtension* DefaultMaterialExt = Cast<UText3DDefaultMaterialExtension>(Text3DComponent->GetMaterialExtension())) { DefaultMaterialExt->SetStyle(EText3DMaterialStyle::Custom); } } if (InParams.FrontMaterial) { Text3DComponent->SetFrontMaterial(InParams.FrontMaterial); } if (InParams.BevelMaterial) { Text3DComponent->SetBevelMaterial(InParams.BevelMaterial); } if (InParams.ExtrudeMaterial) { Text3DComponent->SetExtrudeMaterial(InParams.ExtrudeMaterial); } if (InParams.BackMaterial) { Text3DComponent->SetBackMaterial(InParams.BackMaterial); } } FTcg3dTextParams() {}; FTcg3dTextParams( AActor* RequestingActor, const bool & bIsModify, const bool & bInVisible, const FVector& InSpawnLocation, const FRotator& InSpawnRotation, const FText& InInitialText, float InTextSize, bool bInOutLine, AActor* InOwnerActor, APawn* InInstigatorPawn, UMaterialInterface* InFrontMaterial, UMaterialInterface* InBevelMaterial, UMaterialInterface* InExtrudeMaterial, UMaterialInterface* InBackMaterial, AActor* TargetActor = nullptr) { this->Header.RequestingActor = RequestingActor; this->bIsModify = bIsModify; this->bVisible = bInVisible; this->SpawnLocation = InSpawnLocation; this->SpawnRotation = InSpawnRotation; this->InitialText = InInitialText; this->TextSize = InTextSize; this->bOutLine = bInOutLine; this->FrontMaterial = InFrontMaterial; this->BevelMaterial = InBevelMaterial; this->ExtrudeMaterial = InExtrudeMaterial; this->BackMaterial = InBackMaterial; this->OwnerActor = InOwnerActor; this->InstigatorPawn = InInstigatorPawn; this->TargetActor = TargetActor; } };
TCG3DTextModifierLibrary.cpp 文件路径 : 3DText\TCG3DTextModifierLibrary.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 #include "TCG3DTextModifierLibrary.h" #include "AbilitySystemComponent.h" #include "TCGAbility_Create3DText.h" #include "TCGReplicatedText3DActor.h" #include "GameplayAbilitySystem/SubGameAbility/RequestBridge/TCGRequestCallbackSubsystem.h" TMap<FGuid, TWeakObjectPtr<AActor>> GPendingModifyTargets; bool UTCG3DTextModifierLibrary::ValidateASCAndPC (UAbilitySystemComponent* ASC, APlayerController* OwningPC, const TCHAR* Context) { if (!ASC) { UE_LOG (LogTemp, Warning, TEXT ("%s: ASC is null!" ), Context); return false ; } if (!OwningPC) { UE_LOG (LogTemp, Warning, TEXT ("%s: OwningPC is null!" ), Context); return false ; } return true ; } template <typename TAbility>bool UTCG3DTextModifierLibrary::FindAbilitySpec (UAbilitySystemComponent* ASC, FGameplayAbilitySpec*& OutSpec, TAbility*& OutInstance) { OutSpec = nullptr ; OutInstance = nullptr ; for (FGameplayAbilitySpec& AbilitySpec : ASC->GetActivatableAbilities ()) { if (AbilitySpec.Ability && AbilitySpec.Ability->IsA (TAbility::StaticClass ())) { OutSpec = &AbilitySpec; break ; } } if (!OutSpec) { return false ; } OutInstance = Cast <TAbility>(OutSpec->GetPrimaryInstance ()); if (!OutInstance) { OutInstance = Cast <TAbility>(OutSpec->Ability); } return OutInstance != nullptr ; } bool UTCG3DTextModifierLibrary::RegisterRequestCallback (UAbilitySystemComponent* ASC, const FGuid& RequestId, UObject* CallbackObject, FName CallbackFunctionName, const TCHAR* Context) { if (!CallbackObject || CallbackFunctionName.IsNone ()) { UE_LOG (LogTemp, Warning, TEXT ("%s: CallbackObject/CallbackFunctionName invalid. Request will still be sent but no callback will be executed." ), Context); return false ; } UFunction* CallbackFunc = CallbackObject->FindFunction (CallbackFunctionName); if (!CallbackFunc) { UE_LOG (LogTemp, Warning, TEXT ("%s: Callback function '%s' not found on object '%s'. Request will still be sent but no callback will be executed." ), Context, *CallbackFunctionName.ToString (), *GetNameSafe (CallbackObject)); return false ; } FProperty* FirstParam = nullptr ; FProperty* SecondParam = nullptr ; for (TFieldIterator<FProperty> It (CallbackFunc); It && (It->PropertyFlags & CPF_Parm); ++It) { FProperty* Prop = *It; if (Prop->HasAnyPropertyFlags (CPF_ReturnParm)) { continue ; } if (!FirstParam) { FirstParam = Prop; } else { SecondParam = Prop; break ; } } bool bSignatureOk = false ; if (FirstParam && !SecondParam) { if (const FObjectPropertyBase* ObjProp = CastField <FObjectPropertyBase>(FirstParam)) { UClass* ParamClass = ObjProp->PropertyClass; bSignatureOk = ParamClass && ParamClass->IsChildOf (AActor::StaticClass ()); } } if (!bSignatureOk) { UE_LOG (LogTemp, Warning, TEXT ("%s: Callback function '%s' on '%s' has invalid signature. Expected exactly one param: (AActor*). Request will still be sent but no callback will be executed." ), Context, *CallbackFunctionName.ToString (), *GetNameSafe (CallbackObject)); return false ; } if (UWorld* World = ASC->GetWorld ()) { if (UGameInstance* GI = World->GetGameInstance ()) { if (UTCGRequestCallbackSubsystem* RequestCallbackSubsystem = GI->GetSubsystem <UTCGRequestCallbackSubsystem>()) { UTCGRequestCallbackSubsystem::FTCGOnRequestCompleted Wrapper; Wrapper.BindUFunction (CallbackObject, CallbackFunctionName); RequestCallbackSubsystem->RegisterPendingCallback (RequestId, Wrapper); return true ; } } } return false ; } void UTCG3DTextModifierLibrary::CancelRequestCallback (UAbilitySystemComponent* ASC, const FGuid& RequestId) { if (UWorld* World = ASC ? ASC->GetWorld () : nullptr ) { if (UGameInstance* GI = World->GetGameInstance ()) { if (UTCGRequestCallbackSubsystem* RequestCallbackSubsystem = GI->GetSubsystem <UTCGRequestCallbackSubsystem>()) { RequestCallbackSubsystem->Cancel (RequestId); } } } } bool UTCG3DTextModifierLibrary::Create3DTextAsyncWithPC ( UAbilitySystemComponent* ASC, APlayerController* OwningPC, const FVector& SpawnLocation, const FRotator& SpawnRotation, const FText& InitialText, const float & TextSize, const bool & bOutLine, const bool & bVisible, UMaterialInterface* FrontMaterial, UMaterialInterface* BevelMaterial, UMaterialInterface* ExtrudeMaterial, UMaterialInterface* BackMaterial, UObject* CallbackObject, FName CallbackFunctionName) { static const TCHAR* Context = TEXT ("RequestCreate3DTextAsyncWithPC" ); if (!ValidateASCAndPC (ASC, OwningPC, Context)) { return false ; } FGameplayAbilitySpec* Spec = nullptr ; UTCGAbility_Create3DText* AbilityInstance = nullptr ; if (!FindAbilitySpec <UTCGAbility_Create3DText>(ASC, Spec, AbilityInstance)) { UE_LOG (LogTemp, Warning, TEXT ("%s: Player does not have Create3DText Ability! Please grant it first." ), Context); return false ; } AbilityInstance->LastCreatedActor = nullptr ; const FTcg3dTextParams Params = FTcg3dTextParams ( OwningPC, false , bVisible, SpawnLocation, SpawnRotation, InitialText, TextSize, bOutLine, OwningPC, OwningPC->GetPawn (), FrontMaterial, BevelMaterial, ExtrudeMaterial, BackMaterial, nullptr ); AbilityInstance->CurrentParams = Params; RegisterRequestCallback (ASC, Params.Header.RequestId, CallbackObject, CallbackFunctionName, Context); const bool bSuccess = ASC->TryActivateAbility (Spec->Handle); if (!bSuccess) { UE_LOG (LogTemp, Warning, TEXT ("%s: Failed to activate ability!" ), Context); CancelRequestCallback (ASC, Params.Header.RequestId); } return bSuccess; } bool UTCG3DTextModifierLibrary::Modify3DTextAsyncWithPC ( UAbilitySystemComponent* ASC, APlayerController* OwningPC, AActor* TargetText3DActor, const FVector& SpawnLocation, const FRotator& SpawnRotation, const FText& NewText, const float & NewTextSize, const bool & bNewOutLine, const bool & bVisible, UMaterialInterface* FrontMaterial, UMaterialInterface* BevelMaterial, UMaterialInterface* ExtrudeMaterial, UMaterialInterface* BackMaterial, UObject* CallbackObject, FName CallbackFunctionName) { static const TCHAR* Context = TEXT ("Modify3DTextAsyncWithPC" ); if (!ValidateASCAndPC (ASC, OwningPC, Context)) { return false ; } if (!TargetText3DActor) { UE_LOG (LogTemp, Warning, TEXT ("%s: TargetText3DActor is null!" ), Context); return false ; } FGameplayAbilitySpec* Spec = nullptr ; UTCGAbility_Create3DText* AbilityInstance = nullptr ; if (!FindAbilitySpec <UTCGAbility_Create3DText>(ASC, Spec, AbilityInstance)) { UE_LOG (LogTemp, Warning, TEXT ("%s: Player does not have 3DText Ability! Please grant it first." ), Context); return false ; } const FTcg3dTextParams Params = FTcg3dTextParams ( OwningPC, true , bVisible, SpawnLocation, SpawnRotation, NewText, NewTextSize, bNewOutLine, OwningPC, OwningPC->GetPawn (), FrontMaterial, BevelMaterial, ExtrudeMaterial, BackMaterial, TargetText3DActor); AbilityInstance->CurrentParams = Params; RegisterRequestCallback (ASC, Params.Header.RequestId, CallbackObject, CallbackFunctionName, Context); const bool bSuccess = ASC->TryActivateAbility (Spec->Handle); if (!bSuccess) { UE_LOG (LogTemp, Warning, TEXT ("%s: Failed to activate ability!" ), Context); CancelRequestCallback (ASC, Params.Header.RequestId); } return bSuccess; }
TCG3DTextModifierLibrary.h 文件路径 : 3DText\TCG3DTextModifierLibrary.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "TCGAbility_Create3DText.h" #include "TCG3DTextModifierLibrary.generated.h" class UMaterialInterface ;DECLARE_DYNAMIC_DELEGATE_OneParam(FOnText3DCreatedCallback, AActor*, CreatedActor); UCLASS() class TCG_AWESOMELIVE_API UTCG3DTextModifierLibrary : public UBlueprintFunctionLibrary{ GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "TCG|3DText" , meta = (AutoCreateRefTerm = "SpawnLocation,SpawnRotation,InitialText" )) static bool Create3DTextAsyncWithPC ( UAbilitySystemComponent* ASC, APlayerController* OwningPC, const FVector& SpawnLocation, const FRotator& SpawnRotation, const FText& InitialText, const float & TextSize, const bool & bOutLine, const bool & bVisible, UMaterialInterface* FrontMaterial, UMaterialInterface* BevelMaterial, UMaterialInterface* ExtrudeMaterial, UMaterialInterface* BackMaterial, UObject* CallbackObject, FName CallbackFunctionName) ; UFUNCTION(BlueprintCallable, Category = "TCG|3DText" , meta = (AutoCreateRefTerm = "SpawnLocation,SpawnRotation,NewText" )) static bool Modify3DTextAsyncWithPC ( UAbilitySystemComponent* ASC, APlayerController* OwningPC, AActor* TargetText3DActor, const FVector& SpawnLocation, const FRotator& SpawnRotation, const FText& NewText, const float & NewTextSize, const bool & bNewOutLine, const bool & bVisible, UMaterialInterface* FrontMaterial, UMaterialInterface* BevelMaterial, UMaterialInterface* ExtrudeMaterial, UMaterialInterface* BackMaterial, UObject* CallbackObject, FName CallbackFunctionName) ;private: static bool ValidateASCAndPC (UAbilitySystemComponent* ASC, APlayerController* OwningPC, const TCHAR* Context) ; template<typename TAbility> static bool FindAbilitySpec (UAbilitySystemComponent* ASC, FGameplayAbilitySpec*& OutSpec, TAbility*& OutInstance) ; static bool RegisterRequestCallback (UAbilitySystemComponent* ASC, const FGuid& RequestId, UObject* CallbackObject, FName CallbackFunctionName, const TCHAR* Context) ; static void CancelRequestCallback (UAbilitySystemComponent* ASC, const FGuid& RequestId) ; };
TCGAbility_Create3DText.cpp 文件路径 : 3DText\TCGAbility_Create3DText.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 #include "TCGAbility_Create3DText.h" #include "AbilitySystemComponent.h" #include "Engine/World.h" #include "TimerManager.h" #include "TCGReplicatedText3DActor.h" #pragma optimize("" , off) void UTCGAbility_Create3DText::ActivateAbility ( const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { Super::ActivateAbility (Handle, ActorInfo, ActivationInfo, TriggerEventData); if (!CommitAbility (Handle, ActorInfo, ActivationInfo)) { EndAbility (Handle, ActorInfo, ActivationInfo, true , true ); return ; } if (!GetOwningActorFromActorInfo ()->HasAuthority ()) { UE_LOG (LogTemp, Warning, TEXT ("[Create3DText][ActivateAbility] Calling ServerCreateText3DActor (Client side). RequestId=%s Params: Text='%s' Loc=%s OwnerActor=%s InstigatorPawn=%s" ), *CurrentParams.Header.RequestId.ToString (), *CurrentParams.InitialText.ToString (), *CurrentParams.SpawnLocation.ToString (), *GetNameSafe (CurrentParams.OwnerActor.Get ()), *GetNameSafe (CurrentParams.InstigatorPawn.Get ())); ServerText3DActor (CurrentParams); } else { UE_LOG (LogTemp, Warning, TEXT ("[Create3DText][ActivateAbility] Spawning directly on server (Authority side)." )); if (AActor* CreatedActor = SpawnText3DActor (CurrentParams)) { LastCreatedActor = CreatedActor; OnText3DActorCreated.Broadcast (CreatedActor); UE_LOG (LogTemp, Log, TEXT ("UTCGAbility_Create3DText: Successfully created Text3D Actor on server: %s" ), *CreatedActor->GetName ()); } } EndAbility (Handle, ActorInfo, ActivationInfo, true , false ); } void UTCGAbility_Create3DText::ServerText3DActor_Implementation (const FTcg3dTextParams& Params) { if (Params.bIsModify) { ATCGReplicatedText3DActor* RepTextActor = Cast <ATCGReplicatedText3DActor>(Params.TargetActor); if (RepTextActor && RepTextActor->HasAuthority ()) { RepTextActor->UpdateFromParams_Server (Params); } return ; } UE_LOG (LogTemp, Warning, TEXT ("[Create3DText][ServerRPC] Enter ServerCreateText3DActor_Implementation. RequestId=%s This=%s OwningActor=%s Avatar=%s PC=%s Params: Text='%s' Loc=%s OwnerActor=%s InstigatorPawn=%s" ), *Params.Header.RequestId.ToString (), *GetNameSafe (this ), *GetNameSafe (GetOwningActorFromActorInfo ()), *GetNameSafe (GetAvatarActorFromActorInfo ()), *GetNameSafe (CurrentActorInfo ? CurrentActorInfo->PlayerController.Get () : nullptr ), *Params.InitialText.ToString (), *Params.SpawnLocation.ToString (), *GetNameSafe (Params.OwnerActor.Get ()), *GetNameSafe (Params.InstigatorPawn.Get ())); AActor* CreatedActor = SpawnText3DActor (Params); if (CreatedActor) { LastCreatedActor = CreatedActor; OnText3DActorCreated.Broadcast (CreatedActor); UE_LOG (LogTemp, Log, TEXT ("UTCGAbility_Create3DText: Successfully created Text3D Actor via RPC: %s" ), *CreatedActor->GetName ()); } } AActor* UTCGAbility_Create3DText::SpawnText3DActor (const FTcg3dTextParams& Params) { UE_LOG (LogTemp, Warning, TEXT ("[Create3DText][SpawnOnServer] Enter. RequestId=%s Avatar=%s (Auth=%d, Role=%d) Owning=%s (Auth=%d, Role=%d) Params.OwnerActor=%s Params.InstigatorPawn=%s" ), *Params.Header.RequestId.ToString (), *GetNameSafe (GetAvatarActorFromActorInfo ()), GetAvatarActorFromActorInfo () ? (GetAvatarActorFromActorInfo ()->HasAuthority () ? 1 : 0 ) : -1 , GetAvatarActorFromActorInfo () ? (int32)GetAvatarActorFromActorInfo ()->GetLocalRole () : -1 , *GetNameSafe (GetOwningActorFromActorInfo ()), GetOwningActorFromActorInfo () ? (GetOwningActorFromActorInfo ()->HasAuthority () ? 1 : 0 ) : -1 , GetOwningActorFromActorInfo () ? (int32)GetOwningActorFromActorInfo ()->GetLocalRole () : -1 , *GetNameSafe (Params.OwnerActor.Get ()), *GetNameSafe (Params.InstigatorPawn.Get ())); AActor* AvatarActor = GetAvatarActorFromActorInfo (); if (!AvatarActor) { UE_LOG (LogTemp, Warning, TEXT ("UTCGAbility_Create3DText: Avatar Actor is null!" )); return nullptr ; } UWorld* World = GetWorld (); if (!World) { UE_LOG (LogTemp, Warning, TEXT ("UTCGAbility_Create3DText: World is null!" )); return nullptr ; } if (!AvatarActor->HasAuthority ()) { UE_LOG (LogTemp, Warning, TEXT ("UTCGAbility_Create3DText: Not on server!" )); return nullptr ; } FActorSpawnParameters SpawnParams; SpawnParams.Owner = AvatarActor; SpawnParams.Instigator = Cast <APawn>(AvatarActor); SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; if (Params.OwnerActor) { SpawnParams.Owner = Params.OwnerActor; } if (Params.InstigatorPawn) { SpawnParams.Instigator = Params.InstigatorPawn; } TSubclassOf<AActor> ActorClassToSpawn = Params.Text3DActorClass; if (!ActorClassToSpawn) { ActorClassToSpawn = ATCGReplicatedText3DActor::StaticClass (); } AActor* SpawnedActor = World->SpawnActor <AActor>( ActorClassToSpawn, Params.SpawnLocation, Params.SpawnRotation, SpawnParams ); SpawnedActor->SetReplicates (true ); SpawnedActor->SetReplicateMovement (true ); if (SpawnParams.Owner) { SpawnedActor->SetOwner (SpawnParams.Owner); SpawnedActor->bNetUseOwnerRelevancy = true ; } if (ATCGReplicatedText3DActor* RepTextActor = Cast <ATCGReplicatedText3DActor>(SpawnedActor)) { RepTextActor->InitializeFromParams_Server (Params); } return SpawnedActor; } #pragma optimize("" , on)
TCGAbility_Create3DText.h 文件路径 : 3DText\TCGAbility_Create3DText.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #pragma once #include "CoreMinimal.h" #include "GameplayAbilitySystem/TCGGameplayAbility.h" #include "Struct/FTcg3dTextParams.h" #include "TCGAbility_Create3DText.generated.h" class UText3DComponent ;UCLASS() class TCG_AWESOMELIVE_API UTCGAbility_Create3DText : public UTCGGameplayAbility{ GENERATED_BODY() protected: virtual void ActivateAbility ( const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override; UFUNCTION(Server, Reliable) void ServerText3DActor (const FTcg3dTextParams& Params) ; UFUNCTION(BlueprintCallable, Category = "TCG|3DText" ) AActor* SpawnText3DActor (const FTcg3dTextParams& Params) ; public: UPROPERTY() FTcg3dTextParams CurrentParams; UPROPERTY() AActor* LastCreatedActor; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnText3DActorCreated, AActor*, CreatedActor); UPROPERTY(BlueprintAssignable, Category = "TCG|3DText" ) FOnText3DActorCreated OnText3DActorCreated; };
TCGReplicatedText3DActor.cpp 文件路径 : 3DText\TCGReplicatedText3DActor.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include "TCGReplicatedText3DActor.h" #include "Net/UnrealNetwork.h" #include "Text3DComponent.h" #include "Engine/GameInstance.h" #include "GameplayAbilitySystem/SubGameAbility/RequestBridge/TCGRequestCallbackSubsystem.h" #pragma optimize("" , off) ATCGReplicatedText3DActor::ATCGReplicatedText3DActor () { bReplicates = true ; SetReplicateMovement (true ); bNetUseOwnerRelevancy = true ; } void ATCGReplicatedText3DActor::InitializeFromParams_Server (const FTcg3dTextParams& InParams) { if (!HasAuthority ()) { return ; } ReplicatedParams = InParams; ApplyParamsToComponents (ReplicatedParams); ForceNetUpdate (); } void ATCGReplicatedText3DActor::OnRep_TextParams () { ApplyParamsToComponents (ReplicatedParams); OnText3DInitialized.Broadcast (this ); if (UWorld* World = GetWorld ()) { if (UGameInstance* GI = World->GetGameInstance ()) { if (ReplicatedParams.Header.RequestId.IsValid ()) { if (UTCGRequestCallbackSubsystem* RequestCallbackSubsystem = GI->GetSubsystem <UTCGRequestCallbackSubsystem>()) { RequestCallbackSubsystem->NotifyClientCompleted (ReplicatedParams.Header.RequestId, this ); } } } } } void ATCGReplicatedText3DActor::UpdateFromParams_Server (const FTcg3dTextParams& InParams) { InitializeFromParams_Server (InParams); } void ATCGReplicatedText3DActor::ApplyParamsToComponents (const FTcg3dTextParams& InParams) { if (UText3DComponent* LocalText3DComponent = GetText3DComponent ()) { FTcg3dTextParams::ApplyToComponent (LocalText3DComponent, InParams); } } void ATCGReplicatedText3DActor::GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps (OutLifetimeProps); DOREPLIFETIME (ATCGReplicatedText3DActor, ReplicatedParams); } #pragma optimize("" , on)
TCGReplicatedText3DActor.h 文件路径 : 3DText\TCGReplicatedText3DActor.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #pragma once #include "CoreMinimal.h" #include "Text3DActor.h" #include "TCGAbility_Create3DText.h" #include "TCGReplicatedText3DActor.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTCGOnText3DInitialized, AActor*, Text3DActor); UCLASS() class TCG_AWESOMELIVE_API ATCGReplicatedText3DActor : public AText3DActor{ GENERATED_BODY() public: ATCGReplicatedText3DActor(); UPROPERTY(BlueprintAssignable, Category = "TCG|3DText" ) FTCGOnText3DInitialized OnText3DInitialized; UFUNCTION(BlueprintCallable, Category = "TCG|3DText" ) void InitializeFromParams_Server (const FTcg3dTextParams& InParams) ; UFUNCTION(BlueprintCallable, Category = "TCG|3DText" ) void UpdateFromParams_Server (const FTcg3dTextParams& InParams) ; const FTcg3dTextParams& GetReplicatedParams () const { return ReplicatedParams; } protected: UPROPERTY(ReplicatedUsing = OnRep_TextParams) FTcg3dTextParams ReplicatedParams; UFUNCTION() void OnRep_TextParams () ; void ApplyParamsToComponents (const FTcg3dTextParams& InParams) ; virtual void GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) const override; };
TCGRequestBridgeTypes.h 文件路径 : RequestBridge\TCGRequestBridgeTypes.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #pragma once #include "CoreMinimal.h" #include "GameplayTagContainer.h" #include "TCGRequestBridgeTypes.generated.h" USTRUCT(BlueprintType) struct TCG_AWESOMELIVE_API FTCGRequestHeaderBase { GENERATED_BODY() UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="TCG|Request" ) FGuid RequestId = FGuid::NewGuid(); UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="TCG|Request" ) TObjectPtr<AActor> RequestingActor = nullptr; };
TCGRequestCallbackSubsystem.cpp 文件路径 : RequestBridge\TCGRequestCallbackSubsystem.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include "TCGRequestCallbackSubsystem.h" void UTCGRequestCallbackSubsystem::RegisterPendingCallback (const FGuid& RequestId, const FTCGOnRequestCompleted& Callback) { if (!RequestId.IsValid () || !Callback.IsBound ()) { return ; } PendingCallbacks.Add (RequestId, Callback); } void UTCGRequestCallbackSubsystem::NotifyClientCompleted (const FGuid& RequestId, AActor* ResultActor) { if (!RequestId.IsValid ()) { return ; } FTCGOnRequestCompleted* Callback = PendingCallbacks.Find (RequestId); if (!Callback) { return ; } FTCGOnRequestCompleted Copy = *Callback; PendingCallbacks.Remove (RequestId); if (Copy.IsBound ()) { Copy.Execute (ResultActor); } } void UTCGRequestCallbackSubsystem::Cancel (const FGuid& RequestId) { if (!RequestId.IsValid ()) { return ; } PendingCallbacks.Remove (RequestId); }
TCGRequestCallbackSubsystem.h 文件路径 : RequestBridge\TCGRequestCallbackSubsystem.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #pragma once #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "TCGRequestBridgeTypes.h" #include "TCGRequestCallbackSubsystem.generated.h" UCLASS() class TCG_AWESOMELIVE_API UTCGRequestCallbackSubsystem : public UGameInstanceSubsystem{ GENERATED_BODY() public: DECLARE_DYNAMIC_DELEGATE_OneParam(FTCGOnRequestCompleted, AActor*, ResultActor); UFUNCTION(BlueprintCallable, Category="TCG|Request" ) void RegisterPendingCallback (const FGuid& RequestId, const FTCGOnRequestCompleted& Callback) ; UFUNCTION(BlueprintCallable, Category="TCG|Request" ) void NotifyClientCompleted (const FGuid& RequestId, AActor* ResultActor) ; UFUNCTION(BlueprintCallable, Category="TCG|Request" ) void Cancel (const FGuid& RequestId) ; private: UPROPERTY() TMap<FGuid, FTCGOnRequestCompleted> PendingCallbacks; };
踩坑记录 GAS网络生成的BP需要考虑所有者
如果是在Server端生成的BP,一定要指定“Owner”,它意味着是这个生成的东西在各网络设备之间指定的所有者。 当这个所有者内部需要执行一个网络的Event的时候,它需要通知一个客户端,如果没有Owner,这个Event会被拒绝执行。
随机数问题 注意网络端和服务端生成随机数会不一样。
在GE里拿到“事件触发者”和“事件被影响者”
如上图,我有一个需求,投掷物。 需要给GE传递“投掷物这个物体”和“被撞的人”。 其中,在我这里,“被撞的人”是要执行的ASC的所有者(即这里的Instigator),在应用GE的时候必须提供。
调用 BP_ApplyGameplayEffectToTarget 时,它内部会:
用「作为 Target 的 ASC 的 Owner / Avatar Actor」来构造 FGameplayEffectContextHandle;
在这个 EffectContext 里自动填好 Instigator、EffectCauser 等字段;
把这个 EffectContext 放进 FGameplayEffectSpec,最后应用到 Target;
(正如图上所示,它其实有两个Target节点,上面那个是调用这个事件的Target(蓝图中常见),下面那个是被命中的“被撞的人”,因为碰巧都叫Target所以在这里重复了)
这个比较好理解。但,实际上GC一个内部结构里,有三个储存用来表达“事件责任相关人”的变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 USTRUCT (BlueprintType, meta = (HasNativeBreak = "/Script/GameplayAbilities.AbilitySystemBlueprintLibrary.BreakGameplayCueParameters" , HasNativeMake = "/Script/GameplayAbilities.AbilitySystemBlueprintLibrary.MakeGameplayCueParameters" ))struct FGameplayCueParameters { GENERATED_USTRUCT_BODY () UPROPERTY (BlueprintReadWrite, Category=GameplayCue) TWeakObjectPtr<AActor> Instigator; UPROPERTY (BlueprintReadWrite, Category=GameplayCue) TWeakObjectPtr<AActor> EffectCauser; UPROPERTY (BlueprintReadWrite, Category=GameplayCue) TWeakObjectPtr<const UObject> SourceObject; }
在文件UE_5.6\Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Public\GameplayEffectTypes.h中
这三个分别是:Instigator、EffectCauser、SourceObject。
按照说明,他们“责任”分别的作用是
Instigator:谁是「施加这个效果的控制者」,通常是 Pawn/Character。
EffectCauser:是哪一个 Actor/Component「具体造成了这次效果」,比如武器、投掷物。
SourceObject:一个完全开放给你用的 UObject*,可以是武器实例、技能实例、DataAsset、投掷物等。
这三个字段的值,不一定都有,是否自动填,取决于你走的 GAS 调用路径。通常应用的路径上,Instigator和EffectCauser都是同一个值,都是这个ASC的拥有者。但是SourceObject通常是空的(通过GA主动和触发的GE有机会把GA的SourceObejct传入进去),所以可以自己指定,用作自己的参数。
参考资料
官方教学视频 :我看着这个视频作为最初的入门
知乎GAS系统快速入门
Unreal官方的GAS讲解
关于GA的讲解 :别忘了可以点开B站的AI翻译
一个很不错的关于 GAS系统实操讲解
一个很不错的Lyra示例的讲解
文字讲解Lyra结构 适合作为研究基础,然后配合上面的视频看。