前言 这是个人使用GameplayCueTranslator 的经验总结。
如果不会使用GameplayCue 的话,建议先学习对应内容。这里假定已经基本了解了GAS 系统的相关能力。
我们都知道,可以通过GameplayCue以制作各种表现。但是试想一下,攻击到一个怪物身上,做一个流血是一个很正常的事情,但是同样的攻击如果打到树木上,似乎流血就不太合适了。
但是GameplayCue 又是通过GE 上配置的Tag来的,难道要根据Actor类型在GA中指定不同的GE 以实现不同的GameplayCue ?还是GameplayCue 中再检测对应的Actor实现不同表现?
GameplayCueTranslator 就是UE中的解决方案。它提供了一个对于指定Actor 类型,将CueTag进行转换的功能。
这就是UE中GameplayCueTranslator 的使用情景,虽然简单,但是这个功能在网络上缺乏相应教程。因此写下这篇文章。
GameplayCueTranslator 实际上在源码中写下了整个系统使用的说明。这里在此附上ChatGpt 翻译的版本。
虽然源码写清楚注释很好。但是这种功能真的不应该专门出个教程和文档么……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
实际操作 GameplayTag准备
首先准备GameplayCue的Tag,这里简单分出Normal和Dark、Blaze两种类型。
随后再准备一些用于标注Actor的Tag。这只是为了使用GameplayTag用于区分Actor,可以使用其他方式来判断Tag。
创建GameplayCueTranslator子类
父类选择GameplayCueTranslator类即可。
创建完类后,重载如下两个函数 GetTranslationNameSpawns
和 GameplayCueToTranslationIndex
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include "CoreMinimal.h" #include "GameplayCueTranslator.h" #include "DamageTypeGameplayCueTranslator.generated.h" UCLASS ()class GASLEARN_API UDamageTypeGameplayCueTranslator : public UGameplayCueTranslator{ GENERATED_BODY () public : virtual void GetTranslationNameSpawns (TArray<FGameplayCueTranslationNameSwap>& SwapList) const override ; virtual int32 GameplayCueToTranslationIndex (const FName& TagName, AActor* TargetActor, const FGameplayCueParameters& Parameters) const override ; };
声明可能存在的转换关系 在Translator类中,需要先声明可能发生的转换关系。声明相关代码如图所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void UDamageTypeGameplayCueTranslator::GetTranslationNameSpawns (TArray<FGameplayCueTranslationNameSwap>& SwapList) const { { FGameplayCueTranslationNameSwap Temp; Temp.FromName = FName (TEXT ("Normal" )); Temp.ToNames.Add ( FName (TEXT ("Blaze" )) ); SwapList.Add (Temp); } { FGameplayCueTranslationNameSwap Temp; Temp.FromName = FName (TEXT ("Normal" )); Temp.ToNames.Add ( FName (TEXT ("Light" )) ); SwapList.Add (Temp); } { FGameplayCueTranslationNameSwap Temp; Temp.FromName = FName (TEXT ("Damage.Normal" )); Temp.ToNames.Add ( FName (TEXT ("Damage.Blaze" )) ); SwapList.Add (Temp); } { FGameplayCueTranslationNameSwap Temp; Temp.FromName = FName (TEXT ("Damage.Normal" )); Temp.ToNames.Add ( FName (TEXT ("Damage" ))); Temp.ToNames.Add ( FName (TEXT ("Dark" ))); SwapList.Add (Temp); } }
声明转换关系需要通过FGameplayCueTranslationNameSwap
这个结构体来声明。不需要过于关注具体内容,只需要理解要添加FromName
和ToNames
即可
ToNames
整体相当于一个gameplayTag的部分。它必须是一个存在的gameplayTag部分
一条FGameplayCueTranslationNameSwap
只针对一种Tag转换情况,如果会发生多个的话,需要写多条
这个转换只会检测以GameplayCue 为开头的GameplayTag
GameplayCueTranslator.cpp:197
FGameplayTagContainer AllGameplayCueTags = TagManager->RequestGameplayTagChildren(UGameplayCueSet::BaseGameplayCueTag());
实现转换的具体逻辑 这一步需要实现 GameplayCueToTranslationIndex
函数,用于根据Actor来判断是否需要进行GameplayTag的转换。
这个函数需要返回对应的序号,如果不转换的话,返回INDEX_NONE
即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 int32 UDamageTypeGameplayCueTranslator::GameplayCueToTranslationIndex (const FName& TagName, AActor* TargetActor, const FGameplayCueParameters& Parameters) const { if (!TargetActor) { return INDEX_NONE; } UAbilitySystemComponent* ASC = TargetActor->FindComponentByClass <UAbilitySystemComponent>(); if (!ASC) { return INDEX_NONE; } FGameplayTag BlazeTag = FGameplayTag::RequestGameplayTag (FName ("TranCue.State.Blaze" )); if (ASC->HasMatchingGameplayTag (BlazeTag)) { return 0 ; } FGameplayTag DarkTag = FGameplayTag::RequestGameplayTag (FName ("TranCue.State.Dark" )); if (ASC->HasMatchingGameplayTag (DarkTag)) { return 1 ; } return INDEX_NONE; }
这样子就实现了一个完整的GameplayTranslator逻辑
深入分析 在完成了实际操作后,就相当于了解了GameplayTranslator的使用方式了。
以下的部分,就是对相关代码的进一步分析。对于简单的应用使用来说,已经足够了。鉴于这个设计的完善性,可以说按照直觉来用的话应该都不会出问题。
这里的深入分析主要出于回答以下几个问题:
如何确定哪些UGameplayCueTranslator
在使用?
转换关系是一个树状结构?那么这些节点具体如何记录的消息?
定义多个不一样的 UGameplayCueTranslator
对于相同CueTag的转换会发生什么?如何保证的正确性?
确定可用转换类 FGameplayCueTranslationManager::RefreshNameSwaps 大部分情况下,不需要手动配置有哪些GameplayCueTranslator ,也不必关注他们的启用情况。
在GameplayCueTranslatorManager 中的相关代码。这里会自动遍历所有的子类并进行排序。通过定义的 UGameplayCueTranslator:GetPriority
和 UGameplayCueTranslator:IsEnabled
函数来得到优先级和启用情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void FGameplayCueTranslationManager::RefreshNameSwaps () { AllNameSwaps.Reset (); TArray<UGameplayCueTranslator*> CDOList; for ( TObjectIterator<UClass> It ; It ; ++It ) { UClass* Class = *It; if (Class->IsChildOf (UGameplayCueTranslator::StaticClass ())) { UGameplayCueTranslator* CDO = Class->GetDefaultObject <UGameplayCueTranslator>(); if (CDO->IsEnabled ()) { CDOList.Add (CDO); } } } CDOList.Sort ([](const UGameplayCueTranslator& A, const UGameplayCueTranslator& B) { return (A.GetPriority () > B.GetPriority ()); }); }
转换节点关系 FGameplayCueTranslationManager.TranslationLUT 这个数据结构记录了所有参与了转换的GameplayCueTag节点,每个节点对应着一个GameplayCueTag, 节点为一个FGameplayCueTranslatorNode 的结构体。
简单用断点就可以看出来具体的转换信息存储的方式。
TranslatorNode 对应着每一个参加转换的GameplayCueTag
TranslatorNode 记录下的Links 为FGameplayCueTranslationLink 结构体, 实际是代表着所以会对这个CueTag 进行转换的Translator
每一条Link 通过NodeLookup 将自己在Swaplist 中的顺序和TranslationLUT 中的序号关联起来,以达成和和GueTag 的关联
Link 是按照Translator 进行区分的,在不同Link 中会存在相同的Translator
在得到上述结论之后,就可以更清晰地了解具体转换时的发生逻辑了,总结及时如下图。
转换关系生成 GameplayCueTranslator:BuildTagTranslationTable_r 这里是转换关系的具体生成逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 for (int32 TagIdx=0 ; TagIdx < SplitNames.Num (); ++TagIdx){ for (int32 ToNameIdx=0 ; ToNameIdx < SwapRule.ToNames.Num () && TagIdx < SplitNames.Num (); ++ToNameIdx) { if (SwapRule.ToNames[ToNameIdx] == SplitNames[TagIdx]) { if (ToNameIdx == SwapRule.ToNames.Num ()-1 ) { SwappedNames = SplitNames; int32 NumRemoves = SwapRule.ToNames.Num (); int32 RemoveAtIdx = TagIdx - (SwapRule.ToNames.Num () - 1 ); check (SwappedNames.IsValidIndex (RemoveAtIdx)); SwappedNames.RemoveAt (RemoveAtIdx, NumRemoves, false ); SwappedNames.Insert (SwapRule.FromName, RemoveAtIdx); FString ComposedString = SwappedNames[0 ].ToString (); for (int32 ComposeIdx=1 ; ComposeIdx < SwappedNames.Num (); ++ ComposeIdx) { ComposedString += FString::Printf (TEXT (".%s" ), *SwappedNames[ComposeIdx].ToString ()); } FName ComposedName = FName (*ComposedString); { FGameplayTag ComposedTag = TagManager->RequestGameplayTag (ComposedName, false ); if (ComposedTag.IsValid () == false ) { UE_LOG (LogGameplayCueTranslator, Log, TEXT (" No tag match found, recursing..." )); FGameplayCueTranslatorNodeIndex ParentIdx = GetTranslationIndexForName ( ComposedName, false ); if (ParentIdx.IsValid () == false ) { ParentIdx = GetTranslationIndexForName ( ComposedName, true ); check (ParentIdx.IsValid ()); TranslationLUT[ParentIdx].UsedTranslators.Add ( NameSwapData.ClassCDO ); HasValidRootTag |= BuildTagTranslationTable_r (ComposedName, SwappedNames); } } else { HasValidRootTag = true ; } } if (HasValidRootTag) { FGameplayCueTranslatorNodeIndex ParentIdx = GetTranslationIndexForName (ComposedName, true ); FGameplayCueTranslatorNodeIndex ChildIdx = GetTranslationIndexForName (TagName, true ); FGameplayCueTranslatorNode& ParentNode = TranslationLUT[ParentIdx]; FGameplayCueTranslationLink& NewLink = ParentNode.FindOrCreateLink (NameSwapData.ClassCDO, NameSwapData.NameSwaps.Num ()); NewLink.NodeLookup[SwapRuleIdx] = ChildIdx; FGameplayCueTranslatorNode& ChildNode = TranslationLUT[ChildIdx]; ChildNode.UsedTranslators.Append ( ParentNode.UsedTranslators ); ChildNode.UsedTranslators.Add ( NameSwapData.ClassCDO ); } else { UE_LOG (LogGameplayCueTranslator, Log, TEXT (" No tag match found after recursing. Dead end." )); } break ; } else { TagIdx++; continue ; } } else { break ; } } }
转换过程 FGameplayCueTranslationManager::TranslateTag_Internal 这个函数就是转换的具体逻辑了,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 bool FGameplayCueTranslationManager::TranslateTag_Internal (FGameplayCueTranslatorNode& Node, FGameplayTag& OutTag, const FName& TagName, AActor* TargetActor, const FGameplayCueParameters& Parameters) { for (FGameplayCueTranslationLink& Link : Node.Links) { int32 TranslationIndex = Link.RulesCDO->GameplayCueToTranslationIndex (TagName, TargetActor, Parameters); if (TranslationIndex != INDEX_NONE) { if (Link.NodeLookup.IsValidIndex (TranslationIndex) == false ) { continue ; } FGameplayCueTranslatorNodeIndex NodeIndex = Link.NodeLookup[TranslationIndex]; if (NodeIndex.IsValid ()) { if (TranslationLUT.IsValidIndex (NodeIndex) == false ) { continue ; } FGameplayCueTranslatorNode& InnerNode = TranslationLUT[NodeIndex]; OutTag = InnerNode.CachedGameplayTag; TranslateTag_Internal (InnerNode, OutTag, InnerNode.CachedGameplayTagName, TargetActor, Parameters); return true ; } } } return false ; }
总结 这里基本梳理完了整个GameplayCueTranslator 的使用逻辑和整体设计。
就算不是使用UE 的相关功能,这个转换的结构预生成和设计也具备相当的使用价值。可以在其他方面运用。