From e7142161f97538efbd14ca646c1fb343fbcdbc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Corneli=C3=9Fen?= Date: Thu, 9 Jan 2025 15:46:47 +0100 Subject: [PATCH 1/2] add multiFactory to SpawnComponent to be able to create a list of components for each spawn tick. For backwards compatibility the single component factory is still supported and wrapped internally. --- .../lib/src/components/spawn_component.dart | 63 +++++++++++++--- .../test/components/spawn_component_test.dart | 71 +++++++++++++++++++ 2 files changed, 124 insertions(+), 10 deletions(-) diff --git a/packages/flame/lib/src/components/spawn_component.dart b/packages/flame/lib/src/components/spawn_component.dart index 5aaafcc08f8..3d7c7bfbfa4 100644 --- a/packages/flame/lib/src/components/spawn_component.dart +++ b/packages/flame/lib/src/components/spawn_component.dart @@ -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 Function(int amount)? multiFactory, this.area, this.within = true, this.selfPositioning = false, @@ -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 @@ -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 Function(int amount)? multiFactory, this.area, this.within = true, this.selfPositioning = false, @@ -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 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 Function(int amount) multiFactory; /// The area where the components should be spawned. Shape? area; @@ -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, diff --git a/packages/flame/test/components/spawn_component_test.dart b/packages/flame/test/components/spawn_component_test.dart index 1874a774036..3d3631f524b 100644 --- a/packages/flame/test/components/spawn_component_test.dart +++ b/packages/flame/test/components/spawn_component_test.dart @@ -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() + .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); @@ -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() + .every((c) => c.position == Vector2.all(1000)), + isTrue, + ); + }); testWithFlameGame('Does not spawns when auto start is false', (game) async { final random = Random(0); From 60c1eb1bf3a4a6b179a78eed4f5d4f18be617f3c Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Fri, 10 Jan 2025 00:27:09 +0100 Subject: [PATCH 2/2] Update packages/flame/lib/src/components/spawn_component.dart --- packages/flame/lib/src/components/spawn_component.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flame/lib/src/components/spawn_component.dart b/packages/flame/lib/src/components/spawn_component.dart index 3d7c7bfbfa4..4efad91a04a 100644 --- a/packages/flame/lib/src/components/spawn_component.dart +++ b/packages/flame/lib/src/components/spawn_component.dart @@ -39,7 +39,7 @@ class SpawnComponent extends Component { ), assert( (factory != null) ^ (multiFactory != null), - 'You need to provide either a factory or a multiFactory not both', + 'You need to provide either a factory or a multiFactory, not both.', ), _period = period, multiFactory = multiFactory ?? _wrapFactory(factory!),