【虚幻引擎】【C++】【GAS框架】构建角色基类、属性集、数值钳制

目录

[0. 全局观:这三个阶段在搭什么](#0. 全局观:这三个阶段在搭什么)

[1. 阶段〇:Build.cs 模块配置](#1. 阶段〇:Build.cs 模块配置)

[2. 阶段一:创建带 GAS 能力系统的角色基类](#2. 阶段一:创建带 GAS 能力系统的角色基类)

[2.1 代码:NexusCharacterBase.h](#2.1 代码:NexusCharacterBase.h)

[2.2 代码:NexusCharacterBase.cpp](#2.2 代码:NexusCharacterBase.cpp)

[2.3 头文件要点解析](#2.3 头文件要点解析)

[2.4 构造函数要点解析](#2.4 构造函数要点解析)

[2.5 GAS 的灵魂:InitAbilityActorInfo 的双时机](#2.5 GAS 的灵魂:InitAbilityActorInfo 的双时机)

[3. 阶段二:添加属性集 AttributeSet](#3. 阶段二:添加属性集 AttributeSet)

[3.1 代码:BasicAttributeSet.h](#3.1 代码:BasicAttributeSet.h)

[3.2 代码:BasicAttributeSet.cpp](#3.2 代码:BasicAttributeSet.cpp)

[3.3 角色类的三处改动](#3.3 角色类的三处改动)

[3.4 知识点解析](#3.4 知识点解析)

[4. 阶段三:数值钳制(Clamping)](#4. 阶段三:数值钳制(Clamping))

[4.1 为什么要钳制](#4.1 为什么要钳制)

[4.2 代码改动](#4.2 代码改动)

[4.3 两个回调对比](#4.3 两个回调对比)

[5. 附:动手验证建议](#5. 附:动手验证建议)

[6. 参考资料](#6. 参考资料)


0. 全局观:这三个阶段在搭什么

一句话:创建一个"自带 GAS 能力系统的角色基类",保证它在单机和联网下都能正确初始化;再给它挂上属性账本,并教会账本"不许记越界的数"。

可以把它想象成一个公司的架构:

复制代码
    ANexusCharacterBase(角色 = 公司大楼)
     ├─ AbilitySystemComponent(ASC = 管家)
     │    ├─ GameplayAbility 技能(员工,以后加)
     │    ├─ UBasicAttributeSet 属性集(资产账本,阶段二加入)
     │    │     ├─ Health / MaxHealth(生命 / 最大生命)
     │    │     └─ Stamina / MaxStamina(耐力 / 最大耐力)
     │    └─ GameplayTags 标签(档案标签,以后加)
     └─ CharacterMovement(腿:负责移动、跳跃)

ASC 是 GAS 系统的核心管理者,所有技能、效果与属性均归属于它。各阶段职责如下:

阶段 执行内容 一句话理解
〇 Build.cs 添加模块 为项目通电(编译期引入 GAS 依赖)
一 定义角色基类并绑定 ASC 雇佣管家,并明确其服务对象
二 创建属性集(AttributeSet) 为管家配备资产账本,并打通网络同步通信通道
三 实现属性钳制逻辑 规范账本使用:生命值不可为负,不可超出上限

最终涉及的文件(均位于 Source/Nexus_Demo_GAS/ 目录下):

文件 引入阶段 功能说明
Nexus_Demo_GAS.Build.cs 〇 引入 GAS 的三大核心模块
NexusCharacterBase.h/.cpp 一(二补充属性集成员) 角色基类:负责 ASC 的创建与双端初始化
BasicAttributeSet.h/.cpp 二(三补充钳制逻辑) 属性集:定义 4 个基础属性,包含复制回调与取值范围约束

1. 阶段〇:Build.cs 模块配置

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.
 
using UnrealBuildTool;
 
public class Nexus_Demo_GAS : ModuleRules
{
    public Nexus_Demo_GAS(ReadOnlyTargetRules Target) : base(Target)
    {
        // 预编译头策略:显式或共享 PCH(UE 模板默认值,不用动)
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
 
        // 公开依赖模块:本模块用到、别的模块也能看到的依赖
        PublicDependencyModuleNames.AddRange(new string[]
        {
            "Core",            // UE 核心基础库(容器、数学、日志......)
            "CoreUObject",     // UObject 反射系统(UCLASS/UPROPERTY 的地基)
            "Engine",          // 引擎核心(AActor、ACharacter 等在这里)
            "InputCore",       // 传统输入
            "EnhancedInput",   // 增强输入系统(UE5 新输入)
 
            // ↓↓↓ 启用 GAS 必需的三个模块,缺一不可 ↓↓↓
            "GameplayAbilities", // GAS 本体:ASC、GameplayEffect、GameplayAbility、AttributeSet
            "GameplayTags",      // 标签系统:FGameplayTag / FGameplayTagContainer,GAS 的"神经脉络"
            "GameplayTasks"      // 任务系统:GameplayAbility 执行期间异步任务的底层依赖
        });
 
        // 如果要使用 Slate UI,取消下面注释
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
 
        // 如果要使用联网功能,取消下面注释
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");
 
        // 若要使用 OnlineSubsystemSteam,需在 uproject 文件的 plugins 段添加并设 Enabled 为 true
    }
}

三个 GAS 模块的作用如下:

模块 里面有什么
GameplayAbilities GAS 核心模块:包含 ASC(AbilitySystemComponent)、GameplayAbility、GameplayEffect 以及 AttributeSet 等核心组件
GameplayTags 标签系统:为技能、效果等提供唯一标识,GAS 内部的匹配与逻辑判断大量依赖此系统
GameplayTasks 任务系统:支持在技能执行过程中挂载异步任务(如等待输入、选择目标等),是实现复杂行为的底层支撑

📌 即使暂未使用标签系统,这三个模块也必须添加 ------GameplayAbilities 模块本身依赖于另外两个。

✅ 修改 Build.cs 后,请务必:关闭编辑器 → 右键 .uproject 文件重新生成项目文件 → 重新编译,否则更改不会生效。


2. 阶段一:创建带 GAS 能力系统的角色基类

2.1 代码:NexusCharacterBase.h

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.
 
#pragma once                                  // 防止头文件被重复包含
 
#include "CoreMinimal.h"                      // UE 基础类型(float、FString、日志宏等)
#include "GameFramework/Character.h"          // ACharacter:角色基类(胶囊体、移动组件、跳跃......)
#include "AbilitySystemInterface.h"           // IAbilitySystemInterface:GAS 定义的接口(一份"合同")
#include "AbilitySystemComponent.h"           // UAbilitySystemComponent:GAS 的核心组件类
#include "NexusCharacterBase.generated.h"     // ← UHT 自动生成的反射代码,永远必须是最后一个 include!
 
UCLASS()   // 向反射系统声明:本类参与蓝图、序列化、网络复制,UHT 会据此生成 .generated.h 的内容
class NEXUS_DEMO_GAS_API ANexusCharacterBase : public ACharacter                // 继承1:角色(物理身体+移动)
                                                             , public IAbilitySystemInterface  // 继承2:GAS 接口(签合同)
{
    GENERATED_BODY()   // 占位宏,编译期展开为 UHT 生成的反射代码;UCLASS 类里必须写
 
public:
    // Sets default values for this character's properties 构造函数
    ANexusCharacterBase();
 
    // Ability System Component 能力系统组件(GAS 的大管家:技能/效果/属性/标签都挂在它名下)
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AbilitySystem")
    class UAbilitySystemComponent* AbilitySystemComponent;
 
protected:
    // ASC 的 GameplayEffect 复制模式:C++ 默认成员初始化语法给了初值,
    // 又因为是 EditAnywhere 的 UPROPERTY,蓝图子类可以按角色类型覆盖(玩家=Mixed,AI=Minimal)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AbilitySystem")
    EGameplayEffectReplicationMode AscReplicationMode = EGameplayEffectReplicationMode::Mixed;
 
protected:
    // Called when the game starts or when spawned 游戏开始/生成时调用,只调一次
    virtual void BeginPlay() override;
 
    // 【服务器】Controller 占有(Possess)该 Pawn 时调用 ------ 给服务器侧的 ASC 登记身份
    virtual void PossessedBy(AController* NewController) override;
 
    // 【客户端】PlayerState 复制到达时调用 ------ 给客户端侧的 ASC 登记身份
    virtual void OnRep_PlayerState() override;
 
public:
    // Called every frame 每帧都会调用
    virtual void Tick(float DeltaTime) override;
 
    // Called to bind functionality to input 用来把输入和功能绑定起来
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
 
    // 履行接口合同:把 ASC 上交给 GAS(GAS 内部拿到任意 Actor 都走这条通道取 ASC)
    virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
};

2.2 代码:NexusCharacterBase.cpp

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.
 
#include "NexusCharacterBase.h"
 
#include "Components/CapsuleComponent.h"              // 胶囊体碰撞组件
#include "GameFramework/CharacterMovementComponent.h" // 角色移动组件
 
// ---------- 构造函数:设置类默认值(蓝图子类都会继承这里的设置) ----------
ANexusCharacterBase::ANexusCharacterBase()
{
    // 让角色每帧调用 Tick()。如果不需要每帧逻辑,改成 false 可以提升性能。
    PrimaryActorTick.bCanEverTick = true;
 
    // =====【GAS 第一步:创建 ASC ------ 把"大管家"雇佣进来】=====
    // CreateDefaultSubobject 只能在构造函数里用:组件会成为这个类的固定组成部分,
    // 就像人生下来自带心脏,所有蓝图子类自动继承。
    // TEXT("...") 是子对象名(大纲里显示、用于序列化),创建后不要再改名。
    AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
 
    // 组件默认不联网同步;ASC 必须显式打开,否则多人游戏里客户端看不到技能状态
    AbilitySystemComponent->SetIsReplicated(true);
 
    // 设置 GameplayEffect 的复制模式(Full / Mixed / Minimal,见 2.4 的表格)
    AbilitySystemComponent->SetReplicationMode(AscReplicationMode);
 
    // ---------- 碰撞 ----------
    // 胶囊体是角色的碰撞外壳:半径 35cm、半高 90cm
    GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
 
    // 角色不跟随控制器的旋转(俯仰/偏航/翻滚全关):
    // 否则转视角时第三人称身体会跟着转。视角交给摄像机,身体朝向交给下面移动组件管。
    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = false;
    bUseControllerRotationRoll = false;
 
    // ---------- 移动手感(UE 第三人称模板默认手感,日后调手感主要动这几行) ----------
    GetCharacterMovement()->bOrientRotationToMovement = true;            // 朝移动方向转身
    GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f); // 转身速度:每秒最多 500°(仅偏航),越大越"跟手"
    GetCharacterMovement()->JumpZVelocity = 500.f;                       // 起跳瞬间 Z 向初速度(cm/s),约跳 140cm 高
    GetCharacterMovement()->AirControl = 0.35f;                          // 空中操控系数 0~1:空中保留 35% 的控制力
    GetCharacterMovement()->MaxWalkSpeed = 500.f;                        // 地面最大速度 500cm/s = 5m/s
    GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;                   // 手柄摇杆轻推时的最低速度,防止慢到爬行
    GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;         // 地面刹车力度:松开摇杆后停得多快
    GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;        // 空中刹车力度:比地面稍弱
}
 
// Called when the game starts or when spawned 只调用一次
void ANexusCharacterBase::BeginPlay()
{
    Super::BeginPlay();
}
 
// Called every frame 每帧都会调用
void ANexusCharacterBase::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}
 
// Called to bind functionality to input 用来把输入和功能绑定起来
void ANexusCharacterBase::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
}
 
// ---------- GAS 双端初始化 · 服务器侧 ----------
// PossessedBy 只在【服务器】上、Controller 占有(Possess) Pawn 时被调用,客户端永远收不到。
// 另外:UE 4.24 起,这里会自动把 Pawn 的 Owner 设为新 Controller------
// 这正是"ASC 直接挂在 Pawn 上 + Mixed 复制模式"能够开箱即用的原因(见 2.4 的说明)。
void ANexusCharacterBase::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);   // 永远先调父类,让引擎先完成它的工作
 
    if (AbilitySystemComponent)
    {
        // 给 ASC 登记两个身份:
        //   参数1 OwnerActor  = ASC 逻辑上"住"在谁家(组件挂在角色身上 → 角色自己)
        //   参数2 AvatarActor = ASC 代表"谁"在世界上战斗、谁身上挂效果和动画(也是角色自己)
        AbilitySystemComponent->InitAbilityActorInfo(this, this);
    }
}
 
// ---------- GAS 双端初始化 · 客户端侧 ----------
// OnRep_ 前缀 = 网络复制到达时的自动回调,只在【客户端】执行。
// PlayerState 从服务器复制到客户端的那一刻,客户端就知道"这个角色对应哪个玩家",随即给客户端的 ASC 登记。
// 口诀:服务器走 PossessedBy,客户端走 OnRep_。两处开头的 Super:: 都不能省。
void ANexusCharacterBase::OnRep_PlayerState()
{
    Super::OnRep_PlayerState();
 
    if (AbilitySystemComponent)
    {
        AbilitySystemComponent->InitAbilityActorInfo(this, this);
    }
}
 
// ---------- 履行 IAbilitySystemInterface 的合同 ----------
// GAS 内部拿到任意 Actor 后的标准流程:Cast<IAbilitySystemInterface> → 成功就调这里拿 ASC → 再干活。
// 这就是当初继承接口的意义:GAS 不关心你的类是 Character 还是 Pawn,有这条通道就够了。
UAbilitySystemComponent* ANexusCharacterBase::GetAbilitySystemComponent() const
{
    return AbilitySystemComponent;
}
 

2.3 头文件要点解析

① Include 区 = 取工具箱。

前 4 行分别引入引擎基础、角色基类、GAS 接口与 GAS 组件。NexusCharacterBase.generated.h 是 UE 反射系统(UHT)根据 UCLASS/UPROPERTY 标记自动生成的文件,内含蓝图系统所需的"演员名单"。必须置于最后 include:UHT 生成的代码依赖于此前已知的所有类型定义,若中间插入将导致编译失败。

② 双继承是设计的核心精髓:

  • ACharacter 提供角色的物理形态、移动组件与胶囊体;
  • IAbilitySystemInterface 是 GAS 定义的纯接口(契约) ,仅要求实现 GetAbilitySystemComponent()。

GAS 不强制继承特定基类,而是通过接口机制------它不关心你的角色是 Character、Pawn 还是 PlayerState,只关心"你能否把 ASC 交给我"。任何签了合同(继承 + 实现)的类皆可接入 GAS,这正是"组合优于继承"的典型体现。

③ UPROPERTY 说明符速查表:

说明符 含义
VisibleAnywhere 编辑器或蓝图中仅可见(常用于组件指针,防止被误拖拽替换)
BlueprintReadOnly 蓝图中可读,不可写
EditAnywhere 编辑器中可编辑(适用于类默认值与实例)
BlueprintReadWrite 蓝图中可读可写
Category = "..." 在细节面板中分组,便于查找

④ 前向声明:

class UAbilitySystemComponent* 中的 class 关键字为前向声明------告知编译器"这是一个类,稍后定义",避免头文件包含完整定义,提升编译效率(实际定义在 .cpp 内)。本文件已包含 AbilitySystemComponent.h,因此此处 class 可省略,保留亦无害。

⑤ override 是拼写保镖:

它向编译器明确声明:"我正在重写父类的虚函数,请校验签名"。若父类无此函数,或参数不匹配,编译器将直接报错。注意 GetAbilitySystemComponent 后的 const------接口中声明为 const,重写时必须完全一致。

2.4 构造函数要点解析

复制模式(SetReplicationMode)决定同步范围:

模式 GameplayEffect 同步对象 标签/Cue 提示同步对象 适用场景
Full 所有人 所有人 单机或小规模合作游戏
Mixed 仅拥有者的客户端 所有人 玩家角色(战术数值保密,防作弊)
Minimal 不同步 所有人 AI 怪物(玩家无需知晓其详细数值)

📌 关键细节 :根据 GAS 官方文档(tranek/GASDocumentation),Mixed 模式要求 OwnerActor 的 Owner 为 Controller。PlayerState 天然满足;而 Character 则非如此,但自 UE 4.24 起,PossessedBy() 会自动将 Pawn 的 Owner 设为新控制器。因此在 UE5 环境下,"ASC 挂在 Pawn 上 + Mixed"开箱即用。未来迁移到 PlayerState 时,该规则仍会重现。

2.5 GAS 的灵魂:InitAbilityActorInfo 的双时机

cpp 复制代码
AbilitySystemComponent->InitAbilityActorInfo(this, this);

这是本文件最核心的一行代码,为 ASC 注册两个身份:

  • 参数 1:OwnerActor(拥有者) ------ ASC 在逻辑上"归属"于谁。由于 ASC 是 Character 的子组件,故应设为自身。
  • 参数 2:AvatarActor(化身) ------ ASC 代表"谁"在世界中行动、承受效果与动画。

两者何时不同? 当 ASC 挂载于 PlayerState 时(大型项目主流做法):

  • OwnerActor = PlayerState(数据存储处)
  • AvatarActor = Pawn(表现与战斗实体)

当 ASC 直接挂载于角色身上时,二者合一------合法且常见,适用于中小型项目。

为何需初始化两次? 因服务器与客户端"角色就绪"时机不同:

时机 执行端 触发条件
PossessedBy 仅服务器 Controller 占有(Possess)Pawn 时
OnRep_PlayerState 仅客户端 PlayerState 从服务器复制到达时

口诀:服务器走 PossessedBy,客户端走 OnRep_。

这是 GAS 双端初始化的标准结构。两处开头的 Super::Xxx() 不可遗漏------它们通知父类执行其应有的流程,删除将破坏引擎内部机制。


3. 阶段二:添加属性集 AttributeSet

3.1 代码:BasicAttributeSet.h

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.
 
#pragma once
 
#include "CoreMinimal.h"
#include "AttributeSet.h"               // UAttributeSet:所有属性集的基类
#include "AbilitySystemComponent.h"     // FGameplayAttributeData、复制/访问器相关宏在这里
#include "BasicAttributeSet.generated.h" // ← 永远是最后一个 include
 
/**
 * 基础属性集:生命 / 耐力
 *
 * 三个要点:
 * 1. 每个属性用 FGameplayAttributeData 存(内含 BaseValue + CurrentValue),
 *    而不是普通 float------GameplayEffect 的修改、钳制、网络复制都建立在这个结构上;
 * 2. 属性用 BlueprintReadOnly:属性只应通过 GameplayEffect / SetXxx() 修改,
 *    不给蓝图"直接改内存"的后门,否则会绕过钳制、复制和变化通知;
 * 3. ATTRIBUTE_ACCESSORS_BASIC 宏一行生成本属性的全部访问器(展开见下方注释)。
 */
UCLASS()
class NEXUS_DEMO_GAS_API UBasicAttributeSet : public UAttributeSet
{
    GENERATED_BODY()
 
public:
 
    UBasicAttributeSet();
 
    // ==================== 生命 ====================
 
    // 当前生命值
    // ReplicatedUsing = OnRep_Health:该值从服务器复制到客户端时,自动调用下面的 OnRep_Health
    UPROPERTY(BlueprintReadOnly, Category = "Attributes", ReplicatedUsing = OnRep_Health)
    FGameplayAttributeData Health;
 
    // 宏展开后生成三个访问器:
    //   GetHealthAttribute() ------ 返回 FGameplayAttribute(这个属性的"身份证",钳制/GE 里用它比对身份)
    //   GetHealth()          ------ 读当前值(CurrentValue)
    //   SetHealth(NewVal)    ------ 写入新值(通过 ASC 的属性管线修改)
    //
    // ⚠️ 版本注意:ATTRIBUTE_ACCESSORS_BASIC 是 UE 5.6 起官方内置在 AttributeSet.h 的组合宏;
    //    更旧的版本没有它,编译会报未定义。旧版做法参考 GASDocumentation:
    //    在自己的头文件里自行定义组合宏 ATTRIBUTE_ACCESSORS(同样三个访问器 + 额外的 InitHealth() 初始化器)。
    ATTRIBUTE_ACCESSORS_BASIC(UBasicAttributeSet, Health);
 
    // 最大生命值("上限"本身也是属性:升级、Buff 都可能改它)
    UPROPERTY(BlueprintReadOnly, Category = "Attributes", ReplicatedUsing = OnRep_MaxHealth)
    FGameplayAttributeData MaxHealth;
    ATTRIBUTE_ACCESSORS_BASIC(UBasicAttributeSet, MaxHealth);
 
    // ==================== 耐力 ====================
 
    // 当前耐力值(冲刺等动作消耗的就是它)
    UPROPERTY(BlueprintReadOnly, Category = "Attributes", ReplicatedUsing = OnRep_Stamina)
    FGameplayAttributeData Stamina;
    ATTRIBUTE_ACCESSORS_BASIC(UBasicAttributeSet, Stamina);
 
    // 最大耐力值
    UPROPERTY(BlueprintReadOnly, Category = "Attributes", ReplicatedUsing = OnRep_MaxStamina)
    FGameplayAttributeData MaxStamina;
    ATTRIBUTE_ACCESSORS_BASIC(UBasicAttributeSet, MaxStamina);
 
public:
    // ==================== 网络复制回调 ====================
    // OnRep_ 只在【客户端】被调用:服务器改了属性 → 复制到客户端 → 触发这里。
    // GAMEPLAYATTRIBUTE_REPNOTIFY 宏会通知 ASC"该属性已复制更新",于是 UI / 技能里
    // 通过 GetGameplayAttributeValueChangeDelegate(...) 注册的回调就会带着旧值被触发。
 
    UFUNCTION()
    void OnRep_Health(const FGameplayAttributeData& OldValue) const
    
{
        GAMEPLAYATTRIBUTE_REPNOTIFY(UBasicAttributeSet, Health, OldValue);
    }
 
    UFUNCTION()
    void OnRep_MaxHealth(const FGameplayAttributeData& OldValue) const
    
{
        GAMEPLAYATTRIBUTE_REPNOTIFY(UBasicAttributeSet, MaxHealth, OldValue);
    }
 
    UFUNCTION()
    void OnRep_Stamina(const FGameplayAttributeData& OldValue) const
    
{
        GAMEPLAYATTRIBUTE_REPNOTIFY(UBasicAttributeSet, Stamina, OldValue);
    }
 
    UFUNCTION()
    void OnRep_MaxStamina(const FGameplayAttributeData& OldValue) const
    
{
        GAMEPLAYATTRIBUTE_REPNOTIFY(UBasicAttributeSet, MaxStamina, OldValue);
    }
 
    // 登记哪些属性要复制、复制条件是什么(不 override 这个函数,上面的 OnRep 永远不会被触发)
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};

3.2 代码:BasicAttributeSet.cpp

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.
 
#include "BasicAttributeSet.h"
#include "Net/UnrealNetwork.h"   // DOREPLIFETIME_* 复制登记宏在这里
 
UBasicAttributeSet::UBasicAttributeSet()
{
    // 初始值。
    // 注意:直接给属性赋值绕过了 ASC 管线(不触发钳制/委托),只适合做"出生初始值";
    //        更标准的 GAS 做法是用一个"初始值 GameplayEffect"来初始化属性。
    Health     = 100.f;   // 初始生命
    MaxHealth  = 100.f;   // 最大生命
    Stamina    = 100.f;   // 初始耐力
    MaxStamina = 100.f;   // 最大耐力
}
 
// 登记属性的复制规则:DOREPLIFETIME_CONDITION_NOTIFY(类, 属性, 复制条件, OnRep触发条件)
//   COND_None        = 无条件复制给所有端(进阶可用 COND_OwnerOnly 等做权限控制)
//   REPNOTIFY_Always = 即使新值和旧值相同也触发 OnRep
//                      (比如"回满血"时数值没变,UI 也需要收到通知去刷新)
void UBasicAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME_CONDITION_NOTIFY(UBasicAttributeSet, Health,     COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UBasicAttributeSet, MaxHealth,  COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UBasicAttributeSet, Stamina,    COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UBasicAttributeSet, MaxStamina, COND_None, REPNOTIFY_Always);
}

3.3 角色类的三处改动

属性集不需要新的"接线"流程,只要在角色基类上做三处小改动:

cpp 复制代码
// ① NexusCharacterBase.h:新增属性集成员
//    (class 前缀 = 前向声明,完整定义放 .cpp 里 include)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AbilitySystem")
class UBasicAttributeSet* BasicAttributeSet;
cpp 复制代码
// ② NexusCharacterBase.cpp:include 完整定义
#include "BasicAttributeSet.h"
cpp 复制代码
// ③ NexusCharacterBase.cpp 构造函数:创建属性集
//    以默认子对象方式创建,与 ASC 同属一个角色,ASC 会自动注册它,不需要手动 Add
BasicAttributeSet = CreateDefaultSubobject<UBasicAttributeSet>(TEXT("BasicAttributeSet"));

3.4 知识点解析

① FGameplayAttributeData = 底账 + 实付

这是一个包含两个 float 值的结构体:

  • BaseValue(基础值 / 底账) :永久性的基础数值,瞬时 GameplayEffect 直接修改的是此值;
  • CurrentValue(当前值 / 实付):在 BaseValue 基础上叠加所有持续型 GameplayEffect 修正后的最终结果;
  • GetXxx() 返回的是 CurrentValue ------ UI 展示与逻辑判断均应使用该值。

之所以不使用普通 float,是因为需要支持:

  • GameplayEffect 的加/乘/覆盖等修改;
  • 钳制(Clamping);
  • 网络复制;
  • 属性变化委托(Change Notifiers)------这些机制都依赖于该结构体的完整生命周期管理。

② 为何属性设为 BlueprintReadOnly?

属性仅应通过 GameplayEffect 或 SetXxx() 经由 ASC 管线进行修改。这样可确保:

  • 钳制逻辑生效;
  • 网络复制正常触发;
  • 属性变更通知被正确广播。

直接在构造函数中赋值会绕过上述管线,因此仅适用于角色出生时的初始设定。

③ ATTRIBUTE_ACCESSORS_BASIC 宏(UE 5.6+ 内置)

一行代码即可生成:

  • GetXxxAttribute()(获取属性引用,即"身份证");
  • GetXxx()(读取当前值);
  • SetXxx()(写入新值,触发变更流程)。

旧版本(≤5.5)无此宏,需按 GASDocumentation 推荐方式,在头文件中自定义 ATTRIBUTE_ACCESSORS 组合宏,额外提供一个 InitXxx() 函数用于初始化(直接设置 BaseValue,不触发委托)。

④ 属性网络同步"三件套",缺一不可:

组件 写在哪 作用
ReplicatedUsing = OnRep_Xxx UPROPERTY 声明中 指定当属性复制到达客户端时,调用 OnRep_Xxx 函数
OnRep_Xxx + GAMEPLAYATTRIBUTE_REPNOTIFY 属性集内部 在客户端执行;触发 ASC 广播属性变更委托(携带旧值),用于驱动 UI 更新和技能响应
GetLifetimeReplicatedProps + DOREPLIFETIME_CONDITION_NOTIFY 属性集内部 注册复制规则(未登记则永不复制)

4. 阶段三:数值钳制(Clamping)

4.1 为什么要钳制

属性的所有修改都应约束在合法区间:一次伤害 GE 把 Health 改成 -100、一次治疗 GE 把 Stamina 改成 99999,都会让 UI、死亡判定、消耗逻辑全线出错。GAS 在属性修改管线上设了两个"检查站",分工有官方定论:

  • PreAttributeChange() ------ 负责对当前属性值(CurrentValue)的钳制;

  • PostGameplayEffectExecute() ------ 负责对来自 GameplayEffect 的基础值(BaseValue)修改进行钳制

4.2 代码改动

① BasicAttributeSet.h:新增两个 override 声明 (置于 GetLifetimeReplicatedProps 声明之后,位于同一 public: 区域内即可)

cpp 复制代码
    // 【阶段三新增】属性值即将改变之前的回调 ------ 钳制 CurrentValue 的最佳位置
    virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
 
    // 【阶段三新增】瞬时 GameplayEffect 执行完成之后的回调 ------ 钳制/规范 BaseValue、做善后逻辑
    virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override

② BasicAttributeSet.cpp:新增 include 与两个实现:

cpp 复制代码
#include "BasicAttributeSet.h"
#include "GameplayEffectExtension.h"   // 【阶段三新增】FGameplayEffectModCallbackData 定义在这里
#include "Net/UnrealNetwork.h"
 
// ==================== 阶段三:数值钳制 ====================
 
// 属性值即将改变之前调用(GameplayEffect 等通过 GAS 管线的修改都会经过这里)。
// 参数:Attribute = 这次要改的是哪个属性(用 GetXxxAttribute() 比对"身份证")
//       NewValue  = 即将写入的新值,是【引用】------直接改写它就能实现钳制
//
// ⚠️ 官方文档明确:这里改写的 NewValue 只影响"这一次变化"(CurrentValue 路径),
//    不会永久修正 BaseValue。所以 BaseValue 的钳制要配合下面的 PostGameplayEffectExecute。
void UBasicAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
    Super::PreAttributeChange(Attribute, NewValue);
 
    if (Attribute == GetHealthAttribute())
    {
        // 生命钳制在 [0, MaxHealth]:不会出现负血,也不会溢出上限
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
    }
    else if (Attribute == GetStaminaAttribute())
    {
        // 耐力钳制在 [0, MaxStamina]
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxStamina());
    }
    // MaxHealth / MaxStamina 本身没有在这里钳------它们是"上限",通常由 GE 或成长系统负责
}
 
// 瞬时(Instant) GameplayEffect 在本 ASC 上执行完成之后调用(只在服务器/权威端)。
// 常见用途:读取刚被 GE 改过的值做善后------钳制 BaseValue、触发死亡、播放受击表现......
// Data.EvaluatedData.Attribute : 本次被修改的是哪个属性
// Data.EvaluatedData.Value     : ⚠️ Additive(加减)操作时它是"增量",不是最终值!
void UBasicAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
    Super::PostGameplayEffectExecute(Data);
 
    if (Data.EvaluatedData.Attribute == GetHealthAttribute())
    {
        // 经典写法:把刚被 GE 改过的值再通过 Setter 写回一遍。
        // 作用:让最终值重新走一遍属性管线(再次触发 PreAttributeChange 的钳制、
        // 属性变化委托与复制通知),并把 BaseValue 规范到合法范围内。
        // 更稳妥的加强写法(显式双保险):
        //   SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
        SetHealth(GetHealth());
    }
    else if (Data.EvaluatedData.Attribute == GetStaminaAttribute())
    {
        // 同上:耐力也回写一遍,确保钳制和通知生效
        SetStamina(GetStamina());
    }
}

4.3 两个回调对比

PreAttributeChange PostGameplayEffectExecute
触发时机 属性值即将变更之前(每次变化均触发) 瞬时 Gameplay Effect 执行完毕之后
官方分工 控制 CurrentValue 作用于 Gameplay Effect 的 BaseValue
生效方式 直接修改传入的 NewValue 参数 通过 SetXxx() 将最终结果写回属性
重要注意 仅影响本次变更,不会永久修改 BaseValue 仅在服务器端(权威端)回调执行
典型用途 限制生命值范围(如 Health ∈ [0, MaxHealth]) 回写钳制结果、死亡判定、受击反馈表现

📌 属性系统还包含 PreAttributeBaseChange、PostAttributeChange 等其他回调,初学阶段掌握上述两项即可。


5. 附:动手验证建议

按以下步骤可直观验证钳制机制的生效效果(建议在 Standalone 模式 或 以服务器身份监听 的环境下测试------纯客户端进程无法正确应用 GE):

① 创建一个"扣 200 点血"的 Gameplay Effect:

在内容浏览器中右键 → 选择 GameplayEffect 蓝图类,命名为如 BP_GE_Damage200。打开后进行如下配置:

  • Duration Policy = Instant
  • Modifiers 添加一项:
    • Attribute = Health(从 BasicAttributeSet 中选取)
    • Modifier Op = Additive
    • Magnitude = -200

② 临时测试代码(测试完成后可删除):

在 NexusCharacterBase.h 中添加一个可编辑的 GE 引用:

cpp 复制代码
UPROPERTY(EditAnywhere, Category = "Debug")
TSubclassOf<UGameplayEffect> TestDamageEffect; // 在编辑器中指定为 BP_GE_Damage200

在 BeginPlay 函数中加入以下逻辑:

cpp 复制代码
void ANexusCharacterBase::BeginPlay()
{
    Super::BeginPlay();

    if (TestDamageEffect && AbilitySystemComponent)
    {
        // 将该 GE(等级 1)应用到自身;瞬时效果会立即同步执行完毕
        AbilitySystemComponent->ApplyGameplayEffectToSelf(TestDamageEffect, 1.0f,
            AbilitySystemComponent->MakeEffectContext());
    }

    if (BasicAttributeSet)
    {
        // 预期输出:Health = 0.0(已被钳制),而非 -100
        UE_LOG(LogTemp, Warning, TEXT("[ClampTest] Health = %.1f"), BasicAttributeSet->GetHealth());
    }
}

③ 验证上限钳制效果:

另创建一个 Magnitude = +200 的治疗型 GE,重复上述测试流程。

预期结果: Health 仍为 100(受 MaxHealth 钳制),而非 300。

这表明属性值已成功被上限和下限约束,钳制机制正常工作。


6. 参考资料

  • tranek/GASDocumentation (GitHub)------ 详述钳制回调的分工逻辑(PreAttributeChange 管理 CurrentValue,PostGameplayEffectExecute 管理 BaseValue)、InitAbilityActorInfo 的双时机使用场景、三种复制模式的差异,以及 Mixed 与 Controller 之间的关系。
  • Epic 官方教程《Gameplay Ability System - Best Practices for Setup》 (dev.epicgames.com)------ 确认 ATTRIBUTE_ACCESSORS_BASIC 从 UE 5.6 起已内置于 AttributeSet.h;提供 PostGameplayEffectExecute 中回写与钳制的官方示例,其设计思路与 Lyra 项目中的 ULyraHealthSet 一致。
  • Epic 官方文档《Gameplay Attributes and Attribute Sets》 (dev.epicgames.com)------ 官方定义属性的双值结构(BaseValue 与 CurrentValue)及属性集的可扩展设计点。
相关推荐
上班摸鱼下班游戏2 小时前
Unity 仿生智能算法 从蚁群算法 开始
算法·unity·游戏引擎
破芽2 小时前
learn-claude-code-s05-s11-学习笔记
笔记·学习
lunar_coder2 小时前
【C++】如何解决智能指针的析构函数,如何实现shared_ptr的引用计数以及解决shared_ptr的循环引用
c++·1024程序员节
白白白小纯3 小时前
C++进阶.多态
c++
程序猿乐锅4 小时前
【从0-1学习Python | 第七篇】Python 面向对象基础
开发语言·笔记·后端·python·学习·1024程序员节
fish_zzzz4 小时前
笔记--浅谈内存,存储空间,BIOS芯片,显卡GPU、VRAM(1)
笔记·1024程序员节
凯瑟琳.奥古斯特4 小时前
力扣1016高效解法:剪枝优化技巧
开发语言·c++·算法·leetcode·职场和发展
ACP广源盛139246256734 小时前
PCIe Gen3 交换芯片选型笔记:多路扩展方案 IX7024@ACP 评估
笔记·硬件架构·pcie·国产芯片
深圳老胡4 小时前
STM32 开发中 Trae 内置模型怎么选:Seed、MiniMax、GLM、DeepSeek、Kimi、Qwen 全对比
笔记·stm32·单片机·嵌入式硬件·代码规范·1024程序员节