虚幻引擎的标签系统

概念:

虚幻引擎的标签系统(Tag System)是一种用于标识和分类Actor、Component以及其他对象的灵活机制。标签是简单的字符串,可以附加在对象上,然后通过标签来查询和识别对象。标签系统在游戏逻辑中非常有用,比如触发事件、分类对象、过滤查询等。

前置知识(选择性看):

关于FName的问题:

标签系统主要分为三种:

  1. Actor标签(Actor Tags)

    直接附加在Actor上的标签, 每个Actor都可以有多个标签.

    AMyActor::AMyActor()
    {
        // 在构造函数中添加标签
        Tags.Add("Enemy");
        Tags.Add("Boss");
        Tags.Add("FireDamage");
    }
    
    // 或者在运行时添加
    MyActor->Tags.Add("RecentlySpawned");

  2. 组件标签(Component Tags)

    附加在组件上的标签, 每个组件可以拥有多个标签.

    UCapsuleComponent* Capsule = CreateDefaultSubobject<UCapsuleComponent>("Collision");
    Capsule->ComponentTags.Add("PlayerCollision");
    Capsule->ComponentTags.Add("Trigger");
  3. 游戏标签系统(Gameplay Tags)

    特别牛逼的更高级的标签系统,支持层级结构!

    想要使用这个Gameplay Tags:1.必须要先打开此插件 2.在cpp文件中包含头文件 3.在 YourProject/Config/目录下的DefaultGameplayTags.ini文件中定义所有的要使用的标签 4.未定义的标签传入RequestGameplayTag()函数返回的FGameplayTag是无效的.

    层级结构+如何启动插件:

    完整的 DefaultGameplayTags.ini 文件示例:

    [/Script/GameplayTags.GameplayTagsSettings]
    ConfigFileName=GameplayTagsList.ini
    ImportTagsFromConfig=True
    WarnOnInvalidTags=True
    FastReplication=True
    
    [GameplayTags]
    +GameplayTagList=(Tag="Character")
    +GameplayTagList=(Tag="Character.State")
    +GameplayTagList=(Tag="Character.State.Alive")
    +GameplayTagList=(Tag="Character.State.Dead")
    +GameplayTagList=(Tag="Character.State.Stunned")
    +GameplayTagList=(Tag="Character.Type")
    +GameplayTagList=(Tag="Character.Type.Player")
    +GameplayTagList=(Tag="Character.Type.Enemy")
    +GameplayTagList=(Tag="Character.Type.Enemy.Boss")
    +GameplayTagList=(Tag="Character.Type.Enemy.Minion")
    +GameplayTagList=(Tag="Character.Type.NPC")
    
    ; 可以继续添加更多标签
    +GameplayTagList=(Tag="Ability")
    +GameplayTagList=(Tag="Ability.Attack")
    +GameplayTagList=(Tag="Ability.Attack.Melee")
    +GameplayTagList=(Tag="Ability.Attack.Ranged")

    相关的接口如何使用:

    // 需要启用GameplayTags插件和包含头文件
    #include "GameplayTagContainer.h"
    
    // 添加Gameplay Tag
    FGameplayTag Tag = FGameplayTag::RequestGameplayTag("Character.State.Dead");
    MyGameplayTagContainer.AddTag(Tag);
    
    // 检查Gameplay Tag
    if (MyGameplayTagContainer.HasTag(Tag))
    {
        // 拥有该标签
    }
    
    // 检查父标签:例如,检查是否有任何以"Character.State"开头的标签
    FGameplayTag ParentTag = FGameplayTag::RequestGameplayTag("Character.State");
    if (MyGameplayTagContainer.HasTag(ParentTag))
    {
        // 拥有父标签或其下的任何子标签
    }

    网络同步功能:

    (目前我正在研究中)

  4. 三种标签的对比总结:

    特性角色标签组件标签游戏标签系统Gameplay Tags
    存储位置Actor的Tags数组Component的ComponentTags数组通常存储在FGameplayTagContainer中,可以附加到任何支持的对象上(如Actor、Component)
    层次结构无,平坦的字符串列表无,平坦的字符串列表有,树状结构
    网络同步需要手动同步需要手动同步内置网络同步
    性能简单的字符串比较简单的字符串比较基于树的比较,效率高
    灵活性较低,只能精确匹配较低,只能精确匹配高,支持父标签查询
    使用场景标识Actor的整体属性标识Component的属性复杂的游戏状态、能力、效果管理

标签系统的主要特点:

  • 标签是字符串,可以任意命名,但通常使用点分隔的命名方式(例如:Player.Main)来组织层次结构。

  • 一个对象可以拥有多个标签。

  • 标签可以动态添加和移除。

  • 可以通过标签来快速查找和过滤对象。

  • Actor 的 Tags:定义为 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Actor", AdvancedDisplay) TArray<FName> Tags;

  • Component 的 Tags:继承自 UActorComponent,同样是 TArray<FName> Tags 属性。

