【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
本地化
編輯器本地化有標准方案,參考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,這里使用后者