Skip to content

Commit

Permalink
feat: ComponentKey API (#2566)
Browse files Browse the repository at this point in the history
Adds a new key api on FCS, which will allow users to get a component from the tree, without needing to iterate over all the children or a parent descendants.
  • Loading branch information
erickzanardo committed Jun 20, 2023
1 parent 667a169 commit b3efb61
Show file tree
Hide file tree
Showing 41 changed files with 409 additions and 18 deletions.
46 changes: 46 additions & 0 deletions doc/flame/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,52 @@ If you try to add `MyComponent` to a tree that does not contain `MyAncestorCompo
an assertion error will be thrown.


### Component Keys

Components can have an identification key that allows them to be retrieved from the component tree, from
any point of the tree.

To register a component with a key, simply pass a key to the `key` argument on the component's
constructor:

```dart
final myComponent = Component(
key: ComponentKey.named('player'),
);
```

Then, to retrieve it in a different point of the component tree:

```dart
flameGame.findByKey(ComponentKey.named('player'));
```

There are two types of keys, `unique` and `named`. Unique keys are based on equality of the key
instance, meaning that:

```dart
final key = ComponentKey.unique();
final key2 = key;
print(key == key2); // true
print(key == ComponentKey.unique()); // false
```

Named ones are based on the name that it receives, so:

```dart
final key1 = ComponentKey.named('player');
final key2 = ComponentKey.named('player');
print(key1 == key2); // true
```

When named keys are used, the `findByKeyName` helper can also be used to retrieve the component.


```dart
flameGame.findByKeyName('player');
```


### Querying child components

The children that have been added to a component live in a `QueryableOrderedSet` called
Expand Down
Binary file added examples/assets/images/knight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/assets/images/mage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/assets/images/ranger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/lib/commons/ember.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:meta/meta.dart';

class Ember<T extends FlameGame> extends SpriteAnimationComponent
with HasGameRef<T> {
Ember({super.position, Vector2? size, super.priority})
Ember({super.position, Vector2? size, super.priority, super.key})
: super(
size: size ?? Vector2.all(50),
anchor: Anchor.center,
Expand Down
7 changes: 7 additions & 0 deletions examples/lib/stories/components/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:examples/stories/components/components_notifier_example.dart';
import 'package:examples/stories/components/components_notifier_provider_example.dart';
import 'package:examples/stories/components/composability_example.dart';
import 'package:examples/stories/components/debug_example.dart';
import 'package:examples/stories/components/keys_example.dart';
import 'package:examples/stories/components/look_at_example.dart';
import 'package:examples/stories/components/look_at_smooth_example.dart';
import 'package:examples/stories/components/priority_example.dart';
Expand Down Expand Up @@ -69,5 +70,11 @@ void addComponentsStories(Dashbook dashbook) {
),
codeLink: baseLink('components/time_scale_example.dart'),
info: TimeScaleExample.description,
)
..add(
'Component Keys',
(_) => const KeysExampleWidget(),
codeLink: baseLink('components/keys_example.dart'),
info: KeysExampleWidget.description,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
class ComponentsNotifierExampleWidget extends StatefulWidget {
const ComponentsNotifierExampleWidget({super.key});

static String description = '''
static const String description = '''
Showcases how the components notifier can be used between
a flame game instance and widgets.
Expand Down
118 changes: 118 additions & 0 deletions examples/lib/stories/components/keys_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'dart:async';

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';

class KeysExampleWidget extends StatefulWidget {
const KeysExampleWidget({super.key});

static const String description = '''
Showcases how component keys can be used to find components
from a flame game instance.
Use the buttons to select or deselect the heroes.
''';

@override
State<KeysExampleWidget> createState() => _KeysExampleWidgetState();
}

class _KeysExampleWidgetState extends State<KeysExampleWidget> {
late final KeysExampleGame game = KeysExampleGame();

void selectHero(ComponentKey key) {
final hero = game.findByKey<SelectableClass>(key);
if (hero != null) {
hero.selected = !hero.selected;
}
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: GameWidget(game: game),
),
Positioned(
left: 0,
top: 222,
width: 340,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: () {
selectHero(ComponentKey.named('knight'));
},
child: const Text('Knight'),
),
ElevatedButton(
onPressed: () {
selectHero(ComponentKey.named('mage'));
},
child: const Text('Mage'),
),
ElevatedButton(
onPressed: () {
selectHero(ComponentKey.named('ranger'));
},
child: const Text('Ranger'),
),
],
),
),
],
);
}
}

class KeysExampleGame extends FlameGame {
@override
FutureOr<void> onLoad() async {
await super.onLoad();

final knight = await loadSprite('knight.png');
final mage = await loadSprite('mage.png');
final ranger = await loadSprite('ranger.png');

await addAll([
SelectableClass(
key: ComponentKey.named('knight'),
sprite: knight,
size: Vector2.all(100),
position: Vector2(0, 100),
),
SelectableClass(
key: ComponentKey.named('mage'),
sprite: mage,
size: Vector2.all(100),
position: Vector2(120, 100),
),
SelectableClass(
key: ComponentKey.named('ranger'),
sprite: ranger,
size: Vector2.all(100),
position: Vector2(240, 100),
),
]);
}
}

class SelectableClass extends SpriteComponent {
SelectableClass({
super.position,
super.size,
super.key,
super.sprite,
}) : super(paint: Paint()..color = Colors.white.withOpacity(0.5));

bool _selected = false;
bool get selected => _selected;
set selected(bool value) {
_selected = value;
paint = Paint()
..color = value ? Colors.white : Colors.white.withOpacity(0.5);
}
}
15 changes: 10 additions & 5 deletions examples/lib/stories/input/keyboard_example.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:examples/commons/ember.dart';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/services.dart';
Expand All @@ -17,19 +18,23 @@ class KeyboardExample extends FlameGame with KeyboardEvents {
// Direction in which amber is moving.
final Vector2 _direction = Vector2.zero();

late final Ember _ember;

@override
Future<void> onLoad() async {
_ember = Ember(position: size / 2, size: Vector2.all(100));
add(_ember);
add(
Ember(
key: ComponentKey.named('ember'),
position: size / 2,
size: Vector2.all(100),
),
);
}

@override
void update(double dt) {
super.update(dt);
final ember = findByKeyName<Ember>('ember');
final displacement = _direction.normalized() * _speed * dt;
_ember.position.add(displacement);
ember?.position.add(displacement);
}

@override
Expand Down
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export 'src/collisions/hitboxes/screen_hitbox.dart';
export 'src/components/clip_component.dart';
export 'src/components/components_notifier.dart';
export 'src/components/core/component.dart';
export 'src/components/core/component_key.dart';
export 'src/components/core/component_set.dart';
export 'src/components/core/position_type.dart';
export 'src/components/custom_painter_component.dart';
Expand Down
7 changes: 7 additions & 0 deletions packages/flame/lib/src/components/clip_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class ClipComponent extends PositionComponent implements SizeProvider {
super.anchor,
super.children,
super.priority,
super.key,
}) : _builder = builder;

/// {@macro circle_clip_component}
Expand All @@ -36,6 +37,7 @@ class ClipComponent extends PositionComponent implements SizeProvider {
Anchor? anchor,
Iterable<Component>? children,
int? priority,
ComponentKey? key,
}) {
return ClipComponent(
builder: (size) => Circle(size / 2, size.x / 2),
Expand All @@ -46,6 +48,7 @@ class ClipComponent extends PositionComponent implements SizeProvider {
anchor: anchor,
children: children,
priority: priority,
key: key,
);
}

Expand All @@ -60,6 +63,7 @@ class ClipComponent extends PositionComponent implements SizeProvider {
Anchor? anchor,
Iterable<Component>? children,
int? priority,
ComponentKey? key,
}) {
return ClipComponent(
builder: (size) => Rectangle.fromRect(size.toRect()),
Expand All @@ -70,6 +74,7 @@ class ClipComponent extends PositionComponent implements SizeProvider {
anchor: anchor,
children: children,
priority: priority,
key: key,
);
}

Expand All @@ -85,6 +90,7 @@ class ClipComponent extends PositionComponent implements SizeProvider {
Anchor? anchor,
Iterable<Component>? children,
int? priority,
ComponentKey? key,
}) {
assert(
points.length > 2,
Expand All @@ -107,6 +113,7 @@ class ClipComponent extends PositionComponent implements SizeProvider {
anchor: anchor,
children: children,
priority: priority,
key: key,
);
}

Expand Down
33 changes: 26 additions & 7 deletions packages/flame/lib/src/components/core/component.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
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_set.dart';
import 'package:flame/src/components/core/component_tree_root.dart';
import 'package:flame/src/components/core/position_type.dart';
import 'package:flame/src/components/mixins/coordinate_transform.dart';
import 'package:flame/src/components/mixins/has_game_ref.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:flame/src/text/text_paint.dart';
import 'package:flutter/painting.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';

/// [Component]s are the basic building blocks for a [FlameGame].
///
Expand Down Expand Up @@ -71,8 +67,12 @@ import 'package:vector_math/vector_math_64.dart';
/// 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})
: _priority = priority ?? 0 {
Component({
Iterable<Component>? children,
int? priority,
ComponentKey? key,
}) : _priority = priority ?? 0,
_key = key {
if (children != null) {
addAll(children);
}
Expand Down Expand Up @@ -846,6 +846,13 @@ class Component {
_reAddChildren();
_parent!.onChildrenChanged(this, ChildrenChangeType.added);
_clearMountingBit();

if (_key != null) {
final currentGame = findGame();
if (currentGame is FlameGame) {
currentGame.registerKey(_key!, this);
}
}
}

/// Used by [_reAddChildren].
Expand Down Expand Up @@ -880,6 +887,13 @@ class Component {

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) {
Expand Down Expand Up @@ -918,6 +932,11 @@ class Component {
/// 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);

Expand Down
Loading

0 comments on commit b3efb61

Please sign in to comment.