UI is the backbone of:
- inventories
- crafting
- equipment menus
- quests & story
- shops
- minimaps
- HUD overlays
- dialogue
- pause menu
- game settings
Lua is excellent for UI logic because:
- UI widgets map naturally to tables
- easy event dispatch
- hot-reload speeds iteration
- data binding = reactive UI
- timeline-based animations work perfectly
This chapter builds a modern, scalable UI framework in Lua.
1. UI Widget System
UI widgets are components:
- panels
- labels
- buttons
- images/icons
- windows
- scroll lists
- tooltips
- HUD overlays
We define a generic widget base class.
1.1 Base Widget
local Widget = {}
Widget.__index = Widget
function Widget.new(props)
return setmetatable({
x = props.x or 0,
y = props.y or 0,
w = props.w or 100,
h = props.h or 50,
visible = props.visible ~= false,
children = {},
}, Widget)
end
function Widget:add(child)
table.insert(self.children, child)
end
function Widget:update(dt)
for _, c in ipairs(self.children) do
if c.update then c:update(dt) end
end
end
function Widget:draw()
if not self.visible then return end
for _, c in ipairs(self.children) do
if c.draw then c:draw() end
end
end
return Widget
Widgets can be nested to create complex UI.
2. UI Specific Widgets
We build common widgets:
- Label
- Button
- Image
- Panel
- Window
2.1 Label
local Label = {}
Label.__index = Label
function Label.new(text, props)
local self = setmetatable(Widget.new(props), Label)
self.text = text
return self
end
function Label:draw()
if not self.visible then return end
draw_text(self.text, self.x, self.y)
end
return Label
2.2 Button
local Button = {}
Button.__index = Button
function Button.new(text, props)
local self = setmetatable(Widget.new(props), Button)
self.text = text
self.on_click = props.on_click
return self
end
function Button:draw()
draw_rect(self.x,self.y,self.w,self.h,0.2,0.2,0.2,1)
draw_text(self.text, self.x+10, self.y+10)
end
function Button:handle_input(mx,my)
if mx>self.x and mx<self.x+self.w and my>self.y and my<self.y+self.h then
if self.on_click then self.on_click() end
end
end
return Button
3. UI Manager
Manages:
- input targeting
- window stacking
- dragging
- focus
- modal dialogs
3.1 UI Manager Base
local UIManager = {windows={}, focused=nil}
function UIManager:add(win)
table.insert(self.windows, win)
end
function UIManager:update(dt)
for _, w in ipairs(self.windows) do w:update(dt) end
end
function UIManager:draw()
for _, w in ipairs(self.windows) do w:draw() end
end
3.2 Mouse Input Handling
function UIManager:mouse_pressed(mx,my)
for i=#self.windows,1,-1 do
local w = self.windows[i]
if w:hit(mx,my) then
self.focused = w
if w.mouse_pressed then
w:mouse_pressed(mx,my)
end
return true
end
end
return false
end
Windows stack top-to-bottom.
4. UI Windows (Inventory / Settings / Maps)
Window widget:
local Window = {}
Window.__index = Window
function Window.new(props)
local self = setmetatable(Widget.new(props), Window)
self.title = props.title or "Window"
return self
end
function Window:draw()
if not self.visible then return end
draw_rect(self.x,self.y,self.w,self.h, 0.1,0.1,0.1,0.9)
draw_text(self.title, self.x+10, self.y+10)
Widget.draw(self)
end
function Window:hit(mx,my)
return mx>self.x and mx<self.x+self.w and my>self.y and my<self.y+self.h
end
return Window
5. Data Binding (Reactive UI)
Reactive UI updates automatically based on game state.
Lua makes it easy:
5.1 Bindable Value
local Bindable = {}
Bindable.__index = Bindable
function Bindable.new(value)
return setmetatable({value=value, listeners={}}, Bindable)
end
function Bindable:set(v)
self.value = v
for _, f in ipairs(self.listeners) do f(v) end
end
function Bindable:on_change(func)
table.insert(self.listeners, func)
end
return Bindable
5.2 Example: Health Bar Bound to Player HP
player.hp_bind:on_change(function(value)
ui_healthbar.value = value / player.max_hp
end)
Now HP updates โ bar automatically animates.
6. HUD Elements
HUD includes:
- health bar
- stamina bar
- XP bar
- boss HP bar
- minimap
- objective UI
- weapon cooldowns
- buff/debuff icons
6.1 Health Bar Widget
local HealthBar = {}
HealthBar.__index = HealthBar
function HealthBar.new()
return setmetatable({value=1}, HealthBar)
end
function HealthBar:draw()
draw_rect(20,20,200,20, 0.1,0.0,0.0,1)
draw_rect(20,20,200*self.value,20, 1,0,0,1)
end
return HealthBar
Update from HP binding.
7. Inventory UI (Integrated from Chapter 11)
Example grid inventory:
function InventoryUI:draw()
for i=1,self.inv.size do
local slot = self.inv.slots[i]
local x = self.x + ((i-1)%5)*70
local y = self.y + math.floor((i-1)/5)*70
draw_rect(x,y,64,64, 0.2,0.2,0.2,1)
if slot then
draw_item_icon(slot.id, x,y)
draw_text(slot.count, x+48, y+48)
end
end
end
8. Dialogue UI (From Chapter 12)
Dialogue interface:
function DialogueUI:draw()
draw_rect(50,400,700,120,0,0,0,0.7)
draw_text(self.text, 60,410)
for i, r in ipairs(self.responses) do
draw_text(i .. ". " .. r.text, 60,440 + i*20)
end
end
9. Animated UI Transitions (Tween)
Timeline-based UI animations:
9.1 Tween Utility
function tween(value, target, duration, update)
local t = 0
timeline:add(function()
while t < duration do
local dt = coroutine.yield()
t = t + dt
local p = t/duration
update(value + (target-value)*p)
end
end)
end
9.2 Example: Fade In Window
window.opacity = 0
tween(0,1,0.4,function(v) window.opacity = v end)
9.3 Example: Slide Menu
tween(menu.x, -300, 0.3, function(v)
menu.x = v
end)
10. UI Input Mapping
Input routed through UI first, then gameplay.
10.1 Input Priority
if UIManager:mouse_pressed(mx,my) then
return -- UI consumed input
end
player:handle_input()
11. Pause Menu
Pause menu is just another window with logic:
pause_menu = Window.new({x=200,y=100,w=400,h=300,title="Paused"})
pause_menu:add(Button.new("Resume", {x=220,y=160,w=360,h=40, on_click=resume_game}))
pause_menu:add(Button.new("Quit", {x=220,y=220,w=360,h=40, on_click=quit_game}))
12. Settings Menu (Dynamic Data Binding)
settings.volume_bind:on_change(function(v)
Audio:set_volume(v)
end)
Slider widget emits changes and updates audio immediately.
13. Putting It All Together โ Actual UI Loop
function game:update(dt)
UIManager:update(dt) -- update UI
HUD:update(dt) -- update HUD
player:update(dt) -- gameplay
world:update(dt)
end
function game:draw()
world:draw()
player:draw()
HUD:draw()
UIManager:draw() -- always on top
end
14. Summary of Chapter 15
You now understand:
- Widget system architecture
- Windows, panels, labels, buttons
- UI manager (input, focus, stacks)
- HUD elements
- Data binding (reactive UI)
- Animated UI transitions
- Inventory & dialogue UI
- Pause menus, settings, and modal dialogs
- Input coordination between UI and gameplay
Luaโs table-based structure and coroutine system greatly simplify UI development.
Keep Reading
Follow the engineering thread
Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.