Connect Physicsbodies on TileMap in SpriteKit
Asked Answered
I

2

1

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..

Impresario answered 5/12, 2017 at 2:16 Comment(10)
Tilemaps and physics bodies is a problem Apple should have solved for SpriteKit users. I'm sorry you have to even consider this problem.Foretopgallant
You can always iterate through the tiles and merge bodies together. What you are saying though technically does not make sense. You cannot have 1 body pointing to 2 nodes, so you are going to have to create a new node to be able to support the merged tiles. perhaps Tilemapping is not the solution to your problemCrumpled
@Crumpled unfortunately I have less experience in SpriteKit and don't really understand what you mean. Could you give a short example please? I edited my questions. This is what I tried before. I need the Tilemapping to build my levels, because I try to build a top down RPG.Impresario
if you are building a top down RPG, then do not even use the physics engine, it would be wasteful. Just do a bounding box check, node.intersecsts(node)Crumpled
@Crumpled I need the physicsbodies for the Pathfinding of my enemies. The Pathfinding of GameplayKit needs physicsbodies to detect them as obstacles.Impresario
@Marcel, ok, well then it gets ugly. Basically you step through every tile like you are doing now to create the node. When you hit a wall, you hit another routine where you check the neighboring tiles to make sure if they are walls too, then you check those neighbors, and those neighbors neighbors. You eventually make a bucket fill function ala MSPaint. How do you create your tilemaps now?Crumpled
I create the tilemaps with the Spritekit Tilemap Editor. I just choose the tiles from a tileset and arrange them on the grid. Then I give the wall tiles via userData an identifier and a boolean value.Impresario
Perfect, use the scene editor to create nodes that will handle your physics body, then you overlay this new scene on top of your tilemap. No code will be needed at all, so you can get rid of your for loops. Basically, you have your tilemap, then you have a childnode that contains the nodes used for physicsCrumpled
I tried this before and this was working great. The only problem is that I have to do this manually for every level. I was hoping there's maybe a way to use a function which assign the physicsbodies automatically like the function I mentioned above and additionally group the bodies which are next to each other. But if this would get complicated, I will go with your solution and do it manually. Thanks for the help!Impresario
@Marcel, yeah unfortunately there is no simple solution, Like I said, you can always build the flood fill function, but it will increase your loading time. You can always test the texture init that physics body has, and make the walkable tiles invisible when extracting the texture, but the last time I used that, it did not correctly make bodies when there was a gapCrumpled
C
3

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.

Clotilde answered 14/6, 2019 at 18:42 Comment(1)
Ramon123, thanks, this is really great. Just implemented it and it looks super nice!Anthropogeography
M
1

As Knight0fDragon pointed out, there is no way to do exactly what you have asked. Unfortunately, tile maps in SpriteKit leave much to be desired. But you might try this technique to reduce the number of physics bodies.

Idea #1 - Manually Draw Your Physics Bodies

Create your tile map in the editor. Just paint your tile textures onto the map; don't assign any physics bodies to them. Then keep working in the editor to drag Color Sprites (SKSpriteNodes) over parts of your map that need a physics body. Shape the nodes to make the largest rectangle possible for areas that need physics bodies. This works best for for large, flat surfaces like walls, floors, ceilings, platforms, crates, etc. It's tedious but you end up with far fewer physics bodies in your simulation than if you automatically assign bodies to all tiles like you are doing.

Idea #2 - Use No Physics Bodies

This idea would probably require even more work, but you could potentially avoid using physics bodies altogether. First, create your tile map in the editor. Analyze your map to identify which tiles mark a barrier, beyond which the player should not cross. Assign a user data identifier to that type of tile. You would need different categories of identifiers for different types of barriers, and you may also need to design your artwork to fit this approach.

Once your barrier tiles are sufficiently identified, write code which checks the user data value for the tile currently occupied by the player sprite and restrict the sprite's movement accordingly. For example, if the player enters a title that marks an upper boundary, your movement code would not allow the player sprite to move up. Likewise, if the player enters a tile that marks the leftmost boundary, your movement code will not let the player travel left.

You can check out this related post where I basically suggest the same ideas. Unfortunately, SpriteKit's tile maps have no perfect solution for this problem.

Millennium answered 20/3, 2018 at 23:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.