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 : }
|