UE5 GAS 学习笔记

Unreal GAS系统及网络使用学习

前置知识

UE网络架构

简介

Gameplay Ability System,简称(GAS)是一个健壮的、高度可扩展的、gameplay框架,通常用于构建RPG、MOBA等游戏的完整战斗逻辑框架。通过GAS,可以快速地制作游戏中的主动/被动技能、各种效果buff、计算属性伤害、处理玩家各种战斗状态逻辑。

GAS试图将机制提取为通用的游戏设计模式,并提供框架来解决常见的Gameplay实现问题,同时使上下文因项目而异。

GAS提供了哪些功能?

  1. 实现了带有消耗和冷却功能的角色技能
  2. 处理数值属性(生命、魔法、攻击力、防御力)
  3. 应用状态效果(击飞、着火、眩晕)
  4. 应用游戏标签(GameplayTags)
  5. 生成特效和音效
  6. 完整的网络复制、预测功能

适合使用GAS的项目

  1. C++项目,开发人员有充足的C++开发经验

  2. 使用Dedicated Server专用服务器的联机游戏(单纯的游戏逻辑服务端,与Dedicated Server相对的是Listen Server,其中一位玩家充当服务器,容易出现“主机优势”)

  3. 项目有大量且复杂的技能逻辑设计需求

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]
  1. TryActivateAbility :首先判断一下能不能执行。
  2. ActivateAbility:执行一套技能(有技能动画,比如发射一个火焰)。
  3. CommitAbility:技能释放了成功了,要扣蓝。
  4. Start AbilityTask * 2:技能执行中…技能执行结束。然后应用对应效果。

其中ActivateAbility和End Ability是类似于BeginPlay和EndPlay这样的函数,在蓝图中调用。

蓝图中的设置

主要选项解释

常见蓝图
  • EndAbility 结束这个能力
高级设置
image-20251028173049722
  • 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要做的事情

image-20251028200009953

一般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 两种调用方法。

img

byClass一次只能Activate一个GA,byTag可以Activate任意多个GA,配合Tag容器使用。

如果使用EnhancedInputAction插件来管理输入,要注意在某些设置下trigger会每帧都进行输出。

只要能获取ASC,就可以在任何地方调用GA,比如行为树Task蓝图,甚至在GA蓝图中调用其他GA。

被动调用

Trigger可以理解为一个Tag,当ASC组件收到一个Trigger时,就会自动调用所有拥有该Trigger的GA。

Trigger的Tag在GA的details面板中设置。

img

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开头)

img

受击效果的例子,发送一个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

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)。

它本质是:

  1. 把“函数名 + 参数”打包成一段网络消息(通过 actor channel)
  2. 发送到网络对端
  3. 对端收到后,再去调用对应的 _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 发到服务器。

基础使用

开启插件

image-20251027203033649 image-20251027203540226
  1. C++项目,安装Gameplay Ability插件。
  2. 在Tools菜单下面有GameplayCue Editor有了,就说明开启成功了。
  3. 在新建的蓝图的页面可以看到GamePlay相关的东西

使用Rider快速添加Unreal类

image-20251027215401030

通过这里创建一个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
//实例化ASC
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystem"));

角色类继承IAbilitySystemInterface接口,并实现GetASC函数

1
2
3
4
5
#include "AbilitySystemInterface.h"
class ARPG_UNREAL_API ACharacterBase : public ACharacter, public IAbilitySystemInterface

public:
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的选项了。

image-20251028184812916
赋予GA的方式有很多,比如在需要的蓝图中调用Give Ability节点。还可以使用GE赋予GA。

在官方的Lyra案例里面使用AbilitySet来储存一类Ability,通过加载来实现赋予。参看下文Unreal官方案例“赋予GA”部分。

Unreal官方案例

UE中有一个非常不错的官方示例Lyra里面展示了一个完整的GAS系统。

项目里的设置

image-20251111183652281

这个项目里开发者设置了一些调整选项,可以设定一些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:

/** The actor that owns this component logically */
UPROPERTY(ReplicatedUsing = OnRep_OwningActor)
TObjectPtr<AActor> OwnerActor;

/** The actor that is the physical representation used for abilities. Can be NULL */
UPROPERTY(ReplicatedUsing = OnRep_OwningActor)
TObjectPtr<AActor> AvatarActor;

public:

