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

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());
```

View File

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

View File

@@ -0,0 +1,12 @@
---
title: Daedalic Entertainment公司分享的第三人称摄像机经验
date: 2022-12-09 14:06:53
excerpt:
tags: Camera
rating: ⭐
---
#### blog website
https://www.unrealengine.com/en-US/blog/six-ingredients-for-a-dynamic-third-person-camera
#### github
https://github.com/DaedalicEntertainment/third-person-camera

View File

@@ -0,0 +1,186 @@
#### 前言
因为GameplayAbility属于蓝图c++混合编程框架所以对蓝图类与地图进行版本管理是十分重要的事情。所以本文将在这里介绍git的二进制文件版本管理方案。
#### 使用过程
1. 下载Gitlfshttps://git-lfs.github.com/~~(现在的git都自带lfs就算没有下个SourceTree也会自带lfs)
2. 使用cmdcd到git仓库所在目录执行git lfs install。一般人都在这一步做错如果做错会存在100mb的文件大小限制
3. 此时目录下会出现.gitattributes文件它用于设置监视的扩展名你可以通过输入```git lfs track "*.扩展名"```的方式来添加扩展名。例如想要监视uasset就输入
```git lfs track "*.uasset"```。最后将.gitattributes加入进版本管理```git add .gitattributes```。
4. 现在你就可以用与管理代码文件相同的方式,管理二进制文件了。
```
git add file.uasset
git commit -m "Add design file"
git push origin master
```
推荐使用SourceTree因为如果你第二步操作有误或是第三步没有添加扩展名它会提醒你的。
#### 蓝图合并与Diff工具
Mergehttps://github.com/KennethBuijssen/MergeAssist
Diffhttps://github.com/SRombauts/UE4GitPlugin
# Git Clone --depth 1 以及转化成完整仓库
git clone --depth 1 https://github.com/EpicGames/UnrealEngine.git
git clone --depth 1 --branch 4.25-plus https://github.com/EpicGames/UnrealEngine.git
转换成完整仓库的方法:
git pull --unshallow
再执行命令修改fetch设置获取所有分支即可。
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
# LFS upload missing objects 解决
输入命令,即可
```
git config --global lfs.allowincompletepush false
```
# LFS删除 很久不用的文件
使用prune命令可以删除LFS中的旧文件。
```
git lfs prune options
```
这会删除认为过旧的本地 Git LFS文件没有被引用的文件被认为是过旧的文件
当前切换的提交
一个还没有被推送的提交到远程或者任何在lfs.pruneremotetocheck设置的
一个最近的提交
默认,一个最近的提交是过去十天的任何一个提交,这是通过添加如下内容计算的:
在获取附加的Git LFS历史部分讨论过的lfs.fetchrecentrefsdays属性的值。
lfs.pruneoffsetdays属性的值默认为3
git lfs prune
你可以为配置一个持用Git LFS内容更长的时间
# don't prune commits younger than four weeks (7 + 21)
$ git config lfs.pruneoffsetdays 21
不像Git内置的垃圾回收 Git LFS内容不会自动删除因此定期执行git lfs prune来保留你本地的仓库文件大小是很正确的做法。
你可以测试在git lfs prune dry-run命令执行后有什么效果
$ git lfs prune --dry-run
✔ 4 local objects, 33 retained
4 files would be pruned (2.1 MB)
更精确地查看哪个Git LFS对象被删除可以使用git lfs prune verbose dry-run命令
$ git lfs prune --dry-run --verbose
✔ 4 local objects, 33 retained
4 files would be pruned (2.1 MB)
* 4a3a36141cdcbe2a17f7bcf1a161d3394cf435ac386d1bff70bd4dad6cd96c48 (2.0 MB)
* 67ad640e562b99219111ed8941cb56a275ef8d43e67a3dac0027b4acd5de4a3e (6.3 KB)
* 6f506528dbf04a97e84d90cc45840f4a8100389f570b67ac206ba802c5cb798f (1.7 MB)
* a1d7f7cdd6dba7307b2bac2bcfa0973244688361a48d2cebe3f3bc30babcf1ab (615.7 KB)
通过使用verbose模式输出的十六进制的字符串是被删除的Git LFS对象的SHA-256哈希值也被称作对象ID,或者OIDs。你可以使用在找到引用某个Git LFS对象的路径或者提交章节介绍的技巧去找到其他想要删除的对象。
作为一个额外安全检查工作你可以使用verify-remote选项来检查Git LFS store是否存在想要删除的Git LFS对象的拷贝。
$ git lfs prune --verify-remote
✔ 16 local objects, 2 retained, 12 verified with remote
Pruning 14 files, (1.7 MB)
✔ Deleted 14 files
这让删除过程非常的非常的缓慢但是这可以帮助你明白所有删除的对象都是可以从服务器端恢复的。你可以为你的系统用久开启verify-remote 选项这可以通过全局配置lfs.pruneverifyremotealways属性来实现。
$ git config --global lfs.pruneverifyremotealways true
或者你可以通过去掉global选项来仅仅为当前会话的仓库开启远程验证。
# Git代理
## 配置Sock5代理
git config -global http.proxy socks5://127.0.0.1:2080
git config -global https.proxy socks5://127.0.0.1:2080
### 只对github.com
git config --global http.https://github.com.proxy socks5://127.0.0.1:2080
git config --global https.https://github.com.proxy socks5://127.0.0.1:2080
### 取消代理
git config --global --unset http.https://github.com.proxy)
git config --global --unset https.https://github.com.proxy)
## 增加超时时间
git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks push -v --tags origin GamePlayDevelop:GamePlayDevelop
Pushing to https://github.com/SDHGame/SDHGame.git
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/b6/a8/b6a8fa2ba03f846f04af183bddd2e3838c8b945722b298734a14cf28fd7d1ab1?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56358->127.0.0.1:1080: i/o timeout
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/76/47/76473fed076cd6f729cf97e66e28612526a824b92019ef20e3973dc1797304e8?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56360->127.0.0.1:1080: i/o timeout
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/78/a0/78a0819db84cdd0d33aa176ae94625515059a6c88fec5c3d1e905193f65bfcdd?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56374->127.0.0.1:1080: i/o timeout
LFS: Put "https://github-cloud.s3.amazonaws.com/alambic/media/321955229/24/ab/24ab214470100011248f2422480e8920fb80d23493f11d9f7a598eb1b4661021?actor_id=12018828&key_id=0&repo_id=325822904": read tcp 127.0.0.1:56376->127.0.0.1:1080: i/o timeout
Uploading LFS objects: 99% (712/716), 210 MB | 760 KB/s, done.
error: failed to push some refs to 'https://github.com/SDHGame/SDHGame.git'
git config --global lfs.tlstimeout 300
git config --global lfs.activitytimeout 60
git config --global lfs.dialtimeout 600
git config --global lfs.concurrenttransfers 1
### LFS Upload Failed (miss) 文件路径
解决方案下载所有LFS数据git lfs fetch --all
### 服务器上不存在
解决方案上传指定lfs文件git lfs push origin --object-id [ID]
### LFS objects are missing on push[](https://docs.gitlab.com/ee/topics/git/lfs/troubleshooting.html#lfs-objects-are-missing-on-push "Permalink")
GitLab checks files on push to detect LFS pointers. If it detects LFS pointers, GitLab tries to verify that those files already exist in LFS. If you use a separate server for Git LFS, and you encounter this problem:
1. Verify you have installed Git LFS locally.
2. Consider a manual push with `git lfs push --all`.
### 强制上传LFS
git lfs push origin --all
### I/O timeout when pushing LFS objects[](https://docs.gitlab.com/ee/topics/git/lfs/troubleshooting.html#io-timeout-when-pushing-lfs-objects "Permalink")
If your network conditions are unstable, the Git LFS client might time out when trying to upload files. You might see errors like:
```
LFS: Put "http://example.com/root/project.git/gitlab-lfs/objects/<OBJECT-ID>/15":
read tcp your-instance-ip:54544->your-instance-ip:443: i/o timeout
error: failed to push some refs to 'ssh://example.com:2222/root/project.git'
```
To fix this problem, set the client activity timeout a higher value. For example, to set the timeout to 60 seconds:
```
git config lfs.activitytimeout 60
```
# 引擎Content管理
.gitignore文件中添加
```
Content/
#Content/
!*.uasset
**/Content/*
**/Content/*/*
!**/Content/EngineMaterials/
!**/Content/EngineMaterials/ToonTexture/
```
# 解决git UTF8文件乱码问题
问题:
```bash
未处理的异常System.ArgumentException: Path fragment '“Content/\351\237\263\351\242\221/Cheetah\302\240Mobile_Games_-_\347\254\254\345\215\201\344 \270\203\345\205\263\302\240Cube\302\240\345\207\240\344\275\225\350\277\267\351\230\265.uasset”包含无效的目录分隔符.
1> 在 Tools.DotNETCommon.FileSystemReference.CombineStrings(DirectoryReference BaseDirectory, String[] Fragments)
1> 在 Tools.DotNETCommon.FileReference.Combine(DirectoryReference BaseDirectory, String[] Fragments)
1> 在 UnrealBuildTool.GitSourceFileWorkingSet.AddPath(String Path)
1> 在 UnrealBuildTool.GitSourceFileWorkingSet.OutputDataReceived(Object Sender, DataReceivedEventArgs Args)
1> 在 System.Diagnostics.Process.OutputReadNotifyUser(String data)
1> 在 System.Diagnostics.AsyncStreamReader.FlushMessageQueue()
1> 在 System.Diagnostics.AsyncStreamReader.GetLinesFromStringBuilder()
1> 在 System.Diagnostics.AsyncStreamReader.ReadBuffer(IAsyncResult ar)
1> 在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
1> 在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
1> 在 System.IO.Stream.ReadWriteTask.System.Threading.Tasks .ITaskCompletionAction.Invoke(Task completingTask)
1> 在 System.Threading.Tasks.Task.FinishContinuations()
1> 在 System.Threading.Tasks.Task.Finish(Boolean bUserDelegateExecuted)
1> 在 System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
1> 在 System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
1> 在 System.Threading.ThreadPoolWorkQueue.Dispatch()
```
```bash
git config --global core.quotepath false
```

View File

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

View File

@@ -0,0 +1,312 @@
## tomlooman写的教程
https://www.tomlooman.com/save-system-unreal-engine-tutorial/
## 保存关卡世界数据与状态
要保存世界状态我们必须决定为每个演员存储哪些变量以及我们需要保存在磁盘上的哪些杂项信息。例如每个玩家获得的金钱数。金钱数并不是世界状态的真正组成部分而是属于PlayerState。尽管PlayerState存在于世界中并且事实上是一个角色但我们还是将它们分开处理这样我们就可以根据它以前属于哪个玩家来正确地恢复它。
## Actor数据
对于 Actor 变量,我们存储其名称、变换(位置、旋转、缩放)和一个字节数据数组,其中包含在其 UPROPERTY 中标有“SaveGame”的所有变量。
```
USTRUCT()
struct FActorSaveData
{
GENERATED_BODY()
public:
/* Identifier for which Actor this belongs to */
UPROPERTY()
FName ActorName;
/* For movable Actors, keep location,rotation,scale. */
UPROPERTY()
FTransform Transform;
/* Contains all 'SaveGame' marked variables of the Actor */
UPROPERTY()
TArray<uint8> ByteData;
};
```
## 将变量转换为二进制
要将变量转换为二进制数组我们需要一个FMemoryWriter和FObjectAndNameAsStringProxyArchive它们派生自 FArchive虚幻的数据容器用于各种序列化数据包括您的游戏内容
我们按接口过滤,以避免在我们不想保存的世界中潜在的数千个静态 Actor 上调用 Serialize。存储 Actor 的名称将在稍后用于识别要反序列化(加载)数据的 Actor。您可以想出自己的解决方案例如FGuid主要用于可能没有一致名称的运行时生成的 Actor由于内置系统其余的代码非常简单并在注释中进行了解释
```
void ASGameModeBase::WriteSaveGame()
{
// ... < playerstate saving code ommitted >
// Clear all actors from any previously loaded save to avoid duplicates
CurrentSaveGame->SavedActors.Empty();
// Iterate the entire world of actors
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
// Only interested in our 'gameplay actors', skip actors that are being destroyed
// Note: You might instead use a dedicated SavableObject interface for Actors you want to save instead of re-using GameplayInterface
if (Actor->IsPendingKill() || !Actor->Implements<USGameplayInterface>())
{
continue;
}
FActorSaveData ActorData;
ActorData.ActorName = Actor->GetFName();
ActorData.Transform = Actor->GetActorTransform();
// Pass the array to fill with data from Actor
FMemoryWriter MemWriter(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MemWriter, true);
// Find only variables with UPROPERTY(SaveGame)
Ar.ArIsSaveGame = true;
// Converts Actor's SaveGame UPROPERTIES into binary array
Actor->Serialize(Ar);
CurrentSaveGame->SavedActors.Add(ActorData);
}
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0);
}
```
PS.tomlooman的意思是通过判断Actor是否继承对应接口来判断这个Actor是否需要将数据进行存档。
## 宝箱Actor案例
![](https://i1.wp.com/www.tomlooman.com/wp-content/uploads/2021/06/ue_treasurechests.jpg?w=773&ssl=1)
下面是直接从项目中取出的宝箱。请注意在bLidOpened 变量上标记的ISGameplayInterface继承和“ SaveGame ”。这将是唯一保存到磁盘的变量。默认情况下,我们也存储 Actor 的 FTransform。所以我们可以在地图上推动宝箱启用模拟物理在下一次播放时位置和旋转将与盖子状态一起恢复。
```
UCLASS()
class ACTIONROGUELIKE_API ASItemChest : public AActor, public ISGameplayInterface
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
float TargetPitch;
void Interact_Implementation(APawn* InstigatorPawn);
void OnActorLoaded_Implementation();
protected:
UPROPERTY(ReplicatedUsing="OnRep_LidOpened", BlueprintReadOnly, SaveGame) // RepNotify
bool bLidOpened;
UFUNCTION()
void OnRep_LidOpened();
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* BaseMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
UStaticMeshComponent* LidMesh;
public:
// Sets default values for this actor's properties
ASItemChest();
};
```
```
void ASItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
bLidOpened = !bLidOpened;
OnRep_LidOpened();
}
void ASItemChest::OnActorLoaded_Implementation()
{
OnRep_LidOpened();
}
void ASItemChest::OnRep_LidOpened()
{
float CurrPitch = bLidOpened ? TargetPitch : 0.0f;
LidMesh->SetRelativeRotation(FRotator(CurrPitch, 0, 0));
}
```
## 玩家数据
剩下的就是迭代 PlayerState 实例并让它们也存储数据。虽然 PlayerState 派生自 Actor 并且理论上可以在所有世界 Actor 的迭代过程中保存,但单独执行它很有用,因此我们可以将它们与玩家 ID例如 Steam 用户 ID匹配而不是我们所做的不断变化的 Actor 名称不决定/控制此类运行时生成的 Actor。
### 保存数据
在我的示例中,我选择在保存游戏之前从 PlayerState 获取所有数据。我们通过调用SavePlayerState(USSaveGame* SaveObject); 这让 use 将任何与 SaveGame 对象相关的数据传入,例如 Pawn 的 PlayerId 和 Transform如果玩家当前还活着
>您**可以**选择在这里也使用 SaveGame 属性并通过将其转换为二进制数组来自动存储一些玩家数据,就像我们对 Actors 所做的一样,而不是手动将其写入 SaveGame但您仍然需要手动处理 PlayerID和典当变换。
```
void ASPlayerState::SavePlayerState_Implementation(USSaveGame* SaveObject)
{
if (SaveObject)
{
// Gather all relevant data for player
FPlayerSaveData SaveData;
SaveData.Credits = Credits;
SaveData.PersonalRecordTime = PersonalRecordTime;
// Stored as FString for simplicity (original Steam ID is uint64)
SaveData.PlayerID = GetUniqueId().ToString();
// May not be alive while we save
if (APawn* MyPawn = GetPawn())
{
SaveData.Location = MyPawn->GetActorLocation();
SaveData.Rotation = MyPawn->GetActorRotation();
SaveData.bResumeAtTransform = true;
}
SaveObject->SavedPlayers.Add(SaveData);
}
```
确保在保存到磁盘之前在所有 PlayerState 上调用这些。请务必注意GetUniqueId 仅在您加载了在线子系统(例如 Steam 或 EOS时才相关/一致。
### 加载数据
为了检索玩家数据,我们进行了相反的操作,并且必须在 pawn 生成并准备好之后手动分配玩家的变换。您可以更无缝地覆盖游戏模式中的玩家生成逻辑以使用保存的转换。例如我在HandleStartingNewPlayer期间坚持使用更简单的方法来处理这个问题。
```
void ASPlayerState::LoadPlayerState_Implementation(USSaveGame* SaveObject)
{
if (SaveObject)
{
FPlayerSaveData* FoundData = SaveObject->GetPlayerData(this);
if (FoundData)
{
//Credits = SaveObject->Credits;
// Makes sure we trigger credits changed event
AddCredits(FoundData->Credits);
PersonalRecordTime = FoundData->PersonalRecordTime;
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Could not find SaveGame data for player id '%i'."), GetPlayerId());
}
}
}
```
与在初始关卡加载时处理的加载 Actor 数据不同,对于玩家状态,我们希望在玩家加入之前可能与我们一起玩过的服务器时一一加载它们。我们可以在 GameMode 类中的 HandleStartingNewPlayer 期间这样做。
```
void ASGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
// Calling Before Super:: so we set variables before 'beginplayingstate' is called in PlayerController (which is where we instantiate UI)
ASPlayerState* PS = NewPlayer->GetPlayerState<ASPlayerState>();
if (ensure(PS))
{
PS->LoadPlayerState(CurrentSaveGame);
}
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
// Now we are ready to override spawn location
// Alternatively we could override core spawn location to use store locations immediately (skipping the whole 'find player start' logic)
if (PS)
{
PS->OverrideSpawnTransform(CurrentSaveGame);
}
}
```
正如你所看到的,它甚至被分成了两部分。主要数据会尽快加载和分配,以确保它为我们的 UI 做好准备(这是在 PlayerController 内部特定实现中的“BeginPlayingState”期间创建的并在我们处理位置/旋转之前等待 Pawn 生成.
这是您可以实现它的地方,以便在创建 Pawn 期间您使用加载的数据而不是寻找 PlayerStart就像默认的 Unreal 行为)我选择保持简单。
### 获取玩家数据
下面的函数查找 Player id 并在 PIE 中使用回退,假设我们当时没有加载在线子系统。上面的播放器状态的加载使用此函数。
```
FPlayerSaveData* USSaveGame::GetPlayerData(APlayerState* PlayerState)
{
if (PlayerState == nullptr)
{
return nullptr;
}
// Will not give unique ID while PIE so we skip that step while testing in editor.
// UObjects don't have access to UWorld, so we grab it via PlayerState instead
if (PlayerState->GetWorld()->IsPlayInEditor())
{
UE_LOG(LogTemp, Log, TEXT("During PIE we cannot use PlayerID to retrieve Saved Player data. Using first entry in array if available."));
if (SavedPlayers.IsValidIndex(0))
{
return &SavedPlayers[0];
}
// No saved player data available
return nullptr;
}
// Easiest way to deal with the different IDs is as FString (original Steam id is uint64)
// Keep in mind that GetUniqueId() returns the online id, where GetUniqueID() is a function from UObject (very confusing...)
FString PlayerID = PlayerState->GetUniqueId().ToString();
// Iterate the array and match by PlayerID (eg. unique ID provided by Steam)
return SavedPlayers.FindByPredicate([&](const FPlayerSaveData& Data) { return Data.PlayerID == PlayerID; });
}
```
## 加载世界数据
```
void ASGameModeBase::LoadSaveGame()
{
if (UGameplayStatics::DoesSaveGameExist(SlotName, 0))
{
CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
if (CurrentSaveGame == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("Failed to load SaveGame Data."));
return;
}
UE_LOG(LogTemp, Log, TEXT("Loaded SaveGame Data."));
// Iterate the entire world of actors
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
// Only interested in our 'gameplay actors'
if (!Actor->Implements<USGameplayInterface>())
{
continue;
}
for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
{
if (ActorData.ActorName == Actor->GetFName())
{
Actor->SetActorTransform(ActorData.Transform);
FMemoryReader MemReader(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
Ar.ArIsSaveGame = true;
// Convert binary array back into actor's variables
Actor->Serialize(Ar);
ISGameplayInterface::Execute_OnActorLoaded(Actor);
break;
}
}
}
OnSaveGameLoaded.Broadcast(CurrentSaveGame);
}
else
{
CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::CreateSaveGameObject(USSaveGame::StaticClass()));
UE_LOG(LogTemp, Log, TEXT("Created New SaveGame Data."));
}
}
```
## 从磁盘选择特定的存档
```
void ASGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
FString SelectedSaveSlot = UGameplayStatics::ParseOption(Options, "SaveGame");
if (SelectedSaveSlot.Len() > 0)
{
SlotName = SelectedSaveSlot;
}
LoadSaveGame();
}
```

View File

@@ -0,0 +1,65 @@
---
title: UE5 C++技巧
date: 2022-12-09 17:02:14
excerpt:
tags: c++
rating: ⭐
---
## 取得默认值
```c++
GetDefault<ULyraDeveloperSettings>()->OnPlayInEditorStarted();
GetDefault<ULyraPlatformEmulationSettings>()->OnPlayInEditorStarted();
GetMutableDefault<UContentBrowserSettings>()->SetDisplayPluginFolders(true);
```
## 模块操作
```c++
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
if (AssetRegistryModule.Get().IsLoadingAssets())
{
if (bInteractive)
{
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("DiscoveringAssets", "Still discovering assets. Try again once it is complete."));
}
else
{
UE_LOG(LogLyraEditor, Display, TEXT("Could not run ValidateCheckedOutContent because asset discovery was still being done."));
}
return;
}
```
### 加载动态链接库
```
{
auto dllName = TEXT("assimp-vc141-mt.dll");
#if WITH_VRM4U_ASSIMP_DEBUG
dllName = TEXT("assimp-vc141-mtd.dll");
#endif
{
FString AbsPath = IPluginManager::Get().FindPlugin("VRM4U")->GetBaseDir() / TEXT("ThirdParty/assimp/bin/x64");
//FPlatformProcess::AddDllDirectory(*AbsPath);
assimpDllHandle = FPlatformProcess::GetDllHandle(*(AbsPath / dllName));
}
}
```
## UE4显示MessageBox
struct CORE_API FMessageDialog
```
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("DiscoveringAssets", "Still discovering assets. Try again once it is complete."));
```
## ref
c++ 中ref关键字的应用
ref()方法的返回值是reference_wrapper类型,这个类的源码大概的意思就是维持一个指针,并且重载操作符。
## 版本兼容
```
#if UE_VERSION_OLDER_THAN(4,24,0)
#endif
#if UE_VERSION_OLDER_THAN(5,0,0)
#endif
```

View File

@@ -0,0 +1,225 @@
# 前言
最近在尝试对GameplayAbility进行插件化处理。这么做好处在于这样可以方便后续项目的迭代。UE4官方频道有一个“盗贼之海”团队的经验分享视频他们说制作的GAS Asset复用率能都达到90%这可是相当惊人的程度。而且在不断地项目迭代过程中使用GAS制作的技能、Buff/Debuff会越来越充分。从而加快后续项目的开发速度。所以将GAS插件化是很有必要的。
但使用GAS需要给项目添加一些项目设置比如AssetManager中的DataAsset、指定AssetManager类、添加GameplayTag。在发现网上的添加配置方法无效后我又花了一些时间研究终于找到方法隧有了此文。
# Ue4配置设置方法
Ue4的配置设置方法大致为二通过GConfig设置与直接设置对应UObject变量。
具体的可以参考Ue4的Wiki与一篇Csdn的文章:
>https://www.ue4community.wiki/legacy/config-files-read-and-write-to-config-files-zuoaht01
https://blog.csdn.net/u012999985/article/details/52801264
## 通过GConfig进行修改
通过全局对象GConfig调用GetInt/Float/Rotator/String/Array等函数获取对应的数值调用SetInt/Float/Rotator/String/Array等函数设置数值。**最终需要调用Flush函数将结果写入对应的ini文件中**,不然重启后数据会丢失。
GConfig通过一个Map来存储配置这个你可以通过断点来查看内部数据。
这里我简单补充一些:
- Section属性所处于的区块。
- Key属性的变量名。
- Value对应类型的属性值。
- Filename存储ini文件的位置。Ue4提供了默认位置的字符串变量具体的请下文。
据我所知Section可以通过将对应属性通过编辑器修改找到对应的ini文件即可从所在段落的第一行找到所在的Section。如果是自己写的类Section即为类的资源路径。可通过在ContentBrowser中对指定类执行复制命令之后在外部文本编辑器中粘贴即可得到。Key值就需要用VS或者VSCode之类的编辑器查询了。
>请注意使用以下代码相关的配置的是保存在Saved——对应平台文件夹中的。
下面是Wiki上Rama大神的代码
```
//in your player controller class
void AVictoryController::VictoryConfigGetTests()
{
//Basic Syntax
/*
bool GetString(
const TCHAR* Section,
const TCHAR* Key,
FString&amp; Value,
const FString&amp; Filename
);
*/
if(!GConfig) return;
//~~
//Retrieve Default Game Type
FString ValueReceived;
GConfig->GetString(
TEXT("/Script/Engine.WorldInfo"),
TEXT("GlobalDefaultGameType"),
ValueReceived,
GGameIni
);
ClientMessage("GlobalDefaultGameType");
ClientMessage(ValueReceived);
//Retrieve Max Objects not considered by GC
int32 IntValueReceived = 0;
GConfig->GetInt(
TEXT("Core.System"),
TEXT("MaxObjectsNotConsideredByGC"),
IntValueReceived,
GEngineIni
);
ClientMessage("MaxObjectsNotConsideredByGC");
ClientMessage(FString::FromInt(IntValueReceived));
//Retrieve Near Clip Plane (how close things can get to camera)
float floatValueReceived = 0;
GConfig->GetFloat(
TEXT("/Script/Engine.Engine"),
TEXT("NearClipPlane"),
floatValueReceived,
GEngineIni
);
ClientMessage("NearClipPlane");
ClientMessage(FString::SanitizeFloat(floatValueReceived));
}
```
```
//write to existing Game.ini
//the results get stored in YourGameDir\Saved\Config\Windows
void AVictoryController::VictoryConfigSetTests()
{
if(!GConfig) return;
//~~
//New Section to Add
FString VictorySection = "Victory.Core";
//String
GConfig->SetString (
*VictorySection,
TEXT("RootDir"),
TEXT("E:\UE4\IsAwesome"),
GGameIni
);
//FColor
GConfig->SetColor (
*VictorySection,
TEXT("Red"),
FColor(255,0,0,255),
GGameIni
);
//FVector
GConfig->SetVector (
*VictorySection,
TEXT("PlayerStartLocation"),
FVector(0,0,512),
GGameIni
);
//FRotator
GConfig->SetRotator (
*VictorySection,
TEXT("SunRotation"),
FRotator(-90,0,0),
GGameIni
);
//ConfigCacheIni.h
//void Flush( bool Read, const FString&amp; Filename=TEXT("") );
GConfig->Flush(false,GGameIni);
}
```
ini配置文件的位置字符串变量代码位于CoreGlobals.cpp中。
```
FString GEngineIni; /* Engine ini filename */
/** Editor ini file locations - stored per engine version (shared across all projects). Migrated between versions on first run. */
FString GEditorIni; /* Editor ini filename */
FString GEditorKeyBindingsIni; /* Editor Key Bindings ini file */
FString GEditorLayoutIni; /* Editor UI Layout ini filename */
FString GEditorSettingsIni; /* Editor Settings ini filename */
/** Editor per-project ini files - stored per project. */
FString GEditorPerProjectIni; /* Editor User Settings ini filename */
FString GCompatIni;
FString GLightmassIni; /* Lightmass settings ini filename */
FString GScalabilityIni; /* Scalability settings ini filename */
FString GHardwareIni; /* Hardware ini filename */
FString GInputIni; /* Input ini filename */
FString GGameIni; /* Game ini filename */
FString GGameUserSettingsIni; /* User Game Settings ini filename */
FString GRuntimeOptionsIni; /* Runtime Options ini filename */
FString GInstallBundleIni; /* Install Bundle ini filename*/
FString GDeviceProfilesIni;
```
## 通过UObject进行修改
很遗憾本人没有从GConfig找到对应的设置变量所以添加配置的操作都是通过UObject进行的。主要是通过GEngine这个全局对象以及GetMutableDefault<UObject>()获取对应的UObject这两种方式进行操作。
操作的方式也有两种:
1. 直接调用LoadConfig再入预先设置的ini文件之后调用SaveConfig保存设置。
2. 直接修改UObject的属性变量值之后调用SaveConfig保存设置。
另外需要注意两点:
1. Config的类型分为Config与GlobalConfig调用函数时需要使用对应的类型枚举CPF_Config与CPF_GlobalConfig。
2. 是否在UCLASS(Config=XXX)有设置Config保存位置。
具体操作请见下文代码。
# 使用DataTable来管理GameplayTag
本人在尝试通过修改配置来添加GameplayTag时发现这个方法是无效的。可能是个bug在404的过程中我发现可以通过DataTable来管理Tag。大致的方式是
1. 构建CSV或者JSON文件
2. 导入UE4中并将DataTable类型选择为GameplayTagTableRow。
3. 在ProjectSettings-GameplayTagTableList中添加DataTable即可。
这样做的好处有:
1. GameplayTags的重复利用避免手动输入出错方便下个项目移植。
2. 可以对不同类型Tag进行分表处理在做项目时只需要加载需要的DataTable即可。
DataTable大致格式如下
PS.有关GameplayTag的操作可以查看UGameplayTagsSettings、UGameplayTagsManager与GameplayTagEditorModule中的代码。
# 创建Console命令
将这些代码放在GameMode、GameInstance里会降低性能纯属强迫症放在模块cpp文件中又会报错估计需要将插件设置成后启动模式才行。为了方便使用我创建了一个Console命令并将其放在BlueprintFunctionLibrary中。在编辑器中下入~输入AddGASSettings并按下回车即可添加所需设置。
# 具体代码实现
```
static void AddRPGGameplayAbilityProjectSettings(UWorld *InWorld)
{
UE_LOG(LogRPGGameplayAbility, Warning, TEXT("Add RPGGameplayAbility Project Settings!"));
//Special AssetManager
FString ConfigPath = FPaths::ProjectConfigDir();
GEngine->AssetManagerClassName = FString("/Script/RPGGameplayAbility.RPGAssetManager");
GEngine->SaveConfig(CPF_GlobalConfig, *(ConfigPath + FString("DefaultEngine.ini")));
//Add DataAsset Config
UAssetManagerSettings *AssetmanagerSettings = GetMutableDefault<UAssetManagerSettings>();
FString PluginConfigPath = FPaths::ProjectPluginsDir() + FString("RPGGameplayAbility/Config/");
AssetmanagerSettings->LoadConfig(nullptr, *FString(PluginConfigPath + FString("DefaultGame.ini")));
AssetmanagerSettings->SaveConfig(CPF_Config, *(ConfigPath + FString("DefaultGame.ini")));
//Add GameplayTag
UGameplayTagsSettings *GameplayTagsSettings = GetMutableDefault<UGameplayTagsSettings>();
//GameplayTagsSettings->LoadConfig(nullptr, *FString(PluginConfigPath + FString("DefaultGameplayTags.ini")));
FString TagDataTable{FString("/RPGGameplayAbility/Abilities/DataTables/GameplayTags.GameplayTags")};
if (GameplayTagsSettings->GameplayTagTableList.Find(TagDataTable) == -1)
{
GameplayTagsSettings->GameplayTagTableList.Add(TagDataTable);
UGameplayTagsManager &tagManager = UGameplayTagsManager::Get();
tagManager.DestroyGameplayTagTree();
tagManager.LoadGameplayTagTables(false);
tagManager.ConstructGameplayTagTree();
tagManager.OnEditorRefreshGameplayTagTree.Broadcast();
GameplayTagsSettings->SaveConfig(CPF_Config, *(ConfigPath + FString("DefaultGameplayTags.ini")));
}
}
FAutoConsoleCommandWithWorld AbilitySystemDebugNextCategoryCmd(
TEXT("AddGASSettings"),
TEXT("增加RPGGameplayAbility所需要的项目设置"),
FConsoleCommandWithWorldDelegate::CreateStatic(AddRPGGameplayAbilityProjectSettings));
```

View File

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

Binary file not shown.