Recipe: a settings screen
A settings screen is a list of labelled controls, each tied to one setting. Keep each setting in a Ref, bind the control to it two ways with bindValue or bindSelected, and the rest of the game reads the refs. TwoColumnLayout lines the labels up on the left and the controls on the right.
ts
import {
DEFAULT_STAGE, KeyBinding, KeybindInput, Label, OptionRotator, Ref, Screen, Slider, Toggle, TwoColumnLayout,
} from "willowui";
class Settings {
music: Ref<float> = new Ref<float>(0.8);
effects: Ref<float> = new Ref<float>(1.0);
fullscreen: Ref<boolean> = new Ref<boolean>(false);
difficulty: Ref<int> = new Ref<int>(1);
jumpKey: Ref<KeyBinding> = new Ref<KeyBinding>(KeyBinding.ofKey("Space"));
}
const settings: Settings = new Settings();
const screen: Screen = new Screen();
const title: Label = new Label("title", "Settings", "default", 22.0);
title.setPosition(40.0, 24.0);
screen.add(title);
const form: TwoColumnLayout = new TwoColumnLayout("settings");
form.setPosition(40.0, 70.0);
form.setSize(460.0, 260.0);
screen.add(form);
function label(text: string): Label {
return new Label("label", text, "default", 15.0);
}
const music: Slider = new Slider("music");
music.setRange(0.0, 1.0);
music.setSize(220.0, 24.0);
music.bindValue(settings.music);
form.addRow(label("Music volume"), music);
const effects: Slider = new Slider("effects");
effects.setRange(0.0, 1.0);
effects.setSize(220.0, 24.0);
effects.bindValue(settings.effects);
form.addRow(label("Effects volume"), effects);
const fullscreen: Toggle = new Toggle("fullscreen");
fullscreen.bindValue(settings.fullscreen);
form.addRow(label("Fullscreen"), fullscreen);
const difficulty: OptionRotator = new OptionRotator("difficulty", ["Story", "Normal", "Hard"], "default", 15.0);
difficulty.setSize(220.0, 32.0);
difficulty.bindSelected(settings.difficulty);
form.addRow(label("Difficulty"), difficulty);
const jump: KeybindInput = new KeybindInput("jump", "default", 15.0);
jump.setSize(220.0, 32.0);
jump.bindValue(settings.jumpKey);
form.addRow(label("Jump"), jump);
// The rest of the game reacts to the refs, not to the widgets.
settings.music.on((now: any, before: any): void => {
console.log("music volume is now " + now);
});
DEFAULT_STAGE.setScene(engine.root2d);
DEFAULT_STAGE.add(screen);
App.onFrame((dt: float): void => { DEFAULT_STAGE.update(dt); });Notes:
- Two-way binding. Each
bindValueorbindSelectedcall makes the widget show the ref's value and write the player's changes back into it. Setting a ref from code (loading saved settings, a "Reset to defaults" button) moves the widget too. - Key bindings. A
KeybindInputwaits for the next key press when clicked and stores it as aKeyBinding.setOnConflict(fn)lets you reject a key that is already taken (returnfalse).setCombosEnabled(true)accepts combinations such as Ctrl+S. - Choices.
OptionRotatorsteps through options with left and right arrows, which suits a gamepad.Selectopens a dropdown and binds the same way withbindSelected. - Saving. Read the refs with
peek()when you save, and set them when you load. Nothing else needs to change.
In XML
bind:value, bind:checked, bind:selected and bind:rawValue are two way when the expression is just the name of a ref, so a template can hold the whole settings screen: <Slider bind:value="volume" /> moves with the ref and writes the player's drags back into it. Recipe: a form shows it end to end.
See also
- Reactive state for refs,
onandcomputed. - Interactive widgets for every option of
Slider,Toggle,OptionRotator,SelectandKeybindInput.