Lua for Game Development — Chapter 15: UI Systems, HUD, Menus & Data Binding
Leeting Yan
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.