Alkaline Thunder

An aspiring game developer.


Hosted on GitHub Pages — Theme by mattgraham

Recreating i3wm in Unreal Engine 4

Unreal Engine 4 is a pretty powerful game engine. Though, it’s not really used often for what I use it for. You’ll almost never see a pure UI-based hacking game in it. So, I have to experiment a lot with it on my own and recreate a lot of things from pure scratch with it. Most of the time I can’t use anything on the Marketplace or any existing code to help me. I have to be creative.

One of those times is recreating i3wm, a popular tiling window manager for Linux, inside UMG.

So, what is i3wm?

i3wm is exactly what I said it was. It’s a tiling window manager for Linux. Rather than what you’re probably used to with something like macOS, Windows, GNOME, KDE, etc (who all use either compositors or stacking WMs) where they allow you to drag windows freely around the screen and size them however you want, tiling WMs arrange windows in…well…tiles.

They’re very appealing to developers and hackers. They stay out of your way, they don’t take up many resources, and they’re very command-line oriented. It’s entirely possible to control a tiling window manager without using a mouse once.

Putting a tiling WM in Peacenet

When playing Hacknet and hackmud, I realized their UIs are very static. You don’t get a lot of control over how they’re layed out, which may be a good thing for some players but I like giving players the option to set things up in a way that works well for them. I also want to make the game look like a faux Linux distro while still keeping the Hollywood hacker aesthetic (monospace font, neon glowy text, bright colors on a dark background, fancy animations, etc.)

Initially I thought a stacking WM would do, but I found myself unable to really implement UIs related to gameplay. Some things I could do just fine, but others (such as objective timeouts in missions, tutorial prompts, etc) would either get in the way of your workspace or get covered by your program windows. Not good.

I thought maybe implementing something similar to Hacknet’s UI would work, but then we started to look like a Hacknet clone. Good for Hacknet, not good for us.

I thought maybe I could completely implement i3wm in-game, but then I started to run into similar issues that I did with the stacking WM while planning. Where do I place the mission prompt? What about objective countdowns? Tutorials? System status?

My solution

I decided on basically having my current Hacknet-style UI and i3wm have a baby. I’d start by removing the System Status panel and moving it to the top panel with your player name, world time, settings button, etc.

Then I’d remove the App Launcher in that top panel, replacing it with 4 workspace switcher buttons. Normally, i3wm has 10 workspaces you can switch between but I figured 4 would be enough for my game.

With the workspace switcher in place and fully working, I then added some keybinds to the game.

Before I knew it, I had one of i3wm’s core features implemented in my game. It felt so fluent, streamlined, yet powerful.

Creating the Tiler

i3wm has various different modes that it uses to tile windows. I decided on implementing the Split mode. Basically, you press a keybind/button and it switches between horizontal/vertical split. When you open a new window, it takes your active one, splits its space in half based on the current split direction, and puts the new program in that new space.

When you close a window, the other windows move into place to fill the empty space.

A problem came up…

I thought I could use a simple UMG Grid Panel for this. I quickly found that wasn’t going to work. I experimented with both the regular Grid Panel and the Uniform Grid Panel. They had their advantages but also some serious cons that basically stopped me from using them.

Grid panel issues

Uniform Grid Panel

My proposed solution

I decided instead that I’d use a bit of a hack. Unreal Engine has these things called Boxes. It has both vertical and horizontal ones.

When you place a child inside a Box, that Box lays it out horizontally/vertically depending on the Box’s direction. It’s used a lot in Peacenet to create lists, but I realized it’d work pretty well for a window tiler as well. Why?

Child fill values

A Box Child un UE4 has a “fill” value associated with it. It’s a percentage value indicating how much of the Box’s whitespace the child is allowed to consume. Set it to 0% and the child will only take as much as it needs. Set it to 100% and it’ll take everything it can get.

So if a Box has one child set to 100% fill, that child will take up all the space that the Box takes up. It’ll fill it. If the box has 2 childs with 100% fill, they’ll each take up half the space, so on, so forth. Hooray!

Nested Boxes

