User Interface (UI)

In the context of games, a user interface is a group of visual components that is usually overlaid on the game world. This conveys information to the player, usually about the state of the current level and also allows for player input.

These components include text, buttons, sliders, inputs and toggles amongst others. The screenshot below showcases the UI from a game created with the engine, Industrio.

Industrio User Interface User Interface Example (Industrio)

Getting Started

In GameKit, the user interface (UI) is created and managed by the Scene class. To create a UI for your scene, you override the createUI method as shown below:

import dev.gamekit.core.Application;
import dev.gamekit.core.Input;
import dev.gamekit.core.Scene;
import dev.gamekit.ui.enums.Alignment;
import dev.gamekit.ui.enums.CrossAxisAlignment;
import dev.gamekit.ui.widgets.*;

public class UserInterfaceSample extends Scene {
  private String titleText = "Hello GameKit";

  public UserInterfaceSample() {
    super("UI Scene");
  }

  public static void main(String[] args) {
    Application game = new Application("User Interface") { };
    game.loadScene(new UserInterfaceSample());
    game.run();
  }

  @Override
  protected Widget createUI() {
    return Center.create(
      Column.create(
        Column.config().crossAxisAlignment(CrossAxisAlignment.STRETCH),
        Text.create(
          Text.config().fontSize(72).alignment(Alignment.CENTER),
          titleText
        ),
        Text.create(
          Text.config().fontSize(24).alignment(Alignment.CENTER),
          "User Interface Sample"
        )
      )
    );
  }
}

Here, the root of the UI is a Center widget which, well, centers its child. Within the Center widget, we create a Column widget which stretches its children horizontally and contains two Text widgets. The resulting UI looks like this:

User Interface User Interface Sample
User Interface User Interface Sample (Showing bounds)

Widgets

Widgets are the building blocks of UI in GameKit. Formally speaking, a widget is an abstract representation of a portion of a scene's user interface.

Widgets are used to describe all aspects of a user interface, including physical aspects such as text and buttons to layout effects like padding and alignment.

Widgets in GameKit take heavy inspiration from Flutter's Box Constraint model which I highly recommend checking out for a more grounded understanding of how to create widgets in GameKit.

Widget Catalog

GameKit ships with several widgets which you can use to create most kinds of layout. These are grouped into two types: parent widgets and leaf widgets.

Parent Widgets

As their name implies, these are widgets which nest one or more widgets within them. Parents are generally used for layout and don't have an appearance themselves.

The table below lists and describes all parent widget provided by GameKit.

Widget Type Description
Row MultiChildParent Lays out its children in a horizontal axis
Column MultiChildParent Lays out its children in a vertical axis
Stack MultiChildParent Lays out its children on top of each other
Scaled SingleChildParent Scales its child by a given factor
Padding SingleChildParent Adds internal spacing around its child
Align SingleChildParent Aligns its child within itself
Sized SingleChildParent Constrains its child to a fixed or proportional size
Theme SingleChildParent Sets common theme values for its descendants
Panel SingleChildParent Renders a background behind its child using the 9-patch algorithm
Opacity SingleChildParent Renders its descendants with transparency
Button SingleChildParent Triggers an action when clicked, while rendering its child as-is
Checkbox SingleChildParent Renders a checkbox to the left of its child
Compose SingleChildParent A base for creating custom widget trees

Leaf Widgets

Leaf widgets are standalone widgets which render a distinct appearance.

The table below lists and describes all leaf widget provided by GameKit.

Widget Description
Text Renders text with specific properties
Field A Text widget extension which accepts input
Image Renders an image
Progress Renders a configurable progress bar
Slider A Progress widget extension which updates a value by moving a slider knob
Colored Renders a solid color background
Empty A no-size component which renders nothing. It's is the widget equivalent of null

Widget Instantiation

Widget instances are created by using the static create method on the widget's class. The widget can accept a configuration object, a child widget/list or both, as shown below:

