From 2f2ab1341ea55ac6ffc7598b2f66b0a836afad42 Mon Sep 17 00:00:00 2001 From: Renan <6718144+renancaraujo@users.noreply.github.com> Date: Sun, 6 Dec 2020 20:32:30 +0000 Subject: [PATCH] game widget (#533) --- CHANGELOG.md | 2 + doc/examples/animations/lib/main.dart | 8 +- doc/examples/aseprite/lib/main.dart | 6 +- doc/examples/debug/lib/main.dart | 7 +- .../effects/combined_effects/lib/main.dart | 8 +- .../effects/infinite_effects/lib/main.dart | 6 +- .../effects/sequence_effect/lib/main.dart | 6 +- .../effects/simple/lib/main_move.dart | 9 +- .../effects/simple/lib/main_rotate.dart | 8 +- .../effects/simple/lib/main_scale.dart | 6 +- doc/examples/gestures/lib/main.dart | 12 +- .../gestures/lib/main_mouse_movement.dart | 9 +- doc/examples/gestures/lib/main_multitap.dart | 9 +- .../gestures/lib/main_multitap_advanced.dart | 9 +- .../lib/main_overlapping_tapables.dart | 4 +- doc/examples/gestures/lib/main_scroll.dart | 6 +- doc/examples/gestures/lib/main_tapables.dart | 16 +- doc/examples/go_desktop/lib/main.dart | 7 +- doc/examples/go_desktop/lib/main_desktop.dart | 7 +- doc/examples/isometric/lib/main.dart | 7 +- doc/examples/joystick/lib/main.dart | 7 +- doc/examples/keyboard/lib/main.dart | 6 +- doc/examples/layers/lib/main.dart | 8 +- doc/examples/nine_tile_box/lib/main.dart | 6 +- doc/examples/parallax/lib/main.dart | 8 +- doc/examples/particles/lib/main.dart | 11 +- doc/examples/render_flip/lib/main.dart | 6 +- doc/examples/sprite_batch/lib/main.dart | 6 +- doc/examples/sprites/lib/main.dart | 6 +- doc/examples/sprites/lib/main_base64.dart | 11 +- doc/examples/spritesheet/lib/main.dart | 6 +- doc/examples/text/lib/main.dart | 6 +- doc/examples/timer/lib/main.dart | 8 +- .../lib/example_game.dart | 20 +- .../with_widgets_overlay/lib/main.dart | 64 ++- .../lib/main_dynamic_game.dart | 46 -- doc/game.md | 81 +++- example/lib/main.dart | 8 +- lib/flame.dart | 10 +- lib/game.dart | 1 + lib/game/embedded_game_widget.dart | 35 -- lib/game/game.dart | 111 +++-- lib/game/game_render_box.dart | 8 +- .../{widget_builder.dart => game_widget.dart} | 457 ++++++++++++------ test/base_game_test.dart | 7 +- test/effects/effect_test_utils.dart | 7 +- 46 files changed, 711 insertions(+), 391 deletions(-) delete mode 100644 doc/examples/with_widgets_overlay/lib/main_dynamic_game.dart delete mode 100644 lib/game/embedded_game_widget.dart rename lib/game/{widget_builder.dart => game_widget.dart} (55%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e62682f..7106b6722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Translate README to Russian - Split up Component and PositionComponent to BaseComponent - Unify multiple render methods on Sprite + - Refactored how games are inserted into a flutter tree + - Refactored the widgets overlay API ## 1.0.0-rc2 - Improve IsometricTileMap and Spritesheet classes diff --git a/doc/examples/animations/lib/main.dart b/doc/examples/animations/lib/main.dart index be3e2ca3c..629f0df13 100644 --- a/doc/examples/animations/lib/main.dart +++ b/doc/examples/animations/lib/main.dart @@ -1,7 +1,7 @@ +import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; import 'package:flutter/gestures.dart'; import 'package:flame/flame.dart'; -import 'package:flame/game.dart'; import 'package:flame/extensions/vector2.dart'; import 'package:flame/sprite_animation.dart'; import 'package:flame/components/sprite_animation_component.dart'; @@ -13,7 +13,11 @@ void main() async { final Vector2 size = await Flame.util.initialDimensions(); final game = MyGame(size); - runApp(game.widget); + runApp( + GameWidget( + game: game, + ), + ); } class MyGame extends BaseGame with TapDetector { diff --git a/doc/examples/aseprite/lib/main.dart b/doc/examples/aseprite/lib/main.dart index d8fc17f96..83a6c000d 100644 --- a/doc/examples/aseprite/lib/main.dart +++ b/doc/examples/aseprite/lib/main.dart @@ -8,7 +8,11 @@ import 'package:flutter/material.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); final Vector2 size = await Flame.util.initialDimensions(); - runApp(MyGame(size).widget); + runApp( + GameWidget( + game: MyGame(size), + ), + ); } class MyGame extends BaseGame { diff --git a/doc/examples/debug/lib/main.dart b/doc/examples/debug/lib/main.dart index e5fea20f4..93a0d2600 100644 --- a/doc/examples/debug/lib/main.dart +++ b/doc/examples/debug/lib/main.dart @@ -13,8 +13,11 @@ void main() async { Flame.initializeWidget(); await Flame.util.initialDimensions(); - final myGame = MyGame(); - runApp(myGame.widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class AndroidComponent extends SpriteComponent with Resizable { diff --git a/doc/examples/effects/combined_effects/lib/main.dart b/doc/examples/effects/combined_effects/lib/main.dart index 8ac9c97c7..eb63d6963 100644 --- a/doc/examples/effects/combined_effects/lib/main.dart +++ b/doc/examples/effects/combined_effects/lib/main.dart @@ -2,11 +2,11 @@ import 'package:flame/effects/combined_effect.dart'; import 'package:flame/effects/move_effect.dart'; import 'package:flame/effects/scale_effect.dart'; import 'package:flame/effects/rotate_effect.dart'; +import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; import 'package:flame/extensions/offset.dart'; import 'package:flame/extensions/vector2.dart'; import 'package:flame/flame.dart'; -import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import './square.dart'; @@ -14,7 +14,11 @@ import './square.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.util.fullScreen(); - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class MyGame extends BaseGame with TapDetector { diff --git a/doc/examples/effects/infinite_effects/lib/main.dart b/doc/examples/effects/infinite_effects/lib/main.dart index 4ce25c54f..2ede7700e 100644 --- a/doc/examples/effects/infinite_effects/lib/main.dart +++ b/doc/examples/effects/infinite_effects/lib/main.dart @@ -14,7 +14,11 @@ import './square.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.util.fullScreen(); - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class MyGame extends BaseGame with TapDetector { diff --git a/doc/examples/effects/sequence_effect/lib/main.dart b/doc/examples/effects/sequence_effect/lib/main.dart index bffdb78de..1cceb2058 100644 --- a/doc/examples/effects/sequence_effect/lib/main.dart +++ b/doc/examples/effects/sequence_effect/lib/main.dart @@ -15,7 +15,11 @@ import './square.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.util.fullScreen(); - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class MyGame extends BaseGame with TapDetector { diff --git a/doc/examples/effects/simple/lib/main_move.dart b/doc/examples/effects/simple/lib/main_move.dart index 9f4515a16..5dc2bd887 100644 --- a/doc/examples/effects/simple/lib/main_move.dart +++ b/doc/examples/effects/simple/lib/main_move.dart @@ -1,6 +1,6 @@ +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flame/extensions/vector2.dart'; -import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; import 'package:flame/effects/effects.dart'; import 'package:flame/extensions/offset.dart'; @@ -9,6 +9,7 @@ import './square.dart'; class MyGame extends BaseGame with TapDetector { Square square; + MyGame() { add(square = Square() ..x = 100 @@ -36,5 +37,9 @@ class MyGame extends BaseGame with TapDetector { } void main() { - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } diff --git a/doc/examples/effects/simple/lib/main_rotate.dart b/doc/examples/effects/simple/lib/main_rotate.dart index 581a02269..ba318ea9d 100644 --- a/doc/examples/effects/simple/lib/main_rotate.dart +++ b/doc/examples/effects/simple/lib/main_rotate.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:flutter/material.dart'; import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; import 'package:flame/gestures.dart'; import 'package:flame/anchor.dart'; import 'package:flame/effects/effects.dart'; @@ -30,5 +30,9 @@ class MyGame extends BaseGame with TapDetector { } void main() { - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } diff --git a/doc/examples/effects/simple/lib/main_scale.dart b/doc/examples/effects/simple/lib/main_scale.dart index 0e5ecb817..fc332c488 100644 --- a/doc/examples/effects/simple/lib/main_scale.dart +++ b/doc/examples/effects/simple/lib/main_scale.dart @@ -32,5 +32,9 @@ class MyGame extends BaseGame with TapDetector { } void main() { - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } diff --git a/doc/examples/gestures/lib/main.dart b/doc/examples/gestures/lib/main.dart index f8b62e584..ff90f6602 100644 --- a/doc/examples/gestures/lib/main.dart +++ b/doc/examples/gestures/lib/main.dart @@ -1,11 +1,14 @@ -import 'package:flutter/material.dart'; import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; import 'package:flame/gestures.dart'; import 'package:flame/palette.dart'; void main() { - final game = MyGame(); - runApp(game.widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } /// Includes an example including basic detectors @@ -16,6 +19,9 @@ class MyGame extends Game final _greenPaint = Paint()..color = const Color(0xFF00FF00); final _redPaint = Paint()..color = const Color(0xFFFF0000); + @override + Color backgroundColor() => const Color(0xFFF1F1F1); + Paint _paint; Rect _rect = const Rect.fromLTWH(50, 50, 50, 50); diff --git a/doc/examples/gestures/lib/main_mouse_movement.dart b/doc/examples/gestures/lib/main_mouse_movement.dart index 36880b8ef..38ac5179b 100644 --- a/doc/examples/gestures/lib/main_mouse_movement.dart +++ b/doc/examples/gestures/lib/main_mouse_movement.dart @@ -1,13 +1,16 @@ -import 'package:flutter/material.dart'; import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; import 'package:flame/gestures.dart'; import 'package:flame/palette.dart'; import 'package:flame/extensions/vector2.dart'; import 'package:flame/extensions/offset.dart'; void main() { - final game = MyGame(); - runApp(game.widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class MyGame extends Game with MouseMovementDetector { diff --git a/doc/examples/gestures/lib/main_multitap.dart b/doc/examples/gestures/lib/main_multitap.dart index f704c3bb7..2cde1fdbb 100644 --- a/doc/examples/gestures/lib/main_multitap.dart +++ b/doc/examples/gestures/lib/main_multitap.dart @@ -1,11 +1,14 @@ -import 'package:flutter/material.dart'; import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; import 'package:flame/gestures.dart'; import 'package:flame/palette.dart'; void main() { - final game = MyGame(); - runApp(game.widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } /// Includes an example including advanced detectors diff --git a/doc/examples/gestures/lib/main_multitap_advanced.dart b/doc/examples/gestures/lib/main_multitap_advanced.dart index 979b86d7f..d37d71167 100644 --- a/doc/examples/gestures/lib/main_multitap_advanced.dart +++ b/doc/examples/gestures/lib/main_multitap_advanced.dart @@ -1,11 +1,14 @@ -import 'package:flutter/material.dart'; import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; import 'package:flame/gestures.dart'; import 'package:flame/palette.dart'; void main() { - final game = MyGame(); - runApp(game.widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } /// Includes an example mixing two advanced detectors diff --git a/doc/examples/gestures/lib/main_overlapping_tapables.dart b/doc/examples/gestures/lib/main_overlapping_tapables.dart index 3376f62ba..f2175d754 100644 --- a/doc/examples/gestures/lib/main_overlapping_tapables.dart +++ b/doc/examples/gestures/lib/main_overlapping_tapables.dart @@ -13,7 +13,9 @@ void main() { final widget = Container( padding: const EdgeInsets.all(50), color: const Color(0xFFA9A9A9), - child: game.widget, + child: GameWidget( + game: game, + ), ); runApp(widget); diff --git a/doc/examples/gestures/lib/main_scroll.dart b/doc/examples/gestures/lib/main_scroll.dart index fbd5497b7..eedc2459c 100644 --- a/doc/examples/gestures/lib/main_scroll.dart +++ b/doc/examples/gestures/lib/main_scroll.dart @@ -7,7 +7,11 @@ import 'package:flame/extensions/offset.dart'; void main() { final game = MyGame(); - runApp(game.widget); + runApp( + GameWidget( + game: game, + ), + ); } class MyGame extends Game with ScrollDetector { diff --git a/doc/examples/gestures/lib/main_tapables.dart b/doc/examples/gestures/lib/main_tapables.dart index ba1ab5332..50985962c 100644 --- a/doc/examples/gestures/lib/main_tapables.dart +++ b/doc/examples/gestures/lib/main_tapables.dart @@ -6,15 +6,15 @@ import 'package:flame/components/position_component.dart'; import 'package:flame/components/mixins/tapable.dart'; void main() { - final game = MyGame(); - - final widget = Container( - padding: const EdgeInsets.all(50), - color: const Color(0xFFA9A9A9), - child: game.widget, + runApp( + Container( + padding: const EdgeInsets.all(50), + color: const Color(0xFFA9A9A9), + child: GameWidget( + game: MyGame(), + ), + ), ); - - runApp(widget); } class TapableSquare extends PositionComponent with Tapable { diff --git a/doc/examples/go_desktop/lib/main.dart b/doc/examples/go_desktop/lib/main.dart index d757d87d5..c92f4bc5f 100644 --- a/doc/examples/go_desktop/lib/main.dart +++ b/doc/examples/go_desktop/lib/main.dart @@ -1,8 +1,13 @@ +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import './game.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } diff --git a/doc/examples/go_desktop/lib/main_desktop.dart b/doc/examples/go_desktop/lib/main_desktop.dart index 08d29ca71..2f556d694 100644 --- a/doc/examples/go_desktop/lib/main_desktop.dart +++ b/doc/examples/go_desktop/lib/main_desktop.dart @@ -1,3 +1,4 @@ +import 'package:flame/game.dart'; import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride; import 'package:flutter/material.dart'; @@ -7,5 +8,9 @@ import './game.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } diff --git a/doc/examples/isometric/lib/main.dart b/doc/examples/isometric/lib/main.dart index 6b01de3c8..b5e16a9cb 100644 --- a/doc/examples/isometric/lib/main.dart +++ b/doc/examples/isometric/lib/main.dart @@ -17,8 +17,11 @@ final topLeft = Vector2(x, y); void main() async { WidgetsFlutterBinding.ensureInitialized(); - final game = MyGame(); - runApp(game.widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class Selector extends SpriteComponent { diff --git a/doc/examples/joystick/lib/main.dart b/doc/examples/joystick/lib/main.dart index 2f4f94749..17cdc90c2 100644 --- a/doc/examples/joystick/lib/main.dart +++ b/doc/examples/joystick/lib/main.dart @@ -8,8 +8,11 @@ import 'package:flutter/material.dart'; import 'player.dart'; void main() { - final game = MyGame(); - runApp(game.widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class MyGame extends BaseGame with MultiTouchDragDetector { diff --git a/doc/examples/keyboard/lib/main.dart b/doc/examples/keyboard/lib/main.dart index 7486ac295..d12c4526b 100644 --- a/doc/examples/keyboard/lib/main.dart +++ b/doc/examples/keyboard/lib/main.dart @@ -5,7 +5,11 @@ import 'package:flutter/services.dart'; import 'dart:ui'; -void main() => runApp(MyGame().widget); +void main() => runApp( + GameWidget( + game: MyGame(), + ), + ); class MyGame extends Game with KeyboardEvents { static final Paint _white = Paint()..color = const Color(0xFFFFFFFF); diff --git a/doc/examples/layers/lib/main.dart b/doc/examples/layers/lib/main.dart index 898bfa95f..35f4c02ea 100644 --- a/doc/examples/layers/lib/main.dart +++ b/doc/examples/layers/lib/main.dart @@ -1,6 +1,6 @@ import 'package:flame/extensions/vector2.dart'; -import 'package:flutter/material.dart' hide Animation; import 'package:flame/game.dart'; +import 'package:flutter/material.dart' hide Animation; import 'package:flame/sprite.dart'; import 'package:flame/layer/layer.dart'; import 'package:flame/flame.dart'; @@ -12,7 +12,11 @@ void main() async { await Flame.util.fullScreen(); - runApp(LayerGame().widget); + runApp( + GameWidget( + game: LayerGame(), + ), + ); } class GameLayer extends DynamicLayer { diff --git a/doc/examples/nine_tile_box/lib/main.dart b/doc/examples/nine_tile_box/lib/main.dart index 3a443009e..b2d345280 100644 --- a/doc/examples/nine_tile_box/lib/main.dart +++ b/doc/examples/nine_tile_box/lib/main.dart @@ -11,7 +11,11 @@ void main() async { final size = await Flame.util.initialDimensions(); final game = MyGame(size); - runApp(game.widget); + runApp( + GameWidget( + game: game, + ), + ); } class MyGame extends Game { diff --git a/doc/examples/parallax/lib/main.dart b/doc/examples/parallax/lib/main.dart index 83b43a2a7..c09187bc7 100644 --- a/doc/examples/parallax/lib/main.dart +++ b/doc/examples/parallax/lib/main.dart @@ -1,13 +1,17 @@ import 'package:flame/flame.dart'; -import 'package:flame/game.dart'; import 'package:flame/components/parallax_component.dart'; import 'package:flame/extensions/vector2.dart'; +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.util.fullScreen(); - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class MyGame extends BaseGame { diff --git a/doc/examples/particles/lib/main.dart b/doc/examples/particles/lib/main.dart index b2cae1cbc..90df5ed3e 100644 --- a/doc/examples/particles/lib/main.dart +++ b/doc/examples/particles/lib/main.dart @@ -25,9 +25,16 @@ import 'package:flame/extensions/vector2.dart'; import 'package:flame/sprite.dart'; import 'package:flame/spritesheet.dart'; import 'package:flame/text_config.dart'; -import 'package:flutter/material.dart' hide Animation, Image; +import 'package:flutter/material.dart' hide Image; -void main() async => runApp((await loadGame()).widget); +void main() async { + final game = await loadGame(); + runApp( + GameWidget( + game: game, + ), + ); +} class MyGame extends BaseGame { /// Defines dimensions of the sample diff --git a/doc/examples/render_flip/lib/main.dart b/doc/examples/render_flip/lib/main.dart index a9d13ce53..2414fe80d 100644 --- a/doc/examples/render_flip/lib/main.dart +++ b/doc/examples/render_flip/lib/main.dart @@ -9,7 +9,11 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); final Vector2 size = await Flame.util.initialDimensions(); final game = MyGame(size); - runApp(game.widget); + runApp( + GameWidget( + game: game, + ), + ); } class MyGame extends BaseGame { diff --git a/doc/examples/sprite_batch/lib/main.dart b/doc/examples/sprite_batch/lib/main.dart index 6cf6af6ad..fa4faa1cb 100644 --- a/doc/examples/sprite_batch/lib/main.dart +++ b/doc/examples/sprite_batch/lib/main.dart @@ -11,7 +11,11 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); final Vector2 size = await Flame.util.initialDimensions(); final game = MyGame(size); - runApp(game.widget); + runApp( + GameWidget( + game: game, + ), + ); } class MyGame extends BaseGame { diff --git a/doc/examples/sprites/lib/main.dart b/doc/examples/sprites/lib/main.dart index 8ec46027d..b18a2d180 100644 --- a/doc/examples/sprites/lib/main.dart +++ b/doc/examples/sprites/lib/main.dart @@ -10,7 +10,11 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); final Vector2 size = await Flame.util.initialDimensions(); final game = MyGame(size); - runApp(game.widget); + runApp( + GameWidget( + game: game, + ), + ); } class MyGame extends BaseGame { diff --git a/doc/examples/sprites/lib/main_base64.dart b/doc/examples/sprites/lib/main_base64.dart index 94455142f..eaceb1e55 100644 --- a/doc/examples/sprites/lib/main_base64.dart +++ b/doc/examples/sprites/lib/main_base64.dart @@ -1,13 +1,16 @@ import 'package:flame/extensions/vector2.dart'; -import 'package:flutter/material.dart'; - -import 'package:flame/sprite.dart'; import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:flame/sprite.dart'; import 'dart:ui'; void main() { - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class MyGame extends Game { diff --git a/doc/examples/spritesheet/lib/main.dart b/doc/examples/spritesheet/lib/main.dart index 194f7232a..6ccebcb47 100644 --- a/doc/examples/spritesheet/lib/main.dart +++ b/doc/examples/spritesheet/lib/main.dart @@ -8,7 +8,11 @@ import 'package:flutter/material.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - runApp(MyGame().widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class MyGame extends BaseGame { diff --git a/doc/examples/text/lib/main.dart b/doc/examples/text/lib/main.dart index b8c7798a8..2792c479a 100644 --- a/doc/examples/text/lib/main.dart +++ b/doc/examples/text/lib/main.dart @@ -12,7 +12,11 @@ import 'package:flutter/material.dart'; void main() async { final Vector2 size = await Flame.util.initialDimensions(); - runApp(MyGame(size).widget); + runApp( + GameWidget( + game: MyGame(size), + ), + ); } TextConfig regular = TextConfig(color: BasicPalette.white.color); diff --git a/doc/examples/timer/lib/main.dart b/doc/examples/timer/lib/main.dart index accbae60c..1393795b5 100644 --- a/doc/examples/timer/lib/main.dart +++ b/doc/examples/timer/lib/main.dart @@ -7,10 +7,10 @@ import 'package:flame/extensions/vector2.dart'; import 'package:flame/components/timer_component.dart'; void main() { - runApp(GameWidget()); + runApp(MyGameApp()); } -class GameWidget extends StatelessWidget { +class MyGameApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp(routes: { @@ -26,8 +26,8 @@ class GameWidget extends StatelessWidget { Navigator.of(context).pushNamed('/base_game'); }) ]), - '/game': (BuildContext context) => MyGame().widget, - '/base_game': (BuildContext context) => MyBaseGame().widget + '/game': (BuildContext context) => GameWidget(game: MyGame()), + '/base_game': (BuildContext context) => GameWidget(game: MyBaseGame()) }); } } diff --git a/doc/examples/with_widgets_overlay/lib/example_game.dart b/doc/examples/with_widgets_overlay/lib/example_game.dart index d5884d58a..9f0ed08b5 100644 --- a/doc/examples/with_widgets_overlay/lib/example_game.dart +++ b/doc/examples/with_widgets_overlay/lib/example_game.dart @@ -4,7 +4,7 @@ import 'package:flame/palette.dart'; import 'package:flutter/material.dart'; -class ExampleGame extends Game with HasWidgetsOverlay, TapDetector { +class ExampleGame extends Game with TapDetector { bool isPaused = false; @override @@ -12,26 +12,20 @@ class ExampleGame extends Game with HasWidgetsOverlay, TapDetector { @override void render(Canvas canvas) { - canvas.drawRect(const Rect.fromLTWH(100, 100, 100, 100), - Paint()..color = BasicPalette.white.color); + canvas.drawRect( + const Rect.fromLTWH(100, 100, 100, 100), + Paint()..color = BasicPalette.white.color, + ); } @override void onTap() { if (isPaused) { - removeWidgetOverlay('PauseMenu'); + overlays.remove('PauseMenu'); isPaused = false; } else { - addWidgetOverlay( + overlays.add( 'PauseMenu', - Center( - child: Container( - width: 100, - height: 100, - color: const Color(0xFFFF0000), - child: const Center(child: const Text('Paused')), - ), - ), ); isPaused = true; } diff --git a/doc/examples/with_widgets_overlay/lib/main.dart b/doc/examples/with_widgets_overlay/lib/main.dart index 9768cf3fe..01fb8df50 100644 --- a/doc/examples/with_widgets_overlay/lib/main.dart +++ b/doc/examples/with_widgets_overlay/lib/main.dart @@ -1,7 +1,69 @@ +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import './example_game.dart'; void main() { - runApp(ExampleGame().widget); + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key}) : super(key: key); + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + ExampleGame _myGame; + + Widget pauseMenuBuilder(BuildContext buildContext) { + return Center( + child: Container( + width: 100, + height: 100, + color: const Color(0xFFFF0000), + child: const Center( + child: const Text('Paused'), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Testing addingOverlay'), + ), + body: _myGame == null + ? const Text('Wait') + : GameWidget( + game: _myGame, + overlayBuilderMap: { + "PauseMenu": pauseMenuBuilder, + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => newGame(), + child: const Icon(Icons.add), + ), + ); + } + + void newGame() { + setState(() { + _myGame = ExampleGame(); + print('New game created'); + }); + } } diff --git a/doc/examples/with_widgets_overlay/lib/main_dynamic_game.dart b/doc/examples/with_widgets_overlay/lib/main_dynamic_game.dart deleted file mode 100644 index a0b4c4831..000000000 --- a/doc/examples/with_widgets_overlay/lib/main_dynamic_game.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; - -import './example_game.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - home: MyHomePage(), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key key}) : super(key: key); - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - ExampleGame _myGame; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Testing addingOverlay'), - ), - body: _myGame == null ? const Text('Wait') : _myGame.widget, - floatingActionButton: FloatingActionButton( - onPressed: () => newGame(), - child: const Icon(Icons.add), - ), - ); - } - - void newGame() { - print('New game created'); - _myGame = ExampleGame(); - setState(() {}); - } -} diff --git a/doc/game.md b/doc/game.md index 4ac4be6ec..ca706c731 100644 --- a/doc/game.md +++ b/doc/game.md @@ -5,24 +5,43 @@ The Game Loop module is a simple abstraction over the game loop concept. Basical * The render method takes the canvas ready for drawing the current state of the game. * The update method receives the delta time in seconds since last update and allows you to move to the next state. -The class `Game` can be subclassed and will provide both these methods for you to implement. In return it will provide you with a `widget` property that returns the game widget, that can be rendered in your app. +The `Game` class can be extended and will provide these gameloop methods and then its instance Flutter widget tree via the `GameWidget`. -You can either render it directly in your `runApp`, or you can have a bigger structure, with routing, other screens and menus for your game. +You can add it into the top of you tree (directly as an argument to `runApp`) or inside the usual app-like widget structure (with scaffold, routes, etc.). -To start, just add your game widget directly to your runApp, like this: +Example of usage directly with `runApp`: ```dart - main() { - Game game = MyGameImpl(); - runApp(game.widget); - } + +class MyGameSubClass extends Game { + @override + void render(Canvas canvas) { + // TODO: implement render + } + + @override + void update(double t) { + // TODO: implement update + } +} + + +main() { + runApp( + GameWidget( + game: MyGameSubClass(), + ) + ); +} ``` -Instead of implementing the low level `Game` class, you should probably use the more full-featured `BaseGame` class, or the `Forge2DGame` class if you want to use a physics engine. +It is important to notice that `Game` is an abstract class with just the very basic implementations of the gameloop. + +As an option and more suitable for most cases, there is the full-featured `BaseGame` class. For example, Forge2D games uses `Forge2DGame` class; The `BaseGame` implements a `Component` based `Game` for you; basically it has a list of `Component`s and passes the `update` and `render` calls appropriately. You can still extend those methods to add custom behavior, and you will get a few other features for free, like the passing of `resize` methods (every time the screen is resized the information will be passed to the resize methods of all your components) and also a basic camera feature. The `BaseGame.camera` controls which point in coordinate space should be the top-left of the screen (defaults to [0,0] like a regular `Canvas`). -A very simple `BaseGame` implementation example can be seen below: +A `BaseGame` implementation example can be seen below: ```dart class MyCrate extends SpriteComponent { @@ -33,7 +52,7 @@ A very simple `BaseGame` implementation example can be seen below: @override void onGameResize(Size size) { // we don't need to set the x and y in the constructor, we can set then here - this.x = (size.width - this.width)/ 2; + this.x = (size.width - this.width) / 2; this.y = (size.height - this.height) / 2; } } @@ -49,27 +68,39 @@ To remove components from the list on a `BaseGame` the `markToRemove` method can ## Flutter Widgets and Game instances -Since a Flame game is a widget itself, it is quite easy to use Flutter widgets and Flame game together. But to make it even easier, Flame provides a `mixin` called `HasWidgetsOverlay` which will enable any Flutter widget to be show on top of your game instance, this makes it very easy to create things like a pause menu, or an inventory screen for example. +Since a Flame game can be wrapped in a widget, it is quite easy to use it alongside other Flutter widgets. But still, there is a the Widgets overlay API that makes things even easier. -To use it, simple add the `HasWidgetsOverlay` `mixin` on your game class, by doing so, you will have two new methods available `addWidgetOverlay` and `removeWidgetOverlay`, like the name suggests, they can be used to add or remove widgets overlay above your game. They can be used as shown below: +`Game.overlays` enables to any Flutter widget to be shown on top of a game instance, this makes it very easy to create things like a pause menu, or an inventory screen for example. +This property that will be used to manage the active overlays. + +This management happens via the `.overlays.add` and `.overlays.remove` methods that marks an overlay to be shown and hidden, respectively, via a `String` argument that identifies an overlay. +After that it can be specified which widgets represent each overlay in the `GameWidget` declaration by setting a `overlayBuilderMap`. ```dart -addWidgetOverlay( - "PauseMenu", // Your overlay identifier - Center(child: - Container( - width: 100, - height: 100, - color: const Color(0xFFFF0000), - child: const Center(child: const Text("Paused")), - ), - ) // Your widget, this can be any Flutter widget -); +// inside game methods: +final pauseOverlayIdentifier = "PauseMenu"; -removeWidgetOverlay("PauseMenu"); // Use the overlay identifier to remove the overlay +overlays.add(pauseOverlayIdentifier); // marks "PauseMenu" to be rendered. +overlays.remove(pauseOverlayIdentifier); // marks "PauseMenu" to not be rendered. ``` -Under the hood, Flame uses a [Stack widget](https://api.flutter.dev/flutter/widgets/Stack-class.html) to display the overlay, so it is important to __note that the order which the overlays are added matters__, where the last added overlay, will be in the front of those added before. +```dart +// on the widget declaration +final game = MyGame(); + +Widget build(BuildContext context) { + return GameWidget( + game: game, + overlayBuilderMap: { + "PauseMenu": (ctx) { + return Text("A pause menu"); + }, + }, + ); +} +``` + +The order in which the overlays are declared on the `overlayBuilderMap` defines which overlay will be rendered first. Here you can see a [working example](/doc/examples/with_widgets_overlay) of this feature. diff --git a/example/lib/main.dart b/example/lib/main.dart index 628b8fef6..9ed500c01 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -11,9 +11,11 @@ import 'package:flame/palette.dart'; import 'package:flutter/material.dart'; void main() { - final game = MyGame(); - - runApp(game.widget); + runApp( + GameWidget( + game: MyGame(), + ), + ); } class Palette { diff --git a/lib/flame.dart b/lib/flame.dart index 097d8513b..dd379b807 100644 --- a/lib/flame.dart +++ b/lib/flame.dart @@ -14,6 +14,7 @@ import 'util.dart'; class Flame { // Flame asset bundle, defaults to root static AssetBundle _bundle; + static AssetBundle get bundle => _bundle == null ? rootBundle : _bundle; /// Access a shared instance of [AssetsCache] class. @@ -25,10 +26,11 @@ class Flame { /// Access a shared instance of the [Util] class. static Util util = Util(); - static Future init( - {AssetBundle bundle, - bool fullScreen = true, - DeviceOrientation orientation}) async { + static Future init({ + AssetBundle bundle, + bool fullScreen = true, + DeviceOrientation orientation, + }) async { initializeWidget(); if (fullScreen) { diff --git a/lib/game.dart b/lib/game.dart index 00250ed1c..05b2f92fe 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -1,3 +1,4 @@ // Keeping compatible with earlier versions of Flame export './game/base_game.dart'; export './game/game.dart'; +export './game/game_widget.dart'; diff --git a/lib/game/embedded_game_widget.dart b/lib/game/embedded_game_widget.dart deleted file mode 100644 index 1603238ce..000000000 --- a/lib/game/embedded_game_widget.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart' hide WidgetBuilder; - -import 'game.dart'; -import 'game_render_box.dart'; - -/// This a widget to embed a game inside the Widget tree. You can use it in pair with [BaseGame] or any other more complex [Game], as desired. -/// -/// It handles for you positioning, size constraints and other factors that arise when your game is embedded within the component tree. -/// Provided it with a [Game] instance for your game and the optional size of the widget. -/// Creating this without a fixed size might mess up how other components are rendered with relation to this one in the tree. -/// You can bind Gesture Recognizers immediately around this to add controls to your widgets, with easy coordinate conversions. -class EmbeddedGameWidget extends LeafRenderObjectWidget { - final Game game; - - EmbeddedGameWidget(this.game); - - @override - RenderBox createRenderObject(BuildContext context) { - return RenderConstrainedBox( - child: GameRenderBox(context, game), - additionalConstraints: const BoxConstraints.expand(), - ); - } - - @override - void updateRenderObject( - BuildContext context, - RenderConstrainedBox renderBox, - ) { - renderBox - ..child = GameRenderBox(context, game) - ..additionalConstraints = const BoxConstraints.expand(); - } -} diff --git a/lib/game/game.dart b/lib/game/game.dart index 9f56f9789..79dd28801 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -5,24 +5,23 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart' hide WidgetBuilder; +import 'package:flutter/widgets.dart'; import '../assets/assets_cache.dart'; import '../assets/images.dart'; import '../extensions/vector2.dart'; import '../keyboard.dart'; -import 'widget_builder.dart'; /// Represents a generic game. /// /// Subclass this to implement the [update] and [render] methods. /// Flame will deal with calling these methods properly when the game's widget is rendered. abstract class Game { - // Widget Builder for this Game - final builder = WidgetBuilder(); - final images = Images(); final assets = AssetsCache(); + BuildContext buildContext; + + bool get isAttached => buildContext != null; /// Returns the game background color. /// By default it will return a black color. @@ -48,17 +47,27 @@ abstract class Game { /// Check [AppLifecycleState] for details about the events received. void lifecycleStateChange(AppLifecycleState state) {} - /// Use for caluclating the FPS. + /// Use for calculating the FPS. void onTimingsCallback(List timings) {} - /// Returns the game widget. Put this in your structure to start rendering and updating the game. - /// You can add it directly to the runApp method or inside your widget structure (if you use vanilla screens and widgets). - Widget get widget => builder.build(this); - void _handleKeyEvent(RawKeyEvent e) { (this as KeyboardEvents).onKeyEvent(e); } + /// Marks game as not attached tto any widget tree. + /// + /// Should be called manually. + void attach(PipelineOwner owner, BuildContext context) { + if (isAttached) { + throw UnsupportedError(""" + Game attachment error: + A game instance can only be attached to one widget at a time. + """); + } + buildContext = context; + onAttach(); + } + // Called when the Game widget is attached @mustCallSuper void onAttach() { @@ -67,23 +76,23 @@ abstract class Game { } } + /// Marks game as not attached tto any widget tree. + /// + /// Should not be called manually. + void detach() { + buildContext = null; + onDetach(); + } + // Called when the Game widget is detached @mustCallSuper void onDetach() { // Keeping this here, because if we leave this on HasWidgetsOverlay // and somebody overrides this and forgets to call the stream close // we can face some leaks. - - // Also we only do this in release mode, otherwise when using hot reload - // the controller would be closed and errors would happen - if (this is HasWidgetsOverlay && kReleaseMode) { - (this as HasWidgetsOverlay).widgetOverlayController.close(); - } - if (this is KeyboardEvents) { RawKeyboard.instance.removeListener(_handleKeyEvent); } - images.clearCache(); } @@ -102,29 +111,57 @@ abstract class Game { /// Use this method to load the assets need for the game instance to run Future onLoad() async {} - /// Returns the widget which will be show while the instance is loading - Widget loadingWidget() => Container(); + /// A property that stores an [ActiveOverlaysNotifier] + /// + /// This is useful to render widgets above a game, like a pause menu for example. + /// Overlays visible or hidden via [overlays.add] or [overlays.remove], respectively. + /// + /// Ex: + /// ``` + /// final pauseOverlayIdentifier = "PauseMenu"; + /// overlays.add(pauseOverlayIdentifier); // marks "PauseMenu" to be rendered. + /// overlays.remove(pauseOverlayIdentifier); // marks "PauseMenu" to not be rendered. + /// ``` + /// + /// See also: + /// - [new GameWidget] + /// - [Game.overlays] + final overlays = ActiveOverlaysNotifier(); } -class OverlayWidget { - final String name; - final Widget widget; +/// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance. +/// +/// To learn more, see: +/// - [Game.overlays] +class ActiveOverlaysNotifier extends ChangeNotifier { + final Set _activeOverlays = {}; - OverlayWidget(this.name, this.widget); -} - -mixin HasWidgetsOverlay on Game { - @override - final builder = OverlayWidgetBuilder(); - - final StreamController widgetOverlayController = - StreamController(); - - void addWidgetOverlay(String overlayName, Widget widget) { - widgetOverlayController.sink.add(OverlayWidget(overlayName, widget)); + /// Mark a, overlay to be rendered. + /// + /// See also: + /// - [new GameWidget] + /// - [Game.overlays] + bool add(String overlayName) { + final setChanged = _activeOverlays.add(overlayName); + if (setChanged) { + notifyListeners(); + } + return setChanged; } - void removeWidgetOverlay(String overlayName) { - widgetOverlayController.sink.add(OverlayWidget(overlayName, null)); + /// Mark a, overlay to not be rendered. + /// + /// See also: + /// - [new GameWidget] + /// - [Game.overlays] + bool remove(String overlayName) { + final hasRemoved = _activeOverlays.remove(overlayName); + if (hasRemoved) { + notifyListeners(); + } + return hasRemoved; } + + /// A [Set] of the active overlay names. + Set get value => _activeOverlays; } diff --git a/lib/game/game_render_box.dart b/lib/game/game_render_box.dart index f742b39b7..25717a817 100644 --- a/lib/game/game_render_box.dart +++ b/lib/game/game_render_box.dart @@ -10,11 +10,11 @@ import 'game.dart'; import 'game_loop.dart'; class GameRenderBox extends RenderBox with WidgetsBindingObserver { - BuildContext context; + BuildContext buildContext; Game game; GameLoop gameLoop; - GameRenderBox(this.context, this.game) { + GameRenderBox(this.buildContext, this.game) { gameLoop = GameLoop(gameLoopCallback); WidgetsBinding.instance.addTimingsCallback(game.onTimingsCallback); } @@ -31,7 +31,7 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver { @override void attach(PipelineOwner owner) { super.attach(owner); - game.onAttach(); + game.attach(owner, buildContext); game.pauseEngineFn = gameLoop.pause; game.resumeEngineFn = gameLoop.resume; @@ -46,7 +46,7 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver { @override void detach() { super.detach(); - game.onDetach(); + game.detach(); gameLoop.stop(); _unbindLifecycleListener(); } diff --git a/lib/game/widget_builder.dart b/lib/game/game_widget.dart similarity index 55% rename from lib/game/widget_builder.dart rename to lib/game/game_widget.dart index 6be50cb13..299791f6c 100644 --- a/lib/game/widget_builder.dart +++ b/lib/game/game_widget.dart @@ -1,10 +1,229 @@ -import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart'; -import '../components/mixins/tapable.dart'; -import '../gestures.dart'; -import 'embedded_game_widget.dart'; import 'game.dart'; +import '../gestures.dart'; +import '../components/mixins/tapable.dart'; +import 'game_render_box.dart'; + +typedef GameLoadingWidgetBuilder = Widget Function( + BuildContext, + bool error, +); + +/// A [StatefulWidget] that is in charge of attaching a [Game] instance into the flutter tree +/// +class GameWidget extends StatefulWidget { + /// The game instance in which this widget will render + final T game; + + /// The text direction to be used in text elements in a game. + final TextDirection textDirection; + + /// Builder to provide a widget tree to be built whilst the [Future] provided + /// via [Game.onLoad] is not resolved. + final GameLoadingWidgetBuilder loadingBuilder; + + /// Builder to provide a widget tree to be built between the game elements and + /// the background color provided via [Game.backgroundColor] + final WidgetBuilder backgroundBuilder; + + /// A map to show widgets overlay. + /// + /// See also: + /// - [new GameWidget] + /// - [Game.overlays] + final Map overlayBuilderMap; + + /// Renders a [game] in a flutter widget tree. + /// + /// Ex: + /// ``` + /// ... + /// Widget build(BuildContext context) { + /// return GameWidget( + /// game: MyGameClass(), + /// ) + /// } + /// ... + /// ``` + /// + /// It is also possible to render layers of widgets over the game surface with widget subtrees. + /// + /// To do that a [overlayBuilderMap] should be provided. The visibility of + /// these overlays are controlled by [Game.overlays] property + /// + /// Ex: + /// ``` + /// ... + /// + /// final game = MyGame(); + /// + /// Widget build(BuildContext context) { + /// return GameWidget( + /// game: game, + /// overlayBuilderMap: { + /// "PauseMenu": (ctx) { + /// return Text("A pause menu"); + /// }, + /// }, + /// ) + /// } + /// ... + /// game.overlays.add("PauseMenu"); + /// ``` + const GameWidget({ + Key key, + this.game, + this.textDirection, + this.loadingBuilder, + this.backgroundBuilder, + this.overlayBuilderMap, + }) : 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], + + @override + _GameWidgetState createState() => _GameWidgetState(); +} + +class _GameWidgetState extends State { + Set activeOverlays = {}; + + @override + void initState() { + super.initState(); + addOverlaysListener(widget.game); + loadingFuture = widget.game.onLoad(); + } + + @override + void didUpdateWidget(covariant GameWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.game != widget.game) { + removeOverlaysListener(oldWidget.game); + addOverlaysListener(widget.game); + } + loadingFuture = widget.game.onLoad(); + } + + @override + void dispose() { + super.dispose(); + removeOverlaysListener(widget.game); + } + + // widget overlay stuff + void addOverlaysListener(Game game) { + widget.game.overlays.addListener(onChangeActiveOverlays); + activeOverlays = widget.game.overlays.value; + } + + void removeOverlaysListener(Game game) { + game.overlays.removeListener(onChangeActiveOverlays); + } + + void onChangeActiveOverlays() { + widget.game.overlays.value.forEach((overlayKey) { + assert(widget.overlayBuilderMap.containsKey(overlayKey), + "A non mapped overlay has been added: $overlayKey"); + }); + setState(() { + activeOverlays = widget.game.overlays.value; + }); + } + + // loading future + Future loadingFuture; + + @override + Widget build(BuildContext context) { + Widget internalGameWidget = _GameRenderObjectWidget(widget.game); + + final hasBasicDetectors = _hasBasicGestureDetectors(widget.game); + final hasAdvancedDetectors = _hasAdvancedGesturesDetectors(widget.game); + + assert( + !(hasBasicDetectors && hasAdvancedDetectors), + """ + WARNING: Both Advanced and Basic detectors detected. + Advanced detectors will override basic detectors and the later will not receive events + """, + ); + + if (hasBasicDetectors) { + internalGameWidget = _applyBasicGesturesDetectors( + widget.game, + internalGameWidget, + ); + } else if (hasAdvancedDetectors) { + internalGameWidget = _applyAdvancedGesturesDetectors( + widget.game, + internalGameWidget, + ); + } + + if (_hasMouseDetectors(widget.game)) { + internalGameWidget = _applyMouseDetectors( + widget.game, + internalGameWidget, + ); + } + + List stackedWidgets = [internalGameWidget]; + stackedWidgets = _addBackground(context, stackedWidgets); + stackedWidgets = _addOverlays(context, stackedWidgets); + return Directionality( + textDirection: widget.textDirection ?? + Directionality.maybeOf(context) ?? + TextDirection.ltr, + child: Container( + color: widget.game.backgroundColor(), + child: FutureBuilder( + future: loadingFuture, + builder: (_, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Stack(children: stackedWidgets); + } + return widget.loadingBuilder != null + ? widget.loadingBuilder(context, snapshot.hasError) + : Container(); + }, + ), + ), + ); + } + + List _addBackground(BuildContext context, List stackWidgets) { + if (widget.backgroundBuilder == null) { + return stackWidgets; + } + final backgroundContent = KeyedSubtree( + key: ValueKey(widget.game), + child: widget.backgroundBuilder(context), + ); + stackWidgets.insert(0, backgroundContent); + return stackWidgets; + } + + List _addOverlays(BuildContext context, List stackWidgets) { + if (widget.overlayBuilderMap == null) { + return stackWidgets; + } + final widgets = activeOverlays.map((String overlayKey) { + final builder = widget.overlayBuilderMap[overlayKey]; + return KeyedSubtree( + key: ValueKey(overlayKey), + child: builder(context), + ); + }); + stackWidgets.addAll(widgets); + return stackWidgets; + } +} bool _hasBasicGestureDetectors(Game game) => game is TapDetector || @@ -25,76 +244,11 @@ bool _hasAdvancedGesturesDetectors(Game game) => bool _hasMouseDetectors(Game game) => game is MouseMovementDetector || game is ScrollDetector; -class _GenericTapEventHandler { - void Function(int pointerId) onTap; - void Function(int pointerId) onTapCancel; - void Function(int pointerId, TapDownDetails details) onTapDown; - void Function(int pointerId, TapUpDetails details) onTapUp; -} - -Widget _applyAdvancedGesturesDetectors(Game game, Widget child) { - final Map gestures = {}; - - final List<_GenericTapEventHandler> _tapHandlers = []; - - if (game is HasTapableComponents) { - _tapHandlers.add(_GenericTapEventHandler() - ..onTapDown = game.onTapDown - ..onTapUp = game.onTapUp - ..onTapCancel = game.onTapCancel); - } - - if (game is MultiTouchTapDetector) { - _tapHandlers.add(_GenericTapEventHandler() - ..onTapDown = game.onTapDown - ..onTapUp = game.onTapUp - ..onTapCancel = game.onTapCancel); - } - - if (_tapHandlers.isNotEmpty) { - gestures[MultiTapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => MultiTapGestureRecognizer(), - (MultiTapGestureRecognizer instance) { - instance.onTapDown = (pointerId, d) => - _tapHandlers.forEach((h) => h.onTapDown?.call(pointerId, d)); - instance.onTapUp = (pointerId, d) => - _tapHandlers.forEach((h) => h.onTapUp?.call(pointerId, d)); - instance.onTapCancel = (pointerId) => - _tapHandlers.forEach((h) => h.onTapCancel?.call(pointerId)); - instance.onTap = (pointerId) => - _tapHandlers.forEach((h) => h.onTap?.call(pointerId)); - }, - ); - } - - if (game is MultiTouchDragDetector) { - gestures[ImmediateMultiDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers< - ImmediateMultiDragGestureRecognizer>( - () => ImmediateMultiDragGestureRecognizer(), - (ImmediateMultiDragGestureRecognizer instance) { - instance - ..onStart = (Offset o) { - final drag = DragEvent(); - drag.initialPosition = o; - - game.onReceiveDrag(drag); - - return drag; - }; - }, - ); - } - - return RawGestureDetector( - gestures: gestures, - child: child, - ); -} - Widget _applyBasicGesturesDetectors(Game game, Widget child) { return GestureDetector( + key: const ObjectKey("BasicGesturesDetector"), + behavior: HitTestBehavior.opaque, + // Taps onTap: game is TapDetector ? () => game.onTap() : null, onTapCancel: game is TapDetector ? () => game.onTapCancel() : null, @@ -203,6 +357,67 @@ Widget _applyBasicGesturesDetectors(Game game, Widget child) { ); } +Widget _applyAdvancedGesturesDetectors(Game game, Widget child) { + final Map gestures = {}; + + final List<_GenericTapEventHandler> _tapHandlers = []; + + if (game is HasTapableComponents) { + _tapHandlers.add(_GenericTapEventHandler() + ..onTapDown = game.onTapDown + ..onTapUp = game.onTapUp + ..onTapCancel = game.onTapCancel); + } + + if (game is MultiTouchTapDetector) { + _tapHandlers.add(_GenericTapEventHandler() + ..onTapDown = game.onTapDown + ..onTapUp = game.onTapUp + ..onTapCancel = game.onTapCancel); + } + + if (_tapHandlers.isNotEmpty) { + gestures[MultiTapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => MultiTapGestureRecognizer(), + (MultiTapGestureRecognizer instance) { + instance.onTapDown = (pointerId, d) => + _tapHandlers.forEach((h) => h.onTapDown?.call(pointerId, d)); + instance.onTapUp = (pointerId, d) => + _tapHandlers.forEach((h) => h.onTapUp?.call(pointerId, d)); + instance.onTapCancel = (pointerId) => + _tapHandlers.forEach((h) => h.onTapCancel?.call(pointerId)); + instance.onTap = (pointerId) => + _tapHandlers.forEach((h) => h.onTap?.call(pointerId)); + }, + ); + } + + if (game is MultiTouchDragDetector) { + gestures[ImmediateMultiDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers< + ImmediateMultiDragGestureRecognizer>( + () => ImmediateMultiDragGestureRecognizer(), + (ImmediateMultiDragGestureRecognizer instance) { + instance + ..onStart = (Offset o) { + final drag = DragEvent(); + drag.initialPosition = o; + + game.onReceiveDrag(drag); + + return drag; + }; + }, + ); + } + + return RawGestureDetector( + gestures: gestures, + child: child, + ); +} + Widget _applyMouseDetectors(Game game, Widget child) { return MouseRegion( child: Listener( @@ -216,93 +431,23 @@ Widget _applyMouseDetectors(Game game, Widget child) { ); } -class WidgetBuilder { - Widget build(Game game) { - Widget widget = Container( - color: game.backgroundColor(), - child: Directionality( - textDirection: TextDirection.ltr, - child: EmbeddedGameWidget(game), - ), - ); +class _GenericTapEventHandler { + void Function(int pointerId) onTap; + void Function(int pointerId) onTapCancel; + void Function(int pointerId, TapDownDetails details) onTapDown; + void Function(int pointerId, TapUpDetails details) onTapUp; +} - final hasBasicDetectors = _hasBasicGestureDetectors(game); - final hasAdvancedDetectors = _hasAdvancedGesturesDetectors(game); +class _GameRenderObjectWidget extends LeafRenderObjectWidget { + final Game game; - assert( - !(hasBasicDetectors && hasAdvancedDetectors), - 'WARNING: Both Advanced and Basic detectors detected. Advanced detectors will override basic detectors and the later will not receive events', - ); + _GameRenderObjectWidget(this.game); - if (hasBasicDetectors) { - widget = _applyBasicGesturesDetectors(game, widget); - } else if (hasAdvancedDetectors) { - widget = _applyAdvancedGesturesDetectors(game, widget); - } - - if (_hasMouseDetectors(game)) { - widget = _applyMouseDetectors(game, widget); - } - - return FutureBuilder( - future: game.onLoad(), - builder: (_, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return widget; - } - return game.loadingWidget(); - }, - ); - } -} - -class OverlayGameWidget extends StatefulWidget { - final Widget gameChild; - final HasWidgetsOverlay game; - - OverlayGameWidget({Key key, this.gameChild, this.game}) : super(key: key); - - @override - State createState() => _OverlayGameWidgetState(); -} - -class _OverlayGameWidgetState extends State { - final Map _overlays = {}; - - @override - void initState() { - super.initState(); - widget.game.widgetOverlayController.stream.listen((overlay) { - setState(() { - if (overlay.widget == null) { - _overlays.remove(overlay.name); - } else { - _overlays[overlay.name] = overlay.widget; - } - }); - }); - } - - @override - Widget build(BuildContext context) { - return Directionality( - textDirection: TextDirection.ltr, - child: Stack(children: [widget.gameChild, ..._overlays.values.toList()]), - ); - } -} - -class OverlayWidgetBuilder extends WidgetBuilder { - OverlayWidgetBuilder(); - - @override - Widget build(Game game) { - final container = super.build(game); - - return OverlayGameWidget( - gameChild: container, - game: game as HasWidgetsOverlay, - key: UniqueKey(), + @override + RenderBox createRenderObject(BuildContext context) { + return RenderConstrainedBox( + child: GameRenderBox(context, game), + additionalConstraints: const BoxConstraints.expand(), ); } } diff --git a/test/base_game_test.dart b/test/base_game_test.dart index 8629e3c9c..a8655d44f 100644 --- a/test/base_game_test.dart +++ b/test/base_game_test.dart @@ -4,6 +4,7 @@ import 'package:flame/components/position_component.dart'; import 'package:flame/components/mixins/has_game_ref.dart'; import 'package:flame/components/mixins/resizable.dart'; import 'package:flame/components/mixins/tapable.dart'; +import 'package:flame/game.dart'; import 'package:flame/game/base_game.dart'; import 'package:flame/extensions/vector2.dart'; import 'package:flame/game/game_render_box.dart'; @@ -101,7 +102,7 @@ void main() { Builder( builder: (BuildContext context) { renderBox = GameRenderBox(context, game); - return game.widget; + return GameWidget(game: game); }, ), ); @@ -109,7 +110,9 @@ void main() { renderBox.gameLoopCallback(1.0); expect(component.isUpdateCalled, true); renderBox.paint( - PaintingContext(ContainerLayer(), Rect.zero), Offset.zero); + PaintingContext(ContainerLayer(), Rect.zero), + Offset.zero, + ); expect(component.isRenderCalled, true); renderBox.detach(); }); diff --git a/test/effects/effect_test_utils.dart b/test/effects/effect_test_utils.dart index 9320bd468..f1fc802e8 100644 --- a/test/effects/effect_test_utils.dart +++ b/test/effects/effect_test_utils.dart @@ -4,13 +4,14 @@ import 'package:flame/anchor.dart'; import 'package:flame/components/position_component.dart'; import 'package:flame/effects/effects.dart'; import 'package:flame/extensions/vector2.dart'; -import 'package:flame/game/base_game.dart'; +import 'package:flame/game.dart'; import 'package:flutter_test/flutter_test.dart'; final Random random = Random(); class Callback { bool isCalled = false; + void call() => isCalled = true; } @@ -32,7 +33,9 @@ void effectTest( game.add(component); component.addEffect(effect); final double duration = effect.iterationTime; - await tester.pumpWidget(game.widget); + await tester.pumpWidget(GameWidget( + game: game, + )); double timeLeft = iterations * duration; while (timeLeft > 0) { double stepDelta = 50.0 + random.nextInt(50);