六、RPGTargetType.h/cpp & Targeting

Targeting

技能系统中很重要的一环就是目标的选取,比如LOL中的各色技能。


GameplayAbilities and You 中,将Targeting放在了Advanced话题中。我主要参考这篇文章来讲GAS中的Targeting。
文中以Wait Target Data这个AbilityTask为例。这个task在GAS中有举足轻重的地位,原因如:

  • 提供系统用以可视化技能目标选择
  • 提供玩家发送信息到服务器的框架(Client to Server)


Wait Target Data通常放在Commit Ability之前,玩家在确认释放技能前可以看到技能指示器(参考LOL)。

Class必须是GameplayAbilityTargetActor的子类。共有4种Confirmation type,后两种是Custom,默认提供的是InstantUser ConfirmedUser Confirmed相较于Instant来说,需要额外调用UAbilitySystemComponent::TargetConfirm()或将comfirm绑定在输入上,取消也是类似的,需要调用UAbilitySystemComponent::TargetCancel(),或将cancel绑定在输入上。最后记得不要忘记调用EndAbility

需要注意,task右侧的引脚在客户端和服务端都有效,Valid Data Delegate都会调用。后端关于它的实现采取一种比较有趣的方式: task会生成actor(GameplayAbilityTargetActor)实例,而这些实例不是replicated。(应该是客户端和服务端都生成actor)实际上,服务端通过AbilitySystemComponent将数据发送给客户端。这种方案导致targeting actor的设置有些奇特。(暂时我还没get到)


Target Actor

/**
 * TargetActors are spawned to assist with ability targeting. They are spawned by ability tasks and create/determine the outgoing targeting data passed from one task to another
 *
 * WARNING: These actors are spawned once per ability activation and in their default form are not very efficient
 * For most games you will need to subclass and heavily modify this actor, or you will want to implement similar functions in a game-specific actor or blueprint to avoid actor spawn costs
 * This class is not well tested by internal games, but it is a useful class to look at to learn how target replication occurs
 */

我们通过继承AGameplayAbilityTargetActor创建自定义的TargetActor。其中,有两个主要的函数需要我们重写:

  • virtual void StartTargeting(UGameplayAbility* Ability) override
  • virtual void ConfirmTargetingAndContinue() override

StartTarget

在这里,你可以访问Ability实例,因此你可以使用Ability类里的数据。比如:假如你有一个造墙的技能,那么在目标选择时,需要从Ability实例中得到墙的mesh数据,这样TargetActor才能正确显示墙体指示器。Ability实例中还包含释放该技能的Character引用,因此TargetActor还能访问到Character的信息(人物的tag和attribute可能影响targeting)。

ConfirmTargetingAndContinue

这里有些较难理解的地方,但是我们化繁为简,这个函数最核心的功能是调用TargetDataReadyDelegate,同时传递携带包含我们target data的负载。因此,如果我们想传递两个transforms,分别包含技能施放者位置和目标物体位置,那么代码如下:

// 继承自FGameplayAbilityTargetData
FGameplayAbilityTargetData_LocationInfo *ReturnData = new FGameplayAbilityTargetData_LocationInfo();

// Source Transform
ReturnData->SourceLocation.LocationType = EGameplayAbilityTargetingLocationType::LiteralTransform;
ReturnData->SourceLocation.LiteralTransform = FTransform(SourceLocation);
// Destination Transform
ReturnData->TargetLocation.LocationType = EGameplayAbilityTargetingLocationType::LiteralTransform;
ReturnData->TargetLocation.LiteralTransform = FTransform((TargetLocation - SourceLocation).ToOrientationQuat(), TargetLocation);

// Handle !! 这就是我们为什么new了却不要delete的原因,里面用了智能指针
FGameplayAbilityTargetDataHandle Handle(ReturnData);
// Fire delegate with data handle !!!
TargetDataReadyDelegate.Broadcast(Handle);

关键的数据结构是FGameplayAbilityTargetData(你的TargetingSystem运行后最终的结果数据),引擎里已经提供了一系列的子类用于不同的需求:

  • FGameplayAbilityTargetData_LocationInfo :包含最多两个位置信息数据
  • FGameplayAbilityTargetData_ActorArray : 包含一个Actor数组
  • FGameplayAbilityTargetData_SingleTargetHit : 包含一个碰撞结果(HitResult)

特别注意: 这个方法是将数据从Client端发往Server端。虽然Server端也有一个版本的数值,但它并不准确。因为是从Client端发往Server端,所以数据有被用户篡改的风险,记得要添加验证。

接下来我们自定义一个FGameplayAbilityTargetData类,它包含两个location,一个float,一个int。这个数据类依旧是用于造墙技能,只是这次,我们用鼠标按下的时间(float)来决定墙的高度。代码如下:

USTRUCT(BlueprintType)
struct FGameplayAbilityCastingTargetingLocationInfo: public FGameplayAbilityTargetData
{
    GENERATED_USTRUCT_BODY()

        /**
          如果没记错,属性需要标记UPROPERTY(),才能被序列化
        */

    /** Amount of time the ability has been charged */
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
    float ChargeTime;

