flutter3d_game

What a game on flutter3d adds to an application: the devices it is played with, the run being played, the settings and saves a player keeps, and a drawn view of what a level's simulation moves. The widgets over all of it (touch controls, the settings panel, the credits, the HUD) are flutter3d_game_ui's.

It depends on flutter3d_app for the surface, storage and level loading every application shares, and on flutter3d_sim for the fixed step, levels, actors and saves. It re-exports flutter3d, and from flutter3d_app the view a game is drawn in; everything else in flutter3d_app and flutter3d_sim stays theirs, and a file that needs more imports them. The simulation is plain Dart and lives in flutter3d_sim.

What is in it

ActionMap / DesktopInput / PadInput Input that has forgotten which device it came from: a key, a touch stick (flutter3d_game_ui's TouchControls) and a gamepad button arrive as the same GameAction, through one action map a player rebinds and the settings file keeps.
RunSession Loading a level, restarting it, moving to the next, saving and resuming, and reporting how the run ended.
GameSettings / GameSettingsController The player's settings as a value — a volume per AudioBus, typed SettingKeys, the action map — and the controller that saves each change. The panel that changes them is flutter3d_game_ui's SettingsOverlay.
SaveFile / SettingsFile / DemoFile The three documents a game keeps, through flutter3d_app's Storage.
LevelWalk The walk a level starts with, before it is any genre: a body that collides, jumps and runs, turns where it is dragged, and carries a camera at eye height.
ActorVisuals / FixtureVisuals An actor bound to the node that represents it, and a fixture to the light it drives. The game decides what a torch looks like and hands it in.

Input

One ActionMap holds every binding a game has: the buttons, the composites (WASD as one move), the sticks and the mouse's motion, each with its dead zone, sensitivity and invert. A game makes it once and hands the same object to the keyboard, the pad and the settings screen, so a rebind takes effect on the next key press:

final settings = await SettingsFile(
  appName: 'my_game',
  defaultActions: DesktopInput.defaultActionMap,
).read();
final actions = PadInput.addDefaultsTo(
  settings.actionsOr(DesktopInput.defaultActionMap),
);
final keyboard = DesktopInput(state: input, actions: actions);
final pad = PadInput(state: input, actions: actions)..applySettings(settings);

What a gamepad's controls do is bindings too. The left stick walks through a DualAxisBinding on pad:stick.left, the right one looks; a racing game starts from PadInput.addDrivingDefaultsTo, which binds the stick's two directions (InputSource.padHalfAxis) to its steering as buttons with a magnitude. A numbered slot — a weapon, a hotbar's block — is an action of SlotActions, bound to the number row by default and to the d-pad by PadInput.addSlotDefaultsTo.

Settings

GameSettings is a value: withVolume(AudioBus.music, 0.4), withValue(GameSettingKeys.mouseLook, 1.5), copyWith(...). A game's own setting is a SettingKey under its own namespace, SettingKey<bool>('mygame.subtitles', fallback: true). The file is written in the format envelope (f3d.settings), and a file from before it is read and its names moved to their namespaced ids.

GameSettingsController holds the current value, saves every change and hands it to the game's apply. The panel, flutter3d_game_ui's, is a list of SettingsSections:

import 'package:flutter3d_game_ui/settings.dart';

SettingsOverlay(
  settings: controller,
  opening: keyboard.releaseMouse,
  sections: SettingsSection.standard(
    defaultActions: DesktopInput.defaultActionMap,
    padConnected: pad.isConnected,
  ),
)

What is deliberately not in it

Widgets. Every one of them is flutter3d_game_ui's, so a test or a replaying server that reads the settings and the run resolves none of them.

Anything a particular game says. The list of rebindable actions belongs to the caller, and the settings have never known what a coin or a monster is.

A state-management choice. RunSession is an ordinary class. Two of the three games wrap it in a cubit, and the package does not depend on that.

The racing game's season. Racing moves from one circuit to the next and keeps how far somebody got, which looks like a RunSession but is not one: nobody resumes a race half a lap in, so snapshotOf and restoreInto would be two required overrides returning nothing.

One assembly per game

Every game has exactly one function that turns a level document into a run (stage in each application), and nothing else spawns a level. The platformer once had six copies of that assembly and they had drifted apart. The dungeon had two, and one of them proved the crypt finishable with a loadout the game never gives anybody. dart run tool/structure.dart checks the rule.


Part of flutter3d, an independent implementation of a 3D engine for Flutter. It is not a fork or a binding of another engine, and it is not affiliated with Google or the Flutter project. It has four switchable rendering backends: Impeller via Flutter GPU, WebGL2, WebGPU and a software rasteriser. It loads glTF, OBJ and .f3d, and has six lighting models, shadows, bloom, skinning, animation, BVH culling and picking, plus a deterministic fixed-step game layer with collision, navigation, positional audio, and gamepad and touch input. Four example games (shooter, platformer, racing, strategy) are each built on a genre package: flutter3d_game_shooter, flutter3d_game_platformer, flutter3d_game_racing, flutter3d_game_strategy. A new game starts from the editor's scaffold, which writes one from a template: flutter3d.pleion.dev/first-project/. Documentation: flutter3d.pleion.dev.

Libraries

flutter3d_game
What a game on flutter3d adds to an application: the devices it is played with, the run being played, the settings and saves a player keeps, and what a level's simulation moves, drawn.
testing
What a test of a game reaches for and a game does not: hearing what the game told the tools watching it.