【ZeloEngine】ImGui匯總


【ZeloEngine】ImGui匯總

需求

游戲內置菜單(In-Game Menus)

GM界面,這是一個程序編寫的界面

需求:

  • 程序化的,代碼驅動的界面
  • 無需美術拼UI,注重實用,不太在乎美觀
  • 跨平台
  • 入口,一鍵呼出,PC可以是快捷鍵,手機可以是一個可以移動的小按鈕,手柄可以是一個組合鍵

以下圖片來自《游戲引擎架構》

頑皮狗是自己定制的一個界面,屬於ImGui的子集

Main development menu
在這里插入圖片描述
Rendering submenu
在這里插入圖片描述
Mesh options subsubmenu
在這里插入圖片描述
Background meshes turned off
在這里插入圖片描述

使用Icon

前面雖然說實用優先,不需要表面功夫,不過話說回來

程序開發的界面,Icon是一個很好的優化,美觀,提供視覺引導

這種引導是潛移默化的,即使你不去看手冊,一個有Icon的編輯器用多了,自然會看Icon來識別出哪個功能在哪里

而沒有Icon的編輯器,則一直需要看文字來找位置
在這里插入圖片描述
Images and Icons for Visual Studio - Visual Studio | Microsoft Docs

Icon Download

本地化

編輯器本地化有標准方案,參考Godot

說實話,對於國內團隊,中文編輯器是剛需

大部分人的英文水平,仍然需要中文編輯器來達到最好的編輯效率

ImGui

如何開發/編程指南

  • ImGui基本原理
  • ImGui框架概念
  • ImGui接口
  • 腳本綁定接口
  • 腳本薄封裝框架
  • 參考項目

痛點

原理學完只是第一步,我們要鋪量去寫UI,那工具鏈要是完善的

目前抄了幾個界面后的痛點是:對接口不熟悉,查接口要跳很多文檔

理想的狀態是:Model數據結構=》設計粗略View展示Model=》轉換成代碼=》迭代交互和樣式

接口文檔

https://blog.csdn.net/zolo_mario/article/details/120359861?spm=1001.2014.3001.5501

https://blog.csdn.net/zolo_mario/article/details/120357560?spm=1001.2014.3001.5501

https://blog.csdn.net/zolo_mario/article/details/120359935?spm=1001.2014.3001.5501