// Stack widget needs no configuration, so it just accepts the list of children
Stack.create(
  Text.create("Hello"),
  Text.create("World"),
);

// Columns can accept a list of children and/or a configuration object
Column.create(
  Text.create("Hello"),
  Text.create("World"),
)

Column.create(
  Column.config(),
  Text.create("Hello"),
  Text.create("World"),
)

The configuration object can also be created by calling the static config() method on the widget, then chaining property method calls on it:

Padding.create(
  Padding.config().padding(24, 0, 0, 24), // Top, Right, Bottom, Left
  Column.create(
    Column.config().gapSize(24).crossAxisAlignment(CrossAxisAlignment.CENTER),
    Text.create("Hello"),
    Text.create("World"),
  )
)

UI Updates

UI is rarely static. There comes a point when you need to update variables whose values are used in them. In most game engines, you'd have to get a reference to a UI component instance and calling methods on it.

As an example, here's a screenshot from a previous game project created with the Godot engine. The task here was to update the text field of a UI label node.

You'd notice on line 12, that we grab a references to the $CanvasLayer/LandedLabel node, then on line 44, we update the text string.

Godot Imperative UI Update Godot Imperative UI Update


This approach to updating UI is known makes the UI system an imperative one. With imperative UI, you have to explicitly specify how updates need be performed.

GameKit, like the Flutter framework referenced above, uses another approach in its UI system known as a declarative approach.

With the declarative approach, you specify what the final state of the UI should be, then have the engine compute the steps needed to take the UI from its current state to your desired state.

One core difference is that imperative UI focuses on the component/widget instances, while declarative UI focuses on the state of the UI.

GameKit UI Updates

To put into practice, what we discussed above, let's have a simple layout of a button and text. When the button is pressed, we increase a counter variable which reflects in the text widget.

The state of the UI is the collection of variables and constants which the UI depends on and here, the state of the UI comprises the counter variable.

import dev.gamekit.core.Application;
import dev.gamekit.core.Renderer;
import dev.gamekit.core.Scene;
import dev.gamekit.settings.Resolution;
import dev.gamekit.settings.Settings;
import dev.gamekit.settings.TextAntialiasing;
import dev.gamekit.settings.WindowMode;
import dev.gamekit.ui.enums.Alignment;
import dev.gamekit.ui.enums.CrossAxisAlignment;
import dev.gamekit.ui.events.MouseEvent;
import dev.gamekit.ui.widgets.*;
import dev.gamekit.ui.widgets.Button;

import java.awt.*;

public class UiUpdateExample extends Scene {
  private int counter = 0;

  public UiUpdateExample() {
    super("Playground");
  }

  public static void main(String[] args) {
    Application game = new Application(
      new Settings(
        "Playground",
        WindowMode.WINDOWED,
        Resolution.HD,
        TextAntialiasing.ON
      )
    ) { };
    game.loadScene(new UiUpdateExample());
    game.run();
  }

  @Override
  protected void render() {
    Renderer.clear(Color.BLACK);
  }

  @Override
  protected Widget createUI() {
    return Center.create(
      Column.create(
        Column.config().crossAxisAlignment(CrossAxisAlignment.CENTER).gapSize(24),
        Text.create(
          Text.config().fontSize(72).alignment(Alignment.CENTER),
          String.format("Counter: %d", counter)
        ),
        Sized.create(
          Sized.config().width(128).height(64),
          Button.create(
            Button.config().mouseListener(this::incrementCounter),
            Text.create("Click Me")
          )
        )
      )
    );
  }

  private void incrementCounter(MouseEvent event) {
    if (event.type == MouseEvent.Type.CLICK) {
      counter++;
      updateUI();
    }
  }
}

Our focus in the incrementCounter method in the code above. Here we increment the counter by 1, essentially changing the state of our UI. Anytime you change any UI state, you need to call the updateUI() method of the scene class to reflect the change.

This is all there is to it on updating UI in GameKit. No keeping track of UI instances, no method calls on UI instances. Just change your state and call updateUI().

Running this example results in the application showcased below: