Published on

编辑器扩展:自定义PlaceActors

Authors
  • avatar
    Name
    东哥
    Twitter

前言

image-20210813111004071

有的时候, 为了方便项目需求, 我们需要自定义PlaceActors标签以及对应的类

本文记录如何实现这一扩展

自定义标签

开始之前我们需要了解一下UE4编辑器模块的启动顺序, 可以参考之前的一篇文章 UE4模块启动顺序

关键的一点是, 我们对编辑器添加新的PlaceActors标签需要在编辑器初始化之后, 因为IPlacementModeModule::Get()会启动PlacementMode模块, 而该模块启动时需要构造FPlaceableItem对象, FPlaceableItem对象构造时需要依赖GEditor->FindActorFactoryByClass()函数, 该函数实现如下

UActorFactory* UEditorEngine::FindActorFactoryByClass( const UClass* InClass ) const
{
    for( int32 i = 0 ; i < ActorFactories.Num() ; ++i )
    {
        UActorFactory* Factory = ActorFactories[i];

        if( Factory != NULL && Factory->GetClass() == InClass )
        {
            return Factory;
        }
    }

    return NULL;
}

ActorFactories数组会在UEditorEngine::InitEditor()时完成, 意味着我们注册标签必须在编辑器初始化完成之后.

所以我们实现自定义PlaceActors标签的插件模块必须是PostEngineInit, 或者我们还有一个更安全的办法, 即通过监听编辑器初始化完成的回调事件来实现

FCoreDelegates::OnPostEngineInit.AddRaw(this, &FAdvancedFrameworkEditorModule::RegisterPlaceActors);
void FAdvancedFrameworkEditorModule::RegisterPlaceActors()
{
    FPlacementCategoryInfo MyCategoryInfo(FText::FromString("AdvancedFramework"), "AdvancedFrameworkActors", "AdvancedFrameworkTags", 99);
    if (IPlacementModeModule::Get().IsAvailable())
    {
        IPlacementModeModule::Get().RegisterPlacementCategory(MyCategoryInfo);	IPlacementModeModule::Get().RegisterPlaceableItem(MyCategoryInfo.UniqueHandle, MakeShareable(new FPlaceableItem(*UActorFactoryEmptyActor::StaticClass())));
    }
}

这里我们先添加一个叫AdvancedFrameworkActors的标签, 然后使用引擎默认的EmptyActor来先添加一个对象

先看一下数据结构FPlacementCategoryInfo

struct FPlacementCategoryInfo
{
	FPlacementCategoryInfo(FText InDisplayName, FName InHandle, FString InTag, int32 InSortOrder = 0, bool bInSortable = true)
		: DisplayName(InDisplayName), UniqueHandle(InHandle), SortOrder(InSortOrder), TagMetaData(MoveTemp(InTag)), bSortable(bInSortable)
	{
	}

	/** This category's display name */
	FText DisplayName;

	/** A unique name for this category */
	FName UniqueHandle;

	/** Sort order for the category tab (lowest first) */
	int32 SortOrder;

	/** Optional tag meta data for the tab widget */
	FString TagMetaData;

	/** Optional generator function used to construct this category's tab content. Called when the tab is activated. */
	TFunction<`TSharedRef<SWidget>`()> CustomGenerator;

	/** Whether the items in this category are automatically sortable by name. False if the items are already sorted. */
	bool bSortable;
};

我们显示的名称是构造函数的第二个字符串, 然后第4个参数决定我们的排序优先级, 我们打算放到最下面所以设置成了99

添加对象

接下来要看的是RegisterPlaceableItem()

`TOptional<FPlacementModeID>` FPlacementModeModule::RegisterPlaceableItem(FName CategoryName, const `TSharedRef<FPlaceableItem>`& InItem)
{
	FPlacementCategory* Category = Categories.Find(CategoryName);
	if (Category && !Category->CustomGenerator)
	{
		FPlacementModeID ID = CreateID(CategoryName);
		Category->Items.Add(ID.UniqueID, InItem);
		return ID;
	}
	return `TOptional<FPlacementModeID>`();
}

需要传入一个FPlaceableItem对象, 再来看FPlaceableItem的构造函数

FPlaceableItem(UActorFactory* InFactory, const FAssetData& InAssetData, `TOptional<int32>` InSortOrder = `TOptional<int32>`());

FPlaceableItem(UClass& InAssetClass, `TOptional<int32>` InSortOrder = `TOptional<int32>`());

FPlaceableItem(
    UClass& InAssetClass,
    const FAssetData& InAssetData,
    FName InClassThumbnailBrushOverride = NAME_None,
    `TOptional<FLinearColor>` InAssetTypeColorOverride = `TOptional<FLinearColor>`(),
    `TOptional<int32>` InSortOrder = `TOptional<int32>`(),
    `TOptional<FText>` InDisplayName = `TOptional<FText>`()
)

理论上讲上述构造函数都可以使用, 但是我们本着能方便就方便, 能偷懒就偷懒的原则, 先去看一看引擎自己咋使用的

