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(overlays): Added the 'priority' parameter for overlays #3349

Merged
merged 5 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions doc/flame/overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ by providing an `overlayBuilderMap`.
```dart
// Inside your game:
final pauseOverlayIdentifier = 'PauseMenu';
final secondaryOverlayIdentifier = 'SecondaryMenu';

// Marks 'PauseMenu' to be rendered.
// Marks 'SecondaryMenu' to be rendered.
overlays.add(secondaryOverlayIdentifier, priority: 1);
// Marks 'PauseMenu' to be rendered. Priority = 0 by default
// which means the 'PauseMenu' will be displayed under the 'SecondaryMenu'
overlays.add(pauseOverlayIdentifier);
// Marks 'PauseMenu' to not be rendered.
// Marks 'PauseMenu' to not be rendered.
overlays.remove(pauseOverlayIdentifier);
```

Expand All @@ -34,6 +38,9 @@ Widget build(BuildContext context) {
'PauseMenu': (BuildContext context, MyGame game) {
return Text('A pause menu');
},
'SecondaryMenu': (BuildContext context, MyGame game) {
return Text('A secondary menu');
},
},
);
}
Expand Down
56 changes: 47 additions & 9 deletions examples/lib/stories/system/overlays_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class OverlaysExample extends FlameGame with TapDetector {
..anchor = Anchor.center
..size = Vector2.all(100),
);

// 'SecondaryMenu' will be displayed above 'PauseMenu'
overlays.add('SecondaryMenu', priority: 1);
}

@override
Expand All @@ -45,14 +48,44 @@ class OverlaysExample extends FlameGame with TapDetector {
}
}