标签系统的常见操作(Actor和组件):

1.添加标签

       方法1:  在代码中,可以使用Add方法。

       方法2:  在编辑器中,可以在Actor或组件的细节面板中的“Tags”部分添加标签。

Tags.Add("MyTag");
2.移除标签:

        方法1: 在代码中,使用Tags.Remove()方法,建议先检测是否拥有再删除;

        方法2: 直接把添加标签的代码删掉

        方法3: 在编辑器中,可以在Actor或组件的细节面板中的“Tags”部分删除标签。

// 从Actor移除标签
void AMyActor::RemoveTagFromActor(FName TagName)
{
    if (Tags.Contains(TagName))
    {
        Tags.Remove(TagName);
        UE_LOG(LogTemp, Warning, TEXT("Removed tag '%s' from actor '%s'"), 
               *TagName.ToString(), *GetName());
    }
}
3.检查标签

        方法1(组件和Actor通用): 使用Tags.Contain()方法检查对象是否拥有某个标签。

        方法2(Actor专用): 使用ActorHasTag()函数检查Actor是否拥有某个标签。

        方法3(组件专用): 使用ComponentHasTag()函数检查组件是否拥有某个标签。


/*ActorHasTag() 是 UActor类提供的成员函数,
其底层实现直接调用了 Tags.Contains()*/
bool AActor::ActorHasTag(FName TagName) const
{
    return Tags.Contains(TagName);// 仅一行代码,直接"转发调用"
}

// 也支持FString参数的重载(自动转换为FName)
bool AActor::ActorHasTag(const FString& TagName) const
{
    return Tags.Contains(FName(TagName));
}

// ComponentHasTag() 的源码本质就是直接调用 Tags.Contains()
bool UActorComponent::ComponentHasTag(FName TagName) const
{
    return Tags.Contains(TagName); 
}

