This commit is contained in:
2023-06-29 11:55:02 +08:00
commit 36e95249b1
1236 changed files with 464197 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,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,139 @@
---
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/

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,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,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,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,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,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.

After

Width:  |  Height:  |  Size: 128 KiB

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,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,161 @@
#### 前言
因为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
# LFS upload missing objects 解决
输入命令,即可
```
git config --global lfs.allowincompletepush false
```
# LFS删除 很久不用的文件
使用prune命令可以删除LFS中的旧文件。
```
git lfs prune options
```
这会删除认为过旧的本地 Git LFS文件没有被引用的文件被认为是过旧的文件
当前切换的提交
一个还没有被推送的提交到远程或者任何在lfs.pruneremotetocheck设置的
一个最近的提交
默认,一个最近的提交是过去十天的任何一个提交,这是通过添加如下内容计算的:
在获取附加的Git LFS历史部分讨论过的lfs.fetchrecentrefsdays属性的值。
lfs.pruneoffsetdays属性的值默认为3
git lfs prune
你可以为配置一个持用Git LFS内容更长的时间
# don't prune commits younger than four weeks (7 + 21)
$ git config lfs.pruneoffsetdays 21
不像Git内置的垃圾回收 Git LFS内容不会自动删除因此定期执行git lfs prune来保留你本地的仓库文件大小是很正确的做法。
你可以测试在git lfs prune dry-run命令执行后有什么效果
$ git lfs prune --dry-run
✔ 4 local objects, 33 retained
4 files would be pruned (2.1 MB)
更精确地查看哪个Git LFS对象被删除可以使用git lfs prune verbose dry-run命令
$ git lfs prune --dry-run --verbose
✔ 4 local objects, 33 retained
4 files would be pruned (2.1 MB)
* 4a3a36141cdcbe2a17f7bcf1a161d3394cf435ac386d1bff70bd4dad6cd96c48 (2.0 MB)
* 67ad640e562b99219111ed8941cb56a275ef8d43e67a3dac0027b4acd5de4a3e (6.3 KB)
* 6f506528dbf04a97e84d90cc45840f4a8100389f570b67ac206ba802c5cb798f (1.7 MB)
* a1d7f7cdd6dba7307b2bac2bcfa0973244688361a48d2cebe3f3bc30babcf1ab (615.7 KB)
通过使用verbose模式输出的十六进制的字符串是被删除的Git LFS对象的SHA-256哈希值也被称作对象ID,或者OIDs。你可以使用在找到引用某个Git LFS对象的路径或者提交章节介绍的技巧去找到其他想要删除的对象。
作为一个额外安全检查工作你可以使用verify-remote选项来检查Git LFS store是否存在想要删除的Git LFS对象的拷贝。
$ git lfs prune --verify-remote
✔ 16 local objects, 2 retained, 12 verified with remote
Pruning 14 files, (1.7 MB)
✔ Deleted 14 files
这让删除过程非常的非常的缓慢但是这可以帮助你明白所有删除的对象都是可以从服务器端恢复的。你可以为你的系统用久开启verify-remote 选项这可以通过全局配置lfs.pruneverifyremotealways属性来实现。
$ git config --global lfs.pruneverifyremotealways true
或者你可以通过去掉global选项来仅仅为当前会话的仓库开启远程验证。
# Git代理
## 配置Sock5代理
git config -global http.proxy socks5://127.0.0.1:2080
git config -global https.proxy socks5://127.0.0.1:2080
### 只对github.com
git config --global http.https://github.com.proxy socks5://127.0.0.1:2080
git config --global https.https://github.com.proxy socks5://127.0.0.1:2080
### 取消代理
git config --global --unset http.https://github.com.proxy)
git config --global --unset https.https://github.com.proxy)
## 增加超时时间
git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks push -v --tags origin GamePlayDevelop:GamePlayDevelop
Pushing to https://github.com/SDHGame/SDHGame.git
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/b6/a8/b6a8fa2ba03f846f04af183bddd2e3838c8b945722b298734a14cf28fd7d1ab1?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56358->127.0.0.1:1080: i/o timeout
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/76/47/76473fed076cd6f729cf97e66e28612526a824b92019ef20e3973dc1797304e8?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56360->127.0.0.1:1080: i/o timeout
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/78/a0/78a0819db84cdd0d33aa176ae94625515059a6c88fec5c3d1e905193f65bfcdd?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56374->127.0.0.1:1080: i/o timeout
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/24/ab/24ab214470100011248f2422480e8920fb80d23493f11d9f7a598eb1b4661021?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56376->127.0.0.1:1080: i/o timeout
Uploading LFS objects: 99% (712/716), 210 MB | 760 KB/s, done.
error: failed to push some refs to 'https://github.com/SDHGame/SDHGame.git'
git config --global lfs.tlstimeout 300
git config --global lfs.activitytimeout 60
git config --global lfs.dialtimeout 600
git config --global lfs.concurrenttransfers 1
### LFS Upload Failed (miss) 文件路径
解决方案下载所有LFS数据git lfs fetch --all
### 服务器上不存在
解决方案上传指定lfs文件git lfs push origin --object-id [ID]
### 强制上传LFS
git lfs push origin --all
# 引擎Content管理
.gitignore文件中添加
```
Content/
#Content/
!*.uasset
**/Content/*
**/Content/*/*
!**/Content/EngineMaterials/
!**/Content/EngineMaterials/ToonTexture/
```
# 解决git UTF8文件乱码问题
问题:
```bash
未处理的异常System.ArgumentException: Path fragment '“Content/\351\237\263\351\242\221/Cheetah\302\240Mobile_Games_-_\347\254\254\345\215\201\344 \270\203\345\205\263\302\240Cube\302\240\345\207\240\344\275\225\350\277\267\351\230\265.uasset”包含无效的目录分隔符.
1> 在 Tools.DotNETCommon.FileSystemReference.CombineStrings(DirectoryReference BaseDirectory, String[] Fragments)
1> 在 Tools.DotNETCommon.FileReference.Combine(DirectoryReference BaseDirectory, String[] Fragments)
1> 在 UnrealBuildTool.GitSourceFileWorkingSet.AddPath(String Path)
1> 在 UnrealBuildTool.GitSourceFileWorkingSet.OutputDataReceived(Object Sender, DataReceivedEventArgs Args)
1> 在 System.Diagnostics.Process.OutputReadNotifyUser(String data)
1> 在 System.Diagnostics.AsyncStreamReader.FlushMessageQueue()
1> 在 System.Diagnostics.AsyncStreamReader.GetLinesFromStringBuilder()
1> 在 System.Diagnostics.AsyncStreamReader.ReadBuffer(IAsyncResult ar)
1> 在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
1> 在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
1> 在 System.IO.Stream.ReadWriteTask.System.Threading.Tasks .ITaskCompletionAction.Invoke(Task completingTask)
1> 在 System.Threading.Tasks.Task.FinishContinuations()
1> 在 System.Threading.Tasks.Task.Finish(Boolean bUserDelegateExecuted)
1> 在 System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
1> 在 System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
1> 在 System.Threading.ThreadPoolWorkQueue.Dispatch()
```
```bash
git config --global core.quotepath false
```

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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

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,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,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,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,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,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指令其实也是可行的。只不过想了想好像一般用不着上这种大手术。

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

View File

@@ -0,0 +1,197 @@
---
title: 大钊提供的一种获取UE Private函数的方法
date: 2022-12-09 14:51:47
excerpt: 摘要
tags:
rating: ⭐⭐
---
## Hacker.h
```c++
// Copyright (c) 2015 fjz13. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
#pragma once
#include "MedusaCorePreDeclares.h"
MEDUSA_BEGIN;
namespace Hacker
{
//used to steal class private member
template<typename Tag, typename Tag::type M>
struct PrivateMemberStealer
{
friend typename Tag::type GetPrivate(Tag) { return M; }
};
}
MEDUSA_END;
#define MEDUSA_STEAL_PRIVATE_MEMBER(className,memberType,memberName) \
namespace Medusa{namespace Hacker{\
struct className##_##memberName \
{\
typedef memberType className::*type;\
friend type GetPrivate(className##_##memberName);\
};\
template struct PrivateMemberStealer<className##_##memberName, &className::memberName>;\
}}
#define MEDUSA_REF_PRIVATE_MEMBER(obj,className,memberName) obj->*GetPrivate(::Medusa::Hacker::className##_##memberName())
#define MEDUSA_VAR_PRIVATE_MEMBER(var,obj,className,memberName) auto& var=obj->*(GetPrivate(::Medusa::Hacker::className##_##memberName()));
#define MEDUSA_STEAL_PRIVATE_FUNCTION(className,memberName,returnType,...) \
namespace Medusa{namespace Hacker{\
struct className##_##memberName \
{\
typedef returnType (className::*type)(__VA_ARGS__);\
friend type GetPrivate(className##_##memberName);\
};\
template struct PrivateMemberStealer<className##_##memberName, &className::memberName>;\
}}
#define MEDUSA_REF_PRIVATE_FUNCTION(obj,className,memberName) GetPrivate(::Medusa::Hacker::className##_##memberName())
#define MEDUSA_PRIVATE_FUNCTION_CALL(obj,className,memberName,...) {auto func=GetPrivate(::Medusa::Hacker::className##_##memberName());(obj->*func)(__VA_ARGS__);}
```
## AbcInjection.h
```c++
#pragma once
#include "AbcMagicPreCompiled.h"
#include "Core/Collection/List.h"
namespace AbcInjection
{
void SetMatrixSamples(UGeometryCacheTrack* obj, const FMatrix* MatricesPtr, int32 MatricesCount, const float* SampleTimesPtr, int32 SampleTimesCount);
void AddMatrixSample(UGeometryCacheTrack* obj, const FMatrix& Matrix, const float SampleTime);
void ReserverMatrixSampleSize(UGeometryCacheTrack* obj, int32 size);
void SetNumMaterials(UGeometryCacheTrack* obj, uint32 val);
void ReserveSamples(UGeometryCacheTrack_FlipbookAnimation* obj,uint32 count);
FGeometryCacheMeshData& MutableMeshSampleData(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 index);
void SetMeshSampleTime(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 index, float time);
FGeometryCacheMeshData& MutableMeshData(UGeometryCacheTrack_TransformAnimation* obj);
void RegisterMorphTargets(USkeletalMesh* obj,const Medusa::List<UMorphTarget*>& MorphTargets);
}
```
## AbcInjection.cpp
```c++
#include "AbcInjection.h"
#include "AbcMagicPreCompiled.h"
#include "GeometryCacheTrack.h"
#include "GeometryCacheTrackFlipbookAnimation.h"
#include "GeometryCacheTrackTransformAnimation.h"
#include "Engine/SkeletalMesh.h"
#include "Animation/MorphTarget.h"
#include "Core/Compile/Hacker.h"
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack, TArray<FMatrix>, MatrixSamples);
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack, TArray<float>, MatrixSampleTimes);
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack, uint32, NumMaterials);
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack_FlipbookAnimation, TArray<FGeometryCacheMeshData>, MeshSamples);
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack_FlipbookAnimation, TArray<float>, MeshSampleTimes);
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack_FlipbookAnimation, uint32, NumMeshSamples);
MEDUSA_STEAL_PRIVATE_MEMBER(UGeometryCacheTrack_TransformAnimation, FGeometryCacheMeshData, MeshData);
MEDUSA_STEAL_PRIVATE_MEMBER(USkeletalMesh, TArray<UMorphTarget*>, MorphTargets);
#ifdef ALEMBIC_CORE_419
MEDUSA_STEAL_PRIVATE_FUNCTION(USkeletalMesh, InvalidateRenderData, void);
#endif
namespace AbcInjection
{
void SetMatrixSamples(UGeometryCacheTrack* obj, const FMatrix* MatricesPtr, int32 MatricesCount, const float* SampleTimesPtr, int32 SampleTimesCount)
{
MEDUSA_VAR_PRIVATE_MEMBER(matrixSamples, obj, UGeometryCacheTrack, MatrixSamples);
MEDUSA_VAR_PRIVATE_MEMBER(matrixSampleTimes, obj, UGeometryCacheTrack, MatrixSampleTimes);
matrixSamples.Append(MatricesPtr, MatricesCount);
matrixSampleTimes.Append(SampleTimesPtr, SampleTimesCount);
}
void AddMatrixSample(UGeometryCacheTrack* obj, const FMatrix& Matrix, const float SampleTime)
{
MEDUSA_VAR_PRIVATE_MEMBER(matrixSamples, obj, UGeometryCacheTrack, MatrixSamples);
MEDUSA_VAR_PRIVATE_MEMBER(matrixSampleTimes, obj, UGeometryCacheTrack, MatrixSampleTimes);
matrixSamples.Add(Matrix);
matrixSampleTimes.Add(SampleTime);
}
void ReserverMatrixSampleSize(UGeometryCacheTrack* obj, int32 size)
{
MEDUSA_VAR_PRIVATE_MEMBER(matrixSamples, obj, UGeometryCacheTrack, MatrixSamples);
MEDUSA_VAR_PRIVATE_MEMBER(matrixSampleTimes, obj, UGeometryCacheTrack, MatrixSampleTimes);
matrixSamples.Reserve(size);
matrixSampleTimes.Reserve(size);
}
void SetNumMaterials(UGeometryCacheTrack* obj, uint32 val)
{
MEDUSA_REF_PRIVATE_MEMBER(obj, UGeometryCacheTrack, NumMaterials) = val;
}
void ReserveSamples(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 count)
{
MEDUSA_VAR_PRIVATE_MEMBER(meshSamples, obj, UGeometryCacheTrack_FlipbookAnimation, MeshSamples);
MEDUSA_VAR_PRIVATE_MEMBER(meshSampleTimes, obj, UGeometryCacheTrack_FlipbookAnimation, MeshSampleTimes);
MEDUSA_VAR_PRIVATE_MEMBER(numMeshSamples, obj, UGeometryCacheTrack_FlipbookAnimation, NumMeshSamples);
meshSamples.AddDefaulted(count);
meshSampleTimes.AddDefaulted(count);
numMeshSamples++;
}
FGeometryCacheMeshData& MutableMeshSampleData(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 index)
{
MEDUSA_VAR_PRIVATE_MEMBER(meshSamples, obj, UGeometryCacheTrack_FlipbookAnimation, MeshSamples);
return meshSamples[index];
}
void SetMeshSampleTime(UGeometryCacheTrack_FlipbookAnimation* obj, uint32 index, float time)
{
MEDUSA_VAR_PRIVATE_MEMBER(meshSampleTimes, obj, UGeometryCacheTrack_FlipbookAnimation, MeshSampleTimes);
meshSampleTimes[index] = time;
}
FGeometryCacheMeshData& MutableMeshData(UGeometryCacheTrack_TransformAnimation* obj)
{
MEDUSA_VAR_PRIVATE_MEMBER(meshData, obj, UGeometryCacheTrack_TransformAnimation, MeshData);
return meshData;
}
void RegisterMorphTargets(USkeletalMesh* obj, const Medusa::List<UMorphTarget*>& MorphTargets)
{
MEDUSA_VAR_PRIVATE_MEMBER(morphTargets, obj, USkeletalMesh, MorphTargets);
for (UMorphTarget* morphTarget : MorphTargets)
{
morphTarget->BaseSkelMesh = obj;
morphTarget->MarkPackageDirty();
morphTargets.Add(morphTarget);
}
obj->MarkPackageDirty();
// need to refresh the map
obj->InitMorphTargets();
// invalidate render data
#ifdef ALEMBIC_CORE_419
MEDUSA_PRIVATE_FUNCTION_CALL(obj, USkeletalMesh, InvalidateRenderData);
#endif
}
}
```

View File

