Skip to content

Commit 69ae8a4

Browse files
gnarhardspydon
authored andcommitted
feat: Use a Free List Strategy on BatchItem indexes within SpriteBatch and return index from .add()
1 parent 7ede916 commit 69ae8a4

File tree

2 files changed

+116
-90
lines changed

2 files changed

+116
-90
lines changed

packages/flame/lib/src/sprite_batch.dart

Lines changed: 103 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ extension SpriteBatchExtension on Game {
1313
/// its options.
1414
Future<SpriteBatch> loadSpriteBatch(
1515
String path, {
16-
Color? defaultColor,
17-
BlendMode? defaultBlendMode,
16+
Color defaultColor = const Color(0x00000000),
17+
BlendMode defaultBlendMode = BlendMode.srcOver,
1818
RSTransform? defaultTransform,
1919
Images? imageCache,
2020
bool useAtlas = true,
@@ -122,10 +122,10 @@ enum FlippedAtlasStatus {
122122
class SpriteBatch {
123123
SpriteBatch(
124124
this.atlas, {
125+
this.defaultColor = const Color(0x00000000),
126+
this.defaultBlendMode = BlendMode.srcOver,
125127
this.defaultTransform,
126128
this.useAtlas = true,
127-
this.defaultColor,
128-
this.defaultBlendMode,
129129
Images? imageCache,
130130
String? imageKey,
131131
}) : _imageCache = imageCache,
@@ -136,10 +136,10 @@ class SpriteBatch {
136136
/// When the [images] is omitted, the global [Flame.images] is used.
137137
static Future<SpriteBatch> load(
138138
String path, {
139+
Color defaultColor = const Color(0x00000000),
140+
BlendMode defaultBlendMode = BlendMode.srcOver,
139141
RSTransform? defaultTransform,
140142
Images? images,
141-
Color? defaultColor,
142-
BlendMode? defaultBlendMode,
143143
bool useAtlas = true,
144144
}) async {
145145
final imagesCache = images ?? Flame.images;
@@ -156,37 +156,38 @@ class SpriteBatch {
156156

157157
FlippedAtlasStatus _flippedAtlasStatus = FlippedAtlasStatus.none;
158158

159-
/// List of all the existing batch items.
160-
final _batchItems = <BatchItem>[];
159+
/// Stack of available (freed) indices using ListQueue as a stack.
160+
final Queue<int> _freeIndices = Queue<int>();
161161

162-
/// The sources to use on the [atlas].
163-
final _sources = <Rect>[];
162+
/// Returns the total number of indices that have been allocated.
163+
int get allocatedCount => _nextIndex;
164164

165-
/// The sources list shouldn't be modified directly, that is why an
166-
/// [UnmodifiableListView] is used. If you want to add sources use the
167-
/// [add] or [addTransform] method.
168-
UnmodifiableListView<Rect> get sources {
169-
return UnmodifiableListView<Rect>(_sources);
170-
}
165+
/// Returns the number of currently free indices.
166+
int get freeCount => _freeIndices.length;
171167

172-
/// The transforms that should be applied on the [_sources].
173-
final _transforms = <RSTransform>[];
168+
/// The next index to allocate if no free indices are available.
169+
int _nextIndex = 0;
174170

175-
/// The transforms list shouldn't be modified directly, that is why an
176-
/// [UnmodifiableListView] is used. If you want to add transforms use the
177-
/// [add] or [addTransform] method.
178-
UnmodifiableListView<RSTransform> get transforms {
179-
return UnmodifiableListView<RSTransform>(_transforms);
180-
}
171+
/// Sparse array of batch items, indexed by allocated indices.
172+
final Map<int, BatchItem> _batchItems = {};
173+
174+
/// Returns the number of active batch items.
175+
int get length => _batchItems.length;
181176

182-
/// The background color for the [_sources].
183-
final _colors = <Color>[];
177+
/// Returns the number of indices currently in use.
178+
int get usedCount => _nextIndex - _freeIndices.length;
179+
180+
/// Allocates a new index, reusing freed indices when possible.
181+
int _allocateIndex() {
182+
if (_freeIndices.isNotEmpty) {
183+
return _freeIndices.removeFirst();
184+
}
185+
return _nextIndex++;
186+
}
184187

185-
/// The colors list shouldn't be modified directly, that is why an
186-
/// [UnmodifiableListView] is used. If you want to add colors use the
187-
/// [add] or [addTransform] method.
188-
UnmodifiableListView<Color> get colors {
189-
return UnmodifiableListView<Color>(_colors);
188+
/// Frees an index to be reused later.
189+
void _freeIndex(int index) {
190+
_freeIndices.addFirst(index);
190191
}
191192

192193
/// The atlas used by the [SpriteBatch].
@@ -234,6 +235,12 @@ class SpriteBatch {
234235
/// Does this batch contain any operations?
235236
bool get isEmpty => _batchItems.isEmpty;
236237

238+
// Used to not create new Paint objects in [render] and
239+
// [generateFlippedAtlas].
240+
final _emptyPaint = Paint();
241+
242+
static const _defaultColor = Color(0x00000000);
243+
237244
Future<void> _makeFlippedAtlas() async {
238245
_flippedAtlasStatus = FlippedAtlasStatus.generating;
239246
final key = '$imageKey#with-flips';
@@ -255,12 +262,10 @@ class SpriteBatch {
255262
return picture.toImageSafe(image.width * 2, image.height);
256263
}
257264

258-
int get length => _sources.length;
259-
260265
/// Replace provided values of a batch item at the [index], when a parameter
261266
/// is not provided, the original value of the batch item will be used.
262267
///
263-
/// Throws an [ArgumentError] if the [index] is out of bounds.
268+
/// Throws an [ArgumentError] if the [index] doesn't exist.
264269
/// At least one of the parameters must be different from null.
265270
void replace(
266271
int index, {
@@ -273,11 +278,11 @@ class SpriteBatch {
273278
'At least one of the parameters must be different from null.',
274279
);
275280

276-
if (index < 0 || index >= length) {
277-
throw ArgumentError('Index out of bounds: $index');
281+
if (!_batchItems.containsKey(index)) {
282+
throw ArgumentError('Index does not exist: $index');
278283
}
279284

280-
final currentBatchItem = _batchItems[index];
285+
final currentBatchItem = _batchItems[index]!;
281286
final newBatchItem = BatchItem(
282287
source: source ?? currentBatchItem.source,
283288
transform: transform ?? currentBatchItem.transform,
@@ -286,10 +291,14 @@ class SpriteBatch {
286291
);
287292

288293
_batchItems[index] = newBatchItem;
294+
}
289295

290-
_sources[index] = newBatchItem.source;
291-
_transforms[index] = newBatchItem.transform;
292-
_colors[index] = color ?? _defaultColor;
296+
/// Returns the [BatchItem] at the given [index].
297+
BatchItem getBatchItem(int index) {
298+
if (!_batchItems.containsKey(index)) {
299+
throw ArgumentError('Index does not exist: $index');
300+
}
301+
return _batchItems[index]!;
293302
}
294303

295304
/// Add a new batch item using a RSTransform.
@@ -307,26 +316,15 @@ class SpriteBatch {
307316
/// cosine of the rotation so that they can be reused over multiple calls to
308317
/// this constructor, it may be more efficient to directly use this method
309318
/// instead.
310-
void addTransform({
319+
int addTransform({
311320
required Rect source,
312321
RSTransform? transform,
313322
bool flip = false,
314323
Color? color,
315324
}) {
325+
final index = _allocateIndex();
316326
final batchItem = BatchItem(
317-
source: source,
318-
transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0),
319-
flip: flip,
320-
color: color ?? defaultColor,
321-
);
322-
323-
if (flip && useAtlas && _flippedAtlasStatus.isNone) {
324-
_makeFlippedAtlas();
325-
}
326-
327-
_batchItems.add(batchItem);
328-
_sources.add(
329-
flip
327+
source: flip
330328
? Rect.fromLTWH(
331329
// The atlas is twice as wide when the flipped atlas is generated.
332330
(atlas.width * (_flippedAtlasStatus.isGenerated ? 1 : 2)) -
@@ -335,10 +333,19 @@ class SpriteBatch {
335333
source.width,
336334
source.height,
337335
)
338-
: batchItem.source,
336+
: source,
337+
transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0),
338+
flip: flip,
339+
color: color ?? defaultColor,
339340
);
340-
_transforms.add(batchItem.transform);
341-
_colors.add(color ?? _defaultColor);
341+
342+
if (flip && useAtlas && _flippedAtlasStatus.isNone) {
343+
_makeFlippedAtlas();
344+
}
345+
346+
_batchItems[index] = batchItem;
347+
348+
return index;
342349
}
343350

344351
/// Add a new batch item.
@@ -359,7 +366,7 @@ class SpriteBatch {
359366
/// multiple [RSTransform] objects,
360367
/// it may be more efficient to directly use the more direct [addTransform]
361368
/// method instead.
362-
void add({
369+
int add({
363370
required Rect source,
364371
double scale = 1.0,
365372
Vector2? anchor,
@@ -389,26 +396,31 @@ class SpriteBatch {
389396
);
390397
}
391398

392-
addTransform(
399+
return addTransform(
393400
source: source,
394401
transform: transform,
395402
flip: flip,
396403
color: color,
397404
);
398405
}
399406

407+
/// Removes a batch item at the given [index].
408+
void removeAt(int index) {
409+
if (!_batchItems.containsKey(index)) {
410+
throw ArgumentError('Index does not exist: $index');
411+
}
412+
413+
_batchItems.remove(index);
414+
_freeIndex(index);
415+
}
416+
400417
/// Clear the SpriteBatch so it can be reused.
401418
void clear() {
402-
_sources.clear();
403-
_transforms.clear();
404-
_colors.clear();
405419
_batchItems.clear();
420+
_freeIndices.clear();
421+
_nextIndex = 0;
406422
}
407423

408-
// Used to not create new Paint objects in [render] and
409-
// [generateFlippedAtlas].
410-
final _emptyPaint = Paint();
411-
412424
void render(
413425
Canvas canvas, {
414426
BlendMode? blendMode,
@@ -419,27 +431,38 @@ class SpriteBatch {
419431
return;
420432
}
421433

422-
final renderPaint = paint ?? _emptyPaint;
423-
424-
final hasNoColors = _colors.every((c) => c == _defaultColor);
425-
final actualBlendMode = blendMode ?? defaultBlendMode;
426-
if (!hasNoColors && actualBlendMode == null) {
427-
throw 'When setting any colors, a blend mode must be provided.';
428-
}
434+
paint ??= _emptyPaint;
429435

430436
if (useAtlas && !_flippedAtlasStatus.isGenerating) {
437+
final transforms = _batchItems.values
438+
.map((e) => e.transform)
439+
.toList(growable: false);
440+
final sources = _batchItems.values
441+
.map((e) => e.source)
442+
.toList(growable: false);
443+
final colors = _batchItems.values
444+
.map((e) => e.paint.color)
445+
.toList(growable: false);
446+
447+
final hasNoColors = colors.every((c) => c == _defaultColor);
448+
final actualBlendMode = blendMode ?? defaultBlendMode;
449+
if (!hasNoColors && actualBlendMode == null) {
450+
throw 'When setting any colors, a blend mode must be provided.';
451+
}
452+
431453
canvas.drawAtlas(
432454
atlas,
433-
_transforms,
434-
_sources,
435-
hasNoColors ? null : _colors,
455+
transforms,
456+
sources,
457+
hasNoColors ? null : colors,
436458
actualBlendMode,
437459
cullRect,
438-
renderPaint,
460+
paint,
439461
);
440462
} else {
441-
for (final batchItem in _batchItems) {
442-
renderPaint.blendMode = blendMode ?? renderPaint.blendMode;
463+
for (final index in _batchItems.keys) {
464+
final batchItem = _batchItems[index]!;
465+
paint.blendMode = blendMode ?? paint.blendMode;
443466

444467
canvas
445468
..save()
@@ -449,12 +472,10 @@ class SpriteBatch {
449472
atlas,
450473
batchItem.source,
451474
batchItem.destination,
452-
renderPaint,
475+
paint,
453476
)
454477
..restore();
455478
}
456479
}
457480
}
458-
459-
static const _defaultColor = Color(0x00000000);
460481
}

packages/flame/test/sprite_batch_test.dart

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ void main() {
1616
test('can add to the batch', () {
1717
final image = _MockImage();
1818
final spriteBatch = SpriteBatch(image);
19-
spriteBatch.add(source: Rect.zero);
19+
final index = spriteBatch.add(source: Rect.zero);
2020

21-
expect(spriteBatch.transforms, hasLength(1));
21+
expect(spriteBatch.getBatchItem(index), isNotNull);
2222
});
2323

2424
test('can replace the color of a batch', () {
@@ -28,8 +28,13 @@ void main() {
2828

2929
spriteBatch.replace(0, color: Colors.red);
3030

31-
expect(spriteBatch.colors, hasLength(1));
32-
expect(spriteBatch.colors.first, Colors.red);
31+
final batchItem = spriteBatch.getBatchItem(0);
32+
33+
/// Use .closeTo() to avoid floating point rounding errors.
34+
expect(batchItem.paint.color.a, closeTo(Colors.red.a, 0.001));
35+
expect(batchItem.paint.color.r, closeTo(Colors.red.r, 0.001));
36+
expect(batchItem.paint.color.g, closeTo(Colors.red.g, 0.001));
37+
expect(batchItem.paint.color.b, closeTo(Colors.red.b, 0.001));
3338
});
3439

3540
test('can replace the source of a batch', () {
@@ -38,9 +43,9 @@ void main() {
3843
spriteBatch.add(source: Rect.zero);
3944

4045
spriteBatch.replace(0, source: const Rect.fromLTWH(1, 1, 1, 1));
46+
final batchItem = spriteBatch.getBatchItem(0);
4147

42-
expect(spriteBatch.sources, hasLength(1));
43-
expect(spriteBatch.sources.first, const Rect.fromLTWH(1, 1, 1, 1));
48+
expect(batchItem.source, const Rect.fromLTWH(1, 1, 1, 1));
4449
});
4550

4651
test('can replace the transform of a batch', () {
@@ -49,10 +54,10 @@ void main() {
4954
spriteBatch.add(source: Rect.zero);
5055

5156
spriteBatch.replace(0, transform: RSTransform(1, 1, 1, 1));
57+
final batchItem = spriteBatch.getBatchItem(0);
5258

53-
expect(spriteBatch.transforms, hasLength(1));
5459
expect(
55-
spriteBatch.transforms.first,
60+
batchItem.transform,
5661
isA<RSTransform>()
5762
.having((t) => t.scos, 'scos', 1)
5863
.having((t) => t.ssin, 'ssin', 1)

0 commit comments

Comments
 (0)