//方法1:
if (Actor1->Tags.Contains("ActorTag1"))
{// 执行相关逻辑}
if (Component1->Tags.Contains("ComponentTag1"))
{// 执行相关逻辑}

//方法2:
if (Actor2->ActorHasTag("ActorTag2"))
{// 执行相关逻辑}

//方法3:
if (Component2->ComponentHasTag("ActorTag2"))
{// 执行相关逻辑}

/*题外话:这种简单的 “转发函数” 会被编译器自动优化为内联函数(Inline Function),
编译器会直接将 ComponentHasTag() 的代码替换为 Tags.Contains() 的逻辑,
消除函数调用的开销(比如栈帧创建 / 销毁)。最终生成的机器码完全一致。*/
4.查找带有特定标签的Actor

使用UGameplayStatics::GetAllActorsWithTag函数可以获取世界中所有拥有指定标签的Actor。

注意: FoundActors 是一个虚幻引擎的动态数组(TArray,专门用于存储指向拥有指定标签的 Actor 实例的指针,是代码中承接 UGameplayStatics::GetAllActorsWithTag 函数查询结果的 “容器”,承接过程是自动的,FoundActors不需要程序员显式接受GetAllActorsWithTag的返回值,而且这函数返回的是void.

TArray<AActor*> ActorsWithTag;
UGameplayStatics::GetAllActorsWithTag(GetWorld(), "MyTag", ActorsWithTag);

for (AActor* Enemy : FoundActors)
{
    // 处理所有敌人
}

/*此函数的签名:
static void GetAllActorsWithTag(UWorld* World, FName Tag, 
TArray<AActor*>& OutActors);
*/
5.在蓝图中使用标签

蓝图中有专门的节点用于处理标签,例如“Actor Has Tag”、“Get All Actors With Tag”等。

(本人目前主用C++,等我学会这个再来填坑)

常见的应用场景:

1. 对象分类和过滤

// 区分不同类型的敌人
Enemy->Tags.Add("Enemy.Type.Melee");
// 或者
Enemy->Tags.Add("Enemy.Type.Ranged");

// 战斗中过滤目标
if (Target->ActorHasTag("Enemy.Type.Melee"))
{
    // 使用远程攻击对付近战敌人
    UseRangedAttack();
}

2. 交互系统

// 函数定义:检查玩家周围的可交互物体
void APlayerCharacter::CheckInteractable()
{
    // 创建一个空的Actor数组,用于存储找到的可交互物体
    TArray<AActor*> InteractableActors;
    
    // 使用UGameplayStatics的全局函数查找世界中所有带有"Interactable"标签的Actor
    // 参数1: GetWorld() - 获取当前游戏世界上下文
    // 参数2: "Interactable" - 要查找的标签名称
    // 参数3: InteractableActors - 输出参数,存储找到的所有符合条件的Actor
    UGameplayStatics::GetAllActorsWithTag(GetWorld(), "Interactable", InteractableActors);
    
    // 开始遍历找到的所有可交互Actor
    // InteractableActors是一个数组,这里使用范围for循环逐个处理
    for (AActor* Interactable : InteractableActors)
    {
        // 对每个可交互Actor,检查玩家是否在其交互范围内
        // IsInRange是一个自定义函数,应该返回bool值
        // 参数: Interactable - 当前正在检查的可交互Actor
        if (IsInRange(Interactable))
        {
            // 如果玩家在交互范围内,显示交互提示
            // ShowInteractionPrompt是另一个自定义函数
            // 参数: Interactable - 需要显示提示的可交互Actor
            ShowInteractionPrompt(Interactable);
        }
    }
}

3. 伤害系统

// 函数:对目标应用伤害,考虑目标的伤害免疫和脆弱性
// 参数:Target - 要受到伤害的目标Actor
void AWeapon::ApplyDamage(AActor* Target)
{
    // 检查目标是否对当前伤害类型免疫,并作出反馈
    // 指针是否有效&&如果目标有"Immune.Fire"标签 && 伤害类型是火焰,则免疫伤害
    if (Target && Target->ActorHasTag("Immune.Fire") && DamageType == EDamageType::Fire)
    {
        // 目标免疫这种伤害类型,直接返回,不应用任何伤害
        return; // 目标免疫火焰伤害
    }
    
    // 检查目标是否对当前伤害类型脆弱(受到额外伤害)
    // 如果目标有"Vulnerable.Lightning"标签且伤害类型是闪电
    if (Target->ActorHasTag("Vulnerable.Lightning") && DamageType == EDamageType::Lightning)
    {
        // 目标对闪电伤害脆弱,将基础伤害值翻倍
        Damage *= 2.0f; // 对闪电伤害脆弱,伤害加倍
    }
    
    // 调用实际应用伤害的函数,传递目标Actor和计算后的伤害值
    ApplyDamageToTarget(Target, Damage);
}

4. AI行为

// 函数:敌人AI选择攻击目标
// 功能:从所有玩家中选择一个优先攻击的目标
void AEnemyAI::ChooseTarget()
{
    // 创建一个空的Actor数组,用于存储找到的所有玩家
    TArray<AActor*> PlayerActors;
    
    // 使用全局函数查找世界中所有带有"Player"标签的Actor
    // 这假设所有玩家角色都被标记了"Player"标签
    UGameplayStatics::GetAllActorsWithTag(GetWorld(), "Player", PlayerActors);
    
    // 遍历找到的所有玩家Actor,寻找优先攻击目标
    // 使用范围for循环逐个检查每个玩家
    for (AActor* Player : PlayerActors)
    {
        // 检查当前玩家是否带有"LowHealth"标签(低血量状态)
        // 这是AI的优先级逻辑:优先攻击血量低的玩家
        if (Player->ActorHasTag("LowHealth"))
        {
            // 找到低血量玩家,立即将其设置为当前攻击目标
            SetCurrentTarget(Player);
            
            // 找到目标后立即返回,不再检查其他玩家
            // 这实现了"优先攻击低血量玩家"的AI行为
            return;
        }
    }
    
    // 注意:如果循环结束都没有找到低血量玩家,函数会自然结束
    // 这意味着没有设置任何目标,这可能是个问题
}

项目例子:

在虚幻引擎的角色蓝图的细节面板的Actor组件里面就可修改标签,如果我需要创建一个标签用来管理所有的能够触发压力板的物体.可以加入一个PressurePlateActivator标签.

在蓝图面板修改后记得点左上角的编译.

这样就成功为一个角色蓝图添加上了一个标签:PressurePlateActivator

那么现在如何在程序中访问这个标签,检测这个标签呢?

有一个函数专门用于检测角色是否拥有某个标签,返回值是bool类型的:

bool ActorHasTag("PressurePlateActivator");

/*第一个绑定函数:此函数通过修改门的Mover组件的ShouldMove变量, 控制门的移动,
函数名可自定义,参数必须严格符合OnComponentBeginOverlap委托的定义*/
void UTriggerComponent::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, 
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor && OtherActor->ActorHasTag("PressurePlateActivator")) {
		if (Mover) {
			Mover->ShouldMove = true;
		}
	}
	
}
//第二个绑定函数:此函数同理,函数名可自定义,参数必须严格符合OnComponentEndOverlap委托的定义
void UTriggerComponent::OnOverlapEnd(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OherBodyIndex)
{
	if (OtherActor && OtherActor->ActorHasTag("PressurePlateActivator")) {
		if (Mover) {
			Mover->ShouldMove = false;
		}
	}
}

这样一个压力板就只能由角色触发了,如果还想要别的角色也能触发这个压力板(比如一个椅子),只需在椅子的细节面板给椅子加上一个Actor标签"PressurePlateActivator",注意不是组件标签,别搞错了.

然后还要开启椅子的生成重叠事件,不然椅子无法触发OnComponentBeginOverlap委托和OnComponentEndOverlap委托,也就无法传达椅子和压力板的碰撞行为.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值