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.