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

1

8

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?

Spandau answered 27/12, 2016 at 21:8 Comment(4)
How about some for loops? :)Neolatin
@jtbandes, That is the solution that first comes to mind, but since I need to have a variable for each tile, and specific row and column for each tile, I am not sure how to go about creating the loop.Spandau
Why do you need a variable for each tile? Can't you store them in an array or dictionary?Neolatin
@Neolatin I will give it a try, thanks!Spandau
M
11

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.

Meandrous answered 2/1, 2017 at 23:18 Comment(1)
This seems like a good way to go about it. I am trying to avoid the use of all Xcode GUI tools where possible.Spandau

© 2022 - 2024 — McMap. All rights reserved.