I use this a lot in-game for creating advanced-display buttons for things like hackable services, contacts, etc. Boxes can be nested. UE4 will HAPPILY let me place a Vertical Box in a Horizontal Box in a Vertical Box in a Vertical Box in a Horizontal Box and…you get the point.

I’ve got the pieces of an i3wm tiler.

Once again, I’ve found a way to MacGyver my way toward creating an i3wm tiler using only what UE4’s user interface toolkit offers me. Now it’s time to actually implement it.

Deciding how to split.

Let’s do some pseudocode. We’ll treat ActiveWindow as the player’s active window widget, and SplitMode as their split mode (vertical or horizontal.) NewWindow is the window we’re about to open.

// Get the parent so we know how the window wass split before.
Box Parent = ActiveWindow.GetParent() as Box;

if(SplitMode == vertical)
{
    if(Parent is VerticalBox)
    {
        // All we need to do is add the NewWindow to the Parent.
        Parent.AddChild(NewWindow);
    }
    else
    {
        // We're going to remove the active window from the parent.
        Parent.RemoveChild(ActiveWindow);

        // This Box will hold BOTH these windows, and it'll exist in the 
parent Box.
        VerticalBox NewBox = new VerticalBox();
        Parent.AddChild(NewBox);
        NewBox.AddChild(ActiveWindow);
        NewBox.AddChild(NewWindow);
    }
}
else
{
    if(Parent is HorizontalBox)
    {
        // Add the window to the parent.
        Parent.AddChild(NewWindow);
    }
    else
    {
        // Same as above with the vertical split but horizontal.
        Parent.RemoveChild(ActiveWindow);

        // Create the new box.
        HorizontalBox NewBox = new HorizontalBox();
        NewBox.AddChild(ActiveWindow);
        NewBox.AddChild(NewWindow);
        Parent.AddChild(NewBox);
    }
}

And that gets our splitting done when we open a new window!

What if we don’t have an active window though? Easy! We just look at the root Box of the tiler, and we split based on that.

if(SplitMode == "vertical")
{
    if(RootBox is VerticalBox)
    {
        RootBox.AddChild(NewWindow);
    }
    else
    {
        RootBox.RemoveFromParent();

        VerticalBox NewRoox = new VerticalBox();
        NewBox.AddChild(RootBox);
        NewBox.AddChild(NewWindow);
        Tiler.AddChild(NewBox);
    }
}
else
{
    if(RootBox is HorizontalBox)
    {
        RootBox.AddChild(NewWindow);
    }
    else
    {
        RootBox.RemoveFromParent();

        HorizontalBox NewBox = new HorizontalBox();
        NewBox.AddChild(RootBox);
        NewBox.AddChild(NewWindow);

        Tiler.AddChild(NewBox);
    }
}

So, basically, we check the root box if it matches our split mode. If it does, we just add the window to it. If it doesn’t, we remove the root box from our tiler, create a new box, add the old root to it, then add the new window, then the new box becomes the root box! Hooray!

But what about empty boxes?

Well, when a window opens or closes (or moves), we run this function on the root box’s children.

bool RemoveEmptyBoxes(Box MyBox)
{
    if(MyBox.ChildrenCount() == 0)
        return true;

    foreach(Box ChildBox in MyBox.Children)
    {
        if(RemoveEmptyBox(ChildBox))
        {
            MyBox.RemoveChild(ChildBox);
        }
    }

    return MyBox.ChildrenCount() == 0;
}

So, this is a recursive function. What that means is it will sometimes call itself recursively in order to get its job done. But what does it do? Let’s dissect it.

So, if we call this function on the Root Box, the Root Box will never be removed from the Tiler, but any empty boxes it has will be. Hooray! We just need to run this any time our window layout changes - a.k.a, a window opens, closes, gets moved to a different workspace, etc.

What we can do from here

We already have a pretty advanced window tiler at this point. It doesn’t take too many resources, but it’s very powerful. What else can we add?

Further optimizations

Another optimization we could do, since all windows would have a fill value of 100%, if a box (that isn’t the root) has only one child window, that box is removed from its parent, its window gets placed in its parent, and so on, until there are no child boxes that have only one window.

Conclusion

So, now we have a tiling window manager. And I’ve got a lot of coding to do tonight.