A simple to use and portable options class for the Playdate Lua SDK. It has no dependencies outside the Corelibs and the UI design is based on the Playdate system menu.
- Declarative option definition syntax
- Easy to install portable class (no other dependencies!)
- List, toggle, and slider option styles
- Automatic saving and loading of user settings
- Dirty read support (any Option:read() can be configured to return
nil
when value hasn't changed) - Can lock options from being changed given a value of another option
- Ability to favorite the values of certain options to use how you wish
- Optional "Reset to defaults" button
- Placeholder methods for your own sound effects
- Copy the
options.lua
file into your project's Source folder. import 'path/to/options'
in yourmain.lua
file
Option B: With toybox
- Run
toybox add options
andtoybox update
- Make sure all toyboxes are imported in your code:
import '../toyboxes/toyboxes.lua'
- Initialize the Options class as a global variable. EX:
Opts = Options(...)
(see below for details) - Ensure
gfx.sprite.update()
andplaydate.timer.updateTimers()
are called in your main update loop. - Done!
Compile and build this project as a PDX to try an example app which demos all of the class features. Check out the main.lua
source code to see how you might use this class in your own project.
Initialize the class as a global variable. Opts = Options(definitions, displayOnRight, saveDataPath, onHide)
. The params are detailed here:
- definitions (required
table
): A table that specifies each option and the sections they live in. See the next section for details. - displayOnRight (optional
boolean
): Iftrue
, the menu will draw on the right half of the screen instead of left. - saveDataPath (optional
string
): By default, the class saves user settings tosettings.json
in the game's Data directory. Pass a different string here to override that. - onHide (optional
function
): Function called after the:hide()
function is called.
The Options menu extends the playdate.graphics.sprite
class. If you wish to change the zIndex (defaulted to 9999) or the drawOffset, or any other standard sprite property, that's possible with normal sprite methods after initialization.
Ensure that playdate.graphics.sprite.update()
is called in the main update loop. You will also need playdate.timer.updateTimers()
if you want key repeat timers to work.
All of your game's options must be defined declaratively by passing an option definition object to the Options() initialization. This definition table is a list of sections, and each section contains a header and list of options as follows:
{
{ header="section 1", options = {{name="opt1" ..}, {name="opt2"}, ...}},
{ header="section 2", options = {{name="opt3" ..}, {name="opt4"}, ...}}
}
You must have at least one section, and a section can contain any number of options.
An option definition object can have the following properties (see the main.lua
class for an example):
- name (required
string
): The option's display name in menu - key (optional
string
): Identifier for the option in the userOptions table json output (NEEDS TO BE UNIQUE)- If key is not provided, lowercase name is used as the key
- Set the key to
Options.RESET
to make a "reset to defaults" button
- values (optional
table
): Table of possible values. Only required for normal "List" style options. - style (optional enum:
Options.TOGGLE
,Options.SLIDER
, or none): Defines a special type of option. If omitted, this will be a normal "list" option.- For any of these special option types, the
values
field is not required as it is calculated automatically. Options.TOGGLE
: Boolean toggle switch. Values:{false, true}
Options.SLIDER
: Used to select an integer in a range from min to max (useful for volume controls). Values:{min, min+1, min+2, ... , max}
- For any of these special option types, the
- min (required for
SLIDER
,integer
): Integer minimum value for the slider. can be negative. - max (required for
SLIDER
,integer
): Integer maximum value for the slider. can be negative. Should probably be greater than the min or bad things might happen (haven't tested this) - default (optional
integer
): Index of the value that should be set as default. If omitted, the first item in the values list will be the default.- For a standard list option, make sure to use the index of the item you want in the list, not the item itself.
- For a toggle switch, set default to 1 for
false
, 2 fortrue
. - For a slider, the default is not an index but rather an actual integer value. So if your volume slider goes from 0 to 5, and you want to start on max volume, set the default to 5 (as opposed to 6).
- preview (optional
boolean
): Iftrue
, the options menu will hide while the value of this option is changing to more easily preview the changes. Pressing B or scrolling off the option will end the preview mode. - dirtyRead(optional
boolean
): Iftrue
, reads on this option will returnnil
if the value hasn't changed from the last read. This is useful for expensive operations based on an option value. Read calls can also be "forced" to ignore the dirty state of adirtyRead
option (seemain.lua
example app). - tooltip (optional
string
): Show a tooltip box when the user is selecting an option for additional help - ignoreOnLoad (optional
boolean
): Ignore the stored value for this option when loading the game. Useful for settings that you want to be temporary. - locks (optional
table
): A table detailing another option that should be locked when this option is a certain value. For example, if your game has customizeable controls, you may want to set the A button functionality to a certain value while crank controls are on. Format:lockedOption
: key of the option to locklockedValue
: index of the value to lock the above option tolockedWhen
: when the value of the locking option is equal to this, the lockedOption becomes locked. Use indexes for every option style exceptTOGGLE
, in which case use booleans.
- canFavorite (optional
boolean
): Sets whether or not an option value can be "favorited" by pressing A on the value.- If favorite-able, an option value can be toggled as favorite with the A button
- If any values are favorited, only those are selected from for
Opts:randomize()
- It's up to you what else to do with the favorite values. You can access them with Opts:getFavorites(key). The resulting list is is a list of indexes for the option.values array.
- showValue (optional
boolean
): ForSLIDER
styles only: shows the current value of the slider to the right of the slider widget
The easiest way to use this class is to initialize it as a global variable (see the Installation section). Then you can invoke methods on that class from anywhere in your app to read/write/show/hide.
Options:show()
: Show the Options menu if it's not already open.Options:hide()
: Hide the Options menu (and save the resulting settings to JSON)Options:isVisible()
: Returns true if the Options menu is visible.Options:read(key, ignoreDirty, returnValue)
: Returns the current index of the desired option, or returnsnil
if it's adirtyRead
option and the value is unchanged since the lastread
call.key
(string): Key for the option of interest. If no key is found, returnsnil
.ignoreDirty
(boolean, defaultnil
): If true, the dirty state of an option is ignored and the call will always return a non-nil value (assuming the key exists).returnValue
(boolean, defaultnil
): Return the actual value of the option and not the index of the value. The reason that returning the index is the default is so the exact names of the values can be changed easily without altering how those values are read across the code.- Example: if an option with the key 'color' has the following values
{'red', 'green', 'blue'}
, then to check if the color is 'green' you would doif Opts:read('color') == 2
. If you later want to change the string 'green' to 'dark green' you can simply change the string in the options definition but the code where you check the value remains the same. Alternatively, you could doif Opts:read('color', false, true) == 'green'
if that makes more sense for your use case. In either case, numeric checks are probably faster than string checks, and in the resultingsettings.json
file, a user's selected values are stored as indexes rather than values (except for boolean options).
- Example: if an option with the key 'color' has the following values
Options:write(key, newIndex, keepClean)
: Write a new value (in index form) to the given option key. Normally the user should be the one setting option values by using the option menu in-game, but occasionally you may have a case where you want to write new options values in code.key
(string): Key for the option of interestnewIndex
(integer): The index of the new value If different from the current value, the option is marked dirty (unlesskeepClean
).keepClean
(boolean, defaultnil
): Skip marking the option as dirty if the new value is different.
Options:randomize(listOfKeys)
: Randomly select (and write) new values for a given list of options keys. For example, this is used in Sparrow Solitaire when going to a new layout, as the current background, tileset, and music options can be randomized.listOfKeys
(table of strings): Option keys to randomize. If any of these options have favorites selected, only those favorites are selected from in the randomizer.
Options:getFavorites(key)
: Retrieve a list of a indexes of option values that have been marked as favorite for the given option.key
(string): The desired option key to retrieve favorites from.
Options:saveUserOptions()
: Save current user settings to the game's data folder. You can change the default path by supplying asaveDataPath
parameter on initialization.Options:isOptsDirty()
: returnstrue
if any single option in the whole options class has been changed.Options:markClean()
: Marks the options menu as a whole as clean (this does not alter the individual dirty flags for dirtyRead options!!).
The option class automatically writes the user settings to a file called settings.json
in the game's Data folder. You can change this path by passing the saveDataPath
param in initialization.
Saving will happen automatically when the options menu is hidden, but you might also want to save this data at other points such as when the app is about to terminate. To do that, add something like the following to your main.lua:
function playdate.gameWillTerminate()
Opts:saveUserOptions()
end
Existing user options will be automatically loaded, if found, when the Options class is initialized. If you want to skip loading a particular option, set ignoreOnLoad=true
on that option's definition. User settings are saved in the following format: optionKey:{index, dirtyFlag}
. Options that are not dirtyRead
options look like optionKey:{index}
. Boolean options save the actual boolean instead of the index for the values array.
Override or change the following methods to customize the the background of the options menu:
updateMenuImage()
and drawSideBar()
.
To alter how the tooltips look, modify drawTooltipBox(tooltip)
.
Override or modify the following methods for sound effects:
playOpenSFX()
playCloseSFX()
playResetSFX()
playSelectionSFX(isForward)
To change the fonts for option labels or values, the best place to do it would probably be line 106 or 112 inside the menu.drawCell()
method of Options.lua. To change section heading fonts, check out the draw functions on line 128 menu.drawSectionHeader()
.
Matt Sephton for his original options manager class (since altered beyond recognition), for drawing the switches and slider in code, and for his encouragement in making this useful class open source.