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();
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 를 참고 바람
MultiSelector 미구현, 추후 Pick로직이 확장되면 대응할 필요가 있음
자세한 내용은 ImGuiSelectionBasicStorage 를 참고 바람