This commit is contained in:
2025-08-02 12:09:34 +08:00
commit e70b01cdca
2785 changed files with 575579 additions and 0 deletions

1
03-UnrealEngine/.keep Normal file
View File

@@ -0,0 +1 @@
this file is created for keeping the folder after git.

View File

@@ -0,0 +1,34 @@
# UnrealEngine Overview
```ccard
type: folder_brief_live
imagePrefix: '08-Assets/Images/BingWallpaper/'
noteOnly: false
style: card
col: 4
```
## 卡通渲染
```dataview
TABLE file.tags AS Tags, rating AS Rating,excerpt AS Comment
FROM "03-UnrealEngine/卡通渲染"
SORT rating desc,data asc
```
## Gameplay
```dataview
TABLE file.tags AS Tags, rating AS Rating,excerpt AS Comment
FROM "03-UnrealEngine/Gameplay"
SORT rating desc,data asc
```
## Rendering
```dataview
TABLE file.tags AS Tags, rating AS Rating,excerpt AS Comment
FROM "03-UnrealEngine/Rendering"
SORT rating desc,data asc
```
## Editor
```dataview
TABLE file.tags AS Tags, rating AS Rating,excerpt AS Comment
FROM "03-UnrealEngine/Editor"
SORT rating desc,data asc
```

View File

@@ -0,0 +1,127 @@
---
title: AdvancedLocomotionV4学习笔记1——动画节点
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
本人最近在学习AdvancedLocomotionV4并尝试将其中可用部分移植到自己的项目上。在此分享一些自己的心得。
## 学习建议
首先应该从动画蓝图ALS_AnimBP看起Editor为后处理动画蓝图可以跳过。个人建议从AnimatoinGraph开始看你可以学习到状态机的层次逻辑、合理的动画图表折叠模式、IK的实现、动画融合逻辑之后的文章我会举个例子说明
其中的相关变量驱动着状态机切换与动画融合所以下一步你需要看UpdateGraphAdvancedLocomotionV4将事件与更新逻辑分成了2个Graph之后是EventGraph。这里是动画蓝图与角色类的数据沟通与变量更新逻辑都在于此。完成以上两个步骤后你就了解了这个项目的总体脉络了但还是推荐先把熟悉一下Ue4相关的动画要点。
## Anim Modifiers
一种用于修改 Animation Sequence 或者 Skeleton Asset的蓝图类。
例如,精确定位左脚或右脚踩到地面时的帧来创建自动脚部同步标记(等诸如此类的各种应用)。通过使用该信息,可以将 动画同步标记 添加到脚部骨骼处于最低点(或接触地面)时的帧。
在AdvancedLocomotionV4中有大量的同步组、AnimNotify、以及曲线纯靠手动指定费事费力。所以本人尝试制作了相应的AnimModifier将在下一篇文章中分享。相比之下AdvancedLocomotionV4的Anim Modifiers做的就十分的菜了让人怀疑是不是作者在隐藏些什么。
## Virtual Bones
有了它就无需因为要制作Ik、FK等效果而将原始美术资源返工重做。不过4.24有个限制就是无法重复添加相同父骨骼与相同目标骨骼的虚拟骨骼。
注意LHS_ik_hand_gun用在后处理类型的动画蓝图Editor。
## Anim Curve
可以在SkeletonAsset编辑器中查看所有的Curve并且进行添加。
之后就可以在AnimationSequence中添加Anim Curve。但有个尴尬的问题就是无法查看哪些AnimationSequence设置了指定Curve。没有很好的引用关系图这让学习项目的人十分头疼。总不可能为了学习而去写一个编辑器插件吧……
## Pose Blend 与 Pose Driver
这个两个东西和流程相关,如果没有特殊流程,只适合于制作表情系统或是较为简单的动画。(有大佬告诉我还可以做肌肉效果,毕竟表情也是一种肌肉效果……)
## 节点
### FABRIK
俗话说的正向动力学,关节弯曲效果。
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/IKSetups/diagram_FK.webp)
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/IKSetups/diagramIK.webp)
### 转换空间节点
LocalSpace局部空间骨骼变换坐标系为其父骨骼所在坐标系。
ComponentSpace组件空间骨骼变换坐标系为SkeletalMeshComponent所在坐标系。
使用他们的原因在于,动画蓝图默认处于 局部空间。但特定的混合节点和所有的Skeletal Controls节点都处于组件空间中。所以在使用这些节点时需要进行空间转换。
注意空间转换会带来性能损耗所以建议把Skeletal Controls节点都放在一起进行计算。
### Skeletal Controls
#### AnimDynamics
这个节点是一个小型动力学解算器。可以给SkeletalMeshComponent提供一些次要的物理效果。可以用来给锁链、头发、披风加上简单物理效果。
需要注意:
- 惯性效果是通过把骨骼转换为Box来计算的
- 不计算碰撞,但计算在物理资源中设置的约束
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/NodeReference/SkeletalControls/AnimDynamics/AnimDynamicsChain.gif)
#### Apply a Percentage of Rotation
从指定骨骼获取旋转值,并按照指定比例应用到目标骨骼上。
#### Copy Bone
将指定骨骼数据复制到另一个骨骼上。将上面的模拟效果复制到另一侧的弹药包上。
#### Observe Bone
在界面中显示指定骨骼的Translate信息。
#### Spline IK
样条 Ik解算。用于模拟类似脊椎之类的样条结构骨骼动画效果。
#### Spring Controller
弹簧控制器,用于模拟因为前一个动作所造成的反作用力效果,就好像指定处的骨骼换成了弹簧。
#### Trail Controller
轨迹控制器,可以用于类似贪吃蛇之类动画效果。
#### Look At
让指定骨骼朝向目标骨骼位置。
#### Modify Curve
修改指定名称的曲线值在Animation Sequence中曲线值可以在材质、Niagara、蓝图中获得。
#### Rigid Body
使用在物理资源中设置碰撞体数据进行物理效果模拟。
在使用此节点之前,先在骨骼大纲视图选中所有需要模拟骨骼,点击右键将其设置为 Simulated 类型。
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/NodeReference/SkeletalControls/RigidBody/RigidBody_02.webp)
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/NodeReference/SkeletalControls/RigidBody/RigidBody_11.webp)
#### Hand IK Retargeting
对应用了IK效果的骨骼进行重定向。按照给定参数向指定的FK骨骼进行偏移。
#### Two Bone IK
适用于两根骨骼的反向动力学解算节点。
#### CCDIK
4.21加入的节点。轻量级可用于驱动一段骨骼链。CCDIK提供了定义角约束的功能需要在解算中限制任意骨骼的旋转时较为实用。
#### Bone Driven Controller
通过其中一个骨骼信息来设置另一个骨骼的信息。
例如B.Localtion.x=A.Localtion.y;
https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/SkeletalMeshAnimation/NodeReference/SkeletalControls/BoneDrivenController/
### 视频推荐
Cesar SC制作了UE4 Skeletal Controls系列视频里面有简单演示过程可以帮助你理解以上节点。
https://youtu.be/FrwB6iVh_DY
## PhysicalAsset
刚体类型有用于碰撞的、也有用于模拟的用来做肚子上的肥肉效果如果模拟中出现乱弹现象可以通过删除部分重叠刚体的碰撞来解决。4.18改善了编辑器,可以更好地观察刚体、约束、骨骼之间的关系。操作也方便了。具体的可以去看官方的视频。
## 布料与碰撞
通过在PhysicalAsset中创建胶囊来实现布料碰撞布料只支持胶囊体
注意我在这个过程中遇到了碰撞失效的问题后面发现是因为根骨骼的scale值不为1造成的动画外包在导出文件时候场景单位和导出单位设置有问题造成的。
### biped骨骼与rootmotion
Biped骨骼的根骨骼会强制定义为Biped001且无法修改。所以为了匹配Ue4的RootMotion动画规则。我们需要在在Biped001根节点外面再加一条Root骨骼。
经过测试其他商城中的RootMotion动画也可以正确地重定向Biped骨骼中。
### 布料与环境碰撞
想要与环境碰撞比就必须要StaticMesh的mobility 设置为Static。
在角色的SkeletalMesh中检查cloth-Collide with Environment是否勾选。并且关闭Collide with Attached Children选项。
## 其他有用功能
自动重新导入功能,在编辑器设置-General-Loading & Saving-Auto Reimport。动画师会十分喜欢这个功能因为在DCC保存动画数据后Ue4就会自动更新不用再去点ReImport了。

View File

@@ -0,0 +1,185 @@
---
title: AdvancedLocomotionV4学习笔记2——AnimModifier
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
## 前言
在AdvancedLocomotionV4中大量使用同步组、AnimNotify、AnimationCurve。但那么多动画Asset都要手动添加我肯定是拒绝的。
![](https://pic4.zhimg.com/80/v2-5fbfce38be263cf25a8e68cc0729228b_720w.jpg)
幸亏Ue4在4.16推出了AnimModifier功能以进行自动化操作。文档地址
[Animation Modifiersdocs.unrealengine.com/en-US/Engine/Animation/AnimModifiers/index.html](https://link.zhihu.com/?target=https%3A//docs.unrealengine.com/en-US/Engine/Animation/AnimModifiers/index.html)
本人有幸找到了一篇相关Blog
[http://www.aclockworkberry.com/automated-foot-sync-markers-using-animation-modifiers-unreal-engine/www.aclockworkberry.com/automated-foot-sync-markers-using-animation-modifiers-unreal-engine/](https://link.zhihu.com/?target=http%3A//www.aclockworkberry.com/automated-foot-sync-markers-using-animation-modifiers-unreal-engine/)
并以此为基础编写了生成LocoMotion相关同步组、AnimNotify、AnimationCurve的AnimModifier主要还是因为作者写的东西不太行并且已经将其放入之前写的插件中如觉得好用请给我点赞。
[https://github.com/blueroseslol/BRPluginsgithub.com/blueroseslol/BRPlugins](https://link.zhihu.com/?target=https%3A//github.com/blueroseslol/BRPlugins)
![](https://pic2.zhimg.com/80/v2-20a332af4cb1b32284a7fa948ae5c0f9_720w.jpg)
## 使用效果与使用说明
因为图片上传有大小限制,所以我尽可能得压缩了图片,大家凑合得看看吧。
![动图封面](https://pic2.zhimg.com/v2-cfb839fdc6490532d031545d134fef59_b.jpg)
- PathFilter路径过滤在批量处理时只会处理指定路径的AnimSequence。使用前需要实现修改好对应变量的默认值
- MotionCheckDirection骨骼位移的判断轴向。
- CreateBonePositionCurve生成以Bone为名称的曲线值为对应时间的骨骼高度。
- CreateFootPositionCurve创建类似AdvancedLocomotionV4中的Feet_Position曲线。
- NormalizeCurve将BonePosition曲线归一化。
- FeetBones定义脚部骨骼一般使用foot_l与foot_roffset为骨骼判断偏移值。
- AnimNotfiyClass:指定需要添加的AnimNotify。
- StepOnValue脚着地的判断高度。
- StepNextValue脚离地的判断值。一般是1~5
- InterpModeBonePosition曲线的插值模式。
**AnimModifier生成的结果 VS AdvancedLocomotionV4原版曲线**
![](https://pic2.zhimg.com/80/v2-ad3035bb7113ea8ad84fc5be4d304b01_720w.jpg)
可以看得出 AdvancedLocomotionV4的同步组与AnimNotify略微往前。但我认为我的生成的才是正确的。
## 具体实现说明
本人已经在蓝图中做了详细的注释,所以直接去看蓝图也是没问题的。下面将介绍具体实现方式:
AnimModifier有2个需要实现的事件分别是OnApply与OnRevert指代了应用AnimModifier生成数据与撤销AnimModifier生成的数据。以下展示的是插件中OnApply事件的实现。OnRevert只包含下图的3个节点
![](https://pic3.zhimg.com/80/v2-857245a941d65fb6aaad5aa6d30cd0b2_720w.jpg)
**ShouldApply**
通过判断设置的PathFilter来判断是否处理AnimSequence。
![](https://pic2.zhimg.com/80/v2-0ed332a0c619f54b5cd670b10982d439_720w.jpg)
**RemoveAll**
移除对应的Notify、Curve、Sync轨道。
![](https://pic3.zhimg.com/80/v2-b282e2493ebcac67132993924c8e995e_720w.jpg)
## 创建所需轨道与取得所需数据
在进行完初始化后,按照设定的变量创建各种轨道:
![](https://pic4.zhimg.com/80/v2-ff1d357cb5f17c03953f8743b8df936b_720w.jpg)
按照设置的脚部骨骼进行循环生成对应的曲线轨道并且取得AnimSequence的时长之后再对AnimSequence的每一帧进行逐帧处理。
![](https://pic3.zhimg.com/80/v2-2d7c629243bd3d99ad5cc2865dcf47fe_720w.jpg)
通过GetBoneLocationRelativeToAtTime函数计算相对位移之后再根据的轴向取得偏移值一般都是Z轴方向
![](https://pic4.zhimg.com/80/v2-6dba54c66b9c7137040aba143b849d8b_720w.jpg)
## **GetBoneLocationRelativeToAtTime**
通过FindBonePathToRoot函数取得指定骨骼到根骨骼的骨骼链数组之后将各个的骨骼的Translation值相乘以得到指定骨骼与根骨骼的相对位移值。
## 数据处理
因为之后需要对生成的曲线进行归一化处理,所以在红框处,我将所取得的每帧曲线数据进行存储。
白框处先是通过计算脚部骨骼的相对位移来判断脚是踩到地还是抬起来,之后再设定同步组。
![](https://pic1.zhimg.com/80/v2-32788821fbf38b4dcd599f8de3f9f9e0_720w.jpg)
蓝框处则是在判断左右脚之后设置了对应的曲线值Feet_Position
![](https://pic1.zhimg.com/80/v2-50d5df93fea07280e467264bc3f7f49c_720w.jpg)
这里我重写了AddFloatCurvekeyWithType函数这样就可以给关键帧指定插值方式了。具体代码我放在本文最后了。
## 循环完每一帧之后
归一化曲线
![](https://pic3.zhimg.com/80/v2-17c65f535cb89e5afe4c071ecbdaa74a_720w.jpg)
添加曲线(对应骨骼名称曲线),之后将数据清零。进行下一个骨骼的循环。
![](https://pic3.zhimg.com/80/v2-691c830d589fc5b1488d21aaa066308a_720w.jpg)
## 最后处理
Feet_Position曲线的开头与结尾处没有关键帧此时在进行判断后添加。
![](https://pic4.zhimg.com/80/v2-ef30a0090925cfe664f8fd82807a5487_720w.jpg)
最后再执行FinalizeBoneAnimation。
## AnimationBlueprintLibrary扩展
AnimModifier中的添加曲线函数AddFloatCurveKey与AddFloatCurveKeys没有提供修改曲线插值类型选项所以我简单扩展了一下AnimationBlueprintLibrary具体的可以参考我的插件代码。懒得提交Pull Request了
```cpp
void UAnimBlueprintLibrary::AddFloatCurveKeysWithType(UAnimSequence* AnimationSequence, FName CurveName, const TArray<float>& Times, const TArray<float>& Values, EInterpCurveMode InterpMode)
{
if (AnimationSequence)
{
if (Times.Num() == Values.Num())
{
AddCurveKeysInternal<float, FFloatCurve>(AnimationSequence, CurveName, Times, Values, ERawCurveTrackTypes::RCT_Float, InterpMode);
}
else
{
UE_LOG(LogAnimBlueprintLibrary, Warning, TEXT("Number of Time values %i does not match the number of Values %i in AddFloatCurveKeys"), Times.Num(), Values.Num());
}
}
else
{
UE_LOG(LogAnimBlueprintLibrary, Warning, TEXT("Invalid Animation Sequence for AddFloatCurveKeys"));
}
}
template <typename DataType, typename CurveClass>
void UAnimBlueprintLibrary::AddCurveKeysInternal(UAnimSequence* AnimationSequence, FName CurveName, const TArray<float>& Times, const TArray<DataType>& KeyData, ERawCurveTrackTypes CurveType, EInterpCurveMode InterpMode)
{
checkf(Times.Num() == KeyData.Num(), TEXT("Not enough key data supplied"));
const FName ContainerName = RetrieveContainerNameForCurve(AnimationSequence, CurveName);
if (ContainerName != NAME_None)
{
// Retrieve smart name for curve
const FSmartName CurveSmartName = RetrieveSmartNameForCurve(AnimationSequence, CurveName, ContainerName);
// Retrieve the curve by name
CurveClass* Curve = static_cast<CurveClass*>(AnimationSequence->RawCurveData.GetCurveData(CurveSmartName.UID, CurveType));
if (Curve)
{
const int32 NumKeys = KeyData.Num();
for (int32 KeyIndex = 0; KeyIndex < NumKeys; ++KeyIndex)
{
//Curve->UpdateOrAddKey(, );
FKeyHandle handle= Curve->FloatCurve.UpdateOrAddKey(Times[KeyIndex], KeyData[KeyIndex]);
FRichCurveKey& InKey = Curve->FloatCurve.GetKey(handle);
InKey.InterpMode = RCIM_Linear;
InKey.TangentWeightMode = RCTWM_WeightedNone;
InKey.TangentMode = RCTM_Auto;
if (InterpMode == CIM_Constant)
{
InKey.InterpMode = RCIM_Constant;
}
else if (InterpMode == CIM_Linear)
{
InKey.InterpMode = RCIM_Linear;
}
else
{
InKey.InterpMode = RCIM_Cubic;
if (InterpMode == CIM_CurveAuto || InterpMode == CIM_CurveAutoClamped)
{
InKey.TangentMode = RCTM_Auto;
}
else if (InterpMode == CIM_CurveBreak)
{
InKey.TangentMode = RCTM_Break;
}
else if (InterpMode == CIM_CurveUser)
{
InKey.TangentMode = RCTM_User;
}
}
}
AnimationSequence->BakeTrackCurvesToRawAnimation();
}
}
}
```

View File

@@ -0,0 +1,142 @@
---
title: AdvancedLocomotionV4学习笔记3——思路整理
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
AdvancedLocomotionV4是一个不错的Ue4动画项目它的动画蓝图的层级结构与数据处理结构可以说是教科书级别不管是拿来学习还是直接拿来做项目都是不错的选择。本人通过将这个项目手动移植到自己的Demo中进行学习现写此文作为学习笔记并分享给打算学习该项目者。在大致了解这个项目的思路以后不但可以少走一些弯路提升学习速度而且可以将这个设计思想用在公司的其他项目中。
# 动画蓝图逻辑部分
首先应该从动画蓝图部分开始看起,它的位置在:
> CharacterAssets\MannequinSkeleton\ALS_AnimBP
动画蓝图分为**事件图表**与**动画图表**,这里先看**动画图表**部分。
## 动画图表
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/AnimBP_Source.png)
我认为这里的动画层级比较值得学习。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/AnimBP.png)
在这里我会简单介绍几个状态机以及层级关系至于虚拟骨骼与IK部分则会放在后面的文章中。
**针对流程不通用的动画**ALS_AnimBP只要针对地面上的运行动画Locomotion所以对于一些特殊动画需求比如攀爬系统。因为之前的骨骼控制流程与RagDoll不能共用所以还是适合制作一个子动画蓝图。至于是将子动画蓝图嵌入主动画蓝图中进行Pose融合还是在Character中直接对动画蓝图进行切换。就看大家选择了。
### 状态机与动画融合部分
AdvancedLocomotionV4采用了主Pose与细节动画叠加、多个状态机OutputPose融合的方式实现动画融合逻辑。
将动画拆分Pose与细节动画做法我能想到的好处有
1. 能更方便地调整动画融合结果参数化而且不用返回Dcc软件中调整
2. 可以通过动画节点对一些细节动画进行重用。
3. 增量式的修改,不会影响其他部分的融合结果。
当然这需要动画师掌握Maya里的动画层功能Max我不太清楚。想要了解我上述说的优点可以去看一下Locomotion Detail状态机。
AdvancedLocomotionV4里2个运动类型,站姿类型与蹲姿类型。也因此里面会有多个状态机。多个状态机OutputPose融合的方式会让一些刚学习的开发者遇到困难所以下面会对各个状态机进行大致介绍当你对每个状态机都有一个大概理解后看这个动画融合过程就轻松多了。
**BasePose**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/BasePose.png)
基础Pose控制。切换站姿与蹲姿基础Pose。
**OverlayState**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/OverlayStates.png)
通过叠加的方式切换上半身的Pose。
**BaseLayer**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/BaseLayer.png)
这里才是真正的主要动画融合。因为本人的Demo没有蹲姿状态所以蹲姿相关的融合逻辑就麻烦读者自己去研究了。
**MainMovementState**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/MainMovementStates.png)
这个状态机就和Ue4第三人称模板有点像控制着几个优先级较高的状态着落、跳跃、空中与下落。在LandMovement与Grounded状态中会引用**MainGroundedStates**这个状态机。
**MainGroundedStates**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/MainGroundedStates.png)
这里主要是翻滚、站姿势与蹲姿切换。因为本人的Demo不需要蹲姿翻滚会用Montage来做所以本人在自己的Demo把这个状态机去掉了。在Standing状态中会引用**(N) Locomotion States**状态机;在Crouching LF状态中会引用**(CLF) Locomotion States**状态机。
**(N)Lococmotion States**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CharacterRotation.png)
基础运动状态主要是运动、停止运动以及站立时的角色旋转切换。其中在Moving与Stop状态中有引用**(N)Lococmotion Details**状态机。
**(N)Lococmotion Details**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/PivotContorl.png)
运动细节修饰AdvancedLocomotionV4在这里做了重心偏移效果。这里所有的状态都引用了**N)Lococmotion Cycles**状态机。
**(N)Lococmotion Cycles**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/LocomotionCycle.png)
运动循环动画4方向跑步与走路的动画融合其中左右各有2个前后切换用动画
### 状态机制作顺序与思考
本人介绍的顺序是有外到内,高层到基础。但如果实际制作项目,其顺序应该是相反的。当然你也可以在搭建完的**运动循环动画**部分后按照AdvancedLocomotionV4把这些结构先搭好创建对应的状态机并直接引用之后就需要考虑哪些是可以共用的状态与流程在最外层与BaseLayer中最后再进行细化。
在AdvancedLocomotionV4中展示了基础运动状态、运动细节修饰、运动循环这种经典三段论可以以此为基础进行开发之后再针对需求进行更多的细化。同时还展示了如何插入不同BasePose的动画蹲姿与站姿。我认为学习与熟悉这些思路才是这个项目的真正意义。
## 事件图表
AdvancedLocomotionV4将Tick事件单独剔除作为UpdateGraph。
另外事件图表中的内容大致为BlueprintInitializeAnimation、AnimNotify、蓝图接口实现与其他自定义事件。其中BlueprintInitializeAnimation的逻辑就只是用当前Pawn给Character变量赋值以方便后续从Character获取各种变量。
**UpdateGraph**:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/AnimBPUpdateAnimation.png)
- UpdateCharacterInfo:调用角色类的2个接口函数获取驱动动画用的数据。AnimBP只通过这连个接口函数获取所需的角色数据
- UpdateAimValues使用之前获取的数据计算Aim动画所需变量。包括头部与身体Aim类似黑魂的目标锁定
- UpdateLayerValue从动画Asset中获取对应的曲线值并更新对应变量。
- UpdateFootIk使用之前获取的数据计算脚部Ik效果所需变量。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CharacterBPTick2.png)
下面就是根据不同的运动状态进行计算。
# 动画数据处理部分
动画所需的数据处理逻辑位于角色类中,它的位置在:
>Blueprints\CharacterLogic
- ALS_AnimMan_CharacterBP 一些更换SkeletonMesh之类的逻辑。
- ALS_Base_CharacterBP 动画数据处理与输入控制等其他基础逻辑。
- ALS_Player_Controller UI与摄像机CameraManager控制部分。
另外说明一下AdvancedLocomotionV4会通过表进行一些基础运动数值控制位置在
>AdvancedLocomotionV4\Data\DataTables\MovementModelTable
两个接口函数实现:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/BPI1.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/BP2.png)
换句话说如果出现动画bug也只需要从这些变量开始找问题即可。这些变量都是tick函数中进行更新下面是tick
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CharacterBPTick.png)
因为结构比较清晰,而且看函数名就知道是干什么的关系,这里我就不详细介绍了。
# 虚拟骨骼
AdvancedLocomotionV4的虚拟骨骼都是用来制作Ik效果的除了VB Curves这个骨骼我也搞明白是干什么的。因为4.24版本无法在一个骨骼上创建多个虚拟骨骼这导致了非HumanIK骨骼无法套用AdvancedLocomotionV4的IK效果。所以套用这个模板时最好使用Ue4的骨骼结构。
# Animation Insights
另外推荐4.25新推出的Animation Insights。
https://zhuanlan.zhihu.com/p/147648443
目前感觉有点难用,没有开始与结束捕获的快捷键。也不知道支不支持记录使用自动测试框架的动画结果。
# 有关项目移植
首先需要按照蓝图中InputGraph的输入事件名在项目设置中添加对应的输入事件。
之后重新定义蓝图的父类在蓝图编辑器的File-ReparentBlueprint。至于动画蓝图则可以在对应的Asset上右键Retarget AnimationBlueprint-Duplicate AnimationBlueprint And Target。
PS.我前几天才知道蓝图有一个重新设置父类的功能,之前学习的时候都是手动一个一个复制的……

View File

@@ -0,0 +1,68 @@
---
title: AdvancedLocomotionV4学习笔记4——8方向运动
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
之前研究一AdvancedLocomotionV4的8方向运动就是旋转模式中的LookingDirection与Aiming方式。AdvancedLocomotionV4没有直接使用BlendSpace对8方向动作进行融合而是手动构建了自定义的融合方式本质上是将一个4方向运动动画融合成8方向。
PS4 的血源诅咒在8方向运动上做的不是很好如果尝试做出360度锁定运动就会硌脚的现象。其原因是没有前后运动切换过度动画。这里我们看一下AdvancedLocomotionV4的实现方式
# AnimGraph部分
## 前后运动切换
首先看状态机(N)Lococmotion Cycles
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/LocomotionCycle.png)
中间有6个BlendSpace输入到Cycle Blendering动画层中而这些BlendSpace分别为对应方向的WalkPose、RunPose与Walk、Run的融合。自己制作的话记得给AnimSequence添加同步组
在Cycle Blendering动画层则会对输入的动画进行Cache Pose。并在(N)Direction State状态机中使用。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/MovementDirction.png)
上图为(N)Direction State状态机可以看出一共6个状态但其中LF、LB与RF、RB可以分别认为是分别将L与R拆分为前后两个部分。下图展示了LF与LB
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/LF.gif)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/LB.gif)
其中F与B互相切换除了被MovementDirection枚举控制外还受到Feet_Crossing与HipOrientation_Bias曲线的控制。Feet_Crossing让角色不会在两脚交叉或是两脚前后重合时进行前后切换。HipOrientation_Bias曲线我并没有找到但从字面意思猜应该是当处于指定骨盆偏移方向时限制状态切换。
具体为:
在满足MovementDirection的情况下还需要满足
XF=>XB
```
GetCurveValue(HipOrientation_Bias)>0.5 && GetCurveValue(Feet_Crossing) ==0
=True
```
XB=>XF,条件有2。
```
|GetCurveValue(HipOrientation_Bias)| < 0.5 &&
StateWeiget(MoveLB)==1 &&
GetCurveValue(Feet_Crossing) ==0
=True
```
```
GetCurveValue(HipOrientation_Bias)<-0.5 && GetCurveValue(Feet_Crossing) ==0
=True
```
有了同步组以及切换条件之后前后切换就相当自然了。当然这自然也需要角色类中的进行Rotation的平滑计算。如果旋转过快还是会出现不自然的情况。
## 状态机对应的事件
这里的每个状态还绑定了对应的AnimNotify名称为AnimNotify_Hip XX系列。在EventGraph里可以看到这些AnimNotify用于设置TrackedHipsDirection的值。
# 对角动画融合
AdvancedLocomotionV4在这个状态机中使用了对角融合方式。以下是LF的融合图看得出它是通过VelocityBlend结构体将Velocity的x、y的正数与负数拆成4个部分对F、B、LF、FR4个动画进行融合。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/AnimBlend.png)
如果看过所有状态就会发现4个动画的关系是 当前状态方向、两个临近状态方向与对角方向。这么的做的目的应该是考虑切换至紧邻状态的同时兼顾反方向运动状态。
# AnimationBlueprint中的数据计算部分
主要的数据计算位于UpdateGraphe的UpdateRotationValues中。首先调用Calculate Movement Direction。之后还会计算LYaw、FYaw、BYaw、RYaw的值。这些值之后会被用来设置YawOffset曲线。你可以在(N)Direction State状态机的各个状态中找到看到对应的使用。
## Calculate Movement Direction
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CalculateMovementDirection.png)
首先判断RotationModeVelocityDirection一直为Forward其他模式则会调用CalculateQuadrant函数通过当前的MovementDirection、V当前Velocity的旋转值与AimingRotationContorlRotation的差值以及FR、FL、BR、BL-Threshold进行计算用于确定4个方向范围4个参数的默认值为70、-70、110、-110
-70~70为Forward70~110为Right-70~-110为Left剩下的为Back。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CalculateQuadrant.png)

View File

@@ -0,0 +1,53 @@
---
title: AdvancedLocomotionV4学习笔记5——FootIK实现
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
AdvancedLocomotionV4在FootIK的实现中主要使用Transform Bone设置骨骼位置与虚拟骨骼最终使用Two Bone Ik完成效果设置。除此之外还使用曲线来控制IK与动画的过度。
# AnimGraph部分
因为本人的Demo里角色不需要拿枪所以Hand IK部分以及后处理动画蓝图中的Ik_Hand_Gun我就略过了。
动画图表中的逻辑主要集中在FootIk动画层中
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/FootIk.png)
动画逻辑分为三个部分:
1. 首先计算出FootLock_L/R_Location/Rotation并将骨骼Ik_Foot_L/R变换到对应位置。使用FootLock_L/R_Alpha变量作为融合Alpha。该操作目的应该是先确定IK效果的大致坐标。Ik_Foot_L/R是虚拟骨骼 VB IK_Foot_L/R_Offset与VB IK_Knee_Target_L/R的父骨骼
2. 再计算出FootOffset_L/R_Location、FootOffset_L/R_Rotation将骨骼Ik_Foot_L/R**偏移**到对应位置。还计算了PelvisOffset偏移了骨盆骨骼。
3. 将膝盖处的虚拟骨骼VB Ik_Knee_Target_L与Ik_Knee_Target_R以当前骨骼坐标系偏移vec3(-20,-30,0)vec3(20,30,0)使用Enable_FootIK_L/R权限值控制融合Alpha。
4. 调用TwoBoneIk实现最终的FootIK效果。EffectorTarget为虚拟骨骼VB IK_Foot_L/R_Offset作为位置数据Joint Target为Ik_Knee_Target_L/R作为旋转数据。
# UpdateGraph
上述所用到的数据皆在动画蓝图的UpdateGraph中的Update Foot IK函数计算。
## Update Foot IK
首先调用SetFootLock更新Foot Lock L/R相关数据在当前动画的当前帧中IK骨骼的位移值与旋转值
之后计算IK骨骼的位移与旋转的偏移值。最后计算骨盆IK偏移。
### SetFootLock
1. 从指定的曲线中获取浮点值假定为A
2. 判断 A>=0.99 || A<CurrentFootLockAlpha,就用浮点值A更新CurrentFootLockAlpha(ref)。这样可以实现脚在踏到地面前都不会产生Ik效果但在收回去会有一定的IK与动画融合的效果
3. 如果 CurrentFootLockAlpha>=0.99,就从指定骨骼处获取Location与Rotation来设置CurrentFootLockLocation(ref)与CurrentFootLockRotation(ref)。
4. 如果 CurrentFootLockAlpha>0(有数值)调用SetFootLockOffset函数来更新CurrentFootLockLocation(ref)与CurrentFootLockRotation(ref)。这样做是为了保证在角色在任意移动时,脚部处于都有着正确的位置。
### SetFootLockOffset
1. 分别从角色类与角色移动类中获取当前旋转值与之前的旋转值之后计算出两者相差了多少RotationDifference
2. 获取当前帧与前一帧之间相对于模型的位移的距离。(这一步没完全懂,应该是获取距离之后再进行,世界坐标系=》组件坐标系吧?)
3. 计算偏移后的坐标值并更新LocalLocation(ref)。
4. 计算偏移后的旋转值并更新RotationLocation(ref)。
### SetFootOffsets这一步很关键
在这个函数中发射了射线。
0. 从EnableFootIKCurve曲线中获取浮点值来判断是否对Offset相关变量进行清零。
1. 通过ik_foot_l/r与root骨骼计算出脚底坐标再通过预设的范围变量确定射线检测范围。如果射线有Hit到地面则对ImpactPoint、ImpactNormal进行赋值并配合预设变量FootHeight计算出脚的CurrentTargetLocation最后通过ImpactNormal计算出旋转值并赋值给TargetRotationOffset。
2. 通过CurrentTargetLocation对CurrentLocationOffset进行插值计算从而实现IK的渐变效果。
3. 通过TargetRotationOffset对CurrentRotationOffset进行插值计算从而实现IK的渐变效果。
### SetPelvisIKOffset
0. 如果有IK效果处于激活状态则开始骨盆IK计算。
1. 比较FootOffset L/R 的Z值选择往-Z轴方向偏移多的赋值给PelvisTarget。
2. 比较PelvisTarget与PelvisOffset的Z值根据情况对PelvisOffset进行插值计算从而实现IK的渐变效果。

