Migration Guides¶
This section describes the breaking changes that you need to be aware of when upgrading between major versions of Flame, together with the steps required to migrate your code.
Migrating from v1.38.0 to v2.0.0¶
The gesture listener interfaces removed¶
MultiTapListener, MultiDragListener and ScaleListener have been removed, with no replacement.
They existed so that one adapter could work with either of two implementations: the component-level mixins, or the matching game-level detector. Those detectors were removed earlier in the v2 rewrite, leaving each interface with a single implementation and nothing that consumed it as an interface.
If you implemented one of these directly, use the TapCallbacks, DragCallbacks or
ScaleCallbacks mixin on your component or game instead, which is what they already pointed you at.
FlameDragAdapter is gone too, folded into MultiDragScaleDispatcher as a private class. It was
already @internal, so it could not be constructed from outside Flame.
HasGameReference removed in favour of HasGameRef¶
HasGameReference has been removed. Use HasGameRef instead, which is no longer deprecated and is
now the one and only mixin used to access the game instance from a component. Its accessor is
called gameRef, the game getter and setter are gone:
// Before
class MyComponent extends Component with HasGameReference<MyGame> {
void doSomething() => game.score++;
}
// After
class MyComponent extends Component with HasGameRef<MyGame> {
void doSomething() => gameRef.score++;
}
If you were already using HasGameRef, no changes are required; its accessor remains gameRef.
Setting the game instance explicitly (useful for mocking in tests) is done through gameRef as
well, and the findGame() override behaves exactly as before.
HasWorldReference renamed to HasWorldRef¶
HasWorldReference has been renamed to HasWorldRef, and its accessor world has been renamed to
worldRef, so that it mirrors HasGameRef/gameRef exactly. The old name is gone, there is no
deprecated alias:
// Before
class MyComponent extends Component with HasWorldReference<MyWorld> {
void doSomething() => world.add(AnotherComponent());
}
// After
class MyComponent extends Component with HasWorldRef<MyWorld> {
void doSomething() => worldRef.add(AnotherComponent());
}
Note that this only affects the mixin’s accessor; FlameGame.world and CameraComponent.world are
unchanged. Setting the world instance explicitly (useful for mocking in tests) is now done through
worldRef, and findWorld() behaves exactly as before.
Components that get the mixin indirectly are affected too: Component3D in flame_3d mixes in
HasWorldRef<World3D>, so subclasses reaching for the enclosing world must use worldRef.
Asset prefix removed¶
Images and AssetsCache no longer prepend anything to the paths you give them. Images used to
prepend assets/images/ and AssetsCache used to prepend assets/, both configurable through a
prefix property. That property is gone, along with the prefix constructor argument.
Every asset is now addressed by its full path, exactly as declared in the pubspec.yaml:
// Before
await Flame.images.load('player.png');
final level = await Flame.assets.readJson('levels/level1.json');
// After
await Flame.images.load('assets/images/player.png');
final level = await Flame.assets.readJson('assets/levels/level1.json');
This applies to everything that loads through those caches, including Sprite.load,
SpriteAnimation.load, SpriteBatch.load, Game.loadSprite, Game.loadSpriteAnimation, the
Parallax loaders and ParallaxImageData/ParallaxAnimationData, and the .asset constructors of
SpriteWidget, SpriteAnimationWidget, NineTileBoxWidget and SpriteButton.
If you relied on a custom prefix, there is nothing to replace it with, and nothing to configure: just write the paths you actually want.
// Before
Flame.images.prefix = 'gfx/';
await Flame.images.load('player.png');
// After
await Flame.images.load('gfx/player.png');
Cache keys are now the full path¶
The path is also the key the asset is cached under, so anything that reads the cache by key needs the same full path:
// Before
await Flame.images.load('player.png');
final image = Flame.images.fromCache('player.png');
// After
await Flame.images.load('assets/images/player.png');
final image = Flame.images.fromCache('assets/images/player.png');
This affects Images.fromCache, Images.containsKey, Images.clear, Images.keys,
AssetsCache.fromCache and AssetsCache.clear. It also affects SpriteBatch, whose internal
imageKey is derived from the path you loaded with.
One consequence is a bug fix: Images.load now includes the package in the cache key, matching what
AssetsCache already did. Previously, loading the same filename from two different packages
collided on one key and the second load silently returned the first package’s image.
loadAllImages and loadAllFromPattern require a directory¶
These two methods used the prefix both to filter the asset manifest and to strip it back off the
resulting keys. They now take a required directory argument instead, and cache entries under their
full manifest path. Pass an empty string to scan the whole bundle.
// Before
await Flame.images.loadAllImages();
// After
await Flame.images.loadAllImages(directory: 'assets/images/');
flame_audio¶
The global AudioCache is now created with an empty prefix, so audio paths are full paths too.
FlameAudio.updatePrefix() has been removed, as there is no longer a prefix to update.
// Before
FlameAudio.play('explosion.mp3');
FlameAudio.bgm.play('music/theme.mp3');
// After
FlameAudio.play('assets/audio/explosion.mp3');
FlameAudio.bgm.play('assets/audio/music/theme.mp3');
flame_tiled¶
The prefix argument is gone from TiledComponent.load, RenderableTiledMap.fromFile,
RenderableTiledMap.fromString and FlameTsxProvider.parse. The map’s file name is now a full
path, and the assertion that it must not contain path separators has been removed.
External .tsx tilesets are resolved relative to the map’s own directory, derived from that path.
RenderableTiledMap.fromString has no path to derive from, so its prefix argument became
tsxDirectory.
Watch out for these two, since they change behavior without failing to compile:
RenderableTiledMap.fromString’s tsxDirectory and FlameTsxProvider.parse’s third argument both
default to '' now, where the old prefix defaulted to assets/tiles/. If you call either
directly and rely on that default, pass the directory explicitly.
Tileset and image-layer sources are resolved against a new imagesDirectory argument, which
defaults to assets/images/ and so preserves the previous behavior.
// Before
await TiledComponent.load('map.tmx', Vector2.all(16));
await TiledComponent.load(
'map.tmx',
Vector2.all(16),
prefix: 'assets/maps/',
);
// After
await TiledComponent.load('assets/tiles/map.tmx', Vector2.all(16));
await TiledComponent.load('assets/maps/map.tmx', Vector2.all(16));
Note that TiledAtlas cache keys are now scoped by imagesDirectory, so a key that was
tiles.png is now assets/images/tiles.png.
flame_texturepacker¶
The assetsPrefix argument is gone from atlasFromAssets, TexturePackerAtlas.load and
TexturePackerAtlas.loadAtlas. The atlas path is a full path, and page textures listed inside the
atlas are resolved relative to the atlas’s own directory.
// Before
final atlas = await atlasFromAssets('atlas_map.atlas');
// After
final atlas = await atlasFromAssets('assets/images/atlas_map.atlas');
flame_sprite_fusion¶
The tilemapPrefix argument is gone from SpriteFusionTilemapComponent.load. Both mapJsonFile
and spriteSheetFile are now full paths.
// Before
await SpriteFusionTilemapComponent.load(
mapJsonFile: 'map.json',
spriteSheetFile: 'spritesheet.png',
);
// After
await SpriteFusionTilemapComponent.load(
mapJsonFile: 'assets/tiles/map.json',
spriteSheetFile: 'assets/images/spritesheet.png',
);
VerticalDragDetector and HorizontalDragDetector removed¶
Both game-level mixins have been removed, with no direct replacement in Flame.
They existed only to expose Flutter’s VerticalDragGestureRecognizer and
HorizontalDragGestureRecognizer, whose distinguishing feature is not the filtering itself but how
they behave in Flutter’s gesture arena: an axis-constrained recognizer yields to a competitor on the
other axis. That matters when a GameWidget is nested inside a scrollable, which is a concern of
the widget tree rather than of the game, and it is not something the component-level DragCallbacks
can reproduce.
If your game accepts drags on any axis, use DragCallbacks, which can be mixed directly into your
game class:
// Before
class MyGame extends FlameGame with VerticalDragDetector {
@override
void onVerticalDragUpdate(DragUpdateInfo info) { /* ... */ }
}
// After
class MyGame extends FlameGame with DragCallbacks {
@override
void onDragUpdate(DragUpdateEvent event) { /* ... */ }
}
If you specifically need the arena behavior, wrap your GameWidget in Flutter’s own
GestureDetector and use its
onVerticalDragUpdate / onHorizontalDragUpdate callbacks.
ForcePressDetector removed¶
The ForcePressDetector mixin and its ForcePressInfo event class have been removed, with no
replacement.
It was a niche API, only available on some older Apple’s 3D Touch devices; the iPhone XS and XS Max (2018) were the last models to include it (see Apple’s Models with 3D Touch, a list that Apple even stopped carrying forward after the iOS 14 guide). Every iPhone since, starting with the XR, uses Haptic Touch, which responds to how long a press lasts rather than how hard it is, and so never produces these callbacks. Only a handful of Android devices ever supported it, and some of those (such as the Pixel 2 and 3) have faux pressure sensors that never fired the callbacks anyway.
Combined with force press being the last gesture without an equivalent on the component-level event system, maintaining it was no longer worth the surface area.
If you do still target a 3D Touch device, the gesture remains fully available from Flutter: wrap
your GameWidget in a
GestureDetector and use its
onForcePressStart, onForcePressPeak, onForcePressUpdate and onForcePressEnd callbacks
directly.
Deprecated tap and long press game detectors removed¶
The game-level detector mixins that were deprecated in v1.38.0 have now been removed, together with the event classes that only they used:
Removed |
Use instead |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The replacements are mixed into a component rather than into the game, and each callback takes a single event object:
// Before
class MyGame extends FlameGame with TapDetector {
@override
void onTapDown(TapDownInfo info) {
final position = info.eventPosition.widget;
}
}
// After
class MyComponent extends PositionComponent with TapCallbacks {
@override
void onTapDown(TapDownEvent event) {
final position = event.localPosition;
}
}
Note that a component only receives events that occur on top of it, as determined by
containsLocalPoint(), whereas the old game-level detectors received every event on the game
surface. To keep the old whole-screen behavior, add the mixin to your FlameGame subclass directly
— FlameGame is itself a Component.
See Tap Events and Long Press Events for the full replacement APIs.
ScaleDetector removed¶
The ScaleDetector game mixin has been removed, together with the event classes that only it used:
Removed |
Use instead |
|---|---|
|
|
|
|
|
|
|
|
ScaleUpdateEvent is a strict superset of ScaleUpdateInfo: info.scale.global.x and
info.scale.global.y become event.horizontalScale and event.verticalScale, and
info.delta.global becomes event.focalPointDelta.
There is one behavioral difference to be aware of. The old detector was backed by Flutter’s
ScaleGestureRecognizer, which also emits scale events for a single pointer, with a scale factor
of 1.0 — a quirk that games commonly relied on to pan the camera from within onScaleUpdate. The
new MultiDragScaleGestureRecognizer only emits scale events once two or more pointers are down, so
panning must now be handled with DragCallbacks, which can be combined freely with ScaleCallbacks:
// Before
class MyGame extends FlameGame with ScaleDetector {
@override
void onScaleUpdate(ScaleUpdateInfo info) {
final scale = info.scale.global;
if (!scale.isIdentity()) {
camera.viewfinder.zoom = startZoom * scale.y;
} else {
camera.moveBy((info.delta.global..negate()) / camera.viewfinder.zoom);
}
}
}
// After
class MyGame extends FlameGame with ScaleCallbacks, DragCallbacks {
@override
void onScaleUpdate(ScaleUpdateEvent event) {
camera.viewfinder.zoom = startZoom * event.verticalScale;
}
@override
void onDragUpdate(DragUpdateEvent event) {
// Two-finger pinches emit both drag and scale; skip pan while zooming
if (isScaling) {
return;
}
camera.moveBy((event.localDelta..negate()) / camera.viewfinder.zoom);
}
}
Note that trackpad pinch gestures are not currently recognized by the new system:
MultiDragScaleGestureRecognizer does not yet handle Flutter’s PointerPanZoom events, which is how
a trackpad pinch reaches a scale recognizer. Touchscreen pinches are unaffected.
See Scale Events for the full replacement API.
MultiTouchTapDetector and MultiTouchDragDetector removed¶
Both game-level mixins have been removed:
Removed |
Use instead |
|---|---|
|
|
|
|
The pointerId that used to be passed as a separate first argument is now carried on the event
itself, so simultaneous touches can still be told apart:
// Before
class MyGame extends FlameGame with MultiTouchTapDetector {
@override
void onTapDown(int pointerId, TapDownInfo info) {
taps[pointerId] = info.eventPosition.widget;
}
}
// After
class MyGame extends FlameGame with TapCallbacks {
@override
void onTapDown(TapDownEvent event) {
taps[event.pointerId] = event.canvasPosition;
}
}
TapCallbacks has no equivalent of MultiTouchTapDetector.onTap, which was a direct passthrough of
Flutter’s “tap completed” callback on MultiTapGestureRecognizer. Use onTapUp instead, which fires
at the same point in the gesture.
Because the new mixins are routed through MultiDragScaleDispatcher, the assertion that used to
guard against combining MultiTouchDragDetector with PanDetector in the gesture arena is gone
(as is PanDetector itself; see below).
See Tap Events and Drag Events for the full replacement APIs.
ScrollDetector removed¶
The ScrollDetector game mixin has been removed, together with the event class that only it used:
Removed |
Use instead |
|---|---|
|
|
|
|
The scroll delta is now read directly off the event rather than through a nested wrapper, and the
event carries the usual PositionEvent fields, so the position where the scroll occurred is
available as devicePosition / canvasPosition / localPosition:
// Before
class MyGame extends FlameGame with ScrollDetector {
@override
void onScroll(PointerScrollInfo info) {
camera.viewfinder.zoom += info.scrollDelta.global.y.sign * 0.02;
}
}
// After
class MyGame extends FlameGame with ScrollCallbacks {
@override
void onScroll(ScrollEvent event) {
camera.viewfinder.zoom += event.scrollDelta.y.sign * 0.02;
}
}
Unlike the old detector, which received every scroll event anywhere on the game surface,
ScrollCallbacks is routed by position like the other component callbacks: a component only receives
scrolls that occur on top of it, as determined by containsLocalPoint(). Mixing it into your
FlameGame subclass directly, as above, keeps the old whole-surface behavior.
See Pointer Events for the full replacement API.
MouseMovementDetector removed and PointerMove* renamed to MouseMove*¶
The MouseMovementDetector game mixin has been removed, together with the event class that only it
used. At the same time, the component-level API it is replaced by has been renamed from PointerMove
to MouseMove:
Removed / renamed |
Use instead |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The rename has two reasons. Flame’s PointerMoveEvent collided with Flutter’s class of the same
name, forcing a hide on any file that imported both package:flame/events.dart and
package:flutter/material.dart. And “mouse move” is simply more accurate: the event wraps Flutter’s
PointerHoverEvent and is delivered from a MouseRegion, so it is mouse movement specifically, not
pointer movement in general. MouseMoveDispatcherKey was already named this way.
Migrating from the detector, the callback keeps its onMouseMove name and only the parameter
changes, with the position read directly off the event instead of through the nested eventPosition
wrapper:
// Before
class MyGame extends FlameGame with MouseMovementDetector {
@override
void onMouseMove(PointerHoverInfo info) {
target = info.eventPosition.widget;
}
}
// After
class MyGame extends FlameGame with MouseMoveCallbacks {
@override
void onMouseMove(MouseMoveEvent event) {
target = event.canvasPosition;
}
}
Unlike the old detector, which received every mouse movement anywhere on the game surface,
MouseMoveCallbacks is routed by position like the other component callbacks: a component only
receives movements that occur on top of it, as determined by containsLocalPoint(). Mixing it into
your FlameGame subclass directly, as above, keeps the old whole-surface behavior.
MouseMoveCallbacks additionally offers onMouseMoveStop, which has no equivalent on the old
detector.
flame_test’s createMouseMoveEvent helper now returns a MouseMoveEvent, and if you were using
flame_behaviors, note that it no longer re-exports the legacy *Info event classes.
See Pointer Events for the full replacement API.
PanDetector removed, and with it the whole *Info event hierarchy¶
PanDetector was the last of the game-level gesture detectors, so removing it also removes every
event class that existed to serve them:
Removed |
Use instead |
|---|---|
|
|
|
|
|
|
|
|
|
— |
|
|
|
|
|
|
// Before
class MyGame extends FlameGame with PanDetector {
@override
void onPanStart(DragStartInfo info) {
player.startShooting();
}
@override
void onPanUpdate(DragUpdateInfo info) {
player.move(info.delta.global);
}
@override
void onPanEnd(DragEndInfo info) {
player.stopShooting();
}
}
// After
class MyGame extends FlameGame with DragCallbacks {
@override
void onDragStart(DragStartEvent event) {
super.onDragStart(event);
player.startShooting();
}
@override
void onDragUpdate(DragUpdateEvent event) {
player.move(event.localDelta);
}
@override
void onDragEnd(DragEndEvent event) {
super.onDragEnd(event);
player.stopShooting();
}
}
A few differences to be aware of:
onDragStart,onDragEndandonDragCancelare@mustCallSuper, because they maintain theisDraggedflag; your overrides have to callsuperfirst.There is no equivalent of
onPanDown. UseonDragStart, which fires once the touch slop has been exceeded, exactly likeonPanStartdid.The nested position and delta wrappers are gone:
info.eventPosition.widgetbecomesevent.canvasPosition, andinfo.delta.globalbecomesevent.localDelta(orevent.canvasDeltaif you want the delta before any camera transform is applied).DragEndEventexposesvelocity, but there is no replacement forDragEndInfo.primaryVelocity— it was permanentlynullanyway, since only axis-constrained recognizers ever set it.Every drag event carries a
pointerId, so simultaneous drags can be told apart.PanDetectorcould only ever track one.Like the other component callbacks, drags are routed by position: a component only receives a drag that starts on top of it, as determined by
containsLocalPoint(). MixingDragCallbacksinto yourFlameGamesubclass directly, as above, keeps the old whole-surface behavior.
With this, package:flame/events.dart and package:flame/input.dart no longer export any *Detector
mixin or *Info class, and GestureDetectorBuilder.initializeGestures - which existed only to wire
those detectors up — has been removed.
See Drag Events for the full replacement API.
onDragCancel no longer delegates to onDragEnd¶
DragCallbacks.onDragCancel used to convert the cancellation into an onDragEnd event by default,
which made a cancelled drag look exactly like a completed one. A cancellation means that the gesture
was interrupted (another recognizer won the gesture arena, a second pointer triggered a scale
takeover, a system event, etc.) and it carries no velocity, so components such as drag-to-dismiss
would apply their action even though the drag never finished. This is not a rare event either, since
with MultiDragScaleDispatcher every two finger pinch cancels the individual pointer drags.
The default implementation now only resets isDragged, which means that onDragEnd is no longer
called when a drag is cancelled. If you were relying on the old behavior, override onDragCancel
and forward the event yourself with DragCancelEvent.toDragEnd:
// Before
class MyComponent extends PositionComponent with DragCallbacks {
@override
void onDragEnd(DragEndEvent event) {
super.onDragEnd(event);
// This also ran when the drag was cancelled.
dismiss();
}
}
// After
class MyComponent extends PositionComponent with DragCallbacks {
@override
void onDragEnd(DragEndEvent event) {
super.onDragEnd(event);
dismiss();
}
@override
void onDragCancel(DragCancelEvent event) {
super.onDragCancel(event);
onDragEnd(event.toDragEnd());
}
}
If a cancelled drag should instead be reverted, put that logic in onDragCancel without calling
onDragEnd.
MultiDragDispatcher removed¶
The deprecated MultiDragDispatcher and MultiDragDispatcherKey aliases have been removed. Use
MultiDragScaleDispatcher and MultiDragScaleDispatcherKey instead, if you were using them
directly at all (normally you should just use the mixins).
// Before
game.findByKey(const MultiDragDispatcherKey())
as MultiDragDispatcher?;
// After
game.findByKey(const MultiDragScaleDispatcherKey())
as MultiDragScaleDispatcher?;
Event.handled removed in favour of continuePropagation¶
Events used to carry two independent booleans: handled, which Flame never set nor read, and
continuePropagation, which actually controls whether an event keeps traversing down the component
tree. The former has been removed; continuePropagation is now the single propagation flag on every
event.
By default, an event stops at the first component that can handle it, so a component that “consumes” an event does not need to do anything at all — the components below it will not see it:
// Before
class Square extends RectangleComponent with TapCallbacks {
@override
void onTapDown(TapDownEvent event) {
removeFromParent();
event.handled = true;
}
}
class MyWorld extends World with TapCallbacks {
@override
void onTapDown(TapDownEvent event) {
if (!event.handled) {
add(Square(event.localPosition));
}
}
}
// After
class Square extends RectangleComponent with TapCallbacks {
@override
void onTapDown(TapDownEvent event) {
removeFromParent();
}
}
class MyWorld extends World with TapCallbacks {
@override
void onTapDown(TapDownEvent event) {
add(Square(event.localPosition));
}
}
If you were using handled to let an event reach several components, set
event.continuePropagation = true in the components that should pass it along instead.
The equivalent field on the deprecated *Info event classes (TapDownInfo.handled and friends) has
been removed as well.
add, addAll and addToParent are now synchronous¶
Component.add, Component.addAll and Component.addToParent used to return a future, which made
it look like you could await the addition. That future only covered the child’s loading, never its
mounting, so awaiting it was misleading, and forgetting to await it (or to wrap it in unawaited)
tripped the discarded_futures lint in a lot of games. All three methods now return void.
Drop the await:
// Before
await add(MyComponent());
await addAll([MyComponent(), MyOtherComponent()]);
// After
add(MyComponent());
addAll([MyComponent(), MyOtherComponent()]);
If you were relying on the returned future to know when the child had loaded, await the child’s
loaded future instead:
// Before
await add(crate);
// After
add(crate);
await crate.loaded;
For a batch of children, loaded, mounted and removed are also available on any
Iterable<Component>:
// Before
await addAll(crates);
// After
addAll(crates);
await crates.loaded;
Or, when you need them to be present in children rather than just loaded, await
game.lifecycleEventsProcessed once after adding them.
Load errors are no longer reported by GameWidget.errorBuilder¶
GameWidget.errorBuilder shows a widget when the game’s loading fails, and it used to catch a
failing child’s onLoad as well, because await add(child) chained the child’s error onto the
game’s own onLoad future. Since add no longer returns a future, that chain is gone: a child that
throws in onLoad no longer reaches errorBuilder.
The component itself is not added to the tree, and the rest of the game keeps running. The error is
reported through the child’s loaded future, and if nothing is awaiting it, it is handed to the
current Zone as an uncaught error.
To get the old behavior for a specific child, await its loaded future inside the parent’s
onLoad, which puts the error back onto the future errorBuilder watches:
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
final level = Level();
world.add(level);
// Throws here if Level.onLoad fails, so errorBuilder is shown.
await level.loaded;
}
}
GameWidget.controlled renamed to GameWidget.managed¶
The GameWidget.controlled constructor has been renamed to GameWidget.managed. The behavior is
unchanged; only the name is different.
Replace every usage of GameWidget.controlled with GameWidget.managed:
// Before
GameWidget.controlled(
gameFactory: MyGame.new,
);
// After
GameWidget.managed(
gameFactory: MyGame.new,
);
Game.paused renamed to Game.isPaused¶
The paused getter and setter on Game have been renamed to isPaused, to be consistent with the
other boolean properties in Flame. The behavior is unchanged; only the name is different.
Replace every usage of game.paused with game.isPaused:
// Before
if (game.paused) {
game.paused = false;
}
// After
if (game.isPaused) {
game.isPaused = false;
}
children is now a ComponentList instead of an OrderedSet¶
The ordered_set package is no longer used; children live in a Flame-owned ComponentList. The
iterable surface, query<T>(), and register<T>() are unchanged, so most code compiles as is. If
you imported package:ordered_set types to annotate variables, use ComponentList (from
package:flame/components.dart) instead:
// Before
import 'package:ordered_set/ordered_set.dart';
OrderedSet<Component> children = component.children;
// After
ComponentList children = component.children;
Other changes to be aware of:
children.reversed()is now a getter:children.reversed.Component.strictQueryModeis removed. Strict mode is off by default; to enable it for a component, overridecreateComponentList()to returnComponentList(strictMode: true).query<T>()results are now always in priority order.Removing components while iterating
childrenis allowed; reordering the list while iterating it throwsConcurrentModificationError.
Component.childrenFactory is removed¶
The global children-container factory is gone. Override createComponentList() on the component
instead. The constructor accepts an optional Comparator<Component> that replaces priority
ordering for that parent, which gives custom orderings such as y-sort a supported home:
// Before
Component.childrenFactory = () => OrderedSet.mapping<num, Component>((c) => c.priority);
// After
class YSortedWorld extends World {
@override
ComponentList createComponentList() {
return ComponentList(
comparator: (a, b) => (a as PositionComponent)
.position.y
.compareTo((b as PositionComponent).position.y),
);
}
}
Component.updateTree is non-virtual¶
The update pass runs over a flattened traversal list owned by the game, so updateTree can no
longer be overridden. If you overrode it, mix in CustomTraversal and override its
updateSubtree instead; call super.updateSubtree(dt) to run the standard traversal:
// Before
class SlowMotionArea extends Component {
@override
void updateTree(double dt) => super.updateTree(dt / 2);
}
// After
class SlowMotionArea extends Component with CustomTraversal {
@override
void updateSubtree(double dt) => super.updateSubtree(dt / 2);
}
HasTimeScale is now declared on CustomTraversal, so components other than FlameGame (which
already mixes it in) must mix in CustomTraversal before it:
// Before
class SlowWorld extends World with HasTimeScale {}
// After
class SlowWorld extends World with CustomTraversal, HasTimeScale {}
A HasTimeScale time scale of 0 (or pause()) now stops the update pass for the whole subtree
instead of updating it with a dt of 0, which is what Route.stopTime() relies on.