/** Cached off data about the owning actor that abilities will need to frequently access (movement component, mesh component, anim instance, etc) */
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?

  • 定义角色: Experience(通过 ULyraExperienceDefinition)把一组运行时行为、资源和规则打包成一个可切换的体验(例如不同模式、地图或玩法配置)。

  • 主要用途:在游戏启动或切换体验时告诉引擎“要启用哪些 GameFeatures、默认的 Pawn 数据、以及需要执行哪些动作/赋能”

  • 原因

    • 统一配置与可切换性:把调表(技能、效果、默认 pawn 等)放到 Experience 里,便于在不同玩法/模式间切换和复用,不用把能力写死在代码里。
    • 数据驱动与延迟加载:Experience 可以在运行时启/停 GameFeatures、注入资源,再把需要的能力/效果批量下发。
    • 作用域与生命周期可控:通过 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的结构。

image-20251111172739503

继承自ULyraExperienceDefinition的:B_BasicShooterTestB_BasicShooterTest这个数据类里的Action分组里面,有需要授予的GA的DA表。

比如:**AbilitySet_Elimination(下面那个) 和 HeroData_ShooterGame里面的AbilitySet_**ShooterHero。

  • 首先看AbilitySet_Elimination DA表(它继承自LyraAbilitySet)。
image-20251111171556976

打开这个AbilitySet_Elimination,里面包含两个GA。
GA_ShowLeaderboard_TDM用于按Tab显示计分板。
GA_AutoRespawn用于死亡复活,这两个GA与Character战斗无关,在没用Character时也能执行。

  • 然后看AbilitySet_ShooterPistol DA表(储存在总表的HeroData_ShooterGame里面 它继承自ULyraPawnData)
image-20251111174500121
AbilitySet_ShooterHero

包含Character相关的GA,数量较多。比如GA_Hero_Jump实现了跳跃,GA_Emote实现了跳舞。

  • 然后还有一个AbilitySet_ShooterPisto
image-20251111181356512

它不是定义在B_BasicShooterTestB_BasicShooterTest这张大表里面的。

而是通过WID_Pistol被具体的蓝图调用。

然后对每个GA,调用UAbilitySystemComponent::GiveAbility函数进行赋予。

image-20251111185235151

会在很多地方调用。

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)
{
// Create the instance at creation time
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中指定

image-20251111191635777AbilitySet_ShooterPisto

image-20251111191708052

AbilitySet_ShooterPistol

image-20251111191718509

AbilitySet_ShooterPisto

创建Spec时会把InputTag添加到其DynamicSpecSourceTags,DynamicSpecSourceTags是一个通用Tag容器。
该Tags在结构体FGameplayAbilitySpec里面,也就是说,一个FGameplayAbilitySpec代表的是传递的一个Ability的能力,然后里面有很多DynamicAbilityTags

注册的路径如下:

image-20251111205815528
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
{
...

// Grant the attribute sets.
for (int32 SetIndex = 0; SetIndex < GrantedAttributes.Num(); ++SetIndex)
{
...
}

// Grant the gameplay abilities.
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);
}
}

// Grant the gameplay effects.
for (int32 EffectIndex = 0; EffectIndex < GrantedGameplayEffects.Num(); ++EffectIndex)
{
...
}
}

在授予ASC Ability的时候,将Tags Effect等信息一并传入。

授予GA的逻辑,以上就是服务器初始化ASC并赋予GA的全部过程。

image-20251111210326498

同步客户端

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
//同步GA
void UAbilitySystemComponent::AddReplicatedInstancedAbility(UGameplayAbility* GameplayAbility)
{
//...
const ELifetimeCondition LifetimeCondition = bReplicateAbilitiesToSimulatedProxies ? COND_None : COND_ReplayOrOwner;
AddReplicatedSubObject(GameplayAbility, LifetimeCondition);
}
//SubObject同步
void UActorComponent::AddReplicatedSubObject(UObject* SubObject, ELifetimeCondition NetCondition)
{
if (AActor* MyOwner=GetOwner())
{
MyOwner->AddActorComponentReplicatedSubObject(this, SubObject, NetCondition);
}
}

同步到客户端后表现如下:

img

常用调整命令行

ShowDebug AbilitySystem 展示Ability相关的内容

自己实现方案

通过自定义GA实现复杂参数GAS传递

模块与分层(结构图)

它可以直接从UI层调用。且一个GA可以做多个事情。有回调回UI。

主要关键字:GAS的逻辑 通过创建子GA蓝图 通过蓝图被experience管理 然后被GAS触发 通过RPC传递复杂的设置参数 ServerOnly创建Actor 然后通过Replicated赋值属性回客户端)

image-20260121160114457

通用层(可复用)

    • 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)
  • bIsModifyfalse=Createtrue=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"
/**
* 创建3DText的Gameplay Ability
* 在Ability激活时动态创建Text3D Actor,并自动网络同步
*/

