Extract shared children logic when handling components to ComponentSet (#859)

This commit is contained in:
Luan Nico
2021-07-04 02:44:15 -04:00
committed by GitHub
parent 235b080768
commit f818310e5b
19 changed files with 279 additions and 157 deletions

View File

@ -9,7 +9,7 @@ class Square extends PositionComponent {
}
}
class ParentSquare extends Square {
class ParentSquare extends Square with HasGameRef {
ParentSquare(Vector2 position, Vector2 size) : super(position, size);
@override
@ -27,7 +27,7 @@ class ParentSquare extends Square {
Square(Vector2(70, 200), Vector2(50, 50), angle: 5),
];
children.forEach(addChild);
children.forEach((c) => addChild(c, gameRef: gameRef));
}
}

View File

@ -3,6 +3,7 @@
## [Next]
- Fix camera not ending up in the correct position on long jumps
- Make the `JoystickPlayer` a `PositionComponent`
- Extract shared logic when handling components set in BaseComponent and BaseGame to ComponentSet.
## [1.0.0-releasecandidate.12]
- Fix link to code in example stories

View File

@ -70,7 +70,7 @@ class MyGame extends BaseGame with DoubleTapDetector, TapDetector {
final handled = components.any((c) {
if (c is PositionComponent && c.toRect().overlaps(touchArea)) {
remove(c);
components.remove(c);
return true;
}
return false;

View File

@ -2,6 +2,7 @@ export 'joystick.dart';
export 'src/anchor.dart';
export 'src/components/base_component.dart';
export 'src/components/component.dart';
export 'src/components/component_set.dart';
export 'src/components/isometric_tile_map_component.dart';
export 'src/components/mixins/collidable.dart';
export 'src/components/mixins/draggable.dart';

View File

@ -1,9 +1,6 @@
import 'dart:collection';
import 'dart:ui';
import 'package:meta/meta.dart';
import 'package:ordered_set/comparing.dart';
import 'package:ordered_set/ordered_set.dart';
import '../../game.dart';
import '../../gestures.dart';
@ -12,6 +9,7 @@ import '../effects/effects_handler.dart';
import '../extensions/vector2.dart';
import '../text.dart';
import 'component.dart';
import 'component_set.dart';
import 'mixins/has_game_ref.dart';
/// This can be extended to represent a basic Component for your game.
@ -22,8 +20,7 @@ import 'mixins/has_game_ref.dart';
abstract class BaseComponent extends Component {
final EffectsHandler _effectsHandler = EffectsHandler();
final OrderedSet<Component> _children =
OrderedSet(Comparing.on((c) => c.priority));
late final ComponentSet children = createComponentSet();
/// If the component has a parent it will be set here
BaseComponent? _parent;
@ -31,14 +28,6 @@ abstract class BaseComponent extends Component {
@override
BaseComponent? get parent => _parent;
/// The children list shouldn't be modified directly, that is why an
/// [UnmodifiableListView] is used. If you want to add children use the
/// [addChild] method, and if you want to propagate something to the children
/// use the [propagateToChildren] method.
UnmodifiableListView<Component> get children {
return UnmodifiableListView<Component>(_children);
}
/// This is set by the BaseGame to tell this component to render additional debug information,
/// like borders, coordinates, etc.
/// This is very helpful while debugging. Set your BaseGame debugMode to true.
@ -69,9 +58,9 @@ abstract class BaseComponent extends Component {
@mustCallSuper
@override
void update(double dt) {
children.updateComponentList();
_effectsHandler.update(dt);
_children.removeWhere((c) => c.shouldRemove).forEach((c) => c.onRemove());
_children.forEach((c) => c.update(dt));
children.forEach((c) => c.update(dt));
}
@mustCallSuper
@ -84,7 +73,7 @@ abstract class BaseComponent extends Component {
@override
void renderTree(Canvas canvas) {
render(canvas);
_children.forEach((c) {
children.forEach((c) {
canvas.save();
c.renderTree(canvas);
canvas.restore();
@ -105,21 +94,21 @@ abstract class BaseComponent extends Component {
@override
void onGameResize(Vector2 gameSize) {
super.onGameResize(gameSize);
_children.forEach((child) => child.onGameResize(gameSize));
children.forEach((child) => child.onGameResize(gameSize));
}
@mustCallSuper
@override
void onMount() {
super.onMount();
_children.forEach((child) => child.onMount());
children.forEach((child) => child.onMount());
}
@mustCallSuper
@override
void onRemove() {
super.onRemove();
_children.forEach((child) => child.onRemove());
children.forEach((child) => child.onRemove());
}
/// Called to check whether the point is to be counted as within the component
@ -145,13 +134,7 @@ abstract class BaseComponent extends Component {
/// Get a list of non removed effects
List<ComponentEffect> get effects => _effectsHandler.effects;
/// Uses the game passed in, or uses the game from [HasGameRef] otherwise,
/// to prepare the child component before it is added to the list of children.
/// Note that this component needs to be added to the game first if
/// [this.gameRef] should be used to prepare the child.
/// For children that don't need preparation from the game instance can
/// disregard both the options given above.
Future<void> addChild(Component child, {Game? gameRef}) async {
void prepare(Component child, {Game? gameRef}) {
if (this is HasGameRef) {
final c = this as HasGameRef;
gameRef ??= c.hasGameRef ? c.gameRef : null;
@ -169,39 +152,35 @@ abstract class BaseComponent extends Component {
child._parent = this;
child.debugMode = debugMode;
}
final childOnLoadFuture = child.onLoad();
if (childOnLoadFuture != null) {
await childOnLoadFuture;
}
_children.add(child);
if (isMounted) {
child.onMount();
}
}
Future<void> addChildren(
Iterable<Component> children, {
Game? gameRef,
}) async {
await Future.wait(
children.map(
(child) => addChild(child, gameRef: gameRef),
),
);
/// Uses the game passed in, or uses the game from [HasGameRef] otherwise,
/// to prepare the child component before it is added to the list of children.
/// Note that this component needs to be added to the game first if
/// [this.gameRef] should be used to prepare the child.
/// For children that don't need preparation from the game instance can
/// disregard both the options given above.
Future<void> addChild(Component child, {BaseGame? gameRef}) {
return children.addChild(child, gameRef: gameRef);
}
bool removeChild(Component c) {
return _children.remove(c);
/// Adds mutiple children.
///
/// See [addChild] for details (or `children.addChildren()`).
Future<void> addChildren(List<Component> cs, {BaseGame? gameRef}) {
return children.addChildren(cs, gameRef: gameRef);
}
void clearChildren() {
_children.clear();
}
/// Whether the children list contains the given component.
///
/// This method uses reference equality.
bool containsChild(Component c) => children.contains(c);
bool containsChild(Component c) => _children.contains(c);
void reorderChildren() => _children.rebalanceAll();
/// Call this if any of this component's children priorities have changed
/// at runtime.
///
/// This will call `rebalanceAll` on the [children] ordered set.
void reorderChildren() => children.rebalanceAll();
/// This method first calls the passed handler on the leaves in the tree,
/// the children without any children of their own.
@ -218,7 +197,7 @@ abstract class BaseComponent extends Component {
bool Function(T) handler,
) {
var shouldContinue = true;
for (final child in _children) {
for (final child in children) {
if (child is BaseComponent) {
shouldContinue = child.propagateToChildren(handler);
}
@ -236,4 +215,12 @@ abstract class BaseComponent extends Component {
Vector2 eventPosition(PositionInfo info) {
return isHud ? info.eventPosition.widget : info.eventPosition.game;
}
ComponentSet createComponentSet() {
final components = ComponentSet.createDefault(prepare);
if (this is HasGameRef) {
components.register<HasGameRef>();
}
return components;
}
}

View File

@ -0,0 +1,173 @@
import 'package:ordered_set/comparing.dart';
import 'package:ordered_set/queryable_ordered_set.dart';
import '../../components.dart';
import '../game/base_game.dart';
/// This is a simple wrapper over [QueryableOrderedSet] to be used by
/// [BaseGame] and [BaseComponent].
///
/// Instead of immediatly modifying the component list, all insertion
/// and removal operations are queued to be performed on the next tick.
///
/// This will avoid any concurrent modification exceptions while the game
/// iterates through the component list.
///
/// This wrapper also garantueed that prepare, onLoad, onMount and all the
/// lifecycle methods are called properly.
class ComponentSet extends QueryableOrderedSet<Component> {
/// Components to be added on the next update.
///
/// The component list is only changed at the start of each update to avoid
/// concurrency issues.
final List<Component> _addLater = [];
/// Components to be removed on the next update.
///
/// The component list is only changed at the start of each update to avoid
/// concurrency issues.
final Set<Component> _removeLater = {};
/// This is the "prepare" function that will be called *before* the
/// component is added to the component list by the add/addAll methods.
final void Function(Component child, {BaseGame? gameRef}) prepare;
ComponentSet(
int Function(Component e1, Component e2)? compare,
this.prepare,
) : super(compare);
/// Prepares and registers one component to be added on the next game tick.
///
/// This is the interface compliant version; if you want to provide an
/// explicit gameRef or await for the onLoad, use [addChild].
///
/// Note: the component is only added on the next tick. This method always
/// returns true.
@override
bool add(Component c) {
addChild(c);
return true;
}
/// Prepares and registers a list of components to be added on the next game
/// tick.
///
/// This is the interface compliant version; if you want to provide an
/// explicit gameRef or await for the onLoad, use [addChild].
///
/// Note: the components are only added on the next tick. This method always
/// returns the total lenght of the provided list.
@override
int addAll(Iterable<Component> components) {
addChildren(components);
return components.length;
}
/// Prepares and registers one component to be added on the next game tick.
///
/// This allows you to provide a specific gameRef if this component is being
/// added from within another component that is already on a BaseGame.
/// You can await for the onLoad function, if present.
/// This method can be considered sync for all intents and purposes if no
/// onLoad is provided by the component.
Future<void> addChild(Component c, {BaseGame? gameRef}) async {
prepare(c, gameRef: gameRef);
final loadFuture = c.onLoad();
if (loadFuture != null) {
await loadFuture;
}
_addLater.add(c);
}
/// Prepares and registers a list of component to be added on the next game
/// tick.
///
/// See [addChild] for more details.
Future<void> addChildren(
Iterable<Component> components, {
BaseGame? gameRef,
}) async {
final ps = components.map((c) => addChild(c, gameRef: gameRef));
await Future.wait(ps);
}
/// Marks a component to be removed from the components list on the next game
/// tick.
@override
bool remove(Component c) {
_removeLater.add(c);
return true;
}
/// Marks a list of components to be removed from the components list on the
/// next game tick.
void removeAll(Iterable<Component> components) {
_removeLater.addAll(components);
}
/// Marks all existing components to be removed from the components list on
/// the next game tick.
@override
void clear() {
_removeLater.addAll(this);
}
/// Materializes the component list in reversed order.
Iterable<Component> reversed() {
return toList().reversed;
}
/// Call this on your update method.
///
/// This method effectuates any pending operations of insertion or removal,
/// and thus actually modifies the components set.
/// Note: do not call this while iterating the set.
void updateComponentList() {
_removeLater.addAll(where((c) => c.shouldRemove));
_removeLater.forEach((c) {
c.onRemove();
super.remove(c);
});
_removeLater.clear();
if (_addLater.isNotEmpty) {
final addNow = _addLater.toList(growable: false);
_addLater.clear();
addNow.forEach((c) {
super.add(c);
c.onMount();
});
}
}
@override
void rebalanceAll() {
final elements = toList();
// bypass the wrapper because the components are already added
super.clear();
elements.forEach(super.add);
}
@override
void rebalanceWhere(bool Function(Component element) test) {
// bypass the wrapper because the components are already added
final elements = super.removeWhere(test).toList();
elements.forEach(super.add);
}
/// Creates a [ComponentSet] with a default value for the compare function,
/// using the Component's priority for sorting.
///
/// You must still provide your [prepare] function depending on the context.
static ComponentSet createDefault(
void Function(Component child, {BaseGame? gameRef}) prepare,
) {
return ComponentSet(
Comparing.on<Component>((c) => c.priority),
prepare,
);
}
}

View File

@ -53,6 +53,6 @@ class JoystickComponent extends JoystickController {
void removeAction(int actionId) {
final action = children
.firstWhere((e) => e is JoystickAction && e.actionId == actionId);
removeChild(action);
children.remove(action);
}
}

View File

@ -84,7 +84,7 @@ mixin HasDraggableComponents on BaseGame {
}
void _onGenericEventReceived(bool Function(Draggable) handler) {
for (final c in components.toList().reversed) {
for (final c in components.reversed()) {
var shouldContinue = true;
if (c is BaseComponent) {
shouldContinue = c.propagateToChildren<Draggable>(handler);

View File

@ -1,12 +1,9 @@
import 'package:ordered_set/queryable_ordered_set.dart';
import '../../../game.dart';
import '../../components/mixins/collidable.dart';
import '../../geometry/collision_detection.dart';
mixin HasCollidables on BaseGame {
void handleCollidables() {
final qos = components as QueryableOrderedSet;
collisionDetection(qos.query<Collidable>());
collisionDetection(components.query<Collidable>());
}
}

View File

@ -1,7 +1,7 @@
import '../../../components.dart';
import '../../game/game.dart';
import '../../../game.dart';
mixin HasGameRef<T extends Game> {
mixin HasGameRef<T extends BaseGame> on Component {
T? _gameRef;
T get gameRef {
@ -17,9 +17,10 @@ mixin HasGameRef<T extends Game> {
set gameRef(T gameRef) {
_gameRef = gameRef;
if (this is BaseComponent) {
// TODO(luan) this is wrong, should be done using propagateToChildren
(this as BaseComponent)
.children
.whereType<HasGameRef<T>>()
.query<HasGameRef>()
.forEach((e) => e.gameRef = gameRef);
}
}

View File

@ -35,7 +35,7 @@ mixin HasHoverableComponents on BaseGame {
return true; // always continue
}
for (final c in components.toList().reversed) {
for (final c in components.reversed()) {
if (c is BaseComponent) {
c.propagateToChildren<Hoverable>(_mouseMoveHandler);
}

View File

@ -49,7 +49,7 @@ mixin Tapable on BaseComponent {
mixin HasTapableComponents on BaseGame {
void _handleTapEvent(bool Function(Tapable child) tapEventHandler) {
for (final c in components.toList().reversed) {
for (final c in components.reversed()) {
var shouldContinue = true;
if (c is BaseComponent) {
shouldContinue = c.propagateToChildren<Tapable>(tapEventHandler);

View File

@ -1,8 +1,6 @@
import 'dart:ui';
import 'package:meta/meta.dart';
import 'package:ordered_set/comparing.dart';
import 'package:ordered_set/ordered_set.dart';
import 'package:ordered_set/queryable_ordered_set.dart';
import '../../components.dart';
@ -28,19 +26,7 @@ import 'viewport.dart';
/// It is based on the Component system.
class BaseGame extends Game with FPSCounter {
/// The list of components to be updated and rendered by the base game.
late final OrderedSet<Component> components = createOrderedSet();
/// Components to be added on the next update.
///
/// The component list is only changed at the start of each [update] to avoid
/// concurrency issues.
final List<Component> _addLater = [];
/// Components to be removed on the next update.
///
/// The component list is only changed at the start of each [update] to avoid
/// concurrency issues.
final Set<Component> _removeLater = {};
late final ComponentSet components = createComponentSet();
/// The camera translates the coordinate space after the viewport is applied.
final Camera camera = Camera();
@ -97,14 +83,14 @@ class BaseGame extends Game with FPSCounter {
///
/// You can return a specific sub-class of OrderedSet, like
/// [QueryableOrderedSet] for example, that we use for Collidables.
OrderedSet<Component> createOrderedSet() {
final comparator = Comparing.on<Component>((c) => c.priority);
ComponentSet createComponentSet() {
final components = ComponentSet.createDefault(
(c, {BaseGame? gameRef}) => prepare(c),
);
if (this is HasCollidables) {
final qos = QueryableOrderedSet<Component>(comparator);
qos.register<Collidable>();
return qos;
components.register<Collidable>();
}
return OrderedSet<Component>(comparator);
return components;
}
/// This method is called for every component added.
@ -114,6 +100,11 @@ class BaseGame extends Game with FPSCounter {
/// By default, this calls the first time resize for every component, so don't forget to call super.preAdd when overriding.
@mustCallSuper
void prepare(Component c) {
assert(
hasLayout,
'"prepare/add" called before the game is ready. Did you try to access it on the Game constructor? Use the "onLoad" method instead.',
);
if (c is Collidable) {
assert(
this is HasCollidables,
@ -144,7 +135,7 @@ class BaseGame extends Game with FPSCounter {
}
if (c is HasGameRef) {
(c as HasGameRef).gameRef = this;
c.gameRef = this;
}
// first time resize
@ -156,38 +147,16 @@ class BaseGame extends Game with FPSCounter {
/// This methods is an async operation since it await the `onLoad` method of the component. Nevertheless, this method only need to be waited to finish if by some reason, your logic needs to be sure that the component has finished loading, otherwise, this method can be called without waiting for it to finish as the BaseGame already handle the loading of the component.
///
/// *Note:* Do not add components on the game constructor. This method can only be called after the game already has its layout set, this can be verified by the [hasLayout] property, to add components upon a game initialization, the [onLoad] method can be used instead.
Future<void> add(Component c) async {
assert(
hasLayout,
'"add" called before the game is ready. Did you try to access it on the Game constructor? Use the "onLoad" method instead.',
);
prepare(c);
final loadFuture = c.onLoad();
if (loadFuture != null) {
await loadFuture;
}
_addLater.add(c);
Future<void> add(Component c) {
return components.addChild(c);
}
/// Prepares and registers a list of components to be added on the next game tick
void addAll(Iterable<Component> components) {
components.forEach(add);
}
/// Marks a component to be removed from the components list on the next game tick
void remove(Component c) {
_removeLater.add(c);
}
/// Marks a list of components to be removed from the components list on the next game tick
void removeAll(Iterable<Component> components) {
_removeLater.addAll(components);
}
/// Marks all existing components to be removed from the components list on the next game tick
void clear() {
_removeLater.addAll(components);
/// Adds a list of components, calling addChild for each one.
///
/// The returned Future completes once all are loaded and added.
/// Component loading is done in parallel.
Future<void> addAll(List<Component> cs) {
return components.addChildren(cs);
}
/// This implementation of render basically calls [renderComponent] for every component, making sure the canvas is reset for each one.
@ -223,7 +192,7 @@ class BaseGame extends Game with FPSCounter {
@override
@mustCallSuper
void update(double dt) {
_updateComponentList();
components.updateComponentList();
if (this is HasCollidables) {
(this as HasCollidables).handleCollidables();
@ -233,22 +202,6 @@ class BaseGame extends Game with FPSCounter {
camera.update(dt);
}
void _updateComponentList() {
_removeLater.addAll(components.where((c) => c.shouldRemove));
_removeLater.forEach((c) {
c.onRemove();
components.remove(c);
});
_removeLater.clear();
if (_addLater.isNotEmpty) {
final addNow = _addLater.toList(growable: false);
_addLater.clear();
components.addAll(addNow);
addNow.forEach((component) => component.onMount());
}
}
/// 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.
///

View File

@ -62,6 +62,7 @@ void main() {
final child = MyTap();
final wrapper = MyComposed();
wrapper.addChild(child);
wrapper.update(0); // children are only added on the next tick
expect(wrapper.containsChild(child), true);
});
@ -69,10 +70,15 @@ void main() {
test('removes the child from the component', () {
final child = MyTap();
final wrapper = MyComposed();
wrapper.addChild(child);
expect(true, wrapper.containsChild(child));
wrapper.removeChild(child);
wrapper.addChild(child);
expect(wrapper.containsChild(child), false);
wrapper.update(0); // children are only added on the next tick
expect(wrapper.containsChild(child), true);
wrapper.children.remove(child);
expect(wrapper.containsChild(child), true);
wrapper.update(0); // children are only removed on the next tick
expect(wrapper.containsChild(child), false);
});
@ -81,8 +87,12 @@ void main() {
() async {
final child = MyAsyncChild();
final wrapper = MyComposed();
await wrapper.addChild(child);
final future = wrapper.addChild(child);
expect(wrapper.containsChild(child), false);
await future;
expect(wrapper.containsChild(child), false);
wrapper.update(0);
expect(wrapper.containsChild(child), true);
},
);
@ -107,7 +117,7 @@ void main() {
final game = MyGame();
final children = List.generate(10, (_) => MyTap());
final wrapper = MyComposed();
wrapper.addChildren(children);
wrapper.children.addChildren(children);
game.onResize(size);
game.add(wrapper);

View File

@ -58,7 +58,7 @@ void main() {
priorityComponents.shuffle();
final game = BaseGame()..onResize(Vector2.zero());
game.add(parentComponent);
parentComponent.addChildren(priorityComponents, gameRef: game);
parentComponent.children.addChildren(priorityComponents, gameRef: game);
final children = parentComponent.children;
game.update(0);
componentsSorted(children);
@ -73,7 +73,7 @@ void main() {
priorityComponents.shuffle();
final game = BaseGame()..onResize(Vector2.zero());
game.add(parentComponent);
parentComponent.addChildren(priorityComponents, gameRef: game);
parentComponent.children.addChildren(priorityComponents, gameRef: game);
final children = parentComponent.children;
game.update(0);
componentsSorted(children);
@ -92,7 +92,7 @@ void main() {
final game = BaseGame()..onResize(Vector2.zero());
game.add(grandParentComponent);
grandParentComponent.addChild(parentComponent, gameRef: game);
parentComponent.addChildren(priorityComponents, gameRef: game);
parentComponent.children.addChildren(priorityComponents, gameRef: game);
final children = parentComponent.children;
game.update(0);
componentsSorted(children);

View File

@ -172,7 +172,7 @@ void main() {
// by the function on the component, but the onRemove callback should
// only be called once.
component.remove();
game.remove(component);
game.components.remove(component);
// The component is not removed from the component list until an update has been performed
game.update(0.0);
@ -214,7 +214,7 @@ void main() {
game.update(0.0);
expect(game.components.length, equals(3));
game.clear();
game.components.clear();
// Ensure clear does not remove components directly
expect(game.components.length, equals(3));

View File

@ -1,5 +1,8 @@
# CHANGELOG
## [next]
- Update mechanism by which `BodyComponent`'s are disposed to use the `onRemove` method
## [0.7.3-releasecandidate.12]
- Fix prepareCanvas type error

View File

@ -133,4 +133,10 @@ abstract class BodyComponent<T extends Forge2DGame> extends BaseComponent
bool containsPoint(Vector2 point) {
return body.fixtures.any((fixture) => fixture.testPoint(point));
}
@override
void onRemove() {
super.onRemove();
world.destroyBody(body);
}
}

View File

@ -5,7 +5,6 @@ import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:forge2d/forge2d.dart' hide Timer;
import 'body_component.dart';
import 'contact_callbacks.dart';
import 'forge2d_camera.dart';
@ -54,15 +53,6 @@ class Forge2DGame extends BaseGame {
}
}
@override
void remove(Component component) {
super.remove(component);
if (component is BodyComponent) {
world.destroyBody(component.body);
component.remove();
}
}
void addContactCallback(ContactCallback callback) {
_contactCallbacks.register(callback);
}