Game UI with Nuklear: Part 1

Timur Gafarov
5 min readJan 11, 2022

--

User interface is an important aspect of any game, be it a simple main menu with a few buttons, or a complex multi-window monster with charts and formatted text. Properly managing UI widgets is not easy, especially when they should adapt to different screen resolutions — and, unfortunately, you can’t incorporate an HTML/CSS machinery into your game engine (unless you do everything on Electron and WebGL, of course).

There were countless attempts to make UI libraries for all major game engines. I’ve worked with many of them, and something always felt wrong — either results were too clumsy, or the API felt alien and inconvenient. In my experience, when it comes to gaming, nothing beats immediate mode UI, that has gained popularity thanks to modern GPU powers. What makes immediate mode toolkits especially cool is that they are usually abstractized from underlying rendering technology — you can write your own backend with the graphics API of your choise.

What is immediate mode UI?

For the most part, it means that UI widgets are composed just before they are drawn on the screen. The term relates to immediate mode graphics where client code makes draw calls rather than describes graphics using data structures. In immediate mode you just draw something each frame, in our case — UI widgets. If you are accustomed to HTML-style layouts, this can feel unusual at first. There is no state per se, toolkit doesn’t keep your data. State management is completely up to you. In the simplest case, you just keep a bunch of variables in your UI controller class, and pass them to widgets — when they are changed by external code, UI will react on the next rendering step. HTML world’s burden of DOM manipulation simply doesn’t exist here — immediate mode UI is reactive by design. This is one of its advantages: UI is strongly separated from the data.

But wait, here comes even more interesting part! Immediate mode UI library implements a draw queue — that is, an ordered list of primitives to be drawn so that the resulting image represents your widgets. The library does nothing beyond that. Client application should process this queue and make appropriate draw calls to the underlying graphics API to render primitives. This is great, because you decide how and when this should happen. All the data is available to you — you can even serialize it to a file.

Nuklear toolkit

Dagon uses Nuklear, a minimalistic immediate mode toolkit written in pure ANSI C and licensed under public domain. A true gem of a software! It doesn’t have dependencies, is fully skinnable and customizable, and supports UTF-8. Nuklear support in the engine is implemented as an optional extension, so it should be plugged in with `dagon:nuklear` DUB dependency.

Interaction with the library is done with NuklearGUI object that binds to an Entity. As everything else, this should be done in a class derived from Scene:

import dagon;
import dagon.ext.nuklear;
import dagon.ext.ftfont;
class UIScene: Scene
{
FontAsset font;
NuklearGUI ui;
Entity uiEntity;

override void beforeLoad()
{
font = this.addFontAsset("assets/fonts/DroidSans.ttf", 18);
}

override void afterLoad()
{
ui = New!NuklearGUI(eventManager, assetManager);
ui.addFont(font, 18, ui.localeGlyphRanges);

uiEntity = addEntityHUD();
uiEntity.drawable = ui;
uiEntity.visible = true;

eventManager.showCursor(true);
}

override void onUpdate(Time time)
{
if (uiEntity.visible)
{
ui.update(time);
layout();
}
}

void layout()
{
//...
}
}

Note that I used `ui.localeGlyphRanges` when adding the font. This means that Nuklear will load characters based on system locale (in addition to ASCII). Glyph range is just an array of `uint`, so instead of built-in `ui.localeGlyphRanges` you can use your own.

UI is entirely described in the `layout` method. For example, the following code renders a menu bar with a classic “File” menu:

void layout()
{
if (ui.begin("Menu",
NKRect(0, 0, eventManager.windowWidth, 40), 0))
{
ui.menubarBegin();
{
ui.layoutRowStatic(30, 40, 5);

if (ui.menuBeginLabel("File", NK_TEXT_LEFT,
NKVec2(200, 200)))
{
ui.layoutRowDynamic(25, 1);
if (ui.menuItemLabel("New", NK_TEXT_LEFT))
{ /* do something */ }
if (ui.menuItemLabel("Open", NK_TEXT_LEFT))
{ /* do something */ }
if (ui.menuItemLabel("Save", NK_TEXT_LEFT))
{ /* do something */ }
if (ui.menuItemLabel("Exit", NK_TEXT_LEFT))
{ application.exit(); }
ui.menuEnd();
}
}
ui.menubarEnd();
}
ui.end();
}

Layout can get pretty long, so it is feasible to break it into multiple methods — for example, one method per each widget.

In immediate mode, if-blocks are used to bind actions to UI events. If some widget returns true, then it is clicked. However, Nuklear doesn’t respond to user input right out of the box — this is something that you also have control over. As a bare minimum, you have to notify Nuklear about mouse button events:

override void onMouseButtonDown(int button)
{
bool unfocused = true;
if (uiEntity.visible)
{
ui.inputButtonDown(button);
unfocused = !ui.itemIsAnyActive();
}
view.active = unfocused;
}
override void onMouseButtonUp(int button)
{
bool unfocused = true;
if (uiEntity.visible)
{
ui.inputButtonUp(button);
unfocused = !ui.itemIsAnyActive();
}
view.active = unfocused;
}

Mouse interaction is a little complicated if you use it somewhere outside of Nuklear — for example, to control the 3D camera that orbits the scene. In this case, you have to check UI focus as shown above: if no widget is touched by this mouse event, then the view component can be activated, and vice versa.

Nuklear supports multi-window layouts. Let’s add a window with a text field:

if (ui.begin("Input", NKRect(100, 100, 230, 200),
NK_WINDOW_BORDER | NK_WINDOW_MOVABLE |
NK_WINDOW_TITLE | NK_WINDOW_SCALABLE))
{
static int len = 4;
static char[256] buffer = "test";
ui.layoutRowDynamic(35, 1);
ui.editString(NK_EDIT_FIELD, buffer.ptr, &len, 255, null);
}
ui.end();

In real application, of course, `buffer` should not be static variable in window’s scope block, it should be accessible by external code.

Text input won’t work until you add keyboard input:

override void onTextInput(dchar codePoint)
{
if (uiEntity.visible)
ui.inputUnicode(codePoint);
}

Nuklear is directly compatible with Dagon’s Unicode text input, which means that you can switch keyboard layout during the game and type non-Latin text. Keep in mind that the library can only output characters defined by the glyph range. Unfortunately, there is no dynamic/deferred glyph loading as for now.

You also have to add special actions for some keyboard events, such as backspace, delete and copy/paste combos, to make editing work as usual:

override void onKeyDown(int key)
{
if (uiEntity.visible)
{
if (key == KEY_BACKSPACE)
ui.inputKeyDown(NK_KEY_BACKSPACE);
else if (key == KEY_DELETE)
ui.inputKeyDown(NK_KEY_DEL);
else if (key == KEY_LEFT)
ui.inputKeyDown(NK_KEY_LEFT);
else if (key == KEY_RIGHT)
ui.inputKeyDown(NK_KEY_RIGHT);
else if (key == KEY_C && eventManager.keyPressed[KEY_LCTRL])
ui.inputKeyDown(NK_KEY_COPY);
else if (key == KEY_V && eventManager.keyPressed[KEY_LCTRL])
ui.inputKeyDown(NK_KEY_PASTE);
else if (key == KEY_A && eventManager.keyPressed[KEY_LCTRL])
ui.inputKeyDown(NK_KEY_TEXT_SELECT_ALL);
}
}

Copy/paste support (which is non-trivial taking UTF-8 into account) is provided by the extension, so you don’t have to worry about it.

These are just simple examples of the most basic functionality — Nuklear supports much more! In the second part of the tutorial I’ll show how to implement a basic editor to modify game properties.

--

--