Here's an example of what I do by default; the rest of this post will explain how the code works and why I do things this way:
Imagine you want to add a "mute" button to your game's menu. Can you spot the issue with this code?
function _init() menuitem(1,"mute",function() ismuted=not ismuted -- ... play/pause the music end) -- ... other game init end
The issue: left/right can be used to change the menuitem, and left/right always leaves the menu open afterward (this may be a bug; it's unclear) so there's no indication to the player that they accidentally muted the game! It's a minor issue... but it's easy to accidentally hit left/right when navigating the menu on a gamepad, which makes this worse.
An easy fix: don't respond to L/R (
menuitem(1,"mute",function(bb) if bb>=16 then ... end end)). This is what I'd recommend for games that are short on tokens but willing to spare a few to fix this.
If you'd like a more responsive menu, you can do this:
function _init() menuitem(1,ismuted and "music: off" or "music: on",function() ismuted=not ismuted -- ... play/pause the music -- update the label: menuitem(nil,ismuted and "music: off" or "music: on") -- feels like we could save some tokens... return true end) -- ... other game init end
Now the option shows its status, and updates its label immediately when you change it!
return true -- this keeps the menu open. Without this, if the player presses left (toggling the setting) and then presses enter (trying to close the menu but instead toggling the setting again), they'll wonder why their change isn't sticking. By leaving the menu open, we show them that they actually re-toggled the setting, and they need to choose "continue" instead to resume the game.
Code organization idiom
ismuted and "music: off" or "music: on" is suspicious -- can we save some tokens? Yes, by setting up each menuitem() again at the end of the callback, just to refresh the labels:
function _init() domenuitems() -- setup all menuitem()s -- ... other game init end function domenuitems() menuitem(1,ismuted and "music: off" or "music: on",function() ismuted=not ismuted -- ... play/pause the music domenuitems() -- refresh the label return true end) -- menuitem(2,... -- menuitem(3,... end
I do this for code-organizing reasons -- it's nice to have all the menuitem code in one place -- but it can also save a few tokens, depending on your setup.
Some menuitems should only be show in certain situations -- e.g. don't show "back to title" if we're already on the title screen. You could break each menuitem into its own little setup function (like zep's original
LOAD #CUSTOM_MENU cart does -- check it out, it's nice!) or do what I do:
menuitem(1,condition and "label",...). This will only show the menuitem when
condition is truthy. I call
domenuitems() anytime the menu needs changing (e.g. at the end of
init_title_screen()) and it all works out.
Recommendations / Summary
I put all my
menuitem() calls inside a custom
domenuitems() function. Every callback runs
domenuitems() at the end, to refresh the labels.
There are three types of menu options I commonly use: commands, toggles, and selectors.
- For commands (run some code, e.g. return to the title screen), I wrap the callback code in a
if bb>=16 then ... endblock, to prevent L/R from triggering the command
- For toggles (e.g. mute/unmute the game), my callback always has
return true, leaving the menu open afterward and avoiding the UX issue described above
- For selectors (choose a number from a range, e.g. change levels), my callback has
return bb<16, leaving the menu open only if L/R were used to change the selection. (this is technically unnecessary due to a longstanding bug(?) in PICO-8's menu handling)
Read the code of the example cart at the top of this post for more info!
[Please log in to post a comment]