View File

@@ -0,0 +1,9 @@
---
title: 骨骼共用功能
date: 2022-09-04 19:52:38
excerpt: 摘要
tags:
rating: ⭐
---
Compatible Skeletons
https://docs.unrealengine.com/5.0/en-US/skeletons-in-unreal-engine/#compatibleskeletons

View File

@@ -0,0 +1,34 @@
---
title: Iphone面捕与UE Cloth资料
date: 2023-04-19 14:32:09
excerpt:
tags:
rating: ⭐
---
# Apple 54 BlendShape (相关教程)
0. 制作54 BlendShape 插件
1. https://blendermarket.com/products/faceit
2. https://item.taobao.com/item.htm?spm=a21n57.1.0.0.1bed523caQSJlv&id=712178484301&ns=1&abbucket=0#detail
1. 虚拟人角色与演员表情进行匹配
1. Unreal Engine Live Link Tutorialhttps://youtu.be/L8RckB9ZRJE
2. Facial Control Righttps://www.youtube.com/watch?v=Rus-eOEv7eo
3. Calibrate Metahuman and Live Link Face the right wayhttps://www.youtube.com/watch?v=4elhEGL6iYQ
4. Live Link Face updated with built in face calibration for Virtual Productionhttps://www.youtube.com/watch?v=TEzVqzwpgE4
5. UE4 | Live Link Face | Facial Calibration Setup https://www.youtube.com/watch?v=BuUZW3rqJxM
6. How to Clean and Moddify Livelink Face Mocap in Unreal Engine 5.1https://www.youtube.com/watch?v=dvgZ4yKc4AQ
2. 增加某一BS的权重。
# 布料Kawaii、SRP、Cloth
1. SPCR Joint Dynamics
1. https://github.com/SPARK-inc/SPCRJointDynamicsUE4
2. 国人做的教学视频https://www.bilibili.com/video/BV128411p7vt/?spm_id_from=333.337.search-card.all.click&vd_source=d47c0bb42f9c72fd7d74562185cee290
3. 国人做的教学视频https://www.bilibili.com/video/BV1mg411t7oT/?spm_id_from=333.788&vd_source=d47c0bb42f9c72fd7d74562185cee290
4. 案例关卡MyProject/Content/Example
2. KawaiiPhysics
1. https://github.com/pafuhana1213/KawaiiPhysics/
2. 英文文档建议看日文https://github.com/pafuhana1213/KawaiiPhysics/blob/master/README_en.md
3. 案例关卡Content/KawaiiPhysicsSample/KawaiiPhysicsSample
3. Chaos Cloth
1. https://docs.unrealengine.com/5.1/en-US/clothing-tool-in-unreal-engine---properties-reference/
2. ContentExample Cloth

View File

@@ -0,0 +1,12 @@
---
title: UE5 MotionMatching 教程
date: 2023-07-07 10:56:07
excerpt: 摘要
tags:
rating: ⭐
---
# 文档
MotionWarping 官方文档https://docs.unrealengine.com/5.2/en-US/motion-warping-in-unreal-engine/
# 油管视频
MotionMatchinghttps://www.youtube.com/watch?v=rLEWEQjTOb8

View File

@@ -0,0 +1,21 @@
---
title: BVH相关
date: 2024-01-07 12:33:20
excerpt:
tags:
rating: ⭐
---
# BVH库
- c++
- https://github.com/BartekkPL/bvh-parser
- Maya
- https://github.com/jhoolmans/mayaImporterBVH
```python
import bvh_importer
bvh_importer.BVHImporterDialog()
```
# Blender FBX => BVH
>因为Blender轴向与FBX不同。所以通过Blender导入FBX再导出BHV到Maya结果是错的。
**Blender需要导出YZX轴向BVH导入Maya后结果才正确。** 之后还需要手动将根骨骼改成root。

View File

