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

feat: Implement Snapshot mixin on PositionComponent #2695

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
133 changes: 133 additions & 0 deletions doc/flame/rendering/layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,136 @@ 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
spydon marked this conversation as resolved.
Show resolved Hide resolved

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 Game {
spydon marked this conversation as resolved.
Show resolved Hide resolved
SnapshotComponent root;
Sprite player;

@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 Game {
spydon marked this conversation as resolved.
Show resolved Hide resolved
SnapshotComponent root;
Sprite background1;
Sprite background2;

@override
Future<void> onLoad() async {
// Add a snapshot component
root = SnapshotComponent();
add(root);

// Add some children
background1 = Sprite(await images.load('background1.png'));
root.add(background1);

background2 = Sprite(await images.load('background2.png'));
root.add(background2);
spydon marked this conversation as resolved.
Show resolved Hide resolved

// root will now render once (itself and all it's children) and then cache
// the result. On subsequent render calls, root itself, nor any of it's
// 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 it's entire tree, including it's children.
spydon marked this conversation as resolved.
Show resolved Hide resolved
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
spydon marked this conversation as resolved.
Show resolved Hide resolved
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 Game {
spydon marked this conversation as resolved.
Show resolved Hide resolved
SnapshotComponent root;

@override
Future<void> onLoad() async {
// Add a snapshot component, but don't use it's 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 it's children. If
/// [renderSnapshot] is set to `true`, the component and it's children will be
/// rendered to a cache. Subsequent renders use the cache, dramatically
/// improving performance. This is only effective if the component and it's
/// 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 it's 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
spydon marked this conversation as resolved.
Show resolved Hide resolved
bool get hasSnapshot => _picture != null;

/// Grab the current snapshot. Check it exists first using [hasSnapshot]
spydon marked this conversation as resolved.
Show resolved Hide resolved
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 m = transformMatrix.clone();
m.invert();
canvas.transform(m.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