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,默认提供的是Instant和User Confirmed。User 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中)。