Skip to content

Commit

Permalink
fix: performance improvements on SpriteBatch APIs (#1637)
Browse files Browse the repository at this point in the history
Fix for the performance decrease mentioned in #1614
  • Loading branch information
nathanaelneveux committed Jun 4, 2022
1 parent f55b2e0 commit 4b19a1b
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 102 deletions.
19 changes: 19 additions & 0 deletions packages/flame/lib/src/cache/images.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ class Images {
_assets[name] = _ImageAsset.fromImage(image);
}

/// If the image with [name] exists in the cache that is returned, otherwise
/// the image generated by [imageGenerator] is returned.
///
/// If the [imageGenerator] is used, the resulting [Image] is stored with
/// [name] in the cache.
Future<Image> fetchOrGenerate(
String name,
Future<Image> Function() imageGenerator,
) {
return (_assets[name] ??= _ImageAsset.future(imageGenerator()))
.retrieveAsync();
}

/// Removes the image [name] from the cache.
///
/// No error is raised if the image [name] is not present in the cache.
Expand Down Expand Up @@ -126,6 +139,12 @@ class Images {
/// Whether the cache contains the specified [key] or not.
bool containsKey(String key) => _assets.containsKey(key);

String? findKeyForImage(Image image) {
return _assets.keys.firstWhere(
(k) => _assets[k]?.image?.isCloneOf(image) ?? false,
);
}

/// Waits until all currently pending image loading operations complete.
Future<void> ready() {
return Future.wait(_assets.values.map((asset) => asset.retrieveAsync()));
Expand Down
174 changes: 107 additions & 67 deletions packages/flame/lib/src/sprite_batch.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:collection';
import 'dart:math' show pi;
import 'dart:ui';

import 'package:flame/game.dart';
Expand All @@ -9,34 +10,38 @@ import 'package:flame/src/flame.dart';

extension SpriteBatchExtension on Game {
/// Utility method to load and cache the image for a [SpriteBatch] based on
/// its options
/// its options.
Future<SpriteBatch> loadSpriteBatch(
String path, {
Color defaultColor = const Color(0x00000000),
BlendMode defaultBlendMode = BlendMode.srcOver,
RSTransform? defaultTransform,
Images? imageCache,
bool useAtlas = true,
}) {
return SpriteBatch.load(
path,
defaultColor: defaultColor,
defaultBlendMode: defaultBlendMode,
defaultTransform: defaultTransform,
images: images,
images: imageCache ?? images,
useAtlas: useAtlas,
);
}
}

/// This is the scale value used in [BatchItem.matrix], we can't determine this
/// from the [BatchItem.transform], but we also don't need to do so because it
/// is already calculated inside the transform values.
const _defaultScale = 0.0;

/// A single item in a SpriteBatch.
///
/// Holds all the important information of a batch item,
/// Holds all the important information of a batch item.
class BatchItem {
BatchItem({
required this.source,
required this.transform,
this.flip = false,
required this.color,
}) : paint = Paint()..color = color,
destination = Offset.zero & source.size;

/// The source rectangle on the [SpriteBatch.atlas].
final Rect source;

Expand All @@ -56,26 +61,20 @@ class BatchItem {

/// Fallback matrix for the web.
///
/// Because `Canvas.drawAtlas` is not supported on the web we also
/// Since [Canvas.drawAtlas] is not supported on the web we also
/// build a `Matrix4` based on the [transform] and [flip] values.
final Matrix4 matrix;
late final Matrix4 matrix = Matrix4(
transform.scos, transform.ssin, 0, 0, //
-transform.ssin, transform.scos, 0, 0, //
0, 0, 0, 0, //
transform.tx, transform.ty, 0, 1, //
)
..translate(source.width / 2, source.height / 2)
..rotateY(flip ? pi : 0)
..translate(-source.width / 2, -source.height / 2);

/// Paint object used for the web.
final Paint paint;

BatchItem({
required this.source,
required this.transform,
this.flip = false,
required this.color,
}) : matrix = Matrix4(
transform.scos * (flip ? -1 : 1), transform.ssin, 0, 0, //
-transform.ssin, transform.scos, 0, 0, //
0, 0, _defaultScale, 0, //
transform.tx, transform.ty, 0, 1, //
),
paint = Paint()..color = color,
destination = Offset.zero & source.size;
}

/// The SpriteBatch API allows for rendering multiple items at once.
Expand All @@ -95,6 +94,40 @@ class BatchItem {
/// load method that you are using and each [BatchItem] will be rendered using
/// the [Canvas.drawImageRect] method instead.
class SpriteBatch {
SpriteBatch(
this.atlas, {
this.defaultColor = const Color(0x00000000),
this.defaultBlendMode = BlendMode.srcOver,
this.defaultTransform,
this.useAtlas = true,
Images? imageCache,
String? imageKey,
}) : _imageCache = imageCache,
_imageKey = imageKey;

/// Takes a path of an image, and optional arguments for the SpriteBatch.
///
/// When the [images] is omitted, the global [Flame.images] is used.
static Future<SpriteBatch> load(
String path, {
Color defaultColor = const Color(0x00000000),
BlendMode defaultBlendMode = BlendMode.srcOver,
RSTransform? defaultTransform,
Images? images,
bool useAtlas = true,
}) async {
final _images = images ?? Flame.images;
return SpriteBatch(
await _images.load(path),
defaultColor: defaultColor,
defaultTransform: defaultTransform ?? RSTransform(1, 0, 0, 0),
defaultBlendMode: defaultBlendMode,
useAtlas: useAtlas,
imageCache: _images,
imageKey: path,
);
}

/// List of all the existing batch items.
final _batchItems = <BatchItem>[];

Expand Down Expand Up @@ -129,7 +162,30 @@ class SpriteBatch {
}

/// The atlas used by the [SpriteBatch].
final Image atlas;
Image atlas;

/// The image cache used by the [SpriteBatch] to store image assets.
final Images? _imageCache;

/// When the [_imageCache] isn't specified, the global [Flame.images] is used.
Images get imageCache => _imageCache ?? Flame.images;

/// The root key use by the [SpriteBatch] to store image assets.
final String? _imageKey;

/// When the [_imageKey] isn't specified [imageKey] will return either the key
/// for the [atlas] stored in [imageCache] or a key generated from the
/// identityHashCode.
String get imageKey =>
_imageKey ??
imageCache.findKeyForImage(atlas) ??
'image[${identityHashCode(atlas)}]';

/// Whether any [BatchItem]s needs a flippable atlas.
bool _hasFlips = false;

/// The status of the atlas image loading operations.
bool _atlasReady = true;

/// The default color, used as a background color for a [BatchItem].
final Color defaultColor;
Expand All @@ -153,37 +209,18 @@ class SpriteBatch {
/// Whether to use [Canvas.drawAtlas] or not.
final bool useAtlas;

SpriteBatch(
this.atlas, {
this.defaultColor = const Color(0x00000000),
this.defaultBlendMode = BlendMode.srcOver,
this.defaultTransform,
this.useAtlas = true,
});

/// Takes a path of an image, and optional arguments for the SpriteBatch.
///
/// When the [images] is omitted, the global [Flame.images] is used.
static Future<SpriteBatch> load(
String path, {
Color defaultColor = const Color(0x00000000),
BlendMode defaultBlendMode = BlendMode.srcOver,
RSTransform? defaultTransform,
Images? images,
bool useAtlas = true,
}) async {
return SpriteBatch(
await _generateAtlas(images, path),
defaultColor: defaultColor,
defaultTransform: defaultTransform ?? RSTransform(1, 0, 0, 0),
defaultBlendMode: defaultBlendMode,
useAtlas: useAtlas,
Future<void> _makeFlippedAtlas() async {
_hasFlips = true;
_atlasReady = false;
final key = '$imageKey#with-flips';
atlas = await imageCache.fetchOrGenerate(
key,
() => _generateFlippedAtlas(atlas),
);
_atlasReady = true;
}

static Future<Image> _generateAtlas(Images? images, String path) async {
final _images = images ?? Flame.images;
final image = await _images.load(path);
Future<Image> _generateFlippedAtlas(Image image) {
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
final _emptyPaint = Paint();
Expand All @@ -192,8 +229,7 @@ class SpriteBatch {
canvas.drawImage(image, Offset(-image.width * 2, 0), _emptyPaint);

final picture = recorder.endRecording();
final atlas = picture.toImageSafe(image.width * 2, image.height);
return atlas;
return picture.toImageSafe(image.width * 2, image.height);
}

/// Add a new batch item using a RSTransform.
Expand Down Expand Up @@ -224,12 +260,16 @@ class SpriteBatch {
color: color ?? defaultColor,
);

if (flip && useAtlas && !_hasFlips) {
_makeFlippedAtlas();
}

_batchItems.add(batchItem);

_sources.add(
flip
? Rect.fromLTWH(
atlas.width - source.left - source.width,
(atlas.width * (!_atlasReady ? 2 : 1)) - source.right,
source.top,
source.width,
source.height,
Expand Down Expand Up @@ -315,7 +355,17 @@ class SpriteBatch {
}) {
paint ??= _emptyPaint;

if (!useAtlas) {
if (useAtlas && _atlasReady) {
canvas.drawAtlas(
atlas,
_transforms,
_sources,
_colors,
blendMode ?? defaultBlendMode,
cullRect,
paint,
);
} else {
for (final batchItem in _batchItems) {
paint.blendMode = blendMode ?? paint.blendMode;

Expand All @@ -331,16 +381,6 @@ class SpriteBatch {
)
..restore();
}
} else {
canvas.drawAtlas(
atlas,
_transforms,
_sources,
_colors,
blendMode ?? defaultBlendMode,
cullRect,
paint,
);
}
}
}
Binary file modified packages/flame_tiled/test/assets/4_color_sprite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4b19a1b

Please sign in to comment.