// 创建/修改 3DText 的参数(统一结构)
USTRUCT(BlueprintType)
struct FTcg3dTextParams
{
GENERATED_BODY()

/** 通用请求头:承载 RequestId / RequestType / RequestingActor */
UPROPERTY(BlueprintReadWrite, EditAnywhere)
FTCGRequestHeaderBase Header;

/** Modify 时目标 Actor;Create 时可为空 */
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TObjectPtr<AActor> TargetActor = nullptr;

/** 是否为修改请求(true=Modify, false=Create) 该结构体参数在GA的修改和创建的时候都会调用 所以以此来判断管道*/
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;

// 字体的4个材质参数
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;

// 要使用的3DText Actor类(如果为空则使用默认AActor+TextRenderComponent)
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TSubclassOf<AActor> Text3DActorClass = nullptr;

// 可选:指定该3DText的Owner(通常传入 PlayerController 或 Pawn)。
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TObjectPtr<AActor> OwnerActor = nullptr;

// 可选:指定Instigator(通常是 Pawn)。
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TObjectPtr<APawn> InstigatorPawn = nullptr;

/**
* 将当前参数应用到 `UText3DComponent`。
* 备注:这是纯工具函数,不依赖 Actor/World,方便在服务器/客户端复用。
*/
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);

// 若使用自定义材质,需将 Text3D 材质样式切到 Custom(UE5.6 Text3D 插件)
if (InParams.FrontMaterial || InParams.BevelMaterial || InParams.ExtrudeMaterial || InParams.BackMaterial)
{
if (UText3DDefaultMaterialExtension* DefaultMaterialExt = Cast<UText3DDefaultMaterialExtension>(Text3DComponent->GetMaterialExtension()))
{
DefaultMaterialExt->SetStyle(EText3DMaterialStyle::Custom);
}
}

// 材质应用:Text3D 插件提供具名 API(UE5.6)
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
// Fill out your copyright notice in the Description page of Project Settings.

#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
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "TCGAbility_Create3DText.h"
#include "TCG3DTextModifierLibrary.generated.h"

class UMaterialInterface;

// 委托:当3DText Actor创建完成时回调
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnText3DCreatedCallback, AActor*, CreatedActor);

/**
* 蓝图函数库 - 简化UI调用3DText创建Ability的接口
* 提供动态创建3DText Actor的功能 【不具有复用性】
*/
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
// Fill out your copyright notice in the Description page of Project Settings.

#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);

// GAS逻辑检测
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)
{
// 统一入口:Create/Modify 都走同一个 Server RPC
if (Params.bIsModify)
{
ATCGReplicatedText3DActor* RepTextActor = Cast<ATCGReplicatedText3DActor>(Params.TargetActor);
if (RepTextActor && RepTextActor->HasAuthority())
{
RepTextActor->UpdateFromParams_Server(Params);
}
return;
}

// Create
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;

// 如果调用侧显式提供了Owner/Instigator(通常来自 PlayerController),优先使用它
if (Params.OwnerActor)
{
SpawnParams.Owner = Params.OwnerActor;
}
if (Params.InstigatorPawn)
{
SpawnParams.Instigator = Params.InstigatorPawn;
}

// 如果没有指定类,使用带RepNotify同步的默认实现
TSubclassOf<AActor> ActorClassToSpawn = Params.Text3DActorClass;
if (!ActorClassToSpawn)
{
ActorClassToSpawn = ATCGReplicatedText3DActor::StaticClass();
}

// 如果指定了Actor类,使用指定的类
AActor* SpawnedActor = World->SpawnActor<AActor>(
ActorClassToSpawn,
Params.SpawnLocation,
Params.SpawnRotation,
SpawnParams
);

// 设置Replicate(关键!)
SpawnedActor->SetReplicates(true);
SpawnedActor->SetReplicateMovement(true);

// 如果有Owner,使用Owner相关性(更省带宽,也更符合“归属到某个玩家”的语义)
if (SpawnParams.Owner)
{
SpawnedActor->SetOwner(SpawnParams.Owner);
SpawnedActor->bNetUseOwnerRelevancy = true;
}

// 如果是我们自定义的可复制Text3D Actor,把参数写进RepNotify变量,确保客户端也应用一遍
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
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameplayAbilitySystem/TCGGameplayAbility.h"
#include "Struct/FTcg3dTextParams.h"
#include "TCGAbility_Create3DText.generated.h"

class UText3DComponent;

/**
* 创建并管理动态3DText Actor的Ability
*
* 工作流程:
* 1. 客户端UI调用 → 激活Ability
* 2. Ability在服务器上创建Actor
* 3. Actor自动Replicate到所有客户端
* 4. 返回创建的Actor引用给调用者
*/
UCLASS()
class TCG_AWESOMELIVE_API UTCGAbility_Create3DText : public UTCGGameplayAbility
{
GENERATED_BODY()

protected:
/* 调用链条:
* ActivateAbility -> ServerText3DActor -> SpawnText3DActorOnServer(实际创建)
*/
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;

// Server RPC - 在服务器上创建Actor
UFUNCTION(Server, Reliable)
void ServerText3DActor(const FTcg3dTextParams& Params);

// 在服务器上实际创建Actor的函数(只在服务器上调用)
UFUNCTION(BlueprintCallable, Category = "TCG|3DText")
AActor* SpawnText3DActor(const FTcg3dTextParams& Params);

public:
// 当前创建参数
UPROPERTY()
FTcg3dTextParams CurrentParams;

// 最近创建的Actor引用(用于返回给调用者)
UPROPERTY()
AActor* LastCreatedActor;

// 委托:当Actor创建完成时触发(可在蓝图中绑定)
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);

// 默认就允许用Owner相关性(如果Ability那边设置了Owner的话)
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())
{
// 新:通用请求回调(RequestId 精确匹配)
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);
// TODO: 颜色/材质:将 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" // for FTCG3DTextParams
#include "TCGReplicatedText3DActor.generated.h"

