
Здравствуйте, меня зовут Дмитрий. Я занимаюсь созданием компьютерных игр на Unreal Engine в качестве хобби. Для своего проекта я разрабатываю продцедурно генерируемый уровень. Мой алгоритм расставляет в определенно порядке точки в пространстве (которые я называю корни «roots»), после чего к этим точкам я прикрепляю меши. Но тут возникает проблема в том, что нужно с начала прикрепить меш потом откомпилировать проект и лиш после этого можно увидеть как она встала. Естественно постоянно бегать из окна редактора в окно VS очень долго. И я подумал что можно было-бы для этого использовать редактор blueprint, тем более мне попался на глаза плагин Dungeon architect, в котором расстановка объектов по уровню реализована через blueprint. Собственно здесь я расскажу о создании подобной системы скриншот из которой изображен на первом рисунке.
Итак с начала создадим свой тип файла (подробней можно посмотреть вот эту статью). В классе AssetAction переопределяем функцию OpenAssetEditor.
void FMyObjectAssetAction::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor)
{
const EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone;
for (auto ObjIt = InObjects.CreateConstIterator(); ObjIt; ++ObjIt)
{
UMyObject* PropData = Cast<UMyObject>(*ObjIt);
if (PropData)
{
TSharedRef<FCustAssetEditor> NewCustEditor(new FCustAssetEditor());
NewCustEditor->InitCustAssetEditor(Mode, EditWithinLevelEditor, PropData);
}
}
}
Теперь если мы попытаемся открыть этот файл будет открыто не привычное окно, а то окно, которое мы определим в классе FCustAssetEditor.
class FCustAssetEditor : public FAssetEditorToolkit, public FNotifyHook
{
public:
~FCustAssetEditor();
// IToolkit interface
virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
// FAssetEditorToolkit
virtual FName GetToolkitFName() const override;
virtual FText GetBaseToolkitName() const override;
virtual FLinearColor GetWorldCentricTabColorScale() const override;
virtual FString GetWorldCentricTabPrefix() const override;
void InitCustAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, UMyObject* PropData);
int N;
protected:
void OnGraphChanged(const FEdGraphEditAction& Action);
void SelectAllNodes();
bool CanSelectAllNodes() const;
void DeleteSelectedNodes();
bool CanDeleteNode(class UEdGraphNode* Node);
bool CanDeleteNodes() const;
void DeleteNodes(const TArray<class UEdGraphNode*>& NodesToDelete);
void CopySelectedNodes();
bool CanCopyNodes() const;
void PasteNodes();
void PasteNodesHere(const FVector2D& Location);
bool CanPasteNodes() const;
void CutSelectedNodes();
bool CanCutNodes() const;
void DuplicateNodes();
bool CanDuplicateNodes() const;
void DeleteSelectedDuplicatableNodes();
/** Called when the selection changes in the GraphEditor */
void OnSelectedNodesChanged(const TSet<class UObject*>& NewSelection);
/** Called when a node is double clicked */
void OnNodeDoubleClicked(class UEdGraphNode* Node);
void ShowMessage();
TSharedRef<class SGraphEditor> CreateGraphEditorWidget(UEdGraph* InGraph);
TSharedPtr<SGraphEditor> GraphEditor;
TSharedPtr<FUICommandList> GraphEditorCommands;
TSharedPtr<IDetailsView> PropertyEditor;
UMyObject* PropBeingEdited;
TSharedRef<SDockTab> SpawnTab_Viewport(const FSpawnTabArgs& Args);
TSharedRef<SDockTab> SpawnTab_Details(const FSpawnTabArgs& Args);
FDelegateHandle OnGraphChangedDelegateHandle;
TSharedPtr<FExtender> ToolbarExtender;
TSharedPtr<FUICommandList> MyToolBarCommands;
bool bGraphStateChanged;
void AddToolbarExtension(FToolBarBuilder &builder);
};
Самым важным для нас методом этого класса явлется InitCustAssetEditor. Сначала этот метод создает новый редактор о чем ниже, потом он, создает две новые пустые вкладки:
const TSharedRef<FTabManager::FLayout> StandaloneDefaultLayout = FTabManager::NewLayout("CustomEditor_Layout")
->AddArea
(
FTabManager::NewPrimaryArea()
->SetOrientation(Orient_Vertical)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.1f)
->SetHideTabWell(true)
->AddTab(GetToolbarTabId(), ETabState::OpenedTab)
)
->Split
(
FTabManager::NewSplitter()
->SetOrientation(Orient_Horizontal)
->SetSizeCoefficient(0.2f)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.8f)
->SetHideTabWell(true)
->AddTab(FCustomEditorTabs::ViewportID, ETabState::OpenedTab)
)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.2f)
->SetHideTabWell(true)
->AddTab(FCustomEditorTabs::DetailsID, ETabState::OpenedTab)
)
)
);
Одна из этих вкладок будет вкладкой нашего блюпринт редактора, а вторая нужна для отображения свойств нодов. Собственно вкладки созданы нужно их чем-то заполнить. Заполняет вкладки содержимым метод RegisterTabSpawners
void FCustAssetEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager)
{
WorkspaceMenuCategory = TabManager->AddLocalWorkspaceMenuCategory(FText::FromString("Custom Editor"));
auto WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef();
FAssetEditorToolkit::RegisterTabSpawners(TabManager);
TabManager->RegisterTabSpawner(FCustomEditorTabs::ViewportID, FOnSpawnTab::CreateSP(this, &FCustAssetEditor::SpawnTab_Viewport))
.SetDisplayName(FText::FromString("Viewport"))
.SetGroup(WorkspaceMenuCategoryRef)
.SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Viewports"));
TabManager->RegisterTabSpawner(FCustomEditorTabs::DetailsID, FOnSpawnTab::CreateSP(this, &FCustAssetEditor::SpawnTab_Details))
.SetDisplayName(FText::FromString("Details"))
.SetGroup(WorkspaceMenuCategoryRef)
.SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Details"));
}
TSharedRef<SDockTab> FCustAssetEditor::SpawnTab_Viewport(const FSpawnTabArgs& Args)
{
return SNew(SDockTab)
.Label(FText::FromString("Mesh Graph"))
.TabColorScale(GetTabColorScale())
[
GraphEditor.ToSharedRef()
];
}
TSharedRef<SDockTab> FCustAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Args)
{
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
const FDetailsViewArgs DetailsViewArgs(false, false, true, FDetailsViewArgs::HideNameArea, true, this);
TSharedRef<IDetailsView> PropertyEditorRef = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
PropertyEditor = PropertyEditorRef;
// Spawn the tab
return SNew(SDockTab)
.Label(FText::FromString("Details"))
[
PropertyEditorRef
];
}
Панель свойств нам подойдет стандартная, а вот bluprin редактор мы создадим свой. Создается он в методе CreateGraphEditorWidget.
TSharedRef<SGraphEditor> FCustAssetEditor::CreateGraphEditorWidget(UEdGraph* InGraph)
{
// Create the appearance info
FGraphAppearanceInfo AppearanceInfo;
AppearanceInfo.CornerText = FText::FromString("Mesh tree Editor");
GraphEditorCommands = MakeShareable(new FUICommandList);
{
GraphEditorCommands->MapAction(FGenericCommands::Get().SelectAll,
FExecuteAction::CreateSP(this, &FCustAssetEditor::SelectAllNodes),
FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanSelectAllNodes)
);
GraphEditorCommands->MapAction(FGenericCommands::Get().Delete,
FExecuteAction::CreateSP(this, &FCustAssetEditor::DeleteSelectedNodes),
FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanDeleteNodes)
);
GraphEditorCommands->MapAction(FGenericCommands::Get().Copy,
FExecuteAction::CreateSP(this, &FCustAssetEditor::CopySelectedNodes),
FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanCopyNodes)
);
GraphEditorCommands->MapAction(FGenericCommands::Get().Paste,
FExecuteAction::CreateSP(this, &FCustAssetEditor::PasteNodes),
FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanPasteNodes)
);
GraphEditorCommands->MapAction(FGenericCommands::Get().Cut,
FExecuteAction::CreateSP(this, &FCustAssetEditor::CutSelectedNodes),
FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanCutNodes)
);
GraphEditorCommands->MapAction(FGenericCommands::Get().Duplicate,
FExecuteAction::CreateSP(this, &FCustAssetEditor::DuplicateNodes),
FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanDuplicateNodes)
);
}
SGraphEditor::FGraphEditorEvents InEvents;
InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateSP(this, &FCustAssetEditor::OnSelectedNodesChanged);
InEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &FCustAssetEditor::OnNodeDoubleClicked);
TSharedRef<SGraphEditor> _GraphEditor = SNew(SGraphEditor)
.AdditionalCommands(GraphEditorCommands)
.Appearance(AppearanceInfo)
.GraphToEdit(InGraph)
.GraphEvents(InEvents)
;
return _GraphEditor;
}
Здесь с начала определяются действия и события на которые будет реагировать наш редактор, а потом собственно создается виджет редактора. Наиболее интересным параметром является .GraphToEdit(InGraph) он передает указатель на класс UEdGraphSchema_CustomEditor
UCLASS()
class UEdGraphSchema_CustomEditor : public UEdGraphSchema
{
GENERATED_UCLASS_BODY()
// Begin EdGraphSchema interface
virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override;
virtual void GetContextMenuActions(const UEdGraph* CurrentGraph, const UEdGraphNode* InGraphNode, const UEdGraphPin* InGraphPin, FMenuBuilder* MenuBuilder, bool bIsDebugging) const override;
virtual const FPinConnectionResponse CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const override;
virtual class FConnectionDrawingPolicy* CreateConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) const override;
virtual FLinearColor GetPinTypeColor(const FEdGraphPinType& PinType) const override;
virtual bool ShouldHidePinDefaultValue(UEdGraphPin* Pin) const override;
// End EdGraphSchema interface
};
Этот класс определяет такие вещи как пункты контекстного меню редактора, определяет как будут соединятся ��ежду собой ноды и т.д. Для нас самое главное это возможность создания собственных нод. Это делается в методе GetGraphContextActions.
void UEdGraphSchema_CustomEditor::GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const
{
FFormatNamedArguments Args;
const FName AttrName("Attributes");
Args.Add(TEXT("Attribute"), FText::FromName(AttrName));
const UEdGraphPin* FromPin = ContextMenuBuilder.FromPin;
const UEdGraph* Graph = ContextMenuBuilder.CurrentGraph;
TArray<TSharedPtr<FEdGraphSchemaAction> > Actions;
CustomSchemaUtils::AddAction<URootNode>(TEXT("Add Root Node"), TEXT("Add root node to the prop graph"), Actions, ContextMenuBuilder.OwnerOfTemporaries);
CustomSchemaUtils::AddAction<UBranchNode>(TEXT("Add Brunch Node"), TEXT("Add brunch node to the prop graph"), Actions, ContextMenuBuilder.OwnerOfTemporaries);
CustomSchemaUtils::AddAction<URuleNode>(TEXT("Add Rule Node"), TEXT("Add ruleh node to the prop graph"), Actions, ContextMenuBuilder.OwnerOfTemporaries);
CustomSchemaUtils::AddAction<USwitcherNode>(TEXT("Add Switch Node"), TEXT("Add switch node to the prop graph"), Actions, ContextMenuBuilder.OwnerOfTemporaries);
for (TSharedPtr<FEdGraphSchemaAction> Action : Actions)
{
ContextMenuBuilder.AddAction(Action);
}
}
Как вы видете пока что я создал только четыре ноды итак по списку:
1)Нода URootNode является отображением элемента корень на графе. URootNode также как и элементы типа корень имеют тип.
2)Нода UBranchNode эта нода размещает на уровне статик меш (пока только меши, но можно легко создать ноды и для других элементов обстановки или персонажей)
3)Нода URuleNode эта нода может быть либо открыта либо закрыта в зависимости от заданного условия. Условие естественно задаются в blueprint.
4)Нода USwitcherNode эта нода имеет один вход и два выхода в зависимости от условия может открывать либо правый выход либо левый.
Пока только четыре ноды но если у вас есть идеи можете написать их в комментарии. Давайте посмотрим как они устроены. (Для экономии места я приведу здесь только базовый для них класс, исходники можно скачать по ссылке в конце статьи)
UCLASS()
class UICUSTOM_API UCustomNodeBase : public UEdGraphNode
{
GENERATED_BODY()
public:
virtual TArray<UCustomNodeBase*> GetChildNodes(FRandomStream& RandomStream);
virtual void CreateNodesMesh(UWorld* World, FName ActorTag, FRandomStream& RandomStream, FVector AbsLocation, FRotator AbsRotation);
virtual void PostEditChangeProperty(struct FPropertyChangedEvent& e) override;
TSharedPtr<FNodePropertyObserver> PropertyObserver;
FVector Location;
FRotator Rotation;
};
Здесь мы видим метод GetChildNodes в котором нода передает массив объектов присоединенных к её выходам. И метод CreateNodesMesh в котором нода создает меш или не создает а просто передает дальше значения AbsLocation и AbsRotation. Метод PostEditChangeProperty как вы наверно догадались выполняется когда кто-то меняет свойства ноды.
Но как вы наверно заметили ноды на з��главном рисунке отличаются по внешнему виду от тех, которые мы привыкли видеть. Как же этого добиться. Для этого нужно создать для каждой ноды класс наследник SGraphNode. Как и в прошлый раз здесь я приведу только базовый класс.
class SGraphNode_CustomNodeBase : public SGraphNode, public FNodePropertyObserver
{
public:
SLATE_BEGIN_ARGS(SGraphNode_CustomNodeBase) { }
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs, UCustomNodeBase* InNode);
// SGraphNode interface
virtual void UpdateGraphNode() override;
virtual void CreatePinWidgets() override;
virtual void AddPin(const TSharedRef<SGraphPin>& PinToAdd) override;
virtual void CreateNodeWidget();
// End of SGraphNode interface
// FPropertyObserver interface
virtual void OnPropertyChanged(UEdGraphNode* Sender, const FName& PropertyName) override;
// End of FPropertyObserver interface
protected:
UCustomNodeBase* NodeBace;
virtual FSlateColor GetBorderBackgroundColor() const;
virtual const FSlateBrush* GetNameIcon() const;
TSharedPtr<SHorizontalBox> OutputPinBox;
FLinearColor BackgroundColor;
TSharedPtr<SOverlay> NodeWiget;
};
Наследование класса FNodePropertyObserver нужено исключительно для метода OnPropertyChanged. Самым важным методом является метод UpdateGraphNode именно в нем и создается виджет который мы видим на экране, Остальные методы вызываются из него для создания определенных частей этого виждета.
Прошу не путать класс SGraphNode с классом UEdGraphNode. SGraphNode определяет исключительно внешний вид ноды, в то время как класс UEdGraphNode определяет свойства самой ноды.
Но даже сейчас если запустить проект ноды будут иметь прежний вид. Чтобы изменения внешнего вида вступили в силу, нужно их зарегистрировать. Где это сделать? Конечно же при старте модуля:
void FUICustomEditorModule::StartupModule()
{
//Registrate asset actions for MyObject
FMyObjectAssetAction::RegistrateCustomPartAssetType();
//Registrate detail pannel costamization for TestActor
FMyClassDetails::RegestrateCostumization();
// Register custom graph nodes
TSharedPtr<FGraphPanelNodeFactory> GraphPanelNodeFactory = MakeShareable(new FGraphPanelNodeFactory_Custom);
FEdGraphUtilities::RegisterVisualNodeFactory(GraphPanelNodeFactory);
//Registrate ToolBarCommand for costom graph
FToolBarCommandsCommands::Register();
//Create pool for icon wich show on costom nodes
FCustomEditorThumbnailPool::Create();
}
Хочу заметить что также здесь создается хранилище, для хранения иконок которые будут отображаться на нодах UBranchNode. Регестрация нодов происходит в методе CreateNode класса FGraphPanelNodeFactory_Custom.
TSharedPtr<class SGraphNode> FGraphPanelNodeFactory_Custom::CreateNode(UEdGraphNode* Node) const
{
if (URootNode* RootNode = Cast<URootNode>(Node))
{
TSharedPtr<SGraphNode_Root> SNode = SNew(SGraphNode_Root, RootNode);
RootNode->PropertyObserver = SNode;
return SNode;
}
else if (UBranchNode* BranchNode = Cast<UBranchNode>(Node))
{
TSharedPtr<SGraphNode_Brunch> SNode = SNew(SGraphNode_Brunch, BranchNode);
BranchNode->PropertyObserver = SNode;
return SNode;
}
else if (URuleNode* RuleNode = Cast<URuleNode>(Node))
{
TSharedPtr<SGraphNode_Rule> SNode = SNew(SGraphNode_Rule, RuleNode);
RuleNode->PropertyObserver = SNode;
return SNode;
}
else if (USwitcherNode* SwitcherNode = Cast<USwitcherNode>(Node))
{
TSharedPtr<SGraphNode_Switcher> SNode = SNew(SGraphNode_Switcher, SwitcherNode);
SwitcherNode->PropertyObserver = SNode;
return SNode;
}
return NULL;
}
Генерация осуществляется в классе TestActor.
bool ATestAct::GenerateMeshes()
{
FRandomStream RandomStream = FRandomStream(10);
if (!MyObject)
{
return false;
}
for (int i = 0; i < Roots.Num(); i++)
{
URootNode* RootBuf;
RootBuf = MyObject->FindRootFromType(Roots[i].RootType);
if (RootBuf)
{
RootBuf->CreateNodesMesh(GetWorld(), ActorTag, RandomStream, Roots[i].Location, FRotator(0, 0, 0));
}
}
return true;
}
Здесь мы переберем в цикле все объекты root, каждый из них характерезуется координатой в пространстве и типом. Получив этот объект мы ищем в графе ноду URootNode c таким же типом. Найдя её передаем ей начальные координаты и запускаем метод CreateNodesMesh который пройдет по цепочки через весь граф. Делаем это пока все объекты root не будут обработаны.
Собственно вот и все. Для дальнейшего ознакомления рекомендую смотреть исходники.
Проект с исходным кодом здесь
А я пока расскажу вам как же работает это хозяйство. Генерация осуществляется в объекте TestActor, с начала надо в ручную задать положения и типы объектов root (а что вы хотели проект учебный).

