diff --git a/.github/.cspell/people_usernames.txt b/.github/.cspell/people_usernames.txt index a8155a585..7bfcc866e 100644 --- a/.github/.cspell/people_usernames.txt +++ b/.github/.cspell/people_usernames.txt @@ -6,6 +6,7 @@ erickzanardo # github.com/erickzanardo feroult # github.com/feroult fröber # github.com/Brixto gnarhard # github.com/gnarhard +kenney # kenney.nl Klingsbo # github.com/spydon luanpotter # github.com/luanpotter Lukas # github.com/spydon diff --git a/packages/flame_kenney_xml/.metadata b/packages/flame_kenney_xml/.metadata new file mode 100644 index 000000000..2b377030c --- /dev/null +++ b/packages/flame_kenney_xml/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "7482962148e8d758338d8a28f589f317e1e42ba4" + channel: "stable" + +project_type: package diff --git a/packages/flame_kenney_xml/CHANGELOG.md b/packages/flame_kenney_xml/CHANGELOG.md new file mode 100644 index 000000000..b208df87e --- /dev/null +++ b/packages/flame_kenney_xml/CHANGELOG.md @@ -0,0 +1,4 @@ +## 0.1.0 + + - **FEAT**: Add initial version of `flame_kenney_xml`. + diff --git a/packages/flame_kenney_xml/LICENSE b/packages/flame_kenney_xml/LICENSE new file mode 100644 index 000000000..0cf87ff46 --- /dev/null +++ b/packages/flame_kenney_xml/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Blue Fire + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/flame_kenney_xml/README.md b/packages/flame_kenney_xml/README.md new file mode 100644 index 000000000..e1bda7d67 --- /dev/null +++ b/packages/flame_kenney_xml/README.md @@ -0,0 +1,41 @@ + +

+ + flame + +

+ +

+Adds support for parsing XML sprite sheets from https://kenney.nl, and other sprite sheets on the same format. +

+ +

+ + + + +

