mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 17:06:50 +08:00 
			
		
		
		
	refactor classes in game.dart to separate files
This commit is contained in:
		
							
								
								
									
										4
									
								
								doc/examples/particles/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								doc/examples/particles/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -71,3 +71,7 @@ | ||||
| !**/ios/**/default.pbxuser | ||||
| !**/ios/**/default.perspectivev3 | ||||
| !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages | ||||
|  | ||||
| .flutter-plugins-dependencies | ||||
| macos | ||||
| test | ||||
|  | ||||
| @ -217,7 +217,7 @@ class MyGame extends BaseGame { | ||||
|     return Particle.generate( | ||||
|       count: 5, | ||||
|       generator: (i) => MovingParticle( | ||||
|         curve: Interval(.2, .6, curve: Curves.easeInOutCubic), | ||||
|         curve: const Interval(.2, .6, curve: Curves.easeInOutCubic), | ||||
|         to: randomCellOffset() * .5, | ||||
|         child: CircleParticle( | ||||
|           radius: 5 + rnd.nextDouble() * 5, | ||||
|  | ||||
							
								
								
									
										2
									
								
								doc/examples/sprites/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								doc/examples/sprites/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -70,7 +70,5 @@ | ||||
| !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages | ||||
|  | ||||
| macos | ||||
|  | ||||
| test | ||||
|  | ||||
| .flutter-plugins-dependencies | ||||
|  | ||||
							
								
								
									
										3
									
								
								example/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								example/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -71,3 +71,6 @@ build/ | ||||
| !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages | ||||
|  | ||||
| .flutter-plugins-dependencies | ||||
|  | ||||
| macos | ||||
| test | ||||
|  | ||||
| @ -2,7 +2,7 @@ import 'dart:ui'; | ||||
|  | ||||
| import 'package:flame/box2d/box2d_component.dart'; | ||||
| import 'package:flame/components/component.dart'; | ||||
| import 'package:flame/game.dart'; | ||||
| import 'package:flame/game/base_game.dart'; | ||||
|  | ||||
| class Box2DGame extends BaseGame { | ||||
|   final Box2DComponent box; | ||||
|  | ||||
| @ -2,7 +2,7 @@ import 'dart:ui'; | ||||
|  | ||||
| import 'package:flame/components/mixins/has_game_ref.dart'; | ||||
| import 'package:flame/components/mixins/tapable.dart'; | ||||
| import 'package:flame/game.dart'; | ||||
| import 'package:flame/game/base_game.dart'; | ||||
| import 'package:ordered_set/comparing.dart'; | ||||
| import 'package:ordered_set/ordered_set.dart'; | ||||
|  | ||||
|  | ||||
| @ -1,2 +1,3 @@ | ||||
| // Keeping compatible with earlier versions of Flame | ||||
| export './game/game.dart'; | ||||
| export './game/base_game.dart'; | ||||
|  | ||||
							
								
								
									
										194
									
								
								lib/game/base_game.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								lib/game/base_game.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,194 @@ | ||||
| import 'dart:math' as math; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:flame/components/composed_component.dart'; | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/rendering.dart'; | ||||
| import 'package:flutter/widgets.dart' hide WidgetBuilder; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:ordered_set/comparing.dart'; | ||||
| import 'package:ordered_set/ordered_set.dart'; | ||||
|  | ||||
| import '../components/component.dart'; | ||||
| import '../components/mixins/has_game_ref.dart'; | ||||
| import '../components/mixins/tapable.dart'; | ||||
| import '../position.dart'; | ||||
| import '../gestures.dart'; | ||||
| import 'game.dart'; | ||||
|  | ||||
|  | ||||
| /// This is a more complete and opinionated implementation of Game. | ||||
| /// | ||||
| /// It still needs to be subclasses to add your game logic, but the [update], [render] and [resize] methods have default implementations. | ||||
| /// This is the recommended structure to use for most games. | ||||
| /// It is based on the Component system. | ||||
| abstract class BaseGame extends Game with TapDetector { | ||||
|   /// The list of components to be updated and rendered by the base game. | ||||
|   OrderedSet<Component> components = | ||||
|   OrderedSet(Comparing.on((c) => c.priority())); | ||||
|  | ||||
|   /// Components added by the [addLater] method | ||||
|   final List<Component> _addLater = []; | ||||
|  | ||||
|   /// Current screen size, updated every resize via the [resize] method hook | ||||
|   Size size; | ||||
|  | ||||
|   /// Camera position; every non-HUD component is translated so that the camera position is the top-left corner of the screen. | ||||
|   Position camera = Position.empty(); | ||||
|  | ||||
|   /// List of deltas used in debug mode to calculate FPS | ||||
|   final List<double> _dts = []; | ||||
|  | ||||
|   Iterable<Tapable> get _tapableComponents => | ||||
|       components.where((c) => c is Tapable).cast(); | ||||
|  | ||||
|   @override | ||||
|   void onTapCancel() { | ||||
|     _tapableComponents.forEach((c) => c.handleTapCancel()); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onTapDown(TapDownDetails details) { | ||||
|     _tapableComponents.forEach((c) => c.handleTapDown(details)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onTapUp(TapUpDetails details) { | ||||
|     _tapableComponents.forEach((c) => c.handleTapUp(details)); | ||||
|   } | ||||
|  | ||||
|   /// This method is called for every component added, both via [add] and [addLater] methods. | ||||
|   /// | ||||
|   /// You can use this to setup your mixins, pre-calculate stuff on every component, or anything you desire. | ||||
|   /// By default, this calls the first time resize for every component, so don't forget to call super.preAdd when overriding. | ||||
|   @mustCallSuper | ||||
|   void preAdd(Component c) { | ||||
|     if (debugMode() && c is PositionComponent) { | ||||
|       c.debugMode = true; | ||||
|     } | ||||
|  | ||||
|     // first time resize | ||||
|     if (size != null) { | ||||
|       c.resize(size); | ||||
|     } | ||||
|  | ||||
|     if (c is HasGameRef) { | ||||
|       (c as HasGameRef).gameRef = this; | ||||
|     } | ||||
|  | ||||
|     if (c is ComposedComponent) { | ||||
|       c.components.forEach(preAdd); | ||||
|     } | ||||
|  | ||||
|     c.onMount(); | ||||
|   } | ||||
|  | ||||
|   /// Adds a new component to the components list. | ||||
|   /// | ||||
|   /// Also calls [preAdd], witch in turn sets the current size on the component (because the resize hook won't be called until a new resize happens). | ||||
|   void add(Component c) { | ||||
|     preAdd(c); | ||||
|     components.add(c); | ||||
|   } | ||||
|  | ||||
|   /// Registers a component to be added on the components on the next tick. | ||||
|   /// | ||||
|   /// Use this to add components in places where a concurrent issue with the update method might happen. | ||||
|   /// Also calls [preAdd] for the component added, immediately. | ||||
|   void addLater(Component c) { | ||||
|     preAdd(c); | ||||
|     _addLater.add(c); | ||||
|   } | ||||
|  | ||||
|   /// This implementation of render basically calls [renderComponent] for every component, making sure the canvas is reset for each one. | ||||
|   /// | ||||
|   /// You can override it further to add more custom behaviour. | ||||
|   /// Beware of however you are rendering components if not using this; you must be careful to save and restore the canvas to avoid components messing up with each other. | ||||
|   @override | ||||
|   void render(Canvas canvas) { | ||||
|     canvas.save(); | ||||
|     components.forEach((comp) => renderComponent(canvas, comp)); | ||||
|     canvas.restore(); | ||||
|   } | ||||
|  | ||||
|   /// This renders a single component obeying BaseGame rules. | ||||
|   /// | ||||
|   /// It translates the camera unless hud, call the render method and restore the canvas. | ||||
|   /// This makes sure the canvas is not messed up by one component and all components render independently. | ||||
|   void renderComponent(Canvas canvas, Component c) { | ||||
|     if (!c.loaded()) { | ||||
|       return; | ||||
|     } | ||||
|     if (!c.isHud()) { | ||||
|       canvas.translate(-camera.x, -camera.y); | ||||
|     } | ||||
|     c.render(canvas); | ||||
|     canvas.restore(); | ||||
|     canvas.save(); | ||||
|   } | ||||
|  | ||||
|   /// This implementation of update updates every component in the list. | ||||
|   /// | ||||
|   /// It also actually adds the components that were added by the [addLater] method, and remove those that are marked for destruction via the [Component.destroy] method. | ||||
|   /// You can override it further to add more custom behaviour. | ||||
|   @override | ||||
|   void update(double t) { | ||||
|     components.addAll(_addLater); | ||||
|     _addLater.clear(); | ||||
|  | ||||
|     components.forEach((c) => c.update(t)); | ||||
|     components.removeWhere((c) => c.destroy()).forEach((c) => c.onDestroy()); | ||||
|   } | ||||
|  | ||||
|   /// This implementation of resize passes the resize call along to every component in the list, enabling each one to make their decisions as how to handle the resize. | ||||
|   /// | ||||
|   /// It also updates the [size] field of the class to be used by later added components and other methods. | ||||
|   /// You can override it further to add more custom behaviour, but you should seriously consider calling the super implementation as well. | ||||
|   @override | ||||
|   @mustCallSuper | ||||
|   void resize(Size size) { | ||||
|     this.size = size; | ||||
|     components.forEach((c) => c.resize(size)); | ||||
|   } | ||||
|  | ||||
|   /// Returns whether this [Game] is in debug mode or not. | ||||
|   /// | ||||
|   /// Returns `false` by default. Override to use the debug mode. | ||||
|   /// In debug mode, the [recordDt] method actually records every `dt` for statistics. | ||||
|   /// Then, you can use the [fps] method to check the game FPS. | ||||
|   /// You can also use this value to enable other debug behaviors for your game, like bounding box rendering, for instance. | ||||
|   bool debugMode() => false; | ||||
|  | ||||
|   /// This is a hook that comes from the RenderBox to allow recording of render times and statistics. | ||||
|   @override | ||||
|   void recordDt(double dt) { | ||||
|     if (debugMode()) { | ||||
|       _dts.add(dt); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Returns the average FPS for the last [average] measures. | ||||
|   /// | ||||
|   /// The values are only saved if in debug mode (override [debugMode] to use this). | ||||
|   /// Selects the last [average] dts, averages then, and returns the inverse value. | ||||
|   /// So it's technically updates per second, but the relation between updates and renders is 1:1. | ||||
|   /// Returns 0 if empty. | ||||
|   double fps([int average = 1]) { | ||||
|     final List<double> dts = _dts.sublist(math.max(0, _dts.length - average)); | ||||
|     if (dts.isEmpty) { | ||||
|       return 0.0; | ||||
|     } | ||||
|     final double dtSum = dts.reduce((s, t) => s + t); | ||||
|     final double averageDt = dtSum / average; | ||||
|     return 1 / averageDt; | ||||
|   } | ||||
|  | ||||
|   /// Returns the current time in seconds with microseconds precision. | ||||
|   /// | ||||
|   /// This is compatible with the `dt` value used in the [update] method. | ||||
|   double currentTime() { | ||||
|     return DateTime.now().microsecondsSinceEpoch.toDouble() / | ||||
|         Duration.microsecondsPerSecond; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										38
									
								
								lib/game/embedded_game_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/game/embedded_game_widget.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| import 'package:flutter/rendering.dart'; | ||||
| import 'package:flutter/widgets.dart' hide WidgetBuilder; | ||||
|  | ||||
| import '../position.dart'; | ||||
|  | ||||
| import 'game_render_box.dart'; | ||||
| import 'game.dart'; | ||||
|  | ||||
| /// This a widget to embed a game inside the Widget tree. You can use it in pair with [SimpleGame] 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; | ||||
|   final Position size; | ||||
|  | ||||
|   EmbeddedGameWidget(this.game, {this.size}); | ||||
|  | ||||
|   @override | ||||
|   RenderBox createRenderObject(BuildContext context) { | ||||
|     return RenderConstrainedBox( | ||||
|         child: GameRenderBox(context, game), | ||||
|         additionalConstraints: | ||||
|         BoxConstraints.expand(width: size?.x, height: size?.y)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void updateRenderObject( | ||||
|       BuildContext context, RenderConstrainedBox renderBox) { | ||||
|     renderBox | ||||
|       ..child = GameRenderBox(context, game) | ||||
|       ..additionalConstraints = | ||||
|       BoxConstraints.expand(width: size?.x, height: size?.y); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,25 +1,14 @@ | ||||
| import 'dart:math' as math; | ||||
| import 'dart:ui'; | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:flame/components/composed_component.dart'; | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/rendering.dart'; | ||||
| import 'package:flutter/scheduler.dart'; | ||||
| import 'package:flutter/widgets.dart' hide WidgetBuilder; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:ordered_set/comparing.dart'; | ||||
| import 'package:ordered_set/ordered_set.dart'; | ||||
|  | ||||
| import '../components/component.dart'; | ||||
| import '../components/mixins/has_game_ref.dart'; | ||||
| import '../components/mixins/tapable.dart'; | ||||
| import '../position.dart'; | ||||
| import '../gestures.dart'; | ||||
| import '../keyboard.dart'; | ||||
|  | ||||
| import 'game_loop.dart'; | ||||
| import 'widget_builder.dart'; | ||||
|  | ||||
| /// Represents a generic game. | ||||
| @ -55,7 +44,7 @@ abstract class Game { | ||||
|   void lifecycleStateChange(AppLifecycleState state) {} | ||||
|  | ||||
|   /// Used for debugging | ||||
|   void _recordDt(double dt) {} | ||||
|   void recordDt(double dt) {} | ||||
|  | ||||
|   /// 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). | ||||
| @ -95,13 +84,13 @@ abstract class Game { | ||||
|   bool runOnCreation = true; | ||||
|  | ||||
|   /// Pauses the engine game loop execution | ||||
|   void pauseEngine() => _pauseEngineFn?.call(); | ||||
|   void pauseEngine() => pauseEngineFn?.call(); | ||||
|  | ||||
|   /// Resumes the engine game loop execution | ||||
|   void resumeEngine() => _resumeEngineFn?.call(); | ||||
|   void resumeEngine() => resumeEngineFn?.call(); | ||||
|  | ||||
|   VoidCallback _pauseEngineFn; | ||||
|   VoidCallback _resumeEngineFn; | ||||
|   VoidCallback pauseEngineFn; | ||||
|   VoidCallback resumeEngineFn; | ||||
| } | ||||
|  | ||||
| class OverlayWidget { | ||||
| @ -127,289 +116,4 @@ mixin HasWidgetsOverlay on Game { | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// This is a more complete and opinionated implementation of Game. | ||||
| /// | ||||
| /// It still needs to be subclasses to add your game logic, but the [update], [render] and [resize] methods have default implementations. | ||||
| /// This is the recommended structure to use for most games. | ||||
| /// It is based on the Component system. | ||||
| abstract class BaseGame extends Game with TapDetector { | ||||
|   /// The list of components to be updated and rendered by the base game. | ||||
|   OrderedSet<Component> components = | ||||
|       OrderedSet(Comparing.on((c) => c.priority())); | ||||
|  | ||||
|   /// Components added by the [addLater] method | ||||
|   final List<Component> _addLater = []; | ||||
|  | ||||
|   /// Current screen size, updated every resize via the [resize] method hook | ||||
|   Size size; | ||||
|  | ||||
|   /// Camera position; every non-HUD component is translated so that the camera position is the top-left corner of the screen. | ||||
|   Position camera = Position.empty(); | ||||
|  | ||||
|   /// List of deltas used in debug mode to calculate FPS | ||||
|   final List<double> _dts = []; | ||||
|  | ||||
|   Iterable<Tapable> get _tapableComponents => | ||||
|       components.where((c) => c is Tapable).cast(); | ||||
|  | ||||
|   @override | ||||
|   void onTapCancel() { | ||||
|     _tapableComponents.forEach((c) => c.handleTapCancel()); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onTapDown(TapDownDetails details) { | ||||
|     _tapableComponents.forEach((c) => c.handleTapDown(details)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onTapUp(TapUpDetails details) { | ||||
|     _tapableComponents.forEach((c) => c.handleTapUp(details)); | ||||
|   } | ||||
|  | ||||
|   /// This method is called for every component added, both via [add] and [addLater] methods. | ||||
|   /// | ||||
|   /// You can use this to setup your mixins, pre-calculate stuff on every component, or anything you desire. | ||||
|   /// By default, this calls the first time resize for every component, so don't forget to call super.preAdd when overriding. | ||||
|   @mustCallSuper | ||||
|   void preAdd(Component c) { | ||||
|     if (debugMode() && c is PositionComponent) { | ||||
|       c.debugMode = true; | ||||
|     } | ||||
|  | ||||
|     // first time resize | ||||
|     if (size != null) { | ||||
|       c.resize(size); | ||||
|     } | ||||
|  | ||||
|     if (c is HasGameRef) { | ||||
|       (c as HasGameRef).gameRef = this; | ||||
|     } | ||||
|  | ||||
|     if (c is ComposedComponent) { | ||||
|       c.components.forEach(preAdd); | ||||
|     } | ||||
|  | ||||
|     c.onMount(); | ||||
|   } | ||||
|  | ||||
|   /// Adds a new component to the components list. | ||||
|   /// | ||||
|   /// Also calls [preAdd], witch in turn sets the current size on the component (because the resize hook won't be called until a new resize happens). | ||||
|   void add(Component c) { | ||||
|     preAdd(c); | ||||
|     components.add(c); | ||||
|   } | ||||
|  | ||||
|   /// Registers a component to be added on the components on the next tick. | ||||
|   /// | ||||
|   /// Use this to add components in places where a concurrent issue with the update method might happen. | ||||
|   /// Also calls [preAdd] for the component added, immediately. | ||||
|   void addLater(Component c) { | ||||
|     preAdd(c); | ||||
|     _addLater.add(c); | ||||
|   } | ||||
|  | ||||
|   /// This implementation of render basically calls [renderComponent] for every component, making sure the canvas is reset for each one. | ||||
|   /// | ||||
|   /// You can override it further to add more custom behaviour. | ||||
|   /// Beware of however you are rendering components if not using this; you must be careful to save and restore the canvas to avoid components messing up with each other. | ||||
|   @override | ||||
|   void render(Canvas canvas) { | ||||
|     canvas.save(); | ||||
|     components.forEach((comp) => renderComponent(canvas, comp)); | ||||
|     canvas.restore(); | ||||
|   } | ||||
|  | ||||
|   /// This renders a single component obeying BaseGame rules. | ||||
|   /// | ||||
|   /// It translates the camera unless hud, call the render method and restore the canvas. | ||||
|   /// This makes sure the canvas is not messed up by one component and all components render independently. | ||||
|   void renderComponent(Canvas canvas, Component c) { | ||||
|     if (!c.loaded()) { | ||||
|       return; | ||||
|     } | ||||
|     if (!c.isHud()) { | ||||
|       canvas.translate(-camera.x, -camera.y); | ||||
|     } | ||||
|     c.render(canvas); | ||||
|     canvas.restore(); | ||||
|     canvas.save(); | ||||
|   } | ||||
|  | ||||
|   /// This implementation of update updates every component in the list. | ||||
|   /// | ||||
|   /// It also actually adds the components that were added by the [addLater] method, and remove those that are marked for destruction via the [Component.destroy] method. | ||||
|   /// You can override it further to add more custom behaviour. | ||||
|   @override | ||||
|   void update(double t) { | ||||
|     components.addAll(_addLater); | ||||
|     _addLater.clear(); | ||||
|  | ||||
|     components.forEach((c) => c.update(t)); | ||||
|     components.removeWhere((c) => c.destroy()).forEach((c) => c.onDestroy()); | ||||
|   } | ||||
|  | ||||
|   /// This implementation of resize passes the resize call along to every component in the list, enabling each one to make their decisions as how to handle the resize. | ||||
|   /// | ||||
|   /// It also updates the [size] field of the class to be used by later added components and other methods. | ||||
|   /// You can override it further to add more custom behaviour, but you should seriously consider calling the super implementation as well. | ||||
|   @override | ||||
|   @mustCallSuper | ||||
|   void resize(Size size) { | ||||
|     this.size = size; | ||||
|     components.forEach((c) => c.resize(size)); | ||||
|   } | ||||
|  | ||||
|   /// Returns whether this [Game] is in debug mode or not. | ||||
|   /// | ||||
|   /// Returns `false` by default. Override to use the debug mode. | ||||
|   /// In debug mode, the [_recordDt] method actually records every `dt` for statistics. | ||||
|   /// Then, you can use the [fps] method to check the game FPS. | ||||
|   /// You can also use this value to enable other debug behaviors for your game, like bounding box rendering, for instance. | ||||
|   bool debugMode() => false; | ||||
|  | ||||
|   /// This is a hook that comes from the RenderBox to allow recording of render times and statistics. | ||||
|   @override | ||||
|   void _recordDt(double dt) { | ||||
|     if (debugMode()) { | ||||
|       _dts.add(dt); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Returns the average FPS for the last [average] measures. | ||||
|   /// | ||||
|   /// The values are only saved if in debug mode (override [debugMode] to use this). | ||||
|   /// Selects the last [average] dts, averages then, and returns the inverse value. | ||||
|   /// So it's technically updates per second, but the relation between updates and renders is 1:1. | ||||
|   /// Returns 0 if empty. | ||||
|   double fps([int average = 1]) { | ||||
|     final List<double> dts = _dts.sublist(math.max(0, _dts.length - average)); | ||||
|     if (dts.isEmpty) { | ||||
|       return 0.0; | ||||
|     } | ||||
|     final double dtSum = dts.reduce((s, t) => s + t); | ||||
|     final double averageDt = dtSum / average; | ||||
|     return 1 / averageDt; | ||||
|   } | ||||
|  | ||||
|   /// Returns the current time in seconds with microseconds precision. | ||||
|   /// | ||||
|   /// This is compatible with the `dt` value used in the [update] method. | ||||
|   double currentTime() { | ||||
|     return DateTime.now().microsecondsSinceEpoch.toDouble() / | ||||
|         Duration.microsecondsPerSecond; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// This is a helper implementation of a [BaseGame] designed to allow to easily create a game with a single component. | ||||
| /// | ||||
| /// This is useful to add sprites, animations and other Flame components "directly" to your non-game Flutter widget tree, when combined with [EmbeddedGameWidget]. | ||||
| class SimpleGame extends BaseGame { | ||||
|   SimpleGame(Component c) { | ||||
|     add(c); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// This a widget to embed a game inside the Widget tree. You can use it in pair with [SimpleGame] 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; | ||||
|   final Position size; | ||||
|  | ||||
|   EmbeddedGameWidget(this.game, {this.size}); | ||||
|  | ||||
|   @override | ||||
|   RenderBox createRenderObject(BuildContext context) { | ||||
|     return RenderConstrainedBox( | ||||
|         child: GameRenderBox(context, game), | ||||
|         additionalConstraints: | ||||
|             BoxConstraints.expand(width: size?.x, height: size?.y)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void updateRenderObject( | ||||
|       BuildContext context, RenderConstrainedBox renderBox) { | ||||
|     renderBox | ||||
|       ..child = GameRenderBox(context, game) | ||||
|       ..additionalConstraints = | ||||
|           BoxConstraints.expand(width: size?.x, height: size?.y); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class GameRenderBox extends RenderBox with WidgetsBindingObserver { | ||||
|   BuildContext context; | ||||
|   Game game; | ||||
|   GameLoop gameLoop; | ||||
|  | ||||
|   GameRenderBox(this.context, this.game) { | ||||
|     gameLoop = GameLoop(gameLoopCallback); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool get sizedByParent => true; | ||||
|  | ||||
|   @override | ||||
|   void performResize() { | ||||
|     super.performResize(); | ||||
|     game.resize(constraints.biggest); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void attach(PipelineOwner owner) { | ||||
|     super.attach(owner); | ||||
|     game.onAttach(); | ||||
|  | ||||
|     game._pauseEngineFn = gameLoop.pause; | ||||
|     game._resumeEngineFn = gameLoop.resume; | ||||
|  | ||||
|     if (game.runOnCreation) { | ||||
|       gameLoop.scheduleTick(); | ||||
|     } | ||||
|  | ||||
|     _bindLifecycleListener(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void detach() { | ||||
|     super.detach(); | ||||
|     game.onDetach(); | ||||
|     gameLoop.unscheduleTick(); | ||||
|     _unbindLifecycleListener(); | ||||
|   } | ||||
|  | ||||
|   void gameLoopCallback(double dt) { | ||||
|     if (!attached) { | ||||
|       return; | ||||
|     } | ||||
|     game._recordDt(dt); | ||||
|     game.update(dt); | ||||
|     markNeedsPaint(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void paint(PaintingContext context, Offset offset) { | ||||
|     context.canvas.save(); | ||||
|     context.canvas.translate( | ||||
|         game.builder.offset.dx + offset.dx, game.builder.offset.dy + offset.dy); | ||||
|     game.render(context.canvas); | ||||
|     context.canvas.restore(); | ||||
|   } | ||||
|  | ||||
|   void _bindLifecycleListener() { | ||||
|     WidgetsBinding.instance.addObserver(this); | ||||
|   } | ||||
|  | ||||
|   void _unbindLifecycleListener() { | ||||
|     WidgetsBinding.instance.removeObserver(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||
|     game.lifecycleStateChange(state); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										82
									
								
								lib/game/game_render_box.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								lib/game/game_render_box.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/rendering.dart'; | ||||
| import 'package:flutter/scheduler.dart'; | ||||
| import 'package:flutter/widgets.dart' hide WidgetBuilder; | ||||
|  | ||||
| import 'game_loop.dart'; | ||||
| import 'game.dart'; | ||||
|  | ||||
| class GameRenderBox extends RenderBox with WidgetsBindingObserver { | ||||
|   BuildContext context; | ||||
|   Game game; | ||||
|   GameLoop gameLoop; | ||||
|  | ||||
|   GameRenderBox(this.context, this.game) { | ||||
|     gameLoop = GameLoop(gameLoopCallback); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool get sizedByParent => true; | ||||
|  | ||||
|   @override | ||||
|   void performResize() { | ||||
|     super.performResize(); | ||||
|     game.resize(constraints.biggest); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void attach(PipelineOwner owner) { | ||||
|     super.attach(owner); | ||||
|     game.onAttach(); | ||||
|  | ||||
|     game.pauseEngineFn = gameLoop.pause; | ||||
|     game.resumeEngineFn = gameLoop.resume; | ||||
|  | ||||
|     if (game.runOnCreation) { | ||||
|       gameLoop.scheduleTick(); | ||||
|     } | ||||
|  | ||||
|     _bindLifecycleListener(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void detach() { | ||||
|     super.detach(); | ||||
|     game.onDetach(); | ||||
|     gameLoop.unscheduleTick(); | ||||
|     _unbindLifecycleListener(); | ||||
|   } | ||||
|  | ||||
|   void gameLoopCallback(double dt) { | ||||
|     if (!attached) { | ||||
|       return; | ||||
|     } | ||||
|     game.recordDt(dt); | ||||
|     game.update(dt); | ||||
|     markNeedsPaint(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void paint(PaintingContext context, Offset offset) { | ||||
|     context.canvas.save(); | ||||
|     context.canvas.translate( | ||||
|         game.builder.offset.dx + offset.dx, game.builder.offset.dy + offset.dy); | ||||
|     game.render(context.canvas); | ||||
|     context.canvas.restore(); | ||||
|   } | ||||
|  | ||||
|   void _bindLifecycleListener() { | ||||
|     WidgetsBinding.instance.addObserver(this); | ||||
|   } | ||||
|  | ||||
|   void _unbindLifecycleListener() { | ||||
|     WidgetsBinding.instance.removeObserver(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||
|     game.lifecycleStateChange(state); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										12
									
								
								lib/game/simple_game.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								lib/game/simple_game.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| import 'package:flame/components/component.dart'; | ||||
|  | ||||
| import 'base_game.dart'; | ||||
|  | ||||
| /// This is a helper implementation of a [BaseGame] designed to allow to easily create a game with a single component. | ||||
| /// | ||||
| /// This is useful to add sprites, animations and other Flame components "directly" to your non-game Flutter widget tree, when combined with [EmbeddedGameWidget]. | ||||
| class SimpleGame extends BaseGame { | ||||
|   SimpleGame(Component c) { | ||||
|     add(c); | ||||
|   } | ||||
| } | ||||
| @ -1,5 +1,6 @@ | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import '../gestures.dart'; | ||||
| import 'embedded_game_widget.dart'; | ||||
| import 'game.dart'; | ||||
|  | ||||
| class WidgetBuilder { | ||||
| @ -9,70 +10,53 @@ class WidgetBuilder { | ||||
|     return GestureDetector( | ||||
|       // Taps | ||||
|       onTap: game is TapDetector ? () => (game as TapDetector).onTap() : null, | ||||
|       onTapCancel: game is TapDetector | ||||
|           ? () => (game as TapDetector).onTapCancel() | ||||
|           : null, | ||||
|       onTapDown: game is TapDetector | ||||
|           ? (TapDownDetails d) => (game as TapDetector).onTapDown(d) | ||||
|           : null, | ||||
|       onTapUp: game is TapDetector | ||||
|           ? (TapUpDetails d) => (game as TapDetector).onTapUp(d) | ||||
|           : null, | ||||
|       onTapCancel: game is TapDetector ? () => (game as TapDetector).onTapCancel() : null, | ||||
|       onTapDown: | ||||
|           game is TapDetector ? (TapDownDetails d) => (game as TapDetector).onTapDown(d) : null, | ||||
|       onTapUp: game is TapDetector ? (TapUpDetails d) => (game as TapDetector).onTapUp(d) : null, | ||||
|  | ||||
|       // Secondary taps | ||||
|       onSecondaryTapDown: game is SecondaryTapDetector | ||||
|           ? (TapDownDetails d) => | ||||
|               (game as SecondaryTapDetector).onSecondaryTapDown(d) | ||||
|           ? (TapDownDetails d) => (game as SecondaryTapDetector).onSecondaryTapDown(d) | ||||
|           : null, | ||||
|       onSecondaryTapUp: game is SecondaryTapDetector | ||||
|           ? (TapUpDetails d) => | ||||
|               (game as SecondaryTapDetector).onSecondaryTapUp(d) | ||||
|           ? (TapUpDetails d) => (game as SecondaryTapDetector).onSecondaryTapUp(d) | ||||
|           : null, | ||||
|       onSecondaryTapCancel: game is SecondaryTapDetector | ||||
|           ? () => (game as SecondaryTapDetector).onSecondaryTapCancel() | ||||
|           : null, | ||||
|  | ||||
|       // Double tap | ||||
|       onDoubleTap: game is DoubleTapDetector | ||||
|           ? () => (game as DoubleTapDetector).onDoubleTap() | ||||
|           : null, | ||||
|       onDoubleTap: | ||||
|           game is DoubleTapDetector ? () => (game as DoubleTapDetector).onDoubleTap() : null, | ||||
|  | ||||
|       // Long presses | ||||
|       onLongPress: game is LongPressDetector | ||||
|           ? () => (game as LongPressDetector).onLongPress() | ||||
|           : null, | ||||
|       onLongPress: | ||||
|           game is LongPressDetector ? () => (game as LongPressDetector).onLongPress() : null, | ||||
|       onLongPressStart: game is LongPressDetector | ||||
|           ? (LongPressStartDetails d) => | ||||
|               (game as LongPressDetector).onLongPressStart(d) | ||||
|           ? (LongPressStartDetails d) => (game as LongPressDetector).onLongPressStart(d) | ||||
|           : null, | ||||
|       onLongPressMoveUpdate: game is LongPressDetector | ||||
|           ? (LongPressMoveUpdateDetails d) => | ||||
|               (game as LongPressDetector).onLongPressMoveUpdate(d) | ||||
|           : null, | ||||
|       onLongPressUp: game is LongPressDetector | ||||
|           ? () => (game as LongPressDetector).onLongPressUp() | ||||
|           ? (LongPressMoveUpdateDetails d) => (game as LongPressDetector).onLongPressMoveUpdate(d) | ||||
|           : null, | ||||
|       onLongPressUp: | ||||
|           game is LongPressDetector ? () => (game as LongPressDetector).onLongPressUp() : null, | ||||
|       onLongPressEnd: game is LongPressDetector | ||||
|           ? (LongPressEndDetails d) => | ||||
|               (game as LongPressDetector).onLongPressEnd(d) | ||||
|           ? (LongPressEndDetails d) => (game as LongPressDetector).onLongPressEnd(d) | ||||
|           : null, | ||||
|  | ||||
|       // Vertical drag | ||||
|       onVerticalDragDown: game is VerticalDragDetector | ||||
|           ? (DragDownDetails d) => | ||||
|               (game as VerticalDragDetector).onVerticalDragDown(d) | ||||
|           ? (DragDownDetails d) => (game as VerticalDragDetector).onVerticalDragDown(d) | ||||
|           : null, | ||||
|       onVerticalDragStart: game is VerticalDragDetector | ||||
|           ? (DragStartDetails d) => | ||||
|               (game as VerticalDragDetector).onVerticalDragStart(d) | ||||
|           ? (DragStartDetails d) => (game as VerticalDragDetector).onVerticalDragStart(d) | ||||
|           : null, | ||||
|       onVerticalDragUpdate: game is VerticalDragDetector | ||||
|           ? (DragUpdateDetails d) => | ||||
|               (game as VerticalDragDetector).onVerticalDragUpdate(d) | ||||
|           ? (DragUpdateDetails d) => (game as VerticalDragDetector).onVerticalDragUpdate(d) | ||||
|           : null, | ||||
|       onVerticalDragEnd: game is VerticalDragDetector | ||||
|           ? (DragEndDetails d) => | ||||
|               (game as VerticalDragDetector).onVerticalDragEnd(d) | ||||
|           ? (DragEndDetails d) => (game as VerticalDragDetector).onVerticalDragEnd(d) | ||||
|           : null, | ||||
|       onVerticalDragCancel: game is VerticalDragDetector | ||||
|           ? () => (game as VerticalDragDetector).onVerticalDragCancel() | ||||
| @ -80,20 +64,16 @@ class WidgetBuilder { | ||||
|  | ||||
|       // Horizontal drag | ||||
|       onHorizontalDragDown: game is HorizontalDragDetector | ||||
|           ? (DragDownDetails d) => | ||||
|               (game as HorizontalDragDetector).onHorizontalDragDown(d) | ||||
|           ? (DragDownDetails d) => (game as HorizontalDragDetector).onHorizontalDragDown(d) | ||||
|           : null, | ||||
|       onHorizontalDragStart: game is HorizontalDragDetector | ||||
|           ? (DragStartDetails d) => | ||||
|               (game as HorizontalDragDetector).onHorizontalDragStart(d) | ||||
|           ? (DragStartDetails d) => (game as HorizontalDragDetector).onHorizontalDragStart(d) | ||||
|           : null, | ||||
|       onHorizontalDragUpdate: game is HorizontalDragDetector | ||||
|           ? (DragUpdateDetails d) => | ||||
|               (game as HorizontalDragDetector).onHorizontalDragUpdate(d) | ||||
|           ? (DragUpdateDetails d) => (game as HorizontalDragDetector).onHorizontalDragUpdate(d) | ||||
|           : null, | ||||
|       onHorizontalDragEnd: game is HorizontalDragDetector | ||||
|           ? (DragEndDetails d) => | ||||
|               (game as HorizontalDragDetector).onHorizontalDragEnd(d) | ||||
|           ? (DragEndDetails d) => (game as HorizontalDragDetector).onHorizontalDragEnd(d) | ||||
|           : null, | ||||
|       onHorizontalDragCancel: game is HorizontalDragDetector | ||||
|           ? () => (game as HorizontalDragDetector).onHorizontalDragCancel() | ||||
| @ -101,38 +81,29 @@ class WidgetBuilder { | ||||
|  | ||||
|       // Force presses | ||||
|       onForcePressStart: game is ForcePressDetector | ||||
|           ? (ForcePressDetails d) => | ||||
|               (game as ForcePressDetector).onForcePressStart(d) | ||||
|           ? (ForcePressDetails d) => (game as ForcePressDetector).onForcePressStart(d) | ||||
|           : null, | ||||
|       onForcePressPeak: game is ForcePressDetector | ||||
|           ? (ForcePressDetails d) => | ||||
|               (game as ForcePressDetector).onForcePressPeak(d) | ||||
|           ? (ForcePressDetails d) => (game as ForcePressDetector).onForcePressPeak(d) | ||||
|           : null, | ||||
|       onForcePressUpdate: game is ForcePressDetector | ||||
|           ? (ForcePressDetails d) => | ||||
|               (game as ForcePressDetector).onForcePressUpdate(d) | ||||
|           ? (ForcePressDetails d) => (game as ForcePressDetector).onForcePressUpdate(d) | ||||
|           : null, | ||||
|       onForcePressEnd: game is ForcePressDetector | ||||
|           ? (ForcePressDetails d) => | ||||
|               (game as ForcePressDetector).onForcePressEnd(d) | ||||
|           ? (ForcePressDetails d) => (game as ForcePressDetector).onForcePressEnd(d) | ||||
|           : null, | ||||
|  | ||||
|       // Pan | ||||
|       onPanDown: game is PanDetector | ||||
|           ? (DragDownDetails d) => (game as PanDetector).onPanDown(d) | ||||
|           : null, | ||||
|       onPanStart: game is PanDetector | ||||
|           ? (DragStartDetails d) => (game as PanDetector).onPanStart(d) | ||||
|           : null, | ||||
|       onPanDown: | ||||
|           game is PanDetector ? (DragDownDetails d) => (game as PanDetector).onPanDown(d) : null, | ||||
|       onPanStart: | ||||
|           game is PanDetector ? (DragStartDetails d) => (game as PanDetector).onPanStart(d) : null, | ||||
|       onPanUpdate: game is PanDetector | ||||
|           ? (DragUpdateDetails d) => (game as PanDetector).onPanUpdate(d) | ||||
|           : null, | ||||
|       onPanEnd: game is PanDetector | ||||
|           ? (DragEndDetails d) => (game as PanDetector).onPanEnd(d) | ||||
|           : null, | ||||
|       onPanCancel: game is PanDetector | ||||
|           ? () => (game as PanDetector).onPanCancel() | ||||
|           : null, | ||||
|       onPanEnd: | ||||
|           game is PanDetector ? (DragEndDetails d) => (game as PanDetector).onPanEnd(d) : null, | ||||
|       onPanCancel: game is PanDetector ? () => (game as PanDetector).onPanCancel() : null, | ||||
|  | ||||
|       // Scales | ||||
|       onScaleStart: game is ScaleDetector | ||||
| @ -147,9 +118,7 @@ class WidgetBuilder { | ||||
|  | ||||
|       child: Container( | ||||
|           color: game.backgroundColor(), | ||||
|           child: Directionality( | ||||
|               textDirection: TextDirection.ltr, | ||||
|               child: EmbeddedGameWidget(game))), | ||||
|           child: Directionality(textDirection: TextDirection.ltr, child: EmbeddedGameWidget(game))), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @ -185,8 +154,7 @@ class _OverlayGameWidgetState extends State<OverlayGameWidget> { | ||||
|   Widget build(BuildContext context) { | ||||
|     return Directionality( | ||||
|         textDirection: TextDirection.ltr, | ||||
|         child: | ||||
|             Stack(children: [widget.gameChild, ..._overlays.values.toList()])); | ||||
|         child: Stack(children: [widget.gameChild, ..._overlays.values.toList()])); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -197,7 +165,6 @@ class OverlayWidgetBuilder extends WidgetBuilder { | ||||
|   Widget build(Game game) { | ||||
|     final container = super.build(game); | ||||
|  | ||||
|     return OverlayGameWidget( | ||||
|         gameChild: container, game: game, key: UniqueKey()); | ||||
|     return OverlayGameWidget(gameChild: container, game: game, key: UniqueKey()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -6,9 +6,10 @@ import 'package:flutter/services.dart'; | ||||
| import 'package:flutter/widgets.dart' as widgets; | ||||
|  | ||||
| import 'animation.dart'; | ||||
| import 'game/embedded_game_widget.dart'; | ||||
| import 'game/simple_game.dart'; | ||||
| import 'sprite.dart'; | ||||
| import 'components/animation_component.dart'; | ||||
| import 'game.dart'; | ||||
| import 'position.dart'; | ||||
|  | ||||
| /// Some utilities that did not fit anywhere else. | ||||
|  | ||||
| @ -4,10 +4,10 @@ import 'package:flame/components/composed_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/base_game.dart'; | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:test/test.dart'; | ||||
|  | ||||
| import 'package:flame/game.dart'; | ||||
| import 'package:flame/components/component.dart'; | ||||
|  | ||||
| class MyGame extends BaseGame {} | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:flame/components/mixins/has_game_ref.dart'; | ||||
| import 'package:flame/game/base_game.dart'; | ||||
| import 'package:test/test.dart'; | ||||
|  | ||||
| import 'package:flame/game.dart'; | ||||
| import 'package:flame/components/component.dart'; | ||||
|  | ||||
| class MyGame extends BaseGame { | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:flame/game/base_game.dart'; | ||||
| import 'package:test/test.dart'; | ||||
|  | ||||
| import 'package:flame/game.dart'; | ||||
| import 'package:flame/components/component.dart'; | ||||
| import 'package:flame/components/mixins/resizable.dart'; | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Erlend Fagerheim
					Erlend Fagerheim