Skip to content

feat: Add merging hot reload to flame_cli that keeps component state - #4134

Draft
spydon wants to merge 3 commits into
mainfrom
feat/merging-hot-reload
Draft

spydon wants to merge 3 commits into
mainfrom
feat/merging-hot-reload

Conversation

@spydon

@spydon spydon commented Oct 24, 2026

Copy link
Copy Markdown
Member

Description

A hot reload keeps the state of the game, but most of the code of a Flame game runs once, in constructors and onLoad. Changing a speed, a size or which components a level adds therefore only shows after a hot restart, which loses the state.

This adds flame reload --merge, a hot reload that also applies those changes to the live components while they keep their state. It works as a three way merge:

  1. The game is paused, and two copies of it (shadows) are built with the old code, using the constructor of the game class without arguments. They are loaded and mounted but never shown or updated.
  2. The hot reload runs, and a third shadow is built with the new code.
  3. The fields of the components are read through the Dart VM Service (getObject), so custom classes need no setup.
  4. A value that differs between the old and the new shadow was changed by the code, and is written to the live component with evaluate. Values that differ between the two old shadows aren't deterministic, for example random ones, and are ignored. When the live value changed at runtime too, the code wins and the conflict is reported, or --keep-runtime keeps the live value.
  5. Writes go through the deepest field that can be written. Setters with the public name of a private field are tried first, then setFrom, then replacing the elements of a collection, and lastly plain assignment. That way a final NotifyingVector2 is set through Transform2D.position and the listeners are notified.
  6. Components that the new code declares are moved from the new shadow into the live game, and components that the code doesn't declare anymore are removed. Components spawned at runtime are left alone.

Changes in flame:

  • Component.debugDeclarationIndex, set in debug mode, is the order in which a component was added to its parent before the parent was mounted. It separates declared components from runtime spawns, and matches shadow components to live ones exactly.
  • HotReloadConnector: builds the shadows, describes the trees, and adopts and removes components.
  • FlameGame.isHotReloadShadow: user code can check it to skip side effects in shadows. A shadow doesn't take over the DevTools connection.

Changes in flame_cli:

  • lib/src/merge/tree_match.dart: matches the trees. Old to live is matched exactly by declaration index. Old to new is aligned like a text diff, with a weighted longest common subsequence over types and position hints, and named ComponentKeys are paired first.
  • lib/src/merge/merge.dart: the merge rules, against a reader and a writer interface.
  • lib/src/merge/heap.dart: the VM service reader and writer.

I tested it end to end on the flutter-tester device with a small game:

  • A reload without code changes reports nothing.
  • speed, state.hp, a final List in a plain user class, and a RectangleComponent size were merged. A new CircleComponent was added. The moving player kept its runtime position, and a runtime-spawned component was untouched.
  • In a second round, a changed spawn position was reported as a conflict and applied, a declared rectangle was removed, and the circle added in the first round got its new radius.
  • A reload with a compile error exits with 70, and the game is resumed.

Known limitations, to follow up on:

  • A component added in onLoad to a parent that is already mounted, for example a HUD added to the viewport from the world, counts as a runtime spawn.
  • Values that hold references to components inside collections are skipped, not mapped.
  • Closures are not compared.
  • The game class needs a constructor without arguments, and SingleGameInstance games are not supported.
  • The whole tree is read field by field, which is fine for small games but should be limited to the changed libraries for large ones.

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • [-] I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Related Issues

Related to #4131

@spydon
spydon requested a balanced review from Copilot October 24, 2026 11:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants