Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the proper way to programmatically create a SpriteKit SKTileMap?

I am creating a tile map for an iOS game I am working on. The map is a top down view of an island. Most of the tiles are water, but some are land. The water tile is reused to create the water, but none of the land tiles are used more than once, because all of the land tiles are unique. I have looked through the docs for SKTileDefinition, SKTileGroup, SKTileGroupRule, SKTileSet, and SKTileMap, and this is what I came up with:

func createTileMap() {
    let waterTile = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-1"))
    let waterTileGroup = SKTileGroup(tileDefinition: waterTile)

    let landTile64 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-64"))
    let landTile63 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-63"))
    let landTile56 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-56"))
    let landTile55 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-55"))
    let landTile54 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-54"))
    let landTile53 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-53"))
    let landTile48 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-48"))
    let landTile47 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-47"))
    let landTile46 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-46"))
    let landTile45 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-45"))
    let landTile44 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-44"))
    let landTile43 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-43"))
    let landTile40 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-40"))
    let landTile39 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-39"))
    let landTile37 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-37"))
    let landTile36 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-36"))
    let landTile35 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-35"))
    let landTile34 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-34"))
    let landTile31 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-31"))
    let landTile30 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-30"))
    let landTile27 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-27"))
    let landTile26 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-26"))
    let landTile25 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-25"))
    let landTile23 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-23"))
    let landTile22 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-22"))
    let landTile21 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-21"))
    let landTile20 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-20"))
    let landTile18 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-18"))
    let landTile17 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-17"))
    let landTile13 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-13"))
    let landTile12 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-12"))
    let landTile11 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-11"))
    let landTile3 = SKTileDefinition(texture: SKTexture(imageNamed: "map-tile-3"))

    let landTileGroupRule = SKTileGroupRule(adjacency: .adjacencyAll, tileDefinitions: [landTile64, landTile63, landTile56, landTile55, landTile54, landTile53, landTile48, landTile47, landTile46, landTile45, landTile44, landTile43, landTile40, landTile39, landTile37, landTile36, landTile35, landTile34, landTile31, landTile30, landTile27, landTile26, landTile25, landTile23, landTile22, landTile21, landTile20, landTile18, landTile17, landTile13, landTile12, landTile11, landTile3])

    let landTileGroup = SKTileGroup(rules: [landTileGroupRule])

    let tileSet = SKTileSet(tileGroups: [waterTileGroup, landTileGroup], tileSetType: .grid)

    tileMap = SKTileMapNode(tileSet: tileSet, columns: 8, rows: 8, tileSize: waterTile.size)
    tileMap.fill(with: waterTileGroup)

    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile64, forColumn: 7, row: 0)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile63, forColumn: 6, row: 0)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile56, forColumn: 7, row: 1)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile55, forColumn: 6, row: 1)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile54, forColumn: 5, row: 1)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile53, forColumn: 4, row: 1)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile48, forColumn: 7, row: 2)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile47, forColumn: 6, row: 2)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile46, forColumn: 5, row: 2)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile45, forColumn: 4, row: 2)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile44, forColumn: 3, row: 2)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile43, forColumn: 2, row: 2)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile40, forColumn: 7, row: 3)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile39, forColumn: 6, row: 3)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile37, forColumn: 4, row: 3)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile36, forColumn: 3, row: 3)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile35, forColumn: 2, row: 3)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile34, forColumn: 1, row: 3)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile31, forColumn: 6, row: 4)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile30, forColumn: 5, row: 4)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile27, forColumn: 2, row: 4)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile26, forColumn: 1, row: 4)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile25, forColumn: 0, row: 4)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile23, forColumn: 6, row: 5)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile22, forColumn: 5, row: 5)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile21, forColumn: 4, row: 5)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile20, forColumn: 3, row: 5)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile18, forColumn: 1, row: 5)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile17, forColumn: 0, row: 5)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile13, forColumn: 4, row: 6)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile12, forColumn: 3, row: 6)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile11, forColumn: 2, row: 6)
    tileMap.setTileGroup(landTileGroup, andTileDefinition: landTile3, forColumn: 2, row: 7)

    self.addChild(tileMap)
}

Now, this is pretty gross code, but I am not sure where I can go from here. I am loading each land tile, and then explicitly telling it which column and row it should go in. Any tiles that are not explicitly laid out are water tiles. I would like to avoid using any of Xcode's GUI tools for this, and create the map entirely programmatically. This code works, and it improved performance (I was initially just scrolling over the entire map image, not using tiles), it's just very ugly and screams "There must be a more concise way of doing this!". What am I missing here?

like image 402
SpacemanDeMars Avatar asked Dec 27 '16 21:12

SpacemanDeMars


1 Answers

One approach is to define the map layout in a text file with columns and rows that specify which tile to place at specific locations. Then it will be possible to specify a different map for each game level, while still using the same scene. This example assumes there is only one SKTileSet and flood filling the background is not required.

// Level1.txt
01 01 01 01 01 01 63 64
01 01 01 01 53 54 55 56
01 01 43 44 45 46 47 48
01 34 35 36 37 01 39 40
25 26 27 01 01 30 31 01
17 18 01 20 21 22 23 01
01 01 11 12 13 01 01 01
01 01 03 01 01 01 01 01

Iterate over each column and row, retrieve the specified tile and call setTileGroup.

let path = Bundle.main.path(forResource: "Level1.txt", ofType: nil)
do {
    let fileContents = try String(contentsOfFile:path!, encoding: String.Encoding.utf8)
    let lines = fileContents.components(separatedBy: "\n")

    for row in 0..<lines.count {
        let items = lines[row].components(separatedBy: " ")

        for column in 0..<items.count {
            let tile = tileMap.tileSet.tileGroups.first(where: {$0.name == "map-tile-" + items[column]})
            tileMap.setTileGroup(tile, forColumn: column, row: row)
        }
    }
} catch {
    print("Error loading map")
}

This eliminates the second block of code in your example. If you want to compromise a bit on using Xcode GUI Tools, then the first block of code could be eliminated by creating the SKTileGroup as an sks file.

I would recommend using the GUI Tools when working with SKTileMapNode though, since they provide a lot of functionality.

like image 140
Mark Brownsword Avatar answered Oct 09 '22 14:10

Mark Brownsword