diff --git a/.gitignore b/.gitignore index 9a6c587a..34930fb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -Resources/shaders/ -Resources/User/ -docs/jazzy_output/ +Assets/User/* +Demo/shaders/* +Develop/* +docs/jazzy_output/* +.swiftlint.yml +IDETemplateMacros.plist +SKTiled.xcodeproj/xcshareddata/xcschemes/Demo* *.psd *.html *.js *.css -SKTiled.playground/ -SKTiled.xcworkspace/ -Notes.md -.swiftlint.yml diff --git a/.swift-version b/.swift-version index eb39e538..bf77d549 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -3.3 +4.2 diff --git a/.travis.yml b/.travis.yml index d8955617..0ef39a48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ language: swift -osx_image: xcode9.0 +osx_image: xcode10 xcode_project: SKTiled.xcodeproj -xcode_scheme: SKTiled-iOS +xcode_scheme: + - SKTiled-iOS + - SKTiled-macOS + - SKTiled-tvOS branches: except: - - develop + - develop \ No newline at end of file diff --git a/Resources/CREDITS b/Assets/CREDITS similarity index 78% rename from Resources/CREDITS rename to Assets/CREDITS index 8634d54d..7662116e 100644 --- a/Resources/CREDITS +++ b/Assets/CREDITS @@ -18,3 +18,11 @@ http://opengameart.org/content/sticker-knight-platformer. Pac-Man: Pac-Man is a trademark of Bandai Namco, Inc. + +LoFi Roguelike: +@Oryx +http://realmofthemadgod.wikia.com/wiki/Pixel_Art + +Tiny 16: +@Sharm +https://opengameart.org/content/tiny-16-basic diff --git a/Assets/dungeon-16x16.png b/Assets/dungeon-16x16.png new file mode 100644 index 00000000..bbbc880e Binary files /dev/null and b/Assets/dungeon-16x16.png differ diff --git a/Assets/dungeon-16x16.tmx b/Assets/dungeon-16x16.tmx new file mode 100644 index 00000000..c65522c8 --- /dev/null +++ b/Assets/dungeon-16x16.tmx @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,346,5,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,24,58,2,3,348,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,7,54,25,25,25,25,26,4,27,0,0,0,0,185,187,0, +0,0,0,0,0,0,32,77,93,95,96,48,48,25,50,0,0,1,2,59,51,0, +0,0,1,3,5,0,32,0,76,77,119,232,50,50,50,0,0,208,209,1610613228,97,0, +0,143,491,36,51,0,32,0,0,0,0,34,51,0,259,0,0,118,119,32,120,0, +0,166,117,93,97,0,32,0,0,0,0,34,28,0,32,0,0,0,0,32,0,0, +0,166,0,116,120,0,32,0,0,0,0,34,3221225964,54,55,0,143,54,54,55,0,0, +0,93,3,3,5,0,1073741871,3,2,131,26,57,28,77,78,0,168,118,0,119,0,0, +0,116,93,36,51,0,24,25,44,133,25,27,3221225964,54,54,54,191,0,101,0,0,0, +0,0,116,93,97,0,295,109,67,69,17,18,28,77,77,77,77,0,30,0,0,0, +0,0,0,122,122,0,24,49,62,22,40,204,28,0,139,352,352,352,399,0,0,0, +0,0,0,0,0,0,47,49,49,62,1073741893,64,28,0,162,117,118,119,120,0,0,0, +0,0,0,0,0,0,93,94,95,96,249,96,97,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,116,117,118,119,272,119,120,0,0,0,0,0,0,0,0,0 + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,82,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,105,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,148,149,150,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,172,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,195,0,0,0,0,0,0,0,0,0,0,0,0,0,0,172,0,0,0, +0,0,0,218,0,0,0,0,0,0,0,0,0,0,0,0,0,0,195,0,0,0, +0,0,0,0,148,149,150,0,0,0,0,0,0,0,0,0,0,0,218,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,148,149,150,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,441,0,0,351,469,145,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,212,0,1,3758096851,462,168,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,51,485,37,0,0,0,0, +0,0,0,0,0,0,0,0,0,185,467,190,2147484115,186,59,28,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,490,468,0,490,370,27,97,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,393,32,0,212,393,60,421,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,416,32,0,0,444,0,444,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,277,190,191,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,300,202,214,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,208,209,210,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,461,285,462,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,484,0,485,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,341,341,341,341,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,341,341,341,0,341,341,341,341,0,0,0,341,0,0,0,0, +0,0,0,0,0,0,341,0,341,341,341,341,341,341,0,0,0,341,341,341,341,0, +0,0,0,0,341,0,341,0,0,0,0,0,0,0,0,0,0,0,341,341,341,0, +0,341,341,341,341,341,341,0,0,0,0,0,0,0,341,0,0,0,0,341,0,0, +0,341,0,341,341,0,341,0,0,0,0,341,341,0,341,0,0,0,0,341,0,0, +0,341,0,341,0,0,341,0,0,0,0,341,341,341,341,0,342,341,341,341,0,0, +0,341,341,341,341,0,341,341,0,0,0,341,341,0,0,0,341,0,341,0,0,0, +0,0,341,341,341,341,341,341,341,0,0,0,341,341,341,341,341,0,341,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,341,0,0,0,0,0,341,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,341,341,341,341,341,341,341,364,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,341,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,341,341,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,342,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,341,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,341,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,341,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,341,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,341,341,341,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,341,341,0,341,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,341,341,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,341,341,341,341,341,341,341,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,341,341,0,341,0,341,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,341,0,0,0,341,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,341,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,341,341,341,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + diff --git a/Assets/dungeon-16x16.tsx b/Assets/dungeon-16x16.tsx new file mode 100644 index 00000000..ec1a358a --- /dev/null +++ b/Assets/dungeon-16x16.tsx @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets/dungeon-16x32.png b/Assets/dungeon-16x32.png new file mode 100644 index 00000000..f8a54a73 Binary files /dev/null and b/Assets/dungeon-16x32.png differ diff --git a/Assets/dungeon-16x32.tsx b/Assets/dungeon-16x32.tsx new file mode 100644 index 00000000..ca13eda2 --- /dev/null +++ b/Assets/dungeon-16x32.tsx @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/dungeon-32x32.png b/Assets/dungeon-32x32.png similarity index 100% rename from Resources/dungeon-32x32.png rename to Assets/dungeon-32x32.png diff --git a/Assets/dungeon-32x32.tsx b/Assets/dungeon-32x32.tsx new file mode 100644 index 00000000..8685a5eb --- /dev/null +++ b/Assets/dungeon-32x32.tsx @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/hex-65x65-65x230.png b/Assets/hex-65x65-65x230.png similarity index 100% rename from Resources/hex-65x65-65x230.png rename to Assets/hex-65x65-65x230.png diff --git a/Resources/hex-65x65.tmx b/Assets/hex-65x65.tmx similarity index 86% rename from Resources/hex-65x65.tmx rename to Assets/hex-65x65.tmx index f7bfa942..6678669b 100644 --- a/Resources/hex-65x65.tmx +++ b/Assets/hex-65x65.tmx @@ -1,10 +1,12 @@ - + + - + + @@ -105,17 +107,17 @@ - H4sIAAAAAAAAA3WMMQrAMAwDnUekXpsEAp4y9f9fqwcZhHGGw2CdtEXEnAM+cPDfCSPfCmc4q+gxi9xbPp0XzqRd3g6nYqSdBh5w64WnhduS05ErXaU8nE5bTOX9pkxzOpABAAA= + H4sIAAAAAAAAE3WMMQrAMAwDnUekXpsEAp4y9f9fqwcZhHGGw2CdtEXEnAM+cPDfCSPfCmc4q+gxi9xbPp0XzqRd3g6nYqSdBh5w64WnhduS05ErXaU8nE5bTOX9pkxzOpABAAA= - H4sIAAAAAAAAA22OyQoCQQxEg47LuKBzc/eiKI6C4026P85P9zWTxkIteBSddCqJZnaFBu7wtI9qCGavk3uE2nsPuMBN/qd+8oO/z1ILXuvYr8KfWs7JqKL0VccvVw2gCxvY+U2akWcqevnGKawko+fe96yFk7SWf2mugDlZJT7El+1em8Aetu0uG8EMxn7bG/iVwT6QAQAA + H4sIAAAAAAAAE22OyQoCQQxEg47LuKBzc/eiKI6C4026P85P9zWTxkIteBSddCqJZnaFBu7wtI9qCGavk3uE2nsPuMBN/qd+8oO/z1ILXuvYr8KfWs7JqKL0VccvVw2gCxvY+U2akWcqevnGKawko+fe96yFk7SWf2mugDlZJT7El+1em8Aetu0uG8EMxn7bG/iVwT6QAQAA - H4sIAAAAAAAAA2NgGFxAiIGhYaDdQAkQptD9ADVzzeSQAQAA + H4sIAAAAAAAAE2NgGFxAiIGhYaDdQAkQptD9ADVzzeSQAQAA diff --git a/Resources/isometric-130x230.png b/Assets/isometric-130x230.png similarity index 100% rename from Resources/isometric-130x230.png rename to Assets/isometric-130x230.png diff --git a/Resources/isometric-130x66.tmx b/Assets/isometric-130x66.tmx similarity index 90% rename from Resources/isometric-130x66.tmx rename to Assets/isometric-130x66.tmx index 8935675d..c234a150 100644 --- a/Resources/isometric-130x66.tmx +++ b/Assets/isometric-130x66.tmx @@ -1,7 +1,9 @@ - + - + + + diff --git a/Assets/roguelike-16x16-anim.png b/Assets/roguelike-16x16-anim.png new file mode 100644 index 00000000..3ea42fc8 Binary files /dev/null and b/Assets/roguelike-16x16-anim.png differ diff --git a/Resources/roguelike-16x16.tmx b/Assets/roguelike-16x16.tmx similarity index 54% rename from Resources/roguelike-16x16.tmx rename to Assets/roguelike-16x16.tmx index 2eab4c45..87911909 100644 --- a/Resources/roguelike-16x16.tmx +++ b/Assets/roguelike-16x16.tmx @@ -1,91 +1,76 @@ - + - + - + + + - - - - + - eJztl0sKwzAMREUXzrFq6P3vFEIrGFQ5LbZUe0oWgxODlZfnT0gRkQK5m9h+vC9OWmNbtW2ds35v3KdaXlpc346xfWcOrozlchrvczbDCpwMezb6fMl6z0jOzPmI5vwV32qcLd7RWlmcj5tIfbVHNnneZz1vhPNgUz7lne0T/SGX8mo725+6s1x6rewr+FQe5EPu2lk78vz03HmZybnJu8Mq/jrofUYEJzKpy5bbqHXW6xPXYsRZlBH0pw7R8eg6i9pDHh86Ha2f8d1E5pXm3AbnuwbWXfV/gJGTgZEpTD4ZWBkYlZOJdTbDP4XBJwMjW1icMnCynJ87dcaTyA== + eJzt1EEKgDAMRNHBjdcKeP87SbEDQevCVYL8ByHV1TCUSgAAAP9zbFLMPWbX9d2Nszmf81bL/XnnLr2rubt7Lp9Dffp0npwv547KgNOqu9V0yJg7DK3vQaXQs8u3bivd72K3t8hyf+5wjP91scqXO+0oZ+6acQj1ei8BAACAr04OuhSj - + - + - eJztktsNAjEMBH0caYpXVTzKgKqoi1iX6IyV49cWmpFWyedovSIAAAAA/8dcs3cpoUZjTjVTzblF/5dQowXf31W+HfXN4Kn9da+d+dtk8nyVsWM2Tx/brXq+5yjDZZt2j32Tk+Tq029T0z0PNfeWR5RgY3Tz7hndoWVrm8+Sy/Mm622PsnrqzaNvvYXtNlOXHttt1i4BAAAAAH7xAbu9GBk= + eJztlUsKhTAQBCfe/6AeQwUH2mYevF1nUQXBjNkU80mqAAAAAP7jvNfxrme/JD6CXkq7qd+qr3MKd+p41dexz5OO6qSuXvukZ+dKv/pfa5/qT8+ZO552lsynz8rkp/OfwGfb8zrNfAJ1Uz91W5Wfd3f45aZxAr8jp57sOIk7Tn2QfIMavzunuu/wpk9vpd5Bu+Tzwf3UPd2XAAAAALAPFzWbNh0= - - - - + + + eJztl0sKwCAMRF15rd7/Vl0VREw/OtG8YmAWDSS+jprSnFLKhY5Kdb58zg1ZtVbvus9dvlX31Ksli+ttTZ2782BrTNtTvZ+rGSJwEu6ser54vaeS03M/1Jyz+KJxWryjvbw4rfBaLwKn0s+3EdU/BSfFT+X8JHB+id41KJwz9nwlWw9n7zmbdYdG+3t8N9WMs86Aqm/U/wEiJ4GRJJKfBFYC48VJYl3N8CcR/CQw0kTxlMBJmZ8ngpx/Jg== + + + + + eJzt0tENgzAMRVFLFJikKkMQtmq7Icu0Q7QBIowJ/SRWdY9kRYKPPD1HBAAA4P9U37mYqYsmyusz34bTU+zZ/q7t9n8QHzljf2GZJKjTW85u6VFn9pjTjsg251gViTeJV8f3aPMlXvrMvc3k1Yg8ZJ7nqan2cjtPSneoHeW8tb5y3mXd7btZs8adl971Ed2tpy4t3a3XLgEAAIBfPpTOFm8= + + + eJzt1kEOgjAQRuEJXYh6KQXPoXgO5RyIt1IvZImdhDQhNKZgMe8l/47Fl0kXiBARxWmdiWzcttn49ycjUnk7m+mdhbWVbocA582aWm/3CZ16x7K3XcBN53Kqb+8ZQ286l7MY8OmuiTgvzpJ7vpVdneD7VK8uxPgL57fhjBvOuOGM2xKc3b9R40wpO5/Wc1zAPbse5uPt75Wgk4iIiIiI/q83StFKcw== - - - - + eJztmM1KAzEQx3dbC3rqSVGE+nHyEfoIhZ5aBZ9Bb14FkUIvWj1q8ap3FV9DRfDiVfGgvoUJ3cFhOpNkki0o9g9hd5Ns9pfJzGx2s2yqqf6W2hV/n+OAPpNWv2AI4ZXUCbh3P2H8NmK0vJSVY5fmw9m8i+ou8nhGYKNHUJ95NlcH2jFtu6h9IHC2FLaVGCmHyx+WSNulub6q8H0wZ0/B6bMnHDn7vZq6t6Ke2lCq0647fr6L1bW+WNiGL/l43XM+KudKThzfWkZXXOO2Q6ad2rMeYAcXGxx9uSY0FqCf5dTED7D4/BKLqwuNhR7i1MSPj9UeaS4M9VWXXHF0bcqNKbdMW0j+tFqOYDxl7gFOOx5d/w9TPk35YsYKiSFuvVcibevKn6umbc2UdcbmIXFOrxuVeFbLGRLjLkZgAvvtzcj3NhTPaqJxwJ4aVinOOf8EcTZsOuZDFbsPSX0PgbiYwXEC80vdL1FmSd1qlm1WR+dbVZnRisuTKfaUrrV75u2A/rGcVJhtmP/sH+4Scjx+91LOd4Vvg+iap8yd5oN5NOZRbbyc1PxsIOoDEqdrb4JjB1jxujyhc1/B4mKmlRCbMbET++3YS8whVL7crsmBsDdy+XyIuL1lWfOl6ijtafstFud2LboeP58zMb5RlcusMgdoOK1gfgMHp++9Tn2AxiznyzD+vTk+FOUxH+f35Zoy7UnnMXTkEFpPr8/I9UExtnY/F6KY70LpXqoy/TMmRuuB8ef6f6Xdp9lnLQhtvm9lzIkZpH8rKf8KU3IevrevsFXM92xZnJPWpDjL/if93+1Ztn4D5ze1p7FZ - - - - - + + eJzt1NEJwjAUheFbkwxlp3AIK8bOUXEyK1SncBETUtDmQcRWcx/+D+5DW5IcDqQiAAAAAADNvBFpTbn1n+y5cSK78Lyfcc7c9bntavk9f+1uSyfQ5Rz66MNcFPTyLsvBPKe01yx9ltmP75f+H3wjZjnZlGXt0tQufYv3tlFyd72Zdqqlv1xnU2dxjlZPf7mhErmOc6tKpwEA4L8eJI4dew== - - - - + eJzt00sKwjAUheFbSAfalThXcFxdShfgBsS9VdCpLsVxFTwZVaQo5EEU/g8uCYETzuSaAQCAf7fVNJp96SJfLCqzm86uKt0EQG4XZ3Z14fle2WNEfsp7p5Pus9psXof/uVP2nrin73R46bTWvdVsInoulV1F5KfEdkJZfsfOGfYsdZfBv2XYsxCfuvgde7j0exbil7oAAABg9ARQzReI - - - - + eJztzgENAAAIA6CbyP7tjOGmkIAEAADgvq7tAQAAAF8N120AMQ== - - - - + eJzt07EJgEAQBMC7VixH7MA6tB+L0zp8MBEFAz94hZnkooVl4SIAAACg3pQRc77PryW7VeSBZ0v5ryGP+3Vj6wIn/WWvP+zXtS4AANzsX90Ftg== - - - - + eJzt1b0RQEAQBeAjVYMCFIAGZCKJCqR0IJPrwl93OrCBQMAdAbtr3jdzY8YG++zdHGMAAABAu4xWQKvlDuIQecas9Kw87iTfCn3uBPeUSnICwD8tSv8NBe5OEWol+zAoyXnUUOZESO7ekmOk2nSoT0IyP5FbMttqb0ip37z3jB+cgU7I3L+el6tvevGea16uvmffwTVTGynnDQDgrg3yfg2B - - - - + @@ -95,10 +80,10 @@ - + - - + + @@ -108,12 +93,4 @@ - - - - - - eJztl1EKwzAMQ/u1+x+zu8XYR8EIyZbdZmXQgNm6JPaLbJfs/dq292OP/ZF9x90MHc7VvPtFjMeotM7Os4PFtVPObEw5kVdxdnLncFY+j7kdOJimqzjxk+1VbEzDbt5j/Gp0euPQTrGv4nR8IUfkQS58dmI4+XbPq3hZDSvfSp8up/NblZuM80wfqVqZ5Kjqoyq2wxmf8bszkFPFOMPJ9p3lrGqmm6cVnJk/Jwbmw/E34XR9O5xXMjLOlf2+krOKG+fUWrbGZcV4qt8dTvVcxewyxj7K1rh1EH1m94cpZ6ZTp1YnNe9oz/R0dY1rcP3kDsbiqvtUZZXu8exdNuaverdPcs841cg0ZHnBPWc5mX+Xk+nKcvhrTswr07bbj925DmfmD+9ILB8uJ5tXnOw/T+ccEy0zDa40VbtdTqXlxId7d2PzXdbJ/euIUe1FXd36VJyTPCAnY0C/VT1crafL2amDX3Gutofz4byL8wMKo/+y - - diff --git a/Resources/roguelike-16x16.tsx b/Assets/roguelike-16x16.tsx similarity index 100% rename from Resources/roguelike-16x16.tsx rename to Assets/roguelike-16x16.tsx diff --git a/Resources/staggered-64x192.png b/Assets/staggered-64x192.png similarity index 100% rename from Resources/staggered-64x192.png rename to Assets/staggered-64x192.png diff --git a/Resources/staggered-64x33.tmx b/Assets/staggered-64x33.tmx similarity index 90% rename from Resources/staggered-64x33.tmx rename to Assets/staggered-64x33.tmx index 48a15f16..13196a0c 100644 --- a/Resources/staggered-64x33.tmx +++ b/Assets/staggered-64x33.tmx @@ -1,10 +1,11 @@ - + + @@ -175,7 +176,7 @@ - H4sIAAAAAAAAA+3RSw6AIAwE0PpXPBDX4+jaBQlpCu2gOyVhgemzQyH619dX6KwLoA3K2fKyLojvXlvrj7qW1VyrruYiUbKyoT01/5bNedGZIrOyMnvfIzq8N+vRacd7L457kZKX7SC8dj/5z+x4b+AMSjsB1nJROMRqb87ntcOVfWcwL6+zsEhP6fcHVsmaWs7yLXsBD1XkQDQIAAA= + H4sIAAAAAAAAE+3RSw6AIAwE0PpXPBDX4+jaBQlpCu2gOyVhgemzQyH619dX6KwLoA3K2fKyLojvXlvrj7qW1VyrruYiUbKyoT01/5bNedGZIrOyMnvfIzq8N+vRacd7L457kZKX7SC8dj/5z+x4b+AMSjsB1nJROMRqb87ntcOVfWcwL6+zsEhP6fcHVsmaWs7yLXsBD1XkQDQIAAA= @@ -184,27 +185,27 @@ - H4sIAAAAAAAAA2NgGAWjgDggSoCPD4gAsTAePi4gC6VhakWgtByRdsKAMBY+MfqwAXx6ZfHIwQA2txOyE5/dxOpF10+MW5EBsrtJsROb3fTUCwLEpBda2T0KRsFwAgAWPBsUNAgAAA== + H4sIAAAAAAAAE2NgGAWjgDggSoCPD4gAsTAePi4gC6VhakWgtByRdsKAMBY+MfqwAXx6ZfHIwQA2txOyE5/dxOpF10+MW5EBsrtJsROb3fTUCwLEpBda2T0KRsFwAgAWPBsUNAgAAA== - H4sIAAAAAAAAA2NgGAWjgDggPYB2a0BpdRL0DKR7QUCMBLVDya2DAQwl9w4lt46CUUAIAACSOrP/NAgAAA== + H4sIAAAAAAAAE2NgGAWjgDggPYB2a0BpdRL0DKR7QUCMBLVDya2DAQwl9w4lt46CUUAIAACSOrP/NAgAAA== - H4sIAAAAAAAAA2NgGAWjYOQByYF2AJlgqLobBIay20fBKBgFEAAAiACOYDQIAAA= + H4sIAAAAAAAAE2NgGAWjYOQByYF2AJlgqLobBIay20fBKBgFEAAAiACOYDQIAAA= - H4sIAAAAAAAAA2NgGAWjYGgBTgr0MgExK5n6rCnQywjFpOhH1keOXk4y9PMzIMIIpg/kb3Yi7eVHsgumn5lIvbxoeonVh66fnUy9zGTogwFBCvWOglEwVAAAkqckJTQIAAA= + H4sIAAAAAAAAE2NgGAWjYGgBTgr0MgExK5n6rCnQywjFpOhH1keOXk4y9PMzIMIIpg/kb3Yi7eVHsgumn5lIvbxoeonVh66fnUy9zGTogwFBCvWOglEwVAAAkqckJTQIAAA= - H4sIAAAAAAAAA2NgGAWjYBQMZiA/gHYr0MEOaTrYMZTsFBsAO0kFMkhsdPfSw05yAalhSw07SQX4wpYedhICAHrOs3Q0CAAA + H4sIAAAAAAAAE2NgGAWjYBQMZiA/gHYr0MEOaTrYMZTsFBsAO0kFMkhsdPfSw05yAalhSw07SQX4wpYedhICAHrOs3Q0CAAA @@ -226,7 +227,7 @@ - H4sIAAAAAAAAA+3RMQ7AMAgDQL5H1P+/p2vlYmyqqFPYGC52kogzZ7xJsSubza5cwj7NzHifNXVOfmcjuGXuEl07q94c3TT7aRbJxvPRsc7qDd1M1sNxVW83M8B9uSfed5Kp7BKu86ovc7syJw7/a3dmZV1X+WpX9u+5AVdRE540CAAA + H4sIAAAAAAAAE+3RMQ7AMAgDQL5H1P+/p2vlYmyqqFPYGC52kogzZ7xJsSubza5cwj7NzHifNXVOfmcjuGXuEl07q94c3TT7aRbJxvPRsc7qDd1M1sNxVW83M8B9uSfed5Kp7BKu86ovc7syJw7/a3dmZV1X+WpX9u+5AVdRE540CAAA diff --git a/Resources/staggered-paths-64x33.png b/Assets/staggered-paths-64x33.png similarity index 100% rename from Resources/staggered-paths-64x33.png rename to Assets/staggered-paths-64x33.png diff --git a/Resources/sticker-knight/alter.png b/Assets/sticker-knight/alter.png similarity index 100% rename from Resources/sticker-knight/alter.png rename to Assets/sticker-knight/alter.png diff --git a/Resources/sticker-knight/backgroundArch.png b/Assets/sticker-knight/backgroundArch.png similarity index 100% rename from Resources/sticker-knight/backgroundArch.png rename to Assets/sticker-knight/backgroundArch.png diff --git a/Resources/sticker-knight/backgroundMountain.png b/Assets/sticker-knight/backgroundMountain.png similarity index 100% rename from Resources/sticker-knight/backgroundMountain.png rename to Assets/sticker-knight/backgroundMountain.png diff --git a/Resources/sticker-knight/backgroundTower.png b/Assets/sticker-knight/backgroundTower.png similarity index 100% rename from Resources/sticker-knight/backgroundTower.png rename to Assets/sticker-knight/backgroundTower.png diff --git a/Resources/sticker-knight/backgroundTree.png b/Assets/sticker-knight/backgroundTree.png similarity index 100% rename from Resources/sticker-knight/backgroundTree.png rename to Assets/sticker-knight/backgroundTree.png diff --git a/Resources/sticker-knight/blobBlue.png b/Assets/sticker-knight/blobBlue.png similarity index 100% rename from Resources/sticker-knight/blobBlue.png rename to Assets/sticker-knight/blobBlue.png diff --git a/Resources/sticker-knight/blobGreen.png b/Assets/sticker-knight/blobGreen.png similarity index 100% rename from Resources/sticker-knight/blobGreen.png rename to Assets/sticker-knight/blobGreen.png diff --git a/Resources/sticker-knight/blue.png b/Assets/sticker-knight/blue.png similarity index 100% rename from Resources/sticker-knight/blue.png rename to Assets/sticker-knight/blue.png diff --git a/Resources/sticker-knight/bombStroked.png b/Assets/sticker-knight/bombStroked.png similarity index 100% rename from Resources/sticker-knight/bombStroked.png rename to Assets/sticker-knight/bombStroked.png diff --git a/Resources/sticker-knight/castleWall.png b/Assets/sticker-knight/castleWall.png similarity index 100% rename from Resources/sticker-knight/castleWall.png rename to Assets/sticker-knight/castleWall.png diff --git a/Resources/sticker-knight/cloud.png b/Assets/sticker-knight/cloud.png similarity index 100% rename from Resources/sticker-knight/cloud.png rename to Assets/sticker-knight/cloud.png diff --git a/Resources/sticker-knight/column1.png b/Assets/sticker-knight/column1.png similarity index 100% rename from Resources/sticker-knight/column1.png rename to Assets/sticker-knight/column1.png diff --git a/Resources/sticker-knight/column2.png b/Assets/sticker-knight/column2.png similarity index 100% rename from Resources/sticker-knight/column2.png rename to Assets/sticker-knight/column2.png diff --git a/Resources/sticker-knight/doorBlueStroked.png b/Assets/sticker-knight/doorBlueStroked.png similarity index 100% rename from Resources/sticker-knight/doorBlueStroked.png rename to Assets/sticker-knight/doorBlueStroked.png diff --git a/Resources/sticker-knight/doorGreenStroke.png b/Assets/sticker-knight/doorGreenStroke.png similarity index 100% rename from Resources/sticker-knight/doorGreenStroke.png rename to Assets/sticker-knight/doorGreenStroke.png diff --git a/Resources/sticker-knight/doorRedStroked.png b/Assets/sticker-knight/doorRedStroked.png similarity index 100% rename from Resources/sticker-knight/doorRedStroked.png rename to Assets/sticker-knight/doorRedStroked.png diff --git a/Resources/sticker-knight/doorStroked.png b/Assets/sticker-knight/doorStroked.png similarity index 100% rename from Resources/sticker-knight/doorStroked.png rename to Assets/sticker-knight/doorStroked.png diff --git a/Resources/sticker-knight/earthWall.png b/Assets/sticker-knight/earthWall.png similarity index 100% rename from Resources/sticker-knight/earthWall.png rename to Assets/sticker-knight/earthWall.png diff --git a/Resources/sticker-knight/earthWall2.png b/Assets/sticker-knight/earthWall2.png similarity index 100% rename from Resources/sticker-knight/earthWall2.png rename to Assets/sticker-knight/earthWall2.png diff --git a/Resources/sticker-knight/exit.png b/Assets/sticker-knight/exit.png similarity index 100% rename from Resources/sticker-knight/exit.png rename to Assets/sticker-knight/exit.png diff --git a/Resources/sticker-knight/flare.png b/Assets/sticker-knight/flare.png similarity index 100% rename from Resources/sticker-knight/flare.png rename to Assets/sticker-knight/flare.png diff --git a/Resources/sticker-knight/gemBlueStroked.png b/Assets/sticker-knight/gemBlueStroked.png similarity index 100% rename from Resources/sticker-knight/gemBlueStroked.png rename to Assets/sticker-knight/gemBlueStroked.png diff --git a/Resources/sticker-knight/gemRedStroked.png b/Assets/sticker-knight/gemRedStroked.png similarity index 100% rename from Resources/sticker-knight/gemRedStroked.png rename to Assets/sticker-knight/gemRedStroked.png diff --git a/Resources/sticker-knight/grassLarge.png b/Assets/sticker-knight/grassLarge.png similarity index 100% rename from Resources/sticker-knight/grassLarge.png rename to Assets/sticker-knight/grassLarge.png diff --git a/Resources/sticker-knight/grassSmall.png b/Assets/sticker-knight/grassSmall.png similarity index 100% rename from Resources/sticker-knight/grassSmall.png rename to Assets/sticker-knight/grassSmall.png diff --git a/Resources/sticker-knight/grey.png b/Assets/sticker-knight/grey.png similarity index 100% rename from Resources/sticker-knight/grey.png rename to Assets/sticker-knight/grey.png diff --git a/Resources/sticker-knight/hero.png b/Assets/sticker-knight/hero.png similarity index 100% rename from Resources/sticker-knight/hero.png rename to Assets/sticker-knight/hero.png diff --git a/Resources/sticker-knight/keyGreenStroked.png b/Assets/sticker-knight/keyGreenStroked.png similarity index 100% rename from Resources/sticker-knight/keyGreenStroked.png rename to Assets/sticker-knight/keyGreenStroked.png diff --git a/Resources/sticker-knight/keyRedStroked.png b/Assets/sticker-knight/keyRedStroked.png similarity index 100% rename from Resources/sticker-knight/keyRedStroked.png rename to Assets/sticker-knight/keyRedStroked.png diff --git a/Resources/sticker-knight/keyYellowStroked.png b/Assets/sticker-knight/keyYellowStroked.png similarity index 100% rename from Resources/sticker-knight/keyYellowStroked.png rename to Assets/sticker-knight/keyYellowStroked.png diff --git a/Resources/sticker-knight/platform1.png b/Assets/sticker-knight/platform1.png similarity index 100% rename from Resources/sticker-knight/platform1.png rename to Assets/sticker-knight/platform1.png diff --git a/Resources/sticker-knight/platform2.png b/Assets/sticker-knight/platform2.png similarity index 100% rename from Resources/sticker-knight/platform2.png rename to Assets/sticker-knight/platform2.png diff --git a/Resources/sticker-knight/platform3.png b/Assets/sticker-knight/platform3.png similarity index 100% rename from Resources/sticker-knight/platform3.png rename to Assets/sticker-knight/platform3.png diff --git a/Resources/sticker-knight/platform4.png b/Assets/sticker-knight/platform4.png similarity index 100% rename from Resources/sticker-knight/platform4.png rename to Assets/sticker-knight/platform4.png diff --git a/Resources/sticker-knight/platformBase1.png b/Assets/sticker-knight/platformBase1.png similarity index 100% rename from Resources/sticker-knight/platformBase1.png rename to Assets/sticker-knight/platformBase1.png diff --git a/Resources/sticker-knight/platformBase2.png b/Assets/sticker-knight/platformBase2.png similarity index 100% rename from Resources/sticker-knight/platformBase2.png rename to Assets/sticker-knight/platformBase2.png diff --git a/Resources/sticker-knight/platformBase3.png b/Assets/sticker-knight/platformBase3.png similarity index 100% rename from Resources/sticker-knight/platformBase3.png rename to Assets/sticker-knight/platformBase3.png diff --git a/Resources/sticker-knight/platformBase4.png b/Assets/sticker-knight/platformBase4.png similarity index 100% rename from Resources/sticker-knight/platformBase4.png rename to Assets/sticker-knight/platformBase4.png diff --git a/Resources/sticker-knight/platformBlock1.png b/Assets/sticker-knight/platformBlock1.png similarity index 100% rename from Resources/sticker-knight/platformBlock1.png rename to Assets/sticker-knight/platformBlock1.png diff --git a/Resources/sticker-knight/platformBlock2.png b/Assets/sticker-knight/platformBlock2.png similarity index 100% rename from Resources/sticker-knight/platformBlock2.png rename to Assets/sticker-knight/platformBlock2.png diff --git a/Resources/sticker-knight/platformBlock3.png b/Assets/sticker-knight/platformBlock3.png similarity index 100% rename from Resources/sticker-knight/platformBlock3.png rename to Assets/sticker-knight/platformBlock3.png diff --git a/Resources/sticker-knight/platformBlock4.png b/Assets/sticker-knight/platformBlock4.png similarity index 100% rename from Resources/sticker-knight/platformBlock4.png rename to Assets/sticker-knight/platformBlock4.png diff --git a/Resources/sticker-knight/platformConnector1.png b/Assets/sticker-knight/platformConnector1.png similarity index 100% rename from Resources/sticker-knight/platformConnector1.png rename to Assets/sticker-knight/platformConnector1.png diff --git a/Resources/sticker-knight/platformConnector2.png b/Assets/sticker-knight/platformConnector2.png similarity index 100% rename from Resources/sticker-knight/platformConnector2.png rename to Assets/sticker-knight/platformConnector2.png diff --git a/Resources/sticker-knight/platformConnector3.png b/Assets/sticker-knight/platformConnector3.png similarity index 100% rename from Resources/sticker-knight/platformConnector3.png rename to Assets/sticker-knight/platformConnector3.png diff --git a/Resources/sticker-knight/platformConnector4.png b/Assets/sticker-knight/platformConnector4.png similarity index 100% rename from Resources/sticker-knight/platformConnector4.png rename to Assets/sticker-knight/platformConnector4.png diff --git a/Resources/sticker-knight/pushBlock1.png b/Assets/sticker-knight/pushBlock1.png similarity index 100% rename from Resources/sticker-knight/pushBlock1.png rename to Assets/sticker-knight/pushBlock1.png diff --git a/Resources/sticker-knight/pushBlock2.png b/Assets/sticker-knight/pushBlock2.png similarity index 100% rename from Resources/sticker-knight/pushBlock2.png rename to Assets/sticker-knight/pushBlock2.png diff --git a/Resources/sticker-knight/pushBlock3.png b/Assets/sticker-knight/pushBlock3.png similarity index 100% rename from Resources/sticker-knight/pushBlock3.png rename to Assets/sticker-knight/pushBlock3.png diff --git a/Resources/sticker-knight/shadow.png b/Assets/sticker-knight/shadow.png similarity index 100% rename from Resources/sticker-knight/shadow.png rename to Assets/sticker-knight/shadow.png diff --git a/Resources/sticker-knight/shieldStroked.png b/Assets/sticker-knight/shieldStroked.png similarity index 100% rename from Resources/sticker-knight/shieldStroked.png rename to Assets/sticker-knight/shieldStroked.png diff --git a/Resources/sticker-knight/sign.png b/Assets/sticker-knight/sign.png similarity index 100% rename from Resources/sticker-knight/sign.png rename to Assets/sticker-knight/sign.png diff --git a/Resources/sticker-knight/sk1-32x32.tmx b/Assets/sticker-knight/sk1-32x32.tmx similarity index 96% rename from Resources/sticker-knight/sk1-32x32.tmx rename to Assets/sticker-knight/sk1-32x32.tmx index 52e0dec4..9152282d 100644 --- a/Resources/sticker-knight/sk1-32x32.tmx +++ b/Assets/sticker-knight/sk1-32x32.tmx @@ -1,5 +1,5 @@ - + @@ -192,7 +192,7 @@ - + @@ -207,14 +207,14 @@ - + - + @@ -337,7 +337,7 @@ - + @@ -368,7 +368,7 @@ - + @@ -377,7 +377,7 @@ - + @@ -396,20 +396,20 @@ - + - + - + @@ -419,10 +419,10 @@ - + - + diff --git a/Resources/sticker-knight/sk2-32x32.tmx b/Assets/sticker-knight/sk2-32x32.tmx similarity index 98% rename from Resources/sticker-knight/sk2-32x32.tmx rename to Assets/sticker-knight/sk2-32x32.tmx index 7b42bfe0..f98ced17 100644 --- a/Resources/sticker-knight/sk2-32x32.tmx +++ b/Assets/sticker-knight/sk2-32x32.tmx @@ -1,7 +1,8 @@ - + + @@ -456,7 +457,7 @@ - + @@ -468,7 +469,7 @@ - + diff --git a/Resources/sticker-knight/skeleton.png b/Assets/sticker-knight/skeleton.png similarity index 100% rename from Resources/sticker-knight/skeleton.png rename to Assets/sticker-knight/skeleton.png diff --git a/Resources/sticker-knight/swordStroked.png b/Assets/sticker-knight/swordStroked.png similarity index 100% rename from Resources/sticker-knight/swordStroked.png rename to Assets/sticker-knight/swordStroked.png diff --git a/Resources/sticker-knight/torch.png b/Assets/sticker-knight/torch.png similarity index 100% rename from Resources/sticker-knight/torch.png rename to Assets/sticker-knight/torch.png diff --git a/Resources/sticker-knight/trap.png b/Assets/sticker-knight/trap.png similarity index 100% rename from Resources/sticker-knight/trap.png rename to Assets/sticker-knight/trap.png diff --git a/Resources/sticker-knight/wallDecor1.png b/Assets/sticker-knight/wallDecor1.png similarity index 100% rename from Resources/sticker-knight/wallDecor1.png rename to Assets/sticker-knight/wallDecor1.png diff --git a/Resources/sticker-knight/wallDecor2.png b/Assets/sticker-knight/wallDecor2.png similarity index 100% rename from Resources/sticker-knight/wallDecor2.png rename to Assets/sticker-knight/wallDecor2.png diff --git a/Resources/sticker-knight/wallDecor3.png b/Assets/sticker-knight/wallDecor3.png similarity index 100% rename from Resources/sticker-knight/wallDecor3.png rename to Assets/sticker-knight/wallDecor3.png diff --git a/Resources/sticker-knight/window1.png b/Assets/sticker-knight/window1.png similarity index 100% rename from Resources/sticker-knight/window1.png rename to Assets/sticker-knight/window1.png diff --git a/Resources/sticker-knight/window2.png b/Assets/sticker-knight/window2.png similarity index 100% rename from Resources/sticker-knight/window2.png rename to Assets/sticker-knight/window2.png diff --git a/Resources/sticker-knight/window3.png b/Assets/sticker-knight/window3.png similarity index 100% rename from Resources/sticker-knight/window3.png rename to Assets/sticker-knight/window3.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cd1f55a..02283f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,63 @@ Change Log ========== + +1.20 +----- + +#### Changes + +- optimized tile data storage for faster updates +- support for Tiled templates +- add tvOS demo & framework targets +- Xcode version is 10 +- Swift version is now 4.2 +- Requirements updated: + - macOS: 10.13 + - iOS: 11.0 + - tvOS: 12.0 +- fix spritesheet height bug if excess space existed at image bottom +- fix tile clamping bug that shifts position slightly over time +- fix a bug where querying tiles with a global id returned an improper result +- fix a bug where tile object's tile type wasn't looking for delegate type +- fix a bug where `SKTiledObject` objects improperly parse double arrays +- add `TileRenderMode` flag +- add `TileUpdateMode` flag +- `SKTilemap` & `SKTiledLayerObject` nodes are now subclassed from `SKEffectNode` + - add `SKTilemap.setShader` method + - add `SKTiledLayerObject.setShader` method +- add `SKTiledGeometry` protocol +- add `SKTilemap.getTileset(forTile:)` method +- add `SKTilemap.tileObjects(globalID:)` method +- add `SKTilemap.isValid(coord:)` method +- add `SKTilesetDataSource` delegate +- add `SKTiledSceneCamera.ignoreZoomClamping` flag +- add `SKTiledSceneCamera.ignoreZoomConstraints` flag +- add `SKTiledSceneCamera.notifyDelegatesOnContainedNodesChange` flag +- add `SKTiledSceneCameraDelegate.containedNodesChanged` protocol method +- add `SKTileLayer.tileAt(point:offset:)` method +- add `SKTile.renderFlags` property +- add `SKTileset.delegate` property +- add `SKTileset.setDataTexture(_:imageNamed)` +- `SKTileset.setDataTexture` now returns the previous texture +- add `SKTilesetData.animationAction` property +- add `SKTilesetData.name` property +- renamed `AnimationFrame` -> `TileAnimationFrame` +- add `SKTiledSceneCamera.allowGestures` attribute +- add `SKTiledSceneCamera.setupGestures(for:)` method +- `SKTiledScene.setup` completion handler passes tilemap as argument +- add `SKTilemap.vectorCoordinateForPoint` method. +- add `SKTiledLayerObject.vectorCoordinateForPoint` method. +- `SKTiledObject.boolForKey` , `SKTiledObject.intForKey` & `SKTiledObject.doubleForKey` are now public methods. +- removed `SKTiledSceneCameraDelegate` default methods; protocol methods are now optional +- renamed `SKTiledObject.objectType` -> `SKTiledObject.shapeType` +- renamed `SKObjectGroup.drawObjects` -> `SKObjectGroup.draw` + +#### Breaking + +- nothing + + 1.16 ----- @@ -12,7 +69,8 @@ Change Log - tile animations will respond to `SKTilemap` speed changes, and even run backwards - add `SKTiledSceneCamera.setCameraBounds(bounds:)` - add `SKTileset.getAnimatedTileData` -- add `SKTileset.renderTileData` +- add `SKTileset.setupAnimatedTileData` +- add `SKTileset.getGlobalID(id:)` - add `SKTilesetData.frameAt(index:)` - add `SKTilesetData.setTexture(_:forFrame:)` - add `SKTilesetData.setDuration(interval:forFrame:)` @@ -23,6 +81,11 @@ Change Log - remove `SKTile.pauseAnimation` +#### Breaking + +- animated tiles will no longer render independently; `SKTilemap` node must be added to the `SKScene.update` loop + + 1.15 ----- @@ -74,7 +137,6 @@ Change Log - debug functions moved to `SKTiled+Debug.swift` - add `SKObjectGroup.textObjects` - add `SKTilemap.textObjects` -- add `SKTilemap.showGrid` - add `SKTilemap.showBounds` - add `SKObjectGroup.getObjects(withText:)` - add `SKTilemap.getContentLayers()` diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Back.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Back.png new file mode 100644 index 00000000..2bde6f7a Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Back.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Back@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Back@2x.png new file mode 100644 index 00000000..d383717c Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Back@2x.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..43893b70 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Back.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Back@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..d7b50068 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-center" : { + "x" : 640, + "y" : 384 + } + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 00000000..ca2bbc2c --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,20 @@ +{ + "layers" : [ + { + "filename" : "Front-Close.imagestacklayer" + }, + { + "filename" : "Front-Rear.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..34e1856b --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Front-Close.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Front-Close@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Front-Close.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Front-Close.png new file mode 100644 index 00000000..669ffde4 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Front-Close.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Front-Close@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Front-Close@2x.png new file mode 100644 index 00000000..232f01f2 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Content.imageset/Front-Close@2x.png differ diff --git a/Demo/Assets.xcassets/UI/Buttons/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Contents.json similarity index 100% rename from Demo/Assets.xcassets/UI/Buttons/Contents.json rename to Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Close.imagestacklayer/Contents.json diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..af80b42f --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Front-Rear.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Front-Rear@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front-Rear.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front-Rear.png new file mode 100644 index 00000000..73603539 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front-Rear.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front-Rear@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front-Rear@2x.png new file mode 100644 index 00000000..2410136c Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front-Rear@2x.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Contents.json new file mode 100644 index 00000000..d7b50068 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front-Rear.imagestacklayer/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-center" : { + "x" : 640, + "y" : 384 + } + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..a0d18f67 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Middle.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Middle@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png new file mode 100644 index 00000000..6c5a7520 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Middle@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Middle@2x.png new file mode 100644 index 00000000..975afc10 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Middle@2x.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..73e84155 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-center" : { + "x" : 640, + "y" : 393.5 + } + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Back.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Back.png new file mode 100644 index 00000000..fe0c21f0 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Back.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Back@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Back@2x.png new file mode 100644 index 00000000..99ed9684 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Back@2x.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..43893b70 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Back.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Back@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..ab55eb49 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,16 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "frame-size" : { + "height" : 240, + "width" : 400 + }, + "frame-center" : { + "y" : 120, + "x" : 200 + } + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 00000000..8a9e9872 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,26 @@ +{ + "layers" : [ + { + "filename" : "Front-Close.imagestacklayer" + }, + { + "filename" : "Front-Rear.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "canvasSize" : { + "width" : 400, + "height" : 240 + } + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..93189c02 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Top.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Top@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Top.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Top.png new file mode 100644 index 00000000..613214f8 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Top.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Top@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Top@2x.png new file mode 100644 index 00000000..e8816886 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Content.imageset/Top@2x.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Close.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..5f48a78b --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Front.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Front@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front.png new file mode 100644 index 00000000..a78b304b Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front@2x.png new file mode 100644 index 00000000..5041872d Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Content.imageset/Front@2x.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Contents.json new file mode 100644 index 00000000..1a4841e4 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front-Rear.imagestacklayer/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-center" : { + "x" : 200, + "y" : 120 + } + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..a0d18f67 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Middle.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Middle@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png new file mode 100644 index 00000000..6392631e Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle@2x.png new file mode 100644 index 00000000..69d15f20 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Middle@2x.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..7d8a7a6a --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-center" : { + "x" : 200, + "y" : 123 + } + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 00000000..80a55ba5 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "assets" : [ + { + "filename" : "App Icon - App Store.imagestack", + "size" : "1280x768", + "idiom" : "tv", + "role" : "primary-app-icon" + }, + { + "filename" : "App Icon.imagestack", + "size" : "400x240", + "idiom" : "tv", + "role" : "primary-app-icon" + }, + { + "filename" : "Top Shelf Image.imageset", + "size" : "1920x720", + "idiom" : "tv", + "role" : "top-shelf-image" + }, + { + "filename" : "Top Shelf Image Wide.imageset", + "size" : "2320x720", + "idiom" : "tv", + "role" : "top-shelf-image-wide" + } + ] +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 00000000..b2a55af3 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Top Shelf Wide - tvOS.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Top Shelf Wide - tvOS@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide - tvOS.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide - tvOS.png new file mode 100644 index 00000000..75db5978 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide - tvOS.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide - tvOS@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide - tvOS@2x.png new file mode 100644 index 00000000..6943e8cd Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide - tvOS@2x.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000..80cfbc64 --- /dev/null +++ b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "Top Shelf - tvOS.png", + "scale" : "1x" + }, + { + "idiom" : "tv", + "filename" : "Top Shelf - tvOS@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf - tvOS.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf - tvOS.png new file mode 100644 index 00000000..36f6a803 Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf - tvOS.png differ diff --git a/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf - tvOS@2x.png b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf - tvOS@2x.png new file mode 100644 index 00000000..cafb4dcc Binary files /dev/null and b/Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf - tvOS@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json index c5d32414..f8defbcc 100644 --- a/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,215 +1,217 @@ { - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "NotificationIcon@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "NotificationIcon@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-Small.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-Small@2x.png", - "scale" : "2x" - }, + "images" : [{ + "idiom" : "mac", + "filename" : "icon_16x16.png", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "filename" : "icon_16x16@2x.png", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "filename" : "icon_32x32.png", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "filename" : "icon_32x32@2x.png", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "filename" : "icon_128x128.png", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "filename" : "icon_128x128@2x.png", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "filename" : "icon_256x256.png", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "filename" : "icon_256x256@2x.png", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "filename" : "icon_512x512.png", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "filename" : "icon_512x512@2x.png", + "scale" : "2x", + "size" : "512x512" +}, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-Small@3x.png", - "scale" : "3x" + "idiom" : "ipad", + "filename" : "Icon-40.png", + "scale" : "1x", + "size" : "40x40" }, { - "size" : "40x40", - "idiom" : "iphone", + "idiom" : "ipad", "filename" : "Icon-40@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { - "size" : "40x40", "idiom" : "iphone", - "filename" : "Icon-40@3x.png", - "scale" : "3x" + "filename" : "Icon-60@2x.png", + "scale" : "2x", + "size" : "60x60" }, { - "size" : "57x57", - "idiom" : "iphone", - "filename" : "Icon.png", - "scale" : "1x" + "idiom" : "ipad", + "filename" : "Icon-72.png", + "scale" : "1x", + "size" : "72x72" }, { - "size" : "57x57", - "idiom" : "iphone", - "filename" : "Icon@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "filename" : "Icon-72@2x.png", + "scale" : "2x", + "size" : "72x72" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-60@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x", + "size" : "76x76" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-60@3x.png", - "scale" : "3x" + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x", + "size" : "76x76" }, { - "size" : "20x20", "idiom" : "ipad", - "filename" : "NotificationIcon~ipad.png", - "scale" : "1x" + "filename" : "Icon-Small-50.png", + "scale" : "1x", + "size" : "50x50" }, { - "size" : "20x20", "idiom" : "ipad", - "filename" : "NotificationIcon~ipad@2x.png", - "scale" : "2x" + "filename" : "Icon-Small-50@2x.png", + "scale" : "2x", + "size" : "50x50" }, { - "size" : "29x29", - "idiom" : "ipad", + "idiom" : "iphone", "filename" : "Icon-Small.png", - "scale" : "1x" + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "ipad", + "idiom" : "iphone", "filename" : "Icon-Small@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-40.png", - "scale" : "1x" + "idiom" : "iphone", + "filename" : "Icon.png", + "scale" : "1x", + "size" : "57x57" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-40@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "filename" : "Icon@2x.png", + "scale" : "2x", + "size" : "57x57" }, { - "size" : "50x50", - "idiom" : "ipad", - "filename" : "Icon-Small-50.png", - "scale" : "1x" + "idiom" : "iphone", + "filename" : "Icon-Small@3x.png", + "scale" : "3x", + "size" : "29x29" }, { - "size" : "50x50", - "idiom" : "ipad", - "filename" : "Icon-Small-50@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "filename" : "Icon-40@3x.png", + "scale" : "3x", + "size" : "40x40" }, { - "size" : "72x72", - "idiom" : "ipad", - "filename" : "Icon-72.png", - "scale" : "1x" + "idiom" : "iphone", + "filename" : "Icon-60@3x.png", + "scale" : "3x", + "size" : "60x60" }, { - "size" : "72x72", - "idiom" : "ipad", - "filename" : "Icon-72@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "filename" : "Icon-40@2x.png", + "scale" : "2x", + "size" : "40x40" }, { - "size" : "76x76", "idiom" : "ipad", - "filename" : "Icon-76.png", - "scale" : "1x" + "filename" : "Icon-Small.png", + "scale" : "1x", + "size" : "29x29" }, { - "size" : "76x76", "idiom" : "ipad", - "filename" : "Icon-76@2x.png", - "scale" : "2x" + "filename" : "Icon-Small@2x.png", + "scale" : "2x", + "size" : "29x29" }, { - "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "icon_16x16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "icon_16x16@2x.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "icon_32x32.png", - "scale" : "1x" + "scale" : "2x", + "size" : "83.5x83.5" }, { - "size" : "32x32", - "idiom" : "mac", - "filename" : "icon_32x32@2x.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "icon_128x128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "icon_128x128@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "filename" : "NotificationIcon@2x.png", + "scale" : "2x", + "size" : "20x20" }, { - "size" : "256x256", - "idiom" : "mac", - "filename" : "icon_256x256.png", - "scale" : "1x" + "idiom" : "iphone", + "filename" : "NotificationIcon@3x.png", + "scale" : "3x", + "size" : "20x20" }, { - "size" : "256x256", - "idiom" : "mac", - "filename" : "icon_256x256@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "filename" : "NotificationIcon~ipad.png", + "scale" : "1x", + "size" : "20x20" }, { - "size" : "512x512", - "idiom" : "mac", - "filename" : "icon_512x512.png", - "scale" : "1x" + "idiom" : "ipad", + "filename" : "NotificationIcon~ipad@2x.png", + "scale" : "2x", + "size" : "20x20" }, { - "size" : "512x512", - "idiom" : "mac", - "filename" : "icon_512x512@2x.png", - "scale" : "2x" + "idiom" : "ios-marketing", + "filename" : "ios-marketing.png", + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "pre-rendered" : true + "author" : "xcode", + "version" : 1 } } diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40.png index b6433ba6..88272f17 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png index b2f008d6..75a56edf 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png index 1c296b95..35b1e9de 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png index 1c296b95..35b1e9de 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png index c6ad9efb..7baeba6f 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-72.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-72.png index b9af4cba..c952f870 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-72.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-72.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png index b3e2e6ae..ca228e4d 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-76.png index 0ffe36f0..cbf8ac66 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-76.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png index 7c1faa21..552282bb 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png index 6375ca9b..8c54a7e8 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png index fbc0b2c9..f7ecf465 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png index 781ffd43..2fa9e57b 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small.png index 2865e993..071279ff 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png index eaf0025f..9eb24622 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png index a7879f32..809510e5 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon.png index b9bcdec1..206574ae 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/Icon@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/Icon@2x.png index 4bde7e21..389aaf57 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/Icon@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/Icon@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png index b6433ba6..88272f17 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png b/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png index 151d74c5..43f34fe1 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png b/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png index 3d19e81b..231d7487 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png and b/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png index b6433ba6..88272f17 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128.png index 2db11c38..2de479b0 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png index f6293ff8..b9e4ef39 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16.png index 7b350ab1..5838b8a4 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png index b4cccd0d..3b1cd143 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256.png index f6293ff8..b9e4ef39 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png index e0686dbe..d9cf91cd 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32.png index b4cccd0d..3b1cd143 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png index 5d12d5f8..3cd8d58f 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512.png index e0686dbe..d9cf91cd 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png index 99c7fc2f..41d7ff29 100644 Binary files a/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png and b/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/Demo/Assets.xcassets/AppIcon.appiconset/ios-marketing.png b/Demo/Assets.xcassets/AppIcon.appiconset/ios-marketing.png new file mode 100644 index 00000000..c034bd74 Binary files /dev/null and b/Demo/Assets.xcassets/AppIcon.appiconset/ios-marketing.png differ diff --git a/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm.png b/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm.png deleted file mode 100644 index 3edc4d62..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm@2x.png b/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm@2x.png deleted file mode 100644 index a3ec993d..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm@3x.png b/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm@3x.png deleted file mode 100644 index f5c36890..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/draw-button-norm@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm.png b/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm.png deleted file mode 100644 index 6e2524fa..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm@2x.png b/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm@2x.png deleted file mode 100644 index 6b8b50fe..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm@3x.png b/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm@3x.png deleted file mode 100644 index fff45cd8..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/graph-button-norm@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm.png b/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm.png deleted file mode 100644 index e4a5cfc2..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm@2x.png b/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm@2x.png deleted file mode 100644 index 74e4f8a9..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm@3x.png b/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm@3x.png deleted file mode 100644 index f9f96e42..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/grid-button-norm@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm.png b/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm.png deleted file mode 100644 index 19815e3f..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm@2x.png b/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm@2x.png deleted file mode 100644 index b2260fca..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm@3x.png b/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm@3x.png deleted file mode 100644 index 75169a82..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/next-button-norm@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/Contents.json b/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/Contents.json deleted file mode 100644 index 8f4bed00..00000000 --- a/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "objects-button-norm.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "objects-button-norm@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "objects-button-norm@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm.png b/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm.png deleted file mode 100644 index 10c64788..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm@2x.png b/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm@2x.png deleted file mode 100644 index 0d8cbb35..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm@3x.png b/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm@3x.png deleted file mode 100644 index 4f496bc8..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/objects-button-norm.imageset/objects-button-norm@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/Contents.json b/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/Contents.json deleted file mode 100644 index e46ea02e..00000000 --- a/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "reset-button-norm.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "reset-button-norm@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "reset-button-norm@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm.png b/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm.png deleted file mode 100644 index 5410c11d..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm@2x.png b/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm@2x.png deleted file mode 100644 index 4b515278..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm@3x.png b/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm@3x.png deleted file mode 100644 index b476265d..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/reset-button-norm.imageset/reset-button-norm@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/Contents.json b/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/Contents.json deleted file mode 100644 index 9eb5cc0d..00000000 --- a/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "scale-button-norm.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "scale-button-norm@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "scale-button-norm@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm.png b/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm.png deleted file mode 100644 index f6f9116b..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm@2x.png b/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm@2x.png deleted file mode 100644 index 552d87d5..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm@3x.png b/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm@3x.png deleted file mode 100644 index 980a838a..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/scale-button-norm.imageset/scale-button-norm@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/Contents.json b/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/Contents.json deleted file mode 100644 index cee363ac..00000000 --- a/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "scale-button-pressed.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "scale-button-pressed@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "scale-button-pressed@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed.png b/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed.png deleted file mode 100644 index 152b4694..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed@2x.png b/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed@2x.png deleted file mode 100644 index 1b637c79..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed@3x.png b/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed@3x.png deleted file mode 100644 index d907d4aa..00000000 Binary files a/Demo/Assets.xcassets/UI/Buttons/scale-button-pressed.imageset/scale-button-pressed@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Header.imageset/Contents.json b/Demo/Assets.xcassets/UI/Header.imageset/Contents.json index c832a20d..60483063 100644 --- a/Demo/Assets.xcassets/UI/Header.imageset/Contents.json +++ b/Demo/Assets.xcassets/UI/Header.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "Header-@1x.png", + "filename" : "Header.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "Header-@2x.png", + "filename" : "Header@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "Header-@3x.png", + "filename" : "Header@3x.png", "scale" : "3x" } ], diff --git a/Demo/Assets.xcassets/UI/Header.imageset/Header-@1x.png b/Demo/Assets.xcassets/UI/Header.imageset/Header-@1x.png deleted file mode 100644 index 01a88f53..00000000 Binary files a/Demo/Assets.xcassets/UI/Header.imageset/Header-@1x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Header.imageset/Header-@2x.png b/Demo/Assets.xcassets/UI/Header.imageset/Header-@2x.png deleted file mode 100644 index c43e8973..00000000 Binary files a/Demo/Assets.xcassets/UI/Header.imageset/Header-@2x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Header.imageset/Header-@3x.png b/Demo/Assets.xcassets/UI/Header.imageset/Header-@3x.png deleted file mode 100644 index eef25d2a..00000000 Binary files a/Demo/Assets.xcassets/UI/Header.imageset/Header-@3x.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/Header.imageset/Header.png b/Demo/Assets.xcassets/UI/Header.imageset/Header.png new file mode 100644 index 00000000..bf779c48 Binary files /dev/null and b/Demo/Assets.xcassets/UI/Header.imageset/Header.png differ diff --git a/Demo/Assets.xcassets/UI/Header.imageset/Header@2x.png b/Demo/Assets.xcassets/UI/Header.imageset/Header@2x.png new file mode 100644 index 00000000..ed0d1781 Binary files /dev/null and b/Demo/Assets.xcassets/UI/Header.imageset/Header@2x.png differ diff --git a/Demo/Assets.xcassets/UI/Header.imageset/Header@3x.png b/Demo/Assets.xcassets/UI/Header.imageset/Header@3x.png new file mode 100644 index 00000000..e29232a3 Binary files /dev/null and b/Demo/Assets.xcassets/UI/Header.imageset/Header@3x.png differ diff --git a/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/Contents.json b/Demo/Assets.xcassets/UI/actions.imageset/Contents.json similarity index 66% rename from Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/Contents.json rename to Demo/Assets.xcassets/UI/actions.imageset/Contents.json index bfa1d3c9..5fc5cd0b 100644 --- a/Demo/Assets.xcassets/UI/Buttons/draw-button-norm.imageset/Contents.json +++ b/Demo/Assets.xcassets/UI/actions.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "draw-button-norm.png", + "filename" : "actions.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "draw-button-norm@2x.png", + "filename" : "actions@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "draw-button-norm@3x.png", + "filename" : "actions@3x.png", "scale" : "3x" } ], diff --git a/Demo/Assets.xcassets/UI/actions.imageset/actions.png b/Demo/Assets.xcassets/UI/actions.imageset/actions.png new file mode 100644 index 00000000..cac340b6 Binary files /dev/null and b/Demo/Assets.xcassets/UI/actions.imageset/actions.png differ diff --git a/Demo/Assets.xcassets/UI/actions.imageset/actions@2x.png b/Demo/Assets.xcassets/UI/actions.imageset/actions@2x.png new file mode 100644 index 00000000..3a1cc3d7 Binary files /dev/null and b/Demo/Assets.xcassets/UI/actions.imageset/actions@2x.png differ diff --git a/Demo/Assets.xcassets/UI/actions.imageset/actions@3x.png b/Demo/Assets.xcassets/UI/actions.imageset/actions@3x.png new file mode 100644 index 00000000..6af571ea Binary files /dev/null and b/Demo/Assets.xcassets/UI/actions.imageset/actions@3x.png differ diff --git a/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/Contents.json b/Demo/Assets.xcassets/UI/clamping.imageset/Contents.json similarity index 66% rename from Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/Contents.json rename to Demo/Assets.xcassets/UI/clamping.imageset/Contents.json index cee6c789..49718aa3 100644 --- a/Demo/Assets.xcassets/UI/Buttons/grid-button-norm.imageset/Contents.json +++ b/Demo/Assets.xcassets/UI/clamping.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "grid-button-norm.png", + "filename" : "clamping.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "grid-button-norm@2x.png", + "filename" : "clamping@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "grid-button-norm@3x.png", + "filename" : "clamping@3x.png", "scale" : "3x" } ], diff --git a/Demo/Assets.xcassets/UI/clamping.imageset/clamping.png b/Demo/Assets.xcassets/UI/clamping.imageset/clamping.png new file mode 100644 index 00000000..7891ed66 Binary files /dev/null and b/Demo/Assets.xcassets/UI/clamping.imageset/clamping.png differ diff --git a/Demo/Assets.xcassets/UI/clamping.imageset/clamping@2x.png b/Demo/Assets.xcassets/UI/clamping.imageset/clamping@2x.png new file mode 100644 index 00000000..7fa16a99 Binary files /dev/null and b/Demo/Assets.xcassets/UI/clamping.imageset/clamping@2x.png differ diff --git a/Demo/Assets.xcassets/UI/clamping.imageset/clamping@3x.png b/Demo/Assets.xcassets/UI/clamping.imageset/clamping@3x.png new file mode 100644 index 00000000..f1ec2860 Binary files /dev/null and b/Demo/Assets.xcassets/UI/clamping.imageset/clamping@3x.png differ diff --git a/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/Contents.json b/Demo/Assets.xcassets/UI/dolly.imageset/Contents.json similarity index 66% rename from Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/Contents.json rename to Demo/Assets.xcassets/UI/dolly.imageset/Contents.json index ba4298e8..d6c16a92 100644 --- a/Demo/Assets.xcassets/UI/Buttons/next-button-norm.imageset/Contents.json +++ b/Demo/Assets.xcassets/UI/dolly.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "next-button-norm.png", + "filename" : "dolly.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "next-button-norm@2x.png", + "filename" : "dolly@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "next-button-norm@3x.png", + "filename" : "dolly@3x.png", "scale" : "3x" } ], diff --git a/Demo/Assets.xcassets/UI/dolly.imageset/dolly.png b/Demo/Assets.xcassets/UI/dolly.imageset/dolly.png new file mode 100644 index 00000000..04f71cd5 Binary files /dev/null and b/Demo/Assets.xcassets/UI/dolly.imageset/dolly.png differ diff --git a/Demo/Assets.xcassets/UI/dolly.imageset/dolly@2x.png b/Demo/Assets.xcassets/UI/dolly.imageset/dolly@2x.png new file mode 100644 index 00000000..5044bfeb Binary files /dev/null and b/Demo/Assets.xcassets/UI/dolly.imageset/dolly@2x.png differ diff --git a/Demo/Assets.xcassets/UI/dolly.imageset/dolly@3x.png b/Demo/Assets.xcassets/UI/dolly.imageset/dolly@3x.png new file mode 100644 index 00000000..670eac3f Binary files /dev/null and b/Demo/Assets.xcassets/UI/dolly.imageset/dolly@3x.png differ diff --git a/Demo/Assets.xcassets/UI/effects-default.imageset/Contents.json b/Demo/Assets.xcassets/UI/effects-default.imageset/Contents.json new file mode 100644 index 00000000..e3d4edd7 --- /dev/null +++ b/Demo/Assets.xcassets/UI/effects-default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "effects-default.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "effects-default@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "effects-default@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default.png b/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default.png new file mode 100644 index 00000000..235c98ce Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default.png differ diff --git a/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default@2x.png b/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default@2x.png new file mode 100644 index 00000000..77172e55 Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default@2x.png differ diff --git a/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default@3x.png b/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default@3x.png new file mode 100644 index 00000000..06a91bdf Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-default.imageset/effects-default@3x.png differ diff --git a/Demo/Assets.xcassets/UI/effects-focused.imageset/Contents.json b/Demo/Assets.xcassets/UI/effects-focused.imageset/Contents.json new file mode 100644 index 00000000..9691a37b --- /dev/null +++ b/Demo/Assets.xcassets/UI/effects-focused.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "effects-focused.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "effects-focused@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "effects-focused@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused.png b/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused.png new file mode 100644 index 00000000..1754b8f1 Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused.png differ diff --git a/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused@2x.png b/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused@2x.png new file mode 100644 index 00000000..d40f1f29 Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused@2x.png differ diff --git a/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused@3x.png b/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused@3x.png new file mode 100644 index 00000000..613788fa Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-focused.imageset/effects-focused@3x.png differ diff --git a/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/Contents.json b/Demo/Assets.xcassets/UI/effects-highlight.imageset/Contents.json similarity index 66% rename from Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/Contents.json rename to Demo/Assets.xcassets/UI/effects-highlight.imageset/Contents.json index 91c8acdc..77d60ab8 100644 --- a/Demo/Assets.xcassets/UI/Buttons/graph-button-norm.imageset/Contents.json +++ b/Demo/Assets.xcassets/UI/effects-highlight.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "graph-button-norm.png", + "filename" : "effects-highlight.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "graph-button-norm@2x.png", + "filename" : "effects-highlight@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "graph-button-norm@3x.png", + "filename" : "effects-highlight@3x.png", "scale" : "3x" } ], diff --git a/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight.png b/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight.png new file mode 100644 index 00000000..657a9669 Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight.png differ diff --git a/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight@2x.png b/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight@2x.png new file mode 100644 index 00000000..e31848e0 Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight@2x.png differ diff --git a/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight@3x.png b/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight@3x.png new file mode 100644 index 00000000..db133286 Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-highlight.imageset/effects-highlight@3x.png differ diff --git a/Demo/Assets.xcassets/UI/effects-selected.imageset/Contents.json b/Demo/Assets.xcassets/UI/effects-selected.imageset/Contents.json new file mode 100644 index 00000000..bf288e5f --- /dev/null +++ b/Demo/Assets.xcassets/UI/effects-selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "effects-selected.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "effects-selected@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "effects-selected@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected.png b/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected.png new file mode 100644 index 00000000..1754b8f1 Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected.png differ diff --git a/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected@2x.png b/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected@2x.png new file mode 100644 index 00000000..d40f1f29 Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected@2x.png differ diff --git a/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected@3x.png b/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected@3x.png new file mode 100644 index 00000000..613788fa Binary files /dev/null and b/Demo/Assets.xcassets/UI/effects-selected.imageset/effects-selected@3x.png differ diff --git a/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/Contents.json b/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/Contents.json new file mode 100644 index 00000000..deac9327 --- /dev/null +++ b/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "rotate-device-alt.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "rotate-device-alt@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "rotate-device-alt@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt.png b/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt.png new file mode 100644 index 00000000..908d4e19 Binary files /dev/null and b/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt.png differ diff --git a/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt@2x.png b/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt@2x.png new file mode 100644 index 00000000..10fcf7af Binary files /dev/null and b/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt@2x.png differ diff --git a/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt@3x.png b/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt@3x.png new file mode 100644 index 00000000..d140238f Binary files /dev/null and b/Demo/Assets.xcassets/UI/rotate-device-alt.imageset/rotate-device-alt@3x.png differ diff --git a/Demo/Assets.xcassets/UI/target-100px.imageset/target-100px.png b/Demo/Assets.xcassets/UI/target-100px.imageset/target-100px.png deleted file mode 100644 index 085c7caa..00000000 Binary files a/Demo/Assets.xcassets/UI/target-100px.imageset/target-100px.png and /dev/null differ diff --git a/Demo/Assets.xcassets/UI/target.imageset/Contents.json b/Demo/Assets.xcassets/UI/target.imageset/Contents.json new file mode 100644 index 00000000..448f4bdb --- /dev/null +++ b/Demo/Assets.xcassets/UI/target.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "target.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "target@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "target@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/target.imageset/target.png b/Demo/Assets.xcassets/UI/target.imageset/target.png new file mode 100644 index 00000000..09115953 Binary files /dev/null and b/Demo/Assets.xcassets/UI/target.imageset/target.png differ diff --git a/Demo/Assets.xcassets/UI/target.imageset/target@2x.png b/Demo/Assets.xcassets/UI/target.imageset/target@2x.png new file mode 100644 index 00000000..02545850 Binary files /dev/null and b/Demo/Assets.xcassets/UI/target.imageset/target@2x.png differ diff --git a/Demo/Assets.xcassets/UI/target.imageset/target@3x.png b/Demo/Assets.xcassets/UI/target.imageset/target@3x.png new file mode 100644 index 00000000..66da85aa Binary files /dev/null and b/Demo/Assets.xcassets/UI/target.imageset/target@3x.png differ diff --git a/Demo/Assets.xcassets/UI/update-mode.imageset/Contents.json b/Demo/Assets.xcassets/UI/update-mode.imageset/Contents.json new file mode 100644 index 00000000..58b24949 --- /dev/null +++ b/Demo/Assets.xcassets/UI/update-mode.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "update-mode.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "update-mode@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "update-mode@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode.png b/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode.png new file mode 100644 index 00000000..f7c5188e Binary files /dev/null and b/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode.png differ diff --git a/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode@2x.png b/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode@2x.png new file mode 100644 index 00000000..ec3cc0cb Binary files /dev/null and b/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode@2x.png differ diff --git a/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode@3x.png b/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode@3x.png new file mode 100644 index 00000000..f2b80edb Binary files /dev/null and b/Demo/Assets.xcassets/UI/update-mode.imageset/update-mode@3x.png differ diff --git a/Demo/Assets.xcassets/UI/target-100px.imageset/Contents.json b/Demo/Assets.xcassets/UI/zoom.imageset/Contents.json similarity index 72% rename from Demo/Assets.xcassets/UI/target-100px.imageset/Contents.json rename to Demo/Assets.xcassets/UI/zoom.imageset/Contents.json index e34ab74f..dda34802 100644 --- a/Demo/Assets.xcassets/UI/target-100px.imageset/Contents.json +++ b/Demo/Assets.xcassets/UI/zoom.imageset/Contents.json @@ -2,15 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "target-100px.png", + "filename" : "zoom.png", "scale" : "1x" }, { "idiom" : "universal", + "filename" : "zoom@2x.png", "scale" : "2x" }, { "idiom" : "universal", + "filename" : "zoom@3x.png", "scale" : "3x" } ], @@ -18,4 +20,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/Demo/Assets.xcassets/UI/zoom.imageset/zoom.png b/Demo/Assets.xcassets/UI/zoom.imageset/zoom.png new file mode 100644 index 00000000..35e5da18 Binary files /dev/null and b/Demo/Assets.xcassets/UI/zoom.imageset/zoom.png differ diff --git a/Demo/Assets.xcassets/UI/zoom.imageset/zoom@2x.png b/Demo/Assets.xcassets/UI/zoom.imageset/zoom@2x.png new file mode 100644 index 00000000..c28d5ff0 Binary files /dev/null and b/Demo/Assets.xcassets/UI/zoom.imageset/zoom@2x.png differ diff --git a/Demo/Assets.xcassets/UI/zoom.imageset/zoom@3x.png b/Demo/Assets.xcassets/UI/zoom.imageset/zoom@3x.png new file mode 100644 index 00000000..e9e7d3b1 Binary files /dev/null and b/Demo/Assets.xcassets/UI/zoom.imageset/zoom@3x.png differ diff --git a/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/Contents.json b/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/Contents.json new file mode 100644 index 00000000..011b77dd --- /dev/null +++ b/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "orientation" : "landscape", + "idiom" : "tv", + "filename" : "tvOS-Launch@2x.png", + "extent" : "full-screen", + "minimum-system-version" : "11.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "tv", + "filename" : "tvOS-Launch.png", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/tvOS-Launch.png b/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/tvOS-Launch.png new file mode 100644 index 00000000..3116544a Binary files /dev/null and b/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/tvOS-Launch.png differ diff --git a/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/tvOS-Launch@2x.png b/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/tvOS-Launch@2x.png new file mode 100644 index 00000000..1bd700d9 Binary files /dev/null and b/Demo/Assets.xcassets/tvOS LaunchImage.launchimage/tvOS-Launch@2x.png differ diff --git a/Demo/Demo+Extensions.swift b/Demo/Demo+Extensions.swift new file mode 100644 index 00000000..dd17cb69 --- /dev/null +++ b/Demo/Demo+Extensions.swift @@ -0,0 +1,133 @@ +// +// Demo+Extensions.swift +// SKTiled Demo +// +// Created by Michael Fessenden on 3/28/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + + +import SpriteKit +import GameController + + +extension Notification.Name { + + public struct Demo { + public static let ReloadScene = Notification.Name(rawValue: "com.sktiled.notification.name.demo.reloadScene") + public static let LoadNextScene = Notification.Name(rawValue: "com.sktiled.notification.name.demo.loadNextScene") + public static let FlushScene = Notification.Name(rawValue: "com.sktiled.notification.name.demo.flushScene") + public static let LoadPreviousScene = Notification.Name(rawValue: "com.sktiled.notification.name.demo.loadPreviousScene") + public static let UpdateDebugging = Notification.Name(rawValue: "com.sktiled.notification.name.demo.updateDebugging") + public static let SceneLoaded = Notification.Name(rawValue: "com.sktiled.notification.name.demo.sceneLoaded") + public static let FocusObjectsChanged = Notification.Name(rawValue: "com.sktiled.notification.name.demo.focusObjectsChanged") + public static let WindowTitleUpdated = Notification.Name(rawValue: "com.sktiled.notification.name.demo.windowTitleUpdated") + public static let TileUnderCursor = Notification.Name(rawValue: "com.sktiled.notification.name.demo.tileUnderCursor") + public static let ObjectUnderCursor = Notification.Name(rawValue: "com.sktiled.notification.name.demo.objectUnderCursor") + } + + public struct Debug { + public static let UpdateDebugging = Notification.Name(rawValue: "com.sktiled.notification.name.debug.updateDebugging") + public static let CommandIssued = Notification.Name(rawValue: "com.sktiled.notification.name.debug.commandIssued") + public static let TileUpdated = Notification.Name(rawValue: "com.sktiled.notification.name.debug.tileUpdated") + public static let MapDebuggingChanged = Notification.Name(rawValue: "com.sktiled.notification.name.debug.mapDebuggingChanged") + public static let MapEffectsRenderingChanged = Notification.Name(rawValue: "com.sktiled.notification.name.debug.mapEffectsRenderingChanged") + public static let MapObjectVisibilityChanged = Notification.Name(rawValue: "com.sktiled.notification.name.debug.mapObjectVisibilityChanged") + } +} + + + +extension SKTiledDemoScene { + + /** + Setup game controllers. + */ + @objc public func connectControllers() { + self.isPaused = false + + for controller in GCController.controllers() where controller.microGamepad != nil { + controller.microGamepad?.valueChangedHandler = nil + #if os(tvOS) + log("setting up tvOS remote...", level: .info) + #endif + setupMicroController(controller: controller) + } + } + + /** + Remove game controllers. + */ + @objc public func disconnectControllers() { + self.isPaused = true + } + + /** + + Setup a tvOS remote control. + + - parameter controller: `GCController` controller instance. + */ + public func setupMicroController(controller: GCController) { + + // closure for handling controller actions + controller.microGamepad?.valueChangedHandler = { + + (gamepad: GCMicroGamepad, element: GCControllerElement) in + gamepad.reportsAbsoluteDpadValues = true + gamepad.allowsRotation = true + + guard let skView = self.view, + let cameraNode = self.cameraNode else { + return + } + + // buttonX = play/pause + if ( gamepad.buttonX == element) { + if (gamepad.buttonX.isPressed) { + let nextMode: CameraControlMode = CameraControlMode(rawValue: cameraNode.controlMode.rawValue + 1) ?? .none + cameraNode.controlMode = nextMode + } + + } else if (gamepad.dpad == element) { + + let viewSize = skView.bounds.size + let viewWidth = viewSize.width + let viewHeight = viewSize.width + + let xValue = CGFloat(gamepad.dpad.xAxis.value) + let yValue = CGFloat(gamepad.dpad.yAxis.value) + + let isReleased = (abs(xValue) == 0) || (abs(yValue) == 0) + + + if (cameraNode.controlMode == .zoom) { + if (isReleased == true) { + return + } + + //let currentZoom = cameraNode.zoom + cameraNode.setCameraZoom(cameraNode.zoom + yValue) + } + + // if we're in movement mode, update the camera's position + if (cameraNode.controlMode == .dolly) { + if (isReleased == true) { + return + } + + cameraNode.centerOn(scenePoint: CGPoint(x: viewWidth * xValue, y: viewHeight * yValue)) + } + } + } + } + + + /** + Setup controller notification observers. + */ + public func setupControllerObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(connectControllers), name: Notification.Name.GCControllerDidConnect, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(disconnectControllers), name: Notification.Name.GCControllerDidDisconnect, object: nil) + } +} diff --git a/Demo/Demo+Setup.swift b/Demo/Demo+Setup.swift new file mode 100644 index 00000000..000ccbc1 --- /dev/null +++ b/Demo/Demo+Setup.swift @@ -0,0 +1,50 @@ +// +// Demo+Setup.swift +// SKTiled Demo +// +// Created by Michael Fessenden on 6/28/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import SpriteKit + + +extension SKTiledDemoScene { + + /** + Special setup functions for various included demo content. + + - parameter fileNamed: `String` tiled filename. + */ + func setupDemoLevel(fileNamed: String) { + guard let tilemap = tilemap else { return } + + let baseFilename = fileNamed.components(separatedBy: "/").last! + + let walkableTiles = tilemap.getTilesWithProperty("walkable", true) + let walkableString = (walkableTiles.isEmpty == true) ? "" : ", \(walkableTiles.count) walkable tiles." + log("setting up level: \"\(baseFilename)\"\(walkableString)", level: .debug) + + switch baseFilename { + + case "dungeon-16x16.tmx": + + if let upperGraphLayer = tilemap.tileLayers(named: "Graph-Upper").first { + _ = upperGraphLayer.initializeGraph(walkable: walkableTiles) + } + + if let lowerGraphLayer = tilemap.tileLayers(named: "Graph-Lower").first { + _ = lowerGraphLayer.initializeGraph(walkable: walkableTiles) + } + + + case "roguelike-16x16.tmx": + if let graphLayer = tilemap.tileLayers(named: "Graph").first { + _ = graphLayer.initializeGraph(walkable: walkableTiles) + } + + default: + return + } + } +} diff --git a/Demo/Demo.plist b/Demo/Demo.plist new file mode 100644 index 00000000..5456eb6f --- /dev/null +++ b/Demo/Demo.plist @@ -0,0 +1,48 @@ + + + + + renderQuality + 3 + objectRenderQuality + 8 + textRenderQuality + 8 + maxRenderQuality + 8 + showObjects + + drawGrid + + drawAnchor + + enableEffects + + updateMode + 0 + loggingLevel + 6 + renderCallbacks + + cameraCallbacks + + mouseFilters + 9 + ignoreZoomConstraints + + usePreviousCamera + + allowUserMaps + + demoFiles + + dungeon-16x16 + isometric-130x66 + roguelike-16x16 + staggered-64x33 + hex-65x65 + sk1-32x32 + sk2-32x32 + + + diff --git a/Demo/DemoController.swift b/Demo/DemoController.swift index bf440c16..3289e5a9 100644 --- a/Demo/DemoController.swift +++ b/Demo/DemoController.swift @@ -1,6 +1,6 @@ // // DemoController.swift -// SKTiled +// SKTiled Demo // // Created by Michael Fessenden on 8/4/17. // Copyright © 2017 Michael Fessenden. All rights reserved. @@ -10,22 +10,28 @@ import Foundation import SpriteKit #if os(iOS) || os(tvOS) import UIKit +typealias Color = UIColor +typealias Font = UIFont #else import Cocoa +typealias Color = NSColor +typealias Font = NSFont #endif -/// Controller & Asset manager for the demos. +/// Controller & Asset manager for the demo app public class DemoController: NSObject, Loggable { public var sceneCount: Int = 0 private let fm = FileManager.default static let `default` = DemoController() + var preferences: DemoPreferences! weak public var view: SKView? /// Logging verbosity. - public var loggingLevel: LoggingLevel = SKTiledLoggingLevel + public var loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel + /// Debug visualization options. public var debugDrawOptions: DebugDrawOptions = [] private let demoQueue = DispatchQueue.global(qos: .userInteractive) @@ -33,11 +39,11 @@ public class DemoController: NSObject, Loggable { /// tiled resources public var demourls: [URL] = [] public var currentURL: URL! - private var roots: [URL] = [] private var resources: [URL] = [] - public var resourceTypes: [String] = ["tmx", "tsx", "png"] + public var resourceTypes: [String] = ["tmx", "tsx", "tx", "png"] + /// convenience properties public var tilemaps: [URL] { return resources.filter { $0.pathExtension.lowercased() == "tmx" } } @@ -46,6 +52,14 @@ public class DemoController: NSObject, Loggable { return resources.filter { $0.pathExtension.lowercased() == "tsx" } } + public var templates: [URL] { + return resources.filter { $0.pathExtension.lowercased() == "tx" } + } + + public var images: [URL] { + return resources.filter { ["png", "jpg", "gif"].contains($0.pathExtension.lowercased()) } + } + /// Returns the current demo file index. public var currentIndex: Int { guard let currentURL = currentURL else { return 0 } @@ -62,34 +76,73 @@ public class DemoController: NSObject, Loggable { override public init() { super.init() - Logger.default.loggingLevel = loggingLevel + self.readPreferences() + SKTiledGlobals() // scan for resources if let rpath = Bundle.main.resourceURL { self.addRoot(url: rpath) } + if (self.tilemaps.isEmpty == false) && (self.preferences.demoFiles.isEmpty == false) { + // stash user maps here + var userMaps: [URL] = [] + // loop through the demo files in order to preserve order + for demoFile in self.preferences.demoFiles { + + var fileMatched = false + + // add files included in the demo plist + for tilemap in self.tilemaps { + let pathComponents = tilemap.relativePath.split(separator: "/") + if (pathComponents.count > 1) && (userMaps.contains(tilemap) == false) { + userMaps.append(tilemap) + } + + // get the name of the file + let tilemapName = tilemap.lastPathComponent + let tilemapBase = tilemap.basename + + if (demoFile == tilemapName) || (demoFile == tilemapBase) { + fileMatched = true + self.demourls.append(tilemap) + } + } - if (tilemaps.isEmpty == false) { - demourls = tilemaps - currentURL = demourls.first - } + if (fileMatched == false) { + self.log("cannot find file: \"\(demoFile)\"", level: .error) + } + } - //set up notification for scene to load the next file - NotificationCenter.default.addObserver(self, selector: #selector(reloadScene), name: NSNotification.Name(rawValue: "reloadScene"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(loadNextScene), name: NSNotification.Name(rawValue: "loadNextScene"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(loadPreviousScene), name: NSNotification.Name(rawValue: "loadPreviousScene"), object: nil) + // set the first url + if let firstURL = self.demourls.first { + self.currentURL = firstURL + } + + // append user maps + if (userMaps.isEmpty == false) && (self.preferences.allowUserMaps == true) { + for userMap in userMaps { + guard self.demourls.contains(userMap) == false else { + continue + } + + self.demourls.append(userMap) + } + } + } - NotificationCenter.default.addObserver(self, selector: #selector(toggleMapDemoDrawGridBounds), name: NSNotification.Name(rawValue: "toggleMapDemoDrawGridBounds"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(toggleMapObjectDrawing), name: NSNotification.Name(rawValue: "toggleMapObjectDrawing"), object: nil) + self.setupNotifications() } + + public init(view: SKView) { self.view = view super.init() } // MARK: - Asset Management + /** Add a new root path and scan. @@ -107,7 +160,7 @@ public class DemoController: NSObject, Loggable { */ public func addTilemap(url: URL, at index: Int) { demourls.insert(url, at: index) - loadScene(url: url, usePreviousCamera: false) + loadScene(url: url, usePreviousCamera: preferences.usePreviousCamera) } /** @@ -117,8 +170,15 @@ public class DemoController: NSObject, Loggable { var resourcesAdded = 0 for root in roots { let urls = fm.listFiles(path: root.path, withExtensions: resourceTypes) - resources.append(contentsOf: urls) - resourcesAdded += urls.count + + for url in urls { + guard resources.contains(url) == false else { + continue + } + + resources.append(url) + resourcesAdded += 1 + } } let statusMsg = (resourcesAdded > 0) ? "\(resourcesAdded) resources added." : "WARNING: no resources found." @@ -126,16 +186,118 @@ public class DemoController: NSObject, Loggable { log(statusMsg, level: statusLevel) } + /** + Read demo preferences from property list. + */ + private func readPreferences() { + let configurationURL = URL(fileURLWithPath: "Demo.plist", isDirectory: false, relativeTo: Bundle.main.resourceURL!) + let decoder = PropertyListDecoder() + + if let configData = loadDataFrom(url: configurationURL) { + if let demoPreferences = try? decoder.decode(DemoPreferences.self, from: configData) { + preferences = demoPreferences + self.log("demo preferences loaded.", level: .info) + self.updateGlobalsWithPreferences() + } else { + self.log("preferences could not be loaded.", level: .fatal) + abort() + } + } + } + + // MARK: - Globals + + /** + Update globals with demo prefs. + */ + private func updateGlobalsWithPreferences() { + self.log("updating globals...", level: .info) + + TiledGlobals.default.renderQuality.default = CGFloat(preferences.renderQuality) + TiledGlobals.default.renderQuality.object = CGFloat(preferences.objectRenderQuality) + TiledGlobals.default.renderQuality.text = CGFloat(preferences.textRenderQuality) + TiledGlobals.default.enableRenderCallbacks = preferences.renderCallbacks + TiledGlobals.default.enableCameraCallbacks = preferences.cameraCallbacks + + // Tile animation mode + guard let demoAnimationMode = TileUpdateMode.init(rawValue: preferences.updateMode) else { + log("invalid update mode: \(preferences.updateMode)", level: .error) + return + } + + TiledGlobals.default.updateMode = demoAnimationMode + + // Logging level + guard let demoLoggingLevel = LoggingLevel.init(rawValue: preferences.loggingLevel) else { + log("invalid logging level: \(preferences.loggingLevel)", level: .error) + return + } + + self.loggingLevel = demoLoggingLevel + Logger.default.loggingLevel = demoLoggingLevel + TiledGlobals.default.loggingLevel = demoLoggingLevel + TiledGlobals.default.debug.mouseFilters = TiledGlobals.DebugDisplayOptions.MouseFilters.init(rawValue: preferences.mouseFilters) + } + + /** + Setup notification callbacks. + */ + func setupNotifications() { + //set up notification for scene to load the next file + NotificationCenter.default.addObserver(self, selector: #selector(reloadScene), name: Notification.Name.Demo.ReloadScene, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(loadNextScene), name: Notification.Name.Demo.LoadNextScene, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(loadPreviousScene), name: Notification.Name.Demo.LoadPreviousScene, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(toggleMapDemoDrawGridAndBounds), name: Notification.Name.Debug.MapDebuggingChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(toggleTilemapEffectsRendering), name: Notification.Name.Debug.MapEffectsRenderingChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(toggleMapObjectDrawing), name: Notification.Name.Debug.MapObjectVisibilityChanged, object: nil) + + } + + // MARK: - Helpers + + /** + Load data from a URL. + */ + func loadDataFrom(url: URL) -> Data? { + #if os(macOS) + if let xmlString = try? String(contentsOf: url, encoding: .utf8) { + let xmlData = xmlString.data(using: .utf8)! + self.log("reading: \"\(url.relativePath)\"...", level: .debug) + return xmlData + } + #else + if let xmlData = try? Data(contentsOf: url) { + self.log("reading: \"\(url.relativePath)\"...", level: .debug) + return xmlData + } + #endif + return nil + } + // MARK: - Scene Management + /** + Clear the current scene. + */ + @objc public func flushScene() { + guard let view = self.view else { + log("view is not set.", level: .error) + return + } + + view.presentScene(nil) + let nextScene = SKTiledDemoScene(size: view.bounds.size) + view.presentScene(nextScene) + } + /** Reload the current scene. - parameter interval: `TimeInterval` transition duration. */ - public func reloadScene(_ interval: TimeInterval=0.3) { + @objc public func reloadScene(_ interval: TimeInterval = 0.3) { guard let currentURL = currentURL else { return } - loadScene(url: currentURL, usePreviousCamera: true, interval: interval, reload: true) + loadScene(url: currentURL, usePreviousCamera: preferences.usePreviousCamera, interval: interval, reload: true) } /** @@ -143,13 +305,18 @@ public class DemoController: NSObject, Loggable { - parameter interval: `TimeInterval` transition duration. */ - public func loadNextScene(_ interval: TimeInterval=0.3) { - guard let currentURL = currentURL else { return } + @objc public func loadNextScene(_ interval: TimeInterval = 0.3) { + guard let currentURL = currentURL else { + log("current url does not exist.", level: .error) + return + } + + var nextFilename = demourls.first! if let index = demourls.index(of: currentURL), index + 1 < demourls.count { nextFilename = demourls[index + 1] } - loadScene(url: nextFilename, usePreviousCamera: false, interval: interval, reload: false) + loadScene(url: nextFilename, usePreviousCamera: preferences.usePreviousCamera, interval: interval, reload: false) } /** @@ -157,15 +324,17 @@ public class DemoController: NSObject, Loggable { - parameter interval: `TimeInterval` transition duration. */ - public func loadPreviousScene(_ interval: TimeInterval=0.3) { + @objc public func loadPreviousScene(_ interval: TimeInterval = 0.3) { guard let currentURL = currentURL else { return } var nextFilename = demourls.last! if let index = demourls.index(of: currentURL), index > 0, index - 1 < demourls.count { nextFilename = demourls[index - 1] } - loadScene(url: nextFilename, usePreviousCamera: false, interval: interval, reload: false) + loadScene(url: nextFilename, usePreviousCamera: preferences.usePreviousCamera, interval: interval, reload: false) } + // MARK: - Loading + /** Loads a new demo scene with a named tilemap. @@ -173,101 +342,179 @@ public class DemoController: NSObject, Loggable { - parameter usePreviousCamera: `Bool` transfer camera information. - parameter interval: `TimeInterval` transition duration. */ - internal func loadScene(url: URL, usePreviousCamera: Bool, interval: TimeInterval=0.3, reload: Bool = false) { - guard let view = self.view else { - log("view is not set.", level: .error) + internal func loadScene(url: URL, usePreviousCamera: Bool, interval: TimeInterval = 0.3, reload: Bool = false, _ completion: (() -> Void)? = nil) { + guard let view = self.view, + let preferences = self.preferences else { return } + // loaded from preferences + var showObjects: Bool = preferences.showObjects + var enableEffects: Bool = preferences.enableEffects + var shouldRasterize: Bool = false + var tileUpdateMode: TileUpdateMode? + + + if (tileUpdateMode == nil) { + if let prefsUpdateMode = TileUpdateMode(rawValue: preferences.updateMode) { + tileUpdateMode = prefsUpdateMode + } + } + + // grid visualization + let drawGrid: Bool = preferences.drawGrid + if (drawGrid == true) { + debugDrawOptions.insert([.drawGrid, .drawBounds]) + } + + let drawSceneAnchor: Bool = preferences.drawAnchor + if (drawSceneAnchor == true) { + debugDrawOptions.insert(.drawAnchor) + } + var hasCurrent = false - var liveMode = true var showOverlay = true var cameraPosition = CGPoint.zero var cameraZoom: CGFloat = 1 var isPaused: Bool = false - var showObjects: Bool = false + var currentSpeed: CGFloat = 1 + var ignoreZoomClamping: Bool = false + var zoomClamping: CameraZoomClamping = CameraZoomClamping.none + var ignoreZoomConstraints: Bool = preferences.ignoreZoomConstraints + var sceneInfo: [String: Any] = [:] + + // get current scene info if let currentScene = view.scene as? SKTiledDemoScene { hasCurrent = true if let cameraNode = currentScene.cameraNode { showOverlay = cameraNode.showOverlay cameraPosition = cameraNode.position cameraZoom = cameraNode.zoom + ignoreZoomClamping = cameraNode.ignoreZoomClamping + zoomClamping = cameraNode.zoomClamping + ignoreZoomConstraints = cameraNode.ignoreZoomConstraints } - liveMode = currentScene.liveMode + // pass current values to next tilemap if let tilemap = currentScene.tilemap { - debugDrawOptions = tilemap.defaultLayer.debugDrawOptions + tilemap.dataStorage?.blockNotifications = true + debugDrawOptions = tilemap.debugDrawOptions currentURL = url showObjects = tilemap.showObjects + enableEffects = tilemap.shouldEnableEffects + shouldRasterize = tilemap.shouldRasterize + tileUpdateMode = tilemap.updateMode } isPaused = currentScene.isPaused currentSpeed = currentScene.speed + currentScene.demoController = nil } // update the console let commandString = (reload == false) ? "loading map: \"\(url.filename)\"..." : "reloading map: \"\(url.filename)\"..." updateCommandString(commandString, duration: 3.0) - DispatchQueue.main.async { + + // load the next scene on the main queue + DispatchQueue.main.async { [unowned self] in let nextScene = SKTiledDemoScene(size: view.bounds.size) nextScene.scaleMode = .aspectFill + nextScene.demoController = self + nextScene.receiveCameraUpdates = TiledGlobals.default.enableCameraCallbacks + + // flushing old scene from memory + view.presentScene(nil) // create the transition let transition = SKTransition.fade(withDuration: interval) view.presentScene(nextScene, transition: transition) nextScene.isPaused = isPaused + // setup a new scene with the next tilemap filename nextScene.setup(tmxFile: url.relativePath, inDirectory: (url.baseURL == nil) ? nil : url.baseURL!.path, withTilesets: [], ignoreProperties: false, - loggingLevel: self.loggingLevel, nil) - - nextScene.liveMode = liveMode - sceneInfo["liveMode"] = liveMode - - if (usePreviousCamera == true) { - nextScene.cameraNode?.showOverlay = showOverlay - nextScene.cameraNode?.position = cameraPosition - nextScene.cameraNode?.setCameraZoom(cameraZoom, interval: interval) - } - - guard let tilemap = nextScene.tilemap else { - self.log("tilemap not loaded.", level: .warning) - return - } - - if (hasCurrent == true) { - tilemap.showObjects = (tilemap.boolForKey("showObjects") == true) ? true : showObjects - } - - sceneInfo["hasGraphs"] = (nextScene.graphs.isEmpty == false) - sceneInfo["hasObjects"] = nextScene.tilemap.getObjects().isEmpty == false - - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateUIControls"), object: nil, userInfo: sceneInfo) - nextScene.setupDemoLevel(fileNamed: url.relativePath) - - if (hasCurrent == false) { - self.log("auto-resizing the view.", level: .debug) - nextScene.cameraNode.fitToView(newSize: view.bounds.size) - } - - // TODO: avoid memory spiking -> commenting this out for iOS for now - #if os(macOS) - self.demoQueue.async { - tilemap.defaultLayer.debugDrawOptions = self.debugDrawOptions - } - #endif - - self.sceneCount += 1 - // set the previous scene's speed - nextScene.speed = currentSpeed - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDelegateMenuItems"), object: nil, userInfo: sceneInfo) + loggingLevel: self.loggingLevel) { tilemap in + + // completion handler + if (usePreviousCamera == true) { + nextScene.cameraNode?.showOverlay = showOverlay + nextScene.cameraNode?.position = cameraPosition + nextScene.cameraNode?.setCameraZoom(cameraZoom, interval: interval) + } + + nextScene.cameraNode?.ignoreZoomClamping = ignoreZoomClamping + nextScene.cameraNode?.zoomClamping = zoomClamping + nextScene.cameraNode?.ignoreZoomConstraints = ignoreZoomConstraints + + // if tilemap has a property override to show objects, use it...else use demo prefs + tilemap.showObjects = (tilemap.boolForKey("showObjects") == true) ? true : showObjects + + sceneInfo["hasGraphs"] = (nextScene.graphs.isEmpty == false) + sceneInfo["hasObjects"] = nextScene.tilemap.getObjects().isEmpty == false + sceneInfo["propertiesInfo"] = "--" + + + if (hasCurrent == false) { + self.log("auto-resizing the view.", level: .debug) + nextScene.cameraNode.fitToView(newSize: view.bounds.size) + } + + // add caching here + tilemap.shouldEnableEffects = (tilemap.boolForKey("shouldEnableEffects") == true) ? true : enableEffects + tilemap.shouldRasterize = shouldRasterize + tilemap.updateMode = tileUpdateMode ?? TiledGlobals.default.updateMode + + + self.demoQueue.async { [unowned self] in + tilemap.debugDrawOptions = self.debugDrawOptions + } + + self.sceneCount += 1 + + // set the previous scene's speed + nextScene.speed = currentSpeed + + #if os(iOS) + // for some reason properties label not updating properly + NotificationCenter.default.post( + name: Notification.Name.Demo.UpdateDebugging, + object: tilemap, + userInfo: sceneInfo + ) + #endif + + // setup the demo level scene + nextScene.setupDemoLevel(fileNamed: url.relativePath) + + self.demoQueue.sync { + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + + NotificationCenter.default.post( + name: Notification.Name.Demo.SceneLoaded, + object: nextScene, + userInfo: nil + ) + + NotificationCenter.default.post( + name: Notification.Name.Camera.Updated, + object: nextScene.cameraNode, + userInfo: nil + ) + } + + } // end of completion handler } } @@ -281,8 +528,10 @@ public class DemoController: NSObject, Loggable { guard let scene = view.scene as? SKTiledScene else { return } if let cameraNode = scene.cameraNode { - updateCommandString("fitting to view...", duration: 0.75) - cameraNode.fitToView(newSize: view.bounds.size) + updateCommandString("fitting to view...", duration: 4) + cameraNode.fitToView(newSize: view.bounds.size, transition: 0.25) + } else { + updateCommandString("camera not found", duration: 4) } } @@ -295,10 +544,18 @@ public class DemoController: NSObject, Loggable { if let tilemap = scene.tilemap { updateCommandString("visualizing map bounds...", duration: 3) - tilemap.debugDrawOptions = (tilemap.debugDrawOptions.contains(.drawBounds)) ? tilemap.debugDrawOptions.subtracting([.drawBounds]) : tilemap.debugDrawOptions.insert([.drawBounds]).memberAfterInsert - - let debugInfo: [String: Any] = ["mapGrid": tilemap.debugDrawOptions.contains(.drawGrid), "navGraph": tilemap.debugDrawOptions.contains(.drawGraph), "mapBounds": tilemap.debugDrawOptions.contains(.drawBounds)] - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDelegateMenuItems"), object: nil, userInfo: debugInfo) + + if (tilemap.debugDrawOptions.contains(.drawBounds)) { + tilemap.debugDrawOptions = tilemap.debugDrawOptions.subtracting(.drawBounds) + } else { + tilemap.debugDrawOptions.insert(.drawBounds) + } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) } } @@ -311,26 +568,18 @@ public class DemoController: NSObject, Loggable { if let tilemap = scene.tilemap { updateCommandString("visualizing map grid...", duration: 3) - tilemap.debugDrawOptions = (tilemap.debugDrawOptions.contains(.drawGrid)) ? tilemap.debugDrawOptions.subtracting([.drawGrid]) : tilemap.debugDrawOptions.insert([.drawGrid]).memberAfterInsert - - let debugInfo: [String: Any] = ["mapGrid": tilemap.debugDrawOptions.contains(.drawGrid), "navGraph": tilemap.debugDrawOptions.contains(.drawGraph), "mapBounds": tilemap.debugDrawOptions.contains(.drawBounds)] - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDelegateMenuItems"), object: nil, userInfo: debugInfo) - } - } - - /** - Show/hide the grid & map bounds. - */ - public func toggleMapDemoDrawGridBounds() { - guard let view = self.view, - let scene = view.scene as? SKTiledScene else { return } - if let tilemap = scene.tilemap { - updateCommandString("visualizing map grid & bounds...", duration: 3) - tilemap.debugDrawOptions = (tilemap.debugDrawOptions.contains(.drawGrid)) ? tilemap.debugDrawOptions.subtracting([.drawGrid, .drawBounds]) : tilemap.debugDrawOptions.insert([.drawGrid, .drawBounds]).memberAfterInsert - - let debugInfo: [String: Any] = ["mapGrid": tilemap.debugDrawOptions.contains(.drawGrid), "navGraph": tilemap.debugDrawOptions.contains(.drawGraph), "mapBounds": tilemap.debugDrawOptions.contains(.drawBounds)] - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDelegateMenuItems"), object: nil, userInfo: debugInfo) + if (tilemap.debugDrawOptions.contains(.drawGrid)) { + tilemap.debugDrawOptions = tilemap.debugDrawOptions.subtracting(.drawGrid) + } else { + tilemap.debugDrawOptions.insert(.drawGrid) + } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) } } @@ -351,62 +600,249 @@ public class DemoController: NSObject, Loggable { if (tileLayer.debugDrawOptions.contains(.drawGraph) == false) { graphsDrawn += 1 } + + if (tileLayer.debugDrawOptions.contains(.drawGraph)) { + tileLayer.debugDrawOptions = tileLayer.debugDrawOptions.subtracting([.drawGraph]) + } else { + tileLayer.debugDrawOptions.insert([.drawGraph]) + } - tileLayer.debugDrawOptions = (tileLayer.debugDrawOptions.contains(.drawGraph)) ? tileLayer.debugDrawOptions.subtracting([.drawGraph]) : tileLayer.debugDrawOptions.insert([.drawGraph]).memberAfterInsert graphsCount += 1 } if (graphsCount > 0) && (graphsDrawn > 0) { - tilemap.debugDrawOptions = tilemap.debugDrawOptions.insert([.drawGrid, .drawBounds]).memberAfterInsert updateCommandString("visualizing \(graphsCount) navigation graphs...", duration: 3) - } else { + } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + } + + /** + Show/hide the grid & map bounds. This is meant to be used with the interface buttons/keys to quickly turn grid & bounds drawing on. + */ + @objc public func toggleMapDemoDrawGridAndBounds() { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + if let tilemap = scene.tilemap { + updateCommandString("visualizing map grid & bounds...", duration: 3) + + if (tilemap.debugDrawOptions.contains(.drawGrid) || tilemap.debugDrawOptions.contains(.drawBounds) ) { tilemap.debugDrawOptions = tilemap.debugDrawOptions.subtracting([.drawGrid, .drawBounds]) + } else { + tilemap.debugDrawOptions.insert([.drawGrid, .drawBounds]) } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) } } /** Show/hide current scene objects. */ - public func toggleMapObjectDrawing() { + @objc public func toggleMapObjectDrawing() { guard let view = self.view, let scene = view.scene as? SKTiledScene else { return } if let tilemap = scene.tilemap { let command = (tilemap.showObjects == true) ? "hiding all objects..." : "showing all objects..." updateCommandString(command, duration: 0.75) - tilemap.showObjects = !tilemap.showObjects + let doShowObjects = !tilemap.showObjects + tilemap.showObjects = doShowObjects + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) } } /** - Dump the map statistics to the console. + Show/hide current scene objects. */ - public func printMapStatistics() { + @objc public func toggleObjectBoundaryDrawing() { guard let view = self.view, let scene = view.scene as? SKTiledScene else { return } if let tilemap = scene.tilemap { - updateCommandString("showing map statistics...", duration: 3) - tilemap.mapStatistics() + + let currentObjectBoundsMode = tilemap.debugDrawOptions.contains(.drawObjectBounds) + let command = (currentObjectBoundsMode == true) ? "hiding object boundaries..." : "hiding object boundaries..." + updateCommandString(command, duration: 0.75) + + if (currentObjectBoundsMode == false) { + tilemap.debugDrawOptions.insert(.drawObjectBounds) + } else { + tilemap.debugDrawOptions.remove(.drawObjectBounds) + } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + } + + + // Debug.MapEffectsRenderingChanged + @objc public func toggleTilemapEffectsRendering() { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + + if let tilemap = scene.tilemap { + + let effectsMode = tilemap.shouldEnableEffects + tilemap.shouldEnableEffects = !effectsMode + let effectsStatusString = (effectsMode == true) ? "off" : "on" + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + updateCommandString("effects rendering: \(effectsStatusString)...", duration: 3) } } + @objc public func cycleTilemapUpdateMode() { + guard let view = self.view, + let scene = view.scene as? SKTiledScene, + let tilemap = scene.tilemap else { return } + + + let currentValue = tilemap.updateMode + let nextValue = currentValue.next() + tilemap.updateMode = nextValue + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + + NotificationCenter.default.post( + name: Notification.Name.Map.UpdateModeChanged, + object: tilemap, + userInfo: nil + ) + + tilemap.postRenderStatistics(Date()) { + self.updateCommandString("tile update mode: \(nextValue.name)", duration: 3.0) + } + } + + @objc public func toggleRenderStatistics() { + let statsCurrentState = TiledGlobals.default.enableRenderCallbacks + let statsNextState = !statsCurrentState + + updateCommandString("displaying render stats: \(statsNextState)", duration: 2.0) + TiledGlobals.default.enableRenderCallbacks = statsNextState + + //renderStatisticsMenuItem.state = (TiledGlobals.default.enableRenderCallbacks == true) ? .on : .off + NotificationCenter.default.post( + name: Notification.Name.RenderStats.VisibilityChanged, + object: nil, + userInfo: ["showRenderStats": statsNextState] + ) + } + + // MARK: - Debugging Output + /** - Dump the current resource list to the console. + Dump the current map list to the console. */ - public func dumpCurrentResources() { + public func getCurrentlyLoadedTilemaps() { updateCommandString("showing registered maps...", duration: 3) let headerString = "# Currently loaded files: \(self.demourls.count)" - let titleUnderline = String(repeating: "-", count: headerString.characters.count) + let titleUnderline = String(repeating: "-", count: headerString.count) var outputString = "\n\(headerString)\n\(titleUnderline)" - for (fileIndex, filename) in self.demourls.enumerated() { - let symbol = (fileIndex == (currentIndex - 1)) ? "(x)" : "( )" + let symbol = (fileIndex == (currentIndex - 1)) ? "[x]" : "[ ]" outputString += "\n\(symbol) \"\(filename.filename)\"" } + print(outputString) + } + + /** + Dump the current external map list to the console. + */ + public func getExternallyLoadedAssets() { + updateCommandString("showing external maps...", duration: 3) + + let externalMaps = self.tilemaps.filter({ $0.isBundled == false }) + let headerString = "# External Maps: \(externalMaps.count)" + let titleUnderline = String(repeating: "-", count: headerString.count) + var outputString = "\n\(headerString)\n\(titleUnderline)" + + for (_, url) in externalMaps.enumerated() { + outputString += "\n - \"\(url.relativePath)\"" + } + print("\(outputString)\n\n") + } + + /** + Dump the current asset list to the console. + */ + public func getCurrentlyLoadedAssets() { + updateCommandString("showing loaded assets...", duration: 3) + + let headerString = "# Currently loaded assets: \(self.resources.count)" + let titleUnderline = String(repeating: "-", count: headerString.count) + var outputString = "\n\(headerString)\n\(titleUnderline)" + + let headerSymbol = "✎" + + if !tilemaps.isEmpty { + let mapHeaderString = "\n\(headerSymbol) Tilemaps: \(tilemaps.count)" + let mapTitleUnderline = String(repeating: "-", count: mapHeaderString.count) + outputString += "\n\(mapHeaderString)\n\(mapTitleUnderline)" + for (_, url) in tilemaps.enumerated() { + let isDemoURL = (preferences.demoFiles.contains(url.filename) || preferences.demoFiles.contains(url.basename)) + let symbol = (isDemoURL == true) ? "[x]" : "[ ]" + outputString += "\n - \(symbol) \"\(url.filename)\"" + } + } + + if !tilesets.isEmpty { + let tilesetHeaderString = "\n\(headerSymbol) Tilesets: \(tilesets.count)" + let tilesetTitleUnderline = String(repeating: "-", count: tilesetHeaderString.count) + outputString += "\n\(tilesetHeaderString)\n\(tilesetTitleUnderline)" + for (_, filename) in tilesets.enumerated() { + outputString += "\n - \"\(filename.filename)\"" + } + } + + if !templates.isEmpty { + let templateHeaderString = "\n\(headerSymbol) Templates: \(templates.count)" + let templateTitleUnderline = String(repeating: "-", count: templateHeaderString.count) + outputString += "\n\(templateHeaderString)\n\(templateTitleUnderline)" + for (_, filename) in templates.enumerated() { + outputString += "\n - \"\(filename.filename)\"" + } + } + + if !images.isEmpty { + let imageHeaderString = "\n\(headerSymbol) Images: \(images.count)" + let imageTitleUnderline = String(repeating: "-", count: imageHeaderString.count) + outputString += "\n\(imageHeaderString)\n\(imageTitleUnderline)" + for (_, filename) in images.enumerated() { + outputString += "\n - \"\(filename.filename)\"" + } + } print(outputString) } @@ -414,14 +850,202 @@ public class DemoController: NSObject, Loggable { /** Dump the map statistics to the console. */ - public func dumpCurrentTilemap() { + public func dumpMapStatistics() { guard let view = self.view, let scene = view.scene as? SKTiledScene else { return } + + if let tilemap = scene.tilemap { + updateCommandString("showing map statistics...", duration: 3) + tilemap.dumpStatistics() + } + } + + + public func updateTileUpdateMode(value: Int = -1) { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + + let nextUpdateMode: TileUpdateMode = TileUpdateMode.init(rawValue: value) ?? TiledGlobals.default.updateMode.next() + + if (nextUpdateMode != TiledGlobals.default.updateMode) { + TiledGlobals.default.updateMode = nextUpdateMode + updateCommandString("tile update mode: \(nextUpdateMode.name)", duration: 1) if let tilemap = scene.tilemap { - dump(tilemap) + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) } + } + } + + // MARK: - Layer Isolation/Visibility + /** + Toggle layer isolation. + + - parameter layerID: `String` layer uuid. + - parameter isolated: `Bool` isolated on/off. + */ + public func toggleLayerVisibility(layerID: String, visible isVisible: Bool) { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + + if let tilemap = scene.tilemap { + if let selectedLayer = tilemap.getLayer(withID: layerID) { + let valueString = (isVisible == true) ? "on" : "off" + updateCommandString("setting visibility \(valueString) for layer: \"\(selectedLayer.layerName)\"...", duration: 3) + selectedLayer.isHidden = !isVisible + } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + } + + /** + Toggle layer visibility. + + - parameter layerID: `String` layer uuid. + - parameter isolated: `Bool` isolated on/off. + */ + public func toggleAllLayerVisibility(visible isVisible: Bool) { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + let actionName = (isVisible == true) ? "visible" : "hidden" + updateCommandString("setting all layers \(actionName)...", duration: 3) + + if let tilemap = scene.tilemap { + tilemap.layers.forEach { layer in + layer.isHidden = !isVisible + } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + } + + /** + Toggle layer isolation. + + - parameter layerID: `String` layer uuid. + - parameter isolated: `Bool` isolated on/off. + */ + public func toggleLayerIsolated(layerID: String, isolated isIsolated: Bool) { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + if let tilemap = scene.tilemap { + if let selectedLayer = tilemap.getLayer(withID: layerID) { + selectedLayer.isolateLayer(duration: 0.25) + updateCommandString("isolating layer: \"\(selectedLayer.layerName)\"...", duration: 3) + } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + printLayerIsolatedInfo() } + /** + Disable all layer isolation. + */ + public func turnIsolationOff() { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + if let tilemap = scene.tilemap { + updateCommandString("disabling layer isolation...", duration: 3) + tilemap.getLayers().forEach { layer in + if (layer.isolated == true) { + layer.isolateLayer(duration: 0.25) + } + } + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + printLayerIsolatedInfo() + } + + public func printLayerIsolatedInfo() { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + if let tilemap = scene.tilemap { + updateCommandString("show isolated layers...", duration: 3) + + var headerString = "# Tilemap \"\(tilemap.url.filename)\", isolated:" + let headerUnderline = String(repeating: "-", count: headerString.count ) + headerString = "\n\(headerString)\n\(headerUnderline)\n" + + tilemap.getLayers().forEach { layer in + + let isGroupNode: Bool = (layer as? SKGroupLayer != nil) + let hasChildren: Bool = (layer.childLayers.isEmpty == false) + + let layerSymbol = (isGroupNode == true) ? (hasChildren == true) ? "▿" : "▹" : "" + + let parentCount = layer.parents.count - 1 + let padding = String(repeating: " ", count: parentCount) + let isolatedSymbol = (layer.isolated == true) ? "[x]" : "[ ]" + headerString += "\n \(isolatedSymbol) \(padding) \(layerSymbol) \"\(layer.layerName)\"" + } + + print("\(headerString)\n\n") + } + } + + public func cycleTilemapUpdateMode(mode: String) { + guard let view = self.view, + let scene = view.scene as? SKTiledScene else { return } + + if let updateMode = Int(mode) { + if let newUpdateMode = TileUpdateMode.init(rawValue: updateMode) { + if let tilemap = scene.tilemap { + tilemap.updateMode = newUpdateMode + updateCommandString("setting update mode: \(newUpdateMode.name)", duration: 3) + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + } + } + } + + public func toggleRenderStatsTimeFormat() { + updateCommandString("setting update mode: ", duration: 3) + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: nil, + userInfo: nil + ) + } + + // MARK: - Render Statistics + + public func updateRenderStatistics() {} + + // MARK: - Debug Output + /** Send a command to the UI to update status. @@ -429,24 +1053,233 @@ public class DemoController: NSObject, Loggable { - parameter duration: `TimeInterval` how long the message should be displayed (0 is indefinite). */ public func updateCommandString(_ command: String, duration: TimeInterval = 3.0) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateCommandString"), object: nil, userInfo: ["command": command, "duration": duration]) + NotificationCenter.default.post( + name: Notification.Name.Debug.CommandIssued, + object: nil, + userInfo: ["command": command, "duration": duration] + ) + } + + // MARK: - Debugging + + // this is received as a command from AppDelegate (main menu action) + public func toggleRenderStatistics(value nextState: Bool) { + updateCommandString("displaying render stats: \(nextState)", duration: 2.0) + + NotificationCenter.default.post( + name: Notification.Name.RenderStats.VisibilityChanged, + object: nil, + userInfo: ["showRenderStats": nextState] + ) + } +} + + +/// Class to manage preferences loaded from a property list. +class DemoPreferences: Codable { + + var renderQuality: Double = 0 + var objectRenderQuality: Double = 0 + var textRenderQuality: Double = 0 + var maxRenderQuality: Double = 0 + + var showObjects: Bool = false + var drawGrid: Bool = false + var drawAnchor: Bool = false + var enableEffects: Bool = false + var updateMode: Int = 0 + var allowUserMaps: Bool = true + var loggingLevel: Int = 0 + var renderCallbacks: Bool = true + var cameraCallbacks: Bool = true + var mouseFilters: Int = 0 + var ignoreZoomConstraints: Bool = false + var usePreviousCamera: Bool = false + var demoFiles: [String] = [] + + enum ConfigKeys: String, CodingKey { + case renderQuality + case objectRenderQuality + case textRenderQuality + case maxRenderQuality + case showObjects + case drawGrid + case drawAnchor + case enableEffects + case updateMode + case allowUserMaps + case loggingLevel + case renderCallbacks + case cameraCallbacks + case mouseFilters + case ignoreZoomConstraints + case usePreviousCamera + case demoFiles + } + + required init?(coder aDecoder: NSCoder) {} + + required init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: ConfigKeys.self) + renderQuality = try values.decode(Double.self, forKey: .renderQuality) + objectRenderQuality = try values.decode(Double.self, forKey: .objectRenderQuality) + textRenderQuality = try values.decode(Double.self, forKey: .textRenderQuality) + maxRenderQuality = try values.decode(Double.self, forKey: .maxRenderQuality) + showObjects = try values.decode(Bool.self, forKey: .showObjects) + drawGrid = try values.decode(Bool.self, forKey: .drawGrid) + drawAnchor = try values.decode(Bool.self, forKey: .drawAnchor) + enableEffects = try values.decode(Bool.self, forKey: .enableEffects) + updateMode = try values.decode(Int.self, forKey: .updateMode) + allowUserMaps = try values.decode(Bool.self, forKey: .allowUserMaps) + loggingLevel = try values.decode(Int.self, forKey: .loggingLevel) + renderCallbacks = try values.decode(Bool.self, forKey: .renderCallbacks) + cameraCallbacks = try values.decode(Bool.self, forKey: .cameraCallbacks) + mouseFilters = try values.decode(Int.self, forKey: .mouseFilters) + ignoreZoomConstraints = try values.decode(Bool.self, forKey: .ignoreZoomConstraints) + usePreviousCamera = try values.decode(Bool.self, forKey: .usePreviousCamera) + demoFiles = try values.decode(Array.self, forKey: .demoFiles) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ConfigKeys.self) + try container.encode(renderQuality, forKey: .renderQuality) + try container.encode(objectRenderQuality, forKey: .objectRenderQuality) + try container.encode(textRenderQuality, forKey: .textRenderQuality) + try container.encode(maxRenderQuality, forKey: .maxRenderQuality) + try container.encode(showObjects, forKey: .showObjects) + try container.encode(drawGrid, forKey: .drawGrid) + try container.encode(drawAnchor, forKey: .drawAnchor) + try container.encode(enableEffects, forKey: .enableEffects) + try container.encode(updateMode, forKey: .updateMode) + try container.encode(allowUserMaps, forKey: .allowUserMaps) + try container.encode(loggingLevel, forKey: .loggingLevel) + try container.encode(renderCallbacks, forKey: .renderCallbacks) + try container.encode(cameraCallbacks, forKey: .cameraCallbacks) + try container.encode(mouseFilters, forKey: .mouseFilters) + try container.encode(ignoreZoomConstraints, forKey: .ignoreZoomConstraints) + try container.encode(usePreviousCamera, forKey: .usePreviousCamera) + try container.encode(demoFiles, forKey: .demoFiles) + } +} + + + +extension DemoPreferences: CustomDebugReflectable { + + func dumpStatistics() { + let spacing = " " + var headerString = "\(spacing)Demo Preferences\(spacing)" + let headerUnderline = String(repeating: "-", count: headerString.count ) + + var animModeString = "**invalid**" + if let demoAnimationMode = TileUpdateMode.init(rawValue: updateMode) { + animModeString = demoAnimationMode.name + } + + //var mouseFilterStrings = mouseFilters + + var loggingLevelString = "**invalid**" + if let demoLoggingLevel = LoggingLevel.init(rawValue: loggingLevel) { + loggingLevelString = demoLoggingLevel.description + } + + headerString = "\n\(headerString)\n\(headerUnderline)\n" + headerString += " - render quality: \(renderQuality)\n" + headerString += " - object quality: \(objectRenderQuality)\n" + headerString += " - text quality: \(textRenderQuality)\n" + headerString += " - max render quality: \(maxRenderQuality)\n" + headerString += " - show objects: \(showObjects)\n" + headerString += " - draw grid: \(drawGrid)\n" + headerString += " - draw anchor: \(drawAnchor)\n" + headerString += " - effects rendering: \(enableEffects)\n" + headerString += " - update mode: \(updateMode)\n" + headerString += " - animation mode: \(animModeString)\n" + headerString += " - allow user maps: \(allowUserMaps)\n" + headerString += " - logging level: \(loggingLevelString)\n" + headerString += " - render callbacks: \(renderCallbacks)\n" + headerString += " - camera callbacks: \(cameraCallbacks)\n" + headerString += " - ignore camera contstraints: \(ignoreZoomConstraints)\n" + headerString += " - user previous camera: \(usePreviousCamera)\n" + headerString += " - mouse filters:\n" + + print("\(headerString)\n\n") } } + + extension FileManager { - func listFiles(path: String, withExtensions: [String]=[]) -> [URL] { + func listFiles(path: String, withExtensions: [String] = []) -> [URL] { let baseurl: URL = URL(fileURLWithPath: path) var urls: [URL] = [] enumerator(atPath: path)?.forEach({ (e) in guard let s = e as? String else { return } + let url = URL(fileURLWithPath: s, relativeTo: baseurl) + let pathExtension = url.pathExtension.lowercased() - if withExtensions.contains(url.pathExtension.lowercased()) || (withExtensions.isEmpty) { + if withExtensions.contains(pathExtension) || (withExtensions.isEmpty) { urls.append(url) } }) return urls } } + + +extension TileUpdateMode { + + /// Control string to be used with the render stats menu. + public var uiControlString: String { + switch self { + case .dynamic: return "Cached" + case .full: return "Full" + case .actions: return "SpriteKit Actions" + } + } +} + + + +extension SKTilemap.RenderStatistics { + + /// Returns an attributed string with the current CPU usage percentage. + var processorAttributedString: NSAttributedString { + let fontSize: CGFloat + #if os(iOS) + fontSize = 9 + #elseif os(tvOS) + fontSize = 14 + #else + fontSize = 12 + #endif + + let labelText = "CPU Usage: \(cpuPercentage)%" + let labelStyle = NSMutableParagraphStyle() + labelStyle.alignment = .left + labelStyle.firstLineHeadIndent = 0 + let fontColor: Color + switch cpuPercentage { + case 0...18: + fontColor = Color(hexString: "#7ED321") + case 19...30: + fontColor = Color(hexString: "#FFFFFF") + case 31...49: + fontColor = Color(hexString: "#F8E71C") + case 50...74: + fontColor = Color(hexString: "#F5A623") + default: + fontColor = Color(hexString: "#FD4444") + } + + let cpuStatsAttributes = [ + .font: Font(name: "Courier", size: fontSize)!, + .foregroundColor: fontColor, + .paragraphStyle: labelStyle + ] as [NSAttributedString.Key: Any] + + return NSMutableAttributedString(string: labelText, attributes: cpuStatsAttributes) + } +} diff --git a/Demo/SKTiledDemoScene.swift b/Demo/SKTiledDemoScene.swift index 344dbe95..8770875c 100644 --- a/Demo/SKTiledDemoScene.swift +++ b/Demo/SKTiledDemoScene.swift @@ -1,11 +1,9 @@ // // SKTiledDemoScene.swift -// SKTiled +// SKTiled Demo // // Created by Michael Fessenden on 3/21/16. // Copyright (c) 2016 Michael Fessenden. All rights reserved. -// - import SpriteKit import Foundation @@ -17,10 +15,11 @@ import Cocoa #endif +// special scene class used for the demo public class SKTiledDemoScene: SKTiledScene { - public var uiScale: CGFloat = SKTiledContentScaleFactor - var mouseTracker = MouseTracker() + weak internal var demoController: DemoController? + public var uiScale: CGFloat = TiledGlobals.default.contentScale /// global information label font size. private let labelFontSize: CGFloat = 11 @@ -29,41 +28,21 @@ public class SKTiledDemoScene: SKTiledScene { internal var currentLayer: SKTiledLayerObject? internal var currentTile: SKTile? internal var currentVectorObject: SKTileObject? + internal var currentProxyObject: TileObjectProxy? internal var selected: [SKTiledLayerObject] = [] - - internal var clickshapes: Set = [] - internal var cleanup: Set = [] + internal var focusObjects: [SKNode] = [] internal var plotPathfindingPath: Bool = true internal var graphStartCoordinate: CGPoint? internal var graphEndCoordinate: CGPoint? internal var currentPath: [GKGridGraphNode] = [] + #if os(macOS) + internal var mousePointer: MousePointer! + #endif + private let demoQueue = DispatchQueue(label: "com.sktiled.sktiledDemoScene.demoQueue", qos: .utility) - /// Cleanup tile shapes queue - internal let cleanupQueue = DispatchQueue(label: "com.sktiled.cleanup", qos: .background) - - internal var editMode: Bool = false - - /// Highlight tiles under the mouse - internal var liveMode: Bool = true { - didSet { - guard oldValue != liveMode else { return } - self.cleanupTileShapes() - self.cleanupPathfindingShapes() - self.graphStartCoordinate = nil - self.graphEndCoordinate = nil - } - } - - /// Current coordinate for mouse/touch location. - internal var currentCoordinate: CGPoint = .zero { - didSet { - guard oldValue != currentCoordinate else { return } - self.cleanupTileShapes(coord: currentCoordinate) - } - } override public var isPaused: Bool { willSet { @@ -75,13 +54,29 @@ public class SKTiledDemoScene: SKTiledScene { override public func didMove(to view: SKView) { super.didMove(to: view) + // game controllers + setupControllerObservers() + connectControllers() + #if os(macOS) + cameraNode.ignoreZoomClamping = false updateTrackingViews() - addChild(mouseTracker) - mouseTracker.zPosition = 1000 + #elseif os(iOS) + cameraNode.ignoreZoomClamping = false + #else + cameraNode.ignoreZoomClamping = true #endif - NotificationCenter.default.addObserver(self, selector: #selector(updateCoordinate), name: NSNotification.Name(rawValue: "updateCoordinate"), object: nil) + // allow gestures on iOS + cameraNode.allowGestures = true + #if os(macOS) + if (mousePointer == nil) { + mousePointer = MousePointer() + addChild(mousePointer) + cameraNode.addDelegate(mousePointer) + mousePointer.isHidden = true + } + #endif } override public func didChangeSize(_ oldSize: CGSize) { @@ -90,6 +85,18 @@ public class SKTiledDemoScene: SKTiledScene { updateTrackingViews() #endif updateHud(tilemap) + + guard let cameraNode = cameraNode else { return } + updateCameraInfo(msg: cameraNode.description) + } + + override public func willMove(from view: SKView) { + #if os(macOS) + // clear out old tracking areas + for oldTrackingArea in view.trackingAreas { + view.removeTrackingArea(oldTrackingArea) + } + #endif } /** @@ -98,7 +105,7 @@ public class SKTiledDemoScene: SKTiledScene { - parameter coord: `CGPoint` event point. - returns: `[SKTile]` tile nodes. */ - func getTilesAt(coord: CGPoint) -> [SKTile] { + func tilesAt(coord: CGPoint) -> [SKTile] { var result: [SKTile] = [] guard let tilemap = tilemap else { return result } let tileLayers = tilemap.tileLayers(recursive: true).reversed().filter({ $0.visible == true }) @@ -120,167 +127,123 @@ public class SKTiledDemoScene: SKTiledScene { var result: [SKNode] = [] let nodes = self.nodes(at: point) for node in nodes { - if node is SKTile { - result.append(node) - } - - if node is SKTileObject { + if (node is SKTileObject || node is SKTile) { result.append(node) } } return result } - /** - Add a temporary tile shape to the world at the given coordinate. - - - parameter x: `Int` x-coordinate. - - parameter y: `Int` y-coordinate. - - parameter role: `TileShape.DebugRole` tile display role. - - parameter weight: `CGFloat` pathfinding weight. - */ - func addTileToWorld(_ x: Int, _ y: Int, - role: TileShape.DebugRole = .none, - weight: Float = 1) -> TileShape? { - - guard let tilemap = tilemap else { return nil } - - // validate the coordinate - let layer = tilemap.defaultLayer - let validCoord = layer.isValid(x, y) - - let coord = CGPoint(x: x, y: y) - - let tileColor: SKColor = (validCoord == true) ? tilemap.highlightColor : TiledObjectColors.crimson - let lastZosition = tilemap.lastZPosition + (tilemap.zDeltaForLayers * 2) - - // add debug tile shape - let tile = TileShape(layer: layer, coord: coord, tileColor: tileColor, role: role, weight: weight) - - tile.zPosition = lastZosition - let tilePosition = layer.pointForCoordinate(x, y) - tile.position = tilemap.convert(tilePosition, from: layer) - tilemap.addChild(tile) - - - if (role == .highlight) { - let fadeAction = SKAction.fadeOut(withDuration: 0.4) - tile.run(fadeAction, completion: { - tile.removeFromParent() - }) - } - - return tile - - } - - // MARK: - Deinitialization - deinit { - // Deregister for scene updates - NotificationCenter.default.removeObserver(self, name: Notification.Name(rawValue: "updateCoordinate"), object: nil) - } - // MARK: - Demo - /** - Special setup functions for various included demo content. - - - parameter fileNamed: `String` tiled filename. - */ - func setupDemoLevel(fileNamed: String) { - guard let tilemap = tilemap else { return } - - let baseFilename = fileNamed.components(separatedBy: "/").last! - - let walkableTiles = tilemap.getTilesWithProperty("walkable", true) - let walkableString = (walkableTiles.isEmpty == true) ? "" : ", \(walkableTiles.count) walkable tiles." - log("setting up level: \"\(baseFilename)\"\(walkableString)", level: .info) - - switch baseFilename { - - case "dungeon-16x16.tmx": - if let upperGraphLayer = tilemap.tileLayers(named: "Graph-Upper").first { - _ = upperGraphLayer.initializeGraph(walkable: walkableTiles) - } - - if let lowerGraphLayer = tilemap.tileLayers(named: "Graph-Lower").first { - _ = lowerGraphLayer.initializeGraph(walkable: walkableTiles) - } - - case "pacman.tmx": - if let graphLayer = tilemap.tileLayers(named: "Graph").first { - if let graph = graphLayer.initializeGraph(walkable: walkableTiles) { - - // connect the two tunnels - if let leftTunnel = graph.node(atGridPosition: int2(0, 17) ) { - if let rightTunnel = graph.node(atGridPosition: int2(27, 17)) { - leftTunnel.addConnections(to: [rightTunnel], bidirectional: true) - } - } - } - } - - case "roguelike-16x16.tmx": - if let graphLayer = tilemap.tileLayers(named: "Graph").first { - _ = graphLayer.initializeGraph(walkable: walkableTiles) - } - - - default: - return - } - } - - /** Callback to the GameViewController to reload the current scene. */ public func reloadScene() { - NotificationCenter.default.post(name: Notification.Name(rawValue: "reloadScene"), object: nil) + // call back to the demo controller + NotificationCenter.default.post( + name: Notification.Name.Demo.ReloadScene, + object: nil, + userInfo: nil + ) } /** Callback to the GameViewController to load the next scene. */ public func loadNextScene() { - NotificationCenter.default.post(name: Notification.Name(rawValue: "loadNextScene"), object: nil) + // call back to the demo controller + NotificationCenter.default.post( + name: Notification.Name.Demo.LoadNextScene, + object: nil, + userInfo: nil + ) } /** Callback to the GameViewController to reload the previous scene. */ public func loadPreviousScene() { - NotificationCenter.default.post(name: Notification.Name(rawValue: "loadPreviousScene"), object: nil) + // call back to the demo controller + NotificationCenter.default.post( + name: Notification.Name.Demo.LoadPreviousScene, + object: nil, + userInfo: nil + ) } public func updateMapInfo(msg: String) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), object: nil, userInfo: ["mapInfo": msg]) + + NotificationCenter.default.post( + name: Notification.Name.Demo.UpdateDebugging, + object: nil, + userInfo: ["mapInfo": msg] + ) } public func updateTileInfo(msg: String) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), object: nil, userInfo: ["tileInfo": msg]) + NotificationCenter.default.post( + name: Notification.Name.Demo.UpdateDebugging, + object: nil, + userInfo: ["tileInfo": msg] + ) } + /** + Update the tile properties debugging info. + + - parameter msg: `String` properties string. + */ public func updatePropertiesInfo(msg: String) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), object: nil, userInfo: ["propertiesInfo": msg]) + NotificationCenter.default.post( + name: Notification.Name.Demo.UpdateDebugging, + object: nil, + userInfo: ["propertiesInfo": msg] + ) } public func updateCameraInfo(msg: String) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), object: nil, userInfo: ["cameraInfo": msg]) + NotificationCenter.default.post( + name: Notification.Name.Demo.UpdateDebugging, + object: nil, + userInfo: ["cameraInfo": msg] + ) } public func updatePauseInfo(msg: String) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), object: nil, userInfo: ["pauseInfo": msg]) + NotificationCenter.default.post( + name: Notification.Name.Demo.UpdateDebugging, + object: nil, userInfo: ["pauseInfo": msg] + ) } - public func updateIsolatedInfo(msg: String) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), object: nil, userInfo: ["isolatedInfo": msg]) + public func updateScreenInfo(msg: String) { + NotificationCenter.default.post( + name: Notification.Name.Demo.UpdateDebugging, + object: nil, + userInfo: ["screenInfo": msg] + ) } - public func updateCoordinateInfo(msg: String) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), object: nil, userInfo: ["coordinateInfo": msg]) + /** + Update the camera debugging info. + + - parameter sceneCamera: `SKTiledSceneCamera?` scene camera. + */ + public func updateCameraInfo(_ sceneCamera: SKTiledSceneCamera?) { + var cameraInfo = "Camera:" + if let sceneCamera = sceneCamera { + cameraInfo = sceneCamera.description + } + + NotificationCenter.default.post( + name: Notification.Name.Camera.Updated, + object: sceneCamera, + userInfo: ["cameraInfo": cameraInfo] + ) } + /** Send a command to the UI to update status. @@ -289,21 +252,13 @@ public class SKTiledDemoScene: SKTiledScene { */ public func updateCommandString(_ command: String, duration: TimeInterval = 3.0) { DispatchQueue.main.async { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateCommandString"), object: nil, userInfo: ["command": command, "duration": duration]) - } - } - - /** - Callback to remove coordinates. - - parameter notification: `Notification` notification center callback. - */ - public func updateCoordinate(notification: Notification) { - let tempCoord = CGPoint(x: notification.userInfo!["x"] as! Int, - y: notification.userInfo!["y"] as! Int) - - guard (tempCoord != currentCoordinate) else { return } - currentCoordinate = tempCoord + NotificationCenter.default.post( + name: Notification.Name.Debug.CommandIssued, + object: nil, + userInfo: ["command": command, "duration": duration] + ) + } } /** @@ -312,12 +267,8 @@ public class SKTiledDemoScene: SKTiledScene { - parameter map: `SKTilemap?` tile map. */ public func updateHud(_ map: SKTilemap?) { - guard let view = view else { return } guard let map = map else { return } updateMapInfo(msg: map.description) - - let wintitle = "\(map.url.lastPathComponent) - \(view.bounds.size.shortDescription)" - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateWindowTitle"), object: nil, userInfo: ["wintitle": wintitle]) } /** @@ -385,49 +336,6 @@ public class SKTiledDemoScene: SKTiledScene { arrowShape.zPosition = lastZPosition } - /** - Cleanup all tile shapes outside of the given coordinate. - - - parameter coord: `CGPoint?` current focus coord. - */ - func cleanupTileShapes(coord: CGPoint? = nil) { - // cleanup everything - guard let currentCoord = coord else { - self.enumerateChildNodes(withName: "//*") { node, _ in - if let tile = node as? TileShape { - self.cleanup.insert(tile) - } - } - return - } - - self.enumerateChildNodes(withName: "//*") { node, _ in - - if let tile = node as? TileShape { - - switch tile.role { - case .pathfinding: - break - default: - - // if focus coordinate has changed, initialize all of the current tile shapes - if (tile.coord != currentCoord) { - if (tile.initialized == false) { - if self.clickshapes.contains(tile) { - tile.initialized = true - } - } - } else { - if (tile.initialized == true) { - tile.interactions += 1 - } - } - - } - } - } - } - /** Cleanup all tile shapes representing the current path. */ @@ -445,26 +353,6 @@ public class SKTiledDemoScene: SKTiledScene { override open func update(_ currentTime: TimeInterval) { super.update(currentTime) - for shape in self.clickshapes { - // move moused-over shapes to cleanup... - if (shape.initialized == true) && (shape.interactions > 0) { - - let fadeAction = SKAction.fadeOut(withDuration: 0.2) - shape.run(fadeAction, completion: { - self.clickshapes.remove(shape) - self.cleanup.insert(shape) - }) - } - } - - // cleanup everything in the queue - for tile in self.cleanup { - self.cleanupQueue.async { - self.cleanup.remove(tile) - tile.removeFromParent() - } - } - var coordinateMessage = "" if let graphStartCoordinate = graphStartCoordinate { @@ -473,8 +361,6 @@ public class SKTiledDemoScene: SKTiledScene { coordinateMessage += ", \(currentPath.count) nodes" } } - - updateCoordinateInfo(msg: coordinateMessage) } // MARK: - Delegate Callbacks @@ -486,7 +372,6 @@ public class SKTiledDemoScene: SKTiledScene { override open func didAddTileset(_ tileset: SKTileset) { let imageCount = (tileset.isImageCollection == true) ? tileset.dataCount : 0 - // let statusMessage = (imageCount > 0) ? "images: \(imageCount)" : "rendered: \(tileset.isRendered)" log("tileset added: \"\(tileset.name)\", \(statusMessage)", level: .debug) } @@ -494,12 +379,19 @@ public class SKTiledDemoScene: SKTiledScene { override open func didRenderMap(_ tilemap: SKTilemap) { // update the HUD to reflect the number of tiles created updateHud(tilemap) + + // allow the cache to send notifications + tilemap.dataStorage?.blockNotifications = false + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) } override open func didAddNavigationGraph(_ graph: GKGridGraph) { super.didAddNavigationGraph(graph) - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateUIControls"), - object: nil, userInfo: ["hasGraphs": true]) } } @@ -518,10 +410,7 @@ extension SKTiledDemoScene { let positionInLayer = defaultLayer.touchLocation(touch) let coord = defaultLayer.coordinateAtTouchLocation(touch) - // add a tile shape to the base layer where the user has clicked - // highlight the current coordinate - let _ = addTileToWorld(Int(coord.x), Int(coord.y), role: .highlight) // update the tile information label let coordStr = "Coord: \(coord.shortDescription), \(positionInLayer.roundTo())" @@ -542,6 +431,7 @@ extension SKTiledDemoScene { #if os(macOS) + // Mouse-based event handling extension SKTiledDemoScene { @@ -557,38 +447,7 @@ extension SKTiledDemoScene { cameraNode.mouseDown(with: event) let defaultLayer = tilemap.defaultLayer - - let coord = defaultLayer.coordinateAtMouseEvent(event: event) - - let tileShapesUnderCursor = tileShapesAt(event: event) - for tile in tileShapesUnderCursor where tile.role == .coordinate { - tile.interactions += 1 - } - - if (liveMode == true) && (isPaused == false) { - - // double click - if (event.clickCount > 1) { - plotPathfindingPath = true - let hasStartCoordinate: Bool = (graphStartCoordinate != nil) - - let eventMessage = (hasStartCoordinate == true) ? "clearing coordinate" : "setting coordinate: \(coord.shortDescription)" - graphStartCoordinate = (hasStartCoordinate == true) ? nil : coord - updateCommandString(eventMessage) - - if hasStartCoordinate == true { - cleanupPathfindingShapes() - } - - // single click - } else { - plotPathfindingPath = false - // highlight the current coordinate - if let tile = addTileToWorld(Int(coord.x), Int(coord.y), role: .coordinate) { - self.clickshapes.insert(tile) - } - } - } + _ = defaultLayer.coordinateAtMouseEvent(event: event) } /** @@ -599,128 +458,130 @@ extension SKTiledDemoScene { override open func mouseMoved(with event: NSEvent) { super.mouseMoved(with: event) - guard let tilemap = tilemap else { return } - - if let view = view { - let viewSize = view.bounds.size - - let positionInWindow = event.locationInWindow - let xpos = positionInWindow.x - let ypos = positionInWindow.y - - let dx = (xpos / viewSize.width) - 0.5 - let dy = (ypos / viewSize.height) - 0.5 - - mouseTracker.position.x = positionInWindow.x * (3 * dx) - mouseTracker.position.y = positionInWindow.y * (3 * dy) - //mouseTracker.setOffset(dx: dx, dy: dy) + guard (view != nil), let tilemap = tilemap else { + self.updateScreenInfo(msg: "--") + return } - let defaultLayer = tilemap.defaultLayer + DispatchQueue.main.async { + + let positionInScene = event.location(in: self) + let positionInView = self.convertPoint(toView: positionInScene) + + // debug outputs + var screenInfoString = "--" + let viewPositionString = "view: \(positionInView.shortDescription)" + let scenePositionString = "scene: \(positionInScene.shortDescription)" + var layerPositionString = "layer: --" + var coordInfoString = "coord: --" - // get the position relative as drawn by the - let positionInScene = event.location(in: self) - let positionInLayer = defaultLayer.mouseLocation(event: event) - let coord = defaultLayer.coordinateAtMouseEvent(event: event) - let validCoord = defaultLayer.isValid(Int(coord.x), Int(coord.y)) + if let view = self.view { + let viewSize = view.bounds.size - graphEndCoordinate = (validCoord == true) ? coord : nil + let positionInWindow = event.locationInWindow + let xpos = positionInWindow.x + let ypos = positionInWindow.y - if let endCoord = graphEndCoordinate { - if (plotPathfindingPath == true) { - plotNavigationPath() - drawCurrentPath(withColor: tilemap.navigationColor) + _ = (xpos / viewSize.width) - 0.5 + _ = (ypos / viewSize.height) - 0.5 } - } - // query nodes under the cursor to update the properties label - var propertiesInfoString = "--" + let defaultLayer = tilemap.defaultLayer - let tileShapesUnderCursor = tileShapesAt(event: event) + // get the position relative as drawn by the + _ = event.location(in: self) + let positionInLayer = defaultLayer.mouseLocation(event: event) - for tile in tileShapesUnderCursor where tile.role == .coordinate { - tile.interactions += 1 - } + layerPositionString = "layer: \(positionInLayer.shortDescription)" - currentTile = nil - currentVectorObject = nil + let coord = defaultLayer.coordinateAtMouseEvent(event: event) + let validCoord = defaultLayer.isValid(Int(coord.x), Int(coord.y)) - var currentLayerSet = false - if currentLayer != nil { - if currentLayer!.isolated == true { - currentLayerSet = true - } - } + coordInfoString = "coord: \(coord.shortDescription)" + self.graphEndCoordinate = (validCoord == true) ? coord : nil - if currentLayerSet == false { - currentLayer = nil - } + if (self.graphEndCoordinate != nil) { + if (self.plotPathfindingPath == true) { + self.plotNavigationPath() + self.drawCurrentPath(withColor: tilemap.navigationColor) + } + } - // let renderableNodes = renderableNodesAt(point: positionInScene) - if let focusObject = tilemap.focusObjects.first { - propertiesInfoString = focusObject.description + // query nodes under the cursor to update the properties label + var propertiesInfoString = "--" + self.currentTile = nil + self.currentVectorObject = nil + self.currentProxyObject = nil - if let firstTile = focusObject as? SKTile { - currentTile = firstTile - if currentLayerSet == false { - currentLayer = firstTile.layer + var currentLayerSet = false + if (self.currentLayer != nil) { + if (self.currentLayer!.isolated == true) { + currentLayerSet = true } } - if let firstObject = focusObject as? SKTileObject { - currentVectorObject = firstObject - if currentLayerSet == false { - currentLayer = firstObject.layer - } + if currentLayerSet == false { + self.currentLayer = nil } - } - // update the mouse tracking node - mouseTracker.position = positionInScene - mouseTracker.zPosition = tilemap.lastZPosition * 10 - mouseTracker.coord = coord - mouseTracker.isValid = validCoord + if let focusObject = self.focusObjects.first { + + if let firstTile = focusObject as? SKTile { + + propertiesInfoString = firstTile.description + self.currentTile = firstTile + if currentLayerSet == false { + self.currentLayer = firstTile.layer + } + } - if (tileShapesUnderCursor.isEmpty) { - if (liveMode == true) && (isPaused == false) { - _ = self.addTileToWorld(Int(coord.x), Int(coord.y), role: .highlight) + if let firstObject = focusObject as? SKTileObject { + propertiesInfoString = firstObject.description + self.currentVectorObject = firstObject + if currentLayerSet == false { + self.currentLayer = firstObject.layer + } + } + + if let firstProxy = focusObject as? TileObjectProxy { + self.currentProxyObject = firstProxy + + if let proxyReference = firstProxy.reference { + self.currentVectorObject = proxyReference + propertiesInfoString = proxyReference.description + } + } } - } - // update the focused coordinate - let coordDescription = "\(Int(coord.x)), \(Int(coord.y))" - updateTileInfo(msg: "Coord: \(coordDescription), \(positionInLayer.roundTo())") - updatePropertiesInfo(msg: propertiesInfoString) - let x = Int(coord.x) - let y = Int(coord.y) - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateCoordinate"), object: nil, userInfo: ["x": x, "y": y]) + // update the focused coordinate + let coordDescription = "\(Int(coord.x)), \(Int(coord.y))" + self.updateTileInfo(msg: "Coord: \(coordDescription), \(positionInLayer.roundTo())") + self.updatePropertiesInfo(msg: propertiesInfoString) - // highlight tile & text objects - if (liveMode == true) { - if (currentVectorObject != nil) { - if (currentVectorObject!.isRenderableType == true) { - currentVectorObject!.drawBounds(withColor: TiledObjectColors.dandelion, zpos: nil, duration: 0.35) - currentVectorObject = nil - } - } + // debugging + let outputArray = [viewPositionString, scenePositionString, layerPositionString, coordInfoString] + screenInfoString = outputArray.joined(separator: ", ") + + // send the label data to the view controller + self.updateScreenInfo(msg: screenInfoString) } } - override open func mouseEntered(with event: NSEvent) { - self.mouseTracker.isHidden = false + override open func mouseDragged(with event: NSEvent) { + super.mouseDragged(with: event) + guard let cameraNode = cameraNode else { return } + cameraNode.scenePositionChanged(with: event) } - override open func mouseExited(with event: NSEvent) { - self.mouseTracker.isHidden = true + override open func mouseEntered(with event: NSEvent) { + mousePointer.isHidden = false } - override open func mouseDragged(with event: NSEvent) { - super.mouseDragged(with: event) - guard let cameraNode = cameraNode else { return } - cameraNode.scenePositionChanged(event) + override open func mouseExited(with event: NSEvent) { + mousePointer.isHidden = true } override open func keyDown(with event: NSEvent) { @@ -731,27 +592,22 @@ extension SKTiledDemoScene { super.keyUp(with: event) } - open func tileShapesAt(event: NSEvent) -> [TileShape] { - let positionInScene = event.location(in: self) - // TODO: enumeration error - // https://stackoverflow.com/questions/24626462/error-when-attempting-to-remove-node-from-parent-using-fast-enumeration - // https://stackoverflow.com/questions/32464988/swift-multithreading-error-by-using-enumeratechildnodeswithname?rq=1 - let nodesAtPosition = nodes(at: positionInScene) - return nodesAtPosition.filter { $0 as? TileShape != nil } as! [TileShape] - } - /** Remove old tracking views and add the current. */ open func updateTrackingViews() { if let view = self.view { - let options: NSTrackingAreaOptions = [.mouseEnteredAndExited, .mouseMoved, .activeAlways, .cursorUpdate] + + let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .mouseMoved, .activeAlways, .cursorUpdate] + //let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways] + // clear out old tracking areas for oldTrackingArea in view.trackingAreas { view.removeTrackingArea(oldTrackingArea) } let trackingArea = NSTrackingArea(rect: view.frame, options: options, owner: self, userInfo: nil) + view.addTrackingArea(trackingArea) if let cameraNode = cameraNode { @@ -764,93 +620,16 @@ extension SKTiledDemoScene { #endif -internal class MouseTracker: SKNode { - - private var label = SKLabelNode(fontNamed: "Courier") - private var shadow = SKLabelNode(fontNamed: "Courier") - private var shadowOffset: CGFloat = 1 - private var circle = SKShapeNode() - private let scaleAction = SKAction.scale(by: 1.55, duration: 0.025) - private let scaleSequence: SKAction - - private let scaleSize: CGFloat = 8 - - var coord: CGPoint = .zero { - didSet { - label.text = "x: \(Int(coord.x)), y: \(Int(coord.y))" - shadow.text = label.text - } - } - - var fontSize: CGFloat = 12 { - didSet { - label.fontSize = fontSize - shadow.fontSize = label.fontSize - } - } - - var isValid: Bool = false { - didSet { - guard oldValue != isValid else { return } - circle.run(scaleSequence) - circle.fillColor = (isValid == true) ? SKColor(hexString: "#84EC1C") : SKColor(hexString: "#FE2929") - } - } - - var radius: CGFloat = 4 { - didSet { - circle = SKShapeNode(circleOfRadius: radius) - } - } - - override init() { - scaleSequence = SKAction.sequence([scaleAction, scaleAction.reversed()]) - super.init() - draw() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setOffset(dx: CGFloat, dy: CGFloat) { - let vector = (1 / -dy) * (fabs(dy) * 2) - label.position.y = vector * (scaleSize * 2) - } - - func draw() { - circle = SKShapeNode(circleOfRadius: radius) - addChild(circle) - - addChild(label) - label.addChild(shadow) - shadow.zPosition = label.zPosition - 1 - fontSize = scaleSize * 1.5 - - circle.strokeColor = .clear - label.fontSize = fontSize - shadow.fontSize = fontSize - shadow.fontColor = SKColor.black.withAlphaComponent(0.7) - - shadow.position.x += shadowOffset - shadow.position.y -= shadowOffset - label.position.y += scaleSize * 2 - } -} - - extension SKTiledDemoScene { // MARK: - Delegate Methods + /** Called when the camera positon changes. - parameter newPositon: `CGPoint` updated camera position. */ override public func cameraPositionChanged(newPosition: CGPoint) { - // TODO: remove this notification callback in master - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), - object: nil, - userInfo: ["cameraInfo": cameraNode?.description ?? "nil"]) + updateCameraInfo(cameraNode) } /** @@ -859,9 +638,7 @@ extension SKTiledDemoScene { - parameter newZoom: `CGFloat` camera zoom amount. */ override public func cameraZoomChanged(newZoom: CGFloat) { - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), - object: nil, - userInfo: ["cameraInfo": cameraNode?.description ?? "nil"]) + updateCameraInfo(cameraNode) } /** @@ -873,24 +650,24 @@ extension SKTiledDemoScene { */ override public func cameraBoundsChanged(bounds: CGRect, position: CGPoint, zoom: CGFloat) { // override in subclass - log("camera bounds updated: \(bounds.roundTo()), pos: \(position.roundTo()), zoom: \(zoom.roundTo())", - level: .debug) - + log("camera bounds updated: \(bounds.roundTo()), pos: \(position.roundTo()), zoom: \(zoom.roundTo())", level: .debug) + updateCameraInfo(cameraNode) } - #if os(iOS) || os(tvOS) + #if os(iOS) /** - Called when the scene is double-tapped. (iOS only) + Called when the scene receives a double-tap event (iOS only). - - parameter location: `CGPoint` touch location. + - parameter location: `CGPoint` touch event location. */ override public func sceneDoubleTapped(location: CGPoint) { log("scene was double tapped.", level: .debug) } - #else + #endif + #if os(macOS) /** - Called when the scene is double-clicked. (macOS only) + Called when the scene is double-clicked (macOS only). - parameter event: `NSEvent` mouse click event. */ @@ -900,14 +677,98 @@ extension SKTiledDemoScene { } /** - Called when the mouse moves in the scene. (macOS only) + Called when the mouse moves in the scene (macOS only). - parameter event: `NSEvent` mouse event. */ override public func mousePositionChanged(event: NSEvent) { - //let location = event.location(in: self) + guard let tilemap = tilemap else { return } + + let locationInMap = event.location(in: tilemap) + let nodesUnderCursor = tilemap.nodes(at: locationInMap) + + demoQueue.async { + // populate the focus objects array + self.focusObjects = nodesUnderCursor.filter { node in + (node as? SKTiledGeometry != nil) + } + + // call back to the view controller + DispatchQueue.main.async { + + if !self.focusObjects.isEmpty { + + NotificationCenter.default.post( + name: Notification.Name.Demo.FocusObjectsChanged, + object: self.focusObjects, + userInfo: ["tilemap": tilemap] + ) + + + var currentTile: SKTile? + var currentObject: TileObjectProxy? + + + let doShowTileBounds = TiledGlobals.default.debug.mouseFilters.contains(.tilesUnderCursor) + let proxyIsFocused = (tilemap.showObjects == false) ? TiledGlobals.default.debug.mouseFilters.contains(.objectsUnderCursor) : false + + + for object in self.focusObjects { + + if let tile = object as? SKTile { + if (currentTile == nil) { + currentTile = tile + continue + } + } + + if let obj = object as? SKTileObject { + if let proxy = obj.proxy { + if (currentObject == nil) { + currentObject = proxy + proxy.isFocused = proxyIsFocused + continue + } + } + } + + + if let proxy = object as? TileObjectProxy { + currentObject = proxy + proxy.isFocused = proxyIsFocused + continue + } + } + + if let currentTile = currentTile { + + NotificationCenter.default.post( + name: Notification.Name.Demo.TileUnderCursor, + object: currentTile, + userInfo: nil + ) + } + + + if let currentObject = currentObject { + if let object = currentObject.reference { + NotificationCenter.default.post( + name: Notification.Name.Demo.ObjectUnderCursor, + object: object, + userInfo: nil + ) + } + } + + + currentTile?.frameColor = TiledGlobals.default.debug.tileHighlightColor + currentTile?.highlightColor = TiledGlobals.default.debug.tileHighlightColor + currentTile?.showBounds = doShowTileBounds + } + } + } } - #endif + // MARK: - Keyboard Events @@ -917,11 +778,8 @@ extension SKTiledDemoScene { - parameter eventKey: `UInt16` event key. */ public func keyboardEvent(eventKey: UInt16) { - guard let view = view, - let cameraNode = cameraNode, - let tilemap = tilemap, - let _ = worldNode else { - return + guard let view = view else { + return } // '→' advances to the next scene @@ -934,6 +792,48 @@ extension SKTiledDemoScene { self.loadPreviousScene() } + // '↑' raises the speed + if eventKey == 0x7e { + self.speed += 0.2 + updateCommandString("scene speed: \(speed.roundTo())", duration: 1.0) + } + + // '↓' lowers the speed + if eventKey == 0x7d { + self.speed -= 0.2 + updateCommandString("scene speed: \(speed.roundTo())", duration: 1.0) + } + + // 'h' shows/hides SpriteKit stats + if eventKey == 0x04 { + demoController?.toggleRenderStatistics() + } + + // 'k' clears the scene + if eventKey == 0x28 { + updateCommandString("clearing scene...", duration: 3.0) + + NotificationCenter.default.post( + name: Notification.Name.Demo.FlushScene, + object: nil + ) + } + + // 'p' pauses the scene + if eventKey == 0x23 { + self.isPaused = !self.isPaused + } + + // 'r' reloads the scene + if eventKey == 0xf { + self.reloadScene() + } + + guard let cameraNode = cameraNode else { + return + } + + // '1' sets the camera zoom to 100% if [0x12, 0x53].contains(eventKey) { cameraNode.setCameraZoom(1) @@ -948,114 +848,372 @@ extension SKTiledDemoScene { // 'a' or 'f' fits the map to the current view if eventKey == 0x0 || eventKey == 0x3 { - cameraNode.fitToView(newSize: view.bounds.size) + cameraNode.fitToView(newSize: view.bounds.size, transition: 0.25) updateCommandString("fitting map to view...", duration: 3.0) } - // 'd' shows oversize tiles - if eventKey == 0x2 { - - let oversizeTiles = tilemap.getTiles().filter { - $0.tileSize != tilemap.tileSize + // 'c' adjusts the camera zoom clamp value + if eventKey == 0x8 { + var newClampValue: CameraZoomClamping = .none + switch cameraNode.zoomClamping { + case .none: + newClampValue = .tenth + case .tenth: + newClampValue = .quarter + case .quarter: + newClampValue = .half + case .half: + newClampValue = .third + case .third: + newClampValue = .none } + self.cameraNode.zoomClamping = newClampValue + updateCommandString("camera zoom clamping: \(newClampValue)", duration: 1.0) + } - if (oversizeTiles.isEmpty == false) { - let tileMessage = (oversizeTiles.count == 1) ? "tile" : "tiles" - updateCommandString("drawing bounds for \(oversizeTiles.count) oversize \(tileMessage).", duration: 3.0) - } + guard let tilemap = tilemap, + (worldNode != nil) else { + return + } - oversizeTiles.forEach { - $0.drawBounds(withColor: nil, zpos: nil, duration: 6.0) - } + // 'e' turns off effects rendering + if eventKey == 0xe { + NotificationCenter.default.post( + name: Notification.Name.Debug.MapEffectsRenderingChanged, + object: nil + ) } // 'g' shows the grid for the map default layer. if eventKey == 0x5 { - NotificationCenter.default.post(name: Notification.Name(rawValue: "toggleMapDemoDrawGridBounds"), object: nil) - } - - // 'h' shows/hides SpriteKit stats - if eventKey == 0x04 { - if let view = self.view { - let debugState = !view.showsFPS - - view.showsFPS = debugState - view.showsNodeCount = debugState - view.showsDrawCount = debugState - view.showsPhysics = debugState - view.showsFields = debugState - } + NotificationCenter.default.post( + name: Notification.Name.Debug.MapDebuggingChanged, + object: nil + ) } // 'i' isolates current layer under the mouse (macOS) if eventKey == 0x22 { + + var command = "restoring all layers" + if let currentLayer = currentLayer { let willIsolateLayer = (currentLayer.isolated == false) - let command = (willIsolateLayer == true) ? "isolating layer: \"\(currentLayer.layerName)\"" : "restoring all layers" + command = (willIsolateLayer == true) ? "isolating layer: \"\(currentLayer.layerName)\"" : "restoring all layers" log(command, level: .debug) - updateCommandString(command, duration: 3.0) - - // update the info label (macOS) - let isolatedInfoString = (willIsolateLayer == true) ? "Isolating: \(currentLayer.description)" : "" - updateIsolatedInfo(msg: isolatedInfoString) // isolate the layer - currentLayer.isolateLayer() + currentLayer.isolateLayer(duration: 0.25) + + // no layer selected + } else { + tilemap.getLayers().forEach { layer in + if (layer.isolated == true) { + layer.isolateLayer(duration: 0.25) + } + } } - } - // 'l' toggles live mode - if eventKey == 0x25 { - let command = (self.liveMode == true) ? "disabling live mode" : "enabling live mode" updateCommandString(command, duration: 3.0) - liveMode = !liveMode - let liveModeString = (liveMode == true) ? "Live Mode: On" : "Live Mode: Off" - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDelegateMenuItems"), object: nil, userInfo: ["liveMode": liveModeString]) + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) } - - // 'o' shows/hides object layers + + // 'o' shows/hides objects if eventKey == 0x1f { - NotificationCenter.default.post(name: Notification.Name(rawValue: "toggleMapObjectDrawing"), object: nil) + + NotificationCenter.default.post( + name: Notification.Name.Debug.MapObjectVisibilityChanged, + object: nil + ) } - // 'p' pauses the scene - if eventKey == 0x23 { - self.isPaused = !self.isPaused + + // 't' toggles effects rasterization + if eventKey == 0x11 { + let currentValue = tilemap.shouldRasterize + let commandString = (currentValue == false) ? "on" : "off" + tilemap.shouldRasterize = !currentValue + updateCommandString("rasterization: \(commandString)", duration: 1.0) + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) } - // 'r' reloads the scene - if eventKey == 0xf { - self.reloadScene() + // 'u' cycles tile update mode + if eventKey == 0x20 { + demoController?.cycleTilemapUpdateMode() } - // '↑' raises the speed - if eventKey == 0x7e { - self.speed += 0.5 - updateCommandString("scene speed: \(speed.roundTo())", duration: 1.0) + // 'v' cycles time display for render stats + if eventKey == 0x9 { + let nextFormat: TiledGlobals.TimeDisplayMode = (TiledGlobals.default.timeDisplayMode == .seconds) ? .milliseconds : .seconds + TiledGlobals.default.timeDisplayMode = nextFormat + self.updateCommandString("setting render stats time format: \(TiledGlobals.default.timeDisplayMode)", duration: 2) + + // update controllers + NotificationCenter.default.post( + name: Notification.Name.Globals.Updated, + object: nil, + userInfo: nil + ) } + } + #endif +} - // '↓' lowers the speed - if eventKey == 0x7d { - self.speed -= 0.5 - updateCommandString("scene speed: \(speed.roundTo())", duration: 1.0) + +#if os(macOS) + +/// Debugging HUD display that follows the macOS cursor +internal class MousePointer: SKNode { + + var fontName: String = "Courier" + var fontSize: CGFloat = 12 + + var color: SKColor = SKColor.white + var receiveCameraUpdates: Bool = TiledGlobals.default.enableCameraCallbacks + + var currentTile: SKTile? + var currentObject: SKTileObject? + + var sceneLabel: SKLabelNode? + var coordLabel: SKLabelNode? + var tileLabel: SKLabelNode? + + + var mouseFilters: TiledGlobals.DebugDisplayOptions.MouseFilters { + return TiledGlobals.default.debug.mouseFilters + } + + var lineCount: Int { + var result = 0 + if (mouseFilters.contains(.tileCoordinates)) { + result += 1 + } + if (mouseFilters.contains(.sceneCoordinates)) { + result += 1 } - // 'c' adjusts the camera zoom clamp value - if eventKey == 0x8 { - var newClampValue: CameraZoomClamping = .none - switch cameraNode.zoomClamping { - case .none: - newClampValue = .tenth - case .tenth: - newClampValue = .quarter - case .quarter: - newClampValue = .half - case .half: - newClampValue = .none + if (mouseFilters.contains(.tileDataUnderCursor)) { + result += 1 + } + return result + } + + var drawTileCoordinates: Bool { + return mouseFilters.contains(.tileCoordinates) + } + + var drawSceneCoordinates: Bool { + return mouseFilters.contains(.sceneCoordinates) + } + + var drawTileData: Bool { + return mouseFilters.contains(.tileDataUnderCursor) + } + + var drawLocalID: Bool { + return mouseFilters.contains(.tileLocalID) + } + + override init() { + super.init() + zPosition = 10000 + setupLabels() + setupNotifications() + } + + required init?(coder aDecoder: NSCoder) { + super.init() + setupLabels() + setupNotifications() + } + + func setupLabels() { + if (sceneLabel == nil) { + sceneLabel = SKLabelNode(fontNamed: fontName) + addChild(sceneLabel!) + } + if (coordLabel == nil) { + coordLabel = SKLabelNode(fontNamed: fontName) + addChild(coordLabel!) + } + if (tileLabel == nil) { + tileLabel = SKLabelNode(fontNamed: fontName) + addChild(tileLabel!) + } + } + + func setupNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(tileUnderCursor), name: Notification.Name.Demo.TileUnderCursor, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(objectUnderCursor), name: Notification.Name.Demo.ObjectUnderCursor, object: nil) + } + + @objc func tileUnderCursor(notification: Notification) { + guard let tile = notification.object as? SKTile else { return } + currentTile = tile + } + + @objc func objectUnderCursor(notification: Notification) { + guard let object = notification.object as? SKTileObject else { return } + currentObject = object + } + + + func draw(event: NSEvent, scene: SKScene) { + + if let tiledScene = scene as? SKTiledScene { + if let tilemap = tiledScene.tilemap { + + let scenePosition = event.location(in: scene) + + self.position = scenePosition + let coordinate = tilemap.coordinateAtMouseEvent(event: event) + let coordColor = tilemap.isValid(coord: coordinate) ? color : TiledObjectColors.coral + + + let labelStyle = NSMutableParagraphStyle() + labelStyle.alignment = .center + + let defaultLabelAttributes = [ + .font: NSFont(name: fontName, size: fontSize)!, + .foregroundColor: color, + .paragraphStyle: labelStyle + ] as [NSAttributedString.Key: Any] + + let coordAttributes = [ + .font: NSFont(name: fontName, size: fontSize)!, + .foregroundColor: coordColor, + .paragraphStyle: labelStyle + ] as [NSAttributedString.Key: Any] + + + var labelIndex = 0 + + if (drawSceneCoordinates == true) { + + let outputString = NSMutableAttributedString() + + let labelText = "scene: " + let labelString = NSMutableAttributedString(string: labelText, attributes: defaultLabelAttributes) + let dataString = NSMutableAttributedString(string: scenePosition.shortDescription, attributes: defaultLabelAttributes) + + outputString.append(labelString) + outputString.append(dataString) + sceneLabel?.attributedText = outputString + sceneLabel?.position.y = CGFloat(labelIndex - lineCount / 2) * self.fontSize + self.fontSize + labelIndex += 1 + } + + + if (drawTileCoordinates == true) { + + let outputString = NSMutableAttributedString() + + let labelText = "coord: " + let labelString = NSMutableAttributedString(string: labelText, attributes: defaultLabelAttributes) + let dataString = NSMutableAttributedString(string: coordinate.shortDescription, attributes: coordAttributes) + + outputString.append(labelString) + outputString.append(dataString) + + coordLabel?.attributedText = outputString + coordLabel?.position.y = CGFloat(labelIndex - lineCount / 2) * self.fontSize + self.fontSize + labelIndex += 1 + } + + tileLabel?.isHidden = true + + if (drawTileData == true) { + // tile id: 0, gid: 27 + let outputString = NSMutableAttributedString() + + if let currentTile = currentTile { + + let td = currentTile.tileData + let idsIdentical = (td.id == td.globalID) + + var globalIDString = "\(td.globalID)" + var originalIDString: String? = nil + var idColor = color + + switch currentTile.renderMode { + case .animated(let gid): + if (gid != nil) { + globalIDString = "\(gid!)" + originalIDString = "\(td.globalID)" + idColor = TiledObjectColors.dandelion + } + + default: + break + } + + + let globalIDLabelAttributes = [ + .font: NSFont(name: fontName, size: fontSize)!, + .foregroundColor: idColor, + .paragraphStyle: labelStyle + ] as [NSAttributedString.Key: Any] + + + // contruct the first part of the label + let tileDataString = (idsIdentical == true) ? "tile gid: " : (drawLocalID == true) ? "tile id: \(td.id), gid: " : "tile gid: " + let labelStringFirst = NSMutableAttributedString(string: tileDataString, attributes: defaultLabelAttributes) + outputString.append(labelStringFirst) + + // tile id: 0, gid: + if let originalIDString = originalIDString { + // highlight the global id in yellow + let labelStringSecond = NSMutableAttributedString(string: globalIDString, attributes: globalIDLabelAttributes) + // after, in parenthesis, indicate the ORIGINAL gid + let labelStringThird = NSMutableAttributedString(string: " (\(originalIDString))", attributes: defaultLabelAttributes) + outputString.append(labelStringSecond) + outputString.append(labelStringThird) + + } else { + // just add the normal tile gid + let labelStringSecond = NSMutableAttributedString(string: globalIDString, attributes: defaultLabelAttributes) + outputString.append(labelStringSecond) + } + + tileLabel?.position.y = CGFloat(labelIndex - lineCount / 2) * self.fontSize + self.fontSize + tileLabel?.isHidden = false + tileLabel?.attributedText = outputString + labelIndex += 1 + } + + } } - self.cameraNode.zoomClamping = newClampValue - updateCommandString("camera zoom clamping: \(newClampValue)", duration: 1.0) } } + + deinit { + NotificationCenter.default.removeObserver(self, name: Notification.Name.Demo.TileUnderCursor, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.Demo.ObjectUnderCursor, object: nil) + } } + + +extension MousePointer: SKTiledSceneCameraDelegate { + + /** + Called when the mouse moves in the scene. + + - parameter event: `NSEvent` mouse click event. + */ + func mousePositionChanged(event: NSEvent) { + guard let scene = scene else { return } + self.draw(event: event, scene: scene) + } + +} +#endif diff --git a/README.md b/README.md index 705d4ce1..6d9690ea 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ > SKTiled is a Swift framework for using [Tiled][tiled-url] assets with Apple's SpriteKit. -[![Swift Version][swift3-image]][swift-url] -[![Xcode Version][xcode9-image]][xcode-downloads-url] +[![Swift Version][swift4-image]][swift-url] +[![Xcode Version][xcode10-image]][xcode-downloads-url] [![Build Status][travis-image]][travis-url] [![License][license-image]][license-url] [![Platforms][platforms-image]][platforms-url] @@ -17,8 +17,9 @@ ![Demo Image][demo-iphone-img] -- for Xcode 8, see the [**xcode8**][branch-xcode8-url] branch -- for Swift 4, see the [**swift4**][branch-swift4-url] branch +This branch is for **Xcode 10 & Swift 4.2**. + +- for Xcode 9 & Swift 3.2, see the [**xcode9**][branch-xcode9-url] branch Check out the [Official Documentation](https://mfessenden.github.io/SKTiled). @@ -26,7 +27,7 @@ Check out the [Official Documentation](https://mfessenden.github.io/SKTiled). ## Features - [x] iOS & macOS versions -- [ ] tvOS version +- [x] tvOS version - [x] parses inline & external tilesets - [x] translates custom properties for maps, layers, objects & tiles - [x] renders all projections: (orthogonal, isometric, hexagonal & isometric staggered) @@ -37,18 +38,21 @@ Check out the [Official Documentation](https://mfessenden.github.io/SKTiled). - [x] group nodes - [x] tile objects - [x] text objects -- [ ] tile collision objects +- [x] template objects - [x] custom tile & object classes - [x] generate GKGridGraph graphs from custom attributes - [x] user-definable cost properties for GKGridGraph nodes - [ ] infinite maps +- [ ] tile collision objects +- [ ] parse JSON tilemaps ## Requirements -- iOS 9+ -- macOS 10.12+ -- Xcode 9/Swift 3.3 +- iOS 11 +- tvOS 12 +- macOS 10.13 +- Xcode 10/Swift 4.2 ## Installation @@ -56,12 +60,12 @@ Check out the [Official Documentation](https://mfessenden.github.io/SKTiled). For Carthage installation, create a Cartfile in the root of your project: - github "mfessenden/SKTiled" ~> 1.16 + github "mfessenden/SKTiled" ~> 1.20 For CocoaPods, install via a reference in your podfile: - pod 'SKTiled', '~> 1.16' + pod 'SKTiled', '~> 1.20' ## Usage @@ -108,7 +112,7 @@ let hudLayers = tilemap.getLayers(named: "HUD") as! [SKImageLayer] // query layer at a specific index if let firstLayer = tilemap.getLayer(atIndex: 1) as! SKTileLayer { - firstLayer.showGrid = true + firstLayer.visible = true } ``` @@ -269,14 +273,14 @@ let allWalkable = tilemap.getTilesWithProperty("walkable", true") - [Clint Bellanger: Isometric Tiles Math](http://clintbellanger.net/articles/isometric_math) -[swift4-image]:https://img.shields.io/badge/Swift-4-brightgreen.svg -[swift3-image]:https://img.shields.io/badge/Swift-3.3-brightgreen.svg +[swift4-image]:https://img.shields.io/badge/Swift-4.2-brightgreen.svg +[swift-image]:https://img.shields.io/badge/Swift-3.2-brightgreen.svg [swift-url]: https://swift.org/ [license-image]:https://img.shields.io/badge/License-MIT-blue.svg [license-url]:https://github.com/mfessenden/SKTiled/blob/master/LICENSE [travis-image]:https://travis-ci.org/mfessenden/SKTiled.svg?branch=master [travis-url]:https://travis-ci.org/mfessenden/SKTiled -[platforms-image]:https://img.shields.io/badge/platforms-iOS%20%7C%20macOS-red.svg +[platforms-image]:https://img.shields.io/badge/platforms-iOS%20%7C%20tvOS%20%7C%20macOS-red.svg [platforms-url]:http://www.apple.com [carthage-image]:https://img.shields.io/badge/Carthage-compatible-4BC51D.svg [carthage-url]:https://github.com/Carthage/Carthage @@ -284,15 +288,17 @@ let allWalkable = tilemap.getTilesWithProperty("walkable", true") [xcode8-image]:https://img.shields.io/badge/Xcode-8-orange.svg [xcode9-image]:https://img.shields.io/badge/Xcode-9-orange.svg +[xcode10-image]:https://img.shields.io/badge/Xcode-10.0-orange.svg [xcode-downloads-url]:https://developer.apple.com/download/more/ [pod-url]:https://cocoapods.org/pods/SKTiled [branch-master-url]:https://github.com/mfessenden/SKTiled [branch-xcode8-url]:https://github.com/mfessenden/SKTiled/tree/xcode8 +[branch-xcode9-url]:https://github.com/mfessenden/SKTiled/tree/xcode9 [branch-swift4-url]:https://github.com/mfessenden/SKTiled/tree/swift4 -[header-image]:https://mfessenden.github.io/SKTiled/images/Header-@1x.png +[header-image]:https://mfessenden.github.io/SKTiled/images/header.png [demo-mac-image]:https://mfessenden.github.io/SKTiled/images/demo-macos-iso.png [demo-iphone-img]:https://mfessenden.github.io/SKTiled/images/demo-iphone.png diff --git a/Resources/AppIcon.png b/Resources/AppIcon.png deleted file mode 100644 index 0648bad5..00000000 Binary files a/Resources/AppIcon.png and /dev/null differ diff --git a/Resources/ArcadeNormal.ttf b/Resources/ArcadeNormal.ttf deleted file mode 100755 index 4730e445..00000000 Binary files a/Resources/ArcadeNormal.ttf and /dev/null differ diff --git a/Resources/User/RESOURCES.md b/Resources/User/RESOURCES.md deleted file mode 100644 index a262cadc..00000000 --- a/Resources/User/RESOURCES.md +++ /dev/null @@ -1,3 +0,0 @@ -# User Resources - -To test your own resources, add your own resources to this directory and compile one of the demo targets. diff --git a/Resources/dungeon-16x16.png b/Resources/dungeon-16x16.png deleted file mode 100644 index e6517f5f..00000000 Binary files a/Resources/dungeon-16x16.png and /dev/null differ diff --git a/Resources/dungeon-16x16.tmx b/Resources/dungeon-16x16.tmx deleted file mode 100644 index 2544eb62..00000000 --- a/Resources/dungeon-16x16.tmx +++ /dev/null @@ -1,487 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - eJztk79KA0EQxmc9xVYtRI2EQwQthPgHEowgB2KnPoD451JY2p0m2IqVRd7BIvgKkkIfwELfQtAXsPMbboYb1zs9Ebt88GOT3dlvZ2b3iAYaqLz2HdHIP/hOgSYYAgE4cH/zGwV1MO0xA4ZBJWfPHej/4BuCXXAIjkEMlgX2XzWxXALXswHWPB9e4zq1l6GMO+LfBi/ip6iewDN4cxS3zHwXvMJ43pwX0lfNmfXA9PlczuU9HRPfA2dSs55X5Dv7zbo/15MxMecV+aLWB77PhsxxrXWh4cVzngF97m+NKOL/fB+XlL6DdcmX+70ncbeU9oHV9nwT8bb95XfL974EruR3xeSr3Ev8SU591lv7u4B7OcW4BbbBOJgw+VpY1QJf1oWg+a6ATTAJFsEjZfd2DWKX0S35HdbEV72RcxQZ3xtK35C+sU6OR564J0eUfWvvMraKt5RS4uUz5n6X1wdgVjQZ - - - - - - - - eJxjYBgFo2AUDEYQRCNzM2lk7igYBSMFAACGMgC8 - - - - - - - - eJxjYBgFo2AUDDcwBYinAvE0Kpq5hopmIYPDFOjF56ZbFJhLyE3khi8lbqIloEV6Gc4AAFG6CdA= - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - eJxjYBhZQGHUXKqau5MRwY4Hsq8C8UQqmHsFSoOMv8zI8OAckLGCCubCgBsQGwPxU6C5qmSasZMB7DaGfRC6YReQtgZiGQrd9gpo5hVGBLsIiKWB7EQKze1kRMT7FSjfBkgvZcSthxiwgBE1Pe1hRKXJAaLQcN1PgbuwAR2guaeA9DUqmzsKRhYAAItVGVU= - - - - - - - - eJxjYCAfXADii0B8iQIzsIGzjAwMskB8jpG65j6BmveUyuaOglEwCkbBKBg5AADUzAX6 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - eJxjYBgFo2DgQSgjKqameYTMJ8U+XObhMpeQn5DNIwVgM5cYN+GyH58Yup/x6SMkj8tcQu5B908YieGOTx0hN+Ezm1g3kBvP2OwnRZxYM3HhHBq5l1IzaWEuKQAAmRAeqw== - - - - - - - - eJxjYBicIIyRNuaGjppLM3NBZsIwtc2EsWlhLqVmYMPUchu1/Y5uDq3MpYZ51E5Lo2AUUAsAAFgYChY= - - - - diff --git a/Resources/dungeon-16x32.png b/Resources/dungeon-16x32.png deleted file mode 100644 index 946db806..00000000 Binary files a/Resources/dungeon-16x32.png and /dev/null differ diff --git a/Resources/dungeon-16x32.tsx b/Resources/dungeon-16x32.tsx deleted file mode 100644 index d0e02424..00000000 --- a/Resources/dungeon-16x32.tsx +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/Resources/dungeon-32x32.tsx b/Resources/dungeon-32x32.tsx deleted file mode 100644 index 008883a4..00000000 --- a/Resources/dungeon-32x32.tsx +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/Resources/pacman/pacman-16x16.tsx b/Resources/pacman/pacman-16x16.tsx deleted file mode 100644 index 6c3b57ca..00000000 --- a/Resources/pacman/pacman-16x16.tsx +++ /dev/null @@ -1,253 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Resources/pacman/pacman-8x8.tsx b/Resources/pacman/pacman-8x8.tsx deleted file mode 100644 index a9d81181..00000000 --- a/Resources/pacman/pacman-8x8.tsx +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Resources/pacman/pacman.tmx b/Resources/pacman/pacman.tmx deleted file mode 100644 index 850479d5..00000000 --- a/Resources/pacman/pacman.tmx +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - eJzNVttKA0EMPVrwAq6vXqvgu1ZBW/BVC13rg///NWZxBs+GZC7ubjFw6JKeydnNZCYBprfTHWj8F9sTXFTiUfD0h3X7gpngsxIbQVvBj7YmvSPBScCx4WM/63kcHY/1liHGKqCluCvHX8Jhn/6+G8FdwK3h0/4SDvu03tjQ1qBs/7y99PasZP+8fPI+8f+HgoMKfmk+vbov8VvW6Z0JFgpc99dGXmJcqLzN0T8nOu658x7xmzdG3oDf+ozPnOeot3Zi5/TaDJeN8zlU7x4/Obki3mXwPVToWfeqPtfIvO8s/Op7wIrtGddYPKspvUbxPdM1xPUZzy8K9KD4Vn0uEuv1/r3B72vvgcP8IfWSyk+0ZkS9znL3Jwr1rHnCqs/c/QmDP/Y8wVaS86HzBByO9qf6Ed+Buf4OhwP0717dj5ZGrmp6d2f6vOs4Ue/ZyVVtfwf6592a4V4EH06uavs7awD2TLXFsP6uzfLzOq8+uV/r/p6qT83X67x5yUNJfabWTTl/WvDmpangzUtT2usOtb4E3+vHeRs= - - - - - eJzlVdkJwCAM7S5OIXQHwf2HaX8EkVzP2zYQkBy+JOS4rn9R2PSvFgoEe0YeFD/JN/ejYuBiQ2ysfl/Co+rtO71zmTUeS18j9VqBp+Wf2GVyV+gSafOgxYvkIcl3xeP2CLI/ULze9rPw4st3wdT8akTNe/lv7BBvrf1OeNIttO5HBO/0enL1od7c/tTuEbLP0Txa79EIPOmmWPtTu2XW2Fv6RfKdhcfVrdTXyD2jt9KpflZG+1bqz5k0En9lbg/Uuln7 - - - - - - - - - - - - eJztyqENACAMAMHuvw2eJaoxjIKpakJI0HfmzUcAAAAAv9alL6Oarbs6238AgucGgQ== - - - - - eJztwzENAAAIA7A9+LfMMxOENmkCADdNAwB8sGmEAAs= - - - - - 1up - - - 1440 - - - HIGH SCORE - - - 1440 - - - diff --git a/Resources/pacman/pm-maze-8x8.png b/Resources/pacman/pm-maze-8x8.png deleted file mode 100644 index 39cf0925..00000000 Binary files a/Resources/pacman/pm-maze-8x8.png and /dev/null differ diff --git a/Resources/pacman/pm-sprites-16x16.png b/Resources/pacman/pm-sprites-16x16.png deleted file mode 100644 index 0ab36c5a..00000000 Binary files a/Resources/pacman/pm-sprites-16x16.png and /dev/null differ diff --git a/Resources/roguelike-16x16-anim.png b/Resources/roguelike-16x16-anim.png deleted file mode 100644 index 76eedd95..00000000 Binary files a/Resources/roguelike-16x16-anim.png and /dev/null differ diff --git a/SKTiled.podspec b/SKTiled.podspec index c31f11c1..6c55456a 100644 --- a/SKTiled.podspec +++ b/SKTiled.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SKTiled" - s.version = "1.16" + s.version = "1.20" s.summary = "SKTiled is a framework for using Tiled content with Apple's SpriteKit." s.description = <<-DESC SKTiled is a framework for using Tiled content with Apple's SpriteKit, allowing the creation of game assets from .tmx files. @@ -9,11 +9,10 @@ Pod::Spec.new do |s| s.homepage = "https://github.com/mfessenden/SKTiled" s.license = { :type => 'MIT', :file => 'LICENSE.md' } - s.ios.deployment_target = '9.0' - s.osx.deployment_target = '10.12' + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.13' s.source = { :git => "https://github.com/mfessenden/SKTiled.git", :tag => s.version } s.source_files = 'Sources/*.swift' s.requires_arc = true - s.library = 'z' end diff --git a/SKTiled.xcodeproj/project.pbxproj b/SKTiled.xcodeproj/project.pbxproj index ddfdd5dc..897ad465 100644 --- a/SKTiled.xcodeproj/project.pbxproj +++ b/SKTiled.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 4C01D3A91F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C01D3A61F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift */; }; 4C01D3AA1F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C01D3A61F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift */; }; + 4C112D3A21A1E9DD009E51F7 /* staggered-paths-64x33.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C112D3921A1E9DC009E51F7 /* staggered-paths-64x33.png */; }; + 4C112D3B21A1E9DD009E51F7 /* staggered-paths-64x33.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C112D3921A1E9DC009E51F7 /* staggered-paths-64x33.png */; }; + 4C112D3C21A1E9DD009E51F7 /* staggered-paths-64x33.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C112D3921A1E9DC009E51F7 /* staggered-paths-64x33.png */; }; 4C1774A81D90999C000C0AFD /* hex-65x65-65x230.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CFEA35A1D8D04320055C150 /* hex-65x65-65x230.png */; }; 4C1774AA1D90999C000C0AFD /* isometric-130x230.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CD6D2DA1D8B9EA10083DA7B /* isometric-130x230.png */; }; 4C1774AB1D90999C000C0AFD /* dungeon-16x16.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C88E1AC1D8A242F00FCCFA3 /* dungeon-16x16.png */; }; @@ -24,6 +27,123 @@ 4C1774B81D9099A3000C0AFD /* SKTileset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1DA1D8A4EBE00FCCFA3 /* SKTileset.swift */; }; 4C1774B91D9099A3000C0AFD /* SKTilesetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1DC1D8A4EBE00FCCFA3 /* SKTilesetData.swift */; }; 4C1774BA1D9099A3000C0AFD /* SKTiledObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6AA21D8CC2C100DB56F1 /* SKTiledObject.swift */; }; + 4C1D689121932E6200D2D042 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1D689021932E6200D2D042 /* QueryTests.swift */; }; + 4C1D689221932E6200D2D042 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1D689021932E6200D2D042 /* QueryTests.swift */; }; + 4C1D689321932E6200D2D042 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1D689021932E6200D2D042 /* QueryTests.swift */; }; + 4C24B18920694D2E0027EBD5 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C24B18320694D2D0027EBD5 /* GameViewController.swift */; }; + 4C24B18A20694D2E0027EBD5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4C24B18420694D2D0027EBD5 /* Main.storyboard */; }; + 4C24B18B20694D2E0027EBD5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C24B18620694D2D0027EBD5 /* AppDelegate.swift */; }; + 4C24B18D20694D3B0027EBD5 /* SKTiled.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C24B18120694D2D0027EBD5 /* SKTiled.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4C24B18E20694E740027EBD5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C440B981DAE911000DEC9A4 /* Assets.xcassets */; }; + 4C24B18F20694E780027EBD5 /* SKTiledDemoScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C440B9A1DAE911000DEC9A4 /* SKTiledDemoScene.swift */; }; + 4C24B19020694E7B0027EBD5 /* DemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4184A41F34B51D004E392A /* DemoController.swift */; }; + 4C24B1CE20694FA70027EBD5 /* SKTiled+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3117D81EF84FCF00892D9E /* SKTiled+Debug.swift */; }; + 4C24B1CF20694FA70027EBD5 /* SKTiled+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D71D8A4EBE00FCCFA3 /* SKTiled+Extensions.swift */; }; + 4C24B1D020694FA70027EBD5 /* SKTiled+GameplayKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C01D3A61F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift */; }; + 4C24B1D120694FA70027EBD5 /* SKTilemap+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6A9E1D8CBA8A00DB56F1 /* SKTilemap+Properties.swift */; }; + 4C24B1D220694FA70027EBD5 /* SKTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D01D8A4EBE00FCCFA3 /* SKTile.swift */; }; + 4C24B1D320694FA70027EBD5 /* SKTiledObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6AA21D8CC2C100DB56F1 /* SKTiledObject.swift */; }; + 4C24B1D420694FA70027EBD5 /* SKTiledScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6AA01D8CBB5B00DB56F1 /* SKTiledScene.swift */; }; + 4C24B1D520694FA70027EBD5 /* SKTiledSceneCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D21D8A4EBE00FCCFA3 /* SKTiledSceneCamera.swift */; }; + 4C24B1D620694FA70027EBD5 /* SKTileLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D41D8A4EBE00FCCFA3 /* SKTileLayer.swift */; }; + 4C24B1D720694FA70027EBD5 /* SKTilemap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D51D8A4EBE00FCCFA3 /* SKTilemap.swift */; }; + 4C24B1D820694FA70027EBD5 /* SKTilemapParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D81D8A4EBE00FCCFA3 /* SKTilemapParser.swift */; }; + 4C24B1D920694FA70027EBD5 /* SKTileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D91D8A4EBE00FCCFA3 /* SKTileObject.swift */; }; + 4C24B1DA20694FA70027EBD5 /* SKTileset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1DA1D8A4EBE00FCCFA3 /* SKTileset.swift */; }; + 4C24B1DB20694FA70027EBD5 /* SKTilesetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1DC1D8A4EBE00FCCFA3 /* SKTilesetData.swift */; }; + 4C24B1DC20694FA80027EBD5 /* SKTiled+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3117D81EF84FCF00892D9E /* SKTiled+Debug.swift */; }; + 4C24B1DD20694FA80027EBD5 /* SKTiled+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D71D8A4EBE00FCCFA3 /* SKTiled+Extensions.swift */; }; + 4C24B1DE20694FA80027EBD5 /* SKTiled+GameplayKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C01D3A61F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift */; }; + 4C24B1DF20694FA80027EBD5 /* SKTilemap+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6A9E1D8CBA8A00DB56F1 /* SKTilemap+Properties.swift */; }; + 4C24B1E020694FA80027EBD5 /* SKTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D01D8A4EBE00FCCFA3 /* SKTile.swift */; }; + 4C24B1E120694FA80027EBD5 /* SKTiledObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6AA21D8CC2C100DB56F1 /* SKTiledObject.swift */; }; + 4C24B1E220694FA80027EBD5 /* SKTiledScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6AA01D8CBB5B00DB56F1 /* SKTiledScene.swift */; }; + 4C24B1E320694FA80027EBD5 /* SKTiledSceneCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D21D8A4EBE00FCCFA3 /* SKTiledSceneCamera.swift */; }; + 4C24B1E420694FA80027EBD5 /* SKTileLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D41D8A4EBE00FCCFA3 /* SKTileLayer.swift */; }; + 4C24B1E520694FA80027EBD5 /* SKTilemap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D51D8A4EBE00FCCFA3 /* SKTilemap.swift */; }; + 4C24B1E620694FA80027EBD5 /* SKTilemapParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D81D8A4EBE00FCCFA3 /* SKTilemapParser.swift */; }; + 4C24B1E720694FA80027EBD5 /* SKTileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D91D8A4EBE00FCCFA3 /* SKTileObject.swift */; }; + 4C24B1E820694FA80027EBD5 /* SKTileset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1DA1D8A4EBE00FCCFA3 /* SKTileset.swift */; }; + 4C24B1E920694FA80027EBD5 /* SKTilesetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1DC1D8A4EBE00FCCFA3 /* SKTilesetData.swift */; }; + 4C24B1EB20694FBB0027EBD5 /* dungeon-16x16.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4CAD35BC1F4B9D2C0034CA6C /* dungeon-16x16.tmx */; }; + 4C24B1EC20694FBB0027EBD5 /* hex-65x65.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C8227E81F4E0E63007E4556 /* hex-65x65.tmx */; }; + 4C24B1ED20694FBB0027EBD5 /* isometric-130x66.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C8227E21F4E0D5E007E4556 /* isometric-130x66.tmx */; }; + 4C24B1EF20694FBB0027EBD5 /* roguelike-16x16.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C8227E51F4E0E3F007E4556 /* roguelike-16x16.tmx */; }; + 4C24B1F020694FBB0027EBD5 /* staggered-64x33.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C3184E71F4DCB4200950BBF /* staggered-64x33.tmx */; }; + 4C24B1F120694FBB0027EBD5 /* sk1-32x32.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0D41F53A0F700CE1DEB /* sk1-32x32.tmx */; }; + 4C24B1F220694FBB0027EBD5 /* sk2-32x32.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0D51F53A0F700CE1DEB /* sk2-32x32.tmx */; }; + 4C24B1F320694FC30027EBD5 /* dungeon-16x32.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1031F55A7D500CE1DEB /* dungeon-16x32.tsx */; }; + 4C24B1F420694FC30027EBD5 /* dungeon-32x32.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1091F55A7F300CE1DEB /* dungeon-32x32.tsx */; }; + 4C24B1F720694FC30027EBD5 /* roguelike-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C7DD6211F50E16000C3FE2D /* roguelike-16x16.tsx */; }; + 4C24B1F820694FC80027EBD5 /* dungeon-32x32.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1061F55A7DE00CE1DEB /* dungeon-32x32.png */; }; + 4C24B1F920694FC80027EBD5 /* dungeon-16x16.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C88E1AC1D8A242F00FCCFA3 /* dungeon-16x16.png */; }; + 4C24B1FA20694FC80027EBD5 /* dungeon-16x32.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1001F55A7CA00CE1DEB /* dungeon-16x32.png */; }; + 4C24B1FB20694FC80027EBD5 /* hex-65x65-65x230.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CFEA35A1D8D04320055C150 /* hex-65x65-65x230.png */; }; + 4C24B1FC20694FC80027EBD5 /* isometric-130x230.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CD6D2DA1D8B9EA10083DA7B /* isometric-130x230.png */; }; + 4C24B1FF20694FC80027EBD5 /* roguelike-16x16-anim.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CBA61801D8A0A4600B31FC1 /* roguelike-16x16-anim.png */; }; + 4C24B20020694FC80027EBD5 /* staggered-64x192.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CFEA3571D8D02D20055C150 /* staggered-64x192.png */; }; + 4C24B20220694FCF0027EBD5 /* alter.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0141F53A09F00CE1DEB /* alter.png */; }; + 4C24B20320694FCF0027EBD5 /* backgroundArch.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0151F53A09F00CE1DEB /* backgroundArch.png */; }; + 4C24B20420694FCF0027EBD5 /* backgroundMountain.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0161F53A09F00CE1DEB /* backgroundMountain.png */; }; + 4C24B20520694FCF0027EBD5 /* backgroundTower.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0171F53A09F00CE1DEB /* backgroundTower.png */; }; + 4C24B20620694FCF0027EBD5 /* backgroundTree.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0181F53A09F00CE1DEB /* backgroundTree.png */; }; + 4C24B20720694FCF0027EBD5 /* blobBlue.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0191F53A09F00CE1DEB /* blobBlue.png */; }; + 4C24B20820694FCF0027EBD5 /* blobGreen.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C01A1F53A09F00CE1DEB /* blobGreen.png */; }; + 4C24B20920694FCF0027EBD5 /* blue.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C01B1F53A09F00CE1DEB /* blue.png */; }; + 4C24B20A20694FCF0027EBD5 /* bombStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C01C1F53A09F00CE1DEB /* bombStroked.png */; }; + 4C24B20B20694FCF0027EBD5 /* castleWall.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C01D1F53A09F00CE1DEB /* castleWall.png */; }; + 4C24B20C20694FCF0027EBD5 /* cloud.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C01E1F53A09F00CE1DEB /* cloud.png */; }; + 4C24B20D20694FCF0027EBD5 /* column1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C01F1F53A09F00CE1DEB /* column1.png */; }; + 4C24B20E20694FCF0027EBD5 /* column2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0201F53A09F00CE1DEB /* column2.png */; }; + 4C24B20F20694FCF0027EBD5 /* doorBlueStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0211F53A09F00CE1DEB /* doorBlueStroked.png */; }; + 4C24B21020694FCF0027EBD5 /* doorGreenStroke.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0221F53A09F00CE1DEB /* doorGreenStroke.png */; }; + 4C24B21120694FCF0027EBD5 /* doorRedStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0231F53A09F00CE1DEB /* doorRedStroked.png */; }; + 4C24B21220694FCF0027EBD5 /* doorStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0241F53A09F00CE1DEB /* doorStroked.png */; }; + 4C24B21320694FCF0027EBD5 /* earthWall.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0251F53A09F00CE1DEB /* earthWall.png */; }; + 4C24B21420694FCF0027EBD5 /* earthWall2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0261F53A09F00CE1DEB /* earthWall2.png */; }; + 4C24B21520694FCF0027EBD5 /* exit.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0271F53A09F00CE1DEB /* exit.png */; }; + 4C24B21620694FCF0027EBD5 /* flare.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0281F53A09F00CE1DEB /* flare.png */; }; + 4C24B21720694FCF0027EBD5 /* gemBlueStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0291F53A09F00CE1DEB /* gemBlueStroked.png */; }; + 4C24B21820694FCF0027EBD5 /* gemRedStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C02A1F53A09F00CE1DEB /* gemRedStroked.png */; }; + 4C24B21920694FCF0027EBD5 /* grassLarge.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C02B1F53A09F00CE1DEB /* grassLarge.png */; }; + 4C24B21A20694FCF0027EBD5 /* grassSmall.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C02C1F53A09F00CE1DEB /* grassSmall.png */; }; + 4C24B21B20694FCF0027EBD5 /* grey.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C02D1F53A09F00CE1DEB /* grey.png */; }; + 4C24B21C20694FCF0027EBD5 /* hero.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C02E1F53A09F00CE1DEB /* hero.png */; }; + 4C24B21D20694FCF0027EBD5 /* keyGreenStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C02F1F53A09F00CE1DEB /* keyGreenStroked.png */; }; + 4C24B21E20694FCF0027EBD5 /* keyRedStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0301F53A09F00CE1DEB /* keyRedStroked.png */; }; + 4C24B21F20694FCF0027EBD5 /* keyYellowStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0311F53A09F00CE1DEB /* keyYellowStroked.png */; }; + 4C24B22020694FCF0027EBD5 /* platform1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0321F53A09F00CE1DEB /* platform1.png */; }; + 4C24B22120694FCF0027EBD5 /* platform2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0331F53A09F00CE1DEB /* platform2.png */; }; + 4C24B22220694FCF0027EBD5 /* platform3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0341F53A09F00CE1DEB /* platform3.png */; }; + 4C24B22320694FCF0027EBD5 /* platform4.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0351F53A09F00CE1DEB /* platform4.png */; }; + 4C24B22420694FCF0027EBD5 /* platformBase1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0361F53A09F00CE1DEB /* platformBase1.png */; }; + 4C24B22520694FCF0027EBD5 /* platformBase2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0371F53A09F00CE1DEB /* platformBase2.png */; }; + 4C24B22620694FCF0027EBD5 /* platformBase3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0381F53A09F00CE1DEB /* platformBase3.png */; }; + 4C24B22720694FCF0027EBD5 /* platformBase4.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0391F53A09F00CE1DEB /* platformBase4.png */; }; + 4C24B22820694FCF0027EBD5 /* platformBlock1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C03A1F53A09F00CE1DEB /* platformBlock1.png */; }; + 4C24B22920694FCF0027EBD5 /* platformBlock2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C03B1F53A09F00CE1DEB /* platformBlock2.png */; }; + 4C24B22A20694FCF0027EBD5 /* platformBlock3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C03C1F53A09F00CE1DEB /* platformBlock3.png */; }; + 4C24B22B20694FCF0027EBD5 /* platformBlock4.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C03D1F53A09F00CE1DEB /* platformBlock4.png */; }; + 4C24B22C20694FCF0027EBD5 /* platformConnector1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C03E1F53A09F00CE1DEB /* platformConnector1.png */; }; + 4C24B22D20694FCF0027EBD5 /* platformConnector2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C03F1F53A09F00CE1DEB /* platformConnector2.png */; }; + 4C24B22E20694FCF0027EBD5 /* platformConnector3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0401F53A09F00CE1DEB /* platformConnector3.png */; }; + 4C24B22F20694FCF0027EBD5 /* platformConnector4.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0411F53A09F00CE1DEB /* platformConnector4.png */; }; + 4C24B23020694FCF0027EBD5 /* pushBlock1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0421F53A09F00CE1DEB /* pushBlock1.png */; }; + 4C24B23120694FCF0027EBD5 /* pushBlock2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0431F53A09F00CE1DEB /* pushBlock2.png */; }; + 4C24B23220694FCF0027EBD5 /* pushBlock3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0441F53A09F00CE1DEB /* pushBlock3.png */; }; + 4C24B23320694FCF0027EBD5 /* shadow.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0471F53A09F00CE1DEB /* shadow.png */; }; + 4C24B23420694FCF0027EBD5 /* shieldStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0481F53A09F00CE1DEB /* shieldStroked.png */; }; + 4C24B23520694FCF0027EBD5 /* sign.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0491F53A09F00CE1DEB /* sign.png */; }; + 4C24B23620694FCF0027EBD5 /* skeleton.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C04A1F53A09F00CE1DEB /* skeleton.png */; }; + 4C24B23720694FCF0027EBD5 /* swordStroked.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C04B1F53A09F00CE1DEB /* swordStroked.png */; }; + 4C24B23820694FCF0027EBD5 /* torch.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C04C1F53A09F00CE1DEB /* torch.png */; }; + 4C24B23920694FCF0027EBD5 /* trap.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C04D1F53A09F00CE1DEB /* trap.png */; }; + 4C24B23A20694FCF0027EBD5 /* wallDecor1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C04E1F53A09F00CE1DEB /* wallDecor1.png */; }; + 4C24B23B20694FCF0027EBD5 /* wallDecor2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C04F1F53A09F00CE1DEB /* wallDecor2.png */; }; + 4C24B23C20694FCF0027EBD5 /* wallDecor3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0501F53A09F00CE1DEB /* wallDecor3.png */; }; + 4C24B23D20694FCF0027EBD5 /* window1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0511F53A09F00CE1DEB /* window1.png */; }; + 4C24B23E20694FCF0027EBD5 /* window2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0521F53A09F00CE1DEB /* window2.png */; }; + 4C24B23F20694FCF0027EBD5 /* window3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0531F53A09F00CE1DEB /* window3.png */; }; 4C2FCE4F1F4378DA004AD742 /* SKTiled+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3117D81EF84FCF00892D9E /* SKTiled+Debug.swift */; }; 4C2FCE501F4378E3004AD742 /* SKTiled+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D71D8A4EBE00FCCFA3 /* SKTiled+Extensions.swift */; }; 4C2FCE511F4378EB004AD742 /* SKTiled+GameplayKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C01D3A61F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift */; }; @@ -42,8 +162,6 @@ 4C3117DC1EF84FCF00892D9E /* SKTiled+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3117D81EF84FCF00892D9E /* SKTiled+Debug.swift */; }; 4C3184E91F4DCB4200950BBF /* staggered-64x33.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C3184E71F4DCB4200950BBF /* staggered-64x33.tmx */; }; 4C3184EA1F4DCB4200950BBF /* staggered-64x33.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C3184E71F4DCB4200950BBF /* staggered-64x33.tmx */; }; - 4C3184EB1F4DCB4200950BBF /* staggered-paths-64x33.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C3184E81F4DCB4200950BBF /* staggered-paths-64x33.png */; }; - 4C3184EC1F4DCB4200950BBF /* staggered-paths-64x33.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C3184E81F4DCB4200950BBF /* staggered-paths-64x33.png */; }; 4C4184A51F34B51D004E392A /* DemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4184A41F34B51D004E392A /* DemoController.swift */; }; 4C4184A61F34B51D004E392A /* DemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4184A41F34B51D004E392A /* DemoController.swift */; }; 4C41A5E81F437180001622FF /* SKTiled+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3117D81EF84FCF00892D9E /* SKTiled+Debug.swift */; }; @@ -70,9 +188,25 @@ 4C440BB31DAE913A00DEC9A4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C440BAC1DAE913A00DEC9A4 /* AppDelegate.swift */; }; 4C440BB51DAE913A00DEC9A4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4C440BAF1DAE913A00DEC9A4 /* Main.storyboard */; }; 4C440BB61DAE913A00DEC9A4 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C440BB11DAE913A00DEC9A4 /* GameViewController.swift */; }; + 4C48245F21839B6400B32614 /* dungeon-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C48245E21839B6200B32614 /* dungeon-16x16.tsx */; }; + 4C48246021839B6400B32614 /* dungeon-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C48245E21839B6200B32614 /* dungeon-16x16.tsx */; }; + 4C48246121839B6400B32614 /* dungeon-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C48245E21839B6200B32614 /* dungeon-16x16.tsx */; }; + 4C4ADA682189F3F100DB1A02 /* SKTiled.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C41A5FD1F43739C001622FF /* SKTiled.framework */; }; + 4C4ADA6E2189F45B00DB1A02 /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7DB2184F532002BC924 /* ParserTests.swift */; }; + 4C4ADA6F2189F45B00DB1A02 /* PropertiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7FF2188C12F002BC924 /* PropertiesTests.swift */; }; + 4C4ADA702189F45B00DB1A02 /* TilemapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7FD21875FD9002BC924 /* TilemapTests.swift */; }; + 4C4ADA712189F45B00DB1A02 /* TilesetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A8032188C9A0002BC924 /* TilesetTests.swift */; }; + 4C4ADA722189F45B00DB1A02 /* Tests+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A8072188CD93002BC924 /* Tests+Extensions.swift */; }; + 4C4ADA872189F5FB00DB1A02 /* SKTiled.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C24B14520694A140027EBD5 /* SKTiled.framework */; }; + 4C4ADA8E2189F81100DB1A02 /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7DB2184F532002BC924 /* ParserTests.swift */; }; + 4C4ADA8F2189F81100DB1A02 /* PropertiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7FF2188C12F002BC924 /* PropertiesTests.swift */; }; + 4C4ADA902189F81100DB1A02 /* TilemapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7FD21875FD9002BC924 /* TilemapTests.swift */; }; + 4C4ADA912189F81100DB1A02 /* TilesetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A8032188C9A0002BC924 /* TilesetTests.swift */; }; + 4C4ADA922189F81100DB1A02 /* Tests+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A8072188CD93002BC924 /* Tests+Extensions.swift */; }; + 4C4FAC9A215E9D8C00EEF512 /* Demo+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4FAC99215E9D8C00EEF512 /* Demo+Setup.swift */; }; + 4C4FAC9B215E9D8C00EEF512 /* Demo+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4FAC99215E9D8C00EEF512 /* Demo+Setup.swift */; }; + 4C4FAC9C215E9D8C00EEF512 /* Demo+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4FAC99215E9D8C00EEF512 /* Demo+Setup.swift */; }; 4C5479FE1DC15AF6008B3473 /* GameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5479FD1DC15AF6008B3473 /* GameWindowController.swift */; }; - 4C628F161F3EFA7400FCF929 /* User in Resources */ = {isa = PBXBuildFile; fileRef = 4C628F151F3EFA7400FCF929 /* User */; }; - 4C628F171F3EFA7400FCF929 /* User in Resources */ = {isa = PBXBuildFile; fileRef = 4C628F151F3EFA7400FCF929 /* User */; }; 4C7DD6221F50E16000C3FE2D /* roguelike-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C7DD6211F50E16000C3FE2D /* roguelike-16x16.tsx */; }; 4C7DD6231F50E16000C3FE2D /* roguelike-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C7DD6211F50E16000C3FE2D /* roguelike-16x16.tsx */; }; 4C8227E31F4E0D5E007E4556 /* isometric-130x66.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C8227E21F4E0D5E007E4556 /* isometric-130x66.tmx */; }; @@ -91,6 +225,50 @@ 4C88E1E71D8A4EBE00FCCFA3 /* SKTileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1D91D8A4EBE00FCCFA3 /* SKTileObject.swift */; }; 4C88E1E81D8A4EBE00FCCFA3 /* SKTileset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1DA1D8A4EBE00FCCFA3 /* SKTileset.swift */; }; 4C88E1EA1D8A4EBE00FCCFA3 /* SKTilesetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88E1DC1D8A4EBE00FCCFA3 /* SKTilesetData.swift */; }; + 4C94806B21938BA8002A620B /* portraits-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94804F21938BA6002A620B /* portraits-8x8.tsx */; }; + 4C94806C21938BA8002A620B /* portraits-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94804F21938BA6002A620B /* portraits-8x8.tsx */; }; + 4C94806D21938BA8002A620B /* portraits-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94804F21938BA6002A620B /* portraits-8x8.tsx */; }; + 4C94806E21938BA8002A620B /* portraits-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94804F21938BA6002A620B /* portraits-8x8.tsx */; }; + 4C94806F21938BA8002A620B /* characters-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805021938BA6002A620B /* characters-8x8.tsx */; }; + 4C94807021938BA8002A620B /* characters-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805021938BA6002A620B /* characters-8x8.tsx */; }; + 4C94807121938BA8002A620B /* characters-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805021938BA6002A620B /* characters-8x8.tsx */; }; + 4C94807221938BA8002A620B /* characters-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805021938BA6002A620B /* characters-8x8.tsx */; }; + 4C94807721938BA8002A620B /* characters-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805221938BA6002A620B /* characters-8x8.png */; }; + 4C94807821938BA8002A620B /* characters-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805221938BA6002A620B /* characters-8x8.png */; }; + 4C94807921938BA8002A620B /* characters-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805221938BA6002A620B /* characters-8x8.png */; }; + 4C94807A21938BA8002A620B /* characters-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805221938BA6002A620B /* characters-8x8.png */; }; + 4C94808321938BA8002A620B /* environment-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805521938BA7002A620B /* environment-8x8.png */; }; + 4C94808421938BA8002A620B /* environment-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805521938BA7002A620B /* environment-8x8.png */; }; + 4C94808521938BA8002A620B /* environment-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805521938BA7002A620B /* environment-8x8.png */; }; + 4C94808621938BA8002A620B /* environment-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805521938BA7002A620B /* environment-8x8.png */; }; + 4C94808721938BA8002A620B /* monsters-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805621938BA7002A620B /* monsters-16x16.tsx */; }; + 4C94808821938BA8002A620B /* monsters-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805621938BA7002A620B /* monsters-16x16.tsx */; }; + 4C94808921938BA8002A620B /* monsters-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805621938BA7002A620B /* monsters-16x16.tsx */; }; + 4C94808A21938BA8002A620B /* monsters-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805621938BA7002A620B /* monsters-16x16.tsx */; }; + 4C94808B21938BA8002A620B /* items-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805721938BA7002A620B /* items-8x8.tsx */; }; + 4C94808C21938BA8002A620B /* items-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805721938BA7002A620B /* items-8x8.tsx */; }; + 4C94808D21938BA8002A620B /* items-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805721938BA7002A620B /* items-8x8.tsx */; }; + 4C94808E21938BA8002A620B /* items-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805721938BA7002A620B /* items-8x8.tsx */; }; + 4C94808F21938BA8002A620B /* portraits-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805821938BA7002A620B /* portraits-8x8.png */; }; + 4C94809021938BA8002A620B /* portraits-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805821938BA7002A620B /* portraits-8x8.png */; }; + 4C94809121938BA8002A620B /* portraits-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805821938BA7002A620B /* portraits-8x8.png */; }; + 4C94809221938BA8002A620B /* portraits-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805821938BA7002A620B /* portraits-8x8.png */; }; + 4C94809321938BA8002A620B /* monsters-16x16.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805921938BA7002A620B /* monsters-16x16.png */; }; + 4C94809421938BA8002A620B /* monsters-16x16.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805921938BA7002A620B /* monsters-16x16.png */; }; + 4C94809521938BA8002A620B /* monsters-16x16.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805921938BA7002A620B /* monsters-16x16.png */; }; + 4C94809621938BA8002A620B /* monsters-16x16.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805921938BA7002A620B /* monsters-16x16.png */; }; + 4C94809B21938BA8002A620B /* items-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805B21938BA7002A620B /* items-8x8.png */; }; + 4C94809C21938BA8002A620B /* items-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805B21938BA7002A620B /* items-8x8.png */; }; + 4C94809D21938BA8002A620B /* items-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805B21938BA7002A620B /* items-8x8.png */; }; + 4C94809E21938BA8002A620B /* items-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805B21938BA7002A620B /* items-8x8.png */; }; + 4C9480A721938BA8002A620B /* environment-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805E21938BA8002A620B /* environment-8x8.tsx */; }; + 4C9480A821938BA8002A620B /* environment-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805E21938BA8002A620B /* environment-8x8.tsx */; }; + 4C9480A921938BA8002A620B /* environment-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805E21938BA8002A620B /* environment-8x8.tsx */; }; + 4C9480AA21938BA8002A620B /* environment-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94805E21938BA8002A620B /* environment-8x8.tsx */; }; + 4C9480B321938BA8002A620B /* test-tilemap.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94806121938BA8002A620B /* test-tilemap.tmx */; }; + 4C9480B421938BA8002A620B /* test-tilemap.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94806121938BA8002A620B /* test-tilemap.tmx */; }; + 4C9480B521938BA8002A620B /* test-tilemap.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94806121938BA8002A620B /* test-tilemap.tmx */; }; + 4C9480B621938BA8002A620B /* test-tilemap.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C94806121938BA8002A620B /* test-tilemap.tmx */; }; 4C96C0541F53A09F00CE1DEB /* alter.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0141F53A09F00CE1DEB /* alter.png */; }; 4C96C0551F53A09F00CE1DEB /* alter.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0141F53A09F00CE1DEB /* alter.png */; }; 4C96C0561F53A09F00CE1DEB /* backgroundArch.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0151F53A09F00CE1DEB /* backgroundArch.png */; }; @@ -219,18 +397,6 @@ 4C96C0D71F53A0F700CE1DEB /* sk1-32x32.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0D41F53A0F700CE1DEB /* sk1-32x32.tmx */; }; 4C96C0D81F53A0F700CE1DEB /* sk2-32x32.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0D51F53A0F700CE1DEB /* sk2-32x32.tmx */; }; 4C96C0D91F53A0F700CE1DEB /* sk2-32x32.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0D51F53A0F700CE1DEB /* sk2-32x32.tmx */; }; - 4C96C0E01F53BD8000CE1DEB /* pacman-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DB1F53BD8000CE1DEB /* pacman-16x16.tsx */; }; - 4C96C0E11F53BD8000CE1DEB /* pacman-16x16.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DB1F53BD8000CE1DEB /* pacman-16x16.tsx */; }; - 4C96C0E21F53BD8000CE1DEB /* pacman.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DC1F53BD8000CE1DEB /* pacman.tmx */; }; - 4C96C0E31F53BD8000CE1DEB /* pacman.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DC1F53BD8000CE1DEB /* pacman.tmx */; }; - 4C96C0E41F53BD8000CE1DEB /* pm-maze-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DD1F53BD8000CE1DEB /* pm-maze-8x8.png */; }; - 4C96C0E51F53BD8000CE1DEB /* pm-maze-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DD1F53BD8000CE1DEB /* pm-maze-8x8.png */; }; - 4C96C0E61F53BD8000CE1DEB /* pacman-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DE1F53BD8000CE1DEB /* pacman-8x8.tsx */; }; - 4C96C0E71F53BD8000CE1DEB /* pacman-8x8.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DE1F53BD8000CE1DEB /* pacman-8x8.tsx */; }; - 4C96C0E81F53BD8000CE1DEB /* pm-sprites-16x16.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DF1F53BD8000CE1DEB /* pm-sprites-16x16.png */; }; - 4C96C0E91F53BD8000CE1DEB /* pm-sprites-16x16.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0DF1F53BD8000CE1DEB /* pm-sprites-16x16.png */; }; - 4C96C0EB1F54747900CE1DEB /* ArcadeNormal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0EA1F54747900CE1DEB /* ArcadeNormal.ttf */; }; - 4C96C0EC1F54747900CE1DEB /* ArcadeNormal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C0EA1F54747900CE1DEB /* ArcadeNormal.ttf */; }; 4C96C1011F55A7CA00CE1DEB /* dungeon-16x32.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1001F55A7CA00CE1DEB /* dungeon-16x32.png */; }; 4C96C1021F55A7CA00CE1DEB /* dungeon-16x32.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1001F55A7CA00CE1DEB /* dungeon-16x32.png */; }; 4C96C1041F55A7D500CE1DEB /* dungeon-16x32.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1031F55A7D500CE1DEB /* dungeon-16x32.tsx */; }; @@ -239,13 +405,43 @@ 4C96C1081F55A7DE00CE1DEB /* dungeon-32x32.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1061F55A7DE00CE1DEB /* dungeon-32x32.png */; }; 4C96C10A1F55A7F300CE1DEB /* dungeon-32x32.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1091F55A7F300CE1DEB /* dungeon-32x32.tsx */; }; 4C96C10B1F55A7F300CE1DEB /* dungeon-32x32.tsx in Resources */ = {isa = PBXBuildFile; fileRef = 4C96C1091F55A7F300CE1DEB /* dungeon-32x32.tsx */; }; + 4C9BE4EA216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BE4E9216FE02B006DC74E /* SKTilemap+DataStorage.swift */; }; + 4C9BE4EB216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BE4E9216FE02B006DC74E /* SKTilemap+DataStorage.swift */; }; + 4C9BE4EC216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BE4E9216FE02B006DC74E /* SKTilemap+DataStorage.swift */; }; + 4C9BE4ED216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BE4E9216FE02B006DC74E /* SKTilemap+DataStorage.swift */; }; + 4C9BE4EE216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BE4E9216FE02B006DC74E /* SKTilemap+DataStorage.swift */; }; + 4C9BE4EF216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BE4E9216FE02B006DC74E /* SKTilemap+DataStorage.swift */; }; + 4C9FEB6B219E45BF00C3EEED /* User in Resources */ = {isa = PBXBuildFile; fileRef = 4C9FEB6A219E45BF00C3EEED /* User */; }; + 4C9FEB6C219E45BF00C3EEED /* User in Resources */ = {isa = PBXBuildFile; fileRef = 4C9FEB6A219E45BF00C3EEED /* User */; }; + 4C9FEB6D219E45BF00C3EEED /* User in Resources */ = {isa = PBXBuildFile; fileRef = 4C9FEB6A219E45BF00C3EEED /* User */; }; + 4C9FEB71219F2A8E00C3EEED /* items-alt-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C9FEB70219F2A8D00C3EEED /* items-alt-8x8.png */; }; + 4C9FEB72219F2A8E00C3EEED /* items-alt-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C9FEB70219F2A8D00C3EEED /* items-alt-8x8.png */; }; + 4C9FEB73219F2A8E00C3EEED /* items-alt-8x8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C9FEB70219F2A8D00C3EEED /* items-alt-8x8.png */; }; + 4C9FEB7621A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9FEB7521A1BC5400C3EEED /* SKTiled+Globals.swift */; }; + 4C9FEB7721A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9FEB7521A1BC5400C3EEED /* SKTiled+Globals.swift */; }; + 4C9FEB7821A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9FEB7521A1BC5400C3EEED /* SKTiled+Globals.swift */; }; + 4C9FEB7921A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9FEB7521A1BC5400C3EEED /* SKTiled+Globals.swift */; }; + 4C9FEB7A21A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9FEB7521A1BC5400C3EEED /* SKTiled+Globals.swift */; }; + 4C9FEB7B21A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9FEB7521A1BC5400C3EEED /* SKTiled+Globals.swift */; }; + 4CA6A7DC2184F532002BC924 /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7DB2184F532002BC924 /* ParserTests.swift */; }; + 4CA6A7DE2184F532002BC924 /* SKTiled.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C41A5E01F437118001622FF /* SKTiled.framework */; }; + 4CA6A7FE21875FD9002BC924 /* TilemapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7FD21875FD9002BC924 /* TilemapTests.swift */; }; + 4CA6A8002188C12F002BC924 /* PropertiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A7FF2188C12F002BC924 /* PropertiesTests.swift */; }; + 4CA6A8082188CD93002BC924 /* Tests+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A8072188CD93002BC924 /* Tests+Extensions.swift */; }; + 4CA6A8092188CF54002BC924 /* TilesetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA6A8032188C9A0002BC924 /* TilesetTests.swift */; }; 4CAD35BD1F4B9D2C0034CA6C /* dungeon-16x16.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4CAD35BC1F4B9D2C0034CA6C /* dungeon-16x16.tmx */; }; 4CAD35BE1F4B9D2C0034CA6C /* dungeon-16x16.tmx in Resources */ = {isa = PBXBuildFile; fileRef = 4CAD35BC1F4B9D2C0034CA6C /* dungeon-16x16.tmx */; }; + 4CB6BD1A2139BD2200B1275A /* Demo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4CB6BD192139BD2200B1275A /* Demo.plist */; }; + 4CB6BD1B2139BD2200B1275A /* Demo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4CB6BD192139BD2200B1275A /* Demo.plist */; }; + 4CB6BD1C2139BD2200B1275A /* Demo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4CB6BD192139BD2200B1275A /* Demo.plist */; }; 4CBA61831D8A0A4600B31FC1 /* roguelike-16x16-anim.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CBA61801D8A0A4600B31FC1 /* roguelike-16x16-anim.png */; }; 4CBB6A9F1D8CBA8A00DB56F1 /* SKTilemap+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6A9E1D8CBA8A00DB56F1 /* SKTilemap+Properties.swift */; }; 4CBB6AA11D8CBB5B00DB56F1 /* SKTiledScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6AA01D8CBB5B00DB56F1 /* SKTiledScene.swift */; }; 4CBB6AA31D8CC2C100DB56F1 /* SKTiledObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBB6AA21D8CC2C100DB56F1 /* SKTiledObject.swift */; }; 4CD6D2DE1D8B9EA10083DA7B /* isometric-130x230.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CD6D2DA1D8B9EA10083DA7B /* isometric-130x230.png */; }; + 4CE6415F206C1AEA004BA9E4 /* Demo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6415E206C1AEA004BA9E4 /* Demo+Extensions.swift */; }; + 4CE64160206C1AEA004BA9E4 /* Demo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6415E206C1AEA004BA9E4 /* Demo+Extensions.swift */; }; + 4CE64161206C1AEA004BA9E4 /* Demo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6415E206C1AEA004BA9E4 /* Demo+Extensions.swift */; }; 4CF0FEA11F34EF27000AADEF /* roguelike-16x16-anim.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CBA61801D8A0A4600B31FC1 /* roguelike-16x16-anim.png */; }; 4CF0FEA21F34EF27000AADEF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C440B981DAE911000DEC9A4 /* Assets.xcassets */; }; 4CF0FEA31F34EF27000AADEF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4C440BA31DAE913000DEC9A4 /* Main.storyboard */; }; @@ -254,13 +450,46 @@ 4CFEA35C1D8D04320055C150 /* hex-65x65-65x230.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CFEA35A1D8D04320055C150 /* hex-65x65-65x230.png */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 4C4ADA692189F3F100DB1A02 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4CBA615D1D8A048200B31FC1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4C41A5FC1F43739C001622FF; + remoteInfo = "SKTiled-iOS"; + }; + 4C4ADA882189F5FB00DB1A02 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4CBA615D1D8A048200B31FC1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4C24B14420694A140027EBD5; + remoteInfo = "SKTiled-tvOS"; + }; + 4CA6A7DF2184F532002BC924 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4CBA615D1D8A048200B31FC1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4C41A5DF1F437118001622FF; + remoteInfo = "SKTiled-macOS"; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 4C01D3A61F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKTiled+GameplayKit.swift"; sourceTree = ""; }; 4C0E59501DC046E700921F75 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; - 4C17747F1D909740000C0AFD /* SKTiledDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SKTiledDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C112D3921A1E9DC009E51F7 /* staggered-paths-64x33.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "staggered-paths-64x33.png"; sourceTree = ""; }; + 4C17747F1D909740000C0AFD /* SKTiled Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SKTiled Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C1D689021932E6200D2D042 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; + 4C24B14520694A140027EBD5 /* SKTiled.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SKTiled.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C24B16A20694C470027EBD5 /* SKTiled Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SKTiled Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C24B18120694D2D0027EBD5 /* SKTiled.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SKTiled.h; sourceTree = ""; }; + 4C24B18220694D2D0027EBD5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4C24B18320694D2D0027EBD5 /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; }; + 4C24B18520694D2D0027EBD5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 4C24B18620694D2D0027EBD5 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 4C24B18720694D2D0027EBD5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C3117D81EF84FCF00892D9E /* SKTiled+Debug.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKTiled+Debug.swift"; sourceTree = ""; }; 4C3184E71F4DCB4200950BBF /* staggered-64x33.tmx */ = {isa = PBXFileReference; fileEncoding = 1; lastKnownFileType = text.xml; path = "staggered-64x33.tmx"; sourceTree = ""; }; - 4C3184E81F4DCB4200950BBF /* staggered-paths-64x33.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "staggered-paths-64x33.png"; sourceTree = ""; }; 4C4184A41F34B51D004E392A /* DemoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoController.swift; sourceTree = ""; }; 4C41A5E01F437118001622FF /* SKTiled.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SKTiled.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4C41A5FD1F43739C001622FF /* SKTiled.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SKTiled.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -275,12 +504,17 @@ 4C440BB01DAE913A00DEC9A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 4C440BB11DAE913A00DEC9A4 /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; }; 4C440BB21DAE913A00DEC9A4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4C48245E21839B6200B32614 /* dungeon-16x16.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "dungeon-16x16.tsx"; sourceTree = ""; }; + 4C4ADA632189F3F100DB1A02 /* SKTiledTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SKTiledTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C4ADA672189F3F100DB1A02 /* Info-iOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = ""; }; + 4C4ADA822189F5FB00DB1A02 /* SKTiledTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SKTiledTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C4ADA8D2189F70E00DB1A02 /* Info-tvOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-tvOS.plist"; sourceTree = ""; }; + 4C4FAC99215E9D8C00EEF512 /* Demo+Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Demo+Setup.swift"; sourceTree = ""; }; 4C5479FD1DC15AF6008B3473 /* GameWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameWindowController.swift; sourceTree = ""; }; 4C5812621E255E6400E80BFC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C5812631E255E6400E80BFC /* SKTiled.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SKTiled.h; sourceTree = ""; }; 4C5812671E255E7300E80BFC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C5812681E255E7300E80BFC /* SKTiled.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SKTiled.h; sourceTree = ""; }; - 4C628F151F3EFA7400FCF929 /* User */ = {isa = PBXFileReference; lastKnownFileType = folder; path = User; sourceTree = ""; }; 4C7DD6211F50E16000C3FE2D /* roguelike-16x16.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "roguelike-16x16.tsx"; sourceTree = ""; }; 4C8227E21F4E0D5E007E4556 /* isometric-130x66.tmx */ = {isa = PBXFileReference; fileEncoding = 1; lastKnownFileType = text.xml; path = "isometric-130x66.tmx"; sourceTree = ""; }; 4C8227E51F4E0E3F007E4556 /* roguelike-16x16.tmx */ = {isa = PBXFileReference; fileEncoding = 1; lastKnownFileType = text.xml; path = "roguelike-16x16.tmx"; sourceTree = ""; }; @@ -295,6 +529,17 @@ 4C88E1D91D8A4EBE00FCCFA3 /* SKTileObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKTileObject.swift; sourceTree = ""; }; 4C88E1DA1D8A4EBE00FCCFA3 /* SKTileset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKTileset.swift; sourceTree = ""; }; 4C88E1DC1D8A4EBE00FCCFA3 /* SKTilesetData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKTilesetData.swift; sourceTree = ""; }; + 4C94804F21938BA6002A620B /* portraits-8x8.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "portraits-8x8.tsx"; sourceTree = ""; }; + 4C94805021938BA6002A620B /* characters-8x8.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "characters-8x8.tsx"; sourceTree = ""; }; + 4C94805221938BA6002A620B /* characters-8x8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "characters-8x8.png"; sourceTree = ""; }; + 4C94805521938BA7002A620B /* environment-8x8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "environment-8x8.png"; sourceTree = ""; }; + 4C94805621938BA7002A620B /* monsters-16x16.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "monsters-16x16.tsx"; sourceTree = ""; }; + 4C94805721938BA7002A620B /* items-8x8.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "items-8x8.tsx"; sourceTree = ""; }; + 4C94805821938BA7002A620B /* portraits-8x8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "portraits-8x8.png"; sourceTree = ""; }; + 4C94805921938BA7002A620B /* monsters-16x16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "monsters-16x16.png"; sourceTree = ""; }; + 4C94805B21938BA7002A620B /* items-8x8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "items-8x8.png"; sourceTree = ""; }; + 4C94805E21938BA8002A620B /* environment-8x8.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "environment-8x8.tsx"; sourceTree = ""; }; + 4C94806121938BA8002A620B /* test-tilemap.tmx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "test-tilemap.tmx"; sourceTree = ""; }; 4C96C0141F53A09F00CE1DEB /* alter.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alter.png; sourceTree = ""; }; 4C96C0151F53A09F00CE1DEB /* backgroundArch.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = backgroundArch.png; sourceTree = ""; }; 4C96C0161F53A09F00CE1DEB /* backgroundMountain.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = backgroundMountain.png; sourceTree = ""; }; @@ -357,20 +602,26 @@ 4C96C0511F53A09F00CE1DEB /* window1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = window1.png; sourceTree = ""; }; 4C96C0521F53A09F00CE1DEB /* window2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = window2.png; sourceTree = ""; }; 4C96C0531F53A09F00CE1DEB /* window3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = window3.png; sourceTree = ""; }; - 4C96C0D41F53A0F700CE1DEB /* sk1-32x32.tmx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = "sk1-32x32.tmx"; path = "sticker-knight/sk1-32x32.tmx"; sourceTree = ""; }; - 4C96C0D51F53A0F700CE1DEB /* sk2-32x32.tmx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = "sk2-32x32.tmx"; path = "sticker-knight/sk2-32x32.tmx"; sourceTree = ""; }; - 4C96C0DB1F53BD8000CE1DEB /* pacman-16x16.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = "pacman-16x16.tsx"; path = "pacman/pacman-16x16.tsx"; sourceTree = ""; }; - 4C96C0DC1F53BD8000CE1DEB /* pacman.tmx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = pacman.tmx; path = pacman/pacman.tmx; sourceTree = ""; }; - 4C96C0DD1F53BD8000CE1DEB /* pm-maze-8x8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "pm-maze-8x8.png"; path = "pacman/pm-maze-8x8.png"; sourceTree = ""; }; - 4C96C0DE1F53BD8000CE1DEB /* pacman-8x8.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = "pacman-8x8.tsx"; path = "pacman/pacman-8x8.tsx"; sourceTree = ""; }; - 4C96C0DF1F53BD8000CE1DEB /* pm-sprites-16x16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "pm-sprites-16x16.png"; path = "pacman/pm-sprites-16x16.png"; sourceTree = ""; }; - 4C96C0EA1F54747900CE1DEB /* ArcadeNormal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ArcadeNormal.ttf; sourceTree = ""; }; + 4C96C0D41F53A0F700CE1DEB /* sk1-32x32.tmx */ = {isa = PBXFileReference; fileEncoding = 1; lastKnownFileType = text.xml; name = "sk1-32x32.tmx"; path = "sticker-knight/sk1-32x32.tmx"; sourceTree = ""; }; + 4C96C0D51F53A0F700CE1DEB /* sk2-32x32.tmx */ = {isa = PBXFileReference; fileEncoding = 1; lastKnownFileType = text.xml; name = "sk2-32x32.tmx"; path = "sticker-knight/sk2-32x32.tmx"; sourceTree = ""; }; 4C96C1001F55A7CA00CE1DEB /* dungeon-16x32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "dungeon-16x32.png"; sourceTree = ""; }; 4C96C1031F55A7D500CE1DEB /* dungeon-16x32.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "dungeon-16x32.tsx"; sourceTree = ""; }; 4C96C1061F55A7DE00CE1DEB /* dungeon-32x32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "dungeon-32x32.png"; sourceTree = ""; }; 4C96C1091F55A7F300CE1DEB /* dungeon-32x32.tsx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "dungeon-32x32.tsx"; sourceTree = ""; }; + 4C9BE4E9216FE02B006DC74E /* SKTilemap+DataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKTilemap+DataStorage.swift"; sourceTree = ""; }; + 4C9FEB6A219E45BF00C3EEED /* User */ = {isa = PBXFileReference; lastKnownFileType = folder; path = User; sourceTree = ""; }; + 4C9FEB70219F2A8D00C3EEED /* items-alt-8x8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "items-alt-8x8.png"; sourceTree = ""; }; + 4C9FEB7521A1BC5400C3EEED /* SKTiled+Globals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKTiled+Globals.swift"; sourceTree = ""; }; + 4CA6A7D92184F531002BC924 /* SKTiledTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SKTiledTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4CA6A7DB2184F532002BC924 /* ParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTests.swift; sourceTree = ""; }; + 4CA6A7DD2184F532002BC924 /* Info-macOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-macOS.plist"; sourceTree = ""; }; + 4CA6A7FD21875FD9002BC924 /* TilemapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilemapTests.swift; sourceTree = ""; }; + 4CA6A7FF2188C12F002BC924 /* PropertiesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PropertiesTests.swift; sourceTree = ""; }; + 4CA6A8032188C9A0002BC924 /* TilesetTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TilesetTests.swift; sourceTree = ""; }; + 4CA6A8072188CD93002BC924 /* Tests+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tests+Extensions.swift"; sourceTree = ""; }; 4CAD35BC1F4B9D2C0034CA6C /* dungeon-16x16.tmx */ = {isa = PBXFileReference; fileEncoding = 1; lastKnownFileType = text.xml; path = "dungeon-16x16.tmx"; sourceTree = ""; }; - 4CBA61651D8A048200B31FC1 /* SKTiledDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SKTiledDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4CB6BD192139BD2200B1275A /* Demo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Demo.plist; sourceTree = ""; }; + 4CBA61651D8A048200B31FC1 /* SKTiled Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SKTiled Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4CBA617E1D8A0A3900B31FC1 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 4CBA61801D8A0A4600B31FC1 /* roguelike-16x16-anim.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "roguelike-16x16-anim.png"; sourceTree = ""; }; 4CBB6A9E1D8CBA8A00DB56F1 /* SKTilemap+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKTilemap+Properties.swift"; sourceTree = ""; }; @@ -378,6 +629,7 @@ 4CBB6AA21D8CC2C100DB56F1 /* SKTiledObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKTiledObject.swift; sourceTree = ""; }; 4CD5229E1DA624C400D78D81 /* CREDITS */ = {isa = PBXFileReference; lastKnownFileType = text; path = CREDITS; sourceTree = ""; }; 4CD6D2DA1D8B9EA10083DA7B /* isometric-130x230.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "isometric-130x230.png"; sourceTree = ""; }; + 4CE6415E206C1AEA004BA9E4 /* Demo+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Demo+Extensions.swift"; sourceTree = ""; }; 4CFEA3571D8D02D20055C150 /* staggered-64x192.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "staggered-64x192.png"; sourceTree = ""; }; 4CFEA35A1D8D04320055C150 /* hex-65x65-65x230.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "hex-65x65-65x230.png"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -390,6 +642,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4C24B14120694A140027EBD5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C24B16720694C470027EBD5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4C41A5DC1F437118001622FF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -404,6 +670,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4C4ADA602189F3F100DB1A02 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C4ADA682189F3F100DB1A02 /* SKTiled.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C4ADA7F2189F5FB00DB1A02 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C4ADA872189F5FB00DB1A02 /* SKTiled.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4CA6A7D62184F531002BC924 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CA6A7DE2184F532002BC924 /* SKTiled.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4CBA61621D8A048200B31FC1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -414,11 +704,34 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4C24B17F20694D2D0027EBD5 /* tvOS */ = { + isa = PBXGroup; + children = ( + 4C24B18020694D2D0027EBD5 /* framework */, + 4C24B18320694D2D0027EBD5 /* GameViewController.swift */, + 4C24B18420694D2D0027EBD5 /* Main.storyboard */, + 4C24B18620694D2D0027EBD5 /* AppDelegate.swift */, + 4C24B18720694D2D0027EBD5 /* Info.plist */, + ); + path = tvOS; + sourceTree = ""; + }; + 4C24B18020694D2D0027EBD5 /* framework */ = { + isa = PBXGroup; + children = ( + 4C24B18220694D2D0027EBD5 /* Info.plist */, + 4C24B18120694D2D0027EBD5 /* SKTiled.h */, + ); + path = framework; + sourceTree = ""; + }; 4C440B971DAE911000DEC9A4 /* Demo */ = { isa = PBXGroup; children = ( + 4C51A2982073E4B300D12A56 /* Extensions */, 4C4184A41F34B51D004E392A /* DemoController.swift */, 4C440B9A1DAE911000DEC9A4 /* SKTiledDemoScene.swift */, + 4CB6BD192139BD2200B1275A /* Demo.plist */, 4C440B981DAE911000DEC9A4 /* Assets.xcassets */, ); path = Demo; @@ -450,6 +763,15 @@ path = iOS; sourceTree = ""; }; + 4C51A2982073E4B300D12A56 /* Extensions */ = { + isa = PBXGroup; + children = ( + 4CE6415E206C1AEA004BA9E4 /* Demo+Extensions.swift */, + 4C4FAC99215E9D8C00EEF512 /* Demo+Setup.swift */, + ); + name = Extensions; + sourceTree = ""; + }; 4C5812611E255E6400E80BFC /* framework */ = { isa = PBXGroup; children = ( @@ -471,10 +793,9 @@ 4C7DD6241F50E16600C3FE2D /* tsx */ = { isa = PBXGroup; children = ( + 4C48245E21839B6200B32614 /* dungeon-16x16.tsx */, 4C96C1031F55A7D500CE1DEB /* dungeon-16x32.tsx */, 4C96C1091F55A7F300CE1DEB /* dungeon-32x32.tsx */, - 4C96C0DB1F53BD8000CE1DEB /* pacman-16x16.tsx */, - 4C96C0DE1F53BD8000CE1DEB /* pacman-8x8.tsx */, 4C7DD6211F50E16000C3FE2D /* roguelike-16x16.tsx */, ); name = tsx; @@ -486,6 +807,8 @@ 4C3117D81EF84FCF00892D9E /* SKTiled+Debug.swift */, 4C88E1D71D8A4EBE00FCCFA3 /* SKTiled+Extensions.swift */, 4C01D3A61F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift */, + 4C9FEB7521A1BC5400C3EEED /* SKTiled+Globals.swift */, + 4C9BE4E9216FE02B006DC74E /* SKTilemap+DataStorage.swift */, 4CBB6A9E1D8CBA8A00DB56F1 /* SKTilemap+Properties.swift */, ); name = Extensions; @@ -560,12 +883,56 @@ path = "sticker-knight"; sourceTree = ""; }; - 4C96C0ED1F54748100CE1DEB /* Fonts */ = { + 4C9FEB6E219F292A00C3EEED /* png */ = { isa = PBXGroup; children = ( - 4C96C0EA1F54747900CE1DEB /* ArcadeNormal.ttf */, + 4C94805221938BA6002A620B /* characters-8x8.png */, + 4C94805521938BA7002A620B /* environment-8x8.png */, + 4C9FEB70219F2A8D00C3EEED /* items-alt-8x8.png */, + 4C94805921938BA7002A620B /* monsters-16x16.png */, + 4C94805821938BA7002A620B /* portraits-8x8.png */, + 4C94805B21938BA7002A620B /* items-8x8.png */, ); - name = Fonts; + name = png; + sourceTree = ""; + }; + 4C9FEB6F219F293B00C3EEED /* tsx */ = { + isa = PBXGroup; + children = ( + 4C94805021938BA6002A620B /* characters-8x8.tsx */, + 4C94805E21938BA8002A620B /* environment-8x8.tsx */, + 4C94805721938BA7002A620B /* items-8x8.tsx */, + 4C94805621938BA7002A620B /* monsters-16x16.tsx */, + 4C94804F21938BA6002A620B /* portraits-8x8.tsx */, + ); + name = tsx; + sourceTree = ""; + }; + 4CA6A7DA2184F532002BC924 /* Tests */ = { + isa = PBXGroup; + children = ( + 4CA6A7E52184F68E002BC924 /* Assets */, + 4CA6A7DB2184F532002BC924 /* ParserTests.swift */, + 4CA6A7FF2188C12F002BC924 /* PropertiesTests.swift */, + 4CA6A7FD21875FD9002BC924 /* TilemapTests.swift */, + 4CA6A8032188C9A0002BC924 /* TilesetTests.swift */, + 4C1D689021932E6200D2D042 /* QueryTests.swift */, + 4CA6A8072188CD93002BC924 /* Tests+Extensions.swift */, + 4CA6A7DD2184F532002BC924 /* Info-macOS.plist */, + 4C4ADA672189F3F100DB1A02 /* Info-iOS.plist */, + 4C4ADA8D2189F70E00DB1A02 /* Info-tvOS.plist */, + ); + path = Tests; + sourceTree = ""; + }; + 4CA6A7E52184F68E002BC924 /* Assets */ = { + isa = PBXGroup; + children = ( + 4C94806121938BA8002A620B /* test-tilemap.tmx */, + 4C9FEB6E219F292A00C3EEED /* png */, + 4C9FEB6F219F293B00C3EEED /* tsx */, + ); + path = Assets; sourceTree = ""; }; 4CBA615C1D8A048200B31FC1 = { @@ -574,10 +941,12 @@ 4CBA617E1D8A0A3900B31FC1 /* README.md */, 4C0E59501DC046E700921F75 /* CHANGELOG.md */, 4CBA61931D8A0A6C00B31FC1 /* Sources */, - 4CBA617F1D8A0A4600B31FC1 /* Resources */, + 4CBA617F1D8A0A4600B31FC1 /* Assets */, 4C440B971DAE911000DEC9A4 /* Demo */, 4C440BAB1DAE913A00DEC9A4 /* iOS */, 4C440BA11DAE913000DEC9A4 /* macOS */, + 4C24B17F20694D2D0027EBD5 /* tvOS */, + 4CA6A7DA2184F532002BC924 /* Tests */, 4CBA61661D8A048200B31FC1 /* Products */, ); sourceTree = ""; @@ -585,24 +954,27 @@ 4CBA61661D8A048200B31FC1 /* Products */ = { isa = PBXGroup; children = ( - 4CBA61651D8A048200B31FC1 /* SKTiledDemo.app */, - 4C17747F1D909740000C0AFD /* SKTiledDemo.app */, + 4C17747F1D909740000C0AFD /* SKTiled Demo.app */, + 4CBA61651D8A048200B31FC1 /* SKTiled Demo.app */, + 4C24B16A20694C470027EBD5 /* SKTiled Demo.app */, 4C41A5E01F437118001622FF /* SKTiled.framework */, + 4CA6A7D92184F531002BC924 /* SKTiledTests.xctest */, 4C41A5FD1F43739C001622FF /* SKTiled.framework */, + 4C4ADA632189F3F100DB1A02 /* SKTiledTests.xctest */, + 4C24B14520694A140027EBD5 /* SKTiled.framework */, + 4C4ADA822189F5FB00DB1A02 /* SKTiledTests.xctest */, ); name = Products; sourceTree = ""; }; - 4CBA617F1D8A0A4600B31FC1 /* Resources */ = { + 4CBA617F1D8A0A4600B31FC1 /* Assets */ = { isa = PBXGroup; children = ( - 4C96C0ED1F54748100CE1DEB /* Fonts */, - 4C628F151F3EFA7400FCF929 /* User */, + 4C9FEB6A219E45BF00C3EEED /* User */, 4CD5229E1DA624C400D78D81 /* CREDITS */, 4CAD35BC1F4B9D2C0034CA6C /* dungeon-16x16.tmx */, 4C8227E81F4E0E63007E4556 /* hex-65x65.tmx */, 4C8227E21F4E0D5E007E4556 /* isometric-130x66.tmx */, - 4C96C0DC1F53BD8000CE1DEB /* pacman.tmx */, 4C8227E51F4E0E3F007E4556 /* roguelike-16x16.tmx */, 4C3184E71F4DCB4200950BBF /* staggered-64x33.tmx */, 4C96C0D41F53A0F700CE1DEB /* sk1-32x32.tmx */, @@ -610,19 +982,19 @@ 4C7DD6241F50E16600C3FE2D /* tsx */, 4CDF67CC1D8AE84300589457 /* png */, ); - path = Resources; + path = Assets; sourceTree = ""; }; 4CBA61931D8A0A6C00B31FC1 /* Sources */ = { isa = PBXGroup; children = ( 4C88E1A71D8A1E8F00FCCFA3 /* Extensions */, + 4C88E1D51D8A4EBE00FCCFA3 /* SKTilemap.swift */, 4C88E1D01D8A4EBE00FCCFA3 /* SKTile.swift */, 4CBB6AA21D8CC2C100DB56F1 /* SKTiledObject.swift */, 4CBB6AA01D8CBB5B00DB56F1 /* SKTiledScene.swift */, 4C88E1D21D8A4EBE00FCCFA3 /* SKTiledSceneCamera.swift */, 4C88E1D41D8A4EBE00FCCFA3 /* SKTileLayer.swift */, - 4C88E1D51D8A4EBE00FCCFA3 /* SKTilemap.swift */, 4C88E1D81D8A4EBE00FCCFA3 /* SKTilemapParser.swift */, 4C88E1D91D8A4EBE00FCCFA3 /* SKTileObject.swift */, 4C88E1DA1D8A4EBE00FCCFA3 /* SKTileset.swift */, @@ -640,11 +1012,9 @@ 4C96C1001F55A7CA00CE1DEB /* dungeon-16x32.png */, 4CFEA35A1D8D04320055C150 /* hex-65x65-65x230.png */, 4CD6D2DA1D8B9EA10083DA7B /* isometric-130x230.png */, - 4C96C0DD1F53BD8000CE1DEB /* pm-maze-8x8.png */, - 4C96C0DF1F53BD8000CE1DEB /* pm-sprites-16x16.png */, 4CBA61801D8A0A4600B31FC1 /* roguelike-16x16-anim.png */, 4CFEA3571D8D02D20055C150 /* staggered-64x192.png */, - 4C3184E81F4DCB4200950BBF /* staggered-paths-64x33.png */, + 4C112D3921A1E9DC009E51F7 /* staggered-paths-64x33.png */, ); name = png; sourceTree = ""; @@ -652,6 +1022,14 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 4C24B14220694A140027EBD5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C24B18D20694D3B0027EBD5 /* SKTiled.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4C41A5DD1F437118001622FF /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -675,7 +1053,7 @@ isa = PBXNativeTarget; buildConfigurationList = 4C1774911D909740000C0AFD /* Build configuration list for PBXNativeTarget "Demo - macOS" */; buildPhases = ( - 4CABC4D71DAEE1F900ACA1A4 /* Run xattr */, + 4CABC4D71DAEE1F900ACA1A4 /* Clean Assets */, 4C17747B1D909740000C0AFD /* Sources */, 4C17747C1D909740000C0AFD /* Frameworks */, 4C17747D1D909740000C0AFD /* Resources */, @@ -686,7 +1064,43 @@ ); name = "Demo - macOS"; productName = SKTiled; - productReference = 4C17747F1D909740000C0AFD /* SKTiledDemo.app */; + productReference = 4C17747F1D909740000C0AFD /* SKTiled Demo.app */; + productType = "com.apple.product-type.application"; + }; + 4C24B14420694A140027EBD5 /* SKTiled-tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4C24B15620694A150027EBD5 /* Build configuration list for PBXNativeTarget "SKTiled-tvOS" */; + buildPhases = ( + 4C24B14020694A140027EBD5 /* Sources */, + 4C24B14120694A140027EBD5 /* Frameworks */, + 4C24B14220694A140027EBD5 /* Headers */, + 4C24B14320694A140027EBD5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SKTiled-tvOS"; + productName = SKTiled; + productReference = 4C24B14520694A140027EBD5 /* SKTiled.framework */; + productType = "com.apple.product-type.framework"; + }; + 4C24B16920694C470027EBD5 /* Demo - tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4C24B17C20694C470027EBD5 /* Build configuration list for PBXNativeTarget "Demo - tvOS" */; + buildPhases = ( + 4C4FAC9D2162BCDF00EEF512 /* Clean Assets */, + 4C24B16620694C470027EBD5 /* Sources */, + 4C24B16720694C470027EBD5 /* Frameworks */, + 4C24B16820694C470027EBD5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Demo - tvOS"; + productName = Demo; + productReference = 4C24B16A20694C470027EBD5 /* SKTiled Demo.app */; productType = "com.apple.product-type.application"; }; 4C41A5DF1F437118001622FF /* SKTiled-macOS */ = { @@ -725,11 +1139,65 @@ productReference = 4C41A5FD1F43739C001622FF /* SKTiled.framework */; productType = "com.apple.product-type.framework"; }; + 4C4ADA622189F3F100DB1A02 /* Tests (iOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4C4ADA6D2189F3F100DB1A02 /* Build configuration list for PBXNativeTarget "Tests (iOS)" */; + buildPhases = ( + 4C4ADA5F2189F3F100DB1A02 /* Sources */, + 4C4ADA602189F3F100DB1A02 /* Frameworks */, + 4C4ADA612189F3F100DB1A02 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4C4ADA6A2189F3F100DB1A02 /* PBXTargetDependency */, + ); + name = "Tests (iOS)"; + productName = SKTiledTests; + productReference = 4C4ADA632189F3F100DB1A02 /* SKTiledTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4C4ADA812189F5FB00DB1A02 /* Tests (tvOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4C4ADA8A2189F5FB00DB1A02 /* Build configuration list for PBXNativeTarget "Tests (tvOS)" */; + buildPhases = ( + 4C4ADA7E2189F5FB00DB1A02 /* Sources */, + 4C4ADA7F2189F5FB00DB1A02 /* Frameworks */, + 4C4ADA802189F5FB00DB1A02 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4C4ADA892189F5FB00DB1A02 /* PBXTargetDependency */, + ); + name = "Tests (tvOS)"; + productName = SKTiledTests; + productReference = 4C4ADA822189F5FB00DB1A02 /* SKTiledTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4CA6A7D82184F531002BC924 /* Tests (macOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4CA6A7E12184F532002BC924 /* Build configuration list for PBXNativeTarget "Tests (macOS)" */; + buildPhases = ( + 4CA6A7D52184F531002BC924 /* Sources */, + 4CA6A7D62184F531002BC924 /* Frameworks */, + 4CA6A7D72184F531002BC924 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4CA6A7E02184F532002BC924 /* PBXTargetDependency */, + ); + name = "Tests (macOS)"; + productName = SKTiledTests; + productReference = 4CA6A7D92184F531002BC924 /* SKTiledTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 4CBA61641D8A048200B31FC1 /* Demo - iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 4CBA617B1D8A048300B31FC1 /* Build configuration list for PBXNativeTarget "Demo - iOS" */; buildPhases = ( - 4C3496881EA513D9003900C8 /* Run xattr */, + 4C3496881EA513D9003900C8 /* Clean Assets */, 4CBA61611D8A048200B31FC1 /* Sources */, 4CBA61621D8A048200B31FC1 /* Frameworks */, 4CBA61631D8A048200B31FC1 /* Resources */, @@ -740,7 +1208,7 @@ ); name = "Demo - iOS"; productName = SKTiled; - productReference = 4CBA61651D8A048200B31FC1 /* SKTiledDemo.app */; + productReference = 4CBA61651D8A048200B31FC1 /* SKTiled Demo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -749,25 +1217,50 @@ 4CBA615D1D8A048200B31FC1 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0800; - LastUpgradeCheck = 0900; - ORGANIZATIONNAME = "Michael Fessenden"; + LastSwiftUpdateCheck = 1000; + LastUpgradeCheck = 1000; TargetAttributes = { 4C17747E1D909740000C0AFD = { CreatedOnToolsVersion = 8.0; + LastSwiftMigration = 0920; + ProvisioningStyle = Automatic; + }; + 4C24B14420694A140027EBD5 = { + CreatedOnToolsVersion = 9.2; ProvisioningStyle = Automatic; }; + 4C24B16920694C470027EBD5 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.GameControllers.appletvos = { + enabled = 0; + }; + }; + }; 4C41A5DF1F437118001622FF = { CreatedOnToolsVersion = 8.3.3; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; }; 4C41A5FC1F43739C001622FF = { CreatedOnToolsVersion = 8.3.3; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; + }; + 4C4ADA622189F3F100DB1A02 = { + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + }; + 4C4ADA812189F5FB00DB1A02 = { + 4CA6A7D82184F531002BC924 = { + }; + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + }; + 4CA6A7D82184F531002BC924 = { + ProvisioningStyle = Automatic; }; 4CBA61641D8A048200B31FC1 = { CreatedOnToolsVersion = 7.3; - DevelopmentTeam = 747QKN4G7U; LastSwiftMigration = 0800; ProvisioningStyle = Automatic; }; @@ -786,10 +1279,15 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 4C41A5DF1F437118001622FF /* SKTiled-macOS */, - 4C41A5FC1F43739C001622FF /* SKTiled-iOS */, 4C17747E1D909740000C0AFD /* Demo - macOS */, 4CBA61641D8A048200B31FC1 /* Demo - iOS */, + 4C24B16920694C470027EBD5 /* Demo - tvOS */, + 4C41A5DF1F437118001622FF /* SKTiled-macOS */, + 4C41A5FC1F43739C001622FF /* SKTiled-iOS */, + 4C24B14420694A140027EBD5 /* SKTiled-tvOS */, + 4CA6A7D82184F531002BC924 /* Tests (macOS) */, + 4C4ADA622189F3F100DB1A02 /* Tests (iOS) */, + 4C4ADA812189F5FB00DB1A02 /* Tests (tvOS) */, ); }; /* End PBXProject section */ @@ -816,13 +1314,12 @@ 4C96C0681F53A09F00CE1DEB /* cloud.png in Resources */, 4C96C0921F53A09F00CE1DEB /* platform2.png in Resources */, 4C96C0941F53A09F00CE1DEB /* platform3.png in Resources */, + 4C9FEB6B219E45BF00C3EEED /* User in Resources */, 4CAD35BD1F4B9D2C0034CA6C /* dungeon-16x16.tmx in Resources */, 4C96C06A1F53A09F00CE1DEB /* column1.png in Resources */, 4C96C0A61F53A09F00CE1DEB /* platformBlock4.png in Resources */, 4CF0FEA11F34EF27000AADEF /* roguelike-16x16-anim.png in Resources */, 4C96C0881F53A09F00CE1DEB /* hero.png in Resources */, - 4C96C0E41F53BD8000CE1DEB /* pm-maze-8x8.png in Resources */, - 4C96C0E21F53BD8000CE1DEB /* pacman.tmx in Resources */, 4C96C0C61F53A09F00CE1DEB /* trap.png in Resources */, 4C96C0761F53A09F00CE1DEB /* earthWall.png in Resources */, 4C8227E61F4E0E3F007E4556 /* roguelike-16x16.tmx in Resources */, @@ -840,9 +1337,7 @@ 4C96C0BE1F53A09F00CE1DEB /* sign.png in Resources */, 4C96C0A21F53A09F00CE1DEB /* platformBlock2.png in Resources */, 4C96C09C1F53A09F00CE1DEB /* platformBase3.png in Resources */, - 4C628F161F3EFA7400FCF929 /* User in Resources */, 4C96C0861F53A09F00CE1DEB /* grey.png in Resources */, - 4C96C0E81F53BD8000CE1DEB /* pm-sprites-16x16.png in Resources */, 4C96C06E1F53A09F00CE1DEB /* doorBlueStroked.png in Resources */, 4C96C0BC1F53A09F00CE1DEB /* shieldStroked.png in Resources */, 4C96C0C21F53A09F00CE1DEB /* swordStroked.png in Resources */, @@ -854,17 +1349,17 @@ 4C96C06C1F53A09F00CE1DEB /* column2.png in Resources */, 4C96C0781F53A09F00CE1DEB /* earthWall2.png in Resources */, 4C96C0721F53A09F00CE1DEB /* doorRedStroked.png in Resources */, - 4C3184EB1F4DCB4200950BBF /* staggered-paths-64x33.png in Resources */, 4C96C0D21F53A09F00CE1DEB /* window3.png in Resources */, 4C96C0AA1F53A09F00CE1DEB /* platformConnector2.png in Resources */, + 4CB6BD1A2139BD2200B1275A /* Demo.plist in Resources */, 4C96C0821F53A09F00CE1DEB /* grassLarge.png in Resources */, 4C96C08A1F53A09F00CE1DEB /* keyGreenStroked.png in Resources */, 4C7DD6221F50E16000C3FE2D /* roguelike-16x16.tsx in Resources */, - 4C96C0E61F53BD8000CE1DEB /* pacman-8x8.tsx in Resources */, 4C96C0B01F53A09F00CE1DEB /* pushBlock1.png in Resources */, 4C96C09A1F53A09F00CE1DEB /* platformBase2.png in Resources */, 4C96C0CC1F53A09F00CE1DEB /* wallDecor3.png in Resources */, 4C96C08E1F53A09F00CE1DEB /* keyYellowStroked.png in Resources */, + 4C48245F21839B6400B32614 /* dungeon-16x16.tsx in Resources */, 4C96C07E1F53A09F00CE1DEB /* gemBlueStroked.png in Resources */, 4CF0FEA31F34EF27000AADEF /* Main.storyboard in Resources */, 4CF0FEA41F34EF27000AADEF /* staggered-64x192.png in Resources */, @@ -880,21 +1375,130 @@ 4C96C1041F55A7D500CE1DEB /* dungeon-16x32.tsx in Resources */, 4C96C05C1F53A09F00CE1DEB /* backgroundTree.png in Resources */, 4C1774AA1D90999C000C0AFD /* isometric-130x230.png in Resources */, - 4C96C0EB1F54747900CE1DEB /* ArcadeNormal.ttf in Resources */, 4C96C0B21F53A09F00CE1DEB /* pushBlock2.png in Resources */, 4C96C0AC1F53A09F00CE1DEB /* platformConnector3.png in Resources */, + 4C112D3A21A1E9DD009E51F7 /* staggered-paths-64x33.png in Resources */, 4C96C0AE1F53A09F00CE1DEB /* platformConnector4.png in Resources */, - 4C96C0E01F53BD8000CE1DEB /* pacman-16x16.tsx in Resources */, 4C96C05E1F53A09F00CE1DEB /* blobBlue.png in Resources */, 4C96C0A81F53A09F00CE1DEB /* platformConnector1.png in Resources */, 4C1774AB1D90999C000C0AFD /* dungeon-16x16.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 4C24B14320694A140027EBD5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C24B16820694C470027EBD5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CB6BD1C2139BD2200B1275A /* Demo.plist in Resources */, + 4C24B1F420694FC30027EBD5 /* dungeon-32x32.tsx in Resources */, + 4C24B1F920694FC80027EBD5 /* dungeon-16x16.png in Resources */, + 4C24B23520694FCF0027EBD5 /* sign.png in Resources */, + 4C24B21E20694FCF0027EBD5 /* keyRedStroked.png in Resources */, + 4C24B22220694FCF0027EBD5 /* platform3.png in Resources */, + 4C24B21620694FCF0027EBD5 /* flare.png in Resources */, + 4C24B21A20694FCF0027EBD5 /* grassSmall.png in Resources */, + 4C24B23420694FCF0027EBD5 /* shieldStroked.png in Resources */, + 4C24B21020694FCF0027EBD5 /* doorGreenStroke.png in Resources */, + 4C24B21B20694FCF0027EBD5 /* grey.png in Resources */, + 4C24B21120694FCF0027EBD5 /* doorRedStroked.png in Resources */, + 4C24B23B20694FCF0027EBD5 /* wallDecor2.png in Resources */, + 4C24B23120694FCF0027EBD5 /* pushBlock2.png in Resources */, + 4C24B21C20694FCF0027EBD5 /* hero.png in Resources */, + 4C24B22D20694FCF0027EBD5 /* platformConnector2.png in Resources */, + 4C24B22920694FCF0027EBD5 /* platformBlock2.png in Resources */, + 4C9FEB6D219E45BF00C3EEED /* User in Resources */, + 4C24B18E20694E740027EBD5 /* Assets.xcassets in Resources */, + 4C24B20520694FCF0027EBD5 /* backgroundTower.png in Resources */, + 4C24B20720694FCF0027EBD5 /* blobBlue.png in Resources */, + 4C24B22A20694FCF0027EBD5 /* platformBlock3.png in Resources */, + 4C24B23320694FCF0027EBD5 /* shadow.png in Resources */, + 4C24B1FA20694FC80027EBD5 /* dungeon-16x32.png in Resources */, + 4C24B20620694FCF0027EBD5 /* backgroundTree.png in Resources */, + 4C24B23D20694FCF0027EBD5 /* window1.png in Resources */, + 4C24B21F20694FCF0027EBD5 /* keyYellowStroked.png in Resources */, + 4C24B20D20694FCF0027EBD5 /* column1.png in Resources */, + 4C24B20B20694FCF0027EBD5 /* castleWall.png in Resources */, + 4C24B1EC20694FBB0027EBD5 /* hex-65x65.tmx in Resources */, + 4C24B20820694FCF0027EBD5 /* blobGreen.png in Resources */, + 4C24B18A20694D2E0027EBD5 /* Main.storyboard in Resources */, + 4C24B23220694FCF0027EBD5 /* pushBlock3.png in Resources */, + 4C24B22720694FCF0027EBD5 /* platformBase4.png in Resources */, + 4C24B21720694FCF0027EBD5 /* gemBlueStroked.png in Resources */, + 4C24B1EF20694FBB0027EBD5 /* roguelike-16x16.tmx in Resources */, + 4C24B23820694FCF0027EBD5 /* torch.png in Resources */, + 4C24B1F020694FBB0027EBD5 /* staggered-64x33.tmx in Resources */, + 4C24B20320694FCF0027EBD5 /* backgroundArch.png in Resources */, + 4C24B1F220694FBB0027EBD5 /* sk2-32x32.tmx in Resources */, + 4C24B1F320694FC30027EBD5 /* dungeon-16x32.tsx in Resources */, + 4C24B1FB20694FC80027EBD5 /* hex-65x65-65x230.png in Resources */, + 4C24B21420694FCF0027EBD5 /* earthWall2.png in Resources */, + 4C24B1F720694FC30027EBD5 /* roguelike-16x16.tsx in Resources */, + 4C24B22F20694FCF0027EBD5 /* platformConnector4.png in Resources */, + 4C24B22420694FCF0027EBD5 /* platformBase1.png in Resources */, + 4C24B22520694FCF0027EBD5 /* platformBase2.png in Resources */, + 4C24B22820694FCF0027EBD5 /* platformBlock1.png in Resources */, + 4C24B1FF20694FC80027EBD5 /* roguelike-16x16-anim.png in Resources */, + 4C24B23C20694FCF0027EBD5 /* wallDecor3.png in Resources */, + 4C24B22E20694FCF0027EBD5 /* platformConnector3.png in Resources */, + 4C24B22020694FCF0027EBD5 /* platform1.png in Resources */, + 4C24B1F820694FC80027EBD5 /* dungeon-32x32.png in Resources */, + 4C24B23020694FCF0027EBD5 /* pushBlock1.png in Resources */, + 4C24B23620694FCF0027EBD5 /* skeleton.png in Resources */, + 4C24B22620694FCF0027EBD5 /* platformBase3.png in Resources */, + 4C24B22B20694FCF0027EBD5 /* platformBlock4.png in Resources */, + 4C24B1F120694FBB0027EBD5 /* sk1-32x32.tmx in Resources */, + 4C24B21920694FCF0027EBD5 /* grassLarge.png in Resources */, + 4C24B20C20694FCF0027EBD5 /* cloud.png in Resources */, + 4C24B20220694FCF0027EBD5 /* alter.png in Resources */, + 4C48246121839B6400B32614 /* dungeon-16x16.tsx in Resources */, + 4C24B21320694FCF0027EBD5 /* earthWall.png in Resources */, + 4C24B23720694FCF0027EBD5 /* swordStroked.png in Resources */, + 4C24B22C20694FCF0027EBD5 /* platformConnector1.png in Resources */, + 4C24B21D20694FCF0027EBD5 /* keyGreenStroked.png in Resources */, + 4C24B22320694FCF0027EBD5 /* platform4.png in Resources */, + 4C24B21820694FCF0027EBD5 /* gemRedStroked.png in Resources */, + 4C24B20420694FCF0027EBD5 /* backgroundMountain.png in Resources */, + 4C24B21520694FCF0027EBD5 /* exit.png in Resources */, + 4C24B23F20694FCF0027EBD5 /* window3.png in Resources */, + 4C24B1FC20694FC80027EBD5 /* isometric-130x230.png in Resources */, + 4C24B23E20694FCF0027EBD5 /* window2.png in Resources */, + 4C24B20920694FCF0027EBD5 /* blue.png in Resources */, + 4C24B1EB20694FBB0027EBD5 /* dungeon-16x16.tmx in Resources */, + 4C24B20A20694FCF0027EBD5 /* bombStroked.png in Resources */, + 4C24B22120694FCF0027EBD5 /* platform2.png in Resources */, + 4C24B20E20694FCF0027EBD5 /* column2.png in Resources */, + 4C24B21220694FCF0027EBD5 /* doorStroked.png in Resources */, + 4C24B20020694FC80027EBD5 /* staggered-64x192.png in Resources */, + 4C112D3C21A1E9DD009E51F7 /* staggered-paths-64x33.png in Resources */, + 4C24B20F20694FCF0027EBD5 /* doorBlueStroked.png in Resources */, + 4C24B23920694FCF0027EBD5 /* trap.png in Resources */, + 4C24B1ED20694FBB0027EBD5 /* isometric-130x66.tmx in Resources */, + 4C24B23A20694FCF0027EBD5 /* wallDecor1.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4C41A5DE1F437118001622FF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C94809B21938BA8002A620B /* items-8x8.png in Resources */, + 4C94807721938BA8002A620B /* characters-8x8.png in Resources */, + 4C94808B21938BA8002A620B /* items-8x8.tsx in Resources */, + 4C94808721938BA8002A620B /* monsters-16x16.tsx in Resources */, + 4C94806F21938BA8002A620B /* characters-8x8.tsx in Resources */, + 4C94806B21938BA8002A620B /* portraits-8x8.tsx in Resources */, + 4C9480B321938BA8002A620B /* test-tilemap.tmx in Resources */, + 4C9480A721938BA8002A620B /* environment-8x8.tsx in Resources */, + 4C94809321938BA8002A620B /* monsters-16x16.png in Resources */, + 4C94808F21938BA8002A620B /* portraits-8x8.png in Resources */, + 4C94808321938BA8002A620B /* environment-8x8.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -905,6 +1509,63 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4C4ADA612189F3F100DB1A02 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C94809D21938BA8002A620B /* items-8x8.png in Resources */, + 4C94807921938BA8002A620B /* characters-8x8.png in Resources */, + 4C94808D21938BA8002A620B /* items-8x8.tsx in Resources */, + 4C94808921938BA8002A620B /* monsters-16x16.tsx in Resources */, + 4C94807121938BA8002A620B /* characters-8x8.tsx in Resources */, + 4C9FEB72219F2A8E00C3EEED /* items-alt-8x8.png in Resources */, + 4C94806D21938BA8002A620B /* portraits-8x8.tsx in Resources */, + 4C9480B521938BA8002A620B /* test-tilemap.tmx in Resources */, + 4C9480A921938BA8002A620B /* environment-8x8.tsx in Resources */, + 4C94809521938BA8002A620B /* monsters-16x16.png in Resources */, + 4C94809121938BA8002A620B /* portraits-8x8.png in Resources */, + 4C94808521938BA8002A620B /* environment-8x8.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C4ADA802189F5FB00DB1A02 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C94809E21938BA8002A620B /* items-8x8.png in Resources */, + 4C94807A21938BA8002A620B /* characters-8x8.png in Resources */, + 4C94808E21938BA8002A620B /* items-8x8.tsx in Resources */, + 4C94808A21938BA8002A620B /* monsters-16x16.tsx in Resources */, + 4C94807221938BA8002A620B /* characters-8x8.tsx in Resources */, + 4C9FEB73219F2A8E00C3EEED /* items-alt-8x8.png in Resources */, + 4C94806E21938BA8002A620B /* portraits-8x8.tsx in Resources */, + 4C9480B621938BA8002A620B /* test-tilemap.tmx in Resources */, + 4C9480AA21938BA8002A620B /* environment-8x8.tsx in Resources */, + 4C94809621938BA8002A620B /* monsters-16x16.png in Resources */, + 4C94809221938BA8002A620B /* portraits-8x8.png in Resources */, + 4C94808621938BA8002A620B /* environment-8x8.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4CA6A7D72184F531002BC924 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C94809C21938BA8002A620B /* items-8x8.png in Resources */, + 4C94807821938BA8002A620B /* characters-8x8.png in Resources */, + 4C94808C21938BA8002A620B /* items-8x8.tsx in Resources */, + 4C94808821938BA8002A620B /* monsters-16x16.tsx in Resources */, + 4C94807021938BA8002A620B /* characters-8x8.tsx in Resources */, + 4C9FEB71219F2A8E00C3EEED /* items-alt-8x8.png in Resources */, + 4C94806C21938BA8002A620B /* portraits-8x8.tsx in Resources */, + 4C9480B421938BA8002A620B /* test-tilemap.tmx in Resources */, + 4C9480A821938BA8002A620B /* environment-8x8.tsx in Resources */, + 4C94809421938BA8002A620B /* monsters-16x16.png in Resources */, + 4C94809021938BA8002A620B /* portraits-8x8.png in Resources */, + 4C94808421938BA8002A620B /* environment-8x8.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4CBA61631D8A048200B31FC1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -926,14 +1587,13 @@ 4C96C1021F55A7CA00CE1DEB /* dungeon-16x32.png in Resources */, 4C96C0691F53A09F00CE1DEB /* cloud.png in Resources */, 4C96C0931F53A09F00CE1DEB /* platform2.png in Resources */, + 4C9FEB6C219E45BF00C3EEED /* User in Resources */, 4C96C0951F53A09F00CE1DEB /* platform3.png in Resources */, 4C8227EA1F4E0E63007E4556 /* hex-65x65.tmx in Resources */, 4C96C06B1F53A09F00CE1DEB /* column1.png in Resources */, 4C96C0A71F53A09F00CE1DEB /* platformBlock4.png in Resources */, 4CBA61831D8A0A4600B31FC1 /* roguelike-16x16-anim.png in Resources */, 4C96C0891F53A09F00CE1DEB /* hero.png in Resources */, - 4C96C0E51F53BD8000CE1DEB /* pm-maze-8x8.png in Resources */, - 4C96C0E31F53BD8000CE1DEB /* pacman.tmx in Resources */, 4C96C0C71F53A09F00CE1DEB /* trap.png in Resources */, 4C96C0771F53A09F00CE1DEB /* earthWall.png in Resources */, 4C96C09F1F53A09F00CE1DEB /* platformBase4.png in Resources */, @@ -944,7 +1604,6 @@ 4C96C0C51F53A09F00CE1DEB /* torch.png in Resources */, 4C96C0611F53A09F00CE1DEB /* blobGreen.png in Resources */, 4C88E1AD1D8A242F00FCCFA3 /* dungeon-16x16.png in Resources */, - 4C3184EC1F4DCB4200950BBF /* staggered-paths-64x33.png in Resources */, 4C96C05B1F53A09F00CE1DEB /* backgroundTower.png in Resources */, 4C96C1081F55A7DE00CE1DEB /* dungeon-32x32.png in Resources */, 4C96C0BF1F53A09F00CE1DEB /* sign.png in Resources */, @@ -952,7 +1611,6 @@ 4C96C09D1F53A09F00CE1DEB /* platformBase3.png in Resources */, 4C440BB51DAE913A00DEC9A4 /* Main.storyboard in Resources */, 4C96C0871F53A09F00CE1DEB /* grey.png in Resources */, - 4C96C0E91F53BD8000CE1DEB /* pm-sprites-16x16.png in Resources */, 4C96C06F1F53A09F00CE1DEB /* doorBlueStroked.png in Resources */, 4C96C0BD1F53A09F00CE1DEB /* shieldStroked.png in Resources */, 4C96C0C31F53A09F00CE1DEB /* swordStroked.png in Resources */, @@ -967,21 +1625,21 @@ 4CD6D2DE1D8B9EA10083DA7B /* isometric-130x230.png in Resources */, 4C96C0D31F53A09F00CE1DEB /* window3.png in Resources */, 4C96C0AB1F53A09F00CE1DEB /* platformConnector2.png in Resources */, + 4CB6BD1B2139BD2200B1275A /* Demo.plist in Resources */, 4C96C0831F53A09F00CE1DEB /* grassLarge.png in Resources */, 4C96C08B1F53A09F00CE1DEB /* keyGreenStroked.png in Resources */, 4C7DD6231F50E16000C3FE2D /* roguelike-16x16.tsx in Resources */, - 4C96C0E71F53BD8000CE1DEB /* pacman-8x8.tsx in Resources */, 4C96C0B11F53A09F00CE1DEB /* pushBlock1.png in Resources */, 4C96C09B1F53A09F00CE1DEB /* platformBase2.png in Resources */, 4C96C0CD1F53A09F00CE1DEB /* wallDecor3.png in Resources */, 4C96C08F1F53A09F00CE1DEB /* keyYellowStroked.png in Resources */, + 4C48246021839B6400B32614 /* dungeon-16x16.tsx in Resources */, 4C96C07F1F53A09F00CE1DEB /* gemBlueStroked.png in Resources */, 4CFEA35C1D8D04320055C150 /* hex-65x65-65x230.png in Resources */, 4C3184EA1F4DCB4200950BBF /* staggered-64x33.tmx in Resources */, 4C96C0C11F53A09F00CE1DEB /* skeleton.png in Resources */, 4C96C0671F53A09F00CE1DEB /* castleWall.png in Resources */, 4C96C0D71F53A0F700CE1DEB /* sk1-32x32.tmx in Resources */, - 4C628F171F3EFA7400FCF929 /* User in Resources */, 4C96C0551F53A09F00CE1DEB /* alter.png in Resources */, 4C96C0CB1F53A09F00CE1DEB /* wallDecor2.png in Resources */, 4C96C0A51F53A09F00CE1DEB /* platformBlock3.png in Resources */, @@ -990,11 +1648,10 @@ 4C96C1051F55A7D500CE1DEB /* dungeon-16x32.tsx in Resources */, 4C96C05D1F53A09F00CE1DEB /* backgroundTree.png in Resources */, 4C8227E41F4E0D5E007E4556 /* isometric-130x66.tmx in Resources */, - 4C96C0EC1F54747900CE1DEB /* ArcadeNormal.ttf in Resources */, 4C96C0B31F53A09F00CE1DEB /* pushBlock2.png in Resources */, 4C96C0AD1F53A09F00CE1DEB /* platformConnector3.png in Resources */, + 4C112D3B21A1E9DD009E51F7 /* staggered-paths-64x33.png in Resources */, 4C96C0AF1F53A09F00CE1DEB /* platformConnector4.png in Resources */, - 4C96C0E11F53BD8000CE1DEB /* pacman-16x16.tsx in Resources */, 4C96C05F1F53A09F00CE1DEB /* blobBlue.png in Resources */, 4C96C0A91F53A09F00CE1DEB /* platformConnector1.png in Resources */, 4CAD35BE1F4B9D2C0034CA6C /* dungeon-16x16.tmx in Resources */, @@ -1004,33 +1661,51 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 4C3496881EA513D9003900C8 /* Run xattr */ = { + 4C3496881EA513D9003900C8 /* Clean Assets */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Run xattr"; + name = "Clean Assets"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "$SRCROOT/scripts/clean-assets.sh\n"; + }; + 4C4FAC9D2162BCDF00EEF512 /* Clean Assets */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Clean Assets"; + outputFileListPaths = ( + ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "xattr -rc $PROJECT_DIR\n"; + shellScript = "$SRCROOT/scripts/clean-assets.sh\n"; }; - 4CABC4D71DAEE1F900ACA1A4 /* Run xattr */ = { + 4CABC4D71DAEE1F900ACA1A4 /* Clean Assets */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Run xattr"; + name = "Clean Assets"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/bash; - shellScript = "xattr -rc $PROJECT_DIR"; + shellScript = "$SRCROOT/scripts/clean-assets.sh\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -1045,12 +1720,16 @@ 4C440BA71DAE913000DEC9A4 /* AppDelegate.swift in Sources */, 4C4184A51F34B51D004E392A /* DemoController.swift in Sources */, 4C1774B81D9099A3000C0AFD /* SKTileset.swift in Sources */, + 4C9FEB7621A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */, + 4C4FAC9A215E9D8C00EEF512 /* Demo+Setup.swift in Sources */, 4C1774B21D9099A3000C0AFD /* SKTiledSceneCamera.swift in Sources */, + 4C9BE4ED216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */, 4C1774BA1D9099A3000C0AFD /* SKTiledObject.swift in Sources */, 4C5479FE1DC15AF6008B3473 /* GameWindowController.swift in Sources */, 4C1774B61D9099A3000C0AFD /* SKTilemapParser.swift in Sources */, 4C1774B31D9099A3000C0AFD /* SKTiledScene.swift in Sources */, 4C1774B01D9099A3000C0AFD /* SKTilemap+Properties.swift in Sources */, + 4CE6415F206C1AEA004BA9E4 /* Demo+Extensions.swift in Sources */, 4C1774AF1D9099A3000C0AFD /* SKTiled+Extensions.swift in Sources */, 4C1774B41D9099A3000C0AFD /* SKTileLayer.swift in Sources */, 4C01D3A91F1E07AB00FFAD28 /* SKTiled+GameplayKit.swift in Sources */, @@ -1061,6 +1740,58 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4C24B14020694A140027EBD5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C24B1D720694FA70027EBD5 /* SKTilemap.swift in Sources */, + 4C24B1CF20694FA70027EBD5 /* SKTiled+Extensions.swift in Sources */, + 4C24B1DA20694FA70027EBD5 /* SKTileset.swift in Sources */, + 4C9BE4EC216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */, + 4C24B1D420694FA70027EBD5 /* SKTiledScene.swift in Sources */, + 4C24B1D520694FA70027EBD5 /* SKTiledSceneCamera.swift in Sources */, + 4C24B1D920694FA70027EBD5 /* SKTileObject.swift in Sources */, + 4C24B1D620694FA70027EBD5 /* SKTileLayer.swift in Sources */, + 4C24B1D020694FA70027EBD5 /* SKTiled+GameplayKit.swift in Sources */, + 4C24B1CE20694FA70027EBD5 /* SKTiled+Debug.swift in Sources */, + 4C24B1D820694FA70027EBD5 /* SKTilemapParser.swift in Sources */, + 4C24B1D120694FA70027EBD5 /* SKTilemap+Properties.swift in Sources */, + 4C24B1D220694FA70027EBD5 /* SKTile.swift in Sources */, + 4C24B1DB20694FA70027EBD5 /* SKTilesetData.swift in Sources */, + 4C24B1D320694FA70027EBD5 /* SKTiledObject.swift in Sources */, + 4C9FEB7B21A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C24B16620694C470027EBD5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C24B1E820694FA80027EBD5 /* SKTileset.swift in Sources */, + 4CE64161206C1AEA004BA9E4 /* Demo+Extensions.swift in Sources */, + 4C24B1E720694FA80027EBD5 /* SKTileObject.swift in Sources */, + 4C24B1DE20694FA80027EBD5 /* SKTiled+GameplayKit.swift in Sources */, + 4C24B19020694E7B0027EBD5 /* DemoController.swift in Sources */, + 4C9FEB7821A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */, + 4C24B18F20694E780027EBD5 /* SKTiledDemoScene.swift in Sources */, + 4C24B18B20694D2E0027EBD5 /* AppDelegate.swift in Sources */, + 4C24B1E920694FA80027EBD5 /* SKTilesetData.swift in Sources */, + 4C24B18920694D2E0027EBD5 /* GameViewController.swift in Sources */, + 4C24B1E120694FA80027EBD5 /* SKTiledObject.swift in Sources */, + 4C24B1E620694FA80027EBD5 /* SKTilemapParser.swift in Sources */, + 4C24B1E320694FA80027EBD5 /* SKTiledSceneCamera.swift in Sources */, + 4C24B1E420694FA80027EBD5 /* SKTileLayer.swift in Sources */, + 4C24B1E520694FA80027EBD5 /* SKTilemap.swift in Sources */, + 4C24B1E020694FA80027EBD5 /* SKTile.swift in Sources */, + 4C9BE4EF216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */, + 4C24B1E220694FA80027EBD5 /* SKTiledScene.swift in Sources */, + 4C4FAC9C215E9D8C00EEF512 /* Demo+Setup.swift in Sources */, + 4C24B1DD20694FA80027EBD5 /* SKTiled+Extensions.swift in Sources */, + 4C24B1DC20694FA80027EBD5 /* SKTiled+Debug.swift in Sources */, + 4C24B1DF20694FA80027EBD5 /* SKTilemap+Properties.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4C41A5DB1F437118001622FF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1068,6 +1799,7 @@ 4C41A5F11F437180001622FF /* SKTilemap.swift in Sources */, 4C41A5E91F437180001622FF /* SKTiled+Extensions.swift in Sources */, 4C41A5F41F437180001622FF /* SKTileset.swift in Sources */, + 4C9BE4EA216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */, 4C41A5EE1F437180001622FF /* SKTiledScene.swift in Sources */, 4C41A5EF1F437180001622FF /* SKTiledSceneCamera.swift in Sources */, 4C41A5F31F437180001622FF /* SKTileObject.swift in Sources */, @@ -1079,6 +1811,7 @@ 4C41A5EC1F437180001622FF /* SKTile.swift in Sources */, 4C41A5F51F437180001622FF /* SKTilesetData.swift in Sources */, 4C41A5ED1F437180001622FF /* SKTiledObject.swift in Sources */, + 4C9FEB7921A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1089,6 +1822,7 @@ 4C2FCE581F4378F0004AD742 /* SKTilemap.swift in Sources */, 4C2FCE501F4378E3004AD742 /* SKTiled+Extensions.swift in Sources */, 4C2FCE5B1F4378F0004AD742 /* SKTileset.swift in Sources */, + 4C9BE4EB216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */, 4C2FCE551F4378F0004AD742 /* SKTiledScene.swift in Sources */, 4C2FCE561F4378F0004AD742 /* SKTiledSceneCamera.swift in Sources */, 4C2FCE5A1F4378F0004AD742 /* SKTileObject.swift in Sources */, @@ -1100,6 +1834,46 @@ 4C2FCE531F4378F0004AD742 /* SKTile.swift in Sources */, 4C2FCE5C1F4378F0004AD742 /* SKTilesetData.swift in Sources */, 4C2FCE541F4378F0004AD742 /* SKTiledObject.swift in Sources */, + 4C9FEB7A21A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C4ADA5F2189F3F100DB1A02 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C4ADA6F2189F45B00DB1A02 /* PropertiesTests.swift in Sources */, + 4C4ADA6E2189F45B00DB1A02 /* ParserTests.swift in Sources */, + 4C1D689221932E6200D2D042 /* QueryTests.swift in Sources */, + 4C4ADA712189F45B00DB1A02 /* TilesetTests.swift in Sources */, + 4C4ADA702189F45B00DB1A02 /* TilemapTests.swift in Sources */, + 4C4ADA722189F45B00DB1A02 /* Tests+Extensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C4ADA7E2189F5FB00DB1A02 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C4ADA8F2189F81100DB1A02 /* PropertiesTests.swift in Sources */, + 4C4ADA8E2189F81100DB1A02 /* ParserTests.swift in Sources */, + 4C1D689321932E6200D2D042 /* QueryTests.swift in Sources */, + 4C4ADA912189F81100DB1A02 /* TilesetTests.swift in Sources */, + 4C4ADA902189F81100DB1A02 /* TilemapTests.swift in Sources */, + 4C4ADA922189F81100DB1A02 /* Tests+Extensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4CA6A7D52184F531002BC924 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CA6A7DC2184F532002BC924 /* ParserTests.swift in Sources */, + 4CA6A8092188CF54002BC924 /* TilesetTests.swift in Sources */, + 4C1D689121932E6200D2D042 /* QueryTests.swift in Sources */, + 4CA6A7FE21875FD9002BC924 /* TilemapTests.swift in Sources */, + 4CA6A8002188C12F002BC924 /* PropertiesTests.swift in Sources */, + 4CA6A8082188CD93002BC924 /* Tests+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1108,9 +1882,11 @@ buildActionMask = 2147483647; files = ( 4C3117DC1EF84FCF00892D9E /* SKTiled+Debug.swift in Sources */, + 4CE64160206C1AEA004BA9E4 /* Demo+Extensions.swift in Sources */, 4C440B9F1DAE911000DEC9A4 /* SKTiledDemoScene.swift in Sources */, 4CBB6AA11D8CBB5B00DB56F1 /* SKTiledScene.swift in Sources */, 4C88E1EA1D8A4EBE00FCCFA3 /* SKTilesetData.swift in Sources */, + 4C9FEB7721A1BC5400C3EEED /* SKTiled+Globals.swift in Sources */, 4C440BB31DAE913A00DEC9A4 /* AppDelegate.swift in Sources */, 4C88E1E71D8A4EBE00FCCFA3 /* SKTileObject.swift in Sources */, 4C4184A61F34B51D004E392A /* DemoController.swift in Sources */, @@ -1121,7 +1897,9 @@ 4C88E1E01D8A4EBE00FCCFA3 /* SKTiledSceneCamera.swift in Sources */, 4C88E1E61D8A4EBE00FCCFA3 /* SKTilemapParser.swift in Sources */, 4CBB6A9F1D8CBA8A00DB56F1 /* SKTilemap+Properties.swift in Sources */, + 4C9BE4EE216FE02B006DC74E /* SKTilemap+DataStorage.swift in Sources */, 4C88E1E51D8A4EBE00FCCFA3 /* SKTiled+Extensions.swift in Sources */, + 4C4FAC9B215E9D8C00EEF512 /* Demo+Setup.swift in Sources */, 4C440BB61DAE913A00DEC9A4 /* GameViewController.swift in Sources */, 4C88E1E21D8A4EBE00FCCFA3 /* SKTileLayer.swift in Sources */, 4C88E1DE1D8A4EBE00FCCFA3 /* SKTile.swift in Sources */, @@ -1130,7 +1908,33 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 4C4ADA6A2189F3F100DB1A02 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4C41A5FC1F43739C001622FF /* SKTiled-iOS */; + targetProxy = 4C4ADA692189F3F100DB1A02 /* PBXContainerItemProxy */; + }; + 4C4ADA892189F5FB00DB1A02 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4C24B14420694A140027EBD5 /* SKTiled-tvOS */; + targetProxy = 4C4ADA882189F5FB00DB1A02 /* PBXContainerItemProxy */; + }; + 4CA6A7E02184F532002BC924 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4C41A5DF1F437118001622FF /* SKTiled-macOS */; + targetProxy = 4CA6A7DF2184F532002BC924 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ + 4C24B18420694D2D0027EBD5 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4C24B18520694D2D0027EBD5 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; 4C440BA31DAE913000DEC9A4 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -1165,13 +1969,15 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; - CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1.20; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiledDemo; - PRODUCT_NAME = SKTiledDemo; + PRODUCT_NAME = "SKTiled Demo"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; }; @@ -1184,29 +1990,146 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; - CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1.20; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiledDemo; - PRODUCT_NAME = SKTiledDemo; + PRODUCT_NAME = "SKTiled Demo"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; }; name = Release; }; + 4C24B15720694A150027EBD5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CURRENT_PROJECT_VERSION = 1.20; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "$(SRCROOT)/tvOS/framework/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiled; + PRODUCT_NAME = SKTiled; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + TARGETED_DEVICE_FAMILY = 3; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 4C24B15820694A150027EBD5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1.20; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "$(SRCROOT)/tvOS/framework/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiled; + PRODUCT_NAME = SKTiled; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 4C24B17D20694C470027EBD5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "tvOS LaunchImage"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CURRENT_PROJECT_VERSION = 1.20; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "$(SRCROOT)/tvOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiledDemo; + PRODUCT_NAME = "SKTiled Demo"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + 4C24B17E20694C470027EBD5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "tvOS LaunchImage"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1.20; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "$(SRCROOT)/tvOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiledDemo; + PRODUCT_NAME = "SKTiled Demo"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; 4C41A5E51F437118001622FF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.16; + CURRENT_PROJECT_VERSION = 1.20; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1214,7 +2137,6 @@ INFOPLIST_FILE = "$(SRCROOT)/macOS/framework/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.11; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiled; PRODUCT_NAME = SKTiled; @@ -1223,7 +2145,6 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; }; name = Debug; }; @@ -1233,12 +2154,13 @@ ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 1.16; + CURRENT_PROJECT_VERSION = 1.20; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1246,14 +2168,12 @@ INFOPLIST_FILE = "$(SRCROOT)/macOS/framework/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiled; PRODUCT_NAME = SKTiled; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; }; name = Release; }; @@ -1263,16 +2183,16 @@ ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGNING_REQUIRED = NO; - CURRENT_PROJECT_VERSION = 1.16; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CURRENT_PROJECT_VERSION = 1.20; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/iOS/framework/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiled; @@ -1282,7 +2202,6 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; }; name = Debug; }; @@ -1292,17 +2211,17 @@ ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGNING_REQUIRED = NO; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 1.16; + CURRENT_PROJECT_VERSION = 1.20; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/iOS/framework/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiled; PRODUCT_MODULE_NAME = SKTiled; @@ -1310,7 +2229,155 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 4C4ADA6B2189F3F100DB1A02 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 747QKN4G7U; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "Tests/Info-iOS.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_NAME = SKTiledTests; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4C4ADA6C2189F3F100DB1A02 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 747QKN4G7U; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "Tests/Info-iOS.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_NAME = SKTiledTests; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 4C4ADA8B2189F5FB00DB1A02 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 747QKN4G7U; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "Tests/Info-tvOS.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_NAME = SKTiledTests; + SDKROOT = appletvos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Debug; + }; + 4C4ADA8C2189F5FB00DB1A02 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 747QKN4G7U; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "Tests/Info-tvOS.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_NAME = SKTiledTests; + SDKROOT = appletvos; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Release; + }; + 4CA6A7E22184F532002BC924 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 747QKN4G7U; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "Tests/Info-macOS.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_NAME = SKTiledTests; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 4.2; + }; + name = Debug; + }; + 4CA6A7E32184F532002BC924 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 747QKN4G7U; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "Tests/Info-macOS.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_FAST_MATH = YES; + PRODUCT_NAME = SKTiledTests; + SDKROOT = macosx; + SWIFT_VERSION = 4.2; }; name = Release; }; @@ -1326,12 +2393,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -1339,6 +2408,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1356,12 +2426,15 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "-lz"; + OTHER_SWIFT_FLAGS = "-DRENDER_STATS"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.3; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1378,12 +2451,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -1391,6 +2466,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1402,11 +2478,14 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; OTHER_LDFLAGS = "-lz"; + OTHER_SWIFT_FLAGS = "-DRENDER_STATS"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.3; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -1418,12 +2497,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - DEVELOPMENT_TEAM = 747QKN4G7U; + CURRENT_PROJECT_VERSION = 1.20; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/iOS/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiled; - PRODUCT_NAME = SKTiledDemo; + PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiledDemo; + PRODUCT_NAME = "SKTiled Demo"; PROVISIONING_PROFILE_SPECIFIER = ""; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1435,12 +2514,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - DEVELOPMENT_TEAM = 747QKN4G7U; + CURRENT_PROJECT_VERSION = 1.20; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/iOS/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.supermeerkat.SKTiledDemo; - PRODUCT_NAME = SKTiledDemo; + PRODUCT_NAME = "SKTiled Demo"; PROVISIONING_PROFILE_SPECIFIER = ""; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1458,6 +2537,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 4C24B15620694A150027EBD5 /* Build configuration list for PBXNativeTarget "SKTiled-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4C24B15720694A150027EBD5 /* Debug */, + 4C24B15820694A150027EBD5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4C24B17C20694C470027EBD5 /* Build configuration list for PBXNativeTarget "Demo - tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4C24B17D20694C470027EBD5 /* Debug */, + 4C24B17E20694C470027EBD5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 4C41A5E71F437118001622FF /* Build configuration list for PBXNativeTarget "SKTiled-macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1476,6 +2573,33 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 4C4ADA6D2189F3F100DB1A02 /* Build configuration list for PBXNativeTarget "Tests (iOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4C4ADA6B2189F3F100DB1A02 /* Debug */, + 4C4ADA6C2189F3F100DB1A02 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4C4ADA8A2189F5FB00DB1A02 /* Build configuration list for PBXNativeTarget "Tests (tvOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4C4ADA8B2189F5FB00DB1A02 /* Debug */, + 4C4ADA8C2189F5FB00DB1A02 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4CA6A7E12184F532002BC924 /* Build configuration list for PBXNativeTarget "Tests (macOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4CA6A7E22184F532002BC924 /* Debug */, + 4CA6A7E32184F532002BC924 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 4CBA61601D8A048200B31FC1 /* Build configuration list for PBXProject "SKTiled" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-iOS.xcscheme b/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-iOS.xcscheme index 2a0c1655..0178eff7 100644 --- a/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-iOS.xcscheme +++ b/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-iOS.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + @@ -37,7 +55,6 @@ buildConfiguration = "Release" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-macOS.xcscheme b/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-macOS.xcscheme index 43c2c25a..af64ead6 100644 --- a/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-macOS.xcscheme +++ b/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-macOS.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + @@ -37,7 +55,6 @@ buildConfiguration = "Release" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-tvOS.xcscheme b/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-tvOS.xcscheme new file mode 100644 index 00000000..c880a22c --- /dev/null +++ b/SKTiled.xcodeproj/xcshareddata/xcschemes/SKTiled-tvOS.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/SKTile.swift b/Sources/SKTile.swift index 3f865531..2be37a56 100644 --- a/Sources/SKTile.swift +++ b/Sources/SKTile.swift @@ -8,28 +8,116 @@ import SpriteKit + /** ## Overview ## - The `SKTile` class is a custom SpriteKit sprite that references data from a tileset. + The `TileRenderMode` flag determines how a particular tile instance is rendered. If the default is + specified, the tile renders however the parent tilemap tells it to. Only set this flag to override a + particular tile instance's render behavior. + + ### Properties ### + + | Property | Description | + |:---------|:-----------------------------------------------| + | default | Tile renders at default settings. | + | static | Tile ignores any animation data. | + | ignore | Tile does not take into account its tile data. | + | animated | Animate with a global id value. | + + */ +public enum TileRenderMode { + case `default` + case `static` + case ignore + case animated(gid: Int?) +} + +/** + + ## Overview ## + + The `SKTile` class is a custom SpriteKit sprite node that references its data from a tileset. Tile data (including texture) is stored in `SKTilesetData` property. + + + ### Properties ### + + | Property | Description | + |:----------------------------------|:--------------------------------------------| + | tileSize | tile size (in pixels) | + | tileData | tile data structure | + | layer | parent tile layer | + + ### Instance Methods ### + + | Method | Description | + |:----------------------------------|:------------------------------------------------------------| + | setupPhysics(shapeOf:isDynamic:) | Setup physics for the tile. | + | setupPhysics(rectSize:isDynamic:) | Setup physics for the tile. | + | setupPhysics(withSize:isDynamic:) | Setup physics for the tile. | + | runAnimation() | Play tile animation (if animated). | + | removeAnimation(restore:) | Remove animation. | + | runAnimationAsActions() | Runs a SpriteKit action to animate tile tile (if animated). | + | removeAnimationActions(restore:) | Remove the animation for the current tile. | + */ -open class SKTile: SKSpriteNode, Loggable { +open class SKTile: SKSpriteNode { /// Tile size. open var tileSize: CGSize + + /// Animation frame index + private var frameIndex: UInt8 = 0 + /// Tileset tile data. open var tileData: SKTilesetData + /// Weak reference to the parent layer. weak open var layer: SKTileLayer! + /// Object is visible in camera. + open var visibleToCamera: Bool = true + + /// Don't send updates. + internal var blockNotifications: Bool = false + + /// Render mode for this instance. + open var renderMode: TileRenderMode = TileRenderMode.default { + didSet { + guard (oldValue != renderMode) else { return } + + NotificationCenter.default.post( + name: Notification.Name.Tile.RenderModeChanged, + object: self, + userInfo: ["old": oldValue] + ) + } + } + + /** - ## Overview: + ## Overview ## Alignment hint used to define how to handle tile positioning within layers & objects (in the event the tile size is different than the parent). + + ### Properties ### + + | Property | Description | + |:---------------|:--------------------------------------------| + | topLeft | Tile is positioned at the upper left. | + | top | Tile is positioned at top. | + | topRight | Tile is positioned at the upper right. | + | left | Tile is positioned at the left. | + | center | Tile is positioned in the center. | + | right | Tile is positioned to the right. | + | bottomLeft | Tile is positioned at the bottom left. | + | bottom | Tile is positioned at the bottom. | + | bottomRight | Tile is positioned at the bottom right. | + */ public enum TileAlignmentHint: Int { case topLeft @@ -44,21 +132,37 @@ open class SKTile: SKSpriteNode, Loggable { } // Overlap - fileprivate var tileOverlap: CGFloat = 1.5 // tile overlap amount + fileprivate var tileOverlap: CGFloat = 1.0 // tile overlap amount fileprivate var maxOverlap: CGFloat = 3.0 // maximum tile overlap // Update values private var currentTime : TimeInterval = 0 /// Tile highlight color. - open var highlightColor: SKColor = TiledObjectColors.lime + open var highlightColor: SKColor = TiledGlobals.default.debug.tileHighlightColor /// Tile bounds color. - open var frameColor: SKColor = TiledObjectColors.magenta + open var frameColor: SKColor = TiledGlobals.default.debug.frameColor /// Tile highlight duration. - open var highlightDuration: TimeInterval = 0 + open var highlightDuration: TimeInterval = TiledGlobals.default.debug.highlightDuration internal var boundsKey: String = "BOUNDS" + internal var animationKey: String = "TILE-ANIMATION" + + /** + ## Overview: - /// Enum describing the tile's physics shape. + Describes the tile's physics shape. + + ### Properties ### + + | Property | Description | + |:----------|:-------------------------------| + | none | No physics shape. | + | rectangle | Rectangular object shape. | + | ellipse | Circular object shape. | + | texture | Texture-based shape. | + | path | Open path. | + + */ public enum PhysicsShape { case none case rectangle @@ -71,7 +175,7 @@ open class SKTile: SKSpriteNode, Loggable { open var physicsShape: PhysicsShape = .none /// Tile positioning hint. - internal var alignment: TileAlignmentHint = .bottomLeft + internal var alignment: TileAlignmentHint = TileAlignmentHint.bottomLeft /// Returns the bounding box of the shape. open var bounds: CGRect { @@ -79,19 +183,6 @@ open class SKTile: SKSpriteNode, Loggable { } // MARK: - Init - /** - Initialize the tile with a tile size. - - - parameter tileSize: `CGSize` tile size in pixels. - - returns: `SKTile` tile sprite. - */ - public init(tileSize size: CGSize) { - // create empty tileset data - tileData = SKTilesetData() - tileSize = size - super.init(texture: SKTexture(), color: SKColor.clear, size: tileSize) - colorBlendFactor = 0 - } /** Initialize the tile object with `SKTilesetData`. @@ -102,13 +193,21 @@ open class SKTile: SKSpriteNode, Loggable { required public init?(data: SKTilesetData) { guard let tileset = data.tileset else { return nil } self.tileData = data + self.animationKey += "-\(data.globalID)" self.tileSize = tileset.tileSize super.init(texture: data.texture, color: SKColor.clear, size: fabs(tileset.tileSize)) + + // get render mode from tile data properties + if let rawRenderMode = data.intForKey("renderMode") { + if let newRenderMode = TileRenderMode.init(rawValue: rawRenderMode) { + self.renderMode = newRenderMode + } + } } required public init?(coder aDecoder: NSCoder) { tileData = SKTilesetData() - tileSize = .zero + tileSize = CGSize.zero super.init(coder: aDecoder) } @@ -123,6 +222,20 @@ open class SKTile: SKSpriteNode, Loggable { colorBlendFactor = 0 } + /** + Initialize the tile with a tile size. + + - parameter tileSize: `CGSize` tile size in pixels. + - returns: `SKTile` tile sprite. + */ + public init(tileSize size: CGSize) { + // create empty tileset data + tileData = SKTilesetData() + tileSize = size + super.init(texture: SKTexture(), color: SKColor.clear, size: tileSize) + colorBlendFactor = 0 + } + /** Initialize the tile texture. @@ -137,16 +250,16 @@ open class SKTile: SKSpriteNode, Loggable { colorBlendFactor = 0 } + /** - Force the tile to update it's textures. + Draw the tile. Force the tile to update its textures. - - parameter data: `SKTilesetData` tile data. - - returns: `SKTile` tile sprite. + - parameter debug: `Bool` debug draw. */ - internal func draw() { + open func draw(debug: Bool = false) { removeAllActions() - texture = nil texture = tileData.texture + size = tileData.texture.size() } // MARK: - Physics @@ -157,7 +270,7 @@ open class SKTile: SKSpriteNode, Loggable { - parameter shapeOf: `PhysicsShape` tile physics shape type. - parameter isDynamic: `Bool` physics body is active. */ - open func setupPhysics(shapeOf: PhysicsShape = .rectangle, isDynamic: Bool = false) { + open func setupPhysics(shapeOf: PhysicsShape = PhysicsShape.rectangle, isDynamic: Bool = false) { physicsShape = shapeOf switch physicsShape { @@ -230,7 +343,7 @@ open class SKTile: SKSpriteNode, Loggable { /** Run tile animation. */ - public func runAnimation() { + open func runAnimation() { tileData.runAnimation() } @@ -243,12 +356,44 @@ open class SKTile: SKSpriteNode, Loggable { tileData.removeAnimation(restore: restore) } + // MARK: - Legacy Animation + + /** + Checks if the tile is animated and runs a SpriteKit action to animate it. + */ + open func runAnimationAsActions() { + guard (tileData.isAnimated == true) else { return } + removeAction(forKey: animationKey) + + // run tile action + if let animationAction = tileData.animationAction { + run(animationAction, withKey: animationKey) + } else { + fatalError("cannot get animation action for tile data.") + } + } + + /** + Remove the animation for the current tile. + + - parameter restore: `Bool` restore the tile's first texture. + */ + open func removeAnimationActions(restore: Bool = false) { + removeAction(forKey: animationKey) + + guard tileData.isAnimated == true else { return } + + if (restore == true) { + texture = tileData.texture + } + } + // MARK: - Overlap /** Set the tile overlap amount. - - parameter overlap: `CGFloat` tile overlap. + - parameter overlap: `CGFloat` overlap amount. */ open func setTileOverlap(_ overlap: CGFloat) { // clamp the overlap value. @@ -304,26 +449,24 @@ open class SKTile: SKSpriteNode, Loggable { // rotate right, flip vertically (d, h, v) if (tileData.flipHoriz && tileData.flipVert) { - newZRotation = CGFloat(-Double.pi / 2) // rotate 90deg - newXScale *= -1 // flip horizontally + newZRotation = CGFloat(-Double.pi / 2) // rotate 90deg + newXScale *= -1 // flip horizontally alignment = .bottomLeft } // rotate -90 (d, v) if (!tileData.flipHoriz && tileData.flipVert) { - newZRotation = CGFloat(Double.pi / 2) // rotate -90deg + newZRotation = CGFloat(Double.pi / 2) // rotate -90deg alignment = .topLeft } // rotate right, flip horiz (d) if (!tileData.flipHoriz && !tileData.flipVert) { - - newZRotation = CGFloat(Double.pi / 2) // rotate -90deg - newXScale *= -1 // flip horizontally + newZRotation = CGFloat(Double.pi / 2) // rotate -90deg + newXScale *= -1 // flip horizontally alignment = .topRight } - } else { if (tileData.flipHoriz == true) { newXScale *= -1 @@ -381,7 +524,7 @@ open class SKTile: SKSpriteNode, Loggable { - returns: `[CGPoint]?` array of points. */ - open func getVertices(offset: CGPoint = .zero) -> [CGPoint] { + open func getVertices(offset: CGPoint = CGPoint.zero) -> [CGPoint] { var vertices: [CGPoint] = [] guard let layer = layer else { log("tile \(tileData.id) does not have a layer reference.", level: .debug) @@ -454,12 +597,12 @@ open class SKTile: SKSpriteNode, Loggable { /** Draw the tile's boundary shape. - + - parameter withColor: `SKColor?` optional highlight color. - parameter zpos: `CGFloat?` optional z-position of bounds shape. - parameter duration: `TimeInterval` effect length. */ - internal func drawBounds(withColor: SKColor?=nil, zpos: CGFloat?=nil, duration: TimeInterval = 0) { + internal func drawBounds(withColor: SKColor? = nil, zpos: CGFloat? = nil, duration: TimeInterval = 0) { childNode(withName: boundsKey)?.removeFromParent() // if a color is not passed, use the default frame color @@ -478,11 +621,9 @@ open class SKTile: SKSpriteNode, Loggable { let tilesetTileHeight: CGFloat = tilesetTileSize.height // calculate the offset - // TODO: do this in getvertices? var xOffset: CGFloat = 0 var yOffset: CGFloat = 0 - if let layer = layer { switch layer.orientation { case .orthogonal: @@ -531,6 +672,7 @@ open class SKTile: SKSpriteNode, Loggable { bounds.fillColor = drawColor.withAlphaComponent(0.15) bounds.zPosition = shapeZPos + // add the bounding shape addChild(bounds) // anchor point @@ -570,12 +712,6 @@ open class SKTile: SKSpriteNode, Loggable { } } - // MARK: - Memory - internal func flush() { - self.texture = nil - self.tileData.removeAnimation() - } - // MARK: - Updating /** Render the tile before each frame is rendered. @@ -583,13 +719,29 @@ open class SKTile: SKSpriteNode, Loggable { - parameter deltaTime: `TimeInterval` update interval. */ open func update(_ deltaTime: TimeInterval) { - guard (isPaused == false) else { return } + guard (isPaused == false) && (renderMode != TileRenderMode.ignore) else { return } + + // update texture for static frames + if (tileData.isAnimated == false) { + + // check texture is the tile data texture + if (self.texture != tileData.texture) { + + // reset tile texture & size + self.texture = tileData.texture + self.size = tileData.texture.size() + //self.log("updating static tile id: \(tileData.id)", level: .debug) + } + return + } + // max cycle time (in ms) let cycleTime = tileData.animationTime guard (cycleTime > 0) else { return } // array of frame values - let frames: [AnimationFrame] = (speed >= 0) ? tileData.frames : tileData.frames.reversed() + let frames: [TileAnimationFrame] = (speed >= 0) ? tileData.frames : tileData.frames.reversed() + // increment the current time value currentTime += (deltaTime * abs(Double(speed))) @@ -597,24 +749,32 @@ open class SKTile: SKSpriteNode, Loggable { let ct: Int = Int(currentTime * 1000) // current frame - var cf: Int? = nil + var cf: UInt8? = nil var aggregate = 0 + // get the frame at the current time for (idx, frame) in frames.enumerated() { aggregate += frame.duration - if ct < aggregate { + + if ct < aggregate { if cf == nil { - cf = idx + cf = UInt8(idx) } } } + // set texture for current frame if let currentFrame = cf { - let frame = frames[currentFrame] + + // stash the frame index + frameIndex = currentFrame + let frame = frames[Int(currentFrame)] if let frameTexture = frame.texture { - self.texture = frameTexture + // update sprite size + self.texture = frameTexture + //self.log("updating animated tile id: \(tileData.id)", level: .debug) self.size = frameTexture.size() } } @@ -625,6 +785,82 @@ open class SKTile: SKSpriteNode, Loggable { } +extension TileRenderMode: RawRepresentable { + public typealias RawValue = Int + + public init?(rawValue: RawValue) { + switch rawValue { + case 0: self = .default + case 1: self = .static + case 2: self = .ignore + case -1: self = .animated(gid: nil) + default: self = .animated(gid: rawValue) + } + } + + public var rawValue: RawValue { + switch self { + case .default: return 0 + case .static: return 1 + case .ignore: return 2 + case .animated(let gid): + return gid ?? -1 + } + } +} + + + +extension TileRenderMode: CustomStringConvertible, CustomDebugStringConvertible { + + public func next() -> TileRenderMode { + switch self { + case .default: return .static + case .static: return .ignore + default: return .default + } + } + + public var identifier: String { + switch self { + case .default: return "default" + case .static: return "static" + case .ignore: return "ignore" + case .animated(let gid): + let gidstr = (gid != nil) ? "-\(gid!)" : "" + return "animated\(gidstr)" + } + } + + public var description: String { + switch self { + case .default: return "default" + case .static: return "static" + case .ignore: return "ignore" + case .animated(let gid): + let gidString = (gid != nil) ? "\(gid!)" : "nil" + return "animated: \(gidString)" + } + } + + public var debugDescription: String { + switch self { + case .default: return "" + case .static: return "(static)" + case .ignore: return "(ignore)" + case .animated(let gid): + return (gid != nil) ? "(\(gid!))" : "" + } + } +} + + +extension TileRenderMode: Equatable { + public var hashValue: Int { + return identifier.hashValue + } +} + extension SKTile { @@ -667,7 +903,6 @@ extension SKTile { let fadeAction = SKAction.fadeOut(withDuration: highlightDuration) frameShape.run(fadeAction, completion: { frameShape.removeFromParent() - }) } } @@ -677,7 +912,7 @@ extension SKTile { /// Tile description. override open var description: String { let layerDescription = (layer != nil) ? ", Layer: \"\(layer.layerName)\"" : "" - return "\(tileData.description)\(layerDescription)" + return "\(tileData.description)\(layerDescription) \(renderMode.debugDescription)" } /// Tile debug description. @@ -743,3 +978,45 @@ extension SKTile { } } } + + +extension SKTile.TileAlignmentHint: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch self { + case .topLeft: return "topLeft" + case .top: return "top" + case .topRight: return "topRight" + case .left: return "left" + case .center: return "center" + case .right: return "right" + case .bottomLeft: return "bottomLeft" + case .bottom: return "bottom" + case .bottomRight: return "bottomRight" + } + } + + public var debugDescription: String { + return description + } +} + + +extension SKTile.PhysicsShape: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch self { + case .none: return "Physics Shape: none" + case .rectangle: return "Physics Shape: rectangle" + case .ellipse: return "Physics Shape: ellipse" + case .texture: return "Physics Shape: texture" + case .path: return "Physics Shape: path" + } + } + + public var debugDescription: String { + return description + } +} + + +extension SKTile: SKTiledGeometry {} +extension SKTile: Loggable {} diff --git a/Sources/SKTileLayer.swift b/Sources/SKTileLayer.swift index 4a39e681..dd4c8349 100644 --- a/Sources/SKTileLayer.swift +++ b/Sources/SKTileLayer.swift @@ -17,9 +17,16 @@ import Cocoa +// tuple representing the current render stats +typealias RenderInfo = (idx: Int, path: String, zpos: Double, + sw: Int, sh: Int, tsw: Int, tsh: Int, + offx: Int, offy: Int, ancx: Int, ancy: Int, + tc: Int, obj: Int, vis: Int, gn: Int?) -// index, zpos, size w, size h, tile size w, tile size h, offset x, offset y, anchor x, anchor y, tile count, object count -typealias RenderInfo = (idx: Int, path: String, zpos: Double, sw: Int, sh: Int, tsw: Int, tsh: Int, offx: Int, offy: Int, ancx: Int, ancy: Int, tc: Int, obj: Int, vis: Int, gn: Int?) + + +/// Layer render statistics. +typealias LayerRenderStatistics = (tiles: Int, objects: Int) /** @@ -33,14 +40,32 @@ typealias RenderInfo = (idx: Int, path: String, zpos: Double, sw: Int, sh: Int, - validating coordinates - positioning and alignment - ## Usage ## - Layer properties are accessed via the parent tilemap: + ### Properties ### + + | Property | Description | + |-------------|----------------------------------------------------------------------| + | tilemap | Parent tilemap. | + | index | Layer index. Matches the index of the layer in the source TMX file. | + | size | Layer size (in tiles). | + | tileSize | Layer tile size (in pixels). | + | anchorPoint | Layer anchor point, used to position layers. | + | origin | Layer origin point, used for placing tiles. | + + + ### Instance Methods ### + + | Method | Description | + |-----------------------------------|----------------------------------------------------------------------| + | pointForCoordinate(coord:offset:) | Returns a point for a coordinate in the layer, with optional offset. | + | coordinateForPoint(_:) | Returns a tile coordinate for a given point in the layer. | + | touchLocation(_:) | Returns a converted touch location in map space. | + | coordinateAtTouchLocation(_:) | Returns the tile coordinate at a touch location. | + | isValid(coord:) | Returns true if the coordinate is valid. | + + + ### Usage ### - ```swift - layer.size // size (in tiles) - layer.tileSize // tile size (in pixels) - ``` Coordinate transformation functions return points in the current tilemap projection: ```swift @@ -64,18 +89,20 @@ typealias RenderInfo = (idx: Int, path: String, zpos: Double, sw: Int, sh: Int, coord = groupLayer.coordinateAtMouseEvent(event: mouseClicked) ``` */ -public class SKTiledLayerObject: SKNode, SKTiledObject { +public class SKTiledLayerObject: SKEffectNode, SKTiledObject { /// Reference to the parent tilemap. public var tilemap: SKTilemap + /// Unique layer id. public var uuid: String = UUID().uuidString + /// Layer type. public var type: String! /// Layer index. Matches the index of the layer in the source TMX file. public var index: Int = 0 /// Logging verbosity. - internal var loggingLevel: LoggingLevel = SKTiledLoggingLevel + internal var loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel /// Position in the render tree. public var rawIndex: Int = 0 /// Flattened layer index (internal use only). @@ -87,7 +114,21 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { public var properties: [String: String] = [:] public var ignoreProperties: Bool = false - /// Layer type. + /** + ## Overview ## + + Enum describing layer type. + + ### Constants ### + + | Property | Description | + |:------------|:------------------------------------------| + | tile | Layer contains tile sprite data. | + | object | Layer contains vector objects, text, etc. | + | image | Layer contains a static image. | + | group | Layer container. | + + */ enum TiledLayerType: Int { case none = -1 case tile @@ -96,7 +137,26 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { case group } - /// Tile offset hint for coordinate conversion. + /** + ## Overview ## + + Tile offset hint for coordinate conversion. + + ### Constants ### + + | Property | Description | + |:------------|:------------------------------------------| + | center | Tile is centered. | + | top | Tile is offset at the top. | + | topLeft | Tile is offset at the upper left. | + | topRight | Tile is offset at the upper right. | + | bottom | Tile is offset at the bottom. | + | bottomLeft | Tile is offset at the bottom left. | + | bottomRight | Tile is offset at the bottom right. | + | left | Tile is offset at the left side. | + | right | Tile is offset at the right side. | + + */ public enum TileOffset: Int { case center case top @@ -109,19 +169,19 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { case right } - internal var layerType: TiledLayerType = .none + internal var layerType: TiledLayerType = TiledLayerType.none /// Layer color. public var color: SKColor = TiledObjectColors.gun /// Grid visualization color. - public var gridColor: SKColor = TiledObjectColors.obsidian + public var gridColor: SKColor = TiledGlobals.default.debug.gridColor /// Bounding box color. - public var frameColor: SKColor = TiledObjectColors.obsidian + public var frameColor: SKColor = TiledGlobals.default.debug.frameColor /// Layer highlight color (for highlighting tiles) public var highlightColor: SKColor = SKColor.white /// Layer highlight duration - public var highlightDuration: TimeInterval = 0 - + public var highlightDuration: TimeInterval = TiledGlobals.default.debug.highlightDuration + /// Layer is isolated. public private(set) var isolated: Bool = false /// Layer offset value. @@ -133,26 +193,18 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { public var tileSize: CGSize { return tilemap.tileSize } /// Tile map orientation. internal var orientation: SKTilemap.TilemapOrientation { return tilemap.orientation } + /// Layer anchor point, used to position layers. public var anchorPoint: CGPoint { return tilemap.layerAlignment.anchorPoint } internal var gidErrors: [UInt32] = [] - // convenience properties - public var width: CGFloat { return tilemap.width } - public var height: CGFloat { return tilemap.height } - public var tileWidth: CGFloat { return tilemap.tileWidth } - public var tileHeight: CGFloat { return tilemap.tileHeight } - - public var sizeHalved: CGSize { return tilemap.sizeHalved } - public var tileWidthHalf: CGFloat { return tilemap.tileWidthHalf } - public var tileHeightHalf: CGFloat { return tilemap.tileHeightHalf } - public var sizeInPoints: CGSize { return tilemap.sizeInPoints } - /// Pathfinding graph. public var graph: GKGridGraph! // debug visualizations - public var gridOpacity: CGFloat = 0.25 + public var gridOpacity: CGFloat = TiledGlobals.default.debug.gridOpactity + + /// Debug visualization node internal var debugNode: SKTiledDebugDrawNode! /// Debug visualization options. @@ -168,7 +220,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { public var antialiased: Bool = false public var colorBlendFactor: CGFloat = 1.0 /// Render scaling property. - public var renderQuality: CGFloat = 8 + public var renderQuality: CGFloat = TiledGlobals.default.renderQuality.default /// Name used to access navigation graph. public var navigationKey: String @@ -176,9 +228,6 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { /// Output current zPosition public var currentZPosition: CGFloat { return self.zPosition } - /// Layer contains no animation - public var isStatic: Bool = false - /// Optional background color. public var backgroundColor: SKColor? = nil { didSet { @@ -210,7 +259,6 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { return CGPoint.zero case .isometric: return CGPoint(x: height * tileWidthHalf, y: tileHeightHalf) - // TODO: check for errors with objects case .hexagonal: let startPoint = CGPoint.zero //startPoint.x -= tileWidthHalf @@ -235,15 +283,24 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { return self.bounds.points } - /// Returns layer render statisics - internal var renderStatistics: RenderInfo { + internal var renderInfo: RenderInfo { return (index, path, Double(zPosition), Int(tilemap.size.width), Int(tilemap.size.height), Int(tileSize.width), Int(tileSize.height), Int(offset.x), Int(offset.y), Int(anchorPoint.x), Int(anchorPoint.y), 0, 0, (isHidden == true) ? 0 : 1, nil) } + internal var layerRenderStatistics: LayerRenderStatistics { + return (tiles: 0, objects: 0) + } + + /// Update mode. + public var updateMode: TileUpdateMode { + return tilemap.updateMode + } + + // MARK: - Init /** @@ -257,13 +314,13 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { - returns: `SKTiledLayerObject?` tiled layer, if initialization succeeds. */ public init?(layerName: String, tilemap: SKTilemap, attributes: [String: String]) { - self.tilemap = tilemap self.ignoreProperties = tilemap.ignoreProperties self.navigationKey = layerName super.init() self.debugNode = SKTiledDebugDrawNode(tileLayer: self) self.name = layerName + self.shouldEnableEffects = false // layer offset var offsetx: CGFloat = 0 @@ -318,7 +375,13 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { fatalError("init(coder:) has not been implemented") } + deinit { + removeAllActions() + removeAllChildren() + } + // MARK: - Color + /** Set the layer color with an `SKColor`. @@ -394,7 +457,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { } #endif - // MARK: - Coordinates + // MARK: - Coordinate Conversion /** Returns true if the coordinate is valid. @@ -434,7 +497,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { - parameter offsetY: `CGFloat` y-offset value. - returns: `CGPoint` point in layer (spritekit space). */ - public func pointForCoordinate(coord: CGPoint, offsetX: CGFloat=0, offsetY: CGFloat=0) -> CGPoint { + public func pointForCoordinate(coord: CGPoint, offsetX: CGFloat = 0, offsetY: CGFloat = 0) -> CGPoint { var screenPoint = tileToScreenCoords(coord) var tileOffsetX: CGFloat = offsetX @@ -456,6 +519,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { screenPoint.x += tileOffsetX screenPoint.y += tileOffsetY + return floor(point: screenPoint.invertedY) } @@ -469,6 +533,18 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { return screenToTileCoords(point.invertedY) } + /** + Returns a tile coordinate for a given point in the layer as a `vector_int2`. + + - parameter point: `CGPoint` point in layer. + - returns: `int2` tile coordinate. + */ + public func vectorCoordinateForPoint(_ point: CGPoint) -> int2 { + return screenToTileCoords(point.invertedY).toVec2 + } + + // MARK: Internal Coordinate Mapping + /** Converts a tile coordinate from a point in map space. Note that this function expects scene points to be inverted in y before being passed as input. @@ -521,7 +597,6 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { var pixelX = point.x var pixelY = point.y - switch orientation { case .orthogonal: @@ -533,7 +608,6 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { let tileX = pixelX / tileWidth return CGPoint(x: floor(tileY + tileX), y: floor(tileY - tileX)) - case .hexagonal: // initial offset @@ -566,7 +640,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { // get nearest hexagon var centers: [CGVector] - // flat + // flat-topped if (tilemap.staggerX == true) { let left: Int = Int(tilemap.sideLengthX / 2) let centerX: Int = left + Int(tilemap.columnWidth) @@ -576,6 +650,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { CGVector(dx: centerX, dy: centerY + Int(tilemap.rowHeight)), CGVector(dx: centerX + Int(tilemap.columnWidth), dy: centerY) ] + // pointy } else { let top: Int = Int(tilemap.sideLengthY / 2) @@ -683,6 +758,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { */ internal func tileToScreenCoords(_ coord: CGPoint) -> CGPoint { switch orientation { + case .orthogonal: return CGPoint(x: coord.x * tileWidth, y: coord.y * tileHeight) @@ -701,19 +777,18 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { var pixelX: Int = 0 var pixelY: Int = 0 + // flat if (tilemap.staggerX) { pixelY = tileY * Int(tileHeight + tilemap.sideLengthY) - if tilemap.doStaggerX(tileX) { pixelY += Int(tilemap.rowHeight) } - pixelX = tileX * Int(tilemap.columnWidth) + // pointy } else { pixelX = tileX * Int(tileWidth + tilemap.sideLengthX) - if tilemap.doStaggerY(tileY) { // hex error here? pixelX += Int(tilemap.columnWidth) @@ -735,6 +810,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { */ internal func screenToPixelCoords(_ point: CGPoint) -> CGPoint { switch orientation { + case .isometric: var x = point.x let y = point.y @@ -750,8 +826,9 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { } /** - Converts a coordinate in map space to screen space. See: - http://stackoverflow.com/questions/24747420/tiled-map-editor-size-of-isometric-tile-side + Converts a coordinate in map space to screen space. + + See: http://stackoverflow.com/questions/24747420/tiled-map-editor-size-of-isometric-tile-side - parameter point: `CGPoint` point in map space. - returns: `CGPoint` point in screen space. @@ -772,6 +849,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { } // MARK: - Adding & Removing Nodes + /** Add an `SKNode` child node at the given x/y coordinates. By default, the zPositon will be higher than all of the other nodes in the layer. @@ -782,8 +860,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { - parameter offset: `CGPoint` offset amount. - parameter zpos: `CGFloat?` optional z-position. */ - public func addChild(_ node: SKNode, x: Int=0, y: Int=0, offset: CGPoint = CGPoint.zero, zpos: CGFloat? = nil) { - log("\"\(layerName)\" adding child", level: .warning) + public func addChild(_ node: SKNode, x: Int = 0, y: Int = 0, offset: CGPoint = CGPoint.zero, zpos: CGFloat? = nil) { let coord = CGPoint(x: CGFloat(x), y: CGFloat(y)) addChild(node, coord: coord, offset: offset, zpos: zpos) } @@ -799,7 +876,6 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { */ public func addChild(_ node: SKNode, coord: CGPoint, offset: CGPoint = CGPoint.zero, zpos: CGFloat? = nil) { addChild(node) - log("\"\(layerName)\" adding child", level: .warning) node.position = pointForCoordinate(coord: coord, offsetX: offset.y, offsetY: offset.y) node.position.x += offset.x node.position.y += offset.y @@ -811,7 +887,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { - parameter outsideOf: `CGRect` camera bounds. */ - internal func pruneTiles(_ outsideOf: CGRect) { + internal func pruneTiles(_ outsideOf: CGRect? = nil, zoom: CGFloat = 1, buffer: CGFloat = 2) { /* override in subclass */ } @@ -821,8 +897,8 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { - parameter duration: `TimeInterval` fade-in duration. */ - public func didFinishRendering(duration: TimeInterval=0) { - log("layer rendered: \"\(layerName)\"", level: .debug) + public func didFinishRendering(duration: TimeInterval = 0) { + log(" - layer rendered: \"\(layerName)\"", level: .debug) self.parseProperties(completion: nil) // setup physics for the layer boundary if hasKey("isDynamic") && boolForKey("isDynamic") == true || hasKey("isCollider") && boolForKey("isCollider") == true { @@ -838,7 +914,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { - parameter isDynamic: `Bool` layer is dynamic. */ - public func setupLayerPhysicsBoundary(isDynamic: Bool=false) { + public func setupLayerPhysicsBoundary(isDynamic: Bool = false) { physicsBody = SKPhysicsBody(edgeLoopFrom: self.bounds) physicsBody?.isDynamic = isDynamic } @@ -850,12 +926,27 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { // override in subclass } - - override public var hashValue: Int { + override public var hash: Int { return self.uuid.hashValue } + // MARK: - Shaders + + + /** + Set a shader effect for the layer. + + - parameter named: `String` shader file name. + - parameter uniforms: `[SKUniform]` array of shader uniforms. + */ + public func setShader(named: String, uniforms: [SKUniform] = []) { + let layerShader = SKShader(fileNamed: named) + layerShader.uniforms = uniforms + shouldEnableEffects = true + self.shader = layerShader + } // MARK: - Debugging + /** Visualize the layer's boundary shape. */ @@ -866,8 +957,8 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { public func debugLayer() { /* override in subclass */ - let comma = (propertiesString.characters.isEmpty == false) ? ", " : "" - log("Layer: \(name != nil ? "\"\(layerName)\"" : "null")\(comma)\(propertiesString)", level: .debug) + let comma = (propertiesString.isEmpty == false) ? ", " : "" + self.log("Layer: \(name != nil ? "\"\(layerName)\"" : "null")\(comma)\(propertiesString)", level: .debug) } /** @@ -875,18 +966,32 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { */ public func isolateLayer(duration: TimeInterval = 0) { let hideLayers = (self.isolated == false) + let layersToIgnore = self.parents - //layersToIgnore.forEach({$0.isHidden = false}) - tilemap.layers.filter { layersToIgnore.contains($0) == false}.forEach({ $0.isHidden = hideLayers}) + let layersToProtect = self.childLayers + + tilemap.layers.filter { (layersToIgnore.contains($0) == false) && (layersToProtect.contains($0) == false)}.forEach { layer in + + if (duration == 0) { + layer.isHidden = hideLayers + } else { + let fadeAction = (hideLayers == true) ? SKAction.fadeOut(withDuration: duration) : SKAction.fadeIn(withDuration: duration) + layer.run(fadeAction, completion: { + layer.isHidden = hideLayers + layer.alpha = 1 + }) + } + } + self.isolated = !self.isolated } - /** Render the layer to a texture + /** Render the layer to a texture. - Returns: `SKTexture?` rendered texture. */ internal func render() -> SKTexture? { - let renderSize = tilemap.sizeInPoints * SKTiledContentScaleFactor + let renderSize = tilemap.sizeInPoints * TiledGlobals.default.contentScale let cropRect = CGRect(x: 0, y: -renderSize.height, width: renderSize.width, height: renderSize.height) @@ -898,15 +1003,23 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { return nil } - // MARK: - Memory + // MARK: - Updating + /** - Dump debug images to conserve memory. + Initialize SpriteKit animation actions for the layer. */ - internal func flush() { - debugNode.flush() + public func runAnimationAsActions() { + // override in subclass } - // MARK: - Updating + /** + Remove SpriteKit animations in the layer. + + - parameter restore: `Bool` restore tile/obejct texture. + */ + public func removeAnimationActions(restore: Bool = false) { + self.log("removing SpriteKit actions for layer \"\(self.layerName)\"...", level: .debug) + } /** Update the layer before each frame is rendered. @@ -914,8 +1027,9 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { - parameter currentTime: `TimeInterval` update interval. */ public func update(_ currentTime: TimeInterval) { - guard (isRendered == true && isStatic == false) else { return } - self.position = clampedPosition(point: self.position, scale: SKTiledContentScaleFactor) + guard (isRendered == true) else { return } + // clamp the position of the map & parent nodes + // clampNodePosition(node: self, scale: SKTiledGlobals.default.contentScale) } } @@ -928,7 +1042,27 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { Subclass of `SKTiledLayerObject`, the tile layer is a container for an array of tiles (sprites). Tiles maintain a link to the map's tileset via their `SKTilesetData` property. - ## Usage ## + +### Properties ### + +| Property | Description | +|---------------------------|--------------------------------------------------------| +| tileCount | Returns a count of valid tiles. | + + +### Instance Methods ### + +| Method | Description | +|---------------------------|--------------------------------------------------------| +| getTiles() | Returns an array of current tiles. | +| getTiles(ofType:) | Returns tiles of the given type. | +| getTiles(globalID:) | Returns all tiles matching a global id. | +| getTilesWithProperty(_:_) | Returns tiles matching the given property & value. | +| animatedTiles() | Returns all animated tiles. | +| getTileData(globalID:) | Returns all tiles matching a global id. | +| tileAt(coord:) | Returns a tile at the given coordinate, if one exists. | + + ### Usage ### Accessing a tile at a given coordinate: @@ -936,7 +1070,7 @@ public class SKTiledLayerObject: SKNode, SKTiledObject { let tile = tileLayer.tileAt(2, 6)! ``` - Getting tiles of a certain type: + Query tiles of a certain type: ```swift let floorTiles = tileLayer.getTiles(ofType: "Floor") @@ -955,8 +1089,8 @@ public class SKTileLayer: SKTiledLayerObject { } /// Tuple of layer render statistics. - override internal var renderStatistics: RenderInfo { - var current = super.renderStatistics + override internal var renderInfo: RenderInfo { + var current = super.renderInfo current.tc = tileCount if let graph = graph { current.gn = graph.nodes?.count ?? nil @@ -964,12 +1098,28 @@ public class SKTileLayer: SKTiledLayerObject { return current } + override var layerRenderStatistics: LayerRenderStatistics { + var current = super.layerRenderStatistics + + var tc: Int + switch updateMode { + case .full: + tc = self.tileCount + case .dynamic: + tc = 0 + default: + tc = 0 + } + + current.tiles = tc + return current + } + /// Debug visualization options. override public var debugDrawOptions: DebugDrawOptions { didSet { guard oldValue != debugDrawOptions else { return } - debugNode?.draw() - + debugNode.draw() let doShowTileBounds = debugDrawOptions.contains(.drawTileBounds) tiles.forEach { $0?.showBounds = doShowTileBounds } } @@ -978,18 +1128,17 @@ public class SKTileLayer: SKTiledLayerObject { /// Tile highlight duration override public var highlightDuration: TimeInterval { didSet { - tiles.flatMap { $0 }.forEach { $0.highlightDuration = highlightDuration } + tiles.compactMap { $0 }.forEach { $0.highlightDuration = highlightDuration } } } override public var speed: CGFloat { didSet { guard oldValue != speed else { return } - self.getTiles().forEach {$0.speed = speed} + self.getTiles().forEach { $0.speed = speed } } } - // MARK: - Init /** Initialize with layer name and parent `SKTilemap`. @@ -1023,12 +1172,6 @@ public class SKTileLayer: SKTiledLayerObject { fatalError("init(coder:) has not been implemented") } - - deinit { - removeAllActions() - removeAllChildren() - } - // MARK: - Tiles /** @@ -1054,12 +1197,24 @@ public class SKTileLayer: SKTiledLayerObject { } /** - Returns all current tiles. + Returns a tile at the given screen position, if one exists. + + - parameter point: `CGPoint` screen point. + - parameter offset: `CGPoint` pixel offset. + - returns: `SKTile?` tile object, if it exists. + */ + public func tileAt(point: CGPoint, offset: CGPoint = CGPoint.zero) -> SKTile? { + let coord = coordinateForPoint(point) + return tileAt(coord: coord) + } + + /** + Returns an array of current tiles. - returns: `[SKTile]` array of tiles. */ public func getTiles() -> [SKTile] { - return tiles.flatMap { $0 } + return tiles.compactMap { $0 } } /** @@ -1069,7 +1224,7 @@ public class SKTileLayer: SKTiledLayerObject { - returns: `[SKTile]` array of tiles. */ public func getTiles(ofType: String) -> [SKTile] { - return tiles.flatMap { $0 }.filter { $0.tileData.type == ofType } + return tiles.compactMap { $0 }.filter { $0.tileData.type == ofType } } /** @@ -1079,7 +1234,7 @@ public class SKTileLayer: SKTiledLayerObject { - returns: `[SKTile]` array of tiles. */ public func getTiles(globalID: Int) -> [SKTile] { - return tiles.flatMap { $0 }.filter { $0.tileData.id == globalID } + return tiles.compactMap { $0 }.filter { $0.tileData.globalID == globalID } } /** @@ -1140,10 +1295,12 @@ public class SKTileLayer: SKTiledLayerObject { /** Add tile data array to the layer and render it. - - parameter data: `[Int]` tile data. + - parameter data: `[UInt32]` tile data. + - parameter debug: `Bool` debug mode. - returns: `Bool` data was successfully added. */ - public func setLayerData(_ data: [UInt32], debug: Bool=false) -> Bool { + @discardableResult + public func setLayerData(_ data: [UInt32], debug: Bool = false) -> Bool { if !(data.count == size.count) { log("invalid data size for layer \"\(self.layerName)\": \(data.count), expected: \(size.count)", level: .error) return false @@ -1160,6 +1317,8 @@ public class SKTileLayer: SKTiledLayerObject { let y: Int = index / Int(self.size.width) let coord = CGPoint(x: CGFloat(x), y: CGFloat(y)) + + // build the tile let tile = self.buildTileAt(coord: coord, id: gid) if (tile == nil) { @@ -1173,6 +1332,16 @@ public class SKTileLayer: SKTiledLayerObject { return errorCount == 0 } + /** + Clear the layer of tiles. + */ + public func clearLayer() { + self.tiles.forEach { tile in + tile?.removeFromParent() + } + self.tiles = TilesArray(columns: Int(tilemap.size.width), rows: Int(tilemap.size.height)) + } + /** Build an empty tile at the given coordinates. Returns an existing tile if one already exists, or nil if the coordinate is invalid. @@ -1190,7 +1359,7 @@ public class SKTileLayer: SKTiledLayerObject { let tileData: SKTilesetData? = (gid != nil) ? getTileData(globalID: gid!) : nil - let Tile = tilemap.delegate != nil ? tilemap.delegate!.objectForTileType(named: tileType) : SKTile.self + let Tile = (tilemap.delegate != nil) ? tilemap.delegate!.objectForTileType(named: tileType) : SKTile.self let tile = Tile.init() tile.tileSize = tileSize @@ -1226,7 +1395,7 @@ public class SKTileLayer: SKTiledLayerObject { public func addTileAt(coord: CGPoint, texture: SKTexture? = nil, tileType: String? = nil) -> SKTile? { guard isValid(coord: coord) else { return nil } - let Tile = tilemap.delegate != nil ? tilemap.delegate!.objectForTileType(named: tileType) : SKTile.self + let Tile = (tilemap.delegate != nil) ? tilemap.delegate!.objectForTileType(named: tileType) : SKTile.self let tile = Tile.init() tile.tileSize = tileSize @@ -1287,6 +1456,17 @@ public class SKTileLayer: SKTiledLayerObject { return removeTileAt(coord: coord) } + /** + Clear all tiles. + */ + public func clearTiles() { + self.tiles.forEach { tile in + tile?.removeAnimation() + tile?.removeFromParent() + } + self.tiles = TilesArray(columns: Int(tilemap.size.width), rows: Int(tilemap.size.height)) + } + /** Remove the tile at a given coordinate. @@ -1313,8 +1493,10 @@ public class SKTileLayer: SKTiledLayerObject { // get tile attributes from the current id let tileAttrs = flippedTileFlags(id: id) + + let globalId = Int(tileAttrs.gid) - if let tileData = tilemap.getTileData(globalID: Int(tileAttrs.gid)) { + if let tileData = tilemap.getTileData(globalID: globalId) { // set the tile data flip flags tileData.flipHoriz = tileAttrs.hflip @@ -1333,7 +1515,7 @@ public class SKTileLayer: SKTiledLayerObject { // set the layer property tile.layer = self tile.highlightDuration = highlightDuration - + // get the position in the layer (plus tileset offset) let tilePosition = pointForCoordinate(coord: coord, offsetX: tileData.tileset.tileOffset.x, offsetY: tileData.tileset.tileOffset.y) @@ -1351,14 +1533,22 @@ public class SKTileLayer: SKTiledLayerObject { //tile.zPosition = coord.y if tile.texture == nil { - Logger.default.cache(LogEvent("cannot find a texture for id: \(tileAttrs.gid)", level: .warning, caller: self.logSymbol)) + Logger.default.log("cannot find a texture for id: \(tileAttrs.gid)", level: .warning, symbol: self.logSymbol) } + // add to tile cache + NotificationCenter.default.post( + name: Notification.Name.Layer.TileAdded, + object: tile, + userInfo: ["layer": self] + ) + return tile } else { - Logger.default.cache(LogEvent("invalid tileset data (id: \(id))", level: .error, caller: self.logSymbol)) + Logger.default.log("invalid tileset data (id: \(id))", level: .warning, symbol: self.logSymbol) } + } else { // check for bad gid calls if !gidErrors.contains(tileAttrs.gid) { @@ -1403,28 +1593,30 @@ public class SKTileLayer: SKTiledLayerObject { tile!.setTileOverlap(overlap) } } + // MARK: - Callbacks /** Called when the layer is finished rendering. - parameter duration: `TimeInterval` fade-in duration. */ - override public func didFinishRendering(duration: TimeInterval=0) { + override public func didFinishRendering(duration: TimeInterval = 0) { super.didFinishRendering(duration: duration) } // MARK: - Shaders /** - Set a shader for the tile layer. + Set a shader for tiles in this layer. + - parameter for: `[SKTile]` tiles to apply shader to. - parameter named: `String` shader file name. - parameter uniforms: `[SKUniform]` array of shader uniforms. */ - public func setShader(named: String, uniforms: [SKUniform]=[]) { + public func setShader(for sktiles: [SKTile], named: String, uniforms: [SKUniform] = []) { let shader = SKShader(fileNamed: named) shader.uniforms = uniforms - for tile in tiles.flatMap({$0}) { + for tile in sktiles { tile.shader = shader } } @@ -1434,7 +1626,7 @@ public class SKTileLayer: SKTiledLayerObject { Visualize the layer's boundary shape. */ override public func drawBounds() { - tiles.flatMap({ $0 }).forEach { $0.drawBounds() } + tiles.compactMap{ $0 }.forEach { $0.drawBounds() } super.drawBounds() } @@ -1445,16 +1637,31 @@ public class SKTileLayer: SKTiledLayerObject { } } - // MARK: - Memory + // MARK: - Updating: Tile Layer + /** - Dump debug images to conserve memory. + Run animation actions on all tiles layer. */ - override internal func flush() { - super.flush() - getTiles().forEach { $0.flush() } + override public func runAnimationAsActions() { + super.runAnimationAsActions() + let animatedTiles = getTiles().filter { tile in + tile.tileData.isAnimated == true + } + animatedTiles.forEach { $0.runAnimationAsActions() } } - // MARK: - Updating + /** + Remove tile animations. + + - parameter restore: `Bool` restore tile/obejct texture. + */ + override public func removeAnimationActions(restore: Bool = false) { + super.removeAnimationActions(restore: restore) + let animatedTiles = getTiles().filter { tile in + tile.tileData.isAnimated == true + } + animatedTiles.forEach { $0.removeAnimationActions(restore: restore) } + } /** Update the tile layer before each frame is rendered. @@ -1463,9 +1670,8 @@ public class SKTileLayer: SKTiledLayerObject { */ override public func update(_ currentTime: TimeInterval) { super.update(currentTime) - getTiles().forEach { tile in - tile.update(currentTime) - } + guard (self.updateMode != TileUpdateMode.actions) else { return } + } } @@ -1473,11 +1679,11 @@ public class SKTileLayer: SKTiledLayerObject { /** Represents object group draw order: - - topDown: objects are rendered from top-down + - topdown: objects are rendered from top-down - manual: objects are rendered manually */ internal enum SKObjectGroupDrawOrder: String { - case topDown // default + case topdown // default case manual } @@ -1490,7 +1696,24 @@ internal enum SKObjectGroupDrawOrder: String { The `SKObjectGroup` class is a container for vector object types. Most object properties can be set on the parent `SKObjectGroup` which is then applied to all child objects. - ## Usage ## + ### Properties ### + + | Property | Description | + |-----------------------|------------------------------------------------------------------| + | count | Returns the number of objects in the layer. | + | showObjects | Toggle visibility for all of the objects in the layer. | + | lineWidth | Governs object line width for each object. | + | debugDrawOptions | Debugging display flags. | + + ### Methods ### + + | Method | Description | + |-----------------------|------------------------------------------------------------------| + | addObject | Returns the number of objects in the layer. | + | removeObject | Toggle visibility for all of the objects in the layer. | + | getObject(withID:) | Returns an object with the given id, if it exists. | + + ### Usage ### Adding a child object with optional color override: @@ -1504,7 +1727,7 @@ internal enum SKObjectGroupDrawOrder: String { let doorObject = objectGroup.getObject(named: "Door") ``` - Getting objects of a certain type: + Returning objects of a certain type: ```swift let rockObjects = objectGroup.getObjects(ofType: "Rock") @@ -1512,21 +1735,25 @@ internal enum SKObjectGroupDrawOrder: String { */ public class SKObjectGroup: SKTiledLayerObject { - internal var drawOrder: SKObjectGroupDrawOrder = SKObjectGroupDrawOrder.topDown + internal var drawOrder: SKObjectGroupDrawOrder = SKObjectGroupDrawOrder.topdown fileprivate var objects: Set = [] - + /// Toggle visibility for all of the objects in the layer. public var showObjects: Bool = false { didSet { - objects.filter { $0.isRenderableType == false }.forEach { $0.visible = showObjects } + let proxies = self.getObjectProxies() + + NotificationCenter.default.post( + name: Notification.Name.DataStorage.ProxyVisibilityChanged, + object: proxies, + userInfo: ["visibility": showObjects] + ) } } - /// Returns the number of objects in this layer. public var count: Int { return objects.count } - /// Controls antialiasing for each object override public var antialiased: Bool { didSet { @@ -1548,12 +1775,28 @@ public class SKObjectGroup: SKTiledLayerObject { } /// Returns a tuple of render stats used for debugging. - override internal var renderStatistics: RenderInfo { - var current = super.renderStatistics + override internal var renderInfo: RenderInfo { + var current = super.renderInfo current.obj = count return current } + override var layerRenderStatistics: LayerRenderStatistics { + var current = super.layerRenderStatistics + var oc: Int + + switch updateMode { + case .full: + oc = self.getObjects().count + case .dynamic: + oc = 0 + default: + oc = 0 + } + + current.objects = oc + return current + } /// Render scaling property. override public var renderQuality: CGFloat { @@ -1569,8 +1812,6 @@ public class SKObjectGroup: SKTiledLayerObject { override public var debugDrawOptions: DebugDrawOptions { didSet { guard oldValue != debugDrawOptions else { return } - debugNode?.draw() - let doShowObjects = debugDrawOptions.contains(.drawObjectBounds) objects.forEach { $0.showBounds = doShowObjects } } @@ -1635,23 +1876,18 @@ public class SKObjectGroup: SKTiledLayerObject { return nil } - // if the override color is nil, use the layer color - var objectColor: SKColor = (withColor == nil) ? self.color : withColor! - // if the object has a color property override, use that instead if object.hasKey("color") { if let hexColor = object.stringForKey("color") { - objectColor = SKColor(hexString: hexColor) + object.setColor(color: SKColor(hexString: hexColor)) } } // position the object let pixelPosition = object.position let screenPosition = pixelToScreenCoords(pixelPosition) - object.position = screenPosition.invertedY - // transfer object properties - object.setColor(color: objectColor) + object.position = screenPosition.invertedY object.isAntialiased = antialiased object.lineWidth = lineWidth objects.insert(object) @@ -1660,27 +1896,37 @@ public class SKObjectGroup: SKTiledLayerObject { addChild(object) object.zPosition = (objects.isEmpty == false) ? CGFloat(objects.count) : 0 + + // add to object cache + NotificationCenter.default.post( + name: Notification.Name.Layer.ObjectAdded, + object: object, + userInfo: nil + ) - // hide the object if the tilemap `showObjects` property is set to false - object.visible = (object.isRenderableType == true) ? object.visible : tilemap.showObjects return object } /** - Remove an `SKTileObject` object from the objects set. + Remove an `SKTileObject` object from the object set. - parameter object: `SKTileObject` object. - returns: `SKTileObject?` removed object. */ public func removeObject(_ object: SKTileObject) -> SKTileObject? { + NotificationCenter.default.post( + name: Notification.Name.Layer.ObjectRemoved, + object: object, + userInfo: nil + ) return objects.remove(object) } /** Render all of the objects in the group. */ - public func drawObjects() { - objects.forEach { $0.drawObject() } + public func draw() { + objects.forEach { $0.draw() } } /** @@ -1719,7 +1965,7 @@ public class SKObjectGroup: SKTiledLayerObject { - returns: `[String]` object names in the layer. */ public func objectNames() -> [String] { - return objects.flatMap({ $0.name }) + return objects.compactMap { $0.name } } /** @@ -1742,7 +1988,7 @@ public class SKObjectGroup: SKTiledLayerObject { - returns: `[SKTileObject]` array of matching objects. */ public func getObjects(withText text: String) -> [SKTileObject] { - return objects.filter { $0.text != nil }.filter { $0.text! == text } + return getObjects().filter { $0.text != nil }.filter { $0.text! == text } } /** @@ -1752,7 +1998,7 @@ public class SKObjectGroup: SKTiledLayerObject { - returns: `[SKTileObject]` array of matching objects. */ public func getObjects(named: String) -> [SKTileObject] { - return objects.filter { $0.name != nil }.filter { $0.name! == named } + return getObjects().filter { $0.name != nil }.filter { $0.name! == named } } /** @@ -1771,7 +2017,16 @@ public class SKObjectGroup: SKTiledLayerObject { - returns: `[SKTileObject]` array of matching objects. */ public func getObjects(ofType: String) -> [SKTileObject] { - return objects.filter { $0.type != nil }.filter { $0.type! == ofType } + return getObjects().filter { $0.type != nil }.filter { $0.type! == ofType } + } + + /** + Return object proxies. + + - returns: `[TileObjectProxy]` array of object proxies. + */ + internal func getObjectProxies() -> [TileObjectProxy] { + return objects.compactMap { $0.proxy } } // MARK: - Tile Objects @@ -1809,7 +2064,7 @@ public class SKObjectGroup: SKTiledLayerObject { let object = SKTileObject(width: objectSize.width, height: objectSize.height) object.gid = data.globalID _ = addObject(object) - object.drawObject() + object.draw() return object } @@ -1831,7 +2086,7 @@ public class SKObjectGroup: SKTiledLayerObject { - parameter duration: `TimeInterval` fade-in duration. */ - override public func didFinishRendering(duration: TimeInterval=0) { + override public func didFinishRendering(duration: TimeInterval = 0) { super.didFinishRendering(duration: duration) // setup dynamics for objects. @@ -1842,18 +2097,38 @@ public class SKObjectGroup: SKTiledLayerObject { } } - // MARK: - Memory + // MARK: - Updating: Object Group /** - Dump debug images to conserve memory. + Run animation actions on all tile objects. */ - override internal func flush() { - super.flush() - objects.forEach { $0.flush() } + override public func runAnimationAsActions() { + super.runAnimationAsActions() + let animatedObjects = getObjects().filter { $0.isAnimated == true } + animatedObjects.forEach { $0.tile?.runAnimationAsActions() } } - // MARK: - Updating + /** + Remove tile object animation. + + - parameter restore: `Bool` restore tile/obejct texture. + */ + override public func removeAnimationActions(restore: Bool = false) { + super.removeAnimationActions(restore: restore) + let animatedTiles = getObjects().filter { object in + if let tile = object.tile { + return tile.tileData.isAnimated == true + } + return false + } + + animatedTiles.forEach { object in + object.tile!.removeAnimationActions(restore: restore) + } + } + + // MARK: - Updating /** Update the object group before each frame is rendered. @@ -1861,8 +2136,7 @@ public class SKObjectGroup: SKTiledLayerObject { */ override public func update(_ currentTime: TimeInterval) { super.update(currentTime) - getObjects().forEach { $0.lineWidth = lineWidth } - getObjects().forEach { $0.update(currentTime)} + guard (self.updateMode != TileUpdateMode.actions) else { return } } } @@ -1873,8 +2147,25 @@ public class SKObjectGroup: SKTiledLayerObject { ## Overview ## The `SKImageLayer` object is really nothing more than a sprite with positioning attributes. - - ## Usage ## + + ### Properties ### + + | Property | Description | + |:---------|:-------------------| + | image | Layer image name. | + | wrapX | Wrap horizontally. | + | wrapY | Wrap vertically. | + + + ### Methods ### + + | Method | Description | + |:----------------|:-------------------------| + | setLayerImage | Set the layer's image. | + | setLayerTexture | Set the layer's texture. | + | wrapY | Wrap vertically. | + + ### Usage ### Set the layer image with: @@ -1885,13 +2176,14 @@ public class SKObjectGroup: SKTiledLayerObject { public class SKImageLayer: SKTiledLayerObject { public var image: String! // image name for layer - private var textures: [SKTexture] = [] // texture values - private var sprite: SKSpriteNode? // sprite + private var textures: [SKTexture] = [] // texture values + private var sprite: SKSpriteNode? // sprite public var wrapX: Bool = false // wrap horizontally public var wrapY: Bool = false // wrap vertically // MARK: - Init + /** Initialize with a layer name, and parent `SKTilemap` node. @@ -1935,20 +2227,29 @@ public class SKImageLayer: SKTiledLayerObject { self.sprite!.position.y -= textureSize.height / 2.0 } - public func setLayerTexture(texture: SKTexture) { - let textureSize = texture.size() + /** + Update the layer texture. + - parameter texture: `SKTexture` layer image texture. + */ + public func setLayerTexture(texture: SKTexture) { self.sprite = SKSpriteNode(texture: texture) addChild(self.sprite!) - - //self.sprite!.position.x += textureSize.width / 2 - //self.sprite!.position.y -= textureSize.height / 2.0 } + /** + Set the layer texture with an image name. + + - parameter imageNamed: `String` image name. + - returns: `SKTexture` texture added. + */ private func addTexture(imageNamed named: String) -> SKTexture { let inputURL = URL(fileURLWithPath: named) // read image from file - let imageDataProvider = CGDataProvider(url: inputURL as CFURL)! + guard let imageDataProvider = CGDataProvider(url: inputURL as CFURL) else { + self.log("Image read error: \"\(named)\"", level: .fatal) + fatalError("Error reading image: \"\(named)\"") + } // creare a data provider let image = CGImage(pngDataProviderSource: imageDataProvider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)! @@ -1959,12 +2260,11 @@ public class SKImageLayer: SKTiledLayerObject { return sourceTexture } - required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - // MARK: - Updating + // MARK: - Updating: Image Layer /** Update the image layer before each frame is rendered. @@ -2016,7 +2316,7 @@ internal class BackgroundLayer: SKTiledLayerObject { sprite = SKSpriteNode(texture: nil, color: tilemap.backgroundColor ?? SKColor.clear, size: tilemap.sizeInPoints) addChild(self.sprite!) - log("background size: \(sprite.size.shortDescription)", level: .debug) + self.log("background size: \(sprite.size.shortDescription)", level: .debug) // position sprite sprite!.position.x += tilemap.sizeInPoints.width / 2 @@ -2036,7 +2336,7 @@ internal class BackgroundLayer: SKTiledLayerObject { fatalError("init(coder:) has not been implemented") } - // MARK: - Updating + // MARK: - Updating: Background Layer /** Update the background layer before each frame is rendered. @@ -2105,7 +2405,7 @@ public class SKGroupLayer: SKTiledLayerObject { override public var speed: CGFloat { didSet { guard oldValue != speed else { return } - self.layers.forEach {$0.speed = speed} + self.layers.forEach { $0.speed = speed } } } @@ -2156,7 +2456,7 @@ public class SKGroupLayer: SKTiledLayerObject { - returns: `[String]` layer names. */ public func layerNames() -> [String] { - return layers.flatMap { $0.name } + return layers.compactMap { $0.name } } /** @@ -2166,6 +2466,7 @@ public class SKGroupLayer: SKTiledLayerObject { - parameter clamped: `Bool` clamp position to nearest pixel. - returns: `(success: Bool, layer: SKTiledLayerObject)` add was successful, layer added. */ + @discardableResult public func addLayer(_ layer: SKTiledLayerObject, clamped: Bool = true) -> (success: Bool, layer: SKTiledLayerObject) { // set the zPosition relative to the layer index ** adding multiplier - layers with difference of 1 seem to have z-fighting issues **. @@ -2187,6 +2488,7 @@ public class SKGroupLayer: SKTiledLayerObject { addChild(layer) + layer.zPosition = nextZPosition // override debugging colors @@ -2209,6 +2511,8 @@ public class SKGroupLayer: SKTiledLayerObject { return _layers.remove(layer) } + // MARK: - Updating: Group Layer + /** Update the group layer before each frame is rendered. @@ -2252,10 +2556,20 @@ internal struct Array2D { } +// MARK: - Extensions extension SKTiledLayerObject { - // MARK: - Extensions + // convenience properties + public var width: CGFloat { return tilemap.width } + public var height: CGFloat { return tilemap.height } + public var tileWidth: CGFloat { return tilemap.tileWidth } + public var tileHeight: CGFloat { return tilemap.tileHeight } + + public var sizeHalved: CGSize { return tilemap.sizeHalved } + public var tileWidthHalf: CGFloat { return tilemap.tileWidthHalf } + public var tileHeightHalf: CGFloat { return tilemap.tileHeightHalf } + public var sizeInPoints: CGSize { return tilemap.sizeInPoints } /// Layer transparency. public var opacity: CGFloat { @@ -2292,7 +2606,6 @@ extension SKTiledLayerObject { let coord = CGPoint(x: CGFloat(x), y: CGFloat(y)) let offset = CGPoint(x: dx, y: dy) addChild(node, coord: coord, offset: offset, zpos: zpos) - log("\"\(layerName)\" adding child", level: .warning) } /** @@ -2304,7 +2617,7 @@ extension SKTiledLayerObject { - parameter offsetY: `CGFloat` y-offset value. - returns: `CGPoint` position in layer. */ - public func pointForCoordinate(_ x: Int, _ y: Int, offsetX: CGFloat=0, offsetY: CGFloat=0) -> CGPoint { + public func pointForCoordinate(_ x: Int, _ y: Int, offsetX: CGFloat = 0, offsetY: CGFloat = 0) -> CGPoint { return self.pointForCoordinate(coord: CGPoint(x: CGFloat(x), y: CGFloat(y)), offsetX: offsetX, offsetY: offsetY) } @@ -2359,7 +2672,7 @@ extension SKTiledLayerObject { - parameter offsetY: `CGFloat` y-offset value. - returns: `CGPoint` position in layer. */ - public func pointForCoordinate(vec2: int2, offsetX: CGFloat=0, offsetY: CGFloat=0) -> CGPoint { + public func pointForCoordinate(vec2: int2, offsetX: CGFloat = 0, offsetY: CGFloat = 0) -> CGPoint { return self.pointForCoordinate(coord: vec2.cgPoint, offsetX: offsetX, offsetY: offsetY) } @@ -2391,18 +2704,34 @@ extension SKTiledLayerObject { } override public var description: String { - let topLevel = self.parents.count == 1 - let indexString = (topLevel == true) ? ", index: \(index)" : "" - return "\(layerType.stringValue.capitalized) Layer: \"\(self.path)\"\(indexString), zpos: \(Int(self.zPosition))" + let isTopLevel = self.parents.count == 1 + let indexString = (isTopLevel == true) ? ", index: \(index)" : "" + let layerTypeString = (layerType != TiledLayerType.none) ? layerType.stringValue.capitalized : "Background" + return "\(layerTypeString) Layer: \"\(self.path)\"\(indexString), zpos: \(Int(self.zPosition))" } override public var debugDescription: String { return "<\(description)>" } + + /// Returns a value for use in a dropdown menu. + public var menuDescription: String { + let parentCount = parents.count + let isGrouped: Bool = (parentCount > 1) + var layerSymbol: String = layerType.symbol + let isGroupNode = (layerType == TiledLayerType.group) + let hasChildren: Bool = (childLayers.isEmpty == false) + if (isGroupNode == true) { + layerSymbol = (hasChildren == true) ? "▿" : "▹" + } + + let filler = (isGrouped == true) ? String(repeating: " ", count: parentCount - 1) : "" + return "\(filler)\(layerSymbol) \(layerName)" + } } extension SKTiledLayerObject { - /// Return a string representing the layer name. + /// String representing the layer name (null if not set). public var layerName: String { return self.name ?? "null" } @@ -2422,25 +2751,18 @@ extension SKTiledLayerObject { /// Returns an array of child layers. public var childLayers: [SKNode] { - var result: [SKNode] = [] - enumerateChildNodes(withName: "*") { node, _ in - - if let node = node as? SKTiledLayerObject { - result.append(node) - } - } - return result + return self.enumerate() } /** - Returns an array of tiles/objects. + Returns an array of tiles/objects that conform to the `SKTiledGeometry` protocol. - returns: `[SKNode]` array of child objects. */ public func renderableObjects() -> [SKNode] { var result: [SKNode] = [] enumerateChildNodes(withName: "*") { node, _ in - if (node as? SKTile != nil) || (node as? SKTileObject != nil) { + if (node as? SKTiledGeometry != nil) { result.append(node) } } @@ -2473,37 +2795,79 @@ extension SKTiledLayerObject { let parentNodes = self.parents let isGrouped: Bool = (parentNodes.count > 1) + let isGroupNode: Bool = (self as? SKGroupLayer != nil) let indexString = (isGrouped == true) ? String(repeating: " ", count: digitCount) : "\(index).".zfill(length: digitCount, pattern: " ") let typeString = self.layerType.stringValue.capitalized.zfill(length: 6, pattern: " ", padLeft: false) - let hasChildren: Bool = (childLayers.count > 0) + let hasChildren: Bool = (childLayers.isEmpty == false) - let layerSymbol = (hasChildren == true) ? "▿" : "-" + var layerSymbol: String = " " + if (isGroupNode == true) { + layerSymbol = (hasChildren == true) ? "▿" : "▹" + } let filler = (isGrouped == true) ? String(repeating: " ", count: parentNodes.count - 1) : "" let layerPathString = "\(filler)\(layerSymbol) \"\(layerName)\"" - let layerVisibilityString: String = (self.visible == true) ? "(x)" : "( )" + let layerVisibilityString: String = (self.isolated == true) ? "(i)" : (self.visible == true) ? "[x]" : "[ ]" // layer position string, filters out child layers with no offset var positionString = self.position.shortDescription if (self.position.x == 0) && (self.position.y == 0) { positionString = "" } - - let graphStat = (renderStatistics.gn != nil) ? "\(renderStatistics.gn!)" : "" + + let graphStat = (renderInfo.gn != nil) ? "\(renderInfo.gn!)" : "" return [indexString, typeString, layerVisibilityString, layerPathString, positionString, self.sizeInPoints.shortDescription, self.offset.shortDescription, self.anchorPoint.shortDescription, "\(Int(self.zPosition))", self.opacity.roundTo(2), graphStat] } + + /** + Recursively enumerate child nodes. + + - returns: `[SKNode]` child elements. + */ + internal func enumerate() -> [SKNode] { + var result: [SKNode] = [self] + for child in children { + if let node = child as? SKTiledLayerObject { + result += node.enumerate() + } + } + return result + } } + extension SKTiledLayerObject.TiledLayerType { /// Returns a string representation of the layer type. internal var stringValue: String { return "\(self)".lowercased() } + internal var symbol: String { + switch self { + case .tile: return "⊞" + case .object: return "⧉" + default: return "" + } + } } +extension SKTiledLayerObject.TiledLayerType: CustomStringConvertible, CustomDebugStringConvertible { + var description: String { + switch self { + case .none: return "none" + case .tile: return "tile" + case .object: return "object" + case .image: return "image" + case .group: return "group" + } + } + + var debugDescription: String { + return description + } +} extension Array2D: Sequence { @@ -2524,23 +2888,39 @@ extension Array2D: Sequence { } -extension Array2D: CustomReflectable { +extension Array2D: CustomReflectable, CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + let items = array.compactMap { $0 } + return "Array2D: \(items.count) items" + } + + public var debugDescription: String { + return description + } + public var customMirror: Mirror { var rowdata: [String] = [] + let colSize = 4 + for r in 0.. SKColor { } -extension SKTileLayer { - - public func flatten() { - guard let flattenedTexture = self.render() else { return } - let textureSize = flattenedTexture.size() - var parentGroup: SKGroupLayer? = nil - if let parent = parent, parent is SKGroupLayer { - parentGroup = parent as? SKGroupLayer - } +// MARK: - Deprecated - let imageLayer = self.tilemap.newImageLayer(named: "\(layerName)_FLATTENE", group: parentGroup) - imageLayer.setLayerTexture(texture: flattenedTexture) - imageLayer.offset = offset - imageLayer.zPosition = zPosition +extension SKTiledLayerObject { + @available(*, deprecated, renamed: "runAnimationAsActions") + /** + Initialize SpriteKit animation actions for the layer. + */ + public func runAnimationAsAction() { + self.runAnimationAsActions() + } +} - DispatchQueue.main.async { - self.removeFromParent() +extension SKObjectGroupDrawOrder: CustomStringConvertible, CustomDebugStringConvertible { + var description: String { + switch self { + case .manual: return "manual" + case .topdown: return "topdown" } } - + + var debugDescription: String { + return description + } } -// MARK: - Deprecated extension SKObjectGroup { /** @@ -2621,6 +3003,14 @@ extension SKObjectGroup { } return nil } + + /** + Render all of the objects in the group. + */ + @available(*, deprecated, renamed: "SKObjectGroup.draw()") + public func drawObjects() { + self.draw() + } } @@ -2635,4 +3025,3 @@ extension SKTileLayer { return self.getTiles() } } - diff --git a/Sources/SKTileObject.swift b/Sources/SKTileObject.swift index 0f1e2712..7121512b 100644 --- a/Sources/SKTileObject.swift +++ b/Sources/SKTileObject.swift @@ -9,49 +9,73 @@ import SpriteKit + +/// Generic protocol for renderable Tiled objects. +protocol SKTiledGeometry { + var visibleToCamera: Bool { get set } + func draw(debug: Bool) +} + + + /** ## Overview ## - Structure for managing basic font rendering attributes for [text objects][text-objects]. - + Structure for managing basic font rendering attributes for [**text objects**][text-objects-url]. ### Properties ### - - ```swift - TextObjectAttributes.fontName // font name. - TextObjectAttributes.fontSize // font size. - TextObjectAttributes.fontColor // font color. - TextObjectAttributes.alignment // horizontal/vertical alignment. - TextObjectAttributes.wrap // wrap text. - TextObjectAttributes.isBold // font is bold. - TextObjectAttributes.isItalic // font is italicized. - TextObjectAttributes.isUnderline // font is underlined. - TextObjectAttributes.renderQuality // font resolution. - ``` - - [text-objects]:../objects.html#text-objects + | Property | Description | + |---------------|-------------------------------------| + | fontName | Font name. | + | fontSize | Font size. | + | fontColor | Font color. | + | alignment | Horizontal/vertical text alignment. | + | wrap | Text wraps. | + | isBold | Text is bold. | + | isItalic | Text is italicized. | + | isunderline | Text is underlined. | + | renderQuality | Font scaling attribute. | + + [text-objects-url]:https://doc.mapeditor.org/en/stable/manual/objects/#insert-text */ public struct TextObjectAttributes { /// Font name. - public var fontName: String = "Arial" + public var fontName: String = "Arial" /// Font size. public var fontSize: CGFloat = 16 /// Font color. public var fontColor: SKColor = .black - /// Font alignment. + + /** + + ## Overview ## + + Structure describing text alignment. + + ### Properties ### + + | Property | Description | + |---------------|-------------------------------------| + | horizontal | Horizontal text alignment. | + | vertical | Vertical text alignment. | + + */ public struct TextAlignment { - var horizontal: HoriztonalAlignment = .left - var vertical: VerticalAlignment = .top + var horizontal: HoriztonalAlignment = HoriztonalAlignment.left + var vertical: VerticalAlignment = VerticalAlignment.top + + /// Horizontal text alignment. enum HoriztonalAlignment: String { case left case center case right } + /// Vertical text alignment. enum VerticalAlignment: String { case top case center @@ -69,7 +93,7 @@ public struct TextObjectAttributes { public var isUnderline: Bool = false public var isStrikeout: Bool = false /// Font scaling property. - public var renderQuality: CGFloat = 8 + public var renderQuality: CGFloat = TiledGlobals.default.renderQuality.text public init() {} @@ -83,16 +107,64 @@ public struct TextObjectAttributes { } } + /** ## Overview ## The `SKTileObject` class represents a Tiled vector object type (rectangle, ellipse, polygon & polyline). When the object is created, points can be added either with an array of `CGPoint` objects, or a string. In order to render the object, the `SKTileObject.getVertices()` method is called, which returns the points needed to draw the path. + ### Properties ### + + | Property | Description | + |----------|----------------------------------------------------------------------| + | id | Tiled object id. | + | size | Object size. | + | tileData | Tile data (for [tile objects][tile-objects-url]). | + | text | Text string (for text objects). Setting this redraws the object. | + | bounds | Returns the bounding box of the shape. | + + + [tile-objects-url]:http://docs.mapeditor.org/en/stable/manual/objects/#insert-tile + */ open class SKTileObject: SKShapeNode, SKTiledObject { - // Describes the object shape. - public enum ObjectType: String { + /** + ## Overview ## + + Describes the object type (tile object, text object, etc). + + ### Properties ### + + | Property | Description | + |-----------|----------------------------------------| + | none | Object is a simple vector object type. | + | text | Object is text object. | + | tile | Object is effectively a tile. | + + */ + public enum TiledObjectType: String { + case none + case text + case tile + } + + /** + ## Overview ## + + Describes a vector object shape. + + ### Properties ### + + | Property | Description | + |-----------|--------------------------------| + | rectangle | Rectangular object shape. | + | ellipse | Circular object shape. | + | polygon | Closed polygonal object shape. | + | polyline | Open polygonal object shape. | + + */ + public enum TiledObjectShape: String { case rectangle case ellipse case polygon @@ -109,20 +181,30 @@ open class SKTileObject: SKShapeNode, SKTiledObject { internal var gid: Int! /// Object type. open var type: String! - + /// Object size. open var size: CGSize = CGSize.zero + + /// Object is visible in camera. + open var visibleToCamera: Bool = true - internal var objectType: ObjectType = .rectangle // shape type - internal var points: [CGPoint] = [] // points that describe the object's shape + internal var objectType: TiledObjectType = TiledObjectType.none // object type + internal var shapeType: TiledObjectShape = TiledObjectShape.rectangle // shape type + internal var points: [CGPoint] = [] // points that describe the object's shape - /// Object keys + /// Object keys. internal var tileObjectKey: String = "TILE_OBJECT" internal var textObjectKey: String = "TEXT_OBJECT" internal var boundsKey: String = "BOUNDS" internal var anchorKey: String = "ANCHOR" - - internal var tile: SKTile? // optional tile + + /// Specialized properties. + internal var tile: SKTile? // optional tile + internal var template: String? // optional template reference + internal var isInitialized: Bool = true + + /// Proxy object. + weak internal var proxy: TileObjectProxy? /// Tile data (for tile objects). open var tileData: SKTilesetData? { @@ -130,12 +212,21 @@ open class SKTileObject: SKShapeNode, SKTiledObject { } /// Object bounds color. - open var frameColor: SKColor = TiledObjectColors.magenta - + open var frameColor: SKColor = TiledGlobals.default.debug.objectHighlightColor + /** ## Overview ## - Enum defining object collision type. + Describes tile vector object collision type. + + ### Properties ### + + | Property | Description | + |-----------|-----------------------------------| + | none | No physics collisions. | + | dynamic | Object is a dynamic physics body. | + | collision | Object records collisions only. | + */ public enum CollisionType { case none @@ -148,21 +239,21 @@ open class SKTileObject: SKShapeNode, SKTiledObject { open var ignoreProperties: Bool = false // ignore custom properties /// Physics collision type. open var physicsType: CollisionType = .none - + open var invertPhysics: Bool = false /// Text formatting attributes (for text objects) open var textAttributes: TextObjectAttributes! ///Text object render quality. - open var renderQuality: CGFloat = 8 { + open var renderQuality: CGFloat = TiledGlobals.default.renderQuality.object { didSet { guard (renderQuality != oldValue), - renderQuality <= 16 else { + (renderQuality <= 16) else { return } textAttributes?.renderQuality = renderQuality - drawObject() + draw() } } @@ -176,7 +267,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { open var text: String! { didSet { guard text != oldValue else { return } - drawObject() + draw() } } @@ -204,7 +295,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { /// Signifies that this object is a polygonal type. open var isPolyType: Bool { - return (objectType == .polygon) || (objectType == .polyline) + return (shapeType == .polygon) || (shapeType == .polyline) } override open var speed: CGFloat { @@ -220,9 +311,9 @@ open class SKTileObject: SKShapeNode, SKTiledObject { - parameter width: `CGFloat` object size width. - parameter height: `CGFloat` object size height. - - parameter type: `ObjectType` object shape type. + - parameter type: `TiledObjectShape` object shape type. */ - required public init(width: CGFloat, height: CGFloat, type: ObjectType = .rectangle) { + required public init(width: CGFloat, height: CGFloat, type: TiledObjectShape = .rectangle) { super.init() // Rectangular and ellipse objects get initial points. @@ -234,9 +325,9 @@ open class SKTileObject: SKShapeNode, SKTiledObject { ] } - self.objectType = type + self.shapeType = type self.size = CGSize(width: width, height: height) - drawObject() + draw() } /** @@ -256,6 +347,16 @@ open class SKTileObject: SKShapeNode, SKTiledObject { let startPosition = CGPoint(x: CGFloat(Double(xcoord)!), y: CGFloat(Double(ycoord)!)) position = startPosition + // pass the rest of the values to the setup method + setObjectAttributes(attributes: attributes) + } + + /** + Set initial object attributes. + + - parameter attributes: `[String: String]` object attributes. + */ + func setObjectAttributes(attributes: [String: String]) { if let objectName = attributes["name"] { self.name = objectName } @@ -284,25 +385,31 @@ open class SKTileObject: SKShapeNode, SKTiledObject { visible = (Int(objVis) == 1) ? true : false } + // Rectangular and ellipse objects need initial points. + var initialSize: CGSize = CGSize.zero if (width > 0) && (height > 0) { points = [CGPoint(x: 0, y: 0), CGPoint(x: width, y: 0), CGPoint(x: width, y: height), CGPoint(x: 0, y: height) - ] + ] + + initialSize = CGSize(width: width, height: height) } - self.size = CGSize(width: width, height: height) + self.size = initialSize // object rotation if let degreesValue = attributes["rotation"] { - if let doubleVal = Double(degreesValue) { let radiansValue = CGFloat(doubleVal).radians() self.zRotation = -radiansValue } } + + // optional template reference + template = attributes["template"] } /** @@ -313,7 +420,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { required public init(layer: SKObjectGroup) { super.init() _ = layer.addObject(self) - drawObject() + draw() } required public init?(coder aDecoder: NSCoder) { @@ -327,12 +434,16 @@ open class SKTileObject: SKShapeNode, SKTiledObject { - parameter color: `SKColor` fill & stroke color. - parameter alpha: `CGFloat` alpha component for fill. */ - open func setColor(color: SKColor, withAlpha alpha: CGFloat=0.35, redraw: Bool=true) { + open func setColor(color: SKColor, withAlpha alpha: CGFloat = 0.35, redraw: Bool = true) { self.strokeColor = color - if !(self.objectType == .polyline) && (self.gid == nil) { + if !(self.shapeType == .polyline) && (self.gid == nil) { self.fillColor = color.withAlphaComponent(alpha) } - if redraw == true { drawObject() } + // update proxy + proxy?.objectColor = color + proxy?.fillOpacity = alpha + + if redraw == true { draw() } } /** @@ -341,7 +452,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { - parameter color: `hexString` hex color string. - parameter alpha: `CGFloat` alpha component for fill. */ - open func setColor(hexString: String, withAlpha alpha: CGFloat=0.35, redraw: Bool=true) { + open func setColor(hexString: String, withAlpha alpha: CGFloat = 0.35, redraw: Bool = true) { self.setColor(color: SKColor(hexString: hexString), withAlpha: alpha, redraw: redraw) } @@ -350,17 +461,16 @@ open class SKTileObject: SKShapeNode, SKTiledObject { /** Render the object. */ - open func drawObject(debug: Bool = false) { + open func draw(debug: Bool = false) { guard let layer = layer, let vertices = getVertices(), points.count > 1 else { return } - - - let uiScale: CGFloat = SKTiledContentScaleFactor - - // polyline objects should have no fill - self.fillColor = (self.objectType == .polyline) ? SKColor.clear : self.fillColor + + + let uiScale: CGFloat = TiledGlobals.default.contentScale + self.strokeColor = SKColor.clear + self.fillColor = SKColor.clear self.isAntialiased = layer.antialiased self.lineJoin = .miter @@ -372,7 +482,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { // for some odd reason Tiled tile objects are flipped in the y-axis already, so ignore the translated var translatedVertices: [CGPoint] = (isPolyType == true) ? (gid == nil) ? vertices.map { $0.invertedY } : vertices : (gid == nil) ? vertices.map { $0.invertedY } : vertices - switch objectType { + switch shapeType { case .ellipse: var bezPoints: [CGPoint] = [] @@ -382,7 +492,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { bezPoints.append(lerp(start: point, end: translatedVertices[nextIndex], t: 0.5)) } - let bezierData = bezierPath(bezPoints, closed: true, alpha: 0.75) + let bezierData = bezierPath(bezPoints, closed: true, alpha: shapeType.curvature) self.path = bezierData.path //let controlPoints = bezierData.points @@ -399,7 +509,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { } default: - let closedPath: Bool = (self.objectType == .polyline) ? false : true + let closedPath: Bool = (self.shapeType == .polyline) ? false : true self.path = polygonPath(translatedVertices, closed: closedPath) } @@ -429,7 +539,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { log("Tile object \"\(name ?? "null")\" cannot access tile data for id: \(gid)", level: .error) return } - + self.objectType = .tile // in Tiled, tile data type overrides object type self.type = (tileData.type == nil) ? self.type : tileData.type! @@ -451,8 +561,12 @@ open class SKTileObject: SKShapeNode, SKTiledObject { if (tileData.texture != nil) { childNode(withName: tileObjectKey)?.removeFromParent() - if let tileSprite = SKTile(data: tileData) { - + + // get tile object from delegate + let Tile = (layer.tilemap.delegate != nil) ? layer.tilemap.delegate!.objectForTileType(named: tileData.type) : SKTile.self + + if let tileSprite = Tile.init(data: tileData) { + let boundingBox = polygonPath(translatedVertices) let rect = boundingBox.boundingBox @@ -467,6 +581,8 @@ open class SKTileObject: SKShapeNode, SKTiledObject { isAntialiased = false lineWidth = 0.75 + + // tile objects should have no color strokeColor = SKColor.clear fillColor = SKColor.clear @@ -476,6 +592,13 @@ open class SKTileObject: SKShapeNode, SKTiledObject { // flipped tile flags tileSprite.xScale = (tileData.flipHoriz == true) ? -1 : 1 tileSprite.yScale = (tileData.flipVert == true) ? -1 : 1 + + // add to tile cache + NotificationCenter.default.post( + name: Notification.Name.Layer.TileAdded, + object: tileSprite, + userInfo: ["layer": layer, "object": self] + ) } } } @@ -487,10 +610,12 @@ open class SKTileObject: SKShapeNode, SKTiledObject { textAttributes = TextObjectAttributes() } + self.objectType = .text + // remove the current text object childNode(withName: textObjectKey)?.removeFromParent() - - strokeColor = (debug == false) ? SKColor.clear : layer.gridColor.withAlphaComponent(0.75) + //strokeColor = (debug == false) ? SKColor.clear : layer.gridColor.withAlphaComponent(0.75) + strokeColor = SKColor.clear fillColor = SKColor.clear // render text to an image @@ -505,7 +630,6 @@ open class SKTileObject: SKShapeNode, SKTiledObject { textSprite.zPosition = zPosition - 1 textSprite.setScale(finalScaleValue) textSprite.position = self.bounds.center - } } } @@ -513,19 +637,19 @@ open class SKTileObject: SKShapeNode, SKTiledObject { /** Draw the text object. Scale factor is to allow for text to render clearly at higher zoom levels. - - parameter withScale: `CGFloat` size scale. + - parameter withScale: `CGFloat` render quality scaling. - returns: `CGImage` rendered text image. */ - open func drawTextObject(withScale: CGFloat=8) -> CGImage? { + open func drawTextObject(withScale: CGFloat = 8) -> CGImage? { - let uiScale: CGFloat = SKTiledContentScaleFactor + let uiScale: CGFloat = TiledGlobals.default.contentScale // the object's bounding rect let textRect = self.bounds let scaledRect = textRect * withScale - // need absolute size - let scaledRectSize = fabs(textRect.size) * withScale + // absolute size of the texture rectangle + let scaledRectSize: CGSize = fabs(textRect.size) * withScale return imageOfSize(scaledRectSize, scale: uiScale) { context, bounds, scale in context.saveGState() @@ -535,10 +659,10 @@ open class SKTileObject: SKShapeNode, SKTiledObject { // text block attributes textStyle.alignment = NSTextAlignment(rawValue: textAttributes.alignment.horizontal.intValue)! - let textFontAttributes: [String : Any] = [ - NSFontAttributeName: textAttributes.font, - NSForegroundColorAttributeName: textAttributes.fontColor, - NSParagraphStyleAttributeName: textStyle + let textFontAttributes: [NSAttributedString.Key : Any] = [ + NSAttributedString.Key.font: textAttributes.font, + NSAttributedString.Key.foregroundColor: textAttributes.fontColor, + NSAttributedString.Key.paragraphStyle: textStyle ] // setup vertical alignment @@ -548,14 +672,18 @@ open class SKTileObject: SKShapeNode, SKTiledObject { #else fontHeight = self.text!.boundingRect(with: CGSize(width: bounds.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes).height #endif + + // vertical alignment // center aligned... if (textAttributes.alignment.vertical == .center) { let adjustedRect: CGRect = CGRect(x: scaledRect.minX, y: scaledRect.minY + (scaledRect.height - fontHeight) / 2, width: scaledRect.width, height: fontHeight) #if os(macOS) - NSRectClip(textRect) + __NSRectClip(textRect) #endif - self.text!.draw(in: adjustedRect.offsetBy(dx: 0, dy: 2 * withScale), withAttributes: textFontAttributes) + + let offsetY = 2 * withScale + self.text!.draw(in: adjustedRect.offsetBy(dx: 0, dy: offsetY), withAttributes: textFontAttributes) // top aligned... } else if (textAttributes.alignment.vertical == .top) { @@ -566,7 +694,7 @@ open class SKTileObject: SKShapeNode, SKTiledObject { } else { let adjustedRect: CGRect = CGRect(x: scaledRect.minX, y: scaledRect.minY, width: scaledRect.width, height: fontHeight) #if os(macOS) - NSRectClip(textRect) + __NSRectClip(textRect) #endif self.text!.draw(in: adjustedRect.offsetBy(dx: 0, dy: 2 * withScale), withAttributes: textFontAttributes) } @@ -582,8 +710,8 @@ open class SKTileObject: SKShapeNode, SKTiledObject { - parameter points: `[[CGFloat]]` array of coordinates. - parameter closed: `Bool` close the object path. */ - internal func addPoints(_ coordinates: [[CGFloat]], closed: Bool=true) { - self.objectType = (closed == true) ? ObjectType.polygon : ObjectType.polyline + internal func addPoints(_ coordinates: [[CGFloat]], closed: Bool = true) { + self.shapeType = (closed == true) ? TiledObjectShape.polygon : TiledObjectShape.polyline // create an array of points from the given coordinates points = coordinates.map { CGPoint(x: $0[0], y: $0[1]) } } @@ -597,8 +725,8 @@ open class SKTileObject: SKShapeNode, SKTiledObject { var coordinates: [[CGFloat]] = [] let pointsArray = points.components(separatedBy: " ") for point in pointsArray { - let coords = point.components(separatedBy: ",").flatMap { x in Double(x) } - coordinates.append(coords.flatMap { CGFloat($0) }) + let coords = point.components(separatedBy: ",").compactMap { x in Double(x) } + coordinates.append(coords.compactMap { CGFloat($0) }) } addPoints(coordinates) } @@ -620,33 +748,59 @@ open class SKTileObject: SKShapeNode, SKTiledObject { return offset } } + + /** + Returns the translated points array, correctly orientated. + + - returns: `[CGPoint]?` array of points. + */ + internal func translatedVertices() -> [CGPoint]? { + guard let vertices = getVertices() else { return nil } + let translated = (isPolyType == true) ? (gid == nil) ? vertices.map { $0.invertedY } : vertices : (gid == nil) ? vertices.map { $0.invertedY } : vertices + + var result: [CGPoint] = [] + + if (shapeType == TiledObjectShape.ellipse) { + for (index, point) in translated.enumerated() { + let nextIndex = (index < translated.count - 1) ? index + 1 : 0 + result.append(lerp(start: point, end: translated[nextIndex], t: 0.5)) + } + } else { + result = translated + } + + return result + } /** Draw the object's bounding shape. - + - parameter withColor: `SKColor?` optional highlight color. - parameter zpos: `CGFloat?` optional z-position of bounds shape. - parameter duration: `TimeInterval` effect length. */ - internal func drawBounds(withColor: SKColor?=nil, zpos: CGFloat?=nil, duration: TimeInterval = 0) { - + internal func drawBounds(withColor: SKColor? = nil, zpos: CGFloat? = nil, duration: TimeInterval = 0) { + childNode(withName: boundsKey)?.removeFromParent() childNode(withName: "FIRST_POINT")?.removeFromParent() let tileHeight = (layer != nil) ? layer.tilemap.tileHeight : 8 + // smaller maps look better with thinner lines - let tileHeightDivisor: CGFloat = (tileHeight <= 16) ? 2 : 0.75 + var tileHeightDivisor: CGFloat = (tileHeight <= 16) ? 2 : 0.75 + + // if effects are on + tileHeightDivisor *= 2 // if a color is not passed, use the default frame color let drawColor = (withColor != nil) ? withColor! : self.frameColor // default line width - let defaultLineWidth: CGFloat = (self.layer != nil) ? self.layer.lineWidth * 3 : 4.5 guard let vertices = getVertices() else { return } let flippedVertices = (gid == nil) ? vertices.map { $0.invertedY } : vertices - let renderQuality = (layer != nil) ? layer!.renderQuality : 8 + let renderQuality = (layer != nil) ? layer!.renderQuality : 4 //let vertices = frame.points @@ -664,7 +818,8 @@ open class SKTileObject: SKShapeNode, SKTiledObject { bounds.lineCap = .round bounds.lineJoin = .miter bounds.miterLimit = 0 - bounds.lineWidth = (defaultLineWidth * (renderQuality / 2) / tileHeightDivisor) + + bounds.lineWidth = ( renderQuality / tileHeightDivisor ) bounds.strokeColor = drawColor.withAlphaComponent(0.4) bounds.fillColor = drawColor.withAlphaComponent(0.15) // 0.35 @@ -738,11 +893,11 @@ open class SKTileObject: SKShapeNode, SKTiledObject { } // MARK: - Callbacks - open func didBeginRendering(completion: (() -> ())? = nil) { + open func didBeginRendering(completion: (() -> Void)? = nil) { if completion != nil { completion!() } } - open func didFinishRendering(completion: (() -> ())? = nil) { + open func didFinishRendering(completion: (() -> Void)? = nil) { if completion != nil { completion!() } } @@ -752,12 +907,23 @@ open class SKTileObject: SKShapeNode, SKTiledObject { Setup physics for the object based on properties set up in Tiled. */ open func setupPhysics() { - guard let layer = layer else { return } + guard let layer = layer, + let vertices = getVertices() else { + return + } + guard let objectPath = path else { log("object path not set: \"\(self.name != nil ? self.name! : "null")\"", level: .warning) return } + var physicsPath: CGPath = objectPath + + // fix for flipped tile objects + let flippedVertices = (gid == nil) ? vertices.map { $0.invertedY } : vertices + let curvature: CGFloat = shapeType.curvature + let bezierData = bezierPath(flippedVertices, closed: true, alpha: curvature) + physicsPath = bezierData.path let tileSizeHalved = layer.tilemap.tileSizeHalved @@ -766,13 +932,13 @@ open class SKTileObject: SKShapeNode, SKTiledObject { case 0: physicsBody = SKPhysicsBody(rectangleOf: tileSizeHalved) case 1: - physicsBody = SKPhysicsBody(circleOfRadius: layer.tilemap.tileWidthHalf) + physicsBody = SKPhysicsBody(circleOfRadius: layer.tilemap.tileWidthHalf, center: physicsPath.boundingBox.center) default: - physicsBody = SKPhysicsBody(polygonFrom: objectPath) + physicsBody = SKPhysicsBody(polygonFrom: physicsPath) } } else { - physicsBody = SKPhysicsBody(polygonFrom: objectPath) + physicsBody = SKPhysicsBody(polygonFrom: physicsPath) } @@ -783,12 +949,8 @@ open class SKTileObject: SKShapeNode, SKTiledObject { physicsBody?.restitution = (doubleForKey("restitution") != nil) ? CGFloat(doubleForKey("restitution")!) : 0.4 // bounciness } - - // MARK: - Memory - internal func flush() { - self.path = nil - childNode(withName: tileObjectKey)?.removeFromParent() - childNode(withName: textObjectKey)?.removeFromParent() + open func getPhysicsPath() -> CGPath? { + return nil } // MARK: - Updating @@ -804,16 +966,49 @@ open class SKTileObject: SKShapeNode, SKTiledObject { } +// MARK: - Extensions + + +extension SKTileObject.TiledObjectType { + + /// Returns the name of the object. + var name: String { + switch self { + case .none: return "Object" + case .text: return "Text Object" + case .tile: return "Tile Object" + } + } +} + + +extension SKTileObject.TiledObjectShape { + + /// Returns the curvature value for drawing the object path. + var curvature: CGFloat { + switch self { + case .ellipse: return 0.75 // was 0.5 + default: return 0 + } + } +} + + extension SKTileObject { - override open var hashValue: Int { return id.hashValue } - /// Tile data description. + override open var hash: Int { return id.hashValue } + + /// Object description. override open var description: String { - let comma = propertiesString.characters.isEmpty == false ? ", " : "" - let objectName = name ?? "null" + let comma = propertiesString.isEmpty == false ? ", " : "" + var objectName = "" + if let name = name { + objectName = ", \"\(name)\"" + } let typeString = (type != nil) ? ", type: \"\(type!)\"" : "" + let miscDesc = (objectType == .text) ? ", text quality: \(renderQuality)" : (objectType == .tile) ? ", tile id: \(gid ?? 0)" : "" let layerDescription = (layer != nil) ? ", Layer: \"\(layer.layerName)\"" : "" - return "Object ID: \(id), \"\(objectName)\"\(typeString)\(comma)\(propertiesString)\(layerDescription)" + return "\(objectType.name) id: \(id)\(objectName)\(typeString)\(miscDesc)\(comma)\(propertiesString)\(layerDescription)" } override open var debugDescription: String { @@ -821,7 +1016,7 @@ extension SKTileObject { } open var shortDescription: String { - var result = "Object id: \(self.id)" + var result = "\(objectType.name) id: \(self.id)" result += (self.type != nil) ? ", type: \"\(self.type!)\"" : "" return result } @@ -870,6 +1065,43 @@ extension SKTileObject { } + +extension SKTileObject: Loggable {} +extension SKTileObject: SKTiledGeometry {} + + +extension SKTileObject.TiledObjectType: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch self { + case .none: return "none" + case .text: return "text" + case .tile: return "tile" + } + } + + public var debugDescription: String { + return description + } +} + + + +extension SKTileObject.TiledObjectShape: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch self { + case .rectangle: return "rectangle" + case .ellipse: return "ellipse" + case .polygon: return "polygon" + case .polyline: return "polyline" + } + } + + public var debugDescription: String { + return description + } +} + + extension TextObjectAttributes { #if os(iOS) || os(tvOS) public var font: UIFont { @@ -944,6 +1176,8 @@ extension TextObjectAttributes.TextAlignment.VerticalAlignment { #endif } + + // MARK: - Deprecated extension SKTileObject { diff --git a/Sources/SKTiled+Debug.swift b/Sources/SKTiled+Debug.swift index 9332191b..506c9427 100644 --- a/Sources/SKTiled+Debug.swift +++ b/Sources/SKTiled+Debug.swift @@ -11,21 +11,33 @@ import SpriteKit /** - + ## Overview ## - + A structure representing debug drawing options for **SKTiled** objects. - - ## Properties ## - - ``` - DebugDrawOptions.drawGrid // visualize the objects's grid (tilemap & layers). - DebugDrawOptions.drawBounds // visualize the objects's bounds. - DebugDrawOptions.drawGraph // visualize a layer's navigation graph. - DebugDrawOptions.drawObjectBounds // draw an object's bounds. - DebugDrawOptions.drawTileBounds // draw a tile's bounds. + + ### Usage ### + + ```swift + // show the map's grid & bounds shape + tilemap.debugDrawOptions = [.drawGrid, .drawBounds] + + // turn off layer grid visibility + layer.debugDrawOptions.remove(.drawGrid) ``` - + + ### Properties ### + + | Property | Description | + |:-----------------|:-----------------------------------------| + | drawGrid | Draw the layer's tile grid. | + | drawBounds | Draw the layer's boundary. | + | drawGraph | Visualize the layer's pathfinding graph. | + | drawObjectBounds | Draw vector object bounds. | + | drawTileBounds | Draw tile boundary shapes. | + | drawBackground | Draw the layer's background color. | + | drawAnchor | Draw the layer's anchor point. | + */ public struct DebugDrawOptions: OptionSet { public let rawValue: Int @@ -34,23 +46,296 @@ public struct DebugDrawOptions: OptionSet { self.rawValue = rawValue } - /// Draw the layer's grid. - static public let drawGrid = DebugDrawOptions(rawValue: 1 << 0) - /// Draw the layer's boundary shape. - static public let drawBounds = DebugDrawOptions(rawValue: 1 << 1) - /// Draw the layer's navigation graph. - static public let drawGraph = DebugDrawOptions(rawValue: 1 << 2) - /// Draw object bounds. - static public let drawObjectBounds = DebugDrawOptions(rawValue: 1 << 3) - /// Draw tile bounds. - static public let drawTileBounds = DebugDrawOptions(rawValue: 1 << 4) - static public let drawMouseOverObject = DebugDrawOptions(rawValue: 1 << 5) - static public let drawBackground = DebugDrawOptions(rawValue: 1 << 6) - static public let drawAnchor = DebugDrawOptions(rawValue: 1 << 7) + static public let drawGrid = DebugDrawOptions(rawValue: 1 << 0) // 1 + static public let drawBounds = DebugDrawOptions(rawValue: 1 << 1) // 2 + static public let drawGraph = DebugDrawOptions(rawValue: 1 << 2) // 4 + static public let drawObjectBounds = DebugDrawOptions(rawValue: 1 << 3) // 8 + static public let drawTileBounds = DebugDrawOptions(rawValue: 1 << 4) // 16 + static public let drawMouseOverObject = DebugDrawOptions(rawValue: 1 << 5) // 32 + static public let drawBackground = DebugDrawOptions(rawValue: 1 << 6) // 64 + static public let drawAnchor = DebugDrawOptions(rawValue: 1 << 7) // 128 static public let all: DebugDrawOptions = [.drawGrid, .drawBounds, .drawGraph, .drawObjectBounds, - .drawObjectBounds, .drawMouseOverObject, - .drawBackground, .drawAnchor] + .drawObjectBounds, .drawMouseOverObject, + .drawBackground, .drawAnchor] +} + + +// MARK: - SKTilemap Extensions + + +extension SKTilemap { + + /** + Draw the map bounds. + + - parameter withColor: `SKColor?` optional highlight color. + - parameter zpos: `CGFloat?` optional z-position of bounds shape. + - parameter duration: `TimeInterval` effect length. + */ + internal func drawBounds(withColor: SKColor? = nil, zpos: CGFloat? = nil, duration: TimeInterval = 0) { + // remove old nodes + self.childNode(withName: "MAP_BOUNDS")?.removeFromParent() + self.childNode(withName: "MAP_ANCHOR")?.removeFromParent() + + // if a color is not passed, use the default frame color + let drawColor = (withColor != nil) ? withColor! : self.frameColor + + + let debugZPos = lastZPosition * 50 + + let scaledVertices = getVertices().map { $0 * renderQuality } + let tilemapPath = polygonPath(scaledVertices) + + + let boundsShape = SKShapeNode(path: tilemapPath) // , centered: true) + boundsShape.name = "MAP_BOUNDS" + boundsShape.fillColor = drawColor.withAlphaComponent(0.2) + boundsShape.strokeColor = drawColor + self.addChild(boundsShape) + + + boundsShape.isAntialiased = true + boundsShape.lineCap = .round + boundsShape.lineJoin = .miter + boundsShape.miterLimit = 0 + boundsShape.lineWidth = 1 * (renderQuality / 2) + boundsShape.setScale(1 / renderQuality) + + let anchorRadius = self.tileHeightHalf / 4 + let anchorShape = SKShapeNode(circleOfRadius: anchorRadius * renderQuality) + anchorShape.name = "MAP_ANCHOR" + anchorShape.fillColor = drawColor.withAlphaComponent(0.25) + anchorShape.strokeColor = .clear + boundsShape.addChild(anchorShape) + boundsShape.zPosition = debugZPos + + if (duration > 0) { + let fadeAction = SKAction.fadeAfter(wait: duration, alpha: 0) + boundsShape.run(fadeAction, withKey: "MAP_FADEOUT_ACTION", completion: { + boundsShape.removeFromParent() + }) + } + } +} + + + +/// Anchor point visualization. +internal class AnchorNode: SKNode { + + var radius: CGFloat = 0 + var color: SKColor = SKColor.clear + var labelText = "Anchor" + var labelSize: CGFloat = 18.0 + var renderQuality: CGFloat = TiledGlobals.default.renderQuality.default + + var labelOffsetX: CGFloat = 0 + var labelOffsetY: CGFloat = 0 + + var receiveCameraUpdates: Bool = true + + private var shapeKey = "ANCHOR_SHAPE" + private var labelKey = "ANCHOR_LABEL" + + var sceneScale: CGFloat = 1 + + private var shape: SKShapeNode? { + return childNode(withName: shapeKey) as? SKShapeNode + } + private var label: SKLabelNode? { + return childNode(withName: labelKey) as? SKLabelNode + } + + init(radius: CGFloat, color shapeColor: SKColor, label text: String? = nil, offsetX: CGFloat = 0, offsetY: CGFloat = 0, zoom: CGFloat = 1) { + self.radius = radius + self.color = shapeColor + self.labelOffsetX = offsetX + self.labelOffsetY = offsetY + self.sceneScale = zoom + super.init() + self.labelText = text ?? "" + self.name = "ANCHOR" + self.draw() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func draw() { + shape?.removeFromParent() + label?.removeFromParent() + + + //let sceneScaleInverted = (sceneScale > 1) ? abs(1 - sceneScale) : sceneScale + let scaledRenderQuality = renderQuality * sceneScale + + let minRadius: CGFloat = 4.0 + let maxRadius: CGFloat = 8.0 + var zoomedRadius = (radius / sceneScale) + + // clamp the anchor radius to min/max values + zoomedRadius = (zoomedRadius > maxRadius) ? maxRadius : (zoomedRadius < minRadius) ? minRadius : zoomedRadius + + // debugging + //let clampedString = (isClampedAtMin == true || isClampedAtMax == true) ? " (clamped)" : "" + //let outputString = " - radius: \(zoomedRadius.roundTo(1)) -> \(radius.roundTo())" + + let scaledFontSize = (labelSize * renderQuality) * sceneScale + let scaledOffsetX = (labelOffsetX / sceneScale) + let scaledOffsetY = (labelOffsetY / sceneScale) + + let anchor = SKShapeNode(circleOfRadius: zoomedRadius) + anchor.name = shapeKey + addChild(anchor) + anchor.fillColor = color + anchor.strokeColor = SKColor.clear + anchor.zPosition = parent?.zPosition ?? 100 + + // label + let nameLabel = SKLabelNode(fontNamed: "Courier") + nameLabel.name = labelKey + nameLabel.text = labelText + nameLabel.fontSize = scaledFontSize + anchor.addChild(nameLabel) + nameLabel.zPosition = anchor.zPosition + 1 + nameLabel.position.x += scaledOffsetX + nameLabel.position.y += scaledOffsetY + nameLabel.setScale(1.0 / scaledRenderQuality) + nameLabel.color = .white + } +} + + +extension AnchorNode: SKTiledSceneCameraDelegate { + + func cameraZoomChanged(newZoom: CGFloat) { + if (newZoom != sceneScale) { + sceneScale = newZoom + draw() + } + } +} + + +/// Vector object proxy container overlay. +internal class TileObjectOverlay: SKNode { + + var initialized: Bool = false + var cameraZoom: CGFloat = 1.0 + + override init() { + super.init() + } + + required init?(coder aDecoder: NSCoder) { + super.init() + } +} + + +/// Vector object proxy. +internal class TileObjectProxy: SKShapeNode, SKTiledGeometry { + + weak var container: TileObjectOverlay? + weak var reference: SKTileObject? + + var visibleToCamera: Bool = false + var isRenderable: Bool = false + var animationKey: String = "proxy" + + var showObjects: Bool = false { + didSet { + self.draw() + } + } + + var objectColor = TiledGlobals.default.debug.objectHighlightColor { + didSet { + self.draw() + } + } + + var fillOpacity = TiledGlobals.default.debug.objectFillOpacity { + didSet { + self.draw() + } + } + + var isFocused: Bool = false { + didSet { + guard (oldValue != isFocused) else { return } + removeAction(forKey: animationKey) + if (isFocused == false) && (showObjects == false) { + let fadeAction = SKAction.colorFadeAction(after: 0.5) + self.run(fadeAction, withKey: animationKey) + } else { + self.draw() + } + } + } + + required init(object: SKTileObject, visible: Bool = false, renderable: Bool = false) { + self.reference = object + super.init() + self.animationKey = "highlight-proxy-\(object.id)" + self.name = "proxy-\(object.id)" + object.proxy = self + showObjects = visible + isRenderable = renderable + } + + required init?(coder aDecoder: NSCoder) { + super.init() + } + + func draw(debug: Bool = false) { + + let showFocused = TiledGlobals.default.debug.mouseFilters.contains(.objectsUnderCursor) + let proxyIsVisible = (showObjects == true) || (isFocused == true && showFocused == true) + + self.removeAction(forKey: self.animationKey) + guard let object = reference, + let vertices = object.translatedVertices() else { + self.path = nil + return + } + + // reset scale + self.setScale(1) + + let convertedPoints = vertices.map { + self.convert($0, from: object) + } + + let renderQuality = TiledGlobals.default.renderQuality.object + let objectRenderQuality = renderQuality / 2 + + if (convertedPoints.isEmpty == false) { + + let scaledVertices = convertedPoints.map { $0 * renderQuality } + + let objectPath: CGPath + switch object.shapeType { + case .ellipse: + objectPath = bezierPath(scaledVertices, closed: true, alpha: object.shapeType.curvature).path + default: + objectPath = polygonPath(scaledVertices, closed: true) + } + + self.path = objectPath + self.setScale(1 / renderQuality) + + + let currentStrokeColor = (proxyIsVisible == true) ? self.objectColor : SKColor.clear + let currentFillColor = (proxyIsVisible == true) ? (isRenderable == false) ? currentStrokeColor.withAlphaComponent(fillOpacity) : SKColor.clear : SKColor.clear + + self.strokeColor = currentStrokeColor + self.fillColor = currentFillColor + self.lineWidth = objectRenderQuality + } + } } @@ -58,6 +343,7 @@ public struct DebugDrawOptions: OptionSet { internal class SKTiledDebugDrawNode: SKNode { private var layer: SKTiledLayerObject // parent layer + private var isDefault: Bool = false // is the tilemap default layer private var gridSprite: SKSpriteNode! private var graphSprite: SKSpriteNode! @@ -67,8 +353,9 @@ internal class SKTiledDebugDrawNode: SKNode { private var graphTexture: SKTexture? // GKGridGraph texture private var anchorKey: String = "ANCHOR" - init(tileLayer: SKTiledLayerObject) { + init(tileLayer: SKTiledLayerObject, isDefault def: Bool = false) { layer = tileLayer + isDefault = def anchorKey = "ANCHOR_\(layer.uuid)" super.init() setup() @@ -89,87 +376,72 @@ internal class SKTiledDebugDrawNode: SKNode { /// Debug visualization options. var debugDrawOptions: DebugDrawOptions { - return layer.debugDrawOptions - } - - var showGrid: Bool { - get { - return (gridSprite != nil) ? (gridSprite!.isHidden == false) : false - } set { - DispatchQueue.main.async { - self.drawGrid() - } - } - } - - var showBounds: Bool { - get { - return (frameShape != nil) ? (frameShape!.isHidden == false) : false - } set { - drawBounds() - } - } - - var showGraph: Bool { - get { - return (graphSprite != nil) ? (graphSprite!.isHidden == false) : false - } set { - DispatchQueue.main.async { - self.drawGraph() - } - } + return (isDefault == true) ? layer.tilemap.debugDrawOptions : layer.debugDrawOptions } /** Align with the parent layer. */ func setup() { + let nodeName = (isDefault == true) ? "MAP_DEBUG_DRAW" : "\(layer.layerName.uppercased())_DEBUG_DRAW" + name = nodeName + // set the anchorpoints to 0,0 to match the frame gridSprite = SKSpriteNode(texture: nil, color: .clear, size: layer.sizeInPoints) - gridSprite.anchorPoint = .zero + gridSprite.anchorPoint = CGPoint.zero addChild(gridSprite!) graphSprite = SKSpriteNode(texture: nil, color: .clear, size: layer.sizeInPoints) - graphSprite.anchorPoint = .zero + graphSprite.anchorPoint = CGPoint.zero addChild(graphSprite!) frameShape = SKShapeNode() addChild(frameShape!) + //updateZPosition() + } - //isHidden = true + func updateZPosition() { + let tilemap = layer.tilemap + let zDeltaValue: CGFloat = tilemap.zDeltaForLayers // z-position values - graphSprite!.zPosition = layer.zPosition + layer.tilemap.zDeltaForLayers - gridSprite!.zPosition = layer.zPosition + (layer.tilemap.zDeltaForLayers + 10) - frameShape!.zPosition = layer.zPosition + (layer.tilemap.zDeltaForLayers + 20) + let startZposition = (isDefault == true) ? (tilemap.lastZPosition + zDeltaValue) : layer.zPosition + + graphSprite!.zPosition = startZposition + zDeltaValue + gridSprite!.zPosition = startZposition + (zDeltaValue + 10) + frameShape!.zPosition = startZposition + (zDeltaValue + 20) } /** Update the node with the various options. */ func draw() { - if self.debugDrawOptions.contains(.drawGrid) { - self.drawGrid() - } else { - self.gridSprite?.isHidden = true - } + DispatchQueue.main.async { + self.isHidden = self.debugDrawOptions.isEmpty + if self.debugDrawOptions.contains(.drawGrid) { + self.drawGrid() + } else { + self.gridSprite?.isHidden = true + } - if self.debugDrawOptions.contains(.drawBounds) { - self.drawBounds() - } else { - self.frameShape?.isHidden = true - } + if self.debugDrawOptions.contains(.drawBounds) { + self.drawBounds() + } else { + self.frameShape?.isHidden = true + } - if self.debugDrawOptions.contains(.drawGraph) { - self.drawGraph() - } else { - self.graphSprite?.isHidden = true - } + if self.debugDrawOptions.contains(.drawGraph) { + self.drawGraph() + } else { + self.graphSprite?.isHidden = true + } - if self.debugDrawOptions.contains(.drawAnchor) { - self.drawAnchor() - } else { - childNode(withName: anchorKey)?.removeFromParent() + if self.debugDrawOptions.contains(.drawAnchor) { + self.drawLayerAnchor() + } else { + self.childNode(withName: self.anchorKey)?.removeFromParent() + } + self.updateZPosition() } } @@ -186,7 +458,6 @@ internal class SKTiledDebugDrawNode: SKNode { Visualize the layer's boundary shape. */ func drawBounds() { - let objectPath: CGPath! // grab dimensions from the layer @@ -227,6 +498,8 @@ internal class SKTiledDebugDrawNode: SKNode { // don't draw bounds of hexagonal maps frameShape.strokeColor = layer.frameColor + frameShape.alpha = layer.gridOpacity * 3 + if (layer.orientation == .hexagonal) { frameShape.strokeColor = SKColor.clear } @@ -240,7 +513,6 @@ internal class SKTiledDebugDrawNode: SKNode { /// Display the current tile grid. func drawGrid() { - if (gridTexture == nil) { gridSprite.isHidden = true @@ -250,15 +522,16 @@ internal class SKTiledDebugDrawNode: SKNode { var gridSize = CGSize.zero // scale factor for texture - let uiScale: CGFloat = SKTiledContentScaleFactor + let uiScale: CGFloat = TiledGlobals.default.contentScale // multipliers used to generate smooth lines let imageScale: CGFloat = layer.tilemap.renderQuality - let lineScale: CGFloat = (layer.tilemap.tileHeightHalf > 8) ? 1 : 0.75 // 2:1 + + // line scale should be a multiple of 1 + let lineScale: CGFloat = (layer.tilemap.tileHeightHalf > 8) ? 2 : (layer.tilemap.tileHeightHalf > 4) ? 1 : 0.75 // generate the texture if let gridImage = drawLayerGrid(self.layer, imageScale: imageScale, lineScale: lineScale) { - gridTexture = SKTexture(cgImage: gridImage) gridTexture?.filteringMode = .linear @@ -266,12 +539,12 @@ internal class SKTiledDebugDrawNode: SKNode { let spriteScaleFactor: CGFloat = (1 / imageScale) gridSize = (gridTexture != nil) ? gridTexture!.size() / uiScale : .zero gridSprite.setScale(spriteScaleFactor) - - Logger.default.log("grid texture size: \(gridSize.shortDescription), bpc: \(gridImage.bitsPerComponent), scale: \(imageScale)", level: .debug) + Logger.default.log("grid texture size: \(gridSize.shortDescription), bpc: \(gridImage.bitsPerComponent), line scale: \(lineScale), scale: \(imageScale), content scale: \(uiScale)", level: .debug, symbol: "SKTiledDebugDrawNode") gridSprite.texture = gridTexture gridSprite.alpha = layer.gridOpacity gridSprite.size = gridSize / imageScale + gridSprite.zPosition = zPosition * 3 // need to flip the grid texture in y // currently not doing this to the parent node so that objects will draw correctly. @@ -280,6 +553,8 @@ internal class SKTiledDebugDrawNode: SKNode { #else gridSprite.yScale *= -1 #endif + } else { + self.log("error drawing layer grid.", level: .error) } } gridSprite.isHidden = false @@ -298,7 +573,7 @@ internal class SKTiledDebugDrawNode: SKNode { var graphSize = CGSize.zero // scale factor for texture - let uiScale: CGFloat = SKTiledContentScaleFactor + let uiScale: CGFloat = TiledGlobals.default.contentScale // multipliers used to generate smooth lines let imageScale: CGFloat = layer.tilemap.renderQuality @@ -322,6 +597,7 @@ internal class SKTiledDebugDrawNode: SKNode { graphSprite.texture = graphTexture graphSprite.alpha = layer.gridOpacity * 3 graphSprite.size = graphSize / imageScale + graphSprite.zPosition = zPosition * 3 // need to flip the graph texture in y // currently not doing this to the parent node so that objects will draw correctly. @@ -339,15 +615,9 @@ internal class SKTiledDebugDrawNode: SKNode { /** Visualize the layer's anchor point. */ - func drawAnchor() { - childNode(withName: anchorKey)?.removeFromParent() - - let anchor = SKShapeNode(circleOfRadius: 0.75) + func drawLayerAnchor() { + let anchor = drawAnchor(self, withKey: anchorKey) anchor.name = anchorKey - anchor.strokeColor = .clear - anchor.zPosition = zPosition * 4 - - addChild(anchor) anchor.position = anchorPoint } @@ -365,296 +635,6 @@ internal class SKTiledDebugDrawNode: SKNode { } -// Shape node used for highlighting and placing tiles. -public class TileShape: SKShapeNode { - - public enum DebugRole: Int { - case none - case highlight - case coordinate - case pathfinding - } - - - var tileSize: CGSize - var orientation: SKTilemap.TilemapOrientation = .orthogonal - var color: SKColor - var layer: SKTiledLayerObject - var coord: CGPoint - - var weight: Float = 1 - var role: DebugRole = .none - var useLabel: Bool = false - - var initialized: Bool = false - var interactions: Int = 0 - - var renderQuality: CGFloat = 4 - var zoomFactor: CGFloat { - return layer.tilemap.currentZoom - } - - /** - Initialize with parent layer reference, and coordinate. - - - parameter layer: `SKTiledLayerObject` parent layer. - - parameter coord `CGPoint` tile coordinate. - - parameter tileColor: `SKColor` shape color. - - parameter withLabel: `Bool` render shape with label. - */ - init(layer: SKTiledLayerObject, coord: CGPoint, tileColor: SKColor, role: DebugRole = .none, weight: Float = 1) { - self.layer = layer - self.coord = coord - self.tileSize = layer.tileSize - self.color = tileColor - self.role = role - self.weight = weight - self.useLabel = (self.role == .coordinate) - super.init() - self.orientation = layer.orientation - drawObject() - } - - /** - Initialize with parent layer reference and color. - - - parameter layer: `SKTiledLayerObject` parent layer. - - parameter tileColor: `SKColor` shape color. - - parameter withLabel: `Bool` render shape with label. - */ - public init(layer: SKTiledLayerObject, tileColor: SKColor, role: DebugRole = .none) { - self.layer = layer - self.coord = CGPoint.zero - self.tileSize = layer.tileSize - self.color = tileColor - self.role = role - self.useLabel = (self.role == .coordinate) - super.init() - self.orientation = layer.orientation - drawObject() - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - /** - Run an action that removes the node after a set duration. - */ - public func cleanup() { - let fadeAction = SKAction.fadeAlpha(to: 0, duration: 0.1) - run(fadeAction, completion: { self.removeFromParent()}) - } - - /** - Draw the object. - */ - internal func drawObject() { - // draw the path - var points: [CGPoint] = [] - - let scaledTilesize: CGSize = (tileSize * renderQuality) - let halfWidth: CGFloat = (tileSize.width / 2) * renderQuality - let halfHeight: CGFloat = (tileSize.height / 2) * renderQuality - let tileWidth: CGFloat = (tileSize.width * renderQuality) - let tileHeight: CGFloat = (tileSize.height * renderQuality) - - let tileSizeHalved = CGSize(width: halfWidth, height: halfHeight) - - switch orientation { - case .orthogonal: - let origin = CGPoint(x: -halfWidth, y: halfHeight) - points = rectPointArray(scaledTilesize, origin: origin) - - case .isometric, .staggered: - points = polygonPointArray(4, radius: tileSizeHalved) - - case .hexagonal: - var hexPoints = Array(repeating: CGPoint.zero, count: 6) - let staggerX = layer.tilemap.staggerX - let sideLengthX = layer.tilemap.sideLengthX * renderQuality - let sideLengthY = layer.tilemap.sideLengthY * renderQuality - var variableSize: CGFloat = 0 - - // flat - if (staggerX == true) { - let r = (tileWidth - sideLengthX) / 2 - let h = tileHeight / 2 - variableSize = tileWidth - (r * 2) - hexPoints[0] = CGPoint(x: position.x - (variableSize / 2), y: position.y + h) - hexPoints[1] = CGPoint(x: position.x + (variableSize / 2), y: position.y + h) - hexPoints[2] = CGPoint(x: position.x + (tileWidth / 2), y: position.y) - hexPoints[3] = CGPoint(x: position.x + (variableSize / 2), y: position.y - h) - hexPoints[4] = CGPoint(x: position.x - (variableSize / 2), y: position.y - h) - hexPoints[5] = CGPoint(x: position.x - (tileWidth / 2), y: position.y) - } else { - //let r = tileWidth / 2 - let h = (tileHeight - sideLengthY) / 2 - variableSize = tileHeight - (h * 2) - hexPoints[0] = CGPoint(x: position.x, y: position.y + (tileHeight / 2)) - hexPoints[1] = CGPoint(x: position.x + (tileWidth / 2), y: position.y + (variableSize / 2)) - hexPoints[2] = CGPoint(x: position.x + (tileWidth / 2), y: position.y - (variableSize / 2)) - hexPoints[3] = CGPoint(x: position.x, y: position.y - (tileHeight / 2)) - hexPoints[4] = CGPoint(x: position.x - (tileWidth / 2), y: position.y - (variableSize / 2)) - hexPoints[5] = CGPoint(x: position.x - (tileWidth / 2), y: position.y + (variableSize / 2)) - } - - points = hexPoints.map { $0.invertedY } - } - - // draw the path - self.path = polygonPath(points) - self.isAntialiased = layer.antialiased - self.lineJoin = .miter - self.miterLimit = 0 - self.lineWidth = 1 - - var baseOpacity = layer.gridOpacity - - switch self.role { - case .pathfinding: - var baseColor = SKColor.gray - - switch weight { - case (-2000)...(-1): - baseColor = TiledObjectColors.lime - case 0...10: - baseColor = SKColor.gray - case 11...200: - baseColor = TiledObjectColors.dandelion - case 201...Float.greatestFiniteMagnitude: - baseColor = TiledObjectColors.english - default: - baseColor = SKColor.gray - } - - baseOpacity = 0.8 - self.strokeColor = baseColor.withAlphaComponent(baseOpacity * 2) - self.fillColor = baseColor.withAlphaComponent(baseOpacity * 1.5) - - - default: - self.strokeColor = SKColor.clear - self.fillColor = self.color.withAlphaComponent(baseOpacity * 1.5) - } - - - // anchor - childNode(withName: "ANCHOR")?.removeFromParent() - let anchorRadius: CGFloat = (tileSize.halfHeight / 8) * renderQuality - let anchor = SKShapeNode(circleOfRadius: anchorRadius) - anchor.name = "ANCHOR" - addChild(anchor) - anchor.fillColor = self.color.withAlphaComponent(0.05) - anchor.strokeColor = SKColor.clear - anchor.zPosition = zPosition + 10 - anchor.isAntialiased = layer.antialiased - - - - // coordinate label - childNode(withName: "COORDINATE")?.removeFromParent() - if (useLabel == true) { - let label = SKLabelNode(fontNamed: "Courier") - label.name = "COORDINATE" - label.fontSize = anchorRadius * renderQuality - label.text = "\(Int(coord.x)),\(Int(coord.y))" - addChild(label) - label.zPosition = anchor.zPosition + 10 - } - - setScale(1 / renderQuality) - } -} - - - -extension TileShape { - override public var description: String { - return "Tile Shape: \(coord.shortDescription)" - } - override public var debugDescription: String { return description } - override public var hashValue: Int { return coord.hashValue } -} - - - -internal func == (lhs: TileShape, rhs: TileShape) -> Bool { - return lhs.coord.hashValue == rhs.coord.hashValue -} - - -// MARK: - SKTilemap -extension SKTilemap { - - /** - Return tiles & objects at the given point in the map. - - - parameter point: `CGPoint` position in tilemap. - - returns: `[SKNode]` array of tiles. - */ - public func renderableObjectsAt(point: CGPoint) -> [SKNode] { - let pixelPosition = defaultLayer.screenToPixelCoords(point) - return nodes(at: pixelPosition).filter { node in - (node as? SKTile != nil) || (node as? SKTileObject != nil) - } - } - - /** - Draw the map bounds. - - - parameter withColor: `SKColor?` optional highlight color. - - parameter zpos: `CGFloat?` optional z-position of bounds shape. - - parameter duration: `TimeInterval` effect length. - */ - internal func drawBounds(withColor: SKColor?=nil, zpos: CGFloat?=nil, duration: TimeInterval = 0) { - // remove old nodes - self.childNode(withName: "MAP_BOUNDS")?.removeFromParent() - self.childNode(withName: "MAP_ANCHOR")?.removeFromParent() - - // if a color is not passed, use the default frame color - let drawColor = (withColor != nil) ? withColor! : self.frameColor - - - let debugZPos = lastZPosition * 50 - - let scaledVertices = getVertices().map { $0 * renderQuality } - let tilemapPath = polygonPath(scaledVertices) - - - let boundsShape = SKShapeNode(path: tilemapPath) // , centered: true) - boundsShape.name = "MAP_BOUNDS" - boundsShape.fillColor = drawColor.withAlphaComponent(0.2) - boundsShape.strokeColor = drawColor - self.addChild(boundsShape) - - - boundsShape.isAntialiased = true - boundsShape.lineCap = .round - boundsShape.lineJoin = .miter - boundsShape.miterLimit = 0 - boundsShape.lineWidth = 1 * (renderQuality / 2) - - boundsShape.setScale(1 / renderQuality) - - let anchorRadius = self.tileHeightHalf / 4 - let anchorShape = SKShapeNode(circleOfRadius: anchorRadius * renderQuality) - anchorShape.name = "MAP_ANCHOR" - anchorShape.fillColor = drawColor.withAlphaComponent(0.25) - anchorShape.strokeColor = .clear - boundsShape.addChild(anchorShape) - boundsShape.zPosition = debugZPos - - if (duration > 0) { - let fadeAction = SKAction.fadeAfter(wait: duration, alpha: 0) - boundsShape.run(fadeAction, withKey: "MAP_FADEOUT_ACTION", completion: { - boundsShape.removeFromParent() - }) - } - } -} - - // MARK: - Logging @@ -673,7 +653,7 @@ public enum LoggingLevel: Int { // Log event -public struct LogEvent: Hashable { +struct LogEvent: Hashable { var message: String let level: LoggingLevel let uuid: String = UUID().uuidString @@ -681,50 +661,77 @@ public struct LogEvent: Hashable { var symbol: String? let date = Date() - let file: String = #file + let file: String = #file let method: String = #function - let line: UInt = #line - let column: UInt = #column + let line: UInt = #line + let column: UInt = #column - public init(_ message: String, level: LoggingLevel = .info, caller: String? = nil) { + init(_ message: String, level: LoggingLevel = .info, caller: String? = nil) { self.message = message self.level = level self.symbol = caller } - public var hashValue: Int { + var hashValue: Int { return uuid.hashValue } } +// Simple log event structure. +struct LogQueue { + fileprivate var events: [LogEvent] = [] + mutating func push(_ event: LogEvent) { + if !events.contains(event) { + events.append(event) + } + } + + mutating func pop() -> LogEvent? { + return events.popLast() + } + + func peek() -> LogEvent? { + return events.last + } +} + + // Simple logging class. -public class Logger { +class Logger { - public enum DateFormat { + enum DateFormat { case none case short case long } public var locale = Locale.current - public var dateFormat: DateFormat = .none - static public let `default` = Logger() + public var dateFormat: DateFormat = DateFormat.none + public static let `default` = Logger() - private var logcache: Set = [] - private let logQueue = DispatchQueue.global(qos: .background) - public var loggingLevel: LoggingLevel = .info { - didSet { - print("[\(String(describing: type(of: self)))]: logging level changed: \(loggingLevel)") - } - } + private var logcache: Set = [] + private let logQueue = DispatchQueue(label: "com.sktiled.logger") - /// Print a formatted log message to output. - public func log(_ message: String, level: LoggingLevel = .info, - symbol: String? = nil, file: String = #file, - method: String = #function, line: UInt = #line) { + var loggingLevel: LoggingLevel = LoggingLevel.info + + /** + Print a formatted log message to output. + - parameter message: `String` logging message. + - parameter level: `LoggingLevel` output verbosity. + - parameter symbol: `String?` class sending the message. + */ + func log(_ message: String, + level: LoggingLevel = LoggingLevel.info, + symbol: String? = nil, + file: String = #file, + method: String = #function, + line: UInt = #line) { + + // MARK: Logging Level + // filter events at the current logging level (or higher) if (self.loggingLevel.rawValue > LoggingLevel.none.rawValue) && (level.rawValue <= self.loggingLevel.rawValue) { // format the message @@ -736,21 +743,6 @@ public class Logger { } } - /// Queue log events to be run later. - public func cache(_ event: LogEvent) { - logcache.insert(event) - } - - /// Run and release log events asyncronously. - public func release() { - for event in logcache.sorted() { - logQueue.async { - self.log(event.message, level: event.level) - } - } - logcache = [] - } - /// Formatted time stamp private var timeStamp: String { let formatter = DateFormatter() @@ -762,14 +754,20 @@ public class Logger { } /** - Format the message. + Logger message formatter. */ - private func formatMessage(_ message: String, level: LoggingLevel = .info, symbol: String? = nil, file: String = #file, method: String = #function, line: UInt = #line) -> String { + private func formatMessage(_ message: String, + level: LoggingLevel = LoggingLevel.info, + symbol: String? = nil, + file: String = #file, + method: String = #function, + line: UInt = #line) -> String { + // shorten file name let filename = URL(fileURLWithPath: file).lastPathComponent - if (level == .custom) { + if (level == LoggingLevel.custom) { var formatted = "\(message)" if let symbol = symbol { formatted = "[\(symbol)]: \(formatted)" @@ -777,7 +775,7 @@ public class Logger { return "❗️ \(formatted)" } - if (level == .status) { + if (level == LoggingLevel.status) { var formatted = "\(message)" if let symbol = symbol { formatted = "[\(symbol)]: \(formatted)" @@ -785,13 +783,13 @@ public class Logger { return "▹ \(formatted)" } - if (level == .success) { - return "\n ❊ Success! \(message)" + if (level == LoggingLevel.success) { + return "\n ✽ Success! \(message)" } // result string - var result: [String] = (dateFormat == .none) ? [] : [timeStamp] + var result: [String] = (dateFormat == DateFormat.none) ? [] : [timeStamp] result += (symbol == nil) ? [filename] : ["[" + symbol! + "]"] result += [String(describing: level), message] @@ -801,7 +799,7 @@ public class Logger { // Loggable object protcol. -public protocol Loggable { +protocol Loggable { var logSymbol: String { get } func log(_ message: String, level: LoggingLevel, file: String, method: String, line: UInt) } @@ -809,18 +807,18 @@ public protocol Loggable { // Methods for all loggable objects. extension Loggable { - public var logSymbol: String { + var logSymbol: String { return String(describing: type(of: self)) } - public func log(_ message: String, level: LoggingLevel, file: String = #file, method: String = #function, line: UInt = #line) { + func log(_ message: String, level: LoggingLevel, file: String = #file, method: String = #function, line: UInt = #line) { Logger.default.log(message, level: level, symbol: self.logSymbol, file: file, method: method, line: line) } } extension Logger.DateFormat { - public var formatString: String { + var formatString: String { switch self { case .long: return "yyyy-MM-dd HH:mm:ss" @@ -841,6 +839,15 @@ extension LogEvent: Comparable { } } +extension LogEvent: CustomStringConvertible { + var description: String { + return message + } +} + + + + extension LoggingLevel: Comparable { static public func < (lhs: LoggingLevel, rhs: LoggingLevel) -> Bool { @@ -854,24 +861,18 @@ extension LoggingLevel: Comparable { extension LoggingLevel: CustomStringConvertible { - + /// String representation of logging level. public var description: String { switch self { - case .fatal: - return "FATAL" - case .error: - return "ERROR" - case .warning: - return "WARNING" - case .success: - return "Success" - case .info: - return "INFO" - case .debug: - return "DEBUG" - default: - return "" + case .none: return "none" + case .fatal: return "FATAL" + case .error: return "ERROR" + case .warning: return "WARNING" + case .success: return "Success" + case .info: return "INFO" + case .debug: return "DEBUG" + default: return "?" } } @@ -879,3 +880,126 @@ extension LoggingLevel: CustomStringConvertible { public static let all: [LoggingLevel] = [.none, .fatal, .error, .warning, .success, .info, .debug, .custom] } + +extension TileObjectOverlay { + + internal var objects: [TileObjectProxy] { + let proxies = children.filter { $0 as? TileObjectProxy != nil } + return proxies as? [TileObjectProxy] ?? [TileObjectProxy]() + } + + override var description: String { + return "Objects Overlay: \(objects.count) objects." + } +} + + +extension TileObjectProxy { + + override var description: String { + guard let object = reference else { + return "Object Proxy: nil" + } + return "Object Proxy: \(object.id)" + } + + override var debugDescription: String { + return description + } +} + + + +extension DebugDrawOptions { + + public var strings: [String] { + var result: [String] = [] + + if self.contains(.drawGrid) { + result.append("Draw Grid") + } + if self.contains(.drawBounds) { + result.append("Draw Bounds") + } + if self.contains(.drawGraph) { + result.append("Draw Graph") + } + if self.contains(.drawObjectBounds) { + result.append("Draw Object Bounds") + } + if self.contains(.drawTileBounds) { + result.append("Draw Tile Bounds") + } + if self.contains(.drawMouseOverObject) { + result.append("Draw Mouse Over Object") + } + if self.contains(.drawBackground) { + result.append("Draw Background") + } + if self.contains(.drawAnchor) { + result.append("Draw Anchor") + } + return result + } +} + + +extension DebugDrawOptions: CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable { + + public var description: String { + guard (strings.isEmpty == false) else { + return "none" + } + return strings.joined(separator: ", ") + } + + public var debugDescription: String { + return description + } + + public var customMirror: Mirror { + return Mirror(reflecting: DebugDrawOptions.self) + } +} + + +extension SKTilemap: Loggable {} +extension SKTiledLayerObject: Loggable {} +extension SKTileset: Loggable {} +extension SKTilemapParser: Loggable {} +extension SKTiledDebugDrawNode: Loggable {} + + +extension SKTiledDebugDrawNode: CustomReflectable { + + var customMirror: Mirror { + return Mirror(reflecting: SKTiledDebugDrawNode.self) + } + + override var description: String { + return "Debug Draw Node: \(layer.layerName)" + } + + override var debugDescription: String { + return description + } +} + + +protocol CustomDebugReflectable: class { + func dumpStatistics() +} + + + +extension CustomDebugReflectable { + + func underlined(for string: String, symbol: String? = nil, colon: Bool = true) -> String { + let symbolString = symbol ?? "#" + let colonString = (colon == true) ? ":" : "" + let spacer = String(repeating: " ", count: symbolString.count) + let formattedString = "\(symbolString)\(spacer)\(string)\(colonString)" + let underlinedString = String(repeating: "-", count: formattedString.count) + return "\n\(formattedString)\n\(underlinedString)\n" + } +} diff --git a/Sources/SKTiled+Extensions.swift b/Sources/SKTiled+Extensions.swift index 648c952f..1a2ed2bd 100644 --- a/Sources/SKTiled+Extensions.swift +++ b/Sources/SKTiled+Extensions.swift @@ -16,6 +16,161 @@ import Cocoa #endif +// MARK: - Global Functions + + +/** + Returns current framework version. + + - returns: `String` SKTiled framework version. + */ +func getSKTiledVersion() -> String { + var sktiledVersion = "0" + if let sdkVersion = Bundle(for: SKTilemap.self).infoDictionary?["CFBundleShortVersionString"] { + sktiledVersion = "\(sdkVersion)" + } + return sktiledVersion +} + + +/** + Returns current framework build version. + + - returns: `String` SKTiled framework build version. + */ +func getSKTiledBuildVersion() -> String? { + var buildVersion: String? = nil + if let bundleVersion = Bundle(for: SKTilemap.self).infoDictionary?["CFBundleVersion"] { + buildVersion = "\(bundleVersion)" + } + return buildVersion +} + + +/** + Returns current framework Swift version. + + - returns: `String` Swift version. + */ +public func getSwiftVersion() -> String { + var swiftVersion = "" + #if swift(>=4.2) + swiftVersion = "4.2" + #elseif swift(>=4.1) + swiftVersion = "4.1" + #elseif swift(>=4.0) + swiftVersion = "4.0" + #else + swiftVersion = "invalid" + #endif + return swiftVersion +} + + +/** + Dumps SKTiled framework globals to the console. + */ +public func SKTiledGlobals() { + TiledGlobals.default.dumpStatistics() +} + + +/** + Returns the device scale factor. + + - returns: `CGFloat` device scale. + */ +public func getContentScaleFactor() -> CGFloat { + var scaleFactor: CGFloat = 1.0 + #if os(macOS) + scaleFactor = NSScreen.main!.backingScaleFactor + #endif + + #if os(iOS) + scaleFactor = UIScreen.main.scale + #endif + + #if os(tvOS) + scaleFactor = UIScreen.main.scale + #endif + return scaleFactor +} + + +// MARK: - CPU Usage + +/** + Returns a scaled double representing CPU usage. + + - returns: `Double` scaled percentage of CPU power used by the current app. + */ +public func cpuUsage() -> Double { + var kr: kern_return_t + var task_info_count: mach_msg_type_number_t + + task_info_count = mach_msg_type_number_t(TASK_INFO_MAX) + var tinfo = [integer_t](repeating: 0, count: Int(task_info_count)) + + kr = task_info(mach_task_self_, task_flavor_t(TASK_BASIC_INFO), &tinfo, &task_info_count) + if kr != KERN_SUCCESS { + return -1 + } + + var thread_list: thread_act_array_t? = UnsafeMutablePointer(mutating: [thread_act_t]()) + var thread_count: mach_msg_type_number_t = 0 + defer { + if let thread_list = thread_list { + vm_deallocate(mach_task_self_, vm_address_t(UnsafePointer(thread_list).pointee), vm_size_t(thread_count)) + } + } + + kr = task_threads(mach_task_self_, &thread_list, &thread_count) + + if kr != KERN_SUCCESS { + return -1 + } + + var tot_cpu: Double = 0 + + if let thread_list = thread_list { + + for j in 0 ..< Int(thread_count) { + var thread_info_count = mach_msg_type_number_t(THREAD_INFO_MAX) + var thinfo = [integer_t](repeating: 0, count: Int(thread_info_count)) + kr = thread_info(thread_list[j], thread_flavor_t(THREAD_BASIC_INFO), + &thinfo, &thread_info_count) + if kr != KERN_SUCCESS { + return -1 + } + + let threadBasicInfo = convertThreadInfoToThreadBasicInfo(thinfo) + + if threadBasicInfo.flags != TH_FLAGS_IDLE { + tot_cpu += (Double(threadBasicInfo.cpu_usage) / Double(TH_USAGE_SCALE)) * 100.0 + } + } // for each thread + } + + return tot_cpu +} + + + +func convertThreadInfoToThreadBasicInfo(_ threadInfo: [integer_t]) -> thread_basic_info { + var result = thread_basic_info() + + result.user_time = time_value_t(seconds: threadInfo[0], microseconds: threadInfo[1]) + result.system_time = time_value_t(seconds: threadInfo[2], microseconds: threadInfo[3]) + result.cpu_usage = threadInfo[4] + result.policy = threadInfo[5] + result.run_state = threadInfo[6] + result.flags = threadInfo[7] + result.suspend_count = threadInfo[8] + result.sleep_time = threadInfo[9] + + return result +} + // MARK: - Image Creation Functions @@ -28,34 +183,37 @@ import Cocoa - parameter whatToDraw: function detailing what to draw the image. - returns: `CGImage` result. */ -public func imageOfSize(_ size: CGSize, scale: CGFloat=1, _ whatToDraw: (_ context: CGContext, _ bounds: CGRect, _ scale: CGFloat) -> ()) -> CGImage? { +public func imageOfSize(_ size: CGSize, scale: CGFloat = 1, _ whatToDraw: (_ context: CGContext, _ bounds: CGRect, _ scale: CGFloat) -> Void) -> CGImage? { // create an image of size, not opaque, not scaled UIGraphicsBeginImageContextWithOptions(size, false, scale) - let context = UIGraphicsGetCurrentContext() - context!.interpolationQuality = .high + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + + context.interpolationQuality = .high let bounds = CGRect(origin: CGPoint.zero, size: size) - whatToDraw(context!, bounds, scale) + whatToDraw(context, bounds, scale) let result = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return result!.cgImage! } - #else + /** Returns an image of the given size. - parameter size: `CGSize` size of resulting image. - parameter scale: `CGFloat` scale of result, for macOS that should be 1. - - parameter whatToDraw: `()->()` function detailing what to draw the image. + - parameter whatToDraw: `() -> Void` function detailing what to draw the image. - returns: `CGImage` result. */ -public func imageOfSize(_ size: CGSize, scale: CGFloat=1, _ whatToDraw: (_ context: CGContext, _ bounds: CGRect, _ scale: CGFloat) -> ()) -> CGImage? { +public func imageOfSize(_ size: CGSize, scale: CGFloat = 1, _ whatToDraw: (_ context: CGContext, _ bounds: CGRect, _ scale: CGFloat) -> Void) -> CGImage? { let scaledSize = size let image = NSImage(size: scaledSize) image.lockFocus() - let nsContext = NSGraphicsContext.current()! + let nsContext = NSGraphicsContext.current! nsContext.imageInterpolation = .medium let context = nsContext.cgContext let bounds = CGRect(origin: CGPoint.zero, size: size) @@ -68,7 +226,6 @@ public func imageOfSize(_ size: CGSize, scale: CGFloat=1, _ whatToDraw: (_ conte nsContext.flushGraphics() return imageRef! } - #endif @@ -100,7 +257,7 @@ public func flippedTileFlags(id: UInt32) -> (gid: UInt32, hflip: Bool, vflip: Bo // MARK: - Timers -public func duration(_ block: () -> ()) -> TimeInterval { +public func duration(_ block: () -> Void) -> TimeInterval { let startTime = Date() block() return Date().timeIntervalSince(startTime) @@ -110,21 +267,19 @@ public func duration(_ block: () -> ()) -> TimeInterval { // MARK: - Extensions extension Bool { - init(_ integer: T) { + init(_ integer: T) { self.init(integer != 0) } } -extension Integer { +extension BinaryInteger { init(_ bool: Bool) { self = bool ? 1 : 0 } } - - public extension Int { /// returns number of digits in Int number public var digitCount: Int { @@ -178,11 +333,20 @@ internal extension CGFloat { return self * 180.0 / CGFloat(Double.pi) } + /** + Calculate a linear interpolation between two values. + + - returns: `CGFloat` + */ + internal func lerp(start: CGFloat, end: CGFloat, t: CGFloat) -> CGFloat { + return start + (end - start) * t + } + /** Clamp the CGFloat between two values. Returns a new value. - - parameter v1: `CGFloat` min value. - - parameter v2: `CGFloat` min value. + - parameter minv: `CGFloat` min value. + - parameter maxv: `CGFloat` min value. - returns: `CGFloat` clamped result. */ internal func clamped(_ minv: CGFloat, _ maxv: CGFloat) -> CGFloat { @@ -194,8 +358,8 @@ internal extension CGFloat { /** Clamp the current value between min & max values. - - parameter v1: `CGFloat` min value. - - parameter v2: `CGFloat` min value. + - parameter minv: `CGFloat` min value. + - parameter maxv: `CGFloat` min value. - returns: `CGFloat` clamped result. */ internal mutating func clamp(_ minv: CGFloat, _ maxv: CGFloat) -> CGFloat { @@ -209,7 +373,7 @@ internal extension CGFloat { - parameter decimals: `Int` number of decimals to round to. - returns: `String` rounded display string. */ - internal func roundTo(_ decimals: Int=2) -> String { + internal func roundTo(_ decimals: Int = 2) -> String { return String(format: "%.\(String(decimals))f", self) } @@ -277,7 +441,7 @@ public extension CGPoint { - parameter decimals: `Int` decimals to round to. - returns: `String` display string. */ - public func roundTo(_ decimals: Int=1) -> String { + public func roundTo(_ decimals: Int = 1) -> String { return "x: \(self.x.roundTo(decimals)), y: \(self.y.roundTo(decimals))" } @@ -304,7 +468,9 @@ public extension CGPoint { public var yCoord: Int { return Int(y) } public var description: String { return "x: \(x.roundTo()), y: \(y.roundTo())" } - public var shortDescription: String { return "\(Int(x)),\(Int(y))" } + internal var shortDescription: String { + return "[\(String(format: "%.0f", x)),\(String(format: "%.0f", y))]" + } } @@ -316,10 +482,6 @@ extension CGPoint: Hashable { } -public func == (lhs: CGPoint, rhs: CGPoint) -> Bool { - return lhs.distance(rhs) < 0.000001 -} - public extension CGSize { @@ -328,14 +490,15 @@ public extension CGSize { public var halfWidth: CGFloat { return width / 2.0 } public var halfHeight: CGFloat { return height / 2.0 } - public func roundTo(_ decimals: Int=1) -> String { + public func roundTo(_ decimals: Int = 1) -> String { return "w: \(self.width.roundTo(decimals)), h: \(self.height.roundTo(decimals))" } - public var shortDescription: String { - return "\(self.width.roundTo(0))x\(self.height.roundTo(0))" + internal var shortDescription: String { + return "\(self.width.roundTo(0)) x \(self.height.roundTo(0))" } + /// Returns the size as a vector_float2 public var toVec2: vector_float2 { return vector_float2(Float(width), Float(height)) } @@ -346,6 +509,7 @@ public extension CGRect { /// Initialize with a center point and size. public init(center: CGPoint, size: CGSize) { + self.init() self.origin = CGPoint(x: center.x - size.width / 2.0, y: center.y - size.height / 2.0) self.size = size } @@ -397,11 +561,11 @@ public extension CGRect { - parameter decimals: `Int` decimals to round to. - returns: `String` display string. */ - public func roundTo(_ decimals: Int=1) -> String { + public func roundTo(_ decimals: Int = 1) -> String { return "origin: \(Int(origin.x)), \(Int(origin.y)), size: \(Int(size.width)) x \(Int(size.height))" } - public var shortDescription: String { + internal var shortDescription: String { return "x: \(Int(minX)), y: \(Int(minY)), w: \(width.roundTo()), h: \(height.roundTo())" } } @@ -438,42 +602,19 @@ public extension SKScene { let dy = (pos.y - center.y) return CGVector(dx: dx, dy: dy) } - - /// Returns a tilemap file name. - public var tmxFilename: String? { - var filename: String? = nil - enumerateChildNodes(withName: "*") { node, stop in - if node as? SKTilemap != nil { - if let mapURL = (node as? SKTilemap)?.url { - filename = mapURL.path - stop.pointee = true - } - } - } - return filename - } } -internal extension SKNode { - - /** - Position the node by a percentage of the view size. - */ - internal func posByCanvas(x: CGFloat, y: CGFloat) { - guard let scene = scene else { return } - guard let view = scene.view else { return } - self.position = scene.convertPoint(fromView: (CGPoint(x: CGFloat(view.bounds.size.width * x), y: CGFloat(view.bounds.size.height * (1.0 - y))))) - } +public extension SKNode { /** Run an action with key & optional completion function. - parameter action: `SKAction!` SpriteKit action. - parameter withKey: `String!` action key. - - parameter completion: `() -> ()?` optional completion function. + - parameter completion: `() -> Void?` optional completion function. */ - internal func run(_ action: SKAction!, withKey: String!, completion block: (()->())?) { + public func run(_ action: SKAction!, withKey: String!, completion block: (() -> Void)?) { if let block = block { let completionAction = SKAction.run( block ) let compositeAction = SKAction.sequence([ action, completionAction ]) @@ -482,17 +623,42 @@ internal extension SKNode { run(action, withKey: withKey) } } + + /** + Animate the speed value over the given duration. + + - parameter to: `CGFloat` new speed value. + - parameter duration: `TimeInterval` animation length. + */ + public func speed(to newSpeed: CGFloat, duration: TimeInterval, completion: (() -> Void)? = nil) { + run(SKAction.speed(to: newSpeed, duration: duration), withKey: nil, completion: completion) + } + + /** + Adds a node to the end of the receiver’s list of child nodes. + + - parameter node: `SKNode` new child node. + - parameter fadeIn: `TimeInterval` fade in duration. + */ + public func addChild(_ node: SKNode, fadeIn duration: TimeInterval) { + node.alpha = (duration > 0) ? 0 : node.alpha + self.addChild(node) + + let fadeInAction = SKAction.fadeIn(withDuration: duration) + node.run(fadeInAction) + } } -internal extension SKSpriteNode { + +public extension SKSpriteNode { /** Convenience initalizer to set texture filtering to nearest neighbor. - parameter pixelImage: `String` texture image named. */ - convenience init(pixelImage named: String) { + convenience public init(pixelImage named: String) { self.init(imageNamed: named) self.texture?.filteringMode = .nearest } @@ -508,6 +674,12 @@ public extension SKColor { return hsba } + /// Returns the red, green and blue components of the color. + internal var rgb: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { + let comps = components + return (comps[0], comps[1], comps[2], comps[3]) + } + /** Lightens the color by the given percentage. @@ -550,7 +722,7 @@ public extension SKColor { var int = UInt32() Scanner(string: hex).scanHexInt32(&int) let a, r, g, b: UInt32 - switch hex.characters.count { + switch hex.count { case 3: // RGB (12-bit) (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) case 6: // RGB (24-bit) @@ -628,7 +800,7 @@ public extension SKColor { } public var toVec4: vector_float4 { - return vector_float4(components.map {Float($0)}) + return vector_float4(components.map { Float($0) }) } public var hexDescription: String { @@ -654,12 +826,9 @@ public extension SKColor { } -public extension String { +// MARK: - String - /// Returns `Int` length of the string. - public var length: Int { - return self.characters.count - } +extension String { /** Simple function to split a string with the given pattern. @@ -667,7 +836,7 @@ public extension String { - parameter pattern: `String` pattern to split string with. - returns: `[String]` groups of split strings. */ - public func split(_ pattern: String) -> [String] { + func split(_ pattern: String) -> [String] { return self.components(separatedBy: pattern) } @@ -679,9 +848,9 @@ public extension String { - parameter padLeft: `Bool` toggle this to pad the right. - returns: `String` padded string. */ - public func zfill(length: Int, pattern: String="0", padLeft: Bool=true) -> String { + func zfill(length: Int, pattern: String="0", padLeft: Bool = true) -> String { var filler = "" - let padamt: Int = length - characters.count > 0 ? length - characters.count : 0 + let padamt: Int = length - self.count > 0 ? length - self.count : 0 if padamt <= 0 { return self } for _ in 0.. String { + func pad(_ toSize: Int) -> String { // current string length - let currentLength = self.characters.count + let currentLength = self.count if (toSize < 1) { return self } if (currentLength >= toSize) { return self } var padded = self @@ -714,7 +883,7 @@ public extension String { - parameter replaceWith: replacement `String`. - returns: `String` result. */ - public func substitute(_ pattern: String, replaceWith: String) -> String { + func substitute(_ pattern: String, replaceWith: String) -> String { return self.replacingOccurrences(of: pattern, with: replaceWith) } @@ -725,8 +894,8 @@ public extension String { */ public init(_ bytes: [UInt8]) { self.init() - for b in bytes { - self.append(String(UnicodeScalar(b))) + for byte in bytes { + self.append(String(UnicodeScalar(byte))) } } @@ -735,98 +904,205 @@ public extension String { - returns: `String` scrubbed string. */ - public func scrub() -> String { + func scrub() -> String { var scrubbed = self.replacingOccurrences(of: "\n", with: "") scrubbed = scrubbed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) return scrubbed.replacingOccurrences(of: " ", with: "") } + + /// Captialize the first letter. + var uppercaseFirst: String { + let lowerString = self.lowercased() + let first = lowerString.prefix(1) + return first.uppercased() + lowerString.dropFirst() + } + + func nsRange(fromRange range: Range) -> NSRange { + let from = range.lowerBound + let to = range.upperBound + let location = distance(from: startIndex, to: from) + let length = distance(from: from, to: to) + return NSRange(location: location, length: length) + } + // MARK: URL /// Returns a url for the string. - public var url: URL { return URL(fileURLWithPath: self.expanded) } + var url: URL { return URL(fileURLWithPath: self.expanded) } /// Expand the users home path. - public var expanded: String { return NSString(string: self).expandingTildeInPath } + var expanded: String { return NSString(string: self).expandingTildeInPath } /// Returns the url parent directory. - public var parentURL: URL { + var parentURL: URL { var path = URL(fileURLWithPath: self.expanded) path.deleteLastPathComponent() return path } /// Returns true if the string represents a path that exists. - public var fileExists: Bool { - let fm = FileManager.default - return fm.fileExists(atPath: self) + var fileExists: Bool { + return FileManager.default.fileExists(atPath: self.url.path) } /// Returns true if the string represents a path that exists and is a directory. - public var isDirectory: Bool { - let fm = FileManager.default + var isDirectory: Bool { var isDir : ObjCBool = false - return fm.fileExists(atPath: self, isDirectory: &isDir) + return FileManager.default.fileExists(atPath: self, isDirectory: &isDir) } /// Returns the filename if string is a url. - public var filename: String { - return self.url.lastPathComponent + var filename: String { + return FileManager.default.displayName(atPath: self.url.path) } /// Returns the file basename. - public var basename: String { + var basename: String { return self.url.deletingPathExtension().lastPathComponent } /// Returns the file extension. - public var fileExtension: String { + var fileExtension: String { return self.url.pathExtension } - - /// Captialize the first letter. - public var uppercaseFirst: String { - let first = String(characters.prefix(1)) - return first.uppercased() + String(characters.dropFirst()) - } } -public extension URL { +// MARK: - URL + +extension URL { /// Returns the path file name without file extension. - public var basename: String { + var basename: String { return self.deletingPathExtension().lastPathComponent } /// Returns the file name without the parent directory. - public var filename: String { - return self.lastPathComponent + var filename: String { + return FileManager.default.displayName(atPath: path) } /// Returns the parent path of the file. - public var parent: String? { - var mutableURL = self + var parent: String? { + let mutableURL = self let result = (mutableURL.deletingLastPathComponent().relativePath == ".") ? nil : mutableURL.deletingLastPathComponent().relativePath return result } /// Returns true if the URL represents a path that exists. - public var fileExists: Bool { - let fm = FileManager.default - return fm.fileExists(atPath: self.path) + var fileExists: Bool { + return FileManager.default.fileExists(atPath: self.path) } /// Returns true if the URL represents a path that exists and is a directory. - public var isDirectory: Bool { - let fm = FileManager.default + var isDirectory: Bool { var isDir : ObjCBool = false - return fm.fileExists(atPath: self.path, isDirectory: &isDir) + return FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir) + } + + /// Returns true if the URL represents a path in the app bundle. + var isBundled: Bool { + let mutableURL = self + let result = (mutableURL.deletingLastPathComponent().relativePath == ".") ? nil : mutableURL.deletingLastPathComponent().relativePath + return result == nil + } +} + + +// MARK: - TimeInterval + +extension TimeInterval { + + /// Returns the current value in milleseconds. + var milleseconds: Double { + return Double(self * 1000) } } -public extension SKAction { +// MARK: - Events & Vallbacks + +extension Notification.Name { + + /// IN USE + public struct Tileset { + public static let DataAdded = Notification.Name(rawValue: "com.sktiled.notification.name.tileset.dataAdded") + public static let DataRemoved = Notification.Name(rawValue: "com.sktiled.notification.name.tileset.dataRemoved") + public static let SpriteSheetUpdated = Notification.Name(rawValue: "com.sktiled.notification.name.tileset.spritesheetUpdated") + } + + public struct TileData { + public static let FrameAdded = Notification.Name(rawValue: "com.sktiled.notification.name.tileData.frameAdded") + public static let TextureChanged = Notification.Name(rawValue: "com.sktiled.notification.name.tileData.textureChanged") + public static let ActionAdded = Notification.Name(rawValue: "com.sktiled.notification.name.tileData.actionAdded") + } + + public struct Layer { + public static let TileAdded = Notification.Name(rawValue: "com.sktiled.notification.name.layer.tileAdded") + public static let AnimatedTileAdded = Notification.Name(rawValue: "com.sktiled.notification.name.layer.animatedTileAdded") + public static let ObjectAdded = Notification.Name(rawValue: "com.sktiled.notification.name.layer.objectAdded") + public static let ObjectRemoved = Notification.Name(rawValue: "com.sktiled.notification.name.layer.objectRemoved") + } + + public struct Tile { + public static let DataChanged = Notification.Name(rawValue: "com.sktiled.notification.name.tile.dataChanged") + public static let RenderModeChanged = Notification.Name(rawValue: "com.sktiled.notification.name.tile.renderModeChanged") + } + + public struct Map { + public static let FinishedRendering = Notification.Name(rawValue: "com.sktiled.notification.name.map.finishedRendering") + public static let Updated = Notification.Name(rawValue: "com.sktiled.notification.name.map.updated") + public static let RenderStatsUpdated = Notification.Name(rawValue: "com.sktiled.notification.name.map.renderStatsUpdated") + public static let CacheUpdated = Notification.Name(rawValue: "com.sktiled.notification.name.map.cacheUpdated") + public static let UpdateModeChanged = Notification.Name(rawValue: "com.sktiled.notification.name.map.updateModeChanged") + + } + + public struct DataStorage { + public static let ProxyVisibilityChanged = Notification.Name(rawValue: "com.sktiled.notification.name.dataStorage.proxyVisibilityChanged") + public static let IsolationModeChanged = Notification.Name(rawValue: "com.sktiled.notification.name.dataStorage.isolationModeChanged") + } + + public struct Globals { + public static let Updated = Notification.Name(rawValue: "com.sktiled.notification.name.globals.updated") + } + + public struct Camera { + public static let Updated = Notification.Name(rawValue: "com.sktiled.notification.name.camera.updated") + } + + public struct RenderStats { + public static let StaticTilesUpdated = Notification.Name(rawValue: "com.sktiled.notification.name.renderStats.staticTilesUpdated") + public static let AnimatedTilesUpdated = Notification.Name(rawValue: "com.sktiled.notification.name.renderStats.animatedTilesUpdated") + public static let VisibilityChanged = Notification.Name(rawValue: "com.sktiled.notification.name.renderStats.visibilityChanged") + } +} + + +extension OptionSet where RawValue: FixedWidthInteger { + + public func elements() -> AnySequence { + var remainingBits = rawValue + var bitMask: RawValue = 1 + return AnySequence { + return AnyIterator { + while remainingBits != 0 { + defer { bitMask = bitMask &* 2 } + if remainingBits & bitMask != 0 { + remainingBits = remainingBits & ~bitMask + return Self(rawValue: bitMask) + } + } + return nil + } + } + } +} + + + +extension SKAction { /** Custom action to animate sprite textures with varying frame durations. @@ -852,33 +1128,78 @@ public extension SKAction { return SKAction.sequence(actions) } + /** + Custom action to animate shape colors over a duration. + + - parameter duration: `TimeInterval` time for the effect. + - returns: `SKAction` custom shape fade action. + */ + public class func colorFadeAction(after delay: TimeInterval) -> SKAction { + // Create a custom action for color fade + let duration: TimeInterval = 1.0 + let action = SKAction.customAction(withDuration: duration) {(node, elapsed) in + if let shape = node as? SKShapeNode { + + let currentStroke = shape.strokeColor + let currentFill = shape.fillColor + + // Calculate the changing color during the elapsed time. + let fraction = elapsed / CGFloat(duration) + + let currentStrokeRGB = currentStroke.rgb + let currentFillRGB = currentFill.rgb + let endColorRGB: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) = (0,0,0,0) + + let sred = CGFloat().lerp(start: currentStrokeRGB.red, end: endColorRGB.red, t: fraction) + let sgreen = CGFloat().lerp(start: currentStrokeRGB.green, end: endColorRGB.green, t: fraction) + let sblue = CGFloat().lerp(start: currentStrokeRGB.blue, end: endColorRGB.blue, t: fraction) + let salpha = CGFloat().lerp(start: currentStrokeRGB.alpha, end: endColorRGB.alpha, t: fraction) + + let fred = CGFloat().lerp(start: currentFillRGB.red, end: endColorRGB.red, t: fraction) + let fgreen = CGFloat().lerp(start: currentFillRGB.green, end: endColorRGB.green, t: fraction) + let fblue = CGFloat().lerp(start: currentFillRGB.blue, end: endColorRGB.blue, t: fraction) + let falpha = CGFloat().lerp(start: currentFillRGB.alpha, end: endColorRGB.alpha, t: fraction) + + + let newStokeColor = SKColor(red: sred, green: sgreen, blue: sblue, alpha: salpha) + let newFillColor = SKColor(red: fred, green: fgreen, blue: fblue, alpha: falpha) + + shape.strokeColor = newStokeColor + shape.fillColor = newFillColor + } + } + + return SKAction.afterDelay(delay, performAction: action) + } + + /** Custom action to fade a node's alpha after a pause. - returns: `SKAction` custom fade action. */ - public class func fadeAfter(wait duration: TimeInterval, alpha: CGFloat) -> SKAction { + class func fadeAfter(wait duration: TimeInterval, alpha: CGFloat) -> SKAction { return SKAction.sequence([SKAction.wait(forDuration: duration), SKAction.fadeAlpha(to: alpha, duration: 0.5)]) } /** * Performs an action after the specified delay. */ - public class func afterDelay(_ delay: TimeInterval, performAction action: SKAction) -> SKAction { + class func afterDelay(_ delay: TimeInterval, performAction action: SKAction) -> SKAction { return SKAction.sequence([SKAction.wait(forDuration: delay), action]) } /** * Performs a block after the specified delay. */ - public class func afterDelay(_ delay: TimeInterval, runBlock block: @escaping () -> Void) -> SKAction { + class func afterDelay(_ delay: TimeInterval, runBlock block: @escaping () -> Void) -> SKAction { return SKAction.afterDelay(delay, performAction: SKAction.run(block)) } /** * Removes the node from its parent after the specified delay. */ - public class func removeFromParentAfterDelay(_ delay: TimeInterval) -> SKAction { + class func removeFromParentAfterDelay(_ delay: TimeInterval) -> SKAction { return SKAction.afterDelay(delay, performAction: SKAction.removeFromParent()) } } @@ -1053,7 +1374,7 @@ public func / (lhs: CGSize, rhs: CGFloat) -> CGSize { public func fabs(_ size: CGSize) -> CGSize { - return CGSize(width: fabs(size.width), height: fabs(size.height)) + return CGSize(width: abs(size.width), height: abs(size.height)) } @@ -1064,7 +1385,8 @@ public func + (lhs: CGVector, rhs: CGVector) -> CGVector { public func += (lhs: inout CGVector, rhs: CGVector) { - lhs += rhs + lhs.dx += rhs.dx + lhs.dy += rhs.dy } @@ -1072,51 +1394,51 @@ public func - (lhs: CGVector, rhs: CGVector) -> CGVector { return CGVector(dx: lhs.dx - rhs.dx, dy: lhs.dy - rhs.dy) } - +/* public func -= (lhs: inout CGVector, rhs: CGVector) { lhs -= rhs } - +*/ public func * (lhs: CGVector, rhs: CGVector) -> CGVector { return CGVector(dx: lhs.dx * rhs.dx, dy: lhs.dy * rhs.dy) } - +/* public func *= (lhs: inout CGVector, rhs: CGVector) { lhs *= rhs } - +*/ public func * (vector: CGVector, scalar: CGFloat) -> CGVector { return CGVector(dx: vector.dx * scalar, dy: vector.dy * scalar) } - +/* public func *= (vector: inout CGVector, scalar: CGFloat) { vector *= scalar } - +*/ public func / (lhs: CGVector, rhs: CGVector) -> CGVector { return CGVector(dx: lhs.dx / rhs.dx, dy: lhs.dy / rhs.dy) } - +/* public func /= (lhs: inout CGVector, rhs: CGVector) { lhs /= rhs } - +*/ public func / (lhs: CGVector, rhs: CGFloat) -> CGVector { return CGVector(dx: lhs.dx / rhs, dy: lhs.dy / rhs) } - +/* public func /= (lhs: inout CGVector, rhs: CGFloat) { lhs /= rhs } - +*/ public func lerp(start: CGVector, end: CGVector, t: CGFloat) -> CGVector { return start + (end - start) * t @@ -1163,7 +1485,8 @@ public func + (lhs: int2, rhs: int2) -> int2 { } public func += (lhs: inout int2, rhs: int2) { - lhs += rhs + lhs.x += rhs.x + lhs.y += rhs.y } @@ -1173,7 +1496,9 @@ public func - (lhs: int2, rhs: int2) -> int2 { public func -= (lhs: inout int2, rhs: int2) { - lhs -= rhs + lhs.x -= rhs.x + lhs.y -= rhs.y + } @@ -1182,7 +1507,8 @@ public func * (lhs: int2, rhs: int2) -> int2 { } public func *= (lhs: inout int2, rhs: int2) { - lhs *= rhs + lhs.x *= rhs.x + lhs.y *= rhs.y } @@ -1190,16 +1516,22 @@ public func / (lhs: int2, rhs: int2) -> int2 { return int2(lhs.x / rhs.x, lhs.y / rhs.y) } +// Swift 4 Error /* public func /= (lhs: inout int2, rhs: int2) { lhs /= rhs } -*/ + public func == (lhs: int2, rhs: int2) -> Bool { return (lhs.x == rhs.x) && (lhs.y == rhs.y) } +internal func == (lhs: CGPoint, rhs: CGPoint) -> Bool { + return lhs.distance(rhs) < 0.000001 +} +*/ + extension vector_int2 { @@ -1258,10 +1590,12 @@ public func normalize(_ value: CGFloat, _ minimum: CGFloat, _ maximum: CGFloat) - returns: `CGImage` visual grid texture. */ internal func drawLayerGrid(_ layer: SKTiledLayerObject, - imageScale: CGFloat=8, - lineScale: CGFloat=1) -> CGImage? { + imageScale: CGFloat = 8, + lineScale: CGFloat = 1) -> CGImage? { + + // get the ui scale value for the device - let uiScale: CGFloat = SKTiledContentScaleFactor + let uiScale: CGFloat = TiledGlobals.default.contentScale let size = layer.size let tileWidth = layer.tileWidth * imageScale @@ -1270,9 +1604,15 @@ internal func drawLayerGrid(_ layer: SKTiledLayerObject, let tileWidthHalf = tileWidth / 2 let tileHeightHalf = tileHeight / 2 + // image size is the rendered size let sizeInPoints = (layer.sizeInPoints * imageScale) let defaultLineWidth: CGFloat = (imageScale / uiScale) * lineScale + guard sizeInPoints != CGSize.zero else { + return nil + } + + return imageOfSize(sizeInPoints, scale: uiScale) { context, bounds, scale in // reference to shape path @@ -1281,6 +1621,7 @@ internal func drawLayerGrid(_ layer: SKTiledLayerObject, let innerColor = layer.gridColor // line width should be at least 1 for larger tile sizes let lineWidth: CGFloat = defaultLineWidth + context.setLineWidth(lineWidth) context.setShouldAntialias(true) // layer.antialiased @@ -1357,7 +1698,6 @@ internal func drawLayerGrid(_ layer: SKTiledLayerObject, hexPoints[4] = CGPoint(x: xpos - (tileWidth / 2), y: ypos - (variableSize / 2)) hexPoints[5] = CGPoint(x: xpos - (tileWidth / 2), y: ypos + (variableSize / 2)) } - shapePath = polygonPath(hexPoints) context.addPath(shapePath!) } @@ -1399,7 +1739,7 @@ internal func drawLayerGraph(_ layer: SKTiledLayerObject, // get the ui scale value for the device - let uiScale: CGFloat = SKTiledContentScaleFactor + let uiScale: CGFloat = TiledGlobals.default.contentScale let size = layer.size let tileWidth = layer.tileWidth * imageScale @@ -1429,7 +1769,6 @@ internal func drawLayerGraph(_ layer: SKTiledLayerObject, if let node = graph.node(atGridPosition: int2(Int32(col), Int32(row))) { - fillColor = SKColor.gray if let tiledNode = node as? SKTiledGraphNode { @@ -1579,8 +1918,8 @@ internal func createTempDirectory(named: String) -> URL? { */ internal func writeToFile(_ image: CGImage, url: URL) -> Data { let bitmapRep: NSBitmapImageRep = NSBitmapImageRep(cgImage: image) - let properties = Dictionary() - let data: Data = bitmapRep.representation(using: NSBitmapImageFileType.PNG, properties: properties)! + let properties = Dictionary() + let data: Data = bitmapRep.representation(using: NSBitmapImageRep.FileType.png, properties: properties)! if !((try? data.write(to: URL(fileURLWithPath: url.path), options: [])) != nil) { Logger.default.log("Error: write to file failed.", level: .error) } @@ -1593,33 +1932,22 @@ internal func writeToFile(_ image: CGImage, url: URL) -> Data { internal func drawAnchor(_ node: SKNode, + withKey key: String = "ANCHOR", withLabel: String? = nil, labelSize: CGFloat = 10, labelOffsetX: CGFloat = 0, labelOffsetY: CGFloat = 0, radius: CGFloat = 4, - anchorColor: SKColor = SKColor.red) { - - node.childNode(withName: "ANCHOR")?.removeFromParent() + anchorColor: SKColor = SKColor.red, + zoomScale: CGFloat = 0) -> AnchorNode { - let anchorShape = SKShapeNode(circleOfRadius: radius) - anchorShape.name = "ANCHOR" - node.addChild(anchorShape) - anchorShape.fillColor = anchorColor - anchorShape.strokeColor = .clear - anchorShape.zPosition = node.zPosition + 1 - - if let withLabel = withLabel { - let anchorLabel = SKLabelNode(fontNamed: "Courier") - anchorLabel.text = withLabel - anchorLabel.fontSize = labelSize * 4 - anchorShape.addChild(anchorLabel) - anchorLabel.zPosition = anchorShape.zPosition + 1 - anchorLabel.position.x += labelOffsetX - anchorLabel.position.y += labelOffsetY - anchorLabel.setScale(1.0 / 4.0) - anchorLabel.color = .white - } + node.childNode(withName: key)?.removeFromParent() + let anchor = AnchorNode(radius: radius, color: anchorColor, label: withLabel, offsetX: labelOffsetX, offsetY: labelOffsetY, zoom: zoomScale) + anchor.labelSize = labelSize + node.addChild(anchor) + anchor.position = CGPoint(x: 0, y: 0) + anchor.zPosition = node.zPosition * 10 + return anchor } @@ -1633,7 +1961,7 @@ internal func drawAnchor(_ node: SKNode, - parameter origin: `CGPoint` rectangle origin. - returns: `[CGPoint]` array of points. */ -public func rectPointArray(_ width: CGFloat, height: CGFloat, origin: CGPoint = .zero) -> [CGPoint] { +public func rectPointArray(_ width: CGFloat, height: CGFloat, origin: CGPoint = CGPoint.zero) -> [CGPoint] { let points: [CGPoint] = [ origin, CGPoint(x: origin.x + width, y: origin.y), @@ -1651,7 +1979,7 @@ public func rectPointArray(_ width: CGFloat, height: CGFloat, origin: CGPoint = - parameter origin: `CGPoint` rectangle origin. - returns: `[CGPoint]` array of points. */ -public func rectPointArray(_ size: CGSize, origin: CGPoint = .zero) -> [CGPoint] { +public func rectPointArray(_ size: CGSize, origin: CGPoint = CGPoint.zero) -> [CGPoint] { return rectPointArray(size.width, height: size.height, origin: origin) } @@ -1665,7 +1993,7 @@ public func rectPointArray(_ size: CGSize, origin: CGPoint = .zero) -> [CGPoint] - parameter origin: `CGPoint` origin point. - returns: `[CGPoint]` array of points. */ -public func polygonPointArray(_ sides: Int, radius: CGSize, offset: CGFloat=0, origin: CGPoint = .zero) -> [CGPoint] { +public func polygonPointArray(_ sides: Int, radius: CGSize, offset: CGFloat = 0, origin: CGPoint = CGPoint.zero) -> [CGPoint] { let angle = (360 / CGFloat(sides)).radians() let cx = origin.x // x origin let cy = origin.y // y origin @@ -1691,7 +2019,7 @@ public func polygonPointArray(_ sides: Int, radius: CGSize, offset: CGFloat=0, o - parameter closed: `Bool` path should be closed. - returns: `CGPath` path from the given points. */ -public func polygonPath(_ points: [CGPoint], closed: Bool=true) -> CGPath { +public func polygonPath(_ points: [CGPoint], closed: Bool = true) -> CGPath { let path = CGMutablePath() var mpoints = points let first = mpoints.remove(at: 0) @@ -1713,7 +2041,7 @@ public func polygonPath(_ points: [CGPoint], closed: Bool=true) -> CGPath { - parameter offset: `CGFloat` rotation offset (45 to return a rectangle). - returns: `CGPathf` path from the given points. */ -public func polygonPath(_ sides: Int, radius: CGSize, offset: CGFloat=0, origin: CGPoint=CGPoint.zero) -> CGPath { +public func polygonPath(_ sides: Int, radius: CGSize, offset: CGFloat = 0, origin: CGPoint = CGPoint.zero) -> CGPath { let path = CGMutablePath() let points = polygonPointArray(sides, radius: radius, offset: offset) let cpg = points[0] @@ -1874,19 +2202,6 @@ public func arrowFromPoints(startPoint: CGPoint, } -/** - Returns the device scale factor. - - - returns: `CGFloat` device scale. - */ -public func getContentScaleFactor() -> CGFloat { - #if os(iOS) || os(tvOS) - return UIScreen.main.scale - #else - return NSScreen.main()!.backingScaleFactor - #endif -} - /** Clamp a point to the given scale. @@ -1906,18 +2221,41 @@ internal func clampedPosition(point: CGPoint, scale: CGFloat) -> CGPoint { Clamp the position of a given node (and parent). - parameter node: `SKNode` node to re-position. - - parameter scale: `CGFloat` device scale. + - parameter scale: `CGFloat` device scale. */ -internal func clampPositionWithNode(node: SKNode, scale: CGFloat) { +public func clampNodePosition(node: SKNode, scale: CGFloat) { node.position = clampedPosition(point: node.position, scale: scale) if let parentNode = node.parent { + // check that the parent is not the scene if parentNode != node.scene { - clampPositionWithNode(node: parentNode, scale: scale) + clampNodePosition(node: parentNode, scale: scale) } } } + +/** + Dumps SKTiled framework globals to the console. + */ +@available(*, deprecated, renamed: "SKTiledGlobals()") +public func getSKTiledGlobals() { + TiledGlobals.default.dumpStatistics() +} + + +/** + Clamp the position of a given node (and parent). + + - parameter node: `SKNode` node to re-position. + - parameter scale: `CGFloat` device scale. + */ +@available(*, deprecated, renamed: "clampNodePosition(node:scale:)") +public func clampPositionWithNode(node: SKNode, scale: CGFloat) { + clampNodePosition(node: node, scale: scale) +} + + // MARK: - Compression /** diff --git a/Sources/SKTiled+GameplayKit.swift b/Sources/SKTiled+GameplayKit.swift index 7a080822..10df5722 100644 --- a/Sources/SKTiled+GameplayKit.swift +++ b/Sources/SKTiled+GameplayKit.swift @@ -23,8 +23,8 @@ extension SKTilemap { */ public func gridGraphForLayers(_ layers: [SKTileLayer], walkable: [SKTile], - obstacle: [SKTile]=[], - diagonalsAllowed: Bool=false, + obstacle: [SKTile] = [], + diagonalsAllowed: Bool = false, nodeClass: String? = nil) { layers.forEach { @@ -49,8 +49,8 @@ public extension SKTileLayer { - returns: `GKGridGraph?` navigation graph, if created. */ public func initializeGraph(walkable: [SKTile], - obstacles: [SKTile]=[], - diagonalsAllowed: Bool=false, + obstacles: [SKTile] = [], + diagonalsAllowed: Bool = false, withName: String? = nil, nodeClass: String? = nil) -> GKGridGraph? { @@ -83,6 +83,9 @@ public extension SKTileLayer { if let tiledNode = node as? SKTiledGraphNode { tiledNode.weight = Float(tile.tileData.weight) + // transfer properties + tiledNode.properties = tile.tileData.properties + tiledNode.parseProperties(completion: nil) } if (walkable.contains(tile)) { @@ -103,6 +106,7 @@ public extension SKTileLayer { graph.remove(nodesToRemove) let nodeCount = (graph.nodes != nil) ? graph.nodes!.count : 0 + // logging output let statusMessage = (nodeCount > 0) ? "navigation graph for layer \"\(layerName)\" created with \(nodeCount) nodes." : "could not build a navigation graph for layer \"\(layerName)\"." let statusLevel: LoggingLevel = (nodeCount > 0) ? .info : .warning @@ -118,7 +122,7 @@ public extension SKTileLayer { // unhide the layer & kill textures isHidden = false - getTiles().forEach { $0.texture = nil } + clearTiles() return graph } @@ -142,8 +146,8 @@ public extension SKTileLayer { - returns: `GKGridGraph?` navigation graph, if created. */ public func initializeGraph(walkableIDs: [Int], - obstacleIDs: [Int]=[], - diagonalsAllowed: Bool=false, + obstacleIDs: [Int] = [], + diagonalsAllowed: Bool = false, nodeClass: String? = nil) -> GKGridGraph? { let walkable: [SKTile] = getTiles().filter { tile in @@ -200,7 +204,15 @@ public extension SKTileLayer { [gkgridgraphnode-url]:https://developer.apple.com/documentation/gameplaykit/gkgridgraphnode */ -public class SKTiledGraphNode: GKGridGraphNode { +public class SKTiledGraphNode: GKGridGraphNode, SKTiledObject { + + /// Unique id. + public var uuid: String = UUID().uuidString + + public var type: String! + public var properties: [String : String] = [:] + public var ignoreProperties: Bool = false + public var renderQuality: CGFloat = TiledGlobals.default.renderQuality.default /// Weight property. public var weight: Float = 1.0 @@ -214,7 +226,7 @@ public class SKTiledGraphNode: GKGridGraphNode { - parameter weight: `Float` node weight. - returns: `SKTiledGraphNode` node instance. */ - public init(gridPosition: int2, weight: Float=1.0) { + public init(gridPosition: int2, weight: Float = 1.0) { self.weight = weight super.init(gridPosition: gridPosition) } @@ -240,7 +252,7 @@ public class SKTiledGraphNode: GKGridGraphNode { guard let gridNode = node as? SKTiledGraphNode else { return super.cost(to: node) } - return weight - (1.0 - gridNode.weight) + return connectedNodes.contains(gridNode) ? weight - (1.0 - gridNode.weight) : Float.greatestFiniteMagnitude } /** @@ -300,3 +312,16 @@ extension SKTiledScene { } } + +extension SKTiledGraphNode { + + /** + Parse the tile data's properties value. + + - parameter completion: `() -> Void)?` optional completion function. + */ + public func parseProperties(completion: (() -> Void)?) { + if (ignoreProperties == true) { return } + if (self.type == nil) { self.type = properties.removeValue(forKey: "type") } + } +} diff --git a/Sources/SKTiled+Globals.swift b/Sources/SKTiled+Globals.swift new file mode 100644 index 00000000..d5df02ed --- /dev/null +++ b/Sources/SKTiled+Globals.swift @@ -0,0 +1,406 @@ +// +// SKTiled+Globals.swift +// SKTiled Demo +// +// Created by Michael Fessenden on 8/4/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import SpriteKit +import Metal + + +/** + + ## Overview ## + + The `TiledGlobals` object provides information about the framework, as well as allowing + you to set default **SKTiled** attributes. + + + ### Properties ### + + | Property | Description | + |:----------------------|:------------------------------| + | renderer | SpriteKit renderer. | + | loggingLevel | Logging verbosity. | + | updateMode | Default tile update mode. | + | enableRenderCallbacks | Send render statistics. | + | enableCameraCallbacks | Send camera updates. | + | renderQuality | Global render quality values. | + | contentScale | Retina display scale factor. | + | version | Framework version. | + + ### Usage ### + + **SKTiled** object default values are set in the `TiledGlobals` object. + + ```swift + // access the default singleton instance + let tiledGlobals = TiledGlobals.default + + // disable camera callbacks + tiledGlobals.enableCameraCallbacks = false + + // set debugging mouse filters (macOS) + tiledGlobals.debug.mouseFilters = [.tileCoordinates, .tilesUnderCursor] + + // increase the default text object render quality + tiledGlobals.renderQuality.text = 12.0 + ``` + */ +public class TiledGlobals { + /// Default singleton instance. + static public let `default` = TiledGlobals() + /// Current SpriteKit renderer. + public private(set) var renderer: Renderer = Renderer.metal + /// Default logging verbosity. + public var loggingLevel: LoggingLevel = LoggingLevel.info + /// Default tile update mode. + public var updateMode: TileUpdateMode = TileUpdateMode.dynamic + /// Enable callbacks for render performance statistics. + public var enableRenderCallbacks: Bool = false + /// Enable callbacks from camera to camera delegates. + public var enableCameraCallbacks: Bool = true + /// Default tile/object render quality attributes. + public var renderQuality: RenderQuality = RenderQuality() + /// Debugging display options. + public var debug: DebugDisplayOptions = DebugDisplayOptions() + /// Render statistics display. + public var timeDisplayMode: TimeDisplayMode = TimeDisplayMode.milliseconds + /// Returns the current device backing scale. + public var contentScale: CGFloat { + return getContentScaleFactor() + } + + /// Returns current framework version. + public var version: Version { + return Version(getSKTiledVersion()) + } + + /// Returns current framework build (if any). + internal var build: String? { + return getSKTiledBuildVersion() + } + + private init() { + let device = MTLCreateSystemDefaultDevice() + renderer = (device != nil) ? Renderer.metal : Renderer.opengl + } + + /** + ### Overview ### + + Structure representing the framework version (semantic version). + + #### Properties #### + + | Property | Description | + |:----------------------|:-----------------------------| + | major | Framework major version. | + | minor | Framework minor version. | + | patch | Framework patch version | + + */ + public struct Version { + var major: Int = 0 + var minor: Int = 0 + var patch: Int = 0 + + init(major: Int, minor: Int, patch: Int = 0) { + self.major = major + self.minor = minor + self.patch = patch + } + } + + + /** + ### Overview ### + + Represents object's render quality when dealing with higher resolutions. + + #### Properties #### + + | Property | Description | + |:----------------------|:-----------------------------------------| + | default | Global render quality. | + | object | Object render quality. | + | text | Text object render quality | + | override | Override value. | + + */ + public struct RenderQuality { + var `default`: CGFloat = 3 + var object: CGFloat = 8 + var text: CGFloat = 8 + var override: CGFloat = 0 + } + + /** + ### Overview ### + + Global debug display properties. + + */ + public struct DebugDisplayOptions { + + /// Debug properties for mouse movements. + public var mouseFilters: MouseFilters = MouseFilters.tileCoordinates + /// Debug display properties. + public var highlightDuration: TimeInterval = 0.3 + public var gridOpactity: CGFloat = 0.4 + public var gridColor: SKColor = TiledObjectColors.grass + public var frameColor: SKColor = TiledObjectColors.grass + public var tileHighlightColor: SKColor = TiledObjectColors.lime + public var objectFillOpacity: CGFloat = 0.25 + public var objectHighlightColor: SKColor = TiledObjectColors.coral + public var navigationColor: SKColor = TiledObjectColors.azure + + /** + ### Overview ### + + Global debug display mouse filter options (macOS). + + #### Properties #### + + | Property | Description | + |:----------------------|:-----------------------------------------| + | tileCoordinates | Show tile coordinates. | + | sceneCoordinates | Show scene coordinates. | + | tileDataUnderCursor | Show tile data properties. | + | tilesUnderCursor | Highlight tiles under the cursor. | + | objectsUnderCursor | Highlight objects under the cursor. | + + */ + public struct MouseFilters: OptionSet { + public let rawValue: Int + + static let tileCoordinates = MouseFilters(rawValue: 1 << 0) // 1* + static let tileLocalID = MouseFilters(rawValue: 1 << 1) // 2 + static let sceneCoordinates = MouseFilters(rawValue: 1 << 2) // 4 + static let tileDataUnderCursor = MouseFilters(rawValue: 1 << 3) // 8* + static let tilesUnderCursor = MouseFilters(rawValue: 1 << 4) // 16 + static let objectsUnderCursor = MouseFilters(rawValue: 1 << 5) // 32 + + static public let all: MouseFilters = [.tileCoordinates, .tileLocalID, .sceneCoordinates, .tileDataUnderCursor, .tilesUnderCursor, .objectsUnderCursor] + + public init(rawValue: Int = 0) { + self.rawValue = rawValue + } + } + } + + /** + ## Overview ## + + Display flag for render statistics. + + ### Properties ## + + | Property | Description | + |:----------------------|:-----------------------------------------| + | milliseconds | Show render time in milliseconds. | + | seconds | Show render time in seconds. | + + */ + public enum TimeDisplayMode: Int { + case milliseconds + case seconds + } + + /** + ## Overview ## + + Indicates the current renderer (OpenGL or Metal). + + ### Properties ## + + | Property | Description | + |:---------|:----------------------------------------------------| + | opengl | Indicates the current SpriteKit renderer is OpenGL. | + | metal | Indicates the current SpriteKit renderer is Metal. | + + */ + public enum Renderer { + case opengl + case metal + } +} + + +internal struct TiledObjectColors { + static let azure: SKColor = SKColor(hexString: "#4A90E2") + static let coral: SKColor = SKColor(hexString: "#FD4444") + static let crimson: SKColor = SKColor(hexString: "#D0021B") + static let dandelion: SKColor = SKColor(hexString: "#F8E71C") + static let english: SKColor = SKColor(hexString: "#AF3E4D") + static let grass: SKColor = SKColor(hexString: "#B8E986") + static let gun: SKColor = SKColor(hexString: "#8D99AE") + static let indigo: SKColor = SKColor(hexString: "#274060") + static let lime: SKColor = SKColor(hexString: "#7ED321") + static let magenta: SKColor = SKColor(hexString: "#FF00FF") + static let metal: SKColor = SKColor(hexString: "#627C85") + static let obsidian: SKColor = SKColor(hexString: "#464B4E") + static let pear: SKColor = SKColor(hexString: "#CEE82C") + static let saffron: SKColor = SKColor(hexString: "#F28123") + static let tangerine: SKColor = SKColor(hexString: "#F5A623") + static let turquoise: SKColor = SKColor(hexString: "#44CFCB") +} + + +// MARK: - Extensions + + +extension TiledGlobals: CustomDebugReflectable { + + func dumpStatistics() { + print("\n----------- SKTiled Globals -----------") + print(" - framework version: \(self.version.description)") + print(" - swift version: \(getSwiftVersion())") + + if let buildVersion = self.build { + print(" - build version: \(buildVersion)") + } + + print(" - renderer: \(self.renderer.name)") + print(" - ui scale: \(self.contentScale)") + print(" - logging level: \(self.loggingLevel)") + print(" - update mode: \(self.updateMode.name)") + print(" - render callbacks: \(self.enableRenderCallbacks)") + print(" - camera callbacks: \(self.enableCameraCallbacks)\n") + print(" - Debug Display: ") + print(" - highlight duration: \(self.debug.highlightDuration)") + print(" - grid opacity: \(self.debug.gridOpactity)") + print(" - object fill opacity: \(self.debug.objectFillOpacity)") + print(" - grid color: \(self.debug.gridColor.hexString())\n") + print(" - Render Quality: ") + print(" - default: \(self.renderQuality.default)") + print(" - object: \(self.renderQuality.object)") + print(" - text: \(self.renderQuality.text)") + print(self.renderQuality.override > 0 ? " - override: \(self.renderQuality.override)\n" : "") + print(" - Debug Mouse Filters:") + print(" - raw value: \(self.debug.mouseFilters.rawValue)") + print(" - tile coordinates: \(self.debug.mouseFilters.contains(.tileCoordinates))") + print(" - scene coordinates: \(self.debug.mouseFilters.contains(.sceneCoordinates))") + print(" - tile data: \(self.debug.mouseFilters.contains(.tileDataUnderCursor))") + print(" - highlight tiles: \(self.debug.mouseFilters.contains(.tilesUnderCursor))") + print(" - highlight objects: \(self.debug.mouseFilters.contains(.objectsUnderCursor))") + print("\n---------------------------------------\n") + } +} + + + +extension TiledGlobals.Version { + /** + Initialize with a string (ie "2.1.4"). + */ + init(_ value: String) { + let parts = value.split(separator: ".").compactMap { Int($0) } + switch parts.count { + case 1: + self.major = parts.first! + case 2: + self.major = parts.first! + self.minor = parts[1] + case 3: + self.major = parts.first! + self.minor = parts[1] + self.patch = parts[2] + default: + self.major = 1 + self.minor = 0 + self.patch = 0 + } + } +} + + +extension TiledGlobals.Version: CustomStringConvertible, CustomDebugStringConvertible { + /// String description of the framework version. + public var description: String { return "\(major).\(minor)\(patch > 0 ? ".\(patch)" : "")" } + /// String description of the framework version. + public var debugDescription: String { return self.description } +} + + +extension TiledGlobals.TimeDisplayMode { + + var allModes: [TiledGlobals.TimeDisplayMode] { + return [.seconds, .milliseconds] + } + + var uiControlString: String { + switch self { + case .seconds: return "Seconds" + case .milliseconds: return "Milliseconds" + } + } +} + + + +extension TiledGlobals.Renderer { + + var name: String { + switch self { + case .opengl: return "OpenGL" + case .metal: return "Metal" + } + } +} + + +extension TiledGlobals.DebugDisplayOptions.MouseFilters { + + public var strings: [String] { + var result: [String] = [] + if self.contains(.tileCoordinates) { + result.append("Tile Coordinates") + } + + if self.contains(.tileLocalID) { + result.append("Tile Local ID") + } + + if self.contains(.sceneCoordinates) { + result.append("Scene Coordinates") + } + + if self.contains(.tileDataUnderCursor) { + result.append("Tile Data") + } + + if self.contains(.tilesUnderCursor) { + result.append("Tiles Under Cursor") + } + + if self.contains(.objectsUnderCursor) { + result.append("Objects Under Cursor") + } + + return result + } + +} + + +extension TiledObjectColors { + /// Returns an array of all colors. + static let all: [SKColor] = [azure, coral, crimson, dandelion, + english, grass, gun, indigo, lime, + magenta, metal, obsidian, pear, + saffron, tangerine, turquoise] + + /// Returns an array of all color names. + static let names: [String] = ["azure", "coral", "crimson","dandelion", + "english","grass","gun","indigo","lime", + "magenta","metal","obsidian","pear", + "saffron","tangerine","turquoise"] + /// Returns a random color. + static var random: SKColor { + let randIndex = Int(arc4random_uniform(UInt32(TiledObjectColors.all.count))) + return TiledObjectColors.all[randIndex] + } +} diff --git a/Sources/SKTiledObject.swift b/Sources/SKTiledObject.swift index c42b6114..c68966f2 100644 --- a/Sources/SKTiledObject.swift +++ b/Sources/SKTiledObject.swift @@ -12,12 +12,31 @@ import SpriteKit ## Overview ## - The `SKTiledObject` protocol defines a basic data structure for mapping custom Tiled - properties to SpriteKit objects. Objects conforming to this protocol will - automatically receive properties from the Tiled scene, unless supressed - by setting the object's `ignoreProperties` property. + The `SKTiledObject` protocol defines a basic data structure for mapping custom Tiled + properties to SpriteKit objects. Objects conforming to this protocol will + automatically receive properties from the Tiled scene, unless supressed + by setting the object's `SKTiledObject.ignoreProperties` property. - ## Usage ## + + ### Properties ### + + | Property | Description | + |:-----------------|:------------------------------------------------| + | uuid | Unique object id. | + | type | Tiled object type. | + | properties | Object of custom Tiled properties. | + | ignoreProperties | Ignore Tiled properties. | + | renderQuality | Resolution multiplier value. | + + + ### Instance Methods ### + + | Method | Description | + |:-----------------|:------------------------------------------------| + | parseProperties | Parse function (with optional completion block).| + + + ### Usage ### ```swift // query a Tiled string property @@ -29,17 +48,9 @@ import SpriteKit let isDynamic = tiledObject.boolForKey("isDynamic") == true ``` - ### Properties ### - - ```swift - SKTiledObject.uuid // unique object id. - SKTiledObject.type // object type. - SKTiledObject.properties // dictionary of object properties. - SKTiledObject.ignoreProperties // ignore custom properties. - SKTiledObject.renderQuality // resolution multiplier value. - ``` + [sktiledobject-url]:Protocols/SKTiledObject.html */ -public protocol SKTiledObject: class, Loggable { +@objc public protocol SKTiledObject: class { /// Unique object id (layer & object names may not be unique). var uuid: String { get set } /// Object type. @@ -49,7 +60,7 @@ public protocol SKTiledObject: class, Loggable { /// Ignore custom properties. var ignoreProperties: Bool { get set } /// Parse function (with optional completion block). - func parseProperties(completion: (() -> ())?) + func parseProperties(completion: (() -> Void)?) /// Render scaling property. var renderQuality: CGFloat { get } } @@ -57,10 +68,28 @@ public protocol SKTiledObject: class, Loggable { public extension SKTiledObject { - public var hashValue: Int { return uuid.hashValue } // MARK: - Properties Parsing + + /** + Return a string value for the given key, if it exists. + + ### Usage + + ```swift + if let name = tileData["name"] { + print("tile data is named \"\(name)\"") + } + ``` + + - parameter key: `String` key to query. + - returns: `String?` value for the given key. + */ + public subscript(key: String) -> String? { + return (ignoreProperties == false) ? properties[key] : nil + } + /** Returns true if the node has stored properties. @@ -71,18 +100,32 @@ public extension SKTiledObject { } /** - Returns true if the node has the given property (not case sensitive). + Returns true if the node has the given SKTiled property (case insensitive). - parameter key: `String` key to query. - returns: `Bool` properties has a value for the key. */ public func hasKey(_ key: String) -> Bool { let pnames = properties.keys.map { $0.lowercased() } - return pnames.contains(key.lowercased()) + //return pnames.contains(key.lowercased()) + return !pnames.filter { $0 == key.lowercased()}.isEmpty + } + + /** + Returns a boolean value for the given key. + + - parameter key: `String` properties key. + - returns: `Bool` value for properties key. + */ + public func boolForKey(_ key: String) -> Bool { + if let existingPair = keyValuePair(key: key) { + return Bool(existingPair.value) ?? false || Int(existingPair.value) == 1 + } + return false } /** - Returns a string for the given key. + Returns a string for the given SKTiled key. - parameter key: `String` properties key. - returns: `String` value for properties key. @@ -97,7 +140,33 @@ public extension SKTiledObject { } /** - Sets a named property. Returns the value, or nil if it does not exist. + Returns a integer value for the given key. + + - parameter key: `String` properties key. + - returns: `Int?` value for properties key. + */ + public func intForKey(_ key: String) -> Int? { + if let existingPair = keyValuePair(key: key) { + return Int(existingPair.value) + } + return nil + } + + /** + Returns a float value for the given key. + + - parameter key: `String` properties key. + - returns: `Double?` value for properties key. + */ + public func doubleForKey(_ key: String) -> Double? { + if let existingPair = keyValuePair(key: key) { + return Double(existingPair.value) + } + return nil + } + + /** + Sets a named SKTiled property. Returns the value, or nil if it does not exist. - parameter key: `String` property key. - parameter value: `String` property value. @@ -111,7 +180,7 @@ public extension SKTiledObject { } /** - Remove a named property, returns the value as a string (if property exists). + Remove a named SKTiled property, returns the value as a string (if property exists). - parameter key: `String` property key. - returns: `String?` property value (if it exists). @@ -132,6 +201,7 @@ public extension SKTiledObject { }) } + // MARK: - Helpers /** @@ -142,7 +212,7 @@ public extension SKTiledObject { */ internal func keyValuePair(key: String) -> (key: String, value: String)? { for k in properties.keys { - if k.lowercased() == key.lowercased() { + if (k.lowercased() == key.lowercased()) { return (key: k, value: properties[k]!) } } @@ -171,24 +241,13 @@ public extension SKTiledObject { */ internal func stringArrayForKey(_ key: String, separatedBy: String=",") -> [String] { if let existingPair = keyValuePair(key: key) { - return existingPair.value.components(separatedBy: separatedBy) + return existingPair.value.components(separatedBy: separatedBy).map { + $0.trimmingCharacters(in: CharacterSet.whitespaces) + } } return [String]() } - /** - Returns a integer value for the given key. - - - parameter key: `String` properties key. - - returns: `Int?` value for properties key. - */ - internal func intForKey(_ key: String) -> Int? { - if let existingPair = keyValuePair(key: key) { - return Int(existingPair.value) - } - return nil - } - /** Returns a integer array for the given key. @@ -198,24 +257,14 @@ public extension SKTiledObject { */ internal func integerArrayForKey(_ key: String, separatedBy: String=",") -> [Int] { if let existingPair = keyValuePair(key: key) { - return existingPair.value.components(separatedBy: separatedBy).flatMap { Int($0) } + return existingPair.value.components(separatedBy: ",").map { + $0.trimmingCharacters(in: CharacterSet.whitespaces)}.compactMap { + Int($0) + } } return [Int]() } - /** - Returns a float value for the given key. - - - parameter key: `String` properties key. - - returns: `Double?` value for properties key. - */ - internal func doubleForKey(_ key: String) -> Double? { - if let existingPair = keyValuePair(key: key) { - return Double(existingPair.value) - } - return nil - } - /** Returns a double array for the given key. @@ -225,21 +274,11 @@ public extension SKTiledObject { */ internal func doubleArrayForKey(_ key: String, separatedBy: String=",") -> [Double] { if let existingPair = keyValuePair(key: key) { - return existingPair.value.components(separatedBy: separatedBy).flatMap { Double($0) } + return existingPair.value.components(separatedBy: ",").map { + $0.trimmingCharacters(in: CharacterSet.whitespaces)}.compactMap { + Double($0) + } } return [Double]() } - - /** - Returns a boolean value for the given key. - - - parameter key: `String` properties key. - - returns: `Bool` value for properties key. - */ - internal func boolForKey(_ key: String) -> Bool { - if let existingPair = keyValuePair(key: key) { - return Bool(existingPair.value) ?? false || Int(existingPair.value) == 1 - } - return false - } } diff --git a/Sources/SKTiledScene.swift b/Sources/SKTiledScene.swift index a7b29f01..bc4e11c9 100644 --- a/Sources/SKTiledScene.swift +++ b/Sources/SKTiledScene.swift @@ -13,14 +13,35 @@ import GameplayKit /** ## Overview ## - Delegate for managing `SKTilemap` nodes in an SpriteKit [`SKScene`][skscene-url] scene. + Methods for managing `SKTilemap` nodes in an SpriteKit [`SKScene`][skscene-url] scene. This protocol and the `SKTiledScene` objects are included as a suggested way to use the `SKTilemap` class, but are not required. In this configuration, the tile map is a child of the root node and reference the custom `SKTiledSceneCamera` camera. + ![SKTiledSceneDelegate Overview][sktiledscenedelegate-image-url] + + ### Properties ### + + | Property | Description | + |:---------------------|:-------------------------------------------------------------| + | worldNode | Root container node. Tiled assets are parented to this node. | + | cameraNode | Custom scene camera. | + | tilemap | Tile map node. | + + + ### Instance Methods ### + + | Method | Description | + |:------------------------------------|:--------------------------| + | [load(tmxFile:)][delegate-load-url] | Load a tilemap from disk. | + + + + [delegate-load-url]:SKTiledSceneDelegate.html#load(tmxFile:inDirectory:withTilesets:ignoreProperties:loggingLevel:) [skscene-url]:https://developer.apple.com/reference/spritekit/skscene + [sktiledscenedelegate-image-url]:https://mfessenden.github.io/SKTiled/images/scene-hierarchy.svg */ public protocol SKTiledSceneDelegate: class { /// Root container node. Tiled assets are parented to this node. @@ -30,7 +51,8 @@ public protocol SKTiledSceneDelegate: class { /// Tile map node. var tilemap: SKTilemap! { get set } /// Load a tilemap from disk, with optional tilesets. - func load(tmxFile: String, inDirectory: String?, withTilesets tilesets: [SKTileset], + func load(tmxFile: String, inDirectory: String?, + withTilesets tilesets: [SKTileset], ignoreProperties: Bool, loggingLevel: LoggingLevel) -> SKTilemap? } @@ -41,19 +63,35 @@ public protocol SKTiledSceneDelegate: class { Custom scene type for managing `SKTilemap` nodes. - Conforms to the `SKTiledSceneDelegate` & `SKTilemapDelegate` protocols. + Conforms to the `SKTiledSceneDelegate`, `SKTilemapDelegate` & `SKTilesetDataSource` protocols. + + [SKTiledScene Hierarchy][sktiledscene-hierarchy-img-url] + + ### Properties ### + + | Property | Description | + |:-----------|:---------------------| + | worldNode | Root container node. | + | tilemap | Tile map object. | + | cameraNode | Custom scene camera. | + + + ### Instance Methods ### - ### Properties: ### + | Method | Description | + |:-----------------------|:-------------------------------------------------------------| + | sceneDoubleTapped | Called when the scene receives a double-tap event (iOS only).| + | cameraPositionChanged | Called when the camera positon changes. | + | cameraZoomChanged | Called when the camera zoom changes. | + | cameraBoundsChanged | Called when the camera bounds updated. | + | sceneDoubleClicked | Called when the scene is double-clicked (macOS only). | + | mousePositionChanged | Called when the mouse moves in the scene (macOS only). | - ``` - SKTiledScene.worldNode: `SKNode!` root container node. - SKTiledScene.tilemap: `SKTilemap!` tile map object. - SKTiledScene.cameraNode: `SKTiledSceneCamera!` custom scene camera. - ``` + [sktiledscene-hierarchy-img-url]:https://mfessenden.github.io/SKTiled/images/scene_hierarchy.png */ -open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate, SKTilemapDelegate, Loggable { +open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate, SKTilemapDelegate, SKTilesetDataSource { - /// Root container node. + /// World container node. open var worldNode: SKNode! /// Tile map node. open var tilemap: SKTilemap! @@ -68,6 +106,9 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate private var lastUpdateTime: TimeInterval = 0 private let maximumUpdateDelta: TimeInterval = 1.0 / 60.0 + /// Receive notifications from camera. + open var receiveCameraUpdates: Bool = true + /// Set the tilemap speed override open var speed: CGFloat { didSet { @@ -79,13 +120,14 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate } // MARK: - Init + /** Initialize without a tiled map. - parameter size: `CGSize` scene size. - returns: `SKTiledScene` scene. */ - required public override init(size: CGSize) { + required override public init(size: CGSize) { super.init(size: size) } @@ -103,10 +145,12 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate // setup world node worldNode = SKNode() + worldNode.name = "World" addChild(worldNode) // setup the camera cameraNode = SKTiledSceneCamera(view: view, world: worldNode) + // scene should notified of camera changes cameraNode.addDelegate(self) addChild(cameraNode) camera = cameraNode @@ -116,17 +160,17 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate /** Load and setup a named TMX file, with optional tilesets. - - parameter url: `URL` Tiled file url. - - parameter withTilesets: `[SKTileset]` pre-loaded tilesets. + - parameter url: `URL` Tiled file url. + - parameter withTilesets: `[SKTileset]` pre-loaded tilesets. - parameter ignoreProperties: `Bool` don't parse custom properties. - parameter loggingLevel: `LoggingLevel` logging verbosity. - - parameter completion: `(() -> ())?` optional completion handler. + - parameter completion: `((_ SKTilemap) -> Void)?` optional completion handler. */ open func setup(url: URL, - withTilesets: [SKTileset]=[], + withTilesets: [SKTileset] = [], ignoreProperties: Bool = false, - loggingLevel: LoggingLevel = .info, - _ completion: (() -> ())? = nil) { + loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel, + _ completion: ((_ tilemap: SKTilemap) -> Void)? = nil) { let dirname = url.deletingLastPathComponent() let filename = url.lastPathComponent @@ -141,7 +185,6 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate } - /** Load and setup a named TMX file, with optional tilesets. Allows for an optional completion handler. @@ -150,16 +193,19 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate - parameter withTilesets: `[SKTileset]` optional pre-loaded tilesets. - parameter ignoreProperties: `Bool` don't parse custom properties. - parameter loggingLevel: `LoggingLevel` logging verbosity. - - parameter completion: `(() -> ())?` optional completion handler. + - parameter completion: `((_ SKTilemap) -> Void)?` optional completion handler. */ open func setup(tmxFile: String, inDirectory: String? = nil, - withTilesets tilesets: [SKTileset]=[], + withTilesets tilesets: [SKTileset] = [], ignoreProperties: Bool = false, - loggingLevel: LoggingLevel = .info, - _ completion: (() -> ())? = nil) { + loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel, + _ completion: ((_ tilemap: SKTilemap) -> Void)? = nil) { - guard let worldNode = worldNode else { return } + guard let worldNode = worldNode else { + self.log("cannot access world node.", level: .error) + return + } self.loggingLevel = loggingLevel self.tilemap = nil @@ -173,8 +219,10 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate backgroundColor = tilemap.backgroundColor ?? SKColor.clear // add the tilemap to the world container node. - worldNode.addChild(tilemap) + worldNode.addChild(tilemap, fadeIn: 0.2) self.tilemap = tilemap + + // tilemap will be notified of camera changes cameraNode.addDelegate(self.tilemap) // apply gravity from the tile map @@ -194,11 +242,11 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate } // run completion handler - completion?() + completion?(tilemap) } } - // MARK: - Delegate Callbacks + // MARK: - Tilemap Delegate open func didBeginParsing(_ tilemap: SKTilemap) { // Called when tilemap is instantiated. @@ -232,11 +280,24 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate return SKTileObject.self } + // MARK: - Tileset Delegate + + open func willAddSpriteSheet(to tileset: SKTileset, fileNamed: String) -> String { + // Called when a tileset is about to add a spritesheet image. + return fileNamed + } + + + open func willAddImage(to tileset: SKTileset, forId: Int, fileNamed: String) -> String { + // Called when a tileset is about to add an image to a collection. + return fileNamed + } + // MARK: - Updating /** Called before each frame is rendered. - + - parameter currentTime: `TimeInterval` update interval. */ override open func update(_ currentTime: TimeInterval) { @@ -259,11 +320,11 @@ open class SKTiledScene: SKScene, SKPhysicsContactDelegate, SKTiledSceneDelegate Update the camera bounds. */ open func updateCamera() { - guard let view = view else { return } - let viewSize = view.bounds.size + guard (view != nil) else { return } + + // update camera bounds if let cameraNode = cameraNode { - cameraNode.bounds = CGRect(x: -(viewSize.width / 2), y: -(viewSize.height / 2), - width: viewSize.width, height: viewSize.height) + cameraNode.setCameraBounds(bounds: CGRect(x: -(size.width / 2), y: -(size.height / 2), width: size.width, height: size.height)) } } } @@ -283,14 +344,15 @@ extension SKTiledSceneDelegate where Self: SKScene { */ public func load(tmxFile: String, inDirectory: String? = nil, - withTilesets tilesets: [SKTileset]=[], + withTilesets tilesets: [SKTileset] = [], ignoreProperties: Bool = false, - loggingLevel: LoggingLevel = .info) -> SKTilemap? { + loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel) -> SKTilemap? { if let tilemap = SKTilemap.load(tmxFile: tmxFile, inDirectory: inDirectory, delegate: self as? SKTilemapDelegate, + tilesetDataSource: self as? SKTilesetDataSource, withTilesets: tilesets, ignoreProperties: ignoreProperties, loggingLevel: loggingLevel) { @@ -300,7 +362,7 @@ extension SKTiledSceneDelegate where Self: SKScene { cameraNode.allowMovement = tilemap.allowMovement cameraNode.allowZoom = tilemap.allowZoom cameraNode.setCameraZoom(tilemap.worldScale) - cameraNode.maxZoom = tilemap.maxZoom + cameraNode.maxZoom = tilemap.zoomConstraints.max } return tilemap @@ -320,7 +382,7 @@ extension SKTiledScene { override open func mouseUp(with event: NSEvent) {} override open func mouseEntered(with event: NSEvent) {} override open func mouseExited(with event: NSEvent) {} - + override open func scrollWheel(with event: NSEvent) { guard let cameraNode = cameraNode else { return } cameraNode.scrollWheel(with: event) @@ -332,20 +394,29 @@ extension SKTiledScene { // default methods extension SKTiledScene: SKTiledSceneCameraDelegate { + #if os(iOS) + /** + Called when the scene receives a double-tap event (iOS only). + + - parameter location: `CGPoint` touch event location. + */ + public func sceneDoubleTapped(location: CGPoint) {} + #endif + // MARK: - Delegate Methods /** Called when the camera positon changes. - parameter newPositon: `CGPoint` updated camera position. */ - public func cameraPositionChanged(newPosition: CGPoint) {} + @objc public func cameraPositionChanged(newPosition: CGPoint) {} /** Called when the camera zoom changes. - parameter newZoom: `CGFloat` camera zoom amount. */ - public func cameraZoomChanged(newZoom: CGFloat) {} + @objc public func cameraZoomChanged(newZoom: CGFloat) {} /** Called when the camera bounds updated. @@ -354,29 +425,24 @@ extension SKTiledScene: SKTiledSceneCameraDelegate { - parameter positon: `CGPoint` camera position. - parameter zoom: `CGFloat` camera zoom amount. */ - public func cameraBoundsChanged(bounds: CGRect, position: CGPoint, zoom: CGFloat) {} + @objc public func cameraBoundsChanged(bounds: CGRect, position: CGPoint, zoom: CGFloat) {} - #if os(iOS) || os(tvOS) + #if os(macOS) /** - Called when the scene is double-tapped. (iOS only) - - - parameter location: `CGPoint` touch location. - */ - public func sceneDoubleTapped(location: CGPoint) {} - #else - - /** - Called when the scene is double-clicked. (macOS only) + Called when the scene is double-clicked (macOS only). - parameter event: `NSEvent` mouse click event. */ - public func sceneDoubleClicked(event: NSEvent) {} + @objc public func sceneDoubleClicked(event: NSEvent) {} /** - Called when the mouse moves in the scene. (macOS only) - - - parameter event: `NSEvent` mouse event. + Called when the mouse moves in the scene (macOS only). + + - parameter event: `NSEvent` mouse click event. */ - public func mousePositionChanged(event: NSEvent) {} + @objc public func mousePositionChanged(event: NSEvent) {} #endif } + + +extension SKTiledScene: Loggable {} diff --git a/Sources/SKTiledSceneCamera.swift b/Sources/SKTiledSceneCamera.swift index 14d6917e..db923453 100644 --- a/Sources/SKTiledSceneCamera.swift +++ b/Sources/SKTiledSceneCamera.swift @@ -13,28 +13,67 @@ import UIKit import Cocoa #endif + /** ## Overview ## - Delegate for interacting with `SKTiledSceneCamera`. Classes conforming to this - protocol are notified of camera position & zoom changes. + Methods for interacting with the custom `SKTiledSceneCamera`. Classes conforming to this + protocol are notified of camera position & zoom changes - unless the `SKTiledSceneCameraDelegate.receiveCameraUpdates` + flag is disabled. + + ![Tiled Scene Camera Delegate][tiled-scene-camera-delegate-image] + + ### Properties ### + + | Method | Description | + |---------------------------|----------------------------------------------------------| + | receiveCameraUpdates | Delegate will receive camera updates. | + + + ### Instance Methods ### + + | Method | Description | + |---------------------------|----------------------------------------------------------| + | containedNodesChanged | Called when the nodes in the camera view changes. | + | cameraPositionChanged | Called when the camera positon changes. | + | cameraZoomChanged | Called when the camera zoom changes. | + | cameraBoundsChanged | Called when the camera bounds updated. | + | sceneDoubleClicked | Called when the scene is double-clicked. (macOS only) | + | mousePositionChanged | Called when the mouse moves in the scene. (macOS only) | + | sceneDoubleTapped | Called when the scene is double-tapped. (iOS only) | + + + [tiled-scene-camera-delegate-image]:https://mfessenden.github.io/SKTiled/images/camera-delegate.svg + */ -public protocol SKTiledSceneCameraDelegate: class { - +@objc public protocol SKTiledSceneCameraDelegate: class { + + /** + Allow delegate to receive updates from camera. + */ + @objc var receiveCameraUpdates: Bool { get set } + + /** + Allow delegates to receive updates when nodes in view change. + + - parameter nodes: `[SKNode]` nodes in camera view. + */ + @objc optional func containedNodesChanged(_ nodes: Set) + /** Called when the camera positon changes. - parameter newPositon: `CGPoint` updated camera position. */ - func cameraPositionChanged(newPosition: CGPoint) + @objc optional func cameraPositionChanged(newPosition: CGPoint) /** Called when the camera zoom changes. - parameter newZoom: `CGFloat` camera zoom amount. */ - func cameraZoomChanged(newZoom: CGFloat) + @objc optional func cameraZoomChanged(newZoom: CGFloat) /** Called when the camera bounds updated. @@ -43,56 +82,105 @@ public protocol SKTiledSceneCameraDelegate: class { - parameter positon: `CGPoint` camera position. - parameter zoom: `CGFloat` camera zoom amount. */ - func cameraBoundsChanged(bounds: CGRect, position: CGPoint, zoom: CGFloat) + @objc optional func cameraBoundsChanged(bounds: CGRect, position: CGPoint, zoom: CGFloat) - #if os(iOS) || os(tvOS) + #if os(macOS) /** - Called when the scene is double-tapped. (iOS only) + Called when the scene is double-clicked (macOS only). - - parameter location: `CGPoint` touch location. + - parameter event: `NSEvent` mouse click event. */ - func sceneDoubleTapped(location: CGPoint) - #else + @objc optional func sceneDoubleClicked(event: NSEvent) /** - Called when the scene is double-clicked. (macOS only) + Called when the mouse moves in the scene (macOS only). - - parameter NSEvent: `NSEvent` click event. + - parameter event: `NSEvent` mouse click event. */ - func sceneDoubleClicked(event: NSEvent) + @objc optional func mousePositionChanged(event: NSEvent) + #endif + + #if os(iOS) /** - Called when the mouse moves in the scene. (macOS only) + Called when the scene receives a double-tap event (iOS only). - - parameter event: `NSEvent` mouse event. + - parameter location: `CGPoint` touch event location. */ - func mousePositionChanged(event: NSEvent) + @objc optional func sceneDoubleTapped(location: CGPoint) #endif } + /** - Camera zoom rounding factor. Clamps the zoom value to nearest quarter or half percentage. + + ## Overview ## + + Camera zoom rounding factor. Clamps the zoom value to the nearest whole pixel value in order to alleviate cracks appearing in between individual tiles. + + ### Properties ### + + | Property | Description | + |----------|---------------------------------------------| + | none | Do not clamp camera zoom. | + | half | Clamp zoom to the nearest half-pixel. | + | third | Clamp zoom to the nearest third of a pixel. | + */ public enum CameraZoomClamping: CGFloat { case none = 0 case half = 2 + case third = 3 case quarter = 4 case tenth = 10 } +/** + + ## Overview ## + + Determines how an attached controller interacts with the camera. + + ### Properties ### + + | Property | Description | + |----------|---------------------------------------------| + | none | Controller does not affect camera. | + | dolly | Controller pans the camera. | + | zoom | Controller changes the camera zoom. | + + */ +public enum CameraControlMode: Int { + case none + case dolly + case zoom +} + /** ## Overview ## Custom scene camera that responds to finger gestures and mouse events. - The `SKTiledSceneCamera` is a custom camera meant to be used with a scene conforming to the `SKTiledSceneDelegate` protocol. - The camera defines a position in the scene to render the scene from, with a reference to the `SKTiledSceneDelegate.worldNode` + The `SKTiledSceneCamera` is a custom camera meant to be used with a scene conforming to the `SKTiledSceneDelegate` protocol. + The camera defines a position in the scene to render the scene from, with a reference to the `SKTiledSceneDelegate.worldNode` to interact with tile maps. + ### Properties ### + + | Property | Description | + |---------------|-------------------------------------------------------------------| + | world | World container node. | + | delegates | Array of delegates to notify about camera updates. | + | zoom | Camera zoom value. | + | allowMovement | Toggle to allow camera movement. | + | minZoom | Minimum zoom value. | + | maxZoom | Maximum zoom value. | + | zoomClamping | Clamping factor used to alleviate render artifacts like cracking. | + */ -public class SKTiledSceneCamera: SKCameraNode, Loggable { +public class SKTiledSceneCamera: SKCameraNode { unowned let world: SKNode internal var bounds: CGRect @@ -110,19 +198,55 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { // zoom constraints public var minZoom: CGFloat = 0.2 public var maxZoom: CGFloat = 5.0 + /// Ignore mix/max zoom constraints + public var ignoreZoomConstraints: Bool = false public var isAtMaxZoom: Bool { return zoom == maxZoom } - /// Clamp factor to alleviate cracks in tilemap. - public var zoomClamping: CameraZoomClamping = .none { + + /// Update delegates on visible node changes. + public var notifyDelegatesOnContainedNodesChange: Bool = true + + /// Contained nodes + public var containedNodes: [SKNode] { + return containedNodeSet().filter { node in + return (node as? SKTiledGeometry != nil) + } + } + + // camera control mode (tvOS) + public var controlMode: CameraControlMode = CameraControlMode.none { + didSet { + + NotificationCenter.default.post( + name: Notification.Name.Camera.Updated, + object: self, + userInfo: ["cameraInfo": self.description, + "cameraControlMode": self.controlMode] + ) + } + } + + /// Clamping factor used to alleviate render artifacts like cracking. + public var zoomClamping: CameraZoomClamping = CameraZoomClamping.none { didSet { setCameraZoom(self.zoom) + + NotificationCenter.default.post( + name: Notification.Name.Camera.Updated, + object: self, + userInfo: nil + ) } } + /// Flag to ignore zoom clamping. + public var ignoreZoomClamping: Bool = true + // logger public var loggingLevel: LoggingLevel = .info // gestures - #if os(iOS) || os(tvOS) + + #if os(iOS) /// Gesture recognizer to recognize camera panning public var cameraPanned: UIPanGestureRecognizer! /// Gesture recognizer to recognize double taps @@ -131,12 +255,26 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { public var cameraPinched: UIPinchGestureRecognizer! #endif + /// Turn off to not respond to gestures + public var allowGestures: Bool = false { + didSet { + guard oldValue != allowGestures else { return } + #if os(iOS) + cameraPanned.isEnabled = allowGestures + sceneDoubleTapped.isEnabled = allowGestures + cameraPinched.isEnabled = allowGestures + #endif + } + } + // locations fileprivate var focusLocation: CGPoint = CGPoint.zero fileprivate var lastLocation: CGPoint! // quick & dirty overlay node internal let overlay: SKNode = SKNode() + + /// Flag to show the overlay public var showOverlay: Bool = true { didSet { guard oldValue != showOverlay else { return } @@ -160,32 +298,62 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { addChild(overlay) overlay.isHidden = true - #if os(iOS) || os(tvOS) + #if os(iOS) + setupGestures(for: view) + #endif + + if let clampingMode = CameraZoomClamping(rawValue: TiledGlobals.default.contentScale) { + zoomClamping = clampingMode + } + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + #if os(iOS) + // MARK: - Gestures + + /** + Setup gesture recognizers for navigating the scene. + + - parameter skView: `SKView` current SpriteKit view. + */ + public func setupGestures(for skView: SKView) { // setup pan recognizer cameraPanned = UIPanGestureRecognizer(target: self, action: #selector(cameraPanned(_:))) cameraPanned.minimumNumberOfTouches = 1 cameraPanned.maximumNumberOfTouches = 1 - view.addGestureRecognizer(cameraPanned) - + skView.addGestureRecognizer(cameraPanned) + cameraPanned.isEnabled = allowGestures sceneDoubleTapped = UITapGestureRecognizer(target: self, action: #selector(sceneDoubleTapped(_:))) sceneDoubleTapped.numberOfTapsRequired = 2 - view.addGestureRecognizer(sceneDoubleTapped) + skView.addGestureRecognizer(sceneDoubleTapped) + sceneDoubleTapped.isEnabled = allowGestures // setup pinch recognizer cameraPinched = UIPinchGestureRecognizer(target: self, action: #selector(scenePinched(_:))) - view.addGestureRecognizer(cameraPinched) - #endif - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + skView.addGestureRecognizer(cameraPinched) + cameraPinched.isEnabled = allowGestures } + #endif // MARK: - Delegates /** - Add a camera delegate. + Enable/disable camera callbacks for all delegates. + + - parameter value: `Bool` enable delegate callbacks. + */ + public func enableDelegateCallbacks(_ value: Bool) { + for delegate in self.delegates { + delegate.receiveCameraUpdates = value + } + } + + /** + Add a camera delegate to allow it to be notified of camera changes. - parameter delegate: `SKTiledSceneCameraDelegate` camera delegate. */ @@ -207,6 +375,19 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { } } + /** + Update delegates with the contained nodes array. + */ + internal func updateContainedNodes() { + for delegate in self.delegates { + guard (delegate.receiveCameraUpdates == true) && (notifyDelegatesOnContainedNodesChange == true) else { continue } + + DispatchQueue.main.async { + delegate.containedNodesChanged?(self.containedNodeSet()) + } + } + } + // MARK: - Overlay /** Add an overlay node. @@ -223,14 +404,16 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { - parameter scale: `CGFloat` zoom amount. */ - public func setCameraZoom(_ scale: CGFloat, interval: TimeInterval=0) { + public func setCameraZoom(_ scale: CGFloat, interval: TimeInterval = 0) { + // clamp scaling between min/max zoom - var zoomClamped = scale.clamped(minZoom, maxZoom) + var zoomClamped = (ignoreZoomConstraints == true) ? scale.clamped(minZoom, maxZoom) : scale - // round zoom value to alleviate cracking - zoomClamped = clampZoomValue(zoomClamped, factor: zoomClamping.rawValue) + // round zoom value to alleviate artifact + zoomClamped = (ignoreZoomClamping == false) ? clampZoomValue(zoomClamped, factor: zoomClamping.rawValue) : scale self.zoom = zoomClamped + let zoomAction = SKAction.scale(to: zoomClamped, duration: interval) if (interval == 0) { @@ -245,8 +428,10 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { // notify delegates for delegate in delegates { - delegate.cameraZoomChanged(newZoom: zoomClamped) + delegate.cameraZoomChanged?(newZoom: zoomClamped) } + + self.updateContainedNodes() } /** @@ -296,8 +481,9 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { // notify delegates for delegate in delegates { - delegate.cameraBoundsChanged(bounds: bounds, position: position, zoom: zoom) + delegate.cameraBoundsChanged?(bounds: bounds, position: position, zoom: zoom) } + self.updateContainedNodes() } // MARK: - Movement @@ -313,10 +499,12 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { let dx = position.x - (location.x - previous.x) position = CGPoint(x: dx, y: dy) + // notify delegates for delegate in delegates { - delegate.cameraPositionChanged(newPosition: position) + delegate.cameraPositionChanged?(newPosition: position) } + self.updateContainedNodes() } /** @@ -325,12 +513,14 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { - parameter point: `CGPoint` point to move to. - parameter duration: `TimeInterval` duration of move. */ - public func panToPoint(_ point: CGPoint, duration: TimeInterval=0.3) { + public func panToPoint(_ point: CGPoint, duration: TimeInterval = 0.3) { run(SKAction.move(to: point, duration: duration), completion: { // notify delegates for delegate in self.delegates { - delegate.cameraPositionChanged(newPosition: point) + guard (delegate.receiveCameraUpdates == true) else { continue } + delegate.cameraPositionChanged?(newPosition: point) } + self.updateContainedNodes() }) } @@ -340,12 +530,15 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { - parameter scenePoint: `CGPoint` point in scene. - parameter easeInOut: `TimeInterval` ease in/out speed. */ - public func centerOn(scenePoint point: CGPoint, duration: TimeInterval=0) { + public func centerOn(scenePoint point: CGPoint, duration: TimeInterval = 0) { + defer { // notify delegates for delegate in self.delegates { - delegate.cameraPositionChanged(newPosition: point) + guard (delegate.receiveCameraUpdates == true) else { continue } + delegate.cameraPositionChanged?(newPosition: point) } + self.updateContainedNodes() } if duration == 0 { @@ -366,11 +559,14 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { public func centerOn(_ node: SKNode, duration: TimeInterval = 0) { guard let scene = self.scene else { return } let nodePosition = scene.convert(node.position, from: node) + defer { // notify delegates for delegate in self.delegates { - delegate.cameraPositionChanged(newPosition: nodePosition) + guard (delegate.receiveCameraUpdates == true) else { continue } + delegate.cameraPositionChanged?(newPosition: nodePosition) } + self.updateContainedNodes() } // run the action if duration == 0 { @@ -406,39 +602,46 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { - parameter newSize: `CGSize` updated scene size. - parameter transition: `TimeInterval` transition time. */ - public func fitToView(newSize: CGSize, transition: TimeInterval=0) { + public func fitToView(newSize: CGSize, transition: TimeInterval = 0, verbose: Bool = false) { guard let scene = scene, let tiledScene = scene as? SKTiledSceneDelegate, let tilemap = tiledScene.tilemap else { return } - let tilemapSize = tilemap.sizeInPoints + let mapsize = tilemap.sizeInPoints // (tilemap.sizeInPoints / TiledGlobals.default.contentScale) + self.log("tilemap size: \(mapsize)", level: .info) let tilemapCenter = scene.convert(tilemap.position, from: tilemap) let isPortrait: Bool = newSize.height > newSize.width - let widthFactor: CGFloat = (tilemap.isPortrait == true) ? 0.7 : 0.6 - let heightFactor: CGFloat = (tilemap.isPortrait == true) ? 0.6 : 0.75 + let widthFactor: CGFloat = (tilemap.isPortrait == true) ? 0.85 : 0.75 + let heightFactor: CGFloat = (tilemap.isPortrait == true) ? 0.75 : 0.85 - let screenScaleWidth: CGFloat = isPortrait ? widthFactor : 0.7 - let screenScaleHeight: CGFloat = isPortrait ? heightFactor : 0.7 + let screenScaleWidth: CGFloat = isPortrait ? widthFactor : 0.8 + let screenScaleHeight: CGFloat = isPortrait ? heightFactor : 0.8 // get the usable height/width let usableWidth: CGFloat = newSize.width * screenScaleWidth let usableHeight: CGFloat = newSize.height * screenScaleHeight - let scaleFactor = (isPortrait == true) ? usableWidth / tilemapSize.width : usableHeight / tilemapSize.height + let scaleFactor = (isPortrait == true) ? usableWidth / mapsize.width : usableHeight / mapsize.height //let heightOffset: CGFloat = (usableHeight / 20) let focusPoint = CGPoint(x: tilemapCenter.x, y: tilemapCenter.y) // -heightOffset centerOn(scenePoint: focusPoint) setCameraZoom(scaleFactor, interval: transition) + self.log("fitting to view: \(usableWidth.roundTo()) x \(usableHeight.roundTo()), scale: \(scaleFactor.roundTo())", level: .debug) - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateDebugLabels"), object: nil, userInfo: ["cameraInfo": self.description]) + NotificationCenter.default.post( + name: Notification.Name.Camera.Updated, + object: self, + userInfo: ["cameraInfo": self.description, "cameraControlMode": self.controlMode] + ) } // MARK: - Geometry + /** Returns the points of the camera's bounding shape. @@ -450,13 +653,50 @@ public class SKTiledSceneCamera: SKCameraNode, Loggable { } - +// was: 0.50, 0.25, 0.10 extension CameraZoomClamping { + + public var name: String { + switch self { + case .none: return "None" + case .half: return "Half" + case .third: return "Third" + case .quarter: return "Quarter" + case .tenth: return "Tenth" + } + } + + /** + Returns an array of all camera zoom modes. + + - returns `[CameraZoomClamping]` all camera zoom modes. + */ + static public func allModes() -> [CameraZoomClamping] { + return [CameraZoomClamping.none, CameraZoomClamping.half, CameraZoomClamping.third, CameraZoomClamping.quarter, CameraZoomClamping.tenth] + } + + /** + Returns the next mode in the list. + + - returns `CameraZoomClamping` next mode in the list. + */ + public func next() -> CameraZoomClamping { + switch self { + case .none: return .half + case .half: return .third + case .third: return .quarter + case .quarter: return .tenth + case .tenth: return .none + } + } + /// Minimum possible value. public var minimum: CGFloat { switch self { case .half: return 0.50 + case .third: + return 0.33 case .quarter: return 0.25 case .tenth: @@ -469,11 +709,25 @@ extension CameraZoomClamping { extension SKTiledSceneCamera { + + /// Current clamp status + public var clampDescription: String { + let clampString = (ignoreZoomClamping == false) ? (zoomClamping != .none) ? ", clamp: \(zoomClamping.minimum)" : "" : "" + let modeString = (controlMode != .none) ? ", mode: \(controlMode)" : "" + let clampMode = (ignoreZoomClamping == true) ? ", clamp: off" : "" + return "\(clampString)\(modeString)\(clampMode)" + } + + /// Custom camera info description override public var description: String { guard let scene = scene else { return "Camera: "} let rect = CGRect(origin: scene.convert(position, from: self), size: bounds.size) - let clampString = (zoomClamping != .none) ? ", clamp: \(zoomClamping.minimum)" : "" - return "Camera: \(rect.roundTo()), zoom: \(zoom.roundTo())\(clampString)" + let uiScale = getContentScaleFactor() + let scaleString = (uiScale > 1) ? ", scale: \(uiScale)" : "" + let sizeString = "origin: \(Int(rect.origin.x)), \(Int(rect.origin.y)), size: \(Int(rect.size.width)) x \(Int(rect.size.height))\(scaleString)" + + let result = "Camera: \(sizeString), zoom: \(zoom.roundTo())" + return "\(result)\(clampDescription)" } override public var debugDescription: String { @@ -483,11 +737,33 @@ extension SKTiledSceneCamera { } +// MARK: - Debugging + +extension SKTiledSceneCamera: CustomDebugReflectable { + + public func dumpStatistics() { + print("\n-------------- Camera --------------") + print(" - position: \(position.shortDescription)") + print(" - zoom: \(zoom.roundTo(3))") + print(" - clamping: \(zoomClamping.name)") + print(" - ignore clamping: \(ignoreZoomClamping)") + print(" - control mode: \(controlMode)") + print(" - tiled nodes visible: \(containedNodes.count)") + print(" - world position: \(world.position.shortDescription)") + print("\n - delegates:") + + for delegate in delegates { + let delegateName = String(describing: type(of: delegate) ) + let updateMode = (delegate.receiveCameraUpdates == true) ? "[x]" : "[ ]" + print(" - \(updateMode) `\(delegateName)`") + } + } +} extension SKTiledSceneCamera { - #if os(iOS) || os(tvOS) + #if os(iOS) // MARK: - Gesture Handlers /** @@ -495,9 +771,8 @@ extension SKTiledSceneCamera { - parameter recognizer: `UIPanGestureRecognizer` pan gesture recognizer. */ - public func cameraPanned(_ recognizer: UIPanGestureRecognizer) { - guard (self.scene != nil), - (allowMovement == true) else { return } + @objc public func cameraPanned(_ recognizer: UIPanGestureRecognizer) { + guard (self.scene != nil), (allowMovement == true) else { return } if (recognizer.state == .began) { let location = recognizer.location(in: recognizer.view) @@ -508,6 +783,7 @@ extension SKTiledSceneCamera { if lastLocation == nil { return } let location = recognizer.location(in: recognizer.view) let difference = CGPoint(x: location.x - lastLocation.x, y: location.y - lastLocation.y) + // calls `cameraPositionChanged` centerOn(scenePoint: CGPoint(x: Int(position.x - difference.x), y: Int(position.y - -difference.y))) lastLocation = location } @@ -518,13 +794,12 @@ extension SKTiledSceneCamera { - parameter recognizer: `UITapGestureRecognizer` tap gesture recognizer. */ - public func sceneDoubleTapped(_ recognizer: UITapGestureRecognizer) { - if (recognizer.state == UIGestureRecognizerState.ended) { + @objc public func sceneDoubleTapped(_ recognizer: UITapGestureRecognizer) { + if (recognizer.state == UIGestureRecognizer.State.ended && allowPause) { let location = recognizer.location(in: recognizer.view) for delegate in self.delegates { - recognizer.numberOfTouches - - delegate.sceneDoubleTapped(location: location) + guard (delegate.receiveCameraUpdates == true) else { continue } + delegate.sceneDoubleTapped?(location: location) } } } @@ -534,13 +809,14 @@ extension SKTiledSceneCamera { - parameter recognizer: `UIPinchGestureRecognizer` */ - public func scenePinched(_ recognizer: UIPinchGestureRecognizer) { + @objc public func scenePinched(_ recognizer: UIPinchGestureRecognizer) { guard let scene = self.scene, (allowZoom == true) else { return } if recognizer.state == .began { let location = recognizer.location(in: recognizer.view) focusLocation = scene.convertPoint(fromView: location) // correct + // calls `cameraPositionChanged` centerOn(scenePoint: focusLocation) } @@ -553,8 +829,9 @@ extension SKTiledSceneCamera { } } - #else + #endif + #if os(macOS) // MARK: - Mouse Handlers /** @@ -567,13 +844,14 @@ extension SKTiledSceneCamera { if (event.clickCount > 1) { for delegate in self.delegates { - delegate.sceneDoubleClicked(event: event) + guard (delegate.receiveCameraUpdates == true) else { continue } + delegate.sceneDoubleClicked?(event: event) } } } /** - Track mouse movement in the scene. Location is in local space, so + Track mouse movement in the scene. Location is in local space, so coordinate origin will be the center of the current window. - parameter event: `NSEvent` mouse event. @@ -583,7 +861,8 @@ extension SKTiledSceneCamera { lastLocation = event.location(in: self) for delegate in self.delegates { - delegate.mousePositionChanged(event: event) + guard (delegate.receiveCameraUpdates == true) else { continue } + delegate.mousePositionChanged?(event: event) } } @@ -617,7 +896,12 @@ extension SKTiledSceneCamera { //setCameraZoomAtLocation(scale: zoom, location: position) } - public func scenePositionChanged(_ event: NSEvent) { + /** + Callback for mouse drag events. + + - parameter event: `NSEvent` mouse drag event. + */ + public func scenePositionChanged(with event: NSEvent) { guard (self.scene as? SKTiledScene != nil) else { return } let location = event.location(in: self) @@ -629,23 +913,13 @@ extension SKTiledSceneCamera { lastLocation = location for delegate in delegates { - delegate.cameraPositionChanged(newPosition: position) + delegate.cameraPositionChanged?(newPosition: position) } + self.updateContainedNodes() } } #endif } -/// Default methods. -extension SKTiledSceneCameraDelegate { - public func cameraPositionChanged(newPosition: CGPoint) {} - public func cameraZoomChanged(newZoom: CGFloat) {} - public func cameraBoundsChanged(bounds: CGRect, position: CGPoint, zoom: CGFloat) {} - #if os(iOS) || os(tvOS) - public func sceneDoubleTapped(location: CGPoint) {} - #else - public func sceneDoubleClicked(event: NSEvent) {} - public func mousePositionChanged(event: NSEvent) {} - #endif -} +extension SKTiledSceneCamera: Loggable {} diff --git a/Sources/SKTilemap+DataStorage.swift b/Sources/SKTilemap+DataStorage.swift new file mode 100644 index 00000000..46c9e345 --- /dev/null +++ b/Sources/SKTilemap+DataStorage.swift @@ -0,0 +1,876 @@ +// +// SKTilemap+DataStorage.swift +// SKTiled +// +// Created by Michael Fessenden on 8/12/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import Foundation +import SpriteKit + + +typealias TileList = StorageArray +typealias ObjectsList = StorageArray + +typealias DataCache = [SKTilesetData: TileList] +typealias ActionsCache = [SKTilesetData: SKAction] + + +internal enum CacheIsolationMode: Int { + case none + case `default` + case ignored + case `static` + case animated +} + + + +/// Data structure for storing and recalling tile data efficiently. +internal class TileDataStorage: Loggable { + weak var tilemap: SKTilemap? + // queues + fileprivate let storageQueue = DispatchQueue(label: "com.sktiled.tileDataStorage.storageQueue", qos: .userInteractive, attributes: .concurrent) + // update queue, for tile texture changes + fileprivate let updateQueue = DispatchQueue(label: "com.sktiled.tileDataStorage.updateQueue", qos: .userInteractive) + + var staticTileCache: DataCache = [:] + var animatedTileCache: DataCache = [:] + var actionsCache: ActionsCache = [:] + + var objectsList: ObjectsList + var blockNotifications: Bool = true + + var isolationMode: CacheIsolationMode = CacheIsolationMode.none { + didSet { + guard oldValue != isolationMode else { return } + self.isolateTilesAction() + } + } + + init(map: SKTilemap) { + tilemap = map + objectsList = ObjectsList(queue: self.storageQueue) + setupNotifications() + } + + /** + Setup notifications. + */ + private func setupNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(objectProxyVisibilityChanged), name: Notification.Name.DataStorage.ProxyVisibilityChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(globalsUpdated), name: Notification.Name.Globals.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(objectAddedToLayer), name: Notification.Name.Layer.ObjectAdded, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(objectWasRemovedFromLayer), name: Notification.Name.Layer.ObjectRemoved, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tileAddedToLayer), name: Notification.Name.Layer.TileAdded, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tileDataChanged), name: Notification.Name.Tile.DataChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tileRenderModeChanged), name: Notification.Name.Tile.RenderModeChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tileDataActionAdded), name: Notification.Name.TileData.ActionAdded, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tileDataFrameAdded), name: Notification.Name.TileData.FrameAdded, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tileDataTextureChanged), name: Notification.Name.TileData.TextureChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tilesetSpriteSheetUpdated), name: Notification.Name.Tileset.SpriteSheetUpdated, object: nil) + } + + deinit { + tilemap = nil + blockNotifications = true + + // reset caches + staticTileCache = [:] + animatedTileCache = [:] + actionsCache = [:] + + NotificationCenter.default.removeObserver(self, name: Notification.Name.DataStorage.ProxyVisibilityChanged, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.Globals.Updated, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.Layer.ObjectAdded, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.Layer.ObjectRemoved, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.Layer.TileAdded, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.Tile.DataChanged, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.Tile.RenderModeChanged, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.TileData.ActionAdded, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.TileData.FrameAdded, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.TileData.TextureChanged, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.Tileset.SpriteSheetUpdated, object: nil) + } + + /// Returns an array of all stored tiles. + var allTiles: [SKTile] { + var result: [SKTile] = [] + for items in staticTileCache.enumerated() { + result.append(contentsOf: items.element.value.array) + } + + for items in animatedTileCache.enumerated() { + result.append(contentsOf: items.element.value.array) + } + + return result + } + + // MARK: - Notifications + + @objc func tileAddedToLayer(notification: Notification) { + guard let tile = notification.object as? SKTile else { return } + addTileToCache(tile: tile) + } + + @objc func objectAddedToLayer(notification: Notification) { + guard let object = notification.object as? SKTileObject else { + return + } + guard (objectsList.filter { $0 == object }.isEmpty == true) else { + return + } + + objectsList.append(object) + storageQueue.sync {} + } + + /** + Called when a map or layer node `showObjects` attribute is changed. + + - parameter notification: `Notification` notification. + */ + @objc func objectProxyVisibilityChanged(notification: Notification) { + guard let proxies = notification.object as? [TileObjectProxy], + let userInfo = notification.userInfo as? [String: Bool] else { return } + + let proxiesVisible: Bool = userInfo["visibility"] ?? false + + var proxiesUpdated = 0 + for proxy in proxies { + proxy.showObjects = proxiesVisible + proxy.draw() + proxiesUpdated += 1 + } + } + + + /** + Called when tile data is changed via the tile `renderMode` flag. + + - parameter notification: `Notification` notification. + */ + @objc func tileDataChanged(notification: Notification) { + guard let tile = notification.object as? SKTile else { return } + guard let userInfo = notification.userInfo as? [String: Any], + let oldTileData = userInfo["oldData"] as? SKTilesetData else { + return + } + + // add the tile to the appropriate dictionary, and remove from the previous + let newTileData = tile.tileData + + // old data is animated and new data is (and vice versa) + _ = (oldTileData.isAnimated) == (newTileData.isAnimated) + + let oldTileList: TileList = (oldTileData.isAnimated == true) ? animatedCacheForTileData(oldTileData) : cacheForTileData(oldTileData) + let newTileList: TileList = (newTileData.isAnimated == true) ? animatedCacheForTileData(newTileData) : cacheForTileData(newTileData) + + oldTileList.remove(where: {$0 == tile}) + newTileList.append(tile) + + // transfer attributes + newTileData.frameIndex = oldTileData.frameIndex + newTileData.currentTime = oldTileData.currentTime + + // update the tile render mode after we've changed + tile.renderMode = TileRenderMode.default + } + + /** + Called when a tileset's spritesheet is updated. + + - parameter notification: `Notification` notification. + */ + @objc func tilesetSpriteSheetUpdated(notification: Notification) { + guard let tileset = notification.object as? SKTileset, + let userInfo = notification.userInfo as? [String: Any], + let animatedTiles = userInfo["animatedTiles"] as? [SKTilesetData] else { return } + + updateQueue.async { + for data in animatedTiles { + data.removeAnimation() + data._frames.forEach { frame in + if let frameData = tileset.getTileData(localID: frame.id) { + frame.texture = frameData.texture + } + } + data.runAnimation() + } + } + } + + /** + Called when a tile's render mode is changed. + + - parameter notification: `Notification` notification. + */ + // Tile.RenderModeChanged + @objc func tileRenderModeChanged(notification: Notification) { + guard let tile = notification.object as? SKTile, + let userInfo = notification.userInfo as? [String: Any], + let oldMode = userInfo["old"] as? TileRenderMode else { return } + + guard let tilemap = tilemap else { + return + } + + + let tileData = tile.tileData + tile.drawBounds(withColor: nil, zpos: nil, duration: 0) + + // indicates the tile has an animation override + let tileHadOverridenAnimation = (oldMode.rawValue > 2) || (oldMode.rawValue < 0) + // indicates we need to update the tile (ie pop from one list to another) + var needToUpdateTile = false + if (tileHadOverridenAnimation == true) { + moveTileFrom(tile: tile, globalID: oldMode.rawValue) + } + + + switch tile.renderMode { + + // tile should not animate + case .static: + tile.removeAnimationActions(restore: false) + needToUpdateTile = true + + + // tile will ignore it's tile data + case .ignore: + let existingList: TileList = (tileData.isAnimated == true) ? animatedCacheForTileData(tileData) : cacheForTileData(tileData) + // remove the tile from the current list + existingList.remove(where: { $0 == tile }) + + // tile has requested new tile data + case .animated(let gid): + guard let globalID = gid else { + break + } + + var newDataIsAnimated = false + + if let newTileData = tilemap.getTileData(globalID: globalID) { + let existingList: TileList = (tileData.isAnimated == true) ? animatedCacheForTileData(tileData) : cacheForTileData(tileData) + + // remove the tile from the current list + existingList.remove(where: { $0 == tile }) + + newDataIsAnimated = newTileData.isAnimated + let nextList: TileList = (newDataIsAnimated == true) ? animatedCacheForTileData(newTileData) : cacheForTileData(newTileData) + + nextList.append(tile) + } + + default: + needToUpdateTile = true + } + + + // refresh the tile's texture + if (needToUpdateTile == true) { + + // turn off notifications for the tile while it is updated + tile.blockNotifications = true + + // update the tile with the current texture + if let currentTexture = tile.tileData.texture { + tile.texture = currentTexture + tile.size = currentTexture.size() + + // turn notifications back on + DispatchQueue.main.async { + tile.blockNotifications = false + } + } + } + } + + /** + Called when tile data frames are updated. + + - parameter notification: `Notification` notification. + */ + @objc func tileDataFrameAdded(notification: Notification) { + guard let tileData = notification.object as? SKTilesetData else { return } + tileData.dataChanged = true + } + + /** + Called when a tile data's animation `SKAction` is created. + + - parameter notification: `Notification` notification. + */ + @objc func tileDataActionAdded(notification: Notification) { + guard let tileData = notification.object as? SKTilesetData, + let userInfo = notification.userInfo as? [String: Any], + let action = userInfo["action"] as? SKAction else { return } + + //typealias ActionsCache = [SKTilesetData: SKAction] + self.actionsCache[tileData] = action + } + + /** + Called when a tile data's texture is updated. Previous texture is passed in `userInfo`. + + - parameter notification: `Notification` notification. + */ + @objc func tileDataTextureChanged(notification: Notification) { + guard let tileData = notification.object as? SKTilesetData, + (notification.userInfo as? [String: Any] != nil) else { return } + + // if we're in dynamic mode, all of the textures need to be updated in static... + guard let newTexture = tileData.texture else { + self.log("invalid texture for data: \(tileData.globalID)", level: .warning) + return + } + + let currentTiles = (tileData.isAnimated == true) ? animatedCacheForTileData(tileData) : cacheForTileData(tileData) + + // update every tile that uses this texture + updateQueue.async { + currentTiles.forEach { tile in + + switch tile.renderMode { + case .ignore: break + default: + // turn off notifications for the tile + tile.blockNotifications = true + tile.texture = newTexture + tile.size = newTexture.size() + + // turn notifications back on + DispatchQueue.main.async { + tile.blockNotifications = false + } + } + } + } + } + + @objc func objectWasRemovedFromLayer(notification: Notification) { + guard let object = notification.object as? SKTileObject else { return } + + _ = objectsList.remove(where: { $0 == object}) { obj in + self.log("object removed: \(obj)", level: .debug) + self.tilemap?.objectsOverlay.initialized = false + } + } + + @objc func globalsUpdated(notification: Notification) { + guard let userInfo = notification.userInfo else { return } + + if let newTileColor = userInfo["tileColor"] as? SKColor { + updateQueue.async { + for tile in self.allTiles { + tile.frameColor = newTileColor + tile.highlightColor = newTileColor + } + } + } + + if let newObjectColor = userInfo["objectColor"] as? SKColor { + updateQueue.async { + for object in self.objectsList { + if let proxy = object.proxy { + proxy.objectColor = newObjectColor + } + } + } + } + } + + // MARK: - Caching + + func addTileToCache(tile: SKTile, data: SKTilesetData? = nil, cache: TileList? = nil) { + let tileData = data ?? tile.tileData + let currentCache: TileList = (cache != nil) ? cache! : (tileData.isAnimated == true) ? animatedCacheForTileData(tileData) : cacheForTileData(tileData) + currentCache.append(tile) + } + + /** + Return or create a tile list for the changed data. + + - parameter data: `SKTilesetData` tile data. + - returns `TileList` tile list. + */ + func tileAnimationAction(for data: SKTilesetData) -> SKAction? { + guard let savedAction = actionsCache[data] else { + return nil + } + return savedAction + } + + // MARK: - Helpers + + /** + Return or create a tile list for the static data. + + - parameter data: `SKTilesetData` tile data. + - returns `TileList` tile list. + */ + private func cacheForTileData(_ data: SKTilesetData) -> TileList { + guard let existingCache = staticTileCache[data] else { + let newCache = TileList(queue: self.storageQueue) + staticTileCache[data] = newCache + return newCache + } + return existingCache + } + + /** + Return or create a tile list for the animated data. + + - parameter data: `SKTilesetData` tile data. + - returns `TileList` tile list. + */ + private func animatedCacheForTileData(_ data: SKTilesetData) -> TileList { + guard let existingCache = animatedTileCache[data] else { + let newCache = TileList(queue: self.storageQueue) + animatedTileCache[data] = newCache + return newCache + } + return existingCache + } + + /** + Move tile from one data list to another. + + - parameter tile: `SKTile` tile. + - parameter from: `DataCache` source cache. + - parameter to: `DataCache` destination cache. + - returns `(sucess: Bool, cache: DataCache, removed: DataCache?)` data was succesfully moved. + */ + private func moveTileFrom(tile: SKTile, globalID: Int) { + guard let tilemap = tilemap else { + return + } + + if let oldTileData = tilemap.getTileData(globalID: globalID) { + let nextTileData = tile.tileData + + // move tile out of an animated data list + // put it into another + let existingList: TileList = (oldTileData.isAnimated == true) ? animatedCacheForTileData(oldTileData) : cacheForTileData(oldTileData) + + // remove the tile from the current list + existingList.remove(where: { $0 == tile }) + let nextDataIsAnimated = nextTileData.isAnimated + let nextList: TileList = (nextDataIsAnimated == true) ? animatedCacheForTileData(nextTileData) : cacheForTileData(nextTileData) + nextList.append(tile) + } + } + + + /** + Move tile data from one cache to another. + + - parameter data: `SKTilesetData` tile data removed. + - parameter from: `DataCache` source cache. + - parameter to: `DataCache` destination cache. + - returns `(sucess: Bool, cache: DataCache, removed: DataCache?)` data was succesfully moved. + */ + private func moveDataFrom(data: SKTilesetData, from sourceCache: inout DataCache, to destCache: inout DataCache) -> (sucess: Bool, cache: DataCache, removed: DataCache?) { + guard let sourceIndex = sourceCache.index(forKey: data), + (sourceCache != destCache) else { + return (false, sourceCache, nil) + } + + // get the list of associated tiles + let removedList = sourceCache.remove(at: sourceIndex).value + if let destIndex = destCache.index(forKey: data) { + let existingList = destCache.remove(at: destIndex).value + for tile in existingList { + removedList.append(tile) + } + } + destCache[data] = removedList + return (true, destCache, sourceCache) + } + + /** + Build animation frames for the data. + + - parameter data: `SKTilesetData` tile data. + */ + func buildAnimationForData(data: SKTilesetData) { + updateQueue.async { + data.removeAnimation() + data._frames.forEach { frame in + if let frameData = data.tileset.getTileData(localID: frame.id) { + frame.texture = frameData.texture + } + } + data.runAnimation() + } + } + + func sync() { + storageQueue.sync {} + updateQueue.sync {} + } + + /** + Isolation mode updated. + */ + func isolateTilesAction() { + + for tile in allTiles { + + // true if mode is anything but 'none' + var doHideTile = (isolationMode != .none) + + switch tile.renderMode { + + case .animated(gid: _): + doHideTile = (doHideTile == true) && (isolationMode != .animated) + + case .ignore: + doHideTile = (doHideTile == true) && (isolationMode != .ignored) + + case .static: + doHideTile = (doHideTile == true) && (isolationMode != .static) + + default: + doHideTile = (doHideTile == true) && (isolationMode != .default) + } + + tile.isHidden = doHideTile + } + + NotificationCenter.default.post( + name: Notification.Name.DataStorage.IsolationModeChanged, + object: nil, + userInfo: nil + ) + } +} + + + +extension TileDataStorage: CustomStringConvertible, CustomDebugStringConvertible, CustomDebugReflectable { + + var description: String { + let staticCount = staticTileCache.count + let animatedCount = animatedTileCache.count + return "Tile Data Storage: static: \(staticCount), animated: \(animatedCount)" + } + + var debugDescription: String { + return description + } + + + func dumpStatistics() { + let output = "\n----------- Tile Data Storage -----------" + var staticOutput = underlined(for: "Static") + var animatedOutput = underlined(for: "Animated") + + for item in staticTileCache.enumerated() { + let tileData = item.element.key + let tileList = item.element.value.array + let tileDataHeader = "\n - tile data: \(tileData.globalID) (\(tileList.count) tiles):" + staticOutput += tileDataHeader + + } + + for item in animatedTileCache.enumerated() { + let tileData = item.element.key + let tileList = item.element.value.array + let tileDataHeader = "\n - tile data: \(tileData.globalID) (\(tileList.count) tiles):" + animatedOutput += tileDataHeader + } + + print("\n\(output)\n\(staticOutput)\n\(animatedOutput)") + } +} + + +extension TileDataStorage: CustomReflectable { + + public var customMirror: Mirror { + var staticDataCount = 0 + var animatedDataCount = 0 + var staticTileCount = 0 + var animatedTileCount = 0 + + for (_, item) in staticTileCache.enumerated() { + staticDataCount += 1 + staticTileCount += item.value.count + } + + for (_, item) in animatedTileCache.enumerated() { + animatedDataCount += 1 + animatedTileCount += item.value.count + } + + let staticData: [String: Any] = ["data": staticDataCount, "tiles": staticTileCount] + let animatedData: [String: Any] = ["data": animatedDataCount, "tiles": animatedTileCount] + + return Mirror(TileDataStorage.self, children: + ["static": staticData, + "animated": animatedData, + "objects": objectsList.count] + ) + } +} + + + +/// Thread-safe storage container. +internal class StorageArray: Equatable { + + fileprivate let uuid = UUID().uuidString + fileprivate let queue: DispatchQueue + fileprivate var array: [Element] = [] + + init(queue: DispatchQueue? = nil) { + self.queue = (queue == nil) ? DispatchQueue(label: "com.sktiled.storageQueue", attributes: .concurrent) : queue! + } + + /// Number of elements in the array. + var count: Int { + var result = 0 + queue.sync { result = self.array.count } + return result + } + + /// A Boolean value indicating whether the collection is empty. + var isEmpty: Bool { + var result = false + queue.sync { result = self.array.isEmpty } + return result + } + + /** + + Returns a boolean indicating the array contains the given element. + + - Parameters: + - predicate: `(Element) -> Bool` conditional expression. + - returns: `Bool` array contains the given element. + */ + func contains(where predicate: (Element) -> Bool) -> Bool { + var result = false + queue.sync { result = self.array.contains(where: predicate) } + return result + } + + /** + + Append a new element to the end of the array. + + - Parameters: + - element: `Element` element to append. + */ + func append( _ element: Element) { + queue.async(flags: .barrier) { + self.array.append(element) + } + } + + /** + + Adds new elements to the array. + + - Parameters: + - elements: `[Element]` array of elements to append. + */ + func append(contentsOf elements: [Element]) { + queue.async(flags: .barrier) { + self.array += elements + } + } + + /** + + Insert a new element at the specified position. + + - Parameters: + - element: `Element` array of elements to append. + - index: `Int` index to insert at. + */ + func insert( _ element: Element, at index: Int) { + queue.async(flags: .barrier) { + self.array.insert(element, at: index) + } + } + + /// Removes and returns the element at the specified position. + func remove(at index: Int, completion: ((Element) -> Void)? = nil) { + queue.async(flags: .barrier) { + let element = self.array.remove(at: index) + + DispatchQueue.main.async { + completion?(element) + } + } + } + + /** + + Removes and returns the element at the specified position. + + - Parameters: + - predicate: `(Element) -> Bool` conditional expression. + - completion: `([Element]) -> Void)?` optional completion closure. + */ + func remove(where predicate: @escaping (Element) -> Bool, completion: ((Element) -> Void)? = nil) { + queue.async(flags: .barrier) { + guard let index = self.array.index(where: predicate) else { return } + let element = self.array.remove(at: index) + + DispatchQueue.main.async { + completion?(element) + } + } + } + + /** + + Removes all elements from the array. + + - Parameters: + - completion: `([Element]) -> Void)?` optional completion closure. + */ + func removeAll(completion: (([Element]) -> Void)? = nil) { + queue.async(flags: .barrier) { + let elements = self.array + self.array.removeAll() + + DispatchQueue.main.async { + completion?(elements) + } + } + } + + + /// Accesses the element at the specified position if it exists. + subscript(index: Int) -> Element? { + get { + var result: Element? + queue.sync { + guard self.array.startIndex.., rhs: StorageArray) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} + + + +// Allows the cache array to act as a collection. +extension StorageArray: Sequence { + + internal func makeIterator() -> AnyIterator { + var arrayIndex = 0 + return AnyIterator { + if arrayIndex < self.count { + let element = self[arrayIndex] + arrayIndex+=1 + return element + } else { + arrayIndex = 0 + return nil + } + } + } +} + + +extension StorageArray where Element: Equatable { + /** + + Returns a boolean indicating the array contains the given element. + + - Parameters: + - element: `Element -> Bool` element to query. + - returns: `Bool` array contains the given element. + */ + func contains(_ element: Element) -> Bool { + var result = false + queue.sync { result = self.array.contains(element) } + return result + } +} + + + +extension StorageArray { + + func filter(_ isIncluded: (Element) -> Bool) -> [Element] { + var result: [Element] = [] + queue.sync { result = self.array.filter(isIncluded) } + return result + } + + /// Returns the first index in which an element of the collection satisfies the given predicate. + func index(where predicate: (Element) -> Bool) -> Int? { + var result: Int? + queue.sync { result = self.array.index(where: predicate) } + return result + } + + /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements. + func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] { + var result: [Element] = [] + queue.sync { result = self.array.sorted(by: areInIncreasingOrder) } + return result + } + + /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. + func compactMap(_ transform: (Element) -> Elements?) -> [Elements] { + var result: [Elements] = [] + queue.sync { result = self.array.compactMap(transform) } + return result + } + + /// Calls the given closure on each element in the sequence in the same order as a for-in loop. + func forEach(_ body: (Element) -> Void) { + queue.sync { self.array.forEach(body) } + } +} + + +extension StorageArray: CustomStringConvertible, CustomDebugStringConvertible { + /// A textual representation of the array and its elements. + var description: String { + var result = "" + queue.sync { result = self.array.description } + return result + } + + var debugDescription: String { + return description + } +} diff --git a/Sources/SKTilemap+Properties.swift b/Sources/SKTilemap+Properties.swift index 1c31dfa3..d881f9f0 100644 --- a/Sources/SKTilemap+Properties.swift +++ b/Sources/SKTilemap+Properties.swift @@ -9,13 +9,15 @@ import SpriteKit - public extension SKTilemap { // MARK: - Properties + /** Parse properties from the Tiled TMX file. - */ - public func parseProperties(completion: (() -> ())?) { + + - parameter completion: `Void?` optional completion closure. + */ + public func parseProperties(completion: (() -> Void)?) { if (ignoreProperties == true) { return } if (self.type == nil) { self.type = properties.removeValue(forKey: "type") } @@ -34,6 +36,7 @@ public extension SKTilemap { if (lattr == "gridcolor") { gridColor = SKColor(hexString: value) + TiledGlobals.default.debug.gridColor = gridColor getLayers().forEach { $0.gridColor = gridColor } frameColor = gridColor @@ -44,7 +47,7 @@ public extension SKTilemap { } if (lattr == "gridopacity") { - defaultLayer.gridOpacity = (doubleForKey(attr) != nil) ? CGFloat(doubleForKey(attr)!) : 0.40 + defaultLayer.gridOpacity = (doubleForKey(attr) != nil) ? CGFloat(doubleForKey(attr)!) : TiledGlobals.default.debug.gridOpactity getLayers().forEach {$0.gridOpacity = self.defaultLayer.gridOpacity} } @@ -61,6 +64,7 @@ public extension SKTilemap { getLayers().forEach {$0.highlightColor = highlightColor} // set base layer colors + TiledGlobals.default.debug.tileHighlightColor = highlightColor defaultLayer.highlightColor = highlightColor } @@ -91,11 +95,11 @@ public extension SKTilemap { } if (lattr == "minzoom") { - minZoom = (doubleForKey(attr) != nil) ? CGFloat(doubleForKey(attr)!) : minZoom + zoomConstraints.min = (doubleForKey(attr) != nil) ? CGFloat(doubleForKey(attr)!) : zoomConstraints.min } if (lattr == "maxzoom") { - maxZoom = (doubleForKey(attr) != nil) ? CGFloat(doubleForKey(attr)!) : maxZoom + zoomConstraints.max = (doubleForKey(attr) != nil) ? CGFloat(doubleForKey(attr)!) : zoomConstraints.max } if (lattr == "ignorebackground") { @@ -141,7 +145,7 @@ public extension SKTilemap { if (lattr == "objectcolor") { objectColor = SKColor(hexString: value) } - + if ["nicename", "displayname"].contains(lattr) { displayName = value } @@ -149,6 +153,10 @@ public extension SKTilemap { if (lattr == "navigationcolor") { navigationColor = SKColor(hexString: value) } + + if ["enableeffects", "shouldenableeffects"].contains(lattr) { + shouldEnableEffects = boolForKey(attr) + } } if completion != nil { completion!() } @@ -162,7 +170,7 @@ public extension SKTileset { /** Parse the tileset's properties value. */ - public func parseProperties(completion: (() -> ())?) { + public func parseProperties(completion: (() -> Void)?) { if (ignoreProperties == true) { return } if (self.type == nil) { self.type = properties.removeValue(forKey: "type") } @@ -193,7 +201,7 @@ public extension SKTiledLayerObject { /** Parse the layer's properties value. */ - public func parseProperties(completion: (() -> ())?) { + public func parseProperties(completion: (() -> Void)?) { if (ignoreProperties == true) { return } if (self.type == nil) { self.type = properties.removeValue(forKey: "type") } @@ -239,10 +247,6 @@ public extension SKTiledLayerObject { self.navigationKey = value } - if (lattr == "isstatic") { - isStatic = boolForKey(attr) - } - if completion != nil { completion!() } } } @@ -275,7 +279,7 @@ public extension SKTileLayer { /** Parse the tile layer's properties. */ - override public func parseProperties(completion: (() -> ())?) { + override public func parseProperties(completion: (() -> Void)?) { super.parseProperties(completion: completion) } } @@ -287,7 +291,7 @@ public extension SKObjectGroup { /** Parse the object group's properties. */ - override public func parseProperties(completion: (() -> ())?) { + override public func parseProperties(completion: (() -> Void)?) { if (ignoreProperties == true) { return } for (attr, _ ) in properties { let lattr = attr.lowercased() @@ -307,18 +311,19 @@ public extension SKImageLayer { /** Parse the image layer's properties. */ - override public func parseProperties(completion: (() -> ())?) { + override public func parseProperties(completion: (() -> Void)?) { super.parseProperties(completion: completion) } } +// MARK: - Generic Properties public extension SKTileObject { - // MARK: - Properties + /** Parse the object's properties value. */ - public func parseProperties(completion: (() -> ())?) { + public func parseProperties(completion: (() -> Void)?) { if (ignoreProperties == true) { return } if (self.type == nil) { self.type = properties.removeValue(forKey: "type") } for (attr, value) in properties { @@ -354,7 +359,7 @@ extension SKTileCollisionShape { /** Parse the collision shape's properties. */ - func parseProperties(completion: (() -> ())?) { + func parseProperties(completion: (() -> Void)?) { if (ignoreProperties == true) { return } if (self.type == nil) { self.type = properties.removeValue(forKey: "type") } } @@ -366,7 +371,7 @@ public extension SKTilesetData { /** Parse the tile data's properties value. */ - public func parseProperties(completion: (() -> ())?) { + public func parseProperties(completion: (() -> Void)?) { if (ignoreProperties == true) { return } if (self.type == nil) { self.type = properties.removeValue(forKey: "type") } @@ -380,7 +385,6 @@ public extension SKTilesetData { if (lattr == "walkable") { walkable = boolForKey(attr) } - } if completion != nil { completion!() } diff --git a/Sources/SKTilemap.swift b/Sources/SKTilemap.swift index 075381e6..68856429 100644 --- a/Sources/SKTilemap.swift +++ b/Sources/SKTilemap.swift @@ -10,36 +10,6 @@ import SpriteKit import GameplayKit -/// String representing Tiled application version. -public var SKTiledTiledApplicationVersion: String = "0" - -/// Global logging level. -public var SKTiledLoggingLevel: LoggingLevel = .info - -/// Global device scaling factor for retina displays. -public var SKTiledContentScaleFactor: CGFloat = getContentScaleFactor() - - -internal struct TiledObjectColors { - static let azure: SKColor = SKColor(hexString: "#4A90E2") - static let coral: SKColor = SKColor(hexString: "#FD4444") - static let crimson: SKColor = SKColor(hexString: "#D0021B") - static let dandelion: SKColor = SKColor(hexString: "#F8E71C") - static let english: SKColor = SKColor(hexString: "#AF3E4D") - static let grass: SKColor = SKColor(hexString: "#B8E986") - static let gun: SKColor = SKColor(hexString: "#8D99AE") - static let indigo: SKColor = SKColor(hexString: "#274060") - static let lime: SKColor = SKColor(hexString: "#7ED321") - static let magenta: SKColor = SKColor(hexString: "#FF00FF") - static let metal: SKColor = SKColor(hexString: "#627C85") - static let obsidian: SKColor = SKColor(hexString: "#464B4E") - static let pear: SKColor = SKColor(hexString: "#CEE82C") - static let saffron: SKColor = SKColor(hexString: "#F28123") - static let tangerine: SKColor = SKColor(hexString: "#F5A623") - static let turquoise: SKColor = SKColor(hexString: "#44CFCB") -} - - /// Object rendering order. internal enum RenderOrder: String { case rightDown = "right-down" @@ -89,38 +59,84 @@ internal enum StaggerAxis: String { - `even`: stagger evens. - `odd`: stagger odds. */ -internal enum StaggerIndex: String { +internal enum StaggerIndex: UInt8 { case odd case even } -// Common tile size aliases -internal let TileSizeZero = CGSize(width: 0, height: 0) -internal let TileSize8x8 = CGSize(width: 8, height: 8) -internal let TileSize16x16 = CGSize(width: 16, height: 16) -internal let TileSize32x32 = CGSize(width: 32, height: 32) - - /** ## Overview ## - The `SKTilemapDelegate` protocol defines a delegate that allows the user to interact with a tile map as it is being created and customize its properties. + Describes how the tilemap updates its tiles in your scene. Changing this property will affect your CPU usage, so use it carefully. - ## Callbacks ## + The default mode is `TileUpdateMode.dynamic`, which updates tiles as needed each frame. For best performance, use `TileUpdateMode.actions`, which will + run SpriteKit actions on animated tiles. - Delegate callbacks are called asynchronously as the map is being read from disk and rendered. + ### Usage ### ```swift - SKTilemapDelegate.didBeginParsing(_ tilemap: SKTilemap) - SKTilemapDelegate.didAddTileset(_ tileset: SKTileset) - SKTilemapDelegate.didAddLayer(_ layer: SKTiledLayerObject) - SKTilemapDelegate.didReadMap(_ tilemap: SKTilemap) - SKTilemapDelegate.didRenderMap(_ tilemap: SKTilemap) + // passing the tile update mode to the load function + let tilemap = SKTilemap.load(tmxFile: String, updateMode: TileUpdateMode.dynamic)! + + // updating the attribute on the tilemp node + tilemap.updateMode = TileUpdateMode.actions ``` - ## Custom Objects ## + ### Properties ### + + | Property | Description | + |:----------------------|:-----------------------------------------------------------------| + | dynamic | Dynamically update tiles as needed. | + | full | All tiles are updated each frame. | + | actions | Tiles are not updated, SpriteKit actions are used instead. | + + */ +public enum TileUpdateMode: Int { + case dynamic // dynamically update tiles as needed + case full // all tiles updated + case actions // use SpriteKit actions (no update) +} + + + +// tile size aliases +public let TileSizeZero = CGSize(width: 0, height: 0) +public let TileSize8x8 = CGSize(width: 8, height: 8) +public let TileSize16x16 = CGSize(width: 16, height: 16) +public let TileSize32x32 = CGSize(width: 32, height: 32) + + + +/** + ## Overview ## + + Methods that allow interaction with an `SKTilemap` object as it is being created to customize its properties. + + ### Properties ### + + | Property | Description | + |:-------------------|:-----------------------------------| + | zDeltaForLayers | Default z-distance between layers. | + + ### Instance Methods ### + + Delegate callbacks are called asynchronously as the map is being read from disk and rendered: + + | Method | Description | + |:----------------------|:-----------------------------------------------------------------| + | didBeginParsing | Called when the tilemap is instantiated. | + | didAddTileset | Called when a tileset is added to a map. | + | didAddLayer | Called when a layer is added to a tilemap. | + | didReadMap | Called when the tilemap is finished parsing. | + | didRenderMap | Called when the tilemap layers are finished rendering. | + | didAddNavigationGraph | Called when the a navigation graph is built for a layer. | + | objectForTileType | Specify a custom tile object for use in tile layers. | + | objectForVectorType | Specify a custom object for use in object groups. | + | objectForGraphType | Specify a custom graph node object for use in navigation graphs. | + + ### Custom Objects ### Custom object methods can be used to substitute your own objects for tiles: @@ -132,7 +148,6 @@ internal let TileSize32x32 = CGSize(width: 32, height: 32) return SKTile.self } ``` - */ public protocol SKTilemapDelegate: class { var zDeltaForLayers: CGFloat { get } @@ -149,12 +164,12 @@ public protocol SKTilemapDelegate: class { /** - ## Overview + ## Overview ## - The `SKTilemap` class defines a container for managing layers, tiles (sprites), + The `SKTilemap` class is a container for managing layers of tiles (sprites), vector objects & images. Tile data is stored in `SKTileset` tile sets. - ### Usage + ### Usage ### Maps can be loaded with the class function `SKTilemap.load(tmxFile:)`: @@ -164,21 +179,36 @@ public protocol SKTilemapDelegate: class { } ``` - ### Properties - + ### Properties ### + | Property | Description | + |:------------|:----------------------------------------------| + | mapSize | Size of the map (in tiles). | + | tileSize | Map tile size (in pixels). | + | renderSize | Size of the map in pixels. | + | orientation | Map orientation (orthogonal, isometric, etc.) | + | bounds | Map bounding rect. | + | tilesets | Array of stored tileset instances. | + | allowZoom | Allow camera zooming. | + | layers | Array of child layers. | - ```swift - let mapSize = tilemap.size // returns the size of the map (tiles). - let renderSize = tilemap.sizeInPoints // returns the size of the map (pixels). - let tileSize = tilemap.tileSize // returns the map tile size (pixels). - ``` */ -public class SKTilemap: SKNode, SKTiledObject { +public class SKTilemap: SKEffectNode, SKTiledObject { + /** ## Overview Enum describing map orientation type. + + ### Constants ### + + | Property | Description | + |:------------|:----------------------------------------------| + | orthogonal | Orthogonal(square tiles) tile map. | + | isometric | Isometric tile map. | + | hexagonal | Hexagonal tile map. | + | staggered | Staggered isometric tile map. | + */ public enum TilemapOrientation: String { case orthogonal @@ -187,86 +217,190 @@ public class SKTilemap: SKNode, SKTiledObject { case staggered } - /// File path. + + // MARK: Properties + + /// Source file path. public var url: URL! + /// Unique id. public var uuid: String = UUID().uuidString + /// Tiled application version. - public var tiledversion: String! - public var properties: [String: String] = [:] // custom properties - public var type: String! // map type - public var displayName: String! // map display name + public var tiledversion: String! // the Tiled version of this tilemap + + /// Custom properties. + public var properties: [String: String] = [:] + + /// Map type. + public var type: String! + + /// Ignore custom properties. + public var ignoreProperties: Bool = false + + /// Returns true if all of the child layers are rendered. + public internal(set) var isRendered: Bool = false + + /// Returns the render time of this map. + public internal(set) var mapRenderTime: TimeInterval = 0 + /// Size of map (in tiles). public var size: CGSize + /// Tile size (in pixels). public var tileSize: CGSize + /// Map orientation type. - public var orientation: TilemapOrientation // map orientation - internal var renderOrder: RenderOrder = .rightDown // render order + public var orientation: TilemapOrientation // map orientation + internal var renderOrder: RenderOrder = RenderOrder.rightDown // render order + + /// Map display name. Defaults to the current map source file name (minus the tmx extension). + public var displayName: String? + + /// String representing the map name. Defaults to the current map source file name (minus the tmx extension). + public var mapName: String { + if let dname = self.displayName { + return dname + } + return self.name ?? "map" + } + + // MARK: Updating // Update time properties. - private var lastUpdateTime : TimeInterval = 0 + private var lastUpdateTime: TimeInterval = 0 private let maximumUpdateDelta: TimeInterval = 1.0 / 60.0 + // MARK: Dispatch Queues + private let renderQueue = DispatchQueue(label: "com.sktiled.sktilemap.renderqueue", qos: .userInteractive, attributes: .concurrent) + private let animatedTilesQueue = DispatchQueue(label: "com.sktiled.sktilemap.tiles.animated.renderQueue", qos: .userInteractive, attributes: .concurrent) + private let staticTilesQueue = DispatchQueue(label: "com.sktiled.sktilemap.tiles.static.renderQueue", qos: .userInteractive, attributes: .concurrent) + /// Logging verbosity. - internal var loggingLevel: LoggingLevel = SKTiledLoggingLevel + internal var loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel - internal var maxRenderQuality: CGFloat = 16 // max render quality - /// Scaling value for text objects, etc. - public var renderQuality: CGFloat = 8 { // object render quality. + // MARK: Tile Update Mode + + /// Update mode used for tiles & objects. + public var updateMode: TileUpdateMode = TileUpdateMode.dynamic { + didSet { + guard (updateMode != oldValue) else { return } + // if we are in `none` mode, add/remove spritekit actions + let doRunActions = (updateMode == TileUpdateMode.actions) ? true : false + self.runAnimationAsActions(doRunActions) + } + } + + // MARK: Render Quality + + /// Maximum render quality + internal var maxRenderQuality: CGFloat = 16 + + /// Scaling factor for text objects, etc. + public var renderQuality: CGFloat = 2 { didSet { guard renderQuality != oldValue else { return } layers.forEach { $0.renderQuality = renderQuality.clamped(1, maxRenderQuality) } } } - // Set the speed of child layers + /// Map animation speed override public var speed: CGFloat { didSet { guard oldValue != speed else { return } - self.layers.forEach {$0.speed = speed} + self.layers.forEach { layer in + layer.speed = speed + } } } // hexagonal - public var hexsidelength: Int = 0 // hexagonal side length - internal var staggeraxis: StaggerAxis = .y // stagger axis - internal var staggerindex: StaggerIndex = .odd // stagger index. + public var hexsidelength: Int = 0 // hexagonal side length + internal var staggeraxis: StaggerAxis = StaggerAxis.y // stagger axis + internal var staggerindex: StaggerIndex = StaggerIndex.odd // stagger index. - // camera/scene - public var bounds: CGRect = .zero // current bounds - public var worldScale: CGFloat = 1.0 // initial world scale - public var currentZoom: CGFloat = 1.0 // zoom level + // MARK: Camera - /// Allow camera zoom. + /// Render statistics + public struct CameraZoomConstraints { + public var min: CGFloat = 0.2 + public var max: CGFloat = 5.0 + } + + public var zoomConstraints: CameraZoomConstraints = CameraZoomConstraints() + + /// Indicates map should auto-resize upon view changes. + public internal(set) var autoResize: Bool = false + + /// Map bounds. + public var bounds: CGRect = CGRect.zero + + /// Receive notifications from camera. + public var receiveCameraUpdates: Bool = true + + /// Display bounds that the tilemap is viewable in. + public var cameraBounds: CGRect? + public var nodesInView: [SKNode] = [] + internal var objectsOverlay: TileObjectOverlay = TileObjectOverlay() + + /// Initial world scale + public var worldScale: CGFloat = 1.0 + + /// Map zoom level + public var currentZoom: CGFloat = 1.0 + + /// Allow camera zooming. public var allowZoom: Bool = true + /// Allow camera movement. public var allowMovement: Bool = true - /// Minimum zoom level for the map. - public var minZoom: CGFloat = 0.2 - /// Mximum zoom level for the map. - public var maxZoom: CGFloat = 5.0 - /// Ignore custom properties. - public var ignoreProperties: Bool = false + // MARK: Tilesets /// Current tilesets. + public var firstGID: Int = 0 public var tilesets: Set = [] // current layers - private var _layers: Set = [] // tile map layers + private var _layers: Set = [] // tile map layers + /// Layer count. public var layerCount: Int { return self.layers.count } + /// Object count. + public var objectCount: Int { return self.getObjects(recursive: true).count } + /// Default z-position range between layers. public var zDeltaForLayers: CGFloat = 50 - public var bufferSize: CGFloat = 4.0 - /// Returns true if all of the child layers are rendered. - internal var isRendered: Bool { - return layers.filter { $0.isRendered == false }.isEmpty + + // MARK: Caching + + /// Storage for tile updates + internal var dataStorage: TileDataStorage? + + + // MARK: Render Statistics/Debugging + + /// Render statistics + public struct RenderStatistics { + var updateMode: TileUpdateMode = TileUpdateMode.dynamic + var objectCount: Int = 0 // tile objects + var visibleCount: Int = 0 // visible tiles + var cpuPercentage: Int = 0 // CPU usage + var effectsEnabled: Bool = false // tilemap effects enabled + var updatedThisFrame: Int = 0 // objects updated this frame + var objectsVisible: Bool = false // tilemap has objects visible + var renderTime: TimeInterval = 0 // frame render time } + /// Debugging/Render Statistics + internal var renderStatistics: RenderStatistics = RenderStatistics() + internal var renderStatisticsSampleFrequency: Int = 60 + internal var currentFrameIndex: Int = 0 + + // MARK: Layers + /// Returns a flattened array of child layers. public var layers: [SKTiledLayerObject] { var result: [SKTiledLayerObject] = [] @@ -310,35 +444,45 @@ public class SKTilemap: SKNode, SKTiledObject { } } + /// Debug visualization node + internal var debugNode: SKTiledDebugDrawNode! + + /// Debug visualization options. - public var debugDrawOptions: DebugDrawOptions { - get { - return defaultLayer.debugDrawOptions - } set { - defaultLayer.debugDrawOptions = newValue + public var debugDrawOptions: DebugDrawOptions = [] { + didSet { + debugNode?.draw() + + + let proxiesVisible = debugDrawOptions.contains(.drawObjectBounds) + let proxies = self.getObjectProxies() + + NotificationCenter.default.post( + name: Notification.Name.DataStorage.ProxyVisibilityChanged, + object: proxies, + userInfo: ["visibility": proxiesVisible] + ) } } /// Overlay color. public var overlayColor: SKColor = SKColor(hexString: "#40000000") - // MARK: - Object Colors + // MARK: Object Colors + public var objectColor: SKColor = SKColor.gray - public var color: SKColor = SKColor.clear // used for pausing - public var gridColor: SKColor = TiledObjectColors.obsidian // color used to visualize the tile grid - public var frameColor: SKColor = TiledObjectColors.obsidian // bounding box color - public var highlightColor: SKColor = TiledObjectColors.lime // color used to highlight tiles - public var navigationColor: SKColor = TiledObjectColors.lime // navigation graph color. - internal var autoResize: Bool = false // indicates map should auto-resize when view changes + public var color: SKColor = SKColor.clear // used for pausing + public var gridColor: SKColor = TiledGlobals.default.debug.gridColor // color used to visualize the tile grid + public var frameColor: SKColor = TiledGlobals.default.debug.frameColor // bounding box color + public var highlightColor: SKColor = TiledGlobals.default.debug.tileHighlightColor // color used to highlight tiles + public var navigationColor: SKColor = TiledGlobals.default.debug.navigationColor // navigation graph color. /// dynamics public var gravity: CGVector = CGVector.zero - /// Weak reference to `SKTilemapDelegate` delegate + /// Reference to `SKTilemapDelegate` delegate. weak public var delegate: SKTilemapDelegate? - /// Objects under the mouse cursor. - public var focusObjects: [SKNode] = [] - + /// Map frame. override public var frame: CGRect { //let cy = (heightOffset == 0) ? 0 : (heightOffset / 2) @@ -401,7 +545,7 @@ public class SKTilemap: SKNode, SKTiledObject { } /// Tile overlap amount. 1 is typically a good value. - public var tileOverlap: CGFloat = 0.5 { + public var tileOverlap: CGFloat = 1.0 { didSet { guard oldValue != tileOverlap else { return } for tileLayer in tileLayers(recursive: true) { @@ -411,23 +555,39 @@ public class SKTilemap: SKNode, SKTiledObject { } /// Global property to show/hide all `SKTileObject` objects. - public var showObjects: Bool = false { - didSet { - guard oldValue != showObjects else { return } - for objectGroup in objectGroups(recursive: true) { - objectGroup.showObjects = showObjects + public var showObjects: Bool { + get { + return debugDrawOptions.contains(.drawObjectBounds) + } set { + + if (newValue == true) { + debugDrawOptions.insert(.drawObjectBounds) + } else { + debugDrawOptions = debugDrawOptions.subtracting(.drawObjectBounds) } + + let proxies = self.getObjectProxies() + + NotificationCenter.default.post( + name: Notification.Name.DataStorage.ProxyVisibilityChanged, + object: proxies, + userInfo: ["visibility": newValue] + ) } } /** Show objects for the given layers. - - parameter forLayers: `[SKTiledLayerObject]` include nested layers. + - parameter forLayers: `[SKTiledLayerObject]` array of layers. - returns: `[SKTileLayer]` array of tile layers. */ public func showObjects(forLayers: [SKTiledLayerObject]) { - + forLayers.forEach { layer in + if let objGroup = layer as? SKObjectGroup { + objGroup.showObjects = true + } + } } /** @@ -436,7 +596,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTileLayer]` array of tile layers. */ - public func tileLayers(recursive: Bool=true) -> [SKTileLayer] { + public func tileLayers(recursive: Bool = true) -> [SKTileLayer] { return getLayers(recursive: recursive).sorted(by: { $0.index < $1.index }).filter { $0 as? SKTileLayer != nil } as! [SKTileLayer] } @@ -446,7 +606,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKObjectGroup]` array of object groups. */ - public func objectGroups(recursive: Bool=true) -> [SKObjectGroup] { + public func objectGroups(recursive: Bool = true) -> [SKObjectGroup] { return getLayers(recursive: recursive).sorted(by: { $0.index < $1.index }).filter { $0 as? SKObjectGroup != nil } as! [SKObjectGroup] } @@ -456,7 +616,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKImageLayer]` array of image layers. */ - public func imageLayers(recursive: Bool=true) -> [SKImageLayer] { + public func imageLayers(recursive: Bool = true) -> [SKImageLayer] { return getLayers(recursive: recursive).sorted(by: { $0.index < $1.index }).filter { $0 as? SKImageLayer != nil } as! [SKImageLayer] } @@ -466,7 +626,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKGroupLayer]` array of image layers. */ - public func groupLayers(recursive: Bool=true) -> [SKGroupLayer] { + public func groupLayers(recursive: Bool = true) -> [SKGroupLayer] { return getLayers(recursive: recursive).sorted(by: { $0.index < $1.index }).filter { $0 as? SKGroupLayer != nil } as! [SKGroupLayer] } @@ -500,50 +660,193 @@ public class SKTilemap: SKNode, SKTiledObject { // MARK: - Loading /** - Load a Tiled tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + + - parameter tmxFile: `String` Tiled file name. + - returns: `SKTilemap?` tilemap object (if file read succeeds). + */ + public class func load(tmxFile: String) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: nil, tilesetDataSource: nil, + updateMode: TiledGlobals.default.updateMode, + withTilesets: nil, ignoreProperties: false, + loggingLevel: TiledGlobals.default.loggingLevel, nil) + } + + /** + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + + - parameter tmxFile: `String` Tiled file name. + - parameter completion: `((_ tilemap: SKTilemap) -> Void)?` optional completion block. + - returns: `SKTilemap?` tilemap object (if file read succeeds). + */ + public class func load(tmxFile: String, _ completion: ((_ tilemap: SKTilemap) -> Void)? = nil) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: nil, tilesetDataSource: nil, + updateMode: TiledGlobals.default.updateMode, + withTilesets: nil, ignoreProperties: false, + loggingLevel: TiledGlobals.default.loggingLevel, completion) + } + + /** + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + + - parameter tmxFile: `String` Tiled file name. + - parameter loggingLevel: `LoggingLevel` logging verbosity level. + - returns: `SKTilemap?` tilemap object (if file read succeeds). + */ + public class func load(tmxFile: String, loggingLevel: LoggingLevel) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: nil, tilesetDataSource: nil, + updateMode: TiledGlobals.default.updateMode, withTilesets: nil, + ignoreProperties: false, loggingLevel: loggingLevel, nil) + } + + /** + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + + - parameter tmxFile: `String` Tiled file name. + - parameter delegate: `SKTilemapDelegate` tilemap [delegate](Protocols/SKTilemapDelegate.html) instance. + - returns: `SKTilemap?` tilemap object (if file read succeeds). + */ + public class func load(tmxFile: String, delegate: SKTilemapDelegate) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: delegate, tilesetDataSource: nil, + updateMode: TiledGlobals.default.updateMode, withTilesets: nil, + ignoreProperties: false, loggingLevel: TiledGlobals.default.loggingLevel, nil) + } + + /** + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + + - parameter tmxFile: `String` Tiled file name. + - parameter delegate: `SKTilemapDelegate` tilemap [delegate](Protocols/SKTilemapDelegate.html) instance. + - parameter updateMode: `TileUpdateMode` tile update mode. + - returns: `SKTilemap?` tilemap object (if file read succeeds). + */ + public class func load(tmxFile: String, delegate: SKTilemapDelegate, updateMode: TileUpdateMode) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: delegate, tilesetDataSource: nil, + updateMode: updateMode, withTilesets: nil, + ignoreProperties: false, loggingLevel: TiledGlobals.default.loggingLevel, nil) + } + + /** + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + + - parameter tmxFile: `String` Tiled file name. + - parameter delegate: `SKTilemapDelegate` tilemap [delegate](Protocols/SKTilemapDelegate.html) instance. + - parameter tilesetDataSource: `SKTilesetDataSource` tilemap [`SKTilesetDataSource`](Protocols/SKTilesetDataSource.html) instance. + - returns: `SKTilemap?` tilemap object (if file read succeeds). + */ + public class func load(tmxFile: String, delegate: SKTilemapDelegate, tilesetDataSource: SKTilesetDataSource) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: delegate, tilesetDataSource: tilesetDataSource, + updateMode: TiledGlobals.default.updateMode, withTilesets: nil, + ignoreProperties: false, loggingLevel: TiledGlobals.default.loggingLevel, nil) + } + + + /** + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. - - parameter filename: `String` Tiled file name. + - parameter tmxFile: `String` Tiled file name. + - parameter delegate: `SKTilemapDelegate` tilemap [delegate](Protocols/SKTilemapDelegate.html) instance. + - parameter tilesetDataSource: `SKTilesetDataSource` tilemap [`SKTilesetDataSource`](Protocols/SKTilesetDataSource.html) instance. + - parameter updateMode: `TileUpdateMode` tile update mode. + - returns: `SKTilemap?` tilemap object (if file read succeeds). + */ + public class func load(tmxFile: String, delegate: SKTilemapDelegate, tilesetDataSource: SKTilesetDataSource, updateMode: TileUpdateMode) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: delegate, tilesetDataSource: tilesetDataSource, + updateMode: updateMode, withTilesets: nil, + ignoreProperties: false, loggingLevel: TiledGlobals.default.loggingLevel, nil) + } + + /** + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + + - parameter tmxFile: `String` Tiled file name. + - parameter delegate: `SKTilemapDelegate` tilemap [delegate](Protocols/SKTilemapDelegate.html) instance. + - parameter tilesetDataSource: `SKTilesetDataSource` tilemap [`SKTilesetDataSource`](Protocols/SKTilesetDataSource.html) instance. + - parameter withTilesets: `[SKTileset]` pre-loaded tilesets. + - returns: `SKTilemap?` tilemap object (if file read succeeds). + */ + public class func load(tmxFile: String, delegate: SKTilemapDelegate, tilesetDataSource: SKTilesetDataSource, withTilesets: [SKTileset]) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: delegate, tilesetDataSource: tilesetDataSource, + updateMode: TiledGlobals.default.updateMode, withTilesets: withTilesets, + ignoreProperties: false, loggingLevel: TiledGlobals.default.loggingLevel, nil) + } + + /** + Load a **Tiled** tmx file and return a new `SKTilemap` object. Returns nil if there is a parsing error. + + - parameter tmxFile: `String` Tiled file name. - parameter inDirectory: `String?` search path for assets. - parameter delegate: `SKTilemapDelegate?` optional [`SKTilemapDelegate`](Protocols/SKTilemapDelegate.html) instance. + - parameter tilesetDataSource: `SKTilesetDataSource?` optional [`SKTilesetDataSource`](Protocols/SKTilesetDataSource.html) instance. + - parameter updateMode: `TileUpdateMode` tile update mode. - parameter withTilesets: `[SKTileset]?` optional tilesets. - parameter ignoreProperties: `Bool` ignore custom properties from Tiled. - parameter loggingLevel: `LoggingLevel` logging verbosity level. + - parameter completion: `((_ tilemap: SKTilemap) -> Void)?` optional completion block. - returns: `SKTilemap?` tilemap object (if file read succeeds). */ public class func load(tmxFile: String, inDirectory: String? = nil, delegate: SKTilemapDelegate? = nil, + tilesetDataSource: SKTilesetDataSource? = nil, + updateMode: TileUpdateMode = TiledGlobals.default.updateMode, withTilesets: [SKTileset]? = nil, ignoreProperties noparse: Bool = false, - loggingLevel: LoggingLevel = .info) -> SKTilemap? { + loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel, + _ completion: ((_ tilemap: SKTilemap) -> Void)? = nil) -> SKTilemap? { let startTime = Date() - let queue = DispatchQueue(label: "com.sktiled.renderqueue", qos: .userInteractive) + let queue = DispatchQueue(label: "com.sktiled.loadingQueue", qos: .userInteractive) if let tilemap = SKTilemapParser().load(tmxFile: tmxFile, inDirectory: inDirectory, delegate: delegate, + tilesetDataSource: tilesetDataSource, + updateMode: updateMode, withTilesets: withTilesets, ignoreProperties: noparse, loggingLevel: loggingLevel, renderQueue: queue) { - - let renderTime = Date().timeIntervalSince(startTime) - let timeStamp = String(format: "%.\(String(3))f", renderTime) + // set the map render time attribute + tilemap.mapRenderTime = Date().timeIntervalSince(startTime) + let timeStamp = String(format: "%.\(String(3))f", tilemap.mapRenderTime) Logger.default.log("tilemap \"\(tilemap.mapName)\" rendered in: \(timeStamp)s", level: .success) + + // completion handler + completion?(tilemap) return tilemap } return nil } // MARK: - Init + + /** + Default initializer. + */ + required public init?(coder aDecoder: NSCoder) { + size = CGSize.zero + tileSize = CGSize.zero + orientation = .orthogonal + super.init(coder: aDecoder) + self.setupNotifications() + } + /** Initialize with dictionary attributes from xml parser. - parameter attributes: `Dictionary` attributes dictionary. - - returns: `SKTileMapNode?` + - returns: `SKTilemap?` */ public init?(attributes: [String: String]) { guard let width = attributes["width"] else { return nil } @@ -586,7 +889,7 @@ public class SKTilemap: SKNode, SKTiledObject { // hex stagger index if let hexIndex = attributes["staggerindex"] { - guard let hexindex: StaggerIndex = StaggerIndex(rawValue: hexIndex) else { + guard let hexindex: StaggerIndex = StaggerIndex(string: hexIndex) else { fatalError("stagger index \"\(hexIndex)\" not supported.") } self.staggerindex = hexindex @@ -595,13 +898,15 @@ public class SKTilemap: SKNode, SKTiledObject { // Tiled application version if let tiledVersion = attributes["tiledversion"] { self.tiledversion = tiledVersion - SKTiledTiledApplicationVersion = tiledVersion } // global antialiasing antialiasLines = (currentZoom < 1) super.init() + // turn off effects rendering by default + shouldEnableEffects = false + // set the background color if let backgroundHexColor = attributes["backgroundcolor"] { if (ignoreBackground == false) { @@ -615,17 +920,36 @@ public class SKTilemap: SKNode, SKTiledObject { // keep renderQuality within texture size limits let renderSize = CGSize(width: size.width * tileSize.width, height: size.height * tileSize.height) - let largestDimension = (renderSize.width > renderSize.height) ? renderSize.width : renderSize.height - maxRenderQuality = CGFloat(Int(16000 / (largestDimension * SKTiledContentScaleFactor))) + let largestPixelDimension: CGFloat = (renderSize.width > renderSize.height) ? renderSize.width : renderSize.height + + // calculate the ideal max render quality (max size is 16384) + maxRenderQuality = CGFloat(Int(4000 / (largestPixelDimension * TiledGlobals.default.contentScale))) - // clamp max render quality - maxRenderQuality = (maxRenderQuality > 16) ? 16 : maxRenderQuality + let remainder = maxRenderQuality.truncatingRemainder(dividingBy: 2) + maxRenderQuality += remainder + + // cap maximum render quality + maxRenderQuality = (TiledGlobals.default.renderQuality.override == 0) ? (maxRenderQuality > 16) ? 16 : maxRenderQuality : TiledGlobals.default.renderQuality.override #if os(iOS) renderQuality = maxRenderQuality / 4 #else renderQuality = maxRenderQuality / 2 #endif + + // cap render quality + renderQuality = (renderQuality > maxRenderQuality) ? maxRenderQuality : (renderQuality > 8) ? 8 : renderQuality + + + // debug node + self.debugNode = SKTiledDebugDrawNode(tileLayer: self.defaultLayer, isDefault: true) + self.debugNode.zPosition = zPosition + zDeltaForLayers + self.objectsOverlay.zPosition = zPosition + (zDeltaForLayers * 2) + + addChild(debugNode) + addChild(objectsOverlay) + + self.setupNotifications() } /** @@ -646,10 +970,15 @@ public class SKTilemap: SKNode, SKTiledObject { self.orientation = orientation self.antialiasLines = (currentZoom < 1) super.init() + + // turn off effects rendering by default + shouldEnableEffects = false + self.setupNotifications() } - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + deinit { + self.dataStorage = nil + NotificationCenter.default.removeObserver(self, name: Notification.Name.Layer.ObjectAdded, object: nil) } // MARK: - Tilesets @@ -703,6 +1032,19 @@ public class SKTilemap: SKNode, SKTiledObject { return nil } + /** + Returns the tileset associated with a global id. + + - parameter forTile: `Int` tile global id. + - returns: `SKTileset?` associated tileset. + */ + public func getTileset(forTile: Int) -> SKTileset? { + guard let tiledata = getTileData(globalID: forTile) else { + return nil + } + return tiledata.tileset + } + // MARK: Coordinates /** Returns a point for a given coordinate in the layer, with optional offset values for x/y. @@ -712,7 +1054,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter offsetY: `CGFloat` y-offset value. - returns: `CGPoint` point in layer. */ - public func pointForCoordinate(coord: CGPoint, offsetX: CGFloat=0, offsetY: CGFloat=0) -> CGPoint { + public func pointForCoordinate(coord: CGPoint, offsetX: CGFloat = 0, offsetY: CGFloat = 0) -> CGPoint { return defaultLayer.pointForCoordinate(coord: coord, offsetX: offsetX, offsetY: offsetY) } @@ -736,6 +1078,16 @@ public class SKTilemap: SKNode, SKTiledObject { return defaultLayer.coordinateForPoint(point) } + /** + Returns a tile coordinate for a given point in the layer as a vector_int2. + + - parameter point: `CGPoint` point in layer. + - returns: `int2` tile coordinate. + */ + public func vectorCoordinateForPoint(_ point: CGPoint) -> int2 { + return defaultLayer.vectorCoordinateForPoint(point) + } + // MARK: - Layers /** Returns an array of child layers, sorted by index (first is lowest, last is highest). @@ -743,7 +1095,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTiledLayerObject]` array of layers. */ - public func getLayers(recursive: Bool=true) -> [SKTiledLayerObject] { + public func getLayers(recursive: Bool = true) -> [SKTiledLayerObject] { return (recursive == true) ? self.layers : Array(self._layers) } @@ -762,7 +1114,7 @@ public class SKTilemap: SKNode, SKTiledObject { - returns: `[String]` layer names. */ public func layerNames() -> [String] { - return layers.flatMap { $0.name } + return layers.compactMap { $0.name } } /** @@ -773,6 +1125,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter clamped: `Bool` clamp position to nearest pixel. - returns: `(success: Bool, layer: SKTiledLayerObject)` add was successful, layer added. */ + @discardableResult public func addLayer(_ layer: SKTiledLayerObject, group: SKGroupLayer? = nil, clamped: Bool = true) -> (success: Bool, layer: SKTiledLayerObject) { // if a group is indicated, add it to that instead @@ -881,7 +1234,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTiledLayerObject]` layer objects. */ - public func getLayers(named layerName: String, recursive: Bool=true) -> [SKTiledLayerObject] { + public func getLayers(named layerName: String, recursive: Bool = true) -> [SKTiledLayerObject] { var result: [SKTiledLayerObject] = [] let layersToCheck = self.getLayers(recursive: recursive) if let index = layersToCheck.index(where: { $0.name == layerName }) { @@ -897,7 +1250,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTiledLayerObject]` layer objects. */ - public func getLayers(withPrefix: String, recursive: Bool=true) -> [SKTiledLayerObject] { + public func getLayers(withPrefix: String, recursive: Bool = true) -> [SKTiledLayerObject] { var result: [SKTiledLayerObject] = [] let layersToCheck = self.getLayers(recursive: recursive) if let index = layersToCheck.index(where: { $0.layerName.hasPrefix(withPrefix) }) { @@ -955,7 +1308,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTiledLayerObject]` array of layers. */ - public func getLayers(ofType: String, recursive: Bool=true) -> [SKTiledLayerObject] { + public func getLayers(ofType: String, recursive: Bool = true) -> [SKTiledLayerObject] { return getLayers(recursive: recursive).filter { $0.type != nil }.filter { $0.type! == ofType } } @@ -966,7 +1319,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTileLayer]` array of tile layers. */ - public func tileLayers(named layerName: String, recursive: Bool=true) -> [SKTileLayer] { + public func tileLayers(named layerName: String, recursive: Bool = true) -> [SKTileLayer] { return getLayers(recursive: recursive).filter { $0 as? SKTileLayer != nil }.filter { $0.name == layerName } as! [SKTileLayer] } @@ -977,7 +1330,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTileLayer]` array of tile layers. */ - public func tileLayers(withPrefix: String, recursive: Bool=true) -> [SKTileLayer] { + public func tileLayers(withPrefix: String, recursive: Bool = true) -> [SKTileLayer] { return getLayers(recursive: recursive).filter { $0 as? SKTileLayer != nil }.filter { $0.layerName.hasPrefix(withPrefix) } as! [SKTileLayer] } @@ -988,7 +1341,7 @@ public class SKTilemap: SKNode, SKTiledObject { - returns: `SKTileLayer?` matching tile layer. */ public func tileLayer(atIndex index: Int) -> SKTileLayer? { - if let layerIndex = tileLayers(recursive: false).index(where: { $0.index == index } ) { + if let layerIndex = tileLayers(recursive: false).index(where: {$0.index == index} ) { let layer = tileLayers(recursive: false)[layerIndex] return layer } @@ -1002,7 +1355,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKObjectGroup]` array of object groups. */ - public func objectGroups(named layerName: String, recursive: Bool=true) -> [SKObjectGroup] { + public func objectGroups(named layerName: String, recursive: Bool = true) -> [SKObjectGroup] { return getLayers(recursive: recursive).filter { $0 as? SKObjectGroup != nil }.filter { $0.name == layerName } as! [SKObjectGroup] } @@ -1013,7 +1366,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKObjectGroup]` array of object groups. */ - public func objectGroups(withPrefix: String, recursive: Bool=true) -> [SKObjectGroup] { + public func objectGroups(withPrefix: String, recursive: Bool = true) -> [SKObjectGroup] { return getLayers(recursive: recursive).filter { $0 as? SKObjectGroup != nil }.filter { $0.layerName.hasPrefix(withPrefix) } as! [SKObjectGroup] } @@ -1024,7 +1377,7 @@ public class SKTilemap: SKNode, SKTiledObject { - returns: `SKObjectGroup?` matching group layer. */ public func objectGroup(atIndex index: Int) -> SKObjectGroup? { - if let layerIndex = objectGroups(recursive: false).index(where: { $0.index == index } ) { + if let layerIndex = objectGroups(recursive: false).index(where: {$0.index == index} ) { let layer = objectGroups(recursive: false)[layerIndex] return layer } @@ -1038,7 +1391,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKImageLayer]` array of image layers. */ - public func imageLayers(named layerName: String, recursive: Bool=true) -> [SKImageLayer] { + public func imageLayers(named layerName: String, recursive: Bool = true) -> [SKImageLayer] { return getLayers(recursive: recursive).filter { $0 as? SKImageLayer != nil }.filter { $0.name == layerName } as! [SKImageLayer] } @@ -1049,7 +1402,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKImageLayer]` array of image layers. */ - public func imageLayers(withPrefix: String, recursive: Bool=true) -> [SKImageLayer] { + public func imageLayers(withPrefix: String, recursive: Bool = true) -> [SKImageLayer] { return getLayers(recursive: recursive).filter { $0 as? SKImageLayer != nil }.filter { $0.layerName.hasPrefix(withPrefix) } as! [SKImageLayer] } @@ -1060,7 +1413,7 @@ public class SKTilemap: SKNode, SKTiledObject { - returns: `SKImageLayer?` matching image layer. */ public func imageLayer(atIndex index: Int) -> SKImageLayer? { - if let layerIndex = imageLayers(recursive: false).index(where: { $0.index == index } ) { + if let layerIndex = imageLayers(recursive: false).index(where: {$0.index == index} ) { let layer = imageLayers(recursive: false)[layerIndex] return layer } @@ -1074,7 +1427,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKGroupLayer]` array of group layers. */ - public func groupLayers(named layerName: String, recursive: Bool=true) -> [SKGroupLayer] { + public func groupLayers(named layerName: String, recursive: Bool = true) -> [SKGroupLayer] { return getLayers(recursive: recursive).filter { $0 as? SKGroupLayer != nil }.filter { $0.name == layerName } as! [SKGroupLayer] } @@ -1085,7 +1438,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKGroupLayer]` array of group layers. */ - public func groupLayers(withPrefix: String, recursive: Bool=true) -> [SKGroupLayer] { + public func groupLayers(withPrefix: String, recursive: Bool = true) -> [SKGroupLayer] { return getLayers(recursive: recursive).filter { $0 as? SKGroupLayer != nil }.filter { $0.layerName.hasPrefix(withPrefix) } as! [SKGroupLayer] } @@ -1140,7 +1493,7 @@ public class SKTilemap: SKNode, SKTiledObject { // clamp the layer position if (clamped == true) { - let scaleFactor = SKTiledContentScaleFactor + let scaleFactor = TiledGlobals.default.contentScale layerPos = clampedPosition(point: layerPos, scale: scaleFactor) } layer.position = layerPos @@ -1153,7 +1506,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter clamped: `Bool` clamp position to nearest pixel. - parameter offset: `CGPoint` node offset amount. */ - internal func positionNode(_ node: SKNode, clamped: Bool = true, offset: CGPoint = .zero) { + internal func positionNode(_ node: SKNode, clamped: Bool = true, offset: CGPoint = CGPoint.zero) { var nodePosition = CGPoint.zero @@ -1180,7 +1533,7 @@ public class SKTilemap: SKNode, SKTiledObject { // clamp the node position if (clamped == true) { - let scaleFactor = SKTiledContentScaleFactor + let scaleFactor = TiledGlobals.default.contentScale nodePosition = clampedPosition(point: nodePosition, scale: scaleFactor) } @@ -1218,7 +1571,7 @@ public class SKTilemap: SKNode, SKTiledObject { - returns: `[SKTile]` array of tiles. */ public func tilesAt(coord: CGPoint) -> [SKTile] { - return tileLayers(recursive: true).flatMap { $0.tileAt(coord: coord) } + return tileLayers(recursive: true).compactMap { $0.tileAt(coord: coord) } } /** @@ -1266,7 +1619,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTile]` array of tiles. */ - public func getTiles(recursive: Bool=true) -> [SKTile] { + public func getTiles(recursive: Bool = true) -> [SKTile] { return tileLayers(recursive: recursive).flatMap { $0.getTiles() } } @@ -1277,7 +1630,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTile]` array of tiles. */ - public func getTiles(ofType: String, recursive: Bool=true) -> [SKTile] { + public func getTiles(ofType: String, recursive: Bool = true) -> [SKTile] { return tileLayers(recursive: recursive).flatMap { $0.getTiles(ofType: ofType) } } @@ -1288,7 +1641,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTile]` array of tiles. */ - public func getTiles(globalID: Int, recursive: Bool=true) -> [SKTile] { + public func getTiles(globalID: Int, recursive: Bool = true) -> [SKTile] { return tileLayers(recursive: recursive).flatMap { $0.getTiles(globalID: globalID) } } @@ -1300,7 +1653,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTile]` array of tiles. */ - public func getTilesWithProperty(_ named: String, _ value: Any, recursive: Bool=true) -> [SKTile] { + public func getTilesWithProperty(_ named: String, _ value: Any, recursive: Bool = true) -> [SKTile] { var result: [SKTile] = [] for layer in tileLayers(recursive: recursive) { result += layer.getTilesWithProperty(named, value) @@ -1308,12 +1661,13 @@ public class SKTilemap: SKNode, SKTiledObject { return result } + /** Returns an array of all animated tile objects. - returns: `[SKTile]` array of tiles. */ - public func animatedTiles(recursive: Bool=true) -> [SKTile] { + public func animatedTiles(recursive: Bool = true) -> [SKTile] { return tileLayers(recursive: recursive).flatMap { $0.animatedTiles() } } @@ -1380,6 +1734,17 @@ public class SKTilemap: SKNode, SKTiledObject { return tilesets.flatMap { $0.getTileData(withProperty: named, value) } } + /** + Returns tile data with the given name & animated state. + + - parameter named: `String` data name. + - parameter isAnimated: `Bool` filter data that is animated. + - returns: `[SKTilesetData]` array of tile data. + */ + public func getTileData(named name: String, isAnimated: Bool = false) -> [SKTilesetData] { + return tilesets.flatMap { $0.getTileData(named: name, isAnimated: isAnimated) } + } + // MARK: - Objects /** @@ -1400,7 +1765,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTileObject]` array of objects. */ - public func getObjects(recursive: Bool=true) -> [SKTileObject] { + public func getObjects(recursive: Bool = true) -> [SKTileObject] { return objectGroups(recursive: recursive).flatMap { $0.getObjects() } } @@ -1411,7 +1776,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTileObject]` array of objects. */ - public func getObjects(ofType: String, recursive: Bool=true) -> [SKTileObject] { + public func getObjects(ofType: String, recursive: Bool = true) -> [SKTileObject] { return objectGroups(recursive: recursive).flatMap { $0.getObjects(ofType: ofType) } } @@ -1422,7 +1787,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTileObject]` array of objects. */ - public func getObjects(named: String, recursive: Bool=true) -> [SKTileObject] { + public func getObjects(named: String, recursive: Bool = true) -> [SKTileObject] { return objectGroups(recursive: recursive).flatMap { $0.getObjects(named: named) } } @@ -1433,7 +1798,7 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTileObject]` array of objects. */ - public func getObjects(withText text: String, recursive: Bool=true) -> [SKTileObject] { + public func getObjects(withText text: String, recursive: Bool = true) -> [SKTileObject] { return objectGroups(recursive: recursive).flatMap { $0.getObjects(withText: text) } } @@ -1444,7 +1809,16 @@ public class SKTilemap: SKNode, SKTiledObject { - returns: `SKTileObject?` */ public func getObject(withID id: Int) -> SKTileObject? { - return objectGroups(recursive: true).flatMap { $0.getObject(withID: id) }.first + return objectGroups(recursive: true).compactMap { $0.getObject(withID: id) }.first + } + + /** + Return object proxies. + + - returns: `[TileObjectProxy]` array of object proxies. + */ + internal func getObjectProxies() -> [TileObjectProxy] { + return objectGroups().flatMap { $0.getObjectProxies() } } /** @@ -1453,21 +1827,44 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter recursive: `Bool` include nested layers. - returns: `[SKTileObject]` objects with a tile gid. */ - public func tileObjects(recursive: Bool=true) -> [SKTileObject] { + public func tileObjects(recursive: Bool = true) -> [SKTileObject] { return objectGroups(recursive: recursive).flatMap { $0.tileObjects() } } + /** + Return objects with a tile id. If recursive is false, only returns objects from top-level layers. + + - parameter globalID: `Int` global tile id. + - parameter recursive: `Bool` include nested layers. + - returns: `[SKTileObject]` objects with a tile gid. + */ + public func tileObjects(globalID: Int, recursive: Bool = true) -> [SKTileObject] { + return objectGroups(recursive: recursive).flatMap { $0.tileObjects(globalID: globalID) } + } + /** Return text objects. If recursive is false, only returns objects from top-level layers. - parameter recursive: `Bool` include nested layers. - returns: `[SKTileObject]` text objects. */ - public func textObjects(recursive: Bool=true) -> [SKTileObject] { + public func textObjects(recursive: Bool = true) -> [SKTileObject] { return objectGroups(recursive: recursive).flatMap { $0.textObjects() } } // MARK: - Coordinates + + /** + Returns true if the coordinate is valid. + + - parameter coord: `CGPoint` tile coordinate. + - returns: `Bool` coodinate is valid. + */ + public func isValid(coord: CGPoint) -> Bool { + return defaultLayer.isValid(Int(coord.x), Int(coord.y)) + } + + /** Returns a touch location in negative-y space. @@ -1516,14 +1913,32 @@ public class SKTilemap: SKNode, SKTiledObject { } #endif + // MARK: - Shaders + + /** + Set a shader for the tile layer. + + - parameter named: `String` shader file name. + - parameter uniforms: `[SKUniform]` array of shader uniforms. + */ + public func setShader(named: String, uniforms: [SKUniform] = []) { + let fshader = SKShader(fileNamed: named) + fshader.uniforms = uniforms + shouldEnableEffects = true + self.shader = fshader + } + // MARK: - Callbacks + /** Called when parser has finished reading the map. - parameter timeStarted: `Date` render start time. - parameter tasks: `Int` number of tasks to complete. */ - public func didFinishParsing(timeStarted: Date, tasks: Int=0) {} + public func didFinishParsing(timeStarted: Date, tasks: Int = 0) { + NotificationCenter.default.post(name: Notification.Name(rawValue: "tilemapFinishedParsing"), object: nil, userInfo: ["parseTime": timeStarted]) + } /** Called when parser has finished rendering the map. @@ -1531,7 +1946,8 @@ public class SKTilemap: SKNode, SKTiledObject { - parameter timeStarted: `Date` render start time. */ public func didFinishRendering(timeStarted: Date) { - log("rendering finished!", level: .debug) + // set the `isRendered` property + isRendered = layers.filter { $0.isRendered == false }.isEmpty // set the z-depth of the defaultLayer & background sprite defaultLayer.zPosition = -zDeltaForLayers @@ -1542,55 +1958,401 @@ public class SKTilemap: SKNode, SKTiledObject { // delegate callback defer { self.delegate?.didRenderMap(self) + NotificationCenter.default.post( + name: Notification.Name.Map.FinishedRendering, + object: self, + userInfo: ["renderTime": timeStarted] + ) } + // run animation actions + self.runAnimationAsActions(TiledGlobals.default.updateMode == TileUpdateMode.actions) + // clamp the position of the map & parent nodes - clampPositionWithNode(node: self, scale: getContentScaleFactor()) + clampNodePosition(node: self, scale: TiledGlobals.default.contentScale) + + // set the `SKTilemap.bounds` attribute + //let vertices = getVertices() + + // set the debug zPosition + let debugStartZPosition = (lastZPosition + zDeltaForLayers) + debugNode.zPosition = debugStartZPosition + debugNode.position = defaultLayer.position + objectsOverlay.zPosition = debugStartZPosition + (zDeltaForLayers + 100) + updateProxyObjects() } - // MARK: - Updating + // MARK: - Notifications /** - Update the map as each frame is rendered. - - - parameter currentTime: `TimeInterval` update interval. + Setup notification callbacks. */ - public func update(_ currentTime: TimeInterval) { - // Initialize lastUpdateTime - if (self.lastUpdateTime == 0) { - self.lastUpdateTime = currentTime + internal func setupNotifications() { + // nuttin here + } + + internal func updateProxyObjects() { + guard let dataStorage = dataStorage else { + log("cannot access tile data storage.", level: .error) + return } - // Calculate time since last update - var dt = currentTime - self.lastUpdateTime - dt = dt > maximumUpdateDelta ? maximumUpdateDelta : dt + // clear the layer + objectsOverlay.removeAllChildren() - self.lastUpdateTime = currentTime + var proxyCount = 0 - // Update layers - self.layers.forEach { layer in - layer.update(dt) + renderQueue.sync { + for object in dataStorage.objectsList { + // create a proxy + let proxyObject = TileObjectProxy(object: object, visible: self.showObjects, renderable: object.isRenderableType) + self.objectsOverlay.addChild(proxyObject) + proxyObject.container = self.objectsOverlay + proxyObject.zPosition = self.zDeltaForLayers + proxyObject.draw() + proxyCount += 1 + } } + objectsOverlay.initialized = true } -} -// MARK: - Extensions + /** + Post render stats to listeners. -extension SKTilemap.TilemapOrientation { + - parameter renderStart: `Date` render start date. + - parameter completion: `() -> Void?` optional completion function. + */ + internal func postRenderStatistics(_ renderStart: Date, _ completion: (() -> Void)? = nil) { + // copy the render stats and add render time + var renderStatsToSend = self.renderStatistics.copy() + renderStatsToSend.renderTime = Date().timeIntervalSince(renderStart) - /// Hint for aligning tiles within each layer. - public var alignmentHint: CGPoint { - switch self { - case .orthogonal: - return CGPoint(x: 0.5, y: 0.5) - case .isometric: - return CGPoint(x: 0.5, y: 0.5) - case .hexagonal: - return CGPoint(x: 0.5, y: 0.5) - case .staggered: - return CGPoint(x: 0.5, y: 0.5) - } + renderQueue.sync { + + // update observers + NotificationCenter.default.post( + name: Notification.Name.Map.RenderStatsUpdated, + object: renderStatsToSend + ) + + completion?() + } + } + + + // MARK: - SpriteKit Actions + + /** + Run layer animations as SpriteKit actions. + + - parameter value: `Bool` on/off toggle. + - parameter restore: `Bool` restore textures. + */ + public func runAnimationAsActions(_ value: Bool, restore: Bool = true) { + self.layers.forEach { layer in + if (value == true) { + layer.runAnimationAsActions() + } else { + layer.removeAnimationActions(restore: restore) + } + } + } + + // MARK: - Updating + + + /** + Update the map as each frame is rendered. + + - parameter currentTime: `TimeInterval` update interval. + */ + public func update(_ currentTime: TimeInterval) { + guard (isRendered == true) && (isPaused == false) else { + return + } + + defer { + // sync all queues + staticTilesQueue.sync {} + animatedTilesQueue.sync {} + renderStatistics.updatedThisFrame = 0 + } + + // initialize last update time + if (self.lastUpdateTime == 0) { + self.lastUpdateTime = currentTime + } + + // time since last update + var dt = currentTime - self.lastUpdateTime + dt = dt > maximumUpdateDelta ? maximumUpdateDelta : dt + + self.lastUpdateTime = currentTime + + // (re)draw proxy objects + if (objectsOverlay.initialized == false) { + self.updateProxyObjects() + } + + // update tiles + guard let dataStorage = dataStorage else { return } + + // render start time + let renderStart = Date() + + switch updateMode { + + case .full: + // update cached tiles + self.updateStaticTiles(delta: dt) { fcount in + self.renderStatistics.updatedThisFrame += fcount + } + + // update animated tiles + self.updateAnimatedTiles(delta: dt) { fcount in + self.renderStatistics.updatedThisFrame += fcount + } + + case .dynamic: + // update animated tiles + self.updateAnimatedTiles(delta: dt) { fcount in + self.renderStatistics.updatedThisFrame += fcount + } + + default: + break + } + + if (currentFrameIndex >= renderStatisticsSampleFrequency) { + // update render statistics + renderStatistics.updateMode = updateMode + renderStatistics.objectCount = dataStorage.objectsList.count + renderStatistics.objectsVisible = (showObjects == true) + renderStatistics.visibleCount = nodesInView.count + renderStatistics.effectsEnabled = shouldEnableEffects + + if (TiledGlobals.default.enableRenderCallbacks == true) { + renderStatistics.cpuPercentage = Int(cpuUsage()) + // send render statistics back to the controller + self.postRenderStatistics(renderStart) { + self.currentFrameIndex = 0 + } + } + } + + currentFrameIndex += 1 + } + + /** + Update static tiles. + + - parameter delta: `TimeInterval` current time delta. + */ + internal func updateStaticTiles(delta: TimeInterval, _ completion: ((Int) -> Void)? = nil) { + guard let dataStorage = dataStorage else { return } + + var staticTilesUpdated = 0 + + staticTilesQueue.async { + + for staticItem in dataStorage.staticTileCache.enumerated() { + + let tileData = staticItem.element.key + let tileArray = staticItem.element.value + let tileTexture = tileData.texture + + + // loop through tiles + for tile in tileArray { + + // ignore tiles not in view + if (tile.visibleToCamera) == false { + continue + } + + switch tile.renderMode { + + // tile is ignoring it's tile data, move on + case .ignore: + continue + + default: + + // for `default` & `static`, just update the tile texture and continue... + guard let tileTexture = tileTexture else { + continue + } + + tile.texture = tileTexture + tile.size = tileTexture.size() + } + + staticTilesUpdated += 1 + } + } + + if (TiledGlobals.default.enableRenderCallbacks == true) { + DispatchQueue.main.async { + completion?(staticTilesUpdated) + } + } + } + } + + + /** + Update cached animated tiles. + + - parameter delta: `TimeInterval` current time delta. + */ + internal func updateAnimatedTiles(delta: TimeInterval, _ completion: ((Int) -> Void)? = nil) { + guard let dataStorage = dataStorage else { return } + + var animatedTilesUpdated = 0 + + animatedTilesQueue.async { + for animatedItem in dataStorage.animatedTileCache.enumerated() { + + let tileData = animatedItem.element.key + let tileArray = animatedItem.element.value + + + // figure out which frame of animation we're at... + let cycleTime = tileData.animationTime + guard (cycleTime > 0) else { continue } + + // array of frame values + let frames: [TileAnimationFrame] = (self.speed >= 0) ? tileData.frames : tileData.frames.reversed() + + // increment the current time value + tileData.currentTime += (delta * abs(Double(self.speed))) + + // current time in ms + let ct: Int = Int(tileData.currentTime * 1000) + + // current frame + var cf: UInt8? = nil + + var aggregate = 0 + + // get the frame at the current time + for (idx, frame) in frames.enumerated() { + aggregate += frame.duration + + if ct < aggregate { + if cf == nil { + cf = UInt8(idx) + } + } + } + + // create a pointer to the texture we're planning to use... + var currentTexture: SKTexture? + + // set texture for current frame + if let currentFrame = cf { + + // stash the frame index + tileData.frameIndex = currentFrame + let frame = frames[Int(currentFrame)] + + if let frameTexture = frame.texture { + // update frame texture + currentTexture = frameTexture + } + } + + // the the current time is greater than the animation cycle, reset current time to 0 + if ct >= cycleTime { tileData.currentTime = 0 } + + + // loop through tiles + for tile in tileArray { + + // ignore tiles not in view + if (tile.visibleToCamera) == false { + continue + } + + switch tile.renderMode { + + case .ignore, .static: + continue + + default: + + if let frameTexture = currentTexture { + tile.texture = frameTexture + tile.size = frameTexture.size() + } + } + animatedTilesUpdated += 1 + } + } + + if (TiledGlobals.default.enableRenderCallbacks == true) { + DispatchQueue.main.async { + completion?(animatedTilesUpdated) + } + } + } + } +} + + +extension TileUpdateMode: CustomStringConvertible, CustomDebugStringConvertible { + + public var name: String { + switch self { + case .dynamic: return "dynamic" + case .full: return "full" + case .actions: return "actions" + } + } + + + public var description: String { + return self.name + } + + public var debugDescription: String { + return self.name + } +} + + + +extension TileUpdateMode { + + func allModes() -> [TileUpdateMode] { + return [.dynamic, .full, .actions] + } + + func next() -> TileUpdateMode { + switch self { + case .dynamic: return .full + case .full: return .actions + case .actions: return .dynamic + } + } +} + + +// MARK: - Extensions + +extension StaggerIndex: Hashable { + + init?(string value: String) { + switch value { + case "even": self = .even + case "odd": self = .odd + default: return nil + } + } + + var hashValue: Int { + return (self == .even) ? 1 : 0 } } @@ -1621,16 +2383,9 @@ extension LayerPosition: CustomStringConvertible { extension SKTilemap { - /// String representing the map name. - public var mapName: String { - if let displayName = self.displayName { - return displayName - } - return self.name ?? "null" - } /// Auto-sizing property for map orientation. - internal var isPortrait: Bool { + public var isPortrait: Bool { return sizeInPoints.height > sizeInPoints.width } @@ -1684,7 +2439,9 @@ extension SKTilemap { - returns: `Bool` column should be staggered. */ internal func doStaggerX(_ x: Int) -> Bool { - return staggerX && Bool((x & 1) ^ staggerEven.hashValue) + let hash: Int = (staggerEven == true) ? 1 : 0 + return staggerX && Bool((x & 1) ^ hash) + } /** @@ -1694,7 +2451,8 @@ extension SKTilemap { - returns: `Bool` row should be staggered. */ internal func doStaggerY(_ y: Int) -> Bool { - return !staggerX && Bool((y & 1) ^ staggerEven.hashValue) + let hash: Int = (staggerEven == true) ? 1 : 0 + return !staggerX && Bool((y & 1) ^ hash) } internal func topLeft(_ x: CGFloat, _ y: CGFloat) -> CGPoint { @@ -1763,6 +2521,19 @@ extension SKTilemap { } } + /// Returns all pathfinding graphs in the map + public var graphs: [GKGridGraph] { + return tileLayers().compactMap { $0.graph } + } + + public var isShowingGraphs: Bool { + let visibleGraphLayers = tileLayers().filter{ tileLayer in + tileLayer.debugDrawOptions.contains(.drawGraph) == true + } + return (visibleGraphLayers.isEmpty == false) + } + + /// String representation of the map. override public var description: String { let sizedesc = "\(sizeInPoints.shortDescription): (\(size.shortDescription) @ \(tileSize.shortDescription))" @@ -1773,7 +2544,9 @@ extension SKTilemap { } /// Debug string representation of the map. - override public var debugDescription: String { return description } + override public var debugDescription: String { + return "Tile Map: \"\(mapName)\", \(tileCount) tiles" + } /** Returns an array of tiles/objects. @@ -1783,7 +2556,7 @@ extension SKTilemap { public func renderableObjects() -> [SKNode] { var result: [SKNode] = [] enumerateChildNodes(withName: "*") { node, stop in - if (node as? SKTile != nil) || (node as? SKTileObject != nil) { + if (node as? SKTiledGeometry != nil) { result.append(node) } } @@ -1791,39 +2564,72 @@ extension SKTilemap { } /** - Dump a summary of the current tilemap's layer statistics. + Return tiles & objects at the given point in the map. - - parameter defaul: `Bool` include the map's default layer. + - parameter point: `CGPoint` position in tilemap. + - returns: `[SKNode]` array of tiles. */ - public func mapStatistics(default: Bool = false) { + public func renderableObjectsAt(point: CGPoint) -> [SKNode] { + let pixelPosition = defaultLayer.screenToPixelCoords(point) + return nodes(at: pixelPosition).filter { node in + (node as? SKTiledGeometry != nil) + } + } + + /** + Returns an array of animated tiles/objects. + + - returns: `[SKNode]` array of child objects. + */ + public func animatedObjects() -> [SKNode] { + let renderable = renderableObjects() + return renderable.filter { + if let tile = $0 as? SKTile { + return tile.action(forKey: tile.animationKey) != nil + } + + if let tileObj = $0 as? SKTileObject { + if let tile = tileObj.tile { + return tile.action(forKey: tile.animationKey) != nil + } + } + return false + } + } +} + + +extension SKTilemap: CustomDebugReflectable { + + /** + Dump a summary of the current tilemap's layer statistics. + */ + public func dumpStatistics() { guard (layerCount > 0) else { print("# Tilemap \"\(mapName)\": 0 Layers") return } // collect graphs for each tile layer - let graphs = tileLayers().flatMap { $0.graph } + let graphs = tileLayers().compactMap { $0.graph } // format the header let graphsString = (graphs.isEmpty == false) ? (graphs.count > 1) ? " : \(graphs.count) Graphs" : " : \(graphs.count) Graph" : "" let headerString = "# Tilemap \"\(mapName)\": \(tileCount) Tiles: \(layerCount) Layers\(graphsString)" - let titleUnderline = String(repeating: "-", count: headerString.characters.count) + let titleUnderline = String(repeating: "-", count: headerString.count) var outputString = "\n\(headerString)\n\(titleUnderline)" - var allLayers = self.layers.filter { $0 as? BackgroundLayer == nil } + //let columnTitles = ["Index", "Type", "Visible", "Name", "Position", "Size", "Offset", "Anchor", "Z-Position", "Opacity", "Update", "Static", "Graph"] + let allLayers = self.layers.filter { $0 as? BackgroundLayer == nil } - if (`default` == true) { - allLayers.insert(self.defaultLayer, at: 0) - } - - // grab the stats from each layer + // get the stats from each layer let allLayerStats = allLayers.map { $0.layerStatsDescription } // prefix for each column var prefixes: [String] = ["", "", "", "", "pos", "size", "offset", "anc", "zpos", "opac", "nav"] // buffer for each column - var buffers: [Int] = [1, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1] + var buffers: [Int] = [1, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1] var columnSizes: [Int] = Array(repeating: 0, count: prefixes.count) // get the max column size for each column @@ -1831,7 +2637,7 @@ extension SKTilemap { for stat in stats { let cindex = Int(stats.index(of: stat)!) - let colCharacters = stat.characters.count + let colCharacters = stat.count let prefix = prefixes[cindex] let buffer = buffers[cindex] @@ -1839,14 +2645,14 @@ extension SKTilemap { if colCharacters > 0 { // get the prefix size + buffer - let bufferSize = (prefix.characters.count > 0) ? prefix.characters.count + buffer : 2 + let layerBufferSize = (prefix.isEmpty == false) ? prefix.count + buffer : 2 // this is the size of the column + prefix - let columnSize = colCharacters + bufferSize + let columnSize = colCharacters + layerBufferSize // if this is more than the max, update the column sizes if columnSize > columnSizes[cindex] { - columnSizes[cindex] = columnSize + columnSizes[cindex] = columnSize } } } @@ -1878,10 +2684,10 @@ extension SKTilemap { // for empty values, add an extra buffer var emptyBuffer = 2 - if (stat.characters.count > 0) { + if (stat.isEmpty == false) { emptyBuffer = 0 prefix = prefixes[sidx] - if (prefix.characters.count > 0) { + if (prefix.isEmpty == false) { divider = ": " // for all columns but the last, add a comma if (isLastColumn == false) { @@ -1893,7 +2699,7 @@ extension SKTilemap { currentColumnValue = "\(prefix)\(stat)\(comma)" } - let fillSize = columnSize + comma.characters.count + buffer + emptyBuffer + let fillSize = columnSize + comma.count + buffer + emptyBuffer // pad each string to the right layerOutputString += currentColumnValue.zfill(length: fillSize, pattern: " ", padLeft: false) } @@ -1905,6 +2711,56 @@ extension SKTilemap { } + +extension SKTilemap.TilemapOrientation { + + /// Hint for aligning tiles within each layer. + public var alignmentHint: CGPoint { + switch self { + case .orthogonal: + return CGPoint(x: 0.5, y: 0.5) + case .isometric: + return CGPoint(x: 0.5, y: 0.5) + case .hexagonal: + return CGPoint(x: 0.5, y: 0.5) + case .staggered: + return CGPoint(x: 0.5, y: 0.5) + } + } +} + +extension SKTilemap.TilemapOrientation: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch self { + case .orthogonal: return "orthogonal" + case .isometric: return "isometric" + case .hexagonal: return "hexagonal" + case .staggered: return "staggered" + } + } + public var debugDescription: String { + return description + } +} + + +extension SKTilemap.RenderStatistics { + + /** + Create a copy of the current render statistics. + + - returns: `SKTilemap.RenderStatistics` render statistics for the current frame. + */ + public func copy() -> SKTilemap.RenderStatistics { + return SKTilemap.RenderStatistics(updateMode: self.updateMode, objectCount: self.objectCount, + visibleCount: self.visibleCount, cpuPercentage: self.cpuPercentage, + effectsEnabled: self.effectsEnabled, updatedThisFrame: self.updatedThisFrame, + objectsVisible: self.objectsVisible, renderTime: 0) + } +} + + + /** Default callback methods. */ @@ -1983,73 +2839,84 @@ extension SKTilemapDelegate { } + /* Clamp position of the map & parents when camera changes happen. */ extension SKTilemap: SKTiledSceneCameraDelegate { - public func cameraBoundsChanged(bounds: CGRect, position: CGPoint, zoom: CGFloat) {} + /** + Called when the nodes in the camera view changes. + + - parameter nodes: `Set` nodes in camera view. + */ + public func containedNodesChanged(_ nodes: Set) { + guard (receiveCameraUpdates == true) else { return } + + DispatchQueue.main.async { + self.nodesInView = nodes.filter { + $0.isHidden == false + } + } + } + + /** + Called when the camera bounds updated. + + - parameter bounds: `CGRect` camera view bounds. + - parameter positon: `CGPoint` camera position. + - parameter zoom: `CGFloat` camera zoom amount. + */ + public func cameraBoundsChanged(bounds: CGRect, position: CGPoint, zoom: CGFloat) { + cameraBounds = bounds + } + + /** + Called when the camera positon changes. + + - parameter newPositon: `CGPoint` updated camera position. + */ public func cameraPositionChanged(newPosition: CGPoint) { - // clamp the position of the map & parent nodes - clampPositionWithNode(node: self, scale: getContentScaleFactor()) + // nodesInView } + /** + Called when the camera zoom changes. + + - parameter newZoom: `CGFloat` camera zoom amount. + */ public func cameraZoomChanged(newZoom: CGFloat) { //let oldZoom = currentZoom currentZoom = newZoom antialiasLines = (newZoom < 1) - - // clamp the position of the map & parent nodes - clampPositionWithNode(node: self, scale: getContentScaleFactor()) } #if os(iOS) || os(tvOS) + + /** + Called when the scene receives a double-tap event (iOS only). + + - parameter location: `CGPoint` touch event location. + */ public func sceneDoubleTapped(location: CGPoint) {} #else - public func sceneDoubleClicked(event: NSEvent) { - let coord = coordinateAtMouseEvent(event: event) - let tiles = tilesAt(coord: coord) - log("\(tiles.count) tiles found at \(coord.shortDescription)", level: .debug) - } - - public func mousePositionChanged(event: NSEvent) { - let coord = coordinateAtMouseEvent(event: event) - let locationInMap = event.location(in: self) - //let locationInLayer = mouseLocation(event: event) + /** + Called when the scene is double-clicked (macOS only). - let nodesUnderCursor = nodes(at: locationInMap) + - parameter event: `NSEvent` mouse click event. + */ + public func sceneDoubleClicked(event: NSEvent) {} - focusObjects = [] - for node in nodesUnderCursor { - if node is SKTileObject { - focusObjects.append(node) - } + /** + Called when the mouse moves in the scene (macOS only). - if let tile = node as? SKTile, (tile.texture != nil) { - if tilesAt(coord: coord).contains(tile) { - focusObjects.append(node) - } - } - } - } + - parameter event: `NSEvent` mouse click event. + */ + public func mousePositionChanged(event: NSEvent) {} #endif } -extension TiledObjectColors { - - static let all: [SKColor] = [coral, crimson, english, saffron, - tangerine, dandelion, azure, turquoise, - lime, pear, grass, indigo, metal, gun] - /// Returns a random color. - static var random: SKColor { - let randIndex = Int(arc4random_uniform(UInt32(TiledObjectColors.all.count))) - return TiledObjectColors.all[randIndex] - } -} - - - // MARK: - Deprecated @available(*, deprecated, renamed: "SKTiledLayerObject") @@ -2131,9 +2998,31 @@ extension SKTilemap { /** Output a summary of the current scenes layer data. + + - parameter reverse: `Bool` reverse layer order. */ - @available(*, deprecated, message: "use `mapStatistics(default:)` instead") - public func debugLayers(reverse: Bool=false) { - mapStatistics() + @available(*, deprecated, message: "use `dumpStatistics` instead") + public func debugLayers(reverse: Bool = false) { + dumpStatistics() + } + + /// Minimum zoom level for the map. + @available(*, deprecated, renamed: "SKTilemap.zoomConstraints.min") + public var minZoom: CGFloat { + get { + return zoomConstraints.min + } set { + zoomConstraints.min = newValue + } + } + + /// Maximum zoom level for the map. + @available(*, deprecated, renamed: "SKTilemap.zoomConstraints.max") + public var maxZoom: CGFloat { + get { + return zoomConstraints.max + } set { + zoomConstraints.max = newValue + } } } diff --git a/Sources/SKTilemapParser.swift b/Sources/SKTilemapParser.swift index da74ab4f..5125226d 100644 --- a/Sources/SKTilemapParser.swift +++ b/Sources/SKTilemapParser.swift @@ -9,21 +9,11 @@ import SpriteKit -// XML Parser error types. -internal enum ParsingError: Error { - case attribute(attr: String) - case attributeValue(attr: String, value: String) - case key(key: String) - case index(idx: Int) - case compression(value: String) - case error -} - - internal enum ParsingMode { case none case tmx case tsx + case tx } @@ -32,6 +22,7 @@ internal enum FileType: String { case tmx case tsx case png + case tx } @@ -47,30 +38,57 @@ internal enum CompressionType: String { ## Overview ## - The `SKTilemapParser` class is a custom [`XMLParserDelegate`](https://developer.apple.com/reference/foundation/xmlparserdelegate) + The `SKTilemapParser` class is a custom [`XMLParserDelegate`](https://developer.apple.com/reference/foundation/xmlparserdelegate) parser for reading Tiled TMX and tileset TSX files. - This class is not meant to be called directly, but rather invoked via `SKTilemap.load` class function. + This class is not meant to be instantiated directly, but rather invoked via `SKTilemap.load` class function. */ -internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { +internal class SKTilemapParser: NSObject, XMLParserDelegate { + + // XML Parser error types. + struct ParsingError: Error { + enum ErrorType { + case attribute(attr: String) + case attributeValue(attr: String, value: String) + case key(key: String) + case index(idx: Int) + case compression(value: String) + case error + case infinite + } + + let line: Int + let column: Int + let kind: ErrorType + } private var fileManager = FileManager.default + /// Root path of the current file (defaults to `Bundle.main.bundleURL`) internal var rootPath: URL = Bundle.main.bundleURL internal var fileNames: [String] = [] internal var currentFilename: String! // the current filename being parsed - internal var parsingMode: ParsingMode = .none // current parsing mode - weak var mapDelegate: SKTilemapDelegate? + internal var parsingMode: ParsingMode = ParsingMode.none // current parsing mode + internal var tileUpdateMode: TileUpdateMode = TileUpdateMode.full // tile update mode + + /// Delegates + weak var mapDelegate: SKTilemapDelegate? // tilemap delegate + weak var tilesetDataSource: SKTilesetDataSource? // tileset delegate + internal var tilemap: SKTilemap! fileprivate var encoding: TilemapEncoding = .xml // xml encoding fileprivate var tilesets: [String: SKTileset] = [:] // stash external tilesets by FILE name (ie: ["kong-50x32.tsx": ]) fileprivate var tilesetImagesAdded: Int = 0 // for reporting the number of images added to a collections tileset - fileprivate var loggingLevel: LoggingLevel = SKTiledLoggingLevel // normally warning + fileprivate var loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel fileprivate var activeElement: String? // current object + // template objects + fileprivate var templateObjects: [SKTileObject] = [] // objects reference by template files + fileprivate var activeTemplateTileset: SKTileset? // active template object tileset + fileprivate var lastElement: AnyObject? // last element created fileprivate var elementPath: [AnyObject] = [] // current element path @@ -85,7 +103,6 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { fileprivate var compression: CompressionType = .uncompressed // compression type fileprivate var timer: Date = Date() // timer - fileprivate var finishedParsing: Bool = false fileprivate var ignoreProperties: Bool = false // ignore custom properties fileprivate var layerIndex: Int = 0 @@ -95,35 +112,45 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // MARK: - Loading + /** Load a TMX file and parse it. - - parameter tmxFile: `String` Tiled file name (does not need TMX extension). - - parameter inDirectory: `String?` search path for assets. - - parameter delegate: `SKTilemapDelegate?` optional tilemap delegate instance. - - parameter withTilesets: `[SKTileset]?` use existing tilesets to create the tile map. - - parameter ignoreProperties: `Bool` ignore custom properties from Tiled. - - parameter loggingLevel: `LoggingLevel` logging verbosity. + - parameter tmxFile: `String` Tiled file name (does not need TMX extension). + - parameter inDirectory: `String?` search path for assets. + - parameter delegate: `SKTilemapDelegate?` optional tilemap delegate instance. + - parameter withTilesets: `[SKTileset]?` use existing tilesets to create the tile map. + - parameter tilesetDataSource: `SKTilesetDataSource?` optional [`SKTilesetDataSource`](Protocols/SKTilesetDataSource.html) instance. + - parameter updateMode: `TileUpdateMode` tile update mode. + - parameter ignoreProperties: `Bool` ignore custom properties from Tiled. + - parameter loggingLevel: `LoggingLevel` logging verbosity. - returns: `SKTilemap?` tiled map node. */ internal func load(tmxFile: String, inDirectory: String? = nil, delegate: SKTilemapDelegate? = nil, + tilesetDataSource: SKTilesetDataSource? = nil, + updateMode: TileUpdateMode = TiledGlobals.default.updateMode, withTilesets: [SKTileset]? = nil, ignoreProperties noparse: Bool = false, - loggingLevel: LoggingLevel = .info, + loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel, renderQueue: DispatchQueue) -> SKTilemap? { - - // current parsing mode + // update the logging level + Logger.default.loggingLevel = loggingLevel + + // current parsing mode & map update mode parsingMode = .tmx - + tileUpdateMode = updateMode + // set the delegate property self.mapDelegate = delegate + self.tilesetDataSource = tilesetDataSource self.timer = Date() self.ignoreProperties = noparse self.loggingLevel = loggingLevel - + + // append extension if not already there. var tmxFilename = tmxFile if !tmxFilename.hasSuffix(".tmx") { @@ -150,12 +177,10 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // add existing tilesets if let withTilesets = withTilesets { for tileset in withTilesets { - guard let filename = tileset.filename else { log("tileset \"\(tileset.name)\" has no filename property.", level: .error) continue } - tilesets[filename] = tileset } } @@ -179,6 +204,8 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { parsingMode = .tmx case "tsx": parsingMode = .tsx + case "tx": + parsingMode = .tx default: parsingMode = .none } @@ -211,21 +238,21 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // parse the file let successs: Bool = parser.parse() + // report errors if (successs == false) { + let parseError = parser.parserError let errorLine = parser.lineNumber let errorCol = parser.columnNumber let errorDescription = parseError!.localizedDescription - log("\(parsingMode) parser: \(errorDescription) at line:\(errorLine), column: \(errorCol)", level: .error) - + log("\(parsingMode) parser: \(errorDescription) at line: \(errorLine), column: \(errorCol)", level: .error) } } } - guard let currentMap = self.tilemap else { return nil } // reset tileset data @@ -239,34 +266,40 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { parsingQueue.sync { self.didBeginRendering(currentMap, queue: renderQueue) } - + + currentMap.dataStorage?.sync() return currentMap } /** - Load tilesets from external files. - - - parameter tsxFiles: `[String]` array of tileset filenames. - - parameter inDirectory: `String?` search path for assets. - - parameter delegate: `SKTilemapDelegate?` optional tilemap delegate instance. - - parameter ignoreProperties: `Bool` ignore custom properties from Tiled. - - parameter loggingLevel: `LoggingLevel` logging verbosity. + Pre-load tilesets from external files. + + - parameter tsxFiles: `[String]` array of tileset filenames. + - parameter inDirectory: `String?` search path for assets. + - parameter delegate: `SKTilemapDelegate?` optional tilemap delegate instance. + - parameter tilesetDataSource: `SKTilesetDataSource?` optional tileset data source delegate. + - parameter ignoreProperties: `Bool` ignore custom properties from Tiled. + - parameter loggingLevel: `LoggingLevel` logging verbosity. - returns: `[SKTileset]` tilesets. */ public func load(tsxFiles: [String], inDirectory: String? = nil, delegate: SKTilemapDelegate? = nil, + tilesetDataSource: SKTilesetDataSource? = nil, ignoreProperties noparse: Bool = false, - loggingLevel: LoggingLevel = .info, + loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel, renderQueue: DispatchQueue) -> [SKTileset] { - + + TiledGlobals.default.loggingLevel = loggingLevel + // current parsing mode is tsx parsingMode = .tsx // set the delegate property self.mapDelegate = delegate + self.tilesetDataSource = tilesetDataSource self.timer = Date() self.loggingLevel = loggingLevel self.ignoreProperties = noparse @@ -330,7 +363,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // check that file exists guard self.fileExists(at: currentURL) else { continue } - log("\(parsingMode) parser: reading \(filetype): \"\(currentFile)\"", level: .info) + log("\(parsingMode) parser: reading \(filetype): \"\(currentFile)\"", level: .debug) // set the root path to the current file if let currentParent = currentURL.parent { @@ -353,7 +386,8 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { let errorCol = parser.columnNumber let errorDescription = parseError!.localizedDescription - Logger.default.cache(LogEvent("\(parsingMode) parser: \(errorDescription) at line:\(errorLine), column: \(errorCol)", level: .error, caller: self.logSymbol)) + Logger.default.log("\(parsingMode) parser: \(errorDescription) at line:\(errorLine), column: \(errorCol)", level: .error, symbol: self.logSymbol) + //Logger.default.cache(LogEvent("\(parsingMode) parser: \(errorDescription) at line:\(errorLine), column: \(errorCol)", level: .error, caller: self.logSymbol)) } } } @@ -379,21 +413,21 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { Post-process to render each layer. - parameter tilemap: `SKTilemap` tile map node. + - parameter queue: `DispatchQueue` external queue. - parameter duration: `TimeInterval` fade-in time for each layer. */ - fileprivate func didBeginRendering(_ tilemap: SKTilemap, queue: DispatchQueue, duration: TimeInterval=0.025) { + fileprivate func didBeginRendering(_ tilemap: SKTilemap, queue: DispatchQueue, duration: TimeInterval = 0.025) { let debugLevel: Bool = (loggingLevel.rawValue < 1) ? true : false - + // loop through the layers for layer in tilemap.getLayers(recursive: true) { - // assign each layer a work item let renderItem = DispatchWorkItem { // render object groups if let objectGroup = layer as? SKObjectGroup { - objectGroup.drawObjects() + objectGroup.draw() } // render image layers @@ -402,6 +436,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // render tile layers if let tileLayer = layer as? SKTileLayer { + // get stashed tile data if let tileData = self.data[tileLayer.uuid] { // add the layer data if (tileLayer.setLayerData(tileData, debug: debugLevel) == false) { @@ -412,7 +447,8 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // report errors if tileLayer.gidErrors.isEmpty == false { let gidErrorString : String = tileLayer.gidErrors.reduce("", { "\($0)" == "" ? "\($1)" : "\($0)" + ", " + "\($1)" }) - Logger.default.cache(LogEvent("layer \"\(tileLayer.layerName)\": the following gids could not be found: \(gidErrorString)", level: .warning, caller: self.logSymbol)) + Logger.default.log("layer \"\(tileLayer.layerName)\": the following gids could not be found: \(gidErrorString)", level: .warning, symbol: self.logSymbol) + //Logger.default.cache(LogEvent("layer \"\(tileLayer.layerName)\": the following gids could not be found: \(gidErrorString)", level: .warning, caller: self.logSymbol)) } } @@ -420,7 +456,6 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { self.parsingQueue.sync { layer.didFinishRendering(duration: duration) } - } // add the layer render work item to the external queue @@ -434,10 +469,10 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { } // release logging messages - Logger.default.release() - + //Logger.default.release() + // sync external queue here - queue.sync { + queue.sync { self.tilemap.didFinishRendering(timeStarted: self.timer) } } @@ -490,36 +525,60 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, - attributes attributeDict: [String: String]) { + attributes attributeDict: [String: String]) { activeElement = elementName + // fail on infinite (for now) + if (elementName == "chunk") { + log("infinite maps are not supported.", level: .fatal) + parser.abortParsing() + } + + if (elementName == "map") { + + // create the tilemap guard let tilemap = SKTilemap(attributes: attributeDict) else { self.log("could not create tilemap.", level: .fatal) parser.abortParsing() return } - + + // initialize cache + tilemap.dataStorage = TileDataStorage(map: tilemap) + tilemap.receiveCameraUpdates = TiledGlobals.default.enableCameraCallbacks + + // initialize notifications + tilemap.setupNotifications() + + // set global logging level tilemap.loggingLevel = self.loggingLevel + tilemap.updateMode = self.tileUpdateMode + self.tilemap = tilemap self.tilemap.ignoreProperties = self.ignoreProperties self.tilemap.delegate = self.mapDelegate + + // set the tilemap url property self.tilemap.url = URL(fileURLWithPath: currentFilename) - self.log("Tiled version: \(SKTiledTiledApplicationVersion)", level: .debug) + if let tiledVersion = tilemap.tiledversion { + self.log("Tiled version: \(tiledVersion)", level: .debug) + } if (self.mapDelegate != nil) { self.tilemap.zDeltaForLayers = self.mapDelegate!.zDeltaForLayers } + // get the filename to use as the map name let currentFile = currentFilename.url.lastPathComponent let currentBasename = currentFile.components(separatedBy: ".").first! - // `SKTilemap.filename` represents the tmx filename (minus .tmx extension) + // `SKTilemap.name` represents the tmx filename (minus .tmx extension) self.tilemap.name = currentBasename self.tilemap.displayName = currentBasename - + // run setup functions on tilemap self.mapDelegate?.didBeginParsing(tilemap) @@ -528,7 +587,8 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { } - // MARK: - Tilesets + // MARK: Tilesets + // external will have a 'source' attribute, otherwise 'image' if (elementName == "tileset") { @@ -538,6 +598,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // reading tmx, external tileset if let source = attributeDict["source"] { + // get the first gid attribute guard let firstgid = attributeDict["firstgid"] else { log("external tileset reference \"\(source)\" with no firstgid.", level: .fatal) @@ -548,20 +609,26 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { let firstGID = Int(firstgid)! // check to see if tileset already exists (either an empty new tileset, or we've passed a pre-loaded tileset). - let externalTileset = URL(fileURLWithPath: source, relativeTo: rootPath) - if let existingTileset = tilesets[externalTileset.path] { - self.tilemap?.addTileset(existingTileset) + // if we're in tilemap parsing mode, add the tileset to the map + if (parsingMode == .tmx) { + self.tilemap?.addTileset(existingTileset) - // set the first gid parameter - existingTileset.firstGID = firstGID + // set the first gid parameter + existingTileset.firstGID = firstGID - lastElement = existingTileset + lastElement = existingTileset - // set this to nil, just in case we're looking for a collections tileset. - currentID = nil + // set this to nil, just in case we're looking for a collections tileset. + currentID = nil + + // we're in a template + } else { + // set the current tileset + activeTemplateTileset = existingTileset + } } else { @@ -584,6 +651,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { let tileset = SKTileset(source: source, firstgid: firstGID, tilemap: self.tilemap) tileset.loggingLevel = self.loggingLevel tileset.ignoreProperties = self.ignoreProperties + tileset.url = tilesetFileURL // add tileset to external file list (full file name) tilesets[externalTileset.path] = tileset @@ -683,13 +751,13 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { let parentElement = elementPath.last! if let group = parentElement as? SKGroupLayer { - let _ = group.addLayer(layer) + group.addLayer(layer) layer.rawIndex = layerIndex layerIndex += 1 } if let tilemap = parentElement as? SKTilemap { - let _ = tilemap.addLayer(layer) + tilemap.addLayer(layer) layer.rawIndex = layerIndex layerIndex += 1 } @@ -722,25 +790,28 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { let parentElement = elementPath.last! if let group = parentElement as? SKGroupLayer { - let _ = group.addLayer(objectsGroup) + group.addLayer(objectsGroup) objectsGroup.rawIndex = layerIndex layerIndex += 1 } if let tilemap = parentElement as? SKTilemap { - let _ = tilemap.addLayer(objectsGroup) + tilemap.addLayer(objectsGroup) objectsGroup.rawIndex = layerIndex layerIndex += 1 } - lastElement = objectsGroup } } // 'imagelayer' indicates an Image layer if (elementName == "imagelayer") { - guard let _ = attributeDict["name"] else { parser.abortParsing(); return } + guard (attributeDict["name"] != nil) else { + parser.abortParsing() + return + } + guard let imageLayer = SKImageLayer(tilemap: self.tilemap!, attributes: attributeDict) else { parser.abortParsing() @@ -749,13 +820,13 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { let parentElement = elementPath.last! if let group = parentElement as? SKGroupLayer { - let _ = group.addLayer(imageLayer) + group.addLayer(imageLayer) imageLayer.rawIndex = layerIndex layerIndex += 1 } if let tilemap = parentElement as? SKTilemap { - let _ = tilemap.addLayer(imageLayer) + tilemap.addLayer(imageLayer) imageLayer.rawIndex = layerIndex layerIndex += 1 } @@ -766,7 +837,11 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // 'group' indicates a Group layer if (elementName == "group") { - guard let _ = attributeDict["name"] else { parser.abortParsing(); return } + guard (attributeDict["name"] != nil) else { + parser.abortParsing() + return + } + guard let groupLayer = SKGroupLayer(tilemap: self.tilemap!, attributes: attributeDict) else { log("error parsing group layer.", level: .fatal) @@ -776,13 +851,13 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { let parentElement = elementPath.last! if let group = parentElement as? SKGroupLayer { - let _ = group.addLayer(groupLayer) + group.addLayer(groupLayer) groupLayer.rawIndex = layerIndex layerIndex += 1 } if let tilemap = parentElement as? SKTilemap { - let _ = tilemap.addLayer(groupLayer) + tilemap.addLayer(groupLayer) groupLayer.rawIndex = layerIndex layerIndex += 1 } @@ -812,7 +887,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // get the absolute path to the image - let sourceImagePath = imageURL.path + var sourceImagePath = imageURL.path // update an image layer if let imageLayer = lastElement as? SKImageLayer { @@ -827,21 +902,36 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // otherwise, the image is part of a collections tileset. if let currentID = currentID { + + if let imagePath = self.tilesetDataSource?.willAddImage(to: tileset, forId: currentID, fileNamed: sourceImagePath) { + let replacementImagePath = URL(fileURLWithPath: imagePath, isDirectory: false, relativeTo: rootPath) + sourceImagePath = replacementImagePath.path + } + + // add an image property to the tileset collection let tileData = tileset.addTilesetTile(currentID, source: sourceImagePath) + tilesetImagesAdded += 1 if (tileData == nil) { log("\(parsingMode) parser: Warning: tile id \(currentID) is invalid.", level: .warning) } } else { + // parse tileset properties + tileset.parseProperties(completion: nil) + + // query data source delegate for source image substitution + if let imagePath = self.tilesetDataSource?.willAddSpriteSheet(to: tileset, fileNamed: sourceImagePath) { + let replacementImagePath = URL(fileURLWithPath: imagePath, isDirectory: false, relativeTo: rootPath) + sourceImagePath = replacementImagePath.path + } // add the tileset spritesheet image tileset.addTextures(fromSpriteSheet: sourceImagePath, replace: false, transparent: attributeDict["trans"]) - tileset.parseProperties(completion: nil) - + // delegate callback parsingQueue.sync { - tileset.renderTileData() + tileset.setupAnimatedTileData() self.mapDelegate?.didAddTileset(tileset) } } @@ -890,24 +980,65 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { // id, x, y required if (elementName == "object") { + // template object inside a template file + if (parsingMode == .tx) { - // adding a group to tileset tile - if let _ = lastElement as? SKTileset {} + // get last template object + if let currentObject = templateObjects.popLast() { - // adding a group to object layer - if let objectGroup = lastElement as? SKObjectGroup { + // set the objects initial attributes + currentObject.setObjectAttributes(attributes: attributeDict) + currentObject.visible = (currentObject.gid != nil) - let Object = (tilemap.delegate != nil) ? tilemap.delegate!.objectForVectorType(named: attributeDict["type"]) : SKTileObject.self + // set the last object + lastElement = currentObject - guard let tileObject = Object.init(attributes: attributeDict) else { - log("\(parsingMode) parser: Error creating object.", level: .fatal) - parser.abortParsing() - return + // get local id with flipped flags + if let localID = currentObject.gid { + guard let templateTileset = activeTemplateTileset else { + log("no active tileset for this template.", level: .error) + return + } + + + let flipped = flippedTileFlags(id: UInt32(localID)) + let globalId = templateTileset.getGlobalID(forLocalID: Int(flipped.gid)) + + currentObject.gid = globalId + currentObject.tileData?.flipHoriz = flipped.hflip + currentObject.tileData?.flipVert = flipped.vflip + currentObject.tileData?.flipDiag = flipped.dflip + } } + } else { + // adding a group to tileset tile + if let _ = lastElement as? SKTileset {} + + // adding a group to object layer + if let objectGroup = lastElement as? SKObjectGroup { - let _ = objectGroup.addObject(tileObject) - currentID = tileObject.id + let Object = (tilemap.delegate != nil) ? tilemap.delegate!.objectForVectorType(named: attributeDict["type"]) : SKTileObject.self + + guard let tileObject = Object.init(attributes: attributeDict) else { + log("\(parsingMode) parser: Error creating object.", level: .fatal) + parser.abortParsing() + return + } + + // add the object to the layer + _ = objectGroup.addObject(tileObject) + currentID = tileObject.id + + // template object + if let templateFile = tileObject.template { + let templateURL = URL(fileURLWithPath: templateFile, relativeTo: rootPath) + //if fileManager.fileExists(atPath: templateURL.path) { fileNames.append(templateURL.path) } + fileNames.append(templateURL.path) + tileObject.isInitialized = false + templateObjects.insert(tileObject, at: 0) + } + } } } @@ -917,7 +1048,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { if let objectsgroup = lastElement as? SKObjectGroup { if (currentID != nil) { if let currentObject = objectsgroup.getObject(withID: currentID!) { - currentObject.objectType = .ellipse + currentObject.shapeType = .ellipse } } } @@ -930,8 +1061,8 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { var coordinates: [[CGFloat]] = [] let points = pointsString.components(separatedBy: " ") for point in points { - let coords = point.components(separatedBy: ",").flatMap { x in Double(x) } - coordinates.append(coords.flatMap { CGFloat($0) }) + let coords = point.components(separatedBy: ",").compactMap { x in Double(x) } + coordinates.append(coords.compactMap { CGFloat($0) }) } if let _ = lastElement as? SKTileset { @@ -955,8 +1086,8 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { var coordinates: [[CGFloat]] = [] let points = pointsString.components(separatedBy: " ") for point in points { - let coords = point.components(separatedBy: ",").flatMap { x in Double(x) } - coordinates.append(coords.flatMap { CGFloat($0) }) + let coords = point.components(separatedBy: ",").compactMap { x in Double(x) } + coordinates.append(coords.compactMap { CGFloat($0) }) } if let _ = lastElement as? SKTileset {} @@ -987,12 +1118,13 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { } - if let currentTileData = tileset.getTileData(globalID: currentID + tileset.firstGID) { + // add the frame id to the frames property let animationFrame = currentTileData.addFrame(withID: Int(id)! + tileset.firstGID, interval: Int(duration)!) - - if let frameData = tileset.getTileData(localID: animationFrame.gid) { + + // set the texture for the frame + if let frameData = tileset.getTileData(localID: animationFrame.id) { if let frameTexture = frameData.texture { animationFrame.texture = frameTexture } @@ -1085,7 +1217,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { for (key, value) in properties { tilemap.properties[key] = value } - + tilemap.parseProperties(completion: nil) } @@ -1266,7 +1398,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { } // if we're closing a group layer, pop it from the element path - let _ = elementPath.popLast() + _ = elementPath.popLast() lastElement = nil } @@ -1282,14 +1414,18 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { } } + // embedded tileset only if (elementName == "tileset") { + if tilesetImagesAdded > 0 { if let tileset = lastElement as? SKTileset { - Logger.default.cache(LogEvent("tileset \"\(tileset.name)\" finished, \(tilesetImagesAdded) images added.", level: .debug, caller: self.logSymbol)) + // Logger.default.cache(LogEvent("tileset \"\(tileset.name)\" finished, \(tilesetImagesAdded) images added.", level: .debug, caller: self.logSymbol)) + Logger.default.log("tileset \"\(tileset.name)\" finished, \(tilesetImagesAdded) images added.", level: .debug, symbol: self.logSymbol) tileset.isRendered = true + // delegate callback parsingQueue.sync { - tileset.renderTileData() + tileset.setupAnimatedTileData() self.mapDelegate?.didAddTileset(tileset) } } @@ -1300,6 +1436,17 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { lastElement = nil } + // template reference + if (elementName == "template") { + + if let currentObject = lastElement as? SKTileObject { + currentObject.isInitialized = true + } + + lastElement = nil + activeTemplateTileset = nil + } + // reset character data characterData = "" @@ -1311,10 +1458,6 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { characterData += string } - internal func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { - //if parseError.code == NSXMLParserError.InternalError {} - } - // MARK: - Decoding /** Scrub CSV data. @@ -1337,7 +1480,7 @@ internal class SKTilemapParser: NSObject, XMLParserDelegate, Loggable { compression: CompressionType = .uncompressed) -> [UInt32]? { guard let decodedData = Data(base64Encoded: data, options: .ignoreUnknownCharacters) else { - print("ERROR: data is not base64 encoded.") + log("data is not base64 encoded.", level: .error) return nil } @@ -1365,6 +1508,17 @@ extension FileType: CustomStringConvertible { case .tmx: return "tile map" case .tsx: return "tileset" case .png: return "image" + case .tx: return "template" + } + } +} + + +extension SKTilemapParser.ParsingError.ErrorType: CustomStringConvertible { + var description: String { + switch self { + case .infinite: return "infinite maps are not supported." + default: return "xml parsing error" } } } diff --git a/Sources/SKTileset.swift b/Sources/SKTileset.swift index ce9cf606..35252a6f 100644 --- a/Sources/SKTileset.swift +++ b/Sources/SKTileset.swift @@ -10,35 +10,123 @@ import SpriteKit +/** + ## Overview ## + + Methods which allow the user to dynamically alter the properties of a tileset as it is being created. + + + ### Instance Methods ### + + Delegate callbacks are called asynchronously as the tileset is being rendered. + + | Method | Description | + |--------------------|----------------------------------------------------------------------| + | willAddSpriteSheet | Provide an image name for the tileset before textures are generated. | + | willAddImage | Provide an alernate image name for an image in a collection. | + + ### Usage ### + + Implementing the `SKTilesetDataSource.willAddSpriteSheet` method allows the user to specify different spritesheet images. Take care + that these images have the same dimensions & layout. + + ```swift + extension MyScene: SKTilesetDataSource { + func willAddSpriteSheet(to tileset: SKTileset, fileNamed: String) -> String { + if (currentSeason == .winter) { + return "winter-tiles-16x16.png" + } + if (currentSeason == .summer) { + return "summer-tiles-16x16.png" + } + return fileNamed + } + } + ``` + */ +public protocol SKTilesetDataSource: class { + /** + Provide an image name for the tileset before textures are generated. + + - parameter to: `SKTileset` tileset instance. + - parameter fileNamed: `String` spritesheet name. + - returns: `String` spritesheet name. + */ + func willAddSpriteSheet(to tileset: SKTileset, fileNamed: String) -> String + + /** + Provide an alernate image name for an image in a collection. + + - parameter to: `SKTileset` tileset instance. + - parameter forId: `Int` tile id. + - parameter fileNamed: `String` image name. + - returns: `String` image name. + */ + func willAddImage(to tileset: SKTileset, forId: Int, fileNamed: String) -> String +} + + /** ## Overview ## - The tileset class manages a set of `SKTilesetData` objects, which store tile data including global id and texture. + The tileset class manages a set of `SKTilesetData` objects, which store tile data including global id, texture and animation. Tile data is accessed via a local id, and tiles can be instantiated with the resulting `SKTilesetData` instance: ```swift - let data = tileset.getTileData(localID: 56) - let tile = SKTile(data: data) + if let data = tileset.getTileData(localID: 56) { + let tile = SKTile(data: data) + } ``` + + ### Properties ### + + | Property | Description | + |-----------------------|-------------------------------------------------| + | name | Tileset name. | + | tilemap | Reference to parent tilemap. | + | tileSize | Tile size (in pixels). | + | columns | Number of columns. | + | tilecount | Tile count. | + | firstGID | First tile global id. | + | lastGID | Last tile global id. | + | tileData | Set of tile data structures. | + + + ### Instance Methods ### + + | Method | Description | + |-----------------------|-------------------------------------------------| + | addTextures() | Generate textures from a spritesheet image. | + | addTilesetTile() | Add & return new tile data object. | + + */ -public class SKTileset: SKTiledObject { +public class SKTileset: NSObject, SKTiledObject { + /// Tileset url (external tileset). public var url: URL! + /// Tiled tsx filename (external tileset). public var filename: String! = nil + /// Unique object id. public var uuid: String = UUID().uuidString - public var name: String // tileset name (without file extension) + + /// Tileset name + public var name: String + /// Object type. public var type: String! + /// Reference to parent tilemap. public var tilemap: SKTilemap! + /// Tile size (in pixels). public var tileSize: CGSize - internal var loggingLevel: LoggingLevel = .warning + internal var loggingLevel: LoggingLevel = LoggingLevel.warning // logging level public var columns: Int = 0 // number of columns public var tilecount: Int = 0 // tile count @@ -57,18 +145,19 @@ public class SKTileset: SKTiledObject { /// Tile data set. private var tileData: Set = [] + /// Tile data count. public var dataCount: Int { return tileData.count } - /// Image collection tileset. + /// Indicates the tileset is a collection of images. public var isImageCollection: Bool = false - /// Tileset is stored in an external file. + /// The tileset is stored in an external file. public var isExternalTileset: Bool { return filename != nil } /// Source image transparency color. public var transparentColor: SKColor? public var isRendered: Bool = false - /// Returns the last GID in the tileset. + /// Returns the last global tile id in the tileset. public var lastGID: Int { return tileData.map { $0.id }.max() ?? firstGID } /// Returns the difference in tile size vs. map tile size. @@ -77,7 +166,7 @@ public class SKTileset: SKTiledObject { return CGPoint(x: tileSize.width - tilemap.tileSize.width, y: tileSize.height - tilemap.tileSize.height) } - /// Render scaling property. + /// Scaling factor for text objects, etc. public var renderQuality: CGFloat = 8 { didSet { guard renderQuality != oldValue else { return } @@ -95,9 +184,14 @@ public class SKTileset: SKTiledObject { - parameter offset: `CGPoint` tileset offset value. - returns: `SKTileset` tileset object. */ - public init(name: String, tileSize size: CGSize, firstgid: Int=1, columns: Int=0, offset: CGPoint=CGPoint.zero) { + public init(name: String, tileSize size: CGSize, + firstgid: Int = 1, columns: Int = 0, + offset: CGPoint = CGPoint.zero) { + self.name = name self.tileSize = size + + super.init() self.firstGID = firstgid self.columns = columns self.tileOffset = offset @@ -111,7 +205,10 @@ public class SKTileset: SKTiledObject { - parameter tilemap: `SKTilemap` parent tile map node. - returns: `SKTileset` tile set. */ - public init(source: String, firstgid: Int, tilemap: SKTilemap, offset: CGPoint=CGPoint.zero) { + public init(source: String, firstgid: Int, + tilemap: SKTilemap, offset: CGPoint = CGPoint.zero) { + + let filepath = source.components(separatedBy: "/").last! self.filename = filepath @@ -122,6 +219,8 @@ public class SKTileset: SKTiledObject { // setting these here, even though it may different later self.name = filepath.components(separatedBy: ".")[0] self.tileSize = tilemap.tileSize + + super.init() self.ignoreProperties = tilemap.ignoreProperties } @@ -129,9 +228,11 @@ public class SKTileset: SKTiledObject { Initialize with attributes directly from TMX file. - parameter attributes: `[String: String]` attributes dictionary. - - parameter offset: `CGPoint` offset in x/y. + - parameter offset: `CGPoint` pixel offset in x/y. */ - public init?(attributes: [String: String], offset: CGPoint=CGPoint.zero) { + public init?(attributes: [String: String], + offset: CGPoint = CGPoint.zero) { + // name, width and height are required guard let setName = attributes["name"], let width = attributes["tilewidth"], @@ -164,24 +265,28 @@ public class SKTileset: SKTiledObject { self.name = setName self.tileSize = CGSize(width: Int(width)!, height: Int(height)!) + + super.init() self.tileOffset = offset } /** Initialize with a TSX file name. - - parameter fileNamed: `String` tileset file name. + - parameter fileNamed: `String` tileset file name. + - parameter delegate: `SKTilemapDelegate?` optional tilemap delegate. */ public init(fileNamed: String) { self.name = "" self.tileSize = CGSize.zero + super.init() } // MARK: - Loading /** Loads Tiled tsx files and returns an array of `SKTileset` objects. - - parameter tsxFiles: `[String]` Tiled tileset filenames. + - parameter tsxFiles: `[String]` Tiled tileset filenames. - parameter delegate: `SKTilemapDelegate?` optional [`SKTilemapDelegate`](Protocols/SKTilemapDelegate.html) instance. - parameter ignoreProperties: `Bool` ignore custom properties from Tiled. - returns: `[SKTileset]` tileset objects. @@ -208,19 +313,31 @@ public class SKTileset: SKTiledObject { - parameter replace: `Bool` replace the current texture. - parameter transparent: `String?` optional transparent color hex value. */ - public func addTextures(fromSpriteSheet source: String, replace: Bool=false, transparent: String?=nil) { + public func addTextures(fromSpriteSheet source: String, replace: Bool = false, transparent: String? = nil) { let timer = Date() - self.source = source - // parse the transparent color (NYI) + self.source = source + + // parse the transparent color if let transparent = transparent { transparentColor = SKColor(hexString: transparent) } + if (replace == true) { + let url = URL(fileURLWithPath: source) + self.log("replacing spritesheet with: \"\(url.lastPathComponent)\"", level: .info) + } + let inputURL = URL(fileURLWithPath: self.source!) + self.log("spritesheet: \"\(inputURL.relativePath.filename)\"", level: .debug) + // read image from file - let imageDataProvider = CGDataProvider(url: inputURL as CFURL)! - // creare a data provider + guard let imageDataProvider = CGDataProvider(url: inputURL as CFURL) else { + self.log("Error reading image: \"\(source)\"", level: .fatal) + fatalError("Error reading image: \"\(source)\"") + } + + // creare an image data provider let image = CGImage(pngDataProviderSource: imageDataProvider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)! // create the texture @@ -275,7 +392,7 @@ public class SKTileset: SKTiledObject { if (replace == false) { _ = self.addTilesetTile(tileID, texture: tileTexture) } else { - self.setDataTexture(tileID, texture: tileTexture) + _ = self.setDataTexture(tileID, texture: tileTexture) } x += Int(self.tileSize.width) + self.spacing @@ -293,7 +410,18 @@ public class SKTileset: SKTiledObject { // time results if (replace == false) { let timeStamp = String(format: "%.\(String(3))f", tilesetBuildTime) - Logger.default.cache(LogEvent("tileset \"\(name)\" built in: \(timeStamp)s (\(tilesAdded) tiles)", level: .debug, caller: self.logSymbol)) + Logger.default.log("tileset \"\(name)\" built in: \(timeStamp)s (\(tilesAdded) tiles)", level: .debug, symbol: self.logSymbol) + } else { + + let animatedData: [SKTilesetData] = self.tileData.filter { $0.isAnimated == true } + + // update animated data + NotificationCenter.default.post( + name: Notification.Name.Tileset.SpriteSheetUpdated, + object: self, + userInfo: ["animatedTiles": animatedData] + ) + } } @@ -314,6 +442,9 @@ public class SKTileset: SKTiledObject { texture.filteringMode = .nearest let data = SKTilesetData(id: tileID, texture: texture, tileSet: self) + + // add to tile cache + data.ignoreProperties = ignoreProperties self.tileData.insert(data) data.parseProperties(completion: nil) @@ -347,6 +478,10 @@ public class SKTileset: SKTiledObject { sourceTexture.filteringMode = .nearest let data = SKTilesetData(id: tileID, texture: sourceTexture, tileSet: self) + + // add to tile cache + + data.ignoreProperties = ignoreProperties // add the image name to the source attribute data.source = source @@ -360,16 +495,52 @@ public class SKTileset: SKTiledObject { - parameter tileID: `Int` tile ID. - parameter texture: `SKTexture` texture for tile at the given id. + - returns: `SKTexture?` previous tile data texture. */ - public func setDataTexture(_ id: Int, texture: SKTexture) { + @discardableResult + public func setDataTexture(_ id: Int, texture: SKTexture) -> SKTexture? { guard let data = getTileData(localID: id) else { - if loggingLevel.rawValue <= 1 { + if (loggingLevel.rawValue <= 1) { log("tile data not found for id: \(id)", level: .error) } - return + return nil } + + let current = data.texture.copy() as? SKTexture + let userInfo: [String: Any] = (current != nil) ? ["old": current!] : [:] + texture.filteringMode = .nearest data.texture = texture + + // update observers + NotificationCenter.default.post( + name: Notification.Name.TileData.TextureChanged, + object: data, + userInfo: userInfo + ) + + return current + } + + /** + Set(replace) the texture for a given tile id. + + - parameter tileID: `Int` tile ID. + - parameter imageNamed: `String` source texture name. + - returns: `SKTexture?` old tile data texture. + */ + @discardableResult + public func setDataTexture(_ id: Int, imageNamed: String) -> SKTexture? { + let inputURL = URL(fileURLWithPath: imageNamed) + // read image from file + guard let imageDataProvider = CGDataProvider(url: inputURL as CFURL) else { + return nil + } + + // creare an image data provider + let image = CGImage(pngDataProviderSource: imageDataProvider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)! + let texture = SKTexture(cgImage: image) + return setDataTexture(id, texture: texture) } /** @@ -394,7 +565,8 @@ public class SKTileset: SKTiledObject { - returns: `SKTilesetData?` tile data object. */ public func getTileData(globalID gid: Int) -> SKTilesetData? { - var id = getTileRealID(id: gid) + // parse out flipped flags + var id = rawTileID(id: gid) id = getLocalID(forGlobalID: id) if let index = tileData.index(where: { $0.id == id }) { return tileData[index] @@ -409,7 +581,7 @@ public class SKTileset: SKTiledObject { - returns: `SKTilesetData?` tile data object. */ public func getTileData(localID id: Int) -> SKTilesetData? { - let localID = getTileRealID(id: id) + let localID = rawTileID(id: id) if let index = tileData.index(where: { $0.id == localID }) { return tileData[index] } @@ -425,6 +597,19 @@ public class SKTileset: SKTiledObject { public func getTileData(withProperty property: String) -> [SKTilesetData] { return tileData.filter { $0.properties[property] != nil } } + + /** + Returns tile data with the given name & animated state. + + - parameter named: `String` data name. + - parameter isAnimated: `Bool` filter data that is animated. + - returns: `[SKTilesetData]` array of tile data. + */ + public func getTileData(named name: String, isAnimated: Bool = false) -> [SKTilesetData] { + return tileData.filter { + ($0.name == name) && ($0.isAnimated == isAnimated) + } + } /** Returns tile data with the given type. @@ -466,8 +651,8 @@ public class SKTileset: SKTiledObject { /** Convert a global ID to the tileset's local ID. - - parameter id: `Int` global id. - - returns: `Int` local tile ID. + - parameter gid: `Int` global id. + - returns: `Int` local tile ID. */ public func getLocalID(forGlobalID gid: Int) -> Int { // firstGID is greater than 0 only when added to a tilemap @@ -476,13 +661,24 @@ public class SKTileset: SKTiledObject { return (id < 0) ? gid : id } + /** + Convert a global ID to the tileset's local ID. + + - parameter id: `Int` local id. + - returns: `Int` global tile ID. + */ + public func getGlobalID(forLocalID id: Int) -> Int { + let gid = (firstGID > 0) ? (firstGID + id) - 1 : id + return gid + } + /** Check for tile ID flip flags. - parameter id: `Int` tile ID. - returns: `Int` translated ID. */ - internal func getTileRealID(id: Int) -> Int { + internal func rawTileID(id: Int) -> Int { let uid: UInt32 = UInt32(id) // masks for tile flipping let flippedDiagonalFlag: UInt32 = 0x20000000 @@ -496,63 +692,77 @@ public class SKTileset: SKTiledObject { let gid = uid & flippedMask return Int(gid) } + // MARK: - Rendering /** - Check that all animated frames have textures. + Refresh textures for animated tile data. */ - internal func renderTileData() { + internal func setupAnimatedTileData() { let animatedData = getAnimatedTileData() var framesAdded = 0 var dataFixed = 0 if (animatedData.isEmpty == false) { animatedData.forEach { data in - for frame in data.frames { - if frame.texture == nil { - if let frameData = getTileData(localID: frame.gid) { - if frameData.texture != nil { - frame.texture = frameData.texture - framesAdded += 1 - } + for frame in data.frames where frame.texture == nil{ + if let frameData = getTileData(localID: frame.id) { + if frameData.texture != nil { + frame.texture = frameData.texture + framesAdded += 1 } - } } dataFixed += 1 } } - if framesAdded > 0 { + if (framesAdded > 0) { log("updated \(dataFixed) tile data animations for tileset: \"\(name)\"", level: .debug) } } +} - // MARK: - Debugging + +/// Default methods +extension SKTilesetDataSource { /** - Print out tileset data values. + Called when a tileset is about to render a spritesheet. + + - parameter tileset: `SKTileset` tileset instance. + - parameter fileNamed: `String` tileset instance. + - returns: `String` spritesheet name. */ - internal func debugTileset() { - log("# Tileset: \"\(name)\":", level: .debug) - for data in tileData.sorted(by: {$0.id < $1.id}) { - log("data: \(data)", level: .debug) - } + public func willAddSpriteSheet(to tileset: SKTileset, fileNamed: String) -> String { + return fileNamed } -} + /** + Called when a tileset is about to add an image from a collection. -public func == (lhs: SKTileset, rhs: SKTileset) -> Bool { - return (lhs.hashValue == rhs.hashValue) + - parameter to: `SKTileset` tileset instance. + - parameter forId: `Int` tile id. + - parameter fileNamed: `String` tileset instance. + - returns: `String` spritesheet name. + */ + public func willAddImage(to tileset: SKTileset, forId: Int, fileNamed: String) -> String { + return fileNamed + } } -extension SKTileset: Hashable { - public var hashValue: Int { return name.hashValue } + +public func == (lhs: SKTileset, rhs: SKTileset) -> Bool { + return (lhs.hash == rhs.hash) } -extension SKTileset: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { +extension SKTileset { + + override public var hash: Int { return name.hashValue } + + /// String representation of the tileset object. + override public var description: String { var desc = "Tileset: \"\(name)\" @ \(tileSize), firstgid: \(firstGID), \(dataCount) tiles" if tileOffset.x != 0 || tileOffset.y != 0 { desc += ", offset: \(tileOffset.x)x\(tileOffset.y)" @@ -560,5 +770,5 @@ extension SKTileset: CustomStringConvertible, CustomDebugStringConvertible { return desc } - public var debugDescription: String { return description } + override public var debugDescription: String { return description } } diff --git a/Sources/SKTilesetData.swift b/Sources/SKTilesetData.swift index 5a8700a5..3125dc60 100644 --- a/Sources/SKTilesetData.swift +++ b/Sources/SKTilesetData.swift @@ -9,19 +9,28 @@ import SpriteKit /** - A structure representing frame of animation. Time is stored in milliseconds. + + ## Overview ## + + A structure representing a single frame of animation. Time is stored in milliseconds. + + ### Properties ### + + | Property | Description | + |----------|-------------------------| + | id | unique tile (local) id. | + | duration | frame duration. | + | texture | optional tile texture. | - - parameter gid: `Int` unique tile id. - - parameter duration: `TimeInterval` frame duration. - - parameter texture: `SKTexture?` optional tile texture. */ -public class AnimationFrame { - public var gid: Int = 0 +public class TileAnimationFrame: NSObject { + public var id: Int = 0 public var duration: Int = 0 public var texture: SKTexture? - public init(gid: Int, duration: Int, texture: SKTexture? = nil) { - self.gid = gid + public init(id: Int, duration: Int, texture: SKTexture? = nil) { + super.init() + self.id = id self.duration = duration self.texture = texture } @@ -53,41 +62,110 @@ internal class SKTileCollisionShape: SKTiledObject { The `SKTilesetData` object stores data for a single tileset tile, referencing the tile texture, animation frames (for animated tiles) as well as tile orientation. Also includes navigation properties for tile accessability, and graph node weight. + + ### Properties ### + + | Property | Description | + |------------|-----------------| + | id | Tile id (local) | + | type | Tiled type | + | texture | Tile texture | + | tileOffset | Tile offset | + */ public class SKTilesetData: SKTiledObject { - weak public var tileset: SKTileset! // reference to parent tileset - public var uuid: String = UUID().uuidString // unique id - /// Object type. + weak public var tileset: SKTileset! // reference to parent tileset + + /// Unique id. + public var uuid: String = UUID().uuidString + /// Tile id (local). + public var id: Int = 0 + /// Tiled type. public var type: String! - public var id: Int = 0 // tile id (local) + /// Tile data name. + public var name: String? { + return properties["name"] + } + /// Tile texture. public var texture: SKTexture! /// Source image name (collections tileset) public var source: String! = nil - public var probability: CGFloat = 1.0 // used in Tiled application, might not be useful here. + public var probability: CGFloat = 1.0 // carried over from Tiled application. public var properties: [String: String] = [:] - public var ignoreProperties: Bool = false // ignore custom properties - public var tileOffset: CGPoint = .zero // tile offset + public var ignoreProperties: Bool = false // ignore custom properties + + /// Tile offset. + public var tileOffset: CGPoint = CGPoint.zero /// Render scaling property. public var renderQuality: CGFloat = 8 /// Animated frames. - internal var blockAnimation: Bool = false // block tile animation - internal var _frames: [AnimationFrame] = [] - internal var frames: [AnimationFrame] { + internal var currentTime: TimeInterval = 0 + internal var frameIndex: UInt8 = 0 + internal var blockAnimation: Bool = false // supress tile animation + internal var _frames: [TileAnimationFrame] = [] + public var frames: [TileAnimationFrame] { return (blockAnimation == false) ? _frames : [] } /// Indicates the tile is animated. public var isAnimated: Bool { return frames.isEmpty == false } + + /// Signifies that the tile data has changed. + internal var dataChanged: Bool = false { + didSet { + guard (oldValue != dataChanged) else { return } + // if something has changed, we need to regenerate the skaction + if (dataChanged == true) { + _animationAction = nil + } + } + } + + /// Private animation action. + private var _animationAction: SKAction? + + /// Returns an aniamtion action for the tile data. + public var animationAction: SKAction? { + if (_animationAction != nil) { + return _animationAction + } - /// Max animation duration (in milliseconds). + guard (isAnimated == true), + let tileset = tileset else { return nil } + + var framesData: [(texture: SKTexture, duration: TimeInterval)] = [] + + for frame in frames { + guard let frameTexture = tileset.getTileData(localID: frame.id)?.texture else { + Logger.default.log("cannot access texture data for id: \(frame.id)", level: .error, symbol: "SKTilesetData") + return nil + } + + frameTexture.filteringMode = .nearest + framesData.append((texture: frameTexture, duration: TimeInterval(frame.duration) / 1000)) + } + // return the resulting action + let newAction = SKAction.tileAnimation(framesData) + + NotificationCenter.default.post( + name: Notification.Name.TileData.ActionAdded, + object: self, + userInfo: ["action": newAction] + ) + + _animationAction = newAction + return newAction + } + + /// Tile animation duration (in milliseconds). internal var animationTime: Int { guard (isAnimated == true) else { return 0 } let durations: [Int] = frames.map { $0.duration } - return durations.reduce(0, { $0 + $1 }) + return durations.reduce(0, { $0 + $1 }) } // MARK: Tile Orientation @@ -103,7 +181,7 @@ public class SKTilesetData: SKTiledObject { /// Tile is walkable. public var walkable: Bool = false - /// Tile is an obstacle. + /// Tile represents an obstacle. public var obstacle: Bool = false /// Pathfinding weight. public var weight: CGFloat = 1 @@ -165,6 +243,7 @@ public class SKTilesetData: SKTiledObject { } // MARK: - Animation + /** Add tile animation to the data. @@ -172,15 +251,22 @@ public class SKTilesetData: SKTiledObject { - parameter withID: `Int` id for frame. - parameter interval: `Int` frame interval (in milliseconds). - parameter tileTexture: `SKTexture?` frame texture. - - returns: `AnimationFrame` animation frame container. + - returns: `TileAnimationFrame` animation frame container. */ - public func addFrame(withID: Int, interval: Int, tileTexture: SKTexture? = nil) -> AnimationFrame { + public func addFrame(withID: Int, interval: Int, tileTexture: SKTexture? = nil) -> TileAnimationFrame { var id = withID - // if the tileset firstGID is already set, subtract it to get the internal id + // if the tileset firstGID is already set, subtract it to get the internal (local) id if let tileset = tileset, tileset.firstGID > 0 { id = withID - tileset.firstGID } - let frame = AnimationFrame(gid: id, duration: interval, texture: tileTexture) + let frame = TileAnimationFrame(id: id, duration: interval, texture: tileTexture) + + NotificationCenter.default.post( + name: Notification.Name.TileData.FrameAdded, + object: self, + userInfo: nil + ) + _frames.append(frame) return frame } @@ -189,15 +275,41 @@ public class SKTilesetData: SKTiledObject { Returns an animation frame at the given index. - parameter index: `Int` frame index. - - returns: `AnimationFrame?` animation frame container. + - returns: `TileAnimationFrame?` animation frame container. */ - public func frameAt(index: Int) -> AnimationFrame? { + public func frameAt(index: Int) -> TileAnimationFrame? { guard _frames.indices.contains(index) else { return nil } return frames[index] } + /** + Force the animated frames to update textuers. + */ + public func forceAnimatedFramesUpdate() { + removeAnimation() + _frames.forEach { frame in + if let data = tileset.getTileData(localID: frame.id) { + frame.texture = data.texture + } + } + runAnimation() + } + + /** + Set the texture for the tile data. + + - parameter texture: `SKTexture?` new texture. + - returns: `SKTexture?` old texture (if it exists). + */ + public func setTexture(_ newTexture: SKTexture?) -> SKTexture? { + let previousTexture = self.texture + newTexture?.filteringMode = .nearest + self.texture = newTexture + return previousTexture + } + /** Set the texture for an animated frame at the given index. @@ -234,9 +346,9 @@ public class SKTilesetData: SKTiledObject { Remove a tile animation frame at a given index. - parameter at: `Int` frame index. - - returns: `AnimationFrame?` animation frame (if it exists). + - returns: `TileAnimationFrame?` animation frame (if it exists). */ - public func removeFrame(at index: Int) -> AnimationFrame? { + public func removeFrame(at index: Int) -> TileAnimationFrame? { return _frames.remove(at: index) } @@ -249,7 +361,7 @@ public class SKTilesetData: SKTiledObject { /** Remove tile animation. Animation is not actually destroyed, but rather blocked. - + - parameter restore: `Bool` restore the initial texture. */ public func removeAnimation(restore: Bool = false) { @@ -294,13 +406,15 @@ public func == (lhs: SKTilesetData, rhs: SKTilesetData) -> Bool { extension SKTilesetData: Hashable { - public var hashValue: Int { return id.hashValue } + public var hashValue: Int { + return id.hashValue << 32 ^ globalID.hashValue + } } -extension AnimationFrame: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { return "Frame: \(gid): \(duration)" } - public var debugDescription: String { return "<\(description)>" } +extension TileAnimationFrame { + override public var description: String { return "Frame: \(id): \(duration)" } + override public var debugDescription: String { return "<\(description)>" } } @@ -310,9 +424,16 @@ extension SKTilesetData: CustomStringConvertible, CustomDebugStringConvertible { public var description: String { guard let tileset = tileset else { return "Tile ID: \(id) (no tileset)" } let typeString = (type != nil) ? ", type: \"\(type!)\"" : "" + + var sourceString = "" + if (source != nil) { + let sourceURL = URL(fileURLWithPath: source!, relativeTo: Bundle.main.bundleURL) + sourceString = sourceURL.relativeString + } let framesString = (isAnimated == true) ? ", \(frames.count) frames" : "" let idValue = localID // globalID - let dataString = properties.isEmpty == false ? "Tile ID: \(idValue)\(typeString) @ \(tileset.tileSize.shortDescription)\(framesString), " : "Tile ID: \(idValue)\(typeString) @ \(tileset.tileSize.shortDescription)\(framesString)" + let dataString = (properties.isEmpty == false) ? "Tile ID: \(idValue)\(typeString)\(sourceString) @ \(tileset.tileSize.shortDescription)\(framesString), " + : "Tile ID: \(idValue)\(typeString) @ \(tileset.tileSize.shortDescription)\(framesString)" return "\(dataString)\(propertiesString)" } diff --git a/Tests/Assets/characters-8x8.png b/Tests/Assets/characters-8x8.png new file mode 100644 index 00000000..05f41472 Binary files /dev/null and b/Tests/Assets/characters-8x8.png differ diff --git a/Tests/Assets/characters-8x8.tsx b/Tests/Assets/characters-8x8.tsx new file mode 100644 index 00000000..38c7f464 --- /dev/null +++ b/Tests/Assets/characters-8x8.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/Tests/Assets/environment-8x8.png b/Tests/Assets/environment-8x8.png new file mode 100644 index 00000000..ea5c076e Binary files /dev/null and b/Tests/Assets/environment-8x8.png differ diff --git a/Tests/Assets/environment-8x8.tsx b/Tests/Assets/environment-8x8.tsx new file mode 100644 index 00000000..1202d3e8 --- /dev/null +++ b/Tests/Assets/environment-8x8.tsx @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Assets/items-8x8.png b/Tests/Assets/items-8x8.png new file mode 100644 index 00000000..4ce657d7 Binary files /dev/null and b/Tests/Assets/items-8x8.png differ diff --git a/Tests/Assets/items-8x8.tsx b/Tests/Assets/items-8x8.tsx new file mode 100644 index 00000000..34f5a440 --- /dev/null +++ b/Tests/Assets/items-8x8.tsx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Tests/Assets/items-alt-8x8.png b/Tests/Assets/items-alt-8x8.png new file mode 100644 index 00000000..4ce657d7 Binary files /dev/null and b/Tests/Assets/items-alt-8x8.png differ diff --git a/Tests/Assets/monsters-16x16.png b/Tests/Assets/monsters-16x16.png new file mode 100644 index 00000000..70286cab Binary files /dev/null and b/Tests/Assets/monsters-16x16.png differ diff --git a/Tests/Assets/monsters-16x16.tsx b/Tests/Assets/monsters-16x16.tsx new file mode 100644 index 00000000..b738d49f --- /dev/null +++ b/Tests/Assets/monsters-16x16.tsx @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Assets/portraits-8x8.png b/Tests/Assets/portraits-8x8.png new file mode 100644 index 00000000..f5411918 Binary files /dev/null and b/Tests/Assets/portraits-8x8.png differ diff --git a/Tests/Assets/portraits-8x8.tsx b/Tests/Assets/portraits-8x8.tsx new file mode 100644 index 00000000..1d8bd2c0 --- /dev/null +++ b/Tests/Assets/portraits-8x8.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/Tests/Assets/test-tilemap.tmx b/Tests/Assets/test-tilemap.tmx new file mode 100644 index 00000000..de072e7d --- /dev/null +++ b/Tests/Assets/test-tilemap.tmx @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + eJyFlEEKxDAMA/MMB7rn/P+H20IDZhilh0BbN7Isy55jjHWfus98nyfed7xarN/pp8c6VgHzef4dMIjV8ciRd/v3hXustSQnD2sznuRieYm/gJv49Ngamd9zrqAXMahf4T/WYT0hDn2R9CInvlufyZXa9zv0Gj1CPtR2ynfDNb1qeF3JF1aP5bOZtLwJN+nLeTCf2Ax+7QPzH7XdfjXPUDPzDj2bdpbtL84hOXAOLafNY9pNNhPsh3ncdGXdKSf7x15+zRQ50G/mEfO5/X/iytkzH6TdxjypZtM27UTz0KmHtte5h3bsD1PsNrk= + + + + + eJylVVsOxCAI9FPaA3gDz9mjryZOQ8lM1S4JUQFHyqslpWSNc+PSuI71GKuN9Ry6GuwsnLOzL05X3TtNd3U5oyzkJuSKmH19wT+FvG6+xXDyB3xlH3GZDx5D4b/Jc+AUzj53h/BL4as8ruRX1QwIdaZ0jFQ97Nh07F7TOPeYWHrU+13/5vbj3r0Hoacs6NFf8Ke/uVs/Mc4s7itxnsUk1lC0Z/75b2H4mCcrpPxj8Zr1EfLEKPYCcszmHzi7u+gVzFz44Gcqo3/m5MzmfMFf7esvdRX7YOcuiPk9m+HIgYU9/EH+kU+X48v3uf9XxnlZ0rOPgfsDlksPZg== + + + + + eJxjYBh5QG+gHTAKhgSwZmBowCdvT0B+MAD3IeDGUTAKqA0Mh0i6BwDYjgMh + + + + + eJxjYBh8wJ+BoYEa5nhTw5BBCvzpaBcvHe0arEB5oB1ABUCKH4Jp5opRgA9QO6/hMo8PRxlLz3KFVDAc8iA6AABkzwMq + + + + + eJxjYBi8gGOgHUAnIAul1ehsLzcDJIw56WzvQIHsgXYAEgCGfQM99Y0C/CB3oB0wCugOcgZBXgIAF24DQg== + + + + + eJxjYBgFo2AUjIJRMAoGB4gaaAfQEQAAWngAWw== + + + + + + + + diff --git a/Tests/Info-iOS.plist b/Tests/Info-iOS.plist new file mode 100644 index 00000000..0757197d --- /dev/null +++ b/Tests/Info-iOS.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.2 + CFBundleVersion + rc1 + + diff --git a/Tests/Info-macOS.plist b/Tests/Info-macOS.plist new file mode 100644 index 00000000..0757197d --- /dev/null +++ b/Tests/Info-macOS.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.2 + CFBundleVersion + rc1 + + diff --git a/Tests/Info-tvOS.plist b/Tests/Info-tvOS.plist new file mode 100644 index 00000000..0757197d --- /dev/null +++ b/Tests/Info-tvOS.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.2 + CFBundleVersion + rc1 + + diff --git a/Tests/ParserTests.swift b/Tests/ParserTests.swift new file mode 100644 index 00000000..aff0e287 --- /dev/null +++ b/Tests/ParserTests.swift @@ -0,0 +1,107 @@ +// +// ParserTests.swift +// SKTiledTests +// +// Created by Michael Fessenden on 10/27/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import XCTest +import SpriteKit +@testable import SKTiled + + +class TestMapDelegate: SKTilemapDelegate { + var zDeltaForLayers: CGFloat = 129 + var mapRenderedSuccessfully: Bool = false + init() {} + func didRenderMap(_ tilemap: SKTilemap) { + mapRenderedSuccessfully = true + } +} + +class TestTilesetDelegate: SKTilesetDataSource { + init() {} + func willAddSpriteSheet(to tileset: SKTileset, fileNamed: String) -> String { + + let fileURL = fileNamed.url + var parentURL = fileNamed.parentURL + let filename = FileManager.default.displayName(atPath: fileURL.path) + var result = fileNamed + + if (filename == "items-8x8.png") { + parentURL.appendPathComponent("items-alt-8x8.png") + result = parentURL.path + } + return result + } +} + +/// Test the tiled parser +class ParserTests: XCTestCase { + + var tilemap: SKTilemap? + weak var tilemapDelegate: TestMapDelegate? + weak var tilesetDelegate: TestTilesetDelegate? + var testBundle: Bundle! + let tilemapName = "test-tilemap" + + override func setUp() { + super.setUp() + + if (testBundle == nil) { + testBundle = Bundle(for: type(of: self)) + } + + if (tilemap == nil) { + + tilemapDelegate = TestMapDelegate() + tilesetDelegate = TestTilesetDelegate() + + let mapurl = testBundle!.url(forResource: tilemapName, withExtension: "tmx")! + tilemap = SKTilemap.load(tmxFile: mapurl.path, delegate: tilemapDelegate!, + tilesetDataSource: tilesetDelegate!, loggingLevel: .none) + } + } + + override func tearDown() { + super.tearDown() + } + + /** + Test the that the map can be successfull loaded. + + */ + func testMapExists() { + XCTAssertNotNil(self.tilemap, "❗️tilemap should not be nil.") + } + + /** + Test that the map received the custom values from test delegates. + + */ + func testMapHasCorrectFlagsSet() { + guard let tilemap = tilemap else { + XCTFail("❗️tilemap did not load.") + return + } + + let monstersTileset = tilemap.getTileset(named: "monsters-16x16")! + XCTAssert(tilemap.zDeltaForLayers == 129, "❗️test delegate has a z-delta value of `129`") + XCTAssert(monstersTileset.source.filename == "monsters-16x16.png", "❗️tileset source is incorrect: \"\(monstersTileset.source.filename)\"") + XCTAssert(monstersTileset.tileSize.width == 16, "❗️tileset tile width is incorrect: \"\(monstersTileset.tileSize.width)\"") + } + + /** + Test that the map is calling back to delegates correctly. + + */ + func testMapIsUsingDelegates() { + guard (tilemap != nil), + let tilemapDelegate = tilemapDelegate else { + XCTFail("❗️tilemap did not load.") + return + } + XCTAssertTrue(tilemapDelegate.mapRenderedSuccessfully, "❗️tilemap did not call back to delegate.") + } +} diff --git a/Tests/PerformanceTests.swift b/Tests/PerformanceTests.swift new file mode 100644 index 00000000..bfd0e387 --- /dev/null +++ b/Tests/PerformanceTests.swift @@ -0,0 +1,53 @@ +// +// PerformanceTests.swift +// SKTiledTests +// +// Created by Michael Fessenden on 10/27/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import XCTest +@testable import SKTiled + + +class PerformanceTests: XCTestCase { + + var testBundle: Bundle! + + override func setUp() { + super.setUp() + + + if (testBundle == nil) { + TiledDefaults.shared.loggingLevel = .none + testBundle = Bundle(for: type(of: self)) + } + } + + override func tearDown() { + super.tearDown() + } + + func testSmallMapLoadTime() { + self.measure { + if let mapURL = testBundle!.url(forResource: "test-small", withExtension: "tmx") { + if let map = SKTilemap.load(tmxFile: mapURL.path, loggingLevel: .none) { + map.debugDrawOptions = [.drawGrid, .drawBounds] + } + } + self.stopMeasuring() + } + + } + + func testLargeMapLoadTime() { + self.measure { + if let mapURL = testBundle!.url(forResource: "test-large", withExtension: "tmx") { + if let map = SKTilemap.load(tmxFile: mapURL.path, loggingLevel: .none) { + map.debugDrawOptions = [.drawGrid, .drawBounds] + } + } + self.stopMeasuring() + } + } +} diff --git a/Tests/PropertiesTests.swift b/Tests/PropertiesTests.swift new file mode 100644 index 00000000..07a6c5e7 --- /dev/null +++ b/Tests/PropertiesTests.swift @@ -0,0 +1,94 @@ +// +// PropertiesTests.swift +// SKTiledTests +// +// Created by Michael Fessenden on 10/27/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import XCTest +@testable import SKTiled + + +class PropertiesTests: XCTestCase { + + var testBundle: Bundle! + var tilemap: SKTilemap? + + override func setUp() { + super.setUp() + + if (testBundle == nil) { + TiledGlobals.default.loggingLevel = .none + testBundle = Bundle(for: type(of: self)) + } + + if (tilemap == nil) { + let mapurl = testBundle!.url(forResource: "test-tilemap", withExtension: "tmx")! + tilemap = SKTilemap.load(tmxFile: mapurl.path, loggingLevel: .none) + } + } + + override func tearDown() { + super.tearDown() + } + + /** + Test the `SKTiledObject.stringArrayForKey` function. + + The actual property string is: "tom,dick , harry", so we're also + testing the parser's ability to strip out whitespaces correctly. + + */ + func testStringArrayProperties() { + var stringArrayProperties: [String] = [] + if let tilemap = tilemap { + stringArrayProperties = tilemap.stringArrayForKey("strings") + } + + XCTAssertEqual(stringArrayProperties, ["tom", "dick", "harry"]) + } + + /** + Test the `SKTiledObject.integerArrayForKey` function. + + The actual property string is: " 0,1,2 ,3,4,5,6,7 ,8,9, 10", so we're + also testing the parser's ability to strip out whitespaces correctly. + + */ + func testIntArrayProperties() { + var intArrayProperties: [Int] = [] + if let tilemap = tilemap { + intArrayProperties = tilemap.integerArrayForKey("integers") + } + + XCTAssertEqual(intArrayProperties, [0,1,2,3,4,5,6,7,8,9,10]) + } + + /** + Test the `SKTiledObject.doubleArrayForKey` function. + + */ + func testDoubleArrayProperties() { + var doubleArrayProperties: [Double] = [] + if let tilemap = tilemap { + doubleArrayProperties = tilemap.doubleArrayForKey("doubles") + } + XCTAssertEqual(doubleArrayProperties, [1.2, 5.4, 12, 18.25]) + } + + // MARK: Ignore Properties + + /** + Test the `ignoreProperties` argument in the tilemap load method. + + The `properties` test map has a map boolean property of `allowZoom` + set to false. If the tilemap ignores the properties parsing phase, + the `SKTilemap.allowZoom` should remain true. + */ + func testPropertiesAreIgnored() { + let mapurl = testBundle!.url(forResource: "test-tilemap", withExtension: "tmx")! + let ignoredMap = SKTilemap.load(tmxFile: mapurl.path, ignoreProperties: true)! + XCTAssertFalse(ignoredMap.allowZoom == false, "❗️tilemap `allowZoom` property should still be the default `true` value") + } +} diff --git a/Tests/QueryTests.swift b/Tests/QueryTests.swift new file mode 100644 index 00000000..c766d26b --- /dev/null +++ b/Tests/QueryTests.swift @@ -0,0 +1,91 @@ +// +// TilesetTests.swift +// SKTiledTests +// +// Created by Michael Fessenden on 10/27/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import XCTest +@testable import SKTiled + + +/// Test global id matching & parsing. +class QueryTests: XCTestCase { + + var testBundle: Bundle! + var tilemap: SKTilemap? + + let tilemapName = "test-tilemap" + + override func setUp() { + super.setUp() + + if (testBundle == nil) { + TiledGlobals.default.loggingLevel = .none + testBundle = Bundle(for: type(of: self)) + } + + if (tilemap == nil) { + let mapurl = testBundle!.url(forResource: tilemapName, withExtension: "tmx")! + tilemap = SKTilemap.load(tmxFile: mapurl.path, loggingLevel: .none) + } + } + + override func tearDown() { + super.tearDown() + } + + + /** + Test gid/id matching, etc. + + */ + func testQueryFunctions() { + + let testTilesetName = "items-8x8" + let testLayerName = "Objects" + let testGID: Int = 79 + + guard let tilemap = tilemap else { + XCTFail("❗️tilemap did not load.") + return + } + + // + guard let tileset = tilemap.getTileset(named: testTilesetName) else { + XCTFail("❗️tileset \"\(testTilesetName)\" cannot be loaded.") + return + } + + XCTAssert(tileset.tileSize == CGSize(width: 8, height: 8), "❗️tileset tile size is incorrect.") + XCTAssert(tileset.firstGID == 74, "❗️tileset first gid is incorrect: \(tileset.firstGID)") + + guard let testLayer = tilemap.tileLayers(named: testLayerName).first else { + XCTFail("❗️layer \"\(testLayerName)\" cannot be loaded.") + return + } + + + let testTiles = testLayer.getTiles(globalID: testGID) + XCTAssert(testTiles.count == 3, "❗️tile count for gid \(testGID) is incorrect: \(testTiles.count)") + } + + /** + Test to see if a tileset will return the proper tile from a global id. + Tileset has been given an incorrect firstGID value of: 91 + + Tile Data GID: 130 + - Local ID: 39 + - has `color` property of `#ffa07daa` + */ + func testGlobalIDQuery() { + guard let tilemap = tilemap else { + XCTFail("❗️tilemap did not load.") + return + } + + //let orangeTiles = tilemap.getTiles(globalID: 130) + + } +} diff --git a/Tests/Tests+Extensions.swift b/Tests/Tests+Extensions.swift new file mode 100644 index 00000000..dce87606 --- /dev/null +++ b/Tests/Tests+Extensions.swift @@ -0,0 +1,31 @@ +// +// Tests+Extensions.swift +// SKTiledTests +// +// Created by Michael Fessenden on 10/30/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import Foundation +import XCTest +@testable import SKTiled + + +extension SKTilemap { + + public class func load(tmxFile: String, ignoreProperties: Bool) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: nil, tilesetDataSource: nil, + updateMode: TiledGlobals.default.updateMode, + withTilesets: nil, ignoreProperties: ignoreProperties, + loggingLevel: TiledGlobals.default.loggingLevel) + } + + public class func load(tmxFile: String, delegate: SKTilemapDelegate, tilesetDataSource: SKTilesetDataSource, loggingLevel: LoggingLevel) -> SKTilemap? { + return SKTilemap.load(tmxFile: tmxFile, inDirectory: nil, + delegate: delegate, tilesetDataSource: tilesetDataSource, + updateMode: TiledGlobals.default.updateMode, + withTilesets: nil, ignoreProperties: false, + loggingLevel: loggingLevel) + } +} diff --git a/Tests/TilemapTests.swift b/Tests/TilemapTests.swift new file mode 100644 index 00000000..524651be --- /dev/null +++ b/Tests/TilemapTests.swift @@ -0,0 +1,104 @@ +// +// TilemapTests.swift +// SKTiledTests +// +// Created by Michael Fessenden on 10/27/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import XCTest +@testable import SKTiled + + +class TilemapTests: XCTestCase { + + var testBundle: Bundle! + var tilemap: SKTilemap? + let tilemapName = "test-tilemap" + + override func setUp() { + super.setUp() + + if (testBundle == nil) { + TiledGlobals.default.loggingLevel = .none + testBundle = Bundle(for: type(of: self)) + } + + if (tilemap == nil) { + let mapurl = testBundle!.url(forResource: tilemapName, withExtension: "tmx")! + tilemap = SKTilemap.load(tmxFile: mapurl.path, loggingLevel: .none) + } + } + + override func tearDown() { + super.tearDown() + } + + /** + Test that the tilemap has the correct attributes compared to the source + Tiled file. + + */ + func testBasicMapAttributes() { + guard let tilemap = tilemap else { + XCTFail("❗️tilemap did not load.") + return + } + + + let expectedLayerCount = 7 + let expectedObjectCount = 3 + XCTAssert(tilemap.layerCount == expectedLayerCount, "❗️tilemap layer count incorrect: \(tilemap.layerCount)") + XCTAssert(tilemap.objectCount == expectedObjectCount, "❗️tilemap object count incorrect: \(tilemap.objectCount)") + } + + /** + Test that the tilemap is correctly querying tiles of a certain type. + + */ + func testQueryTilesOfType() { + guard let tilemap = tilemap else { + XCTFail("❗️tilemap did not load.") + return + } + + let expectedTileCount = 191 + let tiles = tilemap.getTiles(ofType: "wall") + XCTAssert(tiles.count == expectedTileCount, "❗️tilemap tile count is incorrect: \(tiles.count)") + } + + /** + Test to make sure the `getObjectProxies` methods work. + */ + func testMapQueryObjectProxies() { + guard let tilemap = tilemap else { + XCTFail("❗️tilemap did not load.") + return + } + + let mapObjects = tilemap.getObjects() + let mapProxies = tilemap.getObjectProxies() + XCTAssert(mapProxies.count == mapObjects.count, "❗️tilemap object proxy count incorrect: \(mapProxies.count), \(mapObjects.count)") + } + + /** + Test to make sure the `getObjectProxies` methods work. + */ + func testLayerQueryObjectProxies() { + guard let tilemap = tilemap else { + XCTFail("❗️tilemap did not load.") + return + } + + let objectGroup = "Collision" + + guard let objLayer = tilemap.objectGroups(named: objectGroup).first else { + XCTFail("❗️layer '\(objectGroup)' not found in \(tilemap.mapName).") + return + } + + let layerObjects = objLayer.getObjects() + let layerProxies = objLayer.getObjectProxies() + XCTAssert(layerProxies.count == layerObjects.count, "❗️layer object proxy count incorrect: \(layerProxies.count), \(layerObjects.count)") + } +} diff --git a/Tests/TilesetTests.swift b/Tests/TilesetTests.swift new file mode 100644 index 00000000..69898a6f --- /dev/null +++ b/Tests/TilesetTests.swift @@ -0,0 +1,106 @@ +// +// TilesetTests.swift +// SKTiledTests +// +// Created by Michael Fessenden on 10/27/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import XCTest +@testable import SKTiled + + +/// Test tileset objects. +class TilesetTests: XCTestCase { + + var testBundle: Bundle! + var tilemap: SKTilemap? + var tileset: SKTileset? + + let tilesetName = "environment-8x8" + + override func setUp() { + super.setUp() + + if (testBundle == nil) { + TiledGlobals.default.loggingLevel = .none + testBundle = Bundle(for: type(of: self)) + } + + if (tilemap == nil) { + let mapurl = testBundle!.url(forResource: "test-tilemap", withExtension: "tmx")! + tilemap = SKTilemap.load(tmxFile: mapurl.path, loggingLevel: .none) + tileset = tilemap?.getTileset(named: tilesetName) + } + } + + override func tearDown() { + super.tearDown() + } + + /** + Test to see if a named tileset can be properly queried. + */ + func testTilesetExists() { + XCTAssertNotNil(tileset, "❗️cannot access tileset: \"\(tilesetName)\"") + } + + /** + Test to see if a named tileset has the correct Tiled properties: + + name="environment-8x8" + tilewidth="8" + tileheight="8" + spacing="0" + tilecount="45" + columns="15" + + */ + func testTilesetProperties() { + guard let tileset = tileset else { + XCTFail("❗️could not load test tileset.") + return + } + + XCTAssert(tileset.name == "environment-8x8", "❗️tileset name incorrect: \(tileset.name)") + XCTAssert(tileset.tileSize == CGSize(width: 8, height: 8), "❗️tileset tile size is incorrect.") + XCTAssert(tileset.spacing == 0, "❗️tileset spacing is incorrect: \(tileset.spacing)") + XCTAssert(tileset.tilecount == 45, "❗️tileset tile count is incorrect: \(tileset.tilecount)") + XCTAssert(tileset.columns == 15, "❗️tileset column count is incorrect: \(tileset.columns)") + XCTAssert(tileset.firstGID == 1, "❗️tileset first gid is incorrect.") + } + + /** + Test to see if a tileset will return the proper tile from a global id. + Tileset has been given an incorrect firstGID value of: 91 + + Tile Data GID: 79 + - Local ID: 5 + - has `color` property of `#ffa07daa` + */ + func testGlobalIDQuery() { + guard let tilemap = tilemap, + (tileset != nil) else { + XCTFail("❗️could not load test assets.") + return + } + + // gid 79 is the key + let keyid = 79 + let expectedLocalID = 5 + let expectedKeyCount = 3 + let keyTiles = tilemap.getTiles(globalID: keyid) + XCTAssert(keyTiles.count == 3, "❗️tile count for gid \(keyid) should be \(expectedKeyCount): \(keyTiles.count)") + + var keyPropertyIsCorrect = true + var keyIDsAreCorrect = true + for tile in keyTiles { + let tileColorHex = tile.tileData.stringForKey("color") + keyPropertyIsCorrect = (tileColorHex != nil) && (tileColorHex == "#ffa07daa") + keyIDsAreCorrect = (tile.tileData.localID == expectedLocalID) && (tile.tileData.globalID == keyid) + } + + XCTAssert(keyPropertyIsCorrect == true, "❗️tiles with gid \(keyid) should have a `color` property") + XCTAssert(keyIDsAreCorrect == true, "❗️tiles with gid \(keyid) should have a local id of \(expectedLocalID)") + } +} diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 4b2cce1a..d8cf92ea 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -1,6 +1,6 @@ // // AppDelegate.swift -// SKTiled +// SKTiled Demo - iOS // // Created by Michael Fessenden on 3/21/16. // Copyright © 2016 Michael Fessenden. All rights reserved. @@ -11,10 +11,12 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - // Thread.sleep(forTimeInterval: 3.0) + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + Thread.sleep(forTimeInterval: 3.0) // Override point for customization after application launch. return true } @@ -41,4 +43,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } + } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 8da77862..85791fd7 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -1,207 +1,340 @@ - - + + - + - + - - + + - - + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + - + + - + + + - + + + + + + + + + + + + + + - + - - - - - + + + + diff --git a/iOS/GameViewController.swift b/iOS/GameViewController.swift index 910e3a3e..cc9a9230 100644 --- a/iOS/GameViewController.swift +++ b/iOS/GameViewController.swift @@ -1,10 +1,11 @@ // // GameViewController.swift -// SKTiled +// SKTiled Demo - iOS // // Created by Michael Fessenden on 3/21/16. // Copyright (c) 2016 Michael Fessenden. All rights reserved. // +// iOS Game View Controller import UIKit import SpriteKit @@ -13,23 +14,54 @@ import SpriteKit class GameViewController: UIViewController, Loggable { let demoController = DemoController.default + var uiColor: UIColor = UIColor(hexString: "#757B8D") + // debugging labels (top) + @IBOutlet weak var rotateDeviceIcon: UIImageView! + @IBOutlet weak var cameraInfoLabel: UILabel! + @IBOutlet weak var pauseInfoLabel: UILabel! + + + // debugging labels (bottom) @IBOutlet weak var mapInfoLabel: UILabel! @IBOutlet weak var tileInfoLabel: UILabel! @IBOutlet weak var propertiesInfoLabel: UILabel! - @IBOutlet weak var cameraInfoLabel: UILabel! - @IBOutlet weak var pauseInfoLabel: UILabel! @IBOutlet weak var debugInfoLabel: UILabel! - @IBOutlet weak var objectsButton: UIButton! + + // demo buttons + @IBOutlet weak var fitButton: UIButton! + @IBOutlet weak var gridButton: UIButton! @IBOutlet weak var graphButton: UIButton! + @IBOutlet weak var objectsButton: UIButton! + @IBOutlet weak var effectsButton: UIButton! + @IBOutlet weak var renderStatsButton: UIButton! + @IBOutlet weak var nextButton: UIButton! + @IBOutlet weak var controlsView: UIStackView! + + // camera mode icons + @IBOutlet weak var controlIconView: UIStackView! + @IBOutlet var demoFileAttributes: NSObject! - @IBOutlet weak var buttonsView: UIStackView! + // render stats + @IBOutlet weak var statsStackView: UIStackView! + @IBOutlet weak var statsHeaderLabel: UILabel! + @IBOutlet weak var statsRenderModeLabel: UILabel! + @IBOutlet weak var statsCPULabel: UILabel! + @IBOutlet weak var statsVisibleLabel: UILabel! + @IBOutlet weak var statsObjectsLabel: UILabel! + @IBOutlet weak var statsActionsLabel: UILabel! + @IBOutlet weak var statsEffectsLabel: UILabel! + @IBOutlet weak var statsUpdatedLabel: UILabel! + @IBOutlet weak var statsRenderLabel: UILabel! + + + var landscapeInitialized: Bool = false var timer = Timer() - var loggingLevel: LoggingLevel = SKTiledLoggingLevel + var loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel override func viewDidLoad() { super.viewDidLoad() @@ -37,6 +69,7 @@ class GameViewController: UIViewController, Loggable { // Configure the view. let skView = self.view as! SKView + loggingLevel = TiledGlobals.default.loggingLevel // setup the controller demoController.loggingLevel = loggingLevel demoController.view = skView @@ -47,52 +80,75 @@ class GameViewController: UIViewController, Loggable { } #if DEBUG - skView.showsFPS = true skView.showsNodeCount = true skView.showsDrawCount = true skView.showsPhysics = false #endif - - skView.showsFields = true /* SpriteKit optimizations */ skView.shouldCullNonVisibleNodes = true skView.ignoresSiblingOrder = true - setupDebuggingLabels() + // initialize the demo interface + setupDemoInterface() + setupButtonAttributes() - NotificationCenter.default.addObserver(self, selector: #selector(updateUIControls), name: NSNotification.Name(rawValue: "updateUIControls"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateDebugLabels), name: NSNotification.Name(rawValue: "updateDebugLabels"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateCommandString), name: NSNotification.Name(rawValue: "updateCommandString"), object: nil) - /* create the game scene */ - demoController.loadScene(url: currentURL, usePreviousCamera: false) + NotificationCenter.default.addObserver(self, selector: #selector(tilemapWasUpdated), name: Notification.Name.Map.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateDebuggingOutput), name: Notification.Name.Demo.UpdateDebugging, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateCommandString), name: Notification.Name.Debug.CommandIssued, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(sceneCameraUpdated), name: Notification.Name.Camera.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(renderStatsUpdated), name: Notification.Name.Map.RenderStatsUpdated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(renderStatisticsVisibilityChanged), name: Notification.Name.RenderStats.VisibilityChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tilemapUpdateModeChanged), name: Notification.Name.Map.UpdateModeChanged, object: nil) + + demoController.loadScene(url: currentURL, usePreviousCamera: demoController.preferences.usePreviousCamera) + + // rotate device icon + addWiggleAnimationToView(viewToAnimate: rotateDeviceIcon) + } + + func addWiggleAnimationToView(viewToAnimate: UIView) { + let easeInOutTiming = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) + let wiggle = CAKeyframeAnimation(keyPath: "transform.rotation.z") + wiggle.duration = 1.000 + wiggle.values = [0.000, 1.571, 1.571, 0.000] as [Float] + wiggle.keyTimes = [0.000, 0.275, 0.625, 1.000] as [NSNumber] + wiggle.timingFunctions = [easeInOutTiming, easeInOutTiming, easeInOutTiming] + wiggle.repeatCount = HUGE + viewToAnimate.layer.add(wiggle, forKey:"wiggle") } - /** Update the scene. */ + /// allow correct rotating override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) (self.view as? SKView)?.scene?.size = size + self.rotateDeviceIcon.isHidden = self.isLandScape + self.statsStackView.isHidden = !(self.isLandScape && TiledGlobals.default.enableRenderCallbacks) + self.renderStatsButton.isHidden = !self.isLandScape } override func viewDidLayoutSubviews() { - // Pause the scene while the window resizes if the game is active. - let skView = self.view as! SKView if let scene = skView.scene { - if let sceneDelegate = scene as? SKTiledSceneDelegate { if let cameraNode = sceneDelegate.cameraNode { - cameraNode.bounds = skView.bounds + cameraNode.setCameraBounds(bounds: skView.bounds) } } } } - - func setupDebuggingLabels() { + + // MARK: - Setup + + + /** + Initialize the debugging labels. + */ + func setupDemoInterface() { mapInfoLabel.text = "Map: " tileInfoLabel.text = "Tile: " - propertiesInfoLabel.text = "Properties:" + propertiesInfoLabel.text = "--" cameraInfoLabel.text = "Camera:" pauseInfoLabel.text = "-" debugInfoLabel.text = "-" @@ -116,8 +172,58 @@ class GameViewController: UIViewController, Loggable { pauseInfoLabel.shadowOffset = shadowOffset debugInfoLabel.shadowOffset = shadowOffset + statsEffectsLabel.shadowColor = shadowColor + controlsView?.alpha = 0.9 + statsUpdatedLabel.isHidden = true + + // stats view + statsHeaderLabel.shadowColor = shadowColor + statsHeaderLabel.shadowOffset = shadowOffset + statsRenderModeLabel.shadowColor = shadowColor + statsRenderModeLabel.shadowOffset = shadowOffset + statsCPULabel.shadowColor = shadowColor + statsCPULabel.shadowOffset = shadowOffset + statsVisibleLabel.shadowColor = shadowColor + statsVisibleLabel.shadowOffset = shadowOffset + statsObjectsLabel.shadowColor = shadowColor + statsObjectsLabel.shadowOffset = shadowOffset + statsActionsLabel.shadowColor = shadowColor + statsActionsLabel.shadowOffset = shadowOffset + statsEffectsLabel.shadowColor = shadowColor + statsEffectsLabel.shadowOffset = shadowOffset + statsUpdatedLabel.shadowColor = shadowColor + statsUpdatedLabel.shadowOffset = shadowOffset + statsRenderLabel.shadowColor = shadowColor + statsRenderLabel.shadowOffset = shadowOffset + + + let deviceIsInLandscapeMode = self.isLandScape + let renderStatsAreVisible = (self.isLandScape && TiledGlobals.default.enableRenderCallbacks) + + rotateDeviceIcon.isHidden = (deviceIsInLandscapeMode == true) + statsStackView.isHidden = !renderStatsAreVisible + renderStatsButton.isHidden = !renderStatsAreVisible } + /** + Set up the control buttons. + */ + func setupButtonAttributes() { + + let allButtons = [fitButton, gridButton, graphButton, objectsButton, effectsButton, renderStatsButton, nextButton] + + // set the button attributes + allButtons.forEach { button in + if let button = button { + button.setTitleColor(UIColor.white, for: .normal) + button.backgroundColor = uiColor + button.layer.cornerRadius = 5 + } + } + } + + // MARK: - Button Events + /** Action called when `fit to view` button is pressed. @@ -133,7 +239,7 @@ class GameViewController: UIViewController, Loggable { - parameter sender: `Any` ui button. */ @IBAction func gridButtonPressed(_ sender: Any) { - self.demoController.toggleMapDemoDrawGridBounds() + self.demoController.toggleMapDemoDrawGridAndBounds() } /** @@ -154,6 +260,15 @@ class GameViewController: UIViewController, Loggable { self.demoController.toggleMapObjectDrawing() } + /** + Action called when `stats` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func statsButtonPressed(_ sender: Any) { + self.demoController.toggleRenderStatistics() + } + /** Action called when `next` button is pressed. @@ -163,6 +278,39 @@ class GameViewController: UIViewController, Loggable { self.demoController.loadNextScene() } + /** + Action called when `effects` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func effectsButtonPressed(_ sender: Any) { + self.demoController.toggleTilemapEffectsRendering() + } + + @IBAction func clampButtonPressed(_ sender: Any) { + let skView = self.view as! SKView + if let tiledScene = skView.scene as? SKTiledScene { + var newClampValue: CameraZoomClamping = .none + switch tiledScene.cameraNode.zoomClamping { + case .none: + newClampValue = .tenth + case .tenth: + newClampValue = .quarter + case .quarter: + newClampValue = .half + case .half: + newClampValue = .third + case .third: + newClampValue = .none + } + + tiledScene.cameraNode.zoomClamping = newClampValue + } + } + + @IBAction func tilemapUpdateModeAction(_ sender: Any) { + demoController.cycleTilemapUpdateMode() + } override var shouldAutorotate: Bool { return true @@ -191,7 +339,7 @@ class GameViewController: UIViewController, Loggable { - parameter notification: `Notification` notification. */ - func updateDebugLabels(notification: Notification) { + @objc func updateDebuggingOutput(notification: Notification) { if let mapInfo = notification.userInfo!["mapInfo"] { mapInfoLabel.text = mapInfo as? String } @@ -200,24 +348,40 @@ class GameViewController: UIViewController, Loggable { tileInfoLabel.text = tileInfo as? String } - var propertiesDefaultText = " " if let propertiesInfo = notification.userInfo!["propertiesInfo"] { if let pinfo = propertiesInfo as? String { - if (pinfo.characters.isEmpty == false) { - propertiesDefaultText = pinfo + if (pinfo.isEmpty == false) { + propertiesInfoLabel.text = pinfo + } else { + propertiesInfoLabel.text = "--" } } - } - - if let cameraInfo = notification.userInfo!["cameraInfo"] { - cameraInfoLabel.text = cameraInfo as? String - } + } if let pauseInfo = notification.userInfo!["pauseInfo"] { pauseInfoLabel.text = pauseInfo as? String + } + } + + /** + Update the camera debug information. + + - parameter notification: `Notification` notification. + */ + @objc func sceneCameraUpdated(notification: Notification) { + guard let camera = notification.object as? SKTiledSceneCamera else { + fatalError("no camera!!") } - propertiesInfoLabel.text = propertiesDefaultText + cameraInfoLabel.text = camera.description + + if (self.isLandScape == false) { + cameraInfoLabel.lineBreakMode = .byWordWrapping + cameraInfoLabel.numberOfLines = 2 + } else { + cameraInfoLabel.lineBreakMode = .byTruncatingTail + cameraInfoLabel.numberOfLines = 1 + } } /** @@ -225,7 +389,7 @@ class GameViewController: UIViewController, Loggable { - parameter notification: `Notification` notification. */ - func updateCommandString(notification: Notification) { + @objc func updateCommandString(notification: Notification) { var duration: TimeInterval = 3.0 if let commandDuration = notification.userInfo!["duration"] { @@ -234,13 +398,8 @@ class GameViewController: UIViewController, Loggable { if let commandString = notification.userInfo!["command"] { - var commandFormatted = commandString as! String - commandFormatted = "\(commandFormatted)".uppercaseFirst - - debugInfoLabel.fadeInThenOut(change: { - self.debugInfoLabel.text = "▹ \(commandFormatted)" - - }, delay: duration) + let commandFormatted = commandString as! String + debugInfoLabel.setTextValue(commandFormatted, animated: true, interval: duration) } } @@ -257,15 +416,101 @@ class GameViewController: UIViewController, Loggable { - parameter notification: `Notification` notification. */ - func updateUIControls(notification: Notification) { - if let hasGraphs = notification.userInfo!["hasGraphs"] { - graphButton.isEnabled = (hasGraphs as? Bool) == true + @objc func tilemapWasUpdated(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { return } + + if (tilemap.hasKey("uiColor")) { + if let hexString = tilemap.stringForKey("uiColor") { + self.uiColor = UIColor(hexString: hexString) + } } - if let hasObjects = notification.userInfo!["hasObjects"] { - objectsButton.isEnabled = (hasObjects as? Bool) == true + let effectsEnabled = (tilemap.shouldEnableEffects == true) + let effectsMessage = (effectsEnabled == true) ? (tilemap.shouldRasterize == true) ? "Effects: on (raster)" : "Effects: on" : "Effects: off" + statsHeaderLabel.text = "Rendering: \(TiledGlobals.default.renderer.name)" + statsRenderModeLabel.text = "Mode: \(tilemap.updateMode.name)" + statsEffectsLabel.text = "\(effectsMessage)" + statsEffectsLabel.isHidden = (effectsEnabled == false) + statsVisibleLabel.text = "Visible: \(tilemap.nodesInView.count)" + statsVisibleLabel.isHidden = (TiledGlobals.default.enableCameraCallbacks == false) + + let graphsCount = tilemap.graphs.count + let hasGraphs = (graphsCount > 0) + + var graphAction = "show" + for layer in tilemap.tileLayers() { + if layer.debugDrawOptions.contains(.drawGraph) { + graphAction = "hide" } } + let graphButtonTitle = (graphsCount > 0) ? (graphsCount > 1) ? "\(graphAction) graphs" : "\(graphAction) graph" : "no graphs" + let hasObjects: Bool = (tilemap.getObjects().isEmpty == false) + + graphButton.isHidden = !hasGraphs + objectsButton.isEnabled = hasObjects + + let gridButtonTitle = (tilemap.debugDrawOptions.contains(.drawGrid)) ? "hide grid" : "show grid" + let objectsButtonTitle = (hasObjects == true) ? (tilemap.showObjects == true) ? "hide objects" : "show objects" : "show objects" + + graphButton.setTitle(graphButtonTitle, for: UIControl.State.normal) + gridButton.setTitle(gridButtonTitle, for: UIControl.State.normal) + objectsButton.setTitle(objectsButtonTitle, for: UIControl.State.normal) + setupButtonAttributes() + + // clean up render stats + statsCPULabel.isHidden = false + statsActionsLabel.isHidden = (tilemap.updateMode != .actions) + statsObjectsLabel.isHidden = false + } + + // MARK: - Debugging + + /** + Updates the render stats debugging info. + + - parameter notification: `Notification` notification. + */ + @objc func renderStatsUpdated(notification: Notification) { + guard let renderStats = notification.object as? SKTilemap.RenderStatistics else { return } + + self.statsHeaderLabel.text = "Rendering: \(TiledGlobals.default.renderer.name)" + self.statsRenderModeLabel.text = "Mode: \(renderStats.updateMode.name)" + self.statsCPULabel.attributedText = renderStats.processorAttributedString + self.statsVisibleLabel.text = "Visible: \(renderStats.visibleCount)" + self.statsVisibleLabel.isHidden = (TiledGlobals.default.enableCameraCallbacks == false) + self.statsObjectsLabel.isHidden = (renderStats.objectsVisible == false) + self.statsObjectsLabel.text = "Objects: \(renderStats.objectCount)" + let renderString = (TiledGlobals.default.timeDisplayMode == .seconds) ? String(format: "%.\(String(6))f", renderStats.renderTime) : String(format: "%.\(String(2))f", renderStats.renderTime.milleseconds) + let timeFormatString = (TiledGlobals.default.timeDisplayMode == .seconds) ? "s" : "ms" + self.statsRenderLabel.text = "Render time: \(renderString)\(timeFormatString)" + + self.statsUpdatedLabel.isHidden = (renderStats.updateMode == .actions) + self.statsUpdatedLabel.text = "Updated: \(renderStats.updatedThisFrame)" + } + + /** + Show/Hide the render stats data. + + - parameter notification: `Notification` notification. + */ + @objc func renderStatisticsVisibilityChanged(notification: Notification) { + guard let userInfo = notification.userInfo as? [String: Any], + let showRenderStats = userInfo["showRenderStats"] as? Bool else { return } + + let titleText = (showRenderStats == true) ? "stats: on" : "stats: off" + self.renderStatsButton.setTitle(titleText, for: .normal) + self.statsStackView.isHidden = !(self.isLandScape && showRenderStats) + } + + /** + Callback when cache is updated. + + - parameter notification: `Notification` notification. + */ + @objc func tilemapUpdateModeChanged(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { return } + self.statsRenderModeLabel.text = "Mode: \(tilemap.updateMode.name)" + } } @@ -278,7 +523,6 @@ extension UILabel { - parameter interval: `TimeInterval` effect length. */ func setTextValue(_ newValue: String, animated: Bool = true, interval: TimeInterval = 0.7) { - guard text != newValue else { return } if animated { animate(change: { self.text = newValue }, interval: interval) } else { @@ -289,37 +533,38 @@ extension UILabel { /** Private function to animate a fade effect. - - parameter change: `() -> ()` closure. + - parameter change: `() -> Void` closure. - parameter interval: `TimeInterval` effect length. */ private func animate(change: @escaping () -> Void, interval: TimeInterval) { - UIView.animate(withDuration: interval, delay: 0.0, options: UIViewAnimationOptions.curveEaseOut, animations: { + let fadeDuration: TimeInterval = 0.5 + + UIView.animate(withDuration: 0, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: { + self.text = "" self.alpha = 1.0 }, completion: { (Bool) -> Void in change() - UIView.animate(withDuration: interval, delay: 0.0, options: UIViewAnimationOptions.curveEaseOut, animations: { + UIView.animate(withDuration: fadeDuration, delay: interval, options: UIView.AnimationOptions.curveEaseOut, animations: { self.alpha = 0.0 }, completion: nil) - }) } +} - func fadeInThenOut(change: @escaping () -> Void, delay: TimeInterval) { - let animationDuration = 0.5 - // Fade in the view - UIView.animate(withDuration: 0, animations: { () -> Void in - self.alpha = 1 - }) { (Bool) -> Void in - // After the animation completes, fade out the view after a delay - change() - - UIView.animate(withDuration: animationDuration, delay: delay, options: [.curveEaseOut], animations: { () -> Void in - self.alpha = 0 - self.text = "" - }, completion: nil) +extension UIViewController { + var isLandScape: Bool { + let orientation = UIDevice.current.orientation + switch orientation { + case .portrait, .portraitUpsideDown: + return false + case .landscapeLeft, .landscapeRight: + return true + default: + guard let window = self.view.window else { return false } + return window.frame.size.width > window.frame.size.height } } } diff --git a/iOS/Info.plist b/iOS/Info.plist index bdd5f027..96cb4d01 100644 --- a/iOS/Info.plist +++ b/iOS/Info.plist @@ -4,8 +4,6 @@ CFBundleDevelopmentRegion en - CFBundleDisplayName - SKTiledDemo CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -17,17 +15,13 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.16 + 1.20 CFBundleSignature ???? CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 1 LSRequiresIPhoneOS - UIAppFonts - - ArcadeNormal.ttf - UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/iOS/framework/Info.plist b/iOS/framework/Info.plist index fe87e7e6..17a791a8 100644 --- a/iOS/framework/Info.plist +++ b/iOS/framework/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.16 + 1.2 CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 1 LSApplicationCategoryType public.app-category.developer-tools NSPrincipalClass diff --git a/macOS/AppDelegate.swift b/macOS/AppDelegate.swift index 63960957..af8c1c7e 100644 --- a/macOS/AppDelegate.swift +++ b/macOS/AppDelegate.swift @@ -1,6 +1,6 @@ // // AppDelegate.swift -// SKTiled +// SKTiled Demo - macOS // // Created by Michael Fessenden on 9/19/16. // Copyright © 2016 Michael Fessenden. All rights reserved. @@ -16,15 +16,39 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var currentLoggingLevel: NSMenuItem! @IBOutlet weak var recentFilesMenu: NSMenuItem! @IBOutlet weak var recentFilesSubmenu: NSMenu! - @IBOutlet weak var liveModeMenuItem: NSMenuItem! - @IBOutlet weak var mapBoundsMenuItem: NSMenuItem! - @IBOutlet weak var mapGridMenuItem: NSMenuItem! - @IBOutlet weak var navigationGraphMenuItem: NSMenuItem! + @IBOutlet weak var updateModeMenuItem: NSMenuItem! + @IBOutlet weak var renderEffectsMenuItem: NSMenuItem! + @IBOutlet weak var loggingLevelMenuItem: NSMenuItem! + @IBOutlet weak var timeDisplayMenuItem: NSMenuItem! + + + // map top menu + @IBOutlet weak var mapMenuItem: NSMenuItem! + @IBOutlet weak var mapDebugDrawMenu: NSMenuItem! + @IBOutlet weak var layerVisibilityMenu: NSMenuItem! + @IBOutlet weak var layerIsolationMenu: NSMenuItem! + + // debug menu + @IBOutlet weak var renderStatisticsMenuItem: NSMenuItem! + @IBOutlet weak var mouseFiltersMenuItem: NSMenuItem! + @IBOutlet weak var tileColorsMenuItem: NSMenuItem! + @IBOutlet weak var objectColorsMenuItem: NSMenuItem! + + + // caching + @IBOutlet weak var isolateTilesMenuItem: NSMenuItem! + + // camera menu + @IBOutlet weak var cameraIgnoreMaxZoomMenuItem: NSMenuItem! + @IBOutlet weak var cameraUserPreviousMenuItem: NSMenuItem! + @IBOutlet weak var cameraCallbacksMenuItem: NSMenuItem! + @IBOutlet weak var cameraCallbacksContainedMenuItem: NSMenuItem! + @IBOutlet weak var cameraZoomClampingMenuItem: NSMenuItem! + func applicationDidFinishLaunching(_ aNotification: Notification) { - // Insert code here to initialize your application - NotificationCenter.default.addObserver(self, selector: #selector(updateDelegateMenuItems), name: NSNotification.Name(rawValue: "updateDelegateMenuItems"), object: nil) + setupNotifications() } func applicationWillTerminate(_ aNotification: Notification) { @@ -36,7 +60,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } var viewController: GameViewController? { - if let window = NSApplication.shared().mainWindow { + if let window = NSApplication.shared.mainWindow { if let controller = window.contentViewController as? GameViewController { return controller } @@ -44,18 +68,147 @@ class AppDelegate: NSObject, NSApplicationDelegate { return nil } + /// Reference to the current scene's tilemap. var tilemap: SKTilemap? { - guard let gameController = viewController else { return nil } - guard let view = gameController.view as? SKView else { return nil } - guard let scene = view.scene as? SKTiledScene else { return nil } + guard let gameController = viewController, + let view = gameController.view as? SKView, + let scene = view.scene as? SKTiledScene else { + return nil + } return scene.tilemap } + + func setupNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(demoSceneLoaded), name: Notification.Name.Demo.SceneLoaded, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(tilemapWasUpdated), name: Notification.Name.Map.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tilemapUpdateModeChanged), name: Notification.Name.Map.UpdateModeChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tilemapCacheUpdated), name: Notification.Name.Map.CacheUpdated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(initializeTilemapInterface), name: Notification.Name.Map.FinishedRendering, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(renderStatisticsVisibilityChanged), name: Notification.Name.RenderStats.VisibilityChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(globalsUpdated), name: Notification.Name.Globals.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(demoCameraUpdated), name: Notification.Name.Camera.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(initializeIsolateTilesMenu), name: Notification.Name.DataStorage.IsolationModeChanged, object: nil) + } + + // MARK: - Interface + + /** + Update the current demo interface when the tilemap has finished rendering. + + - parameter notification: `Notification` tilemap name. + */ + @objc func initializeTilemapInterface(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { + return + } + + // initialize menus for layer functions + self.initializeTilemapMenus(tilemap: tilemap) + + + // tilemap menu + renderEffectsMenuItem.state = (tilemap.shouldEnableEffects == true) ? .on : .off + + // debug menu + renderStatisticsMenuItem.state = (TiledGlobals.default.enableRenderCallbacks == true) ? .on : .off + + // create the logging level menu + self.initializeLoggingLevelMenu() + + // create the mouse filter menu + self.initializeMouseFilterMenu() + + // create the debugging colors menu + self.initializeDebugColorsMenu() + + // update the time display menu + self.updateRenderStatsTimeFormatMenu() + + // initialize the tile isolation menu + self.initializeIsolateTilesMenu() + + guard let window = NSApplication.shared.mainWindow else { + return + } + + if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String { + window.title = "\(appName): \(tilemap.url.filename)" + } + + } + + // MARK: - Menu Initialization + + /** + Initialize debug menus. + + - parameter tilemap: `SKTilemap` tile map object. + */ + @objc func initializeDemoMenus(tilemap: SKTilemap) { + + } + + + + /** + Initialize debug menus. + + - parameter tilemap: `SKTilemap` tile map object. + */ + @objc func initializeDebugMenus(tilemap: SKTilemap) { + + } + + + /** + Callback to update the UI whenever the tilemap is updated in the demo. + + - parameter notification: `Notification` event notification. + */ + @objc func tilemapWasUpdated(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { + return + } + + guard (tilemap.isRendered == true) else { return } + let renderState = (tilemap.shouldEnableEffects == true) ? NSControl.StateValue.on : NSControl.StateValue.off + renderEffectsMenuItem.state = renderState + + // update the tilemap menus + updateTilemapMenus(tilemap: tilemap) + } + + // MARK: - Notification Callbacks + + @objc func tilemapCacheUpdated(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { + return + } + + Logger.default.log("tilemap cache updated: \(tilemap.updateMode.description)", level: .info, symbol: "AppDelegate") + } + + @objc func tilemapUpdateModeChanged(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { + return + } + Logger.default.log("tilemap update mode changed: \(tilemap.updateMode.description)", level: .info, symbol: "AppDelegate") + } + + @objc func demoSceneLoaded(notification: Notification) { + guard (notification.object as? SKTiledScene != nil) else { + return + } + Logger.default.log("new scene loaded", level: .info, symbol: "AppDelegate") + } + // MARK: - Loading & Saving /** Action to launch a file dialog and load map. */ - @IBAction func loadTilemap(_ sender: Any) { + @IBAction func loadTilemapAction(_ sender: Any) { guard let gameController = viewController else { return } // open a file dialog @@ -63,7 +216,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { dialog.title = "Choose a Tiled resource." dialog.allowedFileTypes = ["tmx"] - if (dialog.runModal() == NSModalResponseOK) { + if (dialog.runModal() == NSApplication.ModalResponse.OK) { // tmx file path let result = dialog.url @@ -87,67 +240,309 @@ class AppDelegate: NSObject, NSApplicationDelegate { /** Action to reload the current scene. */ - @IBAction func reloadScene(_ sender: Any) { + @IBAction func reloadTilemapAction(_ sender: Any) { guard let gameController = viewController else { return } let demoController = gameController.demoController demoController.reloadScene() } - @IBAction func currentFilesPressed(_ sender: Any) { + + @IBAction func timeDisplayUpdated(_ sender: NSMenuItem) { + guard let identifier = Int(sender.accessibilityTitle()!) else { + Logger.default.log("invalid identifier: \(sender.accessibilityTitle()!)", level: .error) + return + } + + if let newTimeDisplay = TiledGlobals.TimeDisplayMode.init(rawValue: identifier) { + TiledGlobals.default.timeDisplayMode = newTimeDisplay + } + + self.updateRenderStatsTimeFormatMenu() + } + + @IBAction func mouseFiltersUpdatedAction(_ sender: NSMenuItem) { + guard let idstring = sender.accessibilityTitle() else { return } + + if let identifier = Int(idstring) { + let sentFlag = TiledGlobals.DebugDisplayOptions.MouseFilters.init(rawValue: identifier) + + if TiledGlobals.default.debug.mouseFilters.contains(sentFlag) { + TiledGlobals.default.debug.mouseFilters = TiledGlobals.default.debug.mouseFilters.subtracting(sentFlag) + } else { + TiledGlobals.default.debug.mouseFilters.insert(sentFlag) + } + + // rebuild the mouse filter menu + self.initializeMouseFilterMenu() + } + } + + @IBAction func tileColorsUpdatedAction(_ sender: NSMenuItem) { + guard let colorString = sender.accessibilityTitle() else { return } + + let newColor = SKColor(hexString: colorString) + TiledGlobals.default.debug.tileHighlightColor = newColor + Logger.default.log("setting new tile highlight color: \(colorString)", level: .info, symbol: "AppDelegate") + + NotificationCenter.default.post( + name: Notification.Name.Globals.Updated, + object: nil, + userInfo: ["tileColor": newColor] + ) + + self.initializeDebugColorsMenu() + } + + @IBAction func objectColorsUpdatedAction(_ sender: NSMenuItem) { + guard let colorString = sender.accessibilityTitle() else { return } + + let newColor = SKColor(hexString: colorString) + TiledGlobals.default.debug.objectHighlightColor = newColor + Logger.default.log("setting new object highlight color: \(colorString)", level: .info, symbol: "AppDelegate") + + NotificationCenter.default.post( + name: Notification.Name.Globals.Updated, + object: nil, + userInfo: ["objectColor": newColor] + ) + + self.initializeDebugColorsMenu() + } + + @IBAction func cameraZoomClampingUpdated(_ sender: NSMenuItem) { + guard let idstring = sender.accessibilityTitle() else { return } + + guard let gameController = viewController, + let view = gameController.view as? SKView, + let scene = view.scene as? SKTiledScene else { return } + + + if let identifier = Float(idstring) { + guard let newZoomClampingMode = CameraZoomClamping.init(rawValue: CGFloat(identifier)) else { + return + } + + Logger.default.log("zoom clamping: \(newZoomClampingMode)", level: .info, symbol: "AppDelegate") + scene.cameraNode.zoomClamping = newZoomClampingMode + } + } + + @IBAction func cameraFitToViewAction(_ sender: NSMenuItem) { guard let gameController = viewController else { return } - let demoController = gameController.demoController - demoController.dumpCurrentResources() + gameController.demoController.fitSceneToView() } - @IBAction func dumpCurrentTilemapAction(_ sender: Any) { + @IBAction func cameraIgnoreZoomConstraintsAction(_ sender: NSMenuItem) { + guard let gameController = viewController, + let view = gameController.view as? SKView, + let scene = view.scene as? SKTiledScene else { return } + + + let newIgnoreValue = (sender.state == .off) + scene.cameraNode.ignoreZoomConstraints = newIgnoreValue + Logger.default.log("ignoring camera constraints: \(scene.cameraNode.ignoreZoomConstraints)", level: .info, symbol: "AppDelegate") + self.initializeSceneCameraMenu(camera: scene.cameraNode) + gameController.demoController.preferences.ignoreZoomConstraints = newIgnoreValue + gameController.demoController.updateCommandString("using camera zoom constraints: \(newIgnoreValue)") + + } + + @IBAction func cameraUsePreviousCameraAction(_ sender: NSMenuItem) { guard let gameController = viewController else { return } - let demoController = gameController.demoController - demoController.dumpCurrentTilemap() + + if let preferences = gameController.demoController.preferences { + preferences.usePreviousCamera = (sender.state == .off) + Logger.default.log("setting use previous camera: \(preferences.usePreviousCamera)", level: .info, symbol: "AppDelegate") + gameController.demoController.updateCommandString("setting use previous camera: \(preferences.usePreviousCamera)") + } } - // MARK: - Logging + @IBAction func cameraCallbacksAction(_ sender: NSMenuItem) { + guard let gameController = viewController, + let view = gameController.view as? SKView, + let scene = view.scene as? SKTiledScene else { return } + + let currentState = (sender.state == .on) + let nextState = !currentState + + TiledGlobals.default.enableCameraCallbacks = nextState + gameController.demoController.updateCommandString("enable camera callbacks: \(nextState == true ? "on" : "off")") + + + NotificationCenter.default.post( + name: Notification.Name.Globals.Updated, + object: nil, + userInfo: nil + ) - @IBAction func loggingLevelDebug(_ sender: NSMenuItem) { - Logger.default.loggingLevel = .debug + scene.cameraNode.enableDelegateCallbacks(nextState) } - @IBAction func loggingLevelInfo(_ sender: NSMenuItem) { - Logger.default.loggingLevel = .info + @IBAction func cameraVisibleNodesCallbacksAction(_ sender: NSMenuItem) { + guard let gameController = viewController, + let view = gameController.view as? SKView, + let scene = view.scene as? SKTiledScene else { return } + + let currentState = (sender.state == .on) + let nextState = !currentState + + scene.cameraNode.notifyDelegatesOnContainedNodesChange = nextState + gameController.demoController.updateCommandString("enable camera callbacks: \(nextState == true ? "on" : "off")") } - @IBAction func loggingLevelWarning(_ sender: NSMenuItem) { - Logger.default.loggingLevel = .warning + @IBAction func cameraStatistics(_ sender: NSMenuItem) { + guard let gameController = viewController, + let view = gameController.view as? SKView, + let scene = view.scene as? SKTiledScene else { return } + + scene.cameraNode.dumpStatistics() } - @IBAction func loggingLevelError(_ sender: NSMenuItem) { - Logger.default.loggingLevel = .error + // MARK: - Demo Menu + + @IBAction func showCurrentMapsAction(_ sender: Any) { + guard let gameController = viewController else { return } + let demoController = gameController.demoController + demoController.getCurrentlyLoadedTilemaps() } - @IBAction func loggingLevelCustom(_ sender: NSMenuItem) { - Logger.default.loggingLevel = .custom + @IBAction func showAllAssetsAction(_ sender: Any) { + guard let gameController = viewController else { return } + let demoController = gameController.demoController + demoController.getCurrentlyLoadedAssets() } - @IBAction func loggingLevelSuccess(_ sender: NSMenuItem) { - Logger.default.loggingLevel = .success + @IBAction func getExternallyLoadedAssetsAction(_ sender: Any) { + guard let gameController = viewController else { return } + let demoController = gameController.demoController + demoController.getExternallyLoadedAssets() } - @IBAction func mapStatisticsPressed(_ sender: Any) { + @IBAction func showDemoPreferencesAction(_ sender: Any) { guard let gameController = viewController else { return } let demoController = gameController.demoController - demoController.printMapStatistics() + demoController.preferences.dumpStatistics() } - @IBAction func liveModeToggled(_ sender: NSMenuItem) { + + @IBAction func loggingLevelUpdated(_ sender: NSMenuItem) { + guard let identifier = sender.accessibilityTitle(), + let identifierIntValue = Int(identifier) else { + Logger.default.log("invalid logging identifier: \(sender.accessibilityIdentifier())", level: .error, symbol: "AppDelegate") + return + } + + if let newLoggingLevel = LoggingLevel.init(rawValue: identifierIntValue) { + if (TiledGlobals.default.loggingLevel != newLoggingLevel) { + TiledGlobals.default.loggingLevel = newLoggingLevel + Logger.default.log("global logging level changed: \(newLoggingLevel)", level: .info, symbol: "AppDelegate") + + // update controllers + NotificationCenter.default.post( + name: Notification.Name.Globals.Updated, + object: nil, + userInfo: nil + ) + } + } + } + + + @IBAction func drawObjectBoundariesAction(_ sender: NSMenuItem) { guard let gameController = viewController else { return } - guard let view = gameController.view as? SKView else { return } - guard let scene = view.scene as? SKTiledDemoScene else { return } let demoController = gameController.demoController - let currentLiveMode = scene.liveMode - let commandString = (currentLiveMode == true) ? "disabling live mode..." : "enabling live mode..." - scene.liveMode = !scene.liveMode - demoController.updateCommandString(commandString) + demoController.toggleObjectBoundaryDrawing() - sender.title = (currentLiveMode == true) ? "Live Mode: Off" : "Live Mode: On" + + var currentObjectBoundsMode = false + if let tilemap = self.tilemap { + currentObjectBoundsMode = tilemap.debugDrawOptions.contains(.drawObjectBounds) + } + sender.state = (currentObjectBoundsMode == true) ? .on : .off + } + + + + // MARK: - Map Menu + + @IBAction func mapStatisticsPressed(_ sender: Any) { + guard let gameController = viewController else { return } + let demoController = gameController.demoController + demoController.dumpMapStatistics() + } + + @IBAction func mapRenderQualityPressed(_ sender: Any) { + guard let tilemap = self.tilemap else { return } + + var headerString = "# Tilemap \"\(tilemap.url.filename)\", render quality: \(tilemap.renderQuality) ( max: \(tilemap.maxRenderQuality) ):" + let headerUnderline = String(repeating: "-", count: headerString.count ) + headerString = "\n\(headerString)\n\(headerUnderline)\n" + + //var layerData: [String] = [] + + var longestName = 0 + tilemap.getLayers().forEach { layer in + let layerNameCount = layer.layerName.count + 3 + if layerNameCount > longestName { + longestName = layerNameCount + } + } + + tilemap.getLayers().forEach { layer in + + var layerNameString = "\"\(layer.layerName)\"" + let layerPadding = longestName - layerNameString.count + + if layerPadding > 0 { + let padString = String(repeating: " ", count: layerPadding) + layerNameString = "\(layerNameString)\(padString)" + } + + headerString += "\n - \(layerNameString) → quality: \(layer.renderQuality)" + } + print("\(headerString)\n\n") + } + + @IBAction func toggleRenderTilemapEffects(_ sender: NSMenuItem) { + if let tilemap = self.tilemap { + let currentState = tilemap.shouldEnableEffects + tilemap.shouldEnableEffects = !currentState + Logger.default.log("setting tilemap effects rendering: \(tilemap.shouldEnableEffects)", level: .info) + + let nextState = (tilemap.shouldEnableEffects == true) ? NSControl.StateValue.on : NSControl.StateValue.off + sender.state = nextState + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + } + + + // MARK: - Map Debug Drawing + + @IBAction func debugDrawOptionsUpdated(_ sender: NSMenuItem) { + guard let identifier = Int(sender.accessibilityIdentifier()), + let tilemap = tilemap else { + Logger.default.log("invalid identifier: \(sender.accessibilityIdentifier())", level: .error) + return + } + + let willRemoveOption = sender.state == .on + let drawOption = DebugDrawOptions.init(rawValue: identifier) + if (willRemoveOption == true) { + tilemap.debugDrawOptions = tilemap.debugDrawOptions.subtracting(drawOption) + } else { + tilemap.debugDrawOptions.insert(drawOption) + } + + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap + ) } @IBAction func drawMapBoundsAction(_ sender: NSMenuItem) { @@ -160,7 +555,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { currentBoundsMode = tilemap.debugDrawOptions.contains(.drawBounds) } - sender.title = (currentBoundsMode == true) ? "Map Bounds: Off" : "Map Bounds: On" + sender.state = (currentBoundsMode == true) ? .on : .off } @IBAction func drawMapGridAction(_ sender: NSMenuItem) { @@ -173,7 +568,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { currentGridMode = tilemap.debugDrawOptions.contains(.drawGrid) } - sender.title = (currentGridMode == true) ? "Map Grid: Off" : "Map Grid: On" + sender.state = (currentGridMode == true) ? .on : .off } @IBAction func drawSceneGraphsAction(_ sender: NSMenuItem) { @@ -183,28 +578,502 @@ class AppDelegate: NSObject, NSApplicationDelegate { var currentGraphMode = false if let tilemap = self.tilemap { - currentGraphMode = tilemap.debugDrawOptions.contains(.drawGraph) + currentGraphMode = tilemap.isShowingGraphs } - sender.title = (currentGraphMode == true) ? "Navigation Graph: Off" : "Navigation Graph: On" + sender.state = (currentGraphMode == true) ? .on : .off + } + + // MARK: - Debug Menu + + @IBAction func showGlobalsAction(_ sender: Any) { + TiledGlobals.default.dumpStatistics() } + @IBAction func renderStatisticsVisibilityAction(_ sender: NSMenuItem) { + guard let gameController = viewController else { return } + let demoController = gameController.demoController + let doShowStatistics = (sender.state == .off) + demoController.toggleRenderStatistics(value: doShowStatistics) + } - func updateDelegateMenuItems(notification: Notification) { - if let liveMode = notification.userInfo!["liveMode"] { - liveModeMenuItem.title = (liveMode as? Bool) == true ? "Live Mode: On" : "Live Mode: Off" + @IBAction func isloateCachedTilesAction(_ sender: NSMenuItem) { + guard let identifier = Int(sender.accessibilityIdentifier()) else { + Logger.default.log("invalid identifier: \(sender.accessibilityIdentifier())", level: .error) + return } - if let mapBounds = notification.userInfo!["mapBounds"] { - mapBoundsMenuItem.title = (mapBounds as? Bool) == true ? "Map Bounds: On" : "Map Bounds: Off" + + let removeIsolation = (sender.state == .on) + + guard let tilemap = self.tilemap, + let dataStorage = tilemap.dataStorage else { return } + + guard let newIsolationMode = CacheIsolationMode(rawValue: identifier) else { + Logger.default.log("invalid isolation mode value: \(identifier)", level: .error) + return } - if let mapGrid = notification.userInfo!["mapGrid"] { - mapGridMenuItem.title = (mapGrid as? Bool) == true ? "Map Grid: On" : "Map Grid: Off" + + dataStorage.isolationMode = (removeIsolation == true) ? .none : newIsolationMode + } + + + // MARK: - Callbacks & Helpers + + @IBAction func tilemapUpdateGlobalChanged(_ sender: NSMenuItem) { + guard let gameController = viewController else { return } + let demoController = gameController.demoController + + + guard let identifier = Int(sender.accessibilityIdentifier()) else { + Logger.default.log("invalid identifier: \(sender.accessibilityIdentifier())", level: .error) + return } - if let navGraph = notification.userInfo!["navGraph"] { - navigationGraphMenuItem.title = (navGraph as? Bool) == true ? "Navigation Graph: On" : "Navigation Graph: Off" + if (TileUpdateMode.init(rawValue: identifier) != nil) { + demoController.updateTileUpdateMode(value: identifier) + } + } + + /** + Callback to update the UI whenever the tilemap update mode changes. + + - parameter sender: `NSMenuItem` sender menu item. + */ + @IBAction func cycleTilemapUpdateMode(_ sender: NSMenuItem) { + guard let updateMode = sender.accessibilityTitle() else { return } + guard let gameController = viewController else { return } + let demoController = gameController.demoController + + demoController.cycleTilemapUpdateMode(mode: updateMode) + } + + // MARK: - Layer Toggles + + @IBAction func toggleLayerVisibility(_ sender: NSMenuItem) { + guard let layerID = sender.accessibilityTitle() else { return } + guard let gameController = viewController else { return } + let demoController = gameController.demoController + + let layerIsVisible = (sender.state == .on) + demoController.toggleLayerVisibility(layerID: layerID, visible: !layerIsVisible) + } + + + @IBAction func toggleAllLayerVisibility(_ sender: NSMenuItem) { + guard let identifier = Int(sender.accessibilityIdentifier()) else { + Logger.default.log("invalid identifier: \(sender.accessibilityIdentifier())", level: .error) + return } + + let allLayersVisible = (identifier == 1) + + guard let gameController = viewController else { return } + let demoController = gameController.demoController + demoController.toggleAllLayerVisibility(visible: allLayersVisible) + } + + + @IBAction func isolateLayer(_ sender: NSMenuItem) { + guard let layerID = sender.accessibilityTitle() else { return } + guard let gameController = viewController else { return } + let demoController = gameController.demoController + + let doIsolateLayer = (sender.state == .off) + demoController.toggleLayerIsolated(layerID: layerID, isolated: doIsolateLayer) + } + + @IBAction func turnIsolationOff(_ sender: NSMenuItem) { + guard let gameController = viewController else { return } + let demoController = gameController.demoController + demoController.turnIsolationOff() + } + + // MARK: - Debugging + + /** + Update the render stats menu when the controller's time format changes. + */ + @objc func updateRenderStatsTimeFormatMenu() { + + if let timeDisplaySubMenu = timeDisplayMenuItem.submenu { + timeDisplaySubMenu.removeAllItems() + + for mode in TiledGlobals.default.timeDisplayMode.allModes { + let timeMenuItem = NSMenuItem(title: mode.uiControlString, action: #selector(timeDisplayUpdated), keyEquivalent: "") + timeMenuItem.setAccessibilityTitle("\(mode.rawValue)") + timeMenuItem.state = (TiledGlobals.default.timeDisplayMode.rawValue == mode.rawValue) ? NSControl.StateValue.on : NSControl.StateValue.off + timeDisplaySubMenu.addItem(timeMenuItem) + } + } + } + + // MARK: Show/Hide Render Stats + + @objc func renderStatisticsVisibilityChanged(notification: Notification) { + if let nextState = notification.userInfo!["showRenderStats"] as? Bool { + renderStatisticsMenuItem.state = (nextState == false) ? .off : .on + } + } + + // MARK: - Globals + @objc func globalsUpdated(notification: Notification) { + self.initializeLoggingLevelMenu() + self.updateRenderStatsTimeFormatMenu() + self.cameraCallbacksMenuItem.state = (TiledGlobals.default.enableCameraCallbacks == true) ? .on : .off + } + + @objc func demoCameraUpdated(notification: Notification) { + guard let camera = notification.object as? SKTiledSceneCamera else { + return + } + + self.cameraCallbacksContainedMenuItem.state = (camera.notifyDelegatesOnContainedNodesChange == true) ? .on : .off + self.initializeSceneCameraMenu(camera: camera) + } +} + + + +extension AppDelegate { + + // MARK: - Logging Level Menu + + @objc func initializeLoggingLevelMenu() { + + // update the logging menu + if let loggingLevelSubMenu = loggingLevelMenuItem.submenu { + loggingLevelSubMenu.removeAllItems() + + let allLoggingLevels = LoggingLevel.all + + for loggingLevel in allLoggingLevels { + guard (loggingLevel != LoggingLevel.none) && (loggingLevel != LoggingLevel.custom) else { continue } + + let levelMenuItem = NSMenuItem(title: loggingLevel.description.uppercaseFirst, action: #selector(loggingLevelUpdated(_:)), keyEquivalent: "") + levelMenuItem.setAccessibilityTitle("\(loggingLevel.rawValue)") + levelMenuItem.state = (TiledGlobals.default.loggingLevel == loggingLevel) ? .on : .off + loggingLevelSubMenu.addItem(levelMenuItem) + } + } + } + + @objc func updateLoggingLevelMenu() { + if let loggingLevelSubMenu = loggingLevelMenuItem.submenu { + for menuitem in loggingLevelSubMenu.items { + guard let accessibilityTitle = menuitem.accessibilityTitle() else { continue } + if let identifier = Int(accessibilityTitle) { + menuitem.state = (identifier == TiledGlobals.default.loggingLevel.rawValue) ? .on : .off + } + } + } + } + + // MARK: - Mouse Filter Menu + @objc func initializeMouseFilterMenu() { + // create the mouse filter menu + if let coordinateDisplaySubMenu = mouseFiltersMenuItem.submenu { + coordinateDisplaySubMenu.removeAllItems() + + let allOptions = Array(TiledGlobals.DebugDisplayOptions.MouseFilters.all.elements()) + let optionStrings = TiledGlobals.DebugDisplayOptions.MouseFilters.all.strings + + for (index, option) in allOptions.enumerated() { + guard (index <= optionStrings.count - 1) else { + return + } + + let featureMenuItem = NSMenuItem(title: optionStrings[index], action: #selector(mouseFiltersUpdatedAction), keyEquivalent: "") + let thisOption = TiledGlobals.DebugDisplayOptions.MouseFilters.init(rawValue: option.rawValue) + featureMenuItem.setAccessibilityTitle("\(option.rawValue)") + featureMenuItem.state = TiledGlobals.default.debug.mouseFilters.contains(thisOption) ? NSControl.StateValue.on : NSControl.StateValue.off + coordinateDisplaySubMenu.addItem(featureMenuItem) + } + } + } + + @objc func initializeDebugColorsMenu() { + + let allColors = TiledObjectColors.all + let colorNames = TiledObjectColors.names + + let currentTileColor = TiledGlobals.default.debug.tileHighlightColor + let currentObjectColor = TiledGlobals.default.debug.objectHighlightColor + + // create the tile colors menu + if let tileColorsSubMenu = tileColorsMenuItem.submenu { + tileColorsSubMenu.removeAllItems() + + for (idx, color) in allColors.enumerated() { + + let colorName = colorNames[idx] + + let tileColorMenuItem = NSMenuItem(title: "\(colorName.uppercaseFirst)", action: #selector(tileColorsUpdatedAction), keyEquivalent: "") + tileColorMenuItem.setAccessibilityTitle("\(color.hexString())") + + tileColorMenuItem.state = (color == currentTileColor) ? NSControl.StateValue.on : NSControl.StateValue.off + tileColorsSubMenu.addItem(tileColorMenuItem) + } + } + + if let objectColorsSubMenu = objectColorsMenuItem.submenu { + objectColorsSubMenu.removeAllItems() + + for (idx, color) in allColors.enumerated() { + + let colorName = colorNames[idx] + let objectColorMenuItem = NSMenuItem(title: "\(colorName.uppercaseFirst)", action: #selector(objectColorsUpdatedAction), keyEquivalent: "") + objectColorMenuItem.setAccessibilityTitle("\(color.hexString())") + + objectColorMenuItem.state = (color == currentObjectColor) ? NSControl.StateValue.on : NSControl.StateValue.off + objectColorsSubMenu.addItem(objectColorMenuItem) + } + } + } + + @objc func initializeSceneCameraMenu(camera: SKTiledSceneCamera) { + + cameraIgnoreMaxZoomMenuItem.isEnabled = true + cameraIgnoreMaxZoomMenuItem.state = (camera.ignoreZoomConstraints == true) ? .on : .off + cameraUserPreviousMenuItem.isEnabled = true + cameraZoomClampingMenuItem.isEnabled = true + cameraCallbacksContainedMenuItem.state = (camera.notifyDelegatesOnContainedNodesChange == true) ? .on : .off + + // build/rebuild the camera zoom menu + if let sceneCameraSubMenu = cameraZoomClampingMenuItem.submenu { + sceneCameraSubMenu.removeAllItems() + + for cameraMode in CameraZoomClamping.allModes() { + + let featureMenuItem = NSMenuItem(title: "\(cameraMode.name)", action: #selector(cameraZoomClampingUpdated), keyEquivalent: "") + featureMenuItem.setAccessibilityTitle("\(cameraMode.rawValue)") + + featureMenuItem.state = (cameraMode == camera.zoomClamping) ? NSControl.StateValue.on : NSControl.StateValue.off + sceneCameraSubMenu.addItem(featureMenuItem) + } + } + + if let gameController = viewController { + let preferences = gameController.demoController.preferences + cameraUserPreviousMenuItem.state = (preferences?.usePreviousCamera == true) ? .on : .off + } + } + + // MARK: - Tilemap Menus + + /** + Initialize tilemap menus. + + - parameter tilemap: `SKTilemap` tile map object. + */ + @objc func initializeTilemapMenus(tilemap: SKTilemap) { + + // map debug draw options + if let debugDrawSubmenu = mapDebugDrawMenu.submenu { + debugDrawSubmenu.removeAllItems() + + let allOptions = Array(DebugDrawOptions.all.elements()) + let optionStrings = DebugDrawOptions.all.strings + + + for (index, option) in allOptions.enumerated() { + guard (index <= optionStrings.count - 1) else { + return + } + + + + let layerMenuItem = NSMenuItem(title: optionStrings[index], action: #selector(debugDrawOptionsUpdated), keyEquivalent: "") + layerMenuItem.setAccessibilityIdentifier("\(option.rawValue)") + layerMenuItem.state = (tilemap.debugDrawOptions.contains(option)) ? NSControl.StateValue.on : NSControl.StateValue.off + debugDrawSubmenu.addItem(layerMenuItem) + } + } + + + + if let visibilitySubMenu = layerVisibilityMenu.submenu { + visibilitySubMenu.removeAllItems() + + // add all hidden/all visible + let showAllMenuItem = NSMenuItem(title: "Show all", action: #selector(toggleAllLayerVisibility), keyEquivalent: "") + showAllMenuItem.setAccessibilityIdentifier("\(1)") + + let hideAllMenuItem = NSMenuItem(title: "Hide all", action: #selector(toggleAllLayerVisibility), keyEquivalent: "") + hideAllMenuItem.setAccessibilityIdentifier("\(0)") + + let divider = NSMenuItem.separator() + visibilitySubMenu.addItem(showAllMenuItem) + visibilitySubMenu.addItem(hideAllMenuItem) + visibilitySubMenu.addItem(divider) + + + for layer in tilemap.getLayers() { + let layerMenuItem = NSMenuItem(title: layer.menuDescription, action: #selector(toggleLayerVisibility), keyEquivalent: "") + layerMenuItem.setAccessibilityTitle(layer.uuid) + layerMenuItem.state = (layer.visible == true) ? NSControl.StateValue.on : NSControl.StateValue.off + visibilitySubMenu.addItem(layerMenuItem) + } + } + + if let isolationSubMenu = layerIsolationMenu.submenu { + isolationSubMenu.removeAllItems() + + isolationSubMenu.addItem(NSMenuItem(title: "Isolate: Off", action: #selector(turnIsolationOff), keyEquivalent: "")) + isolationSubMenu.addItem(NSMenuItem.separator()) + + for layer in tilemap.getLayers() { + let layerMenuItem = NSMenuItem(title: layer.menuDescription, action: #selector(isolateLayer), keyEquivalent: "") + layerMenuItem.setAccessibilityTitle(layer.uuid) + layerMenuItem.state = (layer.isolated == true) ? NSControl.StateValue.on : NSControl.StateValue.off + isolationSubMenu.addItem(layerMenuItem) + } + } + + + if let updateModeSubMenu = updateModeMenuItem.submenu { + updateModeSubMenu.removeAllItems() + + for mode in tilemap.updateMode.allModes() { + let modeMenuItem = NSMenuItem(title: mode.uiControlString, action: #selector(cycleTilemapUpdateMode), keyEquivalent: "") + modeMenuItem.setAccessibilityTitle("\(mode.rawValue)") + modeMenuItem.state = (tilemap.updateMode.rawValue == mode.rawValue) ? NSControl.StateValue.on : NSControl.StateValue.off + updateModeSubMenu.addItem(modeMenuItem) + } + } + } + + @objc func updateTilemapMenus(tilemap: SKTilemap) { + + let allLayersHidden = tilemap.allLayersHidden + let allLayersVisible = tilemap.allLayersVisible + + // debug draw options menu + if let debugDrawSubmenu = mapDebugDrawMenu.submenu { + + for menuitem in debugDrawSubmenu.items { + let accessibilityIdentifier = menuitem.accessibilityIdentifier() + + if let identifier = Int(accessibilityIdentifier) { + let menuOption = DebugDrawOptions.init(rawValue: identifier) + menuitem.state = (tilemap.debugDrawOptions.contains(menuOption)) ? .on : .off + + let contains = menuitem.state == .on + } + } + } + + + + // update the tilemap update mode menu + if let updateModeSubMenu = self.updateModeMenuItem.submenu { + for menuitem in updateModeSubMenu.items { + if let identifier = Int(menuitem.accessibilityIdentifier()) { + menuitem.state = (identifier == tilemap.updateMode.rawValue) ? .on : .off + } + } + } + + if let visibilitySubMenu = layerVisibilityMenu.submenu { + + + visibilitySubMenu.items.forEach { menuItem in + if let layerID = menuItem.accessibilityTitle() { + + if let layer = tilemap.getLayer(withID: layerID) { + menuItem.state = (layer.visible == true) ? .on : .off + } + } + + // update the show/hide all menu + if let identifier = Int(menuItem.accessibilityIdentifier()) { + if (identifier == 0) { + //menuItem.state = (allLayersHidden == true) ? .on : .off + menuItem.isEnabled = (allLayersHidden == true) ? false : true + } + + if (identifier == 1) { + //menuItem.state = (allLayersVisible == true) ? .on : .off + menuItem.isEnabled = (allLayersVisible == true) ? false : true + } + } + } + } + + if let isolationSubMenu = layerIsolationMenu.submenu { + isolationSubMenu.items.forEach { menuItem in + if let layerID = menuItem.accessibilityTitle() { + if let layer = tilemap.getLayer(withID: layerID) { + menuItem.state = (layer.isolated == true) ? .on : .off + } + } + } + } + + if let updateModeSubMenu = updateModeMenuItem.submenu { + updateModeSubMenu.items.forEach { menuItem in + if let modeRawValue = menuItem.accessibilityTitle() { + if let modeIntValue = Int(modeRawValue) { + menuItem.state = (modeIntValue == tilemap.updateMode.rawValue) ? .on : .off + } + } + } + } + } + + + @objc func initializeIsolateTilesMenu() { + + // create the mouse filter menu + if let isolateTilesSubMenu = isolateTilesMenuItem.submenu { + isolateTilesSubMenu.removeAllItems() + + // add an off toggle and divider + let isolateOffMenuItem = NSMenuItem(title: "Isolation off", action: #selector(isloateCachedTilesAction), keyEquivalent: "") + isolateOffMenuItem.setAccessibilityIdentifier("\(0)") + let divider = NSMenuItem.separator() + isolateTilesSubMenu.addItem(isolateOffMenuItem) + isolateTilesSubMenu.addItem(divider) + + + let allIsolationModes = CacheIsolationMode.all + var currentIsolationMode = CacheIsolationMode.none + + if let tilemap = tilemap { + if let dataStorage = tilemap.dataStorage { + currentIsolationMode = dataStorage.isolationMode + } + } + + for (_, mode) in allIsolationModes.enumerated() { + + let isolateMenuItem = NSMenuItem(title: "\(mode)", action: #selector(isloateCachedTilesAction), keyEquivalent: "") + isolateMenuItem.setAccessibilityIdentifier("\(mode.rawValue)") + + isolateMenuItem.state = (mode == currentIsolationMode) ? NSControl.StateValue.on : NSControl.StateValue.off + isolateTilesSubMenu.addItem(isolateMenuItem) + } + } + } +} + + +extension SKTilemap { + + /// Returns true if all layers are hidden. + var allLayersHidden: Bool { + return getLayers(recursive: true).reduce(0, { result, layer -> Int in + let hidden: Int = layer.isHidden == true ? 0 : 1 + return result + hidden + }) == 0 + } + + /// Returns true if all layers are visible. + var allLayersVisible: Bool { + return getLayers(recursive: true).reduce(0, { result, layer -> Int in + let visible: Int = layer.isHidden == true ? 1 : 0 + return result + visible + }) == 0 } } diff --git a/macOS/Base.lproj/Main.storyboard b/macOS/Base.lproj/Main.storyboard index c9642eb9..21e2ac23 100644 --- a/macOS/Base.lproj/Main.storyboard +++ b/macOS/Base.lproj/Main.storyboard @@ -1,10 +1,9 @@ - + - + - @@ -17,7 +16,7 @@ - + @@ -31,7 +30,7 @@ - + @@ -49,7 +48,7 @@ - + @@ -63,7 +62,7 @@ - + @@ -74,7 +73,7 @@ - + @@ -84,95 +83,174 @@ - + + + + + + + + + + + + + - + - + - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + - + - + - + + - + - - + + - + + + + + + + + + + + + + + + @@ -180,7 +258,7 @@ - + @@ -194,29 +272,43 @@ - + - - - - + + + + + + + + + + + + + + + + + + - + - + - + @@ -233,92 +325,159 @@ - + - + - - + + - - - - - @@ -337,39 +496,79 @@ - - + + - - - + + + + + + + + + + + - + - - - - + + + + - + - - - - + + + + - + + + + + + + + + - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -378,93 +577,67 @@ + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - + - - - + + + - - + + + + + + + + + + + + - + - - - - - - - - diff --git a/macOS/GameViewController.swift b/macOS/GameViewController.swift index f5dcc6a9..ec0c8407 100644 --- a/macOS/GameViewController.swift +++ b/macOS/GameViewController.swift @@ -1,10 +1,11 @@ // // GameViewController.swift -// SKTiled +// SKTiled Demo - macOS // // Created by Michael Fessenden on 9/19/16. // Copyright © 2016 Michael Fessenden. All rights reserved. // +// macOS Game View Controller import Cocoa import SpriteKit @@ -14,39 +15,57 @@ import AppKit class GameViewController: NSViewController, Loggable { let demoController = DemoController.default + var uiColor: NSColor = NSColor(hexString: "#757B8D") + // debugging labels (top) + @IBOutlet weak var outputTopView: NSStackView! + @IBOutlet weak var cameraInfoLabel: NSTextField! + @IBOutlet weak var isolatedInfoLabel: NSTextField! // macOS only + @IBOutlet weak var pauseInfoLabel: NSTextField! - // debugging labels + + // debugging labels (bottom) + @IBOutlet weak var outputBottomView: NSStackView! @IBOutlet weak var mapInfoLabel: NSTextField! @IBOutlet weak var tileInfoLabel: NSTextField! @IBOutlet weak var propertiesInfoLabel: NSTextField! @IBOutlet weak var debugInfoLabel: NSTextField! - @IBOutlet weak var cameraInfoLabel: NSTextField! - @IBOutlet weak var pauseInfoLabel: NSTextField! - @IBOutlet weak var isolatedInfoLabel: NSTextField! - @IBOutlet weak var coordinateInfoLabel: NSTextField! + // demo buttons + @IBOutlet weak var fitButton: NSButton! + @IBOutlet weak var gridButton: NSButton! @IBOutlet weak var graphButton: NSButton! @IBOutlet weak var objectsButton: NSButton! + @IBOutlet weak var nextButton: NSButton! + @IBOutlet var demoFileAttributes: NSArrayController! + @IBOutlet weak var screenInfoLabel: NSTextField! + + // render stats + @IBOutlet weak var statsStackView: NSStackView! + @IBOutlet weak var statsHeaderLabel: NSTextField! + @IBOutlet weak var statsRenderModeLabel: NSTextField! + @IBOutlet weak var statsCPULabel: NSTextField! + @IBOutlet weak var statsVisibleLabel: NSTextField! + @IBOutlet weak var statsObjectsLabel: NSTextField! + @IBOutlet weak var statsActionsLabel: NSTextField! + @IBOutlet weak var statsEffectsLabel: NSTextField! + @IBOutlet weak var statsUpdatedLabel: NSTextField! + @IBOutlet weak var statsRenderLabel: NSTextField! var timer = Timer() - var loggingLevel: LoggingLevel = SKTiledLoggingLevel + var loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel var commandBackgroundColor: NSColor = NSColor(calibratedWhite: 0.2, alpha: 0.25) override func viewDidLoad() { super.viewDidLoad() - // Configure the view. let skView = self.view as! SKView // setup the controller - #if DEBUG - SKTiledLoggingLevel = .debug - #endif - loggingLevel = SKTiledLoggingLevel + loggingLevel = TiledGlobals.default.loggingLevel demoController.loggingLevel = loggingLevel demoController.view = skView @@ -55,54 +74,70 @@ class GameViewController: NSViewController, Loggable { return } - //debugInfoLabel?.isHidden = true - #if DEBUG - skView.showsFPS = true skView.showsNodeCount = true skView.showsDrawCount = true skView.showsPhysics = false - //debugInfoLabel?.isHidden = false #endif // SpriteKit optimizations skView.shouldCullNonVisibleNodes = true skView.ignoresSiblingOrder = true - setupDebuggingLabels() - - //set up notifications - NotificationCenter.default.addObserver(self, selector: #selector(updateDebugLabels), name: NSNotification.Name(rawValue: "updateDebugLabels"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateWindowTitle), name: NSNotification.Name(rawValue: "updateWindowTitle"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateUIControls), name: NSNotification.Name(rawValue: "updateUIControls"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateCommandString), name: NSNotification.Name(rawValue: "updateCommandString"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(loggingLevelUpdated), name: NSNotification.Name(rawValue: "loggingLevelUpdated"), object: nil) + // intialize the demo interface + setupDemoInterface() + setupButtonAttributes() + + // notifications + + // demo + NotificationCenter.default.addObserver(self, selector: #selector(updateDebuggingOutput), name: Notification.Name.Demo.UpdateDebugging, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(focusObjectsChanged), name: Notification.Name.Demo.FocusObjectsChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateCommandString), name: Notification.Name.Debug.CommandIssued, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateWindowTitle), name: Notification.Name.Demo.WindowTitleUpdated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(flushScene), name: Notification.Name.Demo.FlushScene, object: nil) + + // tilemap callbacks + NotificationCenter.default.addObserver(self, selector: #selector(tilemapWasUpdated), name: Notification.Name.Map.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(renderStatsUpdated), name: Notification.Name.Map.RenderStatsUpdated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tilemapUpdateModeChanged), name: Notification.Name.Map.UpdateModeChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(sceneCameraUpdated), name: Notification.Name.Camera.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(renderStatisticsVisibilityChanged), name: Notification.Name.RenderStats.VisibilityChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tilePropertiesChanged), name: Notification.Name.Tile.RenderModeChanged, object: nil) + + // resolution/content scale change + NotificationCenter.default.addObserver(self, selector: #selector(windowDidChangeBackingProperties), name: NSWindow.didChangeBackingPropertiesNotification, object: nil) + // create the game scene - demoController.loadScene(url: currentURL, usePreviousCamera: false) + demoController.loadScene(url: currentURL, usePreviousCamera: demoController.preferences.usePreviousCamera) } - override func viewDidAppear() { super.viewDidAppear() + setupButtonAttributes() + } + + override func viewWillTransition(to newSize: NSSize) { + super.viewWillTransition(to: newSize) + (self.view as? SKView)?.scene?.size = newSize } /** Set up the debugging labels. (Mimics the text style in iOS controller). */ - func setupDebuggingLabels() { - mapInfoLabel.stringValue = "Map: " - tileInfoLabel.stringValue = "Tile: " - propertiesInfoLabel.stringValue = "Properties:" + func setupDemoInterface() { + mapInfoLabel.stringValue = "" + tileInfoLabel.stringValue = "" + propertiesInfoLabel.stringValue = "" cameraInfoLabel.stringValue = "--" debugInfoLabel.stringValue = "" isolatedInfoLabel.stringValue = "" - coordinateInfoLabel.stringValue = "" // text shadow let shadow = NSShadow() - shadow.shadowOffset = NSSize(width: 2, height: 1) - shadow.shadowColor = NSColor(calibratedWhite: 0.1, alpha: 0.75) - shadow.shadowBlurRadius = 0.5 + shadow.shadowOffset = NSSize(width: 1, height: 2) + shadow.shadowColor = NSColor(calibratedWhite: 0.1, alpha: 0.6) + shadow.shadowBlurRadius = 0.1 mapInfoLabel.shadow = shadow tileInfoLabel.shadow = shadow @@ -111,7 +146,37 @@ class GameViewController: NSViewController, Loggable { cameraInfoLabel.shadow = shadow pauseInfoLabel.shadow = shadow isolatedInfoLabel.shadow = shadow - coordinateInfoLabel.shadow = shadow + + statsHeaderLabel.shadow = shadow + statsRenderModeLabel.shadow = shadow + statsCPULabel.shadow = shadow + statsVisibleLabel.shadow = shadow + statsObjectsLabel.shadow = shadow + statsActionsLabel.shadow = shadow + statsEffectsLabel.shadow = shadow + statsUpdatedLabel.shadow = shadow + statsRenderLabel.shadow = shadow + statsUpdatedLabel.isHidden = true + } + + /** + Set up the control buttons. + */ + func setupButtonAttributes() { + + let normalBevel: CGFloat = 4 + let allButtons = [fitButton, gridButton, graphButton, objectsButton, nextButton] + + // set the button attributes + allButtons.forEach { button in + if let button = button { + button.wantsLayer = true + button.bezelColor = uiColor + button.layer?.shadowColor = uiColor.darken(by: 1.0).cgColor + button.layer?.cornerRadius = normalBevel + button.layer?.backgroundColor = uiColor.cgColor + } + } } /** @@ -129,7 +194,7 @@ class GameViewController: NSViewController, Loggable { - parameter sender: `Any` ui button. */ @IBAction func gridButtonPressed(_ sender: Any) { - self.demoController.toggleMapDemoDrawGridBounds() + self.demoController.toggleMapDemoDrawGridAndBounds() } /** @@ -159,8 +224,6 @@ class GameViewController: NSViewController, Loggable { self.demoController.loadNextScene() } - // MARK: - Tracking - // MARK: - Mouse Events /** @@ -176,12 +239,32 @@ class GameViewController: NSViewController, Loggable { } } + /** + Update the window when resolution/dpi changes. + + - parameter notification: `Notification` callback. + */ + @objc func windowDidChangeBackingProperties(notification: Notification) { + guard (notification.object as? NSWindow != nil) else { return } + let skView = self.view as! SKView + if let tiledScene = skView.scene as? SKTiledDemoScene { + if let tilemap = tiledScene.tilemap { + + NotificationCenter.default.post( + name: Notification.Name.Map.Updated, + object: tilemap, + userInfo: nil + ) + } + } + } + /** Update the window's title bar with the current scene name. - parameter notification: `Notification` callback. */ - func updateWindowTitle(notification: Notification) { + @objc func updateWindowTitle(notification: Notification) { if let wintitle = notification.userInfo!["wintitle"] { if let infoDictionary = Bundle.main.infoDictionary { if let bundleName = infoDictionary[kCFBundleNameKey as String] as? String { @@ -192,25 +275,20 @@ class GameViewController: NSViewController, Loggable { } /** - Update the window's logging menu current value. + Show/hide the current SpriteKit render stats. - parameter notification: `Notification` callback. */ - func loggingLevelUpdated(notification: Notification) { - guard let mainMenu = NSApplication.shared().mainMenu else { - Logger.default.log("cannot access main menu.", level: .warning, symbol: nil) - return - } - - let appMenu = mainMenu.item(withTitle: "Demo")! - if let loggingMenu = appMenu.submenu?.item(withTitle: "Logging") { - if let currentMenuItem = loggingMenu.submenu?.item(withTag: 1024) { - if let loggingLevel = notification.userInfo!["loggingLevel"] as? LoggingLevel { - let newMenuTitle = loggingLevel.description.capitalized - currentMenuItem.title = newMenuTitle - currentMenuItem.isEnabled = false - } - } + @objc func renderStatisticsVisibilityChanged(notification: Notification) { + guard let view = self.view as? SKView else { return } + if let showRenderStats = notification.userInfo!["showRenderStats"] as? Bool { + view.showsFPS = showRenderStats + view.showsNodeCount = showRenderStats + view.showsDrawCount = showRenderStats + view.showsPhysics = showRenderStats + view.showsFields = showRenderStats + + statsStackView.isHidden = !showRenderStats } } @@ -219,8 +297,7 @@ class GameViewController: NSViewController, Loggable { - parameter notification: `Notification` notification. */ - func updateDebugLabels(notification: Notification) { - //coordinateInfoLabel.isHidden = true + @objc func updateDebuggingOutput(notification: Notification) { if let mapInfo = notification.userInfo!["mapInfo"] { mapInfoLabel.stringValue = mapInfo as! String } @@ -233,10 +310,6 @@ class GameViewController: NSViewController, Loggable { propertiesInfoLabel.stringValue = propertiesInfo as! String } - if let cameraInfo = notification.userInfo!["cameraInfo"] { - cameraInfoLabel.stringValue = cameraInfo as! String - } - if let pauseInfo = notification.userInfo!["pauseInfo"] { pauseInfoLabel.stringValue = pauseInfo as! String } @@ -244,26 +317,72 @@ class GameViewController: NSViewController, Loggable { if let isolatedInfo = notification.userInfo!["isolatedInfo"] { isolatedInfoLabel.stringValue = isolatedInfo as! String } + } + + /** + Called when the focus objects in the demo scene have changed. - if let coordinateInfo = notification.userInfo!["coordinateInfo"] { - coordinateInfoLabel.stringValue = coordinateInfo as! String - //coordinateInfoLabel.isHidden = false + - parameter notification: `Notification` notification. + */ + @objc func focusObjectsChanged(notification: Notification) { + guard let focusObjects = notification.object as? [SKTiledGeometry], + let userInfo = notification.userInfo as? [String: Any], + let tilemap = userInfo["tilemap"] as? SKTilemap else { return } + + + if let tileDataStorage = tilemap.dataStorage { + for object in tileDataStorage.objectsList { + if let proxy = object.proxy { + let isFocused = focusObjects.contains(where: { $0 as? TileObjectProxy == proxy }) + proxy.isFocused = isFocused + } + } } } + /** + Update the tile property label. + + - parameter notification: `Notification` notification. + */ + @objc func tilePropertiesChanged(notification: Notification) { + guard let tile = notification.object as? SKTile else { return } + propertiesInfoLabel.stringValue = tile.description + } + + /** + Callback when cache is updated. + + - parameter notification: `Notification` notification. + */ + @objc func tilemapUpdateModeChanged(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { return } + self.statsRenderModeLabel.stringValue = "Mode: \(tilemap.updateMode.name)" + } + + /** + Update the camera debug information. + + - parameter notification: `Notification` notification. + */ + @objc func sceneCameraUpdated(notification: Notification) { + guard let camera = notification.object as? SKTiledSceneCamera else { + fatalError("no camera!!") + } + cameraInfoLabel.stringValue = camera.description + } /** Update the the command string label. - parameter notification: `Notification` notification. */ - func updateCommandString(notification: Notification) { + @objc func updateCommandString(notification: Notification) { timer.invalidate() var duration: TimeInterval = 3.0 if let commandString = notification.userInfo!["command"] { - var commandFormatted = commandString as! String - commandFormatted = "\(commandFormatted)".uppercaseFirst - debugInfoLabel.stringValue = "▹ \(commandFormatted)" + let commandFormatted = commandString as! String + debugInfoLabel.stringValue = "\(commandFormatted)" debugInfoLabel.backgroundColor = commandBackgroundColor //debugInfoLabel.drawsBackground = true } @@ -276,10 +395,11 @@ class GameViewController: NSViewController, Loggable { timer = Timer.scheduledTimer(timeInterval: duration, target: self, selector: #selector(GameViewController.resetCommandLabel), userInfo: nil, repeats: true) } + /** Reset the command string label. */ - func resetCommandLabel() { + @objc func resetCommandLabel() { timer.invalidate() debugInfoLabel.setStringValue("", animated: true, interval: 0.75) debugInfoLabel.backgroundColor = NSColor(calibratedWhite: 0.0, alpha: 0.0) @@ -288,16 +408,100 @@ class GameViewController: NSViewController, Loggable { /** Enables/disable button controls based on the current map attributes. - - parameter notification: `Notification` notification. + - parameter notification: `Notification` notification. */ - func updateUIControls(notification: Notification) { - if let hasGraphs = notification.userInfo!["hasGraphs"] { - graphButton.isEnabled = (hasGraphs as? Bool) == true + @objc func tilemapWasUpdated(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { return } + + if (tilemap.hasKey("uiColor")) { + if let hexString = tilemap.stringForKey("uiColor") { + self.uiColor = NSColor(hexString: hexString) + } } - if let hasObjects = notification.userInfo!["hasObjects"] { - objectsButton.isEnabled = (hasObjects as? Bool) == true + let effectsEnabled = (tilemap.shouldEnableEffects == true) + let effectsMessage = (effectsEnabled == true) ? (tilemap.shouldRasterize == true) ? "Effects: on (raster)" : "Effects: on" : "Effects: off" + statsHeaderLabel.stringValue = "Rendering: \(TiledGlobals.default.renderer.name)" + statsRenderModeLabel.stringValue = "Mode: \(tilemap.updateMode.name)" + statsEffectsLabel.stringValue = "\(effectsMessage)" + statsEffectsLabel.isHidden = (effectsEnabled == false) + statsVisibleLabel.stringValue = "Visible: \(tilemap.nodesInView.count)" + statsVisibleLabel.isHidden = (TiledGlobals.default.enableCameraCallbacks == false) + + let graphsCount = tilemap.graphs.count + let hasGraphs = (graphsCount > 0) + + var graphAction = "show" + for layer in tilemap.tileLayers() { + if layer.debugDrawOptions.contains(.drawGraph) { + graphAction = "hide" + } } + + let graphButtonTitle = (graphsCount > 0) ? (graphsCount > 1) ? "\(graphAction) graphs" : "\(graphAction) graph" : "no graphs" + let hasObjects: Bool = (tilemap.getObjects().isEmpty == false) + + /// ISOLATED LAYERS + let isolatedLayers = tilemap.getLayers().filter({ $0.isolated == true}) + var isolatedInfoString = "" + + if (isolatedLayers.isEmpty == false) { + isolatedInfoString = "Isolated: " + let isolatedLayerNames: [String] = isolatedLayers.map { "\"\($0.layerName)\"" } + isolatedInfoString += isolatedLayerNames.joined(separator: ", ") + } + + isolatedInfoLabel.stringValue = isolatedInfoString + graphButton.isHidden = !hasGraphs + //graphButton.isEnabled = (graphsCount > 0) + objectsButton.isEnabled = hasObjects + + + gridButton.title = (tilemap.debugDrawOptions.contains(.drawGrid)) ? "hide grid" : "show grid" + objectsButton.title = (hasObjects == true) ? (tilemap.showObjects == true) ? "hide objects" : "show objects" : "show objects" + graphButton.title = graphButtonTitle + setupButtonAttributes() + + + // clean up render stats + statsCPULabel.isHidden = false + statsActionsLabel.isHidden = (tilemap.updateMode != .actions) + statsObjectsLabel.isHidden = false + } + + /** + Clear the current scene. + */ + @objc func flushScene() { + demoController.flushScene() + setupDemoInterface() + } + + // MARK: - Debugging + + + /** + Updates the render stats debugging info. + + - parameter notification: `Notification` notification. + */ + @objc func renderStatsUpdated(notification: Notification) { + guard let renderStats = notification.object as? SKTilemap.RenderStatistics else { return } + + self.statsHeaderLabel.stringValue = "Rendering: \(TiledGlobals.default.renderer.name)" + self.statsRenderModeLabel.stringValue = "Mode: \(renderStats.updateMode.name)" + self.statsVisibleLabel.stringValue = "Visible: \(renderStats.visibleCount)" + self.statsVisibleLabel.isHidden = (TiledGlobals.default.enableCameraCallbacks == false) + self.statsObjectsLabel.isHidden = (renderStats.objectsVisible == false) + self.statsObjectsLabel.stringValue = "Objects: \(renderStats.objectCount)" + self.statsCPULabel.attributedStringValue = renderStats.processorAttributedString + let renderString = (TiledGlobals.default.timeDisplayMode == .seconds) ? String(format: "%.\(String(6))f", renderStats.renderTime) : String(format: "%.\(String(2))f", renderStats.renderTime.milleseconds) + let timeFormatString = (TiledGlobals.default.timeDisplayMode == .seconds) ? "s" : "ms" + self.statsRenderLabel.stringValue = "Render time: \(renderString)\(timeFormatString)" + + + self.statsUpdatedLabel.isHidden = (renderStats.updateMode == .actions) + self.statsUpdatedLabel.stringValue = "Updated: \(renderStats.updatedThisFrame)" } } @@ -336,22 +540,34 @@ extension NSTextField { } } + /** + Highlight the label with the given color. + + - parameter color: `NSColor` label color. + - parameter interval: `TimeInterval` effect length. + */ + func highlighWith(color: NSColor, interval: TimeInterval = 3.0) { + animate(change: { + self.textColor = color + }, interval: interval) + } + /** Private function to animate a fade effect. - - parameter change: `() -> ()` closure. + - parameter change: `() -> Void` closure. - parameter interval: `TimeInterval` effect length. */ - private func animate(change: @escaping () -> Void, interval: TimeInterval) { + fileprivate func animate(change: @escaping () -> Void, interval: TimeInterval) { NSAnimationContext.runAnimationGroup({ context in context.duration = interval / 2.0 - context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) + context.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) animator().alphaValue = 0.0 }, completionHandler: { change() NSAnimationContext.runAnimationGroup({ context in context.duration = interval / 2.0 - context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) + context.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) self.animator().alphaValue = 1.0 }, completionHandler: {}) }) diff --git a/macOS/GameWindowController.swift b/macOS/GameWindowController.swift index bfae73c9..a77c3585 100644 --- a/macOS/GameWindowController.swift +++ b/macOS/GameWindowController.swift @@ -1,6 +1,6 @@ // // GameViewController.swift -// SKTiled +// SKTiled Demo - macOS // // Created by Michael Fessenden on 10/18/16. // Copyright © 2016 Michael Fessenden. All rights reserved. @@ -23,6 +23,7 @@ class GameWindowController: NSWindowController, NSWindowDelegate { override func awakeFromNib() { super.awakeFromNib() window?.delegate = self + window?.acceptsMouseMovedEvents = true } // MARK: - Resizing @@ -40,7 +41,7 @@ class GameWindowController: NSWindowController, NSWindowDelegate { /* if let sceneDelegate = scene as? SKTiledSceneDelegate { if let cameraNode = sceneDelegate.cameraNode { - cameraNode.bounds = view.bounds + cameraNode.setCameraBounds(bounds: view.bounds) } }*/ } @@ -68,12 +69,16 @@ class GameWindowController: NSWindowController, NSWindowDelegate { // update the camera bounds if let cameraNode = sceneDelegate.cameraNode { - cameraNode.bounds = view.bounds + cameraNode.setCameraBounds(bounds: view.bounds) } } } - NotificationCenter.default.post(name: Notification.Name(rawValue: "updateWindowTitle"), object: nil, userInfo: ["wintitle": wintitle]) + NotificationCenter.default.post( + name: Notification.Name.Demo.WindowTitleUpdated, + object: nil, + userInfo: ["wintitle": wintitle] + ) } func windowDidEndLiveResize(_ notification: Notification) { diff --git a/macOS/Info.plist b/macOS/Info.plist index 437537e0..cbf31665 100644 --- a/macOS/Info.plist +++ b/macOS/Info.plist @@ -2,10 +2,6 @@ - UIAppFonts - - ArcadeNormal.ttf - CFBundleDevelopmentRegion en CFBundleExecutable @@ -16,14 +12,12 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 - CFBundleName - SKTiledDemo CFBundlePackageType APPL CFBundleShortVersionString - 1.16 + 1.20 CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 1 LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion diff --git a/macOS/framework/Info.plist b/macOS/framework/Info.plist index 6d909a1f..3623b786 100644 --- a/macOS/framework/Info.plist +++ b/macOS/framework/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.16 + 1.2 CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 1 LSApplicationCategoryType public.app-category.developer-tools NSHumanReadableCopyright diff --git a/macOS/framework/SKTiled.h b/macOS/framework/SKTiled.h index 24aab4d7..80529c10 100644 --- a/macOS/framework/SKTiled.h +++ b/macOS/framework/SKTiled.h @@ -8,9 +8,5 @@ #import -//! Project version number for SKTiled. FOUNDATION_EXPORT double SKTiledVersionNumber; - FOUNDATION_EXPORT const unsigned char SKTiledVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import diff --git a/scripts/clean-assets.sh b/scripts/clean-assets.sh new file mode 100755 index 00000000..d28405b0 --- /dev/null +++ b/scripts/clean-assets.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# remove xattrs from assets +xattr -rc $PROJECT_DIR/Assets/. +echo "cleaning project assets: $PROJECT_DIR/Assets" diff --git a/tvOS/AppDelegate.swift b/tvOS/AppDelegate.swift new file mode 100644 index 00000000..7067a67b --- /dev/null +++ b/tvOS/AppDelegate.swift @@ -0,0 +1,47 @@ +// +// AppDelegate.swift +// SKTiled Demo - tvOS +// +// Created by Michael Fessenden on 3/26/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/tvOS/Base.lproj/Main.storyboard b/tvOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..2f4c23d6 --- /dev/null +++ b/tvOS/Base.lproj/Main.storyboard @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tvOS/GameViewController.swift b/tvOS/GameViewController.swift new file mode 100644 index 00000000..8d73b56a --- /dev/null +++ b/tvOS/GameViewController.swift @@ -0,0 +1,481 @@ +// +// GameViewController.swift +// SKTiled Demo - tvOS +// +// Created by Michael Fessenden on 3/26/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// +// tvOS Game View Controller + +import UIKit +import SpriteKit +import GameController + + +class GameViewController: GCEventViewController, Loggable { + + let demoController = DemoController.default + var uiColor: UIColor = UIColor(hexString: "#757B8D") + + // debugging labels (top) + @IBOutlet weak var cameraInfoLabel: UILabel! + + // debugging labels (bottom) + @IBOutlet weak var mapInfoLabel: UILabel! + @IBOutlet weak var debugInfoLabel: UILabel! + + + // demo buttons + @IBOutlet weak var fitButton: UIButton! + @IBOutlet weak var gridButton: UIButton! + @IBOutlet weak var graphButton: UIButton! + @IBOutlet weak var objectsButton: UIButton! + @IBOutlet weak var effectsButton: UIButton! + @IBOutlet weak var updateModeButton: UIButton! + @IBOutlet weak var nextButton: UIButton! + + // render stats + @IBOutlet weak var statsStackView: UIStackView! + @IBOutlet weak var statsHeaderLabel: UILabel! + @IBOutlet weak var statsRenderModeLabel: UILabel! + @IBOutlet weak var statsCPULabel: UILabel! + @IBOutlet weak var statsVisibleLabel: UILabel! + @IBOutlet weak var statsObjectsLabel: UILabel! + @IBOutlet weak var statsActionsLabel: UILabel! + @IBOutlet weak var statsEffectsLabel: UILabel! + @IBOutlet weak var statsUpdatedLabel: UILabel! + @IBOutlet weak var statsRenderLabel: UILabel! + + // container for the buttons + @IBOutlet weak var mainControlsView: UIStackView! + + // camera mode icons + @IBOutlet weak var controlIconView: UIStackView! + + // icon controls + @IBOutlet weak var dollyIcon: UIImageView! + @IBOutlet weak var zoomIcon: UIImageView! + + @IBOutlet var demoFileAttributes: NSObject! + + var timer = Timer() + + var loggingLevel: LoggingLevel = TiledGlobals.default.loggingLevel + + override func viewDidLoad() { + super.viewDidLoad() + + controllerUserInteractionEnabled = true + + // Configure the view. + let skView = self.view as! SKView + + // setup the controller + loggingLevel = TiledGlobals.default.loggingLevel + demoController.loggingLevel = loggingLevel + demoController.view = skView + + guard let currentURL = demoController.currentURL else { + log("no tilemap to load.", level: .warning) + return + } + + #if DEBUG + skView.showsFPS = true + skView.showsNodeCount = true + skView.showsDrawCount = true + #endif + + + skView.showsFPS = true + skView.shouldCullNonVisibleNodes = true + skView.ignoresSiblingOrder = true + + // initialize the demo interface + setupDemoInterface() + setupButtonAttributes() + + // demo notifications + NotificationCenter.default.addObserver(self, selector: #selector(updateDebuggingOutput), name: Notification.Name.Demo.UpdateDebugging, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateCommandString), name: Notification.Name.Debug.CommandIssued, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(tilemapWasUpdated), name: Notification.Name.Map.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(sceneCameraUpdated), name: Notification.Name.Camera.Updated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(renderStatsUpdated), name: Notification.Name.Map.RenderStatsUpdated, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(tilemapUpdateModeChanged), name: Notification.Name.Map.UpdateModeChanged, object: nil) + + /* create the game scene */ + demoController.loadScene(url: currentURL, usePreviousCamera: demoController.preferences.usePreviousCamera) + } + + override func viewDidLayoutSubviews() { + // Pause the scene while the window resizes if the game is active. + + let skView = self.view as! SKView + if let scene = skView.scene { + + if let sceneDelegate = scene as? SKTiledSceneDelegate { + if let cameraNode = sceneDelegate.cameraNode { + cameraNode.setCameraBounds(bounds: view.bounds) + } + } + } + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Release any cached data, images, etc that aren't in use. + } + + /** + Setup the main interface. + */ + func setupDemoInterface() { + + mapInfoLabel.text = "map: " + debugInfoLabel.text = "command: " + + controlIconView.isHidden = true + controlIconView.isHidden = true + dollyIcon.isHidden = false + zoomIcon.isHidden = false + + if let fitButton = fitButton { + fitButton.isEnabled = true + } + + if let gridButton = gridButton { + gridButton.isEnabled = true + } + + if let graphButton = graphButton { + graphButton.isEnabled = true + } + + if let objectsButton = objectsButton { + objectsButton.isEnabled = true + } + + if let nextButton = nextButton { + nextButton.isEnabled = false + } + + if let effectsButton = effectsButton { + effectsButton.isEnabled = true + } + + self.statsUpdatedLabel.isHidden = true + } + + /** + Set up the control buttons. + */ + func setupButtonAttributes() { + let allButtons = [fitButton, gridButton, graphButton, objectsButton, effectsButton, updateModeButton, nextButton] + // set the button attributes + allButtons.forEach { button in + if let button = button { + button.setTitleColor(UIColor.white, for: .normal) + + let buttonColor = (button.state == UIControl.State.normal) ? uiColor : uiColor.withAlphaComponent(0.5) + button.backgroundColor = buttonColor + button.layer.cornerRadius = 4 + } + } + } + + // MARK: - Button Actions + + /** + Action called when `fit to view` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func fitButtonPressed(_ sender: Any) { + self.demoController.fitSceneToView() + } + + /** + Action called when `show grid` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func gridButtonPressed(_ sender: Any) { + self.demoController.toggleMapDemoDrawGridAndBounds() + } + + /** + Action called when `show graph` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func graphButtonPressed(_ sender: Any) { + self.demoController.toggleMapGraphVisualization() + } + + /** + Action called when `show objects` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func objectsButtonPressed(_ sender: Any) { + self.demoController.toggleMapObjectDrawing() + } + + /** + Action called when `next` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func nextButtonPressed(_ sender: Any) { + self.demoController.loadNextScene() + } + + /** + Action called when `effects` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func effectsButtonPressed(_ sender: Any) { + self.demoController.toggleTilemapEffectsRendering() + } + + /** + Action called when `update mode` button is pressed. + + - parameter sender: `Any` ui button. + */ + @IBAction func updateButtonPressed(_ sender: Any) { + self.demoController.cycleTilemapUpdateMode() + } + + // MARK: - Callbacks + + /** + Update the debugging labels with scene information. + + - parameter notification: `Notification` notification. + */ + @objc func updateDebuggingOutput(notification: Notification) { + if let mapInfo = notification.userInfo!["mapInfo"] { + mapInfoLabel.text = mapInfo as? String + } + + if let cameraInfo = notification.userInfo!["cameraInfo"] { + cameraInfoLabel.text = cameraInfo as? String + } + } + + /** + Update the camera control controls. + + - parameter notification: `Notification` notification. + */ + @objc func sceneCameraUpdated(notification: Notification) { + guard let camera = notification.object as? SKTiledSceneCamera else { + fatalError("no camera!!") + } + + //controlIconView.isHidden = true + //dollyIcon.isHidden = true + //zoomIcon.isHidden = true + + var stackViewHidden = true + var dollyHidden = true + var zoomHidden = true + + switch camera.controlMode { + + case .dolly: + stackViewHidden = false + dollyHidden = false + zoomHidden = true + + case .zoom: + stackViewHidden = false + dollyHidden = true + zoomHidden = false + + case .none: + stackViewHidden = true + dollyHidden = false + zoomHidden = false + } + + controlIconView.isHidden = stackViewHidden + + dollyIcon.isHidden = dollyHidden + zoomIcon.isHidden = zoomHidden + + fitButton?.isEnabled = stackViewHidden + gridButton?.isEnabled = stackViewHidden + graphButton?.isEnabled = stackViewHidden + objectsButton?.isEnabled = stackViewHidden + nextButton?.isEnabled = stackViewHidden + + mainControlsView.isHidden = !stackViewHidden + cameraInfoLabel.text = camera.description + } + + /** + Update the the command string label. + + - parameter notification: `Notification` notification. + */ + @objc func updateCommandString(notification: Notification) { + var duration: TimeInterval = 3.0 + + if let commandDuration = notification.userInfo!["duration"] { + duration = commandDuration as! TimeInterval + } + + if let commandString = notification.userInfo!["command"] { + let commandFormatted = commandString as! String + debugInfoLabel.setTextValue(commandFormatted, animated: true, interval: duration) + } + } + + /** + Reset the command string label. + */ + func resetCommandLabel() { + timer.invalidate() + debugInfoLabel.text = "" + } + + /** + Enables/disable button controls based on the current map attributes. + + - parameter notification: `Notification` notification. + */ + @objc func tilemapWasUpdated(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { return } + + if (tilemap.hasKey("uiColor")) { + if let hexString = tilemap.stringForKey("uiColor") { + self.uiColor = UIColor(hexString: hexString) + } + } + + let effectsEnabled = (tilemap.shouldEnableEffects == true) + let effectsMessage = (effectsEnabled == true) ? (tilemap.shouldRasterize == true) ? "Effects: on (raster)" : "Effects: on" : "Effects: off" + + statsHeaderLabel.text = "Rendering: \(TiledGlobals.default.renderer.name)" + statsRenderModeLabel.text = "Mode: \(tilemap.updateMode.name)" + statsVisibleLabel.text = "Visible: \(tilemap.nodesInView.count)" + statsEffectsLabel.text = "\(effectsMessage)" + statsEffectsLabel.isHidden = (effectsEnabled == false) + let graphsCount = tilemap.graphs.count + let hasGraphs: Bool = graphsCount > 0 + + var graphAction = "show" + for layer in tilemap.tileLayers() { + if layer.debugDrawOptions.contains(.drawGraph) { + graphAction = "hide" + } + } + let graphButtonTitle = (graphsCount > 0) ? (graphsCount > 1) ? "\(graphAction) graphs" : "\(graphAction) graph" : "no graphs" + + let hasObjects: Bool = (tilemap.getObjects().isEmpty == false) + + graphButton.isHidden = !hasGraphs + objectsButton.isEnabled = hasObjects + + let gridButtonTitle = (tilemap.debugDrawOptions.contains(.drawGrid)) ? "hide grid" : "show grid" + let objectsButtonTitle = (hasObjects == true) ? (tilemap.showObjects == true) ? "hide objects" : "show objects" : "show objects" + let effectsButtonTitle = (tilemap.shouldEnableEffects == true) ? "effects: on" : "effects: off" + let updateModeTitle = "mode: \(tilemap.updateMode.name)" + + graphButton.setTitle(graphButtonTitle, for: UIControl.State.normal) + gridButton.setTitle(gridButtonTitle, for: UIControl.State.normal) + objectsButton.setTitle(objectsButtonTitle, for: UIControl.State.normal) + effectsButton.setTitle(effectsButtonTitle, for: UIControl.State.normal) + updateModeButton.setTitle(updateModeTitle, for: UIControl.State.normal) + + setupButtonAttributes() + + // clean up render stats + statsCPULabel.isHidden = false + statsActionsLabel.isHidden = (tilemap.updateMode != .actions) + statsObjectsLabel.isHidden = false + } + + + // MARK: - Debugging + + /** + Updates the render stats debugging info. + + - parameter notification: `Notification` notification. + */ + @objc func renderStatsUpdated(notification: Notification) { + guard let renderStats = notification.object as? SKTilemap.RenderStatistics else { return } + + self.statsHeaderLabel.text = "Rendering: \(TiledGlobals.default.renderer.name)" + self.statsRenderModeLabel.text = "Mode: \(renderStats.updateMode.name)" + self.statsCPULabel.attributedText = renderStats.processorAttributedString + self.statsVisibleLabel.text = "Visible: \(renderStats.visibleCount)" + self.statsVisibleLabel.isHidden = (TiledGlobals.default.enableCameraCallbacks == false) + self.statsObjectsLabel.isHidden = (renderStats.objectsVisible == false) + self.statsObjectsLabel.text = "Objects: \(renderStats.objectCount)" + let renderString = (TiledGlobals.default.timeDisplayMode == .seconds) ? String(format: "%.\(String(6))f", renderStats.renderTime) : String(format: "%.\(String(2))f", renderStats.renderTime.milleseconds) + let timeFormatString = (TiledGlobals.default.timeDisplayMode == .seconds) ? "s" : "ms" + self.statsRenderLabel.text = "Render time: \(renderString)\(timeFormatString)" + + self.statsUpdatedLabel.isHidden = (renderStats.updateMode == .actions) + self.statsUpdatedLabel.text = "Updated: \(renderStats.updatedThisFrame)" + + + // update the effects button (tvOS) + let effectsButtonTitle = (renderStats.effectsEnabled == true) ? "effects: on" : "effects: off" + effectsButton.setTitle(effectsButtonTitle, for: UIControl.State.normal) + } + + /** + Callback when cache is updated. + + - parameter notification: `Notification` notification. + */ + @objc func tilemapUpdateModeChanged(notification: Notification) { + guard let tilemap = notification.object as? SKTilemap else { return } + self.statsRenderModeLabel.text = "Mode: \(tilemap.updateMode.name)" + } +} + + +extension UILabel { + /** + Set the string value of the text field, with optional animated fade. + + - parameter newValue: `String` new text value. + - parameter animated: `Bool` enable fade out effect. + - parameter interval: `TimeInterval` effect length. + */ + func setTextValue(_ newValue: String, animated: Bool = true, interval: TimeInterval = 0.7) { + if animated { + animate(change: { self.text = newValue }, interval: interval) + } else { + text = newValue + } + } + + /** + Private function to animate a fade effect. + + - parameter change: `() -> Void` closure. + - parameter interval: `TimeInterval` effect length. + */ + private func animate(change: @escaping () -> Void, interval: TimeInterval) { + let fadeDuration: TimeInterval = 0.5 + + UIView.animate(withDuration: 0, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: { + self.text = "" + self.alpha = 1.0 + }, completion: { (Bool) -> Void in + change() + UIView.animate(withDuration: fadeDuration, delay: interval, options: UIView.AnimationOptions.curveEaseOut, animations: { + self.alpha = 0.0 + }, completion: nil) + }) + } +} diff --git a/tvOS/Info.plist b/tvOS/Info.plist new file mode 100644 index 00000000..89116aff --- /dev/null +++ b/tvOS/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + SKTiled Demo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.20 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UIUserInterfaceStyle + Automatic + + diff --git a/tvOS/framework/Info.plist b/tvOS/framework/Info.plist new file mode 100644 index 00000000..310264e9 --- /dev/null +++ b/tvOS/framework/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.20 + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/tvOS/framework/SKTiled.h b/tvOS/framework/SKTiled.h new file mode 100644 index 00000000..fc986b2e --- /dev/null +++ b/tvOS/framework/SKTiled.h @@ -0,0 +1,19 @@ +// +// SKTiled.h +// SKTiled +// +// Created by Michael Fessenden on 3/26/18. +// Copyright © 2018 Michael Fessenden. All rights reserved. +// + +#import + +//! Project version number for SKTiled. +FOUNDATION_EXPORT double SKTiledVersionNumber; + +//! Project version string for SKTiled. +FOUNDATION_EXPORT const unsigned char SKTiledVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/zlib/include.h b/zlib/include.h deleted file mode 100644 index 4470a1fd..00000000 --- a/zlib/include.h +++ /dev/null @@ -1 +0,0 @@ -#include diff --git a/zlib/module.modulemap b/zlib/module.modulemap deleted file mode 100644 index d7e3f91d..00000000 --- a/zlib/module.modulemap +++ /dev/null @@ -1,5 +0,0 @@ -module zlib [system] { - header "include.h" - link "z" - export * -}