【UE源码精读-ActionRPG】角色基类与武器系统

[TOC]

导读

背包和存档两条线走完,接下来就要啃这个项目最硬的骨头——GAS(GameplayAbilities)。但在一头扎进属性、伤害、能力之前,先花一篇建立"全局视图":ARPGCharacterBase 是整个游戏的枢纽,背包、属性、能力、武器都在它身上汇合。看懂它的结构和生命周期,后面 GAS 几篇才不会迷路。

这一篇不深入 GAS 细节(那是后面几篇的事),只把骨架摸清楚:

  • ARPGCharacterBase 继承了哪些东西?身上挂着哪些成员,各自归谁管?
  • AbilitySystemComponent 和 AttributeSet 是怎么来的、为什么在角色身上?
  • 控制器接管角色(PossessedBy)和离开(UnPossessed)时各做了什么?
  • "武器"在 C++ 里到底是什么——一个 Actor?一个数据资产?
  • 满屏的 Ability.Melee.CloseEvent.Montage.Shared.WeaponHit 这些 GameplayTag 怎么组织的?

阅读前提:读过本系列背包、存档篇,理解 IRPGInventoryInterfaceFRPGItemSlot、Primary Data Asset。

源码范围RPGCharacterBase.h/.cppItems/RPGItem.hItems/RPGWeaponItem.hConfig/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 的职责。

接管:PossessedBycpp: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

离开:UnPossessedcpp: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_ControllerGetLifetimeReplicatedProps

联机时控制器信息是异步复制到客户端的,所以还有两个网络相关的 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_SwordBP_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                       ; 无敌状态

Project Settings 里的 GameplayTags 标签树

层级命名 = 自带"包含匹配"

标签用 . 分层,比如 Ability.Melee.Close。这个层级不是装饰——父标签能匹配子标签

  • 一个查询 Ability.Melee,能命中 Ability.Melee.CloseAbility.Melee.Far
  • 一个查询 Ability,能命中所有 Ability.*

所以你可以用 Ability.Melee 一把捞出所有近战能力,而不用枚举每个具体子类。这是 GameplayTag 相比 enum 最大的优势。

为什么用 Tag 不用 enum

enumFGameplayTag
加新类别改枚举、可能要重编译依赖它的代码配置里加一行,无需改 C++
层级/分组没有,得自己约定天生层级,父匹配子
容器运算自己写FGameplayTagContainer 自带 HasTag / HasAny / HasAll
设计师可改不能能(改 ini / 编辑器面板)

ActionRPG 用标签干三类事,看名字就能归类:

  • Ability.*:给能力分类,配合 ActivateAbilitiesWithTags 按标签批量激活。
  • Event.Montage.*:动画通知(AnimNotify)发出的事件标签,能力等这些事件来结算(伤害管线/AbilityTask 篇会用到 Event.Montage.Shared.WeaponHit)。
  • Status.* / Cooldown.* / EffectContainer.*:状态标记、冷却、效果容器查表键。

现在不用记住每个标签的用法,只要建立"这是个层级化、可配置、能父匹配子的标签词典"的认知。后面 GAS 几篇会反复回到这张表。


六、动手与验收

动手任务

  1. 在编辑器里打开 BP_PlayerCharacter 和某个 BP_Enemy,对比它们在 GameplayAbilitiesDefaultSlottedAbilities 上的配置差异。
  2. PossessedBy 里加日志打印 InventorySource 是否有效,分别用玩家角色和 AI 角色触发,确认 AI 的为无效。
  3. 打开任意一个 BP_Weapon_*,找到它对应的 URPGWeaponItem 数据资产,看 GrantedAbility / WeaponActor 两个字段是怎么填的。
  4. 阅读 DefaultGameplayTags.ini,把所有标签画成一棵层级树。

验收清单

  • 能说出 ARPGCharacterBase 继承的三个身份各管什么。
  • 能画出角色的"职责地图",指出哪些成员属于 GAS、哪些属于背包、哪个是连接两者的桥。
  • 能解释构造函数为什么用 CreateDefaultSubobject、为什么 ASC 要 SetIsReplicated(true)
  • 能复述 PossessedBy 的三步,并解释 InitAbilityActorInfo(this, this) 两个参数为何都是 this。
  • 能说清武器的"数据层(URPGWeaponItem)+ 表现层(WeaponActor)"两层结构。
  • 能解释 GameplayTag 的层级匹配(父匹配子),以及相比 enum 的优势。
内容概要:本文围绕可变桨叶四旋翼无人机的规范控制点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率响应速度,旨在提升无人机在复杂飞行任务中的动态性能控制精度。该仿真研究为无人机飞控系统的设计优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜猫逐梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值