Skip to content

Commit

Permalink
feat!: Adding GameWidget.controlled (#1650)
Browse files Browse the repository at this point in the history
* feat: Adding GameWidget.controlled

* follow up

* Update doc/flame/game.md

* follow up

* Update doc/flame/game.md

Co-authored-by: Lukas Klingsbo <me@lukas.fyi>

* Update packages/flame/lib/src/game/game_widget/game_widget.dart

Co-authored-by: Lukas Klingsbo <me@lukas.fyi>

* Update according to comments

* Move out GameWidget docs

* Update game widget docs

* Update docs

* Update GameWidget.controlled example

* Update .controlled dartdocs

* Updated .controlled dartdocs

* Add example to game_widget.md

* Apply suggestions from code review

Co-authored-by: Renan <6718144+renancaraujo@users.noreply.github.com>

Co-authored-by: Renan Araujo <renan.araujo@verygood.ventures>
Co-authored-by: Renan <6718144+renancaraujo@users.noreply.github.com>
Co-authored-by: Lukas Klingsbo <me@lukas.fyi>
  • Loading branch information
4 people authored Jun 2, 2022
1 parent 4ca65f8 commit 7ef6a51
Show file tree
Hide file tree
Showing 6 changed files with 491 additions and 51 deletions.
1 change: 1 addition & 0 deletions doc/flame/flame.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:hidden:
File structure <structure.md>
GameWidget <game_widget.md>
Game loop <game.md>
Components <components.md>
Platforms <platforms.md>
Expand Down
65 changes: 65 additions & 0 deletions doc/flame/game_widget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# GameWidget

The `GameWidget` is a Flutter `Widget` that is used to insert a [`Game`](game.md) inside the Flutter
widget tree.

It can directly receive a `Game` instance in its default constructor or it can receive a
`GameFactory` function on the `controlled` constructor that will be used to create the game once the
`GameWidget` is inserted in the widget tree.

## Examples

Directly in `runApp`, with either:

```dart
void main() {
final game = MyGame();
runApp(GameWidget(game: game));
}
```

Or:

```dart
void main() {
runApp(GameWidget.controlled(gameFactory: MyGame.new));
}
```

In a `StatefulWidget`:

```dart
class MyGamePage extends StatefulWidget {
@override
State createState() => _MyGamePageState();
}
class _MyGamePageState extends State<MyGamePage> {
late final MyGame _game;
@override
void initState() {
super.initState();
_game = MyGame();
}
@override
void build(BuildContext context) {
return GameWidget(game: _game);
}
}
```

In a `StatelessWidget` with the `gameFactory` argument:

```dart
class MyGamePage extends StatelessWidget {
@override
void build(BuildContext context) {
return GameWidget.controlled(gameFactory: MyGame.new);
}
}
```

Do note that if the `GameWidget.controlled` constructor is used, the `GameWidget.game` field will
always be null.
127 changes: 93 additions & 34 deletions packages/flame/lib/src/game/game_widget/game_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,19 @@ typedef OverlayWidgetBuilder<T extends Game> = Widget Function(
T game,
);

typedef GameFactory<T extends Game> = T Function();

/// A [StatefulWidget] that is in charge of attaching a [Game] instance into the
/// Flutter tree.
class GameWidget<T extends Game> extends StatefulWidget {
/// The game instance in which this widget will render
final T game;
/// The game instance which this widget will render, if the normal constructor
/// is used.
/// If the [GameWidget.controlled] constructor is used, this will always be
/// `null`.
final T? game;

/// A function that creates a [Game] that this widget will render.
final GameFactory<T>? gameFactory;

/// The text direction to be used in text elements in a game.
final TextDirection? textDirection;
Expand Down Expand Up @@ -64,10 +72,18 @@ class GameWidget<T extends Game> extends StatefulWidget {
///
/// Ex:
/// ```
/// // Inside a State...
/// late MyGameClass game;
///
/// @override
/// void initState() {
/// super.initState();
/// game = MyGameClass();
/// }
/// ...
/// Widget build(BuildContext context) {
/// return GameWidget(
/// game: MyGameClass(),
/// game: game,
/// )
/// }
/// ...
Expand Down Expand Up @@ -100,7 +116,7 @@ class GameWidget<T extends Game> extends StatefulWidget {
/// ```
GameWidget({
Key? key,
required this.game,
required T this.game,
this.textDirection,
this.loadingBuilder,
this.errorBuilder,
Expand All @@ -110,15 +126,49 @@ class GameWidget<T extends Game> extends StatefulWidget {
this.focusNode,
this.autofocus = true,
MouseCursor? mouseCursor,
}) : super(key: key) {
}) : gameFactory = null,
super(key: key) {
if (mouseCursor != null) {
game.mouseCursor = mouseCursor;
game!.mouseCursor = mouseCursor;
}
if (initialActiveOverlays != null) {
game.overlays.addAll(initialActiveOverlays);
game!.overlays.addAll(initialActiveOverlays);
}
}

/// Creates a new game instance with the [gameFactory] and then
/// renders that game in the Flutter widget tree.
///
/// Unlike the default constructor [GameWidget.new], this creates a
/// [GameWidget] that controls the creation and disposal of the game instance.
///
/// This removes the necessity of creating the game class outside of the
/// widget or to wrap the [GameWidget] inside of a [StatefulWidget], to keep
/// the state of the game.
///
/// Example:
/// ```
/// ...
/// Widget build(BuildContext context) {
/// return GameWidget.controlled(
/// gameFactory: MyGameClass.new,
/// )
/// }
/// ...
/// ```
const GameWidget.controlled({
Key? key,
required GameFactory<T> this.gameFactory,
this.textDirection,
this.loadingBuilder,
this.errorBuilder,
this.backgroundBuilder,
this.overlayBuilderMap,
this.focusNode,
this.autofocus = true,
}) : game = null,
super(key: key);

/// Renders a [game] in a flutter widget tree alongside widgets overlays.
///
/// To use overlays, the game subclass has to be mixed with HasWidgetsOverlay.
Expand All @@ -127,13 +177,15 @@ class GameWidget<T extends Game> extends StatefulWidget {
}

class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
late T currentGame;

Future<void> get loaderFuture => _loaderFuture ??= (() async {
assert(widget.game.hasLayout);
final onLoad = widget.game.onLoadFuture;
assert(currentGame.hasLayout);
final onLoad = currentGame.onLoadFuture;
if (onLoad != null) {
await onLoad;
}
widget.game.onMount();
currentGame.onMount();
})();

Future<void>? _loaderFuture;
Expand Down Expand Up @@ -181,10 +233,22 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
}
}

void initCurrentGame() {
final widgetGame = widget.game;
currentGame = widgetGame ?? widget.gameFactory!.call();
currentGame.addGameStateListener(_onGameStateChange);
_loaderFuture = null;
}

void disposeCurrentGame() {
currentGame.removeGameStateListener(_onGameStateChange);
currentGame.onRemove();
}

@override
void initState() {
super.initState();
widget.game.addGameStateListener(_onGameStateChange);
initCurrentGame();
_focusNode = widget.focusNode ?? FocusNode();
if (widget.autofocus) {
_focusNode.requestFocus();
Expand All @@ -194,21 +258,17 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
@override
void didUpdateWidget(GameWidget<T> oldWidget) {
super.didUpdateWidget(oldWidget);

if (oldWidget.game != widget.game) {
// Reset the loaderFuture so that onMount will run again
// (onLoad is still cached).
oldWidget.game.removeGameStateListener(_onGameStateChange);
oldWidget.game.onRemove();
_loaderFuture = null;
widget.game.addGameStateListener(_onGameStateChange);
disposeCurrentGame();
initCurrentGame();
}
}

@override
void dispose() {
super.dispose();
widget.game.removeGameStateListener(_onGameStateChange);
widget.game.onRemove();
disposeCurrentGame();
// If we received a focus node from the user, they are responsible
// for disposing it
if (widget.focusNode == null) {
Expand Down Expand Up @@ -236,34 +296,33 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
@override
Widget build(BuildContext context) {
return _protectedBuild(() {
final game = widget.game;
Widget internalGameWidget = _GameRenderObjectWidget(game);
Widget internalGameWidget = _GameRenderObjectWidget(currentGame);

_checkOverlays(widget.game.overlays.value);
_checkOverlays(currentGame.overlays.value);
assert(
!(game is MultiTouchDragDetector && game is PanDetector),
!(currentGame is MultiTouchDragDetector && currentGame is PanDetector),
'WARNING: Both MultiTouchDragDetector and a PanDetector detected. '
'The MultiTouchDragDetector will override the PanDetector and it will '
'not receive events',
);

if (hasBasicGestureDetectors(game)) {
if (hasBasicGestureDetectors(currentGame)) {
internalGameWidget = applyBasicGesturesDetectors(
game,
currentGame,
internalGameWidget,
);
}

if (hasAdvancedGestureDetectors(game)) {
if (hasAdvancedGestureDetectors(currentGame)) {
internalGameWidget = applyAdvancedGesturesDetectors(
game,
currentGame,
internalGameWidget,
);
}

if (hasMouseDetectors(game)) {
if (hasMouseDetectors(currentGame)) {
internalGameWidget = applyMouseDetectors(
game,
currentGame,
internalGameWidget,
);
}
Expand All @@ -280,11 +339,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
autofocus: widget.autofocus,
onKey: _handleKeyEvent,
child: MouseRegion(
cursor: widget.game.mouseCursor,
cursor: currentGame.mouseCursor,
child: Directionality(
textDirection: textDir,
child: Container(
color: game.backgroundColor(),
color: currentGame.backgroundColor(),
child: LayoutBuilder(
builder: (_, BoxConstraints constraints) {
return _protectedBuild(() {
Expand All @@ -293,7 +352,7 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
return widget.loadingBuilder?.call(context) ??
Container();
}
game.onGameResize(size);
currentGame.onGameResize(size);
return FutureBuilder(
future: loaderFuture,
builder: (_, snapshot) {
Expand Down Expand Up @@ -341,11 +400,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
if (widget.overlayBuilderMap == null) {
return stackWidgets;
}
final widgets = widget.game.overlays.value.map((String overlayKey) {
final widgets = currentGame.overlays.value.map((String overlayKey) {
final builder = widget.overlayBuilderMap![overlayKey]!;
return KeyedSubtree(
key: ValueKey(overlayKey),
child: builder(context, widget.game),
child: builder(context, currentGame),
);
});
stackWidgets.addAll(widgets);
Expand Down
Loading

0 comments on commit 7ef6a51

Please sign in to comment.