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

Added fallback support for the web on the SpriteBatch API #612

Merged
merged 23 commits into from
Jan 17, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bb12ff5
Added fallback support for the web on the `SpriteBatch` API
wolfenrain Jan 6, 2021
383ef7d
flutter format
wolfenrain Jan 6, 2021
6525915
Removed unnecessary import
wolfenrain Jan 6, 2021
d5239af
Removed unused Paint
wolfenrain Jan 6, 2021
7f7fcb1
Added a short comment on the Matrix4 code
wolfenrain Jan 6, 2021
8dfba2c
Code cleanup
wolfenrain Jan 6, 2021
318e49f
Updated the add and addTransform code docs
wolfenrain Jan 6, 2021
bb4bc32
Whitespace issues
wolfenrain Jan 6, 2021
a93082a
Refactored the SpriteBatch class
wolfenrain Jan 6, 2021
49508ae
Merge branch 'master' into fallback-support-for-the-spritebatch-api
wolfenrain Jan 6, 2021
370aa3c
Added util on the game class
wolfenrain Jan 7, 2021
7763d35
Merge branch 'fallback-support-for-the-spritebatch-api' of github.com…
wolfenrain Jan 7, 2021
b6caebc
Forgot to pass the cache
wolfenrain Jan 7, 2021
75996b9
Merge branch 'master' of github.com:flame-engine/flame into fallback-…
wolfenrain Jan 7, 2021
7b3fc67
Moved load method to custom extension
wolfenrain Jan 7, 2021
131db29
Removed unused import
wolfenrain Jan 7, 2021
afa3486
Merge branch 'master' into fallback-support-for-the-spritebatch-api
wolfenrain Jan 11, 2021
e45eef7
Updated the SpriteBatch API to allow for read only lists.
wolfenrain Jan 13, 2021
753565e
SpriteBatch uses Vector2 now
wolfenrain Jan 13, 2021
1c9f553
Updated example
wolfenrain Jan 13, 2021
ae77a83
Fixed formatting
wolfenrain Jan 14, 2021
1421625
Switched to for in syntax
wolfenrain Jan 15, 2021
cece274
Merge branch 'master' into fallback-support-for-the-spritebatch-api
wolfenrain Jan 17, 2021
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- Adding shortcut for loading Sprites and SpriteAnimation from the global cache
- Adding loading methods for the different `ParallaxComponent` parts and refactor how the delta velocity works
- Add tests for `Timer` and fix a bug where `progress` was not reported correctly
- Refactored the `SpriteBatch` class to be more elegant.
- Added fallback support for the web on the `SpriteBatch` class
- Added missing documentation on the `SpriteBatch` class
- Added an utility method to load a `SpriteBatch` on the `Game` class

## 1.0.0-rc5
- Option for overlays to be already visible on the GameWidget
Expand Down
8 changes: 4 additions & 4 deletions doc/examples/sprite_batch/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ class MyGame extends BaseGame {

@override
Future<void> onLoad() async {
spriteBatch = await SpriteBatch.withAsset('boom3.png');
spriteBatch = await SpriteBatch.load('boom3.png');

spriteBatch.add(
rect: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128),
source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128),
offset: const Offset(200, 200),
color: Colors.greenAccent,
scale: 2,
Expand All @@ -32,7 +32,7 @@ class MyGame extends BaseGame {
);

spriteBatch.addTransform(
rect: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128),
source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128),
color: Colors.redAccent,
);

Expand All @@ -44,7 +44,7 @@ class MyGame extends BaseGame {
final x = r.nextInt(size.x.toInt()).toDouble();
final y = r.nextInt(size.y ~/ 2).toDouble() + size.y / 2.0;
spriteBatch.add(
rect: Rect.fromLTWH(sx, sy, 128, 128),
source: Rect.fromLTWH(sx, sy, 128, 128),
offset: Offset(x - 64, y - 64),
);
}
Expand Down
275 changes: 237 additions & 38 deletions lib/sprite_batch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,244 @@ import 'dart:ui';

import 'package:flutter/foundation.dart';

import 'assets/images.dart';
import 'extensions/vector2.dart';
import 'flame.dart';
import 'game/game.dart';

