Skip to content

Commit

Permalink
feat: Implement Snapshot mixin on PositionComponent (#2695)
Browse files Browse the repository at this point in the history
This implements a Snapshot mixin on PositionComponent that allows:

    A component to be rendered once then cached (for performance)
    and/or a Picture or Image snapshot to be taken at any time

Internally the snapshot is cached and redrawn as a Picture. After testing with both Image and Picture I found that Image had no performance improvement, and the rendering quality was lower.

A helper method snapshotToImage was added to allow the user better control of generating an Image from the internal Picture snapshot if that is desired.

See the documentation in this PR for more details.
  • Loading branch information
projectitis authored Sep 12, 2023
1 parent 820ece1 commit c1ee24a
Show file tree
Hide file tree
Showing 8 changed files with 515 additions and 5 deletions.
162 changes: 158 additions & 4 deletions doc/flame/rendering/layers.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
# Layers
# Layers and Snapshots

Layers and snapshots share some common features, including the ability to pre-render and cache
objects for improved performance. However, they also have unique features which make them better
suited for different use-cases.

`Snapshot` is a mixin that can be added to any `PositionComponent`. Use this for:

- Mixing in to existing game objects (that are `PositionComponents`).
- Caching game objects, such as sprites, that are complex to render.
- Drawing the same object many times without rendering it each time.
- Capturing an image snapshot to save as a screenshot (for example).

`Layer` is a class. Use or extend this class for:

- Structuring your game with logical layers (e.g. UI, foreground, main, background).
- Grouping objects to form a complex scene, and then caching it (e.g. a background layer).
- Processor support. Layers allow user-defined processors to run pre- and post- render.


## Layers

Layers allow you to group rendering by context, as well as allow you to pre-render things. This
enables, for example, rendering parts of your game that don't change much in memory, like a
Expand All @@ -11,7 +31,7 @@ There are two types of layers on Flame:
- `PreRenderedLayer`: For things that are static.


## DynamicLayer
### DynamicLayer

Dynamic layers are layers that are rendered every time that they are drawn on the canvas. As the
name suggests, it is meant for dynamic content and is most useful for grouping rendering of objects
Expand Down Expand Up @@ -49,7 +69,7 @@ class MyGame extends Game {
```


## PreRenderedLayer
### PreRenderedLayer

Pre-rendered layers are rendered only once, cached in memory and then just
replicated on the game canvas afterwards. They are useful for caching content that doesn't change
Expand Down Expand Up @@ -84,7 +104,7 @@ class MyGame extends Game {
```


## Layer Processors
### Layer Processors

Flame also provides a way to add processors on your layer, which are ways to add effects on the
entire layer. At the moment, out of the box, only the `ShadowProcessor` is available, this processor
Expand Down Expand Up @@ -112,3 +132,137 @@ Custom processors can be created by extending the `LayerProcessor` class.

You can check a working example of layers
[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/layers_example.dart).


## Snapshots

Snapshots are an alternative to layers. The `Snapshot` mixin can be applied to any `PositionComponent`.

```dart
class SnapshotComponent extends PositionComponent with Snapshot {}
class MyGame extends FlameGame {
late final SnapshotComponent root;
@override
Future<void> onLoad() async {
// Add a snapshot component.
root = SnapshotComponent();
add(root);
}
}
```


### Render as a snapshot

Setting `renderSnapshot` to `true` (the default) on a snapshot-enabled component behaves similarly
to a `PreRenderedLayer`. The component is rendered only once, cached in memory and then just
replicated on the game canvas afterwards. They are useful for caching content that doesn't change
during the game, like a background for example.

```dart
class SnapshotComponent extends PositionComponent with Snapshot {}
class MyGame extends FlameGame {
late final SnapshotComponent root;
late final SpriteComponent background1;
late final SpriteComponent background2;
@override
Future<void> onLoad() async {
// Add a snapshot component.
root = SnapshotComponent();
add(root);
// Add some children.
final background1Sprite = Sprite(await images.load('background1.png'));
background1 = SpriteComponent(sprite: background1Sprite);
root.add(background1);
final background2Sprite = Sprite(await images.load('background2.png'));
background2 = SpriteComponent(sprite: background2Sprite);
root.add(background2);
// root will now render once (itself and all its children) and then cache
// the result. On subsequent render calls, root itself, nor any of its
// children, will be rendered. The snapshot will be used instead for
// improved performance.
}
}
```


#### Regenerating a snapshot

A snapshot-enabled component will generate a snapshot of its entire tree, including its children.
If any of the children change (for example, their position changes, or they are animated), call
`takeSnapshot` to update the cached snapshot. If they are changing very frequently, it's best not
to use a `Snapshot` because there will be no performance benefit.

A component rendering a snapshot can still be transformed without incurring any performance cost.
Once a snapshot has been taken, the component may still be scaled, moved and rotated. However, if
the content of the component changes (what it is rendering) then the snapshot must be regenerated
by calling `takeSnapshot`.


### Taking a snapshot

A snapshot-enabled component can be used to generate a snapshot at any time, even if
`renderSnapshot` is set to false. This is useful for taking screen-grabs or any other purpose when
it may be useful to have a static snapshot of all or part of your game.

A snapshot is always generated with no transform applied - i.e. as if the snapshot-enabled
component is at position (0,0) and has no scale or rotation applied.

A snapshot is saved as a `Picture`, but it can be converted to an `Image` using `snapshotToImage`.

```dart
class SnapshotComponent extends PositionComponent with Snapshot {}
class MyGame extends FlameGame {
late final SnapshotComponent root;
@override
Future<void> onLoad() async {
// Add a snapshot component, but don't use its render mode.
root = SnapshotComponent()..renderSnapshot = false;
add(root);
// Other code omitted.
}
// Call something like this to take an image snapshot at any time.
void takeSnapshot() {
root.takeSnapshot();
final image = root.snapshotToImage(200, 200);
}
}
```


### Snapshots that are cropped or off-center

Sometimes your snapshot `Image` may appear cropped, or not in the position that is expected.

This is because the contents of a `Picture` can be positioned anywhere with respect to the origin,
but when it is converted to an `Image`, the image always starts from `0,0`. This means that
anything with a -ve position will be cropped.

The best way to deal with this is to ensure that your `Snapshot` component is always at position
`0,0` with respect to your game and you never move it. This means that the image will usually
contain what you expect it to.

However, this is not always possible. To move (or rotate, or scale etc) the snapshot before
converting it to an image, pass a transformation matrix to `snapshotToImage`.

```dart
// Call something like this to take an image snapshot at any time.
void takeSnapshot() {
// Prepare a matrix to move the snapshot by 200,50.
final matrix = Matrix4.identity()..translate(200.0,50.0);
root.takeSnapshot();
final image = root.snapshotToImage(200, 200, transform: matrix);
}
```
2 changes: 1 addition & 1 deletion doc/flame/rendering/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Colors and Palette <palette.md>
Decorators <decorators.md>
Images, Sprites and Animations <images.md>
Layers <layers.md>
Layers and Snapshots <layers.md>
Particles <particles.md>
Text Rendering <text_rendering.md>
```
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export 'src/components/mixins/keyboard_handler.dart';
export 'src/components/mixins/notifier.dart';
export 'src/components/mixins/parent_is_a.dart';
export 'src/components/mixins/single_child_particle.dart';
export 'src/components/mixins/snapshot.dart';
export 'src/components/mixins/tappable.dart';
export 'src/components/nine_tile_box_component.dart';
export 'src/components/parallax_component.dart';
Expand Down
88 changes: 88 additions & 0 deletions packages/flame/lib/src/components/mixins/snapshot.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'dart:ui';

import 'package:flame/components.dart';

/// A mixin that enables caching a component and all its children. If
/// [renderSnapshot] is set to `true`, the component and its children will be
/// rendered to a cache. Subsequent renders use the cache, dramatically
/// improving performance. This is only effective if the component and its
/// children do not change - i.e. they are not animated and they do not move
/// around relative to each other.
///
/// The [takeSnapshot] and [snapshotAsImage] methods can also be used to take
/// one-off snapshots for screen-grabs or other purposes.
mixin Snapshot on PositionComponent {
bool _renderSnapshot = true;
Picture? _picture;

/// If [renderSnapshot] is `true` then this component and all its children
/// will be rendered once and cached. If [renderSnapshot] is `false`
/// then this component will render normally.
bool get renderSnapshot => _renderSnapshot;
set renderSnapshot(bool value) {
if (_renderSnapshot != value) {
_renderSnapshot = value;
if (_renderSnapshot == true) {
_picture = null;
}
}
}

/// Check if a snapshot exists.
bool get hasSnapshot => _picture != null;

/// Grab the current snapshot. Check it exists first using [hasSnapshot].
Picture get snapshot {
assert(_picture != null, 'No snapshot has been taken');
return _picture!;
}

/// Convert the snapshot to an image with the given [width] and [height].
/// Use [transform] to position the snapshot in the image, or to apply other
/// transforms before the image is generated.
Image snapshotAsImage(int width, int height, {Matrix4? transform}) {
assert(_picture != null, 'No snapshot has been taken');
if (transform == null) {
return _picture!.toImageSync(width, height);
} else {
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
canvas.transform(transform.storage);
canvas.drawPicture(_picture!);
final picture = recorder.endRecording();
return picture.toImageSync(width, height);
}
}

/// Immediately take a snapshot and return it. If [renderSnapshot] is true
/// then the snapshot is also used for rendering. A snapshot is always taken
/// with no transformations (i.e. as if the Snapshot component is at position
/// (0, 0) and has no scale or rotation applied).
Picture takeSnapshot() {
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
final matrix = transformMatrix.clone();
matrix.invert();
canvas.transform(matrix.storage);
super.renderTree(canvas);
_picture = recorder.endRecording();
return _picture!;
}

@override
void renderTree(Canvas canvas) {
if (renderSnapshot) {
if (_picture == null) {
takeSnapshot();
}
canvas.save();
canvas.transform(
transformMatrix.storage,
);
canvas.drawPicture(_picture!);
canvas.restore();
} else {
super.renderTree(canvas);
}
}
}
Binary file added packages/flame/test/_goldens/snapshot_test_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/flame/test/_goldens/snapshot_test_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/flame/test/_goldens/snapshot_test_3.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 c1ee24a

Please sign in to comment.