Widget _pauseMenuBuilder(BuildContext buildContext, OverlaysExample game) {
Widget _pauseMenuBuilder(
BuildContext buildContext,
OverlaysExample game,
GestureTapCallback? onTap,
) {
return Center(
child: Container(
width: 100,
height: 100,
color: Colors.orange,
child: const Center(
child: Text('Paused'),
child: GestureDetector(
onTap: onTap,
child: Container(
width: 100,
height: 100,
color: Colors.orange,
child: const Center(
child: Text('Paused'),
),
),
),
);
}

Widget _secondaryMenuBuilder(BuildContext buildContext, OverlaysExample game) {
return Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 100,
height: 50,
alignment: Alignment.center,
color: Colors.red,
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.music_off_rounded),
Icon(Icons.info),
Icon(Icons.star),
],
),
),
),
);
Expand All @@ -61,8 +94,13 @@ Widget _pauseMenuBuilder(BuildContext buildContext, OverlaysExample game) {
Widget overlayBuilder(DashbookContext ctx) {
return GameWidget<OverlaysExample>(
game: OverlaysExample()..paused = true,
overlayBuilderMap: const {
'PauseMenu': _pauseMenuBuilder,
overlayBuilderMap: {
'PauseMenu': (context, game) => _pauseMenuBuilder(
context,
game,
() => game.onTap(),
),
'SecondaryMenu': _secondaryMenuBuilder,
},
initialActiveOverlays: const ['PauseMenu'],
);
Expand Down
62 changes: 48 additions & 14 deletions packages/flame/lib/src/game/overlay_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ class OverlayManager {
OverlayManager(this._game);

final Game _game;
final List<String> _activeOverlays = [];
final List<_OverlayData> _activeOverlays = [];
final Map<String, OverlayBuilderFunction> _builders = {};

/// The names of all currently active overlays.
UnmodifiableListView<String> get activeOverlays {
return UnmodifiableListView(_activeOverlays);
return UnmodifiableListView(_activeOverlays.map((overlay) => overlay.name));
}

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

/// Clears all active overlays.
void clear() {
Expand All @@ -29,8 +30,10 @@ class OverlayManager {
}

/// Marks the [overlayName] to be rendered.
bool add(String overlayName) {
final setChanged = _addImpl(overlayName);
/// [priority] is used to sort widgets for [buildCurrentOverlayWidgets]
/// The smaller the priority, the sooner your component will be build.
bool add(String overlayName, {int priority = 0}) {
final setChanged = _addImpl(priority: priority, name: overlayName);
if (setChanged) {
_game.refreshWidget(isInternalRefresh: false);
}
Expand All @@ -40,32 +43,38 @@ class OverlayManager {
/// Marks [overlayNames] to be rendered.
void addAll(Iterable<String> overlayNames) {
final initialCount = _activeOverlays.length;
overlayNames.forEach(_addImpl);
overlayNames.forEach((overlayName) => _addImpl(name: overlayName));
if (initialCount != _activeOverlays.length) {
_game.refreshWidget(isInternalRefresh: false);
}
}

bool _addImpl(String name) {
bool _addImpl({required String name, int priority = 0}) {
assert(
_builders.containsKey(name),
'Trying to add an unknown overlay "$name"',
);
if (_activeOverlays.contains(name)) {
if (isActive(name)) {
return false;
}
_activeOverlays.add(name);
_activeOverlays.add(_OverlayData(priority: priority, name: name));
_activeOverlays.sort(_compare);
return true;
}

_OverlayData? _getOverlay(String name) {
return _activeOverlays.where((overlay) => overlay.name == name).firstOrNull;
}

/// Adds a named overlay builder
void addEntry(String name, OverlayBuilderFunction builder) {
_builders[name] = builder;
}

/// Hides the [overlayName].
bool remove(String overlayName) {
final hasRemoved = _activeOverlays.remove(overlayName);
final overlay = _getOverlay(overlayName);
final hasRemoved = _activeOverlays.remove(overlay);
if (hasRemoved) {
_game.refreshWidget(isInternalRefresh: false);
}
Expand All @@ -75,7 +84,8 @@ class OverlayManager {
/// Hides multiple overlays specified in [overlayNames].
void removeAll(Iterable<String> overlayNames) {
final initialCount = _activeOverlays.length;
overlayNames.forEach(_activeOverlays.remove);
_activeOverlays
.removeWhere((overlay) => overlayNames.contains(overlay.name));
if (_activeOverlays.length != initialCount) {
_game.refreshWidget(isInternalRefresh: false);
}
Expand All @@ -84,20 +94,44 @@ class OverlayManager {
@internal
List<Widget> buildCurrentOverlayWidgets(BuildContext context) {
final widgets = <Widget>[];
for (final overlayName in _activeOverlays) {
final builder = _builders[overlayName]!;
for (final overlay in _activeOverlays) {
final builder = _builders[overlay.name]!;
widgets.add(
KeyedSubtree(
key: ValueKey(overlayName),
key: ValueKey(overlay),
child: builder(context, _game),
),
);
}
return widgets;
}

/// Comparator function used to sort overlays.
int _compare(_OverlayData a, _OverlayData b) {
return a.priority - b.priority;
}
}

typedef OverlayBuilderFunction = Widget Function(
BuildContext context,
Game game,
);

@immutable
class _OverlayData {
final int priority;
final String name;

const _OverlayData({required this.priority, required this.name});

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _OverlayData &&
runtimeType == other.runtimeType &&
priority == other.priority &&
name == other.name;

@override
int get hashCode => priority.hashCode ^ name.hashCode;
}
12 changes: 12 additions & 0 deletions packages/flame/test/game/overlays_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ void main() {
expect(overlays.activeOverlays.length, 2);
});

test('can add multiple overlays with priorities', () {
final overlays = FlameGame().overlays
..addEntry('test1', (ctx, game) => Container())
..addEntry('test2', (ctx, game) => Container());
overlays.add('test1', priority: 1);
overlays.add('test2');
expect(overlays.activeOverlays, ['test2', 'test1']);
expect(overlays.isActive('test1'), true);
expect(overlays.isActive('test2'), true);
expect(overlays.activeOverlays.length, 2);
});

test('cannot add an unknown overlay', () {
final overlays = FlameGame().overlays;
expect(
Expand Down