/// sprite atlas with an image and a set of rects and transforms
extension SpriteBatchExtension on Game {
/// Utility method to load and cache the image for a [SpriteBatch] based on its options
Future<SpriteBatch> loadSpriteBatch(
String path, {
Color defaultColor = const Color(0x00000000),
BlendMode defaultBlendMode = BlendMode.srcOver,
RSTransform defaultTransform,
}) {
return SpriteBatch.load(
path,
defaultColor: defaultColor,
defaultBlendMode: defaultBlendMode,
defaultTransform: defaultTransform,
images: images,
);
}
}

/// A single item in a SpriteBatch.
///
/// Holds all the important information of a batch item,
///
/// Web currently does not support `Canvas.drawAtlas`, so a BatchItem will
/// automatically calculate a transform matrix based on the [transform] value, to be
/// used when rendering on the web. It will initialize a [destination] object
/// and a [paint] object.
class BatchItem {
/// The source rectangle on the [SpriteBatch.atlas].
final Rect source;

/// The destination rectangle for the Canvas.
///
/// It will be transformed by [matrix].
final Rect destination;

/// The transform values for this batch item.
final RSTransform transform;

/// The background color for this batch item.
final Color color;

/// Fallback matrix for the web.
///
/// Because `Canvas.drawAtlas` is not supported on the web we also
/// build a `Matrix4` based on the [transform] values.
final Matrix4 matrix;

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

BatchItem({
@required this.source,
@required this.transform,
@required this.color,
}) : assert(source != null),
assert(transform != null),
assert(color != null),
matrix = Matrix4(
wolfenrain marked this conversation as resolved.
Show resolved Hide resolved
transform.scos,
transform.ssin,
0,
0,
-transform.ssin,
transform.scos,
0,
0,
0,
0,
0, // <-- this is the scale value, we can't determine this from a RSTransform,
// but we also don't need to do s because it is already calculated
// inside the transform values.
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.
///
/// This class allows for optimization when you want to draw many parts of an
/// image onto the canvas. It is more efficient than using multiple calls to [drawImageRect]
/// and provides more functionality by allowing each [BatchItem] to have their own transform
/// rotation and color.
///
/// By collecting all the necessary transforms on a single image and sending those transforms
/// in a single batch to the GPU, we can render multiple parts of a single image at once.
///
/// **Note**: Currently web does not support `Canvas.drawAtlas`, which SpriteBatch uses under
/// the hood, instead it will render each [BatchItem] using `Canvas.drawImageRect`, so there
/// might be a performance hit on web when working with many batch items.
class SpriteBatch {
/// List of all the existing batch items.
final _batchItems = <BatchItem>[];

/// The sources to use on the [atlas].
final _sources = <Rect>[];

/// The transforms that should be applied on the [_sources].
final _transforms = <RSTransform>[];
wolfenrain marked this conversation as resolved.
Show resolved Hide resolved

/// The background color for the [_sources].
final _colors = <Color>[];

/// The atlas used by the [SpriteBatch].
final Image atlas;
List<Rect> rects = [];
List<RSTransform> transforms = [];
List<Color> colors = [];

static const defaultBlendMode = BlendMode.srcOver;
static const defaultColor = const Color(0x00000000); // transparent
static final defaultPaint = Paint();
static final defaultTransform = RSTransform(1, 0, 0, 0);
/// The default color, used as a background color for a [BatchItem].
final Color defaultColor;

SpriteBatch(this.atlas);
/// The default transform, used when a transform was not supplied for a [BatchItem].
final RSTransform defaultTransform;

static Future<SpriteBatch> withAsset(String imageName) async {
return SpriteBatch(await Flame.images.load(imageName));
}
/// The default blend mode, used for blending a batch item.
final BlendMode defaultBlendMode;

/// The width of the [atlas].
int get width => atlas.width;

/// The height of the [atlas].
int get height => atlas.height;

/// The size of the [atlas].
Vector2 get size => Vector2Extension.fromInts(width, height);

SpriteBatch(
this.atlas, {
this.defaultColor = const Color(0x00000000),
this.defaultBlendMode = BlendMode.srcOver,
this.defaultTransform,
}) : assert(atlas != null),
assert(defaultColor != null);

/// 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,
}) async {
final _images = images ?? Flame.images;
return SpriteBatch(
await _images.load(path),
defaultColor: defaultColor,
defaultTransform: defaultTransform ?? RSTransform(1, 0, 0, 0),
defaultBlendMode: defaultBlendMode,
);
}

/// Add a new batch item using a RSTransform.
///
/// The [source] parameter is the source location on the [atlas]. You can position it
/// on the canvas using the [offset] parameter.
///
/// The [color] paramater allows you to render a color behind the batch item, as a background color.
///
/// The [add] method may be a simpler way to add a batch item to the batch. However,
/// if there is a way to factor out the computations of the sine and 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({
@required Rect rect,
@required Rect source,
RSTransform transform,
Color color,
}) {
rects.add(rect);
transforms.add(transform ?? defaultTransform);
colors.add(color ?? defaultColor);
final batchItem = BatchItem(
source: source,
transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0),
color: color ?? defaultColor,
);

