Skip to content

Commit

Permalink
feat: Adding ComponentNotifier API (#1889)
Browse files Browse the repository at this point in the history
This adds a proposal for a new API for flame, the ComponentNotifier.

This API offers the user change notifiers classes that are tied to FlameGame and its components so the user can be notified when a component is added, removed or updated.

This will enable users to:

    Take the benefit of reactive programming inside the game
    Have a simple way of watching certain states from the game, on Flutter Widgets

One important note here is that this proposal does not mean to replace integrations like flame_bloc, but rather provider an simple and out of the box solution, without any need of additional packages, since change notifiers are provided by flutter itself.

Opening this as draft for now to get feedback on the implementation, will write tests and docs once we have the final implementation.
  • Loading branch information
erickzanardo committed Nov 26, 2022
1 parent 51a896b commit bd7f51f
Show file tree
Hide file tree
Showing 13 changed files with 671 additions and 2 deletions.
88 changes: 88 additions & 0 deletions doc/flame/components.md
Expand Up @@ -1085,6 +1085,94 @@ Check the example app
for details on how to use it.


## ComponentsNotifier

Most of the time just accessing children and their attributes is enough to build the logic of
your game.

But sometimes, reactivity can help the developer to simplify and write better code, to help with
that Flame provides the `ComponentsNotifier`, which is an implementation of a
`ChangeNotifier` that notifies listeners every time a component is added, removed or manually
changed.

For example, lets say that we want to show a game over text when the player's lives reach zero.

To make the component automatically report when new instances are added or removed, the `Notifier`
mixin can be applied to the component class:

```dart
class Player extends SpriteComponent with Notifier {}
```

Then to listen to changes on that component the `componentsNotifier` method from `FlameGame` can
be used:

```dart
class MyGame extends FlameGame {
int lives = 2;
Future<void> onLoad() {
final playerNotifier = componentsNotifier<Player>()
..addListener(() {
final player = playerNotifier.single;
if (player == null) {
lives--;
if (lives == 0) {
add(GameOverComponent());
} else {
add(Player());
}
}
});
}
}
```

A `Notifier` component can also manually notify its listeners that something changed. Lets expand
the example above to make a hud component to blink when the player has half of their health. In
order to do so, we need that the `Player` component notify a change manually, example:

```dart
class Player extends SpriteComponent with Notifier {
double health = 1;
void takeHit() {
health -= .1;
if (health == 0) {
removeFromParent();
} else if (health <= .5) {
notifyListeners();
}
}
}
```

Then our hud component could look like:

```dart
class Hud extends PositionComponent with HasGameRef {
Future<void> onLoad() {
final playerNotifier = gameRef.componentsNotifier<Player>()
..addListener(() {
final player = playerNotifier.single;
if (player != null) {
if (player.health <= .5) {
add(BlinkEffect());
}
}
});
}
}
```

`ComponentsNotifier`s can also come in handy to rebuild widgets when state changes inside a
`FlameGame`, to help with that Flame provides a `ComponentsNotifierBuilder` widget.

To see an example of its use check the running example
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/components/components_notifier_example.dart);


## ClipComponent

A `ClipComponent` is a component that will clip the canvas to its size and shape. This means that
Expand Down
14 changes: 14 additions & 0 deletions examples/lib/stories/components/components.dart
@@ -1,6 +1,8 @@
import 'package:dashbook/dashbook.dart';
import 'package:examples/commons/commons.dart';
import 'package:examples/stories/components/clip_component_example.dart';
import 'package:examples/stories/components/components_notifier_example.dart';
import 'package:examples/stories/components/components_notifier_provider_example.dart';
import 'package:examples/stories/components/composability_example.dart';
import 'package:examples/stories/components/debug_example.dart';
import 'package:examples/stories/components/game_in_game_example.dart';
Expand Down Expand Up @@ -52,5 +54,17 @@ void addComponentsStories(Dashbook dashbook) {
(_) => GameWidget(game: LookAtSmoothExample()),
codeLink: baseLink('components/look_at_smooth_example.dart'),
info: LookAtExample.description,
)
..add(
'Component Notifier',
(_) => const ComponentsNotifierExampleWidget(),
codeLink: baseLink('components/component_notifier_example.dart'),
info: ComponentsNotifierExampleWidget.description,
)
..add(
'Component Notifier (with provider)',
(_) => const ComponentsNotifierProviderExampleWidget(),
codeLink: baseLink('components/component_notifier_provider_example.dart'),
info: ComponentsNotifierProviderExampleWidget.description,
);
}
111 changes: 111 additions & 0 deletions examples/lib/stories/components/components_notifier_example.dart
@@ -0,0 +1,111 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/widgets.dart';
import 'package:flutter/material.dart';

class ComponentsNotifierExampleWidget extends StatefulWidget {
const ComponentsNotifierExampleWidget({super.key});

static String description = '''
Showcases how the components notifier can be used between
a flame game instance and widgets.
Tap the red dots to defeat the enemies and see the hud being updated
to reflect the current state of the game.
''';

@override
State<ComponentsNotifierExampleWidget> createState() =>
_ComponentsNotifierExampleWidgetState();
}

