Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added ability to control overlays via the RouterComponent #1840

Merged
merged 14 commits into from
Aug 19, 2022
39 changes: 38 additions & 1 deletion doc/flame/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -63,6 +66,40 @@ 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.

## ValueRoute

Expand Down Expand Up @@ -120,4 +157,4 @@ In order to use `ValueRoute`s, two steps are required:
// ... the user was not so sure
}
}
```
```
1 change: 1 addition & 0 deletions packages/flame/lib/game.dart
Original file line number Diff line number Diff line change
@@ -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/components/value_route.dart' show ValueRoute;
Expand Down
59 changes: 59 additions & 0 deletions packages/flame/lib/src/components/overlay_route.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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';

/// [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 {
/// 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);

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) {
final didAdd = game.overlays.add(name);
assert(didAdd, 'An overlay $name was already added before');
}

@mustCallSuper
@override
void onPop(Route nextRoute) {
final didRemove = game.overlays.remove(name);
assert(didRemove, 'An overlay $name was already removed');
}
}

typedef OverlayBuilder = Widget Function(BuildContext context, Game game);
16 changes: 16 additions & 0 deletions packages/flame/lib/src/components/router_component.dart
Original file line number Diff line number Diff line change
@@ -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:flame/src/components/value_route.dart';
import 'package:meta/meta.dart';
Expand Down Expand Up @@ -122,6 +123,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);
}
}

/// Puts [route] on top of the stack and waits until that route is popped.
///
/// More precisely, this method returns a future that can be awaited until
Expand Down
71 changes: 6 additions & 65 deletions packages/flame/lib/src/game/game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -316,7 +317,7 @@ abstract class Game {

set mouseCursor(MouseCursor value) {
_mouseCursor = value;
_refreshWidget();
refreshWidget();
}

@visibleForTesting
Expand All @@ -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<String> _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<String> 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<String> 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<String> get value => _activeOverlays;

/// Returns if the given [overlayName] is active
bool isActive(String overlayName) => _activeOverlays.contains(overlayName);
}
Loading