Support TexturePacker image atlases
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flame_spine/flame_spine.dart';
|
||||
@@ -32,6 +33,7 @@ class GameResourceManager {
|
||||
final RuntimeAsyncGate _asyncGate = RuntimeAsyncGate(initiallyClosed: true);
|
||||
GamePackage? _package;
|
||||
final Map<String, _ImageResourceRecord> _images = {};
|
||||
final Map<String, RuntimeTextureAtlas> _textureAtlases = {};
|
||||
int _cacheBytes = 0;
|
||||
int _accessCounter = 0;
|
||||
|
||||
@@ -49,8 +51,10 @@ class GameResourceManager {
|
||||
|
||||
Future<void> mount(GamePackage package) async {
|
||||
_releaseCachedImages();
|
||||
_textureAtlases.clear();
|
||||
_asyncGate.activate();
|
||||
_package = package;
|
||||
await loadDeclaredTextureAtlases(package.manifest);
|
||||
await preloadDeclaredImages(package.manifest);
|
||||
await preloadDeclaredSpines(package.manifest);
|
||||
}
|
||||
@@ -58,6 +62,7 @@ class GameResourceManager {
|
||||
void dispose() {
|
||||
_asyncGate.close();
|
||||
_releaseCachedImages();
|
||||
_textureAtlases.clear();
|
||||
_package = null;
|
||||
}
|
||||
|
||||
@@ -146,6 +151,17 @@ class GameResourceManager {
|
||||
return _loadImage(keyOrPath, failOnError: false, retain: retain);
|
||||
}
|
||||
|
||||
RuntimeTextureFrame? textureFrame(String keyOrPath, String? frameName) {
|
||||
if (frameName == null || frameName.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final path = _tryResolve(keyOrPath);
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
return _textureAtlases[path]?.frames[frameName];
|
||||
}
|
||||
|
||||
Future<SpineComponent?> createSpineComponent(String? keyOrPath) {
|
||||
return _createSpineComponent(keyOrPath);
|
||||
}
|
||||
@@ -223,6 +239,24 @@ class GameResourceManager {
|
||||
return count;
|
||||
}
|
||||
|
||||
Future<void> loadDeclaredTextureAtlases(GamePackageManifest manifest) async {
|
||||
final activePackage = _package;
|
||||
if (activePackage == null) {
|
||||
return;
|
||||
}
|
||||
for (final entry in manifest.resources.entries) {
|
||||
final resource = entry.value;
|
||||
final atlas = resource.atlas;
|
||||
if (resource.type != GameResourceType.image || atlas == null) {
|
||||
continue;
|
||||
}
|
||||
final imagePath = activePackage.resolveResourcePath(entry.key);
|
||||
final atlasPath = activePackage.resolveResourcePath(atlas);
|
||||
final source = await activePackage.readText(atlasPath);
|
||||
_textureAtlases[imagePath] = RuntimeTextureAtlas.fromJsonString(source);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> preloadDeclaredImages(GamePackageManifest manifest) async {
|
||||
final futures = <Future<void>>[];
|
||||
for (final entry in manifest.resources.entries) {
|
||||
@@ -254,6 +288,100 @@ class GameResourceManager {
|
||||
}
|
||||
}
|
||||
|
||||
class RuntimeTextureAtlas {
|
||||
const RuntimeTextureAtlas({required this.frames});
|
||||
|
||||
final Map<String, RuntimeTextureFrame> frames;
|
||||
|
||||
factory RuntimeTextureAtlas.fromJsonString(String source) {
|
||||
final value = jsonDecode(source);
|
||||
if (value is! Map) {
|
||||
throw const FormatException('Texture atlas JSON must be an object');
|
||||
}
|
||||
final framesValue = value['frames'];
|
||||
final frames = <String, RuntimeTextureFrame>{};
|
||||
if (framesValue is Map) {
|
||||
for (final entry in framesValue.entries) {
|
||||
if (entry.key is! String || entry.value is! Map) {
|
||||
throw const FormatException('Texture atlas frames must be objects');
|
||||
}
|
||||
frames[entry.key as String] = RuntimeTextureFrame.fromMap(
|
||||
Map<String, Object?>.from(entry.value as Map),
|
||||
);
|
||||
}
|
||||
} else if (framesValue is List) {
|
||||
for (final item in framesValue) {
|
||||
if (item is! Map) {
|
||||
throw const FormatException('Texture atlas frames must be objects');
|
||||
}
|
||||
final map = Map<String, Object?>.from(item);
|
||||
final filename = map['filename'];
|
||||
if (filename is! String || filename.isEmpty) {
|
||||
throw const FormatException(
|
||||
'Texture atlas array frames require filename',
|
||||
);
|
||||
}
|
||||
frames[filename] = RuntimeTextureFrame.fromMap(map);
|
||||
}
|
||||
} else {
|
||||
throw const FormatException('Texture atlas frames must be a map or list');
|
||||
}
|
||||
return RuntimeTextureAtlas(frames: frames);
|
||||
}
|
||||
}
|
||||
|
||||
class RuntimeTextureFrame {
|
||||
const RuntimeTextureFrame({
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
final double x;
|
||||
final double y;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
ui.Rect get rect => ui.Rect.fromLTWH(x, y, width, height);
|
||||
|
||||
factory RuntimeTextureFrame.fromMap(Map<String, Object?> map) {
|
||||
final rotated = map['rotated'];
|
||||
if (rotated == true) {
|
||||
throw const FormatException(
|
||||
'Rotated TexturePacker frames are unsupported',
|
||||
);
|
||||
}
|
||||
final frame = map['frame'];
|
||||
if (frame is! Map) {
|
||||
throw const FormatException('TexturePacker frame must be an object');
|
||||
}
|
||||
final frameMap = Map<String, Object?>.from(frame);
|
||||
return RuntimeTextureFrame(
|
||||
x: _number(frameMap, 'x'),
|
||||
y: _number(frameMap, 'y'),
|
||||
width: _positiveNumber(frameMap, 'w'),
|
||||
height: _positiveNumber(frameMap, 'h'),
|
||||
);
|
||||
}
|
||||
|
||||
static double _number(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value is num) {
|
||||
return value.toDouble();
|
||||
}
|
||||
throw FormatException('TexturePacker frame.$key must be a number');
|
||||
}
|
||||
|
||||
static double _positiveNumber(Map<String, Object?> map, String key) {
|
||||
final value = _number(map, key);
|
||||
if (value <= 0) {
|
||||
throw FormatException('TexturePacker frame.$key must be > 0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
enum GameResourceState { idle, loading, ready, failed, disposed }
|
||||
|
||||
class ResourceLoadException implements Exception {
|
||||
|
||||
Reference in New Issue
Block a user