+ +--- + + + +## Getting started + +To get started, first add `flame_kenney_xml` as a dependency in your flutter project. + +```bash +flutter pub add flame_kenney_xml +``` + +Then place the `spritesheet.json` in `assets/` and `spritesheet.png` in `assets/images/` +(or whatever the names of the files are). + +Then load the image and the spritesheet using: + +```dart +final spritesheet = await XmlSpriteSheet.load( + image: 'spritesheet.png', + xml: 'spritesheet.xml`, +); +``` diff --git a/packages/flame_kenney_xml/analysis_options.yaml b/packages/flame_kenney_xml/analysis_options.yaml new file mode 100644 index 000000000..85732fa02 --- /dev/null +++ b/packages/flame_kenney_xml/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flame_lint/analysis_options.yaml diff --git a/packages/flame_kenney_xml/example/.gitignore b/packages/flame_kenney_xml/example/.gitignore new file mode 100644 index 000000000..29a3a5017 --- /dev/null +++ b/packages/flame_kenney_xml/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/flame_kenney_xml/example/.metadata b/packages/flame_kenney_xml/example/.metadata new file mode 100644 index 000000000..36786568e --- /dev/null +++ b/packages/flame_kenney_xml/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + - platform: linux + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/flame_kenney_xml/example/README.md b/packages/flame_kenney_xml/example/README.md new file mode 100644 index 000000000..c7c39f28b --- /dev/null +++ b/packages/flame_kenney_xml/example/README.md @@ -0,0 +1,3 @@ +# flame_kenney_xml example + +An example of how to use the flame_kenney_xml package. diff --git a/packages/flame_kenney_xml/example/analysis_options.yaml b/packages/flame_kenney_xml/example/analysis_options.yaml new file mode 100644 index 000000000..85732fa02 --- /dev/null +++ b/packages/flame_kenney_xml/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flame_lint/analysis_options.yaml diff --git a/packages/flame_kenney_xml/example/assets/images/spritesheet_stone.png b/packages/flame_kenney_xml/example/assets/images/spritesheet_stone.png new file mode 100644 index 000000000..83be767c5 Binary files /dev/null and b/packages/flame_kenney_xml/example/assets/images/spritesheet_stone.png differ diff --git a/packages/flame_kenney_xml/example/assets/license.txt b/packages/flame_kenney_xml/example/assets/license.txt new file mode 100644 index 000000000..188521c56 --- /dev/null +++ b/packages/flame_kenney_xml/example/assets/license.txt @@ -0,0 +1,14 @@ + +############################################################################### + + Physics asset pack by Kenney Vleugels (www.kenney.nl) + + ------------------------------ + + License (CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + You may use these graphics in personal and commercial projects. + Credit (Kenney or www.kenney.nl) would be nice but is not mandatory. + +############################################################################### \ No newline at end of file diff --git a/packages/flame_kenney_xml/example/assets/spritesheet_stone.xml b/packages/flame_kenney_xml/example/assets/spritesheet_stone.xml new file mode 100644 index 000000000..d1940c1d9 --- /dev/null +++ b/packages/flame_kenney_xml/example/assets/spritesheet_stone.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/flame_kenney_xml/example/lib/main.dart b/packages/flame_kenney_xml/example/lib/main.dart new file mode 100644 index 000000000..0a7a0a3ce --- /dev/null +++ b/packages/flame_kenney_xml/example/lib/main.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame_kenney_xml/xml_sprite_sheet.dart'; +import 'package:flutter/material.dart'; + +/// A simple game that adds a random sprite component created from a kenney.nl +/// sprite sheet to the screen when tapped. +void main() { + runApp( + GameWidget.controlled( + gameFactory: () => FlameGame(world: KenneyWorld()), + ), + ); +} + +class KenneyWorld extends World with TapCallbacks { + late final XmlSpriteSheet spritesheet; + + @override + Future onLoad() async { + spritesheet = await XmlSpriteSheet.load( + imagePath: 'spritesheet_stone.png', + xmlPath: 'spritesheet_stone.xml', + ); + add(randomSpriteComponent()); + } + + @override + void onTapDown(TapDownEvent event) { + add(randomSpriteComponent(position: event.localPosition)); + } + + SpriteComponent randomSpriteComponent({Vector2? position}) { + final name = spritesheet.spriteNames.random(); + return SpriteComponent( + sprite: spritesheet.getSprite(name), + position: position, + anchor: Anchor.center, + ); + } +} diff --git a/packages/flame_kenney_xml/example/pubspec.yaml b/packages/flame_kenney_xml/example/pubspec.yaml new file mode 100644 index 000000000..0729cbd7c --- /dev/null +++ b/packages/flame_kenney_xml/example/pubspec.yaml @@ -0,0 +1,23 @@ +name: flame_kenney_xml_example +description: "An example for the `XmlSpriteSheet` used to load kenney.nl assets." + +publish_to: "none" + +version: 1.0.0 + +environment: + sdk: ">=3.4.0 <4.0.0" + +dependencies: + flame: ^1.18.0 + flame_kenney_xml: ^0.1.2 + flutter: + sdk: flutter + +dev_dependencies: + flame_lint: ^1.2.0 + +flutter: + assets: + - assets/ + - assets/images/ diff --git a/packages/flame_kenney_xml/lib/xml_sprite_sheet.dart b/packages/flame_kenney_xml/lib/xml_sprite_sheet.dart new file mode 100644 index 000000000..73b5dc0d5 --- /dev/null +++ b/packages/flame_kenney_xml/lib/xml_sprite_sheet.dart @@ -0,0 +1,66 @@ +import 'package:flame/cache.dart'; +import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/flame.dart'; +import 'package:xml/xml.dart'; +import 'package:xml/xpath.dart'; + +/// A sprite sheet loaded from an XML file and an image. +/// +/// The XML file must be in the format of a ShoeBox XML file, formatted in the +/// same way as the Kenney.nl sprite sheets. +/// https://twitter.com/KenneyNL/status/1777429120936202344 +class XmlSpriteSheet { + XmlSpriteSheet({ + required this.image, + required String xml, + }) { + final document = XmlDocument.parse(xml); + for (final node in document.xpath('//TextureAtlas/SubTexture')) { + final name = node.getAttribute('name')!; + final x = double.parse(node.getAttribute('x')!); + final y = double.parse(node.getAttribute('y')!); + final width = double.parse(node.getAttribute('width')!); + final height = double.parse(node.getAttribute('height')!); + _spriteBoundaries[name] = Rect.fromLTWH(x, y, width, height); + } + } + + /// Load an [XmlSpriteSheet] from an image and an XML file. + /// + /// The [imagePath] should be in relation to `assets/images/`. + /// The [xmlPath] should be in relation to `assets/`. + static Future load({ + required String imagePath, + required String xmlPath, + Images? imageCache, + AssetsCache? assetsCache, + }) async { + final image = await (imageCache ?? Flame.images).load(imagePath); + final xml = await (assetsCache ?? Flame.assets).readFile(xmlPath); + return XmlSpriteSheet(image: image, xml: xml); + } + + final Image image; + final _spriteBoundaries = {}; + + late final List spriteNames = _spriteBoundaries.keys.toList(); + + /// Get a sprite from the sprite sheet by its name. + /// + /// Throws an [ArgumentError] if the sprite is not found. + Sprite getSprite(String name) { + final rect = _spriteBoundaries[name]; + if (rect == null) { + throw ArgumentError('Sprite $name not found'); + } + return Sprite( + image, + srcPosition: rect.topLeft.toVector2(), + srcSize: rect.size.toVector2(), + ); + } + + /// Get a random sprite from the sprite sheet. + Sprite getRandomSprite() => getSprite(spriteNames.random()); +} diff --git a/packages/flame_kenney_xml/pubspec.yaml b/packages/flame_kenney_xml/pubspec.yaml new file mode 100644 index 000000000..c83aa82fe --- /dev/null +++ b/packages/flame_kenney_xml/pubspec.yaml @@ -0,0 +1,30 @@ +name: flame_kenney_xml +description: "Support for Kenney XML spritesheets for the Flame game engine. This package parses XML files produced by Kenney." +version: 0.1.2 +homepage: https://github.com/flame-engine/flame/tree/main/packages/flame_kenney_xml +funding: + - https://opencollective.com/blue-fire + - https://github.com/sponsors/bluefireteam + - https://patreon.com/bluefireoss +topics: + - flame + - spritesheet + - kenney + - tilemap + +environment: + sdk: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" + +dependencies: + flame: ^1.18.0 + flutter: + sdk: flutter + xml: ^6.5.0 + +dev_dependencies: + build_runner: ^2.4.11 + flame_lint: ^1.2.0 + flutter_test: + sdk: flutter + mockito: ^5.4.4 diff --git a/packages/flame_kenney_xml/test/xml_sprite_sheet_test.dart b/packages/flame_kenney_xml/test/xml_sprite_sheet_test.dart new file mode 100644 index 000000000..4617d6759 --- /dev/null +++ b/packages/flame_kenney_xml/test/xml_sprite_sheet_test.dart @@ -0,0 +1,73 @@ +import 'dart:ui'; + +import 'package:flame/cache.dart'; +import 'package:flame_kenney_xml/xml_sprite_sheet.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +class _MockImage extends Mock implements Image { + @override + int get width => 100; + + @override + int get height => 100; +} + +class _MockImages extends Mock implements Images { + @override + Future load(String fileName, {String? key}) async { + return _MockImage(); + } +} + +class _MockAssetsCache extends Mock implements AssetsCache { + @override + Future readFile(String fileName) async { + return ''' + + + + + '''; + } +} + +void main() { + group('XmlSpriteSheet', () { + test('creation from constructor', () { + final spritesheet = XmlSpriteSheet( + image: _MockImage(), + xml: ''' + + + + + ''', + ); + + expect(spritesheet.spriteNames, equals(['sprite1', 'sprite2'])); + final sprite1 = spritesheet.getSprite('sprite1'); + expect(sprite1.src, equals(const Rect.fromLTWH(0, 0, 32, 32))); + final sprite2 = spritesheet.getSprite('sprite2'); + expect(sprite2.src, equals(const Rect.fromLTWH(32, 0, 32, 32))); + }); + + test('creation from load method', () async { + final mockImages = _MockImages(); + final mockAssetsCache = _MockAssetsCache(); + + final spritesheet = await XmlSpriteSheet.load( + imagePath: 'spritesheet_stone.png', + xmlPath: 'spritesheet_stone.xml', + imageCache: mockImages, + assetsCache: mockAssetsCache, + ); + + expect(spritesheet.spriteNames, equals(['sprite1', 'sprite2'])); + final sprite1 = spritesheet.getSprite('sprite1'); + expect(sprite1.src, equals(const Rect.fromLTWH(0, 0, 32, 32))); + final sprite2 = spritesheet.getSprite('sprite2'); + expect(sprite2.src, equals(const Rect.fromLTWH(32, 0, 32, 32))); + }); + }); +}