mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 20:13:50 +08:00
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:
12
doc/game.md
12
doc/game.md
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()),
|
||||
|
||||
57
examples/lib/stories/components/priority.dart
Normal file
57
examples/lib/stories/components/priority.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
104
packages/flame/test/components/priority_test.dart
Normal file
104
packages/flame/test/components/priority_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user