From 6615a55bc581945771072fc99b147cd31318016d Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 11 Aug 2022 18:21:32 -0700 Subject: [PATCH 01/12] OverlayManager moved to separate file --- packages/flame/lib/src/game/game.dart | 71 ++----------------- .../flame/lib/src/game/overlay_manager.dart | 63 ++++++++++++++++ 2 files changed, 69 insertions(+), 65 deletions(-) create mode 100644 packages/flame/lib/src/game/overlay_manager.dart diff --git a/packages/flame/lib/src/game/game.dart b/packages/flame/lib/src/game/game.dart index 43d35dd2492..09403796f02 100644 --- a/packages/flame/lib/src/game/game.dart +++ b/packages/flame/lib/src/game/game.dart @@ -3,6 +3,7 @@ import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame/src/flame.dart'; import 'package:flame/src/game/game_render_box.dart'; +import 'package:flame/src/game/overlay_manager.dart'; import 'package:flame/src/game/projector.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -294,7 +295,7 @@ abstract class Game { VoidCallback? pauseEngineFn; VoidCallback? resumeEngineFn; - /// A property that stores an [_ActiveOverlays] + /// A property that stores an [OverlayManager] /// /// This is useful to render widgets on top of a game, such as a pause menu. /// Overlays can be made visible via [overlays].add or hidden via @@ -306,7 +307,7 @@ abstract class Game { /// overlays.add(pauseOverlayIdentifier); // marks 'PauseMenu' to be rendered. /// overlays.remove(pauseOverlayIdentifier); // hides 'PauseMenu'. /// ``` - late final overlays = _ActiveOverlays(this); + late final overlays = OverlayManager(this); /// Used to change the mouse cursor of the GameWidget running this game. /// Setting the value to null will make the GameWidget defer the choice @@ -316,7 +317,7 @@ abstract class Game { set mouseCursor(MouseCursor value) { _mouseCursor = value; - _refreshWidget(); + refreshWidget(); } @visibleForTesting @@ -333,68 +334,8 @@ abstract class Game { /// When a Game is attached to a `GameWidget`, this method will force that /// widget to be rebuilt. This can be used when updating any property which is /// implemented within the Flutter tree. - void _refreshWidget() { + @internal + void refreshWidget() { gameStateListeners.forEach((callback) => callback()); } } - -/// A helper class used to control the visibility of overlays on a [Game] -/// instance. See [Game.overlays]. -class _ActiveOverlays { - _ActiveOverlays(this._game); - - final Game _game; - final Set _activeOverlays = {}; - - /// Clear all active overlays. - void clear() { - _activeOverlays.clear(); - _game._refreshWidget(); - } - - /// Marks the [overlayName] to be rendered. - bool add(String overlayName) { - final setChanged = _activeOverlays.add(overlayName); - if (setChanged) { - _game._refreshWidget(); - } - return setChanged; - } - - /// Marks [overlayNames] to be rendered. - void addAll(Iterable overlayNames) { - final overlayCountBeforeAdded = _activeOverlays.length; - _activeOverlays.addAll(overlayNames); - - final overlayCountAfterAdded = _activeOverlays.length; - if (overlayCountBeforeAdded != overlayCountAfterAdded) { - _game._refreshWidget(); - } - } - - /// Hides the [overlayName]. - bool remove(String overlayName) { - final hasRemoved = _activeOverlays.remove(overlayName); - if (hasRemoved) { - _game._refreshWidget(); - } - return hasRemoved; - } - - /// Hides multiple overlays specified in [overlayNames]. - void removeAll(Iterable overlayNames) { - final overlayCountBeforeRemoved = _activeOverlays.length; - _activeOverlays.removeAll(overlayNames); - - final overlayCountAfterRemoved = _activeOverlays.length; - if (overlayCountBeforeRemoved != overlayCountAfterRemoved) { - _game._refreshWidget(); - } - } - - /// The names of all currently active overlays. - Set get value => _activeOverlays; - - /// Returns if the given [overlayName] is active - bool isActive(String overlayName) => _activeOverlays.contains(overlayName); -} diff --git a/packages/flame/lib/src/game/overlay_manager.dart b/packages/flame/lib/src/game/overlay_manager.dart new file mode 100644 index 00000000000..285c18f9921 --- /dev/null +++ b/packages/flame/lib/src/game/overlay_manager.dart @@ -0,0 +1,63 @@ + +import 'package:flame/src/game/game.dart'; + +/// A helper class used to control the visibility of overlays on a [Game] +/// instance. See [Game.overlays]. +class OverlayManager { + OverlayManager(this._game); + + final Game _game; + final Set _activeOverlays = {}; + + /// Clear all active overlays. + void clear() { + _activeOverlays.clear(); + _game.refreshWidget(); + } + + /// Marks the [overlayName] to be rendered. + bool add(String overlayName) { + final setChanged = _activeOverlays.add(overlayName); + if (setChanged) { + _game.refreshWidget(); + } + return setChanged; + } + + /// Marks [overlayNames] to be rendered. + void addAll(Iterable overlayNames) { + final overlayCountBeforeAdded = _activeOverlays.length; + _activeOverlays.addAll(overlayNames); + + final overlayCountAfterAdded = _activeOverlays.length; + if (overlayCountBeforeAdded != overlayCountAfterAdded) { + _game.refreshWidget(); + } + } + + /// Hides the [overlayName]. + bool remove(String overlayName) { + final hasRemoved = _activeOverlays.remove(overlayName); + if (hasRemoved) { + _game.refreshWidget(); + } + return hasRemoved; + } + + /// Hides multiple overlays specified in [overlayNames]. + void removeAll(Iterable overlayNames) { + final overlayCountBeforeRemoved = _activeOverlays.length; + _activeOverlays.removeAll(overlayNames); + + final overlayCountAfterRemoved = _activeOverlays.length; + if (overlayCountBeforeRemoved != overlayCountAfterRemoved) { + _game.refreshWidget(); + } + } + + /// The names of all currently active overlays. + Set get value => _activeOverlays; + + /// Returns if the given [overlayName] is active + bool isActive(String overlayName) => _activeOverlays.contains(overlayName); +} From 88406b85d95d3329855a283d36f9b68bf12e7c08 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 11 Aug 2022 19:04:42 -0700 Subject: [PATCH 02/12] OverlayBuilderMap moved to OverlayManager --- .../lib/src/game/game_widget/game_widget.dart | 33 +++------- .../flame/lib/src/game/overlay_manager.dart | 62 +++++++++++++++---- ...s_test.dart => overlays_manager_test.dart} | 34 ++++++---- 3 files changed, 83 insertions(+), 46 deletions(-) rename packages/flame/test/game/{active_overlays_test.dart => overlays_manager_test.dart} (78%) diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index d4b65487e4b..c5aa86981e1 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; import 'package:flame/input.dart'; -import 'package:flame/src/game/game.dart'; import 'package:flame/src/game/game_render_box.dart'; import 'package:flame/src/game/game_widget/gestures.dart'; import 'package:flutter/services.dart'; @@ -121,15 +121,21 @@ class GameWidget extends StatefulWidget { this.loadingBuilder, this.errorBuilder, this.backgroundBuilder, - this.overlayBuilderMap, + Map>? overlayBuilderMap, List? initialActiveOverlays, this.focusNode, this.autofocus = true, MouseCursor? mouseCursor, - }) : gameFactory = null { + }) : gameFactory = null, + overlayBuilderMap = null { if (mouseCursor != null) { game!.mouseCursor = mouseCursor; } + if (overlayBuilderMap != null) { + for (final kv in overlayBuilderMap.entries) { + game!.overlays.addEntry(kv.key, kv.value as OverlayWidgetBuilder); + } + } if (initialActiveOverlays != null) { game!.overlays.addAll(initialActiveOverlays); } @@ -274,15 +280,6 @@ class _GameWidgetState extends State> { } } - void _checkOverlays(Set overlays) { - overlays.forEach((overlayKey) { - assert( - widget.overlayBuilderMap?.containsKey(overlayKey) ?? false, - 'A non mapped overlay has been added: $overlayKey', - ); - }); - } - KeyEventResult _handleKeyEvent(FocusNode focusNode, RawKeyEvent event) { final game = currentGame; if (game is KeyboardEvents) { @@ -296,7 +293,6 @@ class _GameWidgetState extends State> { return _protectedBuild(() { Widget internalGameWidget = _GameRenderObjectWidget(currentGame); - _checkOverlays(currentGame.overlays.value); assert( !(currentGame is MultiTouchDragDetector && currentGame is PanDetector), 'WARNING: Both MultiTouchDragDetector and a PanDetector detected. ' @@ -388,16 +384,7 @@ class _GameWidgetState extends State> { } List _addOverlays(BuildContext context, List stackWidgets) { - if (widget.overlayBuilderMap == null) { - return stackWidgets; - } - final widgets = currentGame.overlays.value.map((String overlayKey) { - final builder = widget.overlayBuilderMap![overlayKey]!; - return KeyedSubtree( - key: ValueKey(overlayKey), - child: builder(context, currentGame), - ); - }); + final widgets = currentGame.overlays.buildCurrentOverlayWidgets(context); stackWidgets.addAll(widgets); return stackWidgets; } diff --git a/packages/flame/lib/src/game/overlay_manager.dart b/packages/flame/lib/src/game/overlay_manager.dart index 285c18f9921..2c692f3e1fe 100644 --- a/packages/flame/lib/src/game/overlay_manager.dart +++ b/packages/flame/lib/src/game/overlay_manager.dart @@ -1,14 +1,24 @@ - import 'package:flame/src/game/game.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; /// A helper class used to control the visibility of overlays on a [Game] /// instance. See [Game.overlays]. +@internal class OverlayManager { OverlayManager(this._game); final Game _game; final Set _activeOverlays = {}; + final Map _builders = {}; + + /// The names of all currently active overlays. + Set get value => _activeOverlays; + + /// Returns if the given [overlayName] is active + bool isActive(String overlayName) => _activeOverlays.contains(overlayName); + /// Clear all active overlays. void clear() { _activeOverlays.clear(); @@ -17,7 +27,7 @@ class OverlayManager { /// Marks the [overlayName] to be rendered. bool add(String overlayName) { - final setChanged = _activeOverlays.add(overlayName); + final setChanged = _addImpl(overlayName); if (setChanged) { _game.refreshWidget(); } @@ -26,15 +36,29 @@ class OverlayManager { /// Marks [overlayNames] to be rendered. void addAll(Iterable overlayNames) { - final overlayCountBeforeAdded = _activeOverlays.length; - _activeOverlays.addAll(overlayNames); - - final overlayCountAfterAdded = _activeOverlays.length; - if (overlayCountBeforeAdded != overlayCountAfterAdded) { + final initialCount = _activeOverlays.length; + overlayNames.forEach(_addImpl); + if (initialCount != _activeOverlays.length) { _game.refreshWidget(); } } + bool _addImpl(String name) { + assert( + _builders.containsKey(name), + 'Trying to add an unknown overlay $name', + ); + if (_activeOverlays.contains(name)) { + return false; + } + _activeOverlays.add(name); + return true; + } + + void addEntry(String name, _OverlayBuilderFunction builder) { + _builders[name] = builder; + } + /// Hides the [overlayName]. bool remove(String overlayName) { final hasRemoved = _activeOverlays.remove(overlayName); @@ -55,9 +79,23 @@ class OverlayManager { } } - /// The names of all currently active overlays. - Set get value => _activeOverlays; - - /// Returns if the given [overlayName] is active - bool isActive(String overlayName) => _activeOverlays.contains(overlayName); + @internal + List buildCurrentOverlayWidgets(BuildContext context) { + final widgets = []; + for (final overlayName in _activeOverlays) { + final builder = _builders[overlayName]!; + widgets.add( + KeyedSubtree( + key: ValueKey(overlayName), + child: builder(context, _game), + ), + ); + } + return widgets; + } } + +typedef _OverlayBuilderFunction = Widget Function( + BuildContext context, + Game game, +); diff --git a/packages/flame/test/game/active_overlays_test.dart b/packages/flame/test/game/overlays_manager_test.dart similarity index 78% rename from packages/flame/test/game/active_overlays_test.dart rename to packages/flame/test/game/overlays_manager_test.dart index 70469a41e68..cc00b8e2463 100644 --- a/packages/flame/test/game/active_overlays_test.dart +++ b/packages/flame/test/game/overlays_manager_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import '../_resources/custom_flame_game.dart'; void main() { - group('_ActiveOverlays', () { + group('OverlaysManager', () { testWidgets( 'Overlay can be added via initialActiveOverlays', (tester) async { @@ -87,14 +87,16 @@ void main() { group('add', () { test('can add an overlay', () { - final overlays = FlameGame().overlays; + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); final added = overlays.add('test'); expect(added, true); expect(overlays.isActive('test'), true); }); - test('wont add same overlay', () { - final overlays = FlameGame().overlays; + test('would not add same overlay twice', () { + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); overlays.add('test'); final added = overlays.add('test'); expect(added, false); @@ -103,7 +105,9 @@ void main() { group('addAll', () { test('can add multiple overlays at once', () { - final overlays = FlameGame().overlays; + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()) + ..addEntry('test2', (ctx, game) => Container()); overlays.addAll(['test', 'test2']); expect(overlays.isActive('test'), true); expect(overlays.isActive('test2'), true); @@ -112,7 +116,9 @@ void main() { group('removeAll', () { test('can remove multiple overlays at once', () { - final overlays = FlameGame().overlays; + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()) + ..addEntry('test2', (ctx, game) => Container()); overlays.addAll(['test', 'test2']); overlays.removeAll(['test', 'test2']); @@ -124,7 +130,8 @@ void main() { group('remove', () { test('can remove an overlay', () { - final overlays = FlameGame().overlays; + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); overlays.add('test'); final removed = overlays.remove('test'); @@ -133,7 +140,8 @@ void main() { }); test('will not result in removal if there is nothing to remove', () { - final overlays = FlameGame().overlays; + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); final removed = overlays.remove('test'); expect(removed, false); }); @@ -141,20 +149,24 @@ void main() { group('isActive', () { test('is true when overlay is active', () { - final overlays = FlameGame().overlays; + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); overlays.add('test'); expect(overlays.isActive('test'), true); }); test('is false when overlay is active', () { - final overlays = FlameGame().overlays; + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); expect(overlays.isActive('test'), false); }); }); group('clear', () { test('clears all overlays', () { - final overlays = FlameGame().overlays; + final overlays = FlameGame().overlays + ..addEntry('test1', (ctx, game) => Container()) + ..addEntry('test2', (ctx, game) => Container()); overlays.add('test1'); overlays.add('test2'); From d8cabcfbd88e1ad5099a8bf0d780eafcee814d0c Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 11 Aug 2022 19:42:15 -0700 Subject: [PATCH 03/12] fix template type coercion --- packages/flame/lib/src/game/game_widget/game_widget.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index c5aa86981e1..10d828b7d70 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -133,7 +133,10 @@ class GameWidget extends StatefulWidget { } if (overlayBuilderMap != null) { for (final kv in overlayBuilderMap.entries) { - game!.overlays.addEntry(kv.key, kv.value as OverlayWidgetBuilder); + game!.overlays.addEntry( + kv.key, + (ctx, game) => kv.value(ctx, game as T), + ); } } if (initialActiveOverlays != null) { From 00a556d3f153dc9bac6655b4e1ebea9fa9372856 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 11 Aug 2022 19:49:38 -0700 Subject: [PATCH 04/12] Support overlayBuilderMap in .controlled constructor too --- .../lib/src/game/game_widget/game_widget.dart | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index 10d828b7d70..f3d10266151 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -68,6 +68,9 @@ class GameWidget extends StatefulWidget { /// Defaults to true. final bool autofocus; + final MouseCursor? mouseCursor; + final List? initialActiveOverlays; + /// Renders a [game] in a flutter widget tree. /// /// Ex: @@ -121,27 +124,13 @@ class GameWidget extends StatefulWidget { this.loadingBuilder, this.errorBuilder, this.backgroundBuilder, - Map>? overlayBuilderMap, - List? initialActiveOverlays, + this.overlayBuilderMap, + this.initialActiveOverlays, this.focusNode, this.autofocus = true, - MouseCursor? mouseCursor, - }) : gameFactory = null, - overlayBuilderMap = null { - if (mouseCursor != null) { - game!.mouseCursor = mouseCursor; - } - if (overlayBuilderMap != null) { - for (final kv in overlayBuilderMap.entries) { - game!.overlays.addEntry( - kv.key, - (ctx, game) => kv.value(ctx, game as T), - ); - } - } - if (initialActiveOverlays != null) { - game!.overlays.addAll(initialActiveOverlays); - } + this.mouseCursor, + }) : gameFactory = null { + _initializeGame(game!); } /// Creates a new game instance with the [gameFactory] and then @@ -172,8 +161,10 @@ class GameWidget extends StatefulWidget { this.errorBuilder, this.backgroundBuilder, this.overlayBuilderMap, + this.initialActiveOverlays, this.focusNode, this.autofocus = true, + this.mouseCursor, }) : game = null; /// Renders a [game] in a flutter widget tree alongside widgets overlays. @@ -181,6 +172,23 @@ class GameWidget extends StatefulWidget { /// To use overlays, the game subclass has to be mixed with HasWidgetsOverlay. @override _GameWidgetState createState() => _GameWidgetState(); + + void _initializeGame(T game) { + if (mouseCursor != null) { + game.mouseCursor = mouseCursor!; + } + if (overlayBuilderMap != null) { + for (final kv in overlayBuilderMap!.entries) { + game.overlays.addEntry( + kv.key, + (ctx, game) => kv.value(ctx, game as T), + ); + } + } + if (initialActiveOverlays != null) { + game.overlays.addAll(initialActiveOverlays!); + } + } } class _GameWidgetState extends State> { @@ -241,8 +249,12 @@ class _GameWidgetState extends State> { } void initCurrentGame() { - final widgetGame = widget.game; - currentGame = widgetGame ?? widget.gameFactory!.call(); + if (widget.game == null) { + currentGame = widget.gameFactory!.call(); + widget._initializeGame(currentGame); + } else { + currentGame = widget.game!; + } currentGame.addGameStateListener(_onGameStateChange); _loaderFuture = null; } From ff6a56bb05e525aa199782e960c37142fa7bf154 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 11 Aug 2022 20:06:06 -0700 Subject: [PATCH 05/12] OverlayRoute class --- .../lib/src/components/overlay_route.dart | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/flame/lib/src/components/overlay_route.dart diff --git a/packages/flame/lib/src/components/overlay_route.dart b/packages/flame/lib/src/components/overlay_route.dart new file mode 100644 index 00000000000..fac2d806968 --- /dev/null +++ b/packages/flame/lib/src/components/overlay_route.dart @@ -0,0 +1,42 @@ +import 'package:flame/src/components/component.dart'; +import 'package:flame/src/components/route.dart'; +import 'package:flame/src/game/game.dart'; +import 'package:flutter/widgets.dart' hide Route; +import 'package:meta/meta.dart'; + +class OverlayRoute extends Route { + OverlayRoute(OverlayBuilder builder, {super.transparent = true}) + : _builder = builder, + super(null); + + OverlayRoute.existing({super.transparent = true}) + : _builder = null, + super(null); + + final OverlayBuilder? _builder; + + @internal + Game get game => findGame()!; + + @override + Component build() { + if (_builder != null) { + game.overlays.addEntry(name, _builder!); + } + return Component(); + } + + @mustCallSuper + @override + void onPush(Route? previousRoute) { + game.overlays.add(name); + } + + @mustCallSuper + @override + void onPop(Route nextRoute) { + game.overlays.remove(name); + } +} + +typedef OverlayBuilder = Widget Function(BuildContext context, Game game); From 8eac2a8b2d69d6faf7fd7d86665b34f2658938da Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 12 Aug 2022 09:36:32 -0700 Subject: [PATCH 06/12] docs --- .../lib/src/components/overlay_route.dart | 16 +++++++++++-- .../lib/src/components/router_component.dart | 16 +++++++++++++ .../lib/src/game/game_widget/game_widget.dart | 24 +++++++++---------- .../flame/lib/src/game/overlay_manager.dart | 23 +++++++++++------- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/packages/flame/lib/src/components/overlay_route.dart b/packages/flame/lib/src/components/overlay_route.dart index fac2d806968..ad2c13ad080 100644 --- a/packages/flame/lib/src/components/overlay_route.dart +++ b/packages/flame/lib/src/components/overlay_route.dart @@ -4,6 +4,16 @@ import 'package:flame/src/game/game.dart'; import 'package:flutter/widgets.dart' hide Route; import 'package:meta/meta.dart'; +/// [OverlayRoute] is a class that allows adding/removing game overlays as if +/// they were ordinary [Route]s. +/// +/// There are several differences between an [OverlayRoute] and the regular +/// [Route]s: +/// - the overlays are always rendered on top of the game canvas, so if you push +/// a regular route on top of an overlay route, the overlay route would still +/// be displayed on top. +/// - the `builder` of an overlay route produces a widget instead of a +/// component. class OverlayRoute extends Route { OverlayRoute(OverlayBuilder builder, {super.transparent = true}) : _builder = builder, @@ -29,13 +39,15 @@ class OverlayRoute extends Route { @mustCallSuper @override void onPush(Route? previousRoute) { - game.overlays.add(name); + final didAdd = game.overlays.add(name); + assert(didAdd, 'An overlay $name was already added before'); } @mustCallSuper @override void onPop(Route nextRoute) { - game.overlays.remove(name); + final didRemove = game.overlays.remove(name); + assert(didRemove, 'An overlay $name was already removed'); } } diff --git a/packages/flame/lib/src/components/router_component.dart b/packages/flame/lib/src/components/router_component.dart index c53e1870fbd..af89471195b 100644 --- a/packages/flame/lib/src/components/router_component.dart +++ b/packages/flame/lib/src/components/router_component.dart @@ -1,4 +1,5 @@ import 'package:flame/src/components/component.dart'; +import 'package:flame/src/components/overlay_route.dart'; import 'package:flame/src/components/route.dart'; import 'package:meta/meta.dart'; @@ -121,6 +122,21 @@ class RouterComponent extends Component { _adjustRoutesVisibility(); } + /// Puts the overlay route [name] on top of the navigation stack. + /// + /// If [name] was already registered as a name of an overlay route, then this + /// method is equivalent to [pushNamed]. If not, then a new [OverlayRoute] + /// will be created based on the overlay with the same name within the root + /// game. + void pushOverlay(String name) { + if (_routes.containsKey(name)) { + assert(_routes[name] is OverlayRoute, '"$name" is not an overlay route'); + pushNamed(name); + } else { + pushRoute(OverlayRoute.existing(), name: name); + } + } + /// Removes the topmost route from the stack, and also removes it as a child /// of the Router. /// diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index f3d10266151..cd39235c147 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -386,22 +386,20 @@ class _GameWidgetState extends State> { }); } - List _addBackground(BuildContext context, List stackWidgets) { - if (widget.backgroundBuilder == null) { - return stackWidgets; + void _addBackground(BuildContext context, List stackWidgets) { + if (widget.backgroundBuilder != null) { + final backgroundContent = KeyedSubtree( + key: ValueKey(widget.game), + child: widget.backgroundBuilder!(context), + ); + stackWidgets.insert(0, backgroundContent); } - final backgroundContent = KeyedSubtree( - key: ValueKey(widget.game), - child: widget.backgroundBuilder!(context), - ); - stackWidgets.insert(0, backgroundContent); - return stackWidgets; } - List _addOverlays(BuildContext context, List stackWidgets) { - final widgets = currentGame.overlays.buildCurrentOverlayWidgets(context); - stackWidgets.addAll(widgets); - return stackWidgets; + void _addOverlays(BuildContext context, List stackWidgets) { + stackWidgets.addAll( + currentGame.overlays.buildCurrentOverlayWidgets(context), + ); } } diff --git a/packages/flame/lib/src/game/overlay_manager.dart b/packages/flame/lib/src/game/overlay_manager.dart index 2c692f3e1fe..f05a830c23c 100644 --- a/packages/flame/lib/src/game/overlay_manager.dart +++ b/packages/flame/lib/src/game/overlay_manager.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flame/src/game/game.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -9,17 +11,21 @@ class OverlayManager { OverlayManager(this._game); final Game _game; - final Set _activeOverlays = {}; - + final List _activeOverlays = []; final Map _builders = {}; /// The names of all currently active overlays. - Set get value => _activeOverlays; + UnmodifiableListView get activeOverlays { + return UnmodifiableListView(_activeOverlays); + } + + @Deprecated('Use .activeOverlays instead. Will be removed in 1.4.0') + Set get value => _activeOverlays.toSet(); /// Returns if the given [overlayName] is active bool isActive(String overlayName) => _activeOverlays.contains(overlayName); - /// Clear all active overlays. + /// Clears all active overlays. void clear() { _activeOverlays.clear(); _game.refreshWidget(); @@ -55,6 +61,7 @@ class OverlayManager { return true; } + /// Adds a named overlay builder void addEntry(String name, _OverlayBuilderFunction builder) { _builders[name] = builder; } @@ -70,11 +77,9 @@ class OverlayManager { /// Hides multiple overlays specified in [overlayNames]. void removeAll(Iterable overlayNames) { - final overlayCountBeforeRemoved = _activeOverlays.length; - _activeOverlays.removeAll(overlayNames); - - final overlayCountAfterRemoved = _activeOverlays.length; - if (overlayCountBeforeRemoved != overlayCountAfterRemoved) { + final initialCount = _activeOverlays.length; + overlayNames.forEach(_activeOverlays.remove); + if (_activeOverlays.length != initialCount) { _game.refreshWidget(); } } From 53cbe99cf146130a19d1ada97072acded73d290b Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 12 Aug 2022 10:07:58 -0700 Subject: [PATCH 07/12] docs --- doc/flame/router.md | 38 +++++++++++++++++++ .../lib/src/components/overlay_route.dart | 5 +++ 2 files changed, 43 insertions(+) diff --git a/doc/flame/router.md b/doc/flame/router.md index bc600029889..87d312d004f 100644 --- a/doc/flame/router.md +++ b/doc/flame/router.md @@ -44,12 +44,15 @@ class MyGame extends FlameGame { 'level-selector': Route(LevelSelectorPage.new), 'settings': Route(SettingsPage.new, transparent: true), 'pause': PauseRoute(), + 'confirm-dialog': OverlayRoute.existing(), }, initialRoute: 'home', ), ); } } + +class PauseRoute extends Route { ... } ``` [Flutter Navigator]: https://api.flutter.dev/flutter/widgets/Navigator-class.html @@ -62,3 +65,38 @@ mounted as children to the `RouterComponent`. The main property of a `Route` is its `builder` -- the function that creates the component with the content of its page. + +In addition, the routes can be either transparent or opaque (default). An opaque prevents the route +below it from rendering or receiving pointer events, a transparent route doesn't. As a rule of +thumb, declare the route opaque if it is full-screen, and transparent if it is supposed to cover +only a part of the screen. + + +## OverlayRoute + +The **OverlayRoute** is a special route that allows adding game overlays via the Router. These +routes are transparent by default. + +There are two constructors for the `OverlayRoute`. The first constructor requires a builder function +that describes how the overlay's widget is to be built. The second constructor can be used when the +builder function was already specified within the `GameWidget`: +```dart +final router = RouterComponent( + routes: { + 'ok-dialog': OverlayRoute( + (context, game) { + return Center( + child: DecoratedContainer(...), + ); + }, + ), // OverlayRoute + 'confirm-dialog': OverlayRoute.existing(), + }, +); +``` + +Overlays that were defined within the `GameWidget` don't even need to be declared within the routes +map beforehand: the `RouterComponent.pushOverlay()` method can do it for you. Once an overlay route +was registered, it can be activated either via the regular `.pushNamed()` method, or via the +`.pushOverlay()` -- the two method will do exactly the same, though you can use the second one to +make it more clear in your code that an overlay is being added instead of a regular route. diff --git a/packages/flame/lib/src/components/overlay_route.dart b/packages/flame/lib/src/components/overlay_route.dart index ad2c13ad080..73079e58bbf 100644 --- a/packages/flame/lib/src/components/overlay_route.dart +++ b/packages/flame/lib/src/components/overlay_route.dart @@ -15,10 +15,15 @@ import 'package:meta/meta.dart'; /// - the `builder` of an overlay route produces a widget instead of a /// component. class OverlayRoute extends Route { + /// An overlay route that uses the specified [builder]. This builder will be + /// registered with the Game's map of overlay builders when this route is + /// first activated. OverlayRoute(OverlayBuilder builder, {super.transparent = true}) : _builder = builder, super(null); + /// An overlay route that corresponds to an overlay that was already declared + /// within GameWidget's `overlayBuilderMap`. OverlayRoute.existing({super.transparent = true}) : _builder = null, super(null); From df938765248704aa90215f5a790ad168825667e1 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 12 Aug 2022 10:16:31 -0700 Subject: [PATCH 08/12] refactor overlay tests --- .../test/game/overlays_manager_test.dart | 141 ++++++++---------- 1 file changed, 59 insertions(+), 82 deletions(-) diff --git a/packages/flame/test/game/overlays_manager_test.dart b/packages/flame/test/game/overlays_manager_test.dart index cc00b8e2463..b27cbf13bcf 100644 --- a/packages/flame/test/game/overlays_manager_test.dart +++ b/packages/flame/test/game/overlays_manager_test.dart @@ -7,7 +7,7 @@ import '../_resources/custom_flame_game.dart'; void main() { group('OverlaysManager', () { testWidgets( - 'Overlay can be added via initialActiveOverlays', + 'overlay can be added via initialActiveOverlays', (tester) async { const key1 = ValueKey('one'); const key2 = ValueKey('two'); @@ -29,7 +29,7 @@ void main() { ); testWidgets( - 'Overlay can be added in onLoad', + 'overlay can be added in onLoad', (tester) async { const key1 = ValueKey('one'); const key2 = ValueKey('two'); @@ -54,7 +54,7 @@ void main() { ); testWidgets( - 'Overlay can be added and removed at runtime', + 'overlay can be added and removed at runtime', (tester) async { const key1 = ValueKey('one'); const key2 = ValueKey('two'); @@ -85,96 +85,73 @@ void main() { }, ); - group('add', () { - test('can add an overlay', () { - final overlays = FlameGame().overlays - ..addEntry('test', (ctx, game) => Container()); - final added = overlays.add('test'); - expect(added, true); - expect(overlays.isActive('test'), true); - }); - - test('would not add same overlay twice', () { - final overlays = FlameGame().overlays - ..addEntry('test', (ctx, game) => Container()); - overlays.add('test'); - final added = overlays.add('test'); - expect(added, false); - }); + test('can add an overlay', () { + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); + final added = overlays.add('test'); + expect(added, true); + expect(overlays.isActive('test'), true); + + final added2 = overlays.add('test'); + expect(added2, false); + expect(overlays.isActive('test'), true); + expect(overlays.activeOverlays.length, 1); }); - group('addAll', () { - test('can add multiple overlays at once', () { - final overlays = FlameGame().overlays - ..addEntry('test', (ctx, game) => Container()) - ..addEntry('test2', (ctx, game) => Container()); - overlays.addAll(['test', 'test2']); - expect(overlays.isActive('test'), true); - expect(overlays.isActive('test2'), true); - }); + test('can add multiple overlays at once', () { + final overlays = FlameGame().overlays + ..addEntry('test1', (ctx, game) => Container()) + ..addEntry('test2', (ctx, game) => Container()); + overlays.addAll(['test1', 'test2']); + expect(overlays.isActive('test1'), true); + expect(overlays.isActive('test2'), true); + expect(overlays.activeOverlays.length, 2); }); - group('removeAll', () { - test('can remove multiple overlays at once', () { - final overlays = FlameGame().overlays - ..addEntry('test', (ctx, game) => Container()) - ..addEntry('test2', (ctx, game) => Container()); - overlays.addAll(['test', 'test2']); + test('can remove an overlay', () { + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); + overlays.add('test'); - overlays.removeAll(['test', 'test2']); - - expect(overlays.isActive('test'), false); - expect(overlays.isActive('test2'), false); - }); + final didRemove = overlays.remove('test'); + expect(didRemove, true); + expect(overlays.isActive('test'), false); }); - group('remove', () { - test('can remove an overlay', () { - final overlays = FlameGame().overlays - ..addEntry('test', (ctx, game) => Container()); - overlays.add('test'); - - final removed = overlays.remove('test'); - expect(removed, true); - expect(overlays.isActive('test'), false); - }); - - test('will not result in removal if there is nothing to remove', () { - final overlays = FlameGame().overlays - ..addEntry('test', (ctx, game) => Container()); - final removed = overlays.remove('test'); - expect(removed, false); - }); - }); + test('will not result in removal if there is nothing to remove', () { + final overlays = FlameGame().overlays + ..addEntry('test', (ctx, game) => Container()); - group('isActive', () { - test('is true when overlay is active', () { - final overlays = FlameGame().overlays - ..addEntry('test', (ctx, game) => Container()); - overlays.add('test'); - expect(overlays.isActive('test'), true); - }); - - test('is false when overlay is active', () { - final overlays = FlameGame().overlays - ..addEntry('test', (ctx, game) => Container()); - expect(overlays.isActive('test'), false); - }); + final didRemove = overlays.remove('test'); + expect(didRemove, false); }); - group('clear', () { - test('clears all overlays', () { - final overlays = FlameGame().overlays - ..addEntry('test1', (ctx, game) => Container()) - ..addEntry('test2', (ctx, game) => Container()); - overlays.add('test1'); - overlays.add('test2'); - - overlays.clear(); + test('can remove multiple overlays at once', () { + final overlays = FlameGame().overlays + ..addEntry('test1', (ctx, game) => Container()) + ..addEntry('test2', (ctx, game) => Container()) + ..addEntry('test3', (ctx, game) => Container()); + overlays.addAll(['test1', 'test2', 'test3']); + expect(overlays.activeOverlays.length, 3); + + overlays.removeAll(['test1', 'test2']); + expect(overlays.isActive('test1'), false); + expect(overlays.isActive('test2'), false); + expect(overlays.isActive('test3'), true); + expect(overlays.activeOverlays.length, 1); + }); - expect(overlays.isActive('test1'), false); - expect(overlays.isActive('test2'), false); - }); + test('clears all overlays', () { + final overlays = FlameGame().overlays + ..addEntry('test1', (ctx, game) => Container()) + ..addEntry('test2', (ctx, game) => Container()); + overlays.add('test1'); + overlays.add('test2'); + + overlays.clear(); + expect(overlays.isActive('test1'), false); + expect(overlays.isActive('test2'), false); + expect(overlays.activeOverlays.length, 0); }); }); } From 0a30588a22a9d2211097f6414364d59ec9136ef5 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 12 Aug 2022 11:10:52 -0700 Subject: [PATCH 09/12] added tests --- packages/flame/lib/game.dart | 1 + .../flame/lib/src/game/overlay_manager.dart | 2 +- .../components/router_component_test.dart | 59 +++++++++++++++++++ .../test/game/overlays_manager_test.dart | 9 +++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/flame/lib/game.dart b/packages/flame/lib/game.dart index 583831b0524..c6441a5e651 100644 --- a/packages/flame/lib/game.dart +++ b/packages/flame/lib/game.dart @@ -1,6 +1,7 @@ /// {@canonicalFor text.TextPaint} /// {@canonicalFor text.TextRenderer} export 'src/collisions/has_collision_detection.dart'; +export 'src/components/overlay_route.dart' show OverlayRoute; export 'src/components/route.dart' show Route; export 'src/components/router_component.dart' show RouterComponent; export 'src/extensions/vector2.dart'; diff --git a/packages/flame/lib/src/game/overlay_manager.dart b/packages/flame/lib/src/game/overlay_manager.dart index f05a830c23c..e0c92f6fc65 100644 --- a/packages/flame/lib/src/game/overlay_manager.dart +++ b/packages/flame/lib/src/game/overlay_manager.dart @@ -52,7 +52,7 @@ class OverlayManager { bool _addImpl(String name) { assert( _builders.containsKey(name), - 'Trying to add an unknown overlay $name', + 'Trying to add an unknown overlay "$name"', ); if (_activeOverlays.contains(name)) { return false; diff --git a/packages/flame/test/components/router_component_test.dart b/packages/flame/test/components/router_component_test.dart index dd8a895dad3..48d7fccceea 100644 --- a/packages/flame/test/components/router_component_test.dart +++ b/packages/flame/test/components/router_component_test.dart @@ -1,6 +1,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/widgets.dart' hide Route, OverlayRoute; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -170,6 +171,64 @@ void main() { expect(router.children.length, 1); expect(router.currentRoute.name, 'A'); }); + + testWidgets( + 'can handle overlays via the Router', + (tester) async { + const key1 = ValueKey('one'); + const key2 = ValueKey('two'); + const key3 = ValueKey('three'); + final game = FlameGame(); + final router = RouterComponent( + initialRoute: 'home', + routes: {'home': Route(Component.new)}, + )..addToParent(game); + + await tester.pumpWidget( + GameWidget( + game: game, + overlayBuilderMap: { + 'first!': (_, __) => Container(key: key1), + 'second': (_, __) => Container(key: key2), + }, + ), + ); + await tester.pump(); + await tester.pump(); + expect(router.stack.length, 1); + expect(router.currentRoute.name, 'home'); + expect(game.overlays.activeOverlays.isEmpty, true); + + router.pushOverlay('second'); + await tester.pump(); + expect(game.overlays.activeOverlays, ['second']); + expect(find.byKey(key1), findsNothing); + expect(find.byKey(key2), findsOneWidget); + + router.pop(); + await tester.pump(); + expect(game.overlays.activeOverlays.isEmpty, true); + expect(find.byKey(key1), findsNothing); + expect(find.byKey(key2), findsNothing); + + router.pushOverlay('first!'); + router.pushOverlay('second'); + await tester.pump(); + expect(game.overlays.activeOverlays, ['first!', 'second']); + expect(find.byKey(key1), findsOneWidget); + expect(find.byKey(key2), findsOneWidget); + + router.pushRoute( + OverlayRoute((ctx, game) => Container(key: key3)), + name: 'new-route', + ); + await tester.pump(); + expect(game.overlays.activeOverlays, ['first!', 'second', 'new-route']); + expect(find.byKey(key1), findsOneWidget); + expect(find.byKey(key2), findsOneWidget); + expect(find.byKey(key3), findsOneWidget); + }, + ); }); } diff --git a/packages/flame/test/game/overlays_manager_test.dart b/packages/flame/test/game/overlays_manager_test.dart index b27cbf13bcf..b2ec4f2215c 100644 --- a/packages/flame/test/game/overlays_manager_test.dart +++ b/packages/flame/test/game/overlays_manager_test.dart @@ -1,4 +1,5 @@ import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -108,6 +109,14 @@ void main() { expect(overlays.activeOverlays.length, 2); }); + test('cannot add an unknown overlay', () { + final overlays = FlameGame().overlays; + expect( + () => overlays.add('wheelbarrow'), + failsAssert('Trying to add an unknown overlay "wheelbarrow"'), + ); + }); + test('can remove an overlay', () { final overlays = FlameGame().overlays ..addEntry('test', (ctx, game) => Container()); From f39feb7226ec47bf9f20f6e2286efd898a5af37f Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 12 Aug 2022 11:12:11 -0700 Subject: [PATCH 10/12] format --- packages/flame/lib/src/components/overlay_route.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flame/lib/src/components/overlay_route.dart b/packages/flame/lib/src/components/overlay_route.dart index 73079e58bbf..79c07be725d 100644 --- a/packages/flame/lib/src/components/overlay_route.dart +++ b/packages/flame/lib/src/components/overlay_route.dart @@ -25,8 +25,8 @@ class OverlayRoute extends Route { /// An overlay route that corresponds to an overlay that was already declared /// within GameWidget's `overlayBuilderMap`. OverlayRoute.existing({super.transparent = true}) - : _builder = null, - super(null); + : _builder = null, + super(null); final OverlayBuilder? _builder; From d33724125fbd2b78966c9dc088749bdfd1b02381 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 12 Aug 2022 15:30:05 -0700 Subject: [PATCH 11/12] nit --- doc/flame/router.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/flame/router.md b/doc/flame/router.md index 87d312d004f..2d924060122 100644 --- a/doc/flame/router.md +++ b/doc/flame/router.md @@ -74,7 +74,7 @@ only a part of the screen. ## OverlayRoute -The **OverlayRoute** is a special route that allows adding game overlays via the Router. These +The **OverlayRoute** is a special route that allows adding game overlays via the router. These routes are transparent by default. There are two constructors for the `OverlayRoute`. The first constructor requires a builder function From bd607230c52d7aa5bd7d81d8a2b24e5890954737 Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 19 Aug 2022 19:04:42 +0200 Subject: [PATCH 12/12] Add missing } --- packages/flame/lib/src/components/router_component.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flame/lib/src/components/router_component.dart b/packages/flame/lib/src/components/router_component.dart index 80c9b233640..2409af7e19d 100644 --- a/packages/flame/lib/src/components/router_component.dart +++ b/packages/flame/lib/src/components/router_component.dart @@ -136,6 +136,7 @@ class RouterComponent extends Component { } else { pushRoute(OverlayRoute.existing(), name: name); } + } /// Puts [route] on top of the stack and waits until that route is popped. ///