SceneManagerWindow

ImGui를 사용한, 테이블 안의 트리노드로 구성. 매 틱마다 UI 렌더 시 호출됨

// Using those as a base value to create width/height that are factor of the size of our font
const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x;
const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing();

ImGui::Begin("Outliner");
static ImGuiSelectionBasicStorage OutlinerSelection;
ImGui::Text("Selection size: %d", OutlinerSelection.Size);
static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_NoBordersInBodyUntilResize | ImGuiTableFlags_Hideable | ImGuiTableFlags_Sortable;

if (ImGui::BeginTable("table", 7, flags, ImVec2(0.0f, TEXT_BASE_HEIGHT * 16)))
{
    ImGui::TableSetupColumn("V", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_IndentDisable);
    ImGui::TableSetupColumn("*", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
    ImGui::TableSetupColumn("P", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
    ImGui::TableSetupColumn("ItemLabel", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_NoHide);
    ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 18.0f);
    ImGui::TableSetupColumn("UUID", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 18.0f);
    ImGui::TableSetupColumn("PUUID", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 18.0f);
    ImGui::TableHeadersRow();

    ActorTreeNode* tree = UEngine::Get().GetWorld()->WorldNode;
    ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect2d;
    ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, OutlinerSelection.Size, -1);
    ActorTreeNode::ApplySelectionRequests(ms_io, tree, &OutlinerSelection);
		ActorTreeNode::DisplayNode(tree, &OutlinerSelection);
    ms_io = ImGui::EndMultiSelect();
    ActorTreeNode::ApplySelectionRequests(ms_io, tree, &OutlinerSelection);

    ImGui::EndTable();
}
ImGui::End();

ActorTreeNode

World의 Actor들을 UI에 나타내기 위하여 만든 트리 구조

class ActorTreeNode
{
    bool                    bVisibility;
    bool                    bUnsaved;
    bool                    bPinned;
    FString                 ItemLabel;
    FString                 Type;
    ActorTreeNode*          Parent;
    TArray<ActorTreeNode*>  Children;
		uint16				          IndexInParent = 0;
    uint32                  UUID;
    AActor*                 Actor;
    bool                    bIsSelected;

public:
	ActorTreeNode(FString InItemLabel, FString InType, ActorTreeNode* InParent, uint32 UUID, AActor* Actor)
		: bVisibility(true)
		, bUnsaved(false)
		, bPinned(false)
		, ItemLabel(std::move(InItemLabel))
		, Type(std::move(InType))
		, Parent(InParent)
		, IndexInParent(InParent ? static_cast<uint16>(Parent->Children.Len()) : 0)
		, UUID(UUID)
		, Actor(Actor)
	{
        if (Parent)
	        Parent->AddChild(this);
        Actor->GetComponents();
	}
	
	static void DisplayNode(ActorTreeNode* node, ImGuiSelectionBasicStorage* selection)
	{
    if (node->Actor && node->Actor->IsGizmoActor()) return;  // 기즈모는 표기 안함
    ImGui::TableNextRow();
    ImGui::TableSetColumnIndex(3);
    const bool is_folder = (!node->Children.IsEmpty());

    ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_SpanAllColumns | ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
    node_flags |= ImGuiTreeNodeFlags_NavLeftJumpsBackHere; // Enable pressing left to jump to parent
		if (!node->GetParent())
        node_flags |= ImGuiTreeNodeFlags_DefaultOpen;

    node->bIsSelected = selection->Contains(node->GetUUID());
    if (node->bIsSelected)
    {
        //       아래에 Pick() 함수를 삽입       //
        if (ImGui::IsWindowFocused() && UEngine::Get().GetObjectByUUID(node->UUID)->IsA<AActor>())   // 에디터 조작중 && Parent 존재 시 (a.k.a World가 아닌 경우)
					FEditorManager::Get().SelectComponent(node->Actor->GetRootComponent());
        node_flags |= ImGuiTreeNodeFlags_Selected;
    }
    else
    {
        node_flags &= ~ImGuiTreeNodeFlags_Selected;
    }

    // Using SetNextItemStorageID() to specify storage id, so we can easily peek into
		// the storage holding open/close stage, using our TreeNodeGetOpen/TreeNodeSetOpen() functions.
    ImGui::SetNextItemSelectionUserData((ImGuiSelectionUserData)(intptr_t)node);
    ImGui::SetNextItemStorageID(node->GetUUID());

    if (is_folder)
    {
        const char* itemlabel = node->ItemLabel.c_char();
        bool open = ImGui::TreeNodeEx(itemlabel, node_flags);
        ImGui::TableSetColumnIndex(0);
        ImGui::PushID(node);
        ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
        ImGui::Checkbox("##bVisible", &node->bVisibility);  //TODO: 체크박스는 네비게이션에서 제외할 것
        ImGui::PopStyleVar();
        ImGui::PopID();
        ImGui::TableSetColumnIndex(4);
        ImGui::TextUnformatted((node->Type).c_char());
        ImGui::TableNextColumn();
        ImGui::TextUnformatted(FString::FromInt(node->GetUUID()).c_char());
       	ImGui::TableNextColumn();
        if (node->Parent)
					ImGui::TextUnformatted(FString::FromInt(node->Parent->GetUUID()).c_char());

        if (open)
        {
					for (auto child : node->Children)
						DisplayNode(child, selection);
	        ImGui::TreePop();
        }
        else if (ImGui::IsItemToggledOpen())
        {
            TreeCloseAndUnselectChildNodes(node, selection);
        }
    }
		else // Display leaf node
    {
        const char* itemlabel = node->ItemLabel.c_char();
        ImGui::TreeNodeEx(itemlabel, node_flags | ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen);
        // 위와 동일 (if(open) 윗 line 까지
        ...
    }
}

작동 방식

public:
	TArray<ActorTreeNode*> ActorTreeNodes;
	ActorTreeNode* WorldNode;
void UEngine::InitWorld()
{
  World = FObjectFactory::ConstructObject<UWorld>();

	// Add ActorTreeNode to World->ActorTreeNodes //
  World->WorldNode = new ActorTreeNode(*World->GetName(), *World->GetClass()->Name, nullptr, World->GetUUID(), nullptr);
	World->ActorTreeNodes.Add(World->WorldNode);
	...
}
template <typename T>
	requires std::derived_from<T, AActor>
T* UWorld::SpawnActor()
{
	T* Actor = FObjectFactory::ConstructObject<T>();

	...

	// ActorTreeNode 생성 및 추가
	ActorTreeNode* NewNode = new ActorTreeNode(*Actor->GetName(), *Actor->GetClass()->Name, WorldNode, Actor->GetUUID(), Actor);
	ActorTreeNodes.Add(NewNode);
	
	return Actor;
}

이제부터 World에 SpawnActor를 사용해 Actor를 추가할 때 마다 SceneManager UI에서 확인 할 수 있게 되었다.

부록

ImGui와 관련된 더 많은 내용은 imgui_demo.cpp 를 참고 바람

TODO

MultiSelector 미구현, 추후 Pick로직이 확장되면 대응할 필요가 있음

자세한 내용은 ImGuiSelectionBasicStorage 를 참고 바람