Possibility to change priority and reorder the component list (#793)

* Possibility to change component priority

* Fix formatting

* Posibility to change priority

* Docs for priority change

* Priority example

* Add section explaining priority

* Update doc/game.md

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Update examples/lib/stories/components/components.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* No null priorities in the super call chain

* Possibility to wait for all children to be loaded

* Test for addChildren

* Introduce parent to Component

* Check whether parent is BaseComponent

Co-authored-by: Erick <erickzanardoo@gmail.com>
This commit is contained in:
Lukas Klingsbo
2021-05-19 19:07:54 +02:00
committed by GitHub
parent 5f78a15770
commit ef7427941c
13 changed files with 301 additions and 25 deletions

View File

@ -62,6 +62,18 @@ want to remove a list of components.
Any component on which the `remove()` method has been called will also be removed. You can do this
simply by doing `yourComponent.remove();`.
## Changing component priorities (render/update order)
To update a component with a new priority you have to call either `BaseGame.changePriority`, or
`BaseGame.changePriorities` if you want to change the priorities of many components at once.
This design is due to the fact that the components doesn't always have access to the component list and
because rebalancing the component list is a fairly computationally expensive operation, so you
would rather reorder the list once after all the priorities have been changed and not once for each
priority change, if you have several changes.
The higher a priority is the later it is rendered and updated, which will make it appear closer on
the screen since it will be rendered on top of any components with lower priority that were rendered
before it.
## Debug mode
Flame's `BaseGame` class provides a variable called `debugMode`, which by default is `false`. It can

View File

@ -6,7 +6,11 @@ import 'package:flame/palette.dart';
class SquareComponent extends PositionComponent {
Paint paint = BasicPalette.white.paint();
SquareComponent() : super(size: Vector2.all(100.0));
SquareComponent({int priority = 0})
: super(
size: Vector2.all(100.0),
priority: priority,
);
@override
void render(Canvas c) {

View File

@ -23,7 +23,7 @@ class MovableSquare extends SquareComponent
final Vector2 velocity = Vector2.zero();
late Timer timer;
MovableSquare() {
MovableSquare() : super(priority: 1) {
addShape(HitboxRectangle());
timer = Timer(3.0)
..stop()
@ -48,9 +48,6 @@ class MovableSquare extends SquareComponent
textRenderer.render(c, text, size / 2, anchor: Anchor.center);
}
@override
int get priority => 1;
@override
void onCollision(Set<Vector2> points, Collidable other) {
if (other is Rock) {
@ -70,6 +67,8 @@ class Map extends Component {
..style = PaintingStyle.stroke;
static final Paint _paintBg = Paint()..color = const Color(0xFF333333);
Map() : super(priority: 0);
@override
void render(Canvas canvas) {
super.render(canvas);
@ -77,9 +76,6 @@ class Map extends Component {
canvas.drawRect(bounds, _paintBorder);
}
@override
int get priority => 0;
static double genCoord() {
return -S + R.nextDouble() * (2 * S);
}
@ -89,7 +85,7 @@ class Rock extends SquareComponent with Hitbox, Collidable, Tapable {
static final unpressedPaint = Paint()..color = const Color(0xFF2222FF);
static final pressedPaint = Paint()..color = const Color(0xFF414175);
Rock(Vector2 position) {
Rock(Vector2 position) : super(priority: 2) {
this.position.setFrom(position);
size.setValues(50, 50);
paint = unpressedPaint;
@ -113,9 +109,6 @@ class Rock extends SquareComponent with Hitbox, Collidable, Tapable {
paint = unpressedPaint;
return true;
}
@override
int get priority => 2;
}
class CameraAndViewportGame extends BaseGame

View File

@ -4,6 +4,12 @@ import 'package:flame/game.dart';
import '../../commons/commons.dart';
import 'composability.dart';
import 'debug.dart';
import 'priority.dart';
const priorityInfo = '''
On this example, click on the square to bring them to the front by changing the
priority.
''';
void addComponentsStories(Dashbook dashbook) {
dashbook.storiesOf('Components')
@ -12,6 +18,12 @@ void addComponentsStories(Dashbook dashbook) {
(_) => GameWidget(game: Composability()),
codeLink: baseLink('components/composability.dart'),
)
..add(
'Priority',
(_) => GameWidget(game: Priority()),
codeLink: baseLink('components/priority.dart'),
info: priorityInfo,
)
..add(
'Debug',
(_) => GameWidget(game: DebugGame()),

View File

@ -0,0 +1,57 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/gestures.dart';
import 'package:flame/palette.dart';
class Square extends PositionComponent with HasGameRef<Priority>, Tapable {
late final Paint paint;
Square(Vector2 position) {
this.position.setFrom(position);
size.setValues(100, 100);
paint = _randomPaint();
}
@override
bool onTapDown(TapDownInfo event) {
final topComponent = gameRef.components.last;
if (topComponent != this) {
gameRef.changePriority(this, topComponent.priority + 1);
}
return false;
}
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRect(size.toRect(), paint);
}
static Paint _randomPaint() {
final rng = Random();
final color = Color.fromRGBO(
rng.nextInt(256),
rng.nextInt(256),
rng.nextInt(256),
0.9,
);
return PaletteEntry(color).paint();
}
}
class Priority extends BaseGame with HasTapableComponents {
@override
Future<void> onLoad() async {
final squares = [
Square(Vector2(100, 100)),
Square(Vector2(160, 100)),
Square(Vector2(170, 150)),
Square(Vector2(110, 150)),
];
addAll(squares);
}
}

View File

@ -18,37 +18,37 @@ void addControlsStories(Dashbook dashbook) {
..add(
'Keyboard',
(_) => GameWidget(game: KeyboardGame()),
codeLink: baseLink('gestures/keyboard.dart'),
codeLink: baseLink('controls/keyboard.dart'),
)
..add(
'Mouse Movement',
(_) => GameWidget(game: MouseMovementGame()),
codeLink: baseLink('gestures/mouse_movement.dart'),
codeLink: baseLink('controls/mouse_movement.dart'),
)
..add(
'Scroll',
(_) => GameWidget(game: ScrollGame()),
codeLink: baseLink('gestures/scroll.dart'),
codeLink: baseLink('controls/scroll.dart'),
)
..add(
'Multitap',
(_) => GameWidget(game: MultitapGame()),
codeLink: baseLink('gestures/multitap.dart'),
codeLink: baseLink('controls/multitap.dart'),
)
..add(
'Multitap Advanced',
(_) => GameWidget(game: MultitapAdvancedGame()),
codeLink: baseLink('gestures/multitap_advanced.dart'),
codeLink: baseLink('controls/multitap_advanced.dart'),
)
..add(
'Tapables',
(_) => GameWidget(game: TapablesGame()),
codeLink: baseLink('gestures/tappables.dart'),
codeLink: baseLink('controls/tapables.dart'),
)
..add(
'Overlaping Tappables',
(_) => GameWidget(game: OverlappingTapablesGame()),
codeLink: baseLink('gestures/overlaping_tappables.dart'),
codeLink: baseLink('controls/overlaping_tappables.dart'),
)
..add(
'Draggables',
@ -63,16 +63,16 @@ void addControlsStories(Dashbook dashbook) {
),
);
},
codeLink: baseLink('gestures/draggables.dart'),
codeLink: baseLink('controls/draggables.dart'),
)
..add(
'Joystick',
(_) => GameWidget(game: JoystickGame()),
codeLink: baseLink('gestures/joystick.dart'),
codeLink: baseLink('controls/joystick.dart'),
)
..add(
'Joystick Advanced',
(_) => GameWidget(game: AdvancedJoystickGame()),
codeLink: baseLink('gestures/advanced_joystick.dart'),
codeLink: baseLink('controls/advanced_joystick.dart'),
);
}

View File

@ -4,6 +4,7 @@
- Replace deprecated analysis option lines-of-executable-code with source-lines-of-code
- Fix the anchor of SpriteWidget
- Add test for re-adding previously removed component
- Add possibility to dynamically change priority of components
- Add onCollisionEnd to make it possible for the user to easily detect when a collision ends
- Adding test coverage to packages
- Fix Text Rendering not working properly

View File

@ -27,6 +27,7 @@ abstract class BaseComponent extends Component {
/// If the component has a parent it will be set here
BaseComponent? _parent;
@override
BaseComponent? get parent => _parent;
/// The children list shouldn't be modified directly, that is why an
@ -57,6 +58,8 @@ abstract class BaseComponent extends Component {
),
);
BaseComponent({int priority = 0}) : super(priority: priority);
/// 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.
@ -171,6 +174,17 @@ abstract class BaseComponent extends Component {
}
}
Future<void> addChildren(
Iterable<Component> children, {
Game? gameRef,
}) async {
await Future.wait(
children.map(
(child) => addChild(child, gameRef: gameRef),
),
);
}
bool removeChild(Component c) {
return _children.remove(c);
}
@ -181,6 +195,8 @@ abstract class BaseComponent extends Component {
bool containsChild(Component c) => _children.contains(c);
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.
/// Then it continues through all other children. The propagation continues

View File

@ -21,20 +21,26 @@ abstract class Component {
/// Whether this component is currently mounted on a game or not
bool get isMounted => _isMounted;
/// If the component has a parent it will be set here
Component? _parent;
Component? get parent => _parent;
/// 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.
final int priority;
int get priority => _priority;
int _priority;
/// Whether this component should be removed or not.
///
/// It will be checked once per component per tick, and if it is true, BaseGame will remove it.
bool shouldRemove = false;
Component({this.priority = 0});
Component({int priority = 0}) : _priority = priority;
/// This method is called periodically by the game engine to request that your component updates itself.
///
@ -88,4 +94,9 @@ abstract class Component {
/// }
/// ```
Future<void>? onLoad() => null;
/// Usually this is not something that the user would want to call since the
/// component list isn't re-ordered when it is called.
/// See BaseGame.changePriority instead.
void changePriorityWithoutResorting(int priority) => _priority = priority;
}

View File

@ -139,8 +139,10 @@ abstract class PositionComponent extends BaseComponent {
this.anchor = Anchor.topLeft,
this.renderFlipX = false,
this.renderFlipY = false,
int priority = 0,
}) : _position = position ?? Vector2.zero(),
_size = size ?? Vector2.zero();
_size = size ?? Vector2.zero(),
super(priority: priority);
@override
bool containsPoint(Vector2 point) {

View File

@ -238,6 +238,58 @@ class BaseGame extends Game with FPSCounter {
/// show extra information on the screen when debug mode is activated
bool debugMode = false;
/// Changes the priority of [component] and reorders the games component list.
///
/// Returns true if changing the component's priority modified one of the
/// components that existed directly on the game and false if it
/// either was a child of another component, if it didn't exist at all or if
/// it was a component added directly on the game but its priority didn't
/// change.
bool changePriority(
Component component,
int priority, {
bool reorderRoot = true,
}) {
if (component.priority == priority) {
return false;
}
component.changePriorityWithoutResorting(priority);
if (reorderRoot) {
if (component.parent != null && component.parent is BaseComponent) {
(component.parent! as BaseComponent).reorderChildren();
} else if (components.contains(component)) {
components.rebalanceAll();
}
}
return true;
}
/// Since changing priorities is quite an expensive operation you should use
/// this method if you want to change multiple priorities at once so that the
/// tree doesn't have to be reordered multiple times.
void changePriorities(Map<Component, int> priorities) {
var hasRootComponents = false;
final parents = <BaseComponent>{};
priorities.forEach((component, priority) {
final wasUpdated = changePriority(
component,
priority,
reorderRoot: false,
);
if (wasUpdated) {
if (component.parent != null && component.parent is BaseComponent) {
parents.add(component.parent! as BaseComponent);
} else {
hasRootComponents |= components.contains(component);
}
}
});
if (hasRootComponents) {
components.rebalanceAll();
}
parents.forEach((parent) => parent.reorderChildren());
}
/// Returns the current time in seconds with microseconds precision.
///
/// This is compatible with the `dt` value used in the [update] method.

View File

@ -98,6 +98,18 @@ void main() {
expect(child.tapped, true);
});
test('add multiple children with addChildren', () {
final game = MyGame();
final children = List.generate(10, (_) => MyTap());
final wrapper = MyComposed();
wrapper.addChildren(children);
game.onResize(size);
game.add(wrapper);
game.update(0.0);
expect(wrapper.children.length, children.length);
});
test('tap on offset children', () {
final game = MyGame();
final child = MyTap()

View File

@ -0,0 +1,104 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:test/test.dart';
class PriorityComponent extends BaseComponent {
PriorityComponent(int priority) : super(priority: priority);
}
void componentsSorted(Iterable<Component> components) {
final priorities = components.map<int>((c) => c.priority).toList();
expect(priorities.toList(), orderedEquals(priorities..sort()));
}
void main() {
group('priority test', () {
test('components with different priorities are sorted in the list', () {
final priorityComponents = List.generate(10, (i) => PriorityComponent(i));
priorityComponents.shuffle();
final game = BaseGame()..onResize(Vector2.zero());
game.addAll(priorityComponents);
game.update(0);
componentsSorted(game.components);
});
test('changing priority should reorder component list', () {
final firstCompopnent = PriorityComponent(-1);
final priorityComponents = List.generate(10, (i) => PriorityComponent(i))
..add(firstCompopnent);
priorityComponents.shuffle();
final game = BaseGame()..onResize(Vector2.zero());
final components = game.components;
game.addAll(priorityComponents);
game.update(0);
componentsSorted(components);
expect(components.first, firstCompopnent);
game.changePriority(firstCompopnent, 11);
expect(components.last, firstCompopnent);
});
test('changing priorities should reorder component list', () {
final priorityComponents = List.generate(10, (i) => PriorityComponent(i));
priorityComponents.shuffle();
final game = BaseGame()..onResize(Vector2.zero());
final components = game.components;
game.addAll(priorityComponents);
game.update(0);
componentsSorted(components);
final first = components.first;
final last = components.last;
game.changePriorities({first: 20, last: -1});
expect(components.first, last);
expect(components.last, first);
});
test('changing child priority should reorder component list', () {
final parentComponent = PriorityComponent(0);
final priorityComponents = List.generate(10, (i) => PriorityComponent(i));
priorityComponents.shuffle();
final game = BaseGame()..onResize(Vector2.zero());
game.add(parentComponent);
parentComponent.addChildren(priorityComponents, gameRef: game);
final children = parentComponent.children;
game.update(0);
componentsSorted(children);
final first = children.first;
game.changePriority(first, 20);
expect(children.last, first);
});
test('changing child priorities should reorder component list', () {
final parentComponent = PriorityComponent(0);
final priorityComponents = List.generate(10, (i) => PriorityComponent(i));
priorityComponents.shuffle();
final game = BaseGame()..onResize(Vector2.zero());
game.add(parentComponent);
parentComponent.addChildren(priorityComponents, gameRef: game);
final children = parentComponent.children;
game.update(0);
componentsSorted(children);
final first = children.first;
final last = children.last;
game.changePriorities({first: 20, last: -1});
expect(children.first, last);
expect(children.last, first);
});
test('changing grand child priority should reorder component list', () {
final grandParentComponent = PriorityComponent(0);
final parentComponent = PriorityComponent(0);
final priorityComponents = List.generate(10, (i) => PriorityComponent(i));
priorityComponents.shuffle();
final game = BaseGame()..onResize(Vector2.zero());
game.add(grandParentComponent);
grandParentComponent.addChild(parentComponent, gameRef: game);
parentComponent.addChildren(priorityComponents, gameRef: game);
final children = parentComponent.children;
game.update(0);
componentsSorted(children);
final first = children.first;
game.changePriority(first, 20);
expect(children.last, first);
});
});
}