Doc/Editor/ImGui/**

imgui的文檔維護比較糟糕,接口文檔都在代碼里,所以我整理了一份文檔

腳本綁定接口

原文檔

imgui_patch.lua

文檔導出的樁文件,基本可用,按需要改即可

腳本綁定方案和接口,可以多看看幾個方案,但是持續維護目前的就夠了

主要是由於C和Lua的差異,腳本接口是有微小差異的,所以需要維護一套

ImGui Demo 自解釋

ImGui本身有很多參數,ImGui本身又是一個參數編輯器框架,所以Demo基本就是自己編輯自己

UI框架

基本架構

ImGui(C++) => sol wrapper => ImGui(Lua) => ImGui Framework

框架主要是做一個薄封裝和分類,便於開發

薄封裝

ImGui接口本身都是全局函數,有兩個問題:

  • 沒有做分類,量很大,從開發者角度有一些冗余
    • 同樣功能不同接口二選一,Column和Table
    • 實際用不到的
    • 過時的接口
    • beta接口
  • 腳本綁定后Lua接口和C接口的差異
  • ImGui更新(docking分支目前仍然不是主分支),后向兼容性的風險

分類

  • panel
  • widget
  • layout
  • plugin

腳本綁定減少重載

既然腳本層封裝了,那么綁定層就不要提供太多重載,影響性能

以MenuItem為例,下面這樣寫太復雜了,封裝成一個接口就可以了

bool MenuItem ( const  char * label, const  char *shortcut = NULL , bool selected = false , bool enabled = true );  
激活時返回真。

bool MenuItem ( const  char * label, const  char * shortcut, bool * p_selected, bool enabled = true );    
激活時返回 true + toggle (*p_selected) 如果 p_selected != NULL
inline bool MenuItem(const std::string &label) { return ImGui::MenuItem(label.c_str()); }

inline bool MenuItem(const std::string &label, const std::string &shortcut) {
    return ImGui::MenuItem(label.c_str(), shortcut.c_str());
}

inline std::tuple<bool, bool> MenuItem(const std::string &label, bool selected) {
    bool activated = ImGui::MenuItem(label.c_str(), nullptr, &selected);
    return std::make_tuple(selected, activated);
}

inline std::tuple<bool, bool> MenuItem(const std::string &label, const std::string &shortcut, bool selected) {
    bool activated = ImGui::MenuItem(label.c_str(), shortcut.c_str(), &selected);
    return std::make_tuple(selected, activated);
}

inline std::tuple<bool, bool> MenuItem(const std::string &label, const std::string &shortcut, bool selected,
                                       bool enabled) {
    bool activated = ImGui::MenuItem(label.c_str(), shortcut.c_str(), &selected, enabled);
    return std::make_tuple(selected, activated);
}
--- Parameters A: text (label), text (shortcut) [0]
--- Parameters B: text (label), text (shortcut), bool (selected)
--- Parameters C: text (label), bool (selected)
--- Returns A: bool (activated)
--- returns B: bool (selected), bool (activated)
--- Overloads
--- activated = ImGui.MenuItem("Label")
--- activated = ImGui.MenuItem("Label", "ALT+F4")
--- selected, activated = ImGui.MenuItem("Label", selected)
--- selected, activated = ImGui.MenuItem("Label", "ALT+F4", selected)
--- selected, activated = ImGui.MenuItem("Label", "ALT+F4", selected, true)
--- ```
function ImGui.MenuItem(label, shortcut, selected) end

本地化

略,沒空

UI圖片

ImGui接受GL的紋理ID

Docking

https://github.com/ocornut/imgui/issues/2109

  • 把多個窗口合並到一個窗口的分頁
  • 把窗口吸附到窗口邊上
  • 多視口,是指可以把imgui窗口拖出Windows窗口獨立存在
    • 這個目前僅在Windows上測試通過
    • 這個功能對代碼的改動比較大

接口

啟用Docking

ImGuiManager.EnableDocking

PanelWindow啟動Docking

local DefaultPanelWindowSettings = {
    dockable = true;
}

撤銷機制 Undo/Redo

https://github.com/ocornut/imgui/issues/1875

Lua腳本兼容性

指針參數

指針參數被額外用作返回值,也就是in+out

lua肯定是沒有這種用法,對應改成額外返回值即可

也就是說,原來一個變量,要改成傳入參數,再傳出結果

lua變量open傳入C時,C API會解包做拷貝傳參,成為局部變量,對lua的open是沒有影響的

p_open還額外攜帶bool信息,lua沒有可空類型,一般還是額外傳參數

bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags)

window->HasCloseButton = (p_open != NULL);

函數重載

C函數重載,基本靠動態解析lua傳來的參數來匹配

有兩個問題,腳本重構火葬場,和解析性能

首先有一個痛點,就是IDE,因為lua IDE是沒法輔助重載的,基本靠文檔和人腦記憶

大批量寫代碼時,跑到才會報錯,非常蛋疼

然后就是重載的越多,解析越復雜

所以應該限制重載

以BeginChild為例

腳本綁定之后沒有額外開銷,最后都是調用一個函數

這里重載是為了默認參數,其實文檔用最長的即可

腳本接口

-- ImGui.BeginChild(...)
-- Parameters: text (name), float (size_x) [O], float (size_y) [O], ImGuiWindowFlags (flags) [O]
-- Returns: bool (shouldDraw)
-- Overloads
shouldDraw = ImGui.BeginChild("Name", 100)
shouldDraw = ImGui.BeginChild("Name", 100)
shouldDraw = ImGui.BeginChild("Name", 100, 200)
shouldDraw = ImGui.BeginChild("Name", 100, 200, true)
shouldDraw = ImGui.BeginChild("Name", 100, 200, true, ImGuiWindowFlags.NoMove)

-- ImGui.EndChild()
ImGui.EndChild()

腳本綁定

bool BeginChild(const std::string &name) { return ImGui::BeginChild(name.c_str()); }

bool BeginChild(const std::string &name, float sizeX) { return ImGui::BeginChild(name.c_str(), {sizeX, 0}); }

bool BeginChild(const std::string &name, float sizeX, float sizeY) {
    return ImGui::BeginChild(name.c_str(), {sizeX, sizeY});
}

bool BeginChild(const std::string &name, float sizeX, float sizeY, bool border) {
    return ImGui::BeginChild(name.c_str(), {sizeX, sizeY}, border);
}

bool BeginChild(const std::string &name, float sizeX, float sizeY, bool border, int flags) {
    return ImGui::BeginChild(name.c_str(), {sizeX, sizeY}, border, static_cast<ImGuiWindowFlags>(flags));
}

ImGui.set_function("BeginChild", sol::overload(
sol::resolve<bool(const std::string &)>(BeginChild),
sol::resolve<bool(const std::string &, float)>(BeginChild),
sol::resolve<bool(const std::string &, float, float)>(BeginChild),
sol::resolve<bool(const std::string &, float, float, bool)>(BeginChild),
sol::resolve<bool(const std::string &, float, float, bool, int)>(BeginChild)
));

調到的內核接口

bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0);

控件ID

為什么控件交互沒有反應?

https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#q-why-is-my-widget-not-reacting-when-i-click-on-it

ImGui隱式維護ID,控件樹中的控件路徑被hash來標識一個控件

每個控件的接口一般都有label,是該控件的ID

所以傳空串就會導致無法交互,解決是用##XXX來標識,這些在顯示時被忽略

因為復雜的控件其實維護了狀態,比如樹節點,有開關狀態,所以在幀之間需要標識,這個控件調用就是這個控件

ID

  • ID可以是字符串(label參數),索引,或者指針
  • 索引指針用於標識列表控件中的item
  • PushID/PopID用於手工構造作用域,來解決沖突
  • 因為label還承擔顯示名字的作用,用##XXX來解決ID沖突

用指針做ID的例子,不過lua腳本沒法這么用,可以用##i來標識

int i;
bool node_open = ImGui.TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Node %d", i);
// ID堆棧/范圍
//閱讀 FAQ(docs/FAQ.md 或 http://dearimgui.org/faq)以了解有關如何在 Dear imgui 中處理 ID 的更多詳細信息。
// - 通過對 ID 堆棧系統的理解來回答和影響這些問題:
// - “問:為什么我的小部件在我點擊時沒有反應?”
// - “問:我怎樣才能擁有帶有空標簽的小部件?”
// - “問:我怎樣才能擁有多個具有相同標簽的小部件?”
// - 簡短版本:ID 是整個 ID 堆棧的哈希值。如果您在循環中創建小部件,您很可能
// 想要推送一個唯一標識符(例如對象指針、循環索引)來唯一區分它們。
// - 您還可以在小部件標簽中使用“標簽##foobar”語法來區分它們。
// - 在這個頭文件中,我們使用“標簽”/“名稱”術語來表示將顯示的字符串 + 用作 ID,
// 而 "str_id" 表示僅用作 ID 且不正常顯示的字符串。
void           PushID ( const  char * str_id);                                     //將字符串推入 ID 堆棧(將哈希字符串)。
void           PushID ( const  char * str_id_begin, const  char * str_id_end);       //將字符串推入 ID 堆棧(將哈希字符串)。
void           PushID ( const  void * ptr_id);                                     //將指針推入 ID 堆棧(將散列指針)。
void           PushID ( int int_id);                                             //將整數推入 ID 堆棧(將散列整數)。
void           PopID ();                                                        //從 ID 堆棧中彈出。
ImGuiID        GetID ( const  char * str_id);                                      //計算唯一 ID(整個 ID 堆棧的哈希值 + 給定參數)。例如,如果您想自己查詢 ImGuiStorage
ImGuiID        GetID ( const  char * str_id_begin, const  char * str_id_end);
ImGuiID        GetID ( const  void * ptr_id);

Cpp Trick in ImGui

避免大函數,拆分成小函數

避免大函數,拆分成小函數,因為鏈接大函數的時間的復雜度是非線性的

ImGui的調用風格

  • 返回bool,給if去觸發邏輯
  • 額外的返回值通過指針參數傳出
  • pStatus承擔了至多三種功能,輸入和返回值,NULL時還是禁用
  • 相對復雜的控件是帶狀態的,有控件ID,輸入參數會被緩存在控件狀態中
bool MenuItem(const char * label, const char *shortcut, bool *p_selected, bool enabled=true);

變長參數

TextXXX文本控件有兩個版本,比如Text和TextV

兩個都是vararg,一個是接口,一個是內部實現

…用va_start和va_end可以收集到一個va_list中

void ImGui::Text(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    TextV(fmt, args);
    va_end(args);
}

void ImGui::TextV(const char* fmt, va_list args)
{
    ImGuiWindow* window = GetCurrentWindow();
    if (window->SkipItems)
        return;

    ImGuiContext& g = *GImGui;
    const char* text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args);
    TextEx(g.TempBuffer, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
}

常見問題

無法獲取鍵盤輸入

https://github.com/ocornut/imgui/issues/2608

io.AddInputCharacter()

Character和Key的概念和接口是分開的

字體要支持中文,否則只能用英文輸入法

AddFontFromMemoryTTF的內存問題

Assertion failed: font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found.",
file imgui_draw.cpp, line 2117

錯誤信息並沒有什么

找到出錯的接口,從內存加載ttf字體

IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_size, float size_pixels);

出錯的原因在於這個接口的NOTE,大意是所有權移交ImFontAtlas,ttf的內存會被delete掉

NB: Transfer ownership of ‘ttf_data’ to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false.
Owned TTF buffer will be deleted after Build().

類似的加載資源的第三方庫接口其實都要注意這點,比如stbimage加載紋理,imgui加載字體,要注意資源的內存是自己還是庫來管理
否則要么是釋放了兩次,運行時報錯,要么是內存泄漏

這里要拷貝一份資源內存交給imgui去使用

對應ZeloEngine的Resource有兩個接口read和readCopy,這里使用后者


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM