parse and use Spritesheet.xml

使用SpriteKit寫遊戲需要用到game asset [1, 2] 。從Kenney [2] 的網站下載的檔案通常會有一個類似xcode的.atlas的東東放在Spritesheet的目錄下,還有一個描述圖集名稱與位置的.xml。內容像是這樣的:
  

這個.xml描述了這張圖 [2]

注意的是獨角鯨(narwhal.png)所在(0, 0)的位置,是在圖片的左上,而SpriteKit描述圖片的座標原點是在左下角。知道座標的關係後,從圖集裡挖出特定動物就比較直覺了。

首先解析.xml格式,把這張.xml 表格轉換成 [TextureModel],TextureModel只是很單純的struct:

  import SpriteKit
import UIKit

struct TextureModel: Identifiable {
    var id: String {
        name
    }
    let name: String
    let position: CGPoint
    let size: CGSize
    var uiImage: UIImage? = nil
}
滿足Identifiable只是方便之後在SwiftUI使用。解析.xml 的過程參考 [3]。我把處理的過
程包成一個class ParseTextureAtlas [4]。完整程式碼如下:

  //
//  ParseTextureAtlas.swift
//  ParseSpriteSheet
//
//  Created by hhwang on 2022/9/4.
//

import SwiftUI
import SpriteKit

class ParseTextureAtlas: NSObject, ObservableObject, XMLParserDelegate {
    @Published var items = [TextureModel]()
    var tagName: String? = nil
    var imagePath: String? = nil
    
    
    func parseTextureAtlas(_ filename: String) {
        if let url = Bundle.main.url(forResource: filename, withExtension: "xml") {
            if let xml = XMLParser(contentsOf: url) {
                xml.delegate = self
                xml.parse()
            }
        }
        if let imagePath = imagePath {
            getItemTexture(from: imagePath)
        }
    }
    
    func getItemTexture(from imagePath: String) {
        let textureAtlas = SKTexture(imageNamed: imagePath)  //load the textureAtlas from the imagePath
        let width = (textureAtlas.size().width) // get the width (in pixel) of the atlas; for normalization
        let height = (textureAtlas.size().height) // get the height (in pixel) of the atlas; for normalization
        
        
        
        for item in items {
            // textureRect: the crop area in the textureAtlas for a specific item/sprite
            // textureRect is using normalized coordinates between (0, 0) - (1, 1), with the origin located at the bottom-left corner
            // the origin of the textureAtlas is NOT normalized (in pixel), and located at the top-left corner.
            
            let textureRect = CGRect(x: item.position.x/width, y: 1-item.position.y/height - item.size.height / height , width: item.size.width/width, height: item.size.height / height)
            
            DispatchQueue.main.async { [unowned self] in
                if let index = self.items.firstIndex(where: {$0.name == item.name}) {
                    // crop the area and turn into an UIImage
                    self.items[index].uiImage = UIImage(cgImage: SKTexture(rect: textureRect, in: textureAtlas).cgImage())
                    
                }
            }
        }
        
    }
    
    
    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        switch elementName {
        case "TextureAtlas":
            for key in attributeDict.keys {
                if let value = attributeDict[key] {
//                    print("\(key): \(value)")
                    imagePath = value
                    tagName = key
                }
            }
        case "SubTexture":
            tagName = "SubTexture"
            var x = 0
            var y = 0
            var width = 0
            var height = 0
            var name: String = ""
            for key in attributeDict.keys {
                switch key {
                case "name":
                    name = attributeDict[key]!.components(separatedBy: ".")[0]
                case "x":
                    x = Int(attributeDict[key]!)!
                case "y":
                    y = Int(attributeDict[key]!)!
                case "width":
                    width = Int(attributeDict[key]!)!
                case "height":
                    height = Int(attributeDict[key]!)!
                default:
                    break
                }
            }
            
            let item = TextureModel(name: name, position: CGPoint(x: x, y: y), size: CGSize(width: width, height: height))
            items.append(item)
            
//            print("\(tagName!): \(item)")
            
        default:
            tagName = nil
            break
        }
    }
    
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        guard tagName != nil else { return }
//        print("\(tagName!) : \(string)")
    }
    
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        tagName = nil
    }
}
  
比較值得注意的是這個函式 func getItemTexture(from:) 裡面用到的座標轉換。最
後在ContentView.swift裡使用這個class:

  
  //
//  ContentView.swift
//  ParseSpriteSheet
//
//  Created by hhwang on 2022/9/4.
//

import SwiftUI
import SpriteKit

struct ContentView: View {
    @EnvironmentObject var textureParser: ParseTextureAtlas

    
    var body: some View {
       
        List {
            ForEach(textureParser.items) { item in
                if item.uiImage != nil {
                    HStack {
                        Image(uiImage: item.uiImage!)
                            .resizable()
                            .scaledToFit()
                        Text(item.name)
                    }
                    .frame(height: 150)
                }
            }
        }
        .onAppear {
            textureParser.parseTextureAtlas("round_nodetails")
        }
        
    }
}
成果截圖:


References: 


Comments

Popular Posts