A menu and state system

This post was published in 2019 and is kept here for reference. It may contain information that's outdated or inaccurate.

I’ve been playing with state systems lately, and decided to build a menu system to play with going between each state.

First of all, my helper functions… I’m using this to print outlined text:

-- print text with an outline
function printo(str,x,y,c1,c2)
 for xoff=-1,1 do
  for yoff=-1,1 do
    print(str,x+xoff,y+yoff,c1)
  end
 end
 print(str,x,y,c2)
end

The menu function has two modes, one for running in _update() and one for running in _draw(), you control this by passing "u" or "d" as the second argument.

The first agument takes a table containing a menu definition. I’ll talk more about this with an example after the wall of code:

function menu(m, mode)
 local ic=#m.items
 if mode == "u" then
  if btnp(2) then
   -- up
   m.selection -= 1
   if m.selection < 1 then
    m.selection = 1
   end
  end
  if btnp(3) then
   -- down
   m.selection += 1
   if m.selection > ic then
    m.selection = ic
   end
  end
  if btnp(5) then
   -- x
   if m.items[m.selection][2] then
    m.items[m.selection][2]()
   end
  end
 end
 if mode == "d" then
  local padding={4,4,0,6}-- t/r/b/l
  local lineheight=8
  -- find longest string
  local widest_str=0
  for i,item in pairs(m.items) do
   if #item[1] > widest_str then
    widest_str = #item[1]
   end
  end
  local width = (widest_str*4)+padding[2]+padding[4]
  local height = (ic*lineheight)+padding[1]+padding[3]
  local xoff = 64 - (width/2)
  local yoff = 64 - (height/2)
  local x0 = xoff
  local y0 = yoff
  local x1 = xoff + width
  local y1 = yoff + height
  -- box
  rect(x0-2,y0+1,x1+2,y1+2, 5)
  rect(x0-1,y0-1,x1+1,y1+1,0)
  rectfill(x0,y0,x1,y1,1)
  rect(x0,y0,x1,y1,7)
  -- aditional text
  if m.guide then
   local _w = #m.guide*4
   local _x = 64 - (_w/2)
   local _y = y1+padding[1]+2
   printo(m.guide,_x,_y, 0, 7)
  end
  -- items
  for i,item in pairs(m.items) do
   if i == m.selection then
    -- selection highlight
    printo(
     item[1],
     xoff+padding[4],
     yoff+padding[1]+((i-1)*lineheight),
     0,7
    )
    if m.items[m.selection][2] then
     palt(0,false)
     palt(1,true)
     local xadj=(5 - time()%3)
     spr(1,x0-xadj,y0+padding[1]+((i-1)*lineheight) - 1)
     pal()
    end
   else
    -- normal item
    printo(
     item[1],
     xoff+padding[4],
     yoff+padding[1]+((i-1)*lineheight),
     0,6
    )
   end
  end
 end
end

There’s a lot going on there, but it’s basically maintain a counter for the user’s current selection, and then if there is a function associated with a menu item call it when the x button is pressed.

Here’s how the menu definitions look

_menu_start = {
  selection = 1,
  items = {
   {"new game", game},
   {"new game", game},
   {"new game", game},
   {"new game", game},
   {"credits", credits}
  },
  guide="❎ to select"
}

_menu_game = {
  selection = 1,
  items = {
   {"game over", die},
   {"congratulations", win},
   {"---"},
   {"back", start}
  }
}

_menu_credits = {
  selection = 1,
  items = {
   {"this sub menu contains lots of"},
   {"strings without actions bound "},
   {"to them, so you can just show "},
   {"multiple lines of text."},
   {"30 characters is a rough limit"},
   {"---"},
   {"back", start}
  },
  guide="enjoy ♥"
}

You can go between menus using a state system. Here’s the barebones of a state system. We assign a new function to _upd() and _drw() each time the state changes.

function _init()
  start() -- defined later
end

--update
function _update()
 _upd()
end

function update_start()
 menu(_menu_start, "u")
end
function update_game()
 menu(_menu_game, "u")
end
function update_credits()
 menu(_menu_credits, "u")
end

--draw
function _draw()
 cls(13)
 _drw()
end

function draw_start()
 menu(_menu_start, "d")
end
function draw_game()
 menu(_menu_game, "d")
end
function draw_credits()
 menu(_menu_credits, "d")
end

-- functions that are called directly
-- through menu selections

function start()
 _upd=update_start
 _drw=draw_start
end
function game()
 _upd=update_game
 _drw=draw_game
end
function credits()
 _upd=update_credits
 _drw=draw_credits
end

And here’s how it looks in action. I’ve got a custom sprite for the selection hand drawn in sprite slot 1:

This post is also available in plain text

[Webmentions]

Want to reply? I've hooked up Webmentions, so give it a go!