@@ -0,0 +1,242 @@
---
title: Qt & FBX相关笔记
date: 2023-11-06 11:45:14
excerpt:
tags:
rating: ⭐
---
# FBX SDK setup for Qt
https://help.autodesk.com/view/FBX/2020/ENU/?guid=FBX_Developer_Help_welcome_to_the_fbx_sdk_html
使用之前建议**仔细查看文档**。FBX Sdk有三种库分别为动态链接库(dll + lib)、静态链接库(/MD)、静态链接库(/MT)。
- 动态链接库使用Qt的导入库功能导入动态库之后只需要再添加一行`DEFINES += FBXSDK_SHARED`即可。例如:
```c++
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
DEFINES += FBXSDK_SHARED
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
Common/Common.cpp \
main.cpp \
mainwindow.cpp
HEADERS += \
Common/Common.h \
mainwindow.h
FORMS += \
mainwindow.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
win32:CONFIG(release, debug|release): LIBS += -L$$PWD/FBXSdk/lib/vs2022/x64/release/ -llibfbxsdk
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/FBXSdk/lib/vs2022/x64/debug/ -llibfbxsdk
INCLUDEPATH += $$PWD/FBXSdk/lib/vs2022/x64/debug
DEPENDPATH += $$PWD/FBXSdk/lib/vs2022/x64/debug
INCLUDEPATH += $$PWD/FBXSdk/include
```
- 静态链接库(/MD)使用Qt的导入库功能导入静态库之后
- 静态链接库(/MT)使用Qt的导入库功能导入静态库之后
## Qt相关设置添加
https://blog.csdn.net/libaineu2004/article/details/105718847
Qt在pro中设置运行时库MT、MTd、MD、MDd重点关注QMAKE_CFLAGS
多线程调试Dll (/MDd) 对应的是MD_DynamicDebug
多线程Dll (/MD) 对应的是MD_DynamicRelease
多线程(/MT) 对应的是MD_StaticRelease
多线程(/MTd)对应的是MD_StaticDebug
##  /NODEFAULTLIB:library
```cpp
win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../LIBRARYNAME/Lib/ -lLIBRARY /NODEFAULTLIB:library
```
## Ubuntu安装需要额外安装Qt
```cpp
sudo apt install --reinstall libgl-dev
```
新版本fbxsdk需要自己link xml2库所以需要手动安装一下
```bash
sudo apt-get install libxml2-dev
sudo apt-get install libxml2
```
之后再qt的pro中添加`LIBS += -lxml2`即可。
之后运行程序会提示# (error while loading shared libraries: libfbxsdk.so: cannot open shared object file: No)
这是因为Linux的动态链接库寻找方式与windows不同所致我们需要添加lib搜索路径
使用VSCode打开/etc/ld.so.conf 输入libfbxsdk.so的路径再运行ldconfig即可解决问题
```
1> vim /etc/ld.so.conf //在新的一行中加入库文件所在目录
2> /usr/lib //添加的目录路径
3> /usr/local/lib //添加的目录路径
3> ldconfig //更新/etc/ld.so.cache文件
```
参考
https://blog.csdn.net/sinat_14854721/article/details/111191139
另一种思路就是使用linuxdeployqt打成类似MAC的APP包
- https://github.com/probonopd/linuxdeployqt
- https://blog.csdn.net/zyhse/article/details/106381937
# FBX结构
![](https://help.autodesk.com/cloudhelp/2020/ENU/FBX-Developer-Help/images/scene_org.png)
FBX SDK场景图是通过`FbxScene`类抽象出来的。场景被组织为节点层次结构 ( `FbxNode`)。场景的根节点通过 访问`FbxScene::GetRootNode()`。场景元素(例如网格、灯光或相机)是通过将`FbxNode`与 的子类组合来定义的`FbxNodeAttribute`。
使用Ansii方式输出就可以使用vscode直接查看内部数据结构。
- GlobalMaterial世界坐标矩阵
- LocalMaterial相对空间矩阵
## 动画
- 动画堆栈 ( `FbxAnimStack`):动画数据最顶层容器,每一个**AnimStack**可以视为一个镜头,包含多个**FbxAnimLayer**。
- 动画层 ( `FbxAnimLayer`):每个**AnimLayer**中可以包含多个**FbxAnimCurve**。
- 动画曲线节点 ( `FbxAnimCurveNode`)
- 动画曲线 ( `FbxAnimCurve`) 动画以曲线的方式存在。可以针对骨骼的Location、Rotation分别添加曲线。
- 动画曲线关键点 ( `FbxAnimCurveKey`):动画关键帧。
每个动画数据都存在对应的节点的属性中Maya中可以给Shape的属性K帧可以通过类似`lAnimCurve = pNode->LclTranslation.GetCurve(pAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y);`的方式取得动画曲线。
UE导出的FBX相关属性为
添加动画关键帧的方式
```c++
FbxAnimCurve* lCurve = lRoot->LclRotation.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true);
if (lCurve)
{
lCurve->KeyModifyBegin();
lTime.SetSecondDouble(0.0);
//添加时间
lKeyIndex = lCurve->KeyAdd(lTime);
//添加数值与插值模式
lCurve->KeySetValue(lKeyIndex, 0.0);
lCurve->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic);
lTime.SetSecondDouble(1.0);
lKeyIndex = lCurve->KeyAdd(lTime);
lCurve->KeySetValue(lKeyIndex, 45.0);
lCurve->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic);
lTime.SetSecondDouble(2.0);
lKeyIndex = lCurve->KeyAdd(lTime);
lCurve->KeySetValue(lKeyIndex, -45.0);
lCurve->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic);
lTime.SetSecondDouble(3.0);
lKeyIndex = lCurve->KeyAdd(lTime);
lCurve->KeySetValue(lKeyIndex, 0.0);
lCurve->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic);
lCurve->KeyModifyEnd();
}
```
### BindPose
遍历所有节点并对取得GlobalTransform矩阵最后添加到Pose里。
```c++
// Now create a bind pose with the link list
if (lClusteredFbxNodes.GetCount())
{
// A pose must be named. Arbitrarily use the name of the patch node.
FbxPose* lPose = FbxPose::Create(pScene,pPatch->GetName());
// default pose type is rest pose, so we need to set the type as bind pose
lPose->SetIsBindPose(true);
for (i=0; i<lClusteredFbxNodes.GetCount(); i++)
{
FbxNode* lKFbxNode = lClusteredFbxNodes.GetAt(i);
FbxMatrix lBindMatrix = lKFbxNode->EvaluateGlobalTransform();
lPose->Add(lKFbxNode, lBindMatrix);
}
// Add the pose to the scene
pScene->AddPose(lPose);
}
```
### ResetPose
```c++
void StoreRestPose(FbxScene* pScene, FbxNode* pSkeletonRoot)
{
// This example show an arbitrary rest pose assignment.
// This rest pose will set the bone rotation to the same value
// as time 1 second in the first stack of animation, but the
// position of the bone will be set elsewhere in the scene.
FbxString lNodeName;
FbxNode* lKFbxNode;
FbxMatrix lTransformMatrix;
FbxVector4 lT,lR,lS(1.0, 1.0, 1.0);
// Create the rest pose
FbxPose* lPose = FbxPose::Create(pScene,"A Bind Pose");
// Set the skeleton root node to the global position (10, 10, 10)
// and global rotation of 45deg along the Z axis.
lT.Set(10.0, 10.0, 10.0);
lR.Set( 0.0, 0.0, 45.0);
lTransformMatrix.SetTRS(lT, lR, lS);
// 添加Root骨骼矩阵到Pose中
lKFbxNode = pSkeletonRoot;
lPose->Add(lKFbxNode, lTransformMatrix, false /*GlobalMatrix*/);
// Set the lLimbNode1 node to the local position of (0, 40, 0)
// and local rotation of -90deg along the Z axis. This show that
// you can mix local and global coordinates in a rest pose.
lT.Set(0.0, 40.0, 0.0);
lR.Set(0.0, 0.0, -90.0);
lTransformMatrix.SetTRS(lT, lR, lS);
// 添加第二个骨骼节点到Pose中
lKFbxNode = lKFbxNode->GetChild(0);
lPose->Add(lKFbxNode, lTransformMatrix, true /*LocalMatrix*/);
// Set the lLimbNode2 node to the local position of (0, 40, 0)
// and local rotation of 45deg along the Z axis.
lT.Set(0.0, 40.0, 0.0);
lR.Set(0.0, 0.0, 45.0);
lTransformMatrix.SetTRS(lT, lR, lS);
// Add the skeleton second node to the pose
lKFbxNode = lKFbxNode->GetChild(0);
lNodeName = lKFbxNode->GetName();
lPose->Add(lKFbxNode, lTransformMatrix, true /*LocalMatrix*/);
// Now add the pose to the scene
pScene->AddPose(lPose);
}
```
## GlobalSettings
场景的轴系统、系统单位、环境照明和时间设置可以通过`FbxScene::GetGlobalSettings()`获取FbxGlobalSettings来进行定义。
## FBXStream & 共享内存
FBXStream案例:https://github.com/hamedsabri/FbxStream
之前考虑通过共享内存来实现文件传递以此来规避IO瓶颈这里可以考虑参考FBXStream。本质是通过文字流使得 文件 <=> string。
这样就可以通过共享内存进行数据传递了。
让张柏林试试对一个文件使用Java进行字符串流化。
文档地址:
- https://help.autodesk.com/view/FBX/2020/ENU/?guid=FBX_Developer_Help_cpp_ref_class_fbx_stream_html
- https://help.autodesk.com/view/FBX/2020/ENU/?guid=FBX_Developer_Help_cpp_ref_class_fbx_importer_html

View File

@@ -0,0 +1,9 @@
---
title: UE5动态修改动画蓝图方案只有链接
date: 2024-02-18 11:00:58
excerpt:
tags:
rating: ⭐
---
- https://forums.unrealengine.com/t/dynamic-sub-anim-instance-node/75720
- https://docs.unrealengine.com/5.3/zh-CN/using-animation-blueprint-linking-in-unreal-engine/

View File

@@ -0,0 +1,163 @@
---
title: UE5动画重定向核心逻辑笔记
date: 2023-08-18 18:08:12
excerpt:
tags:
rating: ⭐
---
# 前言
最近研究过了一下UE的重定向逻辑所以写点笔记作为记录。IK部分没有去看
- FIKRetargetBatchOperation::RunRetarget()编辑器调用的重定向函数。主要是复制对应需要重定向资产并且进行重定向之后通知UI。
- FIKRetargetBatchOperation::RetargetAssets():重定向资产逻辑。
- 复制所有曲线轨道(不含数值)。
- 设置Skeleton与PreviewMesh资产。
- 提前调用重定向资产的PostEditChange(),以避免编辑后钩子函数被调用,产生依赖顺序问题。
- ConvertAnimation()
- 替换动画蓝图并且编译。
# ConvertAnimation()
一开始是一些初始化逻辑主要的是调用UIKRetargetProcessor的Initialize()。里面初始化了
- SourceSkeleton & TargetSkeleton用于重定向计算用的数据载体存储了骨骼链、骨骼、当前重定向Pose等数据。
- RootRetargeter根骨骼重定向器。
- ChainPairsIK、ChainPairsFK用于FK与IK的骨骼链数据。
- UIKRigProcessorIK重定向处理器。
- 所有FIKRigGoal
从FRetargetSkeleton& SourceSkeleton & FTargetSkeleton& TargetSkeleton获取对应参数来进行之后的动画资产重定向循环循环逻辑如下
- 获取动画帧数,并且重新构建骨骼动画数据轨道。
- 取得速度曲线。
- 之后对每一帧Pose进行重定向主要是的逻辑是
- 取得当前帧Pose并将其转化WorldSpace。
- UIKRetargetProcessor Processor->RunRetargeter()
- 将重定向完的结果转化成LocalSpace并且将每个骨骼的数据放入对应骨骼动画数据轨道。
- 调用IAnimationDataController的AddBoneTrack() & SetBoneTrackKeys()给资产设置上关键帧。
# RunRetargeter()
此为核心重定向逻辑分为RunRootRetarget()、RunFKRetarget()、RunIKRetarget()、RunPoleVectorMatching()。
重定向数据会直接修改TargetSkeleton的Pose数据。Root与FK重定向执行完之后会调用UpdateGlobalTransformsBelowBone()更新后续骨骼的WorldSpace Transform。
## RunRootRetarget()
FRootRetargeter::EncodePose()
取得输入的根骨骼Transform数据并给`FRootSource Source`赋值。
FRootRetargeter::DecodePose()
```c++
FVector Position;
{
// 关键InitialTransform 为重定向Pose计算出的数值通过比值计算出Target的Current数值
const FVector RetargetedPosition = Source.CurrentPositionNormalized * Target.InitialHeight;
// 根据RetargetSetting中设置的BlendToSourceWeights与BlendToSource与SourcePosition进行混合。
Position = FMath::Lerp(RetargetedPosition, Source.CurrentPosition, Settings.BlendToSource*Settings.BlendToSourceWeights);
// 应用vertical / horizontal的缩放
FVector ScaledRetargetedPosition = Position;
ScaledRetargetedPosition.Z *= Settings.ScaleVertical;
const FVector HorizontalOffset = (ScaledRetargetedPosition - Target.InitialPosition) * FVector(Settings.ScaleHorizontal, Settings.ScaleHorizontal, 1.0f);
Position = Target.InitialPosition + HorizontalOffset;
// 应用RetargetSetting中Position偏移。
Position += Settings.TranslationOffset;
// blend with alpha
Position = FMath::Lerp(Target.InitialPosition, Position, Settings.TranslationAlpha);
// 记录偏差
Target.RootTranslationDelta = Position - RetargetedPosition;
}
FQuat Rotation;
{
// 计算Source的旋转偏差值
const FQuat RotationDelta = Source.CurrentRotation * Source.InitialRotation.Inverse();
// 将偏移加到Target上
const FQuat RetargetedRotation = RotationDelta * Target.InitialRotation;
// 将RetargetSetting上的旋转偏差值加上。
Rotation = RetargetedRotation * Settings.RotationOffset.Quaternion();
Rotation = FQuat::FastLerp(Target.InitialRotation, Rotation, Settings.RotationAlpha);
Rotation.Normalize();
// 记录Target的旋转偏差值
Target.RootRotationDelta = RetargetedRotation * Target.InitialRotation.Inverse();
}
// 将数据应用到根骨骼上
FTransform& TargetRootTransform = OutTargetGlobalPose[Target.BoneIndex];
TargetRootTransform.SetTranslation(Position);
TargetRootTransform.SetRotation(Rotation);
```
## RunFKRetarget()
遍历所有骨骼链并执行FChainEncoderFK::EncodePose()与FChainDecoderFK::DecodePose()。
FChainEncoderFK::EncodePose()
1. 遍历所有骨骼复制SourceGlobalPose到**CurrentGlobalTransforms**
2. Resize CurrentLocalTransforms
3. FillTransformsWithLocalSpaceOfChain() 转换成LocalSpace数据。
1. 遍历所有骨骼链取得对应骨骼的Index以及父骨骼Index。跳过根骨骼
2. 计算相对Transform后放入对应ChainIndex的OutLocalTransforms中。
4. 根据ChainParentBoneIndex将父骨骼Transform赋予给ChainParentCurrentGlobalTransform。
FChainDecoderFK::DecodePose()
1. 更新IntermediateParentIndices的所有Index的父骨骼WorldSpace Transform。用来更新重定向后哪些不属于骨骼链的骨骼的WorldSpace Transform。在InitializeBoneChainPairs() -> InitializeIntermediateParentIndices()中初始化)
2. 从根骨骼开始重定向以保证最后结果不会发生偏差。计算了Source/Target InitialDelta、Target 根骨骼 Transform后将两者相乘最后应用给所有Source骨骼链。
3. 如果禁用FK重定向则返回当前WorldSpace Transform并且return。
4. 计算Source & Target 骨骼链开始Index。
5. 开始遍历所有骨骼。
1. 取得Target骨骼Index以及InitialTransform。
2. 根据设置的FK的旋转模式执行对应逻辑默认为插值模式也就是他实现了骨骼链的不同数量骨骼间的重定向匹配。计算SourceCurrentTransform与SourceInitialTransform。
1. 使用Target的Params骨骼长度百分比来插值计算出Source的Transform。参考[[#插值计算]]。
2. Params的计算在FChainFK::CalculateBoneParameters(),可以看得出就是根据**当前骨骼长度/总长度**计算百分比最后加入Params。该函数的调用堆栈为UIKRetargetProcessor::Initialize() => UIKRetargetProcessor::InitializeBoneChainPairs() => FRetargetChainPairFK::Initialize() => FChainFK::Initialize()
3. 计算旋转值SourceCurrentRotation、SourceInitialRotation => RotationDelta TargetInitialRotation => **OutRotation**。
4. 计算当前TargetBone的父骨骼 ParentGlobalTransform。
5. 根绝设置的FK TranslationMode计算出 **OutPosition**。
6. 计算OutScale = SourceCurrentScale + (TargetInitialScale - SourceInitialScale),计算出**OutScale**。
7. 对应index的`CurrentGlobalTransforms[ChainIndex]`/`InOutGlobalPose[BoneIndex]`赋值。
6. 进行一些后处理计算,默认状态下跳过。
## 插值计算
```c++
FTransform FChainDecoderFK::GetTransformAtParam(
const TArray<FTransform>& Transforms,
const TArray<float>& InParams,
const float& Param) const
{
if (InParams.Num() == 1)
{
return Transforms[0];
}
if (Param < KINDA_SMALL_NUMBER)
{
return Transforms[0];
}
if (Param > 1.0f - KINDA_SMALL_NUMBER)
{
return Transforms.Last();
}
for (int32 ChainIndex=1; ChainIndex<InParams.Num(); ++ChainIndex)
{
const float CurrentParam = InParams[ChainIndex];
if (CurrentParam <= Param)
{
continue;
}
//关键在这
const float PrevParam = InParams[ChainIndex-1];
const float PercentBetweenParams = (Param - PrevParam) / (CurrentParam - PrevParam);
const FTransform& Prev = Transforms[ChainIndex-1];
const FTransform& Next = Transforms[ChainIndex];
const FVector Position = FMath::Lerp(Prev.GetTranslation(), Next.GetTranslation(), PercentBetweenParams);
const FQuat Rotation = FQuat::FastLerp(Prev.GetRotation(), Next.GetRotation(), PercentBetweenParams).GetNormalized();
const FVector Scale = FMath::Lerp(Prev.GetScale3D(), Next.GetScale3D(), PercentBetweenParams);
return FTransform(Rotation,Position, Scale);
}
checkNoEntry();
return FTransform::Identity;
}
```

View File

@@ -0,0 +1,376 @@
---
title: UE5商城动画重定向插件笔记
date: 2023-09-05 12:02:29
excerpt:
tags:
- AnimationRetargeting
rating: ⭐
---
# MixamoAnimationRetargeting
主要逻辑位于FMixamoSkeletonRetargeter
- UE4MannequinToMixamo_BoneNamesMapping
- UE4MannequinToMixamo_ChainNamesMapping
- UE5MannequinToMixamo_BoneNamesMapping
- UE5MannequinToMixamo_ChainNamesMapping
重定向逻辑位于FMixamoSkeletonRetargeter::Retarget()
## 生成TPose
```c++
static const TArray<FName> Mixamo_PreserveComponentSpacePose_BoneNames = {
"Head",
"LeftToeBase",
"RightToeBase"
#ifdef MAR_UPPERARMS_PRESERVECS_EXPERIMENTAL_ENABLE_
,"RightShoulder"
,"RightArm" ,"LeftShoulder" ,"LeftArm"#endif
};
static const TArray<TPair<FName, FName>> Mixamo_ParentChildBoneNamesToBypassOneChildConstraint = {
{"LeftUpLeg", "LeftLeg"},
{"LeftLeg", "LeftFoot"},
{"LeftFoot", "LeftToeBase"},
{"LeftToeBase", "LeftToe_End"},
{"RightUpLeg", "RightLeg"},
{"RightLeg", "RightFoot"},
{"RightFoot", "RightToeBase"},
{"RightToeBase", "RightToe_End"},
{"Hips", "Spine"}, // Heuristic to try to align better the part.
{"Spine", "Spine1"},
{"Spine1", "Spine2"},
{"Spine2", "Neck"}, // Heuristic to try to align better the part.
{"Neck", "Head"},
{"Head", "HeadTop_End"},
{"LeftShoulder", "LeftArm"},
{"LeftArm", "LeftForeArm"},
{"LeftForeArm", "LeftHand"},
{"LeftHand", "LeftHandMiddle1"}, // Heuristic to try to align better the part.
{"LeftHandIndex1", "LeftHandIndex2"},
{"LeftHandIndex2", "LeftHandIndex3"},
{"LeftHandIndex3", "LeftHandIndex4"},
{"LeftHandMiddle1", "LeftHandMiddle2"},
{"LeftHandMiddle2", "LeftHandMiddle3"},
{"LeftHandMiddle3", "LeftHandMiddle4"},
{"LeftHandPinky1", "LeftHandPinky2"},
{"LeftHandPinky2", "LeftHandPinky3"},
{"LeftHandPinky3", "LeftHandPinky4"},
{"LeftHandRing1", "LeftHandRing2"},
{"LeftHandRing2", "LeftHandRing3"},
{"LeftHandRing3", "LeftHandRing4"},
{"LeftHandThumb1", "LeftHandThumb2"},
{"LeftHandThumb2", "LeftHandThumb3"},
{"LeftHandThumb3", "LeftHandThumb4"},
{"RightShoulder", "RightArm"},
{"RightArm", "RightForeArm"},
{"RightForeArm", "RightHand"},
{"RightHand", "RightHandMiddle1"}, // Heuristic to try to align better the part.
{"RightHandIndex1", "RightHandIndex2"},
{"RightHandIndex2", "RightHandIndex3"},
{"RightHandIndex3", "RightHandIndex4"},
{"RightHandMiddle1", "RightHandMiddle2"},
{"RightHandMiddle2", "RightHandMiddle3"},
{"RightHandMiddle3", "RightHandMiddle4"},
{"RightHandPinky1", "RightHandPinky2"},
{"RightHandPinky2", "RightHandPinky3"},
{"RightHandPinky3", "RightHandPinky4"},
{"RightHandRing1", "RightHandRing2"},
{"RightHandRing2", "RightHandRing3"},
{"RightHandRing3", "RightHandRing4"},
{"RightHandThumb1", "RightHandThumb2"},
{"RightHandThumb2", "RightHandThumb3"},
{"RightHandThumb3", "RightHandThumb4"}
};
RetargetBasePose(
SkeletalMeshes,
ReferenceSkeleton,
Mixamo_PreserveComponentSpacePose_BoneNames,
UEMannequinToMixamo_BoneNamesMapping.GetInverseMapper(),
Mixamo_ParentChildBoneNamesToBypassOneChildConstraint,
/*bApplyPoseToRetargetBasePose=*/true,
UIKRetargeterController::GetController(IKRetargeter_UEMannequinToMixamo)
);
```
## 判断骨骼结构是否符合要求
```c++
bool FMixamoSkeletonRetargeter::IsMixamoSkeleton(const USkeleton * Skeleton) const
{
// We consider a Skeleton "coming from Mixamo" if it has at least X% of the expected bones.
const float MINIMUM_MATCHING_PERCENTAGE = .75f;
// Convert the array of expected bone names (TODO: cache it...).
TArray<FName> BoneNames;
UE4MannequinToMixamo_BoneNamesMapping.GetDestination(BoneNames);
// Look for and count the known Mixamo bones (see comments on IndexLastCheckedMixamoBone and UEMannequinToMixamo_BonesMapping).
constexpr int32 NumBones = (IndexLastCheckedMixamoBone + 1) / 2;
BoneNames.SetNum(NumBones);
FSkeletonMatcher SkeletonMatcher(BoneNames, MINIMUM_MATCHING_PERCENTAGE);
return SkeletonMatcher.IsMatching(Skeleton);
}
bool FSkeletonMatcher::IsMatching(const USkeleton* Skeleton) const
{
// No Skeleton, No matching...
if (Skeleton == nullptr)
{ return false;
}
const int32 NumExpectedBones = BoneNames.Num();
int32 nMatchingBones = 0;
const FReferenceSkeleton & SkeletonRefSkeleton = Skeleton->GetReferenceSkeleton();
for (int32 i = 0; i < NumExpectedBones; ++i)
{ const int32 BoneIndex = SkeletonRefSkeleton.FindBoneIndex(BoneNames[i]);
if (BoneIndex != INDEX_NONE)
{ ++nMatchingBones;
} } const float MatchedPercentage = float(nMatchingBones) / float(NumExpectedBones);
return MatchedPercentage >= MinimumMatchingPerc;
}
```
enum class ETargetSkeletonType
{
ST_UNKNOWN = 0,
ST_UE4_MANNEQUIN,
ST_UE5_MANNEQUIN,
ST_SIZE
};
static const char* const kUE4MannequinToMixamo_BoneNamesMapping[] = {
// UE Mannequin bone name MIXAMO bone name
"root", "root",
"pelvis", "Hips",
"spine_01", "Spine",
"spine_02", "Spine1",
"spine_03", "Spine2",
"neck_01", "Neck",
"head", "head",
"clavicle_l", "LeftShoulder",
"upperarm_l", "LeftArm",
"lowerarm_l", "LeftForeArm",
"hand_l", "LeftHand",
"clavicle_r", "RightShoulder",
"upperarm_r", "RightArm",
"lowerarm_r", "RightForeArm",
"hand_r", "RightHand",
"thigh_l", "LeftUpLeg",
"calf_l", "LeftLeg",
"foot_l", "LeftFoot",
"ball_l", "LeftToeBase",
"thigh_r", "RightUpLeg",
"calf_r", "RightLeg",
"foot_r", "RightFoot",
"ball_r", "RightToeBase",
// From here, ignored to determine if a skeleton is from Mixamo.
// From here, ignored to determine if a skeleton is from UE Mannequin. "index_01_l", "LeftHandIndex1",
"index_02_l", "LeftHandIndex2",
"index_03_l", "LeftHandIndex3",
"middle_01_l", "LeftHandMiddle1",
"middle_02_l", "LeftHandMiddle2",
"middle_03_l", "LeftHandMiddle3",
"pinky_01_l", "LeftHandPinky1",
"pinky_02_l", "LeftHandPinky2",
"pinky_03_l", "LeftHandPinky3",
"ring_01_l", "LeftHandRing1",
"ring_02_l", "LeftHandRing2",
"ring_03_l", "LeftHandRing3",
"thumb_01_l", "LeftHandThumb1",
"thumb_02_l", "LeftHandThumb2",
"thumb_03_l", "LeftHandThumb3",
"index_01_r", "RightHandIndex1",
"index_02_r", "RightHandIndex2",
"index_03_r", "RightHandIndex3",
"middle_01_r", "RightHandMiddle1",
"middle_02_r", "RightHandMiddle2",
"middle_03_r", "RightHandMiddle3",
"pinky_01_r", "RightHandPinky1",
"pinky_02_r", "RightHandPinky2",
"pinky_03_r", "RightHandPinky3",
"ring_01_r", "RightHandRing1",
"ring_02_r", "RightHandRing2",
"ring_03_r", "RightHandRing3",
"thumb_01_r", "RightHandThumb1",
"thumb_02_r", "RightHandThumb2",
"thumb_03_r", "RightHandThumb3",
// Un-mapped bones (at the moment). Here for reference.
//"lowerarm_twist_01_l", nullptr, //"upperarm_twist_01_l", nullptr, //"lowerarm_twist_01_r", nullptr, //"upperarm_twist_01_r", nullptr, //"calf_twist_01_l", nullptr, //"thigh_twist_01_l", nullptr, //"calf_twist_01_r", nullptr, //"thigh_twist_01_r", nullptr, //"ik_foot_root", nullptr, //"ik_foot_l", nullptr, //"ik_foot_r", nullptr, //"ik_hand_root", nullptr, //"ik_hand_gun", nullptr, //"ik_hand_l", nullptr, //"ik_hand_r", nullptr,};
# EasyPose
主要的逻辑都是在MatchPose()中作者还是编写几个辅助函数以及IKRigRetargetChainDetail结构体。
定义srcRtgSkeleton、tgtRtgSkeleton之外还定义了orgTgtRtgSkeleton主要是为了在最后结算最终的Offset。UE的重定向Pose数据使用分离的方式存储Maya也是这样但UE输出的FBX却不是分为RefPose与RetargetPoseOffset。
- 初始化数据
- 初始化IKRigRetargetChainDetail srcIkRigRtgChainDetail & IKRigRetargetChainDetail tgtIkRigRtgChainDetail结构体以用于之后的计算
- 遍历骨骼链
- 根据当前Source & Target ID计算当前长度的百分比如果TargetID > SourceID则**srcId++**,并且跳过当前循环。(主要是考虑到骨骼链内骨骼数量不对等的情况)
- 取得当前SourceBoneLocation以及SourceNextBoneLocation来计算Direction
- 重新生成tgtRtgSkeleton、orgTgtRtgSkeleton的重定向Pose**WorldSpace**
- **之后是核心MatchPose逻辑**
- 计算Direction与TargetBoneDirection的旋转偏移转成**LocalSpace**之后应用到tgtRtgSkeleton的对应Target骨骼上。
- 在X 或者 Y轴对齐的情况下 计算Source ID与Target ID 对应骨骼的旋转Offset转成**LocalSpace**之后应用到tgtRtgSkeleton的对应Target骨骼上。
- 计算Direction与TargetBoneDirection的旋转Offset转成**LocalSpace**之后应用到tgtRtgSkeleton的对应Target骨骼上。以上上步相同
- 使用tgtRtgSkeleton、orgTgtRtgSkeleton计算最终的旋转Offset。
- 使用最终FinalyOffset 应用到 Target的IKRig 的 FIKRetargetPose。
- **tgtId++**
以下是完整的代码:
```c++
int32 UAutomationUtilityLibrary::MatchPose(UIKRetargeter *iKRetargeter) {
int totalRotationsCount = 0;
if(iKRetargeter == nullptr) return 0;
auto Target = ERetargetSourceOrTarget::Target;
auto Source = ERetargetSourceOrTarget::Source;
UIKRetargeterController *ikRtgCtrl = UIKRetargeterController::GetController(iKRetargeter);
if(ikRtgCtrl == nullptr) return 0;
const UIKRigDefinition* SourceIKRig = iKRetargeter->GetSourceIKRig();
const UIKRigDefinition* TargetIKRig = iKRetargeter->GetTargetIKRig();
const TArray<FBoneChain>& srcRtgChains = SourceIKRig->GetRetargetChains();
const TArray<FBoneChain>& tgtRtgChains = TargetIKRig->GetRetargetChains();
const FName& srcRtgRootName = SourceIKRig->GetRetargetRoot();
const FName& tgtRtgRootName = TargetIKRig->GetRetargetRoot();
FRetargetSkeleton srcRtgSkeleton;
FRetargetSkeleton tgtRtgSkeleton, orgTgtRtgSkeleton;
auto tgtRtgPoseName = iKRetargeter->GetCurrentRetargetPoseName(ERetargetSourceOrTarget::Target);
FIKRetargetPose const *tgtRtgPose = iKRetargeter->GetCurrentRetargetPose(ERetargetSourceOrTarget::Target);
srcRtgSkeleton.Initialize(
ikRtgCtrl->GetPreviewMesh(Source),
srcRtgChains,
iKRetargeter->GetCurrentRetargetPoseName(ERetargetSourceOrTarget::Source),
iKRetargeter->GetCurrentRetargetPose(ERetargetSourceOrTarget::Source),
srcRtgRootName);
tgtRtgSkeleton.Initialize(
ikRtgCtrl->GetPreviewMesh(Target),
tgtRtgChains,
tgtRtgPoseName,
iKRetargeter->GetCurrentRetargetPose(ERetargetSourceOrTarget::Target),
tgtRtgRootName);
orgTgtRtgSkeleton.Initialize(
ikRtgCtrl->GetPreviewMesh(Target),
tgtRtgChains,
tgtRtgPoseName,
iKRetargeter->GetCurrentRetargetPose(ERetargetSourceOrTarget::Target),
tgtRtgRootName);
UIKRigProcessor* SourceIKRigProcessor=NewObject<UIKRigProcessor>();
SourceIKRigProcessor->Initialize(SourceIKRig,SourceIKRig->GetPreviewMesh());
UIKRigProcessor* TargetIKRigProcessor=NewObject<UIKRigProcessor>();
TargetIKRigProcessor->Initialize(TargetIKRig,TargetIKRig->GetPreviewMesh());
const FIKRigSkeleton& srcRigSkeleton = SourceIKRigProcessor->GetSkeleton();
const FIKRigSkeleton& tgtRigSkeleton = TargetIKRigProcessor->GetSkeleton();
for(TObjectPtr<URetargetChainSettings> chainSettings : iKRetargeter->GetAllChainSettings()) {
if(chainSettings == nullptr) continue;
const FBoneChain *srcBoneChain = SourceIKRig->GetRetargetChainByName(chainSettings->SourceChain);
const FBoneChain *tgtBoneChain = TargetIKRig->GetRetargetChainByName(chainSettings->TargetChain);
if(srcBoneChain == nullptr || tgtBoneChain == nullptr) continue;
auto chainRoot = FName(TEXT("Root"));
if(chainSettings->SourceChain == chainRoot || chainSettings->TargetChain == chainRoot) {
continue;
}
IKRigRetargetChainDetail srcIkRigRtgChainDetail{srcBoneChain, srcRigSkeleton, srcRtgSkeleton};
IKRigRetargetChainDetail tgtIkRigRtgChainDetail{tgtBoneChain, tgtRigSkeleton, tgtRtgSkeleton};
if(srcIkRigRtgChainDetail.ikRigRetargetMap.IsEmpty() || tgtIkRigRtgChainDetail.ikRigRetargetMap.IsEmpty()) {
//UE_LOG(AutomationFunctionLibrary, Log, TEXT("Chain Empty: %s: %d, %s: %d"), *srcIkRigRtgChainDetail.boneChain->ChainName.ToString(), srcIkRigRtgChainDetail.ikRigRetargetMap.Num(), *tgtIkRigRtgChainDetail.boneChain->ChainName.ToString(), tgtIkRigRtgChainDetail.ikRigRetargetMap.Num());
continue;
}
auto srcWatermarkLoc = srcRtgSkeleton.RetargetGlobalPose[srcIkRigRtgChainDetail.ikRigRetargetMap[0].retargetId].GetTranslation();
int rotationsCount = 0;
for(int32 srcId=0,tgtId=0; srcId<srcIkRigRtgChainDetail.ikRigRetargetMap.Num()-1 && tgtId<tgtIkRigRtgChainDetail.ikRigRetargetMap.Num()-1;) {
auto [srcIkRigId, srcRtgId, srcLenToNext, srcTotalLenUntilCurrent] = srcIkRigRtgChainDetail.ikRigRetargetMap[srcId];
auto [tgtIkRigId, tgtRtgId, tgtLenToNext, tgtTotalLenUntilCurrent] = tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId];
float srcTotalLenToNextPercent = (srcTotalLenUntilCurrent + srcLenToNext) / srcIkRigRtgChainDetail.totalLength;
float tgtTotalLenToNextPercent = (tgtTotalLenUntilCurrent + tgtLenToNext) / tgtIkRigRtgChainDetail.totalLength;
if(tgtTotalLenToNextPercent > srcTotalLenToNextPercent) {
srcId++;
continue;
} else {
auto tgtRtgBoneName = tgtRigSkeleton.BoneNames[tgtIkRigId];
auto diffLenPercentToNext = 1 - ((srcTotalLenToNextPercent - tgtTotalLenToNextPercent) * srcIkRigRtgChainDetail.totalLength / srcLenToNext);
auto srcCurrentBoneLoc = srcRtgSkeleton.RetargetGlobalPose[srcIkRigRtgChainDetail.ikRigRetargetMap[srcId].retargetId].GetTranslation();
auto srcNextBoneLoc = srcRtgSkeleton.RetargetGlobalPose[srcIkRigRtgChainDetail.ikRigRetargetMap[srcId+1].retargetId].GetTranslation();
auto nextPoint = (srcNextBoneLoc - srcCurrentBoneLoc) * (diffLenPercentToNext) + srcCurrentBoneLoc;
auto direction = (nextPoint - srcWatermarkLoc).GetSafeNormal();
srcWatermarkLoc = nextPoint;
//ResetRetargetPose
ikRtgCtrl->ResetRetargetPose(tgtRtgPoseName, TArray<FName>{tgtRtgBoneName}, Target);
tgtRtgSkeleton.GenerateRetargetPose(tgtRtgPoseName, tgtRtgPose, tgtRtgRootName);
orgTgtRtgSkeleton.GenerateRetargetPose(tgtRtgPoseName, tgtRtgPose, tgtRtgRootName);
//根据骨骼链中当前SourceBone与下一个SourceBone计算出Direction世界坐标
//GetBoneDirectionOffset 调用GetBoneDirectionOffset()计算出旋转偏移再转换成LocalSpace旋转再设置给骨骼。
auto const &tgtGlobalTransform = tgtRtgSkeleton.RetargetGlobalPose[tgtRtgId];
auto rotOffset = GetBoneDirectionOffset(tgtRtgSkeleton, tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId].retargetId, tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId+1].retargetId, direction);
rotOffset = GetBoneSpaceOffset(rotOffset, tgtGlobalTransform);
//UE_LOG(AutomationFunctionLibrary, VeryVerbose, TEXT("Matching direction: %s, %s"), *tgtRtgBoneName.ToString(), *rotOffset.ToString());
if(NotEmptyOrIdentity(rotOffset)) {
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Matched direction: %s"), *tgtRtgBoneName.ToString());
ApplyRotationOffset(rotOffset, tgtRtgId, tgtRtgSkeleton);
}
//在X 或者 Y轴对齐的情况下 计算Source与Target 对应骨骼的旋转偏差在转化成LocalSpace后再应用
rotOffset = GetXYRotationOffsetIfAligned(tgtRtgSkeleton.RetargetGlobalPose[tgtRtgId].GetRotation(), srcRtgSkeleton.RetargetGlobalPose[srcRtgId].GetRotation());
rotOffset = GetBoneSpaceOffset(rotOffset, tgtGlobalTransform);
//UE_LOG(AutomationFunctionLibrary, VeryVerbose, TEXT("Matching XY: %s, %s"), *tgtRtgBoneName.ToString(), *rotOffset.ToString());
if(NotEmptyOrIdentity(rotOffset)) {
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Matched XY: %s"), *tgtRtgBoneName.ToString());
ApplyRotationOffset(rotOffset, tgtRtgId, tgtRtgSkeleton);
}
//与第一步相同
rotOffset = GetBoneDirectionOffset(tgtRtgSkeleton, tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId].retargetId, tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId+1].retargetId, direction);
rotOffset = GetBoneSpaceOffset(rotOffset, tgtGlobalTransform);
//UE_LOG(AutomationFunctionLibrary, VeryVerbose, TEXT("Matching direction 2nd: %s, %s"), *tgtRtgBoneName.ToString(), *rotOffset.ToString());
if(NotEmptyOrIdentity(rotOffset)) {
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Matched direction 2nd: %s"), *tgtRtgBoneName.ToString());
ApplyRotationOffset(rotOffset, tgtRtgId, tgtRtgSkeleton);
}
//计算最终的旋转offset
auto finalOffset = GetRotationDifference(orgTgtRtgSkeleton.RetargetLocalPose[tgtRtgId].GetRotation(), tgtRtgSkeleton.RetargetLocalPose[tgtRtgId].GetRotation());
if(NotEmptyOrIdentity(finalOffset)) {
rotationsCount++;
ikRtgCtrl->SetRotationOffsetForRetargetPoseBone(tgtRtgBoneName, finalOffset, Target);
} tgtId++;
}
}
auto [srcIkRigId, srcRtgId, srcLenToNext, srcTotalLenUntilCurrent] = srcIkRigRtgChainDetail.ikRigRetargetMap.Last();
auto [tgtIkRigId, tgtRtgId, tgtLenToNext, tgtTotalLenUntilCurrent] = tgtIkRigRtgChainDetail.ikRigRetargetMap.Last();
auto tgtRtgBoneName = tgtRigSkeleton.BoneNames[tgtIkRigId];
auto rotOffset = GetRotationOffsetIfAligned(tgtRtgSkeleton.RetargetGlobalPose[tgtRtgId].GetRotation(), srcRtgSkeleton.RetargetGlobalPose[srcRtgId].GetRotation());
//UE_LOG(AutomationFunctionLibrary, VeryVerbose, TEXT("Matching rotation: %s, %s"), *tgtRtgBoneName.ToString(), *rotOffset.ToString());
if(NotEmptyOrIdentity(rotOffset)) {
rotationsCount++;
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Matched rotation: %s"), *tgtRtgBoneName.ToString());
ikRtgCtrl->SetRotationOffsetForRetargetPoseBone(tgtRtgBoneName, ikRtgCtrl->GetRotationOffsetForRetargetPoseBone(tgtRtgBoneName, Target) * rotOffset, Target);
} if(rotationsCount > 0) {
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Chain: %s - %s: %d rotations"), *chainSettings->SourceChain.ToString(), *chainSettings->TargetChain.ToString(), rotationsCount);
totalRotationsCount += rotationsCount;
} } /*
if(totalRotationsCount > 0) { UE_LOG(AutomationFunctionLibrary, Log, TEXT("Asset \"%s\": made %d adjustments with pose: \"%s\"."), *iKRetargeter->GetName(), totalRotationsCount, *ikRtgCtrl->GetCurrentRetargetPoseName(Target).ToString()); } else { UE_LOG(AutomationFunctionLibrary, Log, TEXT("Asset \"%s\": pose \"%s\" fit already!"), *iKRetargeter->GetName(), *ikRtgCtrl->GetCurrentRetargetPoseName(Target).ToString()); } */ // ikRtgCtrl->SetRotationOffsetForRetargetPoseBone(MeshRefSkeleton.GetBoneName(EditBoneIndex), Q, ERetargetSourceOrTarget::Target);
return totalRotationsCount;
}
```

View File

@@ -0,0 +1,39 @@
## 文档与视频
https://docs.unrealengine.com/zh-CN/Platforms/AR/HandheldAR/FaceARSample/index.html
https://www.youtube.com/watch?v=AIHoDo7Y4_g
## 前提条件
1. 这个APP没有上架APPStore现在已经上架了所以需要大家自己下载FaceARSample工程并打包。因为你至少需要拥有一台MAC想生成IPA文件则需要购买开发者证书。
2. 需要在光线充足的环境中录制。
3. 对应的角色的面部需要有对应的51个morph具体的可以参考文档。
4. 你必须有一台iPhone X以上的iphone。
## 大致步骤
1. 第一次运行需要打开菜单点击Calibration Mode进行校准。眼睛、帽子会影响校准
2. 如果使用LiveLink将数据传回PC那么就需要在之前点击FaceTracingMap2中的角色再点击Calibrate In Editor进行校准。
3. 使用Sequencer提供的录制功能进行录制。录制结果为Animation Sequence。之后就可以导出FBX了。
### 安装APP
如果你没有MAC或者不想花100刀购买开发者证书你可以选择使用低版本IOS的iphone(大致是2019年9月前的版本)再配合Cydia Impactor工具安装别人打包的IPA需要7天重新安装一次。当然也可以选择越狱的iphone。
本人因为手机已经升级过系统了,所以无法进行下一步测试。
## 其他
本人还查到了其他解决方案,隧在此分享。
### FaceCap与AdvancedSkeleton
FaceCap是AppStore上一款收费的APP可以免费录制3s动画进行测试。使用FaceCap录制面部动画从手机中将生成的fbx文件拷贝的电脑上。之后再通过AdvancedSkeleton插件映射到绑定角色上。
具体操作可以查看AdvancedSkeleton出品的教程
https://www.youtube.com/watch?v=ouf8jDMsXwE
但是这个流程有个问题就是文件需要拷来拷取十分不便。并且App无法像官方工程那样进行定制以提高制作效率。
### FX-Facial-Flow
于是另一个问题就产生了制作的51个morph。
我之前一直以为制作morph只需要让动画师移动一下模型的顶点就可以搞定了。但在和一个动画师朋友沟通发现这个过程需要一个专业模型师与动画师通力合作才能完成。同时制作的morph无法直接在其他模型上使用。
在这里他推荐了FX-Facial-Flow我也大致看了一下说明。应该是使用opencv以及一些深度学习技术。感觉可以提高制作效率。
因为价格不贵,同时还附带教程,所以推荐给大家(本人没有用过):
https://www.aboutcg.org/courseDetails/666/introduce

View File

@@ -0,0 +1,145 @@
---
title: 动画蓝图修型相关
date: 2023-01-12 17:03:24
excerpt:
tags:
rating: ⭐
---
## DAWA数字人
主资产(全裸身体模型)使用`dawa_Skeleton_AnimBlueprint`动画蓝图后处理蓝图用于在编辑器中预览大部分曲线用于控制材质效果EPIC的数字人技术其他衣服使用CopyPose将主资产的动画结果拷贝到模型的骨骼上。
PS.
1. 调试时需要将后处理动画蓝图清除。
2. 动画蓝图的tick部分那么多节点完全可以使用c++以提高性能。
### 流程
1. 表情使用苹果ARKit驱动表情。此外LiveLink Face 也可以用于驱动一些曲线来控制头部旋转但需要使用EvaluateLiveLinkFrame节点。本项目没有使用。
2. AnimLayer biaoqing主要用于计算与更新曲线数值来实现一些材质/(Morph Target/Blend Shape)修型效果。该项目的动画资产并没有设置曲线值,所以这个层大部分逻辑都在空跑。
3. Kawaii节点用于制作乳摇。
4. SpringController 系列节点应该是没用作用的。
5. AnimLayer xiuxing一堆PoseDriver节点与2个上臂骨骼偏移。主要是通过PoseDriver进行修型。
6. AnimLayer bonedriver使用骨骼的一些数值对骨骼数值进行修改。
7. AnimLayer Shoes根据高跟鞋的形状调整脚部骨骼形变。该项目对脚部骨骼调整了旋转。
## Pose Driver
UE使用**Pose Driver**动画节点来实现修型,主要是通过几种类型的数据来驱动**Pose Targets**Morph Target/Blend Shape或者Curves。其主要参数是
- **源骨骼Source Bone** :由哪个骨骼进行驱动?
- **驱动源Drive Source**:根据骨骼的哪个数据进行数据插值?有**Rotation/Location**。
- **仅驱动骨骼Only Drive Bones**Pose Driver节点仅修改阵列中指定的骨骼Source Bone对应的修型Twist骨骼
1. 相当于过滤器相关的形变数据K在动画中位于PoseAsset。
- **姿势目标Pose Targets**指定的用于驱动的Pose集。
1. 着重需要设置好每个Pose的立面角。注意避免万向锁的产生。
2. 每个立面角最好保证有较小的重叠,以保证插值的正确性。
- **驱动输出Drive Output**:选择驱动输出的是**Curves/Poses**。
- **RBFParams**:插值参数。
### 小灰人的修型方案
主要分为**Arm_Left/Right**与**Leg_Left/Right**。修型的方案基本查看骨骼与MorphTarget的名字就可以看出来。
- Arm
- Clavicle
- clavicle_l/r_fwd_30
- clavicle_l/r_down_20
- clavicle_l/r_back_30
- clavicle_l/r_up_40
- Upperarm
- upperarm_l/r_back_45
- upperarm_l/r_out_55
- upperarm_l/r_out110_twist_in_70
- lowerarm_l/r_in_35
- upperarm_l/r_out_85
- upperarm_l/r_fwd_110
- upperarm_l/r_in_10
- upperarm_l/r_out110_twist_out_70
- upperarm_l/r_out_twist_a
- upperarm_l/r_fwd_15
- upperarm_l/r_out_15
- upperarm_l/r_back_10
- upperarm_l/r_out110
- upperarm_l/r_fwd_twist_in_a
- upperarm_l/r_fwd_90
- Lowerarm
- lowerarm_l/r_in_110
- lowerarm_l/r_out_35
- lowerarm_l/r_in_50
- lowerarm_l/r_in_75
- lowerarm_l/r_out_10
- lowerarm_l/r_in_90
- lowerarm_l/r_in_10
- Hand
- hand_l/r_down_90
- hand_l/r_up_20
- hand_l/r_up_90
- Leg
- Calf
- calf_l/r_back_50
- calf_l/r_back_120
- calf_l/r_back_90
- calf_l/r_back_150
- Thigh
- thigh_l/r_bck_10
- thigh_l/r_fwd_10
- thigh_l/r_in_45_out_90
- thigh_l/r_fwd_90
- thigh_l/r_bck_90
- thigh_l/r_bck_50
- thigh_l/r_out_110
- thigh_l/r_out_10
- thigh_l/r_fwd_45
- thigh_l/r_in_50
- thigh_l/r_fwd_110
- thigh_l/r_out_55
- thigh_l/r_out_85
- Foot
- foot_l/r_down_60
- foot_l/r_up_35
### 其他
#### Maya中的类似操作
在Maya中通过**Pose Editor**可以通过指定Pose+Morph实现动捕修型。
#### 使用Pose曲线驱动的程序动画
UE曲线驱动的动画https://docs.unrealengine.com/4.27/zh-CN/AnimatingObjects/SkeletalMeshAnimation/AnimHowTo/CurveDrivenAnimation/
1. 通过AnimSequence资产创建PoseAsset。
2. 使用PoseAsset->CreateAsset->CreateAnimation->ReferencePose创建一个只有参考Pose的空AnimSequence资产。
3. 通过添加Variables Curve对指定的Pose添加曲线来制作曲线驱动动画。
## 厄风女神Echo
角色蓝图没什么东西主要的逻辑位于Echo_AnimBP
- Echo_AnimLayerInterface被Echo_AnimBP引用。但里面没有东西。
- Echo_LiveLink_AnimBP演示基础的LinkLive用法。
后处理动画蓝图使用了Echo_PostProcess_AnimBP。
### Echo_AnimBP
简化简洁的AdvancedLocomotionV4逻辑。动画蓝图层级为
- Locomotion
- MainState
- FullyBodyLayer(Echo_AnimLayerInterface)
- AnimMontageSlot
- Inertialization过滤曲线
- ControlRig
### Echo_PostProcess_AnimBP
1. 将C Biped开头的修型曲线归零。
2. 使用PoseDriver进行修型。
1. neck_02
2. upperarm_l
3. lowerarm_l
4. calf_l
5. hand_l
6. upperarm_r
7. lowerarm_r
8. calf_r
9. hand_r
## 表情
- https://zhuanlan.zhihu.com/p/113396468
- https://arkit-face-blendshapes.com/
# UE5.3新添加的功能 PoseDriverConnect
- https://www.unrealengine.com/marketplace/zh-CN/product/pose-driver-connect
- https://www.unrealengine.com/zh-CN/blog/create-more-realistic-animation-in-less-time-with-pose-driver-connect
主要的功能是通过PoseDriver来驱动其他次级PoseDriver来此来得到更加好的修形效果。

View File

@@ -0,0 +1,96 @@
---
title: 外包过程中的动画重定向以及蒙皮调整经验
date: 2022-08-24 09:52:19
excerpt:
tags:
rating: ⭐
---
## 前言
本人最近给自己的Demo外包了若干角色动画也遇到了一些坑。所以这里给大家分享一下相关经验避免大家再次采坑尤其是那些缺乏经验、精力的独立游戏制作者。当然本人对3DMax完全不熟悉大部分信息都是本人与外包大哥沟通过程中产生的个人认知如有错误还请指正谢谢。
PS.本人因为布料碰撞问题导致重新导入6次角色包括绘制布料、设置碰撞与编写动画蓝图。真是太痛苦了
## 如果角色需要使用Ue4的布料需要额外确认的事情
1. 使用的是否是3DMax。
2. 场景单位问题:角色大小是否正常。
3. 骨骼问题根骨骼的缩放是否为1、旋转是否为0与布料碰撞有关骨盆骨骼也需要检查一下防止因为fbx导出轴向问题导致骨盆骨骼旋转。换句话说就是在拿到绑定后的角色时需要马上使用Ue4进行角色布料碰撞测试。
当然蒙皮与动画肯定是要检查的,一般靠谱的外包都不会犯这些错,所以这里我也不说了(主要是我也不太懂)。这里主要是我个人认为需要注意的问题:
第一点和第二点可以合起来说:因为3DMax的默认单位是英寸在绑定时有可能会出现因为导入模型时的单位不正确而导致角色缩小问题。至少本人遇到了这个问题整整郁闷了1个月。个人认为关键还是需要搞清楚max或者fbx文件的单位、max的系统单位以及输出单位。调整单位的方法有以下3个
## 单位问题
**设置Max场景单位**
Max导出文件单位不正确的主要原因在于文件本身的单位不正确。Ue4的默认单位为厘米而Max默认为英寸。所以我们可以通过调整单位来解决这个问题大致步骤如下 1. Cutomize-UnitsSetup进度单位设置命令。 2. 选择SystemUnitSetup。 3. 将单位设置为厘米。 4. 此时打开单位为英寸的max文件就会提示Units Mismatch,选择Rescale The File Objects To System Unit Scale。 5. 之后导出Fbx注意将导出单位改成厘米。
**场景缩放**
对于蒙皮文件,这个操作可能会产生问题。操作步骤如下: 1. 在Utilities栏中找到RescaleWorldUnits并打开。 2. 设置缩放因子后,点击确定。
**UnrealEngine导入设置**
在Ue4导入选项中有个单位缩放你可以直接在这里设置导入缩放比例。在没有Max源文件只有fbx文件的情况下只能选择这种方法。
> 注意如果你的角色要使用布料系统那么以上方法都会让布料的碰撞失效。经过试验如果根骨骼的缩放不为1以及旋转不为0就会出现碰撞失效的情况。因为这些方法本质就是缩放根骨骼。
一种简单的解决方法就是直接设置角色类骨骼模型的缩放。不过本人因为强迫症所以不推荐这种方法。具体的解决方法请参照**Maya蒙皮传递**这一节。这里我先说一下2个软件的骨骼系统之后再细说解决方法。
## 骨骼类型
据我所知3DMax有CS(biped)与CAT骨骼系统虽然它也有普通的骨骼但是因为角色动画库基本都是以这些骨骼系统为基础所以基本不会使用。
我找的外包大哥用的是CS(biped)。CS有点不爽的地方就是会都出一个Biped节点。
至于CAT结构和Ue4的HumanIK结构相似但外包大哥说这个不太稳定所以没有使用。
对应到Maya则是HumanIK工具结构和Ue4的HumanIK结构相似多出一节脚趾骨当然我目前没有和专门外包Maya动画的公司沟通过不太确定他们是不是使用HumanIK来做角色动画库的。如果公司打算专门给Ue4做动画可以考虑使用Ue4的动画插件。
这些骨骼系统对于动画其实并没有影响但如果你是一个和我一样的拿着自己的工资做Demo的傻逼个人开发者**因为资金有限无法外包所有的动画或是需要使用商场资源制作原型亦或是想套用AdvancedLocomotionSystemV的动画系统。那么我推荐你使用Ue4的标准骨骼进行绑定并使用重定向系统将其他骨骼系统的动画重定向到标准骨骼上。**
理论上动画师还是会使用动画库对应的骨骼系统再绑定一个角色,因为动画需要根据角色模型进行调整。当然绑定的精度不用那么高了。
## 动画重定向
因为本人使用的是Maya所以这里以Maya为主。首先需要制作用于重定向的标准的TPose就是把角色摆成一个十字架可以参考HumanIK工具的标准骨骼。其原因是因为TPose容易统一方便对齐。重定向Pose将会直接影响重定向的质量。所以我推荐在Maya中制作另一个原因在于Ue4的重定向工具无法根据现有骨骼动画处理IK骨骼动画。下面我将简单介绍Ue4与Maya中的重定向流程
**UE4**在Maya中制作完TPose后给骨骼K一帧动画并导出。在Ue4中导入后在得到的AnimationSequence上右键Create-Create PoseAsset之后就可以在RetargetManager左下的ManageRetargetBasePose界面中点击ModifyPose指定创建的TPose。
根据重定向源与目标不同还可以分为这里的非UE4骨骼动画以CS骨骼动画为准
CS=》Mannequin与Mannequin=》CS按照对应骨骼结构在RetargetManager的SetUpRig中选择HumanIK模板并设置对应骨骼即可。如果两个绑定角色的身高相同root骨骼可以置空。如果不同则需要在给CS骨骼再添加一根root骨骼。重定向过程中可能会出现手臂、腿部不正确的问题这可能是twist骨骼位移的问题需要将部分twist骨骼需要置空。
CS=》CS这种情况可能涉及到其他非标准骨骼所以这里需要在重定向源骨骼Asset上右键Create-Create Rig再按照对应骨骼结构填入。
**Maya**选择Rigging模式点击Skeleton-HumanIK运行。之后再点击CreateCharacterDefinition创建角色定义就可以指定对应骨骼了。
你可以手动指定也可以套用官方模板前提是你的骨骼没有改过名称模板有CS、CAT与HIK。大致步骤如下
[https://www.youtube.com/watch?v=ms1wGBjykA8www.youtube.com/watch?v=ms1wGBjykA8](https://link.zhihu.com/?target=https%3A//www.youtube.com/watch%3Fv%3Dms1wGBjykA8)
如果你的骨骼改过名称了就需要手动指定了。如果你的角色骨骼没有处于TPose状态HumanIK界面中的大臂与手会显示为黄色所以遇到这种情况就需要调整当前骨骼为TPose状态。
在设置完重定向源与目标角色后:
1. 点击CreateControlRig添加控制之后角色会被锁住如果要再次修改则需点击边上的锁按钮。
2. 导入重定向源的动画。
3. 在HumanIK的Character中选择目标角色Source选择为重定向源角色此时重定向源的动画数据就已经重定向目标角色上了。
4. 如果你的目标角色为UE4标准骨骼此时就需要将IK骨骼P设置父子关系到对应的控制器上。 应该是将IK骨骼和对应的骨骼建立父子约束先选爹再选儿子这一步应该在设置TPose之前进行。
5. 选中骨骼、模型与控制器,并且导出动画。(记住需要点击烘焙动画选项)
## Maya蒙皮传递
大致操作如下:
1. 复制原有蒙皮文件作为新的蒙皮文件。在删除历史、断开连接后,调整骨骼与模型大小,并且冻结缩放与旋转属性。
2. 将原有蒙皮文件的骨骼进行批量重命名。
3. 将原有蒙皮文件导入新的蒙皮文件中。因为之后需要将模型合并所以需要确定UV通道是否正确如果你像我那样扣了UE4角色的眼睛的话
4. 将原有模型与调整过大小的模型分别进行合并之后切换到Rigging模型运行skin-BakeDeformationToSkinWeights。
5. 在大纲视图中选中对应项按住鼠标中键拖拽到对应栏中以完成指定。指定完选项点击Apply。再等一把王者顺风局的时间蒙皮的就传递好了。
## Maya批量命名脚本
如果你的重定向源骨骼名称与目标相同这就需要进行批量改名了。这是网上找来的脚本对于骨骼需要按住Shift并且点击+,展开所有骨骼节点,再选中批量添加后缀。
```text
from maya.cmds import *
sel = ls(sl=True)
numSel = len(sel)
for i in range(0,numSel,1):
origName = sel[i]
#print origName
rename(origName,origName+'_old')
```
## 结语
本文写的比较仓促如有不明白的地方欢迎交流。Maya中的重定向工作可以通过Python编写插件来实现批量重定向动画本人将会在Demo完成后尝试编写这个插件。

View File

@@ -0,0 +1,79 @@
---
title: 实时重定向与骨骼动画共用相关
date: 2022-09-25 21:20:07
excerpt:
tags:
rating: ⭐
---
## IK Rig
本质上是分离了以前版本动画蓝图中的IK解算逻辑。
大致步骤为:
1. 创建IK Goal与Solver。
2. 选中IK Goal并在想要的解算器上右键Connect Goal To Selected Solver来指定Solver的目标。
3. 选中Solver并且在想要的骨骼上右键Set Root Bone On Select ed Solver来指定Solver的根骨骼目标。
### IK Goal
意为IK目标。
### Solver
解算器类型主要为:
- Body Mover**躯体运动解算器**。需要一个根骨骼和至少两个IK目标相连。主要用于地面对齐应作为第一个解算器与其他解算器配对使用Limb IK或者Full Body IK。
- ![BodyMover4.gif](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/BodyMover4.gif)
- Limb IK**手足/肢体 IK**。需要一个根骨骼和一个IK目标才能起作用。该解算器要求链中有**至少三根骨骼**才能正确工作。
- ![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230118103643.png)
- Full Body IK**全身IK**是个功能齐全的多IK Goal解算器。必须连接一个根骨骼和至少一个IK目标。
- ![FBIK1.gif](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/FBIK1.gif)
- 可以对指定的骨骼设置FullBody设置来限制一些骨骼的旋转/位移。
- ![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230121162327.png)
- Pole Solver**极解算器**,为单个骨骼链提供 **极向量Pole Vector** 控制。必须同时指定根骨骼和末端骨骼,右键点击根骨骼和末端骨骼并选择 **在选定解算器上设置根/末端骨骼Set Root / End Bone on Selected Solver**。在大多数情况中,应将"极解算器Pole Solver"与另一个解算器配对,并将"极解算器Pole Solver"设置为最后执行。
- ![Pole5.gif](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/Pole5.gif)
- Set Transform通常情况下最好将该解算器与其他解算器配对并设置为先执行"设置变换Set Transform"。在这个示例中,臀部的 **设置变换Set Transform** 与腿上的 **手足IKLimb IK** 进行了配对。
- ![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/Transform2.gif)
解算器的添加顺序很重要因为解算器将根据名称旁边显示的数字按顺序执行。对于此IK系统肢体IK应该最后求值。
![Tut2.gif](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/Tut2.gif)
### IK Rig In Animation Blueprint
在为一个SkeletalMesh创建IK Rig之后还可以动画蓝图中进行控制。只需要使用IK Rig动画蓝图节点并且指定**Rig Definition Asset**。并在**IK Rig Editor**中将对应的**IK Goal**设置**Exposure Position/Rotation**。
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230119102819.png)
## IK Rig Retarget
IK Rig的另一个作用就是IK Rig Retarget可以使用IK算法把部分从定性有问题的骨骼动画修正。比如穿高跟鞋=>不穿高跟鞋。大致步骤如下:
1. 为**Source/Target SkeletalMesh**分别设置**IK Rig**资产。
2. 在2个IK Rig资产设置用于重定向的**根骨骼**,一般是骨盆或者臀部骨骼。在对应骨骼上右键选择**Set Retarget Root**。
3. 为肢体部分分别创建骨骼链。
1. ![800](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230119105835.png)
4. 创建**IK Retargeter**资产。 需要设置**Source IKRig Asset**与**Target IKRig Asset**。
6. 在编辑器中的**Current Retarget Pose**设置重定向Pose如果为APose需要设置成TPose
7. 对于脚部部的穿帮可以设置**Fullbody IK**也可以是别的IK解算器与**IK Goal**并且在对应骨骼链中设置对应的IK Goal。
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230121171015.png)
最后导出想要重定向的动画。
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230121171445.png)
PS.
1. 在**IK Retargeter Editor**的details面部中可以通过**Retarget IK**来预览IK Targeting的效果。
2. ThirdPerson模板采用FullBody IK进行IK Retarget。
### UE5实时重定向
在执行以上步骤之后在新SkeletalMesh的动画蓝图中使用**Retarget Pose From Mesh**节点。再设置**IK Retarget**资产即可。
需要注意角色类需要2个模型一起挂载其中一个模型隐藏并且将**Visibility Based Anim Tick Option** 选项设置成**Alway Tick Pose And Refresh Bone**
### Skeleton Compatibility
官方翻译为**可兼容的骨架**,不同骨骼之间共享动画的功能。
https://docs.unrealengine.com/5.0/en-US/skeletons-in-unreal-engine/#compatibleskeletons
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230121214547.png)
这个功能可以用来共享动画,并且不需要重定向。
## 其他
UE5的新功能实时重定向
https://www.bilibili.com/video/BV1HZ4y117Yk/?spm_id_from=333.999.0.0&vd_source=d47c0bb42f9c72fd7d74562185cee290
可以用来制作一些角色特有的动画。也可以用于共享动画。
- https://docs.unrealengine.com/5.0/zh-CN/facial-animation-sharing-in-unreal-engine/
- https://docs.unrealengine.com/5.0/zh-CN/using-sub-anim-instances-in-unreal-engine/

