Settings, config and saves
Open the live demo · Read the source · View on GitHub
A player's choices and a player's progress are two different documents on
purpose: wiping a corrupt save should not cost anybody their key bindings.
GameConfig holds the first as two maps of numbers, SaveFile holds the
second as a level path and a snapshot, and SettingsPanel is the Flutter
widget that lets a player change the first.
Note. None of the three real classes is a dependency of this app.
GameConfigis reimplemented here in full, since it is nothing but two maps.SaveFileis thinner than it looks: underneath its own few lines it is entirely the realStorageandSnapshotthis app already depends on, so this page calls those directly.SettingsPanelneeds aBindingsand a gamepad's dead zone from packages this app does not have, so it is described here rather than built.
Step 1: A config with no file behind it #
/// Everything a player has changed, as two maps of numbers: no file, no
/// path, no platform. Where the bytes go is the application's business.
final class _GameConfig {
_GameConfig({Map<String, double>? volumes, Map<String, double>? settings})
: volumes = <String, double>{...?volumes},
settings = <String, double>{...?settings};
final Map<String, double> volumes;
final Map<String, double> settings;
double volumeOf(String bus) => volumes[bus] ?? 1.0;
void setVolume(String bus, double volume) =>
volumes[bus] = volume.clamp(0.0, 1.0);
double settingOf(String name, double fallback) => settings[name] ?? fallback;
void setSetting(String name, double value) => settings[name] = value;
}
Step 2: A save that is mostly real storage #
/// Where a run in progress is kept. Everything below the field names is the
/// real `Storage` and `Snapshot` this app already depends on.
final class _SaveFile {
_SaveFile({required String appName}) : _storage = defaultStorage(appName);
final Storage _storage;
static const String _name = 'save.json';
({String level, Snapshot run})? read() {
final text = _storage.read(_name);
if (text == null) return null;
final json = jsonDecode(text) as Map<String, Object?>;
return (
level: json['level']! as String,
run: Snapshot.fromJson(json['run']! as Map<String, Object?>),
);
}
bool write(String level, Snapshot run) => _storage.write(
_name,
jsonEncode(<String, Object?>{'level': level, 'run': run.toJson()}),
);
void clear() => _storage.remove(_name);
}
Step 3: Use both #
final config = _GameConfig()
..setVolume('music', 0.6)
..setSetting('a11y.cameraMotion', 0.0);
final musicVolume = config.volumeOf('music');
final sfxVolume = config.volumeOf('sfx');
final cameraMotion = config.settingOf('a11y.cameraMotion', 1.0);
A bus nobody has set defaults to full volume. A save written, read back and cleared answers exactly what each of those should: the level and state it was given, then nothing at all.