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

|
||||
|
||||
每个选项的具体功能请参考文档。
|
||||
2.编辑DefaultGame.ini文件:找到(或创建)一个名为 /Script/Engine.AssetManagerSettings的代码段,添加:
|
||||
```
|
||||
[/Script/Engine.AssetManagerSettings]
|
||||
!PrimaryAssetTypesToScan=ClearArray
|
||||
+PrimaryAssetTypesToScan=(PrimaryAssetType="Map",AssetBaseClass=/Script/Engine.World,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game/Maps")),SpecificAssets=,Rules=(Priority=-1,bApplyRecursively=True,ChunkId=-1,CookRule=Unknown))
|
||||
+PrimaryAssetTypesToScan=(PrimaryAssetType="PrimaryAssetLabel",AssetBaseClass=/Script/Engine.PrimaryAssetLabel,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game")),SpecificAssets=,Rules=(Priority=-1,bApplyRecursively=True,ChunkId=-1,CookRule=Unknown))
|
||||
```
|
||||
3.在代码中操作:重写UAssetManager类中的StartInitialLoading函数并从该处调用ScanPathsForPrimaryAssets。因此,推荐您将所有同类型的主资源放入相同的子文件夹中。这将使资源查找和注册更为迅速。
|
||||
###### 加载资源
|
||||
LoadPrimaryAssets、LoadPrimaryAsset和LoadPrimaryAssetsWithType适用于游戏启动前。
|
||||
之后通过UnloadPrimaryAssets、UnloadPrimaryAsset 和 UnloadPrimaryAssetsWithType卸载。
|
||||
#### 动态注册与加载PrimaryAsset
|
||||
```
|
||||
//从AssetId构建Asset字符串表,并且构建AssetBundle数据
|
||||
UFortAssetManager& AssetManager = UFortAssetManager::Get();
|
||||
FPrimaryAssetId TheaterAssetId = FPrimaryAssetId(UFortAssetManager::FortTheaterInfoType, FName(*TheaterData.UniqueId));
|
||||
|
||||
TArray<FStringAssetReference> AssetReferences;
|
||||
AssetManager.ExtractStringAssetReferences(FFortTheaterMapData::StaticStruct(), &TheaterData, AssetReferences);
|
||||
|
||||
FAssetBundleData GameDataBundles;
|
||||
GameDataBundles.AddBundleAssets(UFortAssetManager::LoadStateMenu, AssetReferences);
|
||||
|
||||
//通过递归的方式,展开AssetBundle数据(获取SecondaryAssets数据)
|
||||
AssetManager.RecursivelyExpandBundleData(GameDataBundles);
|
||||
|
||||
// 注册动态资源
|
||||
AssetManager.AddDynamicAsset(TheaterAssetId, FStringAssetReference(), GameDataBundles);
|
||||
|
||||
// 开始预加载
|
||||
AssetManager.LoadPrimaryAsset(TheaterAssetId, AssetManager.GetDefaultBundleState());
|
||||
```
|
||||
BIN
03-UnrealEngine/Gameplay/Gameplay/AssetManager/Ue4资源加载方式.png
LFS
Normal file
BIN
03-UnrealEngine/Gameplay/Gameplay/AssetManager/Ue4资源加载方式.png
LFS
Normal file
Binary file not shown.
@@ -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构建PMC,PMC的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
|
||||
@@ -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
|
||||
186
03-UnrealEngine/Gameplay/Gameplay/Git & LFS 笔记.md
Normal file
186
03-UnrealEngine/Gameplay/Gameplay/Git & LFS 笔记.md
Normal file
@@ -0,0 +1,186 @@
|
||||
#### 前言
|
||||
因为GameplayAbility属于蓝图c++混合编程框架,所以对蓝图类与地图进行版本管理是十分重要的事情。所以本文将在这里介绍git的二进制文件版本管理方案。
|
||||
|
||||
#### 使用过程
|
||||
1. 下载Gitlfs:https://git-lfs.github.com/~~(现在的git都自带lfs,就算没有下个SourceTree也会自带lfs)
|
||||
2. 使用cmd,cd到git仓库所在目录,执行git lfs install。(一般人都在这一步做错,如果做错会存在100mb的文件大小限制)
|
||||
3. 此时目录下会出现.gitattributes文件,它用于设置监视的扩展名,你可以通过输入```git lfs track "*.扩展名"```的方式来添加扩展名。例如想要监视uasset,就输入
|
||||
```git lfs track "*.uasset"```。最后将.gitattributes加入进版本管理:```git add .gitattributes```。
|
||||
4. 现在你就可以用与管理代码文件相同的方式,管理二进制文件了。
|
||||
```
|
||||
git add file.uasset
|
||||
git commit -m "Add design file"
|
||||
git push origin master
|
||||
```
|
||||
推荐使用SourceTree,因为如果你第二步操作有误或是第三步没有添加扩展名,它会提醒你的。
|
||||
|
||||
#### 蓝图合并与Diff工具
|
||||
Merge:https://github.com/KennethBuijssen/MergeAssist
|
||||
Diff:https://github.com/SRombauts/UE4GitPlugin
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
45
03-UnrealEngine/Gameplay/Gameplay/RuntimeLoadStaticMesh.md
Normal file
45
03-UnrealEngine/Gameplay/Gameplay/RuntimeLoadStaticMesh.md
Normal 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;
|
||||
```
|
||||
@@ -0,0 +1,312 @@
|
||||
## tomlooman写的教程
|
||||
https://www.tomlooman.com/save-system-unreal-engine-tutorial/
|
||||
|
||||
## 保存关卡世界数据与状态
|
||||
要保存世界状态,我们必须决定为每个演员存储哪些变量,以及我们需要保存在磁盘上的哪些杂项信息。例如每个玩家获得的金钱数。金钱数并不是世界状态的真正组成部分,而是属于PlayerState。尽管PlayerState存在于世界中,并且事实上是一个角色,但我们还是将它们分开处理,这样我们就可以根据它以前属于哪个玩家来正确地恢复它。
|
||||
|
||||
## Actor数据
|
||||
对于 Actor 变量,我们存储其名称、变换(位置、旋转、缩放)和一个字节数据数组,其中包含在其 UPROPERTY 中标有“SaveGame”的所有变量。
|
||||
```
|
||||
USTRUCT()
|
||||
struct FActorSaveData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/* Identifier for which Actor this belongs to */
|
||||
UPROPERTY()
|
||||
FName ActorName;
|
||||
|
||||
/* For movable Actors, keep location,rotation,scale. */
|
||||
UPROPERTY()
|
||||
FTransform Transform;
|
||||
|
||||
/* Contains all 'SaveGame' marked variables of the Actor */
|
||||
UPROPERTY()
|
||||
TArray<uint8> ByteData;
|
||||
};
|
||||
```
|
||||
|
||||
## 将变量转换为二进制
|
||||
要将变量转换为二进制数组,我们需要一个FMemoryWriter和FObjectAndNameAsStringProxyArchive,它们派生自 FArchive(虚幻的数据容器,用于各种序列化数据,包括您的游戏内容)。
|
||||
|
||||
我们按接口过滤,以避免在我们不想保存的世界中潜在的数千个静态 Actor 上调用 Serialize。存储 Actor 的名称将在稍后用于识别要反序列化(加载)数据的 Actor。您可以想出自己的解决方案,例如FGuid(主要用于可能没有一致名称的运行时生成的 Actor)由于内置系统,其余的代码非常简单(并在注释中进行了解释)。
|
||||
```
|
||||
void ASGameModeBase::WriteSaveGame()
|
||||
{
|
||||
// ... < playerstate saving code ommitted >
|
||||
|
||||
// Clear all actors from any previously loaded save to avoid duplicates
|
||||
CurrentSaveGame->SavedActors.Empty();
|
||||
|
||||
// Iterate the entire world of actors
|
||||
for (FActorIterator It(GetWorld()); It; ++It)
|
||||
{
|
||||
AActor* Actor = *It;
|
||||
// Only interested in our 'gameplay actors', skip actors that are being destroyed
|
||||
// Note: You might instead use a dedicated SavableObject interface for Actors you want to save instead of re-using GameplayInterface
|
||||
if (Actor->IsPendingKill() || !Actor->Implements<USGameplayInterface>())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FActorSaveData ActorData;
|
||||
ActorData.ActorName = Actor->GetFName();
|
||||
ActorData.Transform = Actor->GetActorTransform();
|
||||
|
||||
// Pass the array to fill with data from Actor
|
||||
FMemoryWriter MemWriter(ActorData.ByteData);
|
||||
|
||||
FObjectAndNameAsStringProxyArchive Ar(MemWriter, true);
|
||||
// Find only variables with UPROPERTY(SaveGame)
|
||||
Ar.ArIsSaveGame = true;
|
||||
// Converts Actor's SaveGame UPROPERTIES into binary array
|
||||
Actor->Serialize(Ar);
|
||||
|
||||
CurrentSaveGame->SavedActors.Add(ActorData);
|
||||
}
|
||||
|
||||
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0);
|
||||
}
|
||||
```
|
||||
|
||||
PS.tomlooman的意思是通过判断Actor是否继承对应接口来判断这个Actor是否需要将数据进行存档。
|
||||
|
||||
## 宝箱Actor案例
|
||||

|
||||
下面是直接从项目中取出的宝箱。请注意在bLidOpened 变量上标记的ISGameplayInterface继承和“ SaveGame ”。这将是唯一保存到磁盘的变量。默认情况下,我们也存储 Actor 的 FTransform。所以我们可以在地图上推动宝箱(启用模拟物理),在下一次播放时,位置和旋转将与盖子状态一起恢复。
|
||||
```
|
||||
UCLASS()
|
||||
class ACTIONROGUELIKE_API ASItemChest : public AActor, public ISGameplayInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(EditAnywhere)
|
||||
float TargetPitch;
|
||||
|
||||
void Interact_Implementation(APawn* InstigatorPawn);
|
||||
|
||||
void OnActorLoaded_Implementation();
|
||||
|
||||
protected:
|
||||
UPROPERTY(ReplicatedUsing="OnRep_LidOpened", BlueprintReadOnly, SaveGame) // RepNotify
|
||||
bool bLidOpened;
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_LidOpened();
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
UStaticMeshComponent* BaseMesh;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
|
||||
UStaticMeshComponent* LidMesh;
|
||||
|
||||
public:
|
||||
// Sets default values for this actor's properties
|
||||
ASItemChest();
|
||||
};
|
||||
```
|
||||
```
|
||||
void ASItemChest::Interact_Implementation(APawn* InstigatorPawn)
|
||||
{
|
||||
bLidOpened = !bLidOpened;
|
||||
OnRep_LidOpened();
|
||||
}
|
||||
|
||||
void ASItemChest::OnActorLoaded_Implementation()
|
||||
{
|
||||
OnRep_LidOpened();
|
||||
}
|
||||
|
||||
void ASItemChest::OnRep_LidOpened()
|
||||
{
|
||||
float CurrPitch = bLidOpened ? TargetPitch : 0.0f;
|
||||
LidMesh->SetRelativeRotation(FRotator(CurrPitch, 0, 0));
|
||||
}
|
||||
```
|
||||
## 玩家数据
|
||||
剩下的就是迭代 PlayerState 实例并让它们也存储数据。虽然 PlayerState 派生自 Actor 并且理论上可以在所有世界 Actor 的迭代过程中保存,但单独执行它很有用,因此我们可以将它们与玩家 ID(例如 Steam 用户 ID)匹配,而不是我们所做的不断变化的 Actor 名称不决定/控制此类运行时生成的 Actor。
|
||||
|
||||
### 保存数据
|
||||
在我的示例中,我选择在保存游戏之前从 PlayerState 获取所有数据。我们通过调用SavePlayerState(USSaveGame* SaveObject); 这让 use 将任何与 SaveGame 对象相关的数据传入,例如 Pawn 的 PlayerId 和 Transform(如果玩家当前还活着)
|
||||
|
||||
>您**可以**选择在这里也使用 SaveGame 属性并通过将其转换为二进制数组来自动存储一些玩家数据,就像我们对 Actors 所做的一样,而不是手动将其写入 SaveGame,但您仍然需要手动处理 PlayerID和典当变换。
|
||||
```
|
||||
void ASPlayerState::SavePlayerState_Implementation(USSaveGame* SaveObject)
|
||||
{
|
||||
if (SaveObject)
|
||||
{
|
||||
// Gather all relevant data for player
|
||||
FPlayerSaveData SaveData;
|
||||
SaveData.Credits = Credits;
|
||||
SaveData.PersonalRecordTime = PersonalRecordTime;
|
||||
// Stored as FString for simplicity (original Steam ID is uint64)
|
||||
SaveData.PlayerID = GetUniqueId().ToString();
|
||||
|
||||
// May not be alive while we save
|
||||
if (APawn* MyPawn = GetPawn())
|
||||
{
|
||||
SaveData.Location = MyPawn->GetActorLocation();
|
||||
SaveData.Rotation = MyPawn->GetActorRotation();
|
||||
SaveData.bResumeAtTransform = true;
|
||||
}
|
||||
|
||||
SaveObject->SavedPlayers.Add(SaveData);
|
||||
}
|
||||
```
|
||||
确保在保存到磁盘之前在所有 PlayerState 上调用这些。请务必注意,GetUniqueId 仅在您加载了在线子系统(例如 Steam 或 EOS)时才相关/一致。
|
||||
|
||||
### 加载数据
|
||||
为了检索玩家数据,我们进行了相反的操作,并且必须在 pawn 生成并准备好之后手动分配玩家的变换。您可以更无缝地覆盖游戏模式中的玩家生成逻辑以使用保存的转换。例如,我在HandleStartingNewPlayer期间坚持使用更简单的方法来处理这个问题。
|
||||
```
|
||||
void ASPlayerState::LoadPlayerState_Implementation(USSaveGame* SaveObject)
|
||||
{
|
||||
if (SaveObject)
|
||||
{
|
||||
FPlayerSaveData* FoundData = SaveObject->GetPlayerData(this);
|
||||
if (FoundData)
|
||||
{
|
||||
//Credits = SaveObject->Credits;
|
||||
// Makes sure we trigger credits changed event
|
||||
AddCredits(FoundData->Credits);
|
||||
|
||||
PersonalRecordTime = FoundData->PersonalRecordTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Could not find SaveGame data for player id '%i'."), GetPlayerId());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
与在初始关卡加载时处理的加载 Actor 数据不同,对于玩家状态,我们希望在玩家加入之前可能与我们一起玩过的服务器时一一加载它们。我们可以在 GameMode 类中的 HandleStartingNewPlayer 期间这样做。
|
||||
```
|
||||
void ASGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
|
||||
{
|
||||
// Calling Before Super:: so we set variables before 'beginplayingstate' is called in PlayerController (which is where we instantiate UI)
|
||||
ASPlayerState* PS = NewPlayer->GetPlayerState<ASPlayerState>();
|
||||
if (ensure(PS))
|
||||
{
|
||||
PS->LoadPlayerState(CurrentSaveGame);
|
||||
}
|
||||
|
||||
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
|
||||
|
||||
// Now we are ready to override spawn location
|
||||
// Alternatively we could override core spawn location to use store locations immediately (skipping the whole 'find player start' logic)
|
||||
if (PS)
|
||||
{
|
||||
PS->OverrideSpawnTransform(CurrentSaveGame);
|
||||
}
|
||||
}
|
||||
```
|
||||
正如你所看到的,它甚至被分成了两部分。主要数据会尽快加载和分配,以确保它为我们的 UI 做好准备(这是在 PlayerController 内部特定实现中的“BeginPlayingState”期间创建的),并在我们处理位置/旋转之前等待 Pawn 生成.
|
||||
|
||||
这是您可以实现它的地方,以便在创建 Pawn 期间您使用加载的数据而不是寻找 PlayerStart(就像默认的 Unreal 行为)我选择保持简单。
|
||||
|
||||
### 获取玩家数据
|
||||
下面的函数查找 Player id 并在 PIE 中使用回退,假设我们当时没有加载在线子系统。上面的播放器状态的加载使用此函数。
|
||||
|
||||
```
|
||||
FPlayerSaveData* USSaveGame::GetPlayerData(APlayerState* PlayerState)
|
||||
{
|
||||
if (PlayerState == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Will not give unique ID while PIE so we skip that step while testing in editor.
|
||||
// UObjects don't have access to UWorld, so we grab it via PlayerState instead
|
||||
if (PlayerState->GetWorld()->IsPlayInEditor())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("During PIE we cannot use PlayerID to retrieve Saved Player data. Using first entry in array if available."));
|
||||
|
||||
if (SavedPlayers.IsValidIndex(0))
|
||||
{
|
||||
return &SavedPlayers[0];
|
||||
}
|
||||
|
||||
// No saved player data available
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Easiest way to deal with the different IDs is as FString (original Steam id is uint64)
|
||||
// Keep in mind that GetUniqueId() returns the online id, where GetUniqueID() is a function from UObject (very confusing...)
|
||||
FString PlayerID = PlayerState->GetUniqueId().ToString();
|
||||
// Iterate the array and match by PlayerID (eg. unique ID provided by Steam)
|
||||
return SavedPlayers.FindByPredicate([&](const FPlayerSaveData& Data) { return Data.PlayerID == PlayerID; });
|
||||
}
|
||||
```
|
||||
|
||||
## 加载世界数据
|
||||
```
|
||||
void ASGameModeBase::LoadSaveGame()
|
||||
{
|
||||
|
||||
if (UGameplayStatics::DoesSaveGameExist(SlotName, 0))
|
||||
{
|
||||
CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
|
||||
if (CurrentSaveGame == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Failed to load SaveGame Data."));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("Loaded SaveGame Data."));
|
||||
|
||||
// Iterate the entire world of actors
|
||||
for (FActorIterator It(GetWorld()); It; ++It)
|
||||
{
|
||||
AActor* Actor = *It;
|
||||
// Only interested in our 'gameplay actors'
|
||||
if (!Actor->Implements<USGameplayInterface>())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
|
||||
{
|
||||
if (ActorData.ActorName == Actor->GetFName())
|
||||
{
|
||||
Actor->SetActorTransform(ActorData.Transform);
|
||||
|
||||
FMemoryReader MemReader(ActorData.ByteData);
|
||||
|
||||
FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
|
||||
Ar.ArIsSaveGame = true;
|
||||
// Convert binary array back into actor's variables
|
||||
Actor->Serialize(Ar);
|
||||
|
||||
ISGameplayInterface::Execute_OnActorLoaded(Actor);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnSaveGameLoaded.Broadcast(CurrentSaveGame);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::CreateSaveGameObject(USSaveGame::StaticClass()));
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("Created New SaveGame Data."));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## 从磁盘选择特定的存档
|
||||
```
|
||||
void ASGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
|
||||
{
|
||||
Super::InitGame(MapName, Options, ErrorMessage);
|
||||
|
||||
FString SelectedSaveSlot = UGameplayStatics::ParseOption(Options, "SaveGame");
|
||||
if (SelectedSaveSlot.Len() > 0)
|
||||
{
|
||||
SlotName = SelectedSaveSlot;
|
||||
}
|
||||
LoadSaveGame();
|
||||
}
|
||||
```
|
||||
65
03-UnrealEngine/Gameplay/Gameplay/UE5 C++技巧.md
Normal file
65
03-UnrealEngine/Gameplay/Gameplay/UE5 C++技巧.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: UE5 C++技巧
|
||||
date: 2022-12-09 17:02:14
|
||||
excerpt:
|
||||
tags: c++
|
||||
rating: ⭐
|
||||
---
|
||||
## 取得默认值
|
||||
```c++
|
||||
GetDefault<ULyraDeveloperSettings>()->OnPlayInEditorStarted();
|
||||
GetDefault<ULyraPlatformEmulationSettings>()->OnPlayInEditorStarted();
|
||||
GetMutableDefault<UContentBrowserSettings>()->SetDisplayPluginFolders(true);
|
||||
```
|
||||
## 模块操作
|
||||
```c++
|
||||
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
||||
|
||||
if (AssetRegistryModule.Get().IsLoadingAssets())
|
||||
{
|
||||
if (bInteractive)
|
||||
{
|
||||
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("DiscoveringAssets", "Still discovering assets. Try again once it is complete."));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogLyraEditor, Display, TEXT("Could not run ValidateCheckedOutContent because asset discovery was still being done."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
### 加载动态链接库
|
||||
```
|
||||
{
|
||||
auto dllName = TEXT("assimp-vc141-mt.dll");
|
||||
#if WITH_VRM4U_ASSIMP_DEBUG
|
||||
dllName = TEXT("assimp-vc141-mtd.dll");
|
||||
#endif
|
||||
{
|
||||
FString AbsPath = IPluginManager::Get().FindPlugin("VRM4U")->GetBaseDir() / TEXT("ThirdParty/assimp/bin/x64");
|
||||
//FPlatformProcess::AddDllDirectory(*AbsPath);
|
||||
assimpDllHandle = FPlatformProcess::GetDllHandle(*(AbsPath / dllName));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## UE4显示MessageBox
|
||||
struct CORE_API FMessageDialog
|
||||
```
|
||||
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("DiscoveringAssets", "Still discovering assets. Try again once it is complete."));
|
||||
```
|
||||
|
||||
## ref
|
||||
c++ 中ref关键字的应用
|
||||
|
||||
ref()方法的返回值是reference_wrapper类型,这个类的源码大概的意思就是维持一个指针,并且重载操作符。
|
||||
|
||||
## 版本兼容
|
||||
```
|
||||
#if UE_VERSION_OLDER_THAN(4,24,0)
|
||||
#endif
|
||||
|
||||
#if UE_VERSION_OLDER_THAN(5,0,0)
|
||||
#endif
|
||||
```
|
||||
@@ -0,0 +1,225 @@
|
||||
# 前言
|
||||
最近在尝试对GameplayAbility进行插件化处理。这么做好处在于这样可以方便后续项目的迭代。UE4官方频道有一个“盗贼之海”团队的经验分享视频,他们说制作的GAS Asset复用率能都达到90%,这可是相当惊人的程度。而且在不断地项目迭代过程中,使用GAS制作的技能、Buff/Debuff会越来越充分。从而加快后续项目的开发速度。所以将GAS插件化是很有必要的。
|
||||
|
||||
但使用GAS需要给项目添加一些项目设置比如:AssetManager中的DataAsset、指定AssetManager类、添加GameplayTag。在发现网上的添加配置方法无效后,我又花了一些时间研究,终于找到方法,隧有了此文。
|
||||
|
||||
# Ue4配置设置方法
|
||||
Ue4的配置设置方法大致为二:通过GConfig设置与直接设置对应UObject变量。
|
||||
具体的可以参考Ue4的Wiki与一篇Csdn的文章:
|
||||
>https://www.ue4community.wiki/legacy/config-files-read-and-write-to-config-files-zuoaht01
|
||||
https://blog.csdn.net/u012999985/article/details/52801264
|
||||
|
||||
## 通过GConfig进行修改
|
||||
通过全局对象GConfig,调用GetInt/Float/Rotator/String/Array等函数获取对应的数值;调用SetInt/Float/Rotator/String/Array等函数设置数值。**最终需要调用Flush函数将结果写入对应的ini文件中**,不然重启后数据会丢失。
|
||||
|
||||
GConfig通过一个Map来存储配置,这个你可以通过断点来查看内部数据。
|
||||
|
||||
这里我简单补充一些:
|
||||
|
||||
- Section:属性所处于的区块。
|
||||
- Key:属性的变量名。
|
||||
- Value:对应类型的属性值。
|
||||
- Filename:存储ini文件的位置。Ue4提供了默认位置的字符串变量,具体的请下文。
|
||||
|
||||
据我所知Section可以通过将对应属性通过编辑器修改,找到对应的ini文件,即可从所在段落的第一行找到所在的Section。如果是自己写的类,Section即为类的资源路径。可通过在ContentBrowser中对指定类执行复制命令,之后在外部文本编辑器中粘贴即可得到。Key值就需要用VS或者VSCode之类的编辑器查询了。
|
||||
|
||||
>请注意,使用以下代码,相关的配置的是保存在Saved——对应平台文件夹中的。
|
||||
|
||||
下面是Wiki上Rama大神的代码:
|
||||
```
|
||||
//in your player controller class
|
||||
void AVictoryController::VictoryConfigGetTests()
|
||||
{
|
||||
//Basic Syntax
|
||||
/*
|
||||
bool GetString(
|
||||
const TCHAR* Section,
|
||||
const TCHAR* Key,
|
||||
FString& Value,
|
||||
const FString& Filename
|
||||
);
|
||||
*/
|
||||
|
||||
if(!GConfig) return;
|
||||
//~~
|
||||
|
||||
//Retrieve Default Game Type
|
||||
FString ValueReceived;
|
||||
GConfig->GetString(
|
||||
TEXT("/Script/Engine.WorldInfo"),
|
||||
TEXT("GlobalDefaultGameType"),
|
||||
ValueReceived,
|
||||
GGameIni
|
||||
);
|
||||
|
||||
ClientMessage("GlobalDefaultGameType");
|
||||
ClientMessage(ValueReceived);
|
||||
|
||||
//Retrieve Max Objects not considered by GC
|
||||
int32 IntValueReceived = 0;
|
||||
GConfig->GetInt(
|
||||
TEXT("Core.System"),
|
||||
TEXT("MaxObjectsNotConsideredByGC"),
|
||||
IntValueReceived,
|
||||
GEngineIni
|
||||
);
|
||||
|
||||
ClientMessage("MaxObjectsNotConsideredByGC");
|
||||
ClientMessage(FString::FromInt(IntValueReceived));
|
||||
|
||||
//Retrieve Near Clip Plane (how close things can get to camera)
|
||||
float floatValueReceived = 0;
|
||||
GConfig->GetFloat(
|
||||
TEXT("/Script/Engine.Engine"),
|
||||
TEXT("NearClipPlane"),
|
||||
floatValueReceived,
|
||||
GEngineIni
|
||||
);
|
||||
|
||||
ClientMessage("NearClipPlane");
|
||||
ClientMessage(FString::SanitizeFloat(floatValueReceived));
|
||||
}
|
||||
```
|
||||
```
|
||||
//write to existing Game.ini
|
||||
//the results get stored in YourGameDir\Saved\Config\Windows
|
||||
void AVictoryController::VictoryConfigSetTests()
|
||||
{
|
||||
if(!GConfig) return;
|
||||
//~~
|
||||
|
||||
//New Section to Add
|
||||
FString VictorySection = "Victory.Core";
|
||||
|
||||
//String
|
||||
GConfig->SetString (
|
||||
*VictorySection,
|
||||
TEXT("RootDir"),
|
||||
TEXT("E:\UE4\IsAwesome"),
|
||||
GGameIni
|
||||
);
|
||||
|
||||
//FColor
|
||||
GConfig->SetColor (
|
||||
*VictorySection,
|
||||
TEXT("Red"),
|
||||
FColor(255,0,0,255),
|
||||
GGameIni
|
||||
);
|
||||
|
||||
//FVector
|
||||
GConfig->SetVector (
|
||||
*VictorySection,
|
||||
TEXT("PlayerStartLocation"),
|
||||
FVector(0,0,512),
|
||||
GGameIni
|
||||
);
|
||||
|
||||
//FRotator
|
||||
GConfig->SetRotator (
|
||||
*VictorySection,
|
||||
TEXT("SunRotation"),
|
||||
FRotator(-90,0,0),
|
||||
GGameIni
|
||||
);
|
||||
|
||||
//ConfigCacheIni.h
|
||||
//void Flush( bool Read, const FString& Filename=TEXT("") );
|
||||
GConfig->Flush(false,GGameIni);
|
||||
}
|
||||
```
|
||||
ini配置文件的位置字符串变量,代码位于CoreGlobals.cpp中。
|
||||
```
|
||||
FString GEngineIni; /* Engine ini filename */
|
||||
|
||||
/** Editor ini file locations - stored per engine version (shared across all projects). Migrated between versions on first run. */
|
||||
FString GEditorIni; /* Editor ini filename */
|
||||
FString GEditorKeyBindingsIni; /* Editor Key Bindings ini file */
|
||||
FString GEditorLayoutIni; /* Editor UI Layout ini filename */
|
||||
FString GEditorSettingsIni; /* Editor Settings ini filename */
|
||||
|
||||
/** Editor per-project ini files - stored per project. */
|
||||
FString GEditorPerProjectIni; /* Editor User Settings ini filename */
|
||||
|
||||
FString GCompatIni;
|
||||
FString GLightmassIni; /* Lightmass settings ini filename */
|
||||
FString GScalabilityIni; /* Scalability settings ini filename */
|
||||
FString GHardwareIni; /* Hardware ini filename */
|
||||
FString GInputIni; /* Input ini filename */
|
||||
FString GGameIni; /* Game ini filename */
|
||||
FString GGameUserSettingsIni; /* User Game Settings ini filename */
|
||||
FString GRuntimeOptionsIni; /* Runtime Options ini filename */
|
||||
FString GInstallBundleIni; /* Install Bundle ini filename*/
|
||||
FString GDeviceProfilesIni;
|
||||
```
|
||||
|
||||
## 通过UObject进行修改
|
||||
很遗憾,本人没有从GConfig找到对应的设置变量,所以添加配置的操作都是通过UObject进行的。主要是通过GEngine这个全局对象以及GetMutableDefault<UObject>()获取对应的UObject,这两种方式进行操作。
|
||||
|
||||
操作的方式也有两种:
|
||||
1. 直接调用LoadConfig再入预先设置的ini文件,之后调用SaveConfig保存设置。
|
||||
2. 直接修改UObject的属性变量值,之后调用SaveConfig保存设置。
|
||||
|
||||
另外需要注意两点:
|
||||
1. Config的类型分为Config与GlobalConfig,调用函数时需要使用对应的类型枚举:CPF_Config与CPF_GlobalConfig。
|
||||
2. 是否在UCLASS(Config=XXX)有设置Config保存位置。
|
||||
|
||||
具体操作请见下文代码。
|
||||
|
||||
# 使用DataTable来管理GameplayTag
|
||||
本人在尝试通过修改配置来添加GameplayTag时,发现这个方法是无效的。(可能是个bug)在404的过程中,我发现可以通过DataTable来管理Tag。大致的方式是:
|
||||
1. 构建CSV或者JSON文件
|
||||
2. 导入UE4中并将DataTable类型选择为GameplayTagTableRow。
|
||||
3. 在ProjectSettings-GameplayTagTableList中添加DataTable即可。
|
||||
|
||||
这样做的好处有:
|
||||
1. GameplayTags的重复利用,避免手动输入出错,方便下个项目移植。
|
||||
2. 可以对不同类型Tag进行分表处理,在做项目时只需要加载需要的DataTable即可。
|
||||
|
||||
DataTable大致格式如下:
|
||||
|
||||
PS.有关GameplayTag的操作可以查看UGameplayTagsSettings、UGameplayTagsManager与GameplayTagEditorModule中的代码。
|
||||
|
||||
# 创建Console命令
|
||||
将这些代码放在GameMode、GameInstance里会降低性能(纯属强迫症),放在模块cpp文件中又会报错(估计需要将插件设置成后启动模式才行)。为了方便使用,我创建了一个Console命令,并将其放在BlueprintFunctionLibrary中。在编辑器中下入~,输入AddGASSettings并按下回车,即可添加所需设置。
|
||||
|
||||
# 具体代码实现
|
||||
```
|
||||
static void AddRPGGameplayAbilityProjectSettings(UWorld *InWorld)
|
||||
{
|
||||
UE_LOG(LogRPGGameplayAbility, Warning, TEXT("Add RPGGameplayAbility Project Settings!"));
|
||||
|
||||
//Special AssetManager
|
||||
FString ConfigPath = FPaths::ProjectConfigDir();
|
||||
GEngine->AssetManagerClassName = FString("/Script/RPGGameplayAbility.RPGAssetManager");
|
||||
GEngine->SaveConfig(CPF_GlobalConfig, *(ConfigPath + FString("DefaultEngine.ini")));
|
||||
|
||||
//Add DataAsset Config
|
||||
UAssetManagerSettings *AssetmanagerSettings = GetMutableDefault<UAssetManagerSettings>();
|
||||
FString PluginConfigPath = FPaths::ProjectPluginsDir() + FString("RPGGameplayAbility/Config/");
|
||||
AssetmanagerSettings->LoadConfig(nullptr, *FString(PluginConfigPath + FString("DefaultGame.ini")));
|
||||
AssetmanagerSettings->SaveConfig(CPF_Config, *(ConfigPath + FString("DefaultGame.ini")));
|
||||
|
||||
//Add GameplayTag
|
||||
UGameplayTagsSettings *GameplayTagsSettings = GetMutableDefault<UGameplayTagsSettings>();
|
||||
//GameplayTagsSettings->LoadConfig(nullptr, *FString(PluginConfigPath + FString("DefaultGameplayTags.ini")));
|
||||
FString TagDataTable{FString("/RPGGameplayAbility/Abilities/DataTables/GameplayTags.GameplayTags")};
|
||||
|
||||
if (GameplayTagsSettings->GameplayTagTableList.Find(TagDataTable) == -1)
|
||||
{
|
||||
GameplayTagsSettings->GameplayTagTableList.Add(TagDataTable);
|
||||
UGameplayTagsManager &tagManager = UGameplayTagsManager::Get();
|
||||
tagManager.DestroyGameplayTagTree();
|
||||
tagManager.LoadGameplayTagTables(false);
|
||||
tagManager.ConstructGameplayTagTree();
|
||||
tagManager.OnEditorRefreshGameplayTagTree.Broadcast();
|
||||
|
||||
GameplayTagsSettings->SaveConfig(CPF_Config, *(ConfigPath + FString("DefaultGameplayTags.ini")));
|
||||
}
|
||||
}
|
||||
|
||||
FAutoConsoleCommandWithWorld AbilitySystemDebugNextCategoryCmd(
|
||||
TEXT("AddGASSettings"),
|
||||
TEXT("增加RPGGameplayAbility所需要的项目设置!"),
|
||||
FConsoleCommandWithWorldDelegate::CreateStatic(AddRPGGameplayAbilityProjectSettings));
|
||||
```
|
||||
8
03-UnrealEngine/Gameplay/Gameplay/判断物体是否在屏幕中的方法.md
Normal file
8
03-UnrealEngine/Gameplay/Gameplay/判断物体是否在屏幕中的方法.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: 判断物体是否在屏幕中的方法
|
||||
date: 2022-12-09 14:08:26
|
||||
excerpt:
|
||||
tags: Camera
|
||||
rating: ⭐
|
||||
---
|
||||

|
||||
BIN
03-UnrealEngine/Gameplay/Gameplay/各平台手柄图片.jpg
LFS
Normal file
BIN
03-UnrealEngine/Gameplay/Gameplay/各平台手柄图片.jpg
LFS
Normal file
Binary file not shown.
Reference in New Issue
Block a user