-
-
Notifications
You must be signed in to change notification settings - Fork 872
/
component.dart
1006 lines (912 loc) · 38 KB
/
component.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flame/components.dart';
import 'package:flame/src/cache/value_cache.dart';
import 'package:flame/src/components/core/component_tree_root.dart';
import 'package:flame/src/components/mixins/coordinate_transform.dart';
import 'package:flame/src/effects/provider_interfaces.dart';
import 'package:flame/src/game/flame_game.dart';
import 'package:flame/src/game/game.dart';
import 'package:flame/src/gestures/events.dart';
import 'package:flutter/painting.dart';
import 'package:meta/meta.dart';
/// [Component]s are the basic building blocks for a [FlameGame].
///
/// Components are quite similar to widgets in Flutter, or to GameObjects in
/// Unity. Any entity within the game can be represented as a Component,
/// especially if that entity has some visual appearance, or if it changes over
/// time. For example, the player, the enemies, flying bullets, clouds in the
/// sky, buildings, rocks, etc -- all can be represented as components. Some
/// components may also represent more abstract entities: effects, behaviors,
/// data stores, groups.
///
/// Components are designed to be organized into a component tree, where every
/// component belongs to a single parent and owns any number of children
/// components. A [Component] must be added to the component tree in order for
/// it to become fully operational. Typically, the [FlameGame] is the root of
/// the component tree.
///
/// The components are added into the component tree using the [add] method, or
/// [addToParent]; and then later can be removed using [remove] or
/// [removeFromParent]. Note that neither adding nor removing are immediate,
/// typically these operations complete by the next game tick.
///
/// Each component goes through several lifecycle stages during its lifetime,
/// at each stage invoking certain user-defined "lifecycle methods":
/// - [onLoad] when the component is first added into the tree;
/// - [onGameResize] + [onMount] when done loading, or when the component is
/// re-added to the component tree after having been removed;
/// - [onRemove] if the component is later removed from the component tree.
///
/// The [onLoad] is only invoked once during the lifetime of the component,
/// which means you can treat it as "async constructor". When [onLoad] is
/// invoked, we guarantee that the game instance can be found via [findGame],
/// and that this game instance already has layout (i.e. knows the size of the
/// canvas).
///
/// The [onMount] is invoked when the component is done loading, and when its
/// parent is properly mounted. If a component is removed from the tree and
/// later added to another component, the [onMount] will be called again. For
/// every call to [onMount] there will be a corresponding call to [onRemove].
///
/// While the component is mounted, the following user-overridable methods are
/// invoked:
/// - [update] on every game tick;
/// - [render] after all components are done updating;
/// - [updateTree] and [renderTree] are more advanced versions of the update
/// and render callbacks, but they rarely need to be overridden by the user;
/// - [onGameResize] every time the size game's Flutter widget changes.
///
/// The [update] and [render] are two most important methods of the component's
/// lifecycle. On every game tick, first the entire component tree will be
/// [update]d, and then all the components will be [render]ed.
///
/// You may also need to override [containsLocalPoint] if the component needs to
/// respond to tap events or similar; the [componentsAtPoint] may also need to
/// be overridden if you have reimplemented [renderTree].
class Component {
Component({
Iterable<Component>? children,
int? priority,
ComponentKey? key,
}) : _priority = priority ?? 0,
_key = key {
if (children != null) {
addAll(children);
}
}
//#region Lifecycle state
/// Bitfield which keeps track of the current state of the component: which
/// lifecycle events it has already executed, and which are currently being
/// executed.
///
/// [_initial]: the original state of the component as it was just created. In
/// this state no events has occurred so far.
///
/// [_loading]: this flag is set while the component is running its [onLoad]
/// method, and can be checked via [isLoading] getter. This bit is turned
/// on when the component starts loading, and then off when it has
/// finished loading.
///
/// [_loaded]: this flag is set after the component finishes running its
/// [onLoad] method, and can be checked via the [isLoaded] getter. Once
/// set, this bit is never turned off.
///
/// [_mounted]: this flag is set when the component becomes properly mounted
/// to the component tree, and then turned off when the component is
/// removed from the tree.
///
/// [_removing]: this bit indicates that the component is scheduled for
/// removal at the earliest possible opportunity, and then cleared when
/// the component is actually removed.
///
/// The lifecycle process of a component is quite complicated. This happens
/// for several reasons: partly because it consists of several stages, between
/// which there are asynchronous or even physical execution gaps. In addition,
/// the lifecycle invokes a number of user-provided callbacks, and those
/// callbacks may attempt to modify the component.
///
/// This is how a typical component's lifecycle progresses:
/// - First, the component is created with the [_state] variable = 0. At this
/// point the only operations that can be done are: to [add] it to another
/// component, or to add other components to it.
/// - When the component is [add]ed to another component (the parent), we do
/// the following:
/// - set the [_parent] variable;
/// - add the component to the parent's queue of pending children;
/// - if the component has been [_loaded] before, then do nothing else and
/// wait until its parent will do the mounting.
/// - otherwise, if the [Game] instance is accessible via [findGame], then
/// we start loading the component;
/// - otherwise we will start loading when the parent becomes mounted. This
/// means we're entering into an execution gap here. During this gap the
/// component is still in the [_initial] state, and it can be [remove]d
/// by the user. When the user removes the component in this state, we
/// simply set the [_parent] to null and remove it from the parent's
/// queue of pending children.
/// - When we [_startLoading] the component, we set the [_loading] bit,
/// and then [onLoad] immediately
/// afterwards. The onLoad will be either sync or async, in both cases we
/// arrange to turn on the [_loaded] bit at the end of [onLoad]'s run.
/// - At this point we're in an execution gap: either the async [onLoad] is
/// waiting to be run, or it already completed, or it was sync to begin
/// with -- in either case we're waiting until the component can be
/// mounted, and in that time the [_loading] bit is still on.
/// During this time the user may request to [remove] the component. If at
/// that moment the component is already loaded, then we remove it by
/// setting parent to null and deleting it from the parent's pending
/// children queue. If, on the other hand, the component is not loaded yet,
/// then we turn on the [_removing] flag only -- this is because we don't
/// want to set [_parent] to null while the [onLoad] may still try to
/// access it.
/// - The next step in the component's lifecycle comes when its parent
/// processes own pending events queue, which only happens after the parent
/// gets mounted. For each component in its queue of pending children, the
/// following checks are performed:
/// - if the component is already [_loaded], then it will now be
/// [_mount]ed;
/// - otherwise, if the component is not even [_loading], then it will
/// now [_startLoading];
/// - otherwise do nothing: need to wait until the component finishes
/// loading.
/// - During [_mount]ing, we perform the following sequence:
/// - first we run [onGameResize];
/// - check if the component was scheduled for removal while waiting in
/// the queue -- if so, remove it immediately without mounting;
/// - clear the [_loading] flag and start the [onMount] callback;
/// - set the [_mounted] bit;
/// - add the component to parent's list of [children];
/// - if the component has its own list of existing children, then mount
/// those;
/// - if the component has a list of pending children, then process the
/// lifecycle events queue, which would attempt to load and/or mount
/// these pending children.
///
/// At this point the component would be at its normal, mounted state. When
/// [remove] is invoked in this state, we (1) turn on the [_removing] bit, and
/// (2) add the component to the "removals" lifecycle queue of its parent. The
/// next time the parent processes its lifecycle event queue, it would take
/// all the components from the "removals", and for each one call the
/// [onRemove] method, clear the [_mounted] and [_removing] flags, and lastly
/// remove the component from the official list of children.
///
/// After a component was removed, it will be [_loaded], but not [_mounted],
/// and its [_parent] will be null. Such component can be re-added into the
/// component tree if needed.
int _state = _initial;
static const int _initial = 0;
static const int _loading = 1;
static const int _loaded = 2;
static const int _mounting = 32;
static const int _mounted = 4;
static const int _removing = 8;
static const int _removed = 16;
/// Whether the component is currently executing its [onLoad] step.
bool get isLoading => (_state & _loading) != 0;
void _setLoadingBit() => _state |= _loading;
void _clearLoadingBit() => _state &= ~_loading;
/// Whether this component has completed its [onLoad] step.
bool get isLoaded => (_state & _loaded) != 0;
void _setLoadedBit() => _state |= _loaded;
@internal
bool get isMounting => (_state & _mounting) != 0;
void _setMountingBit() => _state |= _mounting;
void _clearMountingBit() => _state &= ~_mounting;
/// Whether this component is currently added to a component tree.
bool get isMounted => (_state & _mounted) != 0;
void _setMountedBit() => _state |= _mounted;
void _clearMountedBit() => _state &= ~_mounted;
/// Whether the component is scheduled to be removed.
bool get isRemoving => (_state & _removing) != 0;
void _setRemovingBit() => _state |= _removing;
void _clearRemovingBit() => _state &= ~_removing;
/// Whether the component has been removed. Originally this flag is `false`,
/// but it becomes `true` after the component was mounted and then removed
/// from its parent. The flag becomes `false` again when the component is
/// mounted to a new parent.
bool get isRemoved => (_state & _removed) != 0;
void _setRemovedBit() => _state |= _removed;
void _clearRemovedBit() => _state &= ~_removed;
Completer<void>? _loadCompleter;
Completer<void>? _mountCompleter;
Completer<void>? _removeCompleter;
/// A future that completes when this component finishes loading.
///
/// If the component is already loaded (see [isLoaded]), this returns an
/// already completed future.
Future<void> get loaded {
return isLoaded
? Future.value()
: (_loadCompleter ??= Completer<void>()).future;
}
/// A future that will complete once the component is mounted on its parent.
///
/// If the component is already mounted (see [isMounted]), this returns an
/// already completed future.
Future<void> get mounted {
return isMounted
? Future.value()
: (_mountCompleter ??= Completer<void>()).future;
}
/// A future that completes when this component is removed from its parent.
///
/// If the component is already removed (see [isRemoved]), this returns an
/// already completed future.
Future<void> get removed {
return isRemoved
? Future.value()
: (_removeCompleter ??= Completer<void>()).future;
}
//#endregion
//#region Component tree
/// Who owns this component in the component tree.
///
/// This can be null if the component hasn't been added to the component tree
/// yet, or if it is the root of component tree.
///
/// Setting this property to null is equivalent to [removeFromParent].
Component? get parent => _parent;
Component? _parent;
set parent(Component? newParent) {
if (newParent == _parent) {
return;
} else if (newParent == null) {
removeFromParent();
} else if (_parent == null) {
addToParent(newParent);
} else {
final root = findGame()! as ComponentTreeRoot;
root.enqueueMove(this, newParent);
}
}
/// The children components of this component.
///
/// This getter will automatically create the [ComponentSet] container within
/// the current object if it didn't exist before. Check the [hasChildren]
/// property in order to avoid instantiating the children container.
ComponentSet get children => _children ??= createComponentSet();
bool get hasChildren => _children?.isNotEmpty ?? false;
ComponentSet? _children;
/// `Component.childrenFactory` is the default method for creating children
/// containers within all components. Replace this method if you want to have
/// customized (non-default) [ComponentSet] instances in your project.
static ComponentSetFactory childrenFactory = ComponentSet.new;
/// This method creates the children container for the current component.
/// Override this method if you need to have a custom [ComponentSet] within
/// a particular class.
ComponentSet createComponentSet() => childrenFactory();
/// Returns the closest parent further up the hierarchy that satisfies type=T,
/// or null if no such parent can be found.
///
/// If [includeSelf] is set to true (default is false) then the component
/// which the call is made for is also included in the search.
T? findParent<T extends Component>({bool includeSelf = false}) {
return ancestors(includeSelf: includeSelf).whereType<T>().firstOrNull;
}
/// Returns the first child that matches the given type [T], or null if there
/// are no such children.
T? firstChild<T extends Component>() {
return children.whereType<T>().firstOrNull;
}
/// Returns the last child that matches the given type [T], or null if there
/// are no such children.
T? lastChild<T extends Component>() {
return children.reversed().whereType<T>().firstOrNull;
}
/// An iterator producing this component's parent, then its parent's parent,
/// then the great-grand-parent, and so on, until it reaches a component
/// without a parent.
Iterable<Component> ancestors({bool includeSelf = false}) sync* {
var current = includeSelf ? this : parent;
while (current != null) {
yield current;
current = current.parent;
}
}
/// Recursively enumerates all nested [children].
///
/// The search is depth-first in preorder. In other words, it explores the
/// first child completely before visiting the next sibling, and the root
/// component is visited before its children.
///
/// This ordering of descendants is considered standard in Flame: it is the
/// same order in which the components will normally be updated and rendered
/// on every game cycle. The optional parameter [reversed] allows iterating
/// through the same set of descendants in reverse order.
///
/// The [Iterable] produced by this method is "lazy", which means it will only
/// traverse the component tree when required. This allows efficient chaining
/// of various iterable methods, such as filtering, early stopping, folding,
/// and so on -- see the documentation of the [Iterable] class for details.
Iterable<Component> descendants({
bool includeSelf = false,
bool reversed = false,
}) sync* {
if (includeSelf && !reversed) {
yield this;
}
if (hasChildren) {
final childrenIterable = reversed ? children.reversed() : children;
for (final child in childrenIterable) {
yield* child.descendants(includeSelf: true, reversed: reversed);
}
}
if (includeSelf && reversed) {
yield this;
}
}
/// This method first calls the passed handler on the leaves in the tree,
/// the children without any children of their own.
/// Then it continues through all other children. The propagation continues
/// until the handler returns false, which means "do not continue", or when
/// the handler has been called with all children.
///
/// This method is important to be used by the engine to propagate actions
/// like rendering, taps, etc, but you can call it yourself if you need to
/// apply an action to the whole component chain.
/// It will only consider components of type T in the hierarchy,
/// so use T = Component to target everything.
bool propagateToChildren<T extends Component>(
bool Function(T) handler, {
bool includeSelf = false,
}) {
return descendants(reversed: true, includeSelf: includeSelf)
.whereType<T>()
.every(handler);
}
@internal
static Game? staticGameInstance;
Game? findGame() {
return staticGameInstance ??
((this is Game) ? (this as Game) : _parent?.findGame());
}
/// Whether the children list contains the given component.
///
/// This method uses reference equality.
bool contains(Component c) => _children?.contains(c) ?? false;
//#endregion
//#region Component lifecycle methods
/// Called whenever the size of the top-level Canvas changes.
///
/// In addition, this method will be invoked before each [onMount].
@mustCallSuper
void onGameResize(Vector2 size) => handleResize(size);
/// Late initialization method for [Component].
///
/// Usually, this method is the main place where you initialize your
/// component. This has several advantages over the traditional constructor:
/// - this method can be `async`;
/// - it is invoked when the size of the game canvas is already known.
///
/// If your loading logic requires knowing the size of the game canvas, then
/// add [HasGameRef] mixin and then query `game.size` or
/// `game.canvasSize`.
///
/// The default implementation returns `null`, indicating that there is no
/// need to await anything. When overriding this method, you have a choice
/// whether to create a regular or async function.
///
/// If you need an asynchronous [onLoad], make your override return
/// non-nullable `Future<void>`:
/// ```dart
/// @override
/// Future<void> onLoad() async {
/// // your code here
/// }
/// ```
///
/// Alternatively, if your [onLoad] function doesn't use any `await`ing, then
/// you can declare it as a regular method returning `void`:
/// ```dart
/// @override
/// void onLoad() {
/// // your code here
/// }
/// ```
///
/// The engine ensures that this method will be called exactly once during
/// the lifetime of the [Component] object. Do not call this method manually.
FutureOr<void> onLoad() => null;
/// Called when the component is added to its parent.
///
/// This method only runs when the component is fully loaded, i.e. after
/// [onLoad]. However, [onLoad] only ever runs once for the component, whereas
/// [onMount] runs every time the component is inserted into the game tree.
///
/// This method runs when the component is about to be added to its parent.
/// At this point the [parent] property already holds a reference to this
/// component's parent, however the parent doesn't have this component among
/// its [children] yet.
///
/// After this method completes, the component is added to the parent's
/// children set, and then the flag [isMounted] set to true.
///
/// Example:
/// ```dart
/// @override
/// void onMount() {
/// position = parent!.size / 2;
/// }
/// ```
///
/// See also:
/// - [onRemove] that is called every time the component is removed from the
/// game tree
void onMount() {}
/// Called right before the component is removed from the game.
///
/// This method will only run for a component that was previously mounted into
/// a component tree. If a component was never mounted (for example, when it
/// is removed before it had a chance to mount), then this callback will not
/// trigger. Thus, [onRemove] runs if and only if there was a corresponding
/// [onMount] call before.
void onRemove() {}
/// Called whenever the parent of this component changes size; and also once
/// before [onMount].
///
/// The component may change its own size or perform layout in response to
/// this call. If the component changes size, then it should call
/// [onParentResize] for all its children.
void onParentResize(Vector2 maxSize) {}
/// This method is called periodically by the game engine to request that your
/// component updates itself.
///
/// The time [dt] in seconds (with microseconds precision provided by Flutter)
/// since the last update cycle.
/// This time can vary according to hardware capacity, so make sure to update
/// your state considering this.
/// All components in the tree are always updated by the same amount. The time
/// each one takes to update adds up to the next update cycle.
void update(double dt) {}
/// This method traverses the component tree and calls [update] on all its
/// children according to their [priority] order, relative to the
/// priority of the direct siblings, not the children or the ancestors.
void updateTree(double dt) {
update(dt);
_children?.forEach((c) => c.updateTree(dt));
}
/// This method will be invoked from lifecycle if [child] has been added
/// to or removed from its parent children list.
void onChildrenChanged(Component child, ChildrenChangeType type) {}
void render(Canvas canvas) {}
void renderTree(Canvas canvas) {
render(canvas);
_children?.forEach((c) => c.renderTree(canvas));
// Any debug rendering should be rendered on top of everything
if (debugMode) {
renderDebugMode(canvas);
}
}
//#endregion
//#region Add/remove components
/// Schedules [component] to be added as a child to this component.
///
/// This method is robust towards being called from any place in the user
/// code: you can call it while iterating over the component tree, during
/// mounting or async loading, when the Game object is already loaded or not.
///
/// The cost of this flexibility is that the component won't be added right
/// away. Instead, it will be placed into a queue, and then added later, after
/// it has finished loading, but no sooner than on the next game tick.
///
/// When multiple children are scheduled to be added to the same parent, we
/// start loading all of them as soon as possible. Nevertheless, the children
/// will end up being added to the parent in exactly the same order as they
/// were originally scheduled by the user, regardless of how fast or slow
/// each of them loads.
///
/// A component can be added to a parent which may not be mounted to the game
/// tree yet. In such case, the component will start loading immediately, but
/// its mounting will be delayed until such time when the parent becomes
/// mounted.
///
/// This method returns a future that completes when the component is done
/// loading, and mounting if the parent is currently mounted. However, this
/// future will not guarantee that the component will become "fully mounted":
/// it still needs to be added to the parent's children list, and that
/// operation will only be done on the next game tick.
///
/// A component can only be added to one parent at a time. It is an error to
/// try to add it to multiple parents, or even to the same parent multiple
/// times. If you need to change the parent of a component, use the
/// [parent] setter.
FutureOr<void> add(Component component) => _addChild(component);
/// Adds this component as a child of [parent] (see [add] for details).
FutureOr<void> addToParent(Component parent) => parent._addChild(this);
/// A convenience method to [add] multiple children at once.
Future<void> addAll(Iterable<Component> components) {
final futures = <Future<void>>[];
for (final component in components) {
final future = add(component);
if (future is Future) {
futures.add(future);
}
}
return Future.wait(futures);
}
FutureOr<void> _addChild(Component child) {
assert(
child._parent == null,
'$child cannot be added to $this because it already has a parent: '
'${child._parent}',
);
child._parent = this;
final game = findGame();
if (isMounted && !child.isMounted) {
(game! as FlameGame).enqueueAdd(child, this);
} else {
// This will be reconciled during the mounting stage
children.add(child);
}
if (!child.isLoaded && !child.isLoading && (game?.hasLayout ?? false)) {
return child._startLoading();
}
}
/// Removes a component from the component tree.
///
/// This will call [onRemove] for the component and its children, but only if
/// there was an [onMount] call previously, i.e. when removing a component
/// that was properly mounted.
///
/// A component can be removed even before it finishes mounting, however such
/// component cannot be added back into the tree until it at least finishes
/// loading.
void remove(Component component) => _removeChild(component);
/// Remove the component from its parent in the next tick.
void removeFromParent() => _parent?._removeChild(this);
/// Removes all the children in the list and calls [onRemove] for all of them
/// and their children.
void removeAll(Iterable<Component> components) {
components.forEach(remove);
}
/// Removes all the children for which the [test] function returns true.
void removeWhere(bool Function(Component component) test) {
removeAll([...children.where(test)]);
}
void _removeChild(Component child) {
assert(
child._parent != null,
"Trying to remove a component that doesn't belong to any parent",
);
assert(
child._parent == this,
'Trying to remove a component that belongs to a different parent: this = '
"$this, component's parent = ${child._parent}",
);
if (isMounted) {
final root = findGame()! as ComponentTreeRoot;
if (child.isMounted || child.isMounting) {
if (!child.isRemoving) {
root.enqueueRemove(child, this);
child._setRemovingBit();
}
} else {
root.dequeueAdd(child, this);
child._parent = null;
}
} else {
_children?.remove(child);
child._parent = null;
}
}
/// Changes the current parent for another parent and prepares the tree under
/// the new root.
@Deprecated('Will be removed in 1.9.0. Use the parent setter instead.')
// ignore: use_setters_to_change_properties
void changeParent(Component newParent) {
parent = newParent;
}
//#endregion
//#region Hit Testing
/// Checks whether the [point] is within this component's bounds.
///
/// This method should be implemented for any component that has a visual
/// representation and non-zero size. The [point] is in the local coordinate
/// space.
bool containsLocalPoint(Vector2 point) => false;
/// Same as [containsLocalPoint], but for a "global" [point].
///
/// This will be deprecated in the future, due to the notion of "global" point
/// not being well-defined.
bool containsPoint(Vector2 point) => containsLocalPoint(point);
/// An iterable of descendant components intersecting the given point. The
/// [point] is in the local coordinate space.
///
/// More precisely, imagine a ray originating at a certain point (x, y) on
/// the screen, and extending perpendicularly to the screen's surface into
/// your game's world. The purpose of this method is to find all components
/// that intersect with this ray, in the order from those that are closest to
/// the user to those that are farthest.
///
/// The return value is an [Iterable] of components. If the [nestedPoints]
/// parameter is given, then it will also report the points of intersection
/// for each component in its local coordinate space. Specifically, the last
/// element in the list is the point in the coordinate space of the returned
/// component, the element before the last is in that component's parent's
/// coordinate space, and so on. The [nestedPoints] list must be growable and
/// modifiable.
///
/// The default implementation relies on the [CoordinateTransform] interface
/// to translate from the parent's coordinate system into the local one. Make
/// sure that your component implements this interface if it alters the
/// coordinate system when rendering.
///
/// If your component overrides [renderTree], then it almost certainly needs
/// to override this method as well, so that this method can find all rendered
/// components wherever they are.
Iterable<Component> componentsAtPoint(
Vector2 point, [
List<Vector2>? nestedPoints,
]) sync* {
nestedPoints?.add(point);
if (_children != null) {
for (final child in _children!.reversed()) {
Vector2? childPoint = point;
if (child is CoordinateTransform) {
childPoint = (child as CoordinateTransform).parentToLocal(point);
}
if (childPoint != null) {
yield* child.componentsAtPoint(childPoint, nestedPoints);
}
}
}
if (containsLocalPoint(point)) {
yield this;
}
nestedPoints?.removeLast();
}
//#endregion
//#region Priority
/// Render priority of this component. This allows you to control the order in
/// which your components are rendered.
///
/// Components are always updated and rendered in the order defined by what
/// this number is when the component is added to the game.
/// The smaller the priority, the sooner your component will be
/// updated/rendered.
/// It can be any integer (negative, zero, or positive).
/// If two components share the same priority, they will probably be drawn in
/// the order they were added.
///
/// Note that setting the priority is relatively expensive if the component is
/// already added to a component tree since all siblings have to be re-added
/// to the parent.
int get priority => _priority;
int _priority;
set priority(int newPriority) {
if (_priority != newPriority) {
_priority = newPriority;
final game = findGame();
if (game != null && _parent != null) {
(game as FlameGame).enqueueRebalance(_parent!);
}
}
}
//#endregion
//#region Internal lifecycle management
@internal
LifecycleEventStatus handleLifecycleEventAdd(Component parent) {
assert(!isMounted);
if (parent.isMounted && isLoaded) {
_parent ??= parent;
_mount();
return LifecycleEventStatus.done;
} else {
if (parent.isMounted && !isLoading) {
_startLoading();
}
return LifecycleEventStatus.block;
}
}
@internal
LifecycleEventStatus handleLifecycleEventRemove(Component parent) {
if (_parent == null) {
parent._children?.remove(this);
} else {
_remove();
assert(_parent == null);
}
return LifecycleEventStatus.done;
}
@internal
LifecycleEventStatus handleLifecycleEventMove(Component newParent) {
if (_parent != null) {
_remove();
}
if (newParent.isMounted) {
_parent = newParent;
_mount();
} else {
newParent.add(this);
}
return LifecycleEventStatus.done;
}
@mustCallSuper
@internal
void handleResize(Vector2 size) {
_children?.forEach((child) {
if (child.isLoading || child.isLoaded) {
child.onGameResize(size);
}
});
}
FutureOr<void> _startLoading() {
assert(_state == _initial);
assert(_parent != null);
assert(_parent!.findGame() != null);
assert(_parent!.findGame()!.hasLayout);
_setLoadingBit();
final onLoadFuture = onLoad();
if (onLoadFuture is Future) {
return onLoadFuture.then((dynamic _) => _finishLoading());
} else {
_finishLoading();
}
}
void _finishLoading() {
_clearLoadingBit();
_setLoadedBit();
_loadCompleter?.complete();
_loadCompleter = null;
}
/// Mount the component that is already loaded and has a mounted parent.
void _mount() {
assert(_parent != null && _parent!.isMounted);
assert(isLoaded && !isLoading);
_setMountingBit();
onGameResize(_parent!.findGame()!.canvasSize);
if (_parent is ReadOnlySizeProvider) {
onParentResize((_parent! as ReadOnlySizeProvider).size);
}
if (isRemoved) {
_clearRemovedBit();
} else if (isRemoving) {
_parent = null;
_clearRemovingBit();
_setRemovedBit();
return;
}
debugMode |= _parent!.debugMode;
onMount();
_setMountedBit();
_mountCompleter?.complete();
_mountCompleter = null;
_parent!.children.add(this);
_reAddChildren();
_parent!.onChildrenChanged(this, ChildrenChangeType.added);
_clearMountingBit();
if (_key != null) {
final currentGame = findGame();
if (currentGame is FlameGame) {
currentGame.registerKey(_key!, this);
}
}
}
/// Used by [_reAddChildren].
static final List<Component> _tmpChildren = [];
/// At the end of mounting, we remove all children components and then re-add
/// them one-by-one. The reason for this is that before the current component
/// was mounted, its [children] may have contained components in arbitrary
/// state -- initial, loading, unmounted, etc. However, we don't want to
/// have such components in a component tree. By removing and then re-adding
/// them, we ensure that they are placed in a queue, and will only be placed
/// into [children] once they are fully mounted.
void _reAddChildren() {
if (_children != null && _children!.isNotEmpty) {
assert(_tmpChildren.isEmpty);
_tmpChildren.addAll(_children!);
_children!.clear();
for (final child in _tmpChildren) {
child._parent = null;
_addChild(child);
}
_tmpChildren.clear();
}
}
@internal
void setMounted() {
_setLoadedBit();
_setMountedBit();
_reAddChildren();
}
void _remove() {
assert(_parent != null, 'Trying to remove a component with no parent');
if (_key != null) {
final game = findGame();
if (game is FlameGame) {
game.unregisterKey(_key!);
}
}
_parent!.children.remove(this);
propagateToChildren(
(Component component) {
component
..onRemove()
.._clearMountedBit()
.._clearRemovingBit()
.._setRemovedBit()
.._removeCompleter?.complete()
.._removeCompleter = null
.._parent!.onChildrenChanged(component, ChildrenChangeType.removed)
.._parent = null;
return true;
},
includeSelf: true,
);
}
//#endregion
//#region Debugging assistance
/// Returns whether this [Component] is in debug mode or not.
/// When a child is added to the [Component] it gets the same [debugMode] as
/// its parent has when it is prepared.
///
/// Returns `false` by default. Override it, or set it to true, to use debug
/// mode.
/// You can use this value to enable debug behaviors for your game and many
/// components will
/// show extra information on the screen when debug mode is activated.
bool debugMode = false;
/// How many decimal digits to print when displaying coordinates in the
/// debug mode. Setting this to null will suppress all coordinates from
/// the output.
int? get debugCoordinatesPrecision => 0;
/// A key that can be used to identify this component in the tree.
///
/// It can be used to retrieve this component from anywhere in the tree.
final ComponentKey? _key;
/// The color that the debug output should be rendered with.
Color debugColor = const Color(0xFFFF00FF);
final ValueCache<Paint> _debugPaintCache = ValueCache<Paint>();
final ValueCache<TextPaint> _debugTextPaintCache = ValueCache<TextPaint>();
/// The [debugColor] represented as a [Paint] object.
Paint get debugPaint {
if (!_debugPaintCache.isCacheValid([debugColor])) {
final paint = Paint()
..color = debugColor
..strokeWidth = 0 // hairline-width
..style = PaintingStyle.stroke;
_debugPaintCache.updateCache(paint, [debugColor]);
}
return _debugPaintCache.value!;
}
/// Returns a [TextPaint] object with the [debugColor] set as color for the
/// text.
TextPaint get debugTextPaint {
if (!_debugTextPaintCache.isCacheValid([debugColor])) {
final textPaint = TextPaint(
style: TextStyle(color: debugColor, fontSize: 12),
);
_debugTextPaintCache.updateCache(textPaint, [debugColor]);
}
return _debugTextPaintCache.value!;
}
void renderDebugMode(Canvas canvas) {}
//#endregion
//#region Legacy component placement overrides
/// What coordinate system this component should respect (i.e. should it
/// observe camera, viewport, or use the raw canvas).
///
/// Do note that this currently only works if the component is added directly
/// to the root `FlameGame`.
@Deprecated('''
Use the CameraComponent and add your component to the viewport with
cameraComponent.viewport.add(yourHudComponent) instead.
This will be removed in Flame v2.
''')
PositionType positionType = PositionType.game;
@Deprecated('To be removed in Flame v2')
@protected
Vector2 eventPosition(PositionInfo info) {
switch (positionType) {
case PositionType.game:
return info.eventPosition.game;
case PositionType.viewport:
return info.eventPosition.viewport;
case PositionType.widget:
return info.eventPosition.widget;
}
}