    /** The ID of the Ability that is performing targeting */
    UPROPERTY()
    uint32 UniqueID;

    /** Generic location data for source */
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
    FGameplayAbilityTargetingLocationInfo SourceLocation;

    /** Generic location data for target */
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
    FGameplayAbilityTargetingLocationInfo TargetLocation;

    // -------------------------------------

    virtual bool HasOrigin() const override
    {
        return true;
    }

    virtual FTransform GetOrigin() const override
    {
        return SourceLocation.GetTargetingTransform();
    }

    // -------------------------------------

    virtual bool HasEndPoint() const override
    {
        return true;
    }

    virtual FVector GetEndPoint() const override
    {
        return TargetLocation.GetTargetingTransform().GetLocation();
    }

    virtual FTransform GetEndPointTransform() const override
    {
        return TargetLocation.GetTargetingTransform();
    }

    // -------------------------------------

    virtual UScriptStruct* GetScriptStruct() const override
    {
        return FGameplayAbilityCastingTargetingLocationInfo::StaticStruct();
    }

    virtual FString ToString() const override
    {
        return TEXT("FGameplayAbilityCastingTargetingLocationInfo");
    }

    bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
};

/**  这一部分具体的运作机制不明,但必须要有 */
template<>
struct TStructOpsTypeTraits<FGameplayAbilityCastingTargetingLocationInfo> : public TStructOpsTypeTraitsBase2<FGameplayAbilityCastingTargetingLocationInfo>
{
    enum
    {
        WithNetSerializer = true    // For now this is REQUIRED for FGameplayAbilityTargetDataHandle net serialization to work
    };
};

NetSerialize的实现:

bool FGameplayAbilityCastingTargetingLocationInfo::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
    SourceLocation.NetSerialize(Ar, Map, bOutSuccess);
    TargetLocation.NetSerialize(Ar, Map, bOutSuccess);

    Ar << ChargeTime;
    Ar << UniqueID;

    bOutSuccess = true;
    return true;
}

最后,正如官方头文件所述,TargetActor默认是每执行一次Ability都会创建,然后销毁。为了提高性能,要考虑重用TargetActor。当然,你甚至可以不使用TargetActor,ActionRPG中就是这样。对于造墙这种技能,墙体指示器可能要跟随玩家的移动,所以你需要保存在StartTarget中获得Character的指针或引用,然后在TargetActor的Tick函数中获取Character的位置信息来更新墙体指示器的位置。


RPGTargetType.h/cpp

我们终于可以回到ActionRPG项目上来了。很不幸的是,前面说了一大段的知识点。这个项目并没有用到,而是另辟蹊径。

头文件:

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "ActionRPG.h"
#include "Abilities/GameplayAbilityTypes.h"
#include "Abilities/RPGAbilityTypes.h"
#include "RPGTargetType.generated.h"

class ARPGCharacterBase;
class AActor;
struct FGameplayEventData;

/**
 * Class that is used to determine targeting for abilities
 * It is meant to be blueprinted to run target logic
 * This does not subclass GameplayAbilityTargetActor because this class is never instanced into the world
 * This can be used as a basis for a game-specific targeting blueprint
 * If your targeting is more complicated you may need to instance into the world once or as a pooled actor
 */
UCLASS(Blueprintable, meta = (ShowWorldContextPin))
class ACTIONRPG_API URPGTargetType : public UObject
{
    GENERATED_BODY()

public:
    // Constructor and overrides
    URPGTargetType() {}

    /** Called to determine targets to apply gameplay effects to */
    UFUNCTION(BlueprintNativeEvent)
    void GetTargets(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const;
};

/** Trivial target type that uses the owner */
UCLASS(NotBlueprintable)
class ACTIONRPG_API URPGTargetType_UseOwner : public URPGTargetType
{
    GENERATED_BODY()

public:
    // Constructor and overrides
    URPGTargetType_UseOwner() {}

    /** Uses the passed in event data */
    virtual void GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const override;
};

/** Trivial target type that pulls the target out of the event data */
UCLASS(NotBlueprintable)
class ACTIONRPG_API URPGTargetType_UseEventData : public URPGTargetType
{
    GENERATED_BODY()

public:
    // Constructor and overrides
    URPGTargetType_UseEventData() {}

    /** Uses the passed in event data */
    virtual void GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const override;
};

源文件:

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

#include "Abilities/RPGTargetType.h"
#include "Abilities/RPGGameplayAbility.h"
#include "RPGCharacterBase.h"

void URPGTargetType::GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const
{
    return;
}

void URPGTargetType_UseOwner::GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const
{
    OutActors.Add(TargetingCharacter);
}

void URPGTargetType_UseEventData::GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const
{
    const FHitResult* FoundHitResult = EventData.ContextHandle.GetHitResult();
    if (FoundHitResult)
    {
        OutHitResults.Add(*FoundHitResult);
    }
    else if (EventData.Target)
    {
        OutActors.Add(const_cast<AActor*>(EventData.Target));
    }
}

不同于TargetActor,这里定义的TargetType(继承自UObject)不会被也不能实例化到World中(Actor才能被放置到World中)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容