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: AssetsBundle can be customized in Images and AssetsCache. #2807

Merged
merged 8 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion doc/flame/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,10 @@ flutter:
```

If you want to change this structure, this is possible by using the `prefix` parameter and creating
your instances of `AssetsCache`, `ImagesCache`, `AudioCache`, and `SoundPool`s, instead of using the
your instances of `AssetsCache`, `Images`, and `AudioCache`, instead of using the
global ones provided by Flame.

Additionally, `AssetsCache` and `Images` can receive a custom
[`AssetBundle`](https://api.flutter.dev/flutter/services/AssetBundle-class.html).
This can be used to make Flame look for assets in a different location other the `rootBundle`,
like the file system for example.
23 changes: 17 additions & 6 deletions packages/flame/lib/src/cache/assets_cache.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/services.dart' show rootBundle;
import 'package:flame/flame.dart';
import 'package:flutter/services.dart' show AssetBundle;

/// A class that loads, and caches files.
///
/// It automatically looks for files in the `assets` directory.
class AssetsCache {
final String prefix;
final Map<String, _Asset<dynamic>> _files = {};
AssetsCache({
this.prefix = 'assets/',
AssetBundle? bundle,
}) : bundle = bundle ?? Flame.bundle;

/// The [AssetBundle] from which assets are loaded.
/// defaults to [Flame.bundle].
AssetBundle bundle;

AssetsCache({this.prefix = 'assets/'});
String prefix;
final Map<String, _Asset<dynamic>> _files = {};

/// Removes the file from the cache.
void clear(String file) {
Expand All @@ -22,6 +30,9 @@ class AssetsCache {
_files.clear();
}

/// Returns the number of files in the cache.
int get cacheCount => _files.length;

/// Reads a file from assets folder.
Future<String> readFile(String fileName) async {
if (!_files.containsKey(fileName)) {
Expand Down Expand Up @@ -53,12 +64,12 @@ class AssetsCache {
}

Future<_StringAsset> _readFile(String fileName) async {
final string = await rootBundle.loadString('$prefix$fileName');
final string = await bundle.loadString('$prefix$fileName');
return _StringAsset(string);
}

Future<_BinaryAsset> _readBinary(String fileName) async {
final data = await rootBundle.load('$prefix$fileName');
final data = await bundle.load('$prefix$fileName');
final bytes = Uint8List.view(data.buffer);
return _BinaryAsset(bytes);
}
Expand Down
16 changes: 11 additions & 5 deletions packages/flame/lib/src/cache/images.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';

class Images {
Images({String prefix = 'assets/images/'}) {
this.prefix = prefix;
}
Images({
String prefix = 'assets/images/',
AssetBundle? bundle,
}) : _prefix = prefix,
bundle = bundle ?? Flame.bundle;

final Map<String, _ImageAsset> _assets = {};

/// The [AssetBundle] from which images are loaded.
/// defaults to [Flame.bundle].
AssetBundle bundle;

/// Path prefix to the project's directory with images.
///
/// This path is relative to the project's root, and the default prefix is
Expand Down Expand Up @@ -126,7 +132,7 @@ class Images {
/// Loads all images in the [prefix]ed path that are matching the specified
/// pattern.
Future<List<Image>> loadAllFromPattern(Pattern pattern) async {
final manifestContent = await rootBundle.loadString('AssetManifest.json');
final manifestContent = await bundle.loadString('AssetManifest.json');
final manifestMap = json.decode(manifestContent) as Map<String, dynamic>;
final imagePaths = manifestMap.keys.where((path) {
return path.startsWith(_prefix) && path.toLowerCase().contains(pattern);
Expand Down Expand Up @@ -160,7 +166,7 @@ class Images {
}

Future<Image> _fetchToMemory(String name) async {
final data = await Flame.bundle.load('$_prefix$name');
final data = await bundle.load('$_prefix$name');
final bytes = Uint8List.view(data.buffer);
return decodeImageFromList(bytes);
}
Expand Down
26 changes: 15 additions & 11 deletions packages/flame/test/cache/assets_cache_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:mocktail/mocktail.dart';

import '../fixtures/fixture_reader.dart';

class _AssetsCacheMock extends Mock implements AssetsCache {}
class _MockAssetBundle extends Mock implements AssetBundle {}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -50,9 +50,8 @@ void main() {
final file = await assetsCache.readFile(fileName);
expect(file, isA<String>());

final assetsCacheMock = _AssetsCacheMock();
assetsCacheMock.clear(fileName);
verify(() => assetsCacheMock.clear(fileName)).called(1);
assetsCache.clear(fileName);
expect(assetsCache.cacheCount, equals(0));
});

test('clearCache', () async {
Expand All @@ -62,14 +61,8 @@ void main() {
final file = await assetsCache.readFile(fileName);
expect(file, isA<String>());

final assetsCacheMock = _AssetsCacheMock();
assetsCacheMock.clearCache();
verify(assetsCacheMock.clearCache).called(1);

// If all file was not clear from cache then it will not readBinaryFile
assetsCache.clearCache();
final fileTxtAsBinary = await assetsCache.readBinaryFile(fileName);
expect(fileTxtAsBinary, isA<Uint8List>());
expect(assetsCache.cacheCount, equals(0));
});

testWithFlameGame(
Expand All @@ -86,5 +79,16 @@ void main() {
expect(game.assets, equals(Flame.assets));
},
);

test('bundle can be overridden', () async {
final bundle = _MockAssetBundle();
when(() => bundle.loadString(any())).thenAnswer((_) async => 'Two ducks');

final cache = AssetsCache(bundle: bundle);

final result = await cache.readFile('duck_count');
expect(result, equals('Two ducks'));
verify(() => bundle.loadString('assets/duck_count')).called(1);
});
});
}
22 changes: 22 additions & 0 deletions packages/flame/test/cache/images_test.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import 'dart:convert';
import 'dart:ui';

import 'package:collection/collection.dart';
import 'package:flame/cache.dart';
import 'package:flame/flame.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class _MockAssetBundle extends Mock implements AssetBundle {}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();
// A simple 1x1 pixel encoded as base64 - just so that we have something to
Expand Down Expand Up @@ -125,6 +129,24 @@ void main() {
expect(images.fromCache('image1'), isNotNull);
expect(images.fromCache('image2'), isNotNull);
});

test('can have its bundle overridden', () async {
final bundle = _MockAssetBundle();
when(() => bundle.load(any())).thenAnswer(
(_) async {
final list = base64Decode(pixel.split(',').last);
return ByteData.view(list.buffer);
},
);

final images = Images(bundle: bundle);
final image = await images.load('pixel.png');

expect(image.width, equals(1));
expect(image.height, equals(1));

verify(() => bundle.load('assets/images/pixel.png')).called(1);
});
});
}

Expand Down
6 changes: 6 additions & 0 deletions packages/flame_tiled/lib/src/tiled_component.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'dart:ui';

import 'package:collection/collection.dart';
import 'package:flame/cache.dart';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_tiled/src/renderable_tile_map.dart';
import 'package:flame_tiled/src/tile_atlas.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:tiled/tiled.dart';

Expand Down Expand Up @@ -104,6 +106,8 @@ class TiledComponent<T extends FlameGame> extends PositionComponent
String prefix = 'assets/tiles/',
int? priority,
bool? ignoreFlip,
AssetBundle? bundle,
Images? images,
}) async {
return TiledComponent(
await RenderableTiledMap.fromFile(
Expand All @@ -113,6 +117,8 @@ class TiledComponent<T extends FlameGame> extends PositionComponent
atlasMaxY: atlasMaxY,
ignoreFlip: ignoreFlip,
prefix: prefix,
bundle: bundle,
images: images,
),
priority: priority,
);
Expand Down
31 changes: 25 additions & 6 deletions packages/flame_tiled/test/tile_atlas_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'package:flame/cache.dart';
import 'package:flame/components.dart';
import 'package:flame/flame.dart';
import 'package:flame_tiled/flame_tiled.dart';
import 'package:flame_tiled/src/tile_atlas.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import 'test_asset_bundle.dart';
Expand All @@ -23,9 +24,11 @@ void main() {
});

group('loadImages', () {
late AssetBundle bundle;

setUp(() {
TiledAtlas.atlasMap.clear();
Flame.bundle = TestAssetBundle(
bundle = TestAssetBundle(
imageNames: [
'images/blue.png',
'images/purple_rock.png',
Expand All @@ -47,6 +50,7 @@ void main() {
test('handles empty map', () async {
final atlas = await TiledAtlas.fromTiledMap(
TiledMap(height: 1, tileHeight: 1, tileWidth: 1, width: 1),
images: Images(bundle: bundle),
);

expect(atlas.atlas, isNull);
Expand Down Expand Up @@ -76,8 +80,10 @@ void main() {
);

test('returns single image atlas for simple map', () async {
final images = Images(bundle: bundle);
final atlas = await TiledAtlas.fromTiledMap(
simpleMap,
images: images,
);

expect(atlas.offsets, hasLength(1));
Expand All @@ -86,7 +92,7 @@ void main() {
expect(atlas.atlas!.height, 74);
expect(atlas.key, 'images/green.png');

expect(Flame.images.containsKey('images/green.png'), isTrue);
expect(images.containsKey('images/green.png'), isTrue);

expect(
await imageToPng(atlas.atlas!),
Expand All @@ -97,9 +103,11 @@ void main() {
test('returns cached atlas', () async {
final atlas1 = await TiledAtlas.fromTiledMap(
simpleMap,
images: Images(bundle: bundle),
);
final atlas2 = await TiledAtlas.fromTiledMap(
simpleMap,
images: Images(bundle: bundle),
);

expect(atlas1, isNot(same(atlas2)));
Expand All @@ -108,8 +116,12 @@ void main() {
});

test('packs complex maps with multiple images', () async {
final component =
await TiledComponent.load('isometric_plain.tmx', Vector2(128, 74));
final component = await TiledComponent.load(
'isometric_plain.tmx',
Vector2(128, 74),
bundle: bundle,
images: Images(bundle: bundle),
);

final atlas = TiledAtlas.atlasMap.values.first;
expect(
Expand All @@ -125,6 +137,7 @@ void main() {
test('clearing cache', () async {
await TiledAtlas.fromTiledMap(
simpleMap,
images: Images(bundle: bundle),
);

expect(TiledAtlas.atlasMap.isNotEmpty, true);
Expand All @@ -136,9 +149,11 @@ void main() {
});

group('Single tileset map', () {
late AssetBundle bundle;

setUp(() {
TiledAtlas.atlasMap.clear();
Flame.bundle = TestAssetBundle(
bundle = TestAssetBundle(
imageNames: [
'4_color_sprite.png',
],
Expand All @@ -156,10 +171,14 @@ void main() {
TiledComponent.load(
'single_tile_map_1.tmx',
Vector2(16, 16),
bundle: bundle,
images: Images(bundle: bundle),
),
TiledComponent.load(
'single_tile_map_2.tmx',
Vector2(16, 16),
bundle: bundle,
images: Images(bundle: bundle),
),
]);

Expand Down
Loading