Line data Source code
1 : import 'dart:async'; 2 : import 'dart:ui'; 3 : 4 : import 'package:flutter/rendering.dart'; 5 : import 'package:flutter/scheduler.dart'; 6 : import 'package:flutter/services.dart'; 7 : import 'package:flutter/widgets.dart'; 8 : 9 : import '../assets/assets_cache.dart'; 10 : import '../assets/images.dart'; 11 : import '../extensions/offset.dart'; 12 : import '../extensions/vector2.dart'; 13 : import '../sprite.dart'; 14 : import '../sprite_animation.dart'; 15 : import 'game_render_box.dart'; 16 : import 'mixins/keyboard.dart'; 17 : import 'projector.dart'; 18 : 19 : /// Represents a generic game. 20 : /// 21 : /// Subclass this to implement the [update] and [render] methods. 22 : /// Flame will deal with calling these methods properly when the game's widget is rendered. 23 : abstract class Game extends Projector { 24 : final images = Images(); 25 : final assets = AssetsCache(); 26 : 27 : /// Just a reference back to the render box that is kept up to date by the engine. 28 : GameRenderBox? _gameRenderBox; 29 : 30 : /// Currently attached build context. Can be null if not attached. 31 10 : BuildContext? get buildContext => _gameRenderBox?.buildContext; 32 : 33 : /// Whether the game widget was attached to the Flutter tree. 34 10 : bool get isAttached => buildContext != null; 35 : 36 : /// Current size of the game as provided by the framework; it will be null if layout has not been computed yet. 37 : /// 38 : /// Use [size] and [hasLayout] for safe access. 39 : Vector2? _size; 40 : 41 : /// Current game viewport size, updated every resize via the [onResize] method hook 42 0 : Vector2 get size { 43 0 : assertHasLayout(); 44 0 : return _size!; 45 : } 46 : 47 : /// Indicates if the this game instance had its layout layed into the GameWidget 48 : /// Only this is true, the game is ready to have its size used or in the case 49 : /// of a BaseGame, to receive components. 50 48 : bool get hasLayout => _size != null; 51 : 52 : /// Returns the game background color. 53 : /// By default it will return a black color. 54 : /// It cannot be changed at runtime, because the game widget does not get rebuild when this value changes. 55 8 : Color backgroundColor() => const Color(0xFF000000); 56 : 57 : /// Implement this method to update the game state, given the time [dt] that has passed since the last update. 58 : /// 59 : /// Keep the updates as short as possible. [dt] is in seconds, with microseconds precision. 60 : void update(double dt); 61 : 62 : /// Implement this method to render the current game state in the [canvas]. 63 : void render(Canvas canvas); 64 : 65 : /// This is the resize hook; every time the game widget is resized, this hook is called. 66 : /// 67 : /// The default implementation just sets the new size on the size field 68 25 : @mustCallSuper 69 : void onResize(Vector2 size) { 70 100 : _size = (_size ?? Vector2.zero())..setFrom(size); 71 : } 72 : 73 24 : @protected 74 : void assertHasLayout() { 75 : assert( 76 24 : hasLayout, 77 : '"size" is not ready yet. Did you try to access it on the Game constructor? Use the "onLoad" method instead.', 78 : ); 79 : } 80 : 81 : /// This is the lifecycle state change hook; every time the game is resumed, paused or suspended, this is called. 82 : /// 83 : /// The default implementation does nothing; override to use the hook. 84 : /// Check [AppLifecycleState] for details about the events received. 85 0 : void lifecycleStateChange(AppLifecycleState state) {} 86 : 87 : /// Use for calculating the FPS. 88 0 : void onTimingsCallback(List<FrameTiming> timings) {} 89 : 90 0 : void _handleKeyEvent(RawKeyEvent e) { 91 0 : (this as KeyboardEvents).onKeyEvent(e); 92 : } 93 : 94 : /// Marks game as not attached tto any widget tree. 95 : /// 96 : /// Should be called manually. 97 2 : void attach(PipelineOwner owner, GameRenderBox gameRenderBox) { 98 2 : if (isAttached) { 99 0 : throw UnsupportedError(''' 100 : Game attachment error: 101 : A game instance can only be attached to one widget at a time. 102 : '''); 103 : } 104 2 : _gameRenderBox = gameRenderBox; 105 2 : onAttach(); 106 : } 107 : 108 : // Called when the Game widget is attached 109 2 : @mustCallSuper 110 : void onAttach() { 111 2 : if (this is KeyboardEvents) { 112 0 : RawKeyboard.instance.addListener(_handleKeyEvent); 113 : } 114 : } 115 : 116 : /// Marks game as not attached tto any widget tree. 117 : /// 118 : /// Should not be called manually. 119 2 : void detach() { 120 2 : _gameRenderBox = null; 121 2 : _size = null; 122 2 : onDetach(); 123 : } 124 : 125 : // Called when the Game widget is detached 126 2 : @mustCallSuper 127 : void onDetach() { 128 : // Keeping this here, because if we leave this on HasWidgetsOverlay 129 : // and somebody overrides this and forgets to call the stream close 130 : // we can face some leaks. 131 2 : if (this is KeyboardEvents) { 132 0 : RawKeyboard.instance.removeListener(_handleKeyEvent); 133 : } 134 4 : images.clearCache(); 135 : } 136 : 137 : /// Converts a global coordinate (i.e. w.r.t. the app itself) to a local 138 : /// coordinate (i.e. w.r.t. he game widget). 139 : /// If the widget occupies the whole app ("full screen" games), or is not 140 : /// attached to Flutter, this operation is the identity. 141 4 : Vector2 convertGlobalToLocalCoordinate(Vector2 point) { 142 4 : if (!isAttached) { 143 4 : return point.clone(); 144 : } 145 0 : return _gameRenderBox!.globalToLocal(point.toOffset()).toVector2(); 146 : } 147 : 148 : /// Converts a local coordinate (i.e. w.r.t. the game widget) to a global 149 : /// coordinate (i.e. w.r.t. the app itself). 150 : /// If the widget occupies the whole app ("full screen" games), or is not 151 : /// attached to Flutter, this operation is the identity. 152 0 : Vector2 convertLocalToGlobalCoordinate(Vector2 point) { 153 0 : if (!isAttached) { 154 0 : return point.clone(); 155 : } 156 0 : return _gameRenderBox!.localToGlobal(point.toOffset()).toVector2(); 157 : } 158 : 159 0 : @override 160 : Vector2 unprojectVector(Vector2 vector) => vector; 161 : 162 0 : @override 163 : Vector2 projectVector(Vector2 vector) => vector; 164 : 165 0 : @override 166 : Vector2 unscaleVector(Vector2 vector) => vector; 167 : 168 0 : @override 169 : Vector2 scaleVector(Vector2 vector) => vector; 170 : 171 : /// Utility method to load and cache the image for a sprite based on its options 172 0 : Future<Sprite> loadSprite( 173 : String path, { 174 : Vector2? srcSize, 175 : Vector2? srcPosition, 176 : }) { 177 0 : return Sprite.load( 178 : path, 179 : srcPosition: srcPosition, 180 : srcSize: srcSize, 181 0 : images: images, 182 : ); 183 : } 184 : 185 : /// Utility method to load and cache the image for a sprite animation based on its options 186 0 : Future<SpriteAnimation> loadSpriteAnimation( 187 : String path, 188 : SpriteAnimationData data, 189 : ) { 190 0 : return SpriteAnimation.load( 191 : path, 192 : data, 193 0 : images: images, 194 : ); 195 : } 196 : 197 : /// Flag to tell the game loop if it should start running upon creation 198 : bool runOnCreation = true; 199 : 200 : /// Pauses the engine game loop execution 201 0 : void pauseEngine() => pauseEngineFn?.call(); 202 : 203 : /// Resumes the engine game loop execution 204 0 : void resumeEngine() => resumeEngineFn?.call(); 205 : 206 : VoidCallback? pauseEngineFn; 207 : VoidCallback? resumeEngineFn; 208 : 209 : /// Use this method to load the assets need for the game instance to run 210 8 : Future<void> onLoad() async {} 211 : 212 : /// A property that stores an [ActiveOverlaysNotifier] 213 : /// 214 : /// This is useful to render widgets above a game, like a pause menu for example. 215 : /// Overlays visible or hidden via [overlays].add or [overlays].remove, respectively. 216 : /// 217 : /// Ex: 218 : /// ``` 219 : /// final pauseOverlayIdentifier = 'PauseMenu'; 220 : /// overlays.add(pauseOverlayIdentifier); // marks 'PauseMenu' to be rendered. 221 : /// overlays.remove(pauseOverlayIdentifier); // marks 'PauseMenu' to not be rendered. 222 : /// ``` 223 : /// 224 : /// See also: 225 : /// - GameWidget 226 : /// - [Game.overlays] 227 : final overlays = ActiveOverlaysNotifier(); 228 : } 229 : 230 : /// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance. 231 : /// 232 : /// To learn more, see: 233 : /// - [Game.overlays] 234 : class ActiveOverlaysNotifier extends ChangeNotifier { 235 : final Set<String> _activeOverlays = {}; 236 : 237 : /// Mark a, overlay to be rendered. 238 : /// 239 : /// See also: 240 : /// - GameWidget 241 : /// - [Game.overlays] 242 0 : bool add(String overlayName) { 243 0 : final setChanged = _activeOverlays.add(overlayName); 244 : if (setChanged) { 245 0 : notifyListeners(); 246 : } 247 : return setChanged; 248 : } 249 : 250 : /// Mark a, overlay to not be rendered. 251 : /// 252 : /// See also: 253 : /// - GameWidget 254 : /// - [Game.overlays] 255 0 : bool remove(String overlayName) { 256 0 : final hasRemoved = _activeOverlays.remove(overlayName); 257 : if (hasRemoved) { 258 0 : notifyListeners(); 259 : } 260 : return hasRemoved; 261 : } 262 : 263 : /// A [Set] of the active overlay names. 264 16 : Set<String> get value => _activeOverlays; 265 : 266 : /// Returns if the given [overlayName] is active 267 0 : bool isActive(String overlayName) => _activeOverlays.contains(overlayName); 268 : }