/**
* 一个可复制“显示参数”的Text3D Actor。
* 说明:UE默认只复制Actor/组件的Transform与Movement,
* Text3DComponent 的文本/材质等通常不会自动复制。
* 这里用 RepNotify 把服务器上的参数同步到客户端,再在客户端应用到组件。
*/

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTCGOnText3DInitialized, AActor*, Text3DActor);

UCLASS()
class TCG_AWESOMELIVE_API ATCGReplicatedText3DActor : public AText3DActor
{
GENERATED_BODY()

public:
ATCGReplicatedText3DActor();

// 当客户端通过OnRep收到参数并完成Apply后,会广播该事件
UPROPERTY(BlueprintAssignable, Category = "TCG|3DText")
FTCGOnText3DInitialized OnText3DInitialized;

/**
* 服务器调用:设置初始参数并立即应用一次,随后通过复制同步给客户端。
*/
UFUNCTION(BlueprintCallable, Category = "TCG|3DText")
void InitializeFromParams_Server(const FTcg3dTextParams& InParams);

/** 服务器:在已存在 Actor 上更新复制参数,并 ForceNetUpdate 与客户端回调 */
UFUNCTION(BlueprintCallable, Category = "TCG|3DText")
void UpdateFromParams_Server(const FTcg3dTextParams& InParams);

/** 读取当前复制参数(只读) */
const FTcg3dTextParams& GetReplicatedParams() const { return ReplicatedParams; }

protected:
UPROPERTY(ReplicatedUsing = OnRep_TextParams)
FTcg3dTextParams ReplicatedParams;

/** RepNotify 回调:当 ReplicatedParams 变化时调用 👆有宏在UE网络框架中实现调用*/
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"

/**
* 通用请求头(Base):用于把一次“请求”和未来的回调精确关联。
* 建议各业务 Params 内包含一个 `Header` 字段来统一承载 RequestId / RequestType / RequestingActor。
*/
USTRUCT(BlueprintType)
struct TCG_AWESOMELIVE_API FTCGRequestHeaderBase
{
GENERATED_BODY()

/** 唯一请求ID(客户端生成) */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="TCG|Request")
FGuid RequestId = FGuid::NewGuid();

/** 可选:发起者(通常是 PlayerController 或 Pawn) */
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需要考虑所有者

image-20251114160307855image-20251114160401410

如果是在Server端生成的BP,一定要指定“Owner”,它意味着是这个生成的东西在各网络设备之间指定的所有者。
当这个所有者内部需要执行一个网络的Event的时候,它需要通知一个客户端,如果没有Owner,这个Event会被拒绝执行。

随机数问题

注意网络端和服务端生成随机数会不一样。

在GE里拿到“事件触发者”和“事件被影响者”

image-20251120172105945

如上图,我有一个需求,投掷物。
需要给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()

/** Instigator actor, the actor that owns the ability system component */
UPROPERTY(BlueprintReadWrite, Category=GameplayCue)
TWeakObjectPtr<AActor> Instigator;

/** The physical actor that actually did the damage, can be a weapon or projectile */
UPROPERTY(BlueprintReadWrite, Category=GameplayCue)
TWeakObjectPtr<AActor> EffectCauser;

/** Object this effect was created from, can be an actor or static object. Useful to bind an effect to a gameplay object */
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传入进去),所以可以自己指定,用作自己的参数。

参考资料

  1. 官方教学视频:我看着这个视频作为最初的入门

  2. 知乎GAS系统快速入门

  3. Unreal官方的GAS讲解

  4. 关于GA的讲解:别忘了可以点开B站的AI翻译

  5. 一个很不错的关于GAS系统实操讲解

  6. 一个很不错的Lyra示例的讲解

  7. 文字讲解Lyra结构适合作为研究基础,然后配合上面的视频看。