Init
This commit is contained in:
350
03-UnrealEngine/Gameplay/AI/Ue4 AI相关功能笔记.md
Normal file
350
03-UnrealEngine/Gameplay/AI/Ue4 AI相关功能笔记.md
Normal file
@@ -0,0 +1,350 @@
|
||||
---
|
||||
title: Ue4 AI相关功能笔记
|
||||
date: 2021-09-07 19:57:53
|
||||
tags: AI
|
||||
rating: ⭐️
|
||||
---
|
||||
## 其他文档
|
||||
UE4场景询问系统浅析(EQS与行为树
|
||||
http://www.uejoy.com/?p=500
|
||||
|
||||
UE4 AIModule源码阅读之AI感知系统
|
||||
https://zhuanlan.zhihu.com/p/356716030
|
||||
|
||||
## AI感知系统(AI Perception System)
|
||||
文档:https://docs.unrealengine.com/4.26/zh-CN/InteractiveExperiences/ArtificialIntelligence/AIPerception/
|
||||
|
||||
给角色类添加AIPerception组件并且添加所需要类型的感知,即可实现感知效果。
|
||||
|
||||
- AI伤害:对Event Any Damage、Event Point Damage或Event Radial Damage作出反应
|
||||
- AI听觉:可用于检测由 报告噪点事件(Report Noise Event) 产生的声音
|
||||
- AI感知报告:在PredictionTime秒内向请求者提供PredictedActor的预计位置。
|
||||
- AI视觉:当一个Actor进入视觉半径后,AI感知系统将发出更新信号,并穿过被看到的Actor(举例而言,一个玩家进入该半径,并被具备视觉感知的AI所察觉)。
|
||||
- AI团队:这会通知感知组件的拥有者同团队中有人处在附近(发送该事件的游戏代码也会发送半径距离)。
|
||||
- AI触觉:配置能够检测到AI与物体发生主动碰撞、,或是与物体发生被动碰撞。。举例而言,在潜入类型的游戏中,您可能希望玩家在不接触敌方AI的情况下偷偷绕过他们。使用此感官可以确定玩家与AI发生接触,并能用不同逻辑做出响应。
|
||||
|
||||
除了可以使用绑定FActorPerceptionUpdatedDelegate委托来实现获取目标Actor外,还可以使用GetHostileActorsBySense()(蓝图版本GetCurrentlyPerceivedActors())
|
||||
|
||||
处理函数ProcessStimuli()是从UAIPerceptionSystem::Tick()调用的,所以只需要修改Tick频率即可。PrimaryActorTick.TickInterval即可。
|
||||
|
||||
UAISense_Prediction一般与UAISense_Sight连用,在UAISense_Sight丢失目标后若干秒(使用计时器),调用RequestControllerPredictionEvent,其中PredictionTime为预测时间长短。之后在处理函数中将事件Location传递给黑板变量。
|
||||
|
||||
### UPawnSensingComponent
|
||||
除了UAIPerceptionComponent还可以使用UPawnSensingComponent,有OnSeePawn与OnHearNoise委托。不过UPawnSensingComponent有个问题即它只能“看到”或者“听到”Pawn。
|
||||
|
||||
案例代码:
|
||||
```
|
||||
ARPGAICharacter::ARPGAICharacter(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer.SetDefaultSubobjectClass<ULCMCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
|
||||
{
|
||||
AIControllerClass = ARPGAIController::StaticClass();
|
||||
bUseControllerRotationYaw = true;
|
||||
|
||||
SensingComponent = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("SensingComponent"));
|
||||
SensingComponent->SetPeripheralVisionAngle(90.f);
|
||||
}
|
||||
|
||||
void ARPGAICharacter::PossessedBy(AController* NewController)
|
||||
{
|
||||
Super::PossessedBy(NewController);
|
||||
|
||||
if (SensingComponent)
|
||||
{
|
||||
SensingComponent->OnSeePawn.AddDynamic(this, &ARPGAICharacter::OnSeeTarget);
|
||||
SensingComponent->OnHearNoise.AddDynamic(this, &ARPGAICharacter::OnHearNoise);
|
||||
}
|
||||
}
|
||||
|
||||
void ARPGAICharacter::OnSeeTarget(APawn* Pawn)
|
||||
{
|
||||
ARPGAIController* AIController = Cast<ARPGAIController>(GetController());
|
||||
//Set the seen target on the blackboard
|
||||
if (AIController)
|
||||
{
|
||||
GLog->Log("Oh hello there");
|
||||
AIController->SetSeenTarget(Pawn);
|
||||
}
|
||||
}
|
||||
|
||||
void ARPGAICharacter::OnHearNoise(APawn* Pawn,const FVector& Location, float Volume)
|
||||
{
|
||||
ARPGAIController* AIController = Cast<ARPGAIController>(GetController());
|
||||
//Set the seen target on the blackboard
|
||||
if (AIController)
|
||||
{
|
||||
GLog->Log(FString::Printf(TEXT("Hear Noise:Vec3(%f,%f,%f) Volume:%f"),Location.X,Location.Y,Location.Z,Volume));
|
||||
AIController->SetHearTarget(Pawn,Location,Volume);
|
||||
}
|
||||
}
|
||||
```
|
||||
控制类中一般是将这些数据添加到黑板中。对应HearNoise则需要在角色中添加UPawnNoiseEmitterComponent
|
||||
,之后调用MakeNoise实现。
|
||||
```
|
||||
PawnNoiseEmitterComponent->MakeNoise(this, 1.f, GetActorLocation());
|
||||
```
|
||||
|
||||
## 刺激源(Stimuli Source)
|
||||
该组件封装了场景中可以感知的内容以及可以感知的内容。它通常添加在AI需要感知的Actor中。
|
||||
|
||||
## 调试方法
|
||||
您可以使用AI调试工具调试AI感知。操作方法是在游戏运行时按下撇号(')键,然后按数字键4调出感知信息。
|
||||
|
||||
## 设计思路
|
||||
对于ARPG游戏来说我们只需AI听觉、AI感知报告、AI视觉与AI团队。
|
||||
AI听觉:让AI确定TargetLocation,让AI去最后听到声音的位置查看。
|
||||
AI视觉:让AI发现TargetActor。
|
||||
AI团队:让AI将自己的TargetActor发送给其他AI。
|
||||
|
||||
## UE4 AI阻挡AI解决策略
|
||||
CharacterMovement->bUseRVOAvoidance = true
|
||||
|
||||
## RAMA的插件
|
||||
### nav修改Volume
|
||||
https://nerivec.github.io/old-ue4-wiki/pages/ai-custom-pathing-how-to-use-nav-modifiers-query-filters.html
|
||||
|
||||
## 随机Task实现(蓝图实现)
|
||||
https://answers.unrealengine.com/questions/658501/random-task-in-ai.html
|
||||
|
||||
使用Service节点随机给黑板随机生成一个数字,在下面连接Tasks上加上一个BlackboardBasedCondition限制节点,来实现。
|
||||
|
||||
## BT节点
|
||||
### 黑板相关
|
||||
可以在构造函数中给FBlackboardKeySelector设置过滤
|
||||
```
|
||||
//筛选Actor类型
|
||||
BlackboardKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UBTService_DefaultFocus, BlackboardKey), AActor::StaticClass());
|
||||
//筛选Vector类型
|
||||
BlackboardKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UBTService_DefaultFocus, BlackboardKey));
|
||||
MyVectorKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyVectorKey));
|
||||
|
||||
MyObjectKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyObjectKey), AActor::StaticClass());
|
||||
|
||||
MySpecialActorClassKey.AddClassFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MySpecialActorClassKey), AMySpecialActor::StaticClass());
|
||||
|
||||
MyEnumKey.AddEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), StaticEnum<EMyEnum>());
|
||||
|
||||
MyEnumKey.AddNativeEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), "MyEnum");
|
||||
|
||||
MyIntKey.AddIntFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyIntKey));
|
||||
|
||||
MyFloatKey.AddFloatFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyFloatKey));
|
||||
|
||||
MyBoolKey.AddBoolFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyBoolKey));
|
||||
|
||||
MyRotatorKey.AddRotatorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyRotatorKey));
|
||||
|
||||
MyStringKey.AddStringFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyStringKey));
|
||||
|
||||
MyNameKey.AddNameFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyNameKey));
|
||||
```
|
||||
```
|
||||
//通过行为树组件获取到黑板组件
|
||||
UBlackboardComponent* MyBlackboard = OwnerComp.GetBlackboardComponent();
|
||||
//获取黑板键值
|
||||
UObject* KeyValue = MyBlackboard->GetValue<UBlackboardKeyType_Object>(BlackboardKey.GetSelectedKeyID());
|
||||
//设置黑板键值
|
||||
UBlackboardComponent->SetValueAsObject(TargetActor.SelectedKeyName, nullptr);
|
||||
```
|
||||
如果设置了FBlackboardKeySelector变量,还需要在重写InitializeFromAsset(UBehaviorTree& Asset):
|
||||
```
|
||||
void UMyBTTask::InitializeFromAsset(UBehaviorTree& Asset)
|
||||
{
|
||||
Super::InitializeFromAsset(Asset);
|
||||
|
||||
UBlackboardData* BBAsset = GetBlackboardAsset();
|
||||
if (ensure(BBAsset))
|
||||
{
|
||||
MySpecialKey.ResolveSelectedKey(*BBAsset);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实例化
|
||||
Service节点是默认不被实例化的,这代表这个节点会共用内存。实例化Service的两种方法:
|
||||
|
||||
#### 第一种: 使用内存块.
|
||||
优点: 更加节省,不需要实例化节点,小巧方便.
|
||||
缺点: 内存块的大小是固定的,如果涉及到动态分配内存的类型,例如数组,则无法使用此方法.
|
||||
|
||||
在源码BTService.h文件中,可以看到一段二十行的英文注释,关于实例化的描述翻译如下。
|
||||
|
||||
>由于其中一些可以针对特定的AI进行实例化,因此以下虚拟函数未标记为const:
|
||||
OnBecomeRelevant(来自UBTAuxiliaryNode)
|
||||
OnCeaseRelevant(来自UBTAuxiliaryNode)
|
||||
TickNode(来自UBTAuxiliaryNode)
|
||||
OnSearchStart
|
||||
如果未实例化您的节点(默认行为),请不要在这些函数内更改对象的任何属性!
|
||||
模板节点使用相同的树资产在所有行为树组件之间共享,并且必须将其运行时属性存储在提供的NodeMemory块中(分配大小由GetInstanceMemorySize()确定)
|
||||
|
||||
GetInstanceMemorySize()函数的返回值决定了Service节点可以被分配到用来存储自身属性的内存大小。
|
||||
而此函数是在UBTAuxiliaryNode的父类UBTNode中声明的,并且默认返回值为0!
|
||||
|
||||
>UBTNode不光是UBTAuxiliaryNode的父类,也是UBTTaskNode(任务节点)的父类。
|
||||
也就是说,行为树中 Service(服务节点),Decorator(装饰器节点),Task(任务节点)都是默认不被实例化的!
|
||||
至于如何实例化,我们完全可以直接参考源码提供的BTService_RunEQS的写法。
|
||||
|
||||
首先在自定义Service头文件中声明一个结构体,将需要的变量都放结构体里。
|
||||
在类中声明一个此结构体的变量,并重写GetInstanceMemorySize函数返回结构体所需内存大小。在InitializeMemory函数中可以对结构体变量进行初始化。
|
||||
|
||||
#### 第二种: 直接实例化节点.
|
||||
优点: 可以使用任何复杂类型的属性.
|
||||
缺点: 会为每个节点创建一个新对象.
|
||||
|
||||
这种只要在构造函数中设置标志位 bCreateNodeInstance = true; ,你就可以在头文件中声明任意变量使用了.
|
||||
|
||||
### 更改描述及命名
|
||||
重写GetStaticDescription函数。
|
||||
|
||||
NodeName可以从Details面板修改,也可以在类构造函数中直接 NodeName = “ ”
|
||||
但这里的NodeName并不会修改在AddService时列表中显示的名称。
|
||||
列表中的名称是由类名决定的,除了修改类名,也可以用类说明符DisplayName修改。
|
||||
```
|
||||
UClass(DisplayName="xxxx")
|
||||
```
|
||||
### 蓝图节点为啥会是双本版设计
|
||||
|
||||
源码逻辑:
|
||||
当AIOwner为AIController时,优先调用带AI后缀的函数。(若只实现了不带AI后缀的,也会调用成功)
|
||||
当AIOwner不为AIController时,只调用不带AI后缀的函数。
|
||||
```
|
||||
void UBTService_BlueprintBase::SetOwner(AActor* InActorOwner)
|
||||
{
|
||||
ActorOwner = InActorOwner;
|
||||
AIOwner = Cast<AAIController>(InActorOwner);
|
||||
}
|
||||
|
||||
void UBTService_BlueprintBase::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
|
||||
//判断是否Owner为AIController并且有任一版本函数被实现
|
||||
if (AIOwner != nullptr && (ReceiveTickImplementations & FBTNodeBPImplementationHelper::AISpecific))
|
||||
{
|
||||
ReceiveTickAI(AIOwner, AIOwner->GetPawn(), DeltaSeconds);
|
||||
}
|
||||
//是否实现了不带AI后缀的函数
|
||||
else if (ReceiveTickImplementations & FBTNodeBPImplementationHelper::Generic)
|
||||
{
|
||||
ReceiveTick(ActorOwner, DeltaSeconds);
|
||||
}
|
||||
}
|
||||
```
|
||||
## 只有附加在Composites(合成节点)上的Service,才会调用OnSearchStart。
|
||||
Composites(合成节点)也就是指Sequence,Selector,SimpleParallel节点。
|
||||
|
||||
还有种特殊情况:当Service附加在与Root相连的第一个合成节点上时,SearchStart会被触发两次。
|
||||
|
||||
## Compisiton
|
||||
复合节点:此类节点定义分支的根以及执行该分支的基本规则。
|
||||
|
||||
## Decorator
|
||||

|
||||
|
||||
可以理解为条件节点,连接Composite或者Task节点,决定树中的分支、甚至单个节点能否被执行。
|
||||
|
||||
c++实现可以参考:
|
||||
- UBTDecorator_BlueprintBase
|
||||
- UBTDecorator_BlackboardBase
|
||||
- UBTDecorator_CheckGameplayTagsOnActor
|
||||
- UBTDecorator_Loop
|
||||
|
||||
蓝图节点中可重写的函数有:
|
||||
- ReceiveTick:通用版本的tick函数。如果条件符合会在TickNode()中调用。
|
||||
- ReceiveTickAI:AI版本的tick函数。如果条件符合会在TickNode()中调用。
|
||||
- ReceiveExecutionStart:通用版本的Start函数。如果条件符合会在OnNodeActivation()中调用。
|
||||
- ReceiveExecutionStartAI:AI版本的Start函数。如果条件符合会在OnNodeActivation()中调用。
|
||||
- ReceiveExecutionFinish:通用版本的Finish函数。如果条件符合会在OnNodeDeactivation()中调用。
|
||||
- ReceiveExecutionFinishAI:AI版本的Finish函数。如果条件符合会在OnNodeDeactivation()中调用。
|
||||
- ReceiveObserverActivated:通用版本的Activated函数。如果条件符合会在OnBecomeRelevant()中调用。
|
||||
- ReceiveObserverActivatedAI:AI版本的Activated函数。如果条件符合会在OnBecomeRelevant()中调用。
|
||||
- ReceiveObserverDeactivated:通用版本的Deactivated函数。如果条件符合会在OnCeaseRelevant()中调用。
|
||||
- ReceiveObserverDeactivatedAI:AI版本的Deactivated函数。如果条件符合会在OnCeaseRelevant()中调用。
|
||||
- PerformConditionCheck:通用版本的ConditionCheck函数。如果条件符合会在CalculateRawConditionValueImpl()中调用。
|
||||
- PerformConditionCheckAI:AI版本的ConditionCheck函数。如果条件符合会在CalculateRawConditionValueImpl()中调用。
|
||||
|
||||
## Service
|
||||

|
||||
|
||||
可以理解为并行逻辑节点,连接连接Composite或者Task节点,运行到当前分支时,Service节点将按照之前设定的频率执行。通常是使用这些Service节点检查、更新黑板数据。
|
||||
|
||||
c++可以参考:
|
||||
- UBTService_BlueprintBase
|
||||
- UBTService_BlackboardBase
|
||||
- UBTService_RunEQS
|
||||
|
||||
蓝图节点中可重写的函数有:
|
||||
- ReceiveTick
|
||||
- ReceiveTickAI
|
||||
- ReceiveSearchStart
|
||||
- ReceiveSearchStartAI
|
||||
- ReceiveActivation
|
||||
- ReceiveActivationAI
|
||||
- ReceiveDeactivation
|
||||
- ReceiveDeactivationAI
|
||||
|
||||
### 相关事件
|
||||
- OnSearchStart:所挂靠的合成节点被激活时调用的函数。
|
||||
- TickNode:Tick执行的函数(间隔自定义)。可在构造函数设置 bNotifyTick 来决定是否调用。
|
||||
- OnBecomeRelevant:节点被激活时调用(等同于蓝图Service里的Receive Activation) 需要在构造函数中设置 bNotifyBecomeRelevant = true来开启调用。
|
||||
- OnCeaseRelevant:节点被激活时调用(等同于蓝图Service里的Receive Deactivation) 需要在构造函数中设置 bNotifyCeaseRelevant = true来开启调用。
|
||||
- TickNode,OnBecomeRelevant和OnCeaseRelevant并不是UBTService类的虚函数,而是UBTService的父类UBTAuxiliaryNode的。装饰器UBTDecorator也继承此类。
|
||||
- GetInstanceMemorySize:当节点被实例化时分配的内存大小,关于实例化后面会详谈。
|
||||
|
||||
- Tick函数执行间隔 = Random(Interval-RandomDeviation,Interval+RandomDeviation)
|
||||
在这也就是 = Random(0.5-0.1 , 0.5+0.1), 每次调用都会计算一次。
|
||||
- CallTickonSearchStart:若为True,则会在SearchStart函数触发时,调用一次Tick函数。 若为False,则即使节点已被激活,也要等待一次间隔才能执行第一次Tick函数。
|
||||
- RestartTimeronEachActivation:若为True,则每次节点被激活都会从0开始计算间隔。 若为False,则会记录下上一次已经等待的间隔,在下一次激活时沿用。
|
||||
|
||||
## Task
|
||||
可以理解为具体执行的任务(逻辑)。此类节点是行为树的叶。它们是可执行的操作,没有输出连接。
|
||||
|
||||
UBTTask_BlueprintBase与UBTTask的区别在于
|
||||
以UBTTask_PlaySound这种简单UBTTask为例,主要实现了ExecuteTask(),其他是显示图标与描述的GetStaticDescription()与GetNodeIconName()
|
||||
|
||||
蓝图节点中可重写的函数有:
|
||||
- ReceiveTick:通用版本的tick函数。如果条件符合会在TickTask()中调用。
|
||||
- ReceiveTickAI:AI版本的tick函数。如果条件符合会在TickTask()中调用。
|
||||
- ReceiveExecute:通用版本的Execute函数。如果条件符合会在ExecuteTask()中调用。
|
||||
- ReceiveAbort:通用版本的Abort函数。如果条件符合会在AbortTask()中调用。
|
||||
- ReceiveExecuteAI:AI版本的Execute函数。如果条件符合会在ExecuteTask()中调用。
|
||||
- ReceiveAbortAI:AI版本的Abort函数。如果条件符合会在AbortTask()中调用。
|
||||
|
||||
### RunBehavior 与 RunBehaviorDynamic
|
||||
可以加载其他行为树并且运行。
|
||||
|
||||
- RunBehavior的一个限制是在运行时不能改变该子树资源。存在该限制的原因是给子树的根等级装饰器节点提供支持,这些装饰器节点将注入到父树中。此外,在运行时不能修改运行树的结构。
|
||||
- RunBehaviorDynamic使用行为树组件上的SetDynamicSubtree 函数即可以在运行时分配子树资源。本函数不会为子树的根等级装饰器节点提供支持。
|
||||
|
||||
## EQS
|
||||
大致流程:
|
||||
通过生成器在场景生成检测点获取当前位置的各种数据,之后再通过条件判断获取想要的查询结果。EQS的一些使用范例包括:找到最近的回复剂或弹药、判断出威胁最大的敌人,或者找到能看到玩家的视线(下面就会显示这样的一个示例)。
|
||||
|
||||
### 在Actor类中使用EQS
|
||||
结合原生代码使用EQS
|
||||
虽然EQS查询通常是在行为树中运行,但也可以直接从原生代码使用它。以下示例展示了一个虚构的查询,要在指定区域内为角色或物品寻找安全生成地点:
|
||||
```
|
||||
// 以下名称必须与查询中使用的变量名一致
|
||||
static const FName SafeZoneIndexName = FName(TEXT("SafeZoneIndex"));
|
||||
static const FName SafeZoneRadiusName = FName(TEXT("SafeZoneRadius"));
|
||||
|
||||
// 运行查询,根据区域索引和安全半径寻找安全的生成点
|
||||
bool AMyActor::RunPlacementQuery(const UEnvQuery* PlacementQuery)
|
||||
{
|
||||
if (PlacementQuery)
|
||||
{
|
||||
// 设置查询请求
|
||||
FEnvQueryRequest QueryRequest(PlacementQuery, this);
|
||||
|
||||
// 设置查询参数
|
||||
QueryRequest.SetIntParam(SafeZoneIndexName, SafeZoneIndexValue);
|
||||
QueryRequest.SetFloatParam(SafeZoneRadiusName, SafeZoneRadius);
|
||||
|
||||
// 执行查询
|
||||
QueryRequest.Execute(EEnvQueryRunMode::RandomBest25Pct, this, &AFortAthenaMutator_SpawningPolicyBase::OnEQSSpawnLocationFinished);
|
||||
|
||||
// 返回true说明查询已开始
|
||||
return true;
|
||||
}
|
||||
|
||||
// 返回false说明查询未能开始
|
||||
return false;
|
||||
}
|
||||
```
|
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: 打包项目的Debug方法
|
||||
date: 2020-07-07 13:49:28
|
||||
excerpt:
|
||||
tags:
|
||||
rating: ⭐
|
||||
---
|
||||
# 蓝图深入探讨Blueprints In-depth学习笔记
|
||||
就如大钊所说的,这个视频不管程序还是美术都值得学习一下。
|
||||
## 蓝图开销
|
||||
蓝图主要开销影响:
|
||||
1. 蓝图的节点数是一个较为影响性能的因素,而不是节点所执行的逻辑影响。所以在蓝图中执行循环操作会非常影响性能(执行节点本身的逻辑本质上还是c++)
|
||||
2. 执行遍历Actor(Get All Actor Of Class)也比较消耗资源。避免总是在蓝图中使用,可以提前把对象的引用保存下来。
|
||||
3. tick
|
||||
|
||||
90%的蓝图是不需要开启tick的,你可以在项目设置中关闭Can Blueprint Tick By Default。定时器与事件几乎可以解决所有问题。
|
||||
|
||||
检测蓝图tick中性能消耗最大的因素,将这个因素放入计时器中。但是视觉效果的更新就不适合了,所以这部分可能还是使用c++比较好。(例如海洋系统)
|
||||
|
||||
使用材质来实现一些简单动画可以节约CPU资源,效率会高一些。尽量将一些效果写进材质中,这样可以简化蓝图,提高效率。
|
||||
|
||||
## 蓝图debuger与可视化日志
|
||||
蓝图debug(可以获取断点中的变量数据)
|
||||
你需要在需要监视的节点变量(每个节点上的引脚)上右键,选择监视这个变量,之后就可以在断点触发后,在蓝图debuger中查看变量数据。
|
||||
|
||||
可视化日志:你需要在关卡开始时运行Enable Vislog Recording,之后就可以通过使用可视化日志指令:Vislog Text、Vislog Box Shape、Vislog Location、Vislog Segment。
|
||||
之后再启动游戏,可视化日志就会自动记录。
|
||||
|
||||
作者建议使用这个东西来记录AI运行情况。
|
||||
|
||||
## 查看所有使用tick的actor
|
||||
console中输入dumpticks指令。
|
||||
|
||||
## 内存与Asset载入
|
||||
不要在蓝图进行多重蓝图或者引用巨量资源,防止引擎在加载该蓝图时加载太多的Asset。
|
||||
|
||||
这里作者给出的建议是:
|
||||
1. 使用C++定义关键逻辑的基类。
|
||||
2. 创建一个继承该基类的蓝图类作为蓝图父类。
|
||||
3. 创建另一个蓝图类继承上一个蓝图父类。(相关的Asset会在这个层级的蓝图子类中引用)
|
||||
|
||||
PS.这样的架构还有一个好处,那就是你可以随时将蓝图中的逻辑移植到c++。
|
||||
|
||||
如果你调用了蓝图函数库中任意一个函数,那么整合蓝图库都会被加载,如果蓝图库还引用了其他资源,那么很可能整个游戏资源都被加载。所以一个正确的方法就是不要创建用于专门加载某一Asset函数来加载函数,而是使用通用函数配合变量的方式来进行指定资源的加载。
|
||||
|
||||
PS.使用GameMode来区分菜单操作逻辑与游戏操作逻辑,一个好处就是不用来回引用。(GameMode_Game与GameMode_Menu)
|
||||
|
||||
对于资源强引用,可以使用异步加载。此时资源引用器就会将其显示为粉色,意为弱引用。
|
||||
|
||||
你可以在设置,将bRecompileOnLoad设置为false(蓝图、关卡蓝图、动画蓝图)以提高打开编辑器的速度。
|
||||
|
||||
当然有资源加载就有资源回收,当物体量多时,回收资源会出现卡顿,此时你可以在项目设置中修改资源回收时间(Time Between puring Pending Kill Objects),这对于制作电影项目(非游戏项目)会比较好。对于巨量物体你可以在蓝图中勾选群集功能(Can Be in Cluster),当然最好的方法还是使用c++。
|
||||
|
||||
## 蓝图编译时间
|
||||
1. 蓝图中的蓝图节点数
|
||||
2. 蓝图中的类型转换以及其他类型引用
|
||||
3. 循环引用
|
||||
|
||||
如果类型转换的对象有这复杂的循环引用(比如各个蓝图类互相引用各自的函数),那么一个类型转换将会载入所有与之关联的蓝图类。这可以说是相当恐怖的。
|
||||
|
||||
想要加快速度可以使用:
|
||||
1. 将逻辑封装到蓝图函数中,可以减少编译时间。
|
||||
2. 将逻辑按照功能进行分类,再使用蓝图或者c++将其分离成各种Actor、子Actor、Component。
|
||||
3. 对于类型转换需要进行合理的管理。(比如将带有转换的逻辑使用c++封装成函数、使用蓝图接口、GameplayTag等)
|
||||
|
||||
## 蓝图接口
|
||||
使用流程:
|
||||
1. 创建蓝图接口Asset,并设置接口参数。
|
||||
2. 在需要编写逻辑的蓝图中绑定接口,并且实现接口逻辑。
|
||||
3. 在需要调用的蓝图中调用这个接口就行了。
|
||||
|
||||
使用蓝图接口的好处就在于,你不需要将对象进行类型转换。你可以使用Dose Implement Interface节点来判断是否实现某个蓝图接口。
|
||||
|
||||
## 使用GameplayTag的意义
|
||||
使用标签可以直接对标签进行判断,避免对象类型转化。
|
||||
|
||||
比如给Actor物体绑定一个CanPickup标签,在之后直接判断出来。
|
||||
|
||||
## Show 3D Widget
|
||||
蓝图中一些变量可以开启Show 3D Widget,方便调节数值。
|
||||
|
||||
## 自动测试框架
|
||||
### 使用蓝图进行测试
|
||||
在蓝图使用自动测试框架,其测试用的蓝图类需要继承Functional Test类。
|
||||
|
||||
Prepare Test事件:用于在测试前生成所需要的Actor与数据。
|
||||
Start Test事件:开始进行测试。
|
||||
|
||||
### 使用c++进行测试
|
||||
c++自动测试的视频我就找到了:https://www.bilibili.com/video/av79617236
|
||||
以下是学习该视频的笔记:
|
||||
|
||||
想要编写自己的测试类可以继承FAutomationTestBase类,并重写RunTest函数,之后系统会自动向自动测试框架注册。使用IMPLEMENT_SIMPLE_AUTOMATION_TEST宏标记为简单测试类型。以下是一段案例代码:
|
||||

|
||||
|
||||
使用IMPLEMENT_COMPLEX_AUTOMATION_TEST宏标记为复杂测试类型。以下是一段案例代码:
|
||||

|
||||
看得出可以通过这个方法添加子测试。
|
||||
|
||||
*启动自动调试:*
|
||||
可以使用Ue4的Session Frontend,也可以使用TeamCity,作者的团队也编写了一个测试工具进行工作。
|
||||
|
||||
#### 地图测试
|
||||
11:30处,作者展示一个小案例。大致步骤为:使用GetTest遍历地图文件夹下所有level。之后分别对各个level进行相关测试。比如模拟玩家操作等。
|
||||
|
||||
#### 项目经验
|
||||
23:00左右作者开始介绍项目经验,这个流程较为复杂,推荐大团队使用,这个直接去看视频吧。
|
74
03-UnrealEngine/Gameplay/Debug/UE4 Gameplay Debug技巧.md
Normal file
74
03-UnrealEngine/Gameplay/Debug/UE4 Gameplay Debug技巧.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: UE4 Gameplay Debug技巧
|
||||
date: 2022-12-09 11:30:54
|
||||
excerpt: Debug
|
||||
tags:
|
||||
rating: ⭐
|
||||
---
|
||||
## debug技巧
|
||||
### 在release模式下开启断点的方法
|
||||
如果要debug代码可以用一个非常古典的办法用
|
||||
`#pragma optimize("",off)` 与`#pragma optimize("",on)` 来将一部分代码排除在优化以外
|
||||
|
||||
### Ue4中的可视化debug法——vlog
|
||||
http://api.unrealengine.com/CHN/Gameplay/Tools/VisualLogger/index.html
|
||||
实现GrabDebugSnapshot接口,之后调用UE_VLOG()宏。
|
||||
```c++
|
||||
#if ENABLE_VISUAL_LOG
|
||||
/** Appends information about this actor to the visual logger */
|
||||
virtual void GrabDebugSnapshot(FVisualLogEntry* Snapshot) const override;
|
||||
#endif
|
||||
#if ENABLE_VISUAL_LOG
|
||||
void AGDCCharacter::GrabDebugSnapshot(FVisualLogEntry* Snapshot) const
|
||||
{
|
||||
Super::GrabDebugSnapshot(Snapshot);
|
||||
const int32 CatIndex = Snapshot->Status.AddZeroed();
|
||||
FVisualLogStatusCategory& PlaceableCategory = Snapshot->Status[CatIndex];
|
||||
PlaceableCategory.Category = TEXT("GDC Sample");
|
||||
PlaceableCategory.Add(TEXT("Projectile Class"), ProjectileClass != nullptr ? ProjectileClass->GetName() : TEXT("None"));
|
||||
}
|
||||
#endif
|
||||
void AGDCCharacter::OnFire()
|
||||
{
|
||||
// try and fire a projectile
|
||||
if (ProjectileClass != NULL)
|
||||
{
|
||||
const FRotator SpawnRotation = GetControlRotation();
|
||||
// MuzzleOffset is in camera space, so transform it to world space before offsetting from the character location to find the final muzzle position
|
||||
const FVector SpawnLocation = GetActorLocation() + SpawnRotation.RotateVector(GunOffset);
|
||||
UWorld* const World = GetWorld();
|
||||
if (World != NULL)
|
||||
{
|
||||
// spawn the projectile at the muzzle
|
||||
World->SpawnActor<AGDCProjectile>(ProjectileClass, SpawnLocation, SpawnRotation);
|
||||
UE_VLOG(this, LogFPChar, Verbose, TEXT("Fired projectile (%s) from location (%s) with rotation (%s)"),
|
||||
*ProjectileClass->GetName(),
|
||||
*SpawnLocation.ToString(),
|
||||
*SpawnRotation.ToString());
|
||||
}
|
||||
}
|
||||
// try and play the sound if specified
|
||||
if (FireSound != NULL)
|
||||
{
|
||||
UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
|
||||
}
|
||||
// try and play a firing animation if specified
|
||||
if(FireAnimation != NULL)
|
||||
{
|
||||
// Get the animation object for the arms mesh
|
||||
UAnimInstance* AnimInstance = Mesh1P->GetAnimInstance();
|
||||
if(AnimInstance != NULL)
|
||||
{
|
||||
AnimInstance->Montage_Play(FireAnimation, 1.f);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
之后就可以在visual logger里看到实时数据。除此之外还有一些可视的形状log:
|
||||
1. UE_VLOG_SEGMENT
|
||||
2. UE_VLOG_LOCATION
|
||||
3. UE_VLOG_BOX (axis aligned box)
|
||||
4. UE_VLOG_OBOX (oriented box)
|
||||
5. UE_VLOG_CONE
|
||||
6. UE_VLOG_CYLINDER
|
||||
7. UE_VLOG_CAPSULE
|
166
03-UnrealEngine/Gameplay/Debug/UE_Log.md
Normal file
166
03-UnrealEngine/Gameplay/Debug/UE_Log.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: UE_Log
|
||||
date: 2022-08-31 09:40:26
|
||||
excerpt:
|
||||
tags:
|
||||
rating: ⭐
|
||||
---
|
||||
说明:本文为Wiki上的RAMA大神文章的大致翻译
|
||||
|
||||
## 显示日志
|
||||
- 在游戏模式下,你需要在游戏的快捷方式后面加 -Log,才会在游戏中显示。
|
||||
- 如果想在游戏中看到,需要到Engin.ini中修改参数添加"GameCommandLine=-log,如果没有,则需要按~,输入-Log命令开启。
|
||||
|
||||
## QuickStart
|
||||
UE_LOG(LogTemp, Warning, TEXT("Your message"));
|
||||
|
||||
不用设置标签,简单快速。
|
||||
|
||||
## CustomTag
|
||||
在你的游戏头文件中进行声明:
|
||||
```c++
|
||||
//General Log
|
||||
DECLARE_LOG_CATEGORY_EXTERN(YourLog, Log, All);
|
||||
|
||||
//Logging during game startup
|
||||
DECLARE_LOG_CATEGORY_EXTERN(YourInit, Log, All);
|
||||
|
||||
//Logging for your AI system
|
||||
DECLARE_LOG_CATEGORY_EXTERN(YourAI, Log, All);
|
||||
|
||||
//Logging for Critical Errors that must always be addressed
|
||||
DECLARE_LOG_CATEGORY_EXTERN(YourCriticalErrors, Log, All);
|
||||
```
|
||||
这样输出的Log你就可以知道是哪个部分的,这也是UE_Log很有用的原因。
|
||||
|
||||
之后在你的游戏Cpp文件中定义:
|
||||
```c++
|
||||
//General Log
|
||||
DEFINE_LOG_CATEGORY(YourLog);
|
||||
|
||||
//Logging during game startup
|
||||
DEFINE_LOG_CATEGORY(YourInit);
|
||||
|
||||
//Logging for your AI system
|
||||
DEFINE_LOG_CATEGORY(YourAI);
|
||||
|
||||
//Logging for Critical Errors that must always be addressed
|
||||
DEFINE_LOG_CATEGORY(YourCriticalErrors);
|
||||
```
|
||||
|
||||
## Log格式化
|
||||
### Log Message
|
||||
```c++
|
||||
//"This is a message to yourself during runtime!"
|
||||
UE_LOG(YourLog,Warning,TEXT("This is a message to yourself during runtime!"));
|
||||
```
|
||||
|
||||
### Log an FString
|
||||
```c++
|
||||
%s strings are wanted as TCHAR* by Log, so use *FString()
|
||||
//"MyCharacter's Name is %s"
|
||||
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Name is %s"), *MyCharacter->GetName() );
|
||||
```
|
||||
|
||||
### Log an Int
|
||||
```c++
|
||||
//"MyCharacter's Health is %d"
|
||||
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Health is %d"), MyCharacter->Health );
|
||||
```
|
||||
|
||||
### Log a Float
|
||||
```c++
|
||||
//"MyCharacter's Health is %f"
|
||||
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Health is %f"), MyCharacter->Health );
|
||||
```
|
||||
|
||||
### Log an FVector
|
||||
```c++
|
||||
//"MyCharacter's Location is %s"
|
||||
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Location is %s"),
|
||||
*MyCharacter->GetActorLocation().ToString());
|
||||
```
|
||||
|
||||
### Log an FName
|
||||
```c++
|
||||
//"MyCharacter's FName is %s"
|
||||
UE_LOG(YourLog,Warning,TEXT("MyCharacter's FName is %s"),
|
||||
*MyCharacter->GetFName().ToString());
|
||||
```
|
||||
|
||||
### Log an FString,Int,Float
|
||||
```c++
|
||||
//"%s has health %d, which is %f percent of total health"
|
||||
UE_LOG(YourLog,Warning,TEXT("%s has health %d, which is %f percent of total health"),
|
||||
*MyCharacter->GetName(), MyCharacter->Health, MyCharacter->HealthPercent);
|
||||
```
|
||||
|
||||
## Log的颜色设置
|
||||
```c++
|
||||
//"this is Grey Text"
|
||||
UE_LOG(YourLog,Log,TEXT("This is grey text!"));
|
||||
|
||||
//"this is Yellow Text"
|
||||
UE_LOG(YourLog,Warning,TEXT("This is yellow text!"));
|
||||
|
||||
//"This is Red Text"
|
||||
UE_LOG(YourLog,Error,TEXT("This is red text!"));
|
||||
```
|
||||
|
||||
可以看得出第二个参数是是用来控制颜色的。
|
||||
|
||||
## 向客户端传递信息(网络模式):
|
||||
```c++
|
||||
PlayerController->ClientMessage("Your Message");
|
||||
```
|
||||
|
||||
命令行命令以及Engine.ini配置:
|
||||
Log conventions (in the console, ini files, or environment variables)
|
||||
|
||||
[cat] = a category for the command to operate on, or 'global' for all categories.
|
||||
标签,没有设置就显示所有的Log
|
||||
|
||||
[level] = verbosity level, one of: none, error, warning, display, log, verbose, all, default
|
||||
关卡,显示某某关卡的Log
|
||||
|
||||
At boot time, compiled in default is overridden by ini files setting, which is overridden by command line
|
||||
|
||||
Log console command usage
|
||||
|
||||
Log list - list all log categories
|
||||
|
||||
Log list [string] - list all log categories containing a substring
|
||||
|
||||
Log reset - reset all log categories to their boot-time default
|
||||
|
||||
Log [cat] - toggle the display of the category [cat]
|
||||
|
||||
Log [cat] off - disable display of the category [cat]
|
||||
|
||||
Log [cat] on - resume display of the category [cat]
|
||||
|
||||
Log [cat] [level] - set the verbosity level of the category [cat]
|
||||
|
||||
Log [cat] break - toggle the debug break on display of the category [cat]
|
||||
|
||||
### Log command line
|
||||
- LogCmds=\"[arguments],[arguments]...\" - applies a list of console commands at boot time
|
||||
- LogCmds=\"foo verbose, bar off\" - turns on the foo category and turns off the bar category
|
||||
|
||||
### Environment variables
|
||||
Any command line option can be set via the environment variable UE-CmdLineArgs
|
||||
|
||||
set UE-CmdLineArgs=\"-LogCmds=foo verbose breakon, bar off\"
|
||||
|
||||
### Config file
|
||||
In DefaultEngine.ini or Engine.ini:
|
||||
```ini
|
||||
[Core.Log]
|
||||
global=[default verbosity for things not listed later]
|
||||
[cat]=[level]
|
||||
foo=verbose break
|
||||
```
|
||||
|
||||
## 其他
|
||||
Rama后面的一篇文章提供了显示代码行号、函数名称、类名等功能:
|
||||
https://wiki.unrealengine.com/Logs,_Printing_the_Class_Name,_Function_Name,_Line_Number_of_your_Calling_Code!
|
54
03-UnrealEngine/Gameplay/Debug/打包项目的Debug方法.md
Normal file
54
03-UnrealEngine/Gameplay/Debug/打包项目的Debug方法.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: 打包项目的Debug方法
|
||||
date: 2022-08-24 13:30:08
|
||||
excerpt:
|
||||
tags: debug
|
||||
rating: ⭐
|
||||
---
|
||||
## 参考视频
|
||||
https://www.youtube.com/watch?v=CmWbMT4WAhU
|
||||
|
||||
## Shipping模式保存日志文件
|
||||
1. 在您的 {projectname}.Target.cs 文件的 contrsutor 中,添加以下行: `bUseLoggingInShipping = true;`。
|
||||
2. 根据源码版与官方编译版有额外的2个设置:
|
||||
1. 源码版:增加`BuildEnvironment = TargetBuildEnvironment.Unique`。
|
||||
2. 官方编译版:增加`bOverrideBuildEnvironment = true;`。
|
||||
|
||||
比如:
|
||||
```c#
|
||||
public class GameTarget : TargetRules
|
||||
{
|
||||
public GameTarget(TargetInfo Target) : base(Target)
|
||||
{
|
||||
Type = TargetType.Game;
|
||||
|
||||
// enable logs and debugging for Shipping builds
|
||||
if (Configuration == UnrealTargetConfiguration.Shipping)
|
||||
{
|
||||
BuildEnvironment = TargetBuildEnvironment.Unique;
|
||||
bUseChecksInShipping = true;
|
||||
bUseLoggingInShipping = true;
|
||||
}
|
||||
|
||||
ExtraModuleNames.AddRange( new string[] { "Game" } );
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Debug打包后的游戏
|
||||
1. 以DebugGame模式进行打包。
|
||||
2. 运行游戏,并在任务管理器中找到该游戏进程。
|
||||
3. 右键选择调试。
|
||||
|
||||
或者可以在VS里手动选择附加进程。
|
||||
|
||||
## 调试相关命令行
|
||||
1. 如果想在一开始就进行附加,可以在游戏运行方式中加入`-waitforattach`。
|
||||
2. 在游戏运行方式中加入`-log`就可以在开始时显示log。
|
||||
|
||||
## 生成调试符号
|
||||
**Settings -> Packaging Settings -> Project**中勾选Include Debug Files选项就可以生成PDB文件。
|
||||
|
||||
## UE4Launcher
|
||||
查里鹏开发的工具,可以方便启动UE工程:https://github.com/hxhb/UE4Launcher
|
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Exploring the Gameplay Ability System (GAS) with an Action RPG Unreal Fest 2022学习笔记
|
||||
date: 2022-11-10 21:04:35
|
||||
excerpt:
|
||||
tags: GAS
|
||||
rating: ⭐
|
||||
---
|
||||
## 前言
|
||||
原视频:[Exploring the Gameplay Ability System (GAS) with an Action RPG Unreal Fest 2022](https://www.youtube.com/watch?v=tc542u36JR0&t)
|
||||
|
||||
## InitialSetup
|
||||
首先需要考虑:
|
||||
- 哪些`AttributeSets`需要创建
|
||||
- 如何让角色获取`GrantAbility`
|
||||
- 如何绑定按键与控制器输出事件
|
||||
|
||||
## CharacterSetup
|
||||
- 继承并实现`IAbilitySystemInterface`
|
||||
- 挂载ASC,但一般还是挂载到PlayerState上。
|
||||
- 在构造函数中创建默认子物体:
|
||||
- AbilitySystemComponent
|
||||
- AttributeSetObject
|
||||
|
||||
### PossessedBy
|
||||
- AbilitySystemComponent->InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor)
|
||||
- InitializeAttributes
|
||||
- Listen for tags of interest(via UAsyncATaskGameplayTagAddedRemoved)
|
||||
- Add start-up effects
|
||||
- Bind to Ability activation failed callback
|
||||
|
||||
### BeginPlay
|
||||
Listen to Health Attribute change
|
||||
Bind to ASC->OnImmunity
|
||||
|
||||
## AttributeSets
|
||||
- PrimaryAttributes——核心RPG状态,绝大多数实体需要
|
||||
- Health,MaxHealth,Shields,MaxShields,Stamina
|
||||
- Core RPG Style values(比如Strength,Fortitude(坚韧)等)
|
||||
- SecondaryAttributes——一定深度的RPG状态,非所有实体需要
|
||||
- Elemental resistances(元素抵抗表),Elemental bonuses(元素奖励),Current Elemental build-Ups(当前元素构建方式)
|
||||
- TertiaryAttributes——Bounses(主要用于玩家控制的角色)
|
||||
- Interaction speed bonuses,other specific bonuses and stats
|
||||
- Weapon archetype specific Attribute(武器相关属性)
|
||||
- Charge Percent(充能百分比),Spread(扩散),Charge Count(充能次数)
|
||||
|
||||
### 后续改进
|
||||
- PrimaryAttributes
|
||||
- 将Health等基本属性分割成自己的属性集
|
||||
- 目前,如果我们给一个UObject等设置了一个属性集来跟踪它们的Health,但它们也会保存其他不必要的值。
|
||||
- SecondaryAttributes
|
||||
- 对这个设计很满意,因为它都是与元素有关的状态,以及buff/debuff属性。
|
||||
- TertiaryAttributes
|
||||
- 一些属性可以被分割合并到实体特化的属性集中。
|
||||
- Weapon archetype specific attributes
|
||||
- 一些int类型的Attribute可以使用Stackable GE来代替(属性集只有float类型),通过查询堆栈数目来获取数据。
|
||||
|
||||
## 演讲者团队增加的额外功能
|
||||
- UGameplayAbility
|
||||
- Input
|
||||
- UInputAction InputAction(Enhanced Input System)
|
||||
- Custom Ability Input ID Enum
|
||||
- Booleans/Bitfields 开启一些核心功能 ,我们不想对所有Ability增加通过GameplayTags实现的功能
|
||||
- Activate On Granted
|
||||
- Activate On Input
|
||||
- Can Activate Whilst Interacting
|
||||
- Cannot Activate Whilst Stunned
|
||||
- Cached Owning Character
|
||||
- AsyncAbilityTasks
|
||||
- Attribute Changed
|
||||
- Cooldown Changed
|
||||
- GameplayTag Added Removed
|
||||
- Wait GE Class Added Removed
|
||||

|
||||
|
||||
## 使用DataTable与C++来初始化属性集
|
||||

|
||||
|
||||
或者就是实现一个结构体基于FAttributeSetIniDiscreteLevel。
|
||||
|
||||
## 创建自定义节点来传递GE计算结果与Tags
|
||||
多次GE的积累计算结果。主要是通过AbilitySystemGlobals来实现
|
||||
- GE_Damage
|
||||
- Custom Calculation Class:C++ Damage Execution Calculation
|
||||
- Conditional GEs
|
||||
- GE_BuildUp_Stun
|
||||
- Custom Calculation Class: C++ Build Up Calculation
|
||||
- Conditional GE: GE_Stunned
|
||||
- GE_BuildUp_Burn
|
||||
- Custom Calculation Class: C++ Build Up Calculation
|
||||
- Conditional GE: GE_Burned
|
||||
|
||||

|
||||

|
||||
## 装备属性变化
|
||||

|
||||

|
||||
|
||||
## AbilityTraitsTableExample
|
||||

|
||||
## 该团队AbilityEntitySystem设计
|
||||
用来实现子弹变量、AOE、Cnnstruct、角色等带有ASC的实体。
|
||||

|
||||
|
||||
|
||||

|
181
03-UnrealEngine/Gameplay/GAS/(1)开始编写GameAbility相关逻辑.md
Normal file
181
03-UnrealEngine/Gameplay/GAS/(1)开始编写GameAbility相关逻辑.md
Normal file
@@ -0,0 +1,181 @@
|
||||
## 前言
|
||||
本人最近看了GameplayAbility的wiki与官方的ActionRPG案例,大致对此有了一定了解。所以在此分享相关经验顺便作为学习笔记。
|
||||
|
||||
wiki采用了第三人称模板来进行讲解。讲解了几个主要类的概念、参数以及用法。
|
||||
|
||||
ActionRPG则是一个较为完整的案例:它通过c++ 往几个GameplayAbility的基础类中添加了若干逻辑,使得之更加适合于RPG项目。大部分改动都很不错,甚至直接复制,让它作为你的RPG模板都是没问题。它的主要逻辑以及表现都在于蓝图中(技能与效果),所以它也是一个c++与蓝图结合开发的好例子。(注意:GameAbility无法完全通过蓝图开发)
|
||||
|
||||
所以接下来我也会适当讲解actionRPG的结构。<br>
|
||||
注意:有关网络同步的我都会略过。
|
||||
## 相关资料
|
||||
wiki:
|
||||
https://wiki.unrealengine.com/index.php?title=GameplayAbilities_and_You#Common_Issues
|
||||
|
||||
GameplayAbility文档:
|
||||
https://docs.unrealengine.com/en-us/Gameplay/GameplayAbilitySystem
|
||||
|
||||
actionRPG案例文章:
|
||||
https://docs.unrealengine.com/en-US/Resources/SampleGames/ARPG/index.html
|
||||
|
||||
## 启用GameAbility插件并且在你的项目中添加该模块
|
||||
1. 在编辑(Edit)->插件(Plugins)找到Gameplay Ability 并启用。
|
||||
2. 在你的c++项目(ProjectName).Build.cs文件的PublicDependencyModuleNames变量中添加: "GameplayAbilities", "GameplayTags", "GameplayTasks"。
|
||||
|
||||
例如:
|
||||
```
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks", "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" });
|
||||
```
|
||||
### 注意事项
|
||||
**在模块文件以及c++中包含GameplayBility前,先需要确定是否在项目中启用GameplayAbility插件(默认是不启用的)。不然你的项目编译时会通过,但运行时会显示无法加载游戏模块。**
|
||||
## 建议
|
||||
在模块h文件与cpp中定义这个模块的log标签,这样你可以对log进行过滤显示。
|
||||
```
|
||||
//在h文件中
|
||||
ACTIONRPG_API DECLARE_LOG_CATEGORY_EXTERN(LogActionRPG, Log, All);
|
||||
```
|
||||
```
|
||||
//在cpp文件中
|
||||
DEFINE_LOG_CATEGORY(LogActionRPG);
|
||||
```
|
||||
## 在角色类中挂载自定义的UGameplayAbilityComponent
|
||||
actionRPG中定义了URPGAbilitySystemComponent作为挂载组件类,它实现了以下函数:
|
||||
```
|
||||
//通过Tag(可以是多个tag)来获取激活的技能
|
||||
void GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<URPGGameplayAbility*>& ActiveAbilities);
|
||||
//取得角色等级,这个意义不大,因为不是所有的rpg游戏都有等级这一说
|
||||
int32 GetDefaultAbilityLevel() const;
|
||||
//通过全局的AbilitySystem来获取对应Actor所绑定的组件指针
|
||||
static URPGAbilitySystemComponent* GetAbilitySystemComponentFromActor(const AActor* Actor, bool LookForComponent = false);
|
||||
```
|
||||
```
|
||||
void URPGAbilitySystemComponent::GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<URPGGameplayAbility*>& ActiveAbilities)
|
||||
{
|
||||
TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
|
||||
GetActivatableGameplayAbilitySpecsByAllMatchingTags(GameplayTagContainer, AbilitiesToActivate, false);
|
||||
|
||||
for (FGameplayAbilitySpec* Spec : AbilitiesToActivate)
|
||||
{
|
||||
TArray<UGameplayAbility*> AbilityInstances = Spec->GetAbilityInstances();
|
||||
|
||||
for (UGameplayAbility* ActiveAbility : AbilityInstances)
|
||||
{
|
||||
ActiveAbilities.Add(Cast<URPGGameplayAbility>(ActiveAbility));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
URPGAbilitySystemComponent* URPGAbilitySystemComponent::GetAbilitySystemComponentFromActor(const AActor* Actor, bool LookForComponent)
|
||||
{
|
||||
return Cast<URPGAbilitySystemComponent>(UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor, LookForComponent));
|
||||
}
|
||||
```
|
||||
|
||||
## 定义角色类
|
||||
ARPGCharacterBase继承于ACharacter与接口类IAbilitySystemInterface。
|
||||
|
||||
actionRPG中定义了ARPGCharacterBase作为角色类的基础类。因为这个类会适用于主角、友善NPC、敌方NPC,所有我们不应该在这个类中进行输入绑定、以及各种Camera、movementComponent等非共用性组件的绑定与设置。
|
||||
|
||||
接下来我会讲解我阅读代码的顺序,具体代码可以就请读者参考actionRPG。
|
||||
首先
|
||||
```
|
||||
//GameplayAbilityComponent指针
|
||||
class URPGAbilitySystemComponent* AbilitySystemComponent;
|
||||
//因为继承IAbilitySystemInterface所以需要实现这个接口
|
||||
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
|
||||
//用以判断这个Actor是否初始化了GameplayAbilityComponent
|
||||
bool bAbilitiesInitialized;
|
||||
//用于存储GameplayAbility(角色技能)的容器,并且会在游戏开始时向AbilitySystemComponent进行注册
|
||||
//读者可以在看了我下一篇文章再添加
|
||||
TArray<TSubclassOf<URPGGameplayAbility>> GameplayAbilities;
|
||||
```
|
||||
除此之外,actionRPG还重写了PossessedBy,以此来实现向AbilitySystemComponent注册Ability的功能。Wiki上的教程选择在Beginplay事件中进行注册,之后再PossessedBy中进行刷新(判断技能TArray是否有变化,如果有就进行相应得修改)。在本教程中,我选择actionRPG的方案。
|
||||
```
|
||||
ARPGCharacterBase::ARPGCharacterBase()
|
||||
{
|
||||
AbilitySystemComponent = CreateDefaultSubobject<URPGAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
|
||||
|
||||
bAbilitiesInitialized = false;
|
||||
}
|
||||
|
||||
void ARPGCharacterBase::AddStartupGameplayAbilities()
|
||||
{
|
||||
if (!bAbilitiesInitialized)
|
||||
{
|
||||
for (TSubclassOf<URPGGameplayAbility>& StartupAbility : GameplayAbilities)
|
||||
{
|
||||
AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(StartupAbility,1,INDEX_NONE,this));
|
||||
}
|
||||
bAbilitiesInitialized = true;
|
||||
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("%s"), *FString("All Ablities registered"));
|
||||
}
|
||||
}
|
||||
|
||||
//RemoveStartupGameplayAbilities在actionRPG中的SetCharacterLevel函数中被调用
|
||||
//但这个函数和GameplayAbility框架关系不大,我就省略了
|
||||
void ARPGCharacterBase::RemoveStartupGameplayAbilities()
|
||||
{
|
||||
if (bAbilitiesInitialized)
|
||||
{
|
||||
TArray<FGameplayAbilitySpecHandle> AbilitiesToRemove;
|
||||
for (const FGameplayAbilitySpec& Spec : AbilitySystemComponent->GetActivatableAbilities())
|
||||
{
|
||||
if ((Spec.SourceObject == this) && GameplayAbilities.Contains(Spec.Ability->GetClass()))
|
||||
{
|
||||
AbilitiesToRemove.Add(Spec.Handle);
|
||||
}
|
||||
}
|
||||
|
||||
for (int32 i = 0; i < AbilitiesToRemove.Num(); i++)
|
||||
{
|
||||
AbilitySystemComponent->ClearAbility(AbilitiesToRemove[i]);
|
||||
}
|
||||
bAbilitiesInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ARPGCharacterBase::PossessedBy(AController* NewController)
|
||||
{
|
||||
Super::PossessedBy(NewController);
|
||||
if (AbilitySystemComponent)
|
||||
{
|
||||
AddStartupGameplayAbilities();
|
||||
AbilitySystemComponent->InitAbilityActorInfo(this, this);
|
||||
}
|
||||
}
|
||||
```
|
||||
## UAbilitySystemComponent中的委托
|
||||
```
|
||||
/** Used to register callbacks to ability-key input */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityAbilityKey, /*UGameplayAbility*, Ability, */int32, InputID);
|
||||
|
||||
/** Used to register callbacks to confirm/cancel input */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FAbilityConfirmOrCancel);
|
||||
|
||||
/** Delegate for when an effect is applied */
|
||||
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnGameplayEffectAppliedDelegate, UAbilitySystemComponent*, const FGameplayEffectSpec&, FActiveGameplayEffectHandle);
|
||||
|
||||
/** Called on server whenever a GE is applied to self. This includes instant and duration based GEs. */
|
||||
FOnGameplayEffectAppliedDelegate OnGameplayEffectAppliedDelegateToSelf;
|
||||
|
||||
/** Called on server whenever a GE is applied to someone else. This includes instant and duration based GEs. */
|
||||
FOnGameplayEffectAppliedDelegate OnGameplayEffectAppliedDelegateToTarget;
|
||||
|
||||
/** Called on both client and server whenever a duraton based GE is added (E.g., instant GEs do not trigger this). */
|
||||
FOnGameplayEffectAppliedDelegate OnActiveGameplayEffectAddedDelegateToSelf;
|
||||
|
||||
/** Called on server whenever a periodic GE executes on self */
|
||||
FOnGameplayEffectAppliedDelegate OnPeriodicGameplayEffectExecuteDelegateOnSelf;
|
||||
|
||||
/** Called on server whenever a periodic GE executes on target */
|
||||
FOnGameplayEffectAppliedDelegate OnPeriodicGameplayEffectExecuteDelegateOnTarget;
|
||||
|
||||
/** Register for when an attribute value changes */
|
||||
FOnGameplayAttributeValueChange& GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute);
|
||||
|
||||
/** Callback anytime an ability is ended */
|
||||
FAbilityEnded AbilityEndedCallbacks;
|
||||
|
||||
/** Called with a failure reason when an ability fails to execute */
|
||||
FAbilityFailedDelegate AbilityFailedCallbacks;
|
||||
```
|
@@ -0,0 +1,98 @@
|
||||
## 前言
|
||||
之前使用GameplayTasks的事件处理会出现延迟问题。具体体现在如果使用机制来添加BlockAbilityTag,在狂按技能的情况下会出现Tag不能正常生成导致可以不停ActivateAbility的问题。
|
||||
|
||||
## 问题成因
|
||||
1. 问题的原因就是使用GameplayTasks不能即时添加GE,会有一定延迟。
|
||||
2. 清除GameplayEffect方式有问题,应该使用只清除该技能所附加的GE。
|
||||
|
||||
## 解决思路
|
||||
在解决清除GE问题的情况下:
|
||||
|
||||
1. GASDocument中的解决方法:使用AbilityTag作为BlockAbilityTag,之后通过AnimNotify发送EndAbility事件Tag,提前结束Ability。(不会结束掉Montage播放)
|
||||
2. 在AnimNotifyBegin函数中直接添加GE,根据TotalDuration设置持续时间。(但如果Montage突然被中断,就很难操作了,而且这样会与Ability耦合)
|
||||
3. 增加公共CD(测试过效果不佳)
|
||||
|
||||
## PlayMontage修改
|
||||
1. 传递事件使用专门的Event.Montage.xxxx作为事件标签
|
||||
2. 重写AnimNotify的GetNotifyName函数,使用标签名作为AnimNotify的显示名称。
|
||||
|
||||
|
||||
```
|
||||
void UGDGA_FireGun::EventReceived(FGameplayTag EventTag, FGameplayEventData EventData)
|
||||
{
|
||||
// Montage told us to end the ability before the montage finished playing.
|
||||
// Montage was set to continue playing animation even after ability ends so this is okay.
|
||||
if (EventTag == FGameplayTag::RequestGameplayTag(FName("Event.Montage.EndAbility")))
|
||||
{
|
||||
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only spawn projectiles on the Server.
|
||||
// Predicting projectiles is an advanced topic not covered in this example.
|
||||
if (GetOwningActorFromActorInfo()->GetLocalRole() == ROLE_Authority && EventTag == FGameplayTag::RequestGameplayTag(FName("Event.Montage.SpawnProjectile")))
|
||||
{
|
||||
AGDHeroCharacter* Hero = Cast<AGDHeroCharacter>(GetAvatarActorFromActorInfo());
|
||||
if (!Hero)
|
||||
{
|
||||
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
|
||||
}
|
||||
|
||||
FVector Start = Hero->GetGunComponent()->GetSocketLocation(FName("Muzzle"));
|
||||
FVector End = Hero->GetCameraBoom()->GetComponentLocation() + Hero->GetFollowCamera()->GetForwardVector() * Range;
|
||||
FRotator Rotation = UKismetMathLibrary::FindLookAtRotation(Start, End);
|
||||
|
||||
FGameplayEffectSpecHandle DamageEffectSpecHandle = MakeOutgoingGameplayEffectSpec(DamageGameplayEffect, GetAbilityLevel());
|
||||
|
||||
// Pass the damage to the Damage Execution Calculation through a SetByCaller value on the GameplayEffectSpec
|
||||
DamageEffectSpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), Damage);
|
||||
|
||||
FTransform MuzzleTransform = Hero->GetGunComponent()->GetSocketTransform(FName("Muzzle"));
|
||||
MuzzleTransform.SetRotation(Rotation.Quaternion());
|
||||
MuzzleTransform.SetScale3D(FVector(1.0f));
|
||||
|
||||
FActorSpawnParameters SpawnParameters;
|
||||
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
AGDProjectile* Projectile = GetWorld()->SpawnActorDeferred<AGDProjectile>(ProjectileClass, MuzzleTransform, GetOwningActorFromActorInfo(),
|
||||
Hero, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
|
||||
Projectile->DamageEffectSpecHandle = DamageEffectSpecHandle;
|
||||
Projectile->Range = Range;
|
||||
Projectile->FinishSpawning(MuzzleTransform);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
/** Apply a gameplay effect to the owner of this ability */
|
||||
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName="ApplyGameplayEffectToOwner", meta=(ScriptName="ApplyGameplayEffectToOwner"))
|
||||
FActiveGameplayEffectHandle BP_ApplyGameplayEffectToOwner(TSubclassOf<UGameplayEffect> GameplayEffectClass, int32 GameplayEffectLevel = 1, int32 Stacks = 1);
|
||||
|
||||
/** Non blueprintcallable, safe to call on CDO/NonInstance abilities */
|
||||
FActiveGameplayEffectHandle ApplyGameplayEffectToOwner(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const UGameplayEffect* GameplayEffect, float GameplayEffectLevel, int32 Stacks = 1) const;
|
||||
|
||||
/** Apply a previously created gameplay effect spec to the owner of this ability */
|
||||
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName = "ApplyGameplayEffectSpecToOwner", meta=(ScriptName = "ApplyGameplayEffectSpecToOwner"))
|
||||
FActiveGameplayEffectHandle K2_ApplyGameplayEffectSpecToOwner(const FGameplayEffectSpecHandle EffectSpecHandle);
|
||||
|
||||
FActiveGameplayEffectHandle ApplyGameplayEffectSpecToOwner(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle) const;
|
||||
|
||||
// -------------------------------------
|
||||
// Apply Gameplay effects to Target
|
||||
// -------------------------------------
|
||||
|
||||
/** Apply a gameplay effect to a Target */
|
||||
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName = "ApplyGameplayEffectToTarget", meta=(ScriptName = "ApplyGameplayEffectToTarget"))
|
||||
TArray<FActiveGameplayEffectHandle> BP_ApplyGameplayEffectToTarget(FGameplayAbilityTargetDataHandle TargetData, TSubclassOf<UGameplayEffect> GameplayEffectClass, int32 GameplayEffectLevel = 1, int32 Stacks = 1);
|
||||
|
||||
/** Non blueprintcallable, safe to call on CDO/NonInstance abilities */
|
||||
TArray<FActiveGameplayEffectHandle> ApplyGameplayEffectToTarget(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayAbilityTargetDataHandle& Target, TSubclassOf<UGameplayEffect> GameplayEffectClass, float GameplayEffectLevel, int32 Stacks = 1) const;
|
||||
|
||||
/** Apply a previously created gameplay effect spec to a target */
|
||||
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName = "ApplyGameplayEffectSpecToTarget", meta=(ScriptName = "ApplyGameplayEffectSpecToTarget"))
|
||||
TArray<FActiveGameplayEffectHandle> K2_ApplyGameplayEffectSpecToTarget(const FGameplayEffectSpecHandle EffectSpecHandle, FGameplayAbilityTargetDataHandle TargetData);
|
||||
|
||||
TArray<FActiveGameplayEffectHandle> ApplyGameplayEffectSpecToTarget(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle, const FGameplayAbilityTargetDataHandle& TargetData) const;
|
||||
|
||||
```
|
@@ -0,0 +1,196 @@
|
||||
## 前言
|
||||
TargetActor是GAS用于获取场景中物体、空间、位移等数据的机制,同时也可以用于制作可视化debug工具。,所以非常有必要掌握它。
|
||||
|
||||
一般流程为:使用WaitTargetData_AbilityTask生成TargetActor,之后通过TargetActor的内部函数或者射线获取场景信息,最后通过委托传递携带这些信息构建的FGameplayAbilityTargetDataHandle。
|
||||
|
||||
本文部分描述摘自GASDocumentation_Chinese,翻译的还不错,也请大家给此项目点赞。
|
||||
## TargetData
|
||||
TargetData也就是FGameplayAbilityTargetData是用于通过网络传输定位数据的通用结构体。它主要用于存储目标数据(一般是TArray<TWeakObjectPtr<AActor> >)、FHitResult。当然可以传递一些自定义数据,这个可以参考源码中的FGameplayAbilityTargetData_LocationInfo。
|
||||
|
||||
TargetData一般由TargetActor或者手动创建(较少), 供AbilityTask或者GameplayEffect通过EffectContext使用. 因为其位于EffectContext中,所以Execution,MMC,GameplayCue和AttributeSet的后处理函数都可以访问该TargetData.
|
||||
|
||||
GAS不会直接传递数据,而是会借助FGameplayAbilityTargetDataHandle来进行传递。具体过程是
|
||||
1. 创建TargetData,并且填充数据。
|
||||
2. 创建FGameplayAbilityTargetDataHandle对象(也可以使用带参的构造函数直接构建),并且调用Add()添加上面创建的TargetData。
|
||||
3. 进行传递。
|
||||
|
||||
源代码中GameplayAbilityTargetTypes.h中实现了以下TargetData:
|
||||
- FGameplayAbilityTargetData_LocationInfo
|
||||
- FGameplayAbilityTargetData_ActorArray
|
||||
- FGameplayAbilityTargetData_SingleTargetHit
|
||||
|
||||
基本上是够用了,如果需要创建新的TargetData类型,就需要视携带的数据类型实现以下虚函数:
|
||||
```c++
|
||||
virtual TArray<TWeakObjectPtr<AActor>> GetActors() const;
|
||||
virtual bool SetActors(TArray<TWeakObjectPtr<AActor>> NewActorArray);
|
||||
|
||||
virtual bool HasHitResult() const;
|
||||
virtual const FHitResult* GetHitResult();
|
||||
virtual void ReplaceHitWith(AActor* NewHitActor, const FHitResult* NewHitResult)
|
||||
|
||||
virtual bool HasOrigin() const;
|
||||
virtual FTransform GetOrigin() const;
|
||||
|
||||
virtual bool HasEndPoint() const;
|
||||
virtual FVector GetEndPoint() const;
|
||||
|
||||
virtual FTransform GetEndPointTransform() const;
|
||||
|
||||
/** See notes on delegate definition FOnTargetActorSwapped */
|
||||
virtual bool ShouldCheckForTargetActorSwap() const;
|
||||
```
|
||||
debug相关虚函数
|
||||
```c++
|
||||
/** Returns the serialization data, must always be overridden */
|
||||
virtual UScriptStruct* GetScriptStruct() const
|
||||
{
|
||||
return FGameplayAbilityTargetData::StaticStruct();
|
||||
}
|
||||
|
||||
/** Returns a debug string representation */
|
||||
virtual FString ToString() const;
|
||||
```
|
||||
源代码在实现完类型后,还有附带下面这一段代码,看注释应该和网络同步序列化有关,反正依瓢画葫芦复制+替换类型名称即可。
|
||||
```c++
|
||||
template<>
|
||||
struct TStructOpsTypeTraits<FGameplayAbilityTargetData_ActorArray> : public TStructOpsTypeTraitsBase2<FGameplayAbilityTargetData_ActorArray>
|
||||
{
|
||||
enum
|
||||
{
|
||||
WithNetSerializer = true // For now this is REQUIRED for FGameplayAbilityTargetDataHandle net serialization to work
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## TargetActor
|
||||
你可以把TargetActor理解为一个场景信息探测器,用来获取场景中数据以及进行可视化Debug。一般都是在Ability中通过AbilityTask_WaitTargetData来生成TargetActor(WaitTargetDataUsingActor用来监听已有的TargetActor),之后再通过ValidData委托接收TargetData。
|
||||
|
||||
## GameplayAbilityWorldReticles
|
||||
当使用non-Instant TargetActor定位时,TargetActor可以选择使用ReticleActor(GameplayAbilityWorldReticles)标注当前目标。
|
||||
|
||||
默认情况下,Reticle只会显示在TargetActor的当前有效Target上,如果想要让其显示在其他目标上,就需要自定义TargetActor,手动管理确定与取消事件,让目标持久化。
|
||||
|
||||
ReticleActor可以通过FWorldReticleParameters进行初始化(在TargetActor设置FWorldReticleParameters变量),但FWorldReticleParameters只有一个AOEScale变量,很明显完全不够用。所以你可以通过自定义ActorTarget与参数结构来改进这个功能。
|
||||
|
||||
Reticle默认是不可同步的,但是如果你想向其他玩家展示本地玩家正在定位的目标,那么它也可以被设置为可同步的。
|
||||
|
||||
ReticleActor还带有一些面向蓝图的BlueprintImplementableEvents:
|
||||
```c++
|
||||
/** Called whenever bIsTargetValid changes value. */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
|
||||
void OnValidTargetChanged(bool bNewValue);
|
||||
|
||||
/** Called whenever bIsTargetAnActor changes value. */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
|
||||
void OnTargetingAnActor(bool bNewValue);
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
|
||||
void OnParametersInitialized();
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
|
||||
void SetReticleMaterialParamFloat(FName ParamName, float value);
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
|
||||
void SetReticleMaterialParamVector(FName ParamName, FVector value);
|
||||
```
|
||||
|
||||
GAS默认的UAbilityTask_WaitTargetData节点会在FinalizeTargetActor()中调用AGameplayAbilityTargetActor_Trace::StartTargeting(),进行ReticleActor的Spawn。
|
||||
|
||||
GASShooter中的逻辑为:
|
||||
1. GA_RocketLauncherSecondaryAAset在Configure节点中指定为ReticleClass为BP_SingleTargetReticle。
|
||||
2. 在UGSAT_WaitTargetDataUsingActor::FinalizeTargetActor()中调用StartTargeting()
|
||||
3. 最后会在AGSGATA_Trace(GASShooter中定义的TargetActor的基类)的StartTargeting()中进行Spawn。
|
||||
|
||||
GASShooter中的实现了BP_SingleTargetReticle,大致实现为:
|
||||
1. 在根组件下连接一个WidgetComponent
|
||||
2. 使用Widget3DPassThrough_Masked_OneSided材质,并且绑定一个UI_TargetReticle UMGAsset
|
||||
|
||||
## GameplayEffectContext
|
||||
可以认为这是一个传递数据用的结构体。GameplayEffectContext结构体存有关于GameplayEffectSpec创建者(Instigator)和TargetData的信息,可以向ModifierMagnitudeCalculation/GameplayEffectExecutionCalculation, AttributeSet和GameplayCue之间传递任意数据.
|
||||
|
||||
其他案例:
|
||||
https://www.thegames.dev/?p=62
|
||||
|
||||
使用方法:
|
||||
1. 继承FGameplayEffectContext。
|
||||
2. 重写FGameplayEffectContext::GetScriptStruct()。
|
||||
3. 重写FGameplayEffectContext::Duplicate()。
|
||||
4. 如果新数据需要同步的话, 重写FGameplayEffectContext::NetSerialize()。
|
||||
5. 对子结构体实现TStructOpsTypeTraits, 就像父结构体FGameplayEffectContext有的那样.
|
||||
6. 在AbilitySystemGlobals类中重写AllocGameplayEffectContext()以返回一个新的子结构体对象。(AbilitySystemGlobals还需要注册,请看下节)
|
||||
|
||||
```c++
|
||||
FGameplayEffectContext* UGSAbilitySystemGlobals::AllocGameplayEffectContext() const
|
||||
{
|
||||
return new FGSGameplayEffectContext();
|
||||
}
|
||||
```
|
||||
|
||||
在ExecutionCalculation中,你可以通过FGameplayEffectCustomExecutionParameters获取FGameplayEffectContext:
|
||||
```c++
|
||||
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
|
||||
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(Spec.GetContext().Get());
|
||||
```
|
||||
在GameplayEffectSpec中获取EffectContext:
|
||||
```c++
|
||||
FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
|
||||
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(MutableSpec->GetContext().Get());
|
||||
```
|
||||
在ExecutionCalculation中修改GameplayEffectSpec时要小心.参看GetOwningSpecForPreExecuteMod()的注释.
|
||||
```c++
|
||||
/** Non const access. Be careful with this, especially when modifying a spec after attribute capture. */
|
||||
FGameplayEffectSpec* GetOwningSpecForPreExecuteMod() const;
|
||||
```
|
||||
|
||||
PS.GASShooter实现了带有FGameplayAbilityTargetDataHandle变量的FGSGameplayEffectContext,以此来实现在GameplayCue中访问TargetData, 特别是对于霰弹枪来说十分有用, 因为它可以击打多个敌人并且产生效果。
|
||||
|
||||
## InitGlobalData()
|
||||
>从UE 4.24开始, 必须调用UAbilitySystemGlobals::InitGlobalData()来使用TargetData, 否则你会遇到关于ScriptStructCache的错误, 并且客户端会从服务端断开连接, 该函数只需要在项目中调用一次. Fortnite从AssetManager类的起始加载函数中调用该函数, Paragon是从UEngine::Init()中调用的. 我发现将其放到UEngineSubsystem::Initialize()是个好位置, 这也是样例项目中使用的. 我觉得你应该复制这段模板代码到你自己的项目中以避免出现TargetData的使用问题.
|
||||
|
||||
>如果你在使用AbilitySystemGlobals GlobalAttributeSetDefaultsTableNames时发生崩溃, 可能之后需要像Fortnite一样在AssetManager或GameInstance中调用UAbilitySystemGlobals::InitGlobalData()而不是在UEngineSubsystem::Initialize()中. 该崩溃可能是由于Subsystem的加载顺序引发的, GlobalAttributeDefaultsTables需要加载EditorSubsystem来绑定UAbilitySystemGlobals::InitGlobalData()中的委托.
|
||||
|
||||
上面说的意思是初始化需要通过Subsystem与其节点来进行初始化(这样就不需要通过继承来实现),GASDocument推荐在UEngineSubsystem(UEngine阶段)初始化时执行。下面是自定义AbilitySystemGlobals的注册方式。
|
||||
```c++
|
||||
UAbilitySystemGlobals::Get().InitGlobalData();
|
||||
```
|
||||
可以参考UGSEngineSubsystem。
|
||||
|
||||
>AbilitySystemGlobals类保存有关GAS的全局信息. 大多数变量可以在DefaultGame.ini中设置. 一般你不需要和该类互动, 但是应该知道它的存在. 如果你需要继承像GameplayCueManager或GameplayEffectContext这样的对象, 就必须通过AbilitySystemGlobals来做.
|
||||
|
||||
>想要继承AbilitySystemGlobals, 需要在DefaultGame.ini中设置类名:
|
||||
```ini
|
||||
[/Script/GameplayAbilities.AbilitySystemGlobals]
|
||||
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"
|
||||
```
|
||||
|
||||
## RPGTargetType
|
||||
这是GameplayEffectContainer提供的一种方便的产生TargetData方法。但因为使用CDO(Class Default Object)运行,所以不支持用户输入与取消等功能,功能上也不如TargetActor多。在官方的ActionRPG可以看到演示。这些Target类型定义于URPGTargetType,你可以根据获取Target的方式进行拓展,调用TargetType获取目标的逻辑位于MakeEffectContainerSpecFromContainer:
|
||||
```c++
|
||||
if (Container.TargetType.Get())
|
||||
{
|
||||
TArray<FHitResult> HitResults;
|
||||
TArray<AActor *> TargetActors;
|
||||
const URPGTargetType *TargetTypeCDO = Container.TargetType.GetDefaultObject();
|
||||
AActor *AvatarActor = GetAvatarActorFromActorInfo();
|
||||
TargetTypeCDO->GetTargets(OwningCharacter, AvatarActor, EventData, HitResults, TargetActors);
|
||||
ReturnSpec.AddTargets(HitResults, TargetActors);
|
||||
}
|
||||
```
|
||||
ActionRPG在c++中实现了UseOwner与UseEventData类型,在蓝图中实现了SphereTrace与LineTrace。
|
||||
|
||||
### GASShooter中的用法
|
||||
GASShooter在以下Asset中使用了TargetData与TargetActor
|
||||
- GA_RiflePrimaryInstant
|
||||
- GA_RocketLauncherPrimaryInstant
|
||||
- GA_RocketLauncherPrimaryInstant
|
||||
|
||||
GASShooter中实现了AGSGATA_LineTrace与AGSGATA_SphereTrace等AGameplayAbilityTargetActor(拥有共同的父类AGSGATA_Trace)。
|
||||
|
||||
步骤:
|
||||
在AbilityActivate中
|
||||
1. 类内指定AGSGATA_LineTrace对象作为变量。
|
||||
2. 调用AGSGATA_LineTrace对象的ResetSpread(),初始化参数。
|
||||
3. 调用UGSAT_ServerWaitForClientTargetData,并将ValidData委托绑定HandleTargetData。
|
||||
4. 之后执行客户端逻辑。
|
||||
5. 调用AGSGATA_LineTrace对象的Configure(),来设置具体参数。
|
||||
6. 将AGSGATA_LineTrace对象传入UGSAT_WaitTargetDataUsingActor,并将ValidData委托绑定HandleTargetData。
|
187
03-UnrealEngine/Gameplay/GAS/(12)GAS中的传递数据、解耦与其他技巧.md
Normal file
187
03-UnrealEngine/Gameplay/GAS/(12)GAS中的传递数据、解耦与其他技巧.md
Normal file
@@ -0,0 +1,187 @@
|
||||
## GAS中的传递数据、解耦与其他技巧
|
||||
本文大部分内容来着GASDocumentation与GASDocumentation_Chinese,因为感觉很重要所以在此简单归纳一下。
|
||||
|
||||
https://github.com/tranek/GASDocumentation
|
||||
https://github.com/BillEliot/GASDocumentation_Chinese
|
||||
<!--more-->
|
||||
|
||||
## 响应GameplayTags的变化
|
||||
这个没什么好说的,直接上GASDocumentation_Chinese中的解释:
|
||||
>ASC提供了一个委托(Delegate)用于在GameplayTag添加或移除时触发, 其中EGameplayTagEventType参数可以明确是该GameplayTag添加/移除还是其TagMapCount发生变化时触发.
|
||||
```
|
||||
AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);
|
||||
```
|
||||
回调函数拥有变化的GameplayTag参数和新的TagCount参数.
|
||||
```
|
||||
virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
|
||||
```
|
||||
|
||||
## 向GameplayAbility传递数据
|
||||
一共有4中方法:
|
||||
- 通过设置Ability中的Trigger,之后再在ActivateAbilityFromEvent事件中进行处理。
|
||||
- 使用WaitGameplayEvent之类的AbilityTask对接收到的事件进行处理。
|
||||
- 使用TargetData。
|
||||
- 存储数据在OwnerActor或者AvatarActor中,之后再通过ActorInfo中的Owner或者Avatar来获取数据。
|
||||
|
||||
前两种都可以通过往Event中添加Payload来传递额外信息。Payload可以使用自定义的UObject。TargetData涉及到FGameplayAbilityTargetData、AGameplayAbilityTargetActor等东西,下一篇文章会说。
|
||||
|
||||
## 通过Event/Tag来激活GameplayAbility
|
||||
Ability Trigger是一种不错的解耦方式,大致的使用步骤:
|
||||
1. 在Ability中的设置Trigger中的Ability Trigger(在Class Defaults中)。
|
||||
2. 实现ActivateAbilityFromEvent事件,通过Event中Payload获取自定义UObject中的信息。
|
||||
3. Event的触发方式:通过调用SendGameplayEventToActor()向对应拥有ASC的Actor/Character发送GameplayEvent来触发对应Ability。
|
||||
4. Tag的触发方式:通过GamplayEffect来添加对应Tag来触发对应Ability。(AddLooseGameplayTag()我没试过,但估计也是可以的)
|
||||
|
||||
使用Tag的好处在于Owned Tag Present,即让Ability与Tag保持一样的生命周期,不过很可惜Tag的触发方式并不支持Ability的AbilityTags。注册过程位于ASC的OnGiveAbility()中,主要是通过绑定MonitoredTagChanged事件来实现触发。
|
||||
|
||||
PS.本人使用这个功能实现了FightingStateHandle,即进入战斗从背上拔出武器,离开战斗则将武器放回背上。
|
||||
### GASShooter中的另类用法
|
||||
这里我还想说一下GASShooter中使用Ability Trigger机制的另类用法:Interact,项目中宝箱与按钮门都使用了它。
|
||||
这个系统主要由GA_InteractPassive、GA_InteractActive以及IGSInteractable接口。
|
||||
|
||||
#### IGSInteractable
|
||||
首先所有可以互动的物体都需要继承并且按照对应需求实现对应的几个接口函数:
|
||||
- IsAvailableForInteraction
|
||||
- GetInteractionDuration
|
||||
- GetPreInteractSyncType
|
||||
- GetPostInteractSyncType
|
||||
- PreInteract
|
||||
- PostInteract
|
||||
- CancelInteraction
|
||||
- RegisterInteracter
|
||||
- UnregisterInteracter
|
||||
- InteractableCancelInteraction
|
||||
|
||||
#### GA_InteractPassive与GA_InteractActive
|
||||
GA_InteractPassive与GA_InteractActive会在游戏开始时,在BP_HeroCharacter中注册。
|
||||
|
||||
GA_InteractPassive是一个GrantAbility,它会在游戏开始就被激活。它会执行UGSAT_WaitInteractableTarget,不断地进行射线判断。如果有可用的Interactable目标出现就会进行一些系列的判断,在绑定的InputAction按下后,最终给角色类ASC发送GameplayTask中设置的GameplayEvent(Payload中带有Interactable目标的TargetData)。
|
||||
|
||||
因为GA_InteractActive中设置了Ability Trigger,所以它会接收到GameplayEvent并且处理。Interactable触发后的逻辑都在这里处理(通过TargetData调用IGSInteractable接口函数)。
|
||||
|
||||
## GameplayEffect的GrantAbilities
|
||||
在GameplayEffect也有类似Ability Trigger,就是GrantAbilities,并且有多种移除Ability方式以供选择。但GameplayEffect需要符合一下要求:
|
||||
- StackingType需要为AggregateBySource/AggregateByTarget
|
||||
- DurationType需要为HasDuration/Infinite
|
||||
- 设置的Ability必须没有注册过(GiveAbility)
|
||||
|
||||
同时GameplayAbility类还需要实现OnAvatarSet()虚函数,该函数主要用于处理GrantAbility:
|
||||
```
|
||||
UCLASS()
|
||||
class RPGGAMEPLAYABILITY_API URPGGameplayAbility : public UGameplayAbility
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
{
|
||||
virtual void OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec) override;
|
||||
protected:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "RPGGameplayAbility")
|
||||
bool ActivateAbilityOnGranted;
|
||||
};
|
||||
```
|
||||
```
|
||||
void URPGGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec)
|
||||
{
|
||||
|
||||
if (ActivateAbilityOnGranted)
|
||||
{
|
||||
ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, true);
|
||||
}
|
||||
}
|
||||
```
|
||||
其中ActivateAbilityOnGranted用于判断这个Ability是不是GrantAbility,不然所有所有的Ability都会调用这个函数。
|
||||
|
||||
## 运行时创建动态GameplayEffect
|
||||
```
|
||||
void UGameplayAbilityRuntimeGE::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
|
||||
{
|
||||
if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
|
||||
{
|
||||
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
|
||||
{
|
||||
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
|
||||
}
|
||||
|
||||
// Create the GE at runtime.
|
||||
UGameplayEffect* GameplayEffect = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("RuntimeInstantGE"));
|
||||
GameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant; // Only instant works with runtime GE.
|
||||
|
||||
const int32 Idx = GameplayEffect->Modifiers.Num();
|
||||
GameplayEffect->Modifiers.SetNum(Idx + 1);
|
||||
FGameplayModifierInfo& ModifierInfo = GameplayEffect->Modifiers[Idx];
|
||||
ModifierInfo.Attribute.SetUProperty(UMyAttributeSet::GetMyModifiedAttribute());
|
||||
ModifierInfo.ModifierMagnitude = FScalableFloat(42.f);
|
||||
ModifierInfo.ModifierOp = EGameplayModOp::Override;
|
||||
|
||||
// Apply the GE.
|
||||
FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(GameplayEffect, {}, 0.f); // "new", since lifetime is managed by a shared ptr within the handle
|
||||
ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, FGameplayEffectSpecHandle(GESpec));
|
||||
}
|
||||
EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
|
||||
}
|
||||
```
|
||||
|
||||
## 延长GE的持续时间
|
||||
在GASDocument的4.5.16 中介绍了这个方法,直接上代码了:
|
||||
```
|
||||
bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
|
||||
{
|
||||
if (!Handle.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
|
||||
if (!ActiveGameplayEffect)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FActiveGameplayEffect* AGE = const_cast<FActiveGameplayEffect*>(ActiveGameplayEffect);
|
||||
if (NewDuration > 0)
|
||||
{
|
||||
AGE->Spec.Duration = NewDuration;
|
||||
}
|
||||
else
|
||||
{
|
||||
AGE->Spec.Duration = 0.01f;
|
||||
}
|
||||
|
||||
AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
|
||||
AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
|
||||
AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
|
||||
ActiveGameplayEffects.MarkItemDirty(*AGE);
|
||||
ActiveGameplayEffects.CheckDuration(Handle);
|
||||
|
||||
AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
|
||||
OnGameplayEffectDurationChange(*AGE);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
PS.本人使用使用这个技巧来刷新战斗状态的持续时间。
|
||||
|
||||
## 其他技巧
|
||||
### 添加给ACS附加标签(不通过GE与GA)
|
||||
这个方法不适合联机,只在本地起效果。可以作为一些用于本地效果控制。
|
||||
```
|
||||
FGameplayTag DeadTag = FGameplayTag::RequestGameplayTag(FName("Status.Dead"));
|
||||
AbilitySystemComponent->AddLooseGameplayTag(DeadTag);
|
||||
```
|
||||
|
||||
### 动态修改Ability等级
|
||||
- 移除旧Ability之后再添加新Ability
|
||||
- 增加GameplayAbilitySpec的等级:在服务端上找到GameplayAbilitySpec, 增加它的等级, 并将其标记为Dirty以同步到所属(Owning)客户端。
|
||||
|
||||
### AbilityInputID绑定
|
||||
GASDocument的项目定义了EGSAbilityInputID枚举用于定义所有Ability的输入绑定(名字需要与InputAction一一对应),并且在UGSGameplayAbility中设置AbilityInputID变量,用于输入绑定。
|
||||
```
|
||||
for (TSubclassOf<UGSGameplayAbility>& StartupAbility : CharacterAbilities)
|
||||
{
|
||||
AbilitySystemComponent->GiveAbility(
|
||||
FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
|
||||
}
|
||||
```
|
||||
这里还通过AbilityLevel来传递AbilityID。
|
||||
|
||||
PS.但本人感觉这个方法在一些动作游戏中这个方法可能会不太合适。
|
116
03-UnrealEngine/Gameplay/GAS/(13)附魔与宝石系统实现与给GE GA传参方法.md
Normal file
116
03-UnrealEngine/Gameplay/GAS/(13)附魔与宝石系统实现与给GE GA传参方法.md
Normal file
@@ -0,0 +1,116 @@
|
||||
## DataAsset与UObject
|
||||
数据资产的巨大优势在于,你可以将它们设置为自动在引擎中注册,所以每次你添加一个新的数据资产时,引擎都会自动将其添加到一个列表中。因此,如果你的武器是数据资产,当你在游戏中添加一个新的武器时,它就会自动添加到一个你可以搜索的列表中,所以你不需要每次添加一个新的武器时在你的代码中手动添加绑定。
|
||||
|
||||
如果不需要这种类型的东西,把它变成数据资产就没有什么好处。
|
||||
|
||||
## 附魔
|
||||
附魔分为临时附魔与永久附魔。临时附魔以Buffer的方式存在,其Handle存储在角色类中。永久附魔应用后的Handle则存储在所依附的物品类(武器或者身上的其他装备)中,用于解除装备后去除GE。
|
||||
|
||||
### 存在问题
|
||||
1. 如果武器存在属性,附魔也存在属性,那伤害该如何计算?
|
||||
|
||||
## 宝石(镶嵌物)
|
||||
镶嵌在有限的武器槽中,增加角色属性或者改变武器(攻击)属性。
|
||||
|
||||
### 实现方式
|
||||
在创建新的 Inlay 物品类,里面存储GameplayEffect用于调整角色属性与武器(攻击)属性。应用后的Handle存储在所依附的物品类(武器或者身上的其他装备)中,用于解除装备后去除GE。
|
||||
|
||||
武器的镶嵌物与附魔的初始信息存储在DataAsset中,而玩家runtime与存档信息则通过实例化的URPGWeaponItem来存储。WeaponActor保存对应的GEHandle,数据全都是临时性的,实现对应的方法。
|
||||
|
||||
## GASDocument的武器数值实现
|
||||
- 在物品上使用原始数值变量(推荐)
|
||||
- 在物品类上使用独立的AttributeSet
|
||||
- 在物品类上使用独立的ASC
|
||||
|
||||
具体可以参考GASShooter项目。不使用AttributeSet,直接将数值存在在武器类或者GameplayABility中内。
|
||||
|
||||
### 运行时添加与移除属性级
|
||||
```
|
||||
//On weapon add to inventory:
|
||||
AbilitySystemComponent->SpawnedAttributes.AddUnique(WeaponAttributeSetPointer);
|
||||
AbilitySystemComponent->ForceReplication();
|
||||
|
||||
//On weapon remove from inventory:
|
||||
AbilitySystemComponent->SpawnedAttributes.Remove(WeaponAttributeSetPointer);
|
||||
AbilitySystemComponent->ForceReplication();
|
||||
```
|
||||
|
||||
### 各个方案的好处与坏处
|
||||
使用原始数值变量
|
||||
好处:
|
||||
- 避免使用限制AttributeSets(见下文)
|
||||
限制:
|
||||
- 无法使用现有GameplayEffect工作流程(Cost GEs用于弹药等)
|
||||
- 需要工作来覆盖关键功能UGameplayAbility以检查和应用弹药成本对枪支的浮标
|
||||
|
||||
使用独立的AttributeSet
|
||||
好处:
|
||||
- 可以使用现有GameplayAbility和GameplayEffect工作流程(Cost GEs用于弹药等)
|
||||
- 设置非常小的项目集很简单
|
||||
限制:
|
||||
-您必须AttributeSet为每种武器类型创建一个新类。ASCs只能在功能上拥有一个AttributeSet类的实例,因为更改为在数组中Attribute查找其AttributeSet类的第一个实例ASCs SpawnedAttributes。同一AttributeSet类的其他实例将被忽略。
|
||||
- 由于之前AttributeSet每个AttributeSet类一个实例的原因,您只能在玩家的库存中拥有每种类型的武器之一。
|
||||
- 删除 anAttributeSet是危险的。在 GASShooter 中,如果玩家从火箭中自杀,玩家会立即从他的物品栏中移除火箭发射器(包括 中AttributeSet的ASC)。当服务器复制火箭发射器的弹药Attribute改变时,AttributeSet客户端上不再存在,ASC游戏崩溃。
|
||||
|
||||
|
||||
## 构造GameplayEffect并且修改参数
|
||||
DamageEffectSpecHandle存储着FGameplayEffectSpec的智能指针。通过MakeOutgoingGameplayEffectSpec创建并且修改参数之后再通过ApplyGameplayEffectSpecToTarget应用到指定目标。
|
||||
```
|
||||
UPROPERTY(BlueprintReadOnly, EditAnywhere)
|
||||
TSubclassOf<UGameplayEffect> DamageGameplayEffect;
|
||||
```
|
||||
```
|
||||
FGameplayEffectSpecHandle DamageEffectSpecHandle = MakeOutgoingGameplayEffectSpec(DamageGameplayEffect, GetAbilityLevel());
|
||||
// Pass the damage to the Damage Execution Calculation through a SetByCaller value on the GameplayEffectSpec
|
||||
DamageEffectSpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), Damage);
|
||||
```
|
||||
### 动态创建GE并且应用
|
||||
```
|
||||
// Create a dynamic instant Gameplay Effect to give the bounties
|
||||
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Bounty")));
|
||||
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;
|
||||
|
||||
int32 Idx = GEBounty->Modifiers.Num();
|
||||
GEBounty->Modifiers.SetNum(Idx + 2);
|
||||
|
||||
FGameplayModifierInfo& InfoXP = GEBounty->Modifiers[Idx];
|
||||
InfoXP.ModifierMagnitude = FScalableFloat(GetXPBounty());
|
||||
InfoXP.ModifierOp = EGameplayModOp::Additive;
|
||||
InfoXP.Attribute = UGDAttributeSetBase::GetXPAttribute();
|
||||
|
||||
FGameplayModifierInfo& InfoGold = GEBounty->Modifiers[Idx + 1];
|
||||
InfoGold.ModifierMagnitude = FScalableFloat(GetGoldBounty());
|
||||
InfoGold.ModifierOp = EGameplayModOp::Additive;
|
||||
InfoGold.Attribute = UGDAttributeSetBase::GetGoldAttribute();
|
||||
|
||||
Source->ApplyGameplayEffectToSelf(GEBounty, 1.0f, Source->MakeEffectContext());
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 持续刷新GE,以延长持续时间
|
||||
4.5.16 修改已激活GameplayEffect的持续时间
|
||||
|
||||
## AGameplayAbilityTargetActor
|
||||
4.11.1 Target Data
|
||||
4.11.2 Target Actor 用于获取场景中的信息,或者在场景中生成一些东西。
|
||||
|
||||
|
||||
## Input绑定
|
||||
如果你的ASC位于Character, 那么就在SetupPlayerInputComponent()中包含用于绑定到ASC的函数.
|
||||
|
||||
|
||||
|
||||
## GASDocument上常用的Abilty和Effect
|
||||
1. 眩晕(Stun)
|
||||
2. 奔跑(Sprint)
|
||||
3. 瞄准(Aim Down Sight)
|
||||
4. 生命偷取(Lifesteal)
|
||||
5. 在客户端和服务端中生成随机数
|
||||
6. 暴击(Critical Hits)
|
||||
7. 非堆栈GameplayEffect, 但是只有其最高级(Greatest Magnitude)才能实际影响Target
|
||||
8. 游戏暂停时生成TargetData
|
||||
9. 按钮交互系统(Button Interaction System)
|
52
03-UnrealEngine/Gameplay/GAS/(14)GameplayAbility Debug方法.md
Normal file
52
03-UnrealEngine/Gameplay/GAS/(14)GameplayAbility Debug方法.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 前言
|
||||
之前一直都没有研究过GAS如何debug,所以大致翻译了相关内容
|
||||
>https://github.com/tranek/GASDocumentation#debugging
|
||||
|
||||
中文翻译
|
||||
>https://blog.csdn.net/pirate310/article/details/106311256
|
||||
|
||||
# C++ Debug Tip
|
||||
在非DebugGame Editor模式下模式下,Ue4会对函数进行优化,这会对debug产生一定阻碍。解决方法是:
|
||||
1. 在VisualStudio中将解决方案设置(SolutionConfigurations)改成DebugGame Editor,之后再进行调试。
|
||||
2. 对想要进行Debug的函数增加禁用所有优化的宏。
|
||||
|
||||
使用方法如下:
|
||||
```
|
||||
PRAGMA_DISABLE_OPTIMIZATION_ACTUAL
|
||||
void MyClass::MyFunction(int32 MyIntParameter)
|
||||
{
|
||||
// My code
|
||||
}
|
||||
PRAGMA_ENABLE_OPTIMIZATION_ACTUAL
|
||||
```
|
||||
如果在插件中使用,则需要对插件执行rebuild,以进行重新构建。最后记得在调试完成后将宏删除。
|
||||
|
||||
# 自带的Debug工具
|
||||
## showdebug abilitysystem
|
||||
**显示方法**:按“`”键输入命令:showdebug abilitysystem。这个界面总共三页内容,分别显示Attributes、Effects、Abilities。通过**AbilitySystem.Debug.NextCategory**命令进行翻页。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
PS.PageUp与PageDown可以切换目标。
|
||||
|
||||
## GameplayDebugger
|
||||

|
||||
在进入关卡后,按下“’”键就可以开启这个工具。(就是屏幕上方出现的工具栏)
|
||||
|
||||
这个功能感觉是用来调试非当前操作角色的,选中的角色上会出现一个红色小恶魔图标(如图所示),你可以小键盘的数字键来开启对应的功能。
|
||||
|
||||
可以通过Tab键切换成飞行模式来选中需要调试角色。
|
||||
|
||||
# GameEffect增加/减少 委托绑定
|
||||
|
||||
```
|
||||
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);
|
||||
```
|
||||
|
||||
```
|
||||
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);
|
||||
```
|
18
03-UnrealEngine/Gameplay/GAS/(15)GAS 网络与预测.md
Normal file
18
03-UnrealEngine/Gameplay/GAS/(15)GAS 网络与预测.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## FScopedPredictionWindow、ServerSetReplicatedEvent、ConsumeGenericReplicatedEvent
|
||||
```
|
||||
FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent, IsPredictingClient());
|
||||
|
||||
//Ability是否具有本地预测能力
|
||||
if (IsPredictingClient())
|
||||
{
|
||||
// Tell the server about this
|
||||
AbilitySystemComponent->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey(), AbilitySystemComponent->ScopedPredictionKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
AbilitySystemComponent->ConsumeGenericReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey());
|
||||
}
|
||||
```
|
||||
|
||||
## 预测
|
||||

|
247
03-UnrealEngine/Gameplay/GAS/(16)在联机项目中使用GAS的注意点.md
Normal file
247
03-UnrealEngine/Gameplay/GAS/(16)在联机项目中使用GAS的注意点.md
Normal file
@@ -0,0 +1,247 @@
|
||||
## 前言
|
||||
大多数人应该是会通过学习UE4的ActionRPG项目来入门GAS框架的。但在最近自己做Demo的过程中才发现里面的部分不适用网络联机。比如ActionRPG将Character与Controller的指针存在GameMode,因为Server与Client只有一个GameMode的关系,就不符合多人联机游戏的规则。所以在经过实践之后这里我分享一些要点仅供抛砖引玉。这里我再推荐各位读者去看一下GASDocument以及附带的工程,它展现了正确的设计思路。
|
||||
|
||||
## Ability中的网络同步节点
|
||||
WaitNetSync
|
||||
|
||||
## GASDocument中的处理方式
|
||||
将AbilitySystemComponent以及AttributeSet放在PlayerState中,之后再Character类中的PossesseBy()事件(ServerOnly)与OnRep_PlayerState()事件(Client)给角色类的ASC弱智能指针与AttributeSet弱智能指针赋值。
|
||||
|
||||
```
|
||||
AGDPlayerState::AGDPlayerState()
|
||||
{
|
||||
AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
|
||||
AbilitySystemComponent->SetIsReplicated(true);
|
||||
|
||||
// Mixed mode means we only are replicated the GEs to ourself, not the GEs to simulated proxies. If another GDPlayerState (Hero) receives a GE,
|
||||
// we won't be told about it by the Server. Attributes, GameplayTags, and GameplayCues will still replicate to us.
|
||||
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
|
||||
|
||||
AttributeSetBase = CreateDefaultSubobject<UGDAttributeSetBase>(TEXT("AttributeSetBase"));
|
||||
|
||||
NetUpdateFrequency = 100.0f;
|
||||
}
|
||||
|
||||
// TWeakObjectPtr<class UGDAbilitySystemComponent> AbilitySystemComponent;
|
||||
// TWeakObjectPtr<class UGDAttributeSetBase> AttributeSetBase;
|
||||
// Server only
|
||||
void AGDHeroCharacter::PossessedBy(AController * NewController)
|
||||
{
|
||||
Super::PossessedBy(NewController);
|
||||
|
||||
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
|
||||
if (PS)
|
||||
{
|
||||
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
|
||||
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
|
||||
|
||||
// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
|
||||
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
|
||||
AttributeSetBase = PS->GetAttributeSetBase();
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
// Client only
|
||||
void AGDHeroCharacter::OnRep_PlayerState()
|
||||
{
|
||||
Super::OnRep_PlayerState();
|
||||
|
||||
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
|
||||
if (PS)
|
||||
{
|
||||
// Set the ASC for clients. Server does this in PossessedBy.
|
||||
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
|
||||
|
||||
// Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor.
|
||||
AbilitySystemComponent->InitAbilityActorInfo(PS, this);
|
||||
AttributeSetBase = PS->GetAttributeSetBase();
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
## 预测
|
||||

|
||||
|
||||
## PlayerState
|
||||
PlayerState只在开发多人游戏时有用,在设计上负责管理角色的变量(PlayerId、UniqueId、SessionName与自己定义的变量)。
|
||||
|
||||
### 指定与初始化
|
||||
在GameMode中设置实现的PlayerState类,之后在AController::InitPlayerState()中Spawn。
|
||||
|
||||
### 访问方法
|
||||
- Pawn->PlayerState
|
||||
- Controller->PlayerState(一般用这个)
|
||||
|
||||
另外还可以通过GameState->PlayerArray获取所有有效的GameState实例。
|
||||
|
||||
### PlayerState
|
||||
PlayerState在切换地图时会默认销毁,为了解决这个问题可以通过实现CopyProperties方法来解决。
|
||||
```
|
||||
class ARPGPlayerState : public APlayerState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "ActionRPG")
|
||||
ARPGCharacterBase* Character;
|
||||
protected:
|
||||
/** 这里是为了解决切换地图时的属性丢失问题 */
|
||||
/** Copy properties which need to be saved in inactive PlayerState */
|
||||
virtual void CopyProperties(APlayerState* PlayerState);
|
||||
};
|
||||
```
|
||||
```
|
||||
void ARPGPlayerState::CopyProperties(APlayerState* PlayerState)
|
||||
{
|
||||
//如果不是因为断线
|
||||
if (IsFromPreviousLevel())
|
||||
{
|
||||
ARPGPlayerState* RPGPlayerState=Cast<ARPGPlayerState>(PlayerState);
|
||||
RPGPlayerState->Character = Character;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NetUpdateFrequency
|
||||
PlayerState的NetUpdateFrequency和角色类的效果是一样的,都是调节更新频率。默认数值对于GAS太低了,会导致滞后性。100是个比较高的数值,你也可以按照需求自己调节。
|
||||
|
||||
## Controller
|
||||
### 获取PlayerControllers
|
||||
- GetWorld()->GetPlayerControllerIterator()
|
||||
- PlayerState->GetOwner()
|
||||
- Pawn->GetController()
|
||||
|
||||
### BeginPlay里不做任何与GAS相关的操作
|
||||
因为在服务端,Possession会发生在在BeginPlay前,客户端Possession发生在BeginPlay后,所以我们不能在BeginPlay中写任何有关ASC的逻辑除非PlayerState已经同步。
|
||||
|
||||
### 相关判断服务端客户端状态函数
|
||||
在Ability类中一些操作可能需要判断是服务端还是客户端,比如应用GameplayEffect就在服务端执行(使用HasAuthority()),而创建UI就需要在客户端执行(使用IsLocallyControlled())。
|
||||
|
||||
#### 蓝图
|
||||
- HasAuthority()判断是否是服务端或是作为主机的玩家
|
||||
- IsLocallyControlled()判断是否是本地控制器
|
||||
- IsServer()通过World的ENetMode来段是服务端
|
||||
|
||||
#### c++
|
||||
- 蓝图函数HasAuthority()在c++中为`return (GetLocalRole() == ROLE_Authority);`
|
||||
- IsLocallyControlled(),c++中有对应函数。
|
||||
|
||||
## NetSecurityPolicy
|
||||
GameplayAbility的网络安全策略决定了Ability应该在网络的何处执行. 它为尝试执行限制Ability的客户端提供了保护.
|
||||
|
||||
- ClientOrServer:没有安全需求. 客户端或服务端可以自由地触发该Ability的执行和终止.
|
||||
- ServerOnlyExecution:客户端对该Ability请求的执行会被服务端忽略, 但客户端仍可以请求服务端取消或结束该Ability.
|
||||
- ServerOnlyTermination:客户端对该Ability请求的取消或结束会被服务端忽略, 但客户端仍可以请求执行该Ability.
|
||||
- ServerOnly:服务端控制该Ability的执行和终止, 客户端的任何请求都会被忽略.
|
||||
|
||||
## NetExecutionPolicy
|
||||
推荐看这位大佬的文章:https://zhuanlan.zhihu.com/p/143637846
|
||||
|
||||
## 属性集属性UI更新
|
||||
这里推荐使用GASDocument中的GameplayTasks更新方法。
|
||||
```
|
||||
#pragma once
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
||||
#include "AbilitySystemComponent.h"
|
||||
#include "AsyncTaskAttributeChanged.generated.h"
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnAttributeChanged, FGameplayAttribute, Attribute, float, NewValue, float, OldValue);
|
||||
|
||||
/**
|
||||
* Blueprint node to automatically register a listener for all attribute changes in an AbilitySystemComponent.
|
||||
* Useful to use in UI.
|
||||
*/
|
||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask))
|
||||
class RPGGAMEPLAYABILITY_API UAsyncTaskAttributeChanged : public UBlueprintAsyncActionBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnAttributeChanged OnAttributeChanged;
|
||||
|
||||
// Listens for an attribute changing.
|
||||
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
|
||||
static UAsyncTaskAttributeChanged* ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute);
|
||||
|
||||
// Listens for an attribute changing.
|
||||
// Version that takes in an array of Attributes. Check the Attribute output for which Attribute changed.
|
||||
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
|
||||
static UAsyncTaskAttributeChanged* ListenForAttributesChange(UAbilitySystemComponent* AbilitySystemComponent, TArray<FGameplayAttribute> Attributes);
|
||||
|
||||
// You must call this function manually when you want the AsyncTask to end.
|
||||
// For UMG Widgets, you would call it in the Widget's Destruct event.
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void EndTask();
|
||||
|
||||
protected:
|
||||
UPROPERTY()
|
||||
UAbilitySystemComponent* ASC;
|
||||
|
||||
FGameplayAttribute AttributeToListenFor;
|
||||
TArray<FGameplayAttribute> AttributesToListenFor;
|
||||
|
||||
void AttributeChanged(const FOnAttributeChangeData& Data);
|
||||
};
|
||||
```
|
||||
```
|
||||
#include "Abilities/AsyncTasks/AsyncTaskAttributeChanged.h"
|
||||
UAsyncTaskAttributeChanged* UAsyncTaskAttributeChanged::ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute)
|
||||
{
|
||||
UAsyncTaskAttributeChanged* WaitForAttributeChangedTask = NewObject<UAsyncTaskAttributeChanged>();
|
||||
WaitForAttributeChangedTask->ASC = AbilitySystemComponent;
|
||||
WaitForAttributeChangedTask->AttributeToListenFor = Attribute;
|
||||
|
||||
if (!IsValid(AbilitySystemComponent) || !Attribute.IsValid())
|
||||
{
|
||||
WaitForAttributeChangedTask->RemoveFromRoot();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(WaitForAttributeChangedTask, &UAsyncTaskAttributeChanged::AttributeChanged);
|
||||
|
||||
return WaitForAttributeChangedTask;
|
||||
}
|
||||
|
||||
UAsyncTaskAttributeChanged * UAsyncTaskAttributeChanged::ListenForAttributesChange(UAbilitySystemComponent * AbilitySystemComponent, TArray<FGameplayAttribute> Attributes)
|
||||
{
|
||||
UAsyncTaskAttributeChanged* WaitForAttributeChangedTask = NewObject<UAsyncTaskAttributeChanged>();
|
||||
WaitForAttributeChangedTask->ASC = AbilitySystemComponent;
|
||||
WaitForAttributeChangedTask->AttributesToListenFor = Attributes;
|
||||
|
||||
if (!IsValid(AbilitySystemComponent) || Attributes.Num() < 1)
|
||||
{
|
||||
WaitForAttributeChangedTask->RemoveFromRoot();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (FGameplayAttribute Attribute : Attributes)
|
||||
{
|
||||
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(WaitForAttributeChangedTask, &UAsyncTaskAttributeChanged::AttributeChanged);
|
||||
}
|
||||
|
||||
return WaitForAttributeChangedTask;
|
||||
}
|
||||
|
||||
void UAsyncTaskAttributeChanged::EndTask()
|
||||
{
|
||||
if (IsValid(ASC))
|
||||
{
|
||||
ASC->GetGameplayAttributeValueChangeDelegate(AttributeToListenFor).RemoveAll(this);
|
||||
|
||||
for (FGameplayAttribute Attribute : AttributesToListenFor)
|
||||
{
|
||||
ASC->GetGameplayAttributeValueChangeDelegate(Attribute).RemoveAll(this);
|
||||
}
|
||||
}
|
||||
|
||||
SetReadyToDestroy();
|
||||
MarkPendingKill();
|
||||
}
|
||||
|
||||
void UAsyncTaskAttributeChanged::AttributeChanged(const FOnAttributeChangeData & Data)
|
||||
{
|
||||
OnAttributeChanged.Broadcast(Data.Attribute, Data.NewValue, Data.OldValue);
|
||||
}
|
||||
```
|
75
03-UnrealEngine/Gameplay/GAS/(2)UAttributeSet.md
Normal file
75
03-UnrealEngine/Gameplay/GAS/(2)UAttributeSet.md
Normal file
@@ -0,0 +1,75 @@
|
||||
## 概述
|
||||
在GameplayAbility框架中,UAttributeSet负责管理各种属性。
|
||||
|
||||
## 定义宏
|
||||
在头文件中添加:
|
||||
```
|
||||
// 使用AttributeSet.h的宏
|
||||
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
|
||||
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
|
||||
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
|
||||
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
|
||||
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
|
||||
```
|
||||
参考注释可以得知,使用这些宏可以让你少写各个属性的Get、Set、Init函数。如果不使用这个宏你也可以自己实现对应的函数。
|
||||
|
||||
这里用到了Ue4的反射。
|
||||
## 添加属性
|
||||
再以下格式添加属性:
|
||||
```
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Health")
|
||||
FGameplayAttributeData Health;
|
||||
ATTRIBUTE_ACCESSORS(URPGAttributeSet, Health)
|
||||
```
|
||||
## 重写接口
|
||||
ActionRPG案例中重写了PreAttributeChange与PostGameplayEffectExecute接口。
|
||||
|
||||
前者会在属性被修改时调用,在案例中是为了在血量或魔法上限发生变动时,按比例调整当前血量或者魔法值。后者与GameplayEffect有关,等到编写完GameplayEffect时,再来编写。
|
||||
```
|
||||
void URPGAttributeSet::PreAttributeChange(const FGameplayAttribute & Attribute, float & NewValue)
|
||||
{
|
||||
Super::PreAttributeChange(Attribute, NewValue);
|
||||
|
||||
if (Attribute == GetMaxHealthAttribute())
|
||||
{
|
||||
AdjustAttributeForMaxChange(Health, MaxHealth, NewValue, GetHealthAttribute());
|
||||
}
|
||||
else if (Attribute == GetMaxManaAttribute())
|
||||
{
|
||||
AdjustAttributeForMaxChange(Mana, MaxMana, NewValue, GetManaAttribute());
|
||||
}
|
||||
}
|
||||
|
||||
void URPGAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
|
||||
{
|
||||
Super::PostGameplayEffectExecute(Data);
|
||||
|
||||
}
|
||||
|
||||
void URPGAttributeSet::AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty)
|
||||
{
|
||||
UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent();
|
||||
const float CurrentMaxValue = MaxAttribute.GetCurrentValue();
|
||||
if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && AbilityComp)
|
||||
{
|
||||
// Change current value to maintain the current Val / Max percent
|
||||
const float CurrentValue = AffectedAttribute.GetCurrentValue();
|
||||
float NewDelta = (CurrentMaxValue > 0.f) ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue : NewMaxValue;
|
||||
|
||||
AbilityComp->ApplyModToAttributeUnsafe(AffectedAttributeProperty, EGameplayModOp::Additive, NewDelta);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 个人的使用方案
|
||||
定义URPGAttributeSet作为基础属性集,里面定义通用属性。之后又定义URPGCharacterAttributeSet作为角色专用的属性集。
|
||||
|
||||
之后在角色类中的构造函数中挂载URPGAttributeSet对象。
|
||||
```
|
||||
UPROPERTY()
|
||||
URPGAttributeSet* AttributeSet;
|
||||
```
|
||||
```
|
||||
AttributeSet = CreateDefaultSubobject<URPGAttributeSet>(TEXT("AttributeSet"));
|
||||
```
|
147
03-UnrealEngine/Gameplay/GAS/(3)UGameplayAbility.md
Normal file
147
03-UnrealEngine/Gameplay/GAS/(3)UGameplayAbility.md
Normal file
@@ -0,0 +1,147 @@
|
||||
## UGameplayAbility
|
||||
UGameplayAbility在GameplayAbility框架中代表一个技能(也可以认为是能力,它可以事件主动或者被动技能),我们可以通过继承UGameplayAbility来编写新技能。
|
||||
|
||||
UGameplayAbility主要提供以下功能:
|
||||
- 使用特性:技能cd、技能消耗等
|
||||
- 网络同步支持
|
||||
- 实例支持:non-instance(只在本地运行)、Instanced per owner、Instanced per execution (默认)
|
||||
|
||||
其中GameplayAbility_Montage就是non-instanced ability的案例,non-instanced在网络同步中有若干限制,具体的参考源代码。
|
||||
|
||||
### 使用方式
|
||||
在ActivateAbility事件中编写相关技能逻辑(角色动作、粒子效果、角色数值变动),最后根据具体情况(技能是否施展成功)调用CommitAbility()或EndAbility()。
|
||||
|
||||
如果有特殊可以在对应的事件中编写代码,例如你需要技能释放结束后播放粒子特效:那么就需要在onEndAbility事件中编写代码。
|
||||
|
||||
在c++中,你需要重写ActivateAbility()函数。这里建议直接复制ActivateAbility的代码并且在它的基础上编写逻辑,因为他兼顾了蓝图子类。
|
||||
|
||||
编写了Ability之后就需要将它注册到AbilityComponent中。但首先你需要创建正式用于编写角色逻辑的角色类,ActionRPG案例中将基础的GameplayAbility逻辑都写在URPGCharacterBase类中,所以现在你需要通过继承URPGCharacterBase来编写正式的角色逻辑(包括各种输入、摄像机等等)
|
||||
|
||||
|
||||
此时你只需要在新建的子类的构造函数中手动添加GameplayAbilities数组即可:
|
||||
```
|
||||
GameplayAbilities.Push(UGA_SkillBase::StaticClass());
|
||||
```
|
||||
### 在ActionRPG案例中的做法
|
||||
在ActionRPG案例中,定义了URPGGameplayAbility(继承于UGameplayAbility)作为项目中所有GameplayAbility的基类。它实现了实现了以下方法:
|
||||
```
|
||||
/** Gameplay标签与GameplayEffect Map */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = GameplayEffects)
|
||||
TMap<FGameplayTag, FRPGGameplayEffectContainer> EffectContainerMap;
|
||||
|
||||
/** 读取指定的FRPGGameplayEffectContainer来生成FRPGGameplayEffectContainerSpec */
|
||||
UFUNCTION(BlueprintCallable, Category = Ability, meta = (AutoCreateRefTerm = "EventData"))
|
||||
virtual FRPGGameplayEffectContainerSpec MakeEffectContainerSpecFromContainer(const FRPGGameplayEffectContainer& Container, const FGameplayEventData& EventData, int32 OverrideGameplayLevel = -1);
|
||||
|
||||
/** 通过GameplayTag来搜索EffectContainerMap,并且生成FRPGGameplayEffectContainerSpec */
|
||||
UFUNCTION(BlueprintCallable, Category = Ability, meta = (AutoCreateRefTerm = "EventData"))
|
||||
virtual FRPGGameplayEffectContainerSpec MakeEffectContainerSpec(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel = -1);
|
||||
|
||||
/** 让FRPGGameplayEffectContainerSpec中的effect对指定目标生效 */
|
||||
UFUNCTION(BlueprintCallable, Category = Ability)
|
||||
virtual TArray<FActiveGameplayEffectHandle> ApplyEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec);
|
||||
|
||||
/** 调用MakeEffectContainerSpec生成FRPGGameplayEffectContainerSpec,再让Effect对目标生效 */
|
||||
UFUNCTION(BlueprintCallable, Category = Ability, meta = (AutoCreateRefTerm = "EventData"))
|
||||
virtual TArray<FActiveGameplayEffectHandle> ApplyEffectContainer(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel = -1);
|
||||
```
|
||||
代码很简单,大致可以归纳为:
|
||||
1. 维护一个GameplayTag与RPGGameplayEffectContainer的映射表EffectContainerMap。
|
||||
2. 创建FRPGGameplayEffectContainerSpec。(可以通过GameplayTag查找EffectContainerMap或者通过指定的RPGGameplayEffectContainer)。
|
||||
3. 通过FRPGGameplayEffectContainerSpec,让内部所有effect对目标生效。
|
||||
|
||||
### URPGTargetType
|
||||
父类是UObject,只有一个GetTargets事件。之后通过各种的子类来实现各种的目标效果。
|
||||
|
||||
该类用于实现获取Ability所作用的目标,换句话说是获取目标数据的逻辑(目标Actor数组)。用以实现例如:单体目标,范围目标等等
|
||||
### FRPGGameplayEffectContainer
|
||||
结构体,存储了URPGTargetType对象与UGameplayEffect容器数组对象。
|
||||
### FRPGGameplayEffectContainerSpec
|
||||
RPGGameplayEffectContainer的处理后版本。
|
||||
在URPGGameplayAbility中会调用MakeEffectContainerSpecFromContainer()生成。
|
||||
|
||||
如果FRPGGameplayEffectContainer存在TargetType对象,就会调用它的GetTargets函数来获取HitResult数组与Actor数组。最后调用AddTargets函数来填充FRPGGameplayEffectContainerSpec中的Target信息。
|
||||
|
||||
填充FRPGGameplayEffectContainerSpec的FGameplayEffectSpecHandle数组(FGameplayEffectSpecHandle中包含了FGameplayEffectSpec的智能指针)
|
||||
|
||||
说了那么多,其实就是将Effect应用到所有TargetActor上。
|
||||
### 重要函数
|
||||
从头文件中复制的注释:
|
||||
```
|
||||
CanActivateAbility() - const function to see if ability is activatable. Callable by UI etc
|
||||
|
||||
TryActivateAbility() - Attempts to activate the ability. Calls CanActivateAbility(). Input events can call this directly.
|
||||
- Also handles instancing-per-execution logic and replication/prediction calls.
|
||||
|
||||
CallActivateAbility() - Protected, non virtual function. Does some boilerplate 'pre activate' stuff, then calls ActivateAbility()
|
||||
|
||||
ActivateAbility() - What the abilities *does*. This is what child classes want to override.
|
||||
|
||||
CommitAbility() - Commits reources/cooldowns etc. ActivateAbility() must call this!
|
||||
|
||||
CancelAbility() - Interrupts the ability (from an outside source).
|
||||
|
||||
EndAbility() - The ability has ended. This is intended to be called by the ability to end itself.
|
||||
```
|
||||
## 关于BindAbilityActivationToInputComponent
|
||||
~~这个东西我查了Github、AnswerHUB以及轮廓,没有任何资料(除了作者的Wiki)。看了源代码也看不出所以然,而且ActionRPG里也没有使用这个函数,可以看得出即使不用这个函数也不会影响该框架别的功能(可能会对联机游戏产生影响)。~~
|
||||
|
||||
经过@键盘侠·伍德 的指导,我才知道用法:
|
||||
|
||||
1. 声明一个用于映射输入的枚举类
|
||||
```
|
||||
UENUM(BlueprintType)
|
||||
enum class AbilityInput : uint8
|
||||
{
|
||||
UseAbility1 UMETA(DisplayName = "Use Spell 1"), //This maps the first ability(input ID should be 0 in int) to the action mapping(which you define in the project settings) by the name of "UseAbility1". "Use Spell 1" is the blueprint name of the element.
|
||||
UseAbility2 UMETA(DisplayName = "Use Spell 2"), //Maps ability 2(input ID 1) to action mapping UseAbility2. "Use Spell 2" is mostly used for when the enum is a blueprint variable.
|
||||
UseAbility3 UMETA(DisplayName = "Use Spell 3"),
|
||||
UseAbility4 UMETA(DisplayName = "Use Spell 4"),
|
||||
WeaponAbility UMETA(DisplayName = "Use Weapon"), //This finally maps the fifth ability(here designated to be your weaponability, or auto-attack, or whatever) to action mapping "WeaponAbility".
|
||||
|
||||
//You may also do something like define an enum element name that is not actually mapped to an input, for example if you have a passive ability that isn't supposed to have an input. This isn't usually necessary though as you usually grant abilities via input ID,
|
||||
//which can be negative while enums cannot. In fact, a constant called "INDEX_NONE" exists for the exact purpose of rendering an input as unavailable, and it's simply defined as -1.
|
||||
//Because abilities are granted by input ID, which is an int, you may use enum elements to describe the ID anyway however, because enums are fancily dressed up ints.
|
||||
};
|
||||
```
|
||||
2. 在SetupPlayerInputComponent函数中调用BindAbilityActivationToInputComponent函数
|
||||
|
||||
例如:
|
||||
```
|
||||
void ARPGCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
|
||||
{
|
||||
// Set up gameplay key bindings
|
||||
check(PlayerInputComponent);
|
||||
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
|
||||
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
|
||||
|
||||
PlayerInputComponent->BindAxis("MoveForward", this, &ARPGCharacter::MoveForward);
|
||||
PlayerInputComponent->BindAxis("MoveRight", this, &ARPGCharacter::MoveRight);
|
||||
|
||||
// We have 2 versions of the rotation bindings to handle different kinds of devices differently
|
||||
// "turn" handles devices that provide an absolute delta, such as a mouse.
|
||||
// "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick
|
||||
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
|
||||
PlayerInputComponent->BindAxis("TurnRate", this, &ARPGCharacter::TurnAtRate);
|
||||
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
|
||||
PlayerInputComponent->BindAxis("LookUpRate", this, &ARPGCharacter::LookUpAtRate);
|
||||
|
||||
|
||||
//PlayerInputComponent->BindAction("CastBaseSkill", IE_Pressed, this, &ARPGCharacter::CastBaseSkill);
|
||||
|
||||
AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds("ConfirmInput", "CancelInput", "AbilityInput"));
|
||||
}
|
||||
```
|
||||
3. 在执行GiveAbility函数(注册能力)时,设置输入id。输入id为枚举类中对应的枚举值。(例如本案例中UseAbility1为0,UseAbility2为1,UseAbility3为2)
|
||||
```
|
||||
FGameplayAbilitySpec(TSubclassOf<UGameplayAbility> InAbilityClass, int32 InLevel, int32 InInputID, UObject* InSourceObject)
|
||||
```
|
||||
4. 在项目设置——输入中,按照所设置的输入id所对应的枚举,添加ActionMapping。例如:UseAbility1、UseAbility2、UseAbility3。
|
||||
|
||||
这样做可以达到对Control解耦的目的。因为你调用GiveAbility或者ClearAbility时会自动绑定输入。而不需要手动去角色类或者控制类中手动设置。
|
||||
|
||||
### 有关InputPressed与InputReleased
|
||||
执行了上述输入绑定措施,你就可以通过重写InputPressed与InputReleased,来执行对应的逻辑。
|
||||
调用这两个虚函数的逻辑在UAbilitySystemComponent中的AbilitySpecInputPressed与AbilitySpecInputReleased。
|
||||
|
||||
个人认为这些逻辑还会有可能会在蓝图中编写,所以新继承的类可以创建新的BlueprintNativeEvent,这样对工程开发会更加友好。
|
354
03-UnrealEngine/Gameplay/GAS/(4)UGameplayEffect.md
Normal file
354
03-UnrealEngine/Gameplay/GAS/(4)UGameplayEffect.md
Normal file
@@ -0,0 +1,354 @@
|
||||
## UGameplayEffect
|
||||
UGameplayEffect在框架中主要负责各种数值上的效果,如果技能cd、类似黑魂中的异常效果堆叠与buff,甚至连角色升级时的属性点添加都可以使用它来实现。
|
||||
|
||||
因为大多数逻辑都是设置数据子类的操作,所以对于这个类,本人推荐使用蓝图来进行操作。
|
||||
## 简单使用教程
|
||||
通过继承UGameplayEffect来创建一个新的GameplayEffect类,并在构造函数中对相应的属性进行设置。之后在Ability类中调用ApplyGameplayEffectToOwner函数让GameplayEffect生效。
|
||||
```
|
||||
if (CommitAbility(Handle, ActorInfo, ActivationInfo)) // ..then commit the ability...
|
||||
{
|
||||
// Then do more stuff...
|
||||
const UGameplayEffect* GameplayEffect = NewObject<UGE_DamageBase>();
|
||||
ApplyGameplayEffectToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, GameplayEffect, 5, 1);
|
||||
|
||||
K2_EndAbility();
|
||||
}
|
||||
```
|
||||
具体操作可以参考ActionRPG模板,或者是我的项目代码。
|
||||
## Modifiers
|
||||
本质是一个FGameplayModifierInfo结构体数组用于存储所有数值修改信息。FGameplayModifierInfo包含以下属性:
|
||||
- Attribute 修改的目标属性集中的属性。
|
||||
- ModifierOp 修改方式。例如Override、Add、Multiply。
|
||||
- Magnitude(已被废弃)
|
||||
- ModifierMagnitude 修改的数值与类型,可以配置数据表。
|
||||
- EvaluationChannelSettings (不知道为什么没在编辑器中显示,而且代码中只有一处调用,所以直接跳过)
|
||||
- SourceTags 本身标签行为(Effect生效所需或者忽略的标签)
|
||||
- TargetTags 目标标签行为(Effect生效所需或者忽略的标签)
|
||||
|
||||
可以看得出ModifierMagnitude才是Modifiers的关键,而它的本质是FGameplayEffectModifierMagnitude结构体。但是我们只需要学会初始化它即可。它具有以下四种类型:
|
||||
- ScalableFloat 较为简单的使用方式,使用ScalableFloat进行计算
|
||||
- AttributeBased 基于属性执行计算。
|
||||
- CustomCalculationClass 能够捕获多个属性进行自定义计算
|
||||
- SetByCaller 被蓝图或者代码显式设置
|
||||
|
||||
### ScalableFloat的调用示例
|
||||
ScalableFloat类型是用于设置固定值的简单方式,同时它也支持通过CurveTable配合技能等级设置倍率。(最后结果=固定值*倍率)当然如果你向完全通过CurveTable来控制参数,那就把固定值设置为1即可。
|
||||
```
|
||||
FGameplayModifierInfo info;
|
||||
|
||||
info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
|
||||
info.ModifierOp = EGameplayModOp::Additive;
|
||||
|
||||
//固定值
|
||||
//info.ModifierMagnitude = FGameplayEffectModifierMagnitude(FScalableFloat(100.0));
|
||||
|
||||
//CurveTable控制倍率
|
||||
FScalableFloat damageValue = {1.0};
|
||||
FCurveTableRowHandle damageCurve;
|
||||
static ConstructorHelpers::FObjectFinder<UCurveTable> curveAsset(TEXT("/Game/ActionRPG/DataTable/Damage"));
|
||||
damageCurve.CurveTable = curveAsset.Object;
|
||||
damageCurve.RowName = FName("Damage");
|
||||
damageValue.Curve = damageCurve;
|
||||
|
||||
info.ModifierMagnitude = FGameplayEffectModifierMagnitude(damageValue);
|
||||
Modifiers.Add(info);
|
||||
```
|
||||
PS.技能等级在ApplyGameplayEffectToOwner函数中设置。
|
||||
### AttributeBased的调用示例
|
||||
最终计算过程可以在CalculateMagnitude函数中找到。
|
||||
|
||||
1. 如果尝试捕获到数值(不为None),则将赋值给AttribValue。
|
||||
2. 判断AttributeCalculationType,来计算对应的AttribValue。(我不太了解代码中channel的概念,如果channel不存在,AttribValue为原本值)
|
||||
3. 如果AttributeCurve存在,则将AttribValue作为x轴值来查找y轴值,并进行插值计算,最后将结果赋值给AttribValue。
|
||||
4. 最终计算公式:`$((Coefficient * (AttribValue + PreMultiplyAdditiveValue)) + PostMultiplyAdditiveValue)$`
|
||||
|
||||
### BackingAttribute
|
||||
为GameplayEffect捕获GameplayAttribute的选项。(你可以理解为Lambda表达式的捕获)
|
||||
- AttributeToCapture:捕获属性
|
||||
- AttributeSource:捕获的目标(自身还是目标对象)
|
||||
- bSnapshot:属性是否需要被快照(没仔细看,如果为false,每次都会重新获取吧)
|
||||
|
||||
### AttributeCalculationType
|
||||
默认值为AttributeMagnitude。
|
||||
|
||||
- AttributeMagnitude:使用最后通过属性计算出来的级数
|
||||
- AttributeBaseValue:使用属性基础值
|
||||
- AttributeBonusMagnitude:使用(最后计算值-基础值)
|
||||
- AttributeMagnitudeEvaluatedUpToChannel:不清楚使用方法,关键是在编辑器中,这个选项默认是不显示的
|
||||
```
|
||||
FGameplayModifierInfo info;
|
||||
|
||||
info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
|
||||
info.ModifierOp = EGameplayModOp::Additive;
|
||||
FAttributeBasedFloat damageValue;
|
||||
damageValue.Coefficient = { 1.2f };
|
||||
damageValue.PreMultiplyAdditiveValue = { 1.0f };
|
||||
damageValue.PostMultiplyAdditiveValue = { 2.0f };
|
||||
damageValue.BackingAttribute = FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
|
||||
, EGameplayEffectAttributeCaptureSource::Source, false);
|
||||
FCurveTableRowHandle damageCurve;
|
||||
static ConstructorHelpers::FObjectFinder<UCurveTable> curveAsset(TEXT("/Game/ActionRPG/DataTable/Damage"));
|
||||
damageCurve.CurveTable = curveAsset.Object;
|
||||
damageCurve.RowName = FName("Damage");
|
||||
damageValue.AttributeCurve = damageCurve;
|
||||
damageValue.AttributeCalculationType = EAttributeBasedFloatCalculationType::AttributeMagnitude;
|
||||
|
||||
info.ModifierMagnitude = damageValue;
|
||||
Modifiers.Add(info);
|
||||
```
|
||||
### CustomCalculationClass的调用示例
|
||||
与AttributeCalculationType相比,少了属性捕获,多了CalculationClassMagnitude(UGameplayModMagnitudeCalculation类)。
|
||||
```
|
||||
FGameplayModifierInfo info;
|
||||
|
||||
info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
|
||||
info.ModifierOp = EGameplayModOp::Additive;
|
||||
FCustomCalculationBasedFloat damageValue;
|
||||
damageValue.CalculationClassMagnitude = UDamageMagnitudeCalculation::StaticClass();
|
||||
damageValue.Coefficient = { 1.2f };
|
||||
damageValue.PreMultiplyAdditiveValue = { 1.0f };
|
||||
damageValue.PostMultiplyAdditiveValue = { 2.0f };
|
||||
|
||||
info.ModifierMagnitude = damageValue;
|
||||
Modifiers.Add(info);
|
||||
```
|
||||
PS.如果这个计算过程还取决于外部非GameplayAbility框架的条件,那么你可能需要重写GetExternalModifierDependencyMulticast()函数,以获得FOnExternalGameplayModifierDependencyChange委托。从而实现:当外部条件发生改变时,及时更新计算结果。
|
||||
### UGameplayModMagnitudeCalculation
|
||||
你可以通过继承UGameplayModMagnitudeCalculation来创建自定义的Calculation类。所需实现步骤如下:
|
||||
|
||||
1. 在构造函数中,向RelevantAttributesToCapture数组添加需要捕获的属性。
|
||||
2. 实现CalculateBaseMagnitude事件。(因为BlueprintNativeEvent类型,所以既可以在c++里实现也可以在蓝图中实现,关于两者结合可以参考UGameplayAbility类中ActivateAbility()的写法。
|
||||
|
||||
案例代码如下:
|
||||
```
|
||||
UCLASS(BlueprintType, Blueprintable, Abstract)
|
||||
class ACTIONRPG_API UDamageMagnitudeCalculation : public UGameplayModMagnitudeCalculation
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UDamageMagnitudeCalculation();
|
||||
|
||||
float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
|
||||
};
|
||||
```
|
||||
```
|
||||
UDamageMagnitudeCalculation::UDamageMagnitudeCalculation()
|
||||
{
|
||||
RelevantAttributesToCapture.Add(FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
|
||||
, EGameplayEffectAttributeCaptureSource::Source, false));
|
||||
}
|
||||
|
||||
float UDamageMagnitudeCalculation::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
|
||||
{
|
||||
float damage{ 0.0f};
|
||||
|
||||
FAggregatorEvaluateParameters InEvalParams;
|
||||
|
||||
//捕获失败的容错语句
|
||||
if (!GetCapturedAttributeMagnitude(FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
|
||||
, EGameplayEffectAttributeCaptureSource::Source, false), Spec, InEvalParams, damage)) {
|
||||
|
||||
//如果这个变量会作为除数的话,不能为0
|
||||
damage = 1.0f;
|
||||
}
|
||||
|
||||
return damage;
|
||||
}
|
||||
```
|
||||
## Executions
|
||||
Executions更为简单,而且更加自由。只需要编写Calculation Class即可。它与Modifiers的不同之处在于:一个Modifiers只能修改一个属性,而Executions可以同时改动多个属性。
|
||||
### UGameplayEffectExecutionCalculation
|
||||
这里我就直接复制actionRPG模板的代码了。
|
||||
开头的RPGDamageStatics结构体与DamageStatics函数,可以减少后面的代码量。可以算是FGameplayEffectAttributeCaptureDefinition的语法糖吧。
|
||||
```
|
||||
UCLASS()
|
||||
class ACTIONRPG_API UDamageExecutionCalculation : public UGameplayEffectExecutionCalculation
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UDamageExecutionCalculation();
|
||||
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
|
||||
};
|
||||
```
|
||||
```
|
||||
struct RPGDamageStatics
|
||||
{
|
||||
DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower);
|
||||
DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
|
||||
DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);
|
||||
|
||||
RPGDamageStatics()
|
||||
{
|
||||
// Capture the Target's DefensePower attribute. Do not snapshot it, because we want to use the health value at the moment we apply the execution.
|
||||
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false);
|
||||
|
||||
// Capture the Source's AttackPower. We do want to snapshot this at the moment we create the GameplayEffectSpec that will execute the damage.
|
||||
// (imagine we fire a projectile: we create the GE Spec when the projectile is fired. When it hits the target, we want to use the AttackPower at the moment
|
||||
// the projectile was launched, not when it hits).
|
||||
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, true);
|
||||
|
||||
// Also capture the source's raw Damage, which is normally passed in directly via the execution
|
||||
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, Damage, Source, true);
|
||||
}
|
||||
};
|
||||
|
||||
static const RPGDamageStatics& DamageStatics()
|
||||
{
|
||||
static RPGDamageStatics DmgStatics;
|
||||
return DmgStatics;
|
||||
}
|
||||
|
||||
UDamageExecutionCalculation::UDamageExecutionCalculation()
|
||||
{
|
||||
RelevantAttributesToCapture.Add(DamageStatics().DefensePowerDef);
|
||||
RelevantAttributesToCapture.Add(DamageStatics().AttackPowerDef);
|
||||
RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
|
||||
}
|
||||
|
||||
void UDamageExecutionCalculation::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
|
||||
{
|
||||
UAbilitySystemComponent* TargetAbilitySystemComponent = ExecutionParams.GetTargetAbilitySystemComponent();
|
||||
UAbilitySystemComponent* SourceAbilitySystemComponent = ExecutionParams.GetSourceAbilitySystemComponent();
|
||||
|
||||
AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->AvatarActor : nullptr;
|
||||
AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->AvatarActor : nullptr;
|
||||
|
||||
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
|
||||
|
||||
// Gather the tags from the source and target as that can affect which buffs should be used
|
||||
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
|
||||
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
|
||||
|
||||
FAggregatorEvaluateParameters EvaluationParameters;
|
||||
EvaluationParameters.SourceTags = SourceTags;
|
||||
EvaluationParameters.TargetTags = TargetTags;
|
||||
|
||||
// --------------------------------------
|
||||
// Damage Done = Damage * AttackPower / DefensePower
|
||||
// If DefensePower is 0, it is treated as 1.0
|
||||
// --------------------------------------
|
||||
|
||||
//计算捕获属性的数值。
|
||||
float DefensePower = 0.f;
|
||||
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DefensePowerDef, EvaluationParameters, DefensePower);
|
||||
//因为要做除数所以需要加入容错语句
|
||||
if (DefensePower == 0.0f)
|
||||
{
|
||||
DefensePower = 1.0f;
|
||||
}
|
||||
|
||||
float AttackPower = 0.f;
|
||||
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().AttackPowerDef, EvaluationParameters, AttackPower);
|
||||
|
||||
float Damage = 0.f;
|
||||
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);
|
||||
|
||||
//伤害计算公式
|
||||
float DamageDone = Damage * AttackPower / DefensePower;
|
||||
if (DamageDone > 0.f)
|
||||
{
|
||||
//这里可以修改多个属性
|
||||
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().DamageProperty, EGameplayModOp::Additive, DamageDone));
|
||||
}
|
||||
}
|
||||
```
|
||||
## Period
|
||||
Period指的是周期,一般用于制作周期性技能。
|
||||
```
|
||||
//持续类型,只有设置为HasDuration,技能才能变成周期性的
|
||||
DurationPolicy = EGameplayEffectDurationType::HasDuration;
|
||||
//持续时间
|
||||
DurationMagnitude = FGameplayEffectModifierMagnitude(1.0);
|
||||
//周期,技能生效次数=持续时间/周期
|
||||
Period = 2.0;
|
||||
```
|
||||
|
||||
# FGameplayEffectContainer与Spec
|
||||
EPIC实现了这个结构体
|
||||
调用URPGGameplayAbility::MakeEffectContainerSpecFromContainer
|
||||
使用FGameplayEffectContainer生成FGameplayEffectContainerSpec结构体。
|
||||
|
||||
Spec是实例版本,存储TargetDataHandle与EffectSpecHandle。通过MakeEffectContainerSpecFromContainer进行实例化(但本质是通过Spec的AddTarget进行数据填充)。
|
||||
|
||||
之后再通过
|
||||
```
|
||||
ReturnSpec.TargetGameplayEffectSpecs.Add(MakeOutgoingGameplayEffectSpec(EffectClass, OverrideGameplayLevel));
|
||||
```
|
||||
填充EffectSpec数据。
|
||||
|
||||
**MakeEffectContainerSpec则是个快捷函数**
|
||||
通过FGameplayTag寻找对应的Effect与Target数据。EventData则用于调用TargetType类的GetTarget函数,用于获取符合要求的目标(Actor)。
|
||||
|
||||
在ActionRPG中URPGTargetType_UseEventData的GetTarget用到了EventData。大致逻辑为首先寻找EventData里是否带有EventData.HitResult信息(可以在Send Event To Actor中设置),如果没有则返回EventData.Target信息。
|
||||
```
|
||||
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));
|
||||
}
|
||||
}
|
||||
```
|
||||
ApplyEffectContainer则是个方便函数。
|
||||
|
||||
|
||||
# 实现在AnimNotify中向指定目标引用GameplayEffect
|
||||
|
||||
从GameplayAbilityComponent或者从GameplayAbility中设置.
|
||||
MakeOutgoingGameplayEffectSpec=>
|
||||
ApplyGameplayEffectSpecToTarget 位于UGameplayAbility
|
||||
ApplyGameplayEffectToTarget
|
||||
GameplayEffectSpec.GetContext().AddTarget()
|
||||
|
||||
RemoveGrantedByEffect()函数可以移除Ability中Instance类型的Effect。非常适合来清除翻滚免伤、技能硬直效果。
|
||||
|
||||
|
||||
```
|
||||
FRPGGameplayEffectContainerSpec URPGBlueprintLibrary::AddTargetsToEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec, const TArray<FHitResult>& HitResults, const TArray<AActor*>& TargetActors)
|
||||
{
|
||||
FRPGGameplayEffectContainerSpec NewSpec = ContainerSpec;
|
||||
NewSpec.AddTargets(HitResults, TargetActors);
|
||||
return NewSpec;
|
||||
}
|
||||
|
||||
TArray<FActiveGameplayEffectHandle> URPGBlueprintLibrary::ApplyExternalEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec)
|
||||
{
|
||||
TArray<FActiveGameplayEffectHandle> AllEffects;
|
||||
|
||||
// Iterate list of gameplay effects
|
||||
for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs)
|
||||
{
|
||||
if (SpecHandle.IsValid())
|
||||
{
|
||||
// If effect is valid, iterate list of targets and apply to all
|
||||
for (TSharedPtr<FGameplayAbilityTargetData> Data : ContainerSpec.TargetData.Data)
|
||||
{
|
||||
AllEffects.Append(Data->ApplyGameplayEffectSpec(*SpecHandle.Data.Get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return AllEffects;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo, ActivationInfo, GameplayEffectClass, GameplayEffectLevel);
|
||||
if (SpecHandle.Data.IsValid())
|
||||
{
|
||||
SpecHandle.Data->StackCount = Stacks;
|
||||
|
||||
SCOPE_CYCLE_UOBJECT(Source, SpecHandle.Data->GetContext().GetSourceObject());
|
||||
EffectHandles.Append(ApplyGameplayEffectSpecToTarget(Handle, ActorInfo, ActivationInfo, SpecHandle, Target));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
# 实用函数
|
||||
Wait Input Release
|
167
03-UnrealEngine/Gameplay/GAS/(5)UAbilityTask.md
Normal file
167
03-UnrealEngine/Gameplay/GAS/(5)UAbilityTask.md
Normal file
@@ -0,0 +1,167 @@
|
||||
## UAbilityTask
|
||||
UAbilityTask继承自UGameplayTask(UGameplayTask可以用来写一些行为树中的一些节点),可以用来实现一些异步功能。比如播放montage后各种事件的处理。
|
||||
|
||||
你可以去GameplayAbilities\Public\Abilities\Tasks\目录下寻找作者编写的task类作为相关参考,也可以直接使用(GameplayTasks目录下的案例比较少)。
|
||||
|
||||
当然我更加推荐学习actionRPG项目中的PlayMontageAndWaitForEvent,原因有:1、这个task用得最多2、涉及Task的代码相对较多。
|
||||
|
||||
其他推荐学习的:UGameplayTask_WaitDelay、UAbilityTask_WaitGameplayEvent、UAbilityTask_WaitGameplayTagAdded、UAbilityTask_WaitGameplayEffectApplied
|
||||
## 大致过程
|
||||
1. 声明多个动态多播委托用于处理各种事件。
|
||||
2. 重写所需的虚函数,并且声明相关变量。
|
||||
3. 编写主体函数。
|
||||
|
||||
## 代码分析
|
||||
在PlayMontageAndWaitForEvent中重写了4个虚函数:
|
||||
```
|
||||
//用于在各种委托设置完毕后开始执行真正的Tasks。
|
||||
virtual void Activate() override;
|
||||
|
||||
//从外部取消这个Tasks,默认情况下,会结束任务。
|
||||
virtual void ExternalCancel() override;
|
||||
|
||||
//返回debug字符串,内容为当前播放的Montage名称以及Tasks存储的Montage名称
|
||||
virtual FString GetDebugString() const override;
|
||||
|
||||
//结束并清理Tasks,既可以在Tasks内部调用,可以从该Tasks拥有者调用。
|
||||
//注意:请不要直接调用该函数,你应该调用EndTask()或者TaskOwnerEnded()
|
||||
//注意:重写该函数时,请确保最后调用Super::OnDestroy(bOwnerFinished)
|
||||
virtual void OnDestroy(bool AbilityEnded) override;
|
||||
```
|
||||
## Activate()
|
||||
```
|
||||
void URPGAbilityTask_PlayMontageAndWaitForEvent::Activate()
|
||||
{
|
||||
if (Ability == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool bPlayedMontage = false;
|
||||
URPGAbilitySystemComponent* RPGAbilitySystemComponent = GetTargetASC();
|
||||
|
||||
if (RPGAbilitySystemComponent)
|
||||
{
|
||||
const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
|
||||
UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
|
||||
if (AnimInstance != nullptr)
|
||||
{
|
||||
//绑定事件回调函数
|
||||
EventHandle = RPGAbilitySystemComponent->AddGameplayEventTagContainerDelegate(EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent));
|
||||
|
||||
//播放montage
|
||||
if (RPGAbilitySystemComponent->PlayMontage(Ability, Ability->GetCurrentActivationInfo(), MontageToPlay, Rate, StartSection) > 0.f)
|
||||
{
|
||||
//播放Montage后,其回调函数可能会导致Ability结束,所以我们需要提前结束
|
||||
if (ShouldBroadcastAbilityTaskDelegates() == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
//绑定OnAbilityCancelled
|
||||
CancelledHandle = Ability->OnGameplayAbilityCancelled.AddUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnAbilityCancelled);
|
||||
//绑定OnMontageBlendingOut
|
||||
BlendingOutDelegate.BindUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnMontageBlendingOut);
|
||||
AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, MontageToPlay);
|
||||
//绑定OnMontageEnded
|
||||
MontageEndedDelegate.BindUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnMontageEnded);
|
||||
AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, MontageToPlay);
|
||||
|
||||
ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
|
||||
if (Character && (Character->Role == ROLE_Authority ||
|
||||
(Character->Role == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)))
|
||||
{
|
||||
Character->SetAnimRootMotionTranslationScale(AnimRootMotionTranslationScale);
|
||||
}
|
||||
|
||||
bPlayedMontage = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent call to PlayMontage failed!"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent called on invalid AbilitySystemComponent"));
|
||||
}
|
||||
//播放失败处理
|
||||
if (!bPlayedMontage)
|
||||
{
|
||||
ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent called in Ability %s failed to play montage %s; Task Instance Name %s."), *Ability->GetName(), *GetNameSafe(MontageToPlay),*InstanceName.ToString());
|
||||
if (ShouldBroadcastAbilityTaskDelegates())
|
||||
{
|
||||
OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
|
||||
}
|
||||
}
|
||||
|
||||
SetWaitingOnAvatar();
|
||||
}
|
||||
```
|
||||
|
||||
## ExternalCancel()
|
||||
```
|
||||
check(AbilitySystemComponent);
|
||||
OnAbilityCancelled();
|
||||
Super::ExternalCancel();
|
||||
```
|
||||
OnAbilityCancelled的代码
|
||||
```
|
||||
if (StopPlayingMontage())
|
||||
{
|
||||
// Let the BP handle the interrupt as well
|
||||
if (ShouldBroadcastAbilityTaskDelegates())
|
||||
{
|
||||
OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
|
||||
}
|
||||
}
|
||||
```
|
||||
## OnDestroy()
|
||||
```
|
||||
void URPGAbilityTask_PlayMontageAndWaitForEvent::OnDestroy(bool AbilityEnded)
|
||||
{
|
||||
// Note: Clearing montage end delegate isn't necessary since its not a multicast and will be cleared when the next montage plays.
|
||||
// (If we are destroyed, it will detect this and not do anything)
|
||||
|
||||
// This delegate, however, should be cleared as it is a multicast
|
||||
if (Ability)
|
||||
{
|
||||
Ability->OnGameplayAbilityCancelled.Remove(CancelledHandle);
|
||||
if (AbilityEnded && bStopWhenAbilityEnds)
|
||||
{
|
||||
//停止播放Montage
|
||||
StopPlayingMontage();
|
||||
}
|
||||
}
|
||||
|
||||
URPGAbilitySystemComponent* RPGAbilitySystemComponent = GetTargetASC();
|
||||
if (RPGAbilitySystemComponent)
|
||||
{
|
||||
//移除事件绑定
|
||||
RPGAbilitySystemComponent->RemoveGameplayEventTagContainerDelegate(EventTags, EventHandle);
|
||||
}
|
||||
//这句必须放在最后
|
||||
Super::OnDestroy(AbilityEnded);
|
||||
}
|
||||
```
|
||||
## PlayMontageAndWaitForEvent
|
||||
PlayMontageAndWaitForEvent是Tasks的主体函数。
|
||||
```
|
||||
URPGAbilityTask_PlayMontageAndWaitForEvent* URPGAbilityTask_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(UGameplayAbility* OwningAbility,
|
||||
FName TaskInstanceName, UAnimMontage* MontageToPlay, FGameplayTagContainer EventTags, float Rate, FName StartSection, bool bStopWhenAbilityEnds, float AnimRootMotionTranslationScale)
|
||||
{
|
||||
//用于缩放GAS tasks变量的工具函数,此为非shipping功能,用于交互调试。
|
||||
UAbilitySystemGlobals::NonShipping_ApplyGlobalAbilityScaler_Rate(Rate);
|
||||
|
||||
//使用NewAbilityTask来创建Tasks,并且设置各个变量。
|
||||
URPGAbilityTask_PlayMontageAndWaitForEvent* MyObj = NewAbilityTask<URPGAbilityTask_PlayMontageAndWaitForEvent>(OwningAbility, TaskInstanceName);
|
||||
MyObj->MontageToPlay = MontageToPlay;
|
||||
MyObj->EventTags = EventTags;
|
||||
MyObj->Rate = Rate;
|
||||
MyObj->StartSection = StartSection;
|
||||
MyObj->AnimRootMotionTranslationScale = AnimRootMotionTranslationScale;
|
||||
MyObj->bStopWhenAbilityEnds = bStopWhenAbilityEnds;
|
||||
|
||||
return MyObj;
|
||||
}
|
||||
```
|
124
03-UnrealEngine/Gameplay/GAS/(6)GAS AbilityTask节点功能整理.md
Normal file
124
03-UnrealEngine/Gameplay/GAS/(6)GAS AbilityTask节点功能整理.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 有用的GameplayTasks整理
|
||||
## 前言
|
||||
整理了一下GameplayTasks节点,在此分享给大家。
|
||||
|
||||
## 自带GameplayTasks
|
||||
### ApplyRootMotion系列
|
||||
在对应名称的static函数中调用初始化函数SharedInitAndApply(),其作用为取得CharacterMovementComponent,并调用ApplyRootMotionSource应用构建的FRootMotionSource。
|
||||
在TickTask实现,取得Avatar(角色类)并且判断MovementComponent的当前FRootMotionSource是否运行结束。并且非Simulation客户端上触发OnFinish委托并强制更新Avatar。其成员变量设置了网络同步属性。该系列有:
|
||||
|
||||
- AbilityTask_ApplyRootMotion_Base
|
||||
- AbilityTask_ApplyRootMotionConstantForce
|
||||
- AbilityTask_ApplyRootMotionJumpForce
|
||||
- AbilityTask_ApplyRootMotionMoveToActorForce
|
||||
- AbilityTask_ApplyRootMotionMoveToForce
|
||||
- AbilityTask_ApplyRootMotionRadialForce
|
||||
|
||||
FRootMotionSource为CharacterMovementComponent的RootMotion的通用源,通过一系列运算(覆盖、增加、设置、忽视)将位移与旋转应用到CharacterMovementComponent。可以认为是一个数值驱动、非动画驱动的RootMotion,引擎中实现了以下类型:
|
||||
|
||||
- FRootMotionSource_JumpForce:让目标像跳跃一样移动。
|
||||
- FRootMotionSource_MoveToDynamicForce:在一定时间内强制将目标移动到指定世界坐标(目标可以在这段时间内随意移动)。
|
||||
- FRootMotionSource_MoveToForce:在一定时间内强制将目标移动到指定世界坐标。
|
||||
- FRootMotionSource_RadialForce:在世界指定坐标施加可作用于目标的一个拉扯或者推出的力。
|
||||
- FRootMotionSource_ConstantForce:对目标施加一个固定数值的力。
|
||||
|
||||
本人测试了:
|
||||
|
||||
- JumpForce,感觉其作用是可以编写参数化的Jump。(还可以加各种Curve)可改进的地方就是增加跳跃到最高点的Delegate。
|
||||
- ConstantForce、RadialForce可以做一些场景、怪物击飞效果。
|
||||
- MoveToForce可以做角色与交互式物体互动式的强制位移。
|
||||
- MoveToActorForce可以做类似魔兽世界的飞行点快速旅行功能。
|
||||
|
||||
### GAS系统相关
|
||||
>Ability与AttributeSet
|
||||
|
||||
- AbilityTask_StartAbilityState:用于监视所在Ability的状态,会触发Ability的OnGameplayAbilityStateEnded与OnGameplayAbilityCancelled委托。
|
||||
- AbilityTask_WaitAttributeChange:AbilityTask版本的属性监视Task,拥有WaitForAttributeChange与WaitForAttributeChangeWithComparison(属性比较版本)
|
||||
- AbilityTask_WaitAbilityActivate:实现了3个版本,用于监视指定Ability的激活事件(通过判断Ability的Tag触发OnActivate委托)。
|
||||
- AbilityTask_WaitAbilityCommit:实现了2个版本,用于监视指定Ability的Commit事件(通过判断Ability的Tag触发OnCommit委托)。
|
||||
- AbilityTask_WaitAttributeChangeThreshold:用于监视指定属性变化,数值符合指定比较类型会触发OnChange委托。
|
||||
- AbilityTask_WaitAttributeChangeRatioThreshold:用于监视由2个属性组合计算出比例变化(l例如生命值与最大生命值),符合指定比较类型会触发OnChange委托。
|
||||
|
||||
>GameplayEffect
|
||||
|
||||
- AbilityTask_WaitGameplayEffectApplied:GameplayEffectApplied系列基类,OnApplyGameplayEffectCallback实现了标签过滤与匹配逻辑。开发时不应该直接调用该类。
|
||||
- AbilityTask_WaitGameplayEffectApplied_Self:实现Tag与Query两个版本的,将父类处理函数绑定到ASC的OnPeriodicGameplayEffectExecuteDelegateOnSelf。
|
||||
- AbilityTask_WaitGameplayEffectApplied_Target实现Tag与Query两个版本的,将父类处理函数绑定到ASC的OnPeriodicGameplayEffectExecuteDelegateOnTarget。
|
||||
- AbilityTask_WaitGameplayEffectBlockedImmunity:绑定ASC的OnImmunityBlockGameplayEffectDelegate,检查EffectSpec的CapturedSourceTags与CapturedTargetTags,会触发Blocked委托。
|
||||
- AbilityTask_WaitGameplayEffectRemoved:通过GameplayEffectHandle绑定ASC的OnGameplayEffectRemoved_InfoDelegate。会触发OnChange委托,如果Handle无效则会触发InvalidHandle委托。
|
||||
- AbilityTask_WaitGameplayEffectStackChange:通过GameplayEffectHandle绑定ASC的OnGameplayEffectStackChangeDelegate。会触发OnChange委托,如果Handle无效则会触发InvalidHandle委托。
|
||||
|
||||
>GameplayEvent与GameplayTag
|
||||
|
||||
- AbilityTask_WaitGameplayEvent:根据OnlyMatchExact,绑定ASC的GenericGameplayEventCallbacks与ASC的AddGameplayEventTagContainerDelegate。会触发EventReceived委托。
|
||||
- AbilityTask_WaitGameplayTagBase:WaitGameplayTag基类,因为没有实现回调函数,所以开发时不应该直接调用该类。
|
||||
- AbilityTask_WaitGameplayTag:根据Tag绑定ASC的RegisterGameplayTagEvent(),会触发Added委托。
|
||||
|
||||
- AbilityTask_WaitTargetData:Spawn一个TargetData之后等待接收TargetData。
|
||||
|
||||
### 输入
|
||||
源码的Confirm与Cancel部分有实现3个节点,对于本地端则与ASC的GenericLocalXXXXXCallbacks委托绑定本地版本OnXXXXX回调函数,远程端则调用CallOrAddReplicatedDelegate绑定OnXXXXX回调函数。回调函数在处理完同步相关的逻辑后,会触发FGenericGameplayTaskDelegate类型的OnXXXXX委托。AbilityTask_WaitConfirmCancel会同时监视Confirm与Cancel。
|
||||
|
||||
- AbilityTask_WaitCancel
|
||||
- AbilityTask_WaitConfirm
|
||||
- AbilityTask_WaitConfirmCancel
|
||||
|
||||
>Confirm与Cancel的绑定可以参考GASDocument的4.6.2章节。
|
||||
|
||||
AbilityTask_WaitInputPress: 绑定ASC的AbilityReplicatedEventDelegate(),监视与Ability绑定的InputAction是否按下。在处理同步相关的逻辑后触发OnPress委托。
|
||||
AbilityTask_WaitInputRelease:绑定ASC的AbilityReplicatedEventDelegate(),监视与Ability绑定的InputAction是否松开。在处理同步相关的逻辑后触发OnRelease委托。
|
||||
|
||||
### 网络相关
|
||||
- AbilityTask_NetworkSyncPoint:网络同步等待节点,用于等待Server/Client同步完成。
|
||||
|
||||
### 移动类
|
||||
- AbilityTask_MoveToLocation:通过TickTask来实现让目标在一定时间内移动到指定世界坐标。(官方注释说这个实现方法不太好有待改进)
|
||||
- AbilityTask_WaitMovementModeChange:绑定角色类的MovementModeChangedDelegate,触发OnChange委托。
|
||||
- AbilityTask_WaitVelocityChange:通过TickTask实现。每帧判断Dot(MovementComponent的速度与指定方向)是否超过设定阈值,超过就会触发OnVelocityChage委托。
|
||||
|
||||
### 工具类
|
||||
- AbilityTask_WaitDelay:使用定时器实现,定时触发一次OnFinish委托。
|
||||
- AbilityTask_Repeat:使用定时器实现,定时多次触发OnPerformAction委托。
|
||||
- AbilityTask_SpawnActor:BeginSpawningActor()与FinishSpawningActor()不知道如何在蓝图中调用,所以无法使用SpawnActorDeferred方式。
|
||||
- UAbilityTask_VisualizeTargeting:因为VisualizeTargeting的相关逻辑写在BeginSpawningActor与FinishSpawningActor,所以也存在上述问题。VisualizeTargetingUsingActor则会Spawn一个TargetActor用于追踪Ability,到时候后悔触发TimeElapsed委托。
|
||||
- AbilityTask_WaitOverlap:绑定AvatarActor的Root组件的OnComponentHit委托。触发的OnOverlap委托会附带FGameplayAbilityTargetDataHandle(FGameplayAbilityTargetData_SingleTargetHit处理的TargetData)
|
||||
|
||||
## AbilityAsync
|
||||
UAbilityAsync是一种Ability相关的定制BlueprintAsyncActions,与GameplayAbilityTasks的不同之处,它可以在任意蓝图类中调用,并且生命周期与Ability无关。
|
||||
|
||||
- AbilityAsync_WaitGameplayEffectApplied:等待指定GE(可以限定Target与Source标签)应用到当前Ability所在ASC中。委托来自于ASC。
|
||||
- AbilityAsync_WaitGameplayEvent:等待GameplayEvent。委托来自于ASC。
|
||||
- AbilityAsync_WaitGameplayTag:等待GameplayTag。委托来自于ASC。
|
||||
|
||||
## GASShooter中的GameplayTasks
|
||||
- GSAT_MoveSceneCompRelLocation:在设定时间内按照设定曲线移动指定的SceneComponent,逻辑在TickTasks中处理。
|
||||
- GSAT_PlayMontageAndWaitForEvent:播放Montage并且等待GameplayEvent。
|
||||
- GSAT_PlayMontageForMeshAndWaitForEvent:使指定Mesh播放Montage并且等待GameplayEvent。在GASShooter中用于播放武器Montage,解决播放非Avatar、Owner的Montage的问题。
|
||||
- GSAT_WaitChangeFOV:在设定时间内按照设定曲线设置Fov,逻辑在TickTasks中处理。
|
||||
- GSAT_WaitDelayOneFrame:使用GetWorld()->GetTimerManager().SetTimerForNextTick()调用函数,触发OnFinish委托来实现。
|
||||
- GSAT_WaitInputPressWithTags:将委托AbilitySystemComponent->AbilityReplicatedEventDelegate(根据Ability与PredictionKey取得的同步事件委托)与OnPressCallback绑定,测试模式下直接触发OnPressCallback函数。OnPressCallback逻辑为判断tag是否符合要求,不符合则重置,符合要求则移除AbilityReplicatedEventDelegate绑定,根据Ability的预测类型处理ASC同步事件,最后触发OnPress委托。
|
||||
- GSAT_WaitInteractableTarget:用于检测可交互物体,使用定时器来不断进行射线求交判断。根据结果触发LostInteractableTarget与FoundNewInteractableTarget委托。
|
||||
- GSAT_WaitTargetDataUsingActor:GASShooter的改进版本,等待接收已经生成TargetActor发送的TargetData。绑定Delegate为TargetActor的TargetDataReadyDelegate与CanceledDelegate。服务端还会绑定ASC的AbilityTargetDataSetDelegate与AbilityTargetDataCancelledDelegate,以及处理同步事件。
|
||||
- GSAT_ServerWaitForClientTargetData:绑定ASC的AbilityTargetDataSetDelegate委托。其回调函数会消耗掉客户端ACS的TargetData同步事件。之后触发ValidData委托。(即只处理服务端TargetData同步事件)
|
||||
|
||||
这里的AsyncTask并没有使用UAbilityAsync实现而是直接继承自BlueprintAsyncActions进行实现。在项目中主要在UMG获取对应数据:
|
||||
- AsyncTaskGameplayTagAddedRemoved:监视Tag的增加与减少情况,用于判断状态。
|
||||
- AsyncTaskAttributeChanged:监视数据值的改变情况,用于设置血条、蓝条数值。
|
||||
- AsyncTaskCooldownChanged:监视对应Ability的冷却情况,显示技能图标的CD效果。
|
||||
- AsyncTaskEffectStackChanged:监视GE堆叠情况,用于显示Buffer/Debuffer堆叠情况。
|
||||
|
||||
## 自带的Ability
|
||||
GameplayAbility_Montage:播放Montage并且应用设置好的GE,并且结束是移除GE。(没什么软用)
|
||||
GameplayAbility_CharacterJump:使用Ability实现跳跃,激活条件、跳跃方式的逻辑均调用自Character,可以通过override自定义角色类中对应的函数实现扩展。
|
||||
|
||||
## 小部分Tasks的自动绑定机制
|
||||
像UAbilityTask_WaitTargetData之类的Task会有一些函数没有被调用,这些其实会被UK2Node_LatentGameplayTaskCall进行自动绑定与运行(仅限蓝图),在c++的话需要自己调用这些函数,以及在static函数中调用ReadyForActivation();。会自动绑定运行函数与pin为:
|
||||
```
|
||||
FName FK2Node_LatentAbilityCallHelper::BeginSpawnFuncName(TEXT("BeginSpawningActor"));
|
||||
FName FK2Node_LatentAbilityCallHelper::FinishSpawnFuncName(TEXT("FinishSpawningActor"));
|
||||
FName FK2Node_LatentAbilityCallHelper::BeginSpawnArrayFuncName(TEXT("BeginSpawningActorArray"));
|
||||
FName FK2Node_LatentAbilityCallHelper::FinishSpawnArrayFuncName(TEXT("FinishSpawningActorArray"));
|
||||
FName FK2Node_LatentAbilityCallHelper::SpawnedActorPinName(TEXT("SpawnedActor"));
|
||||
FName FK2Node_LatentAbilityCallHelper::WorldContextPinName(TEXT("WorldContextObject"));
|
||||
FName FK2Node_LatentAbilityCallHelper::ClassPinName(TEXT("Class"));
|
||||
```
|
@@ -0,0 +1,89 @@
|
||||
## 简述
|
||||
GameplayAbility框架使用GameplayTag作为各种状态判断的依据,甚至使用它作为Event进行传递。
|
||||
|
||||
以下是actionRPG的标签设计:
|
||||
```
|
||||
Ability(能力类型:使用物品、连击、随机数值技能、技能)
|
||||
|——item
|
||||
|——Melee
|
||||
|——Ranged
|
||||
|——Skill
|
||||
| └──GA_PlayerSkillMeteorStorm
|
||||
Cooldown(技能冷却:存在即代表技能处于冷却状态)
|
||||
| └──Skill
|
||||
EffectContainer
|
||||
| └──Default
|
||||
Event(事件)
|
||||
| └──Montage
|
||||
| |——layer
|
||||
| | |——Combo
|
||||
| | |——BurstPound
|
||||
| | |——ChestKick
|
||||
| | |——FrontalAttack
|
||||
| | |——GroundPound
|
||||
| | └──JumpSlam
|
||||
| └──Shared
|
||||
| |——UseItem
|
||||
| |——UseSkill
|
||||
| └──WeaponHit
|
||||
Status(状态)
|
||||
└──DamageImmune
|
||||
```
|
||||
|
||||
## 简单用法
|
||||
在编辑-项目设置-GameplayTags选项卡中可以编辑标签。
|
||||
|
||||
具体使用方法可以参考:
|
||||
https://www.unrealengine.com/zh-CN/tech-blog/using-gameplay-tags-to-label-and-organize-your-content-in-ue4?lang=zh-CN
|
||||
## 相关的Tasks类
|
||||
UAbilityTask_WaitGameplayEvent与UAbilityTask_WaitGameplayTagAdded。配合SendGameplayEventToActor函数可以实现防反之类的效果。
|
||||
|
||||
SendGameplayEventToActor的用法可以参考actionRPG项目,在WeaponActor中(位于Content/Blueprint/Weapon/),它的ActorBeginOverlap事件用于实现武器攻击效果。其中它会执行SendGameplayEventToActor函数,给所有有效对象(带有GameplayAbilityComponent)添加事件标签。
|
||||
## GameplayCue
|
||||
介绍:
|
||||
https://docs.unrealengine.com/en-US/Gameplay/GameplayAbilitySystem/GameplayAttributesAndGameplayEffects/index.html
|
||||
|
||||
位于:窗口—GameplayCue编辑器,这个编辑器可以用于创建GameplayCue,但你也可以手动创建,只需要使用蓝图继承GameplayCueNotify_xxx类,之后再绑定GamplayCue标签即可。
|
||||
|
||||
GameplayCue必须使用GameplayCue开头的tag,例如GameplayCue.ElectricalSparks或者"GameplayCue.WaterSplash.Big。
|
||||
|
||||
你可以在蓝图或者c++中重写OnActive、WhileActive()、Removed()、Executed()进行控制。
|
||||
|
||||
我能找到的唯一资料就是Tom Looman大神的视频(可以算是官方教程了)
|
||||
https://youtu.be/Tu5AJKNe1Ok?t=900
|
||||
## 使用GameplayCue编辑器来创建
|
||||
在GameplayCue编辑器中,在新增按钮左边输入栏输入想要创建的标签(必须以GameplayCue.开头),再点击新建即可添加成功。之后就需要为标签添加对应的处理器。
|
||||
|
||||
点击处理器列下方对应新增按钮,之后就会新增处理器会弹出Notify创建界面,以下是我对界面中说明文字的翻译:
|
||||
### GameplayCue通知
|
||||
GameplayCue Notifies are stand alone handlers, similiar to AnimNotifies. Most GameplyCues can be implemented through these notifies. Notifies excel at handling standardized effects. The classes below provide the most common functionality needed.
|
||||
|
||||
GameplayCue Notifies是类似AnimNotifies的独立处理器。大多数GameplyCues都可以使用以下两个notifies实现。notifies处理标准化效果(与自定义BP事件相对)。下面的类提供了最常用的功能。
|
||||
### GameplayCueNotifyStatic
|
||||
A non instantiated UObject that acts as a handler for a GameplayCue. These are useful for one-off "burst" effects.
|
||||
|
||||
一个用于处理GameplayCue的非实例化的UObject,适用于一次性的突发effects。
|
||||
### GameplayCueNotifyActor
|
||||
An instantiated Actor that acts as a handler of a GameplayCue. Since they are instantiated, they can maintain state and tick/update every frame if necessary.
|
||||
|
||||
一个用于处理GameplayCue的实例化的Actor。因为是实例化的,所以可以维护状态,并且在在必要的情况下可以在每一帧进行处理。
|
||||
|
||||
点击会创建AGameplayCueNotify_Actor
|
||||
## 自定义BP事件
|
||||
GameplayCues can also be implemented via custom events on character blueprints.
|
||||
|
||||
To add a custom BP event, open the blueprint and look for custom events starting with GameplayCue.*
|
||||
|
||||
GameplayCues 也可以通过角色蓝图上的自定义事件来实现。想要增加一个自定义蓝图事件,打开蓝图新增与标签同名的自定义事件。(这个本人没有测试成功,因为自定义事件是不能带有.的)
|
||||
## 大致步骤
|
||||
1、可以通过蓝图继承的方式或者GameplayCue编辑器来创建对应的notify
|
||||
2、在创建的notify中设置GameplayCue Tag(这一步成功,Gameplay Cue Editor就能显示绑定)
|
||||
3、重写onActive以及其他状态函数函数。(例如播放粒子效果、声音。如果需要销毁之前播放的Asset,可以在onDestroy中调用reactive函数,并在勾选Notify蓝图中的自动销毁)
|
||||
4、使用GameplayEffect(在GameplayEffect蓝图中的Visible——GameplayCues中添加)或者在ABility类中调用AddGameplayCueToOwner来触发Notify。
|
||||
## 有关Notify总结
|
||||
想要通过GameplayAbility与GameplayEffect触发,需使用GameplayCueNotifyStatic、GameplayCueNotifyActor。
|
||||
|
||||
动画则需要使用AnimNotify与AnimNotifyState。
|
||||
|
||||
在actionRPG项目中全程使用AnimNotify来驱动各种效果(GameplayAbility=》PlayMontage()=》AnimNotify)
|
||||
|
203
03-UnrealEngine/Gameplay/GAS/(8)一种将所有逻辑都写进Ability的GAS Combo方案.md
Normal file
203
03-UnrealEngine/Gameplay/GAS/(8)一种将所有逻辑都写进Ability的GAS Combo方案.md
Normal file
@@ -0,0 +1,203 @@
|
||||
## 前言
|
||||
使用GAS实现Combo技能,本人所知道的有两种思路:<br>
|
||||
**第一种:为每一段Combo创建一个单独的Ability;定制自定义Asset与编辑器,之后再通过自定义编辑器进行编辑。**
|
||||
|
||||
- 优点:最高的自由度,不容易出问题。
|
||||
- 缺点:如果有大量多段或者派生复杂的Combo,那工作量将会相当爆炸。可以说除了工程量大,管理麻烦没有别的缺点。
|
||||
|
||||
**第二种:根据GameplayEvent直接在Ability里直接实现Combo切换逻辑**
|
||||
|
||||
https://zhuanlan.zhihu.com/p/131381892
|
||||
|
||||
在此文中,我分析了ActionRPG的攻击逻辑的同时自己也提出一种通过GameplayEvent实现Combat的方法。这个方法理论上没问题,但实际落地的时候发现存在一个问题:里面使用的PlayMontageAndWaitEvent是GameplayTasks是异步运行的,而通过AnimNotify发送的SendGameplayEvent的函数确是同步函数。所以会导致PlayMontageAndWaitEvent节点接收不到任何事件的问题。经过一段事件的探索,本人摸索到了另一种方案:
|
||||
|
||||
- 优点:一个起点(这个Combo树)一个Ability,方便管理。可以尽可能地把Combo相关的逻辑都整合到一个Ability里。
|
||||
- 缺点:自由度会有一定的限制;因为采用异步函数来管理事件,会导致一定的延迟。不适于格斗游戏这种对低延迟要求比较高项目。
|
||||
|
||||
## 2021.7.30更新
|
||||
如果你使用GAS的输入绑定系统,即在GiveAbility()注册时进行输入绑定(参考GASDocument的4.6.2章节)。可将后面介绍的ListenPlayerInputAction节点换成GAS自带的WaitInputPress或WaitInputRelease。
|
||||
|
||||
## 具体实现
|
||||

|
||||
|
||||
还需要设置AbilityTag与AbilityBlockTag,解决狂按而导致的技能鬼畜问题。
|
||||
|
||||
### URPGAbilityTask_ListenPlayerInputAction
|
||||
构建一个GameplayTasks来监听玩家输入,使用Character类获取InputComponent,之后再进行对应Action的动态函数绑定/解绑。执行前需要判断是否是本地Controller。具体代码如下:
|
||||
```
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/ObjectMacros.h"
|
||||
#include "Abilities/Tasks/AbilityTask.h"
|
||||
#include "RPGAbilityTask_ListenPlayerInputAction.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE( FOnInputActionMulticast );
|
||||
|
||||
UCLASS()
|
||||
class RPGGAMEPLAYABILITY_API URPGAbilityTask_ListenPlayerInputAction : public UAbilityTask
|
||||
{
|
||||
GENERATED_UCLASS_BODY()
|
||||
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnInputActionMulticast OnInputAction;
|
||||
|
||||
virtual void Activate() override;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
|
||||
static URPGAbilityTask_ListenPlayerInputAction* ListenPlayerInputAction(UGameplayAbility* OwningAbility,FName ActionName, TEnumAsByte< EInputEvent > EventType, bool bConsume);
|
||||
|
||||
protected:
|
||||
|
||||
virtual void OnDestroy(bool AbilityEnded) override;
|
||||
|
||||
UPROPERTY()
|
||||
FName ActionName;
|
||||
|
||||
UPROPERTY()
|
||||
TEnumAsByte<EInputEvent> EventType;
|
||||
|
||||
/** 是否消耗输入消息,不再传递到下一个InputComponent */
|
||||
UPROPERTY()
|
||||
bool bConsume;
|
||||
|
||||
int32 ActionBindingHandle;
|
||||
};
|
||||
```
|
||||
```
|
||||
#include "Abilities/AsyncTasks/RPGAbilityTask_ListenPlayerInputAction.h"
|
||||
#include "Components/InputComponent.h"
|
||||
|
||||
URPGAbilityTask_ListenPlayerInputAction::URPGAbilityTask_ListenPlayerInputAction(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{}
|
||||
|
||||
URPGAbilityTask_ListenPlayerInputAction* URPGAbilityTask_ListenPlayerInputAction::ListenPlayerInputAction(UGameplayAbility* OwningAbility,FName ActionName, TEnumAsByte< EInputEvent > EventType, bool bConsume)
|
||||
{
|
||||
URPGAbilityTask_ListenPlayerInputAction* Task=NewAbilityTask<URPGAbilityTask_ListenPlayerInputAction>(OwningAbility);
|
||||
Task->ActionName = ActionName;
|
||||
Task->EventType = EventType;
|
||||
Task->bConsume = bConsume;
|
||||
return Task;
|
||||
}
|
||||
|
||||
void URPGAbilityTask_ListenPlayerInputAction::Activate()
|
||||
{
|
||||
AActor* Owner=GetOwnerActor();
|
||||
UInputComponent* InputComponent=Owner->InputComponent;
|
||||
|
||||
FInputActionBinding NewBinding(ActionName, EventType.GetValue());
|
||||
NewBinding.bConsumeInput = bConsume;
|
||||
NewBinding.ActionDelegate.GetDelegateForManualSet().BindLambda([this]()
|
||||
{
|
||||
if(OnInputAction.IsBound())
|
||||
OnInputAction.Broadcast();
|
||||
});
|
||||
|
||||
ActionBindingHandle=InputComponent->AddActionBinding(NewBinding).GetHandle();
|
||||
}
|
||||
|
||||
void URPGAbilityTask_ListenPlayerInputAction::OnDestroy(bool AbilityEnded)
|
||||
{
|
||||
AActor* Owner = GetOwnerActor();
|
||||
UInputComponent* InputComponent = Owner->InputComponent;
|
||||
if (InputComponent)
|
||||
{
|
||||
InputComponent->RemoveActionBindingForHandle(ActionBindingHandle);
|
||||
}
|
||||
|
||||
Super::OnDestroy(AbilityEnded);
|
||||
}
|
||||
```
|
||||
### 控制MontageSection跳转
|
||||
这里我创建一个控制跳转用Map:
|
||||
|
||||

|
||||
|
||||
其中Key为当前Combo名称_攻击类型。之后根据Value值进行MontageSection跳转。
|
||||
|
||||
### Montage偏移
|
||||
为了Combo之间有更好的过渡,可以通过计算进入招式派生状态的经过时间,最后在Section跳转时进行偏移以实现最佳的连贯性(Combo动画中需要有对应的过渡区域设计)。以下是我实现的MontageSection跳转+偏移函数:
|
||||
```
|
||||
void URPGAbilitySystemComponent::CurrentMontageJumpToSectionWithOffset(FName SectionName,float PositionOffset)
|
||||
{
|
||||
UAnimInstance* AnimInstance = AbilityActorInfo.IsValid() ? AbilityActorInfo->GetAnimInstance() : nullptr;
|
||||
if ((SectionName != NAME_None) && AnimInstance && LocalAnimMontageInfo.AnimMontage)
|
||||
{
|
||||
// AnimInstance->Montage_JumpToSection(SectionName, LocalAnimMontageInfo.AnimMontage);
|
||||
FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(LocalAnimMontageInfo.AnimMontage);
|
||||
if (MontageInstance)
|
||||
{
|
||||
UAnimMontage* Montage=LocalAnimMontageInfo.AnimMontage;
|
||||
const int32 SectionID = Montage->GetSectionIndex(SectionName);
|
||||
if (Montage->IsValidSectionIndex(SectionID))
|
||||
{
|
||||
FCompositeSection & CurSection = Montage->GetAnimCompositeSection(SectionID);
|
||||
const float NewPosition = CurSection.GetTime()+PositionOffset;
|
||||
MontageInstance->SetPosition(NewPosition);
|
||||
MontageInstance->Play(MontageInstance->GetPlayRate());
|
||||
}
|
||||
}
|
||||
|
||||
if (IsOwnerActorAuthoritative())
|
||||
{
|
||||
FGameplayAbilityRepAnimMontage& MutableRepAnimMontageInfo = GetRepAnimMontageInfo_Mutable();
|
||||
|
||||
MutableRepAnimMontageInfo.SectionIdToPlay = 0;
|
||||
if (MutableRepAnimMontageInfo.AnimMontage)
|
||||
{
|
||||
// we add one so INDEX_NONE can be used in the on rep
|
||||
MutableRepAnimMontageInfo.SectionIdToPlay = MutableRepAnimMontageInfo.AnimMontage->GetSectionIndex(SectionName) + 1;
|
||||
}
|
||||
|
||||
AnimMontage_UpdateReplicatedData();
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerCurrentMontageJumpToSectionName(LocalAnimMontageInfo.AnimMontage, SectionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float URPGAbilitySystemComponent::GetCurrentMontagePlaybackPosition() const
|
||||
{
|
||||
UAnimInstance* AnimInstance = AbilityActorInfo.IsValid() ? AbilityActorInfo->GetAnimInstance() : nullptr;
|
||||
if (AnimInstance && LocalAnimMontageInfo.AnimMontage)
|
||||
{
|
||||
FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(LocalAnimMontageInfo.AnimMontage);
|
||||
return MontageInstance->GetPosition();
|
||||
}
|
||||
return 0.f;
|
||||
}
|
||||
```
|
||||
```
|
||||
void URPGGameplayAbility::MontageJumpToSectionWithOffset(FName SectionName,float PositionOffset)
|
||||
{
|
||||
check(CurrentActorInfo);
|
||||
|
||||
URPGAbilitySystemComponent* AbilitySystemComponent = Cast<URPGAbilitySystemComponent>(GetAbilitySystemComponentFromActorInfo_Checked());
|
||||
if (AbilitySystemComponent->IsAnimatingAbility(this))
|
||||
{
|
||||
//调用对应函数
|
||||
AbilitySystemComponent->XXXXXX();
|
||||
}
|
||||
}
|
||||
```
|
||||
### 解决因狂按而导致的技能鬼畜问题
|
||||
在保证可以实现其他技能强制取消当前技能后摇的情况下,我尝试过以下方法,结果都不太理想:
|
||||
- 使用带有AbilityBlockTag的GE,来控制指定时间段里无法重复使用当前技能。
|
||||
- 使用Ability的CoolDown功能,配合GE制作公共CD效果。(时间短了,依然会鬼畜)
|
||||
|
||||
最后采用给当前Ability设置标签,并且把这个标签作为AbilityBlockTag来实现。取消技能后摇的效果可以通过AnimNotify调用EndAbility来实现。
|
||||
|
||||
### 事件处理
|
||||
#### Default
|
||||
处理其他没有指定的事件,一般都是应用GE的。与ActionRPG不同的地方在于:
|
||||
1. 同一个事件第二次接收,Ability会移除第一次应用的GE,这是为了实现类似攻击动作霸体的效果。
|
||||
2. 因为数值计算只在服务端处理,所以执行前需要使用HasAuthority判断一下。
|
||||
|
||||
#### WeaponHit
|
||||
武器其中可攻击对象时的逻辑。除此之外可以使用自定义的UObject作为EventPayload(OptionalObject)传递更多的数据,以此可以实现命中弱点增加伤害等效果。
|
||||
|
||||
#### DerivationCombat
|
||||
这里其实应该是Combo,但之前打错,懒得改了。这里主要是设置控制Combo是否可以派生的变量。
|
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: GameplayTag UPROPERTY Filter
|
||||
date: 2022-09-29 23:20:58
|
||||
excerpt:
|
||||
tags: GAS
|
||||
rating: ⭐
|
||||
---
|
||||
|
||||
`GameplayTags` 和`GameplayTagContainers` 有可选的 `UPROPERTY` 说明符`Meta = (Categories = "GameplayCue")` ,可以用来在蓝图中实现标签的过滤,仅展示出属于父标签为`GameplayCue`的`GameplayTags` 。 要实现此功能也可以通过直接使用 `FGameplayCueTag` 其内部封装了一个带有`Meta = (Categories = "GameplayCue")`的 `FGameplayTag` 。
|
||||
|
||||
当把 `GameplayTag` 当作方法的参数时,可以通过 `UFUNCTION` specifier `Meta = (GameplayTagFilter = "GameplayCue")`完成过滤。(译者注:`GameplayTagContainer` 也已经支持Filter,不再赘述)
|
139
03-UnrealEngine/Gameplay/GAS/GAS杂记录.md
Normal file
139
03-UnrealEngine/Gameplay/GAS/GAS杂记录.md
Normal file
@@ -0,0 +1,139 @@
|
||||
## ActionRPG项目的问题
|
||||
### 联机
|
||||
|
||||
## 攻击判定问题
|
||||
### Overlap无法返回PhysicsBody的BodyIndex
|
||||
因为当前物理系统使用Physx的关系,所以Overlap事件中SweepResult的HitBoneName返回是的None。
|
||||
|
||||
### 如果取得对应的骨骼
|
||||
1. 当前版本4.26,射线是可以取得对应的骨骼的。
|
||||
2. 如果是角色自己跑进 Overlap空间里时返回的BodyIndex是-1,但Overlap空间自己移动碰到角色时是可以正确返回BodyIndex
|
||||
|
||||
## 武器逻辑
|
||||
### LCMCharacter中的实现
|
||||
LCMCharacter里通过在构造函数里创建Mesh并且连接到角色模型的插槽:
|
||||
```
|
||||
WeaponMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Weapon"));
|
||||
WeaponMesh->SetupAttachment(GetMesh(), TEXT("Sword_2"));
|
||||
```
|
||||
|
||||
### ActionRPG中的逻辑
|
||||
创建一个WeaponActor变量,之后通过调用DetachFromActor移除当前武器,再调用AttachActorToComponent
|
||||
在AnimNotifyState的Begin与End事件中,取得Character基类取得WeaponActor变量,再开启与关闭攻击判定。
|
||||
|
||||
## 使用GameplayTask监听输入事件
|
||||
InputComponent定义于Actor.h中。
|
||||
|
||||
初始化输入组件
|
||||
```c++
|
||||
void UUserWidget::InitializeInputComponent()
|
||||
{
|
||||
if ( APlayerController* Controller = GetOwningPlayer() )
|
||||
{
|
||||
InputComponent = NewObject< UInputComponent >( this, UInputSettings::GetDefaultInputComponentClass(), NAME_None, RF_Transient );
|
||||
InputComponent->bBlockInput = bStopAction;
|
||||
InputComponent->Priority = Priority;
|
||||
Controller->PushInputComponent( InputComponent );
|
||||
}
|
||||
else
|
||||
{
|
||||
FMessageLog("PIE").Info(FText::Format(LOCTEXT("NoInputListeningWithoutPlayerController", "Unable to listen to input actions without a player controller in {0}."), FText::FromName(GetClass()->GetFName())));
|
||||
}
|
||||
}
|
||||
```
|
||||
```c++
|
||||
void UUserWidget::ListenForInputAction( FName ActionName, TEnumAsByte< EInputEvent > EventType, bool bConsume, FOnInputAction Callback )
|
||||
{
|
||||
if ( !InputComponent )
|
||||
{
|
||||
InitializeInputComponent();
|
||||
}
|
||||
|
||||
if ( InputComponent )
|
||||
{
|
||||
FInputActionBinding NewBinding( ActionName, EventType.GetValue() );
|
||||
NewBinding.bConsumeInput = bConsume;
|
||||
NewBinding.ActionDelegate.GetDelegateForManualSet().BindUObject( this, &ThisClass::OnInputAction, Callback );
|
||||
|
||||
InputComponent->AddActionBinding( NewBinding );
|
||||
}
|
||||
}
|
||||
|
||||
void UUserWidget::StopListeningForInputAction( FName ActionName, TEnumAsByte< EInputEvent > EventType )
|
||||
{
|
||||
if ( InputComponent )
|
||||
{
|
||||
for ( int32 ExistingIndex = InputComponent->GetNumActionBindings() - 1; ExistingIndex >= 0; --ExistingIndex )
|
||||
{
|
||||
const FInputActionBinding& ExistingBind = InputComponent->GetActionBinding( ExistingIndex );
|
||||
if ( ExistingBind.GetActionName() == ActionName && ExistingBind.KeyEvent == EventType )
|
||||
{
|
||||
InputComponent->RemoveActionBinding( ExistingIndex );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UUserWidget::StopListeningForAllInputActions()
|
||||
{
|
||||
if ( InputComponent )
|
||||
{
|
||||
for ( int32 ExistingIndex = InputComponent->GetNumActionBindings() - 1; ExistingIndex >= 0; --ExistingIndex )
|
||||
{
|
||||
InputComponent->RemoveActionBinding( ExistingIndex );
|
||||
}
|
||||
|
||||
UnregisterInputComponent();
|
||||
|
||||
InputComponent->ClearActionBindings();
|
||||
InputComponent->MarkPendingKill();
|
||||
InputComponent = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
- bConsumeInput:是否应该消耗掉这次输入(控制是否传递到下一个InputStack中的对象)
|
||||
|
||||
## UE4对玩家输入的处理规则
|
||||
|
||||
## 有关FScopedPredictionWindow(AbilityTask的网络预测逻辑)
|
||||
```c++
|
||||
FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent, IsPredictingClient());
|
||||
|
||||
if (IsPredictingClient())
|
||||
{
|
||||
// Tell the server about this
|
||||
AbilitySystemComponent->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey(), AbilitySystemComponent->ScopedPredictionKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
AbilitySystemComponent->ConsumeGenericReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey());
|
||||
}
|
||||
|
||||
// We are done. Kill us so we don't keep getting broadcast messages
|
||||
if (ShouldBroadcastAbilityTaskDelegates())
|
||||
{
|
||||
OnPress.Broadcast(ElapsedTime);
|
||||
}
|
||||
```
|
||||
- IsPredictingClient:判断这个Ability的类型是否为locally predicted(本地预测形),如果是的就需要告诉Server。
|
||||
|
||||
UE4 GameplayAbilitySystem Prediction
|
||||
https://zhuanlan.zhihu.com/p/143637846
|
||||
|
||||
### InstancingPolicy
|
||||
技能执行是后,通常会有一个生成技能的新对象,用于定位追踪该技能。但是大多数时候,技能需要频繁的创建使用,可能需要会出现快速实例技能对象对性能产生一定的性能影响。AbilitySystem提供给了三种实例化技能的策略:
|
||||
|
||||
- 按执行实例化:(Instanced per Execution)这就是前面提到的每执行一个技能时候都会实例化一个技能对象,但是如果技能被频繁的调用时候,该技能就会有一定的运行开销。但是优点在于由于这个技能重新运行时候会重新生成一个实例对象,因而该技能中的变量也会初始化,结束技能时候不必考虑重置变量、状态等问题。如果你的技能不是很频繁使用的化可以考虑使用这种的执行策略。
|
||||
- 按Actor实例化:(Instanced per Actor)当技能首次被执行后,后面每次执行这个技能时候都不会被实例化,会重复使用这一个对象。这个和上面的Instanced per Execution策略相反,每次执行这个技能后需要清理这个技能的变量和状态工作。这种策略适用于频繁使用某个技能使用使用,可能提要执行效率。并且因为技能具有可处理变量和RPC的复制对象,而不是浪费网络带宽和CPU时间,在每次运行时产生新对象。
|
||||
- 非实例化:(Non-Instanced)故名思意,该策略的技能被执行时候不会实例化技能对象,而是技能的CDO对象。这种策略是优越于上面的两种策略的,但是也有一个很大限制---这个策略要求这个技能完全都是由C++编写的,因为蓝图创建图标时候需要对象实例,并且即使是C++编写这个技能,该技能也不能更改其成员变量、不能绑定代理、不能复制变量、不能RPC。因此这个技能在游戏中应用的相对较少,但是一些简单的AI技能可以用到。
|
||||
|
||||
|
||||
### NetExecutionPolicy
|
||||
- 本地预测:(Local Predicted)本地预测是指技能将立即在客户端执行,执行过程中不会询问服务端的正确与否,但是同时服务端也起着决定性的作用,如果服务端的技能执行失败了,客户端的技能也会随之停止并“回滚”。如果服务端和客户端执行的结果不矛盾,客户端会执行的非常流畅无延时。如果技能需要实时响应,可以考虑用这种预测模式。下文会着重介绍这个策略。
|
||||
- 仅限本地:(Local Only)仅在本地客户端运行的技能。这个策略不适用于专用服务器游戏,本地客户端执行了该技能,服务端任然没有被执行,在有些情况下可能出现拉扯情况,原因在于服务端技能没有被执行,技能中的某个状态和客户端不一致。
|
||||
- 服务器启动:(Server Initiated)技能将在服务器上启动并PRC到客户端也执行。可以更准确地复制服务器上实际发生的情况,但是客户端会因缺少本地预测而发生短暂延迟。这种延迟相对来说是比较低的,但是相对于Local Predicted来说还是会牺牲一点流畅性。之前我遇到一种情况,一个没有预测的技能A,和一个带有预测的技能B,A执行了后B不能执行, 现在同时执行技能A和技能B, 由于A需要等待服务器端做验证,B是本地预测技能所以B的客户端会瞬间执行,导致B的客户端被执行过了,服务端失败,出现了一些不可预料的问题了,这种情况需要将技能B的网络策略修改为Server Initiated,这样就会以服务端为权威运行,虽然会牺牲点延迟,但是也是可以在游戏接收的范围内,有时候需要在此权衡。
|
||||
- 仅限服务器:(Server Only)技能将在只在服务器上运行,客户端不会。服务端的技能被执行后,技能修改的任何变量都将被复制,并会将状态传递给客户端。缺点是比较服务端的每个影响都会由延迟同步到客户端端,Server Initiated只在运行时候会有一点滞后。
|
||||
|
||||
|
||||
## 使用GameplayTasks制作Combo技能,武器的碰撞判定必须使用同步函数,也就是在AnimNotify中调用
|
377
03-UnrealEngine/Gameplay/GAS/GameFeatures学习笔记.md
Normal file
377
03-UnrealEngine/Gameplay/GAS/GameFeatures学习笔记.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# GameFeatures学习笔记
|
||||
主要参考了大钊的系列文章:
|
||||
- 《InsideUE5》GameFeatures架构(二)基础用法 https://zhuanlan.zhihu.com/p/470184973
|
||||
- 《InsideUE5》GameFeatures架构(三)初始化 https://zhuanlan.zhihu.com/p/473535854
|
||||
- 《InsideUE5》GameFeatures架构(四)状态机 https://zhuanlan.zhihu.com/p/484763722
|
||||
|
||||
|
||||
- GameFeatures插件实现了Actions执行和GameFeature的装载
|
||||
- ModularGameplay插件实现AddComponent等操作
|
||||
|
||||
通过插件创建向导新创建的GameFeature会放在**GameFeatures**文件夹中,也必须放在**GameFeature**文件夹中。并且会创建**GameFeatureData**并且想AssetManager注册。GameFeatures的运行逻辑为:调用ToggleGameFeaturePlugin=>触发GameFeatureAction=>做出对应的操作。
|
||||
做出对应Action操作的Actor对象需要调用UGameFrameworkComponentManager的AddReceiver()与RemoveReceiver()进行注册与反注册。
|
||||
|
||||
- GameFeature,缩写为GF,就是代表一个GameFeature插件。
|
||||
- CoreGame,特意加上Core,是指的把游戏的本体Module和GameFeature相区分开,即还没有GF发挥作用的游戏本体。
|
||||
- UGameFeatureData,缩写为GFD,游戏功能的纯数据配置资产,用来描述了GF要做的动作。
|
||||
- UGameFeatureAction,单个动作,缩写GFA。引擎已经内建了几个Action,我们也可以自己扩展。
|
||||
- UGameFeaturesSubsystem,缩写为GFS,GF框架的管理类,全局的API都可以在这里找到。父类为UEngineSubsystem。
|
||||
- UGameFeaturePluginStateMachine,缩写为GFSM,每个GF插件都关联着一个状态机来管理自身的加载卸载逻辑。
|
||||
- UGameFrameworkComponentManager,缩写为GFCM,支撑AddComponent Action作用的管理类,记录了为哪些Actor身上添加了哪些Component,以便在GF卸载的时候移除掉。
|
||||
|
||||
## 初始化
|
||||
### UGameFeaturesProjectPolicies
|
||||
GameFeature的加载规则类。UE实现一个了默认类为UDefaultGameFeaturesProjectPolicies。默认是会加载所有的GF插件。如果我们需要自定义自己的策略,比如某些GF插件只是用来测试的,后期要关闭掉,就可以继承重载一个自己的策略对象,在里面可以实现自己的过滤器和判断逻辑。
|
||||
|
||||
- GetPreloadAssetListForGameFeature,返回一个GF进入Loading要预先加载的资产列表,方便预载一些资产,比如数据配置表之类的。
|
||||
- IsPluginAllowed,可重载这个函数来进一步判断某个插件是否允许加载,可以做更细的判断。
|
||||
|
||||
可以在项目设置中修改所使用的类。大钊介绍了Additional Plugin Metadata Keys的用法:
|
||||
|
||||
>举个例子,假设2.0版本的游戏要禁用掉以前1.0版本搞活动时的一个GF插件,可以继承定义一个我们自己的Policy对象,然后在Init里的过滤器里实现自己的筛选逻辑,比如截图里就示例了根据uplugin里的MyGameVersion键来指定版本号,然后对比。这里要注意的是,**要先在项目设置里配置上Additional Plugin Metadata Keys**,才能把uplugin文件里的自定义键识别解析到PluginDetails.AdditionalMetadata里,才可以进行后续的判断。至于要添加什么键,就看各位自己的项目需要了。
|
||||
|
||||

|
||||

|
||||
|
||||
```c++
|
||||
void UMyGameFeaturesProjectPolicies::InitGameFeatureManager()
|
||||
{
|
||||
auto AdditionalFilter = [&](const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions) -> bool
|
||||
{ //可以自己写判断逻辑
|
||||
if (const FString* myGameVersion = PluginDetails.AdditionalMetadata.Find(TEXT("MyGameVersion")))
|
||||
{
|
||||
float verison = FCString::Atof(**myGameVersion);
|
||||
if (verison > 2.0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter); //加载内建的所有可能为GF的插件
|
||||
}
|
||||
```
|
||||
|
||||
GF插件的识别是从解析.uplugin文件开始的。我们可以手动编辑这个json文件来详细的描述GF插件。这里有几个键值得一说:
|
||||
- BuiltInAutoState,这个GF被识别后默认初始的状态,共有4种。如果你设为Active,就代表这个GF就是默认激活的。关于GF的状态我们稍后再详细说说。
|
||||
- AdditionalMetadata,这个刚才已经讲过了被Policy识别用的。
|
||||
- PluginDependencies,我们依然可以在GF插件里设置引用别的插件,别的插件可以是普通的插件,也可以是另外的GF插件。这些被依赖的插件会被递归的进行加载。这个递归加载的机制跟普通的插件机制也是一致的。
|
||||
- ExplicitlyLoaded=true,必须为true,因为GF插件是被显式加载的,不像其他插件可能会默认开启。
|
||||
- CanContainContent=true,必须为true,因为GF插件毕竟至少得有GFD资产。
|
||||
|
||||

|
||||
|
||||
## 状态机
|
||||
对每一个GF而言,我们在使用的过程中,能够使用到的GF状态就4个:Installed、Registered、Loaded、Active。这4个状态之间可以双向转换以进行加载卸载。
|
||||
|
||||
也要注意GF状态的切换流程是一条双向的流水线,可以往加载激活的方向前进,在下图上是用黑色的箭头来表示;也可以往失效卸载的方向走,图上是红色的线表示。而箭头上的文字其实就是一个个GFS类里已经提供的GF加载卸载API。 双向箭头中间的圆角矩形表示的是状态,绿色的状态是我们能看见的,但其实内部还有挺多过渡状态的。过渡状态的概念后面也会解释。值得注意的是,UE5预览版增加了一个Terminal状态,可以把整个插件的内存状态释放掉。
|
||||

|
||||
|
||||
在LoadBuiltInGameFeaturePlugin的最后一步GFS会为每一个GF创建一个UGameFeaturePluginStateMachine对象,用来管理内部的GF状态切换。
|
||||
```c++
|
||||
void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter)
|
||||
{
|
||||
//...省略其他代码
|
||||
UGameFeaturePluginStateMachine* StateMachine = GetGameFeaturePluginStateMachine(PluginURL, true);//创建状态机
|
||||
const EBuiltInAutoState InitialAutoState = (BehaviorOptions.AutoStateOverride != EBuiltInAutoState::Invalid) ? BehaviorOptions.AutoStateOverride : PluginDetails.BuiltInAutoState;
|
||||
const EGameFeaturePluginState DestinationState = ConvertInitialFeatureStateToTargetState(InitialAutoState);
|
||||
|
||||
if (StateMachine->GetCurrentState() >= DestinationState)
|
||||
{
|
||||
// If we're already at the destination or beyond, don't transition back
|
||||
LoadGameFeaturePluginComplete(StateMachine, MakeValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
StateMachine->SetDestinationState(DestinationState, FGameFeatureStateTransitionComplete::CreateUObject(this, &ThisClass::LoadGameFeaturePluginComplete));//更新到目标的初始状态
|
||||
}
|
||||
|
||||
//...省略其他代码
|
||||
}
|
||||
```
|
||||
FGameFeaturePluginState为所有状态机的基础结构体,所有派生结构体都会实现所需的虚函数。
|
||||
```c++
|
||||
struct FGameFeaturePluginState
|
||||
{
|
||||
FGameFeaturePluginState(FGameFeaturePluginStateMachineProperties& InStateProperties) : StateProperties(InStateProperties) {}
|
||||
virtual ~FGameFeaturePluginState();
|
||||
|
||||
/** Called when this state becomes the active state */
|
||||
virtual void BeginState() {}
|
||||
|
||||
/** Process the state's logic to decide if there should be a state transition. */
|
||||
virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) {}
|
||||
|
||||
/** Called when this state is no longer the active state */
|
||||
virtual void EndState() {}
|
||||
|
||||
/** Returns the type of state this is */
|
||||
virtual EGameFeaturePluginStateType GetStateType() const { return EGameFeaturePluginStateType::Transition; }
|
||||
|
||||
/** The common properties that can be accessed by the states of the state machine */
|
||||
FGameFeaturePluginStateMachineProperties& StateProperties;
|
||||
|
||||
void UpdateStateMachineDeferred(float Delay = 0.0f) const;
|
||||
void UpdateStateMachineImmediate() const;
|
||||
|
||||
void UpdateProgress(float Progress) const;
|
||||
|
||||
private:
|
||||
mutable FTSTicker::FDelegateHandle TickHandle;
|
||||
};
|
||||
```
|
||||
|
||||
初始化状态机时候会创建所有状态(UGameFeaturePluginStateMachine::InitStateMachine())。
|
||||
|
||||
### 状态机更新流程
|
||||
SetDestinationState()=>UpdateStateMachine()
|
||||
|
||||
UpdateStateMachine()除了判断与Log之外,主要执行了:
|
||||
- 调用当前状态的UpdateState()
|
||||
- 调用当前状态的EndState()
|
||||
- 调用新状态的BeginState()
|
||||
|
||||
#### 检查存在性
|
||||
首先GF插件的最初始状态是Uninitialized,很快进入UnknownStatus,标明还不知道该插件的状态。然后进入CheckingStatus阶段,这个阶段主要目的就是检查uplugin文件是否存在,一般的GF插件都是已经在本地的,可以比较快通过检测。
|
||||

|
||||
|
||||
#### 加载CF C++模块
|
||||
下一个阶段就是开始尝试加载GF的C++模块。这个阶段的初始状态是Installed,这个我用绿色表示,表明它是个目标状态,区分于过渡状态。目标状态意思是在这个状态可以停留住,直到你手动调用API触发迁移到下一个状态,比如你想要注册或激活这个插件,就会把Installed状态向下一个状态转换。卸载的时候往反方向红色的线前进。接着往下:
|
||||
|
||||
- Mounting阶段内部会触发插件管理器显式的加载这个模块,因此会加载dll,触发StartupModule。在以前Unmounting阶段并不会卸载C++,因此不会调用ShutdownModule。意思就是C++模块一经加载就常驻在内存中了。但在UE5预览版中,加上了这一步,因此现在Unmounting已经可以卸载掉插件的dll了。
|
||||
- WaitingForDependencies,会加载之前uplugin里依赖的其他插件模块,递归加载等待所有其他的依赖项完成之后才会进入下一个阶段。这点其实跟普通的插件加载策略是一致的,因此GF插件本质上其实就是以插件的机制在运作,只不过有些地方有些特殊罢了。
|
||||
|
||||

|
||||
|
||||
#### 加载GameFeatureData
|
||||
在C++模块加载完成之后,下一步就要开始把GF自身注册到GFS里面去。其中最重要的一步是在Registering的时候加载GFD资产,会触发UGameFeaturesSubsystem::LoadGameFeatureData(BackupGameFeatureDataPath);的调用从而完成GFD加载。
|
||||

|
||||
|
||||
#### 预加载资产和配置
|
||||
下一个阶段就是进行加载了。Loading阶段会开始加载两种东西,一是插件的运行时的ini(如…/LearnGF/Saved/Config/WindowsEditor/MyFeature.ini)与C++ dll,另外一项是可以预先加载一些资产,资产列表可以由Policy对象根据每个GF插件的GFD文件来获得,因此我们也可以重载GFD来添加我们想要预先加载的资产列表。其他的资产是在激活阶段根据Action的执行按需加载的。
|
||||

|
||||
|
||||
#### 激活生效
|
||||
在加载完成之后,我们就可以激活这个GF了。Activating会为每个GFD里定义的Action触发OnGameFeatureActivating,而Deactivating触发OnGameFeatureDeactivating。激活和反激活是Action真正做事的时机。
|
||||

|
||||
|
||||
### 详细状态切换图
|
||||
位于GameFeaturePluginStateMachine.h
|
||||
```c++
|
||||
/*
|
||||
+--------------+
|
||||
| |
|
||||
|Uninitialized |
|
||||
| |
|
||||
+------+-------+
|
||||
+------------+ |
|
||||
| * | |
|
||||
| Terminal <-------------~------------------------------
|
||||
| | | |
|
||||
+--^------^--+ | |
|
||||
| | | |
|
||||
| | +------v-------+ |
|
||||
| | | * | |
|
||||
| ----------+UnknownStatus | |
|
||||
| | | |
|
||||
| +------+-------+ |
|
||||
| | |
|
||||
| +-----------v---+ +--------------------+ |
|
||||
| | | | ! | |
|
||||
| |CheckingStatus <-----> ErrorCheckingStatus+-->|
|
||||
| | | | | |
|
||||
| +------+------^-+ +--------------------+ |
|
||||
| | | |
|
||||
| | | +--------------------+ |
|
||||
---------- | | | ! | |
|
||||
| | --------> ErrorUnavailable +----
|
||||
| | | |
|
||||
| | +--------------------+
|
||||
| |
|
||||
+-+----v-------+
|
||||
| * |
|
||||
----------+ StatusKnown |
|
||||
| | |
|
||||
| +-^-----+---+--+
|
||||
| | |
|
||||
| ------~---------~-------------------------
|
||||
| | | | |
|
||||
| +--v-----+---+ +-v----------+ +-----v--------------+
|
||||
| | | | | | ! |
|
||||
| |Uninstalling| |Downloading <-------> ErrorInstalling |
|
||||
| | | | | | |
|
||||
| +--------^---+ +-+----------+ +--------------------+
|
||||
| | |
|
||||
| +-+---------v-+
|
||||
| | * |
|
||||
----------> Installed |
|
||||
| |
|
||||
+-^---------+-+
|
||||
| |
|
||||
------~---------~--------------------------------
|
||||
| | | |
|
||||
+--v-----+--+ +-v---------+ +-----v--------------+
|
||||
| | | | | ! |
|
||||
|Unmounting | | Mounting <---------------> ErrorMounting |
|
||||
| | | | | |
|
||||
+--^-----^--+ +--+--------+ +--------------------+
|
||||
| | |
|
||||
------~----------~-------------------------------
|
||||
| | |
|
||||
| +--v------------------- + +-----v-----------------------+
|
||||
| | | | ! |
|
||||
| |WaitingForDependencies <---> ErrorWaitingForDependencies |
|
||||
| | | | |
|
||||
| +--+------------------- + +-----------------------------+
|
||||
| |
|
||||
------~----------~-------------------------------
|
||||
| | | |
|
||||
+--v-----+----+ +--v-------- + +-----v--------------+
|
||||
| | | | | ! |
|
||||
|Unregistering| |Registering <--------------> ErrorRegistering |
|
||||
| | | | | |
|
||||
+--------^----+ ++---------- + +--------------------+
|
||||
| |
|
||||
+-+--------v-+
|
||||
| * |
|
||||
| Registered |
|
||||
| |
|
||||
+-^--------+-+
|
||||
| |
|
||||
+--------+--+ +--v--------+
|
||||
| | | |
|
||||
| Unloading | | Loading |
|
||||
| | | |
|
||||
+--------^--+ +--+--------+
|
||||
| |
|
||||
+-+--------v-+
|
||||
| * |
|
||||
| Loaded |
|
||||
| |
|
||||
+-^--------+-+
|
||||
| |
|
||||
+--------+---+ +-v---------+
|
||||
| | | |
|
||||
|Deactivating| |Activating |
|
||||
| | | |
|
||||
+--------^---+ +-+---------+
|
||||
| |
|
||||
+-+--------v-+
|
||||
| * |
|
||||
| Active |
|
||||
| |
|
||||
+------------+
|
||||
*/
|
||||
```
|
||||
|
||||
## AddComponents
|
||||
主要逻辑位于ModularGameplay模块中的UGameFrameworkComponentManager。GFCM内部的实现还是蛮复杂和精巧的,可以做到在一个GF激活后,会把激活前已经存在场景中Actor,还有激活后新生成的Actor,都会被正确的添加上Component。这个顺序无关的逻辑是怎么做到的呢?关键的逻辑分为两大部分:
|
||||
|
||||
### AddReceiver的注册
|
||||
核心逻辑位于AddReceiverInternal()。我觉得大钊这里搞错了。
|
||||
|
||||
1. 判断Actor是否存在与当前关卡中。
|
||||
2. 递归这个Actor类的父类,直到AActor。
|
||||
3. 从TMap<FComponentRequestReceiverClassPath, TSet<UClass*>> ReceiverClassToComponentClassMap中寻找这个类对应的Component UClass* 集。
|
||||
4. 如果Component类有效则调用CreateComponentOnInstance(),创建并且挂载到Actor上,并将信息放入TMap<UClass*, TSet<FObjectKey>> ComponentClassToComponentInstanceMap中。
|
||||
5. 查询TMap<FComponentRequestReceiverClassPath, FExtensionHandlerEvent> ReceiverClassToEventMap,并执行委托。
|
||||
|
||||
### AddComponents
|
||||
每个GFA在Activating的时候都会调用OnGameFeatureActivating。而对于UGameFeatureAction_AddComponents这个来说,其最终会调用到AddToWorld。在判断一番是否是游戏世界,是否服务器客户端之类的配置之后。最后真正发生发生的作用是Handles.ComponentRequestHandles.Add(GFCM->AddComponentRequest(Entry.ActorClass, ComponentClass));
|
||||
|
||||
AddComponentRequest会增加ReceiverClassToComponentClassMap中的项。
|
||||
1. 创建FComponentRequest,并添加RequestTrackingMap中的项
|
||||
2. 获取LocalGameInstance以及World,并且在World中遍历指定的Actor(ReceiverClass)
|
||||
3. 如果Actor有效则调用CreateComponentOnInstance(),创建并且挂载到Actor上,并将信息放入TMap<UClass*, TSet<FObjectKey>> ComponentClassToComponentInstanceMap中。
|
||||
|
||||
值得注意的是:
|
||||
1. RequestTrackingMap是TMap<FComponentRequest, int32>的类型,可以看到Key是ReceiverClassPath和ComponentClassPtr的组合,那为什么要用int32作为Value来计数呢?这是因为在不同的GF插件里有可能会出现重复的ActorClass-ComponentClass的组合,比如GF1和GF2里都注册了A1-C2的配置。那在卸载GF的时候,我们知道也只有把两个GF1和GF2统统都卸载之后,这个A1-C2的配置才失效,这个时候这个int32计数才=0。因此才需要有个计数来记录生效和失效的次数。
|
||||
2. ReceiverClassToComponentClassMap会记录ActorClass-多个ComponentClass的组合,其会在上文的AddReceiver的时候被用来查询。
|
||||
3. 同样会发现根据代码逻辑,ensureMsgf这个报错也只在WITH_EDITOR的时候才生效。在Runtime下,依然会不管不顾的根据GFA里的配置为相应ActorClass类型的所有Actor实例添加Component。因此这个时候我们明白,AddReceiver的调用准确的说不过是为了GF生效后为新Spawn的Actor添加一个依然能添加相应组件的机会。
|
||||
4. 返回值为何是TSharedPtr<FComponentRequestHandle>?又为何要Add进Handles.ComponentRequestHandles?其实这个时候就涉及到一个逻辑,当GF失效卸载的时候,之前添加的那些Component应该怎么卸载掉?所以这个时候就采取了一个办法,UGameFeatureAction_AddComponents这个Action实例里(不止一个,不同的GF会生成不同的UGameFeatureAction_AddComponents实例)记录着由它创建出来的组件请求,当这个GF被卸载的时候,会触发UGameFeatureAction_AddComponents的析构,继而释放掉TArray<TSharedPtr<FComponentRequestHandle>> ComponentRequestHandles;,而这是个智能指针,只会在最后一个被释放的时候(其实就是最后一个相关的GF被卸载时候),才会触发FComponentRequestHandle的析构,继而在GFCM里真正的移除掉这个ActorClass-ComponentClass的组合,然后在相应的Actor上删除Component实例。
|
||||
|
||||
### 一套UGameFrameworkComponent
|
||||
UE5增加了UPawnComponent,父类为UGameFrameworkComponent(ModularGameplay模块中的文件)。主要是增加了一些Get胶水函数。
|
||||
|
||||
#### Component
|
||||
Component里面一般会写的逻辑有一种是会把Owner的事件给注册进来,比如为Pawn添加输入绑定的组件,会在UActorComponent的OnRegister的时候,把OwnerPawn的Restarted和ControllerChanged事件注册进来监听,以便在合适时机重新应用输入绑定或移除。这里我是想向大家说明,这也是编写Component的一种常用的范式,提供给大家参考。
|
||||
|
||||
```c++
|
||||
void UPlayerControlsComponent::OnRegister()
|
||||
{
|
||||
Super::OnRegister();
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
APawn* MyOwner = GetPawn<APawn>();
|
||||
|
||||
if (ensure(MyOwner) && World->IsGameWorld())
|
||||
{
|
||||
MyOwner->ReceiveRestartedDelegate.AddDynamic(this, &UPlayerControlsComponent::OnPawnRestarted);
|
||||
MyOwner->ReceiveControllerChangedDelegate.AddDynamic(this, &UPlayerControlsComponent::OnControllerChanged);
|
||||
|
||||
// If our pawn has an input component we were added after restart
|
||||
if (MyOwner->InputComponent)
|
||||
{
|
||||
OnPawnRestarted(MyOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Actor
|
||||
- 蓝图中在BeginPlay与EndPlay事件中调用GameFrameworkComponentManager的AddReceiver与RemoveReceived。
|
||||
- c++ 可以参考《古代山谷》的ModularGameplayActors。里面定义的是一些更Modular的Gameplay基础Actor,重载的逻辑主要有两部分,一是把自己注册给GFCM,二是把自己的一些有用的事件转发给GameFeature的Component,比如截图上的ReceivedPlayer和PlayerTick。
|
||||
- 
|
||||
|
||||
## 扩展
|
||||
### Action扩展(
|
||||
古代山谷额外实现的GFA有:AddAbility、AddInputContextMapping、AddLevelInstance、AddSpawnedActor、AddWorldSystem、WorldActionBase
|
||||
|
||||
在实现Action的时候有一个值得注意的地方是,因为GFS是继承于EngineSubsystem的,因此Action的作用机制也是Engine的,因此在编辑器里播放游戏和停止游戏,这些Action其实都是在激活状态的。因此特意有定义了一个WordActionBase的基类,专门注册了OnStartGameInstance的事件,用来在游戏启动的时候执行逻辑。当然在真正用的时候,还需要通过IsGameWorld来判断哪个世界是要发挥作用的世界。
|
||||
|
||||
### GameFeatrues依赖机制
|
||||
普通的插件一般是被CoreGame所引用,用来实现CoreGame的支撑功能。而GameFeature是给已经运行的游戏注入新的功能的,因此CoreGame理论上来说应该对GameFeature的存在和实现一无所知,所以CoreGame就肯定不能引用GF,而是反过来被GF所引用。资产的引用也是同理,GF的资产可以引用CoreGame的,但不能被反过来引用。
|
||||
|
||||
### 可继承扩展的类
|
||||
- UGameFeaturesProjectPolicies,决定是否加载GF的策略,CoreGame
|
||||
- UGameFeatureStateChangeObserver,注册到GFS里监听GF状态变化,CoreGame
|
||||
- GameFeatureData,为GF添加更多描述数据 UGameFeaturesSubsystemSettings::DefaultGameFeatureDataClass ,CoreGame
|
||||
- GameFeatureAction,更多Action,CoreGame&GF
|
||||
- GameFrameworkComponent,更多Component, CoreGame&GF
|
||||
- ModularActor,更多Actor, CoreGame&GF
|
||||
- IAnimLayerInterface,改变角色动画, CoreGame&GF
|
||||
|
||||
### GameFeature模块协作
|
||||
一些朋友可能会问一个问题,一个游戏玩法用到的模块还是挺多的,GameFeature都能和他们协作进行逻辑的更改吗?下面这个图,我列了一些模块和GameFeature的协作交互方式,大家可以简单看一下。如果有别的模块需要交互,还可以通过增加新的Action类型来实现。
|
||||
|
||||

|
||||
|
||||
### CoreGame预留好逻辑注入点
|
||||
GameFeature的出现,虽然说大大解耦了游戏玩法和游戏本体之间的联系,CoreGame理论上来说应该对GF一无所知,极致理想情况下,CoreGame也不需要做任何改变。但GF明显还做不到这点,还是需要你去修改CoreGame的一些代码,事先预留好逻辑的注入点。要修改的点主要有这些:
|
||||
|
||||
原先的Gameplay Actor修改为从之前的Modular里继承,或者自己手动添加AddReceiver的调用。
|
||||
在动画方面,可以利用Anim Layer Interface,在GF里可以定义不同的动画蓝图来实现这个接口。之后在组件里Link Anim Class Layers绑定上就可以修改动画了。
|
||||
关于数据配置,既可以用AddDataRegistrySource添加数据源到CoreGame里现有的DataRegistry里,也可以在GF里用AddDataRegistry新增新的数据注册表。
|
||||
至于其他的模块部分,例如UI、输入这些,之前已经讲过可以用Action和Component的配合来做。
|
||||
|
||||
移植步骤:
|
||||
创建Plugins/GameFeatures目录
|
||||
创建.uplugin文件
|
||||
资产
|
||||
移动资产到新的GF目录
|
||||
在原目录Fixup redirectors
|
||||
修复所有资产验证问题
|
||||
代码
|
||||
创建.Build.cs
|
||||
迁移项目代码到GF下的Public/Private
|
||||
修复include路径错误
|
||||
修复代码引用错误
|
||||
在DefaultEngine.ini里加CoreRedirects +ClassRedirects=(OldName="/Script/MyGame.MyClass", NewName="/Script/MyFeature.MyClass"
|
||||
|
||||
### Rethink in GF
|
||||
- 首先面对一个项目,你需要去思考哪些游戏功能应该可以拆分成GF?哪些是游戏的基本机制?哪些是可以动态开关的功能?就是确定哪些东西应该抽出来形成一个GF。
|
||||
- Actor逻辑拆分到Component,重新以(组件==功能)的方式来思考,而不是所有逻辑都堆在Actor中。然后这些逻辑,在之前往往是直接写在各种Actor身上,现在你需要重新以(组件==功能)的方式来思考,在以前我们提倡Component是用来实现相对机械的独立于游戏逻辑的基础功能的,现在我们为了适应GameFeature,就需要把一部分Component用来单纯的当做给Actor动态插拔的游戏逻辑实现了。因此你需要把这些逻辑从Actor拆分到Component中去,并做好事件回调注册和转发的相应胶水衔接工作。
|
||||
- 在Gameplay的各个方面考虑注入逻辑的可能,数据、UI、Input、玩法等。如果你是在做新的项目或者玩法,你在一开始设计的时候就要预想到未来支持GameFeature的可能,留好各种逻辑注入点。
|
||||
- 进行资产区域划分,哪些是CoreGame,哪些是GameFeature。当然在资产部分,要进行区域划分,哪些是CoreGame用到的资产,哪些是只在Game Feature里使用的资产,各自划分到不同的文件夹里去。
|
||||
- 考虑GF和CoreGame的通用通信机制,事件总线、分发器、消息队列等。在某些时候,你可能光是利用Action和Component还不够,为了解耦CoreGame和GF,你可能还会需要用到一些通用的通信机制,比如事件总线分发器消息队列之类的。
|
||||
- PAK和DLC是对游戏主体的补充,GF是动态装载卸载功能,虽然也可通过PAK动态加载。自然的,有些人可能会想到GF跟Pak和dlc有一点点像,是不是可以结合一下呢。这里也注意要辨析一下,pak和dlc是在资产级别的补丁,一般来说是在资产内容上对游戏本体的补充。而GF是在逻辑层面,强调游戏功能的动态开关。但是GF也确实可以打包成一个pak来进行独立的分发下载加载,因此GF也是可以配合热更新来使用的。
|
@@ -0,0 +1,152 @@
|
||||
## 如何对Asset进行分块
|
||||
https://docs.unrealengine.com/zh-CN/SharingAndReleasing/Patching/GeneralPatching/ChunkingExample/index.html
|
||||
|
||||
### Asset Bundles
|
||||
Asset Bundles(资源束)可以理解为给特定主资源中的次级资源设置“组”,方便你通过“组名”对加载指定组的次级Asset。
|
||||
#### Asset Bundles设置方法
|
||||
设置的方法有两种:
|
||||
|
||||
1)反射标记法
|
||||
```
|
||||
//在UPROPERTY宏中加入meta = (AssetBundles = "TestBundle")
|
||||
//你就创建了名为TestBundle的Asset Bundles,并且添加了该资源到该资源束中
|
||||
//AssetRegistrySearchable 说明符说明此属性与其值将被自动添加到将此包含为成员变量的所有资源类实例的资源注册表。不可在结构体属性或参数上使用。
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Display, AssetRegistrySearchable, meta = (AssetBundles = "TestBundle"))
|
||||
TAssetPtr<UStaticMesh> MeshPtr;
|
||||
```
|
||||
2)运行时注册
|
||||
```
|
||||
//这里我复制了官方文档中的代码
|
||||
//关键就在于AddDynamicAsset函数
|
||||
//这段代码比较旧了,仅作参考
|
||||
UFortAssetManager& AssetManager = UFortAssetManager::Get();
|
||||
FPrimaryAssetId TheaterAssetId = FPrimaryAssetId(UFortAssetManager::FortTheaterInfoType, FName(*TheaterData.UniqueId));
|
||||
|
||||
//展开下载数据
|
||||
TArray<FStringAssetReference> AssetReferences;
|
||||
AssetManager.ExtractStringAssetReferences(FFortTheaterMapData::StaticStruct(), &TheaterData, AssetReferences);
|
||||
|
||||
//创建所有展开数据的Asset Bundle
|
||||
FAssetBundleData GameDataBundles;
|
||||
GameDataBundles.AddBundleAssets(UFortAssetManager::LoadStateMenu, AssetReferences);
|
||||
|
||||
// 递归延展引用,获得区域中的图块蓝图
|
||||
AssetManager.RecursivelyExpandBundleData(GameDataBundles);
|
||||
|
||||
//注册动态Asset
|
||||
AssetManager.AddDynamicAsset(TheaterAssetId, FStringAssetReference(), GameDataBundles);
|
||||
|
||||
// 开始预载入
|
||||
AssetManager.LoadPrimaryAsset(TheaterAssedId, AssetManager.GetDefaultBundleState());
|
||||
```
|
||||
PS.文档中说了个比较骚的思路,那就是由服务器生成Asset数据(比如随机地图)然后下载到本地再加载。个人猜测:如需持久化,需要使用Pak文件方案来解决,不需持久可以考虑设置一个TAssetPtr,之后指向反序列下载的资源,再加载。
|
||||
#### 按照Asset Bundles名称载入主资源中的次级资源
|
||||
首先看一下载入函数:
|
||||
```
|
||||
//预加载使用
|
||||
TSharedPtr<FStreamableHandle> PreloadPrimaryAssets(const TArray<FPrimaryAssetId>& AssetsToLoad, const TArray<FName>& LoadBundles, bool bLoadRecursive, FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);
|
||||
|
||||
//快速载入非主资源的函数
|
||||
virtual TSharedPtr<FStreamableHandle> LoadAssetList(const TArray<FSoftObjectPath>& AssetList, FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority, const FString& DebugName = TEXT("LoadAssetList"));
|
||||
|
||||
virtual TSharedPtr<FStreamableHandle> LoadPrimaryAssets(const TArray<FPrimaryAssetId>& AssetsToLoad, const TArray<FName>& LoadBundles = TArray<FName>(), FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);
|
||||
|
||||
virtual TSharedPtr<FStreamableHandle> LoadPrimaryAsset(const FPrimaryAssetId& AssetToLoad, const TArray<FName>& LoadBundles = TArray<FName>(), FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);
|
||||
|
||||
virtual TSharedPtr<FStreamableHandle> LoadPrimaryAssetsWithType(FPrimaryAssetType PrimaryAssetType, const TArray<FName>& LoadBundles = TArray<FName>(), FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);
|
||||
```
|
||||
有关用法就是指定你想要加载的主资源,如需加载指定Asset Bundles中的次级资源,则填入指定名称。
|
||||
|
||||
后面3个函数本质上的调用ChangeBundleStateForPrimaryAssets函数,它可以将Asset Bundles作为排除列表。
|
||||
|
||||
### 有关Pak方面的资料
|
||||
关于Pak的生成与简单介绍可以参考:
|
||||
https://blog.ch-wind.com/unrealpak-note/
|
||||
有关载入Pak的方法可以参考:
|
||||
https://zhuanlan.zhihu.com/p/79209172
|
||||
### 有关加载进度
|
||||
使用UAssetManager中的 GetResourceAcquireProgress()可以获得加载进度。在为资产获取资源/块时调用委托,如果所有资源都已获取,则参数为true,如果任何资源失败,则为false。
|
||||
|
||||
在加载资源时,你可以获得FStreamableHandle的智能指针对象,调用BindUpdateDelegate,绑定update委托或者GetProgress()也可以获取当前资源的载入进度。
|
||||
### 待探索的问题
|
||||
1. 文档中所注册过的主资源会在扫描后加载,那么其内部的次级资源会加载么?如果不加载,那如何解答umap会加载地图中所有asset?如果加载那么Asset Bundles有什么意义?
|
||||
2. 文档中并没有说明如何对Asset进行分块,仅仅说明了ShooterGame的所有数据分了3个块。分块是不是就等于分成3个Pak呢?
|
||||
|
||||
### 结语
|
||||
本文仅供抛砖引玉,内容仅供参考。因为本人工作与UE4无关,且仅作为个人爱好,目前尚无没有精力对该篇文章所提的观点进行严格论证。而且Asset的加载逻辑本身就是一个经验性工程,恕本人无大型项目开发经验,遂无法撰写一个综合性的案例,还请见谅。
|
||||
|
||||
### 测试结果
|
||||
1. 如果没有注册指定PrimaryAsset,那么从GetPrimaryAssetDataList是无法获取到指定的PrimaryAsset,同时也无法使用LoadPrimaryAsset等函数进行载入。(默认不会载入注册的PrimaryAsset)
|
||||
2. 使用LoadPrimaryAsset函数载入PrimaryAsset,会让内部的SecondaryAssets也一起载入。(不填写LoadBundles形参)
|
||||
3. 在LoadPrimaryAsset函数中添加LoadBundles形参后,有AssetBundles标记的资源将不会加载。
|
||||
4. 想要对AssetBundles标记的资源进行更多的控制请使用ChangeBundleStateForPrimaryAssets函数。
|
||||
|
||||
### 测试代码
|
||||
这里是我测试用的代码,方便大家用来测试,如果错误还请指正:
|
||||
```
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("%s"), *FString("Start Try!"));
|
||||
|
||||
URPGAssetManager& AssetManager = URPGAssetManager::Get();
|
||||
|
||||
FPrimaryAssetId AssetId = AssetManager.GetPrimaryAssetIdForPath(FString("/Game/ActionRPG/DataAsset/NewDataAsset1.NewDataAsset1"));
|
||||
|
||||
FSoftObjectPath AssetPath=AssetManager.GetPrimaryAssetPath(AssetId);
|
||||
TArray<FAssetData> AssetDataList1;
|
||||
|
||||
//virtual UObject* GetPrimaryAssetObject(const FPrimaryAssetId& PrimaryAssetId) const;
|
||||
UObject* AssetPtr = nullptr;
|
||||
AssetPtr=AssetManager.GetPrimaryAssetObject(AssetId);
|
||||
if (AssetPtr == nullptr) {
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("--- result:%s "),*FString("false"));
|
||||
}
|
||||
else {
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("--- result:%s "),*FString("true"));
|
||||
}
|
||||
|
||||
|
||||
AssetManager.GetPrimaryAssetDataList(FPrimaryAssetType(FName("Test")),AssetDataList1);
|
||||
for (FAssetData data : AssetDataList1)
|
||||
{
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("Name:%s"), *data.AssetName.ToString());
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("Class:%s"), *data.AssetClass.ToString());
|
||||
}
|
||||
|
||||
|
||||
AssetPtr = AssetManager.GetPrimaryAssetObject(AssetId);
|
||||
if (AssetPtr == nullptr) {
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("--- result:%s "), *FString("false"));
|
||||
}
|
||||
else {
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("--- result:%s "), *FString("true"));
|
||||
}
|
||||
|
||||
TArray<FName> LoadBundles = { FName("TestBundle") };
|
||||
TArray<FPrimaryAssetId> AssetsToLoad = {AssetId};
|
||||
//AssetHandle=AssetManager.LoadPrimaryAsset(AssetId,LoadBundles);
|
||||
//AssetHandle=AssetManager.LoadPrimaryAssets(AssetsToLoad,LoadBundles);
|
||||
AssetHandle = AssetManager.ChangeBundleStateForPrimaryAssets(AssetsToLoad, TArray<FName>(), LoadBundles,false);
|
||||
//AssetHandle = AssetManager.PreloadPrimaryAssets(AssetsToLoad, LoadBundles, false);
|
||||
|
||||
|
||||
|
||||
AssetPtr = AssetManager.GetPrimaryAssetObject(AssetId);
|
||||
if (AssetPtr == nullptr) {
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("--- result:%s "), *FString("false"));
|
||||
}
|
||||
else {
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("--- result:%s "), *FString("true"));
|
||||
|
||||
UPrimaryDataAssetTest* ptr=Cast<UPrimaryDataAssetTest>(AssetPtr);
|
||||
if (ptr)
|
||||
{
|
||||
TMap<FPrimaryAssetId, TArray<FName>> BundleStateMap;
|
||||
AssetManager.GetPrimaryAssetBundleStateMap(BundleStateMap);
|
||||
|
||||
bool result1=ptr->MeshPtr.IsPending();
|
||||
bool result2=ptr->MeshPtr2.IsPending();
|
||||
bool result3=ptr->MeshPtr3.IsPending();
|
||||
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("--- subAsset:%s %s %s "), *FString(result1 ? TEXT("ture") : TEXT("false")),*FString(result2 ? TEXT("ture") : TEXT("false")),*FString(result3 ? TEXT("ture") : TEXT("false")));
|
||||
}
|
||||
}
|
||||
```
|
@@ -0,0 +1,166 @@
|
||||
### TAssetPtr
|
||||
因为在wiki上已经有介绍TAssetPtr的内容了,所以我就直接翻译了,并且在末尾给予一定的补充。以下是翻译自wiki的内容:
|
||||
|
||||
一般情况下,我们无需将这个场景中所有Asset都加载了再进入游戏,我们可以先加载必要Asset,在进入场景后,异步加载剩余的非必要资源。而TAssetPtr可以解决这个问题
|
||||
|
||||
TAssetPtr类似于标准指针,其区别在于TAssetPtr指向的资产可能已经加载,也可能还没有加载,如果资产没有加载,则它包含加载该资产所需的信息。(TAssetPtr属于弱指针)
|
||||
|
||||
一个简单的引用Asset的方法就是创建带有UProperty标记的TAssetPtr成员变量,并且在编辑器中指定想要加载的Asset。
|
||||
|
||||
本质上是TSoftClassPtr,包含了FSoftObjectPtr对象。可以通过FSoftObjectPath构造,并**用其监视Asset的状态**。
|
||||
#### 类型
|
||||
变量 | 描述
|
||||
---|---
|
||||
TAssetPtr<T> | 指向尚未加载但可以根据请求加载的资产的指针
|
||||
TAssetSubclassOf<T> | 指向已定义的基类的子类的指针,该子类尚未加载,但可以根据请求加载。用于指向蓝图而不是基本component。(大概是因为蓝图是Asset)
|
||||
|
||||
#### 关键功能
|
||||
.h
|
||||
```
|
||||
/** 定义Asset指针. 别忘记添加UPROPERTY标记 */
|
||||
UPROPERTY(EditAnywhere)
|
||||
TAssetPtr<MyClass> MyAssetPointer;
|
||||
|
||||
/** 定义子类版本. */
|
||||
UPROPERTY(EditAnywhere)
|
||||
TAssetSubclassOf<MyBaseClass> MyAssetSubclassOfPointer;
|
||||
```
|
||||
.cpp
|
||||
```
|
||||
// 调用IsValid()去测试这个AssetPtr是不是指向一个有效的UObject
|
||||
MyAssetPointer.IsValid();
|
||||
|
||||
//调用Get()来返回其指向的UObject(在UObject存在的情况下)
|
||||
MyAssetPointer.Get();
|
||||
|
||||
/** 特别注意TAssetSubclassOf的Get(),它返回的是UClass的指针!! */
|
||||
MyAssetSubclassOfPointer.Get()
|
||||
/** 要正确使用UClass指针,必须使用GetDefaultObject<T>()来获得指向UObject或派生类的指针 */
|
||||
MyAssetSubclassOfPointer.Get()->GetDefaultObject<MyBaseClass>()
|
||||
|
||||
// 调用ToStringReference()返回希望加载的Asset的FStringAssetReference
|
||||
MyAssetPointer.ToStringReference();
|
||||
```
|
||||
#### 如何使用
|
||||
变量 | 描述
|
||||
---|---
|
||||
FStreamableManager | 运行时的Asset流控制管理器,这是用户定义的对象,应该被定义在类似GameInstance之类的方便访问的对象中。
|
||||
FStringAssetReference | 一个包含Asset应用字符串的结构体,能对Asset进行弱引用。
|
||||
|
||||
##### Asset载入器
|
||||
FStreamableManager是异步资源加载器,最好定义在类似GameInstance之类持久性对象中,原因有下:
|
||||
|
||||
1. 访问方便,这使得在需要时加载资产变得很容易。
|
||||
2. 具备持久性,因为你永远不想在加载对象时丢失或销毁对FStreamableManager的引用。
|
||||
|
||||
##### 使用方法
|
||||
###### 简单异步载入
|
||||
允许您加载单个资产并获得它的**强引用**。这意味着在您使用unload手动卸载它之前,它永远不会被垃圾回收。(这个 方法已经被废弃,请使用RequestAsyncLoad,并设置bManageActiveHandle为true)
|
||||
```
|
||||
// the .h
|
||||
TAssetPtr<ABaseItem> MyItem;
|
||||
|
||||
// the .cpp
|
||||
FStringAssetReference AssetToLoad
|
||||
AssetToLoad = MyItem.ToStringReference();
|
||||
AssetLoader.SimpleAsyncLoad(AssetToLoad);
|
||||
```
|
||||
###### 请求式异步载入
|
||||
```
|
||||
//the .h
|
||||
TArray< TAssetPtr<ABaseItem> > MyItems;
|
||||
|
||||
// the .cpp
|
||||
TArray<FStringAssetReference> AssetsToLoad
|
||||
for(TAssetPtr<ABaseItem>& AssetPtr : MyItems) // C++11 ranged loop
|
||||
{
|
||||
AssetsToLoad.AddUnique(AssetPtr.ToStringReference());
|
||||
}
|
||||
AssetLoader.RequestAsyncLoad(AssetsToLoad, FStreamableDelegate::CreateUObject(this, &MyClass::MyFunctionToBeCalledAfterAssetsAreLoaded));
|
||||
```
|
||||
PS.实际看过代码之后发现这个RequestAsyncLoad还有一个回调版本的。
|
||||
```
|
||||
/**
|
||||
* This is the primary streamable operation. Requests streaming of one or more target objects. When complete, a delegate function is called. Returns a Streamable Handle.
|
||||
*
|
||||
* @param TargetsToStream Assets to load off disk
|
||||
* @param DelegateToCall Delegate to call when load finishes. Will be called on the next tick if asset is already loaded, or many seconds later
|
||||
* @param Priority Priority to pass to the streaming system, higher priority will be loaded first
|
||||
* @param bManageActiveHandle If true, the manager will keep the streamable handle active until explicitly released
|
||||
* @param bStartStalled If true, the handle will start in a stalled state and will not attempt to actually async load until StartStalledHandle is called on it
|
||||
* @param DebugName Name of this handle, will be reported in debug tools
|
||||
*/
|
||||
TSharedPtr<FStreamableHandle> RequestAsyncLoad(const TArray<FSoftObjectPath>& TargetsToStream, FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = DefaultAsyncLoadPriority, bool bManageActiveHandle = false, bool bStartStalled = false, const FString& DebugName = TEXT("RequestAsyncLoad ArrayDelegate"));
|
||||
TSharedPtr<FStreamableHandle> RequestAsyncLoad(const FSoftObjectPath& TargetToStream, FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = DefaultAsyncLoadPriority, bool bManageActiveHandle = false, bool bStartStalled = false, const FString& DebugName = TEXT("RequestAsyncLoad SingleDelegate"));
|
||||
|
||||
/** Lambda Wrappers. Be aware that Callback may go off multiple seconds in the future. */
|
||||
TSharedPtr<FStreamableHandle> RequestAsyncLoad(const TArray<FSoftObjectPath>& TargetsToStream, TFunction<void()>&& Callback, TAsyncLoadPriority Priority = DefaultAsyncLoadPriority, bool bManageActiveHandle = false, bool bStartStalled = false, const FString& DebugName = TEXT("RequestAsyncLoad ArrayLambda"));
|
||||
TSharedPtr<FStreamableHandle> RequestAsyncLoad(const FSoftObjectPath& TargetToStream, TFunction<void()>&& Callback, TAsyncLoadPriority Priority = DefaultAsyncLoadPriority, bool bManageActiveHandle = false, bool bStartStalled = false, const FString& DebugName = TEXT("RequestAsyncLoad SingleLambda"));
|
||||
```
|
||||
###### 使用Asset
|
||||
当你的Asset加载完成,别忘记调用Get()来取得它。
|
||||
```
|
||||
MyItem.Get(); // returns a pointer to the LIVE UObject
|
||||
```
|
||||
### 本人额外添加的内容(一些有用的东西)
|
||||
#### FStreamableManager
|
||||
```
|
||||
/**
|
||||
* 同步版本的载入函数,用于载入多个(一组)资源,返回一个handle。
|
||||
*
|
||||
* @param TargetsToStream Assets to load off disk
|
||||
* @param bManageActiveHandle If true, the manager will keep the streamable handle active until explicitly released
|
||||
* @param DebugName Name of this handle, will be reported in debug tools
|
||||
*/
|
||||
TSharedPtr<FStreamableHandle> RequestSyncLoad(const TArray<FSoftObjectPath>& TargetsToStream, bool bManageActiveHandle = false, const FString& DebugName = TEXT("RequestSyncLoad Array"));
|
||||
TSharedPtr<FStreamableHandle> RequestSyncLoad(const FSoftObjectPath& TargetToStream, bool bManageActiveHandle = false, const FString& DebugName = TEXT("RequestSyncLoad Single"));
|
||||
|
||||
/**
|
||||
* 同步版本的载入函数,用于载入单个资源,返回UObject的指针。如果没有找到则返回空指针。
|
||||
*
|
||||
* @param Target Specific asset to load off disk
|
||||
* @param bManageActiveHandle If true, the manager will keep the streamable handle active until explicitly released
|
||||
* @param RequestHandlePointer If non-null, this will set the handle to the handle used to make this request. This useful for later releasing the handle
|
||||
*/
|
||||
UObject* LoadSynchronous(const FSoftObjectPath& Target, bool bManageActiveHandle = false, TSharedPtr<FStreamableHandle>* RequestHandlePointer = nullptr);
|
||||
```
|
||||
#### FStreamableHandle
|
||||
同步或者异步加载的句柄,只要句柄处于激活状态,那么Asset就不会被回收。你可以句柄来控制Asset,以及绑定相应的委托。
|
||||
```
|
||||
/** Bind delegate that is called when load completes, only works if loading is in progress. This will overwrite any already bound delegate! */
|
||||
bool BindCompleteDelegate(FStreamableDelegate NewDelegate);
|
||||
|
||||
/** Bind delegate that is called if handle is canceled, only works if loading is in progress. This will overwrite any already bound delegate! */
|
||||
bool BindCancelDelegate(FStreamableDelegate NewDelegate);
|
||||
|
||||
/** Bind delegate that is called periodically as delegate updates, only works if loading is in progress. This will overwrite any already bound delegate! */
|
||||
bool BindUpdateDelegate(FStreamableUpdateDelegate NewDelegate);
|
||||
|
||||
/**
|
||||
* Blocks until the requested assets have loaded. This pushes the requested asset to the top of the priority list,
|
||||
* but does not flush all async loading, usually resulting in faster completion than a LoadObject call
|
||||
*
|
||||
* @param Timeout Maximum time to wait, if this is 0 it will wait forever
|
||||
* @param StartStalledHandles If true it will force all handles waiting on external resources to try and load right now
|
||||
*/
|
||||
EAsyncPackageState::Type WaitUntilComplete(float Timeout = 0.0f, bool bStartStalledHandles = true);
|
||||
|
||||
/** Gets list of assets references this load was started with. This will be the paths before redirectors, and not all of these are guaranteed to be loaded */
|
||||
void GetRequestedAssets(TArray<FSoftObjectPath>& AssetList) const;
|
||||
|
||||
/** Adds all loaded assets if load has succeeded. Some entries will be null if loading failed */
|
||||
void GetLoadedAssets(TArray<UObject *>& LoadedAssets) const;
|
||||
|
||||
/** Returns first asset in requested asset list, if it's been successfully loaded. This will fail if the asset failed to load */
|
||||
UObject* GetLoadedAsset() const;
|
||||
|
||||
/** Returns number of assets that have completed loading out of initial list, failed loads will count as loaded */
|
||||
void GetLoadedCount(int32& LoadedCount, int32& RequestedCount) const;
|
||||
|
||||
/** Returns progress as a value between 0.0 and 1.0. */
|
||||
float GetProgress() const;
|
||||
```
|
||||
#### FSoftObjectPath
|
||||
一个包含对象的引用字符串的结构体。它可以对按需加载的资产进行弱引用。可以使用UProperties进行标记,使得它可以在编辑器中显示并且可以指定资源。
|
||||
|
||||
可以使用TryLoad进行Asset载入,返回UObject指针,为空则表示载入失败。
|
@@ -0,0 +1,318 @@
|
||||
### 前言
|
||||
AssetManager是一个全局单例类,用于管理各种primary assets和asset bundles。配合配套工具 “(Reference Viewer)、“资源审计(Asset Audit)”可以理清Pak文件中Asset的依赖关系以及所占用的空间。
|
||||
|
||||
自定义AssetManager可以实现自定义Asset、自定义Asset载入逻辑与控制(异步与同步载入、获取载入进度、运行时修改载入优先级、运行时异步加载强制等待等)、通过网络下载Asset并载入、烘焙数据分包(打包成多个pak)。
|
||||
|
||||
在actionRPG项目中通过继承UPrimaryDataAsset来实现自定义DataAsset文件(content browser中能创建的Asset),具体请参照RPGItem,其中包含了包含了文字说明(FTEXT)、物品图标(FSlateBrush)、价格(int32)、相关能力(UGameplayAbility),具体的之后会介绍。
|
||||
|
||||
在ShooterGame中实现了如何将资源分包(打包多个pak)。
|
||||
|
||||
本文将解析actionRPG中所用相关功能,另外功能会在之后的文章中解析。本人参考的资料如下:
|
||||
|
||||
**文档地址:**
|
||||
|
||||
https://docs.unrealengine.com/zh-CN/Engine/Basics/AssetsAndPackages/AssetManagement/index.html
|
||||
|
||||
https://docs.unrealengine.com/en-US/Engine/Basics/AssetsAndPackages/AssetManagement/CookingAndChunking/index.html
|
||||
|
||||
在这一篇文档中,介绍了使用Primary Asset Labels与Rules Overrides对所有需要烘焙的数据进行分包处理(打包成多个Pak文件),这个操作也需要实现自定义的AssetManager类,具体的可以参考**ShooterGame**案例。
|
||||
|
||||
**AnswerHUB、论坛帖子与wiki:**
|
||||
|
||||
从网上下载Asset并使用StreamableManagers加载:(文章较旧仅供参考)
|
||||
https://answers.unrealengine.com/questions/109485/stream-an-asset-from-the-internet.html
|
||||
|
||||
TAssetPtr与Asset异步加载:
|
||||
https://wiki.unrealengine.com/index.php?title=TAssetPtr_and_Asynchronous_Asset_Loading
|
||||
|
||||
(代码较旧仅供参考)
|
||||
https://github.com/moritz-wundke/AsyncPackageStreamer
|
||||
|
||||
在移动端版本更新的工具蓝图函数(和本文内容关系不大)
|
||||
https://docs.unrealengine.com/en-US/Engine/Blueprints/UserGuide/PatchingNodes/index.html
|
||||
|
||||
几种Asset加载方法(文章较旧仅供参考)
|
||||
https://www.sohu.com/a/203578475_667928
|
||||
|
||||
**谁允许你直视本大叔的 的Blog**:
|
||||
- Unreal Engine 4 —— Asset Manager介绍:https://blog.csdn.net/noahzuo/article/details/78815596
|
||||
- Unreal Engine 4 —— Fortnite中的Asset Manager与资源控制:https://blog.csdn.net/noahzuo/article/details/78892664
|
||||
|
||||
**Saeru_Hikari 的Blog**
|
||||
|
||||
动作游戏框架子模块剖析(其一)------DataAsset:https://www.bilibili.com/read/cv2855601/
|
||||
### AssetManager及相关名词简述
|
||||
AssetManager可以使得开发者更加精确地控制资源发现与加载时机。AssetManager是存在于**编辑器**和**游戏**中的**单例全局对象**,用于管理primary assets和asset bundles,我们可以根据自己的需求去重写它。
|
||||
|
||||
#### Assest
|
||||
Asset指的是在Content Browser中看到的那些物件。贴图,BP,音频和地图等都属于Asset文件。
|
||||
#### Asset Registry
|
||||
Asset Registry是Asset注册表,位于Project Settings——AssetManager中(Primany Asset Types To Scan),其中存储了每个的asset的有用信息。这些信息会在asset被储存的时候进行更新。
|
||||
|
||||
#### Streamable Managers
|
||||
Streamable Managers负责进行读取物件并将其放在内存中.
|
||||
|
||||
#### Asset Bundle
|
||||
Asset Bundle是一个Asset的列表,用于将一堆Asset在runtime的时候载入。
|
||||
|
||||
### Primary Assets、Secondary Assets与Primary Asset Labels
|
||||
AssetManagementSystem将资源分为两类:**PrimaryAssets**与**SecondaryAssets**。
|
||||
|
||||
#### PrimaryAssets
|
||||
**PrimaryAssets**可以通过,调用GetPrimaryAssetId()获取的**PrimaryAssetID**对其直接操作。
|
||||
|
||||
将特定UObject类构成的资源指定**PrimaryAssets**,需要重写GetPrimaryAssetId函数,使其返回有效的一个有效的FPrimaryAssetId结构。
|
||||
|
||||
#### SecondaryAssets
|
||||
**SecondaryAssets**不由AssetManagementSystem直接处理,但其被PrimaryAssets引用或使用后引擎便会自动进行加载。默认情况下只有UWorld(关卡Asset )为主资源;所有其他资源均为次资源。
|
||||
|
||||
将**SecondaryAssets**设为**PrimaryAssets**,必须重写GetPrimaryAssetId函数,返回一个有效的 FPrimaryAssetId结构。
|
||||
|
||||
### 自定义DataAsset
|
||||
这里我将通过解读actionRPG中的做法来进行介绍:
|
||||
|
||||
#### 编写自定义AssetManager
|
||||
- 继承UAssetManager创建URPGAssetManager类。
|
||||
- 实现单例类所需的static函数Get()。
|
||||
```
|
||||
static URPGAssetManager& Get();
|
||||
```
|
||||
```
|
||||
URPGAssetManager& URPGAssetManager::Get()
|
||||
{
|
||||
//直接从引擎获取指定的AssetManager
|
||||
URPGAssetManager* This = Cast<URPGAssetManager>(GEngine->AssetManager);
|
||||
if (This)
|
||||
{
|
||||
return *This;
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogActionRPG, Fatal, TEXT("Invalid AssetManager in DefaultEngine.ini, must be RPGAssetManager!"));
|
||||
return *NewObject<URPGAssetManager>(); // never calls this
|
||||
}
|
||||
}
|
||||
```
|
||||
- 除此之外还重写了StartInitialLoading函数,用于在AssetManager初始化扫描PrimaryAsset后初始化GameplayAbility的数据;以及实现了用于强制加载RPGItem类Asset的ForceLoadItem函数(RPGItem为之后创建的自定义DataAsset类)。
|
||||
```
|
||||
void URPGAssetManager::StartInitialLoading()
|
||||
{
|
||||
Super::StartInitialLoading();
|
||||
UAbilitySystemGlobals::Get().InitGlobalData();
|
||||
}
|
||||
```
|
||||
```
|
||||
URPGItem* URPGAssetManager::ForceLoadItem(const FPrimaryAssetId& PrimaryAssetId, bool bLogWarning)
|
||||
{
|
||||
FSoftObjectPath ItemPath = GetPrimaryAssetPath(PrimaryAssetId);
|
||||
|
||||
//使用同步方法来载入Asset,TryLoad内部使用了StaticLoadObject与LoadObject
|
||||
URPGItem* LoadedItem = Cast<URPGItem>(ItemPath.TryLoad());
|
||||
|
||||
if (bLogWarning && LoadedItem == nullptr)
|
||||
{
|
||||
UE_LOG(LogActionRPG, Warning, TEXT("Failed to load item for identifier %s!"), *PrimaryAssetId.ToString());
|
||||
}
|
||||
|
||||
return LoadedItem;
|
||||
}
|
||||
```
|
||||
- 在Project Settings——Engine——General Settings——Default Classes——Asset Manager Class中指定你创建的AssetManager。
|
||||
#### 编写自定义DataAsset
|
||||
- 继承UPrimaryDataAsset创建URPGItem类。
|
||||
- 重写GetPrimaryAssetId(),以此让AssetManager“认识”我们写的DataAsset
|
||||
```
|
||||
FPrimaryAssetId URPGItem::GetPrimaryAssetId() const
|
||||
{
|
||||
//因为这里URPGItem会被作为一个DataAsset使用而不是一个blueprint,所以我们可以使用他的FName。
|
||||
//如果作为blueprint,就需要手动去掉名字中的“_C”
|
||||
return FPrimaryAssetId(ItemType, GetFName());
|
||||
}
|
||||
```
|
||||
- 声明所需的变量,实现所需的函数
|
||||
```
|
||||
class URPGGameplayAbility;
|
||||
|
||||
/** Base class for all items, do not blueprint directly */
|
||||
UCLASS(Abstract, BlueprintType)
|
||||
class ACTIONRPG_API URPGItem : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Constructor */
|
||||
URPGItem()
|
||||
: Price(0)
|
||||
, MaxCount(1)
|
||||
, MaxLevel(1)
|
||||
, AbilityLevel(1)
|
||||
{}
|
||||
|
||||
/** Type of this item, set in native parent class */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Item)
|
||||
FPrimaryAssetType ItemType;
|
||||
|
||||
/** User-visible short name */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
|
||||
FText ItemName;
|
||||
|
||||
/** User-visible long description */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
|
||||
FText ItemDescription;
|
||||
|
||||
/** Icon to display */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
|
||||
FSlateBrush ItemIcon;
|
||||
|
||||
/** Price in game */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
|
||||
int32 Price;
|
||||
|
||||
/** Maximum number of instances that can be in inventory at once, <= 0 means infinite */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Max)
|
||||
int32 MaxCount;
|
||||
|
||||
/** Returns if the item is consumable (MaxCount <= 0)*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = Max)
|
||||
bool IsConsumable() const;
|
||||
|
||||
/** Maximum level this item can be, <= 0 means infinite */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Max)
|
||||
int32 MaxLevel;
|
||||
|
||||
/** Ability to grant if this item is slotted */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Abilities)
|
||||
TSubclassOf<URPGGameplayAbility> GrantedAbility;
|
||||
|
||||
/** Ability level this item grants. <= 0 means the character level */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Abilities)
|
||||
int32 AbilityLevel;
|
||||
|
||||
/** Returns the logical name, equivalent to the primary asset id */
|
||||
UFUNCTION(BlueprintCallable, Category = Item)
|
||||
FString GetIdentifierString() const;
|
||||
|
||||
/** Overridden to use saved type */
|
||||
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
|
||||
};
|
||||
```
|
||||
```
|
||||
bool URPGItem::IsConsumable() const
|
||||
{
|
||||
if (MaxCount <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FString URPGItem::GetIdentifierString() const
|
||||
{
|
||||
return GetPrimaryAssetId().ToString();
|
||||
}
|
||||
|
||||
FPrimaryAssetId URPGItem::GetPrimaryAssetId() const
|
||||
{
|
||||
return FPrimaryAssetId(ItemType, GetFName());
|
||||
}
|
||||
```
|
||||
-之后为了保证FPrimaryAssetType统一,我们可以到URPGAssetManager中添加几个FPrimaryAssetType类型全局静态变量,并在cpp文件中进行赋值。
|
||||
```
|
||||
class ACTIONRPG_API URPGAssetManager : public UAssetManager
|
||||
{
|
||||
//上略
|
||||
|
||||
static const FPrimaryAssetType PotionItemType;
|
||||
static const FPrimaryAssetType SkillItemType;
|
||||
static const FPrimaryAssetType TokenItemType;
|
||||
static const FPrimaryAssetType WeaponItemType;
|
||||
|
||||
//下略
|
||||
}
|
||||
```
|
||||
```
|
||||
//在cpp文件中进行赋值
|
||||
const FPrimaryAssetType URPGAssetManager::PotionItemType = TEXT("Potion");
|
||||
const FPrimaryAssetType URPGAssetManager::SkillItemType = TEXT("Skill");
|
||||
const FPrimaryAssetType URPGAssetManager::TokenItemType = TEXT("Token");
|
||||
const FPrimaryAssetType URPGAssetManager::WeaponItemType = TEXT("Weapon");
|
||||
```
|
||||
之后就可以在URPGItem的派生类中通过这些全局变量给FPrimaryAssetType赋值了,例如:
|
||||
```
|
||||
URPGPotionItem()
|
||||
{
|
||||
ItemType = URPGAssetManager::PotionItemType;
|
||||
}
|
||||
```
|
||||
-最后一步就是编写相应的URPGItem派生类,并在在Project Settings——Game——Asset Manager——Primany Asset Types To Scan中添加。注意PrimanyAssetType必须填写正确,不然引擎是搜索不到的。
|
||||
#### 数据加载
|
||||
项目中会通过ARPGPlayerControllerBase中LoadInventory函数加载DataAsset数据。最终会使用AssetManager.ForceLoadItem来加载Asset(PS.在构造函数中会调用LoadInventory)
|
||||
```
|
||||
bool ARPGPlayerControllerBase::LoadInventory()
|
||||
{
|
||||
InventoryData.Reset();
|
||||
SlottedItems.Reset();
|
||||
|
||||
// Fill in slots from game instance
|
||||
UWorld* World = GetWorld();
|
||||
URPGGameInstanceBase* GameInstance = World ? World->GetGameInstance<URPGGameInstanceBase>() : nullptr;
|
||||
|
||||
if (!GameInstance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const TPair<FPrimaryAssetType, int32>& Pair : GameInstance->ItemSlotsPerType)
|
||||
{
|
||||
for (int32 SlotNumber = 0; SlotNumber < Pair.Value; SlotNumber++)
|
||||
{
|
||||
SlottedItems.Add(FRPGItemSlot(Pair.Key, SlotNumber), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
URPGSaveGame* CurrentSaveGame = GameInstance->GetCurrentSaveGame();
|
||||
URPGAssetManager& AssetManager = URPGAssetManager::Get();
|
||||
if (CurrentSaveGame)
|
||||
{
|
||||
// Copy from save game into controller data
|
||||
bool bFoundAnySlots = false;
|
||||
for (const TPair<FPrimaryAssetId, FRPGItemData>& ItemPair : CurrentSaveGame->InventoryData)
|
||||
{
|
||||
URPGItem* LoadedItem = AssetManager.ForceLoadItem(ItemPair.Key);
|
||||
|
||||
if (LoadedItem != nullptr)
|
||||
{
|
||||
InventoryData.Add(LoadedItem, ItemPair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const TPair<FRPGItemSlot, FPrimaryAssetId>& SlotPair : CurrentSaveGame->SlottedItems)
|
||||
{
|
||||
if (SlotPair.Value.IsValid())
|
||||
{
|
||||
URPGItem* LoadedItem = AssetManager.ForceLoadItem(SlotPair.Value);
|
||||
if (GameInstance->IsValidItemSlot(SlotPair.Key) && LoadedItem)
|
||||
{
|
||||
SlottedItems.Add(SlotPair.Key, LoadedItem);
|
||||
bFoundAnySlots = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bFoundAnySlots)
|
||||
{
|
||||
// Auto slot items as no slots were saved
|
||||
FillEmptySlots();
|
||||
}
|
||||
|
||||
NotifyInventoryLoaded();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load failed but we reset inventory, so need to notify UI
|
||||
NotifyInventoryLoaded();
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
@@ -0,0 +1,117 @@
|
||||
GetResourceAcquireProgress 加载进度函数。
|
||||
|
||||
文档地址:
|
||||
https://docs.unrealengine.com/zh-CN/Engine/Basics/AssetsAndPackages/AssetManagement/index.html
|
||||
|
||||
谁允许你直视本大叔的 的Blog:
|
||||
- https://blog.csdn.net/noahzuo/article/details/78815596
|
||||
- https://blog.csdn.net/noahzuo/article/details/78892664
|
||||
|
||||
#### 简述
|
||||
AssetManager可以使得开发者更加精确地控制资源发现与加载时机。AssetManager是存在于编辑器和游戏中的单例全局对象。
|
||||
|
||||
#### Primary Assets、Secondary Assets与Primary Asset Labels
|
||||
AssetManagementSystem将资源分为两类:**PrimaryAssets**与**SecondaryAssets**。
|
||||
|
||||
##### PrimaryAssets
|
||||
**PrimaryAssets**可以通过,调用GetPrimaryAssetId()获取的**PrimaryAssetID**对其直接操作。
|
||||
|
||||
将特定UObject类构成的资源指定**PrimaryAssets**,需要重写GetPrimaryAssetId函数,使其返回有效的一个有效的FPrimaryAssetId结构。
|
||||
|
||||
##### SecondaryAssets
|
||||
**SecondaryAssets**不由AssetManagementSystem直接处理,但其被PrimaryAssets引用或使用后引擎便会自动进行加载。默认情况下只有UWorld(关卡Asset )为主资源;所有其他资源均为次资源。
|
||||
|
||||
将**SecondaryAssets**设为**PrimaryAssets**,必须重写GetPrimaryAssetId函数,返回一个有效的 FPrimaryAssetId结构。
|
||||
|
||||
#### UAssetManager与FStreamableManager
|
||||
UAssetManager是一个单例对象,负责管理主资源的发现与加载。FStreamableManager对象也被包含在其中,可以用来执行异步加载资源。通过FStreamableHandle(它是一个智能指针)来控制资源的生命周期(加载与卸载)。
|
||||
|
||||
与UAssetManager不同,FStreamableManager可以建立多个实例。
|
||||
#### AssetBundle
|
||||
AssetBundle是与主资源相关特定资源的命名列表。使用
|
||||
```
|
||||
meta = (AssetBundles = "TestBundle")
|
||||
```
|
||||
对UObject中的TAssetPtr类型成员变量或FStringAssetReference中的成员变量的UPROPERTY代码进行标记,即可完成创建。例如:
|
||||
```
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Display, AssetRegistrySearchable, meta = (AssetBundles = "TestBundle"))
|
||||
TAssetPtr<UStaticMesh> MeshPtr;
|
||||
```
|
||||
##### 运行时创建
|
||||
1. 创建FAssetBudleData结构体对象。
|
||||
2. 调用UAssetManager的AddDynamicAsset函数。
|
||||
3. 使PrimaryAssets的ID与AssetBundle中的SecondaryAssets关联起来。
|
||||
|
||||
#### 从硬盘中加载PrimaryAssets
|
||||
程序员可以通过继承UPrimaryDataAsset(它的父类是UDataAsset,拥有加载和保存内置资源束数据的功能)的方式来控制ContentBrowser中的Asset(PrimaryAssets)。
|
||||
|
||||
下面是一个使用UPrimaryDataAsset的范例,它告诉引擎进入什么地图需要什么资源。
|
||||
```
|
||||
/** A zone that can be selected by the user from the map screen */
|
||||
UCLASS(Blueprintable)
|
||||
class FORTNITEGAME_API UFortZoneTheme : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_UCLASS_BODY()
|
||||
|
||||
/** Name of the zone */
|
||||
UPROPERTY(EditDefaultsOnly, Category=Zone)
|
||||
FText ZoneName;
|
||||
|
||||
/** The map that will be loaded when entering this zone */
|
||||
UPROPERTY(EditDefaultsOnly, Category=Zone)
|
||||
TAssetPtr<UWorld> ZoneToUse;
|
||||
|
||||
/** The blueprint class used to represent this zone on the map */
|
||||
UPROPERTY(EditDefaultsOnly, Category=Visual, meta=(AssetBundles = "Menu"))
|
||||
TAssetSubclassOf<class AFortTheaterMapTile> TheaterMapTileClass;
|
||||
};
|
||||
```
|
||||
##### 注册PrimaryAssets步骤
|
||||
###### 如果项目中有自定义的UAssetManager就需要向引擎进行注册
|
||||
修改引擎目录中的DefaultEngine.ini,修改[/Script/Engine.Engine]段中的AssetManagerClassName变量。
|
||||
```
|
||||
[/Script/Engine.Engine]
|
||||
AssetManagerClassName=/Script/Module.UClassName
|
||||
```
|
||||
其中“Module”代表项目的模块名,“UClassName”则代表希望使用的UClass名。在Fortnite中,项目的模块名为“FortniteGame”,希望使用的类则名为 UFortAssetManager(意味着其 UClass 命名为 FortAssetManager)所以第二行应为:
|
||||
```
|
||||
AssetManagerClassName=/Script/FortniteGame.FortAssetManager
|
||||
```
|
||||
###### 向UAssetManager注册PrimaryAssets
|
||||
方法有三:
|
||||
1.在Project Settings——Game——AssetManager中,进入如下设置:
|
||||

|
||||
|
||||
每个选项的具体功能请参考文档。
|
||||
2.编辑DefaultGame.ini文件:找到(或创建)一个名为 /Script/Engine.AssetManagerSettings的代码段,添加:
|
||||
```
|
||||
[/Script/Engine.AssetManagerSettings]
|
||||
!PrimaryAssetTypesToScan=ClearArray
|
||||
+PrimaryAssetTypesToScan=(PrimaryAssetType="Map",AssetBaseClass=/Script/Engine.World,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game/Maps")),SpecificAssets=,Rules=(Priority=-1,bApplyRecursively=True,ChunkId=-1,CookRule=Unknown))
|
||||
+PrimaryAssetTypesToScan=(PrimaryAssetType="PrimaryAssetLabel",AssetBaseClass=/Script/Engine.PrimaryAssetLabel,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game")),SpecificAssets=,Rules=(Priority=-1,bApplyRecursively=True,ChunkId=-1,CookRule=Unknown))
|
||||
```
|
||||
3.在代码中操作:重写UAssetManager类中的StartInitialLoading函数并从该处调用ScanPathsForPrimaryAssets。因此,推荐您将所有同类型的主资源放入相同的子文件夹中。这将使资源查找和注册更为迅速。
|
||||
###### 加载资源
|
||||
LoadPrimaryAssets、LoadPrimaryAsset和LoadPrimaryAssetsWithType适用于游戏启动前。
|
||||
之后通过UnloadPrimaryAssets、UnloadPrimaryAsset 和 UnloadPrimaryAssetsWithType卸载。
|
||||
#### 动态注册与加载PrimaryAsset
|
||||
```
|
||||
//从AssetId构建Asset字符串表,并且构建AssetBundle数据
|
||||
UFortAssetManager& AssetManager = UFortAssetManager::Get();
|
||||
FPrimaryAssetId TheaterAssetId = FPrimaryAssetId(UFortAssetManager::FortTheaterInfoType, FName(*TheaterData.UniqueId));
|
||||
|
||||
TArray<FStringAssetReference> AssetReferences;
|
||||
AssetManager.ExtractStringAssetReferences(FFortTheaterMapData::StaticStruct(), &TheaterData, AssetReferences);
|
||||
|
||||
FAssetBundleData GameDataBundles;
|
||||
GameDataBundles.AddBundleAssets(UFortAssetManager::LoadStateMenu, AssetReferences);
|
||||
|
||||
//通过递归的方式,展开AssetBundle数据(获取SecondaryAssets数据)
|
||||
AssetManager.RecursivelyExpandBundleData(GameDataBundles);
|
||||
|
||||
// 注册动态资源
|
||||
AssetManager.AddDynamicAsset(TheaterAssetId, FStringAssetReference(), GameDataBundles);
|
||||
|
||||
// 开始预加载
|
||||
AssetManager.LoadPrimaryAsset(TheaterAssetId, AssetManager.GetDefaultBundleState());
|
||||
```
|
BIN
03-UnrealEngine/Gameplay/Gameplay/AssetManager/Ue4资源加载方式.png
Normal file
BIN
03-UnrealEngine/Gameplay/Gameplay/AssetManager/Ue4资源加载方式.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Daedalic Entertainment公司分享的第三人称摄像机经验
|
||||
date: 2022-12-09 14:06:53
|
||||
excerpt:
|
||||
tags: Camera
|
||||
rating: ⭐
|
||||
---
|
||||
#### blog website
|
||||
https://www.unrealengine.com/en-US/blog/six-ingredients-for-a-dynamic-third-person-camera
|
||||
|
||||
#### github
|
||||
https://github.com/DaedalicEntertainment/third-person-camera
|
@@ -0,0 +1,312 @@
|
||||
## tomlooman写的教程
|
||||
https://www.tomlooman.com/save-system-unreal-engine-tutorial/
|
||||
|
||||
## 保存关卡世界数据与状态
|
||||
要保存世界状态,我们必须决定为每个演员存储哪些变量,以及我们需要保存在磁盘上的哪些杂项信息。例如每个玩家获得的金钱数。金钱数并不是世界状态的真正组成部分,而是属于PlayerState。尽管PlayerState存在于世界中,并且事实上是一个角色,但我们还是将它们分开处理,这样我们就可以根据它以前属于哪个玩家来正确地恢复它。
|
||||
|
||||
## Actor数据
|
||||
对于 Actor 变量,我们存储其名称、变换(位置、旋转、缩放)和一个字节数据数组,其中包含在其 UPROPERTY 中标有“SaveGame”的所有变量。
|
||||
```
|
||||
USTRUCT()
|
||||
struct FActorSaveData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/* Identifier for which Actor this belongs to */
|
||||
UPROPERTY()
|
||||
FName ActorName;
|
||||
|
||||
/* For movable Actors, keep location,rotation,scale. */
|
||||
UPROPERTY()
|
||||
FTransform Transform;
|
||||
|
||||
/* Contains all 'SaveGame' marked variables of the Actor */
|
||||
UPROPERTY()
|
||||
TArray<uint8> ByteData;
|
||||
};
|
||||
```
|
||||
|
||||
## 将变量转换为二进制
|
||||
要将变量转换为二进制数组,我们需要一个FMemoryWriter和FObjectAndNameAsStringProxyArchive,它们派生自 FArchive(虚幻的数据容器,用于各种序列化数据,包括您的游戏内容)。
|
||||
|
||||
我们按接口过滤,以避免在我们不想保存的世界中潜在的数千个静态 Actor 上调用 Serialize。存储 Actor 的名称将在稍后用于识别要反序列化(加载)数据的 Actor。您可以想出自己的解决方案,例如FGuid(主要用于可能没有一致名称的运行时生成的 Actor)由于内置系统,其余的代码非常简单(并在注释中进行了解释)。
|
||||
```
|
||||
void ASGameModeBase::WriteSaveGame()
|
||||
{
|
||||
// ... < playerstate saving code ommitted >
|
||||
|
||||
// Clear all actors from any previously loaded save to avoid duplicates
|
||||
CurrentSaveGame->SavedActors.Empty();
|
||||
|
||||
// Iterate the entire world of actors
|
||||
for (FActorIterator It(GetWorld()); It; ++It)
|
||||
{
|
||||
AActor* Actor = *It;
|
||||
// Only interested in our 'gameplay actors', skip actors that are being destroyed
|
||||
// Note: You might instead use a dedicated SavableObject interface for Actors you want to save instead of re-using GameplayInterface
|
||||
if (Actor->IsPendingKill() || !Actor->Implements<USGameplayInterface>())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FActorSaveData ActorData;
|
||||
ActorData.ActorName = Actor->GetFName();
|
||||
ActorData.Transform = Actor->GetActorTransform();
|
||||
|
||||
// Pass the array to fill with data from Actor
|
||||
FMemoryWriter MemWriter(ActorData.ByteData);
|
||||
|
||||
FObjectAndNameAsStringProxyArchive Ar(MemWriter, true);
|
||||
// Find only variables with UPROPERTY(SaveGame)
|
||||
Ar.ArIsSaveGame = true;
|
||||
// Converts Actor's SaveGame UPROPERTIES into binary array
|
||||
Actor->Serialize(Ar);
|
||||
|
||||
CurrentSaveGame->SavedActors.Add(ActorData);
|
||||
}
|
||||
|
||||
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0);
|
||||
}
|
||||
```
|
||||
|
||||
PS.tomlooman的意思是通过判断Actor是否继承对应接口来判断这个Actor是否需要将数据进行存档。
|
||||
|
||||
## 宝箱Actor案例
|
||||

|
||||
下面是直接从项目中取出的宝箱。请注意在bLidOpened 变量上标记的ISGameplayInterface继承和“ SaveGame ”。这将是唯一保存到磁盘的变量。默认情况下,我们也存储 Actor 的 FTransform。所以我们可以在地图上推动宝箱(启用模拟物理),在下一次播放时,位置和旋转将与盖子状态一起恢复。
|
||||
```
|
||||
UCLASS()
|
||||
class ACTIONROGUELIKE_API ASItemChest : public AActor, public ISGameplayInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(EditAnywhere)
|
||||
float TargetPitch;
|
||||
|
||||
void Interact_Implementation(APawn* InstigatorPawn);
|
||||
|
||||
void OnActorLoaded_Implementation();
|
||||
|
||||
protected:
|
||||
UPROPERTY(ReplicatedUsing="OnRep_LidOpened", BlueprintReadOnly, SaveGame) // RepNotify
|
||||
bool bLidOpened;
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_LidOpened();
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
UStaticMeshComponent* BaseMesh;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
|
||||
UStaticMeshComponent* LidMesh;
|
||||
|
||||
public:
|
||||
// Sets default values for this actor's properties
|
||||
ASItemChest();
|
||||
};
|
||||
```
|
||||
```
|
||||
void ASItemChest::Interact_Implementation(APawn* InstigatorPawn)
|
||||
{
|
||||
bLidOpened = !bLidOpened;
|
||||
OnRep_LidOpened();
|
||||
}
|
||||
|
||||
void ASItemChest::OnActorLoaded_Implementation()
|
||||
{
|
||||
OnRep_LidOpened();
|
||||
}
|
||||
|
||||
void ASItemChest::OnRep_LidOpened()
|
||||
{
|
||||
float CurrPitch = bLidOpened ? TargetPitch : 0.0f;
|
||||
LidMesh->SetRelativeRotation(FRotator(CurrPitch, 0, 0));
|
||||
}
|
||||
```
|
||||
## 玩家数据
|
||||
剩下的就是迭代 PlayerState 实例并让它们也存储数据。虽然 PlayerState 派生自 Actor 并且理论上可以在所有世界 Actor 的迭代过程中保存,但单独执行它很有用,因此我们可以将它们与玩家 ID(例如 Steam 用户 ID)匹配,而不是我们所做的不断变化的 Actor 名称不决定/控制此类运行时生成的 Actor。
|
||||
|
||||
### 保存数据
|
||||
在我的示例中,我选择在保存游戏之前从 PlayerState 获取所有数据。我们通过调用SavePlayerState(USSaveGame* SaveObject); 这让 use 将任何与 SaveGame 对象相关的数据传入,例如 Pawn 的 PlayerId 和 Transform(如果玩家当前还活着)
|
||||
|
||||
>您**可以**选择在这里也使用 SaveGame 属性并通过将其转换为二进制数组来自动存储一些玩家数据,就像我们对 Actors 所做的一样,而不是手动将其写入 SaveGame,但您仍然需要手动处理 PlayerID和典当变换。
|
||||
```
|
||||
void ASPlayerState::SavePlayerState_Implementation(USSaveGame* SaveObject)
|
||||
{
|
||||
if (SaveObject)
|
||||
{
|
||||
// Gather all relevant data for player
|
||||
FPlayerSaveData SaveData;
|
||||
SaveData.Credits = Credits;
|
||||
SaveData.PersonalRecordTime = PersonalRecordTime;
|
||||
// Stored as FString for simplicity (original Steam ID is uint64)
|
||||
SaveData.PlayerID = GetUniqueId().ToString();
|
||||
|
||||
// May not be alive while we save
|
||||
if (APawn* MyPawn = GetPawn())
|
||||
{
|
||||
SaveData.Location = MyPawn->GetActorLocation();
|
||||
SaveData.Rotation = MyPawn->GetActorRotation();
|
||||
SaveData.bResumeAtTransform = true;
|
||||
}
|
||||
|
||||
SaveObject->SavedPlayers.Add(SaveData);
|
||||
}
|
||||
```
|
||||
确保在保存到磁盘之前在所有 PlayerState 上调用这些。请务必注意,GetUniqueId 仅在您加载了在线子系统(例如 Steam 或 EOS)时才相关/一致。
|
||||
|
||||
### 加载数据
|
||||
为了检索玩家数据,我们进行了相反的操作,并且必须在 pawn 生成并准备好之后手动分配玩家的变换。您可以更无缝地覆盖游戏模式中的玩家生成逻辑以使用保存的转换。例如,我在HandleStartingNewPlayer期间坚持使用更简单的方法来处理这个问题。
|
||||
```
|
||||
void ASPlayerState::LoadPlayerState_Implementation(USSaveGame* SaveObject)
|
||||
{
|
||||
if (SaveObject)
|
||||
{
|
||||
FPlayerSaveData* FoundData = SaveObject->GetPlayerData(this);
|
||||
if (FoundData)
|
||||
{
|
||||
//Credits = SaveObject->Credits;
|
||||
// Makes sure we trigger credits changed event
|
||||
AddCredits(FoundData->Credits);
|
||||
|
||||
PersonalRecordTime = FoundData->PersonalRecordTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Could not find SaveGame data for player id '%i'."), GetPlayerId());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
与在初始关卡加载时处理的加载 Actor 数据不同,对于玩家状态,我们希望在玩家加入之前可能与我们一起玩过的服务器时一一加载它们。我们可以在 GameMode 类中的 HandleStartingNewPlayer 期间这样做。
|
||||
```
|
||||
void ASGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
|
||||
{
|
||||
// Calling Before Super:: so we set variables before 'beginplayingstate' is called in PlayerController (which is where we instantiate UI)
|
||||
ASPlayerState* PS = NewPlayer->GetPlayerState<ASPlayerState>();
|
||||
if (ensure(PS))
|
||||
{
|
||||
PS->LoadPlayerState(CurrentSaveGame);
|
||||
}
|
||||
|
||||
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
|
||||
|
||||
// Now we are ready to override spawn location
|
||||
// Alternatively we could override core spawn location to use store locations immediately (skipping the whole 'find player start' logic)
|
||||
if (PS)
|
||||
{
|
||||
PS->OverrideSpawnTransform(CurrentSaveGame);
|
||||
}
|
||||
}
|
||||
```
|
||||
正如你所看到的,它甚至被分成了两部分。主要数据会尽快加载和分配,以确保它为我们的 UI 做好准备(这是在 PlayerController 内部特定实现中的“BeginPlayingState”期间创建的),并在我们处理位置/旋转之前等待 Pawn 生成.
|
||||
|
||||
这是您可以实现它的地方,以便在创建 Pawn 期间您使用加载的数据而不是寻找 PlayerStart(就像默认的 Unreal 行为)我选择保持简单。
|
||||
|
||||
### 获取玩家数据
|
||||
下面的函数查找 Player id 并在 PIE 中使用回退,假设我们当时没有加载在线子系统。上面的播放器状态的加载使用此函数。
|
||||
|
||||
```
|
||||
FPlayerSaveData* USSaveGame::GetPlayerData(APlayerState* PlayerState)
|
||||
{
|
||||
if (PlayerState == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Will not give unique ID while PIE so we skip that step while testing in editor.
|
||||
// UObjects don't have access to UWorld, so we grab it via PlayerState instead
|
||||
if (PlayerState->GetWorld()->IsPlayInEditor())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("During PIE we cannot use PlayerID to retrieve Saved Player data. Using first entry in array if available."));
|
||||
|
||||
if (SavedPlayers.IsValidIndex(0))
|
||||
{
|
||||
return &SavedPlayers[0];
|
||||
}
|
||||
|
||||
// No saved player data available
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Easiest way to deal with the different IDs is as FString (original Steam id is uint64)
|
||||
// Keep in mind that GetUniqueId() returns the online id, where GetUniqueID() is a function from UObject (very confusing...)
|
||||
FString PlayerID = PlayerState->GetUniqueId().ToString();
|
||||
// Iterate the array and match by PlayerID (eg. unique ID provided by Steam)
|
||||
return SavedPlayers.FindByPredicate([&](const FPlayerSaveData& Data) { return Data.PlayerID == PlayerID; });
|
||||
}
|
||||
```
|
||||
|
||||
## 加载世界数据
|
||||
```
|
||||
void ASGameModeBase::LoadSaveGame()
|
||||
{
|
||||
|
||||
if (UGameplayStatics::DoesSaveGameExist(SlotName, 0))
|
||||
{
|
||||
CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
|
||||
if (CurrentSaveGame == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Failed to load SaveGame Data."));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("Loaded SaveGame Data."));
|
||||
|
||||
// Iterate the entire world of actors
|
||||
for (FActorIterator It(GetWorld()); It; ++It)
|
||||
{
|
||||
AActor* Actor = *It;
|
||||
// Only interested in our 'gameplay actors'
|
||||
if (!Actor->Implements<USGameplayInterface>())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
|
||||
{
|
||||
if (ActorData.ActorName == Actor->GetFName())
|
||||
{
|
||||
Actor->SetActorTransform(ActorData.Transform);
|
||||
|
||||
FMemoryReader MemReader(ActorData.ByteData);
|
||||
|
||||
FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
|
||||
Ar.ArIsSaveGame = true;
|
||||
// Convert binary array back into actor's variables
|
||||
Actor->Serialize(Ar);
|
||||
|
||||
ISGameplayInterface::Execute_OnActorLoaded(Actor);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnSaveGameLoaded.Broadcast(CurrentSaveGame);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::CreateSaveGameObject(USSaveGame::StaticClass()));
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("Created New SaveGame Data."));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## 从磁盘选择特定的存档
|
||||
```
|
||||
void ASGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
|
||||
{
|
||||
Super::InitGame(MapName, Options, ErrorMessage);
|
||||
|
||||
FString SelectedSaveSlot = UGameplayStatics::ParseOption(Options, "SaveGame");
|
||||
if (SelectedSaveSlot.Len() > 0)
|
||||
{
|
||||
SlotName = SelectedSaveSlot;
|
||||
}
|
||||
LoadSaveGame();
|
||||
}
|
||||
```
|
65
03-UnrealEngine/Gameplay/Gameplay/UE5 C++技巧.md
Normal file
65
03-UnrealEngine/Gameplay/Gameplay/UE5 C++技巧.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: UE5 C++技巧
|
||||
date: 2022-12-09 17:02:14
|
||||
excerpt:
|
||||
tags: c++
|
||||
rating: ⭐
|
||||
---
|
||||
## 取得默认值
|
||||
```c++
|
||||
GetDefault<ULyraDeveloperSettings>()->OnPlayInEditorStarted();
|
||||
GetDefault<ULyraPlatformEmulationSettings>()->OnPlayInEditorStarted();
|
||||
GetMutableDefault<UContentBrowserSettings>()->SetDisplayPluginFolders(true);
|
||||
```
|
||||
## 模块操作
|
||||
```c++
|
||||
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
||||
|
||||
if (AssetRegistryModule.Get().IsLoadingAssets())
|
||||
{
|
||||
if (bInteractive)
|
||||
{
|
||||
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("DiscoveringAssets", "Still discovering assets. Try again once it is complete."));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogLyraEditor, Display, TEXT("Could not run ValidateCheckedOutContent because asset discovery was still being done."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
### 加载动态链接库
|
||||
```
|
||||
{
|
||||
auto dllName = TEXT("assimp-vc141-mt.dll");
|
||||
#if WITH_VRM4U_ASSIMP_DEBUG
|
||||
dllName = TEXT("assimp-vc141-mtd.dll");
|
||||
#endif
|
||||
{
|
||||
FString AbsPath = IPluginManager::Get().FindPlugin("VRM4U")->GetBaseDir() / TEXT("ThirdParty/assimp/bin/x64");
|
||||
//FPlatformProcess::AddDllDirectory(*AbsPath);
|
||||
assimpDllHandle = FPlatformProcess::GetDllHandle(*(AbsPath / dllName));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## UE4显示MessageBox
|
||||
struct CORE_API FMessageDialog
|
||||
```
|
||||
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("DiscoveringAssets", "Still discovering assets. Try again once it is complete."));
|
||||
```
|
||||
|
||||
## ref
|
||||
c++ 中ref关键字的应用
|
||||
|
||||
ref()方法的返回值是reference_wrapper类型,这个类的源码大概的意思就是维持一个指针,并且重载操作符。
|
||||
|
||||
## 版本兼容
|
||||
```
|
||||
#if UE_VERSION_OLDER_THAN(4,24,0)
|
||||
#endif
|
||||
|
||||
#if UE_VERSION_OLDER_THAN(5,0,0)
|
||||
#endif
|
||||
```
|
@@ -0,0 +1,225 @@
|
||||
# 前言
|
||||
最近在尝试对GameplayAbility进行插件化处理。这么做好处在于这样可以方便后续项目的迭代。UE4官方频道有一个“盗贼之海”团队的经验分享视频,他们说制作的GAS Asset复用率能都达到90%,这可是相当惊人的程度。而且在不断地项目迭代过程中,使用GAS制作的技能、Buff/Debuff会越来越充分。从而加快后续项目的开发速度。所以将GAS插件化是很有必要的。
|
||||
|
||||
但使用GAS需要给项目添加一些项目设置比如:AssetManager中的DataAsset、指定AssetManager类、添加GameplayTag。在发现网上的添加配置方法无效后,我又花了一些时间研究,终于找到方法,隧有了此文。
|
||||
|
||||
# Ue4配置设置方法
|
||||
Ue4的配置设置方法大致为二:通过GConfig设置与直接设置对应UObject变量。
|
||||
具体的可以参考Ue4的Wiki与一篇Csdn的文章:
|
||||
>https://www.ue4community.wiki/legacy/config-files-read-and-write-to-config-files-zuoaht01
|
||||
https://blog.csdn.net/u012999985/article/details/52801264
|
||||
|
||||
## 通过GConfig进行修改
|
||||
通过全局对象GConfig,调用GetInt/Float/Rotator/String/Array等函数获取对应的数值;调用SetInt/Float/Rotator/String/Array等函数设置数值。**最终需要调用Flush函数将结果写入对应的ini文件中**,不然重启后数据会丢失。
|
||||
|
||||
GConfig通过一个Map来存储配置,这个你可以通过断点来查看内部数据。
|
||||
|
||||
这里我简单补充一些:
|
||||
|
||||
- Section:属性所处于的区块。
|
||||
- Key:属性的变量名。
|
||||
- Value:对应类型的属性值。
|
||||
- Filename:存储ini文件的位置。Ue4提供了默认位置的字符串变量,具体的请下文。
|
||||
|
||||
据我所知Section可以通过将对应属性通过编辑器修改,找到对应的ini文件,即可从所在段落的第一行找到所在的Section。如果是自己写的类,Section即为类的资源路径。可通过在ContentBrowser中对指定类执行复制命令,之后在外部文本编辑器中粘贴即可得到。Key值就需要用VS或者VSCode之类的编辑器查询了。
|
||||
|
||||
>请注意,使用以下代码,相关的配置的是保存在Saved——对应平台文件夹中的。
|
||||
|
||||
下面是Wiki上Rama大神的代码:
|
||||
```
|
||||
//in your player controller class
|
||||
void AVictoryController::VictoryConfigGetTests()
|
||||
{
|
||||
//Basic Syntax
|
||||
/*
|
||||
bool GetString(
|
||||
const TCHAR* Section,
|
||||
const TCHAR* Key,
|
||||
FString& Value,
|
||||
const FString& Filename
|
||||
);
|
||||
*/
|
||||
|
||||
if(!GConfig) return;
|
||||
//~~
|
||||
|
||||
//Retrieve Default Game Type
|
||||
FString ValueReceived;
|
||||
GConfig->GetString(
|
||||
TEXT("/Script/Engine.WorldInfo"),
|
||||
TEXT("GlobalDefaultGameType"),
|
||||
ValueReceived,
|
||||
GGameIni
|
||||
);
|
||||
|
||||
ClientMessage("GlobalDefaultGameType");
|
||||
ClientMessage(ValueReceived);
|
||||
|
||||
//Retrieve Max Objects not considered by GC
|
||||
int32 IntValueReceived = 0;
|
||||
GConfig->GetInt(
|
||||
TEXT("Core.System"),
|
||||
TEXT("MaxObjectsNotConsideredByGC"),
|
||||
IntValueReceived,
|
||||
GEngineIni
|
||||
);
|
||||
|
||||
ClientMessage("MaxObjectsNotConsideredByGC");
|
||||
ClientMessage(FString::FromInt(IntValueReceived));
|
||||
|
||||
//Retrieve Near Clip Plane (how close things can get to camera)
|
||||
float floatValueReceived = 0;
|
||||
GConfig->GetFloat(
|
||||
TEXT("/Script/Engine.Engine"),
|
||||
TEXT("NearClipPlane"),
|
||||
floatValueReceived,
|
||||
GEngineIni
|
||||
);
|
||||
|
||||
ClientMessage("NearClipPlane");
|
||||
ClientMessage(FString::SanitizeFloat(floatValueReceived));
|
||||
}
|
||||
```
|
||||
```
|
||||
//write to existing Game.ini
|
||||
//the results get stored in YourGameDir\Saved\Config\Windows
|
||||
void AVictoryController::VictoryConfigSetTests()
|
||||
{
|
||||
if(!GConfig) return;
|
||||
//~~
|
||||
|
||||
//New Section to Add
|
||||
FString VictorySection = "Victory.Core";
|
||||
|
||||
//String
|
||||
GConfig->SetString (
|
||||
*VictorySection,
|
||||
TEXT("RootDir"),
|
||||
TEXT("E:\UE4\IsAwesome"),
|
||||
GGameIni
|
||||
);
|
||||
|
||||
//FColor
|
||||
GConfig->SetColor (
|
||||
*VictorySection,
|
||||
TEXT("Red"),
|
||||
FColor(255,0,0,255),
|
||||
GGameIni
|
||||
);
|
||||
|
||||
//FVector
|
||||
GConfig->SetVector (
|
||||
*VictorySection,
|
||||
TEXT("PlayerStartLocation"),
|
||||
FVector(0,0,512),
|
||||
GGameIni
|
||||
);
|
||||
|
||||
//FRotator
|
||||
GConfig->SetRotator (
|
||||
*VictorySection,
|
||||
TEXT("SunRotation"),
|
||||
FRotator(-90,0,0),
|
||||
GGameIni
|
||||
);
|
||||
|
||||
//ConfigCacheIni.h
|
||||
//void Flush( bool Read, const FString& Filename=TEXT("") );
|
||||
GConfig->Flush(false,GGameIni);
|
||||
}
|
||||
```
|
||||
ini配置文件的位置字符串变量,代码位于CoreGlobals.cpp中。
|
||||
```
|
||||
FString GEngineIni; /* Engine ini filename */
|
||||
|
||||
/** Editor ini file locations - stored per engine version (shared across all projects). Migrated between versions on first run. */
|
||||
FString GEditorIni; /* Editor ini filename */
|
||||
FString GEditorKeyBindingsIni; /* Editor Key Bindings ini file */
|
||||
FString GEditorLayoutIni; /* Editor UI Layout ini filename */
|
||||
FString GEditorSettingsIni; /* Editor Settings ini filename */
|
||||
|
||||
/** Editor per-project ini files - stored per project. */
|
||||
FString GEditorPerProjectIni; /* Editor User Settings ini filename */
|
||||
|
||||
FString GCompatIni;
|
||||
FString GLightmassIni; /* Lightmass settings ini filename */
|
||||
FString GScalabilityIni; /* Scalability settings ini filename */
|
||||
FString GHardwareIni; /* Hardware ini filename */
|
||||
FString GInputIni; /* Input ini filename */
|
||||
FString GGameIni; /* Game ini filename */
|
||||
FString GGameUserSettingsIni; /* User Game Settings ini filename */
|
||||
FString GRuntimeOptionsIni; /* Runtime Options ini filename */
|
||||
FString GInstallBundleIni; /* Install Bundle ini filename*/
|
||||
FString GDeviceProfilesIni;
|
||||
```
|
||||
|
||||
## 通过UObject进行修改
|
||||
很遗憾,本人没有从GConfig找到对应的设置变量,所以添加配置的操作都是通过UObject进行的。主要是通过GEngine这个全局对象以及GetMutableDefault<UObject>()获取对应的UObject,这两种方式进行操作。
|
||||
|
||||
操作的方式也有两种:
|
||||
1. 直接调用LoadConfig再入预先设置的ini文件,之后调用SaveConfig保存设置。
|
||||
2. 直接修改UObject的属性变量值,之后调用SaveConfig保存设置。
|
||||
|
||||
另外需要注意两点:
|
||||
1. Config的类型分为Config与GlobalConfig,调用函数时需要使用对应的类型枚举:CPF_Config与CPF_GlobalConfig。
|
||||
2. 是否在UCLASS(Config=XXX)有设置Config保存位置。
|
||||
|
||||
具体操作请见下文代码。
|
||||
|
||||
# 使用DataTable来管理GameplayTag
|
||||
本人在尝试通过修改配置来添加GameplayTag时,发现这个方法是无效的。(可能是个bug)在404的过程中,我发现可以通过DataTable来管理Tag。大致的方式是:
|
||||
1. 构建CSV或者JSON文件
|
||||
2. 导入UE4中并将DataTable类型选择为GameplayTagTableRow。
|
||||
3. 在ProjectSettings-GameplayTagTableList中添加DataTable即可。
|
||||
|
||||
这样做的好处有:
|
||||
1. GameplayTags的重复利用,避免手动输入出错,方便下个项目移植。
|
||||
2. 可以对不同类型Tag进行分表处理,在做项目时只需要加载需要的DataTable即可。
|
||||
|
||||
DataTable大致格式如下:
|
||||
|
||||
PS.有关GameplayTag的操作可以查看UGameplayTagsSettings、UGameplayTagsManager与GameplayTagEditorModule中的代码。
|
||||
|
||||
# 创建Console命令
|
||||
将这些代码放在GameMode、GameInstance里会降低性能(纯属强迫症),放在模块cpp文件中又会报错(估计需要将插件设置成后启动模式才行)。为了方便使用,我创建了一个Console命令,并将其放在BlueprintFunctionLibrary中。在编辑器中下入~,输入AddGASSettings并按下回车,即可添加所需设置。
|
||||
|
||||
# 具体代码实现
|
||||
```
|
||||
static void AddRPGGameplayAbilityProjectSettings(UWorld *InWorld)
|
||||
{
|
||||
UE_LOG(LogRPGGameplayAbility, Warning, TEXT("Add RPGGameplayAbility Project Settings!"));
|
||||
|
||||
//Special AssetManager
|
||||
FString ConfigPath = FPaths::ProjectConfigDir();
|
||||
GEngine->AssetManagerClassName = FString("/Script/RPGGameplayAbility.RPGAssetManager");
|
||||
GEngine->SaveConfig(CPF_GlobalConfig, *(ConfigPath + FString("DefaultEngine.ini")));
|
||||
|
||||
//Add DataAsset Config
|
||||
UAssetManagerSettings *AssetmanagerSettings = GetMutableDefault<UAssetManagerSettings>();
|
||||
FString PluginConfigPath = FPaths::ProjectPluginsDir() + FString("RPGGameplayAbility/Config/");
|
||||
AssetmanagerSettings->LoadConfig(nullptr, *FString(PluginConfigPath + FString("DefaultGame.ini")));
|
||||
AssetmanagerSettings->SaveConfig(CPF_Config, *(ConfigPath + FString("DefaultGame.ini")));
|
||||
|
||||
//Add GameplayTag
|
||||
UGameplayTagsSettings *GameplayTagsSettings = GetMutableDefault<UGameplayTagsSettings>();
|
||||
//GameplayTagsSettings->LoadConfig(nullptr, *FString(PluginConfigPath + FString("DefaultGameplayTags.ini")));
|
||||
FString TagDataTable{FString("/RPGGameplayAbility/Abilities/DataTables/GameplayTags.GameplayTags")};
|
||||
|
||||
if (GameplayTagsSettings->GameplayTagTableList.Find(TagDataTable) == -1)
|
||||
{
|
||||
GameplayTagsSettings->GameplayTagTableList.Add(TagDataTable);
|
||||
UGameplayTagsManager &tagManager = UGameplayTagsManager::Get();
|
||||
tagManager.DestroyGameplayTagTree();
|
||||
tagManager.LoadGameplayTagTables(false);
|
||||
tagManager.ConstructGameplayTagTree();
|
||||
tagManager.OnEditorRefreshGameplayTagTree.Broadcast();
|
||||
|
||||
GameplayTagsSettings->SaveConfig(CPF_Config, *(ConfigPath + FString("DefaultGameplayTags.ini")));
|
||||
}
|
||||
}
|
||||
|
||||
FAutoConsoleCommandWithWorld AbilitySystemDebugNextCategoryCmd(
|
||||
TEXT("AddGASSettings"),
|
||||
TEXT("增加RPGGameplayAbility所需要的项目设置!"),
|
||||
FConsoleCommandWithWorldDelegate::CreateStatic(AddRPGGameplayAbilityProjectSettings));
|
||||
```
|
@@ -0,0 +1,161 @@
|
||||
#### 前言
|
||||
因为GameplayAbility属于蓝图c++混合编程框架,所以对蓝图类与地图进行版本管理是十分重要的事情。所以本文将在这里介绍git的二进制文件版本管理方案。
|
||||
|
||||
#### 使用过程
|
||||
1. 下载Gitlfs:https://git-lfs.github.com/~~(现在的git都自带lfs,就算没有下个SourceTree也会自带lfs)
|
||||
2. 使用cmd,cd到git仓库所在目录,执行git lfs install。(一般人都在这一步做错,如果做错会存在100mb的文件大小限制)
|
||||
3. 此时目录下会出现.gitattributes文件,它用于设置监视的扩展名,你可以通过输入```git lfs track "*.扩展名"```的方式来添加扩展名。例如想要监视uasset,就输入
|
||||
```git lfs track "*.uasset"```。最后将.gitattributes加入进版本管理:```git add .gitattributes```。
|
||||
4. 现在你就可以用与管理代码文件相同的方式,管理二进制文件了。
|
||||
```
|
||||
git add file.uasset
|
||||
git commit -m "Add design file"
|
||||
git push origin master
|
||||
```
|
||||
推荐使用SourceTree,因为如果你第二步操作有误或是第三步没有添加扩展名,它会提醒你的。
|
||||
|
||||
#### 蓝图合并与Diff工具
|
||||
Merge:https://github.com/KennethBuijssen/MergeAssist
|
||||
Diff:https://github.com/SRombauts/UE4GitPlugin
|
||||
|
||||
|
||||
|
||||
# LFS upload missing objects 解决
|
||||
输入命令,即可
|
||||
```
|
||||
git config --global lfs.allowincompletepush false
|
||||
```
|
||||
|
||||
# LFS删除 很久不用的文件
|
||||
使用prune命令可以删除LFS中的旧文件。
|
||||
```
|
||||
git lfs prune options
|
||||
```
|
||||
这会删除认为过旧的本地 Git LFS文件,没有被引用的文件被认为是过旧的文件:
|
||||
|
||||
当前切换的提交
|
||||
一个还没有被推送的提交(到远程,或者任何在lfs.pruneremotetocheck设置的)
|
||||
|
||||
一个最近的提交
|
||||
默认,一个最近的提交是过去十天的任何一个提交,这是通过添加如下内容计算的:
|
||||
|
||||
在获取附加的Git LFS历史部分讨论过的lfs.fetchrecentrefsdays属性的值。
|
||||
|
||||
lfs.pruneoffsetdays属性的值(默认为3)。
|
||||
|
||||
git lfs prune
|
||||
|
||||
你可以为配置一个持用Git LFS内容更长的时间:
|
||||
|
||||
# don't prune commits younger than four weeks (7 + 21)
|
||||
$ git config lfs.pruneoffsetdays 21
|
||||
不像Git内置的垃圾回收, Git LFS内容不会自动删除,因此定期执行git lfs prune来保留你本地的仓库文件大小是很正确的做法。
|
||||
|
||||
你可以测试在git lfs prune –dry-run命令执行后有什么效果:
|
||||
|
||||
$ git lfs prune --dry-run
|
||||
✔ 4 local objects, 33 retained
|
||||
4 files would be pruned (2.1 MB)
|
||||
更精确地查看哪个Git LFS对象被删除可以使用git lfs prune –verbose –dry-run命令:
|
||||
|
||||
$ git lfs prune --dry-run --verbose
|
||||
✔ 4 local objects, 33 retained
|
||||
4 files would be pruned (2.1 MB)
|
||||
* 4a3a36141cdcbe2a17f7bcf1a161d3394cf435ac386d1bff70bd4dad6cd96c48 (2.0 MB)
|
||||
* 67ad640e562b99219111ed8941cb56a275ef8d43e67a3dac0027b4acd5de4a3e (6.3 KB)
|
||||
* 6f506528dbf04a97e84d90cc45840f4a8100389f570b67ac206ba802c5cb798f (1.7 MB)
|
||||
* a1d7f7cdd6dba7307b2bac2bcfa0973244688361a48d2cebe3f3bc30babcf1ab (615.7 KB)
|
||||
通过使用–verbose模式输出的十六进制的字符串是被删除的Git LFS对象的SHA-256哈希值(也被称作对象ID,或者OIDs)。你可以使用在找到引用某个Git LFS对象的路径或者提交章节介绍的技巧去找到其他想要删除的对象。
|
||||
|
||||
作为一个额外安全检查工作,你可以使用–verify-remote选项来检查Git LFS store是否存在想要删除的Git LFS对象的拷贝。
|
||||
|
||||
$ git lfs prune --verify-remote
|
||||
✔ 16 local objects, 2 retained, 12 verified with remote
|
||||
Pruning 14 files, (1.7 MB)
|
||||
✔ Deleted 14 files
|
||||
这让删除过程非常的非常的缓慢,但是这可以帮助你明白所有删除的对象都是可以从服务器端恢复的。你可以为你的系统用久开启–verify-remote 选项,这可以通过全局配置lfs.pruneverifyremotealways属性来实现。
|
||||
|
||||
$ git config --global lfs.pruneverifyremotealways true
|
||||
或者你可以通过去掉–global选项来仅仅为当前会话的仓库开启远程验证。
|
||||
|
||||
# Git代理
|
||||
## 配置Sock5代理
|
||||
git config -–global http.proxy socks5://127.0.0.1:2080
|
||||
git config –-global https.proxy socks5://127.0.0.1:2080
|
||||
|
||||
|
||||
### 只对github.com
|
||||
git config --global http.https://github.com.proxy socks5://127.0.0.1:2080
|
||||
git config --global https.https://github.com.proxy socks5://127.0.0.1:2080
|
||||
|
||||
### 取消代理
|
||||
git config --global --unset http.https://github.com.proxy)
|
||||
git config --global --unset https.https://github.com.proxy)
|
||||
|
||||
|
||||
## 增加超时时间
|
||||
git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks push -v --tags origin GamePlayDevelop:GamePlayDevelop
|
||||
Pushing to https://github.com/SDHGame/SDHGame.git
|
||||
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/b6/a8/b6a8fa2ba03f846f04af183bddd2e3838c8b945722b298734a14cf28fd7d1ab1?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56358->127.0.0.1:1080: i/o timeout
|
||||
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/76/47/76473fed076cd6f729cf97e66e28612526a824b92019ef20e3973dc1797304e8?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56360->127.0.0.1:1080: i/o timeout
|
||||
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/78/a0/78a0819db84cdd0d33aa176ae94625515059a6c88fec5c3d1e905193f65bfcdd?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56374->127.0.0.1:1080: i/o timeout
|
||||
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/24/ab/24ab214470100011248f2422480e8920fb80d23493f11d9f7a598eb1b4661021?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56376->127.0.0.1:1080: i/o timeout
|
||||
|
||||
|
||||
Uploading LFS objects: 99% (712/716), 210 MB | 760 KB/s, done.
|
||||
error: failed to push some refs to 'https://github.com/SDHGame/SDHGame.git'
|
||||
|
||||
|
||||
git config --global lfs.tlstimeout 300
|
||||
git config --global lfs.activitytimeout 60
|
||||
git config --global lfs.dialtimeout 600
|
||||
git config --global lfs.concurrenttransfers 1
|
||||
|
||||
|
||||
### LFS Upload Failed (miss) 文件路径
|
||||
解决方案:下载所有LFS数据:git lfs fetch --all
|
||||
|
||||
### 服务器上不存在
|
||||
解决方案:上传指定lfs文件:git lfs push origin --object-id [ID]
|
||||
|
||||
### 强制上传LFS
|
||||
git lfs push origin --all
|
||||
|
||||
# 引擎Content管理
|
||||
.gitignore文件中添加
|
||||
```
|
||||
Content/
|
||||
#Content/
|
||||
!*.uasset
|
||||
**/Content/*
|
||||
**/Content/*/*
|
||||
!**/Content/EngineMaterials/
|
||||
!**/Content/EngineMaterials/ToonTexture/
|
||||
```
|
||||
|
||||
# 解决git UTF8文件乱码问题
|
||||
问题:
|
||||
```bash
|
||||
未处理的异常:System.ArgumentException: Path fragment '“Content/\351\237\263\351\242\221/Cheetah\302\240Mobile_Games_-_\347\254\254\345\215\201\344 \270\203\345\205\263\302\240Cube\302\240\345\207\240\344\275\225\350\277\267\351\230\265.uasset”包含无效的目录分隔符.
|
||||
1> 在 Tools.DotNETCommon.FileSystemReference.CombineStrings(DirectoryReference BaseDirectory, String[] Fragments)
|
||||
1> 在 Tools.DotNETCommon.FileReference.Combine(DirectoryReference BaseDirectory, String[] Fragments)
|
||||
1> 在 UnrealBuildTool.GitSourceFileWorkingSet.AddPath(String Path)
|
||||
1> 在 UnrealBuildTool.GitSourceFileWorkingSet.OutputDataReceived(Object Sender, DataReceivedEventArgs Args)
|
||||
1> 在 System.Diagnostics.Process.OutputReadNotifyUser(String data)
|
||||
1> 在 System.Diagnostics.AsyncStreamReader.FlushMessageQueue()
|
||||
1> 在 System.Diagnostics.AsyncStreamReader.GetLinesFromStringBuilder()
|
||||
1> 在 System.Diagnostics.AsyncStreamReader.ReadBuffer(IAsyncResult ar)
|
||||
1> 在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
|
||||
1> 在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
|
||||
1> 在 System.IO.Stream.ReadWriteTask.System.Threading.Tasks .ITaskCompletionAction.Invoke(Task completingTask)
|
||||
1> 在 System.Threading.Tasks.Task.FinishContinuations()
|
||||
1> 在 System.Threading.Tasks.Task.Finish(Boolean bUserDelegateExecuted)
|
||||
1> 在 System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
|
||||
1> 在 System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
|
||||
1> 在 System.Threading.ThreadPoolWorkQueue.Dispatch()
|
||||
```
|
||||
|
||||
```bash
|
||||
git config --global core.quotepath false
|
||||
```
|
||||
|
8
03-UnrealEngine/Gameplay/Gameplay/判断物体是否在屏幕中的方法.md
Normal file
8
03-UnrealEngine/Gameplay/Gameplay/判断物体是否在屏幕中的方法.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: 判断物体是否在屏幕中的方法
|
||||
date: 2022-12-09 14:08:26
|
||||
excerpt:
|
||||
tags: Camera
|
||||
rating: ⭐
|
||||
---
|
||||

|
BIN
03-UnrealEngine/Gameplay/Gameplay/各平台手柄图片.jpg
Normal file
BIN
03-UnrealEngine/Gameplay/Gameplay/各平台手柄图片.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 173 KiB |
93
03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(1)—LyraEditor.md
Normal file
93
03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(1)—LyraEditor.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: UE5 Lyra学习笔记(1)—LyraEditor
|
||||
date: 2022-08-09 13:55:15
|
||||
tags: Lyra Editor
|
||||
rating: ⭐️⭐️
|
||||
---
|
||||
# LyraEditor
|
||||
主要的实现内容为:
|
||||
- ULyraEditorEngine
|
||||
- UCommandlet
|
||||
- UEditorValidator
|
||||
- 3个全局命令
|
||||
- 1个自定义Asset
|
||||
- FGameEditorStyle
|
||||
|
||||
在FLyraEditorModule启动与载入时,增加GameplayCueNotify类与GameplayCue路径。并且绑定OnBeginPIE()与OnEndPIE()。
|
||||
|
||||
## ULyraEditorEngine
|
||||
在DefaultEngine.ini的[/Script/Engine.Engine]中指定了这个类,来进行替换编辑器功能:
|
||||
```ini
|
||||
UnrealEdEngine=/Script/LyraEditor.LyraEditorEngine
|
||||
EditorEngine=/Script/LyraEditor.LyraEditorEngine
|
||||
```
|
||||
- 重写了PreCreatePIEInstances()
|
||||
- 根据ALyraWorldSettings的布尔变量ForceStandaloneNetMode,来强制设置成**PIE_Standalone**网络模式。
|
||||
- 调用ULyraDeveloperSettings与ULyraPlatformEmulationSettings类的OnPlayInEditorStarted(),向FSlateNotificationManager传递消息。
|
||||
- 返回父类函数结果。
|
||||
- 实现了FirstTickSetup(),并在Tick()中只调用一次。
|
||||
- 让ContenBrowser显示插件文件夹
|
||||
- 判断用户是否有修改停止PIE按键,无修改情况下修改按键为Shift+ESC
|
||||
|
||||
## UCommandlet
|
||||
参考:https://zhuanlan.zhihu.com/p/512610557
|
||||
它的应用场景主要为:
|
||||
- 借助 Commandlet ,我们无需打开 UE 编辑器,即可在命令行下执行某段 C++ 代码。可用来批量处理 UE 工程中的资源等。结合 Jenkins 等自动化处理方案,可较方便地实现 UE 工程的自动化处理。
|
||||
- 典型应用场景如 Resave Packages 、Fixup Redirects、Cooking、Localization Pipeline 、ImportAsset、ContentValidation等。
|
||||
|
||||
引擎中实现了几十个Commandlet,具体可以查看引擎中的UCommandlet的子类。
|
||||
|
||||
#### 使用与实现
|
||||
采用命令行启动:
|
||||
`D:\\MyProject\\MyProjectDir\\MyProject.uproject -skipcompile -run={自定义的Commandlet的名字} {需要的各种参数}`
|
||||
|
||||
实现方法:重写`virtual int32 Main(const FString& Params) override;`即可。
|
||||
|
||||
#### Lyra中的实现
|
||||
在Lyra中实现了UContentValidationCommandlet类。主要用于使用命令对修改过的Asset进行有效性检测。功能基于P4V,所以对于其他版本管理软件需要花点时间进行改写。大致逻辑如下:
|
||||
|
||||
- 从P4V取得所有修改文件列表,并将信息添加到ChangedPackageNames、DeletedPackageNames、ChangedCode、ChangedOtherFiles中。
|
||||
- 根据命令行InPath、OfType、Packages参数获取包名,并添加到ChangedPackageNames数组中。
|
||||
- 调用`GShaderCompilingManager->FinishAllCompilation()`停止其他Shader的编译。这样就不会让有效性检测范围外的Shader错误影响到检测结果。
|
||||
- 调用UEditorValidator::ValidatePackages()与UEditorValidator::ValidateProjectSettings(),最后返回检测结果。
|
||||
|
||||
## UEditorValidator
|
||||
该功能依赖于DataValidation插件。文档地址:https://docs.unrealengine.com/5.0/en-US/data-validation/
|
||||
|
||||
目前只有2种创建验证规则的方式:
|
||||
1. 覆盖UObject的IsDataValid()。
|
||||
2. 创建UEditorValidatorBase的派生类。
|
||||
|
||||
关键函数是CanValidateAsset()与ValidateLoadedAsset()。ValidateLoadedAsset()必须为其每个Asset返回AssetPasses或AssetFails。C++与蓝图实现的Validator将在编辑器启动时自动注册,而Python版本的需要UEditorValidatorSubsystem中调用AddValidator()来进行注册。
|
||||
|
||||
验证资产有2种方法:
|
||||
1. 主动验证。在Asset上右键,Asset Action -> Validate Assets。
|
||||
2. 保存时验证。默认情况下是开启的。设置选项位于Edit -> Editor Preferences -> Advanced -> Data Validation -> Validate On Save
|
||||
|
||||
UEditorValidator几个函数的逻辑为:
|
||||
- ValidateCheckedOutContent:取得ISourceControlModule与AssetRegistryModule,根据文件状态加入对Asset进行验证(调用ValidatePackages()),如果是h文件,会调用GetChangedAssetsForCode()进行验证。最后返回错误信息。会在点击UToolMenus的CheckGameContent按钮时执行这个函数。
|
||||
- ValidatePackages:验证所有Asset返回结果与所有警告与错误字符串。
|
||||
- ValidateProjectSettings:读取**PythonScriptPluginSettings**的**bDeveloperMode**变量。如果为bDeveloperMode为1,打印错误信息,并返回false。
|
||||
- IsInUncookedFolder:判断指定的package是否处于Uncooked文件夹中,并返回文件夹名。
|
||||
- ShouldAllowFullValidation:是否需要完整验证。
|
||||
- CanValidateAsset_Implementation:判断是否可以验证Asset,默认为处于需要烘焙的文件夹中就为true。
|
||||
|
||||
- UEditorValidator_Load:定义检测函数GetLoadWarningsAndErrorsForPackage()。
|
||||
- UEditorValidator_Blueprints:验证非数据蓝图,如有警告与错误则返回EDataValidationResult::Invalid
|
||||
- UEditorValidator_MaterialFunctions:验证AssetClass为UMaterial的Asset,如有警告与错误则返回EDataValidationResult::Invalid
|
||||
- UEditorValidator_SourceControl:检查Asset的依赖是否加入版本管理
|
||||
|
||||
## 3个全局命令
|
||||
- Lyra.CheckChaosMeshCollision:检查所有载入的StaticMesh的Chaos碰撞数据。
|
||||
- Lyra.CreateRedirectorPackage:创建Asset Redirector,并且重定向替换引用。
|
||||
- Lyra.DiffCollectionReferenceSupport:It will list the assets in Old that 'support' assets introduced in New (are referencers directly/indirectly) as well as any loose unsupported assets.The optional third argument controls whether or not multi-supported assets will be de-duplicated (true) or not (false)
|
||||
|
||||
## 自定义Asset
|
||||
主要功能:
|
||||
构建一个EffectTag->Context(物理材质表面类型Tag)与MetaSound Source的表。按需求载入对应的Sound与NiagaraAsset,并且在需要的时候进行播放。在ULyraContextEffectsSubsystem(WorldSubsystem)与AnimNotify_LyraContextEffects中有引用。
|
||||
|
||||
LyraEditor中的实现文件为:
|
||||
- FAssetTypeActions_LyraContextEffectsLibrary
|
||||
- ULyraContextEffectsLibraryFactory
|
||||
|
||||
Asset的UObject对象ULyraContextEffectsLibrary对象定义在LyraGame模块。
|
97
03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(2)—LyraPlugins.md
Normal file
97
03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(2)—LyraPlugins.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: UE5 Lyra学习笔记(2)—LyraPlugins
|
||||
date: 2022-08-09 13:55:20
|
||||
tags: Lyra Editor
|
||||
rating: ⭐️⭐️
|
||||
---
|
||||
# Plugins
|
||||
- AsyncMixin
|
||||
- CommonGame
|
||||
- CommonLoadingScreen
|
||||
- CommonUser
|
||||
- GameFeatures
|
||||
- GameplayMessageRouter
|
||||
- GameSettings
|
||||
- GameSubtitles
|
||||
- LyraExampleContent
|
||||
- LyraExtTool
|
||||
- ModularGameplayActors
|
||||
- PocketWorlds
|
||||
- UIExtension
|
||||
|
||||
## AsyncMixin
|
||||
|
||||
## ModularGameplayActors
|
||||
引擎插件ModularGameplay的Actor版本,增加了UGameFrameworkComponentManager控制的各种基础胶水类,Lyra里的相关类都继承自他们。
|
||||
- AModularGameModeBase(指定了插件里实现的类)
|
||||
- AModularAIController
|
||||
- AModularCharacter
|
||||
- AModularGameStateBase
|
||||
- AModularPawn
|
||||
- AModularPlayerController
|
||||
- AModularPlayerState
|
||||
|
||||
## UIExtension
|
||||
主要实现了`UUIExtensionPointWidget`这个控件以及用于数据控制的`UUIExtensionSubsystem`。`UUIExtensionPointWidget`是一个布局控件,实现一种以数据驱动方式,通过GameplayTag 匹配方式来插入Widget的方式。一般通过在UMG控件中设置参插槽参数;
|
||||
|
||||

|
||||

|
||||
|
||||
在LyraExperienceActionSet中设置AddWidgets(LayoutClass、LayoutID;WidgetClass、SlotID),最后会通过`UCommonUIExtensions::PushContentToLayer_ForPlayer`往屏幕上增加Widget。
|
||||

|
||||
|
||||
这样设计的其中一个优点在于实现解耦,例如:在相关Ability的Add/Remove进行UI注册与卸载,做到一个Ability做到所有的逻辑。
|
||||
|
||||
- UUIExtensionSubsystem
|
||||
- UUIExtensionHandleFunctions:通过FUIExtensionPointHandle判断有效性与卸载函数。
|
||||
- UUIExtensionPointHandleFunctions:通过FUIExtensionPointHandle判断有效性与卸载函数。
|
||||
- RegisterExtensionPointForContext:使用传入数据构建`FUIExtensionPoint`并其填充`ExtensionPointMap`,并且调用`ExtensionPoint`绑定的委托。
|
||||
- RegisterExtensionAsData:使用传入数据构建`FUIExtension`并其填充`ExtensionMap`,并且调用`Extension`绑定的委托。
|
||||
- UnregisterExtensionPoint:通过Handle移除对应`FUIExtensionPoint`。
|
||||
- UnregisterExtension:通过Handle移除对应`FUIExtension`。
|
||||
- 若干蓝图函数。
|
||||
- UMG控件
|
||||
- UUIExtensionPointWidget
|
||||
|
||||
AddWidgets代码位于`UGameFeatureAction_AddWidgets`类中。
|
||||
|
||||
## CommonUI
|
||||
- CommonUI官方文档:https://docs.unrealengine.com/5.0/zh-CN/overview-of-advanced-multiplatform-user-interfaces-with-common-ui-for-unreal-engine/
|
||||
- Introduction to Common UI:https://www.youtube.com/watch?v=TTB5y-03SnE
|
||||
- Lyra跨平台UI开发:https://www.bilibili.com/video/BV1mT4y167Fm?spm_id_from=333.999.0.0&vd_source=d47c0bb42f9c72fd7d74562185cee290
|
||||
|
||||
### CommonUIActionRouterBase
|
||||
这是一个本地玩家用的子系统。可以用于注册InputAction。
|
||||
|
||||
## GameplayMessageRouter
|
||||
Runtime模块实现了`UGameplayMessageSubsystem`与`UAsyncAction_ListenForGameplayMessage`;Editor模块实现了`UK2Node_AsyncAction_ListenForGameplayMessages`节点。
|
||||
|
||||
### FGameplayMessageListenerHandle
|
||||
存储了UGameplayMessageSubsystem的弱指针、Handle ID(int32 ID)、MessageChannel(FGameplayTag Channel)与FDelegateHandle。并且实现了Unregister()与IsValid()。
|
||||
|
||||
### UGameplayMessageSubsystem
|
||||
是一个UGameInstanceSubsystem。成员变量只有`ListenerMap`,记录MessageChannel=>FChannelListenerList的Map:
|
||||
```c++
|
||||
// List of all entries for a given channel
|
||||
struct FChannelListenerList
|
||||
{
|
||||
TArray<FGameplayMessageListenerData> Listeners;
|
||||
int32 HandleID = 0;
|
||||
};
|
||||
private:
|
||||
TMap<FGameplayTag, FChannelListenerList> ListenerMap;
|
||||
```
|
||||
|
||||
- 泛型函数:获取到类型的`UScriptStruct`传入内部函数,最后返回`FGameplayMessageListenerHandle`
|
||||
- RegisterListener
|
||||
- BroadcastMessage
|
||||
- UnregisterListener
|
||||
- 泛型函数调用的内部函数
|
||||
- RegisterListenerInternal:从使用MessageChannel(GameplayTag)从`ListenerMap`找到对应(如果没有则添加)的`FChannelListenerList`,并填充`FGameplayMessageListenerData`数据:仿函数以及其他相关信息。
|
||||
- BroadcastMessageInternal:调用对应MessageChannel(GameplayTag)与UScriptStruct类型的所有仿函数(该过程还会移除无效的仿函数所在项)。
|
||||
- UnregisterListenerInternal:从`ListenerMap`中找到对应MessageChannel(GameplayTag)的`FChannelListenerList`,并从其中移除指定HandleID的`FGameplayMessageListenerData`。如果之后`FChannelListenerList`为空,则从`ListenerMap`移除这个键值。
|
||||
|
||||
### UAsyncAction_ListenForGameplayMessage
|
||||
在Activate()中向`UGameplayMessageSubsystem`注册(Channel、Lambda、`TWeakObjectPtr<UScriptStruct> MessageStructType`、`EGameplayMessageMatch`)。lambda主要作用就是触发OnMessageReceived委托的多播。
|
||||
|
||||
UK2Node_AsyncAction_ListenForGameplayMessages为其封装的蓝图节点。
|
48
03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(3)—GAS.md
Normal file
48
03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(3)—GAS.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: UE5 Lyra学习笔记(3)—GAS
|
||||
date: 2022-08-09 13:55:20
|
||||
tags: Lyra Gameplay
|
||||
rating: ⭐️⭐️
|
||||
---
|
||||
# LyraGame
|
||||
主要逻辑集中在这个模块与插件中,本文先解析GAS相关与GameFeature的逻辑。
|
||||
|
||||
UWorldSubsystem:实现了ULyraAudioMixEffectsSubsystem、
|
||||
|
||||
- LyraGameplayTags:实现单例类 FLyraGameplayTags来管理GAS相关标签。感觉不如使用JSON导入DataTable Asset的方式来管理GameplayTag来得方便。
|
||||
- LyraLogChannels:实现LogLyra、LogLyraExperience、LogLyraAbilitySystem、LogLyraTeams日志标签,以及 返回网络Role字符串函数。
|
||||
|
||||
## AbilitySystem
|
||||
- Abilities
|
||||
- Interaction
|
||||
- Inventory
|
||||
- Player
|
||||
- Weapons
|
||||
- UI
|
||||
|
||||
### Abilities
|
||||
#### LyraGameplayAbility
|
||||
- 实现5个ActorInfo获取函数:GetLyraAbilitySystemComponentFromActorInfo、GetLyraPlayerControllerFromActorInfo、GetControllerFromActorInfo、GetLyraCharacterFromActorInfo、GetHeroComponentFromActorInfo。
|
||||
|
||||
定义ELyraAbilityActivationPolicy与ELyraAbilityActivationGroup枚举,并在类内的定义枚举变量与取值函数。用于,
|
||||
|
||||
c++中实现:
|
||||
- ULyraGameplayAbility_Death:执行类型为ServerInitiated,逻辑为能力激活与结束时,调用角色的ULyraHealthComponent的StartDeath()与FinishDeath()。
|
||||
- ULyraGameplayAbility_Jump:执行类型为LocalPredicted,调用角色的Jump()、UnCrouch()、StopJumping()。
|
||||
- ULyraGameplayAbility_Reset:执行类型为ServerInitiated,角色复活时调用的能力,将各个属性恢复成初始值,调用角色Reset();之后通过UGameplayMessageSubsystem广播FLyraPlayerResetMessage消息。会在GAS组件接收到**GameplayEvent.RequestReset**事件时激活。
|
||||
|
||||
#### Animation
|
||||
ULyraAnimInstance:针对GAS进行了定制:定义**FGameplayTagBlueprintPropertyMap**变量。可以在AnimBP中添加**FGameplayTagBlueprintPropertyMapping**,主要作用是:
|
||||
|
||||
- NativeInitializeAnimation:获取Actor的GAS组件,用于给FGameplayTagBlueprintPropertyMap变量进行初始化。
|
||||
- NativeUpdateAnimation:获取Actor的Movement组件,通过GetGroundInfo()来获取Actor到地面的距离,来给类的GroundDistance变量赋值。
|
||||
- IsDataValid:用判断AnimBP Asset以及GameplayTagPropertyMap变量是否有效。
|
||||
|
||||
### GameFeatures
|
||||
Lyra中实现的
|
||||
|
||||
#### GameFeatureAction
|
||||
|
||||
#### GameModes
|
||||
|
||||
#### Input
|
191
03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(4)—Inventory.md
Normal file
191
03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(4)—Inventory.md
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: UE5 Lyra学习笔记(4)—Inventory
|
||||
date: 2022-08-10 13:55:20
|
||||
tags: Lyra Gameplay
|
||||
rating: ⭐️⭐️
|
||||
---
|
||||
# 前言
|
||||
ULyraInventoryManagerComponent通过GameFeature功能在模块激活时挂载Controller上,具体可以查看ShooterCore的GameFeature配置文件。
|
||||
Lyra中的物品系统采用 **Entry - Instance - Definition - Fragment**的结构。但该系统有没有完成的痕迹,所以个人不建议直接拿来用,但里面有考虑到网络同步的设计值得学习。
|
||||
与ActionRPG项目相比它没有实现SaveGame/LoadGame部分,也没有使用FPrimaryAssetId来减少网络同步以及存档大小。所以也挺可惜的。
|
||||
|
||||
Inventory的主要的资产为ULyraInventoryItemDefinition,前缀名为**ID_**;Equipment的主要资产为ULyraEquipmentDefinition,前缀名为**WID_** 。这些资产位于:
|
||||
- `Plugins\GameFeatures\ShooterCore\Content\Items\`
|
||||
- `Plugins\GameFeatures\ShooterCore\Content\Weapons\`
|
||||
|
||||
# Inventory
|
||||
## Class
|
||||
- ULyraInventoryManagerComponent:整个背包系统的管理组件。使用FLyraInventoryList来管理物品数据。
|
||||
- FLyraInventoryList:类型为`FFastArraySerializer`(为了解决网络传输而实现的类)。内部使用`TArray<FLyraInventoryEntry> Entries`来存储物品数据。
|
||||
- FLyraInventoryEntry:类型为`FFastArraySerializerItem`(为了解决网络传输而实现的类)。内部使用`ULyraInventoryItemInstance`来存储物品数据,内部还有StackCount与LastObservedCount变量(该变量不同步)。
|
||||
- ULyraInventoryItemInstance:物品实例。内部变量有`TSubclassOf<ULyraInventoryItemDefinition> ItemDef`以及一个用于存储物品状态的`FGameplayTagStackContainer`(Lyra定义的Tag堆栈)
|
||||
- ULyraInventoryItemDefinition:物品定义,`UCLASS(Blueprintable, Const, Abstract)`。内部变量有`TArray<TObjectPtr<ULyraInventoryItemFragment>> Fragments`,也就是说一个ItemDefinition会有多个ItemFragment。另外就是用于显示名称的`FText DisplayName`。
|
||||
- ULyraInventoryItemFragment:物品片段,`UCLASS(DefaultToInstanced, EditInlineNew, Abstract)`。可以理解为物品信息碎片,一个物品定义由多个碎片组成。
|
||||
|
||||
相关UClassMeta的官方文档解释:
|
||||
- Blueprintable:将此类公开为用于创建蓝图的可接受基类。默认为`NotBlueprintable`。子类会继承该标签。
|
||||
- Const:此类中的所有属性和函数都是`const`并且导出为`const`。子类会继承该标签。
|
||||
- Abstract:`Abstract`说明符会将类声明为"抽象基类"。阻止用户向关卡中添加此类的Actor。
|
||||
- DefaultToInstanced:此类的所有实例都被认为是"实例化的"。实例化的类(组件)将在构造时被复制。子类会继承该标签。
|
||||
- EditInlineNew:指示可以从虚幻编辑器"属性(Property)"窗口创建此类的对象,而非从现有资源引用。默认行为是仅可通过"属性(Property)"窗口指定对现有对象的引用。子类会继承该标签;子类可通过 `NotEditInlineNew` 来覆盖它。
|
||||
|
||||
### ItemInstance
|
||||
该类为物品的**实例**类,也是物品的操作类。包含了ItemDefinition之外还记录了一些额外的Runtime状态数据(FGameplayTagStackContainer也是一个FFastArraySerializer类),还定义各种操作用函数。
|
||||
|
||||
### ItemDefinition
|
||||
该类为定义物品各种属性的**资产**类,主要通过ItemFragment进行**碎片化描述**。通过蓝图继承的方式来创建一个个的物品定义。
|
||||
|
||||
PS.个人很好奇为什么不把这这个类做成一个DataAsset,因为这个类也就是个静态数据类。至少ULyraPickupDefinition继承自UDataAsset。
|
||||
|
||||
### ItemFragment
|
||||
该类为物品碎片化的属性描述类,同时附带OnInstanceCreated()虚函数,以实现一些属性变化外的特殊效果。
|
||||
|
||||
在Lyra中实现了这一些:
|
||||
- UInventoryFragment_EquippableItem:用于指定**ULyraEquipmentDefinition**(定义于Equipment)类。
|
||||
- UInventoryFragment_SetStats:用于给ItemInstance添加状态标签。
|
||||
- UInventoryFragment_QuickBarIcon:用于指定快捷栏SlateBrush资源以及UI显示名称。
|
||||
- UInventoryFragment_PickupIcon:用于指定骨骼物体(枪械或者血包)、显示名称以及pad颜色。
|
||||
|
||||
### Pickup
|
||||
定义了Pickup相关类:
|
||||
- FInventoryPickup:可以理解为Entry。
|
||||
- FPickupInstance:Instance。
|
||||
- FPickupTemplate:可以理解为Definition。
|
||||
- UPickupable与IPickupable:接口类。
|
||||
|
||||
## AddEntry
|
||||
给FLyraInventoryList添加Entry的逻辑大致为:
|
||||
1. 检查相关变量
|
||||
2. 给`TArray<FLyraInventoryEntry> Entries`添加Entry。
|
||||
1. 使用NewObject()填充Instance指针。
|
||||
2. 使用Instance->SetItemDef()设置ItemDefinition类。
|
||||
3. 通过ItemDefinition取得ItemFragment,并且调用接口函数OnInstanceCreated(),部分ItemDefinition会执行一些特殊逻辑。
|
||||
3. 设置Entry的StackCount,并设置Entry为Dirty,触发更新逻辑。
|
||||
4. 返回Instance指针。
|
||||
|
||||
代码如下:
|
||||
```c++
|
||||
ULyraInventoryItemInstance* FLyraInventoryList::AddEntry(TSubclassOf<ULyraInventoryItemDefinition> ItemDef, int32 StackCount)
|
||||
{
|
||||
ULyraInventoryItemInstance* Result = nullptr;
|
||||
|
||||
check(ItemDef != nullptr);
|
||||
check(OwnerComponent);
|
||||
|
||||
AActor* OwningActor = OwnerComponent->GetOwner();
|
||||
check(OwningActor->HasAuthority());
|
||||
|
||||
|
||||
FLyraInventoryEntry& NewEntry = Entries.AddDefaulted_GetRef();
|
||||
NewEntry.Instance = NewObject<ULyraInventoryItemInstance>(OwnerComponent->GetOwner()); //@TODO: Using the actor instead of component as the outer due to UE-127172
|
||||
NewEntry.Instance->SetItemDef(ItemDef);
|
||||
for (ULyraInventoryItemFragment* Fragment : GetDefault<ULyraInventoryItemDefinition>(ItemDef)->Fragments)
|
||||
{
|
||||
if (Fragment != nullptr)
|
||||
{
|
||||
Fragment->OnInstanceCreated(NewEntry.Instance);
|
||||
}
|
||||
}
|
||||
NewEntry.StackCount = StackCount;
|
||||
Result = NewEntry.Instance;
|
||||
|
||||
//const ULyraInventoryItemDefinition* ItemCDO = GetDefault<ULyraInventoryItemDefinition>(ItemDef);
|
||||
MarkItemDirty(NewEntry);
|
||||
|
||||
return Result;
|
||||
}
|
||||
```
|
||||
|
||||
# Equipment
|
||||
Lyra的装备系统只使用了**Entry - Instance - Definition**结构。
|
||||
|
||||
## Class
|
||||
- ULyraEquipmentManagerComponent:类似ULyraInventoryManagerComponent的装备管理类。
|
||||
- FLyraEquipmentList
|
||||
- FLyraAppliedEquipmentEntry
|
||||
- ULyraEquipmentInstance:装备实例。
|
||||
- ULyraEquipmentDefinition:装备定义。
|
||||
- FLyraEquipmentActorToSpawn:Spawn用的相关数据。
|
||||
- ULyraPickupDefinition:UDataAsset。主要是有ULyraInventoryItemDefinition以及其他美术资产指针。
|
||||
- ULyraWeaponPickupDefinition:ULyraPickupDefinition的子类,储存Spawn用的LocationOffset与Scale数据。
|
||||
- ULyraGameplayAbility_FromEquipment:Equipment类型GA的父类,实现2个ULyraEquipmentInstance/ULyraInventoryItemInstance Get函数。子类有2个蓝图与1个c++类。
|
||||
- ULyraQuickBarComponent
|
||||
|
||||
### ULyraEquipmentInstance
|
||||
OnEquipped ()/OnUnequipped()会调用对用的K2函数(BlueprintImplementableEvent),具体逻辑在蓝图实现。主要的逻辑就是播放对应的Montage动画。
|
||||
|
||||
### ULyraEquipmentDefinition
|
||||
- `TSubclassOf<ULyraEquipmentInstance> InstanceType`:装备类型,通过Class类型来判断装备类型。
|
||||
- `TArray<TObjectPtr<const ULyraAbilitySet>> AbilitySetsToGrant`:装备的附带能力集。
|
||||
- `TArray<FLyraEquipmentActorToSpawn> ActorsToSpawn` :装备Actor。
|
||||
|
||||
## FGameplayTagStack
|
||||
Lyra里定义了FGameplayTagStackContainer 与FGameplayTagStack这2个类来解决GameplayTagStack的同步问题。
|
||||
其中TagToCountMap只是本地/服务器用于快速查找GameplayTag的Stack数而维护的Map,它会在Add/RemoveStack()之外还会在三个网络同步函数进行对应修改。
|
||||
|
||||
- AddStack()
|
||||
- RemoveStack()
|
||||
- 网络同步函数
|
||||
- PreReplicatedRemove()
|
||||
- PostReplicatedAdd()
|
||||
- PostReplicatedChange()
|
||||
|
||||
## ItemDefintion是否可以做成DataAsset
|
||||
蓝图类以及非蓝图类资产,例如关卡和数据资产(`UDataAsset` 类的资产实例)。
|
||||
|
||||
### 非蓝图资产
|
||||
参考官方文档[AssetManagement](https://docs.unrealengine.com/4.27/zh-CN/ProductionPipelines/AssetManagement/)
|
||||
**假如主要资产类型不需要存储蓝图数据,你可以使用非蓝图资产。非蓝图资产在代码中的访问更简单,而且更节省内存。** 如需在编辑器中新建一个非蓝图主资产,请在"高级"内容浏览器窗口中新建一个数据资产,或使用自定义用户界面来创建新关卡等。以这种方式创建资产与创建蓝图类不一样;你创建的资产是类的实例,而非类本身。要访问类,请用 `GetPrimaryAssetObject` 这类C++函数加载它们,或者用蓝图函数(名称中没有Class)。一旦加载后,你就可以直接访问它们并读取数据。
|
||||
|
||||
>因为这些资产是实例而不是类,所以你无法从它们继承类或其他资产。如果你要这样做,例如,如果你想创建一个子资产,继承其父类的值(除了那些显式覆盖的值),你应该使用蓝图类来代替。
|
||||
|
||||
## UI
|
||||
Lyra没有制作背包部分的逻辑,但预留了相关的实现。采用在
|
||||
- `void FLyraInventoryList::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)`
|
||||
- `void FLyraInventoryList::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)`
|
||||
- `void FLyraInventoryList::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)`
|
||||
调用BroadcastChangeMessage()来实现传入数据到其他组件的目的。
|
||||
```c++
|
||||
/** A message when an item is added to the inventory */
|
||||
USTRUCT(BlueprintType)
|
||||
struct FLyraInventoryChangeMessage
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
//@TODO: Tag based names+owning actors for inventories instead of directly exposing the component?
|
||||
UPROPERTY(BlueprintReadOnly, Category=Inventory)
|
||||
UActorComponent* InventoryOwner = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = Inventory)
|
||||
ULyraInventoryItemInstance* Instance = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category=Inventory)
|
||||
int32 NewCount = 0;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category=Inventory)
|
||||
int32 Delta = 0;
|
||||
};
|
||||
|
||||
void FLyraInventoryList::BroadcastChangeMessage(FLyraInventoryEntry& Entry, int32 OldCount, int32 NewCount)
|
||||
{
|
||||
FLyraInventoryChangeMessage Message;
|
||||
Message.InventoryOwner = OwnerComponent;
|
||||
Message.Instance = Entry.Instance;
|
||||
Message.NewCount = NewCount;
|
||||
Message.Delta = NewCount - OldCount;
|
||||
|
||||
UGameplayMessageSubsystem& MessageSystem = UGameplayMessageSubsystem::Get(OwnerComponent->GetWorld());
|
||||
MessageSystem.BroadcastMessage(TAG_Lyra_Inventory_Message_StackChanged, Message);
|
||||
}
|
||||
```
|
||||
|
||||
### StatTags的作用
|
||||
物品系统的ULyraInventoryItemInstance中的来记录某个Tag对应的Int32数量,在蓝图中调用GetStatTagCount来获取对应的数量。
|
||||
```c++
|
||||
UPROPERTY(Replicated)
|
||||
FGameplayTagStackContainer StatTags;
|
||||
```
|
||||
|
||||
AddStatTagCount在Fragment(stat)里被调用,用于设置初始的话数量,RemoveStatTagCount则在 GameplayEffect的cost中被调用。
|
||||
|
||||
PS.可以用来记录 血药的使用量,重量32000 用一次=》30000
|
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: UE5 Lyra学习笔记(5)—QuickBar与Equipment
|
||||
date: 2022-09-16 11:03:03
|
||||
excerpt:
|
||||
tags:
|
||||
rating: ⭐
|
||||
---
|
||||
## 前言
|
||||
|
||||
|
||||
## ULyraQuickBarComponent
|
||||
继承自UControllerComponent(ModularGame的给Controller使用的胶水组件)。
|
||||
|
||||
### 成员变量
|
||||
```c++
|
||||
//QuickBar Slot中的ItemInstance数据
|
||||
UPROPERTY(ReplicatedUsing=OnRep_Slots)
|
||||
TArray<TObjectPtr<ULyraInventoryItemInstance>> Slots;
|
||||
|
||||
//当前激活的SlotIndex
|
||||
UPROPERTY(ReplicatedUsing=OnRep_ActiveSlotIndex)
|
||||
int32 ActiveSlotIndex = -1;
|
||||
|
||||
//当前装备的EquipmentInstance
|
||||
UPROPERTY()
|
||||
TObjectPtr<ULyraEquipmentInstance> EquippedItem;
|
||||
```
|
||||
2个OnRep()会使用GameplayMessageRouter发送Message,该Message最后会被UI接收并且进行对应处理。
|
||||
|
||||
### 函数
|
||||
只说几个相对重要的函数:
|
||||
- CycleActiveSlotForward:激活正向的下一个QuickBarSlot,判断Index是否有效后调用SetActiveSlotIndex()。
|
||||
- CycleActiveSlotBackward:激活反向的上一个QuickBarSlot,判断Index是否有效后调用SetActiveSlotIndex()。
|
||||
- EquipItemInSlot:装备指定的物品,通过`Slot[ActiveSlotIndex]`获取到ItemInstance,ItemInstance通过Fragment获取到EquipmentDefinition后用EquipmentManager调用函数EquipItem()来装备(调用FindEquipmentManager获取)。
|
||||
- UnequipItemInSlot:脱下指定的物品,EquipmentManager调用UnequipItem(EquippedItem)。
|
||||
- FindEquipmentManager:`Cast<AController>(GetOwner()))->GetPawn()->FindComponentByClass<ULyraEquipmentManagerComponent>()`
|
||||
- SetActiveSlotIndex_Implementation:含有`Server, Reliable`标记,依次调用`UnequipItemInSlot(); ActiveSlotIndex = NewIndex; EquipItemInSlot(); OnRep_ActiveSlotIndex(); `
|
||||
- AddItemToSlot:增加QuickBarSlot的ItemInstance数据并调用OnRep_Slots()。
|
||||
- RemoveItemFromSlot:如果处于装备状态会先调用UnequipItemInSlot(),之后移除QuickBarSlot的ItemInstance数据并调用OnRep_Slots()。
|
||||
|
||||
### QuickBarUI
|
||||
- W_QuickBar(组合控件):快捷栏主控件,用于嵌套W_QuickBarSlot。
|
||||
- W_QuickBarSlot:
|
||||
1. 通过GameplayMessageRouter读取FLyraQuickBarActiveIndexChangedMessage数据(QuickBar组件的消息),获取Payload拥有者是否与UI拥有者相同,来播放对应的UI动画。
|
||||
2. 通过GameplayMessageRouter读取FLyraQuickBarSlotsChangedMessage数据(QuickBar组件的消息),获取Payload拥有者以及ItemInstance数据,通过ItemInstance找到到指定的Fragment来获取图标资源来更新UI的图标。
|
||||
- W_WeaponAmmoAndName:取得角色的QuickBar组件,之后获取ActiveSlot的ItemInstance,获取对应的StatTag来设置显示Text(MagazineAmmo、SpareAmmo),再通过Fragment找到对应的图标资产。
|
||||
- 其他控件:
|
||||
- W_ActionTouchButton:通过GameplayMessageRouter读取消息,获得Duration值。
|
||||
|
||||
## ULyraEquipmentManagerComponent
|
||||
继承自UPawnComponent。(ModularGame的给Pawn使用的胶水组件)。
|
||||
|
||||
### 成员变量
|
||||
|
||||
### 相关逻辑
|
||||
- UI
|
||||
- 获取角色对应组件逻辑:`GetOwningPlayer->GetComponentByClass<ULyraQuickBarComponent>();`
|
||||
- C++
|
||||
- Controll组件获取到角色组件方法:`Cast<AController>(GetOwner()))->GetPawn()->FindComponentByClass<ULyraEquipmentManagerComponent>()`
|
||||
|
||||
### ItemInstance获取Pawn的方法
|
||||
```c++
|
||||
void ULyraEquipmentInstance::OnUnequipped()
|
||||
{
|
||||
K2_OnUnequipped();
|
||||
}
|
||||
|
||||
APawn* ULyraEquipmentInstance::GetPawn() const
|
||||
{
|
||||
return Cast<APawn>(GetOuter());
|
||||
}
|
||||
|
||||
APawn* ULyraEquipmentInstance::GetTypedPawn(TSubclassOf<APawn> PawnType) const
|
||||
{
|
||||
APawn* Result = nullptr;
|
||||
if (UClass* ActualPawnType = PawnType)
|
||||
{
|
||||
if (GetOuter()->IsA(ActualPawnType))
|
||||
{
|
||||
Result = Cast<APawn>(GetOuter());
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## UI从ItemDefinition获取数据的方式
|
||||
调用ItemInstance->FindFragmentByClass,再从指定的FragmentClass中获取数据。
|
||||
QuickBar使用了QuickBarIcon
|
||||
|
||||
```c++
|
||||
void ULyraQuickBarComponent::EquipItemInSlot()
|
||||
|
||||
{
|
||||
check(Slots.IsValidIndex(ActiveSlotIndex));
|
||||
check(EquippedItem == nullptr);
|
||||
if (ULyraInventoryItemInstance* SlotItem = Slots[ActiveSlotIndex])
|
||||
{
|
||||
if (const UInventoryFragment_EquippableItem* EquipInfo = SlotItem->FindFragmentByClass<UInventoryFragment_EquippableItem>())
|
||||
{
|
||||
TSubclassOf<ULyraEquipmentDefinition> EquipDef = EquipInfo->EquipmentDefinition;
|
||||
if (EquipDef != nullptr)
|
||||
{
|
||||
if (ULyraEquipmentManagerComponent* EquipmentManager = FindEquipmentManager())
|
||||
{
|
||||
EquippedItem = EquipmentManager->EquipItem(EquipDef);
|
||||
if (EquippedItem != nullptr)
|
||||
{
|
||||
EquippedItem->SetInstigator(SlotItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### QuickBar UI更新机制
|
||||
Lyra选择 tick来 getSlot
|
||||
```c++
|
||||
TArray<ULyraInventoryItemInstance*> GetSlots() const
|
||||
{
|
||||
return Slots;
|
||||
}
|
||||
|
||||
UPROPERTY(ReplicatedUsing=OnRep_Slots)
|
||||
TArray<TObjectPtr<ULyraInventoryItemInstance>> Slots;
|
||||
```
|
||||
|
||||
之后根据Slot进行设置图标数据或者将其设置成null。
|
||||
|
||||
PS.所以我打算这么做:ItemUISlot设置成null,之后遍历消除nullslot
|
56
03-UnrealEngine/Gameplay/Lyra/《Lyra初学者游戏包工程解读》quabqi 视频笔记.md
Normal file
56
03-UnrealEngine/Gameplay/Lyra/《Lyra初学者游戏包工程解读》quabqi 视频笔记.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: 《Lyra初学者游戏包工程解读》 | quabqi 视频笔记
|
||||
date: 2022-08-09 13:55:20
|
||||
tags: Lyra
|
||||
rating: ⭐️
|
||||
---
|
||||
# 《Lyra初学者游戏包工程解读》 | quabqi 视频笔记
|
||||
## ModularGameplay
|
||||
- LyraGameMode/LyraGameState/LyrrPlayerState/LyrrPlayerStart
|
||||
- LyraChracter/LyraPawn/LyraPawnComponent
|
||||
- LyraPlayerController/LyraPlayerBotController
|
||||
|
||||
## GameFeatures
|
||||
GameFeatures=数据驱动+组件化,通过配置自动挂载组件。GameFeatureData - Actions
|
||||
- ShooterCore
|
||||
- ShooterMap
|
||||
- TopDownArena
|
||||
|
||||
## Experience
|
||||
一个GameFeature包含多个Experience;Experience通过Level或者房主指定;所有业务都通过配置定义。
|
||||
配表以实现数据驱动,几乎所有的逻辑都通过配置动态加载。
|
||||
- LyraWorldSettings
|
||||
- LyraUserFacingExperienceDefinition
|
||||
|
||||
## Character、Controller、Camera
|
||||
只提供空壳Class基类,用来挂载各种Component。不再堆积大量业务逻辑,只做基本的消息转发逻辑。
|
||||
|
||||
Controller上使用了输入增强组件,通过触发IA(inputAction)发送GameplayTag,角色收到这些GameplayTag就会激活对应的GA。通过MapingContext这种数据表的方式避免了进行大量if-else的判断。
|
||||
|
||||
## Lyra上的换装系统
|
||||
换装的数据由服务器创建,并同步到客户端,客户端收到后Spawn对应Actor并且Attach到主Mesh上,主Mesh只有骨骼没有实际模型。
|
||||
|
||||
## Lyra动画
|
||||
- 多线程动画:UE5新增一个蓝图子线程Update函数BlueprintThreadSafeUpdateAnimation。
|
||||
- 动画分层:对动画蓝图进行分层,之后通过**PickBestAnimLayer**与**LinkAnimClassLayer**进行动态连接。
|
||||
- 新的LayerBlendPerBone节点:可以调整每个骨骼的混合权重。
|
||||
- UE5动画节点可以绑定回调函数了。
|
||||
- 因为主SkeletalMesh没有模型,所有在其他动画蓝图中需要使用CopyPose将父组件的动画复制过来。
|
||||
|
||||
## 网络同步
|
||||
- GE尽量不开Full模式,改用Mixed/Minimal
|
||||
有些数据在外部同步可以显著减少流量
|
||||
|
||||
## Lyra UI
|
||||
- 主UI分为4层(模态弹出框的分层),逻辑位于W_OverallUILayout
|
||||
- 提供一种UI挂点方案,支持按需求加载不同的UI。实现在UIExtension,**建议复用**。
|
||||
|
||||
### Common UI组件(建议复用)
|
||||
- 支持异步加载资源
|
||||
- 支持UI的Style配置
|
||||
- 支持Widget池,复用UI
|
||||
- 支持根据硬件自动适配,显示/隐藏
|
||||
- 支持Action
|
||||
- 支持返回键/自动关闭
|
||||
- 可见性切换器
|
||||
- Tab页
|
379
03-UnrealEngine/Gameplay/Online/GAS网络联机笔记.md
Normal file
379
03-UnrealEngine/Gameplay/Online/GAS网络联机笔记.md
Normal file
@@ -0,0 +1,379 @@
|
||||
---
|
||||
title: GAS网络联机笔记
|
||||
date: 2022-12-09 14:57:04
|
||||
excerpt:
|
||||
tags: Online
|
||||
rating: ⭐
|
||||
---
|
||||
## 资料链接
|
||||
https://docs.unrealengine.com/en-US/Resources/Showcases/BlueprintMultiplayer/index.html
|
||||
https://docs.unrealengine.com/en-US/ProgrammingAndScripting/Blueprints/UserGuide/OnlineNodes/index.html
|
||||
|
||||
### 视频
|
||||
- 虚幻引擎多人联机网络基础 | Network Multiplayer Fundamentals(真实字幕组) https://www.bilibili.com/video/BV1rV41167Em
|
||||
|
||||
## 测试用启动参数
|
||||
```bash
|
||||
C:\UnrealEngine\UnrealEngine\Engine\Binaries\Win64\UE4Editor.exe "C:\UnrealEngine\Project\SDHGame\SDHGame.uproject" -game -WINDOWED -WinX=0 -WinY=270 -ResX=960 -ResY=600
|
||||
|
||||
C:\UnrealEngine\UnrealEngine\Engine\Binaries\Win64\UE4Editor.exe "C:\UnrealEngine\Project\SDHGame\SDHGame.uproject" /Game/SceneAssets/Maps/GameplayDevelopMap?game=MyGame -server -log
|
||||
```
|
||||
|
||||
## UE4原生部分
|
||||
### 属性复制
|
||||
- 对于不想序列化的临时属性,比如CurrentHealth可以勾选transient
|
||||
|
||||
### RPC
|
||||
|
||||
### 网络模式(ENetMode)
|
||||
- NM_DedicatedServer:纯服务器
|
||||
- NM_ListenServer:客户端与服务器
|
||||
- NM_Client:纯客户端
|
||||
|
||||
在函数中判断当前模式:
|
||||
```c++
|
||||
if(GetNetMode() == NM_DedicatedServer)
|
||||
{}
|
||||
```
|
||||
|
||||
- 局域网联机的FPS游戏
|
||||
- 以NM_Standalone启动, 创建或加入房间
|
||||
- 如果是房主创建房间, 则变为NM_ListenServer
|
||||
- 如果是加入房间, 则变为NM_Client
|
||||
- 广域网MMO等游戏
|
||||
- 服务器以NM_DedicatedServer启动, 并只会是该模式
|
||||
- 客户端以NM_Standalone启动, 连接服务器后变为NM_Client
|
||||
- 命令行传递参数启动程序
|
||||
- 客户端启动参数添加服务器地址, 直接连接, 以NM_Client启动
|
||||
- 客户端启动参数中地图开启?Listen, 以NM_ListenServer启动
|
||||
- 客户端启动参数中添加-Server, 以NM_DedicatedServer启动
|
||||
|
||||
### 流程
|
||||
《Exploring in UE4》关于网络同步的理解与思考[概念理解]:https://zhuanlan.zhihu.com/p/34721113
|
||||
|
||||
主要步骤如下:
|
||||
1. 客户端发送连接请求
|
||||
2. 服务器将在本地调用 AGameMode::PreLogin。这样可以使 GameMode 有机会拒绝连接。
|
||||
3. 如果服务器接受连接,则发送当前地图
|
||||
4. 服务器等待客户端加载此地图,客户端如果加载成功,会发送Join信息到服务器
|
||||
5. 如果接受连接,服务器将调用 AGameMode::Login该函数的作用是创建一个PlayerController,可用于在今后复制到新连接的客户端。成功接收后,这个PlayerController 将替代客户端的临时PlayerController (之前被用作连接过程中的占位符)。
|
||||
此时将调用 APlayerController::BeginPlay。应当注意的是,在此 actor 上调用RPC 函数尚存在安全风险。您应当等待 AGameMode::PostLogin 被调用完成。
|
||||
6. 如果一切顺利,AGameMode::PostLogin 将被调用。这时,可以放心的让服务器在此 PlayerController 上开始调用RPC 函数。
|
||||
|
||||
#### 需要知道的概念
|
||||
- PlayerController一定是客户端第一次链接到服务器,服务器同步过来的这个PlayerController(也就是上面的第五点,后面称其为拥有连接的PlayerController)。进一步来说,这个Controller里面包含着相关的NetDriver,Connection以及Session信息。
|
||||
- 对于任何一个Actor(客户端上),他可以有连接,也可以无连接。一旦Actor有连接,他的Role(控制权限)就是ROLE_AutonomousProxy,如果没有连接,他的Role(控制权限)就是ROLE_SimulatedProxy 。
|
||||
|
||||
#### 问题
|
||||
##### Actor的Role是ROLE_Authority就是服务端么?
|
||||
**并不是**,有了前面的讲述,我们已经可以理解,如果我在客户端创建一个独有的Actor(不能勾选bReplicate)。那么这个Actor的Role就是ROLE_Authority,所以这时候你就不能通过判断他的Role来确定当前调试的是客户端还是服务器。这时候最准确的办法是获取到**NetDiver**,然后通过**NetDiver**找到Connection。(事实上,GetNetMode()函数就是通过这个方法来判断当前是否是服务器的)对于服务器来说,他只有N个ClientConnections,对于客户端来说只有一个serverConnection。
|
||||
|
||||
如何找到NetDriver呢?可以参考下面的图片,从Outer获取到当前的Level,然后通过Level找到World。World里面就有一个NetDiver。当然,方法
|
||||
|
||||
### 其他
|
||||
- 编辑器设置-Multipplayer Options-玩家个数,是的播放时能开启多个窗口(代表多个玩家)进行调试。
|
||||
- 对于角色类除了需要勾选Replicates还需要勾选Replicate Movement
|
||||
|
||||
#### Sessions与相关函数
|
||||
- FindSessions寻找房间,并且生成一个Session数组。
|
||||
|
||||
##### CreateSession
|
||||
- PublicConnections:连接数
|
||||
- UseLan:是否是局域网游戏
|
||||
- OnSuccess=》OpenLevel(在Option中添加listen)这样就会开启监听服务器
|
||||
|
||||
##### JoinSession
|
||||
GetGameInstance(Cast当前自定义游戏实例)=》JoinSession
|
||||
|
||||
### 蓝图多人设计游戏笔记
|
||||
- 关卡蓝图中Beginplay中首先通过GameInstance调用TransitionToState(),状态为Playing
|
||||
- 用一个MainMenu关卡作为菜单界面
|
||||
|
||||
- PlayerState:记录玩家的分数与id(原生实现)。Replicates=true,NetDormancy=Awake
|
||||
- GameState:空
|
||||
- GameMode:实现玩家重生、登录以后的逻辑(将玩家Pawn引用加到对应数组中)以及指定若干类(Pawn、Controller、GameState、PlayerState)
|
||||
- PlayerController:控制角色相关UI以及在登录后执行一次角色重生函数。
|
||||
|
||||
#### GameInstance
|
||||
自带状态枚举:
|
||||
- StartUp
|
||||
- MainMenu
|
||||
- ServerList
|
||||
- LoadingScreen
|
||||
- ErrorDialog
|
||||
- Playing
|
||||
- Unknown
|
||||
|
||||
**实现逻辑**:
|
||||
- 实现Transition():根据枚举执行不同的操作,比如隐藏UI、销毁Session。
|
||||
- 实现IsCurrentState():判断是否枚举是否相同,返回bool。
|
||||
- 显示主菜单逻辑:根据当前游戏状态(Playing或者MainMenu)显示游戏菜单或者退回主菜单(打开主界面关卡)。
|
||||
- 显示载入界面逻辑:切换成载入界面。(如果没有创建UMG就创建并赋值)
|
||||
- HostGameEvent:显示载入界面=》创建Session=》打开游戏地图,
|
||||
- ShowServerListEvent:显示服务器列表UI。
|
||||
- JoinFromServerListEvent:显示载入界面=》JoinSession
|
||||
- 错误处理:打印错误信息。c++代码会触发这些事件,NetworkError与TravelError。
|
||||
|
||||
### ShooterGame c++ 笔记
|
||||
#### RPC UFUNCTION Meta
|
||||
- server:服务端执行
|
||||
- client:客户端执行
|
||||
- NetMulticast:多播
|
||||
- reliable:可靠RPC
|
||||
- unreliable:不可靠RPC
|
||||
- WithValidation:需要验证
|
||||
|
||||
#### Class
|
||||
- AShooterTeamStart 出生点
|
||||
- AShooterCheatManager 作弊管理器
|
||||
|
||||
#### AShooterCharacter
|
||||
```
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Replication
|
||||
|
||||
void AShooterCharacter::PreReplication(IRepChangedPropertyTracker & ChangedPropertyTracker)
|
||||
{
|
||||
Super::PreReplication(ChangedPropertyTracker);
|
||||
|
||||
//只有在这个属性发生变化后,才会在短时间内复制这个属性,这样加入进度中的玩家才不会在后期加入时被喷fx。
|
||||
DOREPLIFETIME_ACTIVE_OVERRIDE(AShooterCharacter, LastTakeHitInfo, GetWorld() && GetWorld()->GetTimeSeconds() < LastTakeHitTimeTimeout);
|
||||
}
|
||||
|
||||
|
||||
void AShooterCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
//只对本地所有者:武器更换请求是本地发起的,其他客户不需要。
|
||||
DOREPLIFETIME_CONDITION(AShooterCharacter, Inventory, COND_OwnerOnly);
|
||||
|
||||
//除了本地拥有者:改变的本地激发者
|
||||
// everyone except local owner: flag change is locally instigated
|
||||
DOREPLIFETIME_CONDITION(AShooterCharacter, bIsTargeting, COND_SkipOwner);
|
||||
DOREPLIFETIME_CONDITION(AShooterCharacter, bWantsToRun, COND_SkipOwner);
|
||||
|
||||
DOREPLIFETIME_CONDITION(AShooterCharacter, LastTakeHitInfo, COND_Custom);
|
||||
|
||||
// everyone
|
||||
DOREPLIFETIME(AShooterCharacter, CurrentWeapon);
|
||||
DOREPLIFETIME(AShooterCharacter, Health);
|
||||
}
|
||||
```
|
||||
#### AShooterPlayerController
|
||||
- 一堆OnlineSubsystem东西,比如查询成绩、加载朋友信息……
|
||||
- SimulateInputKey():用于自动测试。
|
||||
- 控制InGame菜单
|
||||
- ReceivedNetworkEncryptionToken()与ReceivedNetworkEncryptionAck()对传输进行加密。使用一个定义的密钥。
|
||||
|
||||
##### ClientStartOnlineGame_Implementation
|
||||
使用OnlineSession的联机游戏
|
||||
|
||||
#### ShooterGameInstance
|
||||
- HostGame:开房间函数,url参数为地图。(可在FShooterMainMenu::HostGame()查看格式)
|
||||
- JoinSession:进房间,调用Session->JoinSession,切换地图。
|
||||
- BeginHostingQuickMatch:快速游戏,直接切换地图。
|
||||
- OnPostLoadMap:隐藏载入界面。
|
||||
- FindSessions:寻找房间。
|
||||
- HostQuickSession:开始游戏,同时开房间。
|
||||
|
||||
```
|
||||
/** Main menu UI */
|
||||
TSharedPtr<FShooterMainMenu> MainMenuUI;
|
||||
|
||||
/** Message menu (Shown in the even of errors - unable to connect etc) */
|
||||
TSharedPtr<FShooterMessageMenu> MessageMenuUI;
|
||||
|
||||
/** Welcome menu UI (for consoles) */
|
||||
TSharedPtr<FShooterWelcomeMenu> WelcomeMenuUI;
|
||||
|
||||
/** Dialog widget to show non-interactive waiting messages for network timeouts and such. */
|
||||
TSharedPtr<SShooterWaitDialog> WaitMessageWidget;
|
||||
```
|
||||
|
||||
#### Online文件夹中文件
|
||||
GameMode管理游戏的游戏方式与规则:
|
||||
- ShooterGameMode(基类)
|
||||
- ShooterGame_FreeForAll
|
||||
- ShooterGame_TermDeathMatch
|
||||
|
||||
##### AShooterGameSession
|
||||
继承AGameSession。
|
||||
|
||||
匹配:StartMatchmaking()、ContinueMatchmaking()会调用JoinSession()。
|
||||
- RegisterServer
|
||||
- HostSession
|
||||
- FindSessions
|
||||
- JoinSession
|
||||
|
||||
###### 委托
|
||||
```
|
||||
/** Delegate for creating a new session */
|
||||
FOnCreateSessionCompleteDelegate OnCreateSessionCompleteDelegate;
|
||||
/** Delegate after starting a session */
|
||||
FOnStartSessionCompleteDelegate OnStartSessionCompleteDelegate;
|
||||
/** Delegate for destroying a session */
|
||||
FOnDestroySessionCompleteDelegate OnDestroySessionCompleteDelegate;
|
||||
/** Delegate for searching for sessions */
|
||||
FOnFindSessionsCompleteDelegate OnFindSessionsCompleteDelegate;
|
||||
/** Delegate after joining a session */
|
||||
FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate;
|
||||
|
||||
//OnlineSubSystem交互的
|
||||
OnFindSessionsComplete(bool bWasSuccessful)
|
||||
void AShooterGameSession::OnJoinSessionComplete(FName InSessionName, EOnJoinSessionCompleteResult::Type Result)
|
||||
{
|
||||
bool bWillTravel = false;
|
||||
|
||||
UE_LOG(LogOnlineGame, Verbose, TEXT("OnJoinSessionComplete %s bSuccess: %d"), *InSessionName.ToString(), static_cast<int32>(Result));
|
||||
|
||||
IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld());
|
||||
if (OnlineSub)
|
||||
{
|
||||
IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
|
||||
if (Sessions.IsValid())
|
||||
{
|
||||
Sessions->ClearOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegateHandle);
|
||||
}
|
||||
}
|
||||
|
||||
OnJoinSessionComplete().Broadcast(Result);
|
||||
}
|
||||
```
|
||||
|
||||
### 专用服务器
|
||||
https://docs.unrealengine.com/en-US/InteractiveExperiences/Networking/HowTo/DedicatedServers/index.html
|
||||
|
||||
- 打包选择专用服务器(需要使用源码版引擎)
|
||||
- MyProjectServer.exe -log
|
||||
|
||||
### 复制优化
|
||||
设置Replicate中属性,比如:
|
||||
- 当不需要复制时,关闭复制。
|
||||
- 适当降低Net Update Frequency
|
||||
- NetCullDistanceSquared
|
||||
- NetClientTicksPerSecond
|
||||
- NetDormancy:可以一开始将Actor设置为只在初始化同步,之后根据事件调用ForceNetUpdate或者将Net Dormancy设置为Awake。
|
||||
- Relevancy
|
||||
|
||||
命令行输入netprofile,运行一会后,输入netprofile disable来停止记录,之后就可以在Saved中找到网络性能报告了。
|
||||
|
||||
### 服务器切换
|
||||
当前服务器掉线时切换为玩家作为服务器。
|
||||
https://docs.unrealengine.com/zh-CN/InteractiveExperiences/Networking/Travelling/index.html
|
||||
|
||||
### 其他
|
||||
- 蓝图节点IsLocallyController判断角色是本地模拟还是远程控制。在c++是GetRomoteRole
|
||||
- Instigator,每个Actor拥有变量,用于判断是谁触发了XX效果,可以用来谁击杀了玩家,以及谁得分了。
|
||||
- 尽量避免使用RPC,使用OnRep_XX是个好的选择。
|
||||
- Movement属于不可靠复制。可靠复制会占用一个特殊的buff队列,控制每帧发送可靠复制的量,以节约资源。
|
||||
- 主机迁移(当前服务器离线时会让其中一个玩家充当服务器):1、非无缝,会出现加载框
|
||||
- OnValidData标签用于验证数据是否有效,防止作弊,需要实现一个返回值为bool的XXX_ValidData函数。
|
||||
|
||||
### Online Beacon 基类
|
||||
Beacon 类执行的常规操作是请求服务质量信息、在客户端需要加入的游戏中预留空位、接收游戏中玩家名列表、获取正在进行的游戏中的得分和运行时间,等等。 以下类由引擎提供,构成了 Online Beacon 系统的基础:
|
||||
|
||||
#### AOnlineBeacon
|
||||
这是 AOnlineBeaconClient 和 AOnlineBeaconHost 的基类。 它直接派生自 AActor。
|
||||
|
||||
#### AOnlineBeaconHost
|
||||
此类使用其自身的 UNetDriver 获得来自远程客户端电脑的传入 Online Beacon 连接。 接收到连接时,它将在注册 AOnlineBeaconHostObject 实例列表中进行查找,找到与传入客户端匹配的实例并转交连接。 此类通常不需要被派生,因其只管理客户端和注册 AOnlineBeaconHostObject 之间的初始连接。
|
||||
|
||||
#### AOnlineBeaconClient
|
||||
此类的子项连接到主机并执行实际的 RPC。 它们其中一个将在客户端电脑上生成,一个由正确的 AOnlineBeaconHostObject(注册到服务器的 AOnlineBeaconHost)在服务器上生成。 GetBeaconType 函数的输出(即为类名称)将用于对比此类的实例和正确主机对象类的注册实例。 注意:这和普通的 Actor 生成方式(服务器生成 Actor 然后复制到客户端)不同。 然而,客户端和服务器对象副本之间的连接建立后,对象复制将正常进行,任意一方均可向对方执行 RPC,而对象的服务器版本可对属性复制发送命令。 该基类实现 OnConnected 和 OnFailure 函数。这两个函数可由子类覆盖,在连接时执行 RPC,或处理失败连接。 此类是 Online Beacon 系统的主力,将执行 Beacon 所需的客户端端的工作。 在成功连接事件中,服务器上将生成和源实例同步的另一个实例,此例也可执行服务器端的工作,通过客户端和服务器 RPC(或服务器到客户端的复制属性)进行协调和交流。
|
||||
|
||||
#### AOnlineBeaconHostObject
|
||||
此类也应被覆盖,使其和覆盖的 AOnlineBeaconClient 类配对。 将客户端 GetBeaconType 的返回值和保存在 BeaconTypeName 成员变量中的值进行匹配即可完成配对。 服务器的 AOnlineBeaconHost 检测到传入 AOnlineBeaconClient 的配对 AOnlineBeaconHostObject 时,它将指示 AOnlineBeaconHostObject 通过虚拟 SpawnBeaconActor 函数生成 AOnlineBeaconClient 的本地副本。 此函数默认使用 ClientBeaconActorClass 成员变量确定要生成的 actor 类,此类应被设为配对的 AOnlineBeaconClient 类。 它还将在生成对象的服务器副本上调用 SetBeaconOwner,以便客户端对象的服务器端实例与主机对象进行交流。 此设置多数建立在基类中,无需被覆盖。
|
||||
|
||||
### 插件
|
||||
下面是2个牛逼插件
|
||||
- Advanced Steam Sessions
|
||||
- Advanced Session
|
||||
|
||||
常规用法:
|
||||
http://community.metahusk.com/topic/26/community-project-cardinal-menu-system-instructions-help-and-discussion
|
||||
#### 官方插件
|
||||
- Steam Sockets https://docs.unrealengine.com/zh-CN/InteractiveExperiences/Networking/HowTo/SteamSockets/index.html
|
||||
- Online Subsystem
|
||||
- Replication Graph插件 Replication Graph插件是一个用于多人游戏的网络复制系统,它的设计可以很好地适应大量玩家和复制Actor。例如,Epic自己的Fortnite Battle Royale 从一开始就支持每场比赛100名玩家,包含大约50,000个复制的Actor。https://www.bilibili.com/medialist/play/watchlater/BV1Xb411h7hp
|
||||
|
||||
## GAS部分
|
||||
### Replication
|
||||
- `#include "UnrealNetwork.h"`
|
||||
- 给对应的变量添加Replicated标记
|
||||
- 重写void GetLifetimeReplicatedProps(TArray& OutLifetimeProps),并添加对应变量的代码,例如: DOREPLIFETIME_CONDITION_NOTIFY( UMyAttributeSet, MyAttribute, COND_None, REPNOTIFY_Always);
|
||||
- 属性钩子函数UPROPERTY( ReplicatedUsing = OnRep_MyAttribute)、void OnRep_MyAttribute()
|
||||
|
||||
### 需要在c++对应的构造函数中进行初始化
|
||||
```c++
|
||||
AGDPlayerState :: AGDPlayerState()
|
||||
{
|
||||
//创建能力系统组件,并将其设置为显式复制
|
||||
AbilitySystemComponent = CreateDefaultSubobject <UGDAbilitySystemComponent>( TEXT( “ AbilitySystemComponent ”));;
|
||||
AbilitySystemComponent-> SetIsReplicated(true);
|
||||
// ...
|
||||
}
|
||||
|
||||
void APACharacterBase :: PossessedBy(AController * NewController)
|
||||
{
|
||||
Super :: PossessedBy(NewController);
|
||||
|
||||
如果(AbilitySystemComponent)
|
||||
{
|
||||
AbilitySystemComponent-> InitAbilityActorInfo(this,this);
|
||||
}
|
||||
|
||||
// ASC MixedMode复制要求ASC所有者的所有者为控制器。
|
||||
SetOwner(NewController);
|
||||
}
|
||||
|
||||
// Server only
|
||||
void AGDHeroCharacter::PossessedBy(AController * NewController)
|
||||
{
|
||||
Super::PossessedBy(NewController);
|
||||
|
||||
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
|
||||
if (PS)
|
||||
{
|
||||
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
|
||||
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
|
||||
|
||||
// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
|
||||
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
// Client only
|
||||
void AGDHeroCharacter::OnRep_PlayerState()
|
||||
{
|
||||
Super::OnRep_PlayerState();
|
||||
|
||||
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
|
||||
if (PS)
|
||||
{
|
||||
// Set the ASC for clients. Server does this in PossessedBy.
|
||||
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
|
||||
|
||||
// Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor.
|
||||
AbilitySystemComponent->InitAbilityActorInfo(PS, this);
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Net Security Policy
|
||||
A GameplayAbility's NetSecurityPolicy determines where should an ability execute on the network. It provides protection from clients attempting to execute restricted abilities.
|
||||
|
||||
- NetSecurityPolicy Description
|
||||
ClientOrServer No security requirements. Client or server can trigger execution and termination of this ability freely.
|
||||
- ServerOnlyExecution A client requesting execution of this ability will be ignored by the server. Clients can still request that the server cancel or end this ability.
|
||||
- ServerOnlyTermination A client requesting cancellation or ending of this ability will be ignored by the server. Clients can still request execution of the ability.
|
||||
- ServerOnly Server controls both execution and termination of this ability. A client making any requests will be ignored.
|
||||
|
||||
## 专用服务器查找
|
||||
https://answers.unrealengine.com/questions/502967/dedicated-server-find-session-issues.html?sort=oldest
|
||||
|
||||
Dedicated servers must have "Use Presence" and "Allow Join Via Presence" set to false.
|
||||
Maybe that could solve your problem :)
|
117
03-UnrealEngine/Gameplay/Online/OnlineSubsystem使用笔记.md
Normal file
117
03-UnrealEngine/Gameplay/Online/OnlineSubsystem使用笔记.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: OnlineSubsystem使用笔记
|
||||
date: 2022-12-09 14:59:15
|
||||
excerpt:
|
||||
tags: Online
|
||||
rating: ⭐
|
||||
---
|
||||
## 参考
|
||||
《Exploring in UE4》Session与Onlinesubsystem[概念理解]:https://zhuanlan.zhihu.com/p/34257172
|
||||
https://dawnarc.com/2019/07/ue4networking-in-baisc-sessions/
|
||||
|
||||
## 添加模块
|
||||

|
||||
|
||||
## 添加配置
|
||||
在DefaultEngine.ini中的 [OnlineSubsytem]标签下将DefaultPlatformService指定为所需服务。(测试则使用Steam)
|
||||
|
||||
在确定平台后,UE4就会去加载OnlineSubsystem +Name的模块。
|
||||
|
||||
ps1.OnlineSubsystemNull为本地处理模块。
|
||||
ps2.加载成功后还要继续调用对应平台Module的StartupModule函数。如果是steam,还需要到”../Engine/Binaries/ThirdParty/ Steamworks/Steamv132/Win64/”路径下去加载其平台的dll文件(路径可能有些偏差,具体看文件steam_api64.dll的位置) 代码如下:
|
||||
```c++
|
||||
FString RootSteamPath = GetSteamModulePath();
|
||||
FPlatformProcess::PushDllDirectory(*RootSteamPath);
|
||||
SteamDLLHandle = FPlatformProcess::GetDllHandle(*(RootSteamPath + "steam_api64.dll "));
|
||||
```
|
||||
|
||||
### 添加OnlineSubsystemSteam配置
|
||||
一般默认在非Shipping版本或者配置文件OnlineSubsystemSteam的bEnable为false的情况下在初始化OnlinesubsystemSteam的时候(包括其他平台),会CreateSubsystem失败,然后Destroy该Onlinesubsystem。这样引擎会默认创建OnlinesubsystemNull来替代。所以需要将bEnable设置成true。
|
||||
|
||||

|
||||
|
||||
### 配置Steam SDK过程
|
||||
UE4使用steam子系统(发布steam包):https://www.cnblogs.com/VirtualJourneyStudio/archive/2004/01/13/10557044.html
|
||||
|
||||
## 文档笔记
|
||||
https://docs.unrealengine.com/zh-CN/ProgrammingAndScripting/Online/index.html
|
||||
|
||||
static IOnlineSubsystem* Get(const FName& SubsystemName = NAME_None)
|
||||
|
||||
### 主要接口
|
||||
- Achievements:列出游戏中的所有成就,解锁成就,并查看自己和其他用户已解锁的成就。
|
||||
- External UI:打开特定硬件平台或在线服务的内置用户接口。在某些情况下,仅可通过此接口获取部分核心功能的访问权。
|
||||
- Friends:好友和好友列表的相关内容,例如在好友列表中添加用户、阻止和解除阻止用户,以及列出最近遇到的在线玩家。
|
||||
- Leaderboard:访问在线排行榜,包括登记自己的得分(或时间),以及在排行榜中查看好友列表或世界其他玩家的得分。
|
||||
- Online User:收集关于用户的元数据。
|
||||
- Presence:设置用户在线状态的显示方式,例如"在线"、"离开"、"游戏中"等。
|
||||
- Purchase:进行游戏内购和查看购买历史。
|
||||
- Session:创建、撤销和管理在线游戏会话。还包括搜索会话和配对系统。
|
||||
- Store:检索游戏内购可用的条目和特定价格。
|
||||
- User Cloud:提供每个用户云文件存储的接口。
|
||||
|
||||
#### Sessions
|
||||
- 使用所需设置创建新会话
|
||||
- 等待玩家申请加入比赛
|
||||
- 注册想要加入的玩家
|
||||
- 开始会话
|
||||
- 玩游戏
|
||||
- 终止会话
|
||||
- 取消玩家注册
|
||||
|
||||
或者:
|
||||
- 如果你想要变更比赛类型并返回以等待玩家加入,则更新会话
|
||||
- 终止会话
|
||||
|
||||
#### FOnlineSessionSettings
|
||||
FOnlineSessionSettings除了这些基础属性外,可以增加一些自定义属性,具体是往一个FOnlineKeyValuePairs<FName, FOnlineSessionSetting>里添加属性,之后客户端在OnFindSessionsComplete()中再通过指定的FName取出。
|
||||
|
||||
- bAllowJoinInProgress
|
||||
- bIsDedicated
|
||||
- bIsLANMatch
|
||||
- ShouldAdvertise
|
||||
- bUsesPresence
|
||||
- NumPublicConnections
|
||||
- NumPrivateConnections
|
||||
|
||||
```
|
||||
/** Array of custom session settings */
|
||||
FSessionSettings Settings;
|
||||
|
||||
/** Type defining an array of session settings accessible by key */
|
||||
typedef FOnlineKeyValuePairs<FName, FOnlineSessionSetting> FSessionSettings;
|
||||
|
||||
struct FOnlineSessionSetting
|
||||
{
|
||||
public:
|
||||
/** Settings value */
|
||||
FVariantData Data;
|
||||
/** How is this session setting advertised with the backend or searches */
|
||||
EOnlineDataAdvertisementType::Type AdvertisementType;
|
||||
/** Optional ID used in some platforms as the index instead of the session name */
|
||||
int32 ID;
|
||||
}
|
||||
```
|
||||
##### Steam设置
|
||||
对于Steam需要设置以下一些属性:
|
||||
- ShooterHostSettings->Set(SETTING_MATCHING_HOPPER, FString("TeamDeathmatch"), EOnlineDataAdvertisementType::DontAdvertise);
|
||||
- ShooterHostSettings->Set(SETTING_MATCHING_TIMEOUT, 120.0f, EOnlineDataAdvertisementType::ViaOnlineService);
|
||||
- ShooterHostSettings->Set(SETTING_SESSION_TEMPLATE_NAME, FString("GameSession"), EOnlineDataAdvertisementType::DontAdvertise);
|
||||
- ShooterHostSettings->Set(SETTING_GAMEMODE, FString("TeamDeathmatch"), EOnlineDataAdvertisementType::ViaOnlineService);
|
||||
- ShooterHostSettings->Set(SETTING_MAPNAME, GetWorld()->GetMapName(), EOnlineDataAdvertisementType::ViaOnlineService);
|
||||
|
||||
##### EOnlineDataAdvertisementType
|
||||
服务器数据广播方式:
|
||||
```
|
||||
/** Don't advertise via the online service or QoS data */
|
||||
DontAdvertise,
|
||||
/** Advertise via the server ping data only */
|
||||
ViaPingOnly,
|
||||
/** Advertise via the online service only */
|
||||
ViaOnlineService,
|
||||
/** Advertise via the online service and via the ping data */
|
||||
ViaOnlineServiceAndPing
|
||||
```
|
||||
|
||||
## 其他代码参考
|
||||
在Online模块下有个OnlineFramework文件夹。
|
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: 在帧同步战斗上加入UE4的DS(专有服务器)的简单尝试
|
||||
date: 2022-12-09 15:10:14
|
||||
excerpt:
|
||||
tags: Online
|
||||
rating: ⭐
|
||||
---
|
||||
## 原文地址
|
||||
帧同步框架下添加状态同步记录
|
||||
https://zhuanlan.zhihu.com/p/399047125
|
||||
|
||||
在帧同步战斗上加入UE4的DS(专有服务器)的简单尝试
|
||||
https://zhuanlan.zhihu.com/p/480154978
|
||||
|
||||
## 其他工程
|
||||
https://github.com/HiganFish/UE4SpaceShipBattleOL
|
||||
|
||||
## 实现细节
|
||||
DS本质上使用的是状态同步,那么大思路是参考我之前的一个文章参考帧同步框架下添加状态同步记录。
|
||||
|
||||
下面主要讲下实现的一些细节:
|
||||
|
||||
分离服务器和客户端逻辑
|
||||
这个工作主要是把战斗中只需要在服务器执行的逻辑分离出来,比如属性数值计算相关,攻击碰撞检测,技能,buff释放逻辑,子弹释放逻辑,怪物AI等。大思路是服务器只下发角色属性和状态信息,客户端根据逻辑进行相应的状态切换(包括技能释放)但不会执行和真实逻辑有关的计算。
|
||||
|
||||
服务器和客户端通信方式
|
||||
这里使用UE4提供的RPC调用。使用UStruct定义的结构作为协议。省去了自己实现序列化和反序列化的工作。
|
||||
|
||||
调整框架
|
||||
因为使用DS后,GameModeBattle只存在于服务端。固客户端上的BaseBattle就需要另外找个地方可以更新。一番研究后选择了NetPlayerController(继承自PlayerController)。
|
||||
|
||||
根据Replication,客户端的NetPlayerController是由客户端连上服务器后,服务器创建然后复制给客户端的。RPC调用也是写在NetPlayerController里,不管是服务器调用客户端,还是客户端调用服务器。
|
||||
|
||||
调整单位创建流程
|
||||
之前文章里的服务器其实只要跑逻辑部分,不需要Native层相关的资源。现在因为要使用CharacterMovementComponent和移动预测,服务器上的单位也必须把GameActor创建出来。创建后是由Replication复制到客户端,并且同时复制这个单位的逻辑唯一ID属性(PID)。
|
||||
|
||||
原本逻辑上是由BeActor创建GeActor并且创建GameActor。现在需要改成客户端收到服务器创建单位的消息后创建BeActor(也会设置PID)并且创建GeActor,但是不创建GameActor。等GameActor从服务器复制到客户端后在GameActor的StartPlay里启动绑定流程,把这个GameActor绑定到相同PID的BeActor的GeActor上。PID是服务器和客户端标识同一单位的属性。
|
||||
|
||||
删除的时候也是同理。客户端收到删除消息后,把BeActor以及GeActor删除,GameActor由Replication同步来删除。
|
||||
|
||||
这里顺便提下子弹 的做法。子弹因为一般都是以一定速度沿特定轨迹移动,所以没有继承CharacterMovementComponent。为了优化流量消耗,子弹的位置可以不需要服务器每帧更新。只需在创建信息把速度,创建轨迹等参数下发,客户端就可以自行创建这个子弹以及轨迹移动做表现。
|
||||
|
||||
使用UE4 CharacterMovementComponent
|
||||
要使用CharacterMovementComponent,需要使用Character。于是原本的基于Actor的GameActor改为继承Character,并且设置相关的参数,调整碰撞胶囊体的大小。
|
||||
修改逻辑自带的移动组件,主要是每帧把逻辑层的速度设置给MovementComponent的Velocity。并且每帧取得组件Location反设给自带移动组件。因为逻辑上层还是通过访问自带移动组件来进行位置的判断。
|
||||
服务器上也需要每帧把逻辑的信息更新给CharacterMovementComponent,包括位置,旋转和缩放
|
||||
使用移动预测
|
||||
拥有CharacterMovementComponent的Character需要是ROLE_AutonomousProxy才可以进行预测。而且现在的框架上NetPlayerController是先创建,然后才会创建GameActor,因此需要在服务器上执行PlayerController的Posses函数来设置控制的Actor。
|
||||
本地客户端对于ROLE_AutonomousProxy的GameActor需要在Run和Idle对CharacterMovementComponent设置Velocity来驱动组件移动。这样对于移动和停止的Idle,本地客户端不会等服务器的Replication就会立马进行。 而对于其他玩家和怪物,则是ROLE_SimulatedProxy,全程由Replication来进行位置变化
|
||||
怪物寻路
|
||||
使用navmesh,直接在服务器端调用navmesh寻路接口返回得到路径点,在原来的AI移动逻辑上调整代码沿着路径点行走即可。
|
15
03-UnrealEngine/Gameplay/Other/Github搜索技巧.md
Normal file
15
03-UnrealEngine/Gameplay/Other/Github搜索技巧.md
Normal file
@@ -0,0 +1,15 @@
|
||||
2020-07-31 9:53:22
|
||||
# 按照项目名/仓库名搜索(大小写不敏感)
|
||||
in:name xxx
|
||||
# 按照README搜索(大小写不敏感)
|
||||
in:readme xxx
|
||||
# 按照description搜索(大小写不敏感)
|
||||
in:description xxx
|
||||
# stars数大于xxx
|
||||
stars:>xxx
|
||||
# forks数大于xxx
|
||||
forks:>xxx
|
||||
# 编程语言为xxx
|
||||
language:xxx
|
||||
# 最新更新时间晚于YYYY-MM-DD
|
||||
pushed:>YYYY-MM-DD
|
24
03-UnrealEngine/Gameplay/Other/TextRender显示中文的方法.md
Normal file
24
03-UnrealEngine/Gameplay/Other/TextRender显示中文的方法.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: TextRender显示中文的方法
|
||||
date: 2023-01-28 14:01:57
|
||||
excerpt:
|
||||
tags: TextRender
|
||||
rating: ⭐
|
||||
---
|
||||
# 前言
|
||||
早几年学习UE的时候发现TextRender渲染中文的时候会出现□的情况,一直都去解决。最近又遇到这个需求了,最终在官方论坛上找到了解决方案,现在分享给大家。
|
||||
|
||||
# 步骤
|
||||

|
||||
|
||||
1. 导入一个字体生成Font资产或者新建一个Font资产,接下来对Font资产进行设置。
|
||||
2. 将**FontCacheType** 设置成**Offline**。
|
||||
3. 下面设置**ImportOptions**,设置**FontName**为想要的字体名称。
|
||||
4. 设置**UnicodeRange**为**4E00-9FFF** 。
|
||||
5. 勾选**Use Distance Field Alpha**选项。
|
||||
6. 在Font资产编辑器汇总点击Asset-Reimport Font_XXXX,来重新导入字体资产,之后会卡比较长的时间。
|
||||
7. 复制默认字体材质球EngineContent-EngineMaterials->DefaultTextMaterialOpaque,或者新建一个字体材质球作为TextRender的材质,并且修改材质里的Font资产。
|
||||
8. 在对应的TextRender中修改材质与Font资产即可显示中文。
|
||||
|
||||

|
||||
但是可以看得出一些中文符号是没办法正常显示的,原因是**4E00-9FFF**只包含了文字,没有包含中文符号。所以要么是使用英文符号来代替,要么就是提高Unicode-Range的范围。
|
10
03-UnrealEngine/Gameplay/Other/UE Splash 启动画面Logo设置.md
Normal file
10
03-UnrealEngine/Gameplay/Other/UE Splash 启动画面Logo设置.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: UE Splash 启动画面Logo设置
|
||||
date: 2022-11-27 09:27:35
|
||||
excerpt:
|
||||
tags:
|
||||
rating: ⭐
|
||||
---
|
||||
位于ProjectSettings->Platforms->Windows->Splash。
|
||||

|
||||
|
88
03-UnrealEngine/Gameplay/Other/UE4 本地化.md
Normal file
88
03-UnrealEngine/Gameplay/Other/UE4 本地化.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: UE4 本地化
|
||||
date: 2021-02-05 13:55:15
|
||||
tags:
|
||||
rating: ⭐️
|
||||
---
|
||||
## 参考
|
||||
https://gameinstitute.qq.com/community/detail/123008
|
||||
https://blog.csdn.net/u010385624/article/details/89705285
|
||||
|
||||
视频:
|
||||
- UE4 多语言本地化制作思路:https://www.bilibili.com/video/av96177350
|
||||
- Localizing Action RPG Game | Inside Unreal:https://www.youtube.com/watch?v=UD2_TEgxkqs
|
||||
|
||||
|
||||
## 代码中的操作
|
||||
在源文件中(CPP文档中),我们的本地化操作需要借助FText(FText本身就是为了解决显示信息的本地化而构建)进行操作,FText的创建方式有两种
|
||||
|
||||
01.声明文本空间宏方式构建
|
||||
.cpp 顶端
|
||||
```c++
|
||||
//引号内容可以随意
|
||||
#define LOCTEXT_NAMESPACE "UECppTest"
|
||||
.cpp 底端
|
||||
```
|
||||
```c++
|
||||
//放到同文件CPP的底端
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
```
|
||||
对于文本空间声明完毕后即可使用宏LOCTEXT进行FText构建。
|
||||
|
||||
.cpp
|
||||
```c++
|
||||
FText t1 = LOCTEXT("UECppGMB_T1", "Hello");//此代码需要在上面的声明宏内部
|
||||
```
|
||||
02.直接使用宏NSLOCTEXT构建
|
||||
.cpp
|
||||
```c++
|
||||
FText t1 = NSLOCTEXT("UECppTest", "UECppGMB_T1", "Hello");
|
||||
```
|
||||
第二种方法较第一种可以省去空间声明,但是并没有丢弃空间功能,只是在声明时需要额外多提供一个空间名称作为参数传递给宏
|
||||
|
||||
## FText
|
||||
对于文字使用 本地化工具,批量收集FText进行翻译。
|
||||
|
||||
## 非文件
|
||||
对于音频,视频,图片,各种数据文件相关的本地化操作,可以直接右键其蓝图文件,选择Asset localization操作。
|
||||

|
||||
|
||||
创建以后会在本地化文件夹位置自动生成一个对应的文件副本,然后把该文件副本改成你需要替换的本地化文件就好了,注意该副本的文件名一定要与原件文件名一致,这样才能自动调用到本地化版本的文件。测试和使用方法与9步和10步相同。
|
||||
|
||||
## 在蓝图中动态切换语言
|
||||
C++中设置动态切换语言的函数库。
|
||||
创建UBlueprintFunctionLibrary的子类,具体内容如下
|
||||
.h文件
|
||||
```c++
|
||||
#pragma once
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "MyBlueprintFunctionLibrary.generated.h"
|
||||
UCLASS()
|
||||
class LOCALIZATIONTEST_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
/* Change Localization at Runtime. */
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Change Localization"), Category = "Locale")
|
||||
static void ChangeLocalization(FString target);
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Localization"), Category = "Locale")
|
||||
static FString GetLocalization();
|
||||
};
|
||||
.cpp
|
||||
#include "localizationTest.h"
|
||||
#include "MyBlueprintFunctionLibrary.h"
|
||||
void UMyBlueprintFunctionLibrary::ChangeLocalization(FString target)
|
||||
{
|
||||
FInternationalization::Get().SetCurrentCulture(target);
|
||||
}
|
||||
FString UMyBlueprintFunctionLibrary::GetLocalization()
|
||||
{
|
||||
return FInternationalization::Get().GetCurrentCulture().Get().GetName();
|
||||
}
|
||||
```
|
||||
|
||||
## 本地化对话
|
||||
本地化对话以 Dialogue Wave类型的资源为中心。该资源可以通过本地化控制板界面的GatherText 收集其中的 spoken text 和 optional subtitle overrides。
|
||||
|
||||
Dialogue Wave 提供了一种 根据不同说话者与倾听者,说出意思相同的一段话,却使用不同语音与显示字幕的一种方法。
|
||||

|
9
03-UnrealEngine/Gameplay/Other/Ue4 官方的Mod插件与项目.md
Normal file
9
03-UnrealEngine/Gameplay/Other/Ue4 官方的Mod插件与项目.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Ue4 官方的Mod插件与项目
|
||||
date: 2022-12-09 13:39:17
|
||||
excerpt:
|
||||
tags: MOD
|
||||
rating: ⭐
|
||||
---
|
||||
## 快速入门地址
|
||||
https://github.com/EpicGames/UGCExample/blob/release/Documentation/QuickStart.md
|
32
03-UnrealEngine/Gameplay/Other/离线安装VisualStudio2019专业版.md
Normal file
32
03-UnrealEngine/Gameplay/Other/离线安装VisualStudio2019专业版.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 官方文档地址
|
||||
https://docs.microsoft.com/zh-cn/visualstudio/install/create-an-offline-installation-of-visual-studio?view=vs-2019
|
||||
|
||||
# 简单说明
|
||||
VisualStudio2019没有提供安装用的ios镜像文件,但官方提供了另一种离线安装方式,具体操作参看文档。这里简单说明一下就是需要通过命令行来操作。以下是我的用的案例:
|
||||
```
|
||||
vs_professional.exe --layout c:\vslayout ^
|
||||
--add Microsoft.VisualStudio.Workload.NativeDesktop ^
|
||||
--add Microsoft.VisualStudio.Component.Git ^
|
||||
--add Microsoft.VisualStudio.Component.ClassDesigner ^
|
||||
--add Microsoft.Net.Component.4.6.2.TargetingPack ^
|
||||
--add Microsoft.Net.Component.4.7.1.TargetingPack ^
|
||||
--add Microsoft.Net.Component.4.7.2.TargetingPack ^
|
||||
--add Microsoft.Net.Component.4.8.TargetingPack ^
|
||||
--includeRecommended --lang zh-CN ^
|
||||
--addProductLang en-US ^
|
||||
--addProductLang zh-CN
|
||||
```
|
||||
--layout代表了展开路径,也就是下载安装包的位置。
|
||||
--add代表添加的功能包。
|
||||
--addProductLang代表添加的语言包
|
||||
|
||||
在cmd中执行完之后,c:\vslayout就已经有一个指定功能的安装包了。只需要拷贝再执行安装包中的exe文件即可完成安装。
|
||||
|
||||
# 如何获取功能包的名字
|
||||
这个可以通过导出设置功能来获取。在VisualStudioInstall的可用选项卡中找到想要安装版本(社区版、专业版与企业版),点击更多——导出配置。选择想要的功能后,就可以在之前选择路径的地方找到.vsconfig文件。用记事本打开就可以找到勾选功能包的名字。
|
||||
|
||||
有关导出配置的文档:
|
||||
https://docs.microsoft.com/zh-cn/visualstudio/install/import-export-installation-configurations?view=vs-2019
|
||||
|
||||
# 安装的命令行参数示例
|
||||
https://docs.microsoft.com/zh-cn/visualstudio/install/command-line-parameter-examples?view=vs-2019
|
@@ -0,0 +1,393 @@
|
||||
---
|
||||
title: Ue4 c++ UProperty反射 PostEditChangeProperty
|
||||
date: 2022-12-09 13:40:28
|
||||
excerpt:
|
||||
tags: UObject
|
||||
rating: ⭐⭐⭐
|
||||
---
|
||||
## 反射系统
|
||||
https://ikrima.dev/ue4guide/engine-programming/uobject-reflection/uobject-reflection/
|
||||
|
||||
## Property类型判断
|
||||
- UStructProperty:结构体
|
||||
- UMapProperty:TMap
|
||||
- UArrayProperty:TArray
|
||||
属性判断是否是数组或是Map
|
||||
```c++
|
||||
FProperty* PropertyThatChanged = PropertyChangedEvent.Property;
|
||||
if ( PropertyThatChanged != nullptr )
|
||||
{
|
||||
if (PropertyThatChanged->IsA(FArrayProperty::StaticClass()))
|
||||
{
|
||||
FArrayProperty* ArrayProp = CastField<FArrayProperty>(PropertyThatChanged);
|
||||
FProperty* InnerProp = ArrayProp->Inner;
|
||||
if (InnerProp->IsA(FStructProperty::StaticClass()))
|
||||
{
|
||||
const FToonRampData* ToonRampData = ArrayProp->ContainerPtrToValuePtr<FToonRampData>(InnerProp, 0);
|
||||
if(ToonRampData)
|
||||
{
|
||||
}
|
||||
}
|
||||
}else if(PropertyThatChanged->IsA(FMapProperty::StaticClass()))
|
||||
{
|
||||
FArrayProperty* ArrayProp = CastField<FArrayProperty>(PropertyThatChanged);
|
||||
FProperty* InnerProp = ArrayProp->Inner;
|
||||
if (InnerProp->IsA(FStructProperty::StaticClass()))
|
||||
{
|
||||
const FToonRampData* ToonRampData = ArrayProp->ContainerPtrToValuePtr<FToonRampData>(InnerProp, 0);
|
||||
if(ToonRampData)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## 属性变化回调函数
|
||||
```c#
|
||||
virtual void EditorApplyTranslation(const FVector& DeltaTranslation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
|
||||
virtual void EditorApplyRotation(const FRotator& DeltaRotation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
|
||||
virtual void EditorApplyScale(const FVector& DeltaScale, const FVector* PivotLocation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
|
||||
virtual void PostEditMove(bool bFinished) override;
|
||||
virtual void PostEditComponentMove(bool bFinished) override;
|
||||
```
|
||||
|
||||
# 没有Struct()却可以在蓝图找到类型问题笔记
|
||||
## Traits
|
||||
大概率是这个
|
||||
```c++
|
||||
template<>
|
||||
struct TStructOpsTypeTraits<FBox2D> : public TStructOpsTypeTraitsBase2<FBox2D>
|
||||
{
|
||||
enum
|
||||
{
|
||||
WithIdenticalViaEquality = true,
|
||||
WithNoInitConstructor = true,
|
||||
WithZeroConstructor = true,
|
||||
};
|
||||
};
|
||||
IMPLEMENT_STRUCT(Box2D);
|
||||
```
|
||||
## BlueprintCompilerCppBackendValueHelper.cpp
|
||||
```c++
|
||||
bool FEmitDefaultValueHelper::SpecialStructureConstructor(const UStruct* Struct, const uint8* ValuePtr, /*out*/ FString* OutResult)
|
||||
{
|
||||
...
|
||||
if (TBaseStructure<FBox2D>::Get() == Struct)
|
||||
{
|
||||
if (OutResult)
|
||||
{
|
||||
const FBox2D* Box2D = reinterpret_cast<const FBox2D*>(ValuePtr);
|
||||
*OutResult = FString::Printf(TEXT("CreateFBox2D(FVector2D(%s, %s), FVector2D(%s, %s), %s)")
|
||||
, *FEmitHelper::FloatToString(Box2D->Min.X)
|
||||
, *FEmitHelper::FloatToString(Box2D->Min.Y)
|
||||
, *FEmitHelper::FloatToString(Box2D->Max.X)
|
||||
, *FEmitHelper::FloatToString(Box2D->Max.Y)
|
||||
, Box2D->bIsValid ? TEXT("true") : TEXT("false"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
...
|
||||
}
|
||||
struct FStructAccessHelper_StaticData
|
||||
{
|
||||
TMap<const UScriptStruct*, FString> BaseStructureAccessorsMap;
|
||||
TMap<const UScriptStruct*, bool> SupportsDirectNativeAccessMap;
|
||||
TArray<FSoftClassPath> NoExportTypesWithDirectNativeFieldAccess;
|
||||
static FStructAccessHelper_StaticData& Get()
|
||||
{
|
||||
static FStructAccessHelper_StaticData StaticInstance;
|
||||
return StaticInstance;
|
||||
}
|
||||
private:
|
||||
FStructAccessHelper_StaticData()
|
||||
{
|
||||
// These are declared in Class.h; it's more efficient to access these native struct types at runtime using the specialized template functions, so we list them here.
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FRotator>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FTransform>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FLinearColor>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FColor>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FVector>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FVector2D>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FRandomStream>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FGuid>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FTransform>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FBox2D>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFallbackStruct>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFloatRangeBound>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFloatRange>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FInt32RangeBound>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FInt32Range>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFloatInterval>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FInt32Interval>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFrameNumber>::Get());
|
||||
MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFrameTime>::Get());
|
||||
{
|
||||
// Cache the known set of noexport types that are known to be compatible with emitting native code to access fields directly.
|
||||
TArray<FString> Paths;
|
||||
GConfig->GetArray(TEXT("BlueprintNativizationSettings"), TEXT("NoExportTypesWithDirectNativeFieldAccess"), Paths, GEditorIni);
|
||||
for (FString& Path : Paths)
|
||||
{
|
||||
NoExportTypesWithDirectNativeFieldAccess.Add(FSoftClassPath(Path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 大钊的文章中有相似的代码
|
||||
https://zhuanlan.zhihu.com/p/26019216
|
||||
|
||||
Struct的收集
|
||||
对于Struct,我们先来看上篇里生成的代码:
|
||||
```c++
|
||||
static FCompiledInDeferStruct Z_CompiledInDeferStruct_UScriptStruct_FMyStruct(FMyStruct::StaticStruct, TEXT("/Script/Hello"), TEXT("MyStruct"), false, nullptr, nullptr); //延迟注册
|
||||
static struct FScriptStruct_Hello_StaticRegisterNativesFMyStruct
|
||||
{
|
||||
FScriptStruct_Hello_StaticRegisterNativesFMyStruct()
|
||||
{
|
||||
UScriptStruct::DeferCppStructOps(FName(TEXT("MyStruct")),new UScriptStruct::TCppStructOps<FMyStruct>);
|
||||
}
|
||||
} ScriptStruct_Hello_StaticRegisterNativesFMyStruct;
|
||||
```
|
||||
https://zhuanlan.zhihu.com/p/59553490
|
||||
|
||||
ICppStructOps的作用
|
||||
很多朋友在看源码的时候,可能会对UScriptStruct里定义的ICppStructOps类以及模板子类`TCppStructOps<CPPSTRUCT>`感到疑惑。其实它们是C++的一种常见的架构模式,用一个虚函数基类定义一些公共操作,再用一个具体模板子类来实现,从而既可以保存类型,又可以有公共操作接口。
|
||||
针对于UE4这里来说,ICppStructOps就定义了这个结构的一些公共操作。而探测这个C++结构的一些特性就交给了`TCppStructOps<CPPSTRUCT>`类里的`TStructOpsTypeTraits<CPPSTRUCT>`。一些C++结构的信息不能通过模板探测出来的,就需要我们手动标记提供了,所以具体的代码是:
|
||||
|
||||
```c++
|
||||
template <class CPPSTRUCT>
|
||||
struct TStructOpsTypeTraitsBase2
|
||||
{
|
||||
enum
|
||||
{
|
||||
WithZeroConstructor = false, // 0构造,内存清零后就可以了,说明这个结构的默认值就是0
|
||||
WithNoInitConstructor = false, // 有个ForceInit的参数的构造,用来专门构造出0值结构来
|
||||
WithNoDestructor = false, // 是否没有结构有自定义的析构函数, 如果没有析构的话,DestroyStruct里面就可以省略调用析构函数了。默认是有的。结构如果是pod类型,则肯定没有析构。
|
||||
WithCopy = !TIsPODType<CPPSTRUCT>::Value, // 是否结构有自定义的=赋值函数。如果没有的话,在CopyScriptStruct的时候就只需要拷贝内存就可以了
|
||||
WithIdenticalViaEquality = false, // 用==来比较结构
|
||||
WithIdentical = false, // 有一个自定义的Identical函数来专门用来比较,和WithIdenticalViaEquality互斥
|
||||
WithExportTextItem = false, // 有一个ExportTextItem函数来把结构值导出为字符串
|
||||
WithImportTextItem = false, // 有一个ImportTextItem函数把字符串导进结构值
|
||||
WithAddStructReferencedObjects = false, // 有一个AddStructReferencedObjects函数用来添加结构额外的引用对象
|
||||
WithSerializer = false, // 有一个Serialize函数用来序列化
|
||||
WithStructuredSerializer = false, // 有一个结构结构Serialize函数用来序列化
|
||||
WithPostSerialize = false, // 有一个PostSerialize回调用来在序列化后调用
|
||||
WithNetSerializer = false, // 有一个NetSerialize函数用来在网络复制中序列化
|
||||
WithNetDeltaSerializer = false, // 有一个NetDeltaSerialize函数用来在之前NetSerialize的基础上只序列化出差异来,一般用在TArray属性上进行优化
|
||||
WithSerializeFromMismatchedTag = false, // 有一个SerializeFromMismatchedTag函数用来处理属性tag未匹配到的属性值,一般是在结构进行升级后,但值还是原来的值,这个时候用来把旧值升级到新结构时使用
|
||||
WithStructuredSerializeFromMismatchedTag = false, // SerializeFromMismatchedTag的结构版本
|
||||
WithPostScriptConstruct = false,// 有一个PostScriptConstruct函数用在蓝图构造脚本后调用
|
||||
WithNetSharedSerialization = false, // 指明结构的NetSerialize函数不需要用到UPackageMap
|
||||
};
|
||||
};
|
||||
template<class CPPSTRUCT>
|
||||
struct TStructOpsTypeTraits : public TStructOpsTypeTraitsBase2<CPPSTRUCT>
|
||||
{
|
||||
};
|
||||
```
|
||||
|
||||
举个小例子,假如你看到编辑器里某个属性,想在C++里去修改它的值,结果发现它不是public的,甚至有可能连头文件都是private的,这个时候如果对类型系统结构理解不深的人可能就放弃了,但懂的人就知道可以通过这个对象遍历UProperty来查找到这个属性从而修改它。
|
||||
还有一个例子是如果你做了一个插件,调用了引擎编辑器本身的Details面板属性,但又想隐藏其中的一些字段,这个时候如果不修改引擎往往是难以办到的,但是如果知道了属性面板里的属性其实也都是一个个UProperty来的,这样你就可以通过对象路径获得这个属性,然后开启关闭它的某些Flags来达成效果。这也算是一种常规的Hack方式。
|
||||
|
||||
## 《InsideUE4》UObject(十三)类型系统-反射实战
|
||||
https://zhuanlan.zhihu.com/p/61042237
|
||||
|
||||
#### 获取类型对象
|
||||
如果想获取到程序里定义的所有的class,方便的方法是:
|
||||
```c++
|
||||
TArray<UObject*> result;
|
||||
GetObjectsOfClass(UClass::StaticClass(), result); //获取所有的class和interface
|
||||
GetObjectsOfClass(UEnum::StaticClass(), result); //获取所有的enum
|
||||
GetObjectsOfClass(UScriptStruct::StaticClass(), result); //获取所有的struct
|
||||
```
|
||||
GetObjectsOfClass是UE4已经写好的一个很方便的方法,可以获取到属于某个UClass*下面的所有对象。因此如果用UClass::StaticClass()本身,就可以获得程序里定义的所有class。值得注意的是,UE4里的接口是有一个配套的UInterface对象来存储元数据信息,它的类型也是用UClass*表示的,所以也会获得interface。根据前文,enum会生成UEnum,struct会生成UScriptStruct,所以把参数换成UEnum::StaticClass()就可以获得所有的UEnum*对象了,UScriptStruct::StaticClass()就是所有的UScriptStruct*了,最后就可以根据这些类型对象来反射获取类型信息了。
|
||||
而如果要精确的根据一个名字来查找某个类型对象,就可以用UE4里另一个方法:
|
||||
```c++
|
||||
template< class T >
|
||||
inline T* FindObject( UObject* Outer, const TCHAR* Name, bool ExactClass=false )
|
||||
{
|
||||
return (T*)StaticFindObject( T::StaticClass(), Outer, Name, ExactClass );
|
||||
}
|
||||
UClass* classObj=FindObject<UClass>(ANY_PACKAGE,"MyClass"); //获得表示MyClass的UClass*
|
||||
```
|
||||
#### 遍历字段
|
||||
在获取到了一个类型对象后,就可以用各种方式去遍历查找内部的字段了。为此,UE4提供了一个方便的迭代器`TFieldIterator<T>`,可以通过它筛选遍历字段。
|
||||
```c++
|
||||
const UStruct* structClass; //任何复合类型都可以
|
||||
//遍历属性
|
||||
for (TFieldIterator<UProperty> i(structClass); i; ++i)
|
||||
{
|
||||
UProperty* prop=*i;
|
||||
}
|
||||
//遍历函数
|
||||
for (TFieldIterator<UFunction> i(structClass); i; ++i)
|
||||
{
|
||||
UFunction* func=*i;
|
||||
//遍历函数的参数
|
||||
for (TFieldIterator<UProperty> i(func); i; ++i)
|
||||
{
|
||||
UProperty* param=*i;
|
||||
if( param->PropertyFlags & CPF_ReturnParm ) //这是返回值
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
//遍历接口
|
||||
const UClass* classObj; //只有UClass才有接口
|
||||
for (const FImplementedInterface& ii : classObj->Interfaces)
|
||||
{
|
||||
UClass* interfaceClass = ii.Class;
|
||||
}
|
||||
//遍历枚举
|
||||
const UEnum* enumClass;
|
||||
for (int i = 0; i < enumClass->NumEnums(); ++i)
|
||||
{
|
||||
FName name = enumClass->GetNameByIndex(i);
|
||||
int value = enumClass->GetValueByIndex(i);
|
||||
}
|
||||
//遍历元数据
|
||||
#if WITH_METADATA
|
||||
const UObject* obj;//可以是任何对象,但一般是UField才有值
|
||||
UMetaData* metaData = obj->GetOutermost()->GetMetaData();
|
||||
TMap<FName, FString>* keyValues = metaData->GetMapForObject(obj);
|
||||
if (keyValues != nullptr&&keyValues->Num() > 0)
|
||||
{
|
||||
for (const auto& i : *keyValues)
|
||||
{
|
||||
FName key=i.Key;
|
||||
FString value=i.Value;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
//查找属性
|
||||
UProperty* UStruct::FindPropertyByName(FName InName) const
|
||||
{
|
||||
for (UProperty* Property = PropertyLink; Property != NULL; Property = Property->PropertyLinkNext)
|
||||
{
|
||||
if (Property->GetFName() == InName)
|
||||
{
|
||||
return Property;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
//查找函数
|
||||
UFunction* UClass::FindFunctionByName(FName InName, EIncludeSuperFlag::Type IncludeSuper) const;
|
||||
```
|
||||
|
||||
#### 查看继承
|
||||
```c++
|
||||
//得到类型对象后,也可以遍历查看它的继承关系。 遍历继承链条:
|
||||
const UStruct* structClass; //结构和类
|
||||
TArray<FString> classNames;
|
||||
classNames.Add(structClass->GetName());
|
||||
UStruct* superClass = structClass->GetSuperStruct();
|
||||
while (superClass)
|
||||
{
|
||||
classNames.Add(superClass->GetName());
|
||||
superClass = superClass->GetSuperStruct();
|
||||
}
|
||||
FString str= FString::Join(classNames, TEXT("->")); //会输出MyClass->UObject
|
||||
//那反过来,如果想获得一个类下面的所有子类,可以这样:
|
||||
const UClass* classObj; //结构和类
|
||||
TArray<UClass*> result;
|
||||
GetDerivedClasses(classObj, result, false);
|
||||
//函数原型是
|
||||
void GetDerivedClasses(UClass* ClassToLookFor, TArray<UClass *>& Results, bool bRecursive);
|
||||
//那么怎么获取实现了某个接口的所有子类呢?
|
||||
TArray<UObject*> result;
|
||||
GetObjectsOfClass(UClass::StaticClass(), result);
|
||||
TArray<UClass*> classes;
|
||||
for (UObject* obj : result)
|
||||
{
|
||||
UClass* classObj = Cast<UClass>(obj);
|
||||
if (classObj->ImplementsInterface(interfaceClass))//判断实现了某个接口
|
||||
{
|
||||
classes.Add(classObj);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取设置属性值
|
||||
```c++
|
||||
template<typename ValueType>
|
||||
ValueType* UProperty::ContainerPtrToValuePtr(void* ContainerPtr, int32 ArrayIndex = 0) const
|
||||
{
|
||||
return (ValueType*)ContainerVoidPtrToValuePtrInternal(ContainerPtr, ArrayIndex);
|
||||
}
|
||||
template<typename ValueType>
|
||||
ValueType* UProperty::ContainerPtrToValuePtr(UObject* ContainerPtr, int32 ArrayIndex = 0) const
|
||||
{
|
||||
return (ValueType*)ContainerVoidPtrToValuePtrInternal(ContainerPtr, ArrayIndex);
|
||||
}
|
||||
void* UProperty::ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const
|
||||
{
|
||||
//check...
|
||||
return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
|
||||
}
|
||||
void* UProperty::ContainerUObjectPtrToValuePtrInternal(UObject* ContainerPtr, int32 ArrayIndex) const
|
||||
{
|
||||
//check...
|
||||
return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
|
||||
}
|
||||
//获取对象或结构里的属性值地址,需要自己转换成具体类型
|
||||
void* propertyValuePtr = property->ContainerPtrToValuePtr<void*>(object);
|
||||
//包含对象引用的属性可以获得对象
|
||||
UObject* subObject = objectProperty->GetObjectPropertyValue_InContainer(object);
|
||||
//也因为获取到的是存放属性值的指针地址,所以其实也就可以*propertyValuePtr=xxx;方便的设置值了。当然如果是从字符串导入设置进去,UE4也提供了两个方法来导出导入:
|
||||
//导出值
|
||||
virtual void ExportTextItem( FString& ValueStr, const void* PropertyValue, const void* DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope = NULL ) const;
|
||||
//使用
|
||||
FString outPropertyValueString;
|
||||
property->ExportTextItem(outPropertyValueString, property->ContainerPtrToValuePtr<void*>(object), nullptr, (UObject*)object, PPF_None);
|
||||
//导入值
|
||||
const TCHAR* UProperty::ImportText( const TCHAR* Buffer, void* Data, int32 PortFlags, UObject* OwnerObject, FOutputDevice* ErrorText = (FOutputDevice*)GWarn ) const;
|
||||
//使用
|
||||
FString valueStr;
|
||||
prop->ImportText(*valueStr, prop->ContainerPtrToValuePtr<void*>(obj), PPF_None, obj);
|
||||
```
|
||||
|
||||
#### 反射调用函数
|
||||
```c++
|
||||
//方法原型
|
||||
int32 UMyClass::Func(float param1);
|
||||
UFUNCTION(BlueprintCallable)
|
||||
int32 InvokeFunction(UObject* obj, FName functionName,float param1)
|
||||
{
|
||||
struct MyClass_Func_Parms //定义一个结构用来包装参数和返回值,就像在gen.cpp里那样
|
||||
{
|
||||
float param1;
|
||||
int32 ReturnValue;
|
||||
};
|
||||
UFunction* func = obj->FindFunctionChecked(functionName);
|
||||
MyClass_Func_Parms params;
|
||||
params.param1=param1;
|
||||
obj->ProcessEvent(func, ¶ms);
|
||||
return params.ReturnValue;
|
||||
}
|
||||
//使用
|
||||
int r=InvokeFunction(obj,"Func",123.f);
|
||||
```
|
||||
ProcessEvent也是UE4里事先定义好的非常方便的函数,内部会自动的处理蓝图VM的问题。当然,更底层的方法也可以是:
|
||||
```c++
|
||||
//调用1
|
||||
obj->ProcessEvent(func, ¶ms);
|
||||
//调用2
|
||||
FFrame frame(nullptr, func, ¶ms, nullptr, func->Children);
|
||||
obj->CallFunction(frame, ¶ms + func->ReturnValueOffset, func);
|
||||
//调用3
|
||||
FFrame frame(nullptr, func, ¶ms, nullptr, func->Children);
|
||||
func->Invoke(obj, frame, ¶ms + func->ReturnValueOffset);
|
||||
```
|
||||
|
||||
#### 运行时修改类型
|
||||
让我们继续扩宽一下思路,之前已经详细讲解过了各大类型对象的构造过程,最后常常都是到UE4CodeGen_Private里的调用。既然我们已经知道了它运行的逻辑,那我们也可以仿照着来啊!我们也可以在常规的类型系统注册流程执行完之后,在游戏运行的半途过程中,动态的去修改类型甚至注册类型,因为说到底UE4编辑器也就是一个特殊点的游戏而已啊!这种方式有点类似C#的emit的方式,用代码去生成代码然后再编译。这些方式理论上都是可以通的,我来提供一些思路用法,有兴趣的朋友可以自己去实现下,代码贴出来就太长了。
|
||||
1. 修改UField的MetaData信息,其实可以改变字段在编辑器中的显示信息。MetaData里有哪些字段,可以在ObjectMacros.h中自己查看。
|
||||
2. 动态修改UField的相应的各种Flags数据,比如PropertyFlags,StructFlags,ClassFlags等,可以达成在编辑器里动态改变其显示行为的效果。
|
||||
3. 动态添加删除UEnum对象里面的Names字段,就可以动态给enum添加删除枚举项了。
|
||||
4. 动态地给结构或类添加反射属性字段,就可以在蓝图内创建具有不定字段的结构了。当然前提是在结构里预留好属性存放的内存,这样UProperty的Offset才有值可指向。这么做现在想来好像也不知道能用来干嘛。
|
||||
5. 同属性一样,其实参照对了流程,也可以动态的给蓝图里暴露函数。有时候这可以达成某种加密保护的奇效。
|
||||
6. 可以动态的注册新结构,动态的构造出来相应的UScriptStruct其实就可以了。
|
||||
7. 动态注册新类其实也是可以的,只不过UClass的构造稍微要麻烦点,不过也没麻烦到哪去,有需求了就自然能照着源码里的流程自己实现一个流程出来。
|
||||
8. 再甚至,其实某种程度上的用代码动态创建蓝图节点,填充蓝图VM指令其实也是可行的。只不过想了想好像一般用不着上这种大手术。
|
BIN
03-UnrealEngine/Gameplay/UObject/Ue4Object生命周期.jpg
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/Ue4Object生命周期.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 261 KiB |
197
03-UnrealEngine/Gameplay/UObject/大钊提供的一种获取UE Private函数的方法.md
Normal file
197
03-UnrealEngine/Gameplay/UObject/大钊提供的一种获取UE Private函数的方法.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
title: 大钊提供的一种获取UE Private函数的方法
|
||||
date: 2022-12-09 14:51:47
|
||||
excerpt: 摘要
|
||||
tags:
|
||||
rating: ⭐⭐
|
||||
---
|
||||
|
||||
## Hacker.h
|
||||
```c++
|
||||
// Copyright (c) 2015 fjz13. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
#pragma once
|
||||
#include "MedusaCorePreDeclares.h"
|
||||
|
||||
MEDUSA_BEGIN;
|
||||
|
||||
namespace Hacker
|
||||
{
|
||||
//used to steal class private member
|
||||
template<typename Tag, typename Tag::type M>
|
||||
struct PrivateMemberStealer
|
||||
{
|
||||
friend typename Tag::type GetPrivate(Tag) { return M; }
|
||||
};
|
||||
}
|
||||
|
||||
MEDUSA_END;
|
||||
|
||||
#define MEDUSA_STEAL_PRIVATE_MEMBER(className,memberType,memberName) \
|
||||
namespace Medusa{namespace Hacker{\
|
||||
struct className##_##memberName \
|
||||
{\
|
||||
typedef memberType className::*type;\
|
||||
friend type GetPrivate(className##_##memberName);\
|
||||
};\
|
||||
template struct PrivateMemberStealer<className##_##memberName, &className::memberName>;\
|
||||
}}
|
||||
|
||||
#define MEDUSA_REF_PRIVATE_MEMBER(obj,className,memberName) obj->*GetPrivate(::Medusa::Hacker::className##_##memberName())
|
||||
#define MEDUSA_VAR_PRIVATE_MEMBER(var,obj,className,memberName) auto& var=obj->*(GetPrivate(::Medusa::Hacker::className##_##memberName()));
|
||||
|
||||
|
||||
#define MEDUSA_STEAL_PRIVATE_FUNCTION(className,memberName,returnType,...) \
|
||||
namespace Medusa{namespace Hacker{\
|
||||
struct className##_##memberName \
|
||||
{\
|
||||
typedef returnType (className::*type)(__VA_ARGS__);\
|
||||
friend type GetPrivate(className##_##memberName);\
|
||||
};\
|
||||
template struct PrivateMemberStealer<className##_##memberName, &className::memberName>;\
|
||||
}}
|
||||
|
||||
#define MEDUSA_REF_PRIVATE_FUNCTION(obj,className,memberName) GetPrivate(::Medusa::Hacker::className##_##memberName())
|
||||
#define MEDUSA_PRIVATE_FUNCTION_CALL(obj,className,memberName,...) {auto func=GetPrivate(::Medusa::Hacker::className##_##memberName());(obj->*func)(__VA_ARGS__);}
|
||||
```
|
||||
|
||||
## AbcInjection.h
|
||||
```c++
|
||||
#pragma once
|
||||
#include "AbcMagicPreCompiled.h"
|
||||
#include "Core/Collection/List.h"
|
||||
|
||||
namespace AbcInjection
|
||||
{
|
||||
void SetMatrixSamples(UGeometryCacheTrack* obj, const FMatrix* MatricesPtr, int32 MatricesCount, const float* SampleTimesPtr, int32 SampleTimesCount);
|
||||
void AddMatrixSample(UGeometryCacheTrack* obj, const FMatrix& Matrix, const float SampleTime);
|
||||
void ReserverMatrixSampleSize(UGeometryCacheTrack* obj, int32 size);
|
||||
void SetNumMaterials(UGeometryCacheTrack* obj, uint32 val);
|
||||
|
||||
|
||||
void ReserveSamples(UGeometryCacheTrack_FlipbookAnimation* obj,uint32 count);
|
||||
FGeometryCacheMeshData& MutableMeshSampleData(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 index);
|
||||
void SetMeshSampleTime(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 index, float time);
|
||||
|
||||
FGeometryCacheMeshData& MutableMeshData(UGeometryCacheTrack_TransformAnimation* obj);
|
||||
|
||||
void RegisterMorphTargets(USkeletalMesh* obj,const Medusa::List<UMorphTarget*>& MorphTargets);
|
||||
}
|
||||
```
|
||||
|
||||
## AbcInjection.cpp
|
||||
```c++
|
||||
#include "AbcInjection.h"
|
||||
#include "AbcMagicPreCompiled.h"
|
||||
#include "GeometryCacheTrack.h"
|
||||
#include "GeometryCacheTrackFlipbookAnimation.h"
|
||||
#include "GeometryCacheTrackTransformAnimation.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "Animation/MorphTarget.h"
|
||||
|
||||
#include "Core/Compile/Hacker.h"
|
||||
|
||||
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack, TArray<FMatrix>, MatrixSamples);
|
||||
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack, TArray<float>, MatrixSampleTimes);
|
||||
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack, uint32, NumMaterials);
|
||||
|
||||
|
||||
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack_FlipbookAnimation, TArray<FGeometryCacheMeshData>, MeshSamples);
|
||||
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack_FlipbookAnimation, TArray<float>, MeshSampleTimes);
|
||||
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack_FlipbookAnimation, uint32, NumMeshSamples);
|
||||
|
||||
|
||||
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack_TransformAnimation, FGeometryCacheMeshData, MeshData);
|
||||
|
||||
MEDUSA_STEAL_PRIVATE_MEMBER(USkeletalMesh, TArray<UMorphTarget*>, MorphTargets);
|
||||
|
||||
#ifdef ALEMBIC_CORE_419
|
||||
MEDUSA_STEAL_PRIVATE_FUNCTION(USkeletalMesh, InvalidateRenderData, void);
|
||||
#endif
|
||||
|
||||
|
||||
namespace AbcInjection
|
||||
{
|
||||
void SetMatrixSamples(UGeometryCacheTrack* obj, const FMatrix* MatricesPtr, int32 MatricesCount, const float* SampleTimesPtr, int32 SampleTimesCount)
|
||||
{
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(matrixSamples, obj, UGeometryCacheTrack, MatrixSamples);
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(matrixSampleTimes, obj, UGeometryCacheTrack, MatrixSampleTimes);
|
||||
matrixSamples.Append(MatricesPtr, MatricesCount);
|
||||
matrixSampleTimes.Append(SampleTimesPtr, SampleTimesCount);
|
||||
}
|
||||
|
||||
void AddMatrixSample(UGeometryCacheTrack* obj, const FMatrix& Matrix, const float SampleTime)
|
||||
{
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(matrixSamples, obj, UGeometryCacheTrack, MatrixSamples);
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(matrixSampleTimes, obj, UGeometryCacheTrack, MatrixSampleTimes);
|
||||
|
||||
matrixSamples.Add(Matrix);
|
||||
matrixSampleTimes.Add(SampleTime);
|
||||
}
|
||||
|
||||
void ReserverMatrixSampleSize(UGeometryCacheTrack* obj, int32 size)
|
||||
{
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(matrixSamples, obj, UGeometryCacheTrack, MatrixSamples);
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(matrixSampleTimes, obj, UGeometryCacheTrack, MatrixSampleTimes);
|
||||
matrixSamples.Reserve(size);
|
||||
matrixSampleTimes.Reserve(size);
|
||||
}
|
||||
|
||||
void SetNumMaterials(UGeometryCacheTrack* obj, uint32 val)
|
||||
{
|
||||
MEDUSA_REF_PRIVATE_MEMBER(obj, UGeometryCacheTrack, NumMaterials) = val;
|
||||
}
|
||||
|
||||
void ReserveSamples(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 count)
|
||||
{
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(meshSamples, obj, UGeometryCacheTrack_FlipbookAnimation, MeshSamples);
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(meshSampleTimes, obj, UGeometryCacheTrack_FlipbookAnimation, MeshSampleTimes);
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(numMeshSamples, obj, UGeometryCacheTrack_FlipbookAnimation, NumMeshSamples);
|
||||
|
||||
meshSamples.AddDefaulted(count);
|
||||
meshSampleTimes.AddDefaulted(count);
|
||||
numMeshSamples++;
|
||||
}
|
||||
|
||||
FGeometryCacheMeshData& MutableMeshSampleData(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 index)
|
||||
{
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(meshSamples, obj, UGeometryCacheTrack_FlipbookAnimation, MeshSamples);
|
||||
return meshSamples[index];
|
||||
}
|
||||
|
||||
void SetMeshSampleTime(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 index, float time)
|
||||
{
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(meshSampleTimes, obj, UGeometryCacheTrack_FlipbookAnimation, MeshSampleTimes);
|
||||
meshSampleTimes[index] = time;
|
||||
}
|
||||
|
||||
FGeometryCacheMeshData& MutableMeshData(UGeometryCacheTrack_TransformAnimation* obj)
|
||||
{
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(meshData, obj, UGeometryCacheTrack_TransformAnimation, MeshData);
|
||||
return meshData;
|
||||
}
|
||||
|
||||
void RegisterMorphTargets(USkeletalMesh* obj, const Medusa::List<UMorphTarget*>& MorphTargets)
|
||||
{
|
||||
MEDUSA_VAR_PRIVATE_MEMBER(morphTargets, obj, USkeletalMesh, MorphTargets);
|
||||
|
||||
for (UMorphTarget* morphTarget : MorphTargets)
|
||||
{
|
||||
morphTarget->BaseSkelMesh = obj;
|
||||
morphTarget->MarkPackageDirty();
|
||||
morphTargets.Add(morphTarget);
|
||||
}
|
||||
|
||||
obj->MarkPackageDirty();
|
||||
// need to refresh the map
|
||||
obj->InitMorphTargets();
|
||||
// invalidate render data
|
||||
|
||||
#ifdef ALEMBIC_CORE_419
|
||||
MEDUSA_PRIVATE_FUNCTION_CALL(obj, USkeletalMesh, InvalidateRenderData);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
```
|
Reference in New Issue
Block a user