//PlacementModeModule.cpp  L123
FPlacementCategory* Category = Categories.Find(CategoryName);
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryEmptyActor::StaticClass(), SortOrder += 10)));
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryCharacter::StaticClass(), SortOrder += 10)));
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryPawn::StaticClass(), SortOrder += 10)));
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryPointLight::StaticClass(), SortOrder += 10)));
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryPlayerStart::StaticClass(), SortOrder += 10)));
// Cube
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryBasicShape::StaticClass(), FAssetData(`LoadObject<UStaticMesh>`(nullptr, *UActorFactoryBasicShape::BasicCube.ToString())), FName("ClassThumbnail.Cube"), BasicShapeColorOverride, SortOrder += 10, NSLOCTEXT("PlacementMode", "Cube", "Cube"))));
// Sphere
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryBasicShape::StaticClass(), FAssetData(`LoadObject<UStaticMesh>`(nullptr, *UActorFactoryBasicShape::BasicSphere.ToString())), FName("ClassThumbnail.Sphere"), BasicShapeColorOverride, SortOrder += 10, NSLOCTEXT("PlacementMode", "Sphere", "Sphere"))));
// Cylinder
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryBasicShape::StaticClass(), FAssetData(`LoadObject<UStaticMesh>`(nullptr, *UActorFactoryBasicShape::BasicCylinder.ToString())), FName("ClassThumbnail.Cylinder"), BasicShapeColorOverride, SortOrder += 10, NSLOCTEXT("PlacementMode", "Cylinder", "Cylinder"))));
// Cone
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryBasicShape::StaticClass(), FAssetData(`LoadObject<UStaticMesh>`(nullptr, *UActorFactoryBasicShape::BasicCone.ToString())), FName("ClassThumbnail.Cone"), BasicShapeColorOverride, SortOrder += 10, NSLOCTEXT("PlacementMode", "Cone", "Cone"))));
// Plane
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryBasicShape::StaticClass(), FAssetData(`LoadObject<UStaticMesh>`(nullptr, *UActorFactoryBasicShape::BasicPlane.ToString())), FName("ClassThumbnail.Plane"), BasicShapeColorOverride, SortOrder += 10, NSLOCTEXT("PlacementMode", "Plane", "Plane"))));

Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryTriggerBox::StaticClass(), SortOrder += 10)));
Category->Items.Add(CreateID(), MakeShareable(new FPlaceableItem(*UActorFactoryTriggerSphere::StaticClass(), SortOrder += 10)));

看上述代码, 所以引擎自己都使用一个UActorFactory对象, 观察这个类


UCLASS(collapsecategories, hidecategories=Object, editinlinenew, config=Editor, abstract, transient)
class UNREALED_API UActorFactory : public UObject
{
	GENERATED_UCLASS_BODY()

	/** Name used as basis for 'New Actor' menu. */
	UPROPERTY()
	FText DisplayName;

	/** Indicates how far up the menu item should be. The higher the number, the higher up the list.*/
	UPROPERTY(config)
	int32 MenuPriority;

	/** name of actor subclass this actorfactory creates - dynamically loaded.  Overrides NewActorClass. */
	UPROPERTY(config)
	FString NewActorClassName;

	/**  AActor  subclass this ActorFactory creates. */
	UPROPERTY()
	`TSubclassOf<class AActor>`  NewActorClass;
//...........
};

看了一些引擎的实现, 重点就是上面这些成员变量, 以及部分虚函数的实现, 比如

virtual bool CanCreateActorFrom( const FAssetData& AssetData, FText& OutErrorMsg );
virtual AActor* SpawnActor( UObject* Asset, ULevel* InLevel, const FTransform& Transform, EObjectFlags ObjectFlags, const FName Name );

其实一些类比如骨骼模型SkeletalMesh, 直接拖拽到编辑器世界空间中他会创建一个Actor来完成骨骼模型的显示, 而我们其实可以不需要这一步骤(如果我们的对象本身就是一个普通的actor)

所以可以这样添加

IPlacementModeModule::Get().RegisterPlaceableItem(MyCategoryInfo.UniqueHandle, MakeShareable(new FPlaceableItem(nullptr, FAssetData(XXX:StaticClass()))));

我这边在项目设置里丢进去一个数组TArray<TSubclassOf> PlacementActors;

那就可以动态的扩展这一个标签下的显示对象

if(UAF_GameSetting_Default* st = UAF_BlueprintLibrary::GetDefaultSetting())
{
    if (st->PlacementActors.Num()>0)
    {
        for (auto i : st->PlacementActors)
        {
            IPlacementModeModule::Get().RegisterPlaceableItem(MyCategoryInfo.UniqueHandle, MakeShareable(new FPlaceableItem(nullptr, FAssetData(i))));
        }
    }
}
	IPlacementModeModule::Get().RegisterPlaceableItem(MyCategoryInfo.UniqueHandle, MakeShareable(new FPlaceableItem(*UAF_ActorFactory::StaticClass())));

最后实现效果如下

image-20210813110941324

image-20210813111004071