View File

@@ -0,0 +1,236 @@
---
title: FBXAnimation导入逻辑
date: 2023-12-11 11:28:36
excerpt:
tags:
rating: ⭐
---
# BVHImport插件中的案例
```c++
UObject* UBVHImportFactory::FactoryCreateFile(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, const FString& FileName, const TCHAR* Parms, FFeedbackContext* Warn, bool& bOutOperationCanceled)
{
if (!shouldImport) {
return nullptr;
}
bool fileSuccess = BVHFile.ImportData(TCHAR_TO_ANSI(*FileName));
const bool bIsUnattended = (IsAutomatedImport()
|| FApp::IsUnattended()
|| IsRunningCommandlet()
|| GIsRunningUnattendedScript);
// Check if it's a re-import
if (InParent != nullptr)
{
UObject* ExistingObject = StaticFindObject(UObject::StaticClass(), InParent, *InName.ToString());
if (ExistingObject)
{
//reimport逻辑略
}
}
if (fileSuccess)
{
UAnimSequence* AnimSequence = NewObject<UAnimSequence>(InParent, InName, Flags & ~EObjectFlags::RF_Transactional);
if (Skeleton == NULL) {
//创建骨骼资产略
}
else {
AnimSequence->SetSkeleton(Skeleton);//Skeleton bvhSkel
AnimSequence->SetPreviewMesh(PreviewMesh);
}
ExtractAnimDataFromBVHFile(AnimSequence);
return AnimSequence;
}
return nullptr;
}
```
```c++
void UBVHImportFactory::ExtractAnimDataFromBVHFile(UAnimSequence* AnimSequence) {
if (AnimSequence)
{
// create animation
IAnimationDataController& Controller = AnimSequence->GetController();
Controller.OpenBracket(LOCTEXT("ImportBVHAnimation", "Importing BVH Animation"));
Controller.ResetModel();
// Write animation data into animation sequence.
// Extract transform of hip to create root motion.
const FReferenceSkeleton& RefSkeleton = AnimSequence->GetSkeleton()->GetReferenceSkeleton();
const FName RootName = RefSkeleton.GetBoneName(0);
const int32 NumOfKeys = BVHFile.Root->FrameOffset.Num();
AnimSequence->ImportFileFramerate = (float)BVHFile.Header.DataRate;
AnimSequence->ImportResampleFramerate = BVHFile.Header.DataRate;
Controller.SetPlayLength(float(NumOfKeys - 1) / (float)BVHFile.Header.DataRate);
Controller.SetFrameRate(FFrameRate(BVHFile.Header.DataRate, 1));
RecursiveReadKeysFromNode(Controller, BVHFile.Root.Get());
Controller.NotifyPopulated();
Controller.CloseBracket();
AnimSequence->AdditiveAnimType = EAdditiveAnimationType::AAT_None;
AnimSequence->PostEditChange();
FAssetRegistryModule::AssetCreated(AnimSequence);
AnimSequence->MarkPackageDirty();
}
}
```
```c++
void UBVHImportFactory::RecursiveReadKeysFromNode(IAnimationDataController& Controller, FNode* Node)
{
if (Node)
{
Controller.AddBoneTrack(Node->Name,false);
Controller.SetBoneTrackKeys(Node->Name, Node->FrameOffset, Node->FrameQuat, Node->FrameScale,false);
if (Node->Children.Num() > 0)
{
for (NodePtr Child : Node->Children)
{
RecursiveReadKeysFromNode(Controller, Child.Get());
}
}
}
}
```
# UE5中使用FBXSDK导入动画逻辑
- UAnimSequence * UEditorEngine::ImportFbxAnimation
- UAnimSequence * UnFbx::FFbxImporter::ImportAnimations
UE5中FBXSDK相关函数调用方式
- bool FFbxImporter::OpenFile(FString Filename)
- bool FFbxImporter::ImportFile(FString Filename, bool bPreventMaterialNameClash /*=false*/)
# 参考
1. Interchange\\Runtime\\Source\\Parsers
1. InterchangeFbxParser.Build.cs
2. FbxInclude.hFBXSDK头文件包含问题。
# UE5中使用FBXSDK导出动画逻辑
1. FFbxExporter::ExportSkeletalMeshToFbx => FFbxExporter::ExportAnimSequence => FFbxExporter::ExportAnimSequenceToFbx
2. FFbxExporter::CorrectAnimTrackInterpolation
直接导出会有问题所以UE在这里做了一步Correct
```c++
// The curve code doesn't differentiate between angles and other data, so an interpolation from 179 to -179
// will cause the bone to rotate all the way around through 0 degrees. So here we make a second pass over the
// rotation tracks to convert the angles into a more interpolation-friendly format.
FFbxExporter::CorrectAnimTrackInterpolation()
{
void FFbxExporter::CorrectAnimTrackInterpolation( TArray<FbxNode*>& BoneNodes, FbxAnimLayer* InAnimLayer )
{
// Add the animation data to the bone nodes
for(int32 BoneIndex = 0; BoneIndex < BoneNodes.Num(); ++BoneIndex)
{
FbxNode* CurrentBoneNode = BoneNodes[BoneIndex];
// Fetch the AnimCurves
FbxAnimCurve* Curves[3];
Curves[0] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_X, true);
Curves[1] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, true);
Curves[2] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true);
for(int32 CurveIndex = 0; CurveIndex < 3; ++CurveIndex)
{
FbxAnimCurve* CurrentCurve = Curves[CurveIndex];
CurrentCurve->KeyModifyBegin();
float CurrentAngleOffset = 0.f;
for(int32 KeyIndex = 1; KeyIndex < CurrentCurve->KeyGetCount(); ++KeyIndex)
{
float PreviousOutVal = CurrentCurve->KeyGetValue( KeyIndex-1 );
float CurrentOutVal = CurrentCurve->KeyGetValue( KeyIndex );
float DeltaAngle = (CurrentOutVal + CurrentAngleOffset) - PreviousOutVal;
if(DeltaAngle >= 180)
{
CurrentAngleOffset -= 360;
}
else if(DeltaAngle <= -180)
{
CurrentAngleOffset += 360;
}
CurrentOutVal += CurrentAngleOffset;
CurrentCurve->KeySetValue(KeyIndex, CurrentOutVal);
}
CurrentCurve->KeyModifyEnd();
}
}
}
```
## AnimSequence生成逻辑
```c++
//创建UAnimSequence
FString ParentPath = FString::Printf(TEXT("%s/%s"), *FPackageName::GetLongPackagePath(*Outer->GetName()), *SequenceName);
UObject* ParentPackage = CreatePackage( *ParentPath);
UObject* Object = LoadObject<UObject>(ParentPackage, *SequenceName, NULL, (LOAD_Quiet | LOAD_NoWarn), NULL);
UAnimSequence * DestSeq = Cast<UAnimSequence>(Object);
//设置骨骼
DestSeq->SetSkeleton(Skeleton);
//设置文件帧率率与重采样率修改帧率记得调用IAnimationDataController.OpenBracket()、NotifyPopulated()、CloseBracket()
IAnimationDataController& AnimationDataController = AnimSequence->GetController();
AnimationDataController.OpenBracket(FText::FromString("Importing Animation"));
DestSeq->ImportFileFramerate = GetOriginalFbxFramerate();
DestSeq->ImportResampleFramerate = ResampleRate;
AnimationDataController.SetFrameRate(FFrameRate(AnimSequencesFPS,1));
AnimationDataController.SetPlayLength(FacialTime);
AnimationDataController.NotifyPopulated();
AnimationDataController.CloseBracket();
```
### 导入Curve相关逻辑
1. UnFbx::FFbxImporter::ImportAnimation()
2. UnFbx::FFbxImporter::ImportBlendShapeCurves()
3. UnFbx::FFbxImporter::ImportCurveToAnimSequence() => UnFbx::FFbxImporter::ImportCurve()
## 异步机制改进
```c++
void URuntimeAudioImporterLibrary::ImportAudioFromFile(const FString& FilePath, ERuntimeAudioFormat AudioFormat)
{
AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, [WeakThis = TWeakObjectPtr<URuntimeAudioImporterLibrary>(this), FilePath, AudioFormat]() mutable
{
if (!WeakThis.IsValid())
{ UE_LOG(LogRuntimeAudioImporter, Error, TEXT("Failed to import audio from file '%s' because the RuntimeAudioImporterLibrary object has been destroyed"), *FilePath);
return;
}
FGCObjectScopeGuard Guard(WeakThis.Get());
if (!FPaths::FileExists(FilePath))
{ WeakThis->OnResult_Internal(nullptr, ERuntimeImportStatus::AudioDoesNotExist);
return;
}
AudioFormat = AudioFormat == ERuntimeAudioFormat::Auto ? GetAudioFormat(FilePath) : AudioFormat;
AudioFormat = AudioFormat == ERuntimeAudioFormat::Invalid ? ERuntimeAudioFormat::Auto : AudioFormat;
TArray64<uint8> AudioBuffer;
if (!LoadAudioFileToArray(AudioBuffer, *FilePath))
{ WeakThis->OnResult_Internal(nullptr, ERuntimeImportStatus::LoadFileToArrayError);
return;
}
WeakThis->ImportAudioFromBuffer(MoveTemp(AudioBuffer), AudioFormat);
});}
```
# 其他导入代码
```c++
USoundWave* FFacialAnimationImportItem::ImportSoundWave(const FString& InSoundWavePackageName, const FString& InSoundWaveAssetName, const FString& InWavFilename) const
{
}
```

View File

@@ -0,0 +1,31 @@
---
title: LevelSequence
date: 2023-07-04 17:38:34
excerpt:
tags:
rating: ⭐
---
# ALevelSequenceActor
## FLevelSequenceActorDetails
通过OnOpenLevelSequenceForActor打开Sequence编辑器。
```c++
FReply FLevelSequenceActorDetails::OnOpenLevelSequenceForActor()
{
if( LevelSequenceActor.IsValid() )
{
UObject* LoadedObject = LevelSequenceActor.Get()->GetSequence();
if (LoadedObject != nullptr)
{
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(LoadedObject);
}
}
return FReply::Handled();
}
```
## FLevelSequenceEditorToolkit

View File

@@ -0,0 +1,32 @@
---
title: UE消息对话框使用
date: 2022-09-28 14:41:08
excerpt:
tags:
rating: ⭐
---
```c++
void FSimpleEditorsModule::PluginButtonClicked()
{
//消息框显示的消息内容
FText DialogText = FText::Format(
LOCTEXT("PluginButtonDialogText", "Add code to {0} in {1} to override this button's actions"),
FText::FromString(TEXT("FSimpleEditorsModule::PluginButtonClicked()")),
FText::FromString(TEXT("SimpleEditors.cpp"))
);
EAppReturnType::Type ReturnType = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText);
if (ReturnType == EAppReturnType::Type::Ok)
{
//消息框OK按钮被点击执行
UE_LOG(LogTemp,Log,TEXT("Click OK Button."))
FMessageDialog::ShowLastError(); //弹出默认的系统原生消息对话框
}
else
{
//消息框Cancel按钮被点击执行
UE_LOG(LogTemp, Log, TEXT("Click Cancel Button."))
FMessageDialog::Debugf(DialogText); //弹出默认的OK消息对话框
}
}
```

View File

@@ -0,0 +1,168 @@
---
title: UTexture2D的读取与写入数据并且保存成Asset
date: 2022-11-04 10:11:39
excerpt:
tags:
rating: ⭐
---
## UTexture2D的读取与写入数据
在阅读@YivanLee的文章
https://zhuanlan.zhihu.com/p/45171840
我发现“**向UTexture2D写入数据**”这节有些纰漏:
1. 作者没有说明Texture2D->PlatformData->Mips[0].BulkData.LockReadOnly()返回空指针的详细情况。
2. 作者实现的写入是临时性的。
在查阅国外资料时我在isaratech的网站里找到解决方案在此我将进行简单说明。原文地址在结尾会介绍
## 从UTexture2D读取数据
读取方法如下:
```
const FColor* FormatedImageData = static_cast<const FColor*>( MyTexture2D->PlatformData->Mips[0].BulkData.LockReadOnly());
for(int32 X = 0; X < MyTexture2D->SizeX; X++)
{
    for (int32 Y = 0; Y < MyTexture2D->SizeY; Y++)
    {
        FColor PixelColor = FormatedImageData[Y * MyTexture2D->SizeX + X];
        // Do the job with the pixel
    }
}
MyTexture2D->PlatformData->Mips[0].BulkData.Unlock();
```
但有的时候LockReadOnly()会返回空指针这个时候你需要去检查UTexture2D的设置
1. CompressionSettings 是否为VectorDisplacementmap。
2. MipGenSettings 是否为NoMipmaps。
3. SRGB 是否为未勾选状态。
但通常情况下都不可能同时符合这3个条件所以我们需要将UTexture2D的3个选项设置成上述这3个状态。在处理所需操作后再进行恢复。代码如下
```
TextureCompressionSettings OldCompressionSettings = MyTexture2D->CompressionSettings; TextureMipGenSettings OldMipGenSettings = MyTexture2D->MipGenSettings; bool OldSRGB = MyTexture2D->SRGB;
MyTexture2D->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
MyTexture2D->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
MyTexture2D->SRGB = false;
MyTexture2D->UpdateResource();
const FColor* FormatedImageData = static_cast<const FColor*>( MyTexture2D->PlatformData->Mips[0].BulkData.LockReadOnly());
for(int32 X = 0; X < MyTexture2D->SizeX; X++)
{
    for (int32 Y = 0; Y < MyTexture2D->SizeY; Y++)
    {
        FColor PixelColor = FormatedImageData[Y * MyTexture2D->SizeX + X];
        //做若干操作
    }
}
MyTexture2D->PlatformData->Mips[0].BulkData.Unlock();
Texture->CompressionSettings = OldCompressionSettings;
Texture->MipGenSettings = OldMipGenSettings;
Texture->SRGB = OldSRGB;
Texture->UpdateResource();
```
## 向UTexture2D写入数据
写入操作也是类似不过我们需要调用若干UPackage与FAssetRegistryModule的代码。
下文中的Asset相关操作需要在包含头文件
># include "AssetRegistryModule.h"
同时需要在项目模块c#文件的PublicDependencyModuleNames.AddRange中加入"AssetTools"。
### 取得Package的指针
这里可以使用CreatePackage来创建新的包或是调用FindPackage来取得已有包的UPackage指针。包这个概念来源于UE3与UDK时代当时引擎所有的资源都存放在一个一个的包中修改过资源后还需要选中并且点击保存包选项才能真正保存但是UE4淡化了这个概念。
你可以把包理解为Content下各个文件夹。
```
//创建名为PackageName值的包
//PackageName为FString类型
FString AssetPath = TEXT("/Game/")+ PackageName+ TEXT("/");
AssetPath += TextureName;
UPackage* Package = CreatePackage(NULL, *AssetPath);
Package->FullyLoad();
```
FindPackage本人还没用过不过用法应该差不多。
### 取得UTexture2D的指针
你可以创建一个新的UTexture2D也可以通过蓝图指定。
```
//创建
UTexture2D* NewTexture = NewObject<UTexture2D>(Package, *TextureName, RF_Public | RF_Standalone | RF_MarkAsRootSet);
NewTexture->AddToRoot();            
NewTexture->PlatformData = new FTexturePlatformData();  
NewTexture->PlatformData->SizeX = TextureWidth;
NewTexture->PlatformData->SizeY = TextureHeight;
NewTexture->PlatformData->NumSlices = 1;
//设置像素格式
NewTexture->PlatformData->PixelFormat = EPixelFormat::PF_B8G8R8A8;
```
### 写入数据
```
//创建一个uint8的数组并取得指针
//这里需要考虑之前设置的像素格式
uint8* Pixels = new uint8[TextureWidth * TextureHeight * 4];
for (int32 y = 0; y < TextureHeight; y++)
{
    for (int32 x = 0; x < TextureWidth; x++)
    {
        int32 curPixelIndex = ((y * TextureWidth) + x);
        //这里可以设置4个通道的值
        //这里需要考虑像素格式之前设置了PF_B8G8R8A8那么这里通道的顺序就是BGRA
        Pixels[4 * curPixelIndex] = 100;
        Pixels[4 * curPixelIndex + 1] = 50;
        Pixels[4 * curPixelIndex + 2] = 20;
        Pixels[4 * curPixelIndex + 3] = 255;
    }
}
//创建第一个MipMap
FTexture2DMipMap* Mip = new FTexture2DMipMap();
NewTexture->PlatformData->Mips.Add(Mip);
Mip->SizeX = TextureWidth;
Mip->SizeY = TextureHeight;
//锁定Texture让它可以被修改
Mip->BulkData.Lock(LOCK_READ_WRITE);
uint8* TextureData = (uint8*)Mip->BulkData.Realloc(TextureWidth * TextureHeight * 4);
FMemory::Memcpy(TextureData, Pixels, sizeof(uint8) * TextureHeight * TextureWidth * 4);
Mip->BulkData.Unlock();
//通过以上步骤,我们完成数据的临时写入
//执行完以下这两个步骤编辑器中的asset会显示可以保存的状态如果是指定Asset来获取UTexture2D的指针的情况下
NewTexture->Source.Init(TextureWidth, TextureHeight, 1, 1, ETextureSourceFormat::TSF_BGRA8, Pixels);
NewTexture->UpdateResource();
```
### 创建Asset并清理无用数据
```
Package->MarkPackageDirty();
FAssetRegistryModule::AssetCreated(NewTexture);
//通过asset路径获取包中文件名
FString PackageFileName = FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension());
//进行保存
bool bSaved = UPackage::SavePackage(Package, NewTexture, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *PackageFileName, GError, nullptr, true, true, SAVE_NoError);
delete[] Pixels;
```
之后你就可以在编辑器中找到新生成的Asset。如果你是向选中的Asset写入数据执行到上一步调用UPackage::SavePackage即可进行保存。
## 总结
本文包含以下知识:
1. UTexture2D的部分结构、属性以及读取设置方法。
2. UPackage部分的部分操作。
3. Asset的创建操作。
读者可以执行通过阅读UPackage、FAssetRegistryModule的代码学习Asset更多的Asset操作。
## 个人推荐的isaratech网站文章
网站
https://isaratech.com/
生成一个新的贴图Asset保存
https://isaratech.com/save-a-procedurally-generated-texture-as-a-new-asset/
生成一个新的带有节点的MaterialAsset
https://isaratech.com/ue4-programmatically-create-a-new-material-and-inner-nodes/

View File

@@ -0,0 +1,186 @@
---
title: UnrealEngine的命令行操作方式
date: 2023-04-27 11:15:14
excerpt:
tags: CommandLet
rating: ⭐
---
# 前言
最近想实现使用UnrealEditor-Cmd进行资产处理并且渲染的功能这里简单归纳一下。
# 命令行启动方式
## CommandLet
继承UCommandLet重写Run()相关逻辑写在里面即可。启动参数大致如下:
```bash
UnrealEditor-Cmd.exe ProjectPath -run=CommandletName
```
这里很推荐去看一下`UImportAssetsCommandlet`里面实现了使用Json传参的方法。
### 变相执行ConsoleCommand
创建一个CommandLet并且接收参数并在最后下列代码即可。
```c++
GEditor->Exec(World, TEXT("MAP REBUILD ALLDIRTYFORLIGHTING"));
```
## Python
- 官方文档https://docs.unrealengine.com/4.27/en-US/ProductionPipelines/ScriptingAndAutomation/Python/
```bash
UnrealEditor-Cmd.exe ProjectPath -ExecutePythonScript="c:\my_script.py"
```
还存在另一种方法编辑器启动时环境最小不包含UI或渲染。该方法执行起来非常快但是加载脚本需要交互的关卡和其他种类资源时比较棘手。在命令行中添加以下参数-`run=pythonscript -script=<script_file>`比如:
```bash
UnrealEditor-Cmd.exe -run=pythonscript -script="c:\\my_script.py"
```
## AutomationTest
一个另类的思路就是使用自动测试工具。通过`IMPLEMENT_COMPLEX_AUTOMATION_TEST`实现一个自动测试类之后实现`RunTest`即可。启动参数:
```bash
UnrealEditor-Cmd.exe ProjectPath -AutomationTestName -Execcmds="Command-Line Arguments"
```
大致过程可以参考视频https://www.youtube.com/watch?v=kJd5-jY46Gk
视频中的启动参数:
```python
import subprocess
engine = "C:/work/Epic/UE_5.0/Engine/Binaries/win64/UnrealEditor.exe"
project = "C:/Work/Prototypes/CompileBlueprintProj/CompileBlueprintProj.uproject"
log_location = "C:/Work/Prototypes/CompileBlueprintProj/blueprint_results.log"
cmd =[
engine,project,
f"-abslog={flog_location}",
"-editortest",
"-Execcmds= "Automation SetFilter Stress, Automation listAutomation RunTest Project.Blueprints.compile Blueprints\"","-testexit=\ "Automation Test Queue Empty\"",
cmd = " ".join(cmd)
subprocess.run(cmd)
```
引擎内的FCompileBlueprintsTest具体代码
```c++
IMPLEMENT_COMPLEX_AUTOMATION_TEST(FCompileBlueprintsTest, "Project.Blueprints.Compile Blueprints", EAutomationTestFlags::EditorContext | EAutomationTestFlags::StressFilter)
/************************************************************************/
/* FCompileBlueprintsTest */
/************************************************************************/
/** Requests a enumeration of all blueprints to be loaded */
void FCompileBlueprintsTest::GetTests(TArray<FString>& OutBeautifiedNames, TArray <FString>& OutTestCommands) const
{
FBlueprintAutomationTestUtilities::CollectTestsByClass(UBlueprint::StaticClass(), OutBeautifiedNames, OutTestCommands, /*bool bIgnoreLoaded =*/false);
}
bool FCompileBlueprintsTest::RunTest(const FString& Parameters)
{
UE_LOG(LogBlueprintAutomationTests, Log, TEXT("Beginning compile test for %s"), *Parameters);
return FBlueprintAutomationTestUtilities::CompileBlueprint(Parameters);
}
/**
* Simulates the user pressing the blueprint's compile button (will load the * blueprint first if it isn't already). ** @param BlueprintAssetPath The asset object path that you wish to compile. * @return False if we failed to load the blueprint, true otherwise
*/
static bool CompileBlueprint(const FString& BlueprintAssetPath)
{
UBlueprint* BlueprintObj = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), NULL, *BlueprintAssetPath));
if (!BlueprintObj || !BlueprintObj->ParentClass)
{
UE_LOG(LogBlueprintAutomationTests, Error, TEXT("Failed to compile invalid blueprint, or blueprint parent no longer exists."));
return false;
}
UPackage* const BlueprintPackage = BlueprintObj->GetOutermost();
// compiling the blueprint will inherently dirty the package, but if there
// weren't any changes to save before, there shouldn't be after
bool const bStartedWithUnsavedChanges = (BlueprintPackage != nullptr) ? BlueprintPackage->IsDirty() : true;
FKismetEditorUtilities::CompileBlueprint(BlueprintObj, EBlueprintCompileOptions::SkipGarbageCollection);
if (BlueprintPackage != nullptr)
{
BlueprintPackage->SetDirtyFlag(bStartedWithUnsavedChanges);
}
return true;
}
```
# 编辑器内调用
## Python
- 官方文档https://docs.unrealengine.com/4.27/en-US/ProductionPipelines/ScriptingAndAutomation/Python/
将OutputLog的ConsoleCommand的类型从CMD=>Python之后输入Python命令即可。
## CustomConsoleCommand
除了UFUNCTION中指定Exec之外因为这个只能在部分类中实现并不通用就是使用`IConsoleManager::Get().RegisterConsoleCommand()`,卸载函数为`IConsoleManager::Get().UnregisterConsoleObject()`一般会在Module的StartupModule()/ShutdownModule()或者Subsystem的对应函数中进行注册/卸载。用法如下:
```c++
IConsoleManager::Get().RegisterConsoleCommand(
TEXT("ConsoleCommandName"),
TEXT("Useage Info"),
FConsoleCommandDelegate::CreateStatic(&UYourClass::Function),
ECVF_Default);
```
相关的FConsoleCommandDelegate委托都位于IConsoleManager.h根据需求选择。
# 其他
## CommandLet Server
```c++
int32 FEditorDomainSaveServer::Run()
{
if (!TryInitialize())
{
Shutdown();
return 1;
}
while (PollShouldRun())
{
bool bIsIdle = true;
TickPendingPackages(bIsIdle);
PollIncomingConnections(bIsIdle);
PollConnections(bIsIdle);
TickMaintenance(bIsIdle);
}
Shutdown();
return 0;
}
void FEditorDomainSaveServer::TickMaintenance(bool bIsIdle)
{
using namespace UE::EditorDomainSave;
SetIdle(bIsIdle);
double CurrentTime = FPlatformTime::Seconds();
if (bIsIdle)
{
if (!HasExpectedConnections() && CurrentTime - IdleStartTime > Constants::ServerAbdicationCooldownSeconds)
{
if (TryAbdicate())
{
return;
}
}
}
double CollectGarbageCooldownSeconds = bIsIdle ?
Constants::CollectGarbageIdleCooldownSeconds :
Constants::CollectGarbageActiveCooldownSeconds;
bool bCollectedGarbageAfterIdle = bIsIdle && LastGarbageTime >= IdleStartTime;
if (!bCollectedGarbageAfterIdle && CurrentTime - LastGarbageTime > CollectGarbageCooldownSeconds)
{
CollectGarbage(RF_NoFlags);
LastGarbageTime = FPlatformTime::Seconds();
}
if (bIsIdle)
{
FPlatformProcess::Sleep(Constants::ServerIdleSleepPeriodSeconds);
}
}
```
## 日志写入文件
```bash
f"-abslog={log_location}"
```