После этого выбираем в свойствах файл MyObject, в котором мы должны построить граф, определяющий какие меши будут созданы.
Итак как-же задать правило для ноды rule и switcher. Для этого нажимаем плюсик в свойства чтобы создать новый блюпринт.

Но он оказывается пустым что-же делать дальше? Нужно нажать Override NodeBool.

Теперь можно или открыть или закрыть ноду.

Все аналогично и для switchera. У ноды Brunch есть такое же правило для задания координаты и поворота. Кроме того она имеет выход, это значит если к ней прикрепить другую Brunch то она в качестве привязки будет использовать координату предыдущей.
Осталось только нажать кнопку Generate Meshes на панели свойств TestActor, и наслаждаться результатом.

Надеюсь вам понравилась эта статья. Она оказалась намного длинней чем раньше, боялся что не допишу до конца.
P.S После того как я написал статью, я попробовал собрать игру и она не собралась. Чтобы игру можно было собрать надо в файле CustomNods.h внести следующие исправления:
class UICUSTOM_API UCustomNodeBase : public UEdGraphNode
{
GENERATED_BODY()
public:
virtual TArray<UCustomNodeBase*> GetChildNodes(FRandomStream& RandomStream);
virtual void CreateNodesMesh(UWorld* World, FName ActorTag, FRandomStream& RandomStream, FVector AbsLocation, FRotator AbsRotation);
#if WITH_EDITORONLY_DATA
virtual void PostEditChangeProperty(struct FPropertyChangedEvent& e) override;
#endif //WITH_EDITORONLY_DATA
TSharedPtr<FNodePropertyObserver> PropertyObserver;
};
То есть мы должны исключить все функции кроме GetChildNodes и CreateNodesMesh из класса ноды при помощи оператора #if WITH_EDITORONLY_DATA. В остальных нодах надо сделать тоже самое.
И соответственно CustomNods.cpp:
TArray<UCustomNodeBase*> UCustomNodeBase::GetChildNodes(FRandomStream& RandomStream)
{
TArray<UCustomNodeBase*> ChildNodes;
return ChildNodes;
}
void UCustomNodeBase::CreateNodesMesh(UWorld* World, FName ActorTag, FRandomStream& RandomStream, FVector AbsLocation, FRotator AbsRotation)
{
TArray<UCustomNodeBase*>ChailNodes = GetChildNodes(RandomStream);
for (int i = 0; i < ChailNodes.Num(); i++)
{
ChailNodes[i]->CreateNodesMesh(World, ActorTag, RandomStream, AbsLocation, AbsRotation);
}
}
#if WITH_EDITORONLY_DATA
void UCustomNodeBase::PostEditChangeProperty(struct FPropertyChangedEvent& e)
{
if (PropertyObserver.IsValid())
{
FName PropertyName = (e.Property != NULL) ? e.Property->GetFName() : NAME_None;
PropertyObserver->OnPropertyChanged(this, PropertyName);
}
Super::PostEditChangeProperty(e);
}
#endif //WITH_EDITORONLY_DATA
Если вы уже скачали файл проекта пожалуйста перекачайте его заново.
P.P.S Продолжение
