Support TexturePacker image atlases

This commit is contained in:
gem
2026-06-09 12:49:01 +08:00
parent e2a584d4dc
commit 38f6e0c0c9
16 changed files with 343 additions and 26 deletions

View File

@@ -10,6 +10,9 @@ void main() {
'type': 'button',
'parent': 'top_bar',
'asset': 'dice_normal',
'frame': 'dice_idle.png',
'pressedFrame': 'dice_pressed.png',
'disabledFrame': 'dice_disabled.png',
'sourceX': 4,
'sourceY': 5,
'sourceWidth': 64,
@@ -80,6 +83,9 @@ void main() {
expect(node.type, 'button');
expect(node.parent, 'top_bar');
expect(node.asset, 'dice_normal');
expect(node.frame, 'dice_idle.png');
expect(node.pressedFrame, 'dice_pressed.png');
expect(node.disabledFrame, 'dice_disabled.png');
expect(node.sourceX, 4);
expect(node.sourceY, 5);
expect(node.sourceWidth, 64);
@@ -163,6 +169,9 @@ void main() {
expect(node.textShadowOffsetX, isNull);
expect(node.textShadowOffsetY, isNull);
expect(node.textShadowBlur, isNull);
expect(node.frame, isNull);
expect(node.pressedFrame, isNull);
expect(node.disabledFrame, isNull);
expect(node.sourceX, isNull);
expect(node.sourceY, isNull);
expect(node.sourceWidth, isNull);
@@ -210,6 +219,9 @@ void main() {
'sourceY': 4,
'sourceWidth': 40,
'sourceHeight': 41,
'frame': 'piece.png',
'pressedFrame': 'piece_down.png',
'disabledFrame': 'piece_disabled.png',
'sliceLeft': 5,
'sliceTop': 6,
'sliceRight': 7,
@@ -249,6 +261,9 @@ void main() {
expect(updated.sourceY, 4);
expect(updated.sourceWidth, 40);
expect(updated.sourceHeight, 41);
expect(updated.frame, 'piece.png');
expect(updated.pressedFrame, 'piece_down.png');
expect(updated.disabledFrame, 'piece_disabled.png');
expect(updated.sliceLeft, 5);
expect(updated.sliceTop, 6);
expect(updated.sliceRight, 7);
@@ -296,19 +311,13 @@ void main() {
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'image',
'sourceWidth': 0,
}),
() =>
RuntimeNode.fromMap({'id': 'a', 'type': 'image', 'sourceWidth': 0}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'image',
'sliceLeft': -1,
}),
() =>
RuntimeNode.fromMap({'id': 'a', 'type': 'image', 'sliceLeft': -1}),
throwsFormatException,
);
expect(

View File

@@ -26,6 +26,7 @@ void main() {
'board': {
'type': 'image',
'path': 'assets/board.png',
'atlas': 'assets/board.json',
'preload': 'lazy',
'group': 'board',
},
@@ -53,6 +54,7 @@ void main() {
expect(manifest.display.scaleMode, 'fit');
expect(manifest.resources['board']?.type, 'image');
expect(manifest.resources['board']?.path, 'assets/board.png');
expect(manifest.resources['board']?.atlas, 'assets/board.json');
expect(manifest.resources['board']?.preload, GameResourcePreload.lazy);
expect(manifest.resources['board']?.group, 'board');
expect(manifest.resources['roll']?.type, GameResourceType.audio);

View File

@@ -1,6 +1,7 @@
import 'dart:async' as async;
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' show Rect;
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
@@ -61,6 +62,19 @@ void main() {
},
);
test('loads TexturePacker atlas frames for image resources', () async {
final resources = GameResourceManager();
final package = await _createTextureAtlasPackage('texture_atlas');
await resources.mount(package);
final idle = resources.textureFrame('ui', 'button_idle.png');
final pressed = resources.textureFrame('ui', 'button_pressed.png');
expect(idle?.rect, Rect.fromLTWH(2, 3, 40, 20));
expect(pressed?.rect, Rect.fromLTWH(44, 3, 40, 20));
expect(resources.textureFrame('ui', 'missing.png'), isNull);
});
test('exports image debug json and evicts failed records', () async {
final resources = GameResourceManager();
final package = await _createPackage('debug_json');
@@ -332,6 +346,83 @@ Future<GamePackage> _createPackage(
);
}
Future<GamePackage> _createTextureAtlasPackage(String name) async {
final root = await Directory.systemTemp.createTemp('resource_${name}_');
Directory('${root.path}/assets').createSync(recursive: true);
File('${root.path}/assets/ui.png').writeAsBytesSync(_pngBytes);
File('${root.path}/assets/ui.json').writeAsStringSync('''
{
"frames": {
"button_idle.png": {
"frame": { "x": 2, "y": 3, "w": 40, "h": 20 },
"rotated": false,
"trimmed": false
}
}
}
''');
addTearDown(() {
if (root.existsSync()) {
root.deleteSync(recursive: true);
}
});
final hashAtlas = RuntimeTextureAtlas.fromJsonString(
File('${root.path}/assets/ui.json').readAsStringSync(),
);
expect(hashAtlas.frames['button_idle.png']?.rect, Rect.fromLTWH(2, 3, 40, 20));
final arrayAtlas = RuntimeTextureAtlas.fromJsonString('''
{
"frames": [
{
"filename": "button_pressed.png",
"frame": { "x": 44, "y": 3, "w": 40, "h": 20 },
"rotated": false,
"trimmed": false
}
]
}
''');
final mergedAtlas = '''
{
"frames": {
"button_idle.png": {
"frame": { "x": 2, "y": 3, "w": 40, "h": 20 },
"rotated": false,
"trimmed": false
},
"button_pressed.png": {
"frame": { "x": ${arrayAtlas.frames['button_pressed.png']!.x}, "y": 3, "w": 40, "h": 20 },
"rotated": false,
"trimmed": false
}
}
}
''';
File('${root.path}/assets/ui.json').writeAsStringSync(mergedAtlas);
return GamePackage.file(
rootPath: root.path,
manifest: GamePackageManifest(
gameId: 'test',
name: 'Test',
version: '0.1.0',
runtimeApiVersion: 1,
entry: 'scripts/main.lua',
assetsBase: 'assets',
resources: const {
'ui': GameResource(
type: GameResourceType.image,
path: 'assets/ui.png',
atlas: 'assets/ui.json',
preload: GameResourcePreload.lazy,
),
},
),
);
}
Future<GamePackage> _createMultiImagePackage(String name) async {
final root = await Directory.systemTemp.createTemp('resource_${name}_');
Directory('${root.path}/assets').createSync(recursive: true);