LCOV - code coverage report
Current view: top level - lib/src/game - base_game.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 82 92 89.1 %
Date: 2021-08-10 15:50:53 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:ui';
       2             : 
       3             : import 'package:meta/meta.dart';
       4             : import 'package:ordered_set/queryable_ordered_set.dart';
       5             : 
       6             : import '../../components.dart';
       7             : import '../../extensions.dart';
       8             : import '../components/component.dart';
       9             : import '../components/mixins/collidable.dart';
      10             : import '../components/mixins/draggable.dart';
      11             : import '../components/mixins/has_collidables.dart';
      12             : import '../components/mixins/has_game_ref.dart';
      13             : import '../components/mixins/hoverable.dart';
      14             : import '../components/mixins/tappable.dart';
      15             : import '../fps_counter.dart';
      16             : import 'camera.dart';
      17             : import 'game.dart';
      18             : import 'projector.dart';
      19             : import 'viewport.dart';
      20             : 
      21             : /// This is a more complete and opinionated implementation of Game.
      22             : ///
      23             : /// BaseGame should be extended to add your game logic.
      24             : /// [update], [render] and [onResize] methods have default implementations.
      25             : /// This is the recommended structure to use for most games.
      26             : /// It is based on the Component system.
      27             : class BaseGame extends Game with FPSCounter {
      28             :   /// The list of components to be updated and rendered by the base game.
      29          48 :   late final ComponentSet components = createComponentSet();
      30             : 
      31             :   /// The camera translates the coordinate space after the viewport is applied.
      32             :   final Camera camera = Camera();
      33             : 
      34             :   /// The viewport transforms the coordinate space depending on your chosen
      35             :   /// implementation.
      36             :   /// The default implementation no-ops, but you can use this to have a fixed
      37             :   /// screen ratio for example.
      38          48 :   Viewport get viewport => _viewport;
      39             : 
      40             :   Viewport _viewport = DefaultViewport();
      41           2 :   set viewport(Viewport value) {
      42           2 :     if (hasLayout) {
      43           0 :       final previousSize = canvasSize;
      44           0 :       _viewport = value;
      45           0 :       onResize(previousSize);
      46             :     } else {
      47           2 :       _viewport = value;
      48             :     }
      49           8 :     _combinedProjector = Projector.compose([camera, value]);
      50             :   }
      51             : 
      52             :   late Projector _combinedProjector;
      53             : 
      54             :   final Vector2 _sizeBuffer = Vector2.zero();
      55             : 
      56             :   /// This is overwritten to consider the viewport transformation.
      57             :   ///
      58             :   /// Which means that this is the logical size of the game screen area as
      59             :   /// exposed to the canvas after viewport transformations and camera zooming.
      60             :   ///
      61             :   /// This does not match the Flutter widget size; for that see [canvasSize].
      62          24 :   @override
      63             :   Vector2 get size {
      64          24 :     assertHasLayout();
      65          24 :     return _sizeBuffer
      66          72 :       ..setFrom(viewport.effectiveSize)
      67          96 :       ..scale(1 / camera.zoom);
      68             :   }
      69             : 
      70             :   /// This is the original Flutter widget size, without any transformation.
      71           1 :   Vector2 get canvasSize {
      72           1 :     assertHasLayout();
      73           2 :     return viewport.canvasSize;
      74             :   }
      75             : 
      76          24 :   BaseGame() {
      77          48 :     camera.gameRef = this;
      78         120 :     _combinedProjector = Projector.compose([camera, viewport]);
      79             :   }
      80             : 
      81             :   /// This method setps up the OrderedSet instance used by this game, before
      82             :   /// any lifecycle methods happen.
      83             :   ///
      84             :   /// You can return a specific sub-class of OrderedSet, like
      85             :   /// [QueryableOrderedSet] for example, that we use for Collidables.
      86          24 :   ComponentSet createComponentSet() {
      87          24 :     final components = ComponentSet.createDefault(
      88          46 :       (c, {BaseGame? gameRef}) => prepare(c),
      89             :     );
      90          24 :     if (this is HasCollidables) {
      91           2 :       components.register<Collidable>();
      92             :     }
      93             :     return components;
      94             :   }
      95             : 
      96             :   /// This method is called for every component added.
      97             :   /// It does preparation on a component before any update or render method is called on it.
      98             :   ///
      99             :   /// You can use this to setup your mixins, pre-calculate stuff on every component, or anything you desire.
     100             :   /// By default, this calls the first time resize for every component, so don't forget to call super.preAdd when overriding.
     101          23 :   @mustCallSuper
     102             :   void prepare(Component c) {
     103             :     assert(
     104          23 :       hasLayout,
     105             :       '"prepare/add" called before the game is ready. Did you try to access it on the Game constructor? Use the "onLoad" method instead.',
     106             :     );
     107             : 
     108          23 :     if (c is Collidable) {
     109             :       assert(
     110           2 :         this is HasCollidables,
     111             :         'You can only use the Hitbox/Collidable feature with games that has the HasCollidables mixin',
     112             :       );
     113             :     }
     114          23 :     if (c is Tappable) {
     115             :       assert(
     116           4 :         this is HasTappableComponents,
     117             :         'Tappable Components can only be added to a BaseGame with HasTappableComponents',
     118             :       );
     119             :     }
     120          23 :     if (c is Draggable) {
     121             :       assert(
     122           3 :         this is HasDraggableComponents,
     123             :         'Draggable Components can only be added to a BaseGame with HasDraggableComponents',
     124             :       );
     125             :     }
     126          23 :     if (c is Hoverable) {
     127             :       assert(
     128           2 :         this is HasHoverableComponents,
     129             :         'Hoverable Components can only be added to a BaseGame with HasHoverableComponents',
     130             :       );
     131             :     }
     132             : 
     133          23 :     if (debugMode && c is BaseComponent) {
     134           0 :       c.debugMode = true;
     135             :     }
     136             : 
     137          23 :     if (c is HasGameRef) {
     138           5 :       c.gameRef = this;
     139             :     }
     140             : 
     141             :     // first time resize
     142          46 :     c.onGameResize(size);
     143             :   }
     144             : 
     145             :   /// Prepares and registers a component to be added on the next game tick
     146             :   ///
     147             :   /// This methods is an async operation since it await the `onLoad` method of
     148             :   /// the component. Nevertheless, this method only need to be waited to finish
     149             :   /// if by some reason, your logic needs to be sure that the component has
     150             :   /// finished loading, otherwise, this method can be called without waiting
     151             :   /// for it to finish as the BaseGame already handle the loading of the
     152             :   /// component.
     153             :   ///
     154             :   /// *Note:* Do not add components on the game constructor. This method can
     155             :   /// only be called after the game already has its layout set, this can be
     156             :   /// verified by the [hasLayout] property, to add components upon a game
     157             :   /// initialization, the [onLoad] method can be used instead.
     158          21 :   Future<void> add(Component c) {
     159          42 :     return components.addChild(c);
     160             :   }
     161             : 
     162             :   /// Adds a list of components, calling addChild for each one.
     163             :   ///
     164             :   /// The returned Future completes once all are loaded and added.
     165             :   /// Component loading is done in parallel.
     166           4 :   Future<void> addAll(Iterable<Component> cs) {
     167           8 :     return components.addChildren(cs);
     168             :   }
     169             : 
     170             :   /// Removes a component from the component list, calling onRemove for it and
     171             :   /// its children.
     172           0 :   void remove(Component c) {
     173           0 :     components.remove(c);
     174             :   }
     175             : 
     176             :   /// Removes all the components in the list and calls onRemove for all of them
     177             :   /// and their children.
     178           0 :   void removeAll(Iterable<Component> cs) {
     179           0 :     components.removeAll(cs);
     180             :   }
     181             : 
     182             :   /// This implementation of render basically calls [renderComponent] for every component, making sure the canvas is reset for each one.
     183             :   ///
     184             :   /// You can override it further to add more custom behavior.
     185             :   /// 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.
     186           3 :   @override
     187             :   @mustCallSuper
     188             :   void render(Canvas canvas) {
     189           9 :     viewport.render(canvas, (c) {
     190          12 :       components.forEach((comp) => renderComponent(c, comp));
     191             :     });
     192             :   }
     193             : 
     194             :   /// This renders a single component obeying BaseGame rules.
     195             :   ///
     196             :   /// It translates the camera unless hud, call the render method and restore the canvas.
     197             :   /// This makes sure the canvas is not messed up by one component and all components render independently.
     198           3 :   void renderComponent(Canvas canvas, Component c) {
     199           3 :     canvas.save();
     200           3 :     if (!c.isHud) {
     201           6 :       camera.apply(canvas);
     202             :     }
     203           3 :     c.renderTree(canvas);
     204           3 :     canvas.restore();
     205             :   }
     206             : 
     207             :   /// This implementation of update updates every component in the list.
     208             :   ///
     209             :   /// It also actually adds the components added via [add] since the previous tick,
     210             :   /// and remove those that are marked for destruction via the [Component.shouldRemove] method.
     211             :   /// You can override it further to add more custom behavior.
     212          21 :   @override
     213             :   @mustCallSuper
     214             :   void update(double dt) {
     215          42 :     components.updateComponentList();
     216             : 
     217          21 :     if (this is HasCollidables) {
     218           2 :       (this as HasCollidables).handleCollidables();
     219             :     }
     220             : 
     221          82 :     components.forEach((c) => c.update(dt));
     222          42 :     camera.update(dt);
     223             :   }
     224             : 
     225             :   /// This implementation of resize passes the resize call along to every
     226             :   /// component in the list, enabling each one to make their decisions as how to handle the resize.
     227             :   ///
     228             :   /// It also updates the [size] field of the class to be used by later added components and other methods.
     229             :   /// You can override it further to add more custom behavior, but you should seriously consider calling the super implementation as well.
     230             :   /// This implementation also uses the current [viewport] in order to transform the coordinate system appropriately.
     231          24 :   @override
     232             :   @mustCallSuper
     233             :   void onResize(Vector2 canvasSize) {
     234          24 :     super.onResize(canvasSize);
     235          72 :     viewport.resize(canvasSize.clone());
     236          51 :     components.forEach((c) => c.onGameResize(size));
     237             :   }
     238             : 
     239             :   /// Returns whether this [Game] is in debug mode or not.
     240             :   ///
     241             :   /// Returns `false` by default. Override it, or set it to true, to use debug mode.
     242             :   /// You can use this value to enable debug behaviors for your game and many components will
     243             :   /// show extra information on the screen when debug mode is activated
     244             :   bool debugMode = false;
     245             : 
     246             :   /// Changes the priority of [component] and reorders the games component list.
     247             :   ///
     248             :   /// Returns true if changing the component's priority modified one of the
     249             :   /// components that existed directly on the game and false if it
     250             :   /// either was a child of another component, if it didn't exist at all or if
     251             :   /// it was a component added directly on the game but its priority didn't
     252             :   /// change.
     253           1 :   bool changePriority(
     254             :     Component component,
     255             :     int priority, {
     256             :     bool reorderRoot = true,
     257             :   }) {
     258           2 :     if (component.priority == priority) {
     259             :       return false;
     260             :     }
     261           1 :     component.changePriorityWithoutResorting(priority);
     262             :     if (reorderRoot) {
     263           3 :       if (component.parent != null && component.parent is BaseComponent) {
     264           2 :         (component.parent! as BaseComponent).reorderChildren();
     265           2 :       } else if (components.contains(component)) {
     266           2 :         components.rebalanceAll();
     267             :       }
     268             :     }
     269             :     return true;
     270             :   }
     271             : 
     272             :   /// Since changing priorities is quite an expensive operation you should use
     273             :   /// this method if you want to change multiple priorities at once so that the
     274             :   /// tree doesn't have to be reordered multiple times.
     275           1 :   void changePriorities(Map<Component, int> priorities) {
     276             :     var hasRootComponents = false;
     277             :     final parents = <BaseComponent>{};
     278           2 :     priorities.forEach((component, priority) {
     279           1 :       final wasUpdated = changePriority(
     280             :         component,
     281             :         priority,
     282             :         reorderRoot: false,
     283             :       );
     284             :       if (wasUpdated) {
     285           3 :         if (component.parent != null && component.parent is BaseComponent) {
     286           2 :           parents.add(component.parent! as BaseComponent);
     287             :         } else {
     288           3 :           hasRootComponents |= components.contains(component);
     289             :         }
     290             :       }
     291             :     });
     292             :     if (hasRootComponents) {
     293           2 :       components.rebalanceAll();
     294             :     }
     295           3 :     parents.forEach((parent) => parent.reorderChildren());
     296             :   }
     297             : 
     298             :   /// Returns the current time in seconds with microseconds precision.
     299             :   ///
     300             :   /// This is compatible with the `dt` value used in the [update] method.
     301           0 :   double currentTime() {
     302           0 :     return DateTime.now().microsecondsSinceEpoch.toDouble() /
     303             :         Duration.microsecondsPerSecond;
     304             :   }
     305             : 
     306           1 :   @override
     307             :   Vector2 projectVector(Vector2 vector) {
     308           2 :     return _combinedProjector.projectVector(vector);
     309             :   }
     310             : 
     311           5 :   @override
     312             :   Vector2 unprojectVector(Vector2 vector) {
     313          10 :     return _combinedProjector.unprojectVector(vector);
     314             :   }
     315             : 
     316           1 :   @override
     317             :   Vector2 scaleVector(Vector2 vector) {
     318           2 :     return _combinedProjector.scaleVector(vector);
     319             :   }
     320             : 
     321           1 :   @override
     322             :   Vector2 unscaleVector(Vector2 vector) {
     323           2 :     return _combinedProjector.unscaleVector(vector);
     324             :   }
     325             : }

Generated by: LCOV version 1.15