View 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合成节点也就是指SequenceSelectorSimpleParallel节点。
还有种特殊情况当Service附加在与Root相连的第一个合成节点上时SearchStart会被触发两次。
## Compisiton
复合节点:此类节点定义分支的根以及执行该分支的基本规则。
## Decorator
![](https://docs.unrealengine.com/4.26/Images/InteractiveExperiences/ArtificialIntelligence/BehaviorTrees/BehaviorTreeNodeReference/BehaviorTreeNodeReferenceDecorators/CustomDecorator.webp)
可以理解为条件节点连接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
![](https://docs.unrealengine.com/4.26/Images/InteractiveExperiences/ArtificialIntelligence/BehaviorTrees/BehaviorTreeNodeReference/BehaviorTreeNodeReferenceServices/NewCustomService_01.webp)
可以理解为并行逻辑节点连接连接Composite或者Task节点,运行到当前分支时,Service节点将按照之前设定的频率执行。通常是使用这些Service节点检查、更新黑板数据。
c++可以参考:
- UBTService_BlueprintBase
- UBTService_BlackboardBase
- UBTService_RunEQS
蓝图节点中可重写的函数有:
- ReceiveTick
- ReceiveTickAI
- ReceiveSearchStart
- ReceiveSearchStartAI
- ReceiveActivation
- ReceiveActivationAI
- ReceiveDeactivation
- ReceiveDeactivationAI
### 相关事件
- OnSearchStart所挂靠的合成节点被激活时调用的函数。
- TickNodeTick执行的函数间隔自定义。可在构造函数设置 bNotifyTick 来决定是否调用。
- OnBecomeRelevant节点被激活时调用等同于蓝图Service里的Receive Activation 需要在构造函数中设置 bNotifyBecomeRelevant = true来开启调用。
- OnCeaseRelevant节点被激活时调用等同于蓝图Service里的Receive Deactivation 需要在构造函数中设置 bNotifyCeaseRelevant = true来开启调用。
- TickNodeOnBecomeRelevant和OnCeaseRelevant并不是UBTService类的虚函数而是UBTService的父类UBTAuxiliaryNode的。装饰器UBTDecorator也继承此类。
- GetInstanceMemorySize当节点被实例化时分配的内存大小关于实例化后面会详谈。
- Tick函数执行间隔 = RandomInterval-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;
}
```

View File

@@ -0,0 +1,127 @@
---
title: AnimNode
date: 2023-08-08 12:23:11
excerpt:
tags:
rating: ⭐
---
# 前言
参考:
- https://zhuanlan.zhihu.com/p/611398524
## 动画逻辑节点AnimNode
AnimNode是动画节点的纯逻辑类用于运行时执行。实际上是一个Struct。
### 更新Update
节点的Update用于根据Weight计算动画的各种权重。因为Weight会在下一阶段清空。如果按照Epic的编写习惯我们应该在Update里面拿到所有外部数据并且预计算保证Evaluate可以直接使用。
### 评估Evaluate
根据上一个节点的Pose计算出输出到下个节点的Pose。这是动画节点最重要的部分。正常来说我们应该把骨骼计算部分都放在这里。
注意Update和Evaluate都有可能运行在子线程上除了读写AnimInstanceProxy外操作其他东西都不是线程安全的尽可能不要碰外部的UObject。
### 根节点Root
也叫Output Pose节点。根节点是最重要的节点。对于用户来说他是所有动画逻辑的输出节点。但是对于蓝图来说他是整个蓝图节点的开始。AnimInstance将从这里开始建立整个动画节点的树状联系。
![](https://pic4.zhimg.com/80/v2-67104412823695737b0a0382b8e5992b_720w.webp)
### 动画节点的属性
AnimNode通过他们的属性从外部获取信息。尽可能通过属性拿到想要的数据而不是在运行时一层层往上Cast然后获取。
AnimNode里面的EditAnywhere的UProperty都会在动画蓝图里暴露出来。有几个特殊的Meta
- AlwaysAsPin
- NeverAsPin
- PinShownByDefault
- PinHiddenByDefault
他们可以控制属性要不要作为pin暴露出去。
变成Pin后可以直接从变量连接过去图中的Translation也可以直接绑定属性图中的Alpha
如果连接变量的话要注意节点是否仍然保持着FastGraph闪电图标
## 骨骼索引BoneIndex
BoneIndex有三种我们一一解释。
### SkeletonBoneIndex
首先Skeleton的骨骼包含着所有Mesh的所有骨骼。每当新增骨骼只要名字不重复就可以插入到Skeleton里面。Skeleton主要保存着他们的父子关系所以不同网格体之间的同名骨骼的父子关系一定要正确。
在Skeleton里面所有的骨骼都有一个唯一的Name和唯一的ParentIndex然后保存在BoneTree里面。骨骼在这里面的Index就属于SkeletonBoneIndex了。
SkeletonBoneIndex主要用于查找父骨骼当然很多地方已经直接帮你缓存下来了实际应用中很少需要自己去Skeleton里面查找。
### MeshBoneIndex
MeshBoneIndex是当前的骨骼网格体用到的骨骼的Index。
用于显示骨架网格体所以输出的话是输出MeshBoneIndex。
### CompactPoseBoneIndex
当前Lod用到的骨骼的Index在RequiredBone里面保存。
通过`XXBone.Initialize(RequiredBones)` 进行初始化。
我们在动画节点里面一般通过FBoneReference来操作骨骼。BoneRefrence会通过BoneName获得3个骨骼索引。如果传入的是BoneContainer`BoneIndex`保存的是MeshIndex如果传入的是Skeleton的话`BoneIndex`保存的是SkeletonIndex。
Epic网站上也有一篇文章作为参考[[AI偶像陪伴项目笔记]](https://zhuanlan.zhihu.com/p/611398524#ref_1)。
# FAnimNode
- Initialize_AnyThread用于初始化数据初始化ComponentPose、AlphaBoolBlend、AlphaScaleBiasClamp。
- CacheBones_AnyThread用于缓存Pose。
- GatherDebugData获取Debug数据。
- Evaluate计算动画结果。
- EvaluateSkeletalControl_AnyThread在其他线程计算骨骼结果。
# VMC4UE
1. 支持多端口(多台捕捉机器)一起工作。
***存在问题***
1. 只做了简单的BoneTransform与Morph适配。没有针对VMC协议的其他内容进行适配。
## AnimNode_ModifyVMC4UEBones
继承自FAnimNode_SkeletalControlBase。
- Initialize_AnyThread调用InitializeBoneReferences()
- InitializeBoneReferences初始化了之后需要用到BoneReferences、InitialBones。
- CacheBones_AnyThread**重写函数**ComponentPose.CacheBones()。
- Evaluate无实现
- **EvaluateSkeletalControl_AnyThread**:核心逻辑。
- 判断VRMMapping是否经过初始化如果没有则调用BuildMapping()主要是将节点设置的VRMMapping.BoneMapping数据传递给`TMap<FName, FName> BoneMappingSkeletonToVMC`
- 调用UVMC4UEBlueprintFunctionLibrary::GetStreamingSkeletalMeshTransform()
- 开启StreamingSkeletalMeshTransform的线程读写锁。
- 计算根骨骼Transform。
- 获取FComponentSpacePoseContext.Pose与BoneContainer的引用开始遍历所有骨骼使用BoneIndex
- 根据BoneIndex`TArray<FBoneReference> BoneReferences`获取BoneName。
- 根据BoneName判断BoneMappingSkeletonToVMC是否包含该骨骼。如包含该骨骼则提取对应的骨骼数据不包则从InitialBones中取对应骨骼的值RefPose
- 获取ParentBoneIndex并将父骨骼的Transform乘上如果父骨骼是根骨骼则乘以之前计算的RootTransform。
- 因为骨骼Index是从Root开始算的这样相当将之前整个骨骼链的数据都乘上了。
- 将计算完成的新值替换OutBoneTransforms中的。
- IsValidToEvaluate
- BuildMapping
成员变量作用:
- `TArray<FBoneReference> BoneReferences`存储BoneIndex、BoneName、CachedCompactPoseIndex。
- `TMap<FName, FName> BoneMappingSkeletonToVMC`:VRM角色骨骼映射表。VRM4U可以生成不确定
- `UVMC4UEVRMMapping* PrevVRMMapping`:上一个VRMMaping用来检测是否需要重新驱动VRMMaping。
- `UVMC4UEVRMMapping* VRMMapping`:用于指定VRMMaping资源。
- `TArray<FTransform> InitialBones`:记录初始RefPose的骨骼形变找不到骨骼的情况会使用。
### VRMMaping的作用
存储VRM角色骨骼映射表与BlendShape信息。
VMC软件只用于驱动VRM角色或者与VRM骨骼结构相同的角色角色的骨骼数量、排序大概率不同会使得Index不同。所以需要搞一个BoneName映射表用来寻找驱动的骨骼。
## GetStreamingSkeletalMeshTransform
Create部分的逻辑主要是创建NewStreamingSkeletalMeshTransform并且绑定一个UUEOSCReceiver对象。由对接受到数据进行处理。
端口与NewStreamingSkeletalMeshTransform的Map存储在OSCManager中。
### OnReceivedVMC
根据VMC Address其实是数据类型进行对应判断
- /VMC/Ext/Root/Pos
- /VMC/Ext/Bone/Pos
- /VMC/Ext/Blend/Val
- /VMC/Ext/Blend/Apply
按照VMC的协议格式将数据进行反序列化并且填充到UVMC4UEStreamingSkeletalMeshTransform中。
## FAnimNode_ModifyVMC4UEMorph
继承自FAnimNode_Base。大致逻辑与AnimNode_ModifyVMC4UEBones相似。
主要逻辑位于Evaluate_AnyThread():
1. 判断是否需要重新构建VRMMaping如有需要调用BuildMapping()进行构建。
2. 调用UVMC4UEBlueprintFunctionLibrary::GetStreamingSkeletalMeshTransform()。
3. 从AnimInstanceProxy取得Skeleton。
4. 相关有效性判断。
5. Reset所有MorphStates的值为0。
6. 开启StreamingSkeletalMeshTransform的线程读写锁。
7. StreamingSkeletalMeshTransform->CurrentBlendShapes获取所有BlendShape进行遍历。
1. 寻找名字匹配的BlendShape.Clips不匹配则直接进入下一次循环。
2. 寻找名字匹配的BlendShape.Meshs不匹配则直接进入下一次循环。
3. 修改对应MorphStates[MorphName]的数值。
8. 输出所有MorphState.Value。

View File

@@ -0,0 +1,14 @@
---
title: UE5 Multi-Threaded Animation & Thread Safe
date: 2023-08-07 14:50:55
excerpt:
tags: Animation
rating: ⭐⭐
---
## 前言
具体可以参考Lyra
- https://docs.unrealengine.com/5.0/en-US/animation-optimization-in-unreal-engine/
- https://docs.unrealengine.com/5.0/en-US/animation-in-lyra-sample-game-in-unreal-engine/
1. 首先需要 **Project Settings -> General Settings -> Anim Blueprints** 勾选**Allow Multi Threaded Animation Update**。
2. 之后再动画蓝图的ClassSettings中勾选**Use Multi Threaded Animation Update**。

View File

@@ -0,0 +1,107 @@
---
title: 打包项目的Debug方法
date: 2020-07-07 13:49:28
excerpt:
tags:
rating: ⭐
---
# 蓝图深入探讨Blueprints In-depth学习笔记
就如大钊所说的,这个视频不管程序还是美术都值得学习一下。
## 蓝图开销
蓝图主要开销影响:
1. 蓝图的节点数是一个较为影响性能的因素而不是节点所执行的逻辑影响。所以在蓝图中执行循环操作会非常影响性能执行节点本身的逻辑本质上还是c++
2. 执行遍历ActorGet 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宏标记为简单测试类型。以下是一段案例代码
![image](https://pic2.zhimg.com/v2-4ed963c4891f9c8ba259b5ef07862ed9_r.jpg)
使用IMPLEMENT_COMPLEX_AUTOMATION_TEST宏标记为复杂测试类型。以下是一段案例代码
![image](https://pic4.zhimg.com/v2-3a54d102aa229a40d6e7e5ee9b8f7a87_r.jpg)
看得出可以通过这个方法添加子测试。
*启动自动调试:*
可以使用Ue4的Session Frontend也可以使用TeamCity作者的团队也编写了一个测试工具进行工作。
#### 地图测试
11:30处作者展示一个小案例。大致步骤为使用GetTest遍历地图文件夹下所有level。之后分别对各个level进行相关测试。比如模拟玩家操作等。
#### 项目经验
2300左右作者开始介绍项目经验这个流程较为复杂推荐大团队使用这个直接去看视频吧。

View File

@@ -0,0 +1,13 @@
---
title: Untitled
date: 2024-09-18 15:14:24
excerpt:
tags:
rating: ⭐
---
# 编译问题
## dte80a.tlh
https://stackoverflow.com/questions/63527007/cannot-compile-unreal-engine-4-25-with-visual-studio-2019-7-2
>`尝试删除目录“Engine\Intermediate\Build\Win64\UE4Editor\Development\VisualStudioDTE”并重建。`

View 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

View 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!

View 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

View File

@@ -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比如StrengthFortitude坚韧
- 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
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111112940.png)
## 使用DataTable与C++来初始化属性集
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111114735.png)
或者就是实现一个结构体基于FAttributeSetIniDiscreteLevel。
## 创建自定义节点来传递GE计算结果与Tags
多次GE的积累计算结果。主要是通过AbilitySystemGlobals来实现
- GE_Damage
- Custom Calculation ClassC++ 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
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111140430.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111140516.png)
## 装备属性变化
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111141459.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111141539.png)
## AbilityTraitsTableExample
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111141647.png)
## 该团队AbilityEntitySystem设计
用来实现子弹变量、AOE、Cnnstruct、角色等带有ASC的实体。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111142713.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111142509.png)

View 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;
```

View File

@@ -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;
```

View File

@@ -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。

View 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中设置的GameplayEventPayload中带有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.但本人感觉这个方法在一些动作游戏中这个方法可能会不太合适。

View 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)

View 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**命令进行翻页。
![image](https://cdn.jsdelivr.net/gh/tranek/GASDocumentation@master/Images/showdebugpage1.png)
![image](https://cdn.jsdelivr.net/gh/tranek/GASDocumentation@master/Images/showdebugpage2.png)
![image](https://cdn.jsdelivr.net/gh/tranek/GASDocumentation@master/Images/showdebugpage3.png)
PS.PageUp与PageDown可以切换目标。
## GameplayDebugger
![image](https://cdn.jsdelivr.net/gh/tranek/GASDocumentation@master/Images/gameplaydebugger.png)
在进入关卡后,按下“’”键就可以开启这个工具。(就是屏幕上方出现的工具栏)
这个功能感觉是用来调试非当前操作角色的,选中的角色上会出现一个红色小恶魔图标(如图所示),你可以小键盘的数字键来开启对应的功能。
可以通过Tab键切换成飞行模式来选中需要调试角色。
# GameEffect增加/减少 委托绑定
```
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);
```
```
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);
```

View 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());
}
```
## 预测
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/GAS%E9%A2%84%E6%B5%8B.jpg)

View 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();
}
// ...
}
```
## 预测
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/GAS%E9%A2%84%E6%B5%8B.jpg)
## 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);
}
```

View 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"));
```

View 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为0UseAbility2为1UseAbility3为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这样对工程开发会更加友好。

View 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相比少了属性捕获多了CalculationClassMagnitudeUGameplayModMagnitudeCalculation类
```
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

View File

@@ -0,0 +1,167 @@
## UAbilityTask
UAbilityTask继承自UGameplayTaskUGameplayTask可以用来写一些行为树中的一些节点可以用来实现一些异步功能。比如播放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;
}
```

View 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_WaitTargetDataUsingActorGASShooter的改进版本等待接收已经生成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"));
```

View File

@@ -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

View 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。
## 具体实现
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/ComboAbilityBP.png)
还需要设置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:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/ComboMap.png)
其中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作为EventPayloadOptionalObject传递更多的数据以此可以实现命中弱点增加伤害等效果。
#### DerivationCombat
这里其实应该是Combo但之前打错懒得改了。这里主要是设置控制Combo是否可以派生的变量。

View File

@@ -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不再赘述

View File

