ui and hud

🕹️ ebitengineer

UI, HUD & GUI Component Architecture Reference #

This module covers 9-slice panel rendering, flexbox anchoring math, generic status/progress indicators (e.g. health, stamina, fuel, charge level, timer, or score progress), and widget state machines for Ebitengine games.


1. 9-Slice Panel Scaling #

9-slice rendering scales UI dialogs and containers to any width/height without stretching or distorting corner pixels:

go
type NineSlice struct {
	Image                 *ebiten.Image
	CornerW, CornerH      int
	CenterW, CenterH      int
}

func (ns *NineSlice) Draw(screen *ebiten.Image, destX, destY, width, height float64) {
	cw, ch := float64(ns.CornerW), float64(ns.CornerH)
	sw := width - (cw * 2)
	sh := height - (ch * 2)

	op := &ebiten.DrawImageOptions{}

	// Draw 9 grid patches: 4 corners (unscaled), 4 borders (scaled 1D), 1 center (scaled 2D)
	patches := []struct {
		srcRect image.Rectangle
		dx, dy, dw, dh float64
	}{
		// Top-Left Corner
		{image.Rect(0, 0, ns.CornerW, ns.CornerH), destX, destY, cw, ch},
		// Top Border
		{image.Rect(ns.CornerW, 0, ns.CornerW+ns.CenterW, ns.CornerH), destX + cw, destY, sw, ch},
		// Top-Right Corner
		{image.Rect(ns.CornerW+ns.CenterW, 0, ns.CornerW*2+ns.CenterW, ns.CornerH), destX + cw + sw, destY, cw, ch},
		// Left Border
		{image.Rect(0, ns.CornerH, ns.CornerW, ns.CornerH+ns.CenterH), destX, destY + ch, cw, sh},
		// Center Fill
		{image.Rect(ns.CornerW, ns.CornerH, ns.CornerW+ns.CenterW, ns.CornerH+ns.CenterH), destX + cw, destY + ch, sw, sh},
		// Right Border
		{image.Rect(ns.CornerW+ns.CenterW, ns.CornerH, ns.CornerW*2+ns.CenterW, ns.CornerH+ns.CenterH), destX + cw + sw, destY + ch, cw, sh},
		// Bottom-Left Corner
		{image.Rect(0, ns.CornerH+ns.CenterH, ns.CornerW, ns.CornerH*2+ns.CenterH), destX, destY + ch + sh, cw, ch},
		// Bottom Border
		{image.Rect(ns.CornerW, ns.CornerH+ns.CenterH, ns.CornerW+ns.CenterW, ns.CornerH*2+ns.CenterH), destX + cw, destY + ch + sh, sw, ch},
		// Bottom-Right Corner
		{image.Rect(ns.CornerW+ns.CenterW, ns.CornerH+ns.CenterH, ns.CornerW*2+ns.CenterW, ns.CornerH*2+ns.CenterH), destX + cw + sw, destY + ch + sh, cw, ch},
	}

	for _, p := range patches {
		if p.dw <= 0 || p.dh <= 0 { continue }
		sub := ns.Image.SubImage(p.srcRect).(*ebiten.Image)
		op.GeoM.Reset()
		op.GeoM.Scale(p.dw/float64(p.srcRect.Dx()), p.dh/float64(p.srcRect.Dy()))
		op.GeoM.Translate(p.dx, p.dy)
		screen.DrawImage(sub, op)
	}
}

2. Anchoring & Layout Math #

Anchor UI elements relative to screen corners or virtual canvas edges:

go
type AnchorType int

const (
	AnchorTopLeft AnchorType = iota
	AnchorTopCenter
	AnchorTopRight
	AnchorCenter
	AnchorBottomLeft
	AnchorBottomRight
)

func GetAnchoredPosition(anchor AnchorType, screenW, screenH, elementW, elementH, offsetX, offsetY float64) (float64, float64) {
	switch anchor {
	case AnchorTopLeft:
		return offsetX, offsetY
	case AnchorTopCenter:
		return (screenW-elementW)/2 + offsetX, offsetY
	case AnchorTopRight:
		return screenW - elementW - offsetX, offsetY
	case AnchorCenter:
		return (screenW-elementW)/2 + offsetX, (screenH-elementH)/2 + offsetY
	case AnchorBottomLeft:
		return offsetX, screenH - elementH - offsetY
	case AnchorBottomRight:
		return screenW - elementW - offsetX, screenH - elementH - offsetY
	default:
		return offsetX, offsetY
	}
}

3. Generic Progress & Status Indicators #

Render smooth filled HUD bars (adaptable for health, stamina, boost fuel, charge timers, or progress meters) with border outlines and lerp animations:

go
type ProgressBar struct {
	X, Y, Width, Height float64
	CurrentVal, MaxVal  float64
	FillColor, BgColor  color.Color
}

func (pb *ProgressBar) Draw(screen *ebiten.Image) {
	// Background
	bgImg := ebiten.NewImage(int(pb.Width), int(pb.Height))
	bgImg.Fill(pb.BgColor)
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(pb.X, pb.Y)
	screen.DrawImage(bgImg, op)

	// Fill ratio
	ratio := pb.CurrentVal / pb.MaxVal
	if ratio < 0 { ratio = 0 }
	if ratio > 1 { ratio = 1 }

	fillW := pb.Width * ratio
	if fillW > 0 {
		fillImg := ebiten.NewImage(int(fillW), int(pb.Height))
		fillImg.Fill(pb.FillColor)
		screen.DrawImage(fillImg, op)
	}
}

4. Interactive Button Widget State Machine #

Manage Normal, Hover, Pressed, and Disabled states:

go
type ButtonState int

const (
	StateNormal ButtonState = iota
	StateHover
	StatePressed
	StateDisabled
)

type Button struct {
	Bounds Rect
	State  ButtonState
	Text   string
}

func (b *Button) Update(cursorX, cursorY float64, isClicked bool) bool {
	if b.State == StateDisabled {
		return false
	}

	isHovered := b.Bounds.X <= cursorX && cursorX <= b.Bounds.X+b.Bounds.Width &&
		b.Bounds.Y <= cursorY && cursorY <= b.Bounds.Y+b.Bounds.Height

	if isHovered {
		if isClicked {
			b.State = StatePressed
			return true // Action triggered!
		}
		b.State = StateHover
	} else {
		b.State = StateNormal
	}
	return false
}