@@ -0,0 +1,204 @@
---
title: LookDev流程
date: 2022-12-21 11:25:40
excerpt:
tags: LookDev
rating: ⭐⭐⭐
---
# 前言
## 工作内容
- [[#流程解析]]
- [x] [[#从拍摄真实环境照片到UE复现流程]](可选)
- [ ] [[#材质检查]]
- [[#配套的UE插件]]
- [ ] 材质等窗口中的PreviewScene设置位于Windows- PreviewSceneSettings中
- [ ] PBR 灯光
- [ ] PBR Camera
- [ ] OpenColorIO 与sRPG、Tonemapping(ACES)
- DCC软件配套设置
- [ ] UE
- [ ] Maya
- [ ] SubstancePainter
- [ ] 配套的规范文档与检测工具
- [ ] PPT与教学视频
## 相关资料
- LookDevelopment
- UnrealEngine
- [x] [Enabling a Look Development Workflow for UE4](https://www.youtube.com/watch?v=70uNXZbUJVE)
- [x] [Learn About Exterior Rendering and Vegetation Look Development in Unreal Engine](https://www.youtube.com/watch?v=Ehg4sLxOH1o)
- [ ] 李文磊:[关于建立资产统一审查预览环境的一些说明](https://www.unrealengine.com/zh-CN/tech-blog/a-few-tips-for-building-unified-assets-reviewing-enviroment)
- [ ] [安柏霖物理正确的lookdev[1/2]--光照环境](https://zhuanlan.zhihu.com/p/338352699)
- [ ] [安柏霖物理正确的lookdev[2/2]--物理矫正](https://zhuanlan.zhihu.com/p/338352994)
- [ ] [陈浮生Unreal Engine 4 Look Dev制作浅析分享](https://zhuanlan.zhihu.com/p/394608910)
- [ ] [日天UE5引擎LookDev场景](https://zhuanlan.zhihu.com/p/500774794)
- Maya
- [在maya中还原substance painter材质效果视频教程](https://zhuanlan.zhihu.com/p/22741348)
- 材质检查
- [PBRBaseColor表](https://physicallybased.info/)
- SubstancePainter
- https://substance3d.adobe.com/tutorials/courses/the-pbr-guide-part-1-zh
- https://substance3d.adobe.com/tutorials/courses/the-pbr-guide-part2-zh
- Other
- [Lighting and LookDev environment with physical units](https://www.slideshare.net/KurumiUranishi/lighting-and-lookdev-environment-with-physical-units)
- 其他人的LookDev预设
- Swats tuts
- https://www.youtube.com/watch?v=OnbZvAO4enU
- https://www.youtube.com/watch?v=YDFElKTs-Qk
- [Dušan Ković的Maya Arnold LookDev工具](https://www.youtube.com/watch?v=VgkF3AsRAYo)
- https://www.youtube.com/watch?v=oII26qMdpK8
- https://www.youtube.com/watch?v=jyq11xOp-B4
## 概念整理
开发LookDevelopment流程主要目的是**建立一套流程,统一的资产评估标准,使得生产出来的资产符合物理规律**的同时保证资产**看起来漂亮**。
不同的人对于LookDevelopment的考虑与需求也不同个人的方案如下
1. 构建一个矫正用关卡与配套蓝图工具。
1. 用作XR拍摄、三维扫描、PBR流程等光照环境矫正。
2. 构建一套多光照的室内外与影棚环境关卡与配套蓝图工具。
1. 用作资产检查。
3. 相关DCC的矫正工作
4. 构建一个,类似李文磊视频中的图表。
1. 方便在以后的创作进行环境参考。
5. 以上方案都需要设置色彩管理,保证所有的显示设备(不同色域)看起来效果一样。
参考:
1. [[#从拍摄真实环境照片到UE复现流程]](可选)
备注:
1. 建立LookDev Neutrally 场景,***建立UE调试场景最终应该让场景中的色卡呈现矫正后的标准颜色。***
1. 关键的一点就是通过Gray球的亮度确定HDR贴图的亮度。
2. 比色卡最好能贴在UI上。
3. 在其他DCC软件中还原。
# 流程解析
## 从拍摄真实环境照片到UE复现流程
视频地址:[Enabling a Look Development Workflow for UE4](https://www.youtube.com/watch?v=70uNXZbUJVE)
主要过程是通过在现实世界拍摄比色卡照片与8方向照片再使用8方向照片制作一个带有色彩管理数据的HDR Cubemap。最后再使用矫正环境关卡与内置蓝图工具进行手动环境矫正。但该流程中有部分步骤较为繁琐如果不追求极致的准确性其实是可以省略或者改进。
为什么不去网上下载HDR图片再进行矫正呢因为你没办法了解到所有的细节也无法保证颜色的正确性。所以有条件还是现场去做一下。
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_Example.png)
### 所需工具
1. Full frame DSLR with good AEB features (具有良好AEB功能的全画幅数码相机)
2. Remote trigger (遥控触发器)
3. General purpose lens, e.g 24-105 Macro (通用镜头如24-105 Macro)
4. 8mm fisheye, e.g Sigma EX DG fisheye (8毫米鱼眼镜头如适马EX DG鱼眼镜头)
5. Good quality tripod & head (优质的三脚架和云台)
6. Nodal Ninja
7. Nodal EZ-Leveller
8. 18% Grey/White balance card (18%灰/白平衡卡)
9. X-Rite ColorChecker
10. 18% grey calibration ball (18%灰色校准球)
11. Chrome calibration ball (铬制校准球)
12. Lightweight tripod & clamp (轻型三脚架和夹具)
一些额外省钱方式:
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_Cheat.png)
### 拍摄ColorCkecker与8方向矫正球照片
主要是获取所有动态范围光照信息之后再UE中进行还原。下面2个图主要介绍HDR的重要性
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_DynamicRangePhoto.png)
户外场景:
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_DynamicRangePhoto_OutDoor.png)
8方向拍摄以保证可以获得所有的信息。**注意在拍摄过程中不能修改EV值。**
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_EightDirectionPhotos.png)
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_EightDirectionPhotos_GraySphere.png)
GraySpher、ChromeSphere与比色卡需要放在中心点上。切保证摄像机正对着球的中心所以得每个球各拍一次。
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_EightDirectionPhotos_GraySphereChromeSphere.png)
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_ColorCkeck.png)
### 拍摄并且制作全景图
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_PanoramaPhoto.png)
#### 操作步骤
1. 拍摄比色卡以此来获得颜色变换矩阵取得色彩管理文件并对色差进行neutrally。
2. “标准”包围式拍摄矫正球与地面照片,并且应用上述取得的色彩管理文件。
3. 拍摄全景贴图并在PtGUI中拼接。
4. 在全景图中将主要光源与支架Mask掉通过PS的仿制图章……我认为这个不太完美![500](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_Cubemap.png)
![500](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_Cubemap_PS.png)
该团队还编写一个自动化工具对之前拍摄的8方向照片进行矫正大致用到一下库
- OpenimagelO: image read/write/processing
- Colour: (colour science lib) reference Macbeth values & space conversion
- DCRAW wrapper: RAW to TIFF conversion
- Exiftool wrapper: read/write image Exif & ICC metadata
- Pyzbar: QR code decoding
- OpenCV: simplified image read/write & image rectification
### 颜色矫正
相关软件
- Adobe LightingRoom
- Photoshop
- ColorChecker Camera Calibration
- 达芬奇
软件操作参考视频:
- https://www.youtube.com/watch?v=4DOXXhMgQAs
- https://www.youtube.com/watch?v=GD-0H_38vrM&t=390s
- https://www.bilibili.com/video/BV1p54y1B77j/?spm_id_from=333.337.search-card.all.click&vd_source=d47c0bb42f9c72fd7d74562185cee290
### UE部分
场景里需要有这2个蓝图Actor蓝图相关逻辑位于视频28:29左右位置。
- BP_LightStudioHDR环境球模型与天光
1. 能够设置HDR Image的旋转与亮度材质实例控制
2. SkyLight Capture
3. 模拟场景中的主要光源视频中演示了面光源与聚光灯视频34:00
- 色卡、矫正球模型
#### ChromeSphere矫正
这一步主要复现场景中的灯光。
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_Scene_ChromeSphereAndPhoto1.png)
![700](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_Scene_ChromeSphereAndPhoto2.png)
这一步需要确定关闭所有后处理效果。
![500](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_Scene_DisablePostProcess.png)
#### GraySphere
GraySphere 矫正需要进入HDR Eye Adaptation。主要是为了匹配照片与场景GraySphere的亮度。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/LookDev_Scene_GraySphere_HDR.png)
需要分别矫正环境光IBL与场景内主要光源。矫正环境光时需要关闭场景内其他光源比较球体暗面亮度来调节环境球HDR亮度材质
PS.但这样其实有问题,因为这些灯光亮度是直接可以测算出来的……
## 线性空间 => sRGB => Tonemapping
## Lighting and LookDev environment with physical units
## 材质检查
摘自Substance教学。
![800](https://cdn.allegorithmic.com/images/academy/7b04d72d-432f-48b3-a1b6-efd0e75f2108)
图60表面是金属吗金属/粗糙度工作流程
![800](https://cdn.allegorithmic.com/images/academy/d2eb04b9-4a7e-4ed9-aca8-949c193b8723)
图61表面是金属吗高光反射/光泽度工作流程。
# 配套的UE插件
对于美术来说只需要了解基础的概念以及掌握对应的矫正工具用法即可生产出正确的资产,对此需要开发一些插件以满足需求:
## LookDev环境模板
1. LookDev Neutrally场景
2. LookDev 模型预览场景(室内/室外)
1. 设置近剪裁面 10=>1
2. 方向光勾选Atomosphere/Fog Sun Light 与LightShaftOcclusion
3. AtomosphericFog勾选Atomopshere Affect Sun Illumination
4. 光线追踪相关控制选项。
1. 使用AutomaticMaterial 包中的比色卡
2.
### 环境
晴天、阴天和室内
室外晴天,室外阴天和室内
# DCC软件中的环境调整
- Maya
- Substance Painter

View File

@@ -0,0 +1,9 @@
---
title: MovieRenderQueue命令行渲染
date: 2022-12-21 11:28:53
excerpt:
tags: MovieRenderQueue
rating: ⭐
---
https://zhuanlan.zhihu.com/p/177696943

View File

@@ -0,0 +1,85 @@
## 前言
突然想起几个月前在Quixel发布的一篇技术文章里面归纳了几个场景物体融合方式主要是与地形融合甚是不错。隧简单翻译并分享给大家。
原文地址https://quixel.com/blog/2020/1/22/blending-megascans-assets-in-ue4?utm_campaign=Tutorials&utm_source=youtube&utm_medium=social&utm_content=Goddess%20Temple%20in%20UE4:%20Lighting%20%26%20Blending
这里使用MegaScans中的"Damp Soil"、"Cut Grass 2×2 M"与"Grass Clumps"资产。
## World aligned texturing
![](https://blog.megascans.se/wp-content/uploads/2020/01/image14-1.jpg)
直接译为**世界坐标映射贴图**。通过模型顶点的世界坐标将贴图映射到模型上。大致原理如下:
![](https://blog.megascans.se/wp-content/uploads/2020/01/image19.gif)
官方的AutomotiveMaterialPack与TwinmotionMaterialsPack材质包使用这个方法。这个方法广泛用在CAD、建筑等方面。这里介绍的主要是与地形融合的思路.
大致用法如下:
![](https://blog.megascans.se/wp-content/uploads/2020/01/image25.jpg)
上图展示了通过控制x、y值来控制贴图平铺效果。
映射到球体模型的效果如下:
![](https://blog.megascans.se/wp-content/uploads/2020/01/image16-min.gif)
一个完整的PBR材质大概是这个样子的
![](https://blog.megascans.se/wp-content/uploads/2020/01/image17.jpg)
材质实现https://blueprintue.com/blueprint/101g7t_k/
>这个方法的缺点在于平面与模型的接缝处过度十分突兀。但它的性能十分的好,配合其他方法使用能得到很好的效果。
## Pixel depth offset
![](https://blog.megascans.se/wp-content/uploads/2020/01/image5.gif)
为了解决过度突兀的问题,我们可以通过**像素深度偏移**(Pixel depth offset)配合DitherTemporalAA节点来解决这个问题。
大致用法如下:
![](https://blog.megascans.se/wp-content/uploads/2020/01/image22.png)
效果如下:
![](https://blog.megascans.se/wp-content/uploads/2020/01/image15.gif)
>仔细观察过度处可以看得出过度非常自然了。同时因为这个方法主要用到了TAA的抖动算法通过视觉暂留现象来实现半透明过度所以性能十分的好。缺点是材质Alpha较低时交界面容易产生错误的混合效果。当Alpha较高时容易产生TAA特有的鬼影现象。
![](https://blog.megascans.se/wp-content/uploads/2020/01/image13.gif)
材质实现https://blueprintue.com/blueprint/bwa_kbpc/
## Distance field mesh blending
![](https://blog.megascans.se/wp-content/uploads/2020/01/image9.gif)
该文作者认为这个可能是最佳方案。因为**距离场模型融合**是直接修改地形顶点,属于真实的过度效果。所以不会出现任何抖动问题。
大致效果如下:
https://www.bilibili.com/video/BV1ef4y1X7QZ/
{% raw %}
<iframe src="//player.bilibili.com/player.html?aid=287028163&bvid=BV1ef4y1X7QZ&cid=234182542&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
{% endraw %}
使用之前需要执行以下步骤:
1. 在Settings-Project Settings中搜索distance fields确保Generate Mesh Distance Fields选项处于勾选状态。
2. 在想要融合到地面的物体的属性中取消勾选Affect Distance Field Lighting选项。
![](https://blog.megascans.se/wp-content/uploads/2020/01/image20.png)
接下来就创建材质了:
![](https://blog.megascans.se/wp-content/uploads/2020/01/image10-1.jpg)
右边的粉色框中节点将会连入Normal槽你可以用一个参数变量来控制混合强度。因为当前材质没有混合地形与模型物体的纹理只是平滑了交接面附近的法线。所以为了得到更好的效果我们使用WorldAlignedTexture工作流程并给它们与地形接近的纹理这会达到一个更好的融合效果。
由于我们禁用了Affects Distance Field Lighting这使得该融合物体无法使用距离场AO以及距离场阴影。这个方法性能消耗较大用了曲面细分以及置换虽然会有很好的效果但如果你对性能优化以及场景的把控不足还是不建议使用。
材质实现https://blueprintue.com/blueprint/bqo9vg8i/
## Runtime virtual texturing
实时虚拟贴图RVT这个文档上都有就不翻译了。
材质实现https://blueprintue.com/blueprint/md12pcb5/
## 另一个视频中的优化技巧
这里我再追加另一个Quiexl视频中的性能技巧
- 使用Ue4的MergeActor将多个Mesh进行合并。视频2:00
- 使用MegaScans的Ue4插件将多个材质合并到一起。视频2:50
具体操作请参考https://www.bilibili.com/video/BV1Zv41117xB/
{% raw %}
<iframe src="//player.bilibili.com/player.html?aid=244561268&bvid=BV1Zv41117xB&cid=233968254&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
{% endraw %}

View File

@@ -0,0 +1,136 @@
---
title: IOS&Android Package
date: 2023-05-05 10:59:01
excerpt:
tags:
rating: ⭐
---
# 命令行
## Win
```bash
cd D:/UnrealEngine/UnrealEngine4_YueHua/Engine/Build/BatchFiles /d
RunUAT.bat BuildGraph -target="Make Installed Build Win64" -script=Engine/Build/InstalledEngineBuild.xml -set:WithDDC=true -set:SignExecutables=false -set:EmbedSrcSrvInfo=false -set:GameConfigurations=Development;Shipping -set:WithFullDebugInfo=false -set:HostPlatformEditorOnly=false -set:AnalyticsTypeOverride= -set:HostPlatformDDCOnly=true -set:WithWin64=true -set:WithMac=false -set:WithAndroid=false -set:WithIOS=false -set:WithTVOS=false -set:WithLinux=false -set:WithLinuxArm64=false -set:CompileDatasmithPlugins=false -set:WithServer=false -set:WithClient=false -set:WithHoloLens=false -clean
```
## Android
```bash
cd D:/UnrealEngine/UnrealEngine4_YueHua/Engine/Build/BatchFiles /d
RunUAT.bat BuildGraph -target="Make Installed Build Win64" -script=Engine/Build/InstalledEngineBuild.xml -set:WithDDC=true -set:SignExecutables=false -set:EmbedSrcSrvInfo=false -set:GameConfigurations=Development;Shipping -set:WithFullDebugInfo=false -set:HostPlatformEditorOnly=false -set:AnalyticsTypeOverride= -set:HostPlatformDDCOnly=false -set:WithWin64=true -set:WithMac=false -set:WithAndroid=true -set:WithIOS=false -set:WithTVOS=false -set:WithLinux=false -set:WithLinuxArm64=false -set:CompileDatasmithPlugins=false -set:WithServer=false -set:WithClient=false -set:WithHoloLens=false
RunUAT.bat BuildGraph -target="Make Installed Build Win64" -script=Engine/Build/InstalledEngineBuild.xml -set:WithDDC=true -set:SignExecutables=false -set:EmbedSrcSrvInfo=false -set:WithFullDebugInfo=false -set:HostPlatformEditorOnly=false -set:AnalyticsTypeOverride= -set:HostPlatformDDCOnly=false -set:WithWin64=true -set:WithMac=false -set:WithAndroid=true -set:WithIOS=false -set:WithTVOS=false -set:WithLinux=false -set:WithLinuxArm64=false -set:CompileDatasmithPlugins=false -set:WithServer=false -set:WithClient=false -set:WithHoloLens=false
```
```
RunUAT Turnkey -command=Verifysdk -platform=Android
```
## MAC
```bash
cd ./Desktop/UnrealEngine/Engine/Build/BatchFiles
./RunUAT.sh BuildGraph -target="Make Installed Build MAC" -script=Engine/Build/InstalledEngineBuild.xml -set:WithDDC=true -set:SignExecutables=false -set:EmbedSrcSrvInfo=false -set:GameConfigurations=Development;Shipping -set:WithFullDebugInfo=false -set:HostPlatformEditorOnly=true -set:AnalyticsTypeOverride= -set:HostPlatformDDCOnly=false -set:WithWin64=false -set:WithMac=true -set:WithAndroid=false -set:WithIOS=true -set:WithTVOS=false -set:WithLinux=false -set:WithLinuxArm64=false -set:CompileDatasmithPlugins=false -set:WithServer=false -set:WithClient=false -set:WithHoloLens=false -clean
```
# MAC&IOS
## Windows
安装环境:
- iTunes 12.10.11.2不安装IOS SDK就无法检测到。
文档:
- Building for iOS on Windows[https://docs.unrealengine.com/5.1/en-US/building-ios-projects-on-windows-in-unreal-engine/](https://docs.unrealengine.com/5.1/en-US/building-ios-projects-on-windows-in-unreal-engine/)
- Provisioning Profiles and Signing Certificates[https://docs.unrealengine.com/5.1/en-US/setting-up-ios-tvos-and-ipados-provisioning-profiles-and-signing-certificates-for-unreal-engine-projects/](https://docs.unrealengine.com/5.1/en-US/setting-up-ios-tvos-and-ipados-provisioning-profiles-and-signing-certificates-for-unreal-engine-projects/)
### GenerateProjectFiles跳过SDK问题
运行GenerateProjectFiles.bat时提示
>Some Platforms were skipped due to invalid SDK setup: Mac, IOS, Android, Linux, LinuxArm64, TVOS.
查看`Engine\Programs\UnrealBuildTool`目录下的`Log_GPF.txt`
```
Note: Android toolchain NDK r25b recommended
Registering build platform: Android - buildable: False
Registering build platform: UnrealBuildTool.IOSPlatformFactory
Registering build platform: IOS - buildable: False
Registering build platform: UnrealBuildTool.LinuxPlatformFactory
Registering build platform: Linux - buildable: False
Registering build platform: LinuxArm64 - buildable: False
Registering build platform: UnrealBuildTool.MacPlatformFactory
Registering build platform: Mac - buildable: False
Registering build platform: UnrealBuildTool.TVOSPlatformFactory
Registering build platform: TVOS - buildable: False
Registering build platform: UnrealBuildTool.WindowsPlatformFactory
Found Windows 10 SDK root at C:\Program Files (x86)\Windows Kits\10 (1)
Found Windows 10 SDK root at C:\Program Files (x86)\Windows Kits\10 (2)
```
- 问题原因
- 安装Itunes太新或者没有安装itunes。
- 解决方法
- 卸载**itunes**以及**Apple Mobile Device Support**。之后安装12.10.11.2版本的**itunes**。
### SSH登录设置
MAC
Apple
ssh bluerose@192.168.2.164
ECDSA key fingerprint is SHA256:fkRVqfbPPmbLUB4dn3QRFpATQykuu1AtlWmNLMSDXtU.
SSH还需要生成秘钥文件之后放置在对应文件夹里
### 证书与BaseEngine.ini
证书需要后缀名为p12的证书可以通过MAC的钥匙串进行转换。具体操作为
1. 选择想要导出的证书,右键点击导出。
2. 选择p12类型
3. 不要设置密码。
可以参考https://blog.51cto.com/u_15318120/3241489
>. cer证书只包含公钥p12证书可能同时包含公钥和私钥。这就是他们的区别除了xcode开发工具打包都需要p12。
需要通过编辑器先在`Platform-IOS`先导入一次证书通过后再把DefaultEngine.ini中的IOS设置加入到BaseEngine.ini中。
比如:
DefaultEngine.ini
```ini
[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
BundleDisplayName=eto
BundleName=eto
BundleIdentifier=cn.tectree.eto
MobileProvision=eto.mobileprovision
SigningCertificate=iPhone Developer: xiangtian luo (RD2564LPA4)
```
BaseEngine.ini
```ini
[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
BundleDisplayName=etoBundleName=eto
BundleIdentifier=cn.tectree.eto
MobileProvision=eto.mobileprovision
SigningCertificate=iPhone Developer: xiangtian luo (RD2564LPA4)
```
## MAC
![[安装Mac UE开发环境]]
# Android
UE5.1为准的安装环境:
- Android Studio 4.0
- Android Cmd line tool 8.0
- Android NDK r25b
- Android SDK
- Recommended: SDK 32
- Minimum for compiling UE: SDK 30
具体步骤参考https://docs.unrealengine.com/5.1/en-US/how-to-set-up-android-sdk-and-ndk-for-your-unreal-engine-development-environment/
因为打包Android时一定会运行**Extra/SetupAndroid.bat**,所以需要完全按照文档步骤进行操作,比如:
- 使用Android Studio4.0
- 使用默认的安装位置。
设置完之后需要运行`GenerateProjectFiles.bat`并且编译一下。
# 插件构建
```bash
cd D:\UnrealEngine\UE_5.2\Engine\Build\Batchfiles
RunUAT.bat BuildPlugin -plugin="D:\UnrealEngine\Project\Plugins\MultiDraw\Plugins\MultiDraw\MultiDraw.uplugin" -package="D:\MultiDraw"
```

View File

@@ -0,0 +1,63 @@
## UE插件
1. 像素化插件
- http://www.volumesoffun.com/blog/
- https://github.com/volumesoffun/cubiquity-for-unreal-engine
2. MotionBuilder Live Link:https://github.com/ue4plugins/MobuLiveLink
3. WeiXinSDKhttps://github.com/tkzcfc/WeiXinSDK
4. SpoutUE4:https://github.com/ZimaXXX/SpoutUE4
5. A fish flock AI:https://github.com/no5ix/flock-ai-ue4-plugin
7. Simple Windows:https://github.com/Jordonbc/UE4_SimpleWindows
8. Dark Souls Camera:https://github.com/donanroherty/UE4_DarkSoulsCamera
9. VOX4UPlugin:https://github.com/mik14a/VOX4UPlugin
10. AircraftHud:https://github.com/unoctanium/AircraftHud
11. ue4_mesh_deformation_toolkit:https://github.com/normalvector/ue4_mesh_deformation_toolkit
12. RuntimeMeshLoader:https://github.com/GameInstitute/RuntimeMeshLoader
13. PsReplayKit:https://github.com/PushkinStudio/PsReplayKit
14. 3dRudder SDK:https://github.com/3DRudder/3DRudderSDK_Unreal_Engine
16. protobuf:https://github.com/marshal-it/Protobuffer_UE4
17. Boost / PCL Unreal Third-Party Plugin:https://github.com/hausfrau87/Boost_PCL_UnrealThirdPartyPlugin
18. UE4-Binary-Builderhttps://github.com/ryanjon2040/UE4-Binary-Builder
19. UMGConvertSlate:https://github.com/sxh910924/UMGConvertSlate
20. TestPak:https://github.com/ArcEcho/TestPak
21. Painting-ue4:https://github.com/coding2233/Painting-ue4
22. Unreal XunFei Speech:https://github.com/303snowing/UnrealXunFeiSpeech
23. LocalSimulation:https://github.com/peterlnewton/LocalSimulation
24. Hybrid iOS Native and Unreal Engine:https://github.com/detroit-labs/UE-iOS-Hybrid
25. state machine:https://github.com/lhurt51/UE4-CppStateMachinePlugin
26. Hotline Miami Template:https://github.com/CarstenZarbock/HotlineMiamiTemplate
27. UE4-Physics-Missile:https://github.com/MitchKeenan0/UE4-Physics-Missile
28. MMTContent:https://github.com/BoredEngineer/MMT_Content
29. Spout-UE4:https://github.com/AleDel/Spout-UE4
30. Cover Generator:https://github.com/Deams51/CoverGenerator-UE4
31. GameSavePlugin:https://github.com/marshal-it/GameSavePlugin
32. Input Buffering plug-in:https://github.com/Isatin/UE4InputBuffer
33. BuoyancySystem:https://github.com/Jay2645/BuoyancySystem
34. UE4_Boost:https://github.com/NullP0inter/UE4_Boost
35. UE4-DynamicSubAnimInstance:https://github.com/crimsonstrife/UE4-DynamicSubAnimInstance
36. UE4-MySQLPlugin:https://github.com/anhanjinj/UE4-MySQLPlugin
37. AlipayOpenapiCpp:https://github.com/ArthasModern/AlipayOpenapiCpp
38. UE4AndroidCam:https://github.com/kevinjosue2326/UE4AndroidCam
39. ExtraWindow:https://github.com/crashangelbr/ExtraWindow
40. ExtraWindowCam:https://github.com/chaosgrid/ExtraCamWindow
41. KiruroboMocapPlugin:https://github.com/kirurobo/KiruroboMocapPlugin
42. MySQLConnectorUE4Plugin:https://github.com/KhArtNJava/MySQLConnectorUE4Plugin
43. VRUMGPlugin:https://github.com/mitchemmc/VRUMGPlugin
44. Simple Data Integration Plugin:https://github.com/HeliosOrg/SimpleDataIntegrationPlugin
45. MineSweeper:https://github.com/fengkan/MineSweeper
46. Runtime Mesh Component:https://github.com/Koderz/RuntimeMeshComponent
47. Climbing-Movement-Component:https://github.com/Deema35/Climbing-Movement-Component
48. socketio-client-ue4:https://github.com/getnamo/socketio-client-ue4
49. SanwuUEUtilites:https://github.com/sanwu/jjboomsky
50. Creature:https://github.com/kestrelm/Creature_UE4
51. ActionRPGGame:https://github.com/iniside/ActionRPGGame
52. FliteTTSPlugin:https://github.com/ideoservo/FliteTTSPlugin
53. Sphinx:https://github.com/shanecolb/sphinx-ue4
55. DoN AI Navigation Plugin:https://github.com/VSZue/DonAINavigation
56. Procedural mesh examples:https://github.com/SiggiG/ProceduralMeshes
57. Chartboost:https://github.com/getsetgames/Chartboost
58. MMT_Plugin:https://github.com/BoredEngineer/MMT_Plugin
59. UnrealMessageBusDemo:https://github.com/BhaaLseN/UnrealMessageBusDemo
60. RakNet plugin for Unreal Engine 4:https://github.com/yujen/RN4UE4
61. UE4Downloader:https://github.com/chaiyuntian/UE4Downloader
62. UE4ActorSaveLoad:https://github.com/shinaka/UE4ActorSaveLoad
63. DataAccess:https://github.com/afuzzyllama/DataAccessUE4

View File

@@ -0,0 +1,177 @@
---
title: 使用Python开发Maya插件学习笔记
date: 2022-11-04 10:20:38
excerpt:
tags:
rating: ⭐
---
# 前言
前段时间研究使用Maya重定向动画但一个一个手动操作还是有点心烦所以我花了2天时间学习了Pyhton并写了这个插件本人有c++、qt、JavaScript经验所以学的快在这个过程也积累了一些心得在此分享给大家。另外祝大家劳动节快乐。
以下是我写的插件一个通过HumanIK批量重定向动画的工具
https://github.com/blueroseslol/DccTool
可以帮助动画公司将biped骨骼动画批量重定向到Ue4或者其他骨骼上。里面有很多HumanIK控制代码以及文件导入与导出代码可以参考。如果有什么问题欢迎交流。
# 前期准备
开发环境搭建推荐看这篇文章 https://www.jianshu.com/p/813b2cc71ca2
本人是通过《Maya Python游戏与影视编程指南》一书来学习实用Python开发Maya插件书中也介绍了Python的语法。使得没有Python基础的人也可以很好的学习。同时他也介绍了Maya插件开发的命令模式与API模式。通俗的说就是用Maya的内置命令与实用Maya的API。Maya API更加适合专业插件开发者使用。举个书中例子API中的基础对象MObject是一个指针对象。所以使用c#或者c++会更加适合API模式的开发吧。
不过这本书的很多翻译都感觉怪怪,这是它位移的缺点。
我使用Vscode进行开发除了必须的Python打开一个Py文件就会提示安装插件外我还使用**MayaPy**与**MayaCode**。为了能让MayaPy将代码发送到Maya中执行还需要再Maya中执行一段开启端口的命令
**Mel**
```
commandPort -name "localhost:7001" -sourceType "mel" -echoOutput;
```
**Python**
```
import maya.cmds as cmds
cmds.commandPort(name=":7001", sourceType="mel",echoOutput=True)
```
端口与MayaPy中设置的端口有关。如果你不想每次启动Maya都手动执行命令那么可以新建一个脚本文件并将代码填入。之后放到指定目录中
```
Windows: <drive>:\Documents and Settings\<你的windows用户名>\My Documents\maya\<你maya的版本号>\scripts
其实就是我的文档下面maya文件夹
MacOSX: ~/Library/Preferences/Autodesk/maya/<你maya的版本号>/scripts.
Linux: ~/maya/<你maya的版本号>/scripts.
```
**Maya Python路径设置及代码自动补全**<br>
VS Code中按Ctrl+Shift+P输入Settings打开settings.json配置文件在大括号里加入下面代码
```
//python.pythonPath是指定Python命令路径请根据你maya的安装路径来做修改
"python.pythonPath": "C:/Program Files/Autodesk/Maya2019/bin/mayapy.exe",
//python.autoComplete.extraPaths是代码自动补全路径同样根据你自己的maya安装路径来写
"python.autoComplete.extraPaths": "C:/Program Files/Autodesk/Maya2019/devkit/other/pymel/extras/completion/py"
```
注意:settings.json文件中每一项设置用","隔开,最后一项设置后面没有",",如果报错,检查一下是不是这里出现了问题。
# 编码篇
初次学习可以参考YivanLee的文章
https://zhuanlan.zhihu.com/p/76957745
我认为首先你需要了解Maya中的物体都是节点式的当然我个人认为Maya的节点更加偏向于组件而非Houdini那样的流程节点。
## 文档与搜索技巧
请使用谷歌进行搜索不推荐bing以及baidu包括搜索API这样可以节约大量时间。
**官方文档**http://help.autodesk.com/view/MAYAUL/2018/ENU/
>Pyside2是python版本的Qt库。你只要看一下它的模块数目就能明白它的强大。另外百度的时候请搜索PyQt5虽然Pyside才是官方正版。
**Pyside2**https://doc.qt.io/qtforpython/modules.html
>PyMel与maya.cmds不同在于它返回的不是字符串而是一个PyNode对象。它可以直接修改节点属性值无需调用getAttr与setAttr。更加适合于习惯了OOP语言人士使用。同时PyMel可以简化GUI的构建。但你看了文档就会明白这个玩意就是个残废。
**Pymel文档**http://help.autodesk.com/cloudhelp/2018/JPN/Maya-Tech-Docs/PyMel/modules.html
**FBX Mel命令**https://knowledge.autodesk.com/zh-hans/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2019/CHS/Maya-DataExchange/files/GUID-335F2172-DD4D-4D79-B969-55238C08F2EF-htm.html
# pyside2与GUI
实用Python构建Maya插件UI有3种方式
1. 调用Maya内置命令创建。
2. 直接调用Pyside2函数创建。
本人使用第三种使用Qt的界面设计师工具创建。虽然本质上就是第二种方法但效率高。需要注意的是maya目录下的designer.exe是不能直接使用的。需要将安装目录下的qt-plugins中的所有文件都复制到bin所在目录中。之后的步骤就是用Qt的界面设计师工具设计界面了。
但本人电脑上有Qt所有直接就用自己这个版本了。推荐还是用Maya目录下的版本自己去下个5.6版本的也是可以的)原因后面会说。
Qt的界面设计师工具可以输出*.ui文件但Python是无法直接使用的,(虽然可以通过loadUI载入但只能调用Mel命令无法关联python函数)所以之后需要安装Pyside2目的是为了使用 pyside-uic.exe工具它可以将 *.ui文件转化为python代码。
Pyside2对应python3.x所以你需要下载3.0的版本并安装。安装完之后打开CMD切换到安装目录下的script文件夹。执行
```
pip install PySide2
```
时候再执行
```
//请注意文件路径,推荐奖*.ui文件复制到script文件夹中
pyside-uic -o output.py input.ui
```
可能是我用的Qt版本与Maya的不同最后生成出来的python存在一些小错误按钮上的setText函数中会多出一个莫名其妙的函数。还有一个问题我倒最后也没搞懂Pyside2不是对应python3.x与Qt5么那为什么Maya使用python2.7却可以调用Pyside2呢是因为预编译了对应的库么
# 学习建议
我个人建议如果你想深入地使用pyside2开发插件强烈推荐先去学习一到两个月的Qt。之后再来学习pyside2你就会非常的顺利。尤其需要了解的是Qt的信号与槽机制、GUI绘制与线程、Qt事件传递机制。
# 实用代码
## 防止窗口重复创建
```
def main():
global win
try:
win.close() # 为了不让窗口出现多个因为第一次运行还没初始化所以要try在这里尝试先关闭再重新新建一个窗口
except:
pass
//MainWindow为窗口类
win = MainWindow()
win.show()
```
## 信号槽与解决生命周期问题
```
//其中SIGNAL需要先导入
from PySide2.QtCore import SIGNAL, QObject
class MainWindow(QWidget, Ui_Form):
def slotBtnClicked(self):
//为了防止消息框易一出现就被回收,需要给它设置父对象
msgBox = QMessageBox(self)
msgBox.setText(u"The document has been modified.")
msgBox.setInformativeText(u"Do you want to save your changes?")
msgBox.setStandardButtons(QMessageBox.Save)
msgBox.setDefaultButton(QMessageBox.Save)
msgBox.show()
def __init__(self, parent=None):
self.pushButton_stop.clicked.connect(self.slotBtnClicked)
QObject.connect(self.pushButton_targetSkin,SIGNAL('clicked()'), self.slotBtnClicked)
```
## python HumanIK
相关的控制代码可以在 安装目录\scripts\others下搜索hik找到主要在hikCharacterControlsUI.mel与hikGlobalUtils.mel文件中也可以参考我插件中的代码。
## FBX导出命令
导入文件可以只用cmds.file命令但是导出就不太好用了比如需要烘焙动画什么的所以需要调用以下Mel命令。
```
# FBX Exporter options. Set as required.
# You can find a reference guide here: http://download.autodesk.com/us/fbx/20112/Maya/_index.html
# Just add/change what you need.
# Geometry
mm.eval("FBXExportSmoothingGroups -v true")
mm.eval("FBXExportHardEdges -v false")
mm.eval("FBXExportTangents -v false")
mm.eval("FBXExportSmoothMesh -v true")
mm.eval("FBXExportInstances -v false")
mm.eval("FBXExportReferencedContainersContent -v false")
# Animation
mm.eval("FBXExportBakeComplexAnimation -v true")
mm.eval("FBXExportBakeComplexStart -v "+str(exportStartFrame[x]))
mm.eval("FBXExportBakeComplexEnd -v "+str(exportEndFrame[x]))
mm.eval("FBXExportBakeComplexStep -v 1")
# mm.eval("FBXExportBakeResampleAll -v true")
mm.eval("FBXExportUseSceneName -v false")
mm.eval("FBXExportQuaternion -v euler")
mm.eval("FBXExportShapes -v true")
mm.eval("FBXExportSkins -v true")
# Constraints
mm.eval("FBXExportConstraints -v false")
# Cameras
mm.eval("FBXExportCameras -v false")
# Lights
mm.eval("FBXExportLights -v false")
# Embed Media
mm.eval("FBXExportEmbeddedTextures -v false")
# Connections
mm.eval("FBXExportInputConnections -v false")
# Axis Conversion
mm.eval("FBXExportUpAxis y")
# Export!
mm.eval("FBXExport -f "+exportNames[x]+".fbx -s")
```

View File

@@ -0,0 +1,391 @@
---
title: 基于插件的SkeletalMesh多Pass绘制方案
date: 2022-11-04 10:25:07
excerpt:
tags:
rating: ⭐
---
>本文仅供抛转引玉为大家提供一个可行思路。因为本人目前仅仅是个Ue4业余爱好者手头没有含有Lod的骨骼模型做测试所以请勿在未测试的情况下直接把本插件用于生产。
## 前言
在阅读了 @白昼行姜暗夜摸王 的系列文章我感觉通过改引擎的方式来实现模型外描边着实不太方便。在仔细阅读代码后发现文章中修改代码的相关函数不是虚函数就是包含在虚函数中。于是乎我就感觉可以通过插件的方式来实现多pass的绘制方案便开始尝试。
另外感谢@大钊 @YivanLee 在我尝试过程中给予的帮助。
## 思路说明
>在重写函数的过程中因为引擎部分类是定义在cpp中以及部分函数因为没有ENGINE_API或是Private导致无法无法使用。有一个lod更新的逻辑我实在没办法绕过所以就删除了。所以这个插件对于有lod的模型可能会有问题解决思路我会说。
### 创建自定义SkeletalMeshComponent类
1. 继承USkeletalMeshComponent,重写GetUsedMaterials与CreateSceneProxy函数。
2. 添加用于多pass渲染的UMaterialInterface指针。NeedSecondPass变量其实可以不用因为既然你要使用这个类那肯定要启用这个功能
```c++
#pragma once
#include "CoreMinimal.h"
#include "Engine/Classes/Components/SkeletalMeshComponent.h"
#include "StrokeSkeletalMeshComponent.generated.h"
UCLASS(ClassGroup=(Rendering, Common), hidecategories=Object, editinlinenew, meta=(BlueprintSpawnableComponent))
class UStrokeSkeletalMeshComponent : public USkeletalMeshComponent
{
GENERATED_UCLASS_BODY()
//~ Begin UPrimitiveComponent Interface
virtual void GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials = false) const override;
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
//~ End UPrimitiveComponent Interface
//virtual UMaterialInterface* GetMaterial(int32 MaterialIndex) const override;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
UMaterialInterface* SecondPassMaterial = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
bool NeedSecondPass=false;
};
```
3. 实现GetUsedMaterials与CreateSceneProxy函数并包含相应头文件。
### 头文件
```c++
#include "StrokeSkeletalMeshComponent.h"
#include "StrokeSkeletalMeshSceneProxy.h"
#include "Engine/Public/Rendering/SkeletalMeshRenderData.h"
#include "Engine/Public/SkeletalRenderPublic.h"
```
### GetUsedMaterials
自定义的SkeletalMeshComponent只运行OutMaterials.Add(SecondPassMaterial);会导致引擎找不到Material的ShaderMap。在尝试之处我追踪了Skeletal的MeshDraw绘制流程发现竟然是VertexFactory指针为空造成的。所以我判断应该是自定义的Component没有把Material进行注册造成的问题。
之后我尝试了手动将MaterialInterface的指针添加到SkeletalMesh的Lod数组中的material。虽然解决了问题但是遇到重复修改会导致组件栏里多出一堆材质asset显示虽然之后通过判断slotname进行数组管理解决了问题但是结果还是不完美。
不过之后在继续查看了源代码后我发现只要调用UpdateUVChannelData就可以解决问题。
```c++
void UStrokeSkeletalMeshComponent::GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials /*= false*/) const
{
if (SkeletalMesh)
{
// The max number of materials used is the max of the materials on the skeletal mesh and the materials on the mesh component
const int32 NumMaterials = FMath::Max(SkeletalMesh->Materials.Num(), OverrideMaterials.Num());
for (int32 MatIdx = 0; MatIdx < NumMaterials; ++MatIdx)
{
// GetMaterial will determine the correct material to use for this index.
UMaterialInterface* MaterialInterface = GetMaterial(MatIdx);
OutMaterials.Add(MaterialInterface);
}
//这里是我添加的代码
if (NeedSecondPass)
{
OutMaterials.Add(SecondPassMaterial);
SkeletalMesh->UpdateUVChannelData(false);
}
}
if (bGetDebugMaterials)
{
#if WITH_EDITOR
//这段代码无法绕过,而且因为不太重要,就直接注释掉了
//if (UPhysicsAsset* PhysicsAssetForDebug = GetPhysicsAsset())
//{
// PhysicsAssetForDebug->GetUsedMaterials(OutMaterials);
//}
#endif
}
}
```
### CreateSceneProxy
CreateSceneProxy比较简单只需要创建自己的自定义Proxy就可以了
```c++
FPrimitiveSceneProxy* UStrokeSkeletalMeshComponent::CreateSceneProxy()
{
ERHIFeatureLevel::Type SceneFeatureLevel = GetWorld()->FeatureLevel;
//定义自定义Proxy类指针
FStrokeSkeletalMeshSceneProxy* Result = nullptr;
//FSkeletalMeshSceneProxy* Result = nullptr;
FSkeletalMeshRenderData* SkelMeshRenderData = GetSkeletalMeshRenderData();
// Only create a scene proxy for rendering if properly initialized
if (SkelMeshRenderData &&
SkelMeshRenderData->LODRenderData.IsValidIndex(PredictedLODLevel) &&
!bHideSkin &&
MeshObject)
{
// Only create a scene proxy if the bone count being used is supported, or if we don't have a skeleton (this is the case with destructibles)
int32 MaxBonesPerChunk = SkelMeshRenderData->GetMaxBonesPerSection();
int32 MaxSupportedNumBones = MeshObject->IsCPUSkinned() ? MAX_int32 : GetFeatureLevelMaxNumberOfBones(SceneFeatureLevel);
if (MaxBonesPerChunk <= MaxSupportedNumBones)
{
//使用new创建指针对象
Result = ::new FStrokeSkeletalMeshSceneProxy(this, SkelMeshRenderData);
}
}
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
SendRenderDebugPhysics(Result);
#endif
return Result;
}
```
### 创建自定义SkeletalMeshSceneProxy类
1. 继承FSkeletalMeshSceneProxy,重写GetDynamicMeshElements函数。
2. 定义USkinnedMeshComponent指针用于获取自定义SkeletalMeshComponent的变量。之后使用需要使用强制转换
```c++
#pragma once
#include "Engine/Public/SkeletalMeshTypes.h"
class FStrokeSkeletalMeshSceneProxy : public FSkeletalMeshSceneProxy
{
public:
//在UStrokeSkeletalMeshComponent中调用这个构造函数初始化
FStrokeSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData);
virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override;
private:
UPROPERTY()
const USkinnedMeshComponent* ComponentPtr;
};
```
```c++
FStrokeSkeletalMeshSceneProxy::FStrokeSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData):
FSkeletalMeshSceneProxy(Component,InSkelMeshRenderData),ComponentPtr(Component)
{}
```
3. 实现GetDynamicMeshElements函数并包含头文件。
### 头文件
```c++
#include "StrokeSkeletalMeshSceneProxy.h"
#include "Engine/Public/SkeletalMeshTypes.h"
#include "Engine/Public/TessellationRendering.h"
#include "Engine/Public/SkeletalRenderPublic.h"
```
### GetDynamicMeshElements
```c++
void FStrokeSkeletalMeshSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FStrokeSkeletalMeshSceneProxy_GetMeshElements);
if (!MeshObject)
{
return;
}
MeshObject->PreGDMECallback(ViewFamily.Scene->GetGPUSkinCache(), ViewFamily.FrameNumber);
//这句代码无法绕过,所以只能注释掉了
/*
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
if (VisibilityMap & (1 << ViewIndex))
{
const FSceneView* View = Views[ViewIndex];
MeshObject->UpdateMinDesiredLODLevel(View, GetBounds(), ViewFamily.FrameNumber);
}
}
*/
const FEngineShowFlags& EngineShowFlags = ViewFamily.EngineShowFlags;
const int32 LODIndex = MeshObject->GetLOD();
check(LODIndex < SkeletalMeshRenderData->LODRenderData.Num());
const FSkeletalMeshLODRenderData& LODData = SkeletalMeshRenderData->LODRenderData[LODIndex];
if (LODSections.Num() > 0)
{
const FLODSectionElements& LODSection = LODSections[LODIndex];
check(LODSection.SectionElements.Num() == LODData.RenderSections.Num());
for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
{
const FSkelMeshRenderSection& Section = Iter.GetSection();
const int32 SectionIndex = Iter.GetSectionElementIndex();
const FSectionElementInfo& SectionElementInfo = Iter.GetSectionElementInfo();
bool bSectionSelected = false;
#if WITH_EDITORONLY_DATA
// TODO: This is not threadsafe! A render command should be used to propagate SelectedEditorSection to the scene proxy.
if (MeshObject->SelectedEditorMaterial != INDEX_NONE)
{
bSectionSelected = (MeshObject->SelectedEditorMaterial == SectionElementInfo.UseMaterialIndex);
}
else
{
bSectionSelected = (MeshObject->SelectedEditorSection == SectionIndex);
}
#endif
//下面函数的代码
// If hidden skip the draw
check(MeshObject->LODInfo.IsValidIndex(LODIndex));
bool bHide= MeshObject->LODInfo[LODIndex].HiddenMaterials.IsValidIndex(SectionElementInfo.UseMaterialIndex) && MeshObject->LODInfo[LODIndex].HiddenMaterials[SectionElementInfo.UseMaterialIndex];
if (bHide || Section.bDisabled)
{
continue;
}
//这个函数因为没有导出,但是比较简单所以上面就直接把代码复制出来了
/* error LNK2019
if (MeshObject->IsMaterialHidden(LODIndex, SectionElementInfo.UseMaterialIndex) || Section.bDisabled)
{
continue;
}
*/
const UStrokeSkeletalMeshComponent* StrokeSkeletalMeshComponent = dynamic_cast<const UStrokeSkeletalMeshComponent *>(ComponentPtr);
if (SectionElementInfo.Material== StrokeSkeletalMeshComponent->SecondPassMaterial)
{
continue;
}
GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, SectionElementInfo, true, Collector);
//进行第二个pass绘制
//关键点在于修改FSectionElementInfo中的Material并且再次调用绘制函数GetDynamicElementsSection
if (StrokeSkeletalMeshComponent->NeedSecondPass) {
FSectionElementInfo Info = FSectionElementInfo(SectionElementInfo);
if (StrokeSkeletalMeshComponent->SecondPassMaterial == nullptr) {
continue;
}
Info.Material = StrokeSkeletalMeshComponent->SecondPassMaterial;
GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, Info, true, Collector);
}
}
}
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
if (VisibilityMap & (1 << ViewIndex))
{
if (PhysicsAssetForDebug)
{
DebugDrawPhysicsAsset(ViewIndex, Collector, ViewFamily.EngineShowFlags);
}
if (EngineShowFlags.MassProperties && DebugMassData.Num() > 0)
{
FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex);
if (MeshObject->GetComponentSpaceTransforms())
{
const TArray<FTransform>& ComponentSpaceTransforms = *MeshObject->GetComponentSpaceTransforms();
for (const FDebugMassData& DebugMass : DebugMassData)
{
if (ComponentSpaceTransforms.IsValidIndex(DebugMass.BoneIndex))
{
const FTransform BoneToWorld = ComponentSpaceTransforms[DebugMass.BoneIndex] * FTransform(GetLocalToWorld());
DebugMass.DrawDebugMass(PDI, BoneToWorld);
}
}
}
}
if (ViewFamily.EngineShowFlags.SkeletalMeshes)
{
RenderBounds(Collector.GetPDI(ViewIndex), ViewFamily.EngineShowFlags, GetBounds(), IsSelected());
}
if (ViewFamily.EngineShowFlags.Bones)
{
DebugDrawSkeleton(ViewIndex, Collector, ViewFamily.EngineShowFlags);
}
}
}
#endif
}
```
>UpdateMinDesiredLODLevel函数处的代码被我注释掉了大致的解决方法为调用父类的GetDynamicMeshElements函数再运行一次GetDynamicElementsSection绘制自己的pass。
下面的代码我没测试过,仅为说明意思,很多地方是父类运行过的逻辑,可以删除。
```c++
QUICK_SCOPE_CYCLE_COUNTER(STAT_FStrokeSkeletalMeshSceneProxy_GetMeshElements);
//运行父类函数
FStrokeSkeletalMeshSceneProxy::GetDynamicMeshElements(Views,ViewFamily,VisibilityMap,Collector);
const FEngineShowFlags& EngineShowFlags = ViewFamily.EngineShowFlags;
const int32 LODIndex = MeshObject->GetLOD();
check(LODIndex < SkeletalMeshRenderData->LODRenderData.Num());
const FSkeletalMeshLODRenderData& LODData = SkeletalMeshRenderData->LODRenderData[LODIndex];
if (LODSections.Num() > 0)
{
const FLODSectionElements& LODSection = LODSections[LODIndex];
check(LODSection.SectionElements.Num() == LODData.RenderSections.Num());
for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
{
const FSkelMeshRenderSection& Section = Iter.GetSection();
const int32 SectionIndex = Iter.GetSectionElementIndex();
const FSectionElementInfo& SectionElementInfo = Iter.GetSectionElementInfo();
bool bSectionSelected = false;
#if WITH_EDITORONLY_DATA
// TODO: This is not threadsafe! A render command should be used to propagate SelectedEditorSection to the scene proxy.
if (MeshObject->SelectedEditorMaterial != INDEX_NONE)
{
bSectionSelected = (MeshObject->SelectedEditorMaterial == SectionElementInfo.UseMaterialIndex);
}
else
{
bSectionSelected = (MeshObject->SelectedEditorSection == SectionIndex);
}
#endif
//下面函数的代码
// If hidden skip the draw
check(MeshObject->LODInfo.IsValidIndex(LODIndex));
bool bHide= MeshObject->LODInfo[LODIndex].HiddenMaterials.IsValidIndex(SectionElementInfo.UseMaterialIndex) && MeshObject->LODInfo[LODIndex].HiddenMaterials[SectionElementInfo.UseMaterialIndex];
if (bHide || Section.bDisabled)
{
continue;
}
const UStrokeSkeletalMeshComponent* StrokeSkeletalMeshComponent = dynamic_cast<const UStrokeSkeletalMeshComponent *>(ComponentPtr);
if (SectionElementInfo.Material== StrokeSkeletalMeshComponent->SecondPassMaterial)
{
continue;
}
//进行第二个pass绘制
//关键点在于修改FSectionElementInfo中的Material并且再次调用绘制函数GetDynamicElementsSection
if (StrokeSkeletalMeshComponent->NeedSecondPass) {
FSectionElementInfo Info = FSectionElementInfo(SectionElementInfo);
if (StrokeSkeletalMeshComponent->SecondPassMaterial == nullptr) {
continue;
}
Info.Material = StrokeSkeletalMeshComponent->SecondPassMaterial;
GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, Info, true, Collector);
}
}
}
```
4. 解决FSkeletalMeshSectionIter未定义的报错问题。(原因在开头说了)
在Engine\Source\Runtime\Engine\Private\SkeletalMesh.cpp中找到FSkeletalMeshSectionIter类并将所有代码复制到你定义的SkeletalMeshSceneProxy所在的cpp文件中。
完成以上步骤你就得到一个2个pass的SkeletalMeshComponent。
## 关于绘制轮廓的问题
@白昼行姜暗夜摸王 的文章采用了使用了绘制背面的方法来得到轮廓,但是很可惜,因为以下原因,我无法在不修改源码的情况下实现这个方法:
1. SkeletalMeshSceneProxy的MeshBatch设置逻辑在绘制函数中。
2. 因为绘制函数有很多核心函数因为没倒出或是private所以无法重写。
3. 判断剔除的IsLocalToWorldDeterminantNegative()函数不是虚函数。
4. bIsLocalToWorldDeterminantNegative是父类的private变量无法修改。
当然可以使用一些c++黑科技来突破3、4不过我暂时没有那种技术。
## 曲线救国方法
绘制背面本质上还是一种深度偏移,所以我们可以采用深度偏移的方法来解决。
材质我没有仔细调所以在离模型较远的地方会显得有点“脏”只需要在Mask与深度偏移添加模型距离的判断来进行参数调整就完美。
## 插件地址及使用方法
### 插件地址
https://github.com/blueroseslol/BRPlugins
### 使用方法
1. 创建一个Actor蓝图。
2. 创建你定义的SkeletalMeshComponent。
3. 赋予轮廓材质并勾选NeedSecondPass。
## 延伸思考
我觉得可以通过在自定义的Component类中定义一个TArray<MaterialInterface*>用于存储各个Pass的材质之后再自己定义的SceneProxy中进行for循环绘制不就实现类似unity的多pass材质系统了。
至少StaticMesh因为MeshBatch的设置过程并没有在绘制函数中所以我们可以任意修改MeshBatch设置而可以完美实现上述猜想。下一篇文章我将介绍StaticMesh的实现方法。

View File

@@ -0,0 +1,373 @@
---
title: 基于插件的StaticMesh多Pass绘制方案
date: 2022-11-04 10:26:59
excerpt:
tags:
rating: ⭐
---
## 前言
StaticMesh与SkeletalMesh的实现方法比较类似不过两者的绘制方式有很大的不同。不过庆幸的是Static的MeshBatch设置在绘制函数的外面。
## 思路说明
### 创建自定义StaticMeshComponent
1. 继承UStaticMeshComponent,重写GetUsedMaterials与CreateSceneProxy函数。
2. 添加用于多pass渲染的UMaterialInterface指针。NeedSecondPass变量其实可以不用因为既然你要使用这个类那肯定要启用这个功能
```c++
#pragma once
#include "CoreMinimal.h"
#include "Engine/Classes/Components/StaticMeshComponent.h"
#include "StrokeStaticMeshComponent.generated.h"
UCLASS(ClassGroup=(Rendering, Common), hidecategories=Object, editinlinenew, meta=(BlueprintSpawnableComponent))
class UStrokeStaticMeshComponent : public UStaticMeshComponent
{
GENERATED_UCLASS_BODY()
//~ Begin UPrimitiveComponent Interface
virtual void GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials = false) const override;
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
//~ End UPrimitiveComponent Interface
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
UMaterialInterface* SecondPassMaterial = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
bool NeedSecondPass=false;
};
```
3. 实现GetUsedMaterials与CreateSceneProxy函数并包含相应头文件。
### 头文件
```
#include "StrokeStaticMeshSceneProxy.h"
```
### GetUsedMaterials
```c++
void UStrokeStaticMeshComponent::GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials /*= false*/) const
{
if (GetStaticMesh() && GetStaticMesh()->RenderData)
{
TMap<int32, UMaterialInterface*> MapOfMaterials;
for (int32 LODIndex = 0; LODIndex < GetStaticMesh()->RenderData->LODResources.Num(); LODIndex++)
{
FStaticMeshLODResources& LODResources = GetStaticMesh()->RenderData->LODResources[LODIndex];
int32 MaterialNum = 0;
for (int32 SectionIndex = 0; SectionIndex < LODResources.Sections.Num(); SectionIndex++)
{
// Get the material for each element at the current lod index
int32 MaterialIndex = LODResources.Sections[SectionIndex].MaterialIndex;
if (!MapOfMaterials.Contains(MaterialIndex))
{
MapOfMaterials.Add(MaterialIndex, GetMaterial(MaterialIndex));
MaterialNum++;
}
}
//这里是我添加的代码
if (NeedSecondPass)
{
bool NeedAddMaterial = true;
for (int i = 0; i < MapOfMaterials.Num(); ++i)
{
if (MapOfMaterials[i]== SecondPassMaterial)
{
NeedAddMaterial = false;
}
}
if (NeedAddMaterial)
{
MapOfMaterials.Add(MaterialNum, SecondPassMaterial);
}
}
}
if (MapOfMaterials.Num() > 0)
{
//We need to output the material in the correct order (follow the material index)
//So we sort the map with the material index
MapOfMaterials.KeySort([](int32 A, int32 B) {
return A < B; // sort keys in order
});
//Preadd all the material item in the array
OutMaterials.AddZeroed(MapOfMaterials.Num());
//Set the value in the correct order
int32 MaterialIndex = 0;
for (auto Kvp : MapOfMaterials)
{
OutMaterials[MaterialIndex++] = Kvp.Value;
}
}
}
}
```
### CreateSceneProxy
```c++
FPrimitiveSceneProxy* UStrokeStaticMeshComponent::CreateSceneProxy()
{
if (GetStaticMesh() == nullptr || GetStaticMesh()->RenderData == nullptr)
{
return nullptr;
}
const TIndirectArray<FStaticMeshLODResources>& LODResources = GetStaticMesh()->RenderData->LODResources;
if (LODResources.Num() == 0 || LODResources[FMath::Clamp<int32>(GetStaticMesh()->MinLOD.Default, 0, LODResources.Num() - 1)].VertexBuffers.StaticMeshVertexBuffer.GetNumVertices() == 0)
{
return nullptr;
}
//与SkeletalMeshComponent中写的类似直接通过new来创建
FPrimitiveSceneProxy* Proxy = ::new FStrokeStaticMeshSceneProxy(this, false);
#if STATICMESH_ENABLE_DEBUG_RENDERING
SendRenderDebugPhysics(Proxy);
#endif
return Proxy;
}
```
### 创建自定义StaticMeshSceneProxy
**这里与SkeletalMeshSceneProxy不同GetDynamicMeshElements没有起到绘制作用真正起到作用的是DrawStaticElements。**
1. 继承FStaticMeshSceneProxy,重写DrawStaticElements函数。
2. 定义UStaticMeshComponent指针用于获取自定义StaticMeshComponent的变量。之后使用需要使用强制转换
```c++
#pragma once
#include "Engine\Public\StaticMeshResources.h"
class FStrokeStaticMeshSceneProxy : public FStaticMeshSceneProxy
{
public:
//在UStrokeStaticMeshComponent中调用这个构造函数初始化
FStrokeStaticMeshSceneProxy(UStaticMeshComponent* Component, bool bForceLODsShareStaticLighting);
virtual void DrawStaticElements(FStaticPrimitiveDrawInterface* PDI) override;
private:
UPROPERTY()
const UStaticMeshComponent* ComponentPtr;
};
```
3. 复制DrawStaticElements中所需函数的代码。
将Engine\Source\Runtime\Engine\Private\StaticMeshRender.cpp中的AllowShadowOnlyMesh、UseLightPropagationVolumeRT2函数代码复制到你定义的StaticMeshSceneProxy类所在的cpp文件中。
4. 实现DrawStaticElements函数并包含头文件。
与SkeletalMesh的绘制函数不同它有3处需要调用绘制函数的地方。其中有一处因为不存在lod所以代码有些不同所以请不要在未测试的情况下使用本插件进行生产项目。
不过一个好消息是我们可以在绘制前设置MeshBatch从而完美结局描边问题。
```c++
void FStrokeStaticMeshSceneProxy::DrawStaticElements(FStaticPrimitiveDrawInterface* PDI)
{
checkSlow(IsInParallelRenderingThread());
if (!HasViewDependentDPG())
{
// Determine the DPG the primitive should be drawn in.
uint8 PrimitiveDPG = GetStaticDepthPriorityGroup();
int32 NumLODs = RenderData->LODResources.Num();
//Never use the dynamic path in this path, because only unselected elements will use DrawStaticElements
bool bIsMeshElementSelected = false;
const auto FeatureLevel = GetScene().GetFeatureLevel();
//check if a LOD is being forced
if (ForcedLodModel > 0)
{
int32 LODIndex = FMath::Clamp(ForcedLodModel, ClampedMinLOD + 1, NumLODs) - 1;
const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
// Draw the static mesh elements.
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
#if WITH_EDITOR
if (GIsEditor)
{
const FLODInfo::FSectionInfo& Section = LODs[LODIndex].Sections[SectionIndex];
bIsMeshElementSelected = Section.bSelected;
PDI->SetHitProxy(Section.HitProxy);
}
#endif // WITH_EDITOR
const int32 NumBatches = GetNumMeshBatches();
PDI->ReserveMemoryForMeshes(NumBatches);
for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
{
FMeshBatch MeshBatch;
if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, MeshBatch))
{
PDI->DrawMesh(MeshBatch, FLT_MAX);
//以下是我添加的代码
const FLODInfo& ProxyLODInfo = LODs[LODIndex];
UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[SectionIndex].Material;
const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
{
continue;
}
if (StrokeStaticMeshComponent->NeedSecondPass) {
if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
continue;
}
MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
//设置反转剔除选项
MeshBatch.ReverseCulling = true;
PDI->DrawMesh(MeshBatch, FLT_MAX);
}
}
}
}
}
else //no LOD is being forced, submit them all with appropriate cull distances
{
for (int32 LODIndex = ClampedMinLOD; LODIndex < NumLODs; LODIndex++)
{
const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
float ScreenSize = GetScreenSize(LODIndex);
bool bUseUnifiedMeshForShadow = false;
bool bUseUnifiedMeshForDepth = false;
if (GUseShadowIndexBuffer && LODModel.bHasDepthOnlyIndices)
{
const FLODInfo& ProxyLODInfo = LODs[LODIndex];
// The shadow-only mesh can be used only if all elements cast shadows and use opaque materials with no vertex modification.
// In some cases (e.g. LPV) we don't want the optimization
bool bSafeToUseUnifiedMesh = AllowShadowOnlyMesh(FeatureLevel);
bool bAnySectionUsesDitheredLODTransition = false;
bool bAllSectionsUseDitheredLODTransition = true;
bool bIsMovable = IsMovable();
bool bAllSectionsCastShadow = bCastShadow;
for (int32 SectionIndex = 0; bSafeToUseUnifiedMesh && SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
const FMaterial* Material = ProxyLODInfo.Sections[SectionIndex].Material->GetRenderProxy()->GetMaterial(FeatureLevel);
// no support for stateless dithered LOD transitions for movable meshes
bAnySectionUsesDitheredLODTransition = bAnySectionUsesDitheredLODTransition || (!bIsMovable && Material->IsDitheredLODTransition());
bAllSectionsUseDitheredLODTransition = bAllSectionsUseDitheredLODTransition && (!bIsMovable && Material->IsDitheredLODTransition());
const FStaticMeshSection& Section = LODModel.Sections[SectionIndex];
bSafeToUseUnifiedMesh =
!(bAnySectionUsesDitheredLODTransition && !bAllSectionsUseDitheredLODTransition) // can't use a single section if they are not homogeneous
&& Material->WritesEveryPixel()
&& !Material->IsTwoSided()
&& !IsTranslucentBlendMode(Material->GetBlendMode())
&& !Material->MaterialModifiesMeshPosition_RenderThread()
&& Material->GetMaterialDomain() == MD_Surface;
bAllSectionsCastShadow &= Section.bCastShadow;
}
if (bSafeToUseUnifiedMesh)
{
bUseUnifiedMeshForShadow = bAllSectionsCastShadow;
// Depth pass is only used for deferred renderer. The other conditions are meant to match the logic in FDepthPassMeshProcessor::AddMeshBatch.
bUseUnifiedMeshForDepth = ShouldUseAsOccluder() && GetScene().GetShadingPath() == EShadingPath::Deferred && !IsMovable();
if (bUseUnifiedMeshForShadow || bUseUnifiedMeshForDepth)
{
const int32 NumBatches = GetNumMeshBatches();
PDI->ReserveMemoryForMeshes(NumBatches);
for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
{
FMeshBatch MeshBatch;
if (GetShadowMeshElement(LODIndex, BatchIndex, PrimitiveDPG, MeshBatch, bAllSectionsUseDitheredLODTransition))
{
bUseUnifiedMeshForShadow = bAllSectionsCastShadow;
MeshBatch.CastShadow = bUseUnifiedMeshForShadow;
MeshBatch.bUseForDepthPass = bUseUnifiedMeshForDepth;
MeshBatch.bUseAsOccluder = bUseUnifiedMeshForDepth;
MeshBatch.bUseForMaterial = false;
PDI->DrawMesh(MeshBatch, ScreenSize);
//以下是我添加的代码
const FLODInfo& ProxyLODInfo = LODs[LODIndex];
UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[0].Material;
const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
{
continue;
}
if (StrokeStaticMeshComponent->NeedSecondPass) {
if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
continue;
}
//通过MeshBatch设置Material与反转剔除选项
MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
//设置反转剔除选项
MeshBatch.ReverseCulling = true;
PDI->DrawMesh(MeshBatch, FLT_MAX);
}
}
}
}
}
}
// Draw the static mesh elements.
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
#if WITH_EDITOR
if (GIsEditor)
{
const FLODInfo::FSectionInfo& Section = LODs[LODIndex].Sections[SectionIndex];
bIsMeshElementSelected = Section.bSelected;
PDI->SetHitProxy(Section.HitProxy);
}
#endif // WITH_EDITOR
const int32 NumBatches = GetNumMeshBatches();
PDI->ReserveMemoryForMeshes(NumBatches);
for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
{
FMeshBatch MeshBatch;
if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, MeshBatch))
{
// If we have submitted an optimized shadow-only mesh, remaining mesh elements must not cast shadows.
MeshBatch.CastShadow &= !bUseUnifiedMeshForShadow;
MeshBatch.bUseAsOccluder &= !bUseUnifiedMeshForDepth;
MeshBatch.bUseForDepthPass &= !bUseUnifiedMeshForDepth;
PDI->DrawMesh(MeshBatch, ScreenSize);
//以下是我添加的代码
const FLODInfo& ProxyLODInfo = LODs[LODIndex];
UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[SectionIndex].Material;
const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
{
continue;
}
if (StrokeStaticMeshComponent->NeedSecondPass) {
if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
continue;
}
MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
//
MeshBatch.ReverseCulling = true;
PDI->DrawMesh(MeshBatch, FLT_MAX);
}
}
}
}
}
}
}
}
```
## 插件地址及使用方法
### 插件地址
https://github.com/blueroseslol/BRPlugins
### 使用方法
1. 创建一个Actor蓝图。
2. 创建你定义的StaticMeshComponent。
3. 赋予轮廓材质并勾选NeedSecondPass。
使用这个材质的效果:

View File

@@ -0,0 +1,84 @@
---
title: 虚幻商城发布插件指南
date: 2022-11-04 10:17:18
excerpt: 摘要
tags:
rating: ⭐
---
## 前言
前段时间本人尝试发布自己写的插件到Market Place因为每次审核会需要1~3天时间不等所以在这里分享一下我的经验
## 添加插件描述
- CreatedByURL一般填写作者介绍的网址github仓库与主页、blog、小蓝鸟都可以的。
- Description对插件的简单描述。
- DocsURL文档网址这里我链接到自己的blog里的文章。也可以使用Github建的主页。
- MarketplaceURL在点击Add Product后就会生成一个产品ID之后将ID替换下面的URL即可。比如我的产品地址为https://publish.unrealengine.com/v3/edit-product/657c763ab2e244edbcca955eb81d8377
- SupportURL支持网址一般会使用discord。
额外需要注意的就是模块白名单(黑名单),这个需要与发布页面的**Supported Target Platforms**对应。EPIC会在审查时进行构建测试所以最好是对应平台编译测试通过添加此平台。
以下是我的插件描述仅供参考:
```
"CreatedByURL": "http://www.blueroses.top/",
"Description": "This plugin uses UnrealEngine's MeshDraw framework to achieve a multi-Pass effect looks like Unity's, with no post-processing required to achieve strokes and cutoff.",
"DocsURL": "http://www.blueroses.top/2022/05/02/multidraw-document/",
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/657c763ab2e244edbcca955eb81d8377",
"SupportURL": "https://discord.gg/y4fWqYsf4U",
"Modules": [
    {
        "Name": "MultiDraw",
        "Type": "Runtime",
        "LoadingPhase": "Default",
        "WhitelistPlatforms": [ "Win64" ]
    }
]
```
## Code
给所有的代码文件都加上版权声明模块cs、h、cpp文件都需要例如我这里填写了
>// Copyright 2022 BlueRose, Inc. All Rights Reserved.
有一个技巧在项目创建时可以在Project-Description-Legal-CopyrightNotice里填写之后创建新类都会自动加上版权信息。
## Content
所有会被引用的二进制Asset都必须放置到插件的Content里最后还需要使用Fix Up Redirectors In Folder对Redirector进行清理。
如果你有使用UE的ContentExample项目中的DemoRoom或者其他EPIC项目的资源那就需要注意排查将带有UnrealEngine、EPIC、UE4等Logo标志、符号的Texture与Materials都删除不然系统检查会无法通过。
案例关卡的可以放在插件的Content里但建议还是将其分离前期可能会修改2~3次再加上还需要构建其他版本的插件如果将案例关卡内容放在插件里就会占用可观的网盘空间。
## 打包与构建测试
使用构建Bat来快速构建多个版本的插件压缩包文件。
>"D:\UnrealEngine\UE_5.0\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -Plugin="D:\UnrealEngine\Project\MarketPlaceProject\Plugins\MultiDraw\MultiDraw.uplugin" -Package="D:\UnrealEngine\Project\Plugins\MultiDraw\" -Rocket -VS2019
为了能看到Log我们可以使用
>cmd /k "xxxx.bat"  
打包插件完成之后上传之前把Binaries与Intermediate删除压缩成zip格式上传到google或者onedriver之类的就可以了。
## 截图
画廊一般会放置截图而且这些图片必须为1080p分辨率所以本人这里使用了UE内置的截图功能
https://docs.unrealengine.com/4.27/zh-CN/WorkingWithMedia/CapturingMedia/TakingScreenshots/
>HighResShot filename=PATH (XxY OR Multiplier) CaptureX CaptureY CaptureW CaptureH bMaskUsingCustomDepth bDumpBufferVisualizationTargets bCaptureHDR bDateTimeAsFilename
- XxY:指定截图的尺寸宽度x高度
- 乘数Multiplier:根据指定的值放大截图尺寸。
- CaptureX CaptureY CaptureW CaptureH:用整数定义视口中要用于截图的区域。
- bMaskUsingCustomDepth:用一个布尔值0或1来控制是否要用自定义深度Custom Depth缓冲作为捕获的遮罩。详情请参见自定义深度遮罩。
- bDumpBufferVisualizationTargets:用一个布尔值0或1来控制是否要将GBuffer中的每一个通道都捕获为一张图片并将其导出。
- bCaptureHDR:用一个布尔值0或1来控制是否要使用.EXR文件格式来捕获HDR图片。
- bDateTimeAsFilename:用一个布尔值0或1来控制是否要在生成的文件名中加入时间戳。
因为展示需要1080p所以我们只需要使用这个命令即可。
>HighResShot 1920x1080
## 视频推荐
我推荐查看以下这3个视频
Unreal Engine How To Sell Plugins #1: "Create Plugin"
https://www.youtube.com/watch?v=v3_HkAU_rls
Unreal Engine How To Sell Plugins #3: "PackagePlugin"
https://www.youtube.com/watch?v=SKArLiShb9c
Unreal Engine How To Sell Plugins #4: "Submitting Plugin for Approval"
https://www.youtube.com/watch?v=-6QYlGVRIkI
## 本人插件
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/MultiDraw_MarketPlace.png)
想制作backface outline、Fur没在插件中实现等需要多Pass的功能的效果可以考虑使用我的插件
https://www.unrealengine.com/marketplace/en-US/product/multidraw

View File

@@ -0,0 +1,103 @@
## 前言
本文仅为使用Ornatrix制作角色头发的流程笔记只为记录制作流程。但因为本人非专职美术没有分析过面片头发也没有制作头发经验所以本文仅供参考。
## Ornatrix的优点
插件目前支持max、maya、c4d。拥有大部分xGen的功能我无法保证所有功能都有除了制作头发之外它还可以制作羽毛与编织物。以下是官方介绍视频
https://www.bilibili.com/video/av80061009
个人觉得优点有下:
### 节点式的流程
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/OrnatrixOutline.png)
节点式流程就代表着流程是非线性的,这意味着你可以任意拖动节点位移或者可以随时对节点树中的节点进行修改,并预览效果。同时你可以把做成完成的头发保存为预设,下次再遇到相似的发型就可以直接使用了,以此加快制作进程。
### 各种使用节点
梳理节点:
你可以通过画几个箭头就完成对发型的大致梳理。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/SurfaceOnCombTool.png)
旋转节点这个节点对于游戏面片头发的制作相当不错。另外Ornatrix可以设置生成面片的段数、UV等其他属性这也是我放弃xGen转而使用Ornatrix的原因。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/CardGenerate1.png)
使用旋转节点前
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/CardGenerate2.png)
使用旋转节点后
另外还有Frizz、Curl等节点出个大形的速度非常快。
## EPIC模型分析
这里本人使用虚幻争霸中的一个角色作为参考因为本人能力有限所以使用官方提供的头发材质进行渲染。这样只需要制作出符合高度贴图、id贴图即可。这里我为了方便分析贴图所以给不同的头发进行填色以此来判断头发组成。因为本人不知道如何在Maya中解决透明排序的问题所以结果就凑合地看吧
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/HairColorVisual.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/HairColor.png)
- 红色为最底层的头发,形状和头皮一样为了防止穿帮。
- 蓝色为主要头发,头发相对于红色部分会稍微稀疏一些。
- 剩下的黄色、绿色、亮红色为点缀部分。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/HairColorVisualLayer.png)
## 制作流程
### 设计发型与分块
尽管Ornaterix可以通过各种分组手段对不同类型头发进行分组使得可以将所有头发效果都做到一个Ornaterix节点中。但是我还是选择了使用多个Ornaterix物体来制作不同类型的头发。我这么做的原因有以下几点
1. 方便分组,可以随时隐藏头发以便于观察其他头发。
2. 如果其中一个崩溃报错了,不至于所有的工作都白费了。
### 制作头发
以下我分享以下我所知道的制作思路首先我本人采用了导入外部引导曲线的方式来制作头发因为之前学习了一段时间的xGen所以可以直接将之前制作的引导曲线导入Ornatrix中。
- 对于一些短发可以直接生成通过生成几根引导曲线,生成头发。之后再用这些头发生成更多的引导曲线来生成头发模型。
- 对于一些有着具体形状或是沿着路径生成的头发可以使用Ornatrix的发片add hair from mesh strips功能快速生成适用于睫毛、打底层头发等。
上述这些曲线以及面片生成都是带了历时的,你可以随时通过修改面片与曲线来调整头发生成结果。
模型替代:
https://www.bilibili.com/video/av56489034
这个视频中可以看出,作者使用了三棱锥模型,而非普通面片,这个可以使用(add Mesh from stands)中的proxy mesh stands来实现。从视频中可以看得出使用这个方法增强了头发的立体感。
长发不适合用随机生成的方法来制作,且需要精准控制遮盖、效果与面数,所以对于长发我推荐使用官方的方法来做:
https://www.bilibili.com/video/av79618784
### 导出模型
直接导出貌似是无效的可能是我Maya的问题我搜索了油管与文档都没有发现导出方法。所以以下的方法我不能保证一定是正确的
在加入Mesh Frome Stand节点后将Maya设置为多边形选择模式选中需要导出的面之后选择复制面命令。之后多边形就会出现在大纲视图的Or节点下面了。
### 烘焙id贴图与高度贴图
我因为对Vray、Arnold烘焙流程不熟且不会烘焙分组的id贴图所以还是选择简单易懂的xNormal流程
https://www.bilibili.com/video/av56489034
使用这个流程在烘焙id贴图前还需要把毛发模型进行分组并导出。作者提供的插件是根据百分比随机从当前物体中选择物体所以如果你要分成5份就需要依次使用20%、25%、33%、50%分离物体并导出。因此对于这个流程,插件还有很大的改进空间。
如果你对Arnold或者Vray以及maya节点系统熟悉的话可以选择官方流程这个流程的优点是制作完马上就可以预览效果
https://www.bilibili.com/video/av79619146
### 是先制作模型还是先制作贴图?
个人还是倾向于先制作贴图,因为制作贴图本身不会受制于模型,而且在之后的模型制作中就可以随时预览制作效果了。如果先制作模型,很可能会出现因为效果没有达到预期,而造成返工修改模型的情况。
## 官方网站与国内购买网站
官方网站:
http://www.ephere.com/
官方油管频道:
https://www.youtube.com/channel/UCzzR4qt--4OcXgvyUTsgW6w
文档地址:
https://ephere.com/plugins/autodesk/maya/ornatrix/docs/2/
官方频道上的教程挺多的再加上这个插件使用不难很多基础功能光看英文就能知道如何使用再看看文档与视频就差不多。但如果你想快速入门我推荐aboutcg的教程。
国内有aboutcg代理了以下是购买网址
https://tool.aboutcg.com/tool/ornatrix-maya/
## 学习过的资料
https://new.80.lv/articles/tips-tricks-on-hair-for-games/
视频的话我已上传到B站
https://www.bilibili.com/video/av56489034
Ornatrix官方视频
https://www.bilibili.com/video/av79618784
https://www.bilibili.com/video/av79619146
https://www.bilibili.com/video/av79618545
## 使用插件
我使用了《Tips & Tricks on Hair for Games》作者提供的Maya插件。
```
https://pan.baidu.com/s/1DDAj9bdMTMzvpU9ZnFevrw w6ar
```

View File

@@ -0,0 +1,67 @@
## 前言
4.24出了头发渲染与模拟功能,这真是一件令人兴奋的事,所以我稍微花了点时间进行测试,在此分享一些经验。
### 使用步骤
使用步骤大致如下具体的可以参考文档https://docs.unrealengine.com/en-US/Engine/HairRendering/QuickStart/index.html
1. 首先在项目设置启用Support Compute Skincache,并禁用Tick Animation on Skeletal Mesh Init。
2. 启用Alembic Groom Importer与Groom插件之后重启引擎。此时大概会编译大概5000多的Shader
![image](https://pic3.zhimg.com/v2-05069ca0849d59afee7d02e289edbf26_r.jpg)
1. 导入带有Groom信息的abc缓存文件。
![image](https://pic3.zhimg.com/v2-3c65d36047e05d909b39106e5fce1e7a_r.jpg)
4. 设置Groom Asset属性。主要是设置头发宽度
5. 设置Groom材质。ShadingModel需要设置hairGroom对象的材质有两种设置方式1、从ContentBrowser中设置GroomAsset的属性 2、在场景中设置Groom对象的属性
![image](https://pic3.zhimg.com/v2-a60f943d7d801da07a7834949e7c9786_r.jpg)
6. 将GroomAsset拖入场景中并且attach到场景中的骨骼模型上并调整位置。直接放入蓝图的骨骼模型引擎会崩溃
![image](https://pic1.zhimg.com/v2-c9b7ed1a85e63911c8430e809488e420_r.jpg)
7. 设置场景中的Groom物体属性给它挂载Niagara Particle System并且设置GroomAssetSystem。
![image](https://pic4.zhimg.com/v2-5877574508ff6a77038ea770e36d1793_r.jpg)
![image](https://pic2.zhimg.com/v2-ba1db289e98e5478476fb7bb43071b51_r.jpg)
8. 设置Niagara物理参数。
**最终结果:**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/hair.gif)
**关于头发对齐问题与Bind Groom to Skeletal Mesh选项**
我是复制了头皮模型并以此为基础制作头发的。所以理论上头发是不需要对齐。但导入后头发还是需要手动位移可能是我导出的时候没有勾选Unreal Engine Export的关系
**Bind Groom to Skeletal Mesh**这个选项应该让Groom根据蒙皮数据进行运动但我用下来感觉这个选项没什么卵用如有正确思路还请告知而且头发会发生错误的位移。然后在你勾选这个选项后将GroomAsset清空或者将Groom对象从骨骼物体上解除Attacked都会触发一个LOD的断言从而导致引擎直接关闭。这明显是因为容错逻辑没有写完所造成的的但也可以理解毕竟4.24.1出的时候都要过年了。
**Hair to Guide Density**
导入的头发与Groom和Ornatrix渲染的头发量Hair From Guides中的Render Count有关和Ornatrix的引导曲线无关。因为本人没有看过源代码所以以下是个人的无责任猜测
Ue4在导入头发时(abc里的数据还是类似曲线一样的东西),会根据头发分布重新生成"引导曲线"以此来进行物理模拟。因为我在进行测试的时候发现3000个引导线与9000个引导线在静态环境下渲染结果与帧数都是相同。而在设置Niagara Particle System与GroomAssetSystem时会耗费较长的时间。
所以Hair to Guide Density的默认参数0.1,这个就不要更改了。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/Hair.png)
从这个图中可以看出一些靠近边缘的区域需要增加头发密度不然就有可能出现图中这种25岁程序员的头发。
### Ornatrix导出abc
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Character/OrnatrixExport.png)
导出abc的时候需要在Ornatrix的大纲视图选中最上面的节点实际上就是选中Ornatrix的形状节点你可以Maya的关系编辑器中通过查看上下游节点进行查看。
之后点击“导出当前选择”选择OrnatrixAlembic文件类型。勾选
文件类型特定选项-Export Components-Unreal Engine Export选项之后就可以导出了。
### Groom ID
Ornatrix中可以通过StandGroup对头发进行分组编辑。Ue4会通过这些StandGroup作为Groom ID数据。你可以对Groom ID的头发使用不同的参数。这样可以实现一个GroomAssetabc文件存储头发、睫毛、眉毛多种类型毛发不同的宽度与材质
Strand Groups一般是通过Edit Guides节点进行指定的。指定步骤如下
1. 在Edit Guides,点击Edit Stands按钮进行编辑Stands模式。
2. 选中需要添加组的头发。
3. 之后在StandsGroups选项卡中勾选Use Stands Groups并在调整Stands Group Index后点击Assign。
4. 此时取消头发选择,如果之前选中过的头发变色了,就代表分组已经成功。
**Ornatrix导出文档**
https://ephere.com/plugins/autodesk/maya/ornatrix/docs/2/Exporting_Hair_to_Alembic.html
**Strand Groups文档**
https://ephere.com/plugins/autodesk/maya/ornatrix/docs/2/Strand_Groups.html
### 个人评价
因为本人电脑存在问题,打开一个测试场景帧数都是不稳定的,所以本人没有进行性能方面的测试。但是我感觉目前渲染效率还不错(帧数没有下降特别多)。目前存在问题,容易崩。再过几个版本迭代就可以直接拿来做项目了吧。
### 官方文档中所说的限制
1、帧数受Groom大小、分辨率与硬件影响。
2、目前不支持多屏显示与VR。

View File

@@ -0,0 +1,163 @@
---
title: RenderDoc使用技巧
date: 2022-09-30 11:17:29
excerpt:
tags: RenderDoc
rating: ⭐⭐
---
## 前言
参考https://zhuanlan.zhihu.com/p/568990608
## UE相关设置
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20220930121129.png)
### UE5中的改动
UE5中这些名称有了一定的变化
| 旧名称 | 新名称 | 注解 |
| --------------------------------------- | ------------------------ | -------------------------------------------------------------------------- |
| r.Shaders.KeepDebugInfo | r.Shaders.Symbols | 通过生成符号并将其写入主机的磁盘来启用着色器调试PC符号仍以内联方式存储。 |
| r.Shaders.KeepDebugInfo(被划分为两部分) | r.Shaders.ExtraData | 生成着色器名称和其他"额外"着色器数据。 |
| r.Shaders.PrepareExportedDebugInfo | r.Shaders.GenerateSymbol | 生成符号,但不将其写入磁盘(备注符号存储在DDC中) |
| r.Shaders.ExportDebugInfo | r.Shaders.WriteSymbols | 如果符号已生成,则将其写入磁盘。 |
## 其他设置
renderdoc.BinaryPath  // 查看RenderDoc的安装路径
renderdoc.BinaryPath "C:\Program Files\RenderDoc"  // 设置RenderDoc的安装路径为C:\Program Files\RenderDoc
renderdoc.CaptureAllActivity 1   // 勾选CaptureAllActivity获取编辑器所有viewport和窗口的渲染数据而不仅仅是当前viewport  获取UI的Draw需要开启该开关
renderdoc.CaptureCallstacks // 查看CaptureCallstacks获取图形API的调用堆栈是否勾选
renderdoc.EnableCrashHandler 0 // 取消勾选EnableCrashHandler截帧崩溃时是否使用RenderDoc的crash handler
renderdoc.ReferenceAllResources  // 查看ReferenceAllResources获取所有mesh、材质纹理等渲染资源开启该选项会导致帧文件很大是否勾选
renderdoc.SaveAllInitials 1 // 勾选SaveAllInitials获取所有mesh、材质纹理等渲染资源的初始状态开启该选项会导致帧文件很大
renderdoc.ShowHelpOnStartup  // 查看ShowHelpOnStartup启动编辑器时是否弹出RenderDoc的帮助对话框是否勾选
>调试Slate所需命令renderdoc.CaptureAllActivity 1
## 截帧命令
- renderdoc.CaptureFrameCount  10  // 连续截10帧并保存到一个rdc文件中
- renderdoc.CapturePIE 12 // 在编辑器中将当前地图Play然后连续截取12帧
- renderdoc.CaptureDelayInSeconds 0   // 设置Delay的单位为帧
- renderdoc.CaptureDelayInSeconds 1   // 设置Delay的单位为秒
- renderdoc.CaptureDelay 15  // 当Delay的单位为帧时表示设置延迟15帧当Delay的单位为秒时表示设置延迟15秒
//  这个只是设置仍然需要调用renderdoc.CaptureFrame等来发起截帧动作
## 重新编译Shader
- r.RecompileRenderer重新编译所有RenderModule。
- recompileshaders位于`bool RecompileShaders(const TCHAR* Cmd, FOutputDevice& Ar);`可以编译指定的Material。命令默认编译所有Shader快捷键是Ctrl+Shift+.。`recompileshaders material <material name>`
- Changed编译修改过文件
- Global编译GlobalShader
- Material **MaterialName**:附带一个参数材质名称
- All所有文件
- **ShaderFileName**编译指定的Shader
有人说`recompileshaders <USF FileName>`可以只编译指定的USF文件但实际测试会报错。或许可以试试Material版本比如`recompileshaders material M_SGSSS`
## 截取非管线Shader的方法
在你EnqueueRenderCommand前加一个FScopedCapture 跑到你的Cmd的时候就可以自动Renderdoc截帧 ,类为FcopedCapture。
## 使用技巧
### 修改ms显示耗时
![](https://pic1.zhimg.com/80/v2-804879d0e02dbd0622f6480b9dd5040c_720w.webp)
### 过滤高耗时DrawCall
使用$action(duration > 0.8ms) 进行过滤
![](https://pic4.zhimg.com/80/v2-6598ae8de61ecf7256aa2b2860fa622b_720w.webp)
### **调试VS**
Mesh Viewer中vs input选中或在preview窗口中鼠标右键选中顶点在选中顶点行上右键debug thie vertices
![](https://pic3.zhimg.com/80/v2-3001ac53f0fa085e47df236c0430d2ea_720w.webp)
### **调试PS**
Texture Viewer中右键选择像素在Pixel Context中心就是选中的像素选择需要调试的历史时间点击“Debug”调试
![](https://pic1.zhimg.com/80/v2-c7ab8ffce562b57fe12234a1a39952c4_720w.webp)
### 修改VS
![](https://pic4.zhimg.com/80/v2-b7acd2f64495b688274e6bb5ca6ea06b_720w.webp)
选中dc高亮绘制
![](https://pic1.zhimg.com/80/v2-246cc1577f0a61d68926299a30e488d8_720w.webp)
Pipeline state进入VS
![](https://pic2.zhimg.com/80/v2-37211dff3c6e57067255d6e91d08c6a1_720w.webp)
修改坐标
![](https://pic1.zhimg.com/80/v2-16f647910f37dc54ba7e09c199813a60_720w.webp)
Texture View中预览位置变化
### 修改PS
![](https://pic1.zhimg.com/80/v2-ba7d6b23e94fcf541c5815beacfbf674_720w.webp)
进入pipeline state 编辑ps
![](https://pic3.zhimg.com/80/v2-7442e83ed016b30a3bdd1745912c9c6e_720w.webp)
修改前颜色
![](https://pic2.zhimg.com/80/v2-4878401ef7cae939c76912135106500d_720w.webp)
修改base color为红色
![](https://pic3.zhimg.com/80/v2-cb6a39670c6d94c8703314239f00ebd2_720w.webp)
修改后效果预览
### 查看深度模板测试结果
![](https://pic1.zhimg.com/80/v2-10b497754fb1ed7ba0e12ced038e6fd4_720w.webp)
红色测试不同多,绿色测试通过
### 查看纹理在那些事件引用
![](https://pic4.zhimg.com/80/v2-ee8e0720527ca27045b8cefdeb483433_720w.webp)
![](https://pic2.zhimg.com/80/v2-8c3ed3a794c7b2fca04d8fda4ade6195_720w.webp)
PS 资源种点击链接
![](https://pic1.zhimg.com/80/v2-c70907dd5c33daf560f1be6405bc2a34_720w.webp)
Resource Inspector中右侧查看那些事件使用了此资源
### 纹理太暗
![](https://pic3.zhimg.com/80/v2-d7bdb13af09c789d374523b5df1e6b7e_720w.webp)
![](https://pic2.zhimg.com/80/v2-07d9e1d97230e47a22d641aae5cbbaa1_720w.webp)
### **查看DrawCall耗时**
![](https://pic2.zhimg.com/80/v2-9fc83fdaadf6a453e9dbccefb83ff65d_720w.webp)
### 查看纹理输入输出
![](https://pic2.zhimg.com/80/v2-f464f36300516d1c4c613820d2779fbd_720w.webp)
![](https://pic4.zhimg.com/80/v2-3e224c777f579d5bdcc8752a06231217_720w.webp)
### 重名名纹理
![](https://pic4.zhimg.com/80/v2-a08a7d270a4c8de3c952298ba209bcfb_720w.webp)
### 如何对比数据
将过滤后的数据导出为文本使用对比工具进行对比。用于发现dc耗时问题
![](https://pic3.zhimg.com/80/v2-358566c0e1500019840fc914d7394fc2_720w.webp)
![](https://pic4.zhimg.com/80/v2-84f8f79ac5d241946aeabaa59576a947_720w.webp)
![](https://pic4.zhimg.com/80/v2-6dca8e57407f84970125b9114b9bde2f_720w.webp)

View File

@@ -0,0 +1,134 @@
---
title: UE5RayTracing篇-1-——NSight-Graphics
date: 2022-11-11 17:51:43
excerpt:
tags:
rating: ⭐
---
最近打算开始研究UE的RayTracing但欲先利其事必先利器必要的Debug手段还是需要会用。在发现RenderDoc不支持RayTracing后花了些时间学习了一下NSight Graphics。一般的流程就是使用NSight启动要调试的游戏项目进程之后按F11截帧并使用所需工具查看。下面的章节将会简单介绍一下它的功能。
<!--more-->
## UE RayTracing与Shader开发相关
UE的RayTracing Debug界面需要输入命令后才会显示
```
show raytracingdebug 1
r.RayTracing.DebugVisualizationMode Performance
r.RayTracing.DebugVisualizationMode Traversal Node
```
UE5.0.1这些命令无效5.02虽然可以显示Debug界面了但里面不会渲染任何东西。
为了能够截取到Shader的源码并使用RenderDoc的断点调试需要开启Shader开发变量。位于Engine/Config/ConsoleVariables.ini中
```
//删除前面的//即可开启
r.ShaderDevelopmentMode=1
r.Shaders.Optimize=0
```
## NSight
介绍视频:
- https://youtu.be/yBIKsjd2dJk
- https://youtu.be/LKR5XIW1lgs
文档:
- https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/index.html
- https://docs.nvidia.com/nsight-graphics/AdvancedLearning/index.html
个人觉得相比RenderDocNSight的优势在于
- 更加注重性能统计与优化,适合图形程序使用。
- 拥有Rtx、VR相关工具可以让你debug Rtx、VR程序。
- 2个截帧文件比较功能。
PS.2020.6.1 附加进程到UE5会直接崩溃UE4.27虽然可以启动但依然会有问题。2021.2~2022.3的Shader Profiler的Soucre功能会因为SM不是SM6而只能查看Shader的汇编指令。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_SM6.png)
只能等UE5完全支持SM6后才能使用该功能。测试版本5.02开启项目设置里的DirectX12 (SM6,Experimental)功能也不行)
### 连接到进程
NSight有3种
1. 启动进程并附加设置ApplicationExcutable后点击Launch即可。调试UE4的话还需要设置CommandLineArguments为项目路径比如D:/UnrealEngine/Project/RtxRendering/RtxRendering.uproject
![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/frame_debugger_connect.01.png)
2. 手动连接已启动的进程是由通过NSight Graphics启动的进程才能连接选择启动的进程之后点Attach即可。
![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/frame_debugger_attach.02.png)
3. 远程启动在远程机器上运行Nsight Remote Monitor程序之后再本机上输入远程机器的IP即可。
进入界面后点击Capture for Live Analysis即可截取帧信息默认的截取按键为F11
### NSight工具
NSight有5种工具需要在Connect to process界面中的左下角选择
- Frame Debugger基本用这个 ![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/range_profiler.01.png)
- Frame Profiler ![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/shaderprofiler_summarypage.png)
- Generate C++ Capture
- GPU Trace Profiler
- System Trace
在选择完工具连接进程之后就会显示对应的界面。部分功能需要开启设备访问权限,没开启会显示:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_Error.png)
需要在Nvidia控制面板里进行开启步骤如下
![](https://developer.nvidia.com/sites/default/files/akamai/tools/Common/SupportSolutions/ERR_NVGPUCTRPERM_NvCtrlPanel-EnableDeveloperSettings.PNG)
![](https://developer.nvidia.com/sites/default/files/akamai/tools/Common/SupportSolutions/ERR_NVGPUCTRPERM_NvCtrlPanel-AllowAccessToPerfCtrs.PNG)
其他平台的步骤可以参考https://developer.nvidia.com/nvidia-development-tools-solutions-err_nvgpuctrperm-permission-issue-performance-counters
#### Frame Debugger与Frame Profiler
Frame Debugger工具包含了Frame Profiler这些工具里Shader Profiler工具最为重要。
- Shader Profiler
- Summary![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/shaderprofiler_summarypage.png)
- Function Summary显示Shader函数的GPU性能占用分布。
- Hot Spots显示Shader代码的GPU性能占用分布。
- Source 可以打开特定Shader文件并且显示每行代码的GPU性能占用分布。NSight2022.3版本无法显示UE5的Shader源码![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/shaderprofiler_sourcecorrelation.png)
- All Resource可以查看渲染说用到的贴图、Buffer以及场景模型的BLAS![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_AllResource.png)![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_BLAS.png)
- Geometry该功能需要选中绘制命令才能显示对应的多边形。具体操作是Scrubber中选中Event右键选择Open Event List再从Event窗口选择绘制事件即可Filter中输入Draw就可以找到绘制事件
- PixelHistory查看一个像素绘制过程的工具。可以通过在Resource中选择绘制的RT之后使用该工具点击想要查看的像素。 ![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_PixelHistory.png) ![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_PixelHistory2.png)
- Shader Timeing HeatingRtx相关工具可以查看Rtx Shader在屏幕上的资源占用分布图。但有bug。分析UE5的某个分发Ray事件必然会崩。![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_ShaderTimingHeatmap.png)
PS.Ctrl+滚轮放大与缩小Event时间轴在左上角Frame Debugger里可以打开其他工具所有的UI介绍可以参考https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/index.html#frame_debugging_profiling_ui
#### Generate C++ Capture
进入工具,点击**Generate C++ Capture**后会捕获并且生成一个VS工程。并且显示以下界面
![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/generated_capture_numbered.01.png)
该功能与RenderDoc保存截帧文件类似但NSight是截取当前帧的渲染情况并且生成一个不断渲染当前帧的程序以便后续复现与Debug。之后依次点击Build=>Execute=>Connect即可进入Frame Debugger界面。该工具还支持嵌入式Linux、桌面Linux。
#### GPU Trace
NSight的截帧功能在连接界面可以设置截帧范围截取的帧数另一个相比RenderDoc的优势就是比较2个帧文件。启动进程后点击**Generate GPU Trace Capture**就可以截取帧文件。并进入类似Frame Debugger的界面。
![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/gpu_trace_launch.01.png)
![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/gpu_trace_capture.01.png)
同时可以设置为高级模式,以截取更多的信息:
![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/gpu_trace_options_advanced_mode_config.png)
![](https://docs.nvidia.com/nsight-tools/nsight-graphics/UserGuide/graphics/gpu_trace_capture_advanced_mode_warning.png)
#### System Trace
需要额外下载Nsight Systems工具才能使用。该工具主要分析CPU与GPU直接交互情况。
#### 其他Rt Shader相关的功能
APIInspector的Rt Shader会显示额外所需的HitTable等数据
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_APIInspector_RtxShader.png)
##### Acceleration Structure View
首先可以看一下UE中的相关文档https://docs.unrealengine.com/5.0/en-US/ray-tracing-performance-guide-in-unreal-engine/
- BLASBottom Level Acceleration Structure Updates。静态模型的BLAS会在模型载入时构建。动态物体与骨骼物体会每帧构建。
- TLASTop Level Acceleration Structure。TLAS会每帧构建。
在NSigh中可以从API Inspector或All Resource只能显示BLAS中打开并查看。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/NSight_ShowTLAS.png)
视角控制:
- WASD — 向前、向后、向左或向右移动相机
- 箭头键- 向前、向后、向左或向右移动相机
- E/Q - 向上/向下移动相机
- Shift/Ctrl - 更快/更慢地移动相机
- 鼠标滚轮——放大/缩小
- 鼠标左键 + 拖动- 向前或向后移动相机并向左或向右旋转
- 鼠标右键 + 拖动- 旋转相机
- 鼠标中间 + 拖动- 跟踪相机(向上、向下、向左、向右移动)
- 鼠标左键 + 鼠标右键 + Drag — 跟踪相机(上、下、左、右)
- ALT + 鼠标左键 + 拖动- 围绕选定几何体旋转相机
- ALT + 鼠标右键 + 拖动- 放大/缩小
- 双击或 F - 将相机聚焦在选定的几何体上

View File

@@ -0,0 +1,57 @@
---
title: UE5RayTracing篇-2-——Pix与NSight-Graphics补充
date: 2022-11-11 17:53:44
excerpt:
tags:
rating: ⭐
---
## 前言
最近继续学习Rtx所以就总结一下相关debug工具的使用方法。当然如果你是A卡用户大概就需要[【新鲜资讯】Radeon™光线追踪分析器RRA1.0正式上线](https://zhuanlan.zhihu.com/p/549772737)。
## Pix
下载地址位于https://devblogs.microsoft.com/pix/download/
1. 首先启用`Pix for UnrealEngine`插件。
2.`ConsoleVariables.ini`中添加`r.D3D12.AutoAttachPIX=1`,或者在启动命令也就是`CommandLineArguments`最后添加`-attachPIX`
3. 在Pix中设置启动参数点击启动就可以截帧了。
>注意Pix与RenderDoc插件冲突需要关闭RenderDoc插件才能正常运行。
可以通过点击GUI中的照相机图标或者按下键盘上的PrintScreen截图键键来截帧。直接点击摄像机图标是没办法截到场景信息的所以只能用鼠标点击场景窗口让窗口获得焦点之后再按截图键才能截到。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221114134346.png)
查看渲染过程只需要GPUCapture如果要调试性能就需要使用TimingCapture。具体操作可以参考PIX的操作视频教程位于https://www.youtube.com/watch?v=rLClOkrE47w&list=PLeHvwXyqearWuPPxh6T03iwX-McPG5LkB&index=2
PS.Collect Timing Data需要在win10上开启`开发人员模式`,具体操作为
1. 打开开始菜单,输入`开发者`
2. 进入`开发者选项`,勾选开发人员模式。
### 4个功能Tab
Pix的主要功能集中在这4个Tab中。
Overview主要是展示一下EventList在点击`CollectTimingData`下方会显示时间轴。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221114155739.png)
Pipeline显示当前Event的Shader、管线状态以及其他相关数据。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221114152359.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221114151354.png)
想要DebugShader可以在Pipeline找到对应Shader并在Shader文件上右键点击`Open In Debugger`后进行调试。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221114154405.png)
当然也可以在输入的RT上双击鼠标左键之后点击`DebugPixel`进行debug。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221114154555.png)
Debugdebug时需要点击左上方Overview下面的运行按钮这里我已经点了。这里我很好奇为啥没有类似Renderdoc的断点功能。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221114155712.png)
Tools大概是一个测试性能的工具可以测试Basic Information、Depth/Stencil、Primitives and Rasterization、Bandwidth、TDR Analysis、ExecuteIndirect。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221114155948.png)
## NSight For VisualStudio
之后测试了一下NSight的VS插件虽然本质上就是一个帮你填写启动参数的工具但的确方便。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111175647.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111175715.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111182344.png)
测试完之后就能查看光追的一些参数。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111183638.png)

View File

@@ -0,0 +1,77 @@
---
title: CAD可视化与MovieRenderQueue笔记
date: 2021-09-16 13:55:15
tags: Film
rating: ⭐️
---
## 前言
本文为一些个人经验总结,仅供参考。
## DataSmith
### ReTessellation
对DataSmith导入的物体进行重新细分增加或者减少面数这样就不用重新导入了。
## DataPrep Tools
可以代替python来实现Asset处理自动化。当然会Python就可以定制自动化命令了。
## 初始化
1. 将曝光单位设置成EV100可选
2. 使用SM_ColorCalibrator调整场景曝光度。推荐使用Automotive Materials中BP_Calibration
3. 仅使用天光作为照明源。环境球使用引擎的SM_SkySphere。
4. 启用r.RayTracing.Reflections.ReflectionCaptures使用ReflectionCaptures作为最后一次反射反弹结果。
## 解决临时轴心问题
1. 创一个空Actor作为轴心并设置成Moveable之后将其移动到对应位置。
2. 将CAD模型以及其根物体都改成Moveable之后将之前的空Actor作为root节点。
## 使用Automotive Materials的问题
在Raytracing下使用Automotive Materials会出现第二次反射物体变黑的问题这是官方做的优化措施但CAD产品可视化会注重反射效果所以需要把这个措施给去掉。具体做法是把父材质中最后的MF_Bounce_Rough去掉就可以了。
## MovieRenderQueue
### 输出Alpha
Movie Render Queue中有.exr和.png两种图片格式可输出带Alp通道但仅勾选“output Alpha"还不行。须在项目设置中找到”Enable alpha channel support in post processing“(在后期处理中启用透明度通道支持改为“Linear color space only”仅限线性空间颜色
>勾选“output Alpha"已经被替换为Accumulate Alpha选项在Output选项卡中。
同时还需要将天空球设置为Hidden In Game。
### 抗锯齿:
不管怎么设置画面还是会出现一些噪点如果是制作视频可以使用MovieRenderQueue来解决本人使用的参数为
- Temporal Sample Count16
- Render Warm Up Count64
- Engine Warm Up Count64
本人只需要好的反射效果所以TAA采样数不会设置太高。其他有动态模糊以及其他与TAA相关的效果就需要适当把Temporal Sample Count提高了。
在抗锯齿中
关闭TAA
测试结果:渲染结果没有锯齿,但部分反射效果会有微小锯齿
### 控制台变量:
- r.RayTracing.Reflections.MaxBounce 7
- r.RayTracing.Reflections.SamplesPerPixel 64
7次光线反弹以及64spp效果已经达到要求了。
### 关闭降噪器
使用MovieRenderQueue时因为不太关注实时性能所以可以关闭降噪器来提高渲染效果精度。
- r.AmbientOcclusion.Denoiser: 0
- r.DiffuseIndirect.Denoiser: 0
- r.RayTracing.SkyLight.Denoiser: 0
- r.Reflections.Denoiser: 0
- r.Shadow.Denoiser: 0
- r.RayTracing.GlobalIllumination.Denoiser: 0
经过测试,在关闭反射降噪器之后,对于拉丝金属表面这种有微小细节的表面,细节会锐利很多。
## 修复渲染Slot第一帧时的问题
在使用Sequence以及MovieRenderQueue渲染的时候经常发现Slot的第一帧会出现渲染错误。解决方法是将CameraCut中的轨道往前拉1~2帧即可。
## 增加物体运动的连贯感
简单的方法就是使用动态模糊或是提高帧数。但我制作的视频可能会随时暂停查看具体细节,所以我选择提高帧数并且关闭运动模糊。
## DLSS
DLSS可以在同一画质下提高帧率。因此你可以在使用DLSS的情况下适当提高采样以减少噪点。
需要注意目前版本不会对MovieRenderQueue产生效果4.27已经可以了)。

View File

@@ -0,0 +1,32 @@
---
title: 使用MovieRenderQueue输出视频格式
date: 2023-04-28 11:05:46
excerpt:
tags:
rating: ⭐
---
# 前言
具体可以参考**Command Line Encoder**章节官方文档https://docs.unrealengine.com/5.1/en-US/cinematic-rendering-export-formats-in-unreal-engine/
# 下载编码器
首先我们需要下载编码器这里使用ffmpeg。下载地址
https://ffmpeg.org/download.html#build-windows
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230428111356.png)
这里我选择第一个网址之后选择一个FullBuild版本即可。
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230428111637.png)
# 设置编码器
`ProjectSettings - Plugins - Movie Pipeline CLI Encode`中进行设置。
- Executable Path设置ffmpeg.exe的路径。
- Video Codec填写视频编码格式我这里填写libx264。
- Audio Codec填写音频编码格式我这里填写aac。
- Output File Extension填写视频后缀名我这里填写mp4。
具体可以参考doc/ffmpeg-codecs.html的**Video-Encoders**与**Audio-Encoders**以及doc/ffmpeg-formats.html。
# 输出用于剪辑的更高质量视频
具体可以参考官方文档的**Apple ProRes Video Codecs**或**Avid DNx Video Codecs**章节。
大致步骤是启用**Apple ProRes Media**或**Avid DNxHR/DNxMXF Media Plugin**插件。之后就可以在MRQ中添加输出**Apple ProRes**或**Avid DNx [8bit]**,最终输出的格式为**mov**与**mxf**。

View File

@@ -0,0 +1,81 @@
---
title: 提高光线追踪反射效果
date: 2021-02-07 13:55:15
tags: Film
rating: ⭐️
---
## 关闭与光追冲突的选项
## 让反射效果柔和
r.Reflections.Denoiser 1
## 解决低精度法线造成的反射效果不佳的问题
- 项目设置-Engine-Rendering-Optimizations-Support depth only index=>HeightPrecisionNormals
- 针对个别物体可以通过勾选StaticMesh的Use High Precision Tangent Basis,之后电解Apply Changes
## 对最后一次光线反弹后对环境球进行采样
r.RayTracing.Reflection.ReflectionCaptures 1
## 调整天光的Raytracing采样数目
默认是4可以适当提高
## MovieRenderQueue
输出设置
- Anti-Aliasing:SpatialSampleCount:5
- TemporalSampleCount:3
- Override AntioAliasing:true
文档中的参数rtx2080ti
- 空间采样数量Spatial Sample Count1
- 临时采样数量Temporal Sample Count64
- 覆盖抗锯齿模式Override Anti Aliasing Mode已启用Enabled
- 抗锯齿方法Anti Aliasing MethodNone
- 渲染预热计数Render Warm Up Count120
- 引擎预热计数Engine Warm Up Count120
### Console Variable
![MRQ_Configure_Console](https://docs.unrealengine.com/Images/RenderingAndGraphics/RayTracing/MovieRenderQueue/MRQ_Configure_ConsoleFull.webp)
- r.MotionBlurQuality: 4
- r.MotionBlurSeparable: 1
- r.DepthOfFieldQuality: 4
- r.BloomQuality: 5
- r.Tonemapper.Quality: 5
- r.RayTracing.GlobalIllumination: 1
- r.RayTracing.GlobalIllumination.MaxBounces: 2
- r.RayTracing.Reflections.MaxRoughness: 1
- r.RayTracing.Reflections.MaxBounces: 2
- r.RayTracing.Reflections.Shadows: 2
- r.RayTracing.GlobalIllumination.FinalGatherDistance [number of units]
### 其他光线追踪控制台命令
许多光线追踪特征值已针对实时使用进行了优化。这意味着它们通过减少样本数量,限制最大反射数量或其他措施,从而牺牲质量以换取性能。
下面是你可以在影片渲染队列中使用的更多控制台变量,以质量换取性能。这一点特别有用,因为仅当从队列运行渲染时,此功能才执行这些命令,并且对于你可能已在编辑器中的后期处理体积中设置的任何实时设置,该设置不会永久覆盖。
逐像素采样: 每个光线追踪功能都可以使用很少或很多样本生成最终结果。去噪器使用像素较少,通常用于计算量繁重的任务。借助影片渲染队列,你可以选择禁用降噪器,并增加逐像素样本,以便提高质量。
部分示例为:
- r.RayTracing.Reflections.SamplesPerPixel
- r.RayTracing.Shadow.SamplesPerPixel
- r.RayTracing.GlobalIllumination.SamplesPerPixel
最大反射数Maximum Number of Bounces在场景中进行多次反射或光线反射生成更自然、更高质量的效果从而让光线跟踪功能例如反射、全局光照和透明涂层从中受益。这些设置对于实时渲染来说开销很大。
- r.RayTracing.GlobalIllumination.MaxBounces
- r.RayTracing.Reflections.MaxBounces
- r.RayTracing.Reflections.MaxUnderCoatBounces
天空光照Sky Light 在实时光线追踪中,为反射和全局光照等功能计算每帧时,由于距离无限,天空光照可能造成额外的开销。
使用影片渲染队列工作时以下CVAR可以在光线跟踪中启用其他天空光照选项
- r.RayTracing.GlobalIllumination.EvalSkyLight
- r.RayTracing.SkyLight.EnableTwoSidedGeometry
- r.RayTracing.Reflections.RayTraceSkyLightContribution
- r.RayTracing.SkyLight.EnableMaterials
## 视频笔记
地址https://www.bilibili.com/video/BV1dZ4y1H7f2
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/MovieRenderQueue.png)
### 需要调节的参数
- 灯光的采样数目
- 后处理里搜索Sample 反射设置为16 AO 10

View File

@@ -0,0 +1,206 @@
# Ue4照明技术引导视频笔记(静态光照部分)
https://www.youtube.com/watch?v=jCsrWzt9F28&list=WL&index=7&t=0s
https://forums.unrealengine.com/unreal-engine/events/107078-training-livestream-lighting-techniques-and-guides-jan-24-live-from-epic-hq?134388=
https://cdn2.unrealengine.com/CommunityTab%2FEnglish%2F2017%2FJan+23%2FLiveStream_Lighting-11f85d1762b463154b5f53f7468e135f65955bed.zip
https://wiki.unrealengine.com/LightingTroubleshootingGuide
## Volume
### Lightmass Importance Volume
用于提高指定区域内的LightMap烘焙效果主要是间接照明区域外的默认为低质量效果。
### LightMass Character Indirect Detail Volume
提高区域内的间接光照缓存的密度(缓存点会更加密集)。
## 间接光照缓存
### 模型中光照选项
## LightMass设置
### 解决LightMass漏光问题
1、提高间接光照的质量WorldSetting-》Indirect Lighting Quality<br>
2、降低WorldSetting-》Indirect Lighting Smoothness的值(但是构建时间会增加)
3、尽量使用整块模型
## 地形阴影
### 远距离阴影(Far Shadow)
一般是方向光作用在地形上的阴影使用的是Cascaded Shadow Maps。<br>
相关选项有:
方向光源-》Cascaded Shadow Maps-》 Far Shadow Cascade Count阴影精细程度与Far ShadowDistance显示距离
可以通过show=》advanced=》ShadowFrustums显示阴影调试只对方向光有效。以此可以观察Cascaded的级数切换距离。
地形是默认开启Far Shadow的Actor需要手动开启。
## 自发光物体
后处理空间中除了设置GI倍率还可以自定义GI的颜色。
讲了自发光材质的GI这里有了个取巧的方式通过PostProcessVolume绑定自发光物体来调整Gi好尴尬
2017年的版本还是通过LPV来实现的GI。
## LightMass Debug
在构建选项里有一个Use Error Coloring选项可以用来标记UV不合格的物题但在产品级质量构建中则不会显示这些Debug标记。
## Lighting Info位于构建按钮菜单中
里面可以调整LightMap渲染密度选项、空间占用大小以及分辨率调整。
# Ue4照明技术引导视频笔记(动态光照部分)
https://www.youtube.com/watch?v=nm1slxtF_qA
https://forums.unrealengine.com/unreal-engine/events/113380-training-livestream-lighting-techniques-and-guides-2-dynamic-light-april-4-live-from-epic-hq?140547=
## 距离场光线追踪技术
首先需要开启距离场生成,在项目设置——渲染——灯光——勾选 生成模型距离场 选项。
级联阴影在显示远阴影时为了效果会增加阴影级数这样会增加消耗而距离场阴影的消耗相对较小所以会采样近距离级联远距离距离场阴影的配合方式。同时它可以解决因为屏幕空间渲染技术而导致的阴影失真的问题。另一个主要用处就是距离场AO了。或者可以使用距离场来写Shader。
模型距离场在模型Asset设置中有个分辨率缩放选项的分辨率会影响阴影效果投射到别的物题上的阴影与自阴影。从视频可以看出阴影缺少了很多细节而且并不正确。此时勾选Show——Visualize——MeshDistanceField查看模型距离场从而选择合适的分辨率对debug阴影有很大帮助。
### 如何开启
需要在灯光处勾选“Ray Tracing Distance Field Shadow”选项开启。
### 较为的使用场景
渲染树木植被等高面数、复杂物体的阴影。
### 植被渲染注意
对于树叶等需要双面渲染的物体需要在模型设置中勾选Two-Sided Distance Field Generation以生成正确的距离场。
### 级联阴影切换
可以对灯光中的Cascaded Shadow Maps——Dynamic Shadow Distance MoveableLight进行设置来达到级联阴影与软阴影的切换靠近物体会切换成级联阴影
### 几个调试用命令
r.Shadow.MaxResolution 可选参数256~2048默认2048
r.Shadow.MinResolution 可选参数16~128默认32
r.Shadow.FadeResolution 可选参数64~2048默认64
r.Shadow.DistanceScale 可选参数0.5~2默认1
r.Shadow.RadiusThreshold 可选参数0.01~0.5默认0.03
r.Shadow.CSM.MaxCascades 可选参数1~16默认3
r.Shadow.CSM.TransitionScale可选参数0~2默认1
r.ShadowQuality 可选参数1~5默认5
r.LightFunctionQuality 可选参数0~2默认2
r.DistanceFieldShadowing True~False默认True
r.DistanceFieldAO True~False默认True
r.ParticleLightQuality 可选参数0~2默认1
### 其他注意事项
光源中的SourceRadius会对软阴影产生影响学过光追的人都知道为什么
## 胶囊阴影
因为也是一种软阴影方案所以光源的LightSourceAngle选项也会对此产生影响。
### 如何启用
1. 在角色Asset中指定ShaowPhysicsAsset。
2. 在角色Asset的Lighting选项卡中勾选Capsule Direct Shadow或者Capsule Indirect Shadow
## 接触阴影(Contact Shadows)
ContactShadowLength参数大于0时就会开启默认是关闭的。视频使用值为0.02
### 原理
将接触阴影的长度设为大于零的值后,渲染器将通过场景的深度缓存从像素的位置到光源进行光线追踪。举一个典型的例子来说,将接触阴影长度的最大值设为 1此处的 1 则代表光线遍历整个屏幕。而将接触阴影长度的值设为 0.5 则意味着光线遍历半个屏幕。注意:场景深度缓存中的获得的采样将保持不变,意味着增加接触阴影的长度时将出现更多噪点(穿帮)。长度为 0.75 的接触阴影比长度为 0.1 的接触阴影生成的噪点更多。
### 应用场景
1. 当前平台只支持一个光源且需要使用Marching实现的视差Shader。
2. 角色细节阴影与墙面植物(爬山虎)阴影。
## 级联阴影
级联阴影分为近阴影与远阴影。两者皆可以设置数量、过度距离以及过度参数(在光源中设置)。
当然想要真正显示远阴影则需要勾选物体中的Far Shadow选项。
## 其他技巧
### 天光(SkyLight)
天光也有距离场AO的选项在调试时需要关闭别的光源进行调试。
### HDRI Backdrop
https://docs.unrealengine.com/en-US/Engine/Rendering/LightingAndShadows/HDRIBackdrop/index.html
4.23处的新功能对于CAD产品展示有很大帮助。
# Wiki笔记
部分条目的解释有配图,我懒得贴了,所以推荐直接看原文。
## 问题解答
### 静态部分
#### 为什么我的阴影死黑?
在光照条件下深黑色阴影通常意味着没有填充光。这种情况经常发生在室外环境中一个方向的光代表太阳。UE4有一个内置的方法来提供一个影响填充光的世界我们把它称为天光。
#### 不想生成光照贴图
1. Rendering——Lighting——AllowStaticLighting确保引擎不会生成光照贴图。项目级
2. 打开世界设置——Lightmass——Force No Precomputed Lighting。(关卡级)
#### 双面渲染物体的阴影问题
1. 在StaticMesh——Lighting——LightMassSetting中勾选UseTwoSidedLighting。仅对静态光照
2. 在物体的材质选项中勾选TwoSided
#### 灯光上出现X标记
因为有超过4个光源重叠在一起这样会严重影响性能。动态光在该重叠区域会强制设为一个动态光源而静态光源会在烘焙光照时弹出重叠提示。
### 动态部分
#### 阴影不正确
##### 方向光
###### Dynamic Shadow Distance Movable
以摄像机为起点的阴影覆盖距离该值为0则代表禁用该功能。
###### Dynamic Shadow Distance Stationary
以摄像机为起点的阴影覆盖距离该值为0则代表禁用该功能。
###### Num Dynamic Shadow Cascades
view frustum被分割成的级联的数量。更多的级联将导致更好的阴影分辨率但会显著增加渲染成本。
###### Cascade Distribution Exponent
控制级联的分布,是离相机更近(高值)还是更远(低值)。值1表示转换将与分辨率成正比。
###### Cascade Transition Exponent
级联之间的过度参数,较低的数值产生会较生硬的过度,而较高的数值会产生较平缓的过度。
###### Shadow Distance Fadeout Fraction
阴影淡出参数。较高的值会使阴影显得较淡一些,而较低的值会使阴影显得较深一些。
###### Far Shadow
级联阴影的远阴影开关,可以解决当摄像机处于较远距离时,物体的阴影会消失的问题。
#### 调整级联阴影以得到更好的效果
通过调整上述设置,可以很好地调整阴影的出血值和精度。下一节将尝试调整到最佳成都,以获得更好的精度与阴影。找到一种适合任何特定游戏的平衡将需要耗费时间、精力来进行大量测试。
以下是默认设置下会出现的问题。
![image](https://d26ilriwvtzlb.cloudfront.net/a/ae/1AdjustingSettings2_ProblemAreas.png)
调整后的结果
![image](https://d26ilriwvtzlb.cloudfront.net/8/8c/1AdjustingSettings2_finalResult.png)
##### 对于所有的动态光源
###### Shadow Bias
控制阴影在场景中的精确程度。默认值是0.5,这是权衡了精度与性能的值。
###### Shadow Filter Sharpness
控制阴影边缘的锐化程度。
##### 为什么可移动光源在较远时其遮挡关系会出错
![image](https://d3ar1piqh1oeli.cloudfront.net/4/4a/1.png/642px-1.png)
![image](https://d3ar1piqh1oeli.cloudfront.net/0/09/4.png/642px-4.png)
为了解释这一点我们首先需要了解Ue4的渲染优化方法。引擎根据场景深度来判断场景中Mesh的可见性首先物体需要处于摄像机矩阵中如果Mesh过于远离摄像机Mesh将不会被渲染或是被遮挡。所以就会出现图中的现象。
你可能会注意到,当选择物体时,灯光恢复正常。这是预料之中的,因为它处于焦点状态。
解决方法是在Mesh属性界面调整Bounds Scale选项的大小。默认值设置为1.0。建议的调整范围为1.1、1.2左右,调整的量不宜过大,会影响性能与阴影质量。
可以viewport > Show > Advanced > Bounds 开启包围盒显示进行debug。
另一个解决思路就是使用聚光灯或者使用静态光烘焙光照贴图。
### 静态部分
你可以在世界设置中的LightMass调整以下参数以获得更好的效果
Indirect Lighting Quality设置成2或者更高。
Indirect Lighting Smoothness通常被设置为0.65~0.7之间。数值越低噪点会越多。
#### 如何控制静态照明的全局照明效果?以及光线反弹的美妙之处
默认情况下LightMass的光线反弹次数为3我们可以在Settings > World Settings > LightMass
中修改反弹次数以获得更好的效果。
光线的第一次反弹所需的计算时间是最长的。之后的反弹对实际构建时间的影响较小,但对视觉效果的影响也要小得多。
#### 解决阴影“脏”的问题
首先这种现象的原因与GI中的间接光照有关。
以下使用默认场景来创建2个测试用的关卡其中Mesh的光照贴图分辨率设为256后期空间开启人眼自适应。
![1st Interior Room, lit with direct and 1st bounce lighting](https://d3ar1piqh1oeli.cloudfront.net/4/42/3_Room1_Clean.png/553px-3_Room1_Clean.png)
<center>直接光照与一次反弹的间接光照</center>
![image](https://d3ar1piqh1oeli.cloudfront.net/c/c6/4_Room2_dirty.png/551px-4_Room2_dirty.png)
<center>只有一次反弹的间接光照</center>
一个较为暴力的手法就是提高光照贴图分辨率,但这不是最好的方法。因为这样做会增大内存消耗。
![image](https://d3ar1piqh1oeli.cloudfront.net/7/78/5_lightmap1024.png/550px-5_lightmap1024.png)
<center>光照贴图分辨率为1024的结果</center>
在WorldSettings——LightMass中可以对以下参数进行设置
##### Indirect Lighting Quality
控制间接光照的质量,以获得更佳的效果。
##### Indirect Lighting Smoothness
控制光照贴图中细节的平滑度。
##### Num Indirect Lighting Bounces
间接光照光线的反弹次数。
在对这些参数进行调整后即使是光照贴图分辨率为512其效果也比1024的好了。
![image](https://d3ar1piqh1oeli.cloudfront.net/0/0a/15_FinalResult.png/551px-15_FinalResult.png)
<center>光照贴图分辨率为512</center>
![image](https://d3ar1piqh1oeli.cloudfront.net/7/78/5_lightmap1024.png/550px-5_lightmap1024.png)
<center>光照贴图分辨率为1024</center>

View File

@@ -0,0 +1,7 @@
---
title: VirualShadowMap
date: 2023-03-18 09:27:10
excerpt:
tags:
rating: ⭐
---

View File

@@ -0,0 +1,244 @@
本文为《Lighting with Unreal Engine Masterclass》视频的学习笔记地址为https://youtu.be/ihg4uirMcec。
其中有关LightMass的笔记
http://www.tomlooman.com/lighting-with-unreal-engine-jerome/
翻译版
https://mp.weixin.qq.com/s/CUVJ57s_qsCNzdVHw5OhNQ
### 首先确定你的材质是物理正确的
几个常见的错误:
BaseColor太暗或者太亮
Metallic应该为0或者1。黑色或者白色Mask
Specular应该为0~1范围的浮点数。不应该为颜色值。它会导致反射亮度不一致
常见材质的BaseColor数值都可以网上查询到我记得《全局光照技术》中有说。世界中没有物体是纯白、纯黑的即RGB为0或者1
PS.关于“Metallic应该为0或者1”演讲者的意思是纯材质排除混合材质情况。
~~因为迪士尼的渲染模型也是基于微表面的如果在金属上的一个块微表面区域有40%的区域覆盖着 3D扫描用喷漆表面完全哑光材质那这块微表面的光照该如何计算呢~~
~~而迪士尼的渲染模型里Metallic就有插值效果0-1。如果有问题那为什么不在Shader里直接判断材质属于金属还是绝缘体。~~
### 如何实现一个良好的关照场景
#### 关闭后处理空间中干扰项
以下操作都在Post Process Volume中。用于去除对场景亮度的干扰。
##### 禁用自动曝光
视频中的操作是在后处理空间中将最低亮度与最高亮度设为1。
目前版本已经使用物理灯光所以第一条应该改为将Lens-Exposure
中的Min Ev100与Max Ev100设为0。
##### 禁用SSAO以及SSR空间反射
将ScreenSpaceReflection中的Intensity设为0。将Ambient Occlusion的Intensity设为0。反射球可以不用删掉。
##### 保持默认Tone Mapping
确定Tonemapper新版本为Film中值为默认值。
##### 关闭Vignetting与Bloom效果
将Lens-Bloom中的Intensity设为0。将Lens-Image Effect中Vignette Intensity设为0。
#### 设置测试用球(与色卡
视频使用了Chrome球(BaseColor 1、Metallic 1、 Roughness 0)与Gray球 50%灰(BaseColor 0.18(Linear)、Roughness 1、Metallic 0)进行测试。使用亮度为3.141593(Pi)的方向光此时灰球的亮度为0.5。使用Color Picker、或者截图使用Photoshop查看
但其实引擎已经预制了颜色校准工具只需要在View Options中勾选Show Engine Content之后搜索ColorCalibrator将SM_ColorCalibrator拖到场景中即可。
## 动态实时灯光VS静态烘焙灯光
### 静态烘焙灯光优缺点
优点:
- 拥有GI效果
- 消耗较少的软阴影效果
- 低GPU性能消耗较小
缺点:
- 迭代效率低
- 光照效果是静止的
- 当地图较大时,会占用大量显存。
### 动态实时灯光优缺点
优点:
- 所见即所得
- 光照效果是动态的
缺点:
- 没有GI效果
- GPU性能消耗较大
### 静态烘焙灯光
#### 场景流程
1. 放入方向光(stationary)
2. LightMass烘焙(GI)
3. 放入天光(static)
PS.因为新版本的改动,天光已经可以进行多次反弹,并且可以发射光子了。所以我认为应该第四部,再次烘焙灯光。
#### Min LightMap Resolution(静态模型Detials)
控制UV块之间Padding值。良好的Padding值可以减少像素浪费以及、提高阴影效果在固定LightMap分辨率下。Min LightMap Resolution应该被设置为小于或者等于LightMap Resolution的值。作者一般直接设为与LightMap Resolution相等的值
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightmapresolution.jpg?resize=768%2C323&ssl=1)
#### Num Indirect Lighting Bounce(世界设置)
许多用户喜欢把这个值提高到 100 次,不过好在这个对烘焙时长影响不大,但同时对光照质量影响也不大。实际使用这个值设置为 3 - 5 次就足够了。
下面是对比数据:
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_1.jpg?ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_2.jpg?resize=768%2C325&ssl=1)
![image](https://i0.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_3.jpg?resize=768%2C338&ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_4.jpg?resize=768%2C331&ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_5.jpg?ssl=1)
#### Static Lighting Level Scale(世界设置)
改变lightmass的级别。默认值为 1越小细节越多最终我们使用 0.1。
其原理是增加单位的区域的采样点的量。但这样会产生大量噪点。
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightlevelscale_1.jpg?resize=768%2C453&ssl=1)
![image](https://i0.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightlevelscale_2.jpg?resize=768%2C455&ssl=1)
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightlevelscale_3.jpg?resize=768%2C452&ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightlevelscale_4.jpg?resize=768%2C457&ssl=1)
#### Indirect Lighting Smoothness(世界设置)
该值大于1是会对LightMap进行平滑处理反之会进行锐化过高的数值会导致漏光现象。对于这个属性作者建议设置为0.7~1。
#### Indirect Lighting Quality(世界设置)
增加进行final gathering运算时的采样光线量。提高数值可以减少间接照明时的噪点但代价是极大地增加构建时间。
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_indirectlightingquality_1.jpg?ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_indirectlightingquality_2.jpg?resize=768%2C450&ssl=1)
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_indirectlightingquality_5.jpg?resize=768%2C452&ssl=1)
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_indirectlightingquality_10.jpg?resize=768%2C447&ssl=1)
#### Lighting Build Quality(在构建下拉按钮中)
调整LightMass烘焙质量的总开关。用于整体调整烘焙质量对烘焙时长影响很大。
- Preview耗时 2 分 16 秒)预览级
- Medium耗时 7 分 48 秒)中级
- High耗时 13 分 58 秒)高级
- Production耗时 30 分 22 秒)产品级
作者的习惯:大部分的工作时间都使用预览级,有时会切换到中级进行效果预览。在周末可能会切换到高级或者产品级进行烘焙来查看效果。
#### Lighting Level Scale与Indirect Lighting Quality的设置技巧
保持Static Lighting Level Scale × Indirect Lighting Quality = 1。你就可以得到一个噪点较少的场景。
#### Volume Light Sample Scale
减少Static Lighting Level Scale会增加这些空间采样样本密度
#### 添加LightMass Portal提高室内场景光照质量
给室内场景中的窗户添加LightMass Portal以此可以减少噪点。
其原理是模拟天光效果,向室内发射光子。但也因此会在增加发射光子阶段(渲染过程中)的时间。
## Lighting Scenarios
在游戏制作过程中有时会存在同一场景使用多套Lightmap的情况模拟天气系统以及一天中不同时段的光照等。在Unreal 4引擎中可以利用Lighting Scenarios功能达到此效果。使用Lighting Scenarios时首先需要创建不同的光照Level然后在Level设置页面将其设置为Lighting Scenario类型如下图所示
![image](https://pic2.zhimg.com/80/v2-ce6f0a63eac3abcfcdedf1b68ce108cd_hd.jpg)
另外设置Level的加载方式为使用Blueprint加载
![image](https://pic4.zhimg.com/80/v2-e52d3177593069c8043ca6e8a7879107_hd.jpg)
接着对不同Level设置不同的光照进行烘焙。烘焙结束后在Persistent Level Blueprint中添加Load Stream Level节点对Level进行加载并设置要加载的Level名称Day如下图所示
![image](https://pic3.zhimg.com/80/v2-c3db25960facf2c84981647472c3f416_hd.jpg)
### 动态实时灯光
#### 场景流程
1. 放入方向光(moveable)
2. 放入天光(moveable)
3. 开启DistanceFieldAO
4. 开启SSAO
#### 距离场技术
距离场因为存储了空间信息可以解决屏幕空间渲染技术的缺陷。比如SSAO。距离阴影可以产生与级联阴影不同的软阴影效果。同时你可以在材质编辑器获取距离场数值以制作一些特殊效果。
需要注意的:
- 只有静态物体可以使用距离场
- 避免非规整缩放
- 巨大物体的距离场分辨率会很低(这里建议把大物体进行拆分)
- 距离场AO的更新会分布在多个帧中所以你没有办法得到真正的实时AO。但这个通常不会引起玩家注意。
- 你可以通过提高距离场分辨率来修复一些距离场技术渲染不正确的情况。
- 距离场AO是可以着色的你可以使用它来模拟一些光线反弹效果。
#### Shadow Map
##### Shadow Bias
控制阴影在场景中的精确程度。默认值是0.5,这是权衡了精度与性能的值。
但是在渲染角色时,需要适当权衡这个值,以求在效果正确的基础上对性能消耗最小。
##### 级联阴影
当在一个较大表面上投射阴影时,一个贴图的分辨率可能是不够的,而且会有大量的像素损失。于是就出现使用多个贴图拼在一起显示阴影的技术。
Lighting condition EV100
Daylight
Light sand or snow in full or slightly hazy sunlight (distinct shadows)a 16
Typical scene in full or slightly hazy sunlight (distinct shadows)a, b 15
Typical scene in hazy sunlight (soft shadows) 14
Typical scene, cloudy bright (no shadows) 13
Typical scene, heavy overcast 12
Areas in open shade, clear sunlight 12
Outdoor, natural light
Rainbows
Clear sky background 15
Cloudy sky background 14
Sunsets and skylines
Just before sunset 1214
At sunset 12
Just after sunset 911
The Moon,c altitude > 40°
Full 15
Gibbous 14
Quarter 13
Crescent 12
Blood 0 to 3[6]
Moonlight, Moon altitude > 40°
Full 3 to 2
Gibbous 4
Quarter 6
Aurora borealis and australis
Bright 4 to 3
Medium 6 to 5
Milky Way galactic center 11 to 9
Outdoor, artificial light
Neon and other bright signs 910
Night sports 9
Fires and burning buildings 9
Bright street scenes 8
Night street scenes and window displays 78
Night vehicle traffic 5
Fairs and amusement parks 7
Christmas tree lights 45
Floodlit buildings, monuments, and fountains 35
Distant views of lighted buildings 2
Indoor, artificial light
Galleries 811
Sports events, stage shows, and the like 89
Circuses, floodlit 8
Ice shows, floodlit 9
Offices and work areas 78
Home interiors 57
Christmas tree lights 45
https://youtu.be/t_tT7pwO_j8
对于曝光的建议是:在调整最大最小值后与偏移后(最暗与最亮处的效果匹配),把切换速度调成最高。
该视频的流程:
CubeMap=》Skylight=>LightMaps=>Lightmass Protals=》LightMass Setting=》LightMass Important Volume=》Post Process=》Turn off AO=》Reflection probes
#### 帕拉共
帕拉共因为考虑多平台部分平台可能不支持自适应所以曝光的范围比较小旧版本0.35~0.45
#### 曝光与灯光强度
根据Daedalus51的测试及一些数据方向光强度可以达到125000Lux然后天光5000cd/m²配合16EV值那个只是为了测试场景的数值。
Daedalus51的灯光学院系列视频
https://www.youtube.com/user/51Daedalus/videos
当然这些视频也已经搬运到国内了:
https://www.bilibili.com/av59548243/
物理灯光文档
https://docs.unrealengine.com/zh-CN/Engine/Rendering/LightingAndShadows/PhysicalLightUnits/index.html
### 天光问题
有人提出了一个问题大多数HDR文件你是不知道他用了什么曝光标准以及拍摄时的光照强度
天光转换ev100 0=》16 1=》2^16
#### 使用天空来实现天光效果
添加一个自发光材质给球可以使用EditorSkySphere缩放到Sky Distance Threshold大小后LightMass就会把这个球当做是天空球用以烘焙天光。
https://zhuanlan.zhihu.com/p/70288801
SkySphere的问题 解决方案在材质里乘以 39808
室内保证打光均匀。
选择合理的曝光数值
#### 黑夜曝光方案
min brightness of 0.1 and a max brightness of 1适当增加天光的亮度。

View File

@@ -0,0 +1,132 @@
## 前言
本文是对于《Daedalus51的灯光学院系列视频》、《Lighting with Unreal Engine Masterclass视频》、《Ue4照明技术引导系列视频》的总结。相关网址与链接可以参看前一篇文章。
撰写本文时《Daedalus51的灯光学院系列视频》只看完第一章所以本文后续还会更改请各位读者注意。另外因为本人技术能力有限如有错误还请各位指正。
### 三个视频的观看建议
>Lighting with Unreal Engine Masterclass
从简到深地讲解了静态光照与动态光照特性对比、2种光照方案的技术点以及Epic的基本打光流程。缺点是没有很好的讲解具体的打光思路以及流程。
>Ue4照明技术引导系列视频
该系列一共2个视频主要介绍了光照技术中的各种细节与参数。是功能与参数讲解向视频。PS.个人感觉配套的Wiki比视频有用多了Wiki中讲解若干阴影问题的解决方法。本视频已经被搬运到B站中(机翻)英双字幕。https://www.bilibili.com/video/av59548243/
>Daedalus51的灯光学院系列视频
一共19个视频看完需要1~2个星期。一共4个章节每个章节都是一个案例。通过这个视频你可以大致了解到Ue4的打光流程、每步操作的意义以及部分功能背后技术的原理。让你了解如何判断场景出了哪些问题出现什么问题该如何解决什么时候该做什么。
建议的观看顺序为《Lighting with Unreal Engine Masterclass》=》
《Ue4照明技术引导系列视频》=》《Daedalus51的灯光学院系列视频》
## 灯光类型对比
以下几点都是直接翻译视频中的观点,这里我总结一下:
>动态光照的主要优点是所有效果都是实时的对于效果的迭代会非常快。主要缺点是没有GI。
>静态光照的主要优点有GI但因为效果不是实时的迭代的效率相当会相对较低。
静态光照一般会使用Stationary类型的灯光其中的Dynamic Shadow Distance参数是切换动态与静态阴影开关当为0是所有阴影都将是烘焙的当数值较大时(5000)所有近距离阴影都将是动态的。Daedalus51推荐的数值为3000~8000之间。在这个数值的距离观察时会切换为静态阴影
### 静态烘焙灯光优缺点
优点:
- 拥有GI效果
- 消耗较少的软阴影效果
- 低GPU性能消耗较小
缺点:
- 迭代效率低
- 光照效果是静止的
- 当地图较大时,会占用大量显存。
### 动态实时灯光优缺点
优点:
- 所见即所得
- 光照效果是动态的
缺点:
- 没有GI效果
- GPU性能消耗较大
### Static
烘焙直接光照与间接光照(GI)
GPU性能消耗最少
### Stationary
烘焙间接光照
高质量的直接关照与阴影(动态)
高质量与中等性能消耗
四个及以上的灯光重叠限制多出的灯会使用static方式计算
### Movable
动态光照与阴影
GPU性能消耗最多
没有间接光照
## 前置流程
以下两点主要参考于Lighting with Unreal Engine Masterclass视频部分来自于Daedalus51的视频。
### 实现没有干扰的默认光照场景
使用初始模板建立的场景。
#### 关闭后处理空间中干扰项
以下操作都在Post Process Volume中。用于去除对场景亮度的干扰。
##### 禁用自动曝光
视频中的操作是在后处理空间中将最低亮度与最高亮度设为1。
目前版本已经使用物理灯光所以第一条应该改为将Lens-Exposure
中的Min Ev100与Max Ev100设为0。
##### 禁用SSAO以及SSR空间反射
将ScreenSpaceReflection中的Intensity设为0。将Ambient Occlusion的Intensity设为0。反射球可以不用删掉()。
##### 保持默认Tone Mapping
确定Tonemapper新版本为Film中值为默认值。
##### 关闭Vignetting与Bloom效果
将Lens-Bloom中的Intensity设为0。将Lens-Image Effect中Vignette Intensity设为0。
##### 杂项
后处理的GI选项只对LightMass烘焙过的GI有效。、
不建议使用后处理的AO中的Cubemap Texture已经被天光代替
#### 确认天光与方向光为默认数值
天光亮度为1
方向光亮度为3.141593(π)
#### 设置测试用球(与色卡)
视频使用了Chrome球(BaseColor 1、Metallic 1、 Roughness 0)与Gray球 50%灰(BaseColor 0.18(Linear)、Roughness 1、Metallic 0)进行测试。使用亮度为3.141593(Pi)的方向光此时灰球的亮度为0.5。使用Color Picker、或者截图使用Photoshop查看
但其实引擎已经预制了颜色校准工具只需要在View Options中勾选Show Engine Content之后搜索ColorCalibrator将SM_ColorCalibrator拖到场景中即可。
### 确定你的材质是物理正确的
在将模型导入并赋予材质后检查场景:
《Lighting with Unreal Engine Masterclass》说
几个常见的错误:
BaseColor太暗或者太亮
Metallic应该为0或者1。黑色或者白色Mask
Specular应该为0~1范围的浮点数。不应该为颜色值。它会导致反射亮度不一致
常见材质的BaseColor数值都可以网上查询到我记得《全局光照技术》中有说。世界中没有物体是纯白、纯黑的即RGB为0或者1
PS.关于“Metallic应该为0或者1”演讲者的意思是纯材质排除混合材质情况。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/PbrDiffuse.png)
#### 关于BaseColor
Daedalus51提出场景中的BaseColor应该要较为平缓亮度、对比度与饱和度别太高这一点也在在Ue4的LightMass文档中的“使用LightMass获取最佳质量”章节中有提到。
特别是饱和度饱和度如果过高会让人感觉画面奇怪。1.5章节开头就在调整贴图的饱和度与亮度。
检查的方法是切换到BaseColor查看模式截图再使用Photoshop的Histogram查看颜色分布。Daedalus51使用的方法是使用PS对截图使用平均模糊滤镜之后观察图片的各个值。
但从视频中可以看出,这个步骤更多的是靠经验,所以这里还是建议各位仔细看下对应视频。
##### 在Ue4中修改贴图
一种简单修复方法就是打开TextureAsset直接修改里面的饱和度、亮度、对比度等选项。但是作者还是建议使用Photoshop修改源文件再更新Asset
## 结语
在下一篇文章中我会总结各个步骤的重要细节以及各个功能所应用的场景。但是文章只能记录操作步骤与流程不能记录使用经验所以本人依然推荐想深入学习光照的人去看一下Daedalus51的视频。
#### Daedalus51视频中的场景
第一章节:
户外傍晚:
使用纯动态光照
距离场AOSSAO好像关闭了
CubeMap天光并且自定义下半球颜色
其他为后处理
户外早晨:
使用静态光照Stationary的动态阴影不算的话
CubeMap天光并且自定义下半球颜色
第二章节:
洞穴场景(可以看成是室内)。

View File

@@ -0,0 +1,341 @@
## 前言
在这一章,我将会总结一些有关静态光照相关的参数、使用场景与设置技巧。
本文图较多,手机党请注意。本文仅作为个人笔记使用,不保证所有操作都是正确的,请各位谨慎阅读。
### Daedalus51的流程
首先我总结一下Daedalus51的流程如有遗漏还请指正。以下流程仅为笔记记录使用不一定正确。
个人不太推荐看第二章节后半段文档视频,因为作者一直都在调节材质(场景、粒子)。如果是建筑可视化方向的可以直接看第三章。
#### 1.5章节的流程
1. 去除干扰场景光照的后处理效果(本系列第一篇文章)
2. 检查贴图的固有色是否正确并进行调整。(本系列第一篇文章)
3. 调整天光以符合设计目标。(本系列第一篇文章)
4. 设置曝光相关属性。
5. 将所有灯光都是设置为Stationary类型。并开始设置方向光与其他主光源的基础属性方向、亮度等
6. 设置LightMass属性使用预览级对场景进行测试渲染。
7. 开启LightingOnly模式对光照进行检查。
8. 给场景中的带有反射属性的物体添加反射球并调整属性。Daedalus51的流程中没有关闭空间反射如果你按照我之前写的关闭空间反射这里要记得开启之后开启Reflections模式查看场景反射情况。
9. 设置场景植被(有光照无关)
10. 调整场景雾气
11. 后处理调色
**测试烘焙**<br>
注意去掉世界设置中的Force Precompute Lighting选项。
关闭Compress LightMaps选项压缩灯光贴图为了追求效果但如果游戏项目推荐打开这样可以节约显存占用。
作者一般不会再一开始就开启LightMass的AO烘焙选项世界设置中。因为一般情况下都会也应该使用SkyLight所以我们应该把LightMass的EnvironmentColor设置为0。
**突破材质贴图数目限制**<br>
因为全都使用静态烘焙灯光所以在对应材质中Usage选项卡中勾选Use With Static Lighting这样材质中的贴图采样数数统计才会正确(因为使用LightMass会占用2张贴图)
材质编辑器的Texture Sampler节点的Sampler Source选项决定了一个材质所能使用贴图数量使用Shared:Wrap选项可以突破Dx11的贴图数量限制。
**Stationary类型灯光阴影说明**<br>
Station类型的灯光对于静态物体会烘焙灯光对于动态物体会使用动态阴影。
Dynamic Shadow Distance StationaryLight是切换开关当为0是所有阴影都将是烘焙的当数值较大时(5000)所有近距离阴影都将是动态的。作者推荐3000~8000之间。在这个数值的距离观察时会切换为静态阴影
**有关反射球**
反射球会影响所有物体产生影响(高反射物体如果没有反射球,渲染结果往往是错误的),你可以选择给小物体使用小反射球,之后在给所有物体一个大反射球。(小反射球的优先级高)
**提高空间灯光样本密度(本人加的私货)**
树木、草地等植被因为模型比较密集所以无法获取准确的LightVolume样本信息。可以使用Volumetric LightMap Density Volume来增加空间样本数量。
其他相关空间还有:<br>
**Lightmass Importance Volume**
用于提高指定区域内的LightMap烘焙效果主要是间接照明区域外的默认为低质量效果。
**LightMass Character Indirect Detail Volume**
提高区域内的间接光照缓存的密度(缓存点会更加密集)。
#### 2.1章节的流程
1. 删除无用灯光因为Daedalus51在帮人改场景并将其他灯光关闭。
2. 这个关卡原本没有使用LightMass可以看得出缺少AO
可以使用距离场AO等技术补充在发现后处理空间开启了Dynamic Indirect Lighting后查看LPV其实并没有开启。
3. 检查场景模型UV之后使用基础参数进行测试渲染。
4. 在调整后屏幕空间反射质量后使用LightingOnly查看光照烘焙效果。洞穴墙壁太亮度发现是因为模型没有设置为静态导致没有参与烘焙
5. 设置反射球,以消除错误的反射。
6. 关闭指数雾与其他粒子雾(会影响场景亮度)并且将场景中的射灯模型的自发光材质属性降低,重置所有后处理,至此完成了场景基础修改。(这个理论上应该放到第一步中)
7. 使用BaseColor模式查看固有色并调整贴图的饱和度与亮度。
8. 放置LightMass Portal提高指定区域在LightMass烘焙中所产生的的光线量以此来提高洞穴内的效果。
9. 因为场景方向光没有对准洞口所以作者选择关闭方向光提高天光的间接照明亮度并且在WorldSettings中将间接照明反弹提高到100。目前版本应该设置天光反弹次数而作者的版本没有这个选项所以在视频中场景还是很黑
10. 调节曝光与人眼适应。
11. 调整各种材质效果,与贴花效果。(设置贴花的光照模型)
12. 逐个开启之前关闭的光源并调整光源的颜色与范围。调整灯光颜色以及优化Stationary重叠问题并且把一些作用范围较小的灯光设置为动态不产生阴影以对效果进行快速迭代。
13. 继续调节场景中的细节(粒子、材质、灯光)
14. 增加指数雾与使用反转法线球体制作的体积雾。
15. 调节后处理效果。
**有关LPV**<br>
Daedalus51并不推荐在室内设计中使用LPV外部主光源不能对室内产生足够多的间接照明。因为LPV只适用于方向光所以我们应该关闭Dynamic Indirect Lighting选项
**有关屏幕反射**
因为默认状态下r.sss.quality为3所以在后处理空间中调整质量到60以上都和60是一样的。r.sss.quality 2命令对应的最高质量为50输入r.sss.quality 4 可以解锁100质量。
#### 3.1章节的流程
需要解决两个问题:
- 场景中的窗子太小,同时室内灯光没有开启,导致室内光照不足。(可以看得出参考图的曝光度很高,连天空都看不出细节)
- 场景中植物与材质不符合场景。
大致流程如下:
1. 首先进入LightMap Density模式查看光照贴图密度情况并调整分辨率到合适值。保证附近模型的光照贴图密度大致相同
2. 检查后处理空间、天光与方向光并重置至默认数值。建筑可视化应该关闭屏幕空间AO因为LightMass会烘焙效果更好的阴影
3. 将曝光度设置为1让户外与室内曝光统一。这么调整之后室内会变得很黑此时Daedalus51认为我们不应该直接调整天光与方向光的强度与间接照明倍率而是应该调整曝光数值来解决这个问题。
4. 关闭除了天光外的方向光与其他光源并检查LightMass Portl、Lightmass Importance Volume是否设置正确。
5. 修改LightMass选项后进行预览级质量烘焙。提高反弹次数6=》10设置打包灯光与阴影贴图最大大小关闭压缩LightMap功能
6. 对比参考图后发现是曝光的问题之后修改exposure bias0=>4.6),使得场景达到合适的亮度。
7. 删除BP_Sky_Sphere、高度雾与指数雾添加新的使用Hdr的天空球(参照第二章节中的内容:场景捕获)后进行高级质量烘焙。
8. 删除场景中的反射球使用一个Box反射球作为主反射球并将它大致放置在场景中间之后调整Capture Offset数值来校准反射球。
9. 关闭玻璃材质属性中的屏幕反射功能Translucency-Screen Space Reflections反射球效果依然有效。
10. 检查材质是否正确,并进行调整(固有色、贴图等)
11. 在观察到地板的不正常反射后在后处理空间中开启了屏幕空间反射并调节了反射质量。r.sss.quality 4r.sss.Temporal 0之后发现反射噪点是材质粗糙度造成。
12. 调节LightMass属性Static Lighting Level Scale 1=》0.1Indirect Lighting Quality 1=》6Indirect Lighting Smoothness 1=》0.6使用高级质量进行烘焙。这次烘焙花费4小时烘焙的光照可以说相当好了但在细微处还是有一些噪点作者建议可以可以Indirect Lighting Quality再提高一些。
13. 给场景带有高金属度与低粗糙度的物体添加反射球以解决物体渲染结果不对的问题。
14. 视频3.3的时候作者的电脑突然原地爆炸了鸽了段施加但也因为更新了4.18版本可喜可贺。使用Hdr+天光反弹渲染效果变得更加自然了。作者还在场景中添加了方向光、聚光灯等主光源。修改了地板材质并开启LightMass的AO选项以掩盖SSR的问题
15. 作者展示一个骚操作在材质编辑器中使用PreComputedAOMask节点来获取LightMass烘焙的AO信息。
16. 作者将BP_Sky_Sphere加回来同时把天光类型改成了使用CubeMap。
**使用PreComputedAOMask与澡波贴图来实现一些边角污垢效果**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/PreComputedAOMaskEffect.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/PreComputedAOMaskEffectDark.png)
**解决植被顶点动画模糊的问题**
在Project Settins-Rendering-Optimizations勾选Accurate Velocities from Vertex Deformation选项让TAA能够获取正确的速度值即可解决。这对植被渲染十分重要。
#### 4.1章节的流程
这次修改场景的要求是在保证黑暗之魂3场景风格的基础上尽量让场景光照看起来真实。
Daedalus51首先检查了场景
场景的阳光比较强烈,但是天空却看上去很灰暗,场景的角落也比较黑,看来不真实。一些暗处的物体根本看不到细节。
存在问题:
- 场景的阳光比较强烈,但是天空却看上去很灰暗,场景的角落也比较黑,看来不真实。一些暗处的物体根本看不到细节。
- 很多材质开启曲面细分功能但是曲面细分倍率设置的有问题。曲面细分还会导致物体再LightingOnly模式下不显示所以Daedalus51把曲面细分关掉了。
- 天空看来是淡黄色的,但天光却设置为蓝色(可以看得出建筑物有天光反弹的蓝色)。
- 植被没有渲染细节阴影,但光照贴图分辨率却很高。
场景重置操作内容:
- 重置方向光去掉颜色设置亮度为pi设置间接光照强度为1关闭Light Shalf Bloom
- 重置级联阴影数值但保留Dynamic Shadow Distance Stationary的参数
- 关闭材质的曲面细分功能
- 重置世界设置中的LightMass设置。
- 重置天光参数,亮度、颜色、间接照明强度。关闭半球固定色功能。
- 重置后处理空间,并调整曝光数值。
1. 检查场景,将一些参数重置。
2. 调整LightMass设置中的天光反弹与间接光照反弹1=》10。
3. 删除BP_SkySphere,加入Hdr贴图制作的天空球并通过旋转与位移将天空球与太阳匹配。
4. 关闭大气雾。
5. 调整场景中物体的LightMapDensity主要是降低因为有一些物体不需要细节阴影你无法从一片叶子上获得更多的视觉效果而且可以解决TextureStreamPool溢出与烘焙时间长的问题
6. 因为调节了所有的光照贴图密度所以还需要调节世界设置中的Packed Light And Shadow Map Texture Size。
7. 调整天光关闭指数雾气让天光重新捕获场景。调整方向光的间接照明强度从1=》1.5,让场景更加有艺术感。
8. 使用中级质量对场景进行烘焙。
9. 在烘焙的时候作者检查了场景的BaseColor。
10. 烘焙完成进入LightingOnly检查烘焙结果。可以看得出光照烘焙得不错但场景还是挺黑的这说明BaseColor有问题。
11. 在修改完所有BaseColor贴图与部分材质后再次进行烘焙。并在烘焙过程中移除fake灯光。
12. 作者将场景中的火炬资源整合成一个Actor蓝图(灯光、粒子、模型)同时将灯光的间接照明强度从1=》1.6。并再次使用中级质量进行烘焙。
13. 在项目设置中勾选Accurate Velocities from Vertex Deformation选项解决TAA鬼影问题。
14. 在4.6章节作者开始分析黑魂场景,并指出一些补光错误(黄色的太阳光、背景与雾气,但部分地面上却是蓝色的)
15. 作者并不喜欢默认的BP_Sky_Sphere,他更喜欢Hdr天空。同时他给出一种使用FlowMap让Hdr天空变得动态的方法。GDC14 顽皮狗的演讲之后作者分享了一篇Ue4实现方案http://kedama.works/archives/5但很明显这个方案还有巨大的改进空间。
于是在4.6 55min左右作者开始展示改进过的Shader不过还是存在一个顶部图像破裂的bug。这个内容我放在第二篇文章里
**调整前与调整后的LightMapDensity**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/原始LightMapDensity.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/调整过的LightMapDensity.PNG)
**有关体积光照贴图**
Unreal会根据场景模型分布情况分配体积采样点对于重要的地方可以通过添加体积的方式调整采样点间距**动态物体**会根据这些采样点进行插值着色来实现GI效果。但LightMass只会对静态物体进行采样而完全忽略动态物体。
这里作者又提到了树因为树不适合使用LightMass烘焙光照因为树的结构复杂会导致烘焙时间长而且效果很差但是如果不使用LightMass就不会有GI效果。所以作者想到了设置两组树第一组静态用于烘焙体积光照贴图之后在场景中隐藏。另一种动态用于在场景中展示。这样就解决上面说的问题。
但这样做不能算是个完美的方法所以作者希望EPIC能开发让一些特定的动态物体影响空间探针的功能。
**第一次烘焙完的结果**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/但场景还是死黑.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/光照烘焙的不错.png)
**有关BaseColor**
在检查BaseColor的过程中发现地面的泥土材质的BaseColor太黑了这个贴图的中间值为40而达斯·维达那身黑乎乎的材质颜色Vec3(52,52,52)的中间值也至少为50。具体的做法是调整亮度与饱和度。
所以作者对以下贴图进行修改:
- 调整了泥土贴图40=》81
- 调整了地面的砖石贴图70=>104
- 调整了台阶贴图
- 调整了柱子贴图68=》104
- 调整了砖墙贴图97=》125
- 调整了墙体贴图的金属部分,黑色=》中灰色
- 等场景中的其他贴图
作者不建议固有色的中间值低于50或者高于230。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/PbrDiffuse.png)
>4.5章节作者额外补充一些东西个人强烈建议大家去看一下虽然物理灯光因为引擎存在Bug没有讲完。
首先作者介绍了一下GPULightMass可以看得出烘焙速度非常快。这次作者演示的场景比较简单但包含了室内与室外部分非常适合讲解新的物理灯光系统。
**使用物理灯光的原因**
之前版本中Unreal4的灯光使用的是虚幻单位这是的之前版本的灯光没有一个物理值的衡量标准。你很可能会因为后处理空间、曝光等因素影响而无法设置出正确的亮度值。例如你已经将一个区域的效果调整的很自然但是别的区域就会很奇怪bloom、阴影、光照等效果互相不匹配。 而前人已经积累了大量的摄影知识与经验,所以我们没有必要光靠感觉来设置灯光亮度与曝光度,而需要使用摄影标准来设置灯光,从而使得场景更加真实。
## 环境设置
### 天光
类型static 关闭半球固定色
### 方向光
级联阴影的动态阴影距离设置为12000
### 天空球
引擎天空球模型+Hdr图+对应材质,具体参考第二篇文章写的内容。
### 后处理空间
保持默认
### LightMass设置
静态光照细节缩放等级0.1 光线反弹次数10 天光反弹次数10 间接照明质量4 间接照明平滑0.8 ## 视频中的操作过程 ### 使用摄像机参数来调整曝光度 所以我们能做的第一件事情就是通过sunny16原则来设置相机。 具体步骤为在后处理空间——曝光中将模式改成手动之后按照sunny16原则的户外晴天数值来设置Camera属性。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/使用摄像机属性来调整曝光.png)
### 设置天空球亮度
但此时画面还是黑乎乎的这说明场景中的灯光强度太弱了。此时你直接调整天光强度是无效的因为默认的天光是场景捕获模式。此时设置天空球亮度设置材质在视频中亮度提升到68000左右达到了一个不错的效果。这个时候再进行烘焙模型的样子就不再是黑乎乎的了。 >使用像素检测器,可以检查对应像素的光照强度
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/调整环境球亮度中.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/调整环境球亮度.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/环境球亮度调节完成后烘焙天光.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/室外环境合适的曝光度.png)
### 设置方向光
将方向光亮度设置为75000阴天中午再次烘焙光照。 此时场景出了些问题,金属反射不正常(屏幕空间反射)。可能是作者用的版本对于物理灯光的支持不太好。
### 设置室内曝光度
按照sunny16原则的室内数值来设置Camera属性。但是通过相机参数来调整曝光度有个问题那就是曝光度是固定死的无法根据场景亮度进行自适应。这里作者吐槽自适应曝光不能使用物理参数这个问题目前版本已经解决
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/室内环境合适的曝光度.png)
作者登录了一个灯光供应商的网站,网站上提供个灯光的详细参数,有亮度、色温等。 之后作者讲解了坎德拉与流明这种光照单位,这里可以参考日天大佬的文章。 之后作者展示了点光源切换坎德拉与流明但是效果却不一样的问题。作者使用的版本是4.19)也因为这个原因作者没有继续讲解物理灯光流程,着实可惜。
在4.6章节,作者推荐了这篇文章。我看了一下挺不错的,所以我会在第五篇文章中对其进行简单翻译,原文地址:
https://80.lv/articles/setting-lighting-in-unreal-engine-4-20/
### LightMass Debug
在构建选项里有一个Use Error Coloring选项可以用来标记UV不合格的物题但在产品级质量构建中则不会显示这些Debug标记。
### 静态光照流程及属性总结
这些内容主要来自于《Lighting with Unreal Engine Masterclass》、《解密照片级表现技巧一些关于UE4建筑表现的废话》以及李文磊的《在UE4中实现灯光之美》
#### Min LightMap Resolution(静态模型Detials)
控制UV块之间Padding值。良好的Padding值可以减少像素浪费以及、提高阴影效果在固定LightMap分辨率下。Min LightMap Resolution应该被设置为小于或者等于LightMap Resolution的值。作者一般直接设为与LightMap Resolution相等的值
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightmapresolution.jpg?resize=768%2C323&ssl=1)
#### Export Resolution Scale
缩放导出此材质属性时的分辨率。当需要细节时,这对于提高材质分辨率非常有用。
第一幅图像中使用了一个小光源但是光照图的分辨率太低无法捕捉到清晰的半透明阴影。第二幅图像中材质导出的分辨率过低由Export Resolution Scale控制无法捕捉到清晰的阴影。
![image](https://docs.unrealengine.com/4.27/Images/RenderingAndGraphics/Lightmass/5LowLightmapResolution.jpg)
![image](https://docs.unrealengine.com/4.27/Images/RenderingAndGraphics/Lightmass/5LowExportResolutionScale.jpg)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/分辨率影响.jpg)
#### Num Indirect Lighting Bounce(世界设置)
许多用户喜欢把这个值提高到 100 次,不过好在这个对烘焙时长影响不大,但同时对光照质量影响也不大。实际使用这个值设置为 3 - 5 次就足够了。
下面是对比数据:
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_1.jpg?resize=768%2C325ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_2.jpg?resize=768%2C325ssl=1)
![image](https://i0.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_3.jpg?resize=768%2C325ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_4.jpg?resize=768%2C325ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightbounce_5.jpg?resize=768%2C325ssl=1)
#### Static Lighting Level Scale(世界设置)
改变lightmass的级别。默认值为 1越小细节越多最终我们使用 0.1。
其原理是增加单位的区域的采样点的量。但这样会产生大量噪点。
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightlevelscale_1.jpg?resize=768%2C325ssl=1)
![image](https://i0.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightlevelscale_2.jpg?resize=768%2C325ssl=1)
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightlevelscale_3.jpg?resize=768%2C325ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_lightlevelscale_4.jpg?resize=768%2C325ssl=1)
#### Indirect Lighting Smoothness(世界设置)
该值大于1是会对LightMap进行平滑处理反之会进行锐化过高的数值会导致漏光现象。对于这个属性作者建议设置为0.7~1。
#### Indirect Lighting Quality(世界设置)
增加进行final gathering运算时的采样光线量。提高数值可以减少间接照明时的噪点但代价是极大地增加构建时间。
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_indirectlightingquality_1.jpg?resize=768%2C325ssl=1)
![image](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_indirectlightingquality_2.jpg?resize=768%2C325ssl=1)
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_indirectlightingquality_5.jpg?resize=768%2C325ssl=1)
![image](https://i2.wp.com/www.tomlooman.com/wp-content/uploads/2017/10/ue4_indirectlightingquality_10.jpg?resize=768%2C325ssl=1)
#### Lighting Build Quality(在构建下拉按钮中)
调整LightMass烘焙质量的总开关。用于整体调整烘焙质量对烘焙时长影响很大。
- Preview耗时 2 分 16 秒)预览级
- Medium耗时 7 分 48 秒)中级
- High耗时 13 分 58 秒)高级
- Production耗时 30 分 22 秒)产品级
作者的习惯:大部分的工作时间都使用预览级,有时会切换到中级进行效果预览。在周末可能会切换到高级或者产品级进行烘焙来查看效果。
#### Lighting Level Scale与Indirect Lighting Quality的设置技巧
保持Static Lighting Level Scale × Indirect Lighting Quality = 1。你就可以得到一个噪点较少的场景。
#### Volume Light Sample Scale
减少Static Lighting Level Scale会增加这些空间采样样本密度
#### 添加LightMass Portal提高室内场景光照质量
给室内场景中的窗户添加LightMass Portal以此可以减少噪点。
其原理是模拟天光效果,向室内发射光子。但也因此会在增加发射光子阶段(渲染过程中)的时间。
## Lighting Scenarios
在游戏制作过程中有时会存在同一场景使用多套Lightmap的情况模拟天气系统以及一天中不同时段的光照等。在Unreal 4引擎中可以利用Lighting Scenarios功能达到此效果。使用Lighting Scenarios时首先需要创建不同的光照Level然后在Level设置页面将其设置为Lighting Scenario类型如下图所示
![image](https://pic2.zhimg.com/80/v2-ce6f0a63eac3abcfcdedf1b68ce108cd_hd.jpg)
另外设置Level的加载方式为使用Blueprint加载
![image](https://pic4.zhimg.com/80/v2-e52d3177593069c8043ca6e8a7879107_hd.jpg)
接着对不同Level设置不同的光照进行烘焙。烘焙结束后在Persistent Level Blueprint中添加Load Stream Level节点对Level进行加载并设置要加载的Level名称Day如下图所示
![image](https://pic3.zhimg.com/80/v2-c3db25960facf2c84981647472c3f416_hd.jpg)
## BaseLightmass.ini参数
如果你是做室内效果Ue4默认设置的GI可能无法满足你的需求。你可以选择修改引擎配置文件BaseLightmass.ini的参数。以下内容个人感觉给的数值过于巨大将耗费大量时间请大家酌情使用。
**NumHemisphereSample**半球采样数默认值16想要更好的效果可以给128。
**Num Irradiance Calsulation Photons**参与照度计算光子的数量默认是400如果不满足效果可以给2048甚至更多。
结论就是数值越高消除的BUG越多当然花费时间越长所以国外大神给出了一组不同阶段的测试参数用来平衡时间与质量。
### 测试渲染
- NumHemisphereSamples = 64
- IndirectPhotonDensity = 3000
- IndirectIrradiancePhotonDensity = 1500
- IndirectPhotonSearchDistance = 180
### 进一步渲染
- NumHemisphereSamples = 128
- IndirectPhotonDensity = 6000
- IndirectIrradiancePhotonDensity = 3000
- IndirectPhotonSearchDistance = 180
### 最终渲染
- NumHemisphereSamples = 256 or 512 (above 512 will have not much difference)
- IndirectPhotonDensity = 12000 (If there are still flickers of artificial lighting, increase this value along with the parameter below)
- IndirectIrradiancePhotonDensity = 8000
- IndirectPhotonSearchDistance = 180 (180 - 240)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/默认设置.png)
默认设置
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/高质量设置.png)
#### 使用GPULightMass
使用LightMass烘焙光照往往比较慢所以你可以尝试使用GPULightMass。
论坛中的GPULightMass主题
https://forums.unrealengine.com/development-discussion/rendering/1460002-luoshuang-s-gpulightmass
日天大佬写的简单使用说明:
https://zhuanlan.zhihu.com/p/71584366
如果想要最新版本可以在UnrealEngine的github分支中找到。
#### 最终渲染效果提升
这一步视项目需求与目标平台性能而定。
##### 反射质量
在控制台中输入r.SSR.Quality 4
如果电脑性能较强可以修改引擎Shader文件夹中的ScreenSpaceReflections.usf这个文件。在最高质量4中的参数步数以及射线数都是1212。你显卡牛逼的话都调成64。吃不消就往下调。反正你会发现你的帧数会有明显变化。没有效果那么修改完usf文件后退出引擎重新进一次然后控制台输入r.SSR.Quality 4。
##### 抗锯齿
在控制台中输入r.PostProcessAAQuality 6

View File

@@ -0,0 +1,154 @@
## 天光
天光亮度一般来说还是保持为1比较好但以天光作为主要照明是例外夜晚场景
天光有“场景捕获”与“使用CubeMap”两种使用方向。但是本质上还是围绕着CubeMap所以作者更加倾向于使用CubeMap。除非你实现了类似CryEngine的昼夜交替系统。
Daedalus51在视频1.1最后讲解了Skylight技巧推荐大家去看。
### 调试方法
往场景中放入一个Sphere与Cube模型并赋予Chrome材质引擎Content中。假设你已经关闭后处理空间中的屏幕空间反射效果。这样就可以观察天光是否达到你的要求。
在静态光照下LightMass会烘焙出GI效果你可以开启LightingOnly模式查看GI效果。
![image](https://docs.unrealengine.com/4.27/Images/BuildingWorlds/LightingAndShadows/LightTypes/SkyLight/SkyLight_1.webp)
### 使用CubeMap
也就是使用Hdr环境贴图赋予Hdr后记得修改CubeMap Resolution贴图最长边的值必须为2的幂
### 场景捕获
对远景与环境球进行采样,再作为天光数据对场景进行照明。
如果你希望把Hdr图作为环境球在场景中渲染那步骤如下
1. 往场景中加入环境球模型想省事可以使用EngineContent中的SM_SkySphere。
2. 使用以下材质(这里我就直接拿日天大佬的图来展示):
![image](https://pic1.zhimg.com/80/v2-6cb7ed806245a4ef264b909e67fd29a8_hd.jpg)
另外材质的光照模型选择自发光,勾选双面渲染选项。
3. 设置Sky Distance Threshold属性并调整环境球模型默认的SM_SkySphere太小了
4. 关闭模型的Cast Shadow选项。
5. 点击SkyLight标签中Recature按钮你可以在蓝图调用RecaptureSKy函数来进行更新
使用Exr贴图来制作天空球材质好处在于不需要使用材质中的旋转节点直接连入自发光节点即可。同时可以通过旋转模型来旋转天空球。
#### 其他参数说明
**Sky Distance Threshold**当物体处于该数值半径外时天光Actor为坐标为原点天光会对该物体进行采样。
**Capture Emissive**仅对自发光材质进行采样以减少性能消耗。当使用采集每一帧Capture Every Frame建议使用此方法。
**更新天光**需要点击SkyLight标签中**Recature**按钮。
### 下半球颜色控制
在场景中使用了环境球或是默认场景蓝图天球并且天光使用场景捕获时我们只需要上半球的光照照亮场景而下半球的颜色往往会造成“天光漏光”的问题天光不能穿过地面对物体进行照明。此时我们就需要勾选“Lower Hemisphere Is Solid Color”选项并且将“Lower Hemisphere Color”设置为黑色。
在Daedalus51的视频中他展示了另一种使用思路。首先他的场景中是有一大片草地的如果Lower Hemisphere Color使用黑色其实是不正确的。于是他使用PS对开启BaseColor模式的场景进行均匀取色均匀模糊后再取色。再用该颜色设置Lower Hemisphere Color的值。这样效果会更加真实一些。
但也有例外情况,比如汽车等机械产品展示。一般来说这类项目一般会采用**影棚Hdr贴图**此时就需要关闭“Lower Hemisphere Is Solid Color”选项。因为底部的光照效果我们也是需要的。
### 距离场AO
《Lighting with Unreal Engine Masterclass》视频中有提到天光的AO也是距离场AO的重要组成部分。所以如果你要使用距离场AO别忘记天光部分。记得开启AO与距离场观察模式查看其质量
视频偏后段展示了通过设置AO颜色来模拟GI效果不过本人不太推荐使用。
### 动态天光注意事项
如果你的天光是动态类型的,那就需要开启距离场阴影相关功能,不然天光是不会投射阴影的。
### 天光反弹反弹(静态光照有效)
在世界设置中可以设置天光的反弹次数,提高反弹次数以提高效果。
## 动态环境球效果
在Daedalus51的4.6章节视频中,他演示一种动态环境球效果:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/Unreal4DynamicSky.gif)
如果在云层移动速度较慢的情况下,效果还不错。以下是材质实现:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/SkyMaterial.png)
作者在其基础上进行了一定改进但是环境球的顶部会有图像畸变bug。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/Unreal4dynamicSky.png)
## 调整曝光与ToneMapping
1.2 21min左右开始讲解有关曝光的知识这里强烈建议看一下。不过需要注意该视频制作时虚幻引擎还没有加入物理灯光。
https://www.bilibili.com/video/av59548243/?p=2
刚发现官方已经做了曝光功能的解析视频。强烈推荐去看一下。
https://www.youtube.com/watch?v=Q1xi8NwpIqA
>核心思想在于:因为前人已经积累了大量的摄影知识与经验,所以我们没有必要光靠感觉来设置灯光亮度与曝光度,而需要使用摄影标准来设置灯光,从而使得场景更加真实。
### 恶补相关知识
这里我推荐看一下日天大佬文章与Wiki可以恶补相关知识。<br>
虚幻中灯光的物理单位计算公式:
https://zhuanlan.zhihu.com/p/69248316
公式推导:
https://zhuanlan.zhihu.com/p/69348892
只需要知道```$E=3.14\times2^{EV}$```就可以了
物理灯光文档
https://docs.unrealengine.com/zh-CN/Engine/Rendering/LightingAndShadows/PhysicalLightUnits/index.html
曝光值
https://en.wikipedia.org/wiki/Exposure_value
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/35F7CFB7707F4C8699720C28F3409060.octet-stream)
Sunny16
https://en.wikipedia.org/wiki/Sunny_16_rule
EV使用推荐
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/Sunny16.png)
### 操作过程
#### 调整曝光
在设置天光与其他主光源亮度前需要确定好曝光值。在1.2节27min左右Daedalus51打开Visualize——HDR(人眼适应),通过该工具来查看亮度分布、以及亮度调整情况情况。以此来调节后处理空间中的曝光度范围。(调整曝光范围0.03~2 => 0.4~2)
#### 开启ToneMappering
1.2章节34min处这段ToneMappering介绍这段推荐只看英文字幕。
Daedalus51通过命令行查看是否开启ToneMappering输入r.TonemapperFilm,可以查看是否开启ToneMappering功能。0为关闭1为开启输入r.TonemapperFilm 1就可开启ToneMapper功能。
Daedalus51之后调整了ToneMapping曲线在后处理的PostProcess-Film中不过个人不建议调整除非对调色十分了解。
##### 有关Console命令的技巧
这里几个操作都与Console命令有关所以介绍一下Daedalus51介绍的技巧
**查看所有命令详细信息**在Ue4编辑器窗口中点击Help-ConsoleVariables就可以得到console命令表可以查看详细说明。
**在项目启动时默认执行一些命令**
在项目目录的Config文件中DefaultEngine.ini(/Script/Engine.RenderSettings)中输入命令。例如r.SSS.Quality=4
##### 有关其他后处理效果
因为ToneMappering会影响到后处理中调色效果所以Daedalus51推荐使用Lut来做后处理。
## 本人对于曝光的看法
本文主要是以笔记的形式来记录在视频中所学习到东西,所以比较杂乱。因为在看完所有视频与资料后,我在此总结一下我对曝光的看法。
在物理灯光换算方面日天大佬花费大量功夫整理出一张换算表这样是效率最高的工作方法。当然我是一个嫌麻烦的人也没有时间仔细研究这些东西还是使用Unreal4的像素检测器直接检测亮度比较方便。
对于曝光,我大致有两种方案:
### 以EV0作为基准的
一个是以EV0作为基准来调整场景灯光的传统方法。之后再通过曝光换算公式将实际灯光亮度转化为EV0的亮度最后再调整自动曝光范围。但这样也就是失去了物理灯光的意义了。
虽然“虚幻争霸”是采用这种曝光方案来布置灯光。但Unreal4马上就要推出物理大气,同时“堡垒之夜”中已经使用了日天系统Sun And Day可以说“物理化”是一个势在必行的趋势。
### 以真实环境EV作为基准
一个是根据场景的环境特征确定EV使用真实灯光的亮度数据。因为是真实世界测量出的数据所以你可以很容易且快速精准地做出逼真效果。相当适合于建筑室内设计等需求照片级渲染质量需求。
#### 解决场景HDR差距较大所产生光照问题
以上两种方案都需要解决场景动态范围差距较大所产生的暗部亮部效果无法兼顾的“问题”。
严格的说这不能算是一个问题,因为这就是正确的渲染结果。
李文磊老师在《在UE4中实现灯光之美》演讲中有说到
>以我个人经验我觉得这个发范围超过8个档位以外的像素大家可能需要小心点如果是0的EV基础值低于-5.3可能会死黑高于3可能会爆掉这是个人的经验。
我们可以通过减少HDR插件来避免这种“问题”的产生具体可以使用以下两种方式
**降低室外天光亮度**
降低室外主光源的亮度来匹配室内EV。避免使用高EV的环境HDR图比如正午大晴天。选择低EV的环境HDR图例如早晨无云、正午多云的环境HDR图。再使用恰当的EV范围进行自动曝光效果就比较好了。
如果不想让用户看到自动曝光的转换过程,可以提高自动曝光速度。
**提高室内灯光亮度**
提高室内灯光的亮度来匹配室外EV。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/提高室内亮度.jpg)
### 动态环境球与天光捕获问题
上文有介绍Daedalus51的动态环境球方案。那么问题来了天光捕获的是哪一帧的环境球我们无法确定。那么天光烘焙结果就是不确定的么那么TrueSky之类的RayMarching材质天光捕获是否会正确呢
因为本人没有TrueSky而且这个问题也无伤大雅所以本人就不研究了。
#### 个人对于动态天空的意见
其实本人不太喜欢Daedalus51的环境球方案因为会穿帮。个人还是倾向于改造BP_Sky_Sphere。BP_Sky_Sphere的问题主要在云与大气上。
云的话还是推荐做成RayMarching计算透明度与阳光散射效果如果是不运动的云可以预计算效果
![image](http://storage.googleapis.com/wzukusers/user-22455410/images/582eb811b16d53XZwq6l/Tracing_Shadows.gif)
大气效果应该不是物理的不然Unreal4也不会在4.24推出物理大气系统。

View File

@@ -0,0 +1,154 @@
## 前言
动态光照需要使用多种照明技术协同工作这一点与只需要LightMass的静态光照不同。所以这一章我将会总结一些有关动态光照相关的参数、使用场景与设置技巧。
### Daedalus51的流程
#### 1.2章节的流程
1. 去除干扰场景光照的后处理效果(本系列第一篇文章)
2. 检查贴图的固有色是否正确并进行调整。(本系列第一篇文章)
3. 调整天光以符合设计目标。(本系列第一篇文章)
4. 分别开启Visualize Distance Field Ambient Occlusion、Visualize Mesh Distance Field检查距离场以及对应效果。
5. 设置曝光相关属性。
6. 设置方向光与其他主光源的基础属性(方向、亮度等)
7. 给场景中的带有反射属性的物体添加反射球并调整属性。Daedalus51的流程中没有关闭空间反射如果你按照我之前写的关闭空间反射这里要记得开启之后开启Reflections模式查看场景反射情况。
8. 添加补光灯光,以及伪造灯光来增加气氛
9. 放置高度雾。(雾可以改变场景色调)
10. 后处理调色。
**如何补光**<br>
使用 关闭Cast Shadow、Min Roughness为1的点光源进行补光。这种灯光十分柔和可以用于提亮局部死黑部分。配合Light Channel可以只对某一些物体起作用。
**阴影建议**<br>
如果灯光开启了动态阴影那么请不要使用点光源而需要使用聚光灯。Daedalus51的理由是:点光源会向6个方向投射阴影类似Box对性能消耗较大。但事实上因为Ue4光照缓存机制所以只有第一次渲染才会有如此大的消耗
当若干几个物体较黑时可以考虑使用聚光灯并且设置Min Roughness为1,这样可以达到提亮物体的作用。
### 调试用命令
- r.Shadow.MaxResolution 可选参数256~2048默认2048
- r.Shadow.MinResolution 可选参数16~128默认32
- r.Shadow.FadeResolution 可选参数64~2048默认64
- r.Shadow.DistanceScale 可选参数0.5~2默认1
- r.Shadow.RadiusThreshold 可选参数0.01~0.5默认0.03
- r.Shadow.CSM.MaxCascades 可选参数1~16默认3
- r.Shadow.CSM.TransitionScale可选参数0~2默认1
- r.ShadowQuality 可选参数1~5默认5
- r.LightFunctionQuality 可选参数0~2默认2
- r.DistanceFieldShadowing True~False默认True
- r.DistanceFieldAO True~False默认True
- r.ParticleLightQuality 可选参数0~2默认1
- r.Shadow.UnbuiltPreviewInGame
- r.Shadow.UnbuiltNumWholeSceneDynamicShadowCascades
- r.Shadow.MaxCSMResolution
- r.CapsuleShadows
更多技巧请参考第二章的《有关Console命令的技巧》段落。
### 级联阴影(Cascaded Shadow Maps)
级联阴影分为近阴影与远阴影。两者皆可以设置数量、过度距离以及过度参数(在光源中设置)。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/级联阴影.png)
级联阴影技术介绍https://gameinstitute.qq.com/community/detail/117522
下面这部分内容总结于《Lighting Troubleshooting Guide》部分条目的解释有配图我也懒得贴了所以推荐直接看原文。<BR>
https://wiki.unrealengine.com/LightingTroubleshootingGuide
##### 方向光特有属性
###### Dynamic Shadow Distance Movable
以摄像机为起点的阴影覆盖距离该值为0则代表禁用该功能。
###### Dynamic Shadow Distance Stationary
以摄像机为起点的阴影覆盖距离该值为0则代表禁用该功能。
###### Num Dynamic Shadow Cascades
view frustum被分割成的级联的数量。更多的级联将导致更好的阴影分辨率但会显著增加渲染成本。
###### Cascade Distribution Exponent
控制级联的分布,是离相机更近(高值)还是更远(低值)。值1表示转换将与分辨率成正比。
###### Cascade Transition Exponent
级联之间的过度参数,较低的数值产生会较生硬的过度,而较高的数值会产生较平缓的过度。
###### Shadow Distance Fadeout Fraction
阴影淡出参数。较高的值会使阴影显得较淡一些,而较低的值会使阴影显得较深一些。
##### 对于所有的动态光源
###### Shadow Bias
控制阴影在场景中的精确程度。默认值是0.5,这是权衡了精度与性能的值。
###### Shadow Filter Sharpness
控制阴影边缘的锐化程度。
#### 调整级联阴影以得到更好的效果
通过调整上述设置,可以很好地调整阴影的出血值和精度。下一节将尝试调整到最佳成都,以获得更好的精度与阴影。找到一种适合任何特定游戏的平衡将需要耗费时间、精力来进行大量测试。
以下是默认设置下会出现的问题。
![image](https://d26ilriwvtzlb.cloudfront.net/a/ae/1AdjustingSettings2_ProblemAreas.png)
调整后的结果
![image](https://d26ilriwvtzlb.cloudfront.net/8/8c/1AdjustingSettings2_finalResult.png)
#### 远距离阴影(Far Shadow)
一般是方向光作用在地形上的阴影使用的是Cascaded Shadow Maps。<br>
相关选项有:<br>
方向光源-》Cascaded Shadow Maps-》 Far Shadow Cascade Count阴影精细程度与Far ShadowDistance显示距离
地形是默认开启Far Shadow的。如果想对Actor启用则需要手动开启。
#### 级联阴影调试
可以通过show=》advanced=》ShadowFrustums显示阴影调试只对方向光有效。以此可以观察Cascaded的级数切换距离。
### 距离场光线追踪技术
级联阴影在显示远阴影时为了效果会增加阴影级数这样会增加消耗而距离场阴影的消耗相对较小所以会采样近距离级联远距离距离场阴影的配合方式。同时它可以解决因为屏幕空间渲染技术而导致的阴影失真的问题。另一个主要用处就是距离场AO了。或者可以使用距离场来写Shader。
注意距离场技术只适用于Static Mesh。
### 如何开启
首先需要开启距离场生成,在项目设置——渲染——灯光——勾选 生成模型距离场 选项。
之后在需要的灯光处勾选“Ray Tracing Distance Field Shadow”选项开启。
### 距离场检查
距离场的构建质量将决定距离场相关技术的渲染效果。距离场分为模型距离场与全局距离场。你可以通过在Show > Visualize >中勾选Global Distance Field、Mesh Distance Field、Distance Field Ambient Occlusion来对渲染效果进行debug。尤其是AO一下子就能找到效果不好的地方。
Daedalus51推荐给这些选项设置快捷键来提高效率。在编辑器设置中搜索Visualize Global Distance Field、Visualize Mesh Distance Field、Visualize Distance Field Ambient Occlusion后设置快捷键即可。
如果距离场效果不佳可以在对应StaticMesh编辑器中设置Distance Field Resoulition Scale来调高分辨率记得按Apply Changes
### 距离场模型问题
因为距离场使用体积贴图来存储空间信息,所以对于巨大物体(比如摩天大楼)应该把模型拆成小块,以减少性能损耗。
同理,模型的形状也不应该奇形怪状,这样会大大的影响距离场构建质量。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/模型形状不佳而导致距离场构建不佳.png)
### 植被渲染注意
因为植被面数高、结构复杂等因为因素不适合使用LightMass、SSAO进行渲染但距离场AO却十分适合。
对于树叶等需要双面渲染的物体需要在模型设置中勾选Two-Sided Distance Field Generation以生成正确的距离场。
### 级联阴影切换
可以对灯光中的Cascaded Shadow Maps——Dynamic Shadow Distance MoveableLight进行设置来达到级联阴影与软阴影的切换靠近物体会切换成级联阴影
取值可以参考静态光照的Dynamic Shadow Distance属性。
### 其他注意事项
光源中的SourceRadius、SourceLength、LightSourceAngle会对软阴影产生影响学过光追的人都知道为什么
天光也有距离场AO的选项千万别忘记了。
## 胶囊阴影
这是骨骼物体的软阴影方案所以光源的SourceRadius、SourceLength、LightSourceAngle选项也会对此产生影响。
- Capsule Direct Shadow此属性将启用来自直接可移动光照的柔和阴影。
- Capsule Indirect Shadow此属性将启用来自预计算光照光照图和天光的柔和阴影。
- Capsule Direct Shadow Min Visibility调整胶囊阴影的明暗度
### 如何启用
1. 在角色Asset中指定ShaowPhysicsAsset。
2. 在角色Asset的Lighting选项卡中勾选Capsule Direct Shadow或者Capsule Indirect Shadow
### 接触阴影(Contact Shadows)
是一种为了弥补级联阴影的缺陷,用于补充角色细节、场景细节阴影的技术。它是一种根据深度信息进行着色的后处理渲染技术。
ContactShadowLength参数大于0时就会开启默认是关闭的。。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/接触阴影.png)
https://docs.unrealengine.com/zh-CN/Engine/Rendering/LightingAndShadows/ContactShadows/index.html
#### 原理
将接触阴影的长度设为大于零的值后,渲染器将通过场景的深度缓存从像素的位置到光源进行光线追踪。举一个典型的例子来说,将接触阴影长度的最大值设为 1此处的 1 则代表光线遍历整个屏幕。而将接触阴影长度的值设为 0.5 则意味着光线遍历半个屏幕。注意:场景深度缓存中的获得的采样将保持不变,意味着增加接触阴影的长度时将出现更多噪点(穿帮)。长度为 0.75 的接触阴影比长度为 0.1 的接触阴影生成的噪点更多。
#### 应用场景
1. 当前平台只支持一个光源且需要使用Marching实现的视差Shader。
2. 角色细节阴影与墙面植物(爬山虎)阴影。
### Light Propagation Volumes
### HDRIBackdrop
4.23的新功能这个东西会把地面部分的HDR投射到一个平面上这样场景中物体的阴影就可以投射到“HDR背景上”。适合于产品展示。奥秘就在这个环境球上。
当然Engine还提供了其他环境球可以根据需要选择。如何选择请参考文档https://docs.unrealengine.com/4.27/en-US/BuildingWorlds/LightingAndShadows/HDRIBackdrop/
不过使用的时候需要注意以下设置:
- HDR贴图设置最大纹理尺寸Maximum Texture Size应与已导入HDR图像的较大分辨率值匹配。
- 应将 Mip生成设置Mip Gen Settings设为NoMipmaps以使用完全品质。
- 调整Hdr Size拍摄场景的大小
- 如果原始设置下如果摄像仪偏离Projection Center点远处的背景就会产生形变解决的方法是勾选Use Camera Projection选项

View File

@@ -0,0 +1,43 @@
## 相关学习资料
### Unreal Circle线下培训专场 2019-09-10带翻译新手教程
https://www.bilibili.com/video/av67278159/?p=9
**建议:除非你一点基础都没有不然不推荐学习。**
### Ue4照明技术引导(静态光照) 2017-1-25视频、学习用场景、wiki、新手教程比Unreal Circle那个稍微进阶一些
- https://www.youtube.com/watch?v=jCsrWzt9F28&list=WL&index=7&t=0s
- https://forums.unrealengine.com/unreal-engine/events/107078-training-livestream-lighting-techniques-and-guides-jan-24-live-from-epic-hq?134388=
- https://cdn2.unrealengine.com/CommunityTab%2FEnglish%2F2017%2FJan+23%2FLiveStream_Lighting-11f85d1762b463154b5f53f7468e135f65955bed.zip
- https://wiki.unrealengine.com/LightingTroubleshootingGuide
**建议讲解了一些较为有用细节问题间接光照缓存、自发光物体烘焙、远阴影等配套的Wiki更是总结大部分光照问题。但是4.18更新了较多内容,有一些知识已经不适用了,但依然推荐学习。**
### Ue4照明技术引导(动态光照)
- https://www.youtube.com/watch?v=nm1slxtF_qA
- https://forums.unrealengine.com/unreal-engine/events/113380-training-livestream-lighting-techniques-and-guides-2-dynamic-light-april-4-live-from-epic-hq?140547=
**建议讲解了级联阴影、距离场、细节阴影的相关细节与几个debug技巧推荐学习**
### 解密照片级表现技巧 2016-7-27为数不多的中文干货教程
此为静态光照、与LightMass使用经验教程主要讲一些参数的经验值
https://www.engineworld.cn/forum.php?mod=viewthread&tid=432&highlight=%E8%A7%A3%E5%AF%86%E7%85%A7%E7%89%87%E7%BA%A7%E8%A1%A8%E7%8E%B0%E6%8A%80%E5%B7%A7
**建议:里面的参数已经可以使用,但本人不建议那么把数值调那么大,建议各位在逐步测试后,再使用。**
### 虚幻4GPU构建方式和注意事项
https://www.bilibili.com/video/av66480934 <br>
https://forums.unrealengine.com/development-discussion/rendering/1460002-luoshuang-s-gpulightmass
**建议国人开发的东西此大神已经是EPIC员工了效果还不错建议大家去尝试。**
### 精通 UE4 Lightmass 2017-11-02为数不多的中文干货教程
本质是对官方视频 https://www.youtube.com/watch?v=ihg4uirMcec
的解读<br>
https://mp.weixin.qq.com/s/CUVJ57s_qsCNzdVHw5OhNQ
### UE4 国外顶级大佬灯光系统全面讲解(机翻字幕)
https://www.bilibili.com/video/av59548243/p1
### 虚幻4中的物理灯光
https://zhuanlan.zhihu.com/p/69248316
https://zhuanlan.zhihu.com/p/69348892
https://zhuanlan.zhihu.com/p/70288801
https://zhuanlan.zhihu.com/p/71515823
https://zhuanlan.zhihu.com/p/71584366

View File

@@ -0,0 +1,199 @@
## 前言
在这一章我将会翻译一些我个人觉得有用的文章以此来作为这个系列的结尾。
本文图较多,手机党请注意。本文仅作为个人笔记使用,不保证所有操作都是正确的,请各位谨慎阅读
## 推荐学习顺序
对于有一定基础知道各种灯光概念却不知如何下手的读者首先推荐观看Daedalus51的第一章节视频。因为第一章节中的知识与技巧几乎覆盖了整合视频。后续的视频虽然也还行但是相对来说意义就没那么大除非你相当有时间。
之后就是推荐看我写的《灯光总结系列》,里面总结各种操作与参数,你可以把它作为参考书进行对照。但这几篇文章因为本人能力、精力有限,没有经过仔细整理,看完会些混乱。
所以之后我推荐看Daedalus51推荐的流程文章与李文磊老师的《在UE4中实现灯光之美》。
另外推荐几个官方的视频:
Ue4曝光入门
https://www.youtube.com/watch?v=Ehg4sLxOH1o
Ue4校色入门
https://www.youtube.com/watch?v=0HMFczWSRig
Ue4后处理入门
https://www.youtube.com/watch?v=aGsUU_bvOgw&feature=youtu.be
## Daedalus51推荐的流程文章
原文地址https://80.lv/articles/setting-lighting-in-unreal-engine-4-20/
**场景初步调试**
首先清理场景这一步包括了调整材质、移除所有旧灯光、放置Lightmass Importance Volumes与Lightmass Portals。一开始的测试渲染会使用默认的LightMass设置。基础效果差不多后调整到以下参数并切换至中级效果进行烘焙至到最终渲染。
体积光照贴图密度取决于环境雾精度。如果对性能没有要求可以使用更高的采样与更高的光照贴图分辨率。
![image](https://new-cdn.80.lv/upload/content/27/images/5d2c340c7051a/widen_920x0.jpg)
再光照模式下查看效果,场景只烘焙了自发光。
![image](https://new-cdn.80.lv/upload/content/90/images/5d2c340d4579f/widen_920x0.jpg)
光照贴图密度
![image](https://new-cdn.80.lv/upload/content/90/images/5d2c340dea0d7/widen_920x0.jpg)
最终烘焙所用的LightMass设置参数
**设置主要光源**
在设置完场景后,开始规划主要光源。首先需要一个来自外部的明亮光源来提供室内绝大多数的照明,但需要保证室内暗部
的灯光依然可见。所以作者选择阴天正午时分的Hdr环境图来充当天光(使用场景捕获+环境球模型+环境球材质)。调整天光材质的亮度至到其物理亮度达到阴天亮度值使用Ue4的像素检测工具可以检测物理亮度值。你可以使用Ue4的像素检测工具检查环境球基础关键区域天空、亮度云层、暗部云层、太阳但在检查时需要关闭ToneMappering并保持曝光度为0
以及关闭雾气等处于摄像鱼天空之间的显示效果,不然像素检测工具会给出错误的数值。
中午时分的天空亮度大约在400(云彩暗部)~10000云彩亮部 `${cd/m^2}$` 。基于作者想要达到效果曝光值EV定在5~7之间就比较室内渲染。对于外部的环境球可以使用EV10。注意一下HDR亮度
![image](https://new-cdn.80.lv/upload/content/2d/images/5d2c340e80bbe/widen_920x0.jpg)
**自发光光源**
作者以相同的方式来设置自发光物体。以下是室内LightingOnly模式下只烘焙自发光的效果应该还有天光本人并不赞同使用自发光来代替灯光除非你的灯光形状非同一般。
![image](https://new-cdn.80.lv/upload/content/9a/images/5d2c340f54990/widen_920x0.jpg)
因为场景中的金属物体会反射到环境球,所以下一步就应该添加反射球了。
细节光照模式(关闭屏幕空间反射)第一张为只有环境球反射高光效果。第二张为只有反射球效果。
![image](https://new-cdn.80.lv/upload/content/c5/images/5d2c341001d0d/contain_620x465.jpg)
![image](https://new-cdn.80.lv/upload/content/c5/images/5d2c3410db9c0/contain_620x465.jpg)
**灯光形状与反射捕捉**
在几次测试烘焙后作者开始放置实际光源与反射球。作者更加倾向于以真实环境作为参考来设置灯光属性例如光源形状与参数。作者会尽可能地使用Static类型的灯光如果该灯光会作用于角色等动态物体那就会使用Stationary类型的灯光。体积光照贴图可以弥补动态光照的不足但这取决于你对显存与GPU运算能力的把控。
作者更喜欢使用色温来控制灯光颜色。毕竟大多数灯光厂家都会标注灯光色温而且Unreal4可以直接通过色温来控制灯光颜色。以下是场景中主光源的设置参数
![image](https://new-cdn.80.lv/upload/content/72/images/5d2c341182d05/widen_920x0.jpg)
**后处理校色与效果打磨**
因为本人对校色不了解,这边就直接贴原文了:
Once Im happy with the lighting, I move over to color grading and polishing. I adjusted some of the Base Colors to push them more in line with a “vintage” green/orange color palette too. During early color grading explorations, I tend to grade in DaVinci Resolve because of the control and scopes, but I eventually recreate the grade directly in the engine to keep it HDR-ready. Unreal lacks some of the tools available in Resolve, but you can achieve a near identical result with what Epic has provided thus far.
Depending on the look, you can usually get most of the way there with the Global controls, then fine tune the shadows, mid-tones, and highlights with their respective controls. Highlights and Shadows will have a threshold value that acts as the range of what is considered a highlight or shadow, and anything in between those two will then be considered mid-tones.
To explain my color grading a little easier, here is a screenshot of my settings. Its worth pointing out that I avoid adjusting the tone mapper. Combined with color grading, it can get very messy to manage. Its better to consider your tone mapper as your “film stock,” and only modify it if you want to change the overall look and not on a per shot basis. I also try to work in stages and as simple as possible. Instead of jumping around between the different controls, I go from Global to Shadows, to Midtones, to Highlights (or whatever I know is the most effective for my grade) and only use what I need. If I can achieve the look I want with one or two sliders, compared to 5 or 6 with 10% influence, Id rather do that because its cleaner, easier to control, and easier to revisit later on without being completely lost.
![image](https://new-cdn.80.lv/upload/content/ab/images/5d2c34122ebc9/widen_920x0.jpg)
- White Balance: I warmed up the scene slightly to increase the feeling of everything being bathed in yellow/orange from the incandescent bulbs and diffused light fixtures
- Tint: a similar concept to the White Balance, but on a green/magenta scale. I pushed it more towards green to aid in the yellowish/sickly feeling. It also helps make the orange in the Base Color pop a little more
- Global:
- Saturation: I adjusted the saturation overall to bring out the orange more
- Shadows:
- Saturation: Desaturated shadows. A common look that film stocks have along with desaturated highlights, but the ACES tone mapper does that one for you
- Contrast: I use Contrast pretty often because its designed around keeping 18% gray neutral and it naturally gives you complementary colors, quickly getting you a more pleasing color palette
- Offset: Added a little bit of blue to the shadows
- Midtones:
- Contrast: Same idea as the shadows. Pushed complimentary colors a tiny bit which helped with additional blues to smooth out the shadow offset
- Gain: Works kind of like a multiplier, and I wanted to get some bleaching around the light areas
- Highlights:
- Gain: Mostly tinted the white spots (emissives and sky) green to push the same feeling as the overall Tint from above, but with a tiny bit of a roll-off into the higher end of the mid-tones to add to the “bleaching”
## 《在Ue4中实现灯光之美》中的灯光Fake技巧
在传统的打光我们会去调整灯光的反差比如提高对比降低对比比如在太阳底下我们拍摄电影我们会通过柔光板或者遮光板去降低反差在UE里面也可以通过一些简单的方式通过透明物体去投影按需求去控制影子的浓度降低对比度。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/Ue4遮光板.jpg)
使用贴图与定制的贴花材质来区域阴影伪造。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/HackAO材质.jpg)
使用后处理材质控制场景亮度、饱和度。对于使用LightMass烘焙的场景可以在调整曝光度比值与间接光照强度。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/HackAO.jpg)
一些场景的光线强度分布差距较大时,会出现只能讨好一边的情况,所以我们可以其中一边为标准调整另一边的灯光亮度。在传统的拍电影过程中也能实现,拍电影的灯光功率都非常大,为了平衡室内外的灯光,室内就是打了这么亮的灯光,当然在引擎中或者游戏中我们非常容易的就能实现。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Lighting/Hack曝光度.jpg)
## 问题解答
原文地址:
https://wiki.unrealengine.com/LightingTroubleshootingGuide
### 静态部分
#### 为什么我的阴影死黑?
在光照条件下深黑色阴影通常意味着没有填充光。这种情况经常发生在室外环境中一个方向的光代表太阳。UE4有一个内置的方法来提供一个影响填充光的世界我们把它称为天光。
#### 不想生成光照贴图
1. Rendering——Lighting——AllowStaticLighting确保引擎不会生成光照贴图。项目级
2. 打开世界设置——Lightmass——Force No Precomputed Lighting。(关卡级)
#### 双面渲染物体的阴影问题
1. 在StaticMesh——Lighting——LightMassSetting中勾选UseTwoSidedLighting。仅对静态光照
2. 在物体的材质选项中勾选TwoSided
#### 灯光上出现X标记
因为有超过4个光源重叠在一起这样会严重影响性能。动态光在该重叠区域会强制设为一个动态光源而静态光源会在烘焙光照时弹出重叠提示。
### 动态部分
#### 阴影不正确
##### 方向光
###### Dynamic Shadow Distance Movable
以摄像机为起点的阴影覆盖距离该值为0则代表禁用该功能。
###### Dynamic Shadow Distance Stationary
以摄像机为起点的阴影覆盖距离该值为0则代表禁用该功能。
###### Num Dynamic Shadow Cascades
view frustum被分割成的级联的数量。更多的级联将导致更好的阴影分辨率但会显著增加渲染成本。
###### Cascade Distribution Exponent
控制级联的分布,是离相机更近(高值)还是更远(低值)。值1表示转换将与分辨率成正比。
###### Cascade Transition Exponent
级联之间的过度参数,较低的数值产生会较生硬的过度,而较高的数值会产生较平缓的过度。
###### Shadow Distance Fadeout Fraction
阴影淡出参数。较高的值会使阴影显得较淡一些,而较低的值会使阴影显得较深一些。
###### Far Shadow
级联阴影的远阴影开关,可以解决当摄像机处于较远距离时,物体的阴影会消失的问题。
#### 调整级联阴影以得到更好的效果
通过调整上述设置,可以很好地调整阴影的出血值和精度。下一节将尝试调整到最佳程度,以获得更好的精度与阴影。找到一种适合任何特定游戏的平衡将需要耗费时间、精力来进行大量测试。
以下是默认设置下会出现的问题。
![image](https://d26ilriwvtzlb.cloudfront.net/a/ae/1AdjustingSettings2_ProblemAreas.png)
调整后的结果
![image](https://d26ilriwvtzlb.cloudfront.net/8/8c/1AdjustingSettings2_finalResult.png)
##### 对于所有的动态光源
###### Shadow Bias
控制阴影在场景中的精确程度。默认值是0.5,这是权衡了精度与性能的值。
###### Shadow Filter Sharpness
控制阴影边缘的锐化程度。
##### 为什么可移动光源在较远时其遮挡关系会出错
![image](https://d3ar1piqh1oeli.cloudfront.net/4/4a/1.png/642px-1.png)
![image](https://d3ar1piqh1oeli.cloudfront.net/0/09/4.png/642px-4.png)
为了解释这一点我们首先需要了解Ue4的渲染优化方法。引擎根据场景深度来判断场景中Mesh的可见性首先物体需要处于摄像机矩阵中如果Mesh过于远离摄像机Mesh将不会被渲染或是被遮挡。所以就会出现图中的现象。
你可能会注意到,当选择物体时,灯光恢复正常。这是预料之中的,因为它处于焦点状态。
解决方法是在Mesh属性界面调整Bounds Scale选项的大小。默认值设置为1.0。建议的调整范围为1.1、1.2左右,调整的量不宜过大,会影响性能与阴影质量。
可以viewport > Show > Advanced > Bounds 开启包围盒显示进行debug。
另一个解决思路就是使用聚光灯或者使用静态光烘焙光照贴图。
### 静态部分
你可以在世界设置中的LightMass调整以下参数以获得更好的效果
Indirect Lighting Quality设置成2或者更高。
Indirect Lighting Smoothness通常被设置为0.65~0.7之间。数值越低噪点会越多。
#### 如何控制静态照明的全局照明效果?以及光线反弹的美妙之处
默认情况下LightMass的光线反弹次数为3我们可以在Settings > World Settings > LightMass
中修改反弹次数以获得更好的效果。
光线的第一次反弹所需的计算时间是最长的。之后的反弹对实际构建时间的影响较小,但对视觉效果的影响也要小得多。
#### 解决阴影“脏”的问题
首先这种现象的原因与GI中的间接光照有关。
以下使用默认场景来创建2个测试用的关卡其中Mesh的光照贴图分辨率设为256后期空间开启人眼自适应。
![1st Interior Room, lit with direct and 1st bounce lighting](https://d3ar1piqh1oeli.cloudfront.net/4/42/3_Room1_Clean.png/553px-3_Room1_Clean.png)
<center>直接光照与一次反弹的间接光照</center>
![image](https://d3ar1piqh1oeli.cloudfront.net/c/c6/4_Room2_dirty.png/551px-4_Room2_dirty.png)
<center>只有一次反弹的间接光照</center>
一个较为暴力的手法就是提高光照贴图分辨率,但这不是最好的方法。因为这样做会增大内存消耗。
![image](https://d3ar1piqh1oeli.cloudfront.net/7/78/5_lightmap1024.png/550px-5_lightmap1024.png)
<center>光照贴图分辨率为1024的结果</center>
在WorldSettings——LightMass中可以对以下参数进行设置
##### Indirect Lighting Quality
控制间接光照的质量,以获得更佳的效果。
##### Indirect Lighting Smoothness
控制光照贴图中细节的平滑度。
##### Num Indirect Lighting Bounces
间接光照光线的反弹次数。
在对这些参数进行调整后即使是光照贴图分辨率为512其效果也比1024的好了。
![image](https://d3ar1piqh1oeli.cloudfront.net/0/0a/15_FinalResult.png/551px-15_FinalResult.png)
<center>光照贴图分辨率为512</center>
![image](https://d3ar1piqh1oeli.cloudfront.net/7/78/5_lightmap1024.png/550px-5_lightmap1024.png)
<center>光照贴图分辨率为1024</center>

View File

@@ -0,0 +1,16 @@
---
title: 解决UE5植被阴影出现噪点的问题
date: 2022-09-26 11:04:44
excerpt:
tags: Lumen
rating: ⭐
---
## 调节参数
默认为32改成4。
>r.Lumen.ScreenProbeGather.DownsampleFactor 4
默认为0.02改成0.01,阴影会变亮一些,也可以减少噪点。
>r.Lumen.ScreenProbeGather.ScreenTraces.HZBTraversal.RelativeDepthThickness = "0.01"
最后需要将预览质量从EPIC调整到Cinematic草上的噪点就没有了。

View File

@@ -0,0 +1,38 @@
---
title: 音乐控制DMX灯光的简单方法
date: 2022-11-09 17:13:17
excerpt: 摘要
tags:
rating: ⭐
---
## 前言
最近公司有个活涉及到DMX打光的活还比较急。为了减少美术同学的压力所以我就想通过音乐分析+播放Sequence的方式来实现。
UE有自带基于FFT的音乐分析功能但只能根据频率进行对应的判断比较麻烦所以放弃了。具体操作可以参考
https://www.bilibili.com/video/BV1FP411c79v/
## AudioAnalysisTools
这里推荐使用audio-analysis-tools原因是免费且开源默认带有一些预设IsHiHat、IsBeat、IsKick、IsSnare同时可以通过频率范围来判断节拍。但也正因为此一些功能不够完善需要自己改一下。
下载地址https://www.unrealengine.com/marketplace/zh-CN/product/audio-analysis-tools
以下是我做的一个案例通过音乐分析与K帧来播放对应的子Sequence预先K好DXM动画来实现音乐控制DMX灯光效果
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/AudioAnalysisTools_1.png)
IsBeatRange是根据指定频率声音的音量进行判断这里可以根据需要来设置判断条件
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/AudioAnalysisTools_2.png)
因为逻辑是写在Actor中的所以搞了按键事件来停止音乐播放。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/AudioAnalysisTools_3.png)
不过需要注意启用这个插件之后导入Wav就会变成这个资产。如果想要获得SoundWave资产就必须停用插件并且再次导入。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/AudioAnalysisTools_4.png)
## DMX移植
只需要在新工程里启用DMX插件之后将官方的DMX的工程迁移即可。需要注意的是Sequence里的需要使用DMX里面设置好DMXLibrary以及补丁这中文翻译槽点满满即可。
## 后续工作
这个方案有一些缺点:
1. AudioAnalysisTools需要播放声音才能获取的数据。
2. AudioAnalysisTools无法使用UE自带的Asset同时会导致无法在导入后生成SoundWave Asset。
3. 无法在Sequence停止播放时停下更好的控制或者在拖动轨道时执行正确的行为。
这就需要通过自定义Sequence Track以及修改插件才能完美实现。

View File

@@ -0,0 +1,10 @@
---
title: Vertex Interpolator材质节点
date: 2022-11-04 10:01:35
excerpt:
tags:
rating: ⭐
---
推荐查看视频https://www.youtube.com/watch?v=KyjlrKwbXCw
本质上是一种将VS数据进行插值最后传递到PS中的节点。需要与CustomUV、VertexColor配合使用。

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