diff --git a/packages/flame/lib/src/sprite_batch.dart b/packages/flame/lib/src/sprite_batch.dart index a543112b1eb..796abb9123c 100644 --- a/packages/flame/lib/src/sprite_batch.dart +++ b/packages/flame/lib/src/sprite_batch.dart @@ -13,8 +13,8 @@ extension SpriteBatchExtension on Game { /// its options. Future loadSpriteBatch( String path, { - Color? defaultColor, - BlendMode? defaultBlendMode, + Color defaultColor = const Color(0x00000000), + BlendMode defaultBlendMode = BlendMode.srcOver, RSTransform? defaultTransform, Images? imageCache, bool useAtlas = true, @@ -37,9 +37,9 @@ class BatchItem { BatchItem({ required this.source, required this.transform, - Color? color, + this.color = const Color(0x00000000), this.flip = false, - }) : paint = Paint()..color = color ?? const Color(0x00000000), + }) : paint = Paint()..color = color, destination = Offset.zero & source.size; /// The source rectangle on the [SpriteBatch.atlas]. @@ -85,6 +85,9 @@ class BatchItem { /// Paint object used for the web. final Paint paint; + + /// The color of the batch item. + final Color color; } @internal @@ -124,7 +127,7 @@ class SpriteBatch { this.atlas, { this.defaultTransform, this.useAtlas = true, - this.defaultColor, + this.defaultColor = const Color(0x00000000), this.defaultBlendMode, Images? imageCache, String? imageKey, @@ -146,7 +149,7 @@ class SpriteBatch { return SpriteBatch( await imagesCache.load(path), defaultTransform: defaultTransform ?? RSTransform(1, 0, 0, 0), - defaultColor: defaultColor, + defaultColor: defaultColor ?? const Color(0x00000000), defaultBlendMode: defaultBlendMode, useAtlas: useAtlas, imageCache: imagesCache, @@ -156,37 +159,38 @@ class SpriteBatch { FlippedAtlasStatus _flippedAtlasStatus = FlippedAtlasStatus.none; - /// List of all the existing batch items. - final _batchItems = []; + /// Stack of available (freed) indices using ListQueue as a stack. + final Queue _freeIndices = Queue(); - /// The sources to use on the [atlas]. - final _sources = []; + /// Returns the total number of indices that have been allocated. + int get allocatedCount => _nextIndex; - /// The sources list shouldn't be modified directly, that is why an - /// [UnmodifiableListView] is used. If you want to add sources use the - /// [add] or [addTransform] method. - UnmodifiableListView get sources { - return UnmodifiableListView(_sources); - } + /// Returns the number of currently free indices. + int get freeCount => _freeIndices.length; - /// The transforms that should be applied on the [_sources]. - final _transforms = []; + /// The next index to allocate if no free indices are available. + int _nextIndex = 0; - /// The transforms list shouldn't be modified directly, that is why an - /// [UnmodifiableListView] is used. If you want to add transforms use the - /// [add] or [addTransform] method. - UnmodifiableListView get transforms { - return UnmodifiableListView(_transforms); - } + /// Sparse array of batch items, indexed by allocated indices. + final Map _batchItems = {}; - /// The background color for the [_sources]. - final _colors = []; + /// Returns the number of active batch items. + int get length => _batchItems.length; - /// The colors list shouldn't be modified directly, that is why an - /// [UnmodifiableListView] is used. If you want to add colors use the - /// [add] or [addTransform] method. - UnmodifiableListView get colors { - return UnmodifiableListView(_colors); + /// Returns the number of indices currently in use. + int get usedCount => _nextIndex - _freeIndices.length; + + /// Allocates a new index, reusing freed indices when possible. + int _allocateIndex() { + if (_freeIndices.isNotEmpty) { + return _freeIndices.removeFirst(); + } + return _nextIndex++; + } + + /// Frees an index to be reused later. + void _freeIndex(int index) { + _freeIndices.addFirst(index); } /// The atlas used by the [SpriteBatch]. @@ -210,7 +214,7 @@ class SpriteBatch { 'image[${identityHashCode(atlas)}]'; /// The default color, used as a background color for a [BatchItem]. - final Color? defaultColor; + final Color defaultColor; /// The default transform, used when a transform was not supplied for a /// [BatchItem]. @@ -234,6 +238,10 @@ class SpriteBatch { /// Does this batch contain any operations? bool get isEmpty => _batchItems.isEmpty; + // Used to not create new Paint objects in [render] and + // [generateFlippedAtlas]. + final _emptyPaint = Paint(); + Future _makeFlippedAtlas() async { _flippedAtlasStatus = FlippedAtlasStatus.generating; final key = '$imageKey#with-flips'; @@ -255,12 +263,10 @@ class SpriteBatch { return picture.toImageSafe(image.width * 2, image.height); } - int get length => _sources.length; - /// Replace provided values of a batch item at the [index], when a parameter /// is not provided, the original value of the batch item will be used. /// - /// Throws an [ArgumentError] if the [index] is out of bounds. + /// Throws an [ArgumentError] if the [index] doesn't exist. /// At least one of the parameters must be different from null. void replace( int index, { @@ -273,11 +279,11 @@ class SpriteBatch { 'At least one of the parameters must be different from null.', ); - if (index < 0 || index >= length) { - throw ArgumentError('Index out of bounds: $index'); + if (!_batchItems.containsKey(index)) { + throw ArgumentError('Index does not exist: $index'); } - final currentBatchItem = _batchItems[index]; + final currentBatchItem = _batchItems[index]!; final newBatchItem = BatchItem( source: source ?? currentBatchItem.source, transform: transform ?? currentBatchItem.transform, @@ -286,10 +292,14 @@ class SpriteBatch { ); _batchItems[index] = newBatchItem; + } - _sources[index] = newBatchItem.source; - _transforms[index] = newBatchItem.transform; - _colors[index] = color ?? _defaultColor; + /// Returns the [BatchItem] at the given [index]. + BatchItem getBatchItem(int index) { + if (!_batchItems.containsKey(index)) { + throw ArgumentError('Index does not exist: $index'); + } + return _batchItems[index]!; } /// Add a new batch item using a RSTransform. @@ -307,26 +317,15 @@ class SpriteBatch { /// cosine of the rotation so that they can be reused over multiple calls to /// this constructor, it may be more efficient to directly use this method /// instead. - void addTransform({ + int addTransform({ required Rect source, RSTransform? transform, bool flip = false, Color? color, }) { + final index = _allocateIndex(); final batchItem = BatchItem( - source: source, - transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0), - flip: flip, - color: color ?? defaultColor, - ); - - if (flip && useAtlas && _flippedAtlasStatus.isNone) { - _makeFlippedAtlas(); - } - - _batchItems.add(batchItem); - _sources.add( - flip + source: flip ? Rect.fromLTWH( // The atlas is twice as wide when the flipped atlas is generated. (atlas.width * (_flippedAtlasStatus.isGenerated ? 1 : 2)) - @@ -335,10 +334,19 @@ class SpriteBatch { source.width, source.height, ) - : batchItem.source, + : source, + transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0), + flip: flip, + color: color ?? defaultColor, ); - _transforms.add(batchItem.transform); - _colors.add(color ?? _defaultColor); + + if (flip && useAtlas && _flippedAtlasStatus.isNone) { + _makeFlippedAtlas(); + } + + _batchItems[index] = batchItem; + + return index; } /// Add a new batch item. @@ -359,7 +367,7 @@ class SpriteBatch { /// multiple [RSTransform] objects, /// it may be more efficient to directly use the more direct [addTransform] /// method instead. - void add({ + int add({ required Rect source, double scale = 1.0, Vector2? anchor, @@ -389,7 +397,7 @@ class SpriteBatch { ); } - addTransform( + return addTransform( source: source, transform: transform, flip: flip, @@ -397,18 +405,23 @@ class SpriteBatch { ); } + /// Removes a batch item at the given [index]. + void removeAt(int index) { + if (!_batchItems.containsKey(index)) { + throw ArgumentError('Index does not exist: $index'); + } + + _batchItems.remove(index); + _freeIndex(index); + } + /// Clear the SpriteBatch so it can be reused. void clear() { - _sources.clear(); - _transforms.clear(); - _colors.clear(); _batchItems.clear(); + _freeIndices.clear(); + _nextIndex = 0; } - // Used to not create new Paint objects in [render] and - // [generateFlippedAtlas]. - final _emptyPaint = Paint(); - void render( Canvas canvas, { BlendMode? blendMode, @@ -419,27 +432,38 @@ class SpriteBatch { return; } - final renderPaint = paint ?? _emptyPaint; - - final hasNoColors = _colors.every((c) => c == _defaultColor); - final actualBlendMode = blendMode ?? defaultBlendMode; - if (!hasNoColors && actualBlendMode == null) { - throw 'When setting any colors, a blend mode must be provided.'; - } + paint ??= _emptyPaint; if (useAtlas && !_flippedAtlasStatus.isGenerating) { + final transforms = _batchItems.values + .map((e) => e.transform) + .toList(growable: false); + final sources = _batchItems.values + .map((e) => e.source) + .toList(growable: false); + final colors = _batchItems.values + .map((e) => e.color) + .toList(growable: false); + + final hasNoColors = colors.every((c) => c == defaultColor); + final actualBlendMode = blendMode ?? defaultBlendMode; + if (!hasNoColors && actualBlendMode == null) { + throw 'When setting any colors, a blend mode must be provided.'; + } + canvas.drawAtlas( atlas, - _transforms, - _sources, - hasNoColors ? null : _colors, + transforms, + sources, + hasNoColors ? null : colors, actualBlendMode, cullRect, - renderPaint, + paint, ); } else { - for (final batchItem in _batchItems) { - renderPaint.blendMode = blendMode ?? renderPaint.blendMode; + for (final index in _batchItems.keys) { + final batchItem = _batchItems[index]!; + paint.blendMode = blendMode ?? paint.blendMode; canvas ..save() @@ -449,12 +473,10 @@ class SpriteBatch { atlas, batchItem.source, batchItem.destination, - renderPaint, + paint, ) ..restore(); } } } - - static const _defaultColor = Color(0x00000000); } diff --git a/packages/flame/test/sprite_batch_test.dart b/packages/flame/test/sprite_batch_test.dart index 84ec3ee606f..0910bd16208 100644 --- a/packages/flame/test/sprite_batch_test.dart +++ b/packages/flame/test/sprite_batch_test.dart @@ -16,9 +16,9 @@ void main() { test('can add to the batch', () { final image = _MockImage(); final spriteBatch = SpriteBatch(image); - spriteBatch.add(source: Rect.zero); + final index = spriteBatch.add(source: Rect.zero); - expect(spriteBatch.transforms, hasLength(1)); + expect(spriteBatch.getBatchItem(index), isNotNull); }); test('can replace the color of a batch', () { @@ -28,8 +28,13 @@ void main() { spriteBatch.replace(0, color: Colors.red); - expect(spriteBatch.colors, hasLength(1)); - expect(spriteBatch.colors.first, Colors.red); + final batchItem = spriteBatch.getBatchItem(0); + + /// Use .closeTo() to avoid floating point rounding errors. + expect(batchItem.paint.color.a, closeTo(Colors.red.a, 0.001)); + expect(batchItem.paint.color.r, closeTo(Colors.red.r, 0.001)); + expect(batchItem.paint.color.g, closeTo(Colors.red.g, 0.001)); + expect(batchItem.paint.color.b, closeTo(Colors.red.b, 0.001)); }); test('can replace the source of a batch', () { @@ -38,9 +43,9 @@ void main() { spriteBatch.add(source: Rect.zero); spriteBatch.replace(0, source: const Rect.fromLTWH(1, 1, 1, 1)); + final batchItem = spriteBatch.getBatchItem(0); - expect(spriteBatch.sources, hasLength(1)); - expect(spriteBatch.sources.first, const Rect.fromLTWH(1, 1, 1, 1)); + expect(batchItem.source, const Rect.fromLTWH(1, 1, 1, 1)); }); test('can replace the transform of a batch', () { @@ -49,10 +54,10 @@ void main() { spriteBatch.add(source: Rect.zero); spriteBatch.replace(0, transform: RSTransform(1, 1, 1, 1)); + final batchItem = spriteBatch.getBatchItem(0); - expect(spriteBatch.transforms, hasLength(1)); expect( - spriteBatch.transforms.first, + batchItem.transform, isA() .having((t) => t.scos, 'scos', 1) .having((t) => t.ssin, 'ssin', 1)