Skip to content

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 bindValue or bindSelected call 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 KeybindInput waits for the next key press when clicked and stores it as a KeyBinding. setOnConflict(fn) lets you reject a key that is already taken (return false). setCombosEnabled(true) accepts combinations such as Ctrl+S.
  • Choices. OptionRotator steps through options with left and right arrows, which suits a gamepad. Select opens a dropdown and binds the same way with bindSelected.
  • 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 ​