使用 ImGui 开发编辑器和调试工具本页总览使用 ImGui 开发编辑器和调试工具 1. 引言 在游戏开发过程中,一个直观、高效的用户界面(UI)对于编辑器和调试工具至关重要。然而 Dora SSR 作为一个聚焦于代码的引擎,暂时没有提供内置的编辑器或是调试工具。因此,需要开发者根据自己游戏作品的需求,自行进行快速开发和定制。但是,Dora SSR 提供了 ImGui 库,可以极为方便地开发这类辅助 UI。 ImGui(Immediate Mode GUI)是一种即时模式的图形用户界面库,以其简洁、高效的特点被广泛应用。接下来本教程将介绍如何使用 Dora SSR 提供的 ImGui 库开发游戏编辑器或调试工具的 UI。 2. ImGui 框架的理念、优势和劣势 2.1 理念 ImGui 的核心理念是即时模式,这意味着 UI 是在每一帧都被重新绘制的。这与传统的保留模式(Retained Mode)不同,后者维护着一个 UI 状态树,而即时模式则是根据当前的程序状态直接绘制 UI。 2.2 优势 易于使用:无需管理复杂的 UI 状态,直接在代码中描述 UI 元素。 快速迭代:适合快速原型设计和调试工具的开发。 轻量级:无需整合大型的 UI 框架,减少了资源消耗。 高度灵活:可以轻松地嵌入到现有的游戏引擎渲染循环中。 2.3 劣势 不适合复杂的 UI:对于需要高度交互和复杂布局的应用,可能不太适用。 样式有限:默认的视觉风格较为简单,定制化需要额外的工作。可能无法满足游戏作品的视觉要求。 性能开销:在非常复杂的 UI 场景下,每帧重绘可能会带来性能问题。 4. 基本使用方法 4.1 创建一个简单的窗口 以下示例展示了如何创建一个简单的 ImGui 窗口: LuaTealTypeScriptYueScriptlocal ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")threadLoop(function() ImGui.Begin("示例窗口", function() ImGui.Text("欢迎使用 Dora SSR 的 ImGui!") ImGui.Separator() ImGui.TextWrapped("这是一个简单的示例窗口,展示了基本的文本和分隔线。") end)end)local ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")threadLoop(function(): boolean ImGui.Begin("示例窗口", function() ImGui.Text("欢迎使用 Dora SSR 的 ImGui!") ImGui.Separator() ImGui.TextWrapped("这是一个简单的示例窗口,展示了基本的文本和分隔线。") end) return falseend)import { threadLoop } from "Dora";import * as ImGui from "ImGui";threadLoop(() => { ImGui.Begin("示例窗口", () => { ImGui.Text("欢迎使用 Dora SSR 的 ImGui!"); ImGui.Separator(); ImGui.TextWrapped("这是一个简单的示例窗口,展示了基本的文本和分隔线。"); }); return false;});_ENV = Dora Dora.ImGuithreadLoop -> Begin "示例窗口", -> Text "欢迎使用 Dora SSR 的 ImGui!" Separator! TextWrapped "这是一个简单的示例窗口,展示了基本的文本和分隔线。" 说明: threadLoop 函数用于在主线程中循环执行操作。 ImGui.Begin 函数用于创建一个窗口,并指定窗口的标题。 ImGui.Text 函数用于绘制文本。 ImGui.Separator 函数用于绘制一个分隔线。 ImGui.TextWrapped 函数用于绘制一段带有自动换行的文本。 4.2 添加交互元素 您可以在窗口中添加按钮、输入框等交互元素: LuaTealTypeScriptYueScriptlocal ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Buffer <const> = require("Buffer")local inputText = Buffer(200)inputText.text = "默认文本"threadLoop(function() ImGui.Begin("交互示例", function() if ImGui.Button("点击我") then print("按钮被点击!") end if ImGui.InputText("输入框", inputText) then print("输入内容:" .. inputText.text) end end)end)local ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Buffer <const> = require("Buffer")local inputText = Buffer(200)inputText.text = "默认文本"threadLoop(function(): boolean ImGui.Begin("交互示例", function() if ImGui.Button("点击我") then print("按钮被点击!") end if ImGui.InputText("输入框", inputText) then print("输入内容:" .. inputText.text) end end) return falseend)import { threadLoop, Buffer } from "Dora";import * as ImGui from "ImGui";const inputText = Buffer(200);inputText.text = "默认文本";threadLoop(() => { ImGui.Begin("交互示例", () => { if (ImGui.Button("点击我")) { print("按钮被点击!"); } if (ImGui.InputText("输入框", inputText)) { print(`输入内容:${inputText.text}`); } }); return false;});_ENV = Dora Dora.ImGuiinputText = with Buffer 200 .text = "默认文本"threadLoop -> Begin "交互示例", -> if Button "点击我" print "按钮被点击!" if InputText "输入框", inputText print "输入内容:" .. inputText.text 说明: ImGui.Button 函数用于创建一个按钮,并指定按钮的标签。 ImGui.InputText 函数用 于创建一个输入框,并指定输入框的标签和缓冲区。 5. 创建游戏编辑器的使用示例 5.1 对象属性编辑器 对象属性编辑器是游戏编辑器中的核心组件,用于查看和修改游戏对象的属性。 LuaTealTypeScriptYueScriptlocal ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Buffer <const> = require("Buffer")local Vec2 <const> = require("Vec2")-- 假设我们有一个游戏对象,其属性如下local gameObject = { name = "Player", position = { x = 0.0, y = 0.0 }, rotation = 0.0, scale = { x = 1.0, y = 1.0 }, isActive = true}local nameBuffer = Buffer(100)nameBuffer.text = gameObject.namethreadLoop(function() ImGui.SetNextWindowSize(Vec2(300, 400), "FirstUseEver") ImGui.Begin("对象属性编辑器", function() -- 编辑对象名称 if ImGui.InputText("名称", nameBuffer) then gameObject.name = nameBuffer.text end -- 编辑位置 local changed, x, y = ImGui.InputFloat2("位置", gameObject.position.x, gameObject.position.y) if changed then gameObject.position.x = x gameObject.position.y = y end -- 编辑旋转 local changed, rotation = ImGui.DragFloat("旋转", gameObject.rotation, 1.0, 0.0, 360.0, "%.1f°") if changed then gameObject.rotation = rotation end -- 编辑缩放 local changed, sx, sy = ImGui.InputFloat2("缩放", gameObject.scale.x, gameObject.scale.y) if changed then gameObject.scale.x = sx gameObject.scale.y = sy end -- 编辑激活状态 local changed, isActive = ImGui.Checkbox("是否激活", gameObject.isActive) if changed then gameObject.isActive = isActive end -- 输出当前对象的状态 if ImGui.Button("输出状态") then print("当前对象状态:") p(gameObject) end end)end)local ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Buffer <const> = require("Buffer")local Vec2 <const> = require("Vec2")-- 假设我们有一个游戏对象,其属性如下local gameObject = { name = "Player", position = { x = 0.0, y = 0.0 }, rotation = 0.0, scale = { x = 1.0, y = 1.0 }, isActive = true}local nameBuffer = Buffer(100)nameBuffer.text = gameObject.namethreadLoop(function(): boolean ImGui.SetNextWindowSize(Vec2(300, 400), "FirstUseEver") ImGui.Begin("对象属性编辑器", function() -- 编辑对象名称 if ImGui.InputText("名称", nameBuffer) then gameObject.name = nameBuffer.text end -- 编辑位置 local changed, x, y = ImGui.InputFloat2("位置", gameObject.position.x, gameObject.position.y) if changed then gameObject.position.x = x gameObject.position.y = y end -- 编辑旋转 local changed, rotation = ImGui.DragFloat("旋转", gameObject.rotation, 1.0, 0.0, 360.0, "%.1f°") if changed then gameObject.rotation = rotation end -- 编辑缩放 local changed, sx, sy = ImGui.InputFloat2("缩放", gameObject.scale.x, gameObject.scale.y) if changed then gameObject.scale.x = sx gameObject.scale.y = sy end -- 编辑激活状态 local changed, isActive = ImGui.Checkbox("是否激活", gameObject.isActive) if changed then gameObject.isActive = isActive end -- 输出当前对象的状态 if ImGui.Button("输出状态") then print("当前对象状态:") p(gameObject) end end) return falseend)import { threadLoop, Buffer, Vec2 } from "Dora";import * as ImGui from "ImGui";import { SetCond } from "ImGui";const gameObject = { name: "Player", position: { x: 0.0, y: 0.0 }, rotation: 0.0, scale: { x: 1.0, y: 1.0 }, isActive: true}const nameBuffer = Buffer(100);nameBuffer.text = gameObject.name;threadLoop(() => { ImGui.SetNextWindowSize(Vec2(300, 400), SetCond.FirstUseEver); ImGui.Begin("对象属性编辑器", () => { // 编辑对象名称 if (ImGui.InputText("名称", nameBuffer)) { gameObject.name = nameBuffer.text; } // 编辑位置 { const [changed, x, y] = ImGui.InputFloat2("位置", gameObject.position.x, gameObject.position.y); if (changed) { gameObject.position.x = x; gameObject.position.y = y; } } // 编辑旋转 { const [changed, rotation] = ImGui.DragFloat("旋转", gameObject.rotation, 1.0, 0.0, 360.0, "%.1f°"); if (changed) { gameObject.rotation = rotation; } } // 编辑缩放 { const [changed, sx, sy] = ImGui.InputFloat2("缩放", gameObject.scale.x, gameObject.scale.y); if (changed) { gameObject.scale.x = sx; gameObject.scale.y = sy; } } // 编辑激活状态 { const [changed, isActive] = ImGui.Checkbox("是否激活", gameObject.isActive); if (changed) { gameObject.isActive = isActive; } } // 输出当前对象的状态 if (ImGui.Button("输出状态")) { print("当前对象状态:"); p(gameObject); } }); return false;});_ENV = Dora Dora.ImGuigameObject = name: "Player" position: x: 0.0, y: 0.0 rotation: 0.0, scale: x: 1.0, y: 1.0 isActive: truenameBuffer = with Buffer 100 .text = gameObject.namethreadLoop -> SetNextWindowSize Vec2(300, 400), "FirstUseEver" Begin "对象属性编辑器", -> -- 编辑对象名称 if InputText "名称", nameBuffer gameObject.name = nameBuffer.text -- 编辑位置 if changed, x, y := InputFloat2 "位置", gameObject.position.x, gameObject.position.y gameObject.position.x = x gameObject.position.y = y -- 编辑 旋转 if changed, rotation := DragFloat "旋转", gameObject.rotation, 1.0, 0.0, 360.0, "%.1f°" gameObject.rotation = rotation -- 编辑缩放 if changed, sx, sy := InputFloat2 "缩放", gameObject.scale.x, gameObject.scale.y gameObject.scale.x = sx gameObject.scale.y = sy -- 编辑激活状态 if changed, isActive := Checkbox "是否激活", gameObject.isActive gameObject.isActive = isActive -- 输出当前对象的状态 if Button "输出状态" print "当前对象状态:" p gameObject 说明: 使用 InputText 编辑字符串属性 。 使用 InputFloat2 和 DragFloat 编辑数值属性。 使用 Checkbox 编辑布尔值属性。 5.2 场景层级视图 场景层级视图用于显示场景中所有的游戏对象,以树形结构呈现。 LuaTealTypeScriptYueScriptlocal ImGui = require("ImGui")local threadLoop = require("threadLoop")-- 假设我们有一个场景对象列表,包含父子关系local sceneObjects = { { name = "Root", children = { { name = "Player", children = {} }, { name = "Enemy", children = { { name = "Enemy1", children = {} }, { name = "Enemy2", children = {} }, } }, } }}local leafFlags = {"Leaf"}local empty = function() end-- 递归函数,用于绘制场景树local function drawSceneTree(nodes) for _, node in ipairs(nodes) do if #node.children > 0 then ImGui.TreeNode(node.name, function() drawSceneTree(node.children) end) else ImGui.TreeNodeEx(node.name, node.name, leafFlags, empty) end endendthreadLoop(function() ImGui.Begin("场景层级视图", function() drawSceneTree(sceneObjects) end)end)local ImGui = require("ImGui")local threadLoop = require("threadLoop")local record Node name: string children: {Node}end-- 假设我们有一个场景对象列表,包含父子关系local sceneObjects: {Node} = { { name = "Root", children = { { name = "Player", children = {} }, { name = "Enemy", children = { { name = "Enemy1", children = {} }, { name = "Enemy2", children = {} }, } }, } }}local leafFlags = {"Leaf"}local empty = function() end-- 递归函数,用于绘制场景树local function drawSceneTree(nodes: {Node}) for _, node in ipairs(nodes) do if #node.children > 0 then ImGui.TreeNode(node.name, function() drawSceneTree(node.children) end) else ImGui.TreeNodeEx(node.name, node.name, leafFlags, empty) end endendthreadLoop(function(): boolean ImGui.Begin("场景层级视图", function() drawSceneTree(sceneObjects) end) return falseend)import * as ImGui from "ImGui";import { threadLoop } from "Dora";import { TreeNodeFlag } from "ImGui";interface Node { name: string; children: Node[];}// 假设我们有一个场景对象列表,包含父子关系const sceneObjects: Node[] = [ { name: "Root", children: [ { name: "Player", children: [] }, { name: "Enemy", children: [ { name: "Enemy1", children: [] }, { name: "Enemy2", children: [] } ] } ] }];const leafFlags = [TreeNodeFlag.Leaf];const empty = () => {};// 递归函数,用于绘制场景树function drawSceneTree(nodes: Node[]) { for (const node of nodes) { if (node.children.length > 0) { ImGui.TreeNode(node.name, () => { drawSceneTree(node.children); }); } else { ImGui.TreeNodeEx(node.name, node.name, leafFlags, empty); } }}threadLoop(() => { ImGui.Begin("场景层级视图", () => { drawSceneTree(sceneObjects); }); return false;});_ENV = Dora Dora.ImGui-- 假设我们有一个场景对象列表,包含父子关系sceneObjects = * name: "Root" children: * name: "Player" children: [] * name: "Enemy" children: * name: "Enemy1" children: [] * name: "Enemy2" children: []leafFlags = {"Leaf"}empty = ->-- 递归函数,用于绘制场景树drawSceneTree = (sceneObjects) -> for node in *sceneObjects if #node.children > 0 TreeNode node.name, -> drawSceneTree node.children else TreeNodeEx node.name, node.name, leafFlags, emptythreadLoop -> Begin "场景层级视图", -> drawSceneTree sceneObjects 说明: 使用 TreeNode 和 TreePop 创建树形结构。 递归地绘制每个节点和其子节点。 5.3 资源浏览器 资源浏览器用于查看和选择项目中的资源,例如纹理、模型和音频文件。 LuaTealTypeScriptYueScriptlocal ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")-- 资源列表local resources = { textures = { "texture1.png", "texture2.png", "texture3.png" }, models = { "model1.obj", "model2.obj" }, sounds = { "sound1.wav", "sound2.wav" }}threadLoop(function() ImGui.Begin("资源浏览器", function() if ImGui.CollapsingHeader("纹理") then for _, texture in ipairs(resources.textures) do if ImGui.Selectable(texture) then print("选中了纹理:" .. texture) end end end if ImGui.CollapsingHeader("模型") then for _, model in ipairs(resources.models) do if ImGui.Selectable(model) then print("选中了模型:" .. model) end end end if ImGui.CollapsingHeader("音频") then for _, sound in ipairs(resources.sounds) do if ImGui.Selectable(sound) then print("选中了音频:" .. sound) end end end end)end)local ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")-- 资源列表local resources = { textures = { "texture1.png", "texture2.png", "texture3.png" }, models = { "model1.obj", "model2.obj" }, sounds = { "sound1.wav", "sound2.wav" }}threadLoop(function(): boolean ImGui.Begin("资源浏览器", function() if ImGui.CollapsingHeader("纹理") then for _, texture in ipairs(resources.textures) do if ImGui.Selectable(texture) then print("选中了纹理:" .. texture) end end end if ImGui.CollapsingHeader("模型") then for _, model in ipairs(resources.models) do if ImGui.Selectable(model) then print("选中了模型:" .. model) end end end if ImGui.CollapsingHeader("音频") then for _, sound in ipairs(resources.sounds) do if ImGui.Selectable(sound) then print("选中了音频:" .. sound) end end end end) return falseend)import * as ImGui from "ImGui";import { threadLoop } from "Dora";// 资源列表const resources = { textures: ["texture1.png", "texture2.png", "texture3.png"], models: ["model1.obj", "model2.obj"], sounds: ["sound1.wav", "sound2.wav"]};threadLoop(() => { ImGui.Begin("资源浏览器", () => { if (ImGui.CollapsingHeader("纹理")) { for (const texture of resources.textures) { if (ImGui.Selectable(texture)) { print(`选中了纹理:${texture}`); } } } if (ImGui.CollapsingHeader("模型")) { for (const model of resources.models) { if (ImGui.Selectable(model)) { print(`选中了模型:${model}`); } } } if (ImGui.CollapsingHeader("音频")) { for (const sound of resources.sounds) { if (ImGui.Selectable(sound)) { print(`选中了音频:${sound}`); } } } }); return false;});_ENV = Dora Dora.ImGui-- 资源列表resources = textures: ["texture1.png", "texture2.png", "texture3.png"] models: ["model1.obj", "model2.obj"] sounds: ["sound1.wav", "sound2.wav"]threadLoop -> Begin "资源浏览器", -> if CollapsingHeader "纹理" for _, texture in ipairs resources.textures if Selectable texture print "选中了纹理:" .. texture if CollapsingHeader "模型" for _, model in ipairs resources.models if Selectable model print "选中了模型:" .. model if CollapsingHeader "音频" for _, sound in ipairs resources.sounds if Selectable sound print "选中了音频:" .. sound 说明: 使用 CollapsingHeader 分组展示资源类型。 使用 Selectable 列表项,允许用户选择资源。 5.4 材质编辑器 材质编辑器允许用户调整材质的属性,例如颜色、纹理和着色器参数。 LuaTealTypeScriptYueScriptlocal ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Buffer <const> = require("Buffer")local Color3 <const> = require("Color3")-- 材质对象local material = { name = "BasicMaterial", color = { r = 255, g = 255, b = 255 }, texture = "default.png", shininess = 32.0}-- 可用纹理列表local textures = { "default.png", "texture1.png", "texture2.png" }local currentTextureIndex = 1local nameBuffer = Buffer(100)nameBuffer.text = material.namethreadLoop(function() ImGui.Begin("材质编辑器", function() -- 编辑材质名称 if ImGui.InputText("名称", nameBuffer) then material.name = nameBuffer.text end -- 编辑颜色 local color = Color3(material.color.r, material.color.g, material.color.b) if ImGui.ColorEdit3("颜色", color) then material.color.r, material.color.g, material.color.b = color.r, color.g, color.b end local changed = false changed, currentTextureIndex = ImGui.Combo("纹理", currentTextureIndex, textures) if changed then material.texture = textures[currentTextureIndex] end -- 编辑光泽度 local changed, shininess = ImGui.DragFloat("光泽度", material.shininess, 1.0, 0.0, 128.0, "%.0f") if changed then material.shininess = shininess end -- 输出当前材质的状态 if ImGui.Button("输出状态") then print("当前材质状态:") p(material) end end)end)local ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Buffer <const> = require("Buffer")local Color3 <const> = require("Color3")-- 材质对象local material = { name = "BasicMaterial", color = { r = 255, g = 255, b = 255 }, texture = "default.png", shininess = 32.0}-- 可用纹理列表local textures = { "default.png", "texture1.png", "texture2.png" }local currentTextureIndex = 1local nameBuffer = Buffer(100)nameBuffer.text = material.namethreadLoop(function(): boolean ImGui.Begin("材质编辑器", function() -- 编辑材质名称 if ImGui.InputText("名称", nameBuffer) then material.name = nameBuffer.text end -- 编辑颜色 local color = Color3(material.color.r, material.color.g, material.color.b) if ImGui.ColorEdit3("颜色", color) then material.color.r, material.color.g, material.color.b = color.r, color.g, color.b end local changed = false changed, currentTextureIndex = ImGui.Combo("纹理", currentTextureIndex, textures) if changed then material.texture = textures[currentTextureIndex] end -- 编辑光泽度 local changed, shininess = ImGui.DragFloat("光泽度", material.shininess, 1.0, 0.0, 128.0, "%.0f") if changed then material.shininess = shininess end -- 输出当前材质的状态 if ImGui.Button("输出状态") then print("当前材质状态:") p(material) end end) return falseend)import * as ImGui from "ImGui";import { threadLoop, Buffer, Color3 } from "Dora";// 材质对象const material = { name: "BasicMaterial", color: { r: 255, g: 255, b: 255 }, texture: "default.png", shininess: 32.0};// 可用纹理列表const textures = ["default.png", "texture1.png", "texture2.png"];let currentTextureIndex = 1;const nameBuffer = Buffer(100);nameBuffer.text = material.name;threadLoop(() => { ImGui.Begin("材质编辑器", () => { // 编辑材质名称 if (ImGui.InputText("名称", nameBuffer)) { material.name = nameBuffer.text; } // 编辑颜色 const color = Color3(material.color.r, material.color.g, material.color.b); if (ImGui.ColorEdit3("颜色", color)) { material.color.r = color.r; material.color.g = color.g; material.color.b = color.b; } // 选择纹理 { let changed = false; [changed, currentTextureIndex] = ImGui.Combo("纹理", currentTextureIndex, textures) if (changed) { material.texture = textures[currentTextureIndex]; } } // 编辑光泽度 { const [changed, shininess] = ImGui.DragFloat("光泽度", material.shininess, 1.0, 0.0, 128.0, "%.0f") if (changed) { material.shininess = shininess; } } // 输出当前材质的状态 if (ImGui.Button("输出状态")) { print("当前材质状态:"); p(material); } }); return false;});_ENV = Dora Dora.ImGui-- 材质对象material = name: "BasicMaterial" color: { r: 255, g: 255, b: 255 } texture: "default.png" shininess: 32.0textures = ["default.png", "texture1.png", "texture2.png"]currentTextureIndex = 1nameBuffer = with Buffer 100 .text = material.namethreadLoop -> Begin "材质编辑器", -> if InputText "名称", nameBuffer material.name = nameBuffer.text -- 编辑颜色 color = Color3 material.color.r, material.color.g, material.color.b if ColorEdit3 "颜色", color material.color.r, material.color.g, material.color.b = color.r, color.g, color.b -- 选择纹理 changed, currentTextureIndex = Combo "纹理", currentTextureIndex, textures if changed material.texture = textures[currentTextureIndex] -- 编辑光泽度 changed, shininess = DragFloat "光泽度", material.shininess, 1.0, 0.0, 128.0, "%.0f" if changed material.shininess = shininess -- 输出当前材质的状态 if Button "输出状态" print "当前材质状态:" p material 说明: 使用 ColorEdit3 进行颜色选择。 使用 Combo 创建下拉菜单供用户选择 纹理。 使用 DragFloat 调整数值参数。 5.5 控制台窗口 实现一个简单的控制台窗口,用于输入命令和显示日志。 LuaTealTypeScriptYueScriptlocal ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Buffer <const> = require("Buffer")local Vec2 <const> = require("Vec2")local logs = {}local inputBuffer = Buffer(200)threadLoop(function() ImGui.SetNextWindowSize(Vec2(300, 200), "FirstUseEver") ImGui.Begin("控制台", function() -- 显示日志区域 ImGui.BeginChild("LogArea", Vec2(0, -25), function() for _, log in ipairs(logs) do ImGui.TextWrapped(log) end if ImGui.GetScrollY() >= ImGui.GetScrollMaxY() then ImGui.SetScrollHereY(1.0) end end) -- 输入区域 if ImGui.InputText("输入命令", inputBuffer, { "EnterReturnsTrue" }) then local command = inputBuffer.text table.insert(logs, "> " .. command) -- 执行命令(这里简单地回显) table.insert(logs, "执行结果:命令 [" .. command .. "] 已执行。") inputBuffer.text = "" end end)end)local ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Buffer <const> = require("Buffer")local Vec2 <const> = require("Vec2")local logs: {string} = {}local inputBuffer = Buffer(200)threadLoop(function(): boolean ImGui.SetNextWindowSize(Vec2(300, 200), "FirstUseEver") ImGui.Begin("控制台", function() -- 显示日志区域 ImGui.BeginChild("LogArea", Vec2(0, -25), function() for _, log in ipairs(logs) do ImGui.TextWrapped(log) end if ImGui.GetScrollY() >= ImGui.GetScrollMaxY() then ImGui.SetScrollHereY(1.0) end end) -- 输入区域 if ImGui.InputText("输入命令", inputBuffer, { "EnterReturnsTrue" }) then local command = inputBuffer.text table.insert(logs, "> " .. command) -- 执行命令(这里简单地回显) table.insert(logs, "执行结果:命令 [" .. command .. "] 已执行。") inputBuffer.text = "" end end) return falseend)import * as ImGui from "ImGui";import { threadLoop, Buffer, Vec2 } from "Dora";import { InputTextFlag, SetCond } from "ImGui";const logs: string[] = [];const inputBuffer = Buffer(200);threadLoop(() => { ImGui.SetNextWindowSize(Vec2(300, 200), SetCond.FirstUseEver); ImGui.Begin("控制台", () => { // 显示日志区域 ImGui.BeginChild("LogArea", Vec2(0, -25), () => { for (const log of logs) { ImGui.TextWrapped(log); } if (ImGui.GetScrollY() >= ImGui.GetScrollMaxY()) { ImGui.SetScrollHereY(1.0); } }); // 输入区域 if (ImGui.InputText("输入命令", inputBuffer, [InputTextFlag.EnterReturnsTrue])) { const command = inputBuffer.text; logs.push(`> ${command}`); logs.push(`执行结果:命令 [${command}] 已执行。`); inputBuffer.text = ""; } }); return false;});_ENV = Dora Dora.ImGui-- 日志列表logs = {}inputBuffer = Buffer 200threadLoop -> Begin "控制台", -> -- 显示日志区域 BeginChild "LogArea", Vec2(0, -25), -> for log in *logs TextWrapped log if GetScrollY! >= GetScrollMaxY! SetScrollHereY 1.0 -- 输入区域 if InputText "输入命令", inputBuffer, ["EnterReturnsTrue",] command = inputBuffer.text table.insert logs, "> " .. command table.insert logs, "执行结果:命令 [" .. command .. "] 已执行。" inputBuffer.text = "" 说明: 使用 BeginChild 创建日志显示区域。 使用 InputText 接受用户输入,并在按下回车时处理命令。 使用 SetScrollHereY 保持滚动条在底部。 5.6 状态栏和工具栏 在编辑器窗口中添加 状态栏和工具栏,提供常用功能的快捷入口。 LuaTealTypeScriptYueScriptlocal ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Vec2 <const> = require("Vec2")threadLoop(function() ImGui.Begin("编辑器主窗口", { "MenuBar", "AlwaysAutoResize" }, function() -- 工具栏 ImGui.BeginMenuBar(function() ImGui.BeginMenu("文件", function() if ImGui.MenuItem("新建") then print("新建文件") end if ImGui.MenuItem("保存") then print("保存文件") end end) ImGui.BeginMenu("编辑", function() if ImGui.MenuItem("撤销") then print("撤销操作") end end) end) -- 主内容区域 ImGui.Text("这里是主内容区域") ImGui.Dummy(Vec2(0, 100)) -- 状态栏 ImGui.BeginChild("StatusBar", Vec2(0, 20), function() ImGui.Text("状态:就绪") end) end)end)local ImGui <const> = require("ImGui")local threadLoop <const> = require("threadLoop")local Vec2 <const> = require("Vec2")threadLoop(function(): boolean ImGui.Begin("编辑器主窗口", { "MenuBar", "AlwaysAutoResize" }, function() -- 工具栏 ImGui.BeginMenuBar(function() ImGui.BeginMenu("文件", function() if ImGui.MenuItem("新建") then print("新建文件") end if ImGui.MenuItem("保存") then print("保存文件") end end) ImGui.BeginMenu("编辑", function() if ImGui.MenuItem("撤销") then print("撤销操作") end end) end) -- 主内容区域 ImGui.Text("这里是主内容区域") ImGui.Dummy(Vec2(0, 100)) -- 状态栏 ImGui.BeginChild("StatusBar", Vec2(0, 20), function() ImGui.Text("状态:就绪") end) end) return falseend)import * as ImGui from "ImGui";import { WindowFlag } from "ImGui";import { threadLoop, Vec2 } from "Dora";threadLoop(() => { ImGui.Begin("编辑器主窗口", [ WindowFlag.MenuBar, WindowFlag.AlwaysAutoResize ], () => { // 工具栏 ImGui.BeginMenuBar(() => { ImGui.BeginMenu("文件", () => { if (ImGui.MenuItem("新建")) { print("新建文件"); } if (ImGui.MenuItem("保存")) { print("保存文件"); } }); ImGui.BeginMenu("编辑", () => { if (ImGui.MenuItem("撤销")) { print("撤销操作"); } }); }); // 主内容区域 ImGui.Text("这里是主内容区域") ImGui.Dummy(Vec2(0, 100)) // 状态栏 ImGui.BeginChild("StatusBar", Vec2(0, 20), () => { ImGui.Text("状态:就绪") }) }); return false;});_ENV = Dora Dora.ImGuithreadLoop -> Begin "编辑器主窗口", ["MenuBar", "AlwaysAutoResize"], -> -- 工具栏 BeginMenuBar -> BeginMenu "文件", -> if MenuItem "新建" print "新建文件" if MenuItem "保存" print "保存文件" BeginMenu "编辑", -> if MenuItem "撤销" print "撤销操作" -- 主内容区域 Text "这里是主内容区域" Dummy Vec2 0, 100 -- 状态栏 BeginChild "StatusBar", Vec2(0, 20), -> Text "状态:就绪" 说明: 使用 BeginMenuBar 和 EndMenuBar 创建菜单栏或工具栏。 在主窗口中添加一个 BeginChild,用于模拟状态栏。 6. 优化技巧:提取匿名函数以减少内存分配 6.1 问题分析 在使用 ImGui 库进行开发时,每一帧可能会创建大量的匿名函数(闭包),这会导致频繁的内存分配和垃圾回收,进而影响性能。 6.2 解决方案 提取匿名函数:将匿名函数提取为局部函数,避免每帧创建新的函数对象。 6.3 优化 方法 6.3.1 将匿名函数提取为局部函数 示例: 优化前: LuaTealTypeScriptYueScriptthreadLoop(function() ImGui.Begin("示例窗口", function() ImGui.Text("这是一个示例窗口") end)end)threadLoop(function(): boolean ImGui.Begin("示例窗口", function() ImGui.Text("这是一个示例窗口") end) return falseend)threadLoop(() => { ImGui.Begin("示例窗口", () => { ImGui.Text("这是一个示例窗口"); }); return false;});threadLoop -> Begin "示例窗口", -> Text "这是一个示例窗口" 优化后: LuaTealTypeScriptYueScriptlocal function drawExampleWindow() ImGui.Text("这是一个示例窗口")endthreadLoop(function() ImGui.Begin("示例窗口", drawExampleWindow)end)local function drawExampleWindow() ImGui.Text("这是一个示例窗口")endthreadLoop(function() ImGui.Begin("示例窗口", drawExampleWindow)end)drawExampleWindow = () => { ImGui.Text("这是一个示例窗口");};threadLoop(() => { ImGui.Begin("示例窗口", drawExampleWindow); return false;});drawExampleWindow = -> Text "这是一个示例窗口"threadLoop -> Begin "示例窗口", drawExampleWindow 6.3.2 使用函数缓存机制 示例: 优化前: LuaTealTypeScriptYueScriptlocal objects = { { name = "Object1", id = 1 }, { name = "Object2", id = 2 }, { name = "Object3", id = 3 },}threadLoop(function() ImGui.Begin("对象列表", function() for i, obj in ipairs(objects) do ImGui.TreeNode(obj.name, function() ImGui.Text("对象 ID:" .. obj.id) end) end end)end)local objects = { { name = "Object1", id = 1 }, { name = "Object2", id = 2 }, { name = "Object3", id = 3 },}threadLoop(function(): boolean ImGui.Begin("对象列表", function() for i, obj in ipairs(objects) do ImGui.TreeNode(obj.name, function() ImGui.Text("对象 ID:" .. obj.id) end) end end) return falseend)const objects = [ { name: "Object1", id: 1 }, { name: "Object2", id: 2 }, { name: "Object3", id: 3 },];threadLoop(() => { ImGui.Begin("对象列表", () => { for (const obj of objects) { ImGui.TreeNode(obj.name, () => { ImGui.Text(`对象 ID:${obj.id}`); }); } }); return false;});objects = * name: "Object1", id: 1 * name: "Object2", id: 2 * name: "Object3", id: 3threadLoop -> Begin "对象列表", -> for obj in *objects TreeNode obj.name, -> Text "对象 ID:" .. obj.id 优化后: LuaTealTypeScriptYueScriptlocal objects = { { name = "Object1", id = 1 }, { name = "Object2", id = 2 }, { name = "Object3", id = 3 },}local function getTreeNodeFunction(obj) if not obj.nodeFunction then obj.nodeFunction = function() ImGui.Text("对象 ID:" .. obj.id) end end return obj.nodeFunctionendlocal function drawObjectList() for _, obj in ipairs(objects) do ImGui.TreeNode(obj.name, getTreeNodeFunction(obj)) endendthreadLoop(function(): boolean ImGui.Begin("对象列表", drawObjectList) return falseend)local record Object name: string id: integer nodeFunction: function()endlocal objects: {Object} = { { name = "Object1", id = 1 }, { name = "Object2", id = 2 }, { name = "Object3", id = 3 },}local function getTreeNodeFunction(obj: Object): function() if not obj.nodeFunction then obj.nodeFunction = function() ImGui.Text("对象 ID:" .. obj.id) end end return obj.nodeFunctionendlocal function drawObjectList() for _, obj in ipairs(objects) do ImGui.TreeNode(obj.name, getTreeNodeFunction(obj)) endendthreadLoop(function(): boolean ImGui.Begin("对象列表", drawObjectList) return falseend)interface Object { name: string; id: number; nodeFunction?: (this: void) => void;}const objects: Object[] = [ { name: "Object1", id: 1 }, { name: "Object2", id: 2 }, { name: "Object3", id: 3 },];const getTreeNodeFunction = (obj: Object): () => void => { if (!obj.nodeFunction) { obj.nodeFunction = () => { ImGui.Text(`对象 ID:${obj.id}`); }; } return obj.nodeFunction;};const drawObjectList = () => { for (const obj of objects) { ImGui.TreeNode(obj.name, getTreeNodeFunction(obj)); }};threadLoop(() => { ImGui.Begin("对象列表", drawObjectList); return false;});objects = * name: "Object1", id: 1 * name: "Object2", id: 2 * name: "Object3", id: 3getTreeNodeFunction = (obj) -> if not obj.nodeFunction obj.nodeFunction = -> ImGui.Text "对象 ID:" .. obj.id obj.nodeFunctiondrawObjectList = -> for obj in *objects TreeNode obj.name, getTreeNodeFunction objthreadLoop -> Begin "对象列表", drawObjectList 6.3.3 将复用的变量提取到闭包外 示例: 优化前: LuaTealTypeScriptYueScriptthreadLoop(function() ImGui.Begin("示例窗口", { "AlwaysAutoResize" }, function() ImGui.Text("这是一个示例窗口") end)end)threadLoop(function(): boolean ImGui.Begin("示例窗口", { "AlwaysAutoResize" }, function() ImGui.Text("这是一个示例窗口") end) return falseend)threadLoop(() => { ImGui.Begin("示例窗口", [ WindowFlag.AlwaysAutoResize ], () => { ImGui.Text("这是一个示例窗口"); }); return false;});threadLoop -> Begin "示例窗口", [ "AlwaysAutoResize" ], -> Text "这是一个示例窗口" 优化后: LuaTealTypeScriptYueScriptlocal windowFlags = { "AlwaysAutoResize" }local drawFunction = function() ImGui.Text("这是一个示例窗口")endthreadLoop(function() ImGui.Begin("示例窗口", windowFlags, drawFunction)end)local windowFlags = { "AlwaysAutoResize" }local drawFunction = function() ImGui.Text("这是一个示例窗口")endthreadLoop(function(): boolean ImGui.Begin("示例窗口", windowFlags, drawFunction) return falseend)const windowFlags = [ WindowFlag.AlwaysAutoResize ];const drawFunction = () => { ImGui.Text("这是一个示例窗口");};threadLoop(() => { ImGui.Begin("示例窗口", windowFlags, drawFunction); return false;});windowFlags = [ "AlwaysAutoResize" ]drawFunction = -> Text "这是一个示例窗口"threadLoop -> Begin "示例窗口", windowFlags, drawFunction 6.4 总结 通过将匿名函数提取到闭包的外层,您可以: 减少每帧的内存分配:避免频繁创建新的函数和对象,降低垃圾回收的压力。 提高性能:减少不必要的开销,使您的游戏编辑器运行得更流畅。 改善代码结构:将逻辑清晰地分离,提高代码的可读性和可维护性。 7. 开发建议 充分利用即时模式:由于 ImGui 是即时模式的,您可以根据实时的程序状态动态更新 UI。 注意性能:在复杂的 UI 中,尽量减少不必要的绘制,必要时可以使用条件判断来控制 UI 元素的更新。 组织代码结构:将重复使用的 UI 组件封装成函数,提升代码的可读性和可维护性。 监控性能:使用性能分析工具,监测内存分配和 CPU 占用,及时发现性能瓶颈。 代码审查:定期审查代码,寻找可能的优化点,避免不必要的资源浪费。 学习最佳实践:更多的 ImGui 使用方法,请参考官方文档和社区经验,学习并应用最佳的编码实践。 8. 结论 通过本教程,您应该对如何使用 Dora SSR 的 ImGui 库开发游戏编辑器或调试工具的 UI 有了全面的了解。ImGui 以其简洁、高效的特点,非常适合用于工具开发和快速原型设计。希望您能在实际项目中充分发挥其优势。