flutter3d
Showcase Changelog 38 packages API reference

Settings, config and saves

since 0.7.0 Widgets and the rest

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. GameConfig is reimplemented here in full, since it is nothing but two maps. SaveFile is thinner than it looks: underneath its own few lines it is entirely the real Storage and Snapshot this app already depends on, so this page calls those directly. SettingsPanel needs a Bindings and 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.