Still, a solid UI is a must, and we all waste far too much time doing things over and over again that we can do one time the right way, and never have to worry about again.
Check out the Library and follow along while you read!
DM UI: Lifecycle
We've got the process of creating huds down and building their children from templates. But what about getting stuff actually onto the player's screen?
That's where the ui manager comes in. We need a UI manager that helps make it easy to begin, update, and end the life of our huds. I have a rule with UIs in general, that UIs should talk to the client that owns them, rather than their siblings. This is similar to my traversal rules for components. So our UI manager needs to keep a handle on what's going on during its lifetime.
Second, we need to make certain that no behavior ever happens within a UI that isn't during one of a number of events:
1) In response to a player using an input command (mouse proc, keyboard macro, command line verb, or Topic() call).
2) During the UI update cycle.
In other words, our UI shouldn't really link directly into gameplay at all. What's going on in the game should drive the UI, but the gameplay scenario shouldn't need to know how UIs work in order for them to be kept up to date.
This helps us wall off gameplay and content from UIs, making their code easier to write and manage.
So long as we follow these rules, and set our game up to take advantage of them, we're on easy street.
Creation and destruction
The first step of getting a UI up and running is going to be getting objects into the screen. Let's build a quick UI manager.
client
var/tmp
ui/ui
proc
InitializeUI()
ui = new/ui(src)
Del()
ui?.client = null
ui = null
..()
ui
var/tmp
client/client
alist/hud = alist()
list/active_huds
New(client/client)
src.client = client
..()
proc
operator+=(B)
if(!islist(B))
B = list(B)
var/id
for(var/hud/adding in B)
//active huds MUST have a vis_id; if none is provided, make one up
if(!(id = adding.vis_id))
id = (adding.vis_id = "\ref[adding]")
//don't allow to huds with the same id to be active.
var/hud/old = hud[id]
if(old)
src -= old
hud[id] = adding
operator-=(B)
if(!islist(B))
B = list(B)
for(var/hud/removing in B)
if(active_huds[removing])
Hide(removing)
hud -= removing.vis_id
operator[](idx)
return hud[idx]
operator[]=(idx,B)
if(!istext(idx))
throw EXCEPTION("Invalid index: [idx]")
var/hud/adding = astype(B,/hud)
if(!adding && B)
throw EXCEPTION("Invalid value for list: [B]")
var/hud/old = hud[idx]
if(old)
if(old==adding) return
src -= old
adding.vis_id = idx
hud[idx] = adding
Show(hud/showing)
set waitfor = 0
if(!showing || active_huds[showing]) return
usr = client.mob
var/id
if(!(id = showing.vis_id) || hud[id]!=showing)
src += showing
if(showing.screen_loc)
client.screen += showing
else
showing.vis_flags &= ~VIS_HIDE
active_huds[showing] = 1
if(showing.tick_lag<1#INF && showing.next_tick==1#INF)
showing.next_tick = world.time //server.next_tick (change world.time to server.next_tick after the next section)
showing.Show()
Hide(hud/hiding)
set waitfor = 0
usr = client.mob
if(hiding && active_huds[hiding])
hiding.Hide()
if(hiding.screen_loc)
client.screen -= hiding
else
hiding.vis_flags |= VIS_HIDE
active_huds -= hiding
hiding.next_tick = 1#INF
Del()
client?.screen -= active_huds
..()
hud
var/tmp
next_tick = 1#INF
tick_lag = 1#INF
proc
Show()
Hide()
This system will allow you to add, remove, index, assign, and show and hide huds by their vis_id. You will need to call client.Initialize() somewhere in your project. Wherever the point is where the client should build its UI, that's where you will call Initialize(). It only needs to be called once, and all it does is set up the ui.
This approach is just like what we did with hud_objs in the first place. We're just making setting up and managing the life cycle of huds easy and intuitive. You want a hud to show up on the screen? You've got to create it, then client.ui.Show() it. You want to build a UI that will be shown at a later time? That's just client.ui += new/hud/whatever().
We did create a circular reference to the client here, but you'll also notice we're not passing that client reference or even a reference to the owning ui datum down to the huds. That's because we're going to make usr a valid way to figure out the owner of a hud. Anywhere that a hook gets called through ui, we're going to be changing usr in that call chain so that usr.client can be used to quickly get the owner of a hud.
Sure. usr in proc is bad, generally, but that's only because usr is unreliable when used in certain procs. We're forcing usr to be reliable, so there's no bad here. You only need to know what you already needed to know about usr: When it's valid, and what procs are verb-like. We've always said that usr is fine in mouse procs, and that's because mouse procs are given a usr when they are called. So why can't this be true for our own code? Why can't we adopt the pattern of forcing usr validity elsewhere? This simplifies the hell out of design, and helps us avoid having to do intensive reference cleaning throughout our code any time we want an object to be garbage collected.
The life cycle of a HUD now consists of being added to the ui's hud collection, shown, hidden, and eventually removed from a ui's hud collection. I left a little bit of code in there for actually allowing hud elements to have actions every N ticks, which we'll take a look at in a minute.
Ticking
We want to take advantage of UIs being given the option of having regular updates. We achieve this by looping over a client's active ui elements every few ticks and checking if that element has a pending tick update. Here's an example of how my gameloop is set up:
server
var/tmp
running = 0
next_tick = 0
New()
Run()
proc
Run()
set waitfor = 0
if(running) return
running = 1
Start()
while(running)
BeginTick()
sleep(world.tick_lag)
Stop()
Start()
set waitfor = 0
BeginTick()
set waitfor = 0
Tick()
set waitfor = 0
for(var/client/c)
if((usr = c.mob))
c.Tick()
usr = null
EndTick()
set waitfor = 0
for(var/client/c)
if((usr = c.mob))
c.EndTick()
usr = null
next_tick = world.time + world.tick_lag
Stop()
set waitfor = 0
var/server/server = new()
world/Tick()
server.Tick()
server.EndTick()
client/proc
Tick()
set waitfor = 0
EndTick()
set waitfor = 0
This system allows you to define a structured game loop and ticking cycle. I'd love to write an article covering this concept more in detail at a later date, but for the moment all you need to know is this:
When the world starts up, we create a datum called server. The server datum runs a tick loop in three places in your code:
1) At the beginning of a tick (BeginTick)
2) During a tick (Tick)
3) At the end of a tick (EndTick())
This allows us to control when portions of our code actually run during a tick. Having a structured game loop massively reduces the pain of writing a game, and helps you write simpler code. Even though technically it's less efficient than using the built-in scheduler, the code you wind up writing in the long run will balance that additional cost.
Another advantage here, is that you get the ability to know exactly how long each phase of a tick takes, so you can figure out exactly which part of your game's logic is causing your ticks to run into overtime, and you can more easily divide up the work your game is doing across multiple ticks to slim down intensive, game-stopping overrun cycles.
Let's harness this approach to build an update loop for your UI.
client/EndTick()
ui.Tick()
ui/proc
//called during client.EndTick()
Tick()
set waitfor = 0
var/time = world.time
for(var/hud/ticking in active)
if(ticking.next_tick<=time)
ticking.next_tick = time + (ticking.tick_lag || world.tick_lag)
ticking.Tick()
hud/proc
//called during ui.Tick()
Tick()
set waitfor = 0
And there you have it! You can now do stuff over time with a hud by adding code to ui.Tick(). In order for a hud to receive Tick() calls, it needs to be Show()ed to the ui first, and it needs to have a tick_lag value set, which is the amount of time (in deciseconds) between ticks. You can set this to 0 to match the world.tick_lag value.
But why do we have both a BeginTick(), a Tick(), and an EndTick() proc?
The reason that I set this up the way I did, is so that we can have more granular control of when things happen so that we can do less work checking for edge cases. BeginTick() is mostly designed to do world setup stuff at the beginning a tick. Cleaning up junk data from the last tick, and generally preparing things for the actual tick. This is designed to happen before the rest of the sleeping procs in the default scheduler wake up this tick. The regular server Tick() is supposed to be where you actually run your gameplay. All clients connected to a mob get a Tick() call during this phase. EndTick() also gives clients an EndTick() call. This is because clients typically can have many things happen to them while the turns are being processed in order for each player, enemy, projectile, etc. The EndTick() gives us an opportunity to update the UI ONE time, even if many actions have resulted in something that should change the information that is being shown to the player through the UI. This saves you processing power, and simplifies the code you need to write. Instead of updating the player's healthbar every single time they take damage, you just check to see if the health bar needs to be updated at the end of the tick, update the health bar if it does, and if not, you do nothing. Yeah, you spend a few cycles making that check, but you save all the work you'd be doing checking if the attacked mob has a client, looking for its health bar in the ui, and making a bunch of appearance changes to that ui that never wind up being seen by the player anyway, and let me tell you, as your world scales up, appearance mutation and animate() calls get expensive.
Now, let's imagine a scenario during a turn:
On Player A's turn, player A uses a skill that turns 20% of their health into mana (40 health).
Player A is currently on fire too. Player A takes 10 damage from the fire.
On Player B's turn, they use a melee attack against Player A, hitting them for 100 damage.
During the projectile phase, player A gets hit by an arrow that was shot a few seconds ago by player C. They take an additional 50 damage.
If we were to update the player's ui each time the player took damage, we'd be doing 4 ui updates for the player this frame.
So we'd need to do each of these instructions 4 times:
if(damaged.client)
damaged.client.health_bar.Update(health,max_health)
//in the Update function:
bar.pixel_w = -width + (health / max_health) * width
With a ticker approach, we're wasting a few cycles every frame checking if there's been a change, but our operation is pretty close to the same when it's been changed.
if(usr.health!=last_cur || usr.max_health!=last_max)
bar.pixel_w = -width + (last_cur = usr.health) / (last_max = usr.max_health) * width
The expensive part is really the appearance mutation. Everything else is pretty negligible. So by saving 3 appearance changes, we're speeding everything up, while requiring you to write less code. In the first example, the code for changing the user's health bar is gonna be literally everywhere in your project, and anywhere you modify the player's health without calling the healthbar update function is going to cause your UI to get out of sync and give the user bad information. The latter approach will never get out of sync, and all of the code for the healthbar stays defined under the health bar.
Making it even easier
Let's also make a system that allows us to define something analogous to our hud templates, so we can make it really easy to set up the user's UI. If you create a new UI element, the game should just know to put it into the screen when a client connects.
We're going to revisit prototype inspection at world startup:
var/alist/hud_scenes = init_hud_scenes()
proc/init_hud_scenes()
var/alist/categories = alist()
for(var/item in typesof(/hud))
var/category = initial(item:scene), id = initial(item:vis_id)
if(id && category)
(categories[category] ||= list()) += item
return categories
hud/var/tmp
scene
We've just added a variable to all top-level huds called scene. When a hud is given both a vis_id and a scene, it will be added to the global hud_scenes list. This will allow us to grab all huds that have a matching scene name quickly, and then create all of them, and add them to the user's UI.
Let's also add some functions for managing scenes within the UI.
ui
proc
AddScene(name,rename=name)
var/list/adding = list()
for(var/creating in global.hud_scenes[name])
var/elem = new creating
elem:scene = rename
adding += elem
src += adding
return adding
RemoveScene(name)
var/list/removing = list()
for(var/id, elem in hud)
if(elem:scene==name)
removing += elem
if(active_huds[elem])
Hide(elem)
src -= removing
return removing
ShowScene(name,exclusive=0)
var/list/showing = list()
for(var/id, elem in hud)
if(elem:scene==name)
showing += elem
Show(elem)
else if(exclusive && elem:scene)
Hide(elem)
if(!length(showing))
for(var/elem in (showing = AddScene(name)))
Show(elem)
return showing
HideScene(name)
var/list/hiding = list()
for(var/id, elem in hud)
if(elem:scene==name)
Hide(elem)
hiding += elem
return hiding
SetScene(name,rename=name)
var/list/removing = list(), list/showing = list()
for(var/id, elem in hud)
if(elem:scene==name)
showing += elem
Show(elem)
else if(elem:scene)
removing += elem
if(!length(showing))
for(var/elem in AddScene(name,rename))
Show(elem)
if(length(removing))
src -= removing
Now we have some functions for managing the state of the user's UI. We can define scenes for specific packages of huds. So for instance, if you have a title screen, you can spin up everything the title screen needs by putting all the huds that make up that title screen in the scene of "title", giving them all vis_ids, and then calling ShowScene("title") on the user's ui.
The exclusive argument for the ShowScene function will cause any hud that has a scene name to hide when we show the new scene.
SetScene does both adding, removal, and hiding. Any hud that has a scene name, but doesn't match the new scene will actually be hidden, AND removed, triggering destruction of that hud. When a hud doesn't have a scene name, it's assumed to live outside of the scene system. Still, having whole packages of uis that you can quickly spin up is super useful, so I've provided means of defining a scene, but renaming all of the members of that scene on the fly. This will allow you to build a "default" UI that is always present no matter what scene you are in, but still use the scene system to add the whole thing to the user's UI all at once. You'd simply give these defaults a scene id, and when you add them to the ui, you'd use the rename argument in either SetScene or AddScene to change the scene name to null.
The best part about this system, is once you set up your scene transitions, you don't need to add individual elements to the screen. You can just define the new hud, set its scene name, set its vis_id, and it'll get added to the user's ui automatically.
More
I've got a lot more to share on the subject of UI. I have been surviving off of contract work for the last year. I decided to commit myself to pixel art, code, and tutoring full time, rather than grind myself to the nub at a job I don't extract joy from. I love what I do, and I want to keep doing it, and producing great resources for all of you. However, the amount of time doing these writeups and researching these techniques, and proving out the concepts is higher than you might think. A system can look great when it's on paper, but once it hits the real world, all kinds of chaos can happen. That's why coming back with best practices is often so difficult; I have to take these experiments to their conclusion to figure out what works, and what doesn't, so I don't bring the lot of you with me for all the disasters I've explored on my own.
I'm currently working on some premium articles related to UI. This series will show you the way to build what I've built, but my premium articles will show you in detail how to construct general purpose UI widgets, how I built tools that speed up the creation of UI assets, how my asset structure schema cuts down the cost and time of UI development, and additional data structures and visual effects that I've discovered that make doing beautiful UIs in BYOND not just possible, but easy.
I've started a Ko-fi page. It's just a lil' guy at the moment, but it will soon be stuffed to the goiter with free and premium content. If you like what I do, please consider dropping a donation, subbing, or even purchasing a commission from my list of products and services.
Check out my Ko-fi

My premium articles will always be connected to one of my tutorials on this forum. So keep an eye out in the replies to my tutorials for any links to my ko-fi blog as I release additional content.
As always, BYOND members get special treatment. I offer discounts to my services for BYOND members and top donors.