game widget (#533)

This commit is contained in:
Renan
2020-12-06 20:32:30 +00:00
committed by renancaraujo
parent 4939b708dd
commit 2f2ab1341e
46 changed files with 711 additions and 391 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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(),
),
);
}

View File

@ -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(),
),
);
}

View File

@ -32,5 +32,9 @@ class MyGame extends BaseGame with TapDetector {
}
void main() {
runApp(MyGame().widget);
runApp(
GameWidget(
game: MyGame(),
),
);
}

View File

@ -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);

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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 {

View File

@ -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(
runApp(
Container(
padding: const EdgeInsets.all(50),
color: const Color(0xFFA9A9A9),
child: game.widget,
child: GameWidget(
game: MyGame(),
),
),
);
runApp(widget);
}
class TapableSquare extends PositionComponent with Tapable {

View File

@ -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(),
),
);
}

View File

@ -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(),
),
);
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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);

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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);

View File

@ -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())
});
}
}

View File

@ -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;
}

View File

@ -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<MyHomePage> {
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<ExampleGame>(
game: _myGame,
overlayBuilderMap: {
"PauseMenu": pauseMenuBuilder,
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => newGame(),
child: const Icon(Icons.add),
),
);
}
void newGame() {
setState(() {
_myGame = ExampleGame();
print('New game created');
});
}
}

View File

@ -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<MyHomePage> {
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(() {});
}
}

View File

@ -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
class MyGameSubClass extends Game {
@override
void render(Canvas canvas) {
// TODO: implement render
}
@override
void update(double t) {
// TODO: implement update
}
}
main() {
Game game = MyGameImpl();
runApp(game.widget);
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 {
@ -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.

View File

@ -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 {

View File

@ -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<void> init(
{AssetBundle bundle,
static Future<void> init({
AssetBundle bundle,
bool fullScreen = true,
DeviceOrientation orientation}) async {
DeviceOrientation orientation,
}) async {
initializeWidget();
if (fullScreen) {

View File

@ -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';

View File

@ -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();
}
}

View File

@ -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<FrameTiming> 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<void> 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<String> _activeOverlays = {};
OverlayWidget(this.name, this.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;
}
mixin HasWidgetsOverlay on Game {
@override
final builder = OverlayWidgetBuilder();
final StreamController<OverlayWidget> widgetOverlayController =
StreamController();
void addWidgetOverlay(String overlayName, Widget widget) {
widgetOverlayController.sink.add(OverlayWidget(overlayName, widget));
/// 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;
}
void removeWidgetOverlay(String overlayName) {
widgetOverlayController.sink.add(OverlayWidget(overlayName, null));
}
/// A [Set] of the active overlay names.
Set<String> get value => _activeOverlays;
}

View File

@ -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();
}

View File

@ -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<T extends Game> 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<String, WidgetBuilder> 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<GameWidget> {
Set<String> activeOverlays = {};
@override
void initState() {
super.initState();
addOverlaysListener(widget.game);
loadingFuture = widget.game.onLoad();
}
@override
void didUpdateWidget(covariant GameWidget<Game> 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<void> 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<Widget> 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<Widget> _addBackground(BuildContext context, List<Widget> 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<Widget> _addOverlays(BuildContext context, List<Widget> 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<Type, GestureRecognizerFactory> 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(),
(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<Type, GestureRecognizerFactory> 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(),
(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),
),
);
final hasBasicDetectors = _hasBasicGestureDetectors(game);
final hasAdvancedDetectors = _hasAdvancedGesturesDetectors(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) {
widget = _applyBasicGesturesDetectors(game, widget);
} else if (hasAdvancedDetectors) {
widget = _applyAdvancedGesturesDetectors(game, widget);
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;
}
if (_hasMouseDetectors(game)) {
widget = _applyMouseDetectors(game, widget);
}
class _GameRenderObjectWidget extends LeafRenderObjectWidget {
final Game game;
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);
_GameRenderObjectWidget(this.game);
@override
State<StatefulWidget> createState() => _OverlayGameWidgetState();
}
class _OverlayGameWidgetState extends State<OverlayGameWidget> {
final Map<String, Widget> _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(),
RenderBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(
child: GameRenderBox(context, game),
additionalConstraints: const BoxConstraints.expand(),
);
}
}

View File

@ -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();
});

View File

@ -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);