Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add multiFactory to SpawnComponent #3440

Merged
merged 4 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
spydon marked this conversation as resolved.
Show resolved Hide resolved
),
_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
Loading