@@ -0,0 +1,197 @@
---
title: Untitled
date: 2024-01-29 13:23:24
excerpt:
tags:
rating: ⭐
---
# 前言
相关函数主要为:
- AbilitySystemComponent->BindToInputComponent()只绑定名称为AbilityConfirm与AbilityCancel的InputAction。
- AbilitySystemComponent->BindAbilityActivationToInputComponent()输入InputComponent、FGameplayAbilityInputBinds、FTopLevelAssetPath枚举类路径、枚举ID之后绑定GA与Input。
- AbilitySystemComponent->SetBlockAbilityBindingsArray()设置Block列表。
# GAS
BindAbilityActivationToInputComponent()的绑定方式如果ASC在Character中可以在SetupPlayerInputComponent()中进行绑定:
```c
// Bind to AbilitySystemComponent
FTopLevelAssetPath AbilityEnumAssetPath = FTopLevelAssetPath(FName("/Script/GASDocumentation"), FName("EGDAbilityInputID"));
AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"),
FString("CancelTarget"), AbilityEnumAssetPath, static_cast<int32>(EGDAbilityInputID::Confirm), static_cast<int32>(EGDAbilityInputID::Cancel)));
```
其内部主要是类似这样的方式进行绑定:
```c++
// Pressed event
{
FInputActionBinding AB(FName(*FullStr), IE_Pressed);
AB.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UAbilitySystemComponent::AbilityLocalInputPressed, idx);
InputComponent->AddActionBinding(AB);
}
// Released event
{
FInputActionBinding AB(FName(*FullStr), IE_Released);
AB.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UAbilitySystemComponent::AbilityLocalInputReleased, idx);
InputComponent->AddActionBinding(AB);
}
```
AbilityLocalInputPressed()与AbilityLocalInputReleased()会执行:
1. 触发多播委托
2. TryActivateAbility() 或者 AbilitySpecInputPressed()。
感觉可以自己实现一个BindAbilityActivationToInputComponent()的绑定逻辑ASC自带的逻辑有点2。
GAS使用的是**InputComponent->AddActionBinding**,而增强输入使用**PlayerEnhancedInputComponent->BindAction**。本质是一样的。
```c++
FInputActionBinding& BindAction( const FName ActionName, const EInputEvent KeyEvent, UserClass* Object, typename FInputActionHandlerSignature::TMethodPtr< UserClass > Func )
{
FInputActionBinding AB( ActionName, KeyEvent );
AB.ActionDelegate.BindDelegate(Object, Func);
return AddActionBinding(MoveTemp(AB));
}
```
## 绑定输入而不激活能力
如果您不希望`GameplayAbilities`在按下输入时自动激活,但仍将它们绑定到输入以与 一起使用`AbilityTasks`,则可以向您的`UGameplayAbility`子类添加一个新的 bool 变量`bActivateOnInput`,该变量默认为`true`并覆盖`UAbilitySystemComponent::AbilityLocalInputPressed()`。
```c
void UGSAbilitySystemComponent::AbilityLocalInputPressed(int32 InputID)
{
// Consume the input if this InputID is overloaded with GenericConfirm/Cancel and the GenericConfim/Cancel callback is bound
if (IsGenericConfirmInputBound(InputID))
{
LocalInputConfirm();
return;
}
if (IsGenericCancelInputBound(InputID))
{
LocalInputCancel();
return;
}
// ---------------------------------------------------------
ABILITYLIST_SCOPE_LOCK();
for (FGameplayAbilitySpec& Spec : ActivatableAbilities.Items)
{
if (Spec.InputID == InputID)
{
if (Spec.Ability)
{
Spec.InputPressed = true;
if (Spec.IsActive())
{
if (Spec.Ability->bReplicateInputDirectly && IsOwnerActorAuthoritative() == false)
{
ServerSetInputPressed(Spec.Handle);
}
AbilitySpecInputPressed(Spec);
// Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server.
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());
}
else
{
UGSGameplayAbility* GA = Cast<UGSGameplayAbility>(Spec.Ability);
if (GA && GA->bActivateOnInput)
{
// Ability is not active, so try to activate it
TryActivateAbility(Spec.Handle);
}
}
}
}
}
}
```
# 暂时需要使用GAS默认的枚举绑定Input的理由
GameplayAbilitySpec.h的InputPressed
```c++
UPROPERTY(NotReplicated)
uint8 InputPressed:1;
```
该变量用于本地输入判断,可以使用自己的代码代替,但涉及到以下几个函数,工作量比较大。
- UAbilitySystemComponent
- AbilityLocalInputReleased
- AbilitySpecInputPressed
- InternalServerTryActivateAbility
- InternalTryActivateAbility
- TryActivateAbility
- 间接关系
- BindAbilityActivationToInputComponent
- SetBlockAbilityBindingsArray
- PressInputID
- ReleaseInputID
- UAbilityTask_WaitInputPress
- UAbilityTask_WaitInputRelease
理论上是可以将其修改成增强输入的版本只需要将枚举循环改成InputActionContext循环即可具体逻辑在角色类中
```c++
void AGSHeroCharacter::BindASCInput()
{
if (!bASCInputBound && IsValid(AbilitySystemComponent) && IsValid(InputComponent))
{
AbilitySystemComponent->BindAbilityActivationToInputComponent(InputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"),
FString("CancelTarget"), FString("EGSAbilityInputID"), static_cast<int32>(EGSAbilityInputID::Confirm), static_cast<int32>(EGSAbilityInputID::Cancel)));
bASCInputBound = true;
}
}
FGameplayAbilityInputBinds(FString InConfirmTargetCommand, FString InCancelTargetCommand, FTopLevelAssetPath InEnumPathName, int32 InConfirmTargetInputID = INDEX_NONE, int32 InCancelTargetInputID = INDEX_NONE)
: ConfirmTargetCommand(InConfirmTargetCommand)
, CancelTargetCommand(InCancelTargetCommand)
, EnumPathName(InEnumPathName)
, ConfirmTargetInputID(InConfirmTargetInputID)
, CancelTargetInputID(InCancelTargetInputID)
{
}
```
**ConfirmTarget**、**CancelTarget**需要在DefaultInput.ini中添加对应的配置名字需要对应
**EGSAbilityInputID**为枚举类名称,**EGSAbilityInputID::Confirm**与**EGSAbilityInputID::Cancel**为枚举的index需要与GameplayAbilitySpec中的AbilityInputID对应。具体是在GiveAbility中进行参考[[#GASShooter中给Ability注册按键ID]]。
之后还需要在URPGGameplayAbility中将枚举**ERPGAbilityInputID**修改成
```c++
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "RPGGameplayAbility|Input")
ERPGAbilityInputID AbilityInputID = ERPGAbilityInputID::None;
```
## GASShooter中给Ability注册按键ID
```c++
void AGSCharacterBase::AddCharacterAbilities()
{
// Grant abilities, but only on the server
if (GetLocalRole() != ROLE_Authority || !IsValid(AbilitySystemComponent) || AbilitySystemComponent->bCharacterAbilitiesGiven)
{
return;
}
for (TSubclassOf<UGSGameplayAbility>& StartupAbility : CharacterAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
}
AbilitySystemComponent->bCharacterAbilitiesGiven = true;
}
```
# SDHGame
李兄是在状态机每个节点的StateBegin()中调用CreateDynamicSkillInputActionMappingContext()进行动态绑定的。FSDHSkillInputActionBinding里包含FEnhancedActionKeyMapping相关数据。
```c++
void USDHSMStateIns::OutputActionKeyMapping(TArray<FEnhancedActionKeyMapping>& OutBindings, const TArray<FSDHSkillInputActionBinding>& InputSkillBinding)
{
for (int32 i = 0; i < InputSkillBinding.Num(); i++)
{
FEnhancedActionKeyMapping NewMapping;
NewMapping.Key = InputSkillBinding[i].Key;
NewMapping.Action = InputSkillBinding[i].Action;
OutBindings.Add(MoveTemp(NewMapping));
}
}
```

View 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对玩家输入的处理规则
## 有关FScopedPredictionWindowAbilityTask的网络预测逻辑
```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和一个带有预测的技能BA执行了后B不能执行 现在同时执行技能A和技能B 由于A需要等待服务器端做验证B是本地预测技能所以B的客户端会瞬间执行导致B的客户端被执行过了服务端失败出现了一些不可预料的问题了这种情况需要将技能B的网络策略修改为Server Initiated这样就会以服务端为权威运行虽然会牺牲点延迟但是也是可以在游戏接收的范围内有时候需要在此权衡。
- 仅限服务器Server Only技能将在只在服务器上运行客户端不会。服务端的技能被执行后技能修改的任何变量都将被复制并会将状态传递给客户端。缺点是比较服务端的每个影响都会由延迟同步到客户端端Server Initiated只在运行时候会有一点滞后。
## 使用GameplayTasks制作Combo技能武器的碰撞判定必须使用同步函数也就是在AnimNotify中调用

View 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缩写为GFSGF框架的管理类全局的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里才可以进行后续的判断。至于要添加什么键就看各位自己的项目需要了。
![](https://pic3.zhimg.com/v2-eb32702550e4d8517b1bcdbc08620c8a_r.jpg)
![](https://pic1.zhimg.com/v2-30bb4136f249d6a50ea0121bd53b4c04_r.jpg)
```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资产。
![](https://pic1.zhimg.com/v2-96d6ed62d48871fe9f6d1cda3b297af4_r.jpg)
## 状态机
对每一个GF而言我们在使用的过程中能够使用到的GF状态就4个Installed、Registered、Loaded、Active。这4个状态之间可以双向转换以进行加载卸载。
也要注意GF状态的切换流程是一条双向的流水线可以往加载激活的方向前进在下图上是用黑色的箭头来表示也可以往失效卸载的方向走图上是红色的线表示。而箭头上的文字其实就是一个个GFS类里已经提供的GF加载卸载API。 双向箭头中间的圆角矩形表示的是状态绿色的状态是我们能看见的但其实内部还有挺多过渡状态的。过渡状态的概念后面也会解释。值得注意的是UE5预览版增加了一个Terminal状态可以把整个插件的内存状态释放掉。
![](https://pic1.zhimg.com/v2-d67731c19467330cff71f51e2698705c_r.jpg)
在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插件都是已经在本地的可以比较快通过检测。
![](https://pic2.zhimg.com/v2-429db98dfa46f7f387de6841aae718d9_r.jpg)
#### 加载CF C++模块
下一个阶段就是开始尝试加载GF的C++模块。这个阶段的初始状态是Installed这个我用绿色表示表明它是个目标状态区分于过渡状态。目标状态意思是在这个状态可以停留住直到你手动调用API触发迁移到下一个状态比如你想要注册或激活这个插件就会把Installed状态向下一个状态转换。卸载的时候往反方向红色的线前进。接着往下
- Mounting阶段内部会触发插件管理器显式的加载这个模块因此会加载dll触发StartupModule。在以前Unmounting阶段并不会卸载C++因此不会调用ShutdownModule。意思就是C++模块一经加载就常驻在内存中了。但在UE5预览版中加上了这一步因此现在Unmounting已经可以卸载掉插件的dll了。
- WaitingForDependencies会加载之前uplugin里依赖的其他插件模块递归加载等待所有其他的依赖项完成之后才会进入下一个阶段。这点其实跟普通的插件加载策略是一致的因此GF插件本质上其实就是以插件的机制在运作只不过有些地方有些特殊罢了。
![](https://pic1.zhimg.com/v2-823dae0fa911b90c334f860d436b53f8_r.jpg)
#### 加载GameFeatureData
在C++模块加载完成之后下一步就要开始把GF自身注册到GFS里面去。其中最重要的一步是在Registering的时候加载GFD资产会触发UGameFeaturesSubsystem::LoadGameFeatureData(BackupGameFeatureDataPath);的调用从而完成GFD加载。
![](https://pic3.zhimg.com/v2-9abd72961b0ae6b08f24f22276f370ca_r.jpg)
#### 预加载资产和配置
下一个阶段就是进行加载了。Loading阶段会开始加载两种东西一是插件的运行时的ini(如…/LearnGF/Saved/Config/WindowsEditor/MyFeature.ini)与C++ dll,另外一项是可以预先加载一些资产资产列表可以由Policy对象根据每个GF插件的GFD文件来获得因此我们也可以重载GFD来添加我们想要预先加载的资产列表。其他的资产是在激活阶段根据Action的执行按需加载的。
![](https://pic2.zhimg.com/v2-95b5552a06842280cc361cf929b82351_r.jpg)
#### 激活生效
在加载完成之后我们就可以激活这个GF了。Activating会为每个GFD里定义的Action触发OnGameFeatureActivating而Deactivating触发OnGameFeatureDeactivating。激活和反激活是Action真正做事的时机。
![](https://pic4.zhimg.com/v2-645173c77bfeb968145f7eb53576e7bf_r.jpg)
### 详细状态切换图
位于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父类为UGameFrameworkComponentModularGameplay模块中的文件。主要是增加了一些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。
- ![](https://pic3.zhimg.com/v2-d380d507c096b8b2ce14ab63c330f442_r.jpg)
## 扩展
### 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更多ActionCoreGame&GF
- GameFrameworkComponent更多Component CoreGame&GF
- ModularActor更多Actor CoreGame&GF
- IAnimLayerInterface改变角色动画 CoreGame&GF
### GameFeature模块协作
一些朋友可能会问一个问题一个游戏玩法用到的模块还是挺多的GameFeature都能和他们协作进行逻辑的更改吗下面这个图我列了一些模块和GameFeature的协作交互方式大家可以简单看一下。如果有别的模块需要交互还可以通过增加新的Action类型来实现。
![](https://pic1.zhimg.com/80/v2-31547d8ae95b2ea17018200c90b01e2c_720w.jpg)
### 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也是可以配合热更新来使用的。

View File

@@ -0,0 +1,8 @@
---
title: Untitled
date: 2025-07-25 15:39:41
excerpt:
tags:
rating: ⭐
---
- UE4/UE5 GAS共享冷却插件(SharedCoolingAbility) https://zhuanlan.zhihu.com/p/32216887423

View File

@@ -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")));
}
}
```

View File

@@ -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指针为空则表示载入失败。

View File

@@ -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**
动作游戏框架子模块剖析(其一)------DataAssethttps://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);
//使用同步方法来载入AssetTryLoad内部使用了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来加载AssetPS.在构造函数中会调用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;
}
```

View File

@@ -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中的AssetPrimaryAssets
下面是一个使用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中进入如下设置
![image](https://docs.unrealengine.com/Images/Engine/Basics/AssetsAndPackages/AssetManagement/ProjectSettingsAssetManager.jpg)
每个选项的具体功能请参考文档。
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());
```

Binary file not shown.

View File

@@ -0,0 +1,276 @@
---
title: Untitled
date: 2025-01-05 11:32:25
excerpt:
tags:
rating: ⭐
---
# Assimp
加载选项:
- aiProcess_JoinIdenticalVertices
- aiProcess_RemoveComponent
- aiProcess_OptimizeMeshes推荐移除会导致模型只有一个Material。
真正的数据都存储在Scene节点`mMeshes[]``mMaterials[]`中。其RootNode、ChildrenNode存储对应的数据Index。
- mMeshes[]
- mVertices[]
- mNormals[]
- mTextureCoords[]
- mFaces[]
- mIndices[]
- mMaterialIndex
## Materials
```c++
aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];
```
# 可用组件
## URuntimeMeshComponent
第三方插件实现。
## UDynamicMeshComponent
1. https://zhuanlan.zhihu.com/p/506779703
2. https://www.bilibili.com/opus/798754326935764996
3. https://zhuanlan.zhihu.com/p/649062059
## UProceduralMeshComponent
使用现在StaticMesh构建PMCPMC的MaterialSection是正常的。
1. UKismetProceduralMeshLibrary::CopyProceduralMeshFromStaticMeshComponent()
```c++
void UKismetProceduralMeshLibrary::CopyProceduralMeshFromStaticMeshComponent(UStaticMeshComponent* StaticMeshComponent, int32 LODIndex, UProceduralMeshComponent* ProcMeshComponent, bool bCreateCollision)
{
if( StaticMeshComponent != nullptr &&
StaticMeshComponent->GetStaticMesh() != nullptr &&
ProcMeshComponent != nullptr )
{
UStaticMesh* StaticMesh = StaticMeshComponent->GetStaticMesh();
//// MESH DATA
int32 NumSections = StaticMesh->GetNumSections(LODIndex);
for (int32 SectionIndex = 0; SectionIndex < NumSections; SectionIndex++)
{
// Buffers for copying geom data
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FVector2D> UVs1;
TArray<FVector2D> UVs2;
TArray<FVector2D> UVs3;
TArray<FProcMeshTangent> Tangents;
// Get geom data from static mesh
GetSectionFromStaticMesh(StaticMesh, LODIndex, SectionIndex, Vertices, Triangles, Normals, UVs, Tangents);
// Create section using data
TArray<FLinearColor> DummyColors;
ProcMeshComponent->CreateMeshSection_LinearColor(SectionIndex, Vertices, Triangles, Normals, UVs, UVs1, UVs2, UVs3, DummyColors, Tangents, bCreateCollision);
}
//// SIMPLE COLLISION
// Clear any existing collision hulls
ProcMeshComponent->ClearCollisionConvexMeshes();
if (StaticMesh->GetBodySetup() != nullptr)
{
// Iterate over all convex hulls on static mesh..
const int32 NumConvex = StaticMesh->GetBodySetup()->AggGeom.ConvexElems.Num();
for (int ConvexIndex = 0; ConvexIndex < NumConvex; ConvexIndex++)
{
// Copy convex verts to ProcMesh
FKConvexElem& MeshConvex = StaticMesh->GetBodySetup()->AggGeom.ConvexElems[ConvexIndex];
ProcMeshComponent->AddCollisionConvexMesh(MeshConvex.VertexData);
}
}
//// MATERIALS
for (int32 MatIndex = 0; MatIndex < StaticMeshComponent->GetNumMaterials(); MatIndex++)
{
ProcMeshComponent->SetMaterial(MatIndex, StaticMeshComponent->GetMaterial(MatIndex));
}
}
}
```
```c++
void UKismetProceduralMeshLibrary::GetSectionFromStaticMesh(UStaticMesh* InMesh, int32 LODIndex, int32 SectionIndex, TArray<FVector>& Vertices, TArray<int32>& Triangles, TArray<FVector>& Normals, TArray<FVector2D>& UVs, TArray<FProcMeshTangent>& Tangents)
{
if( InMesh != nullptr )
{
if (!InMesh->bAllowCPUAccess)
{
FMessageLog("PIE").Warning()
->AddToken(FTextToken::Create(LOCTEXT("GetSectionFromStaticMeshStart", "Calling GetSectionFromStaticMesh on")))
->AddToken(FUObjectToken::Create(InMesh))
->AddToken(FTextToken::Create(LOCTEXT("GetSectionFromStaticMeshEnd", "but 'Allow CPU Access' is not enabled. This is required for converting StaticMesh to ProceduralMeshComponent in cooked builds.")));
}
if (InMesh->GetRenderData() != nullptr && InMesh->GetRenderData()->LODResources.IsValidIndex(LODIndex))
{
const FStaticMeshLODResources& LOD = InMesh->GetRenderData()->LODResources[LODIndex];
if (LOD.Sections.IsValidIndex(SectionIndex))
{
// Empty output buffers
Vertices.Reset();
Triangles.Reset();
Normals.Reset();
UVs.Reset();
Tangents.Reset();
// Map from vert buffer for whole mesh to vert buffer for section of interest
TMap<int32, int32> MeshToSectionVertMap;
const FStaticMeshSection& Section = LOD.Sections[SectionIndex];//获取指定的MeshSection
const uint32 OnePastLastIndex = Section.FirstIndex + Section.NumTriangles * 3;//计算最后一个VertexIndex
FIndexArrayView Indices = LOD.IndexBuffer.GetArrayView();//获得IndexArray
//遍历所有IndexBuffer并且复制顶点
for (uint32 i = Section.FirstIndex; i < OnePastLastIndex; i++)
{
uint32 MeshVertIndex = Indices[i];//取得VertexIndex
// See if we have this vert already in our section vert buffer, and copy vert in if not
// 从VertexBuffers.StaticMeshVertexBuffer中读取并且添加Vertex Position、Normal、UVs、Tangents构建MeshToSectionVertMap作为缓存如果能在Map找到则直接返回Index。
int32 SectionVertIndex = GetNewIndexForOldVertIndex(MeshVertIndex, MeshToSectionVertMap, LOD.VertexBuffers, Vertices, Normals, UVs, Tangents);
// Add to index buffer
Triangles.Add(SectionVertIndex);
}
}
}
}
}
static int32 GetNewIndexForOldVertIndex(int32 MeshVertIndex, TMap<int32, int32>& MeshToSectionVertMap, const FStaticMeshVertexBuffers& VertexBuffers, TArray<FVector>& Vertices, TArray<FVector>& Normals, TArray<FVector2D>& UVs, TArray<FProcMeshTangent>& Tangents)
{
int32* NewIndexPtr = MeshToSectionVertMap.Find(MeshVertIndex);
if (NewIndexPtr != nullptr)
{
return *NewIndexPtr;
}
else
{
// Copy position
int32 SectionVertIndex = Vertices.Add((FVector)VertexBuffers.PositionVertexBuffer.VertexPosition(MeshVertIndex));
// Copy normal
Normals.Add(FVector4(VertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(MeshVertIndex)));
check(Normals.Num() == Vertices.Num());
// Copy UVs
UVs.Add(FVector2D(VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(MeshVertIndex, 0)));
check(UVs.Num() == Vertices.Num());
// Copy tangents
FVector4 TangentX = (FVector4)VertexBuffers.StaticMeshVertexBuffer.VertexTangentX(MeshVertIndex);
FProcMeshTangent NewTangent(TangentX, TangentX.W < 0.f);
Tangents.Add(NewTangent);
check(Tangents.Num() == Vertices.Num());
MeshToSectionVertMap.Add(MeshVertIndex, SectionVertIndex);
return SectionVertIndex;
}
}
```
Editor转换成StaticMesh逻辑
FProceduralMeshComponentDetails::ClickedOnConvertToStaticMesh()
https://forums.unrealengine.com/t/procedural-mesh-not-saving-all-of-its-sections-to-static-mesh/382319/10
感觉链接中的这个代码是正确:
```c++
//Hallo from Unreal Forums
UStaticMesh* UWeedFarmerBFL::SaveProcmesh(UProceduralMeshComponent* ProcMesh, FString SavePath, FString Name)
{
UProceduralMeshComponent* ProcMeshComp = ProcMesh;
if (ProcMeshComp != nullptr)
{
FString PackageName = SavePath;
FRawMesh RawMesh;
TArray<UMaterialInterface*> MeshMaterials;
const int32 NumSections = ProcMeshComp->GetNumSections();
int32 VertexBase = 0;
for (int32 SectionIdx = 0; SectionIdx < NumSections; SectionIdx++)
{
FProcMeshSection* ProcSection = ProcMeshComp->GetProcMeshSection(SectionIdx);
// Copy verts
for (FProcMeshVertex& Vert : ProcSection->ProcVertexBuffer)
{
RawMesh.VertexPositions.Add(FVector3f(Vert.Position));
}
// Copy 'wedge' info
int32 NumIndices = ProcSection->ProcIndexBuffer.Num();
for (int32 IndexIdx = 0; IndexIdx < NumIndices; IndexIdx++)
{
int32 Index = ProcSection->ProcIndexBuffer[IndexIdx];
RawMesh.WedgeIndices.Add(Index + VertexBase);
FProcMeshVertex& ProcVertex = ProcSection->ProcVertexBuffer[Index];
FVector3f TangentX = FVector3f(ProcVertex.Tangent.TangentX);
FVector3f TangentZ = FVector3f(ProcVertex.Normal);
FVector3f TangentY = FVector3f(
(TangentX ^ TangentZ).GetSafeNormal() * (ProcVertex.Tangent.bFlipTangentY ? -1.f : 1.f));
RawMesh.WedgeTangentX.Add(TangentX);
RawMesh.WedgeTangentY.Add(TangentY);
RawMesh.WedgeTangentZ.Add(TangentZ);
RawMesh.WedgeTexCoords[0].Add(FVector2f(ProcVertex.UV0));
RawMesh.WedgeColors.Add(ProcVertex.Color);
}
// copy face info
int32 NumTris = NumIndices / 3;
for (int32 TriIdx = 0; TriIdx < NumTris; TriIdx++)
{
RawMesh.FaceMaterialIndices.Add(SectionIdx);
RawMesh.FaceSmoothingMasks.Add(0); // Assume this is ignored as bRecomputeNormals is false
}
// Remember material
MeshMaterials.Add(ProcMeshComp->GetMaterial(SectionIdx));
// Update offset for creating one big index/vertex buffer
VertexBase += ProcSection->ProcVertexBuffer.Num();
}
// If we got some valid data.
if (RawMesh.VertexPositions.Num() > 3 && RawMesh.WedgeIndices.Num() > 3)
{
// Then find/create it.
UPackage* Package = CreatePackage(*PackageName);
check(Package);
// Create StaticMesh object
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(Package, FName(*Name), RF_Public | RF_Standalone);
StaticMesh->InitResources();
FGuid::NewGuid() = StaticMesh->GetLightingGuid();
//StaticMesh->GetLightingGuid() = FGuid::NewGuid();
// Create a Source Model then set it to variable
StaticMesh->AddSourceModel();
FStaticMeshSourceModel& SrcModel = StaticMesh->GetSourceModel(0);
// Add source to new StaticMesh
SrcModel.BuildSettings.bRecomputeNormals = false;
SrcModel.BuildSettings.bRecomputeTangents = false;
SrcModel.BuildSettings.bRemoveDegenerates = false;
SrcModel.BuildSettings.bUseHighPrecisionTangentBasis = false;
SrcModel.BuildSettings.bUseFullPrecisionUVs = false;
SrcModel.BuildSettings.bGenerateLightmapUVs = true;
SrcModel.BuildSettings.SrcLightmapIndex = 0;
SrcModel.BuildSettings.DstLightmapIndex = 1;
SrcModel.RawMeshBulkData->SaveRawMesh(RawMesh);
// Copy materials to new mesh
for (UMaterialInterface* Material : MeshMaterials)
{
StaticMesh->GetStaticMaterials().Add(FStaticMaterial(Material));
}
//Set the Imported version before calling the build
StaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion;
// Build mesh from source
StaticMesh->Build(false);
StaticMesh->PostEditChange();
// Notify asset registry of new asset
FAssetRegistryModule::AssetCreated(StaticMesh);
return StaticMesh;
}
}
return nullptr;
}
```
# RuntimeFBXImport

View File

@@ -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

View File

@@ -0,0 +1,186 @@
#### 前言
因为GameplayAbility属于蓝图c++混合编程框架所以对蓝图类与地图进行版本管理是十分重要的事情。所以本文将在这里介绍git的二进制文件版本管理方案。
#### 使用过程
1. 下载Gitlfshttps://git-lfs.github.com/~~(现在的git都自带lfs就算没有下个SourceTree也会自带lfs)
2. 使用cmdcd到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工具
Mergehttps://github.com/KennethBuijssen/MergeAssist
Diffhttps://github.com/SRombauts/UE4GitPlugin
# Git Clone --depth 1 以及转化成完整仓库
git clone --depth 1 https://github.com/EpicGames/UnrealEngine.git
git clone --depth 1 --branch 4.25-plus https://github.com/EpicGames/UnrealEngine.git
转换成完整仓库的方法:
git pull --unshallow
再执行命令修改fetch设置获取所有分支即可。
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
# 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 objects are missing on push[](https://docs.gitlab.com/ee/topics/git/lfs/troubleshooting.html#lfs-objects-are-missing-on-push "Permalink")
GitLab checks files on push to detect LFS pointers. If it detects LFS pointers, GitLab tries to verify that those files already exist in LFS. If you use a separate server for Git LFS, and you encounter this problem:
1. Verify you have installed Git LFS locally.
2. Consider a manual push with `git lfs push --all`.
### 强制上传LFS
git lfs push origin --all
### I/O timeout when pushing LFS objects[](https://docs.gitlab.com/ee/topics/git/lfs/troubleshooting.html#io-timeout-when-pushing-lfs-objects "Permalink")
If your network conditions are unstable, the Git LFS client might time out when trying to upload files. You might see errors like:
```
LFS: Put "http://example.com/root/project.git/gitlab-lfs/objects/<OBJECT-ID>/15":
read tcp your-instance-ip:54544->your-instance-ip:443: i/o timeout
error: failed to push some refs to 'ssh://example.com:2222/root/project.git'
```
To fix this problem, set the client activity timeout a higher value. For example, to set the timeout to 60 seconds:
```
git config lfs.activitytimeout 60
```
# 引擎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
```

View File

@@ -0,0 +1,45 @@
---
title: Untitled
date: 2024-11-05 16:57:23
excerpt:
tags:
rating: ⭐
---
# 前言
- StaticMesh:https://zhuanlan.zhihu.com/p/702589732
- 其他代码
- [RuntimeStaticMeshImporter](https://github.com/RianeDev/RuntimeStaticMeshImporter)
- [RuntimeMeshLoader](https://github.com/Chrizey91/RuntimeMeshLoader)
- [[Assimp & UProceduralMeshComponent实时载入StaticMesh]]
## 相关函数
```text
bool FSceneImporter::ProcessMeshData(FAssetData& MeshData)
UStaticMesh::BuildFromMeshDescription
```
```c++
FDatasmithMeshElementPayload MeshPayload;
{
if (!Translator->LoadStaticMesh(MeshElement, MeshPayload))
{ // If mesh cannot be loaded, add scene's resource path if valid and retry
bool bSecondTrySucceeded = false;
if (FPaths::DirectoryExists(SceneElement->GetResourcePath()) && FPaths::IsRelative(MeshElement->GetFile()))
{ MeshElement->SetFile( *FPaths::Combine(SceneElement->GetResourcePath(), MeshElement->GetFile()) );
bSecondTrySucceeded = Translator->LoadStaticMesh(MeshElement, MeshPayload);
}
if (!bSecondTrySucceeded)
{ // #ueent_datasmithruntime: TODO : Update FAssetFactory
ActionCounter.Add(MeshData.Referencers.Num());
FAssetRegistry::UnregisteredAssetsData(StaticMesh, SceneKey, [](FAssetData& AssetData) -> void
{
AssetData.AddState(EAssetState::Completed);
AssetData.Object.Reset();
});
UE_LOG(LogDatasmithRuntime, Warning, TEXT("CreateStaticMesh: Loading file %s failed. Mesh element %s has not been imported"), MeshElement->GetFile(), MeshElement->GetLabel());
return true;
} }}
TArray< FMeshDescription >& MeshDescriptions = MeshPayload.LodMeshes;
```

View File

@@ -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案例
![](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2021/06/ue_treasurechests.jpg?w=773&ssl=1)
下面是直接从项目中取出的宝箱。请注意在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();
}
```

View 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
```

View File

@@ -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&amp; Value,
const FString&amp; 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&amp; 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));
```

View File

@@ -0,0 +1,8 @@
---
title: 判断物体是否在屏幕中的方法
date: 2022-12-09 14:08:26
excerpt:
tags: Camera
rating: ⭐
---
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/ConvertWorldLocationToScreenLocation.jpeg)

BIN
03-UnrealEngine/Gameplay/Gameplay/各平台手柄图片.jpg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,17 @@
---
title: UE5 Http相关
date: 2023-11-30 21:31:42
excerpt:
tags:
rating: ⭐
---
# 案例
https://dev.epicgames.com/community/learning/tutorials/ZdXD/call-rest-api-using-http-json-from-ue5-c
# Base64
https://zhuanlan.zhihu.com/p/344540241
# Gzip
https://forums.unrealengine.com/t/why-does-fcompression-not-support-gzip-for-uncompressmemory/357255/3
https://github.com/gtreshchev/RuntimeArchiver

View File

@@ -0,0 +1,8 @@
---
title: UE5 protobuf
date: 2024-04-16 17:42:31
excerpt:
tags:
rating: ⭐
---
在虚幻中使用protobuf进阶之μpb https://zhuanlan.zhihu.com/p/685359558

View 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模块。

View 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控件中设置参插槽参数
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lyra/Lyra_W_ShootHUDLayout.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lyra/Lyra_W_ShootHUDLayout_WidgetSetting.png)
在LyraExperienceActionSet中设置AddWidgetsLayoutClass、LayoutIDWidgetClass、SlotID最后会通过`UCommonUIExtensions::PushContentToLayer_ForPlayer`往屏幕上增加Widget。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lyra/Lyra_LyraExperienceActionSet.png)
这样设计的其中一个优点在于实现解耦例如在相关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 UIhttps://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 IDint32 ID、MessageChannelFGameplayTag 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为其封装的蓝图节点。

View 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

View 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。
- FPickupInstanceInstance。
- 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装备定义。
- FLyraEquipmentActorToSpawnSpawn用的相关数据。
- ULyraPickupDefinitionUDataAsset。主要是有ULyraInventoryItemDefinition以及其他美术资产指针。
- ULyraWeaponPickupDefinitionULyraPickupDefinition的子类储存Spawn用的LocationOffset与Scale数据。
- ULyraGameplayAbility_FromEquipmentEquipment类型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在Fragmentstat里被调用用于设置初始的话数量RemoveStatTagCount则在 GameplayEffect的cost中被调用。
PS.可以用来记录 血药的使用量重量32000 用一次=》30000

View File

@@ -0,0 +1,134 @@
---
title: UE5 Lyra学习笔记(5)—QuickBar与Equipment
date: 2022-09-16 11:03:03
excerpt:
tags:
rating: ⭐
---
## 前言
## ULyraQuickBarComponent
继承自UControllerComponentModularGame的给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]`获取到ItemInstanceItemInstance通过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来设置显示TextMagazineAmmo、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

View File

@@ -0,0 +1,135 @@
---
title: UE5 Lyra学习笔记(6)—CommonUI
date: 2024-03-22 15:03:59
excerpt:
tags:
rating: ⭐
---
# 前言
- 全局管理类
- UGameUIManagerSubsystem父类为UGameInstanceSubsystem主要的功能是管理CurrentPolicy(UGameUIPolicy)。
- UGameUIPolicy将UPrimaryGameLayout添加到Viewport中。
- UPrimaryGameLayout
- Widgets
使用[[#GameplayAbility]]检测玩家输入并且开启UI。
# UPrimaryGameLayout
父类为UCommonUserWidget。主要功能为UILayer以及Dormant状态管理。
## UILayer
使用`TMap<FGameplayTag, TObjectPtr<UCommonActivatableWidgetContainerBase>> Layers;`进行多UI层的管理不同Layer使用GameplayTag进行区分。Lyra的UILayer分为以下4个层
- UI.Layer.Game
- UI.Layer.GameMenu
- UI.Layer.Menu
- UI.Layer.Modal
```c++
template <typename ActivatableWidgetT = UCommonActivatableWidget>
TSharedPtr<FStreamableHandle> PushWidgetToLayerStackAsync(FGameplayTag LayerName, bool bSuspendInputUntilComplete, TSoftClassPtr<UCommonActivatableWidget> ActivatableWidgetClass, TFunction<void(EAsyncWidgetLayerState, ActivatableWidgetT*)> StateFunc)
{
static_assert(TIsDerivedFrom<ActivatableWidgetT, UCommonActivatableWidget>::IsDerived, "Only CommonActivatableWidgets can be used here");
static FName NAME_PushingWidgetToLayer("PushingWidgetToLayer");
const FName SuspendInputToken = bSuspendInputUntilComplete ? UCommonUIExtensions::SuspendInputForPlayer(GetOwningPlayer(), NAME_PushingWidgetToLayer) : NAME_None;
FStreamableManager& StreamableManager = UAssetManager::Get().GetStreamableManager();
TSharedPtr<FStreamableHandle> StreamingHandle = StreamableManager.RequestAsyncLoad(ActivatableWidgetClass.ToSoftObjectPath(), FStreamableDelegate::CreateWeakLambda(this,
[this, LayerName, ActivatableWidgetClass, StateFunc, SuspendInputToken]()
{
UCommonUIExtensions::ResumeInputForPlayer(GetOwningPlayer(), SuspendInputToken);
ActivatableWidgetT* Widget = PushWidgetToLayerStack<ActivatableWidgetT>(LayerName, ActivatableWidgetClass.Get(), [StateFunc](ActivatableWidgetT& WidgetToInit) {
StateFunc(EAsyncWidgetLayerState::Initialize, &WidgetToInit);
});
StateFunc(EAsyncWidgetLayerState::AfterPush, Widget);
})
);
// Setup a cancel delegate so that we can resume input if this handler is canceled.
StreamingHandle->BindCancelDelegate(FStreamableDelegate::CreateWeakLambda(this,
[this, StateFunc, SuspendInputToken]()
{
UCommonUIExtensions::ResumeInputForPlayer(GetOwningPlayer(), SuspendInputToken);
StateFunc(EAsyncWidgetLayerState::Canceled, nullptr);
})
);
return StreamingHandle;
}
template <typename ActivatableWidgetT = UCommonActivatableWidget>
ActivatableWidgetT* PushWidgetToLayerStack(FGameplayTag LayerName, UClass* ActivatableWidgetClass)
{
return PushWidgetToLayerStack<ActivatableWidgetT>(LayerName, ActivatableWidgetClass, [](ActivatableWidgetT&) {});
}
template <typename ActivatableWidgetT = UCommonActivatableWidget>
ActivatableWidgetT* PushWidgetToLayerStack(FGameplayTag LayerName, UClass* ActivatableWidgetClass, TFunctionRef<void(ActivatableWidgetT&)> InitInstanceFunc)
{
static_assert(TIsDerivedFrom<ActivatableWidgetT, UCommonActivatableWidget>::IsDerived, "Only CommonActivatableWidgets can be used here");
if (UCommonActivatableWidgetContainerBase* Layer = GetLayerWidget(LayerName))
{
return Layer->AddWidget<ActivatableWidgetT>(ActivatableWidgetClass, InitInstanceFunc);
}
return nullptr;
}
```
### RegisterLayer
用于向PrimaryGameLayout注册UILayer。为了方便管理里面的CommonActivatabbleWidgetStack)
```c++
void RegisterLayer(UPARAM(meta = (Categories = "UI.Layer")) FGameplayTag LayerTag, UCommonActivatableWidgetContainerBase* LayerWidget);
```
Content/UI的W_OverallUILayout调用它是一个根组件为Overlay下面有4个子组件(CommonActivatabbleWidgetStack)
- GameLayer_Stack游戏内UI类似HUD。
- GameMenu_Stack游戏相关的 "菜单",例如游戏中的库存用户界面。
- Menu_Stack设置界面等。
- Model_Stack游戏内的模态确认对话框、错误对话框。
# GameplayAbility
Lyra使用2个位于Plugins/ShooterCore/Content/Input/Abilites/
- GAB_ShowWidget_WhenInputPressed
- GAB_ShowWidget_WhileInputHeld
GAB_ShowWidget_WhenInputPressed![[GAB_ShowWidget_WhenInputPressed.png|800]]
GAB_ShowWidget_WhileInputHeld略微复杂一些会多一些![[GAB_ShowWidget_WhileInputHeld.png|1200]]
## GameplayTasks
- UCancellableAsyncAction
- [[#UAsyncAction_PushContentToLayerForPlayer]]
核心函数逻辑如下主要调用UPrimaryGameLayout::PushWidgetToLayerStackAsync()
```c++
void UAsyncAction_PushContentToLayerForPlayer::Activate()
{
if (UPrimaryGameLayout* RootLayout = UPrimaryGameLayout::GetPrimaryGameLayout(OwningPlayerPtr.Get()))
{
TWeakObjectPtr<UAsyncAction_PushContentToLayerForPlayer> WeakThis = this;
StreamingHandle = RootLayout->PushWidgetToLayerStackAsync<UCommonActivatableWidget>(LayerName, bSuspendInputUntilComplete, WidgetClass, [this, WeakThis](EAsyncWidgetLayerState State, UCommonActivatableWidget* Widget) {
if (WeakThis.IsValid())
{
switch (State)
{
case EAsyncWidgetLayerState::Initialize:
BeforePush.Broadcast(Widget);
break;
case EAsyncWidgetLayerState::AfterPush:
AfterPush.Broadcast(Widget);
SetReadyToDestroy();
break;
case EAsyncWidgetLayerState::Canceled:
SetReadyToDestroy();
break;
}
}
SetReadyToDestroy();
});
}
else
{
SetReadyToDestroy();
}
}
```

View File

@@ -0,0 +1,222 @@
---
title: Untitled
date: 2025-07-25 14:49:41
excerpt:
tags:
rating: ⭐
---
# 前言
社区文章:
- [虚幻杂记4 PreLoadScreen与_LoadingScreen_](https://zhuanlan.zhihu.com/p/608502007)
- [UE5 _Lyra_项目的解析与学习三加载屏幕与RootLayoutUI创建流程](https://zhuanlan.zhihu.com/p/18123648655)
- 其他相关
- [UE4 MoviePlayer](https://zhuanlan.zhihu.com/p/346492104)
# 相关CVar参数
- CommonLoadingScreen.AlwaysShow总是显示
- CommonLoadingScreen.LogLoadingScreenReasonEveryFrame
- CommonLoadingScreen.HoldLoadingScreenAdditionalSecs载入完成后的等待时间。
# Lyra引用逻辑
ULyraExperienceManagerComponent
PreLoadMapWithContext委托会在UEngine::LoadMap()中被调用。
# 相关逻辑
- CommonLoadingScreen常规LoadingScreen。
- ***ULoadingScreenManager***:核心逻辑。
- UCommonLoadingScreenSettings各种设置参数。
- ULoadingProcessTask继承ILoadingProcessInterface接口只需覆盖ShouldShowLoadingScreen()。
- CommonStartupLoadingScreen模块游戏启动时的LoadingScreen。
- FCommonStartupLoadingScreenModuleStartupModule()、FPreLoadScreenManager::OnPreLoadScreenManagerCleanUp()绑定OnPreLoadScreenManagerCleanUp()。
- SCommonPreLoadingScreenWidgetLoadingScreen Slate控件。
## ULoadingScreenManager(UGameInstanceSubsystem)
- Initialize()绑定PreLoadMapWithContext与PostLoadMapWithWorld委托。
- Deinitialize()移除BlockInput、移除Widget、移除委托、关闭Tickable。
- ShouldCreateSubsystem()覆盖接口函数对于非Server端都会加载LoadingScreen。
- FTickableGameObject
- Tick():调用**ShouldCreateSubsystem()**计算TimeUntilNextLogHeartbeatSeconds。
- GetTickableTickType()
- IsTickable()如果GameInstance有效且拥有游戏窗口就可以进行Tick。
- GetStatId()STATGROUP_Tickables
- GetTickableGameObjectWorld()GetGameInstance()->GetWorld();
- RegisterLoadingProcessor()用于坐车任务在ULoadingProcessTask::CreateLoadingScreenProcessTask()被调用。
- UnregisterLoadingProcessor()用于卸载任务在ULoadingProcessTask::Unregister()被调用。
- **HandlePreLoadMap()**修改bCurrentlyInLoadMap为true之后调用UpdateLoadingScreen()。
- **HandlePostLoadMap()**修改bCurrentlyInLoadMap为false。
- **UpdateLoadingScreen()**通过ShouldShowLoadingScreen()来判断是ShowLoadingScreen()还是HideLoadingScreen()。并且中间使用FThreadHeartBeat::Get().MonitorCheckpointStart() / MonitorCheckpointEnd()来检查线程心跳?
- ***CheckForAnyNeedToShowLoadingScreen()***
- **ShouldShowLoadingScreen**():主要通过***CheckForAnyNeedToShowLoadingScreen()*** 判断是否应该显示LoadingScreen。如果HoldLoadingScreenAdditionalSecs大于0且当前经过时间依然小于HoldLoadingScreenAdditionalSecs也会显示LoadingScreen。
- IsShowingInitialLoadingScreen()判断是否正在显示初始化LoadingScreen。
- [[#ShowLoadingScreen()]]
- [[#HideLoadingScreen()]]
- RemoveWidgetFromViewport()从Viewport上移除LoadingScreenWidget。
- StartBlockingInput()通过FSlateApplication::Get().RegisterInputPreProcessor(MakeShareable< FLoadingScreenInputPreProcessor >(new FLoadingScreenInputPreProcessor()))来Block所有输入。
- StopBlockingInput()FSlateApplication::Get().UnregisterInputPreProcessor()接触Block。
- [[#ChangePerformanceSettings()]]:修改性能相关设置。
- TODO
- @TODO: Why can GetLocalPlayers() have nullptr entries? Can it really?
- @TODO: Test with PIE mode set to simulate and decide how much (if any) loading screen action should occur
- @TODO: Allow other things implementing ILoadingProcessInterface besides GameState/PlayerController (and owned components) to register as interested parties
- @TODO: ChangeMusicSettings (either here or using the LoadingScreenVisibilityChanged delegate)
- @TODO: Studio analytics (FireEvent_PIEFinishedLoading / tracking PIE startup time for regressions, either here or using the LoadingScreenVisibilityChanged delegate)
### ShowLoadingScreen()
```c++
void ULoadingScreenManager::ShowLoadingScreen()
{
if (bCurrentlyShowingLoadingScreen)
{
return;
}
//在Engine载入阶段不会执行后续ShowLoadingScreen逻辑
if (FPreLoadScreenManager::Get() && FPreLoadScreenManager::Get()->HasActivePreLoadScreenType(EPreLoadScreenTypes::EngineLoadingScreen))
{
return;
}
TimeLoadingScreenShown = FPlatformTime::Seconds();
bCurrentlyShowingLoadingScreen = true;
CSV_EVENT(LoadingScreen, TEXT("Show"));
const UCommonLoadingScreenSettings* Settings = GetDefault<UCommonLoadingScreenSettings>();
if (IsShowingInitialLoadingScreen())//已经显示LoadingScreen
{
UE_LOG(LogLoadingScreen, Log, TEXT("Showing loading screen when 'IsShowingInitialLoadingScreen()' is true."));
UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen);
}
else
{
UE_LOG(LogLoadingScreen, Log, TEXT("Showing loading screen when 'IsShowingInitialLoadingScreen()' is false."));
UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen);
UGameInstance* LocalGameInstance = GetGameInstance();
// LoadingScreen显示时Block所有Input
StartBlockingInput();
LoadingScreenVisibilityChanged.Broadcast(/*bIsVisible=*/ true);
// 创建LoadingScreen Widget
TSubclassOf<UUserWidget> LoadingScreenWidgetClass = Settings->LoadingScreenWidget.TryLoadClass<UUserWidget>();
if (UUserWidget* UserWidget = UUserWidget::CreateWidgetInstance(*LocalGameInstance, LoadingScreenWidgetClass, NAME_None))
{
LoadingScreenWidget = UserWidget->TakeWidget();
}
else
{
UE_LOG(LogLoadingScreen, Error, TEXT("Failed to load the loading screen widget %s, falling back to placeholder."), *Settings->LoadingScreenWidget.ToString());
LoadingScreenWidget = SNew(SThrobber);
}
// Add to the viewport at a high ZOrder to make sure it is on top of most things
// 将LoadingScreenWidget显示到Viewport上。
UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient();
GameViewportClient->AddViewportWidgetContent(LoadingScreenWidget.ToSharedRef(), Settings->LoadingScreenZOrder);
ChangePerformanceSettings(/*bEnableLoadingScreen=*/ true);
if (!GIsEditor || Settings->ForceTickLoadingScreenEvenInEditor)
{
// Tick Slate to make sure the loading screen is displayed immediately
FSlateApplication::Get().Tick();
}
}
}
```
### HideLoadingScreen()
```c++
void ULoadingScreenManager::HideLoadingScreen()
{
if (!bCurrentlyShowingLoadingScreen)
{
return;
}
//取消Block Input
StopBlockingInput();
if (IsShowingInitialLoadingScreen())
{
UE_LOG(LogLoadingScreen, Log, TEXT("Hiding loading screen when 'IsShowingInitialLoadingScreen()' is true."));
UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen);
}
else
{
UE_LOG(LogLoadingScreen, Log, TEXT("Hiding loading screen when 'IsShowingInitialLoadingScreen()' is false."));
UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen);
UE_LOG(LogLoadingScreen, Log, TEXT("Garbage Collecting before dropping load screen"));
//更新垃圾回收之间的计时器,以便在下一次机会时运行垃圾回收。
GEngine->ForceGarbageCollection(true);
RemoveWidgetFromViewport();
ChangePerformanceSettings(/*bEnableLoadingScreen=*/ false);
// Let observers know that the loading screen is done
LoadingScreenVisibilityChanged.Broadcast(/*bIsVisible=*/ false);
}
CSV_EVENT(LoadingScreen, TEXT("Hide"));
const double LoadingScreenDuration = FPlatformTime::Seconds() - TimeLoadingScreenShown;
UE_LOG(LogLoadingScreen, Log, TEXT("LoadingScreen was visible for %.2fs"), LoadingScreenDuration);
bCurrentlyShowingLoadingScreen = false;
}
```
### ChangePerformanceSettings()
```c++
void ULoadingScreenManager::ChangePerformanceSettings(bool bEnabingLoadingScreen)
{
UGameInstance* LocalGameInstance = GetGameInstance();
UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient();
//设置Shader编译模式默认为后台编译显示LoadingScreen时切换成Fast模式。
FShaderPipelineCache::SetBatchMode(bEnabingLoadingScreen ? FShaderPipelineCache::BatchMode::Fast : FShaderPipelineCache::BatchMode::Background);
//在LoadingScreen显示阶段阶段关闭世界渲染。
GameViewportClient->bDisableWorldRendering = bEnabingLoadingScreen;
//如果加载界面显示,请确保优先级为流式传输。
if (UWorld* ViewportWorld = GameViewportClient->GetWorld())
{
if (AWorldSettings* WorldSettings = ViewportWorld->GetWorldSettings(false, false))
{
WorldSettings->bHighPriorityLoadingLocal = bEnabingLoadingScreen;
}
}
if (bEnabingLoadingScreen)
{
// 当加载屏幕可见时,设置新的挂起检测超时倍数
double HangDurationMultiplier;
if (!GConfig || !GConfig->GetDouble(TEXT("Core.System"), TEXT("LoadingScreenHangDurationMultiplier"), /*out*/ HangDurationMultiplier, GEngineIni))
{
HangDurationMultiplier = 1.0;
}
FThreadHeartBeat::Get().SetDurationMultiplier(HangDurationMultiplier);
// 在加载界面显示时,请勿报告任何问题。
FGameThreadHitchHeartBeat::Get().SuspendHeartBeat();
}
else
{
// Restore the hang detector timeout when we hide the loading screen
FThreadHeartBeat::Get().SetDurationMultiplier(1.0);
// 现在加载屏幕已关闭,简历报告出现故障。
FGameThreadHitchHeartBeat::Get().ResumeHeartBeat();
}
}
```
### FPreLoadScreenManager

View 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包含多个ExperienceExperience通过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页

View File

@@ -0,0 +1,136 @@
---
title: Mass扩展笔记
date: 2024-09-12 15:32:15
excerpt:
tags:
rating: ⭐
---
#
1. 创建 UMassEntityTraitBase UMassVisualizationTrait
2. 相关的渲染设置逻辑位于UMassVisualizationComponent。
# UMassVisualizationComponent
```c++
void UMassVisualizationComponent::ConstructStaticMeshComponents()
{
AActor* ActorOwner = GetOwner();
check(ActorOwner);
UE_MT_SCOPED_WRITE_ACCESS(InstancedStaticMeshInfosDetector);
for (FMassInstancedStaticMeshInfo& Info : InstancedStaticMeshInfos)
{
// Check if it is already created
if (!Info.InstancedStaticMeshComponents.IsEmpty())
{
continue;
}
// Check if there are any specified meshes for this visual type
if(Info.Desc.Meshes.Num() == 0)
{
UE_LOG(LogMassRepresentation, Error, TEXT("No associated meshes for this intanced static mesh type"));
continue;
}
for (const FStaticMeshInstanceVisualizationMeshDesc& MeshDesc : Info.Desc.Meshes)
{
FISMCSharedData* SharedData = ISMCSharedData.Find(MeshDesc);
UInstancedStaticMeshComponent* ISMC = SharedData ? SharedData->ISMC : nullptr;
if (SharedData)
{
SharedData->RefCount += 1;
}
else
{
ISMC = NewObject<UInstancedStaticMeshComponent>(ActorOwner);
ISMC->SetStaticMesh(MeshDesc.Mesh);
for (int32 ElementIndex = 0; ElementIndex < MeshDesc.MaterialOverrides.Num(); ++ElementIndex)
{
if (UMaterialInterface* MaterialOverride = MeshDesc.MaterialOverrides[ElementIndex])
{
ISMC->SetMaterial(ElementIndex, MaterialOverride);
}
}
ISMC->SetCullDistances(0, 1000000); // @todo: Need to figure out what to do here, either LOD or cull distances.
ISMC->SetupAttachment(ActorOwner->GetRootComponent());
ISMC->SetCanEverAffectNavigation(false);
ISMC->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
ISMC->SetCastShadow(MeshDesc.bCastShadows);
ISMC->Mobility = MeshDesc.Mobility;
ISMC->SetReceivesDecals(false);
ISMC->RegisterComponent();
ISMCSharedData.Emplace(MeshDesc, FISMCSharedData(ISMC));
}
Info.InstancedStaticMeshComponents.Add(ISMC);
}
// Build the LOD significance ranges
TArray<float> AllLODSignificances;
auto UniqueInsertOrdered = [&AllLODSignificances](const float Significance)
{
int i = 0;
for (; i < AllLODSignificances.Num(); ++i)
{
// I did not use epsilon check here on purpose, because it will make it hard later meshes inside.
if (Significance == AllLODSignificances[i])
{
return;
}
if (AllLODSignificances[i] > Significance)
{
break;
}
}
AllLODSignificances.Insert(Significance, i);
};
for (const FStaticMeshInstanceVisualizationMeshDesc& MeshDesc : Info.Desc.Meshes)
{
UniqueInsertOrdered(MeshDesc.MinLODSignificance);
UniqueInsertOrdered(MeshDesc.MaxLODSignificance);
}
Info.LODSignificanceRanges.SetNum(AllLODSignificances.Num() - 1);
for (int i = 0; i < Info.LODSignificanceRanges.Num(); ++i)
{
FMassLODSignificanceRange& Range = Info.LODSignificanceRanges[i];
Range.MinSignificance = AllLODSignificances[i];
Range.MaxSignificance = AllLODSignificances[i+1];
Range.ISMCSharedDataPtr = &ISMCSharedData;
for (int j = 0; j < Info.Desc.Meshes.Num(); ++j)
{
const FStaticMeshInstanceVisualizationMeshDesc& MeshDesc = Info.Desc.Meshes[j];
const bool bAddMeshInRange = (Range.MinSignificance >= MeshDesc.MinLODSignificance && Range.MinSignificance < MeshDesc.MaxLODSignificance);
if (bAddMeshInRange)
{
Range.StaticMeshRefs.Add(MeshDesc);
}
}
}
}
}
```
设置灯光通道:
```c++
void UPrimitiveComponent::SetLightingChannels(bool bChannel0, bool bChannel1, bool bChannel2)
{
if (bChannel0 != LightingChannels.bChannel0 ||
bChannel1 != LightingChannels.bChannel1 ||
bChannel2 != LightingChannels.bChannel2)
{
LightingChannels.bChannel0 = bChannel0;
LightingChannels.bChannel1 = bChannel1;
LightingChannels.bChannel2 = bChannel2;
if (SceneProxy)
{
SceneProxy->SetLightingChannels_GameThread(LightingChannels);
}
MarkRenderStateDirty();
}
}
```

View 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里面包含着相关的NetDriverConnection以及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
GetGameInstanceCast当前自定义游戏实例=》JoinSession
### 蓝图多人设计游戏笔记
- 关卡蓝图中Beginplay中首先通过GameInstance调用TransitionToState()状态为Playing
- 用一个MainMenu关卡作为菜单界面
- PlayerState记录玩家的分数与id原生实现。Replicates=trueNetDormancy=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&amp; 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-> SetIsReplicatedtrue;
// ...
}
void APACharacterBase :: PossessedByAController * NewController
{
Super :: PossessedByNewController;
如果AbilitySystemComponent
{
AbilitySystemComponent-> InitAbilityActorInfothisthis;
}
// ASC MixedMode复制要求ASC所有者的所有者为控制器。
SetOwnerNewController;
}
// 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 :)

View 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/
## 添加模块
![](https://oscimg.oschina.net/oscnet/6501b00e721d9867e9422c057d79a3f8323.png)
## 添加配置
在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。
![](https://img-blog.csdn.net/20180203094035717?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMjk5OTk4NQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
### 配置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文件夹。

View File

@@ -0,0 +1,7 @@
---
title: Untitled
date: 2024-11-13 18:40:45
excerpt:
tags:
rating: ⭐
---

View File

@@ -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移动逻辑上调整代码沿着路径点行走即可。

View 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

View File

@@ -0,0 +1,15 @@
---
title: 未命名
date: 2025-03-07 21:10:06
excerpt:
tags:
rating: ⭐
---
# 前言
- UE5 C++工程中使用官方自带的OpenCV库插件开发:https://zhuanlan.zhihu.com/p/646105667
- 参考工程
- https://github.com/Temaran/UE4Webcamera
- 用的是OBS推流的方式所以意义不大https://github.com/wickerman123/UE4_WebcamPanel
- OpenCV
- C++使用多线程优化opencv获取摄像头图像并实时显示OpenCV_VERSION 4.6.0 https://zhuanlan.zhihu.com/p/593361913
- 怎么在 python 中实现 opencv 的 GPU 加速? https://www.zhihu.com/question/585836723/answer/2908625938

View File

@@ -0,0 +1,24 @@
---
title: TextRender显示中文的方法
date: 2023-01-28 14:01:57
excerpt:
tags: TextRender
rating: ⭐
---
# 前言
早几年学习UE的时候发现TextRender渲染中文的时候会出现□的情况一直都去解决。最近又遇到这个需求了最终在官方论坛上找到了解决方案现在分享给大家。
# 步骤
![450](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/3860c2f3ccfba6adad440ba3455d5c9e618ebd64.jpeg)
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资产即可显示中文。
![TextRender_Result.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/TextRender_Result.png)
但是可以看得出一些中文符号是没办法正常显示的,原因是**4E00-9FFF**只包含了文字没有包含中文符号。所以要么是使用英文符号来代替要么就是提高Unicode-Range的范围。

View File

@@ -0,0 +1,10 @@
---
title: UE Splash 启动画面Logo设置
date: 2022-11-27 09:27:35
excerpt:
tags:
rating: ⭐
---
位于ProjectSettings->Platforms->Windows->Splash。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/UE_Slash.png)

View 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 Unrealhttps://www.youtube.com/watch?v=UD2_TEgxkqs
## 代码中的操作
在源文件中CPP文档中我们的本地化操作需要借助FTextFText本身就是为了解决显示信息的本地化而构建进行操作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操作。
![](http://gadimg-10045137.image.myqcloud.com/20180312/5aa6511819e30.png)
创建以后会在本地化文件夹位置自动生成一个对应的文件副本然后把该文件副本改成你需要替换的本地化文件就好了注意该副本的文件名一定要与原件文件名一致这样才能自动调用到本地化版本的文件。测试和使用方法与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 提供了一种 根据不同说话者与倾听者,说出意思相同的一段话,却使用不同语音与显示字幕的一种方法。
![](https://img-blog.csdnimg.cn/20190430105849913.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTAzODU2MjQ=,size_16,color_FFFFFF,t_70)

View 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

View 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

View File

@@ -0,0 +1,21 @@
---
title: Untitled
date: 2023-08-15 18:39:16
excerpt:
tags:
rating: ⭐
---
# UBT生成解决方案
生成解决方案格式:
```bash
# bash path in Engine\Source\Programs\StandaloneApplication
$ UnrealBuildTool.exe -notinstallengine -ProjectFiles StandaloneApplication
```
之后就会在`Engine\Intermediate\ProjectFiles`出现解决方案。
比如:
```bash
cd D:\UnrealEngine\UnrealEngine5\Engine\Source\Programs\HotUpdateGuard
D:\UnrealEngine\UnrealEngine5\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.exe -notinstallengine -ProjectFiles HotUpdateGuard
```
之后在`D:\UnrealEngine\UnrealEngine5\Engine\Intermediate\ProjectFiles`出现解决方案。

View File

@@ -0,0 +1,8 @@
---
title: 设置UE5VS版本
date: 2023-08-03 12:25:48
excerpt:
tags: VisualStudio
rating: ⭐
---
![[SetUnrealVisualStudioVersion.png]]

View File

@@ -0,0 +1,184 @@
---
title: Node-Pinus
date: 2023-08-01 17:23:58
excerpt:
tags: TypeScript
rating: ⭐
---
# 前言
- Node-Pinus游戏服务器框架:https://github.com/node-pinus/pinus
- 案例:https://github.com/node-pinus/pinus/tree/master/examples/simple-example
- Pomelo的wiki:https://github.com/NetEase/pomelo/wiki/Home-in-Chinese
# ReadME
## 启动方法
1. 执行npm-install.bat或npm-install.sh
2. 编译游戏服
```
cd game-server
npm run build
```
## 启动游戏服
```bash
cd dist
node app
```
显示“all servers startup in xxx ms”即表示启动成功
## 启动网页服务器
```bash
cd web-server
node app
```
显示“Please log on http://127.0.0.1:3001/index.html”即表示启动成功
## 进入客户端网页
浏览器输入
http://127.0.0.1:3001/index.html
点击“Test Game Server”如返回“game server is ok.”即表示连接游戏服务器返回成功
## 调试游戏服务器的方法
1. 安装vscode
2. 在game-server目录启动vscode
3. 按照正常流程启动游戏服
4. 在“调试”界面选择Attach To Connector或Attach To Master
5. 按F5把调试器挂上去然后就可以断点调试了。
# Pomelo相关资料
## Pomelo工具和库介绍
pomelo 提供了一系列的工具和库供开发者使用,这些工具和库能够协助开发者更好地完成应用开发、调试以及部署等工作。这些工具和库涵盖全面,有管理控制工具,有用来做压力测试的工具,也有一些比较通用的库。
- 命令行工具pomelopomelo框架提供的一个较简单的工具该工具可以帮助开发者更便捷、更有效地进行应用开发包括创建项目、启动应用、停止应用、关闭应用等等请参考pomelo命令行工具使用。
- pomelo-clipomelo-cli是一个pomelo服务器群的管理客户端通过连接注册到master服务器可以对服务器群进行较为高级的管理如运行时动态的添加关闭服务器查看服务器的状态等等。请参考pomelo-cli更详细的文档。
- pomelo-robotpomelo-robot是一个用来对pomelo游戏框架进行性能测试的工具可以帮助开发者做一些压力测试请参考pomelo-robot更详细的文档
- pomelo-daemonpomelo-daemon 提供了一个 daemon 服务可以用这个服务来进行分布式部署以及日志收集。请参考pomelo-daemon的使用。
- pomelo-admin-webpomelo-admin-web 是 pomelo 框架中基于pomelo-admin开发的web端监控的模块可以通过 web 端的方式来对游戏服务器集群的运行状态性能日志等进行实时的监控。请参考pomelo-admin-web工具的使用。
- pomelo-syncpomelo-sync 模块是用来管理游戏进程中需要持久化的数据在内存与存储系统之间同步的。请参考pomelo sync 使用文档
- pomelo-protobufpomelo-protobuf 是对google protobuf的一个实现借助javascript的语言特性实现了类.proto文件的运行时解析并用在pomelo框架中完成对要传输消息的压缩。protobuf不仅可以用在服务端也同样可以用于web客户端。具体请参考pomelo-protobuf。
## Chat源码
这个是很简单的应用,其代码结构如下图:
![源码结构图](https://github.com/NetEase/pomelo/wiki/images/source.png)
#### [](https://github.com/NetEase/pomelo/wiki/chat%E6%BA%90%E7%A0%81%E4%B8%8B%E8%BD%BD%E4%B8%8E%E5%AE%89%E8%A3%85#game-server)game-server
game-server目录放的是所有游戏服务器的逻辑以文件app.js作为入口运行游戏的所有逻辑和功能。从图上可以看出其servers里面有三个目录分别是gateconnectorchat。在pomelo中使用路径来区分服务器类型因此三个目录代表了三种不同类型的服务器每一个目录下面可以定义handler,remote,定义了handler和remote就决定了这个服务器的行为。
- 对于gate服务器其逻辑实现代码在其gateHandler.js中它接受客户端查询connector的请求返回给客户端一个可以连接的connector的(ip,port);
- connector服务器其逻辑代码在entryHandler.js中它主要完成接受客户端的请求维护与客户端的连接路由客户端的请求到chat服务器;
- chat服务器其既有handler代码也有remote代码 handler中处理用户的send请求而remote是当有用户加入或者退出的时候由connector来发起远程调用时调用的。在remote里由于涉及到用户的加入和退出所以会有对channel的操作。
game-server 的子目录config下面是游戏服务器所用到的配置文件存放的地方配置信息使用JSON格式包含有日志master服务器和其他服务器的配置信息。除了这个pomelo所需的配置信息外一般情况下也将游戏逻辑所需要的配置信息放到这个目录下例如数据库的配置信息地图信息等。
logs子目录下存放游戏服务器产生的所有的日志信息。
#### [](https://github.com/NetEase/pomelo/wiki/chat%E6%BA%90%E7%A0%81%E4%B8%8B%E8%BD%BD%E4%B8%8E%E5%AE%89%E8%A3%85#web-server)web-server
由于我们这个聊天应用的客户端是web所以需要一个web服务器。在这个目录下主要是客户端的jscss和静态资源等等。在本例子中里面有用户登录聊天的逻辑的js文件等等。我们在这个例子教程中更多地关注的是服务器端的逻辑以及功能对于客户端我们几乎不需要怎么修改其代码直接使用默认就好。
### chat分析
我们要搭建的pomelo聊天室具有如下的运行架构
![](https://github.com/NetEase/pomelo/wiki/images/multi-chat.png)
在这个架构里前端服务器也就是connector专门负责承载连接 后端的聊天服务器则是处理具体逻辑的地方。 这样扩展的运行架构具有如下优势:
- 负载分离:这种架构将承载连接的逻辑与后端的业务处理逻辑完全分离,这样做是非常必要的, 尤其是广播密集型应用(例如游戏和聊天)。密集的广播与网络通讯会占掉大量的资源,经过分离后业务逻辑的处理能力就不再受广播的影响。
- 切换简便因为有了前、后端两层的架构用户可以任意切换频道或房间都不需要重连前端的websocket。
- 扩展性好用户数的扩展可以通过增加connector进程的数量来支撑。频道的扩展可以通过哈希分区等算法负载均衡到多台聊天服务器上。理论上这个架构可以实现频道和用户的无限扩展。
#### 客户端
聊天室的逻辑包括以下几个部分:
- 用户进入聊天室这部分逻辑负责把用户信息注册到session并让用户加入聊天室的channel。
- 用户发起聊天: 这部分包括了用户从客户端发起请求,服务端接收请求等功能。
- 广播用户的聊天: 所有在同一个聊天室的客户端收到请求并显示聊天内容。
- 用户退出: 这部分需要做一些清理工作包括session和channel的清理。
客户端首先要给gate服务器查询一个connector服务器gate给其回复一个connector的地址及端口号这里没有列出完整的代码具体的代码在路径web-server/public/js/client.js中,详细代码略去见client.js:
```javascript
function queryEntry(uid, callback) {
var route = 'gate.gateHandler.queryEntry';
// ...
}
$("#login").click(function() {
username = $("#loginUser").attr("value");
rid = $('#channelList').val();
// ...
//query entry of connection
queryEntry(username, function(host, port) {
pomelo.init({
host: host,
port: port,
log: true
}, function() {
// ...
});
});
});
```
客户端在查询到connector后需要发请求给connector服务器 第一次请求要给connector进程因为首次进入时需要绑定对应的uid信息这里略去详细代码:
```javascript
pomelo.request('connector.entryHandler.enter', {username: username, rid: rid}, function(){
// ...
});
```
当用户发起聊天的时候会请求服务chat.chatHandler.send大致代码如下:
```javascript
pomelo.request('chat.chatHandler.send', {content:msg, from: username, target: msg.target}, function(data) {
// ...
});
```
当有用户加入、离开以及发起聊天时,同房间的人将会收到服务端推送来的相应消息,这些在客户端是以回调的方式进行添加的,大致代码如下:
```javascript
pomelo.on('onAdd', function(data) {
// ...
});
pomelo.on('onLeave', function(data) {
// ...
});
pomelo.on('onChat', function(data) {
// ...
});
```
客户端的详细代码都在目录web-server/public/js/client.js文件中这里客户端的js是使用component进行管理的,详细请参阅component的参考文档。
#### 服务端
我们知道在pomelo中只要定义了一个服务器的handler和remote那么就定义了这个服务器的行为就决定了这个服务器的类型。在本例子中有三种服务器gateconnectorchat,它们完成的具体逻辑如下:
gate完成客户端对connector的查询在其handler里有其实现的代码由于在这里本例中仅仅配置了一台connector服务器因此直接返回其信息给客户端即可然后客户端就可以连接到connector了。
```javascript
handler.queryEntry = function(msg, session, next) {
var uid = msg.uid;
// ...
};
```
connector接受用户的连接完成用户的注册及绑定维护客户端session信息处理客户端的断开连接其逻辑代码在connector/handler/entryHandler.js中。大致如下
```javascript
handler.enter = function(msg, session, next) {
var self = this;
var rid = msg.rid;
var uid = msg.username + '*' + rid
var sessionService = self.app.get('sessionService');
// .....
};
```
chat服务器是执行聊天逻辑的地方它维护channel信息一个房间就是一个channel一个channel里有多个用户当有用户发起聊天的时候就会将其内容广播到整个channel。chat服务器还会接受connector的远程调用完成channel维护中的用户的加入以及离开因此chat服务器不仅定义了handler还定义了remote。当有客户端连接到connector上后connector会向chat发起远程过程调用chat会将登录的用户加到对应的channel中其大致代码为
```javascript
// chatHandler.js
handler.send = function(msg, session, next) {
var rid = session.get('rid');
var username = session.uid.split('*')[0];
// .....
};
// chatRemote.js
ChatRemote.prototype.add = function(uid, sid, name, flag, cb) {
var channel = this.channelService.getChannel(name, flag);
};
ChatRemote.prototype.kick = function(uid, sid, name) {
var channel = this.channelService.getChannel(name, false);
// ...
};
```
注意 在实现具体的Handler的时候最后需要调用next其中next的签名为 next(err, resp).如果没有出现错误那么err为空即可如果不是request请求而是notify的话则一样需要调用next此时resp参数是不需要的一般情况下如果没有错误的话就直接使用next(null)即可。
服务器配置信息在config目录下现在我们只关注servers.json, master.json。master.json配置是master服务器的配置信息包括地址端口号servers.json配置具体的应用服务器信息。在配置文件中分为development和production两种环境表示开发环境和产品环境我们在pomelo start后面可以通过-e可以指定使用哪个环境更多帮助参见pomelo start --help。

View File

@@ -0,0 +1,96 @@
---
title: 未命名
date: 2025-07-20 11:39:10
excerpt:
tags:
rating: ⭐
---
# 相关资料
- 官方文档:https://puerts.github.io/docs/puerts/unreal/manual
- 调试指南:https://puerts.github.io/docs/puerts/unreal/vscode_debug
- 脚本调用引擎API:https://puerts.github.io/docs/puerts/unreal/script_call_uclass
- 更多用法可以参考Puerts Demo https://puerts.github.io/docs/puerts/unreal/demos
- 尤其推荐***QuickStart.ts***:https://github.com/chexiongsheng/puerts_unreal_demo/blob/master/TypeScript/QuickStart.ts
- FAQ https://github.com/Tencent/puerts/blob/master/doc/unreal/zhcn/faq.md
- 哪里可以找到答案:
- https://github.com/Tencent/puerts/issues
- https://github.com/Tencent/puerts/discussions
# QuickStart
## Setup
1. 安装Nodejs v22.17.1。
2. 通过安装全局typeScrpit模块。
```bash
npm install -g typescript
```
3. 安装VSCode插件
![[Puerts_VSCode_TS.png]]
## Puerts Project Setup(跳过)
1. 下载Puerts插件:https://github.com/Tencent/puerts/releases
1. 可选版本有Nodejs一般情况下使用这个可使用NPM添加其他的库、V8纯净环境以及Quickjs包体小适合手机
2. 编译插件。
3. 进入插件目录`Plugins\Puerts`执行`node enable_puerts_module.js`
4. 在项目根目录下执行`npm init`并且添加。之后重新生成一次VS解决方案并且执行`npm install`。其中Mocha是必须安装的否则会出现找到编译后的js文件。
```json
"dependencies": {
"@types/mocha": "^10.0.10"
}
```
5. 打开工程,在引擎中点击 ue.d.ts 该功能用于生成项目、引擎符号信息生成之后就能找到相关符号了。如果想在ts文件中调用新增的蓝图&C++方法也需要点击ue.d.ts才能找到对应符号。可以阅读该文了解详细信息 https://puerts.github.io/docs/puerts/unreal/script_call_uclass
![[Puerts_UE_D_TS.png]]
6. 在`ProjectSettings - Packaging - Additional Not-Asset Directories to Package`中添加`Content/javaScript`。
# 调试方法
具体可以参考:
- Puerts Inspector指南在UE4和Unity里调试Javascript:https://zhuanlan.zhihu.com/p/359598262
调试器的选择有:
1. 在Chrome输入`devtools://devtools/bundled/inspector.html?v8only=true&ws=127.0.0.1:8080`。
2. 在Chrome输入`chrome://inspect`点击Configure...输入IP&Port后点击Inspect。
3. 使用VSCode进行调试。
1. 在`Launch Program`处点击`add Configuration`。
2. 选择`Node.js: Attach`。
3. 设置端口。
4. 点击绿色箭头即可调试。
***以下有2种添加调试入口的方式一般选择第二种勾选后启动游戏会处于冻结状态需要调试器VSCode、Chrome连上才能继续运行。如果没有设置好调试器可以打开Config\DefaultPuerts.ini把WaitDebugger改为False来跳过***
**添加调试入口方式1:自创建虚拟机模式下调试配置**
创建FJsEnv传入调试端口
```c++
//8080是调试端口 GameScript = MakeShared<puerts::FJsEnv>(std::make_unique<puerts::DefaultJSModuleLoader>(TEXT("JavaScript")), std::make_shared<puerts::FDefaultLogger>(), 8080);
```
阻塞等待调试器链接
```c++
GameScript = MakeShared<puerts::FJsEnv>(std::make_unique<puerts::DefaultJSModuleLoader>(TEXT("JavaScript")), std::make_shared<puerts::FDefaultLogger>(), 8080);
GameScript->WaitDebugger();
GameScript->Start("QuickStart", Arguments);
```
**添加调试入口方式2自动绑定模式下调试配置**
1. 菜单上选择`Edit->ProjectSettings`,打开设置页面后在`Plugins -> Puerts Setting`页面中开启调试以及设置端口。
# 开发方法
## 继承引擎类功能
类似c++的继承式开发方法。
https://puerts.github.io/docs/puerts/unreal/uclass_extends
## 蓝图mixin
一般用于给蓝图添加一些TS逻辑。
https://puerts.github.io/docs/puerts/unreal/mixin
## 直接运行脚本
通过在c++对应类中GameMode、GameInstance等调用执行脚本的函数来调用指定名称的脚本。
```c++
GameScript = MakeShared<puerts::FJsEnv>(std::make_unique<puerts::DefaultJSModuleLoader>(TEXT("JavaScript")), std::make_shared<puerts::FDefaultLogger>(), 8080);
GameScript->Start("QuickStart", Arguments);
```
可以参考:https://github.com/chexiongsheng/puerts_unreal_demo/blob/master/TypeScript/QuickStart.ts·
# NEPY的问题
NEPY插件有好几个痛点
1. 通过自动生成C++代码进行符号导出所以每次使用脚本调用新的引擎类都会产生c++编译我支援的项目中每次都会产生1000+的编译任务,相当影响团队的开发效率。
2. Python是弱类型语言编码时容易写出bug或者卡壳。
3. NEPY针对蓝图的支持比较糟糕Puerts提供了mix蓝图方案。
4. NEPY的资料太少遇到问题大概率只能自己解决。Puerts的资料相对较多有bug或者问题都可以在github向作者提问。

View File

@@ -0,0 +1,483 @@
---
title: Puerts——学习资料归纳
date: 2023-07-24 15:12:29
excerpt:
tags: Puerts TypeScript
rating: ⭐
---
# 前言
https://github.com/Tencent/puerts
- [安装方法](https://github.com/Tencent/puerts/blob/master/doc/unreal/zhcn/install.md)
- [FAQ](https://github.com/Tencent/puerts/blob/master/doc/unreal/zhcn/faq.md)
- [更新日志](https://github.com/Tencent/puerts/blob/master/doc/unreal/zhcn/changelog.md)
## Puerts
- 相关文章&视频:
- [UE引擎里头跑个nodejs服务器是怎样一种体验](https://zhuanlan.zhihu.com/p/428250631)
- [在你的ios、android应用中嵌入官方版nodejs是什么感觉](https://zhuanlan.zhihu.com/p/568969543)
- [[UnrealCircle深圳] puerts-UE下TypeScript编程插件 | 腾讯 车雄生](https://www.bilibili.com/video/BV1oB4y1A7dY/?spm_id_from=333.337.search-card.all.click&vd_source=d47c0bb42f9c72fd7d74562185cee290)
- 案例工程:
- https://github.com/chexiongsheng/puerts_unreal_demo
- 知乎文章:
- UE5 PuerTS学习与实践https://zhuanlan.zhihu.com/p/632862773
- UE4下基于V8实现的代码热刷新https://zhuanlan.zhihu.com/p/364505146
- PuerTSjs调用ue的过程https://zhuanlan.zhihu.com/p/396751427
- 基于Puerts的编辑器UI开发-Mixin的非最佳实践https://zhuanlan.zhihu.com/p/551338775
- 调试
- UE4PuerTS的js调试相关https://zhuanlan.zhihu.com/p/406387721
- Puerts Inspector指南在UE4和Unity里调试Javascripthttps://zhuanlan.zhihu.com/p/359598262
## TypeScript
- http://www.patrickzhong.com/TypeScript/zh/tutorials/typescript-in-5-minutes.html
- https://ts.xcatliu.com/
- https://jkchao.github.io/typescript-book-chinese/#why
# TypeScript Setup
安装:
```bash
npm install -g typescript
```
编译:
```bash
tsc xxx.ts
```
# Puerts
## Setup
1. 将对应版本的Puerts放入插件目录。比如puerts_nodejs的`puerts_nodejs\puerts_nodejs\Puerts`直接放到插件目录即可。
2. 编译插件。
3. 进入插件目录`Plugins\Puerts`执行`node enable_puerts_module.js`
4. 在项目根目录下执行`npm init`并且添加。之后重新生成一次VS解决方案并且执行`npm install`。其中Mocha是必须安装的否则会出现找到编译后的js文件。
```json
"dependencies": {
"@types/react": "^15.6.6",
"@types/react-reconciler": "^0.18.0",
"@types/mocha": "^7.0.1"
}
```
4. 代码写在项目目录下的`TypeScript`中,之后就会在编辑器`Blueprints/TypeScript`目录下出现资产图标。
5. ~~加入ReactUMG。~~ 非必须
1. 进入 Content/javascript 目录 npm init 创建 package.json。
2. 创建成功后,向文件中粘贴如下内容
```json
"dependencies": {
"react": "^16.11.0",
"react-reconciler": "^0.23.0"
}
```
3. 然后 npm install 一次。
4. 打开工程根目录的 tsconfig.json 在 typeRoots 中 加入 "Plugins/ReactUMG/Typing"。
6. 打开 工程,在引擎中点击 ue.d.ts 。
7. 在`ProjectSettings - Packaging - Additional Not-Asset Directories to Package`中添加`Content/javaScript`。
8. 在项目的Source下模块文件中添加`"JsEnv", "UMG", "Puerts"`,比如`PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "JsEnv", "UMG", "Puerts" });`
### Node环境设置
目前最新版本的Puerts只需修改JsEnv模块文件`JsEnv.Build.cs`里的UseNodejs为true即可。
### 调试方法
具体可以参考:
- Puerts Inspector指南在UE4和Unity里调试Javascript:https://zhuanlan.zhihu.com/p/359598262
调试器的选择有:
1. 在Chrome输入`devtools://devtools/bundled/inspector.html?v8only=true&ws=127.0.0.1:8080`。
2. 在Chrome输入`chrome://inspect`点击Configure...输入IP&Port后点击Inspect。
3. 使用VSCode进行调试。
1. 在`Launch Program`处点击`add Configuration`。
2. 选择`Node.js: Attach`。
3. 设置端口。
4. 点击绿色箭头即可调试。
#### 方式1自创建虚拟机模式下调试配置
创建FJsEnv传入调试端口
```c++
//8080是调试端口 GameScript = MakeShared<puerts::FJsEnv>(std::make_unique<puerts::DefaultJSModuleLoader>(TEXT("JavaScript")), std::make_shared<puerts::FDefaultLogger>(), 8080);
```
阻塞等待调试器链接
```c++
GameScript = MakeShared<puerts::FJsEnv>(std::make_unique<puerts::DefaultJSModuleLoader>(TEXT("JavaScript")), std::make_shared<puerts::FDefaultLogger>(), 8080);
GameScript->WaitDebugger();
GameScript->Start("QuickStart", Arguments);
```
#### 方式2自动绑定模式下调试配置
1. 菜单上选择`Edit->ProjectSettings`,打开设置页面后在`Plugins -> Puerts Setting`页面中开启调试以及设置端口。
# TypeScript
- Express TypeScript化改造:https://www.jianshu.com/p/978628a15027
## Express
#### 使用ts-node将ts文件编译在内存中
在使用`ts-node`之前需要进行全局安装
```shell
$ npm install ts-node -g
# 用ts-node直接运行项目这个库会将我们的ts文件编译成js文件保存在内存中进行引用
$ ts-node ./bin/www
# 热更新模式
$ ts-node-dev ./bin/www
```
虽然`ts-node`可以帮我们直接运行ts文件但在开发完成后部署在生产环境时还是推荐使用`tsc`打包出来的`js`文件会更加稳定。
#### 配置npm脚本
```json
"scripts": {
"start": "ts-node app.ts",
"dev": "ts-node-dev app.ts",
"build": "tsc",
"server": "node ./dist/app.js"
},
```
# Puerts
Puerts的使用方法主要为
- 继承
- 继承原生UE C++类:继承并且实现之后就会在
- Mixin
- 在C++类中调用TypeScript文件
## puerts_unreal_demo 食用方法
项目默认使用UTsGameInstance可以在cpp中修改OnStart()中调用的TS脚本文件名来查看结果。优先查看TS文件
- QuickStart
- UsingMixin
- UsingMakeUClass
- UI
- UsingWidget
- UsingReactUMG
### 蓝图相关
```javascript
//蓝图加载
//UE.Class.Load方式
//let bpClass = UE.Class.Load('/Game/StarterContent/TestBlueprint.TestBlueprint_C')
//let bpActor = UE.GameplayStatics.BeginDeferredActorSpawnFromClass(gameInstance, bpClass, undefined) as UE.Game.StarterContent.TestBlueprint.TestBlueprint_C;
blueprint.load(UE.Game.StarterContent.TestBlueprint.TestBlueprint_C);
const TestBlueprint_C = UE.Game.StarterContent.TestBlueprint.TestBlueprint_C; //别名
let bpActor = UE.GameplayStatics.BeginDeferredActorSpawnFromClass(gameInstance, TestBlueprint_C.StaticClass(), undefined) as UE.Game.StarterContent.TestBlueprint.TestBlueprint_C;
UE.GameplayStatics.FinishSpawningActor(bpActor, undefined);
bpActor.Foo(false, 8000, 9000);
//如果确定后续不需要使用TestBlueprint_C了应该unload节省内存
blueprint.unload(TestBlueprint_C);
//蓝图结构体加载
//UE.UserDefinedStruct.Load方式
//let TestStruct = UE.UserDefinedStruct.Load("UserDefinedStruct'/Game/StarterContent/TestStruct.TestStruct'");
//let testStruct = UE.NewStruct(TestStruct) as UE.Game.StarterContent.TestStruct.TestStruct;
blueprint.load(UE.Game.StarterContent.TestStruct.TestStruct);
const TestStruct = UE.Game.StarterContent.TestStruct.TestStruct;
let testStruct = new TestStruct();
testStruct.age = 10;
testStruct.speed = 5;
bpActor.Bar(testStruct);
blueprint.unload(TestStruct);
//蓝图枚举
console.log("-------------------------15---------------------------");
console.log(UE.Game.StarterContent.TestEnum.TestEnum.Blue);
console.log(UE.Game.StarterContent.TestEnum.TestEnum.Red);
console.log(UE.Game.StarterContent.TestEnum.TestEnum.Green);
```
### Delegate
```javascript
//Delegate
function MutiCast1(i) {
    console.warn("MutiCast1<<<", i);
}
function MutiCast2(i) {
    console.warn("MutiCast2>>>", i);
    actor.NotifyWithInt.Remove(MutiCast2);//调用一次后就停掉
}
actor.NotifyWithInt.Add(MutiCast1)
actor.NotifyWithInt.Add(MutiCast2)
console.log("NotifyWithString.IsBound", actor.NotifyWithString.IsBound());
console.log("NotifyWithRefString.IsBound", actor.NotifyWithRefString.IsBound());
actor.NotifyWithRefString.Bind((strRef) => {
//console.error("NotifyWithRefString");
console.log("NotifyWithRefString", $unref(strRef));
$set(strRef, "out to NotifyWithRefString");//引用参数输出
});
console.log("NotifyWithString.IsBound", actor.NotifyWithString.IsBound());
console.log("NotifyWithRefString.IsBound", actor.NotifyWithRefString.IsBound());
actor.NotifyWithStringRet.Bind((inStr) => {
return "////" + inStr;
});
actor.NotifyWithInt.Broadcast(888999);
let strRef = $ref("666");
actor.NotifyWithRefString.Execute(strRef);
console.log("out str:" + $unref(strRef));
let retStr = actor.NotifyWithStringRet.Execute("console.log('hello world')");
console.log("ret str:" + retStr);
console.log("waiting native call script...........");
//Pass JsFunction as Delegate
function IsJohn(str:string) : boolean {
return str == "John";
}
obj.PassJsFunctionAsDelegate(toManualReleaseDelegate(IsJohn));
//release after using
releaseManualReleaseDelegate(IsJohn);
//unhandledRejection
on('unhandledRejection', function(reason: any) {
console.log('unhandledRejection~~~');
});
new Promise(()=>{
throw new Error('unhandled rejection');
});
```
## 热更新方法
https://zhuanlan.zhihu.com/p/364505146
## UE C++调用Puerts脚本方法
```c++
//h
TSharedPtr<puerts::FJsEnv> GameScript;
//cpp
GameScript = MakeShared<puerts::FJsEnv>();
TArray<TPair<FString, UObject*>> Arguments;
Arguments.Add(TPair<FString, UObject*>(TEXT("GameInstance"), this));
GameScript->Start("QuickStart", Arguments);
```
```javascript
//在FJsEnv启动调用Start时传入的参数可以通过argv获取。如果是继承ue类方式这里的argv是空的
let gameInstance = (argv.getByName("GameInstance") as UE.GameInstance);
let actor =  UE.GameplayStatics.BeginDeferredActorSpawnFromClass(gameInstance, UE.MainActor.StaticClass(), undefined) as UE.MainActor;
UE.GameplayStatics.FinishSpawningActor(actor, undefined);
console.log(actor.GetName());
console.log(actor.K2_GetActorLocation().ToString());
```
## UE C++访问Puerts
通过UDynamicDelegateProxy其成员记录了绑定的虚拟机与JS函数。
# QuickStart.ts
```ts
import * as UE from 'ue'
import {$ref, $unref, $set, argv, on, toManualReleaseDelegate, releaseManualReleaseDelegate, blueprint} from 'puerts';
let obj = new UE.MainObject();
//调试器通过websocket发送断点信息可能断点生效前脚本已经执行完备可以通过debugger语句来主动触发断点
//debugger;
//成员访问
console.log("------------------------0----------------------------");
console.log("before set", obj.MyString)
obj.MyString = "PPPPP";
console.log("after set", obj.MyString)
//简单类型参数函数
console.log("------------------------1----------------------------");
let sum = obj.Add(100, 300);
console.log('sum', sum)
//复杂类型参数函数
console.log("------------------------2----------------------------");
obj.Bar(new UE.Vector(1, 2, 3));
//引用类型参数函数
console.log("------------------------3----------------------------");
let vectorRef = $ref(new UE.Vector(1, 2, 3))
obj.Bar2(vectorRef);
obj.Bar($unref(vectorRef));
//静态方法
console.log("-----------------------4-----------------------------");
let str1 = UE.JSBlueprintFunctionLibrary.GetName();
let str2 = UE.JSBlueprintFunctionLibrary.Concat(', ', str1);
UE.JSBlueprintFunctionLibrary.Hello(str2);
//扩展方法和C#的扩展方法类似
console.log("-----------------------5-----------------------------");
let v = new UE.Vector(3, 2, 1)
console.log(v.ToString());
v.Set(8, 88, 888)
console.log(v.ToString());
//静态wrap
console.log("-----------------------6-----------------------------");
let vec = new UE.Vector(1, 2, 3)
console.log('vec', vec.ToString())
vec.X = 3
vec.Y = 2
vec.Z = 1
vec.Normalize(1)
console.log('vec', vec.ToString())
console.log(vec.Projection().ToString())
console.log('vec', vec.ToString())
//枚举
console.log("-----------------------7-----------------------------");
obj.EnumTest(UE.EToTest.V1);
obj.EnumTest(UE.EToTest.V13);
//默认值
console.log("-----------------------8-----------------------------");
obj.DefaultTest();
obj.DefaultTest("hello john");
obj.DefaultTest("hello john", 1024);
obj.DefaultTest("hello john", 1024, new UE.Vector(7, 8, 9));
//定长数组
console.log("-----------------------9-----------------------------");
console.log("MyFixSizeArray.Num()", obj.MyFixSizeArray.Num())
console.log("MyFixSizeArray[32]", obj.MyFixSizeArray.Get(32))
console.log("MyFixSizeArray[33]", obj.MyFixSizeArray.Get(33))
console.log("MyFixSizeArray[34]", obj.MyFixSizeArray.Get(34))
obj.MyFixSizeArray.Set(33, 1000)
console.log("MyFixSizeArray[32]", obj.MyFixSizeArray.Get(32))
console.log("MyFixSizeArray[33]", obj.MyFixSizeArray.Get(33))
console.log("MyFixSizeArray[34]", obj.MyFixSizeArray.Get(34))
//TArray
console.log("------------------------10----------------------------");
function printTArray<T>(arr: UE.TArray<T>)
{
console.log("-----Num:", arr.Num());
for(var i=0; i < arr.Num(); i++) {
console.log(i, ":", arr.Get(i));
}
}
printTArray(obj.MyArray);
obj.MyArray.Add(888);
obj.MyArray.Set(0, 7);
printTArray(obj.MyArray);
//TSet
console.log("------------------------11----------------------------");
console.log(obj.MySet.Num())
console.log(obj.MySet.Contains("John"));
console.log(obj.MySet.Contains("Che"));
console.log(obj.MySet.Contains("Hello"));
//TMap
console.log("------------------------12----------------------------");
console.log(obj.MyMap.Get("John"))
console.log(obj.MyMap.Get("Che"))
console.log(obj.MyMap.Get("Hello"))
obj.MyMap.Add("Che", 10)
console.log(obj.MyMap.Get("Che"))
//ArrayBuffer
console.log("-------------------------13---------------------------");
let ab = obj.ArrayBuffer;
let u8a1 = new Uint8Array(ab);
for (var i = 0; i < u8a1.length; i++) {
console.log(i, u8a1[i]);
}
obj.ArrayBufferTest(ab);
obj.ArrayBufferTest(new Uint8Array(ab));
let ab2 = obj.ArrayBufferTest(new Uint8Array(ab, 5));
let u8a2 = new Uint8Array(ab2);
console.log(u8a2.length);
for (var i = 0; i < u8a2.length; i++) {
console.log(i, u8a2[i]);
}
//引擎方法
console.log("--------------------------14--------------------------");
//在FJsEnv启动调用Start时传入的参数可以通过argv获取。如果是继承ue类方式这里的argv是空的
let gameInstance = (argv.getByName("GameInstance") as UE.GameInstance);
let actor = UE.GameplayStatics.BeginDeferredActorSpawnFromClass(gameInstance, UE.MainActor.StaticClass(), undefined) as UE.MainActor;
UE.GameplayStatics.FinishSpawningActor(actor, undefined);
console.log(actor.GetName());
console.log(actor.K2_GetActorLocation().ToString());
//蓝图加载
//UE.Class.Load方式
//let bpClass = UE.Class.Load('/Game/StarterContent/TestBlueprint.TestBlueprint_C')
//let bpActor = UE.GameplayStatics.BeginDeferredActorSpawnFromClass(gameInstance, bpClass, undefined) as UE.Game.StarterContent.TestBlueprint.TestBlueprint_C;
blueprint.load(UE.Game.StarterContent.TestBlueprint.TestBlueprint_C);
const TestBlueprint_C = UE.Game.StarterContent.TestBlueprint.TestBlueprint_C; //别名
let bpActor = UE.GameplayStatics.BeginDeferredActorSpawnFromClass(gameInstance, TestBlueprint_C.StaticClass(), undefined) as UE.Game.StarterContent.TestBlueprint.TestBlueprint_C;
UE.GameplayStatics.FinishSpawningActor(bpActor, undefined);
bpActor.Foo(false, 8000, 9000);
//如果确定后续不需要使用TestBlueprint_C了应该unload节省内存
blueprint.unload(TestBlueprint_C);
//蓝图结构体加载
//UE.UserDefinedStruct.Load方式
//let TestStruct = UE.UserDefinedStruct.Load("UserDefinedStruct'/Game/StarterContent/TestStruct.TestStruct'");
//let testStruct = UE.NewStruct(TestStruct) as UE.Game.StarterContent.TestStruct.TestStruct;
blueprint.load(UE.Game.StarterContent.TestStruct.TestStruct);
const TestStruct = UE.Game.StarterContent.TestStruct.TestStruct;
let testStruct = new TestStruct();
testStruct.age = 10;
testStruct.speed = 5;
bpActor.Bar(testStruct);
blueprint.unload(TestStruct);
//蓝图枚举
console.log("-------------------------15---------------------------");
console.log(UE.Game.StarterContent.TestEnum.TestEnum.Blue);
console.log(UE.Game.StarterContent.TestEnum.TestEnum.Red);
console.log(UE.Game.StarterContent.TestEnum.TestEnum.Green);
//Delegate
console.log("--------------------------16--------------------------");
function MutiCast1(i) {
console.warn("MutiCast1<<<", i);
}
function MutiCast2(i) {
console.warn("MutiCast2>>>", i);
actor.NotifyWithInt.Remove(MutiCast2);//调用一次后就停掉
}
actor.NotifyWithInt.Add(MutiCast1)
actor.NotifyWithInt.Add(MutiCast2)
console.log("NotifyWithString.IsBound", actor.NotifyWithString.IsBound());
console.log("NotifyWithRefString.IsBound", actor.NotifyWithRefString.IsBound());
actor.NotifyWithRefString.Bind((strRef) => {
//console.error("NotifyWithRefString");
console.log("NotifyWithRefString", $unref(strRef));
$set(strRef, "out to NotifyWithRefString");//引用参数输出
});
console.log("NotifyWithString.IsBound", actor.NotifyWithString.IsBound());
console.log("NotifyWithRefString.IsBound", actor.NotifyWithRefString.IsBound());
actor.NotifyWithStringRet.Bind((inStr) => {
return "////" + inStr;
});
actor.NotifyWithInt.Broadcast(888999);
let strRef = $ref("666");
actor.NotifyWithRefString.Execute(strRef);
console.log("out str:" + $unref(strRef));
let retStr = actor.NotifyWithStringRet.Execute("console.log('hello world')");
console.log("ret str:" + retStr);
console.log("waiting native call script...........");
//Pass JsFunction as Delegate
function IsJohn(str:string) : boolean {
return str == "John";
}
obj.PassJsFunctionAsDelegate(toManualReleaseDelegate(IsJohn));
//release after using
releaseManualReleaseDelegate(IsJohn);
//unhandledRejection
on('unhandledRejection', function(reason: any) {
console.log('unhandledRejection~~~');
});
new Promise(()=>{
throw new Error('unhandled rejection');
});
```
# 打包流程
生成的js脚本不是ue资产文件(`*.asset`),需要手动设置打包。
到“项目设置/打包/Additional Not-Asset Directories to Package”把Content下的“JavaScript”目录添加进去。

View File

@@ -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结构体
- UMapPropertyTMap
- UArrayPropertyTArray
属性判断是否是数组或是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会生成UEnumstruct会生成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, &params);
    return params.ReturnValue;
}
//使用
int r=InvokeFunction(obj,"Func",123.f);
```
ProcessEvent也是UE4里事先定义好的非常方便的函数内部会自动的处理蓝图VM的问题。当然更底层的方法也可以是
```c++
//调用1
obj->ProcessEvent(func, &params);
//调用2
FFrame frame(nullptr, func, &params, nullptr, func->Children);
obj->CallFunction(frame, &params + func->ReturnValueOffset, func);
//调用3
FFrame frame(nullptr, func, &params, nullptr, func->Children);
func->Invoke(obj, frame, &params + func->ReturnValueOffset);
```
#### 运行时修改类型
让我们继续扩宽一下思路之前已经详细讲解过了各大类型对象的构造过程最后常常都是到UE4CodeGen_Private里的调用。既然我们已经知道了它运行的逻辑那我们也可以仿照着来啊我们也可以在常规的类型系统注册流程执行完之后在游戏运行的半途过程中动态的去修改类型甚至注册类型因为说到底UE4编辑器也就是一个特殊点的游戏而已啊这种方式有点类似C#的emit的方式,用代码去生成代码然后再编译。这些方式理论上都是可以通的,我来提供一些思路用法,有兴趣的朋友可以自己去实现下,代码贴出来就太长了。
1. 修改UField的MetaData信息其实可以改变字段在编辑器中的显示信息。MetaData里有哪些字段可以在ObjectMacros.h中自己查看。
2. 动态修改UField的相应的各种Flags数据比如PropertyFlagsStructFlagsClassFlags等可以达成在编辑器里动态改变其显示行为的效果。
3. 动态添加删除UEnum对象里面的Names字段就可以动态给enum添加删除枚举项了。
4. 动态地给结构或类添加反射属性字段就可以在蓝图内创建具有不定字段的结构了。当然前提是在结构里预留好属性存放的内存这样UProperty的Offset才有值可指向。这么做现在想来好像也不知道能用来干嘛。
5. 同属性一样,其实参照对了流程,也可以动态的给蓝图里暴露函数。有时候这可以达成某种加密保护的奇效。
6. 可以动态的注册新结构动态的构造出来相应的UScriptStruct其实就可以了。
7. 动态注册新类其实也是可以的只不过UClass的构造稍微要麻烦点不过也没麻烦到哪去有需求了就自然能照着源码里的流程自己实现一个流程出来。
8. 再甚至其实某种程度上的用代码动态创建蓝图节点填充蓝图VM指令其实也是可行的。只不过想了想好像一般用不着上这种大手术。

BIN
03-UnrealEngine/Gameplay/UObject/Ue4Object生命周期.jpg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,40 @@
# ClassFlags :
|Name |Feature |Trait |Value|Description |UCLASS |Related to UPROPERTY|
|------------------------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|--------------------|
|CLASS_Abstract |Blueprint | |0x00000001|指定这个类是抽象基类,不可实例化 |[Abstract](../Specifier/UCLASS/Blueprint/Abstract/Abstract.md) | |
|CLASS_Const |Blueprint |Inherit |0x00010000|该类的所有属性和函数都是const的也应该被暴露为const |[Const](../Specifier/UCLASS/Blueprint/Const/Const.md) | |
|CLASS_CompiledFromBlueprint |Blueprint | |0x00040000u|指定该类从蓝图的编译中创建 | | |
|CLASS_NewerVersionExists |Blueprint | |0x80000000u| | | |
|CLASS_NoExport |UHT | |0x00000100u|不暴露到C++头文件,不生成注册代码 |[NoExport](../Specifier/UCLASS/UHT/NoExport.md) | |
|CLASS_CustomConstructor |UHT | |0x00008000u|不创建一个默认构造函数只在C++环境下使用 |[CustomConstructor](../Specifier/UCLASS/UHT/CustomConstructor.md) | |
|CLASS_Deprecated |Editor |Inherit |0x02000000u|显示废弃警告 |[Deprecated](../Specifier/UCLASS/Development/Deprecated/Deprecated.md) | |
|CLASS_HideDropDown |Editor | |0x04000000u|类不在右键选择框中显示 |[HideDropDown](../Specifier/UCLASS/TypePicker/HideDropDown/HideDropDown.md) | |
|CLASS_EditInlineNew |Editor | |0x00001000u|对象可以通过EditinlineNew按钮构造 |[EditInlineNew](../Specifier/UCLASS/Instance/EditInlineNew/EditInlineNew.md), [NotEditInlineNew](../Specifier/UCLASS/Instance/NotEditInlineNew.md) | |
|CLASS_Hidden |Editor | |0x01000000u|不在编辑器的类浏览器和edit inline new中显示 | | |
|CLASS_CollapseCategories |Editor | |0x00002000u|属性在展示时不分目录 |[CollapseCategories](../Specifier/UCLASS/Category/CollapseCategories/CollapseCategories.md), [DontCollapseCategories](../Specifier/UCLASS/Category/DontCollapseCategories.md) | |
|CLASS_NotPlaceable |Behavior |Inherit |0x00000200u|不能被放置在场景中 |[Deprecated](../Specifier/UCLASS/Development/Deprecated/Deprecated.md), [NotPlaceable](../Specifier/UCLASS/Scene/NotPlaceable/NotPlaceable.md), [Placeable](../Specifier/UCLASS/Scene/Placeable/Placeable.md)| |
|CLASS_ReplicationDataIsSetUp |Behavior | |0x00000800u|是否在该类仍然需要调用SetUpRuntimeReplicationData | | |
|CLASS_MinimalAPI |DllExport | |0x00080000u|指定该类的最小导出,只导出获得类指针的函数 |[MinimalAPI](../Specifier/UCLASS/UHT/MinimalAPI/MinimalAPI.md) | |
|CLASS_RequiredAPI |DllExport |DefaultC++, Internal |0x00100000u|指定该类必须具有DLL导出导出所有函数和属性 |[UCLASS()](../Specifier/UCLASS/UHT/UCLASS().md) | |
| |DllExport | | | | | |
|CLASS_DefaultToInstanced |LoadConstruct |Inherit |0x00200000u|指定引用到该类的所有引用都默认创建个实例对象 |[DefaultToInstanced](../Specifier/UCLASS/Instance/DefaultToInstanced/DefaultToInstanced.md) | |
|CLASS_HasInstancedReference |LoadConstruct |Inherit |0x00800000u|类拥有组件属性 | | |
|CLASS_Parsed |LoadConstruct | |0x00000010u|成功解析完成 | | |
|CLASS_TokenStreamAssembled |LoadConstruct |DefaultC++ |0x00400000u|指定父类的TokenStream已经被成功合并到自身类上 |[UCLASS()](../Specifier/UCLASS/UHT/UCLASS().md) | |
|CLASS_LayoutChanging |LoadConstruct | | |指定该类的内存布局已经被改变因此目前还不能创建CDO | | |
|CLASS_Constructed |LoadConstruct |DefaultC++ |0x20000000u|类已经被构造完成 |[UCLASS()](../Specifier/UCLASS/UHT/UCLASS().md) | |
|CLASS_NeedsDeferredDependencyLoading|LoadConstruct |Inherit | |指定该类需要延迟依赖加载 |[NeedsDeferredDependencyLoading](../Specifier/UCLASS/Blueprint/NeedsDeferredDependencyLoading.md) | |
|CLASS_Transient |LoadConstruct |Inherit |0x00000008u|透明的,在序列化的时候被跳过 |[Transient](../Specifier/UCLASS/Serialization/Transient/Transient.md), [NonTransient](../Specifier/UCLASS/Serialization/NonTransient.md) | |
|CLASS_MatchedSerializers |LoadConstruct |DefaultC++, Internal |0x00000020u| |[UCLASS()](../Specifier/UCLASS/UHT/UCLASS().md), [MatchedSerializers](../Specifier/UCLASS/Serialization/MatchedSerializers/MatchedSerializers.md) | |
|CLASS_Native |Traits |DefaultC++ |0x00000080u|指定为原生类C++里创建的类 |[UCLASS()](../Specifier/UCLASS/UHT/UCLASS().md) | |
|CLASS_Intrinsic |Traits |DefaultC++ |0x10000000u|类在C++中定义且没有UHT生成的代码 |[Intrinsic](../Specifier/UCLASS/UHT/Intrinsic.md), [UCLASS()](../Specifier/UCLASS/UHT/UCLASS().md) | |
|CLASS_Interface |Traits | |0x00004000u|该类是一个接口 |[Interface](../Specifier/UCLASS/UHT/Interface.md) | |
|CLASS_Optional |Traits |Inherit |0x00000010u|This object type may not be available in certain context. (i.e. game runtime or in certain configuration). Optional class data is saved separately to other object types. (i.e. might use sidecar files) |[Optional](../Specifier/UCLASS/Serialization/Optional/Optional.md) | |
|CLASS_Config |Config |Inherit |0x00000004u|在构造的时候载入对象的config配置 | | |
|CLASS_DefaultConfig |Config |Inherit |0x00000002u|保存对象配置到DefaultXXX.ini而不是Local必须和CLASS_Config连用 |[DefaultConfig](../Specifier/UCLASS/Config/DefaultConfig/DefaultConfig.md) | |
|CLASS_ProjectUserConfig |Config |Inherit |0x00000040u|指定settings的config文件保存在Project/User*.ini 和CLASS_GlobalUserConfig类似 |[ProjectUserConfig](../Specifier/UCLASS/Config/ProjectUserConfig/ProjectUserConfig.md) | |
|CLASS_PerObjectConfig |Config |Inherit |0x00000400u|对每个对象进行配置,而不是在类级别 |[PerObjectConfig](../Specifier/UCLASS/Config/PerObjectConfig.md) | |
|CLASS_GlobalUserConfig |Config |Inherit |0x08000000u|类Setttings被保存到<AppData>/..../Blah.ini |[GlobalUserConfig](../Specifier/UCLASS/Config/GlobalUserConfig/GlobalUserConfig.md) | |
|CLASS_ConfigDoNotCheckDefaults |Config |Inherit |0x40000000u|指定对象配置将不会检查base/defaults ini |[ConfigDoNotCheckDefaults](../Specifier/UCLASS/Config/ConfigDoNotCheckDefaults.md) | |
|HasCustomFieldNotify | | | | |[CustomFieldNotify](../Specifier/UCLASS/UHT/CustomFieldNotify.md) | |

View File

@@ -0,0 +1,7 @@
# CLASS_Abstract
Value: 0x00000001
Description: 指定这个类是抽象基类,不可实例化
Feature: Blueprint
Status: Not started
UCLASS: Abstract (../../Specifier/UCLASS/Abstract.md)

View File

@@ -0,0 +1,7 @@
# CLASS_CollapseCategories
Value: 0x00002000u
Description: 属性在展示时不分目录
Feature: Editor
Status: Not started
UCLASS: CollapseCategories (../../Specifier/UCLASS/CollapseCategories.md), DontCollapseCategories (../../Specifier/UCLASS/DontCollapseCategories.md)

View File

@@ -0,0 +1,6 @@
# CLASS_CompiledFromBlueprint
Value: 0x00040000u
Description: 指定该类从蓝图的编译中创建
Feature: Blueprint
Status: Not started

Some files were not shown because too many files have changed in this diff Show More