UE4 的一些功能实现

本文对 UE4 的一些功能代码做了汇总,可能具备参考价值。

最近开始做 UE4 相关的需求,实现了一个模型自动化导入插件。要求在导入模型后,自动生成角色蓝图和动画蓝图,并把 Actor 放置到场景中自动播放动画。

整个过程要求一键导入,因此,需要使用代码来实现编辑器的一些操作。

这里不得不吐槽下 UE4 的文档,API 不全还难搜,以至于某些功能还得去翻看源码才知道调用了哪个接口。

所以,打算整理下本次涉及到的一些功能实现。由于 UE4 只是初学,本文只做记录不做讲解,若存在纰漏请见谅。

FBX 文件导入

创建 UFbxFactory ,设置 UAssetImportTask ,调用 ImportObject 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
UFbxFactory* fbxFactory = NewObject<UFbxFactory>();
fbxFactory->ImportUI->MeshTypeToImport = FBXIT_StaticMesh;
fbxFactory->ImportUI->OriginalImportType = FBXIT_StaticMesh;

fbxFactory->ImportUI->StaticMeshImportData->bCombineMeshes = true;
fbxFactory->ImportUI->StaticMeshImportData->ImportUniformScale = 1.0f;
fbxFactory->ImportUI->bImportMaterials = true;
fbxFactory->ImportUI->bImportTextures = true;
fbxFactory->ImportUI->bAutoComputeLodDistances = true;
fbxFactory->ImportUI->StaticMeshImportData->bAutoGenerateCollision = true;

UAssetImportTask* importTask = NewObject<UAssetImportTask>();
importTask->bAutomated = true;
importTask->bSave = true;

fbxFactory->SetAssetImportTask(importTask);

bool&& canceled = false;
fbxFactory->ImportObject(
UStaticMesh::StaticClass(),
folderPackage,
*FBXName,
EObjectFlags::RF_Transactional | EObjectFlags::RF_Public | EObjectFlags::RF_Standalone,
path,
nullptr,
canceled
);

设置导入完成回调,用于执行下一步操作:

1
FEditorDelegates::OnAssetPostImport.AddUFunction(this, STATIC_FUNCTION_FNAME(TEXT("UFBXLoader::LoadCallback")));

注意 this 必须继承自 UObject

文件拷贝

1
2
3
4
5
FString pluginsFolder = FPaths::ConvertRelativePathToFull(FPaths::ProjectPluginsDir());
FString contentFolder = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());
const FString bpPath = pluginsFolder + "BlendShape/Resources/BP.uasset";
const FString copyPath = contentFolder + "BP.uasset";
IFileManager::Get().Copy(*copyPath, *bpPath);

注意 Copy 里需要传入绝对路径。

资源加载

通过 LoadObject 加载 UObject 对象:

1
2
3
4
5
UBlueprint* sourceBlueprint = LoadObject<UBlueprint>(NULL, *sourceBlueprintPath);
USkeletalMesh* mesh = LoadObject<USkeletalMesh>(NULL, *skeletalPath, NULL, LOAD_None, NULL);
USkeleton* newSkeleton = LoadObject<USkeleton>(NULL, *newSkeletonPath, NULL, LOAD_None, NULL);
UPoseAsset* poseAsset = LoadObject<UPoseAsset>(NULL, *poseAssetPath, NULL, LOAD_None, NULL);
UAnimBlueprint* animBP = LoadObject<UAnimBlueprint>(NULL, *animBPPath, NULL, LOAD_None, NULL);

这里的路径基于 Content Browser 的路径。

通过 LoadClass 加载 UClass 对象:

1
UClass* animClass = LoadClass<UAnimInstance>(NULL, *animClassPath);

这里的路径同样是基于 Content Browser,但是资源的名称需要写成 name.name_C

动画蓝图重定向

自动生成动画蓝图的前提是有一份原始的动画蓝图,并且事先连接好事件图表,设置好 PoseAsset,然后调用 RetargetAnimations 方法重定向生成一份新的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
USkeleton* oldSkeleton = LoadObject<USkeleton>(NULL, *oldSkeletonPath, NULL, LOAD_None, NULL);
USkeleton* newSkeleton = LoadObject<USkeleton>(NULL, *newSkeletonPath, NULL, LOAD_None, NULL);

TArray<FAssetData> assetsToRetarget;
assetsToRetarget.Add(FAssetData((UObject*)poseAsset));
assetsToRetarget.Add(FAssetData((UObject*)animBP));

bool bRetargetReferredAssets = true;
bool bConvertSpace = true;

EditorAnimUtils::FNameDuplicationRule nameRule;
nameRule.Prefix = meshName + "_";
nameRule.FolderPath = FString(GameFolder) + "/" + newSkeletonFolder;