class _ComponentsNotifierExampleWidgetState
extends State<ComponentsNotifierExampleWidget> {
@override
void initState() {
super.initState();

game = ComponentNotifierExample();
}

late final ComponentNotifierExample game;

@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: GameWidget(game: game),
),
Positioned(
left: 16,
top: 16,
child: ComponentsNotifierBuilder<Enemy>(
notifier: game.componentsNotifier<Enemy>(),
builder: (context, notifier) {
return GameHud(
remainingEnemies: notifier.components.length,
onReplay: game.replay,
);
},
),
),
],
),
);
}
}

class GameHud extends StatelessWidget {
const GameHud({
super.key,
required this.remainingEnemies,
required this.onReplay,
});

final int remainingEnemies;
final VoidCallback onReplay;

@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: remainingEnemies == 0
? ElevatedButton(
onPressed: onReplay,
child: const Text('Play again'),
)
: Text('Remaining enemies: $remainingEnemies'),
),
);
}
}

class Enemy extends CircleComponent with Tappable, Notifier {
Enemy({super.position})
: super(
radius: 20,
paint: Paint()..color = const Color(0xFFFF0000),
);

@override
bool onTapUp(_) {
removeFromParent();
return true;
}
}

class ComponentNotifierExample extends FlameGame with HasTappables {
@override
Future<void> onLoad() async {
replay();
}

void replay() {
add(Enemy(position: Vector2(100, 100)));
add(Enemy(position: Vector2(200, 100)));
add(Enemy(position: Vector2(300, 100)));
}
}
@@ -0,0 +1,105 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ComponentsNotifierProviderExampleWidget extends StatefulWidget {
const ComponentsNotifierProviderExampleWidget({super.key});

static String description = '''
Similar to the Components Notifier example, but uses provider
instead of the built in ComponentsNotifierBuilder widget.
''';

@override
State<ComponentsNotifierProviderExampleWidget> createState() =>
_ComponentsNotifierProviderExampleWidgetState();
}

class _ComponentsNotifierProviderExampleWidgetState
extends State<ComponentsNotifierProviderExampleWidget> {
@override
void initState() {
super.initState();

game = ComponentNotifierExample();
}

late final ComponentNotifierExample game;

@override
Widget build(BuildContext context) {
return Scaffold(
body: MultiProvider(
providers: [
Provider<ComponentNotifierExample>.value(value: game),
ChangeNotifierProvider<ComponentsNotifier<Enemy>>(
create: (_) => game.componentsNotifier<Enemy>(),
),
],
child: Stack(
children: [
Positioned.fill(
child: GameWidget(game: game),
),
const Positioned(
left: 16,
top: 16,
child: GameHud(),
),
],
),
),
);
}
}

class GameHud extends StatelessWidget {
const GameHud({super.key});

@override
Widget build(BuildContext context) {
final enemies = context.watch<ComponentsNotifier<Enemy>>().components;

return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: enemies.isEmpty
? ElevatedButton(
child: const Text('Play again'),
onPressed: () {
context.read<ComponentNotifierExample>().replay();
},
)
: Text('Remaining enemies: ${enemies.length}'),
),
);
}
}

class Enemy extends CircleComponent with Tappable, Notifier {
Enemy({super.position})
: super(
radius: 20,
paint: Paint()..color = const Color(0xFFFF0000),
);

@override
bool onTapUp(_) {
removeFromParent();
return true;
}
}

class ComponentNotifierExample extends FlameGame with HasTappables {
@override
Future<void> onLoad() async {
replay();
}

void replay() {
add(Enemy(position: Vector2(100, 100)));
add(Enemy(position: Vector2(200, 100)));
add(Enemy(position: Vector2(300, 100)));
}
}
1 change: 1 addition & 0 deletions examples/pubspec.yaml
Expand Up @@ -23,6 +23,7 @@ dependencies:
google_fonts: ^2.3.2
meta: ^1.8.0
padracing: ^1.0.0
provider: ^6.0.3
rogue_shooter: ^0.1.0
trex_game: ^0.1.0

Expand Down
2 changes: 2 additions & 0 deletions packages/flame/lib/components.dart
Expand Up @@ -3,6 +3,7 @@ export 'src/anchor.dart';
export 'src/collisions/has_collision_detection.dart';
export 'src/collisions/hitboxes/screen_hitbox.dart';
export 'src/components/clip_component.dart';
export 'src/components/components_notifier.dart';
export 'src/components/core/component.dart';
export 'src/components/core/component_set.dart';
export 'src/components/core/position_type.dart';
Expand All @@ -21,6 +22,7 @@ export 'src/components/mixins/has_game_ref.dart' show HasGameRef;
export 'src/components/mixins/has_paint.dart';
export 'src/components/mixins/hoverable.dart';
export 'src/components/mixins/keyboard_handler.dart';
export 'src/components/mixins/notifier.dart';
export 'src/components/mixins/parent_is_a.dart';
export 'src/components/mixins/single_child_particle.dart';
export 'src/components/mixins/tappable.dart';
Expand Down

0 comments on commit bd7f51f

Please sign in to comment.