[TOC]
导读
背包和存档两条线走完,接下来就要啃这个项目最硬的骨头——GAS(GameplayAbilities)。但在一头扎进属性、伤害、能力之前,先花一篇建立"全局视图":ARPGCharacterBase 是整个游戏的枢纽,背包、属性、能力、武器都在它身上汇合。看懂它的结构和生命周期,后面 GAS 几篇才不会迷路。
这一篇不深入 GAS 细节(那是后面几篇的事),只把骨架摸清楚:
ARPGCharacterBase继承了哪些东西?身上挂着哪些成员,各自归谁管?- AbilitySystemComponent 和 AttributeSet 是怎么来的、为什么在角色身上?
- 控制器接管角色(
PossessedBy)和离开(UnPossessed)时各做了什么? - "武器"在 C++ 里到底是什么——一个 Actor?一个数据资产?
- 满屏的
Ability.Melee.Close、Event.Montage.Shared.WeaponHit这些 GameplayTag 怎么组织的?
阅读前提:读过本系列背包、存档篇,理解
IRPGInventoryInterface、FRPGItemSlot、Primary Data Asset。源码范围:
RPGCharacterBase.h/.cpp、Items/RPGItem.h、Items/RPGWeaponItem.h、Config/DefaultGameplayTags.ini。引擎版本 4.27.2。
一、ARPGCharacterBase:三接口于一身的枢纽
先看类声明(RPGCharacterBase.h:19-20):
UCLASS()
class ACTIONRPG_API ARPGCharacterBase : public ACharacter,
public IAbilitySystemInterface, // ① 我有一套能力系统
public IGenericTeamAgentInterface // ② 我有阵营(AI 用来分敌我)
继承链是 ACharacter → ARPGCharacterBase → BP_Character(蓝图)。和项目里所有 RPG*Base 一样,C++ 基类不直接用,而是被蓝图继承后在配置里引用。三个身份各管一摊:
ACharacter:UE 自带的角色基类,带CharacterMovementComponent(移动)、骨骼网格、胶囊体。走路、跳跃这些不用自己写。IAbilitySystemInterface:GAS 的"入场券"。只要求实现一个函数GetAbilitySystemComponent(),GAS 全家桶就能找到这个角色的 ASC。IGenericTeamAgentInterface:阵营接口,给 AI 感知系统判断敌我用(GetGenericTeamId())。
职责地图:身上挂了什么
角色的成员变量可以按"归谁管"分成四组,记住这张地图,后面读代码就不会乱:
ARPGCharacterBase
├─ 【能力系统 GAS】
│ ├─ AbilitySystemComponent ASC,能力/效果/标签的总线
│ ├─ AttributeSet 属性集(血、蓝、攻、防、移速…)
│ ├─ CharacterLevel 角色等级(影响能力强度)
│ ├─ GameplayAbilities 出生自带能力
│ ├─ DefaultSlottedAbilities 各槽位的兜底能力
│ └─ PassiveGameplayEffects 出生自带的被动效果
├─ 【背包】
│ ├─ InventorySource TScriptInterface<IRPGInventoryInterface>(指向 Controller)
│ ├─ InventoryUpdateHandle ┐ 订阅背包委托的句柄
│ └─ InventoryLoadedHandle ┘
├─ 【能力↔物品的桥】
│ └─ SlottedAbilities 槽位 → 已授予能力句柄
└─ 【对蓝图的回调钩子】
└─ OnDamaged / OnHealthChanged / OnManaChanged / OnMoveSpeedChanged
可以看出:角色是"背包"和"GAS"两大系统的交汇点。背包通过 InventorySource 接口接进来,GAS 通过 ASC/AttributeSet 长在身上,而 SlottedAbilities 就是连接两者的桥——“装备了什么 → 授予什么能力”(这座桥的细节是 GAS 整合篇的主题)。
二、构造函数:ASC 与 AttributeSet 的诞生
GAS 的两个核心组件在构造函数里就创建好了(RPGCharacterBase.cpp:8-19):
ARPGCharacterBase::ARPGCharacterBase()
{
// 创建 ASC,并显式开启网络复制
AbilitySystemComponent = CreateDefaultSubobject<URPGAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
// 创建属性集(默认就会复制)
AttributeSet = CreateDefaultSubobject<URPGAttributeSet>(TEXT("AttributeSet"));
CharacterLevel = 1;
bAbilitiesInitialized = false;
}
三个要点:
CreateDefaultSubobject是构造期专用:它在对象构造时创建子组件,UE 的反射/序列化系统会正确认领这俩。这是 UE 创建默认组件的标准姿势,不能用NewObject替代。- ASC 显式
SetIsReplicated(true):能力系统要在联机时同步,必须开复制。AttributeSet 默认就复制,所以没单独写。 bAbilitiesInitialized = false:一个守卫标志。能力还没初始化时,属性变化的回调不该触发蓝图特效(避免出生设初值时就播一堆受击动画)。
谁来调那些
Handle*回调? 头文件最后一行friend URPGAttributeSet;(RPGCharacterBase.h:208)把属性集设成了友元。属性一变,URPGAttributeSet直接调角色的HandleDamage/HandleHealthChanged等 protected 函数,再由它们触发OnDamaged等蓝图事件。这条"属性→角色→蓝图"的回调链是伤害管线篇的主角,这里先知道入口在哪。
三、PossessedBy / UnPossessed:角色的"上下线"
角色是"躯壳",控制器(玩家或 AI)接管它的瞬间,要做一串初始化;离开时要清理。这正是 PossessedBy / UnPossessed 的职责。
接管:PossessedBy(cpp:211-230)
void ARPGCharacterBase::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// ① 把控制器赋给背包接口引用——AI 控制器没实现接口,会赋值失败,InventorySource 保持无效
InventorySource = NewController;
if (InventorySource)
{
// ② 订阅背包的 Native 委托(装备变了刷能力、读档完了刷能力)
InventoryUpdateHandle = InventorySource->GetSlottedItemChangedDelegate()
.AddUObject(this, &ARPGCharacterBase::OnItemSlotChanged);
InventoryLoadedHandle = InventorySource->GetInventoryLoadedDelegate()
.AddUObject(this, &ARPGCharacterBase::RefreshSlottedGameplayAbilities);
}
// ③ 初始化 GAS:告诉 ASC 谁是 Owner、谁是 Avatar,然后授予出生能力
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
AddStartupGameplayAbilities();
}
}
这三步把背包和 GAS 都接上了:
- ① 接口赋值即筛选:
TScriptInterface赋值时会检查这个 UObject 有没有实现URPGInventoryInterface。玩家 Controller 实现了 → 成功;AI 的AAIController没实现 →InventorySource无效。于是 AI 角色天然不走背包驱动的能力逻辑(背包篇详述过)。 - ② 订阅的是 Native 委托:要"先于 UI"响应装备变化,所以订
*Native版(委托篇详述过)。 - ③
InitAbilityActorInfo(this, this):告诉 ASC,OwnerActor 和 AvatarActor 都是角色自己。本项目 ASC 长在角色身上(不是放在 PlayerState),所以两个参数都是this。
离开:UnPossessed(cpp:232-245)
void ARPGCharacterBase::UnPossessed()
{
// 绑了必须解,否则失效的角色还挂在 Controller 委托上,下次广播会调到悬空对象
if (InventorySource && InventoryUpdateHandle.IsValid())
{
InventorySource->GetSlottedItemChangedDelegate().Remove(InventoryUpdateHandle);
InventoryUpdateHandle.Reset();
InventorySource->GetInventoryLoadedDelegate().Remove(InventoryLoadedHandle);
InventoryLoadedHandle.Reset();
}
InventorySource = nullptr;
}
绑定与解绑成对出现——PossessedBy 绑、UnPossessed 解,这是 UE 委托使用的铁律。
网络补给:OnRep_Controller 与 GetLifetimeReplicatedProps
联机时控制器信息是异步复制到客户端的,所以还有两个网络相关的 override:
void ARPGCharacterBase::OnRep_Controller()
{
Super::OnRep_Controller();
// 控制器在客户端更新了 → ASC 的 ActorInfo 要重新刷新
if (AbilitySystemComponent)
{
AbilitySystemComponent->RefreshAbilityActorInfo();
}
}
void ARPGCharacterBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ARPGCharacterBase, CharacterLevel); // 注册 CharacterLevel 参与网络复制
}
OnRep_Controller:客户端收到控制器复制后回调,重新RefreshAbilityActorInfo让 ASC 的内部引用指向正确的控制器。PossessedBy是服务器侧的接管,OnRep_Controller是客户端侧的"补登记",两者配合才能让 GAS 在联机下工作正常。GetLifetimeReplicatedProps:UE 声明"哪些UPROPERTY要复制"的标准入口。这里只复制了CharacterLevel(它带Replicated标记,影响能力强度,客户端要知道)。
不必纠结联机细节——单机玩本项目这些回调也都会走,理解"控制器变化时 ASC 需要重新登记"这个因果就够了。
四、武器系统:数据资产 + WeaponActor 两层
C++ 程序员第一次找"武器类"常会困惑:Source/ 里没有 Weapon.cpp 这种武器逻辑类。因为 ActionRPG 的武器分成两层,一层在 C++(数据),一层在蓝图(表现)。
数据层:URPGWeaponItem 是个数据资产
武器在 C++ 里是一个 Primary Data Asset。基类 URPGItem 提供了"物品能授予能力"的字段(Items/RPGItem.h):
/** 装备此物品时授予的能力 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Abilities)
TSubclassOf<URPGGameplayAbility> GrantedAbility;
/** 授予能力的等级 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Abilities)
int32 AbilityLevel;
武器子类 URPGWeaponItem 在它之上只加了一个字段(Items/RPGWeaponItem.h):
UCLASS()
class ACTIONRPG_API URPGWeaponItem : public URPGItem
{
GENERATED_BODY()
public:
URPGWeaponItem()
{
ItemType = URPGAssetManager::WeaponItemType; // 构造时钉死类型为 Weapon
}
/** 要生成的武器 Actor(视觉模型 + 命中检测,蓝图实现) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Weapon)
TSubclassOf<AActor> WeaponActor;
};
所以一把武器的"数据定义"就是:类型(Weapon) + 授予哪个能力(GrantedAbility) + 能力几级(AbilityLevel) + 对应哪个 WeaponActor 蓝图。它是纯数据,不含任何行为逻辑。
表现层:WeaponActor 是蓝图里的网格 + 碰撞
WeaponActor 字段指向的,是一个 AActor 蓝图——它才是真正出现在世界里、挂在角色手上的那把武器模型,负责攻击动画时用碰撞体检测命中。

这层全在蓝图(BP_Weapon_Sword、BP_Weapon_Hammer 等),逻辑大致是:攻击动画播放时开启武器碰撞 → 碰到敌人 ActorBeginOverlap → 发起一次 GameplayEvent → 触发能力结算伤害。C++ 这边不掺和具体碰撞,只通过数据资产把"哪把武器用哪个 Actor"声明清楚。
串起来:装备一把武器会发生什么
把数据层和表现层接起来,装备武器的完整故事是:
背包里把剑拖进武器槽 (SetSlottedItem)
└─ 角色收到 OnItemSlotChanged(订阅的 Native 委托)
├─ 读 URPGWeaponItem.GrantedAbility → 授予对应 GAS 能力(进 SlottedAbilities)
└─ 读 URPGWeaponItem.WeaponActor → 蓝图生成武器模型挂到手上
左边那条"授予能力"是 GAS 整合篇的硬核内容;右边"生成模型"是蓝图表现。数据资产是两者共同的"说明书"——这就是把武器拆成数据层 + 表现层的好处:设计师改一个数据资产,能力和模型一起换,C++ 一行不用动。
五、GameplayTag:层级化的"标签词典"
GAS 里到处是 FGameplayTag。它本质是层级化的字符串标识,集中定义在 Config/DefaultGameplayTags.ini。本项目的全部标签:
Ability.Item ; 物品类能力
Ability.Melee / Ability.Melee.Close / Ability.Melee.Far ; 近战及其子类
Ability.Ranged ; 远程
Ability.Skill ; 技能
Cooldown.Skill ; 冷却标签
EffectContainer.Default ; 效果容器查表键
Event.Montage.Player.Combo.* ; 玩家连招动画事件(BurstPound/ChestKick/…)
Event.Montage.Shared.UseItem / UseSkill / WeaponHit ; 共享动画事件
Status.DamageImmune ; 无敌状态

层级命名 = 自带"包含匹配"
标签用 . 分层,比如 Ability.Melee.Close。这个层级不是装饰——父标签能匹配子标签:
- 一个查询
Ability.Melee,能命中Ability.Melee.Close和Ability.Melee.Far。 - 一个查询
Ability,能命中所有Ability.*。
所以你可以用 Ability.Melee 一把捞出所有近战能力,而不用枚举每个具体子类。这是 GameplayTag 相比 enum 最大的优势。
为什么用 Tag 不用 enum
enum | FGameplayTag | |
|---|---|---|
| 加新类别 | 改枚举、可能要重编译依赖它的代码 | 配置里加一行,无需改 C++ |
| 层级/分组 | 没有,得自己约定 | 天生层级,父匹配子 |
| 容器运算 | 自己写 | FGameplayTagContainer 自带 HasTag / HasAny / HasAll |
| 设计师可改 | 不能 | 能(改 ini / 编辑器面板) |
ActionRPG 用标签干三类事,看名字就能归类:
Ability.*:给能力分类,配合ActivateAbilitiesWithTags按标签批量激活。Event.Montage.*:动画通知(AnimNotify)发出的事件标签,能力等这些事件来结算(伤害管线/AbilityTask 篇会用到Event.Montage.Shared.WeaponHit)。Status.*/Cooldown.*/EffectContainer.*:状态标记、冷却、效果容器查表键。
现在不用记住每个标签的用法,只要建立"这是个层级化、可配置、能父匹配子的标签词典"的认知。后面 GAS 几篇会反复回到这张表。
六、动手与验收
动手任务
- 在编辑器里打开
BP_PlayerCharacter和某个BP_Enemy,对比它们在GameplayAbilities、DefaultSlottedAbilities上的配置差异。 - 在
PossessedBy里加日志打印InventorySource是否有效,分别用玩家角色和 AI 角色触发,确认 AI 的为无效。 - 打开任意一个
BP_Weapon_*,找到它对应的URPGWeaponItem数据资产,看GrantedAbility/WeaponActor两个字段是怎么填的。 - 阅读
DefaultGameplayTags.ini,把所有标签画成一棵层级树。
验收清单
- 能说出
ARPGCharacterBase继承的三个身份各管什么。 - 能画出角色的"职责地图",指出哪些成员属于 GAS、哪些属于背包、哪个是连接两者的桥。
- 能解释构造函数为什么用
CreateDefaultSubobject、为什么 ASC 要SetIsReplicated(true)。 - 能复述
PossessedBy的三步,并解释InitAbilityActorInfo(this, this)两个参数为何都是 this。 - 能说清武器的"数据层(URPGWeaponItem)+ 表现层(WeaponActor)"两层结构。
- 能解释 GameplayTag 的层级匹配(父匹配子),以及相比 enum 的优势。
1341

被折叠的 条评论
为什么被折叠?