_batchItems.add(batchItem);

_sources.add(batchItem.source);
_transforms.add(batchItem.transform);
_colors.add(batchItem.color);
}

/// Add a new batch item.
///
/// The [source] parameter is the source location on the [atlas]. You can position it
/// on the canvas using the [offset] parameter.
///
/// You can transform the sprite from its [offset] using [scale], [rotation] and [anchor].
///
/// The [color] paramater allows you to render a color behind the batch item, as a background color.
///
/// This method creates a new [RSTransform] based on the given transform arguments. If many [RSTransform] objects are being
/// created and there is a way to factor out the computations of the sine and cosine of the rotation
/// (which are computed each time this method is called) and reuse them over multiple [RSTransform] objects,
/// it may be more efficient to directly use the more direct [addTransform] method instead.
void add({
@required Rect rect,
@required Rect source,
double scale = 1.0,
Offset anchor = Offset.zero,
double rotation = 0,
Offset offset = Offset.zero,
Color color,
}) {
final transform = RSTransform.fromComponents(
scale: scale,
anchorX: anchor.dx,
anchorY: anchor.dy,
rotation: rotation,
translateX: offset.dx,
translateY: offset.dy,
);
addTransform(rect: rect, transform: transform, color: color);
RSTransform transform;

// If any of the transform arguments is different from the defaults,
// then we create one. This is to prevent unnecessary computations
// of the sine and cosine of the rotation.
if (scale != 1.0 ||
wolfenrain marked this conversation as resolved.
Show resolved Hide resolved
anchor != Offset.zero ||
rotation != 0 ||
offset != Offset.zero) {
transform = RSTransform.fromComponents(
scale: scale,
anchorX: anchor.dx,
anchorY: anchor.dy,
rotation: rotation,
translateX: offset.dx,
translateY: offset.dy,
);
}

addTransform(source: source, transform: transform, color: color);
}

/// Clear the SpriteBatch so it can be reused.
void clear() {
rects.clear();
transforms.clear();
colors.clear();
_sources.clear();
_transforms.clear();
_colors.clear();
_batchItems.clear();
}

void render(
Expand All @@ -70,14 +248,35 @@ class SpriteBatch {
Rect cullRect,
Paint paint,
}) {
canvas.drawAtlas(
atlas,
transforms,
rects,
colors,
blendMode ?? defaultBlendMode,
cullRect,
paint ?? defaultPaint,
);
paint ??= Paint();

if (kIsWeb) {
for (var i = 0; i < _batchItems.length; i++) {
final batchItem = _batchItems[i];
wolfenrain marked this conversation as resolved.
Show resolved Hide resolved
paint..blendMode = blendMode ?? paint.blendMode ?? defaultBlendMode;

canvas
..save()
..transform(batchItem.matrix.storage)
..drawRect(batchItem.destination, batchItem.paint)
..drawImageRect(
atlas,
batchItem.source,
batchItem.destination,
paint,
)
..restore();
}
} else {
canvas.drawAtlas(
atlas,
_transforms,
_sources,
_colors,
blendMode ?? defaultBlendMode,
cullRect,
paint,
);
}
}
}