EditorAnimUtils::RetargetAnimations(oldSkeleton, newSkeleton, assetsToRetarget, bRetargetReferredAssets, &nameRule, bConvertSpace);

角色蓝图生成

复制一个蓝图类不能简单地拷贝文件,正确的操作在编辑器里叫 Duplicate ,这样可以指定新蓝图类的类名。

1
UEditorAssetLibrary::DuplicateLoadedAsset(sourceBlueprint, blueprintPath);

路径同样是基于 Content Browser ,最终新类名由 blueprintPath 的文件名指定。

角色蓝图设置 SkeletalMesh

SkeletalMesh 需要在 UBlueprintRootComponent 下的 USkeletalMeshComponent 上添加。

但是我没有找到获取 UBlueprintRootComponent 的方式,只找到 AActor 的。

所以这里的做法是,先创建一个空的 AActor,在上面修改 Component,再把 AActorComponent 添加到 UBlueprint 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FPreviewScene* scene = new FPreviewScene();

FActorSpawnParameters spawnparam;
spawnparam.bAllowDuringConstructionScript = true;
AActor* tempActor = scene->GetWorld()->SpawnActor<AActor>(AActor::StaticClass(), FTransform::Identity);

USceneComponent* rootComponent = NewObject<USceneComponent>(tempActor, FName(ActorRootComponentName));
tempActor->AddOwnedComponent(rootComponent);
tempActor->AddInstanceComponent(rootComponent);
rootComponent->RegisterComponent();
tempActor->SetRootComponent(rootComponent);

FString skeletalPath = FString(GameFolder) + "/" + toFolder + "/" + meshName;
USkeletalMesh* mesh = LoadObject<USkeletalMesh>(NULL, *skeletalPath, NULL, LOAD_None, NULL);
USkeletalMeshComponent* meshComponent = NewObject<USkeletalMeshComponent>(tempActor, FName(*UKismetSystemLibrary::GetObjectName(mesh)));
tempActor->AddOwnedComponent(meshComponent);
tempActor->AddInstanceComponent(meshComponent);
meshComponent->RegisterComponent();
meshComponent->SetSkeletalMesh(mesh);

meshComponent->AttachToComponent(tempActor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);

FKismetEditorUtilities::AddComponentsToBlueprint(blueprint, tempActor->GetComponents().Array());
delete(scene);

角色蓝图关联动画蓝图

读取 UClass 后,通过 SetAnimClass 指定动画蓝图类:

1
2
3
4
FString animClassName = meshName + "_" + SourceAnimBPName;
FString animClassPath = "AnimBlueprint'" + FString(GameFolder) + "/" + toFolder + "/" + animClassName + "." + animClassName + "_C'";
UClass* animClass = LoadClass<UAnimInstance>(NULL, *animClassPath);
meshComponent->SetAnimClass(animClass);

Actor 放置到场景

通过 SpawnActor ,可以在场景中添加一个 Actor 对象:

1
2
3
UClass* bpClass = blueprint->StaticClass();
UWorld* world = GEditor->GetEditorWorldContext().World();
AActor *actor = world->SpawnActor<AActor>(blueprint->GeneratedClass, FVector(-250, 0, 100), FRotator(0, 90, 0));

为了后续方便获取到这个 Actor,给 Actor 加上 Tag :

1
actor->Tags.Add(ActorTag);

修改 Actor 属性

先通过 Tag 获取到场景中的 Actor 对象:

1
2
3
4
TArray<AActor*> actors;
UWorld* world = GWorld->GetWorld();
UGameplayStatics::GetAllActorsWithTag(world, ActorTag, actors);
AActor* actor = actors[0];

通过 FindFieldChecked 获取 FProperty

1
FArrayProperty* arrayProperty = FindFieldChecked<FArrayProperty>(actor->GetClass(), *name);

通过 ContainerPtrToValuePtrFProperty 里读取属性值:

1
TArray<FString> arrayOfStrings = *arrayProperty->ContainerPtrToValuePtr<TArray<FString>>(actor);

通过 SetPropertyValue_InContainer 修改 Actor 的属性值:

1
strProperty->SetPropertyValue_InContainer(actor, path);

Runtime 切换摄像机

我们希望在运行的时候,切换到特定的视角。这里的做法是动态添加 ACameraActor,调整位置,并在 Runtime 时把视角切换到新添加的 ACameraActor 上。

由于是在 Runtime 执行切换,所以要先继承 ACameraActor,在子类的 BeginPlay 方法里实现切换逻辑:

1
2
3
4
5
6
7
8
void AFaceCameraActor::BeginPlay()
{
GetWorld()->RegisterAutoActivateCamera(this, 0);
APlayerController* playerController = UGameplayStatics::GetPlayerController(this, 0);
playerController->SetViewTarget(this);

Super::BeginPlay();
}

参考