Over the last few months I've been building a
node-based scene editor
for game projects and also as an excuse to spend some quality time with
Dear ImGui
.
Most of the
game projects I've worked on
are built with Java using
libGDX
, and this node editor work is motivated in part because I've repeatedly bounced off of the default ui layout system built into libGDX:
Scene2d.ui
. It works quite well for relatively simple user interfaces but every time I think I have a grip on it, I run into some layout issue that
devolves into poking at layout code randomly and with increasing desperation trying to get it to cooperate.
Dark Souls - I want you, to git gud!
Based on some sophisticated user interfaces built with the Scene2d toolkit; such as
Spine
and
Talos VFX
, this is clearly a đ
skill issue
and I should just stop whining and git gud. That said, it's possible that I'm a closet masochist and secretly enjoy being frustrated by
the "I have no idea what I'm doing" stage of learning new things, because instead of reading the docs and source code, and experimenting
until I solidified my understanding, I took this struggle with Scene2d as a sign that I should make my life even harder by learning not
just a different UI library, but one that requires Java bindings to work in my accustomed language and framework!
Since Dear ImGui and imgui-node-editor are c++ libraries, the first step was to pick one of the
Java bindings libraries
, and the two most likely picks were:
Both already include the imgui-node-editor extension in the bindings, and while
gdx-imgui
is more directly tied into libGDX it is also a bit less mature than
imgui-java
and I ran into a couple issues with how the node editor extension is set up, so I went with
imgui-java
. If you just need vanilla imgui support in a libGDX application then
gdx-imgui
may be a better choice.
There is some platform-specific code that can't live directly in the libGDX
core
module, so I created an interface that can be implemented in both the
lwjgl3
and
core
modules:
The implementation of this interface for desktop platforms in the
lwjgl3
module has to be created on app launch, and I pass it into the game's
ApplicationAdapter
class (called
Main
in this snippet) where it can be used by the platform-agnostic implementation:
// in lwjgl3/Lwjgl3Launcher.java...
private static Lwjgl3Application createApplication() {
var imguiDesktop = new ImGuiDesktop();
var configuration = getDefaultConfiguration();
return new Lwjgl3Application(new Main(imguiDesktop), configuration);
}
And the
lwjgl3
implementation looks roughly like this:
public class ImGuiDesktop implements ImGuiPlatform {
public long window;
public ImGuiImplGlfw imGuiGlfw;
public ImGuiImplGl3 imGuiGl3;
public final Map<String, ImFont> fontMap = new HashMap<>();
@Override
public void init() {
if (Gdx.graphics instanceof Lwjgl3Graphics lwjgl3Graphics) {
imGuiGlfw = new ImGuiImplGlfw();
imGuiGl3 = new ImGuiImplGl3();
window = lwjgl3Graphics.getWindow().getWindowHandle();
if (window == 0) {
throw new GdxRuntimeException("Failed to create the GLFW window");
}
ImGui.createContext();
ImGui.getIO().setIniFilename(null);
initDocking();
initFonts();
imGuiGlfw.init(window, true);
imGuiGl3.init("#version 150");
} else {
throw new GdxRuntimeException("This ImGui platform requires Lwjgl3");
}
}
@Override
public void startFrame() {
imGuiGl3.newFrame();
imGuiGlfw.newFrame();
}
@Override
public void endFrame() {
imGuiGl3.renderDrawData(ImGui.getDrawData());
}
@Override
public void dispose() {
imGuiGl3.shutdown();
imGuiGl3 = null;
imGuiGlfw.shutdown();
imGuiGlfw = null;
}
@Override
public ImFont getFont(String name) {
return fontMap.get(name);
}
private void initDocking() {
// NOTE: skipped in this example, docking support configured here if needed
}
private void initFonts() {
// NOTE: skipped in this example, but I'll come back to fonts later
}
}
Then the platform-agnostic implementation can call into the desktop implementation as needed:
public class ImGuiCore implements ImGuiPlatform {
private final ImGuiPlatform platform;
private InputProcessor imGuiInputProcessor;
public ImGuiCore(ImGuiPlatform platform) {
this.platform = platform;
this.imGuiInputProcessor = null;
}
// NOTE: call this in the main ApplicationAdapter's 'create()' lifecycle method
@Override
public void init() {
platform.init();
}
// NOTE: call this in the main ApplicationAdapter's 'dispose()' lifecycle method
@Override
public void dispose() {
platform.dispose();
ImGui.destroyContext();
}
@Override
public void startFrame() {
// restore the input processor after ImGui caught all inputs
if (imGuiInputProcessor != null) {
Gdx.input.setInputProcessor(imGuiInputProcessor);
imGuiInputProcessor = null;
}
platform.startFrame();
ImGui.newFrame();
}
@Override
public void endFrame() {
ImGui.render();
platform.endFrame();
// if ImGui wants to capture the input, disable libGDX's input processor
if (ImGui.getIO().getWantCaptureKeyboard() || ImGui.getIO().getWantCaptureMouse()) {
imGuiInputProcessor = Gdx.input.getInputProcessor();
Gdx.input.setInputProcessor(null);
}
}
@Override
public ImFont getFont(String name) {
return platform.getFont(name);
}
}
Finally, in a game screen where you want to use imgui, setup the
render()
lifecycle method like this:
@Override
public void render(SpriteBatch batch) {
ScreenUtils.clear(backgroundColor);
imgui.startFrame();
ImGui.pushStyleVar(ImGuiStyleVar.WindowRounding, 10f);
// update within the imgui frame to make sure the imgui input processor is active
imguiScene.update();
// imgui window fills entire screen, layout of panes and widgets is handled internally in the scene
ImGui.setNextWindowPos(0, 0, ImGuiCond.Always);
ImGui.setNextWindowSize(ImGui.getMainViewport().getSize());
imguiScene.render();
ImGui.popStyleVar();
imgui.endFrame();
}
The one quirk of
imgui-node-editor
that has had the most substantial impact on the way I planned to build the editor is that any widgets that rely on the
imgui
child window API just don't work when embedded in a node.
Unfortunately this impacts some foundational widget types:
Using any of these widgets in a node requires some fiddly workarounds if it's even possible at all. For example, I don't think the
Table
API is usable in a node or at least I haven't figured out how to make it work yet.
The main way to work around this limitation is to 'fake' the widget type in the node context; i.e. between
NodeEditor.begin/endNode()
, and then use it 'normally' outside that context, typically by triggering a popup on a click within the node context, and then displaying
the popup after
NodeEditor.endNode()
, often in a
NodeEditor.suspend/resume()
pair.
This is a little vague so let's look at a concrete example for the
combo
widget...
Node sizing can be challenging, mostly because the nodes are set up to expand to fit their content. What this means in practice is that I
have to really fight to get a node layout just right.
// add these utility methods wherever it's appropriate, like the main 'Node' class
public void beginColumn() {
ImGui.beginGroup();
}
public void nextColumn() {
ImGui.endGroup();
ImGui.sameLine();
ImGui.beginGroup();
}
public void endColumn() {
ImGui.endGroup();
}
// usage between a 'NodeEditor.begin/endNode()' pair
public void renderNodeContents() {
ImGui.setNextItemWidth(columnSize1);
beginColumn();
// render widgets in first column
ImGui.setNextItemWidth(columnSize2);
nextColumn();
// render widgets in second column
endColumn();
}
Since I've spent a fair amount of time on this, I plan to make a standalone node editor available on github. There are several
architectural things that I have yet to work out about how to make it generally useful for any game rather than the one I'm currently
working on. I'll update this post with a link to the repo once I've started work on it.
Additionally, since there are some fundamental incompatibilities between
imgui-node-editor
and some of the core widgets provided by
imgui
, I've been considering setting up my own library using bindings created with the new
Java Foreign Functions and Memory API
similar to what I did with
SDL3
as a learning project:
github - jsdl3
. This would allow me to integrate some of the workarounds that folks have put together that haven't been merged into
imgui-node-editor
itself for various reasons so that people could use more (if not all) of the
imgui
widgets directly rather than having to implement workarounds.