swiftsprite-kitskphysicsbodysktilemapnode

Connect Physicsbodies on TileMap in SpriteKit


I use the following function to append physicsbodies on tiles from a SKTileMapNode:

 static func addPhysicsBody(to tileMap: SKTileMapNode, and tileInfo: String){

    let tileSize = tileMap.tileSize
    let halfWidth = CGFloat(tileMap.numberOfColumns) / 2 * tileSize.width
    let halfHeight = CGFloat(tileMap.numberOfRows) / 2 * tileSize.height

    for row in 0..<tileMap.numberOfColumns{
        for column in 0..<tileMap.numberOfRows{
            let tileDefinition = tileMap.tileDefinition(atColumn: column, row: row)
            let isCorrectTile = tileDefinition?.userData?[tileInfo] as? Bool
            if isCorrectTile ?? false && tileInfo == "wall"{

                let x = CGFloat(column) * tileSize.width - halfWidth
                let y = CGFloat(row) * tileSize.height - halfHeight

                let tileNode = SKNode()
                tileNode.position = CGPoint(x: x, y: y)
                tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2, y: tileSize.height / 2))
                tileNode.physicsBody!.isDynamic = false
                tileNode.physicsBody!.restitution = 0.0
                tileNode.physicsBody!.categoryBitMask = Constants.PhysicsCategories.wall
                tileNode.physicsBody!.collisionBitMask = Constants.PhysicsCategories.player | Constants.PhysicsCategories.npc | Constants.PhysicsCategories.enemy
                nodesForGraph.append(tileNode)
                tileMap.addChild(tileNode)
            }
        }
    }
}

However if I use this, I have a physicsbody per tile. I want to connect physicsbodies to bigger ones to get a better performance. I know that this can be with init(bodies: [SKPhysicsBody]). But how can I do that?
How can I find out which body is next to another body to group them?
The physicsbodies in the tileMap aren't all next to each other. Some are big blocks of physicsbodies, some are single physicsbodies with no bodies next to them. So I can't simply put every physicsbody in an array and group them.
Here's an image that shows how it looks like at the moment.

TileMap Physics

I hope the explanation is clear enough. If not, I will try to explain it better.

Has anyone done this before and can point me in the right direction? I would appreciate any help.

EDIT: Before I tried this:

static var bodies = [SKPhysicsBody]()
static func addPhysicsBody(to tileMap: SKTileMapNode, and tileInfo: String){

    let tileSize = tileMap.tileSize
    let halfWidth = CGFloat(tileMap.numberOfColumns) / 2 * tileSize.width
    let halfHeight = CGFloat(tileMap.numberOfRows) / 2 * tileSize.height

    for column in 0..<tileMap.numberOfColumns{
        for row in 0..<tileMap.numberOfRows{
            let tileDefinition = tileMap.tileDefinition(atColumn: column, row: row)
            let isCorrectTile = tileDefinition?.userData?[tileInfo] as? Bool
            if isCorrectTile ?? false && tileInfo == "wall"{

                let x = CGFloat(column) * tileSize.width - halfWidth
                let y = CGFloat(row) * tileSize.height - halfHeight

                let tileNode = SKNode()
                tileNode.position = CGPoint(x: x, y: y)
                tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2, y: tileSize.height / 2))
                tileNode.physicsBody!.isDynamic = false
                tileNode.physicsBody!.restitution = 0.0
                tileNode.physicsBody!.categoryBitMask = Constants.PhysicsCategories.wall
                tileNode.physicsBody!.collisionBitMask = Constants.PhysicsCategories.player | Constants.PhysicsCategories.npc | Constants.PhysicsCategories.enemy
                //nodesForGraph.append(tileNode)
                bodies.append(tileNode.physicsBody!)
                tileMap.addChild(tileNode)
            }
        }
    }
    tileMap.physicsBody = SKPhysicsBody(bodies: bodies)
}

But when I do this, the physicsbodies are totally messed up..


Solution

  • I recommend applying a line sweep algorithm to merge the tiles together.

    You can do this in four steps;

    1. Iterate through the position of the tiles in your SKTileMap.

    2. Find the tiles that are adjacent to one another.

    3. For each group of adjacent tiles, collect:

      • a down-left corner coordinate and
      • an up-right corner coordinate.
    4. Draw a square, and move on to the next group of tiles until you run out of tile coordinates.


    The first step: creating an array containing all of your position nodes.

    func tilephysics() {
    
        let tilesize = tileMap.tileSize
        let halfwidth = CGFloat(tileMap.numberOfColumns) / 2.0 * tilesize.width
        let halfheight =  CGFloat(tileMap.numberOfRows) / 2.0 * tilesize.height
    
        for col in 0 ..< tileMap.numberOfColumns {
    
            for row in 0 ..< tileMap.numberOfRows {
    
                if (tileMap.tileDefinition(atColumn: col, row: row)?.userData?.value(forKey: "ground") != nil) {
    
                    let tileDef = tileMap.tileDefinition(atColumn: col, row: row)!
                    let tile = SKSpriteNode()
    
                    let x = round(CGFloat(col) * tilesize.width - halfwidth + (tilesize.width / 2))
                    let y = round(CGFloat(row) * tilesize.height - halfheight + (tilesize.height / 2))
    
                    tile.position = CGPoint(x: x, y: y)
                    tile.size = CGSize(width: tileDef.size.width, height: tileDef.size.height)
    
                    tileArray.append(tile)
                    tilePositionArray.append(tile.position)
                }
            }
        }
        algorithm()
    }
    

    The second and third step: finding adjacent tiles, collecting the two corner coordinates, and adding them to an array:

    var dir = [String]()
    var pLoc = [CGPoint]()
    var adT = [CGPoint]()
    
    func algorithm(){
    
        let width = tileMap.tileSize.width
        let height = tileMap.tileSize.height
        let rWidth = 0.5 * width
        let rHeight = 0.5 * height
    
        var ti:Int = 0
        var ti2:Int = 0
        var id:Int = 0
        var dl:CGPoint = CGPoint(x: 0, y: 0)
    
        var tLE = [CGPoint]()
        var tRE = [CGPoint]()
    
        for t in tilePositionArray {
    
            if (ti-1 < 0) || (tilePositionArray[ti-1].y != tilePositionArray[ti].y - height) {
    
                dl = CGPoint(x: t.x - rWidth, y: t.y - rHeight)
    
            }
    
            if (ti+1 > tilePositionArray.count-1) {
                tLE.append(dl)
    
                tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
    
            } else if (tilePositionArray[ti+1].y != tilePositionArray[ti].y + height) {
    
                if let _ = tRE.first(where: {
    
                    if $0 == CGPoint(x: t.x + rWidth - width, y: t.y + rHeight) {id = tRE.index(of: $0)!}
    
                    return $0 == CGPoint(x: t.x + rWidth - width, y: t.y + rHeight)}) {
    
                    if tLE[id].y == dl.y {
    
                        tRE[id] = CGPoint(x: t.x + rWidth, y: t.y + rHeight)
    
                    } else {
    
                        tLE.append(dl)
    
                        tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
    
                    }
    
                } else {
    
                    tLE.append(dl)
    
                    tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
    
                }
    
            }
    
            ti+=1
    
        }
    

    The fourth step: drawing a rectangle and moving on to the next shape:

    for t in tLE {
    
            let size = CGSize(width: abs(t.x - tRE[ti2].x), height: abs(t.y - tRE[ti2].y))
            let loadnode = SKNode()
    
            loadnode.physicsBody = SKPhysicsBody(rectangleOf: size)
            loadnode.physicsBody?.isDynamic = false
            loadnode.physicsBody?.affectedByGravity = false
            loadnode.physicsBody?.restitution = 0
    
            loadnode.physicsBody?.categoryBitMask = 2
    
            loadnode.position.x = t.x + size.width / 2
            loadnode.position.y = t.y + size.height / 2
    
            scene.addChild(loadnode)
    
            ti2 += 1
    
        }
    }
    

    Apply these steps correctly, and you should see that your tiles are merged together in large squares; like so:

    Screenshot without visuals for comparison

    Screenshot without visuals showing the physicsbodies

    I had a lot of fun solving this problem. If I have helped you, let me know. I only recently started coding and am looking for new challenges. Please reach out to me if you have challenges or projects I could possibly contribute to.