Skip to content

Commit

Permalink
add multiFactory to SpawnComponent to be able to create a list of com…
Browse files Browse the repository at this point in the history
…ponents for each spawn tick. For backwards compatibility the single component factory is still supported and wrapped internally.
  • Loading branch information
pcornelissen committed Jan 9, 2025
1 parent f4a75d5 commit e714216
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 10 deletions.
63 changes: 53 additions & 10 deletions packages/flame/lib/src/components/spawn_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ import 'package:flame/math.dart';
/// [SpawnComponent.periodRange] constructor.
/// If you want to set the position of the spawned components yourself inside of
/// the [factory], set [selfPositioning] to true.
/// You can either provide a factory that returns one component or a
/// multiFactory which returns a list of components. In this case the amount
/// parameter will be increased by the number of returned components.
/// {@endtemplate}
class SpawnComponent extends Component {
/// {@macro spawn_component}
SpawnComponent({
required this.factory,
required double period,
PositionComponent Function(int amount)? factory,
List<PositionComponent> Function(int amount)? multiFactory,
this.area,
this.within = true,
this.selfPositioning = false,
Expand All @@ -33,7 +37,12 @@ class SpawnComponent extends Component {
!(selfPositioning && area != null),
"Don't set an area when you are using selfPositioning=true",
),
assert(
(factory != null) ^ (multiFactory != null),
'You need to provide either a factory or a multiFactory not both',
),
_period = period,
multiFactory = multiFactory ?? _wrapFactory(factory!),
_random = random ?? randomFallback;

/// Use this constructor if you want your components to spawn within an
Expand All @@ -42,9 +51,10 @@ class SpawnComponent extends Component {
/// spawns and [maxPeriod] will be the maximum amount of time before it
/// spawns.
SpawnComponent.periodRange({
required this.factory,
required double minPeriod,
required double maxPeriod,
PositionComponent Function(int amount)? factory,
List<PositionComponent> Function(int amount)? multiFactory,
this.area,
this.within = true,
this.selfPositioning = false,
Expand All @@ -58,13 +68,44 @@ class SpawnComponent extends Component {
),
_period = minPeriod +
(random ?? randomFallback).nextDouble() * (maxPeriod - minPeriod),
multiFactory = multiFactory ?? _wrapFactory(factory!),
_random = random ?? randomFallback;

/// The function used to create a new component to spawn.
///
/// [amount] is the amount of components that the [SpawnComponent] has spawned
/// so far.
///
/// Be aware: internally the component uses a factory that creates a list of
/// components.
/// If you have set such a factory it was wrapped to create a list. The
/// factory getter wraps it again to return the first element of the list and
/// fails when the list is empty!
PositionComponent Function(int amount) get factory => (int amount) {
final result = multiFactory.call(amount);
assert(
result.isNotEmpty,
'The factory call yielded no result, which is required when calling'
' the single result factory',
);
return result.elementAt(0);
};

set factory(PositionComponent Function(int amount) newFactory) {
multiFactory = _wrapFactory(newFactory);
}

static List<PositionComponent> Function(int amount) _wrapFactory(
PositionComponent Function(int amount) newFactory,
) {
return (int amount) => [newFactory.call(amount)];
}

/// The function used to create new components to spawn.
///
/// [amount] is the amount of components that the [SpawnComponent] has spawned
/// so far.
PositionComponent Function(int amount) factory;
List<PositionComponent> Function(int amount) multiFactory;

/// The area where the components should be spawned.
Shape? area;
Expand Down Expand Up @@ -146,16 +187,18 @@ class SpawnComponent extends Component {
period: _period,
repeat: true,
onTick: () {
final component = factory(amount);
final components = multiFactory(amount);
if (!selfPositioning) {
component.position = area!.randomPoint(
random: _random,
within: within,
);
for (final component in components) {
component.position = area!.randomPoint(
random: _random,
within: within,
);
}
}
parent?.add(component);
parent?.addAll(components);
updatePeriod();
amount++;
amount += components.length;
},
autoStart: autoStart,
tickWhenLoaded: spawnWhenLoaded,
Expand Down
71 changes: 71 additions & 0 deletions packages/flame/test/components/spawn_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,44 @@ void main() {
);
});

testWithFlameGame(
'Spawns multiple components within rectangle',
(game) async {
final random = Random(0);
final shape = Rectangle.fromCenter(
center: Vector2(100, 200),
size: Vector2.all(200),
);
final spawn = SpawnComponent(
multiFactory: (_) =>
[PositionComponent(), PositionComponent(), PositionComponent()],
period: 1,
area: shape,
random: random,
);
final world = game.world;
await world.ensureAdd(spawn);
game.update(0.5);
expect(world.children.length, 1); //1 being the spawnComponent
game.update(0.5);
game.update(0.0);
expect(world.children.length, 4); //1+3 spawned components
game.update(1.0);
game.update(0.0);
expect(world.children.length, 7); //1+2*3 spawned components

for (var i = 0; i < 1000; i++) {
game.update(random.nextDouble());
}
expect(
world.children
.query<PositionComponent>()
.every((c) => shape.containsPoint(c.position)),
isTrue,
);
},
);

testWithFlameGame('Spawns components within circle', (game) async {
final random = Random(0);
final shape = Circle(Vector2(100, 200), 100);
Expand Down Expand Up @@ -140,6 +178,39 @@ void main() {
isTrue,
);
});
testWithFlameGame('Can self position multiple components', (game) async {
final random = Random(0);
final spawn = SpawnComponent(
multiFactory: (_) => [
PositionComponent(position: Vector2.all(1000)),
PositionComponent(position: Vector2.all(1000)),
PositionComponent(position: Vector2.all(1000)),
],
period: 1,
selfPositioning: true,
random: random,
);
final world = game.world;
await world.ensureAdd(spawn);
game.update(0.5);
expect(world.children.length, 1); //1 spawned component
game.update(0.5);
game.update(0.0);
expect(world.children.length, 4); //1+3 spawned components
game.update(1.0);
game.update(0.0);
expect(world.children.length, 7); //1+2*3 spawned components

for (var i = 0; i < 1000; i++) {
game.update(random.nextDouble());
}
expect(
world.children
.query<PositionComponent>()
.every((c) => c.position == Vector2.all(1000)),
isTrue,
);
});

testWithFlameGame('Does not spawns when auto start is false', (game) async {
final random = Random(0);
Expand Down

0 comments on commit e714216

Please sign in to comment.