Configure Physics Bodies Using User Data

To keep the player from leaving the dungeon, you’ll set up physics bodies around the dungeon walls. However, there’s a small problem: tile map nodes do not expose their tiles as nodes, which means you can’t assign physics bodies to the individual tiles. Instead, you’ll use custom user data, more specifically, the userData property of the SKNode class.

The userData property of the SKNode class is an NSMutableDictionary that you can use to store custom values known as “user data.” These custom values can include integers, floats, booleans, and strings. You can either set these up in code or, more conveniently, using the Scene Editor and the Attributes Inspector (there’s a User Data section near the bottom).

For Val’s Revenge, you’ll set up two custom values:

  • bodySize: Indicates the size of the physics body to use.
  • bodyOffset: Specifies the offset to use for the body.

The question now is, “Where do you set up these values and how does it all work?”

Working with User Data and Tiles

In the Project Navigator, select the MainTileSet.sks file. With the Attributes Inspector open, expand the Left Edge tile group rule and select the left_edge tile definition inside.

At the bottom of the Attributes Inspector, you’ll see the User Data section, which looks like this:

images/ExtendingYourGameWorldWithTileMaps/scene-editor-user-data.png

Click the + button in the lower-left corner to add a new entry. Set the Name to bodySize and switch the Type to String. For the Value, enter {20,128}.

With this user data entry, you’re specifying that the body size will be (w: 20, h: 128). So why are you setting a body size?

To better understand, look at the following image:

images/ExtendingYourGameWorldWithTileMaps/SceneEditor-TilePhysicsBody.png

Notice that each tile is 128 × 128—including the alpha area of the tile. If you set the physics body’s size using the full size of the tile, the player wouldn’t be able to get close to the wall, as you can see in the image on the left. However, if you reduce the physics body size to 20 × 128, the player can get closer to the wall, as shown in the image on the right. Remember, when you add a physics body to a node, it defaults to placing that body in the center of the node. So when you resize the body, it’ll resize from both directions, keeping the body in the center of the node.

Now the left_edge tile definition is set up, select the right_edge tile definition inside the Right Edge tile group rule, and add a new user data entry of bodySize, String, {20,128}. Once again, you’re specifying the body size will be (w: 20, h: 128).

The left and right edges are now set up. Next, you need to set up the top and bottom edges. You’ll use essentially the same settings, but instead of specifying a body size of (w: 20, h: 128), you need to use a size of (w: 128, h: 20), which will shrink the top and bottom of the physics body.

Select the up_edge tile definition inside the Up Edge tile group rule. This time, add a new user data entry of bodySize, String, {128,20}. Do the same for the down_edge tile definition inside of the Down Edge tile group rule.

For the four corner edges, you’ll create a square body instead. Individually select the upper_right_edge, lower_right_edge, lower_left_edge, and upper_left_edge tile definitions, and for each one, add a new user data entry of bodySize, String, {84,84}.

The main outside dungeon walls are now set up. But there’s still a matter of the inner walls, more specifically, the upper_right_corner, lower_right_corner, lower_left_corner, and upper_left_corner.

Remember, when you add a physics body to a node, it places that body in the center of the node. For these inner corner pieces, you’ll need to use an offset. To understand why, look at the following image:

images/ExtendingYourGameWorldWithTileMaps/SceneEditor-TilePhysicsBodyOffset.png

Notice that without the offset, the player wouldn’t be able to step onto the floor. But this is where it might get a little complicated because you need to move the offset up or down and left or right depending on which tile you’re setting up. For example, with the Lower Right Corner, you need to move the offset up and over to the left using (x: -32, y: 32).

First, to make it easier, add a bodySize user data string value of {64,64} for the four corners. Then, go back and add a new bodyOffset string value for each one, taking special note of which values are positive and which are negative:

  • upper_right_corner: {-32,-32}
  • lower_right_corner: {-32,32}
  • lower_left_corner: {32,32}
  • upper_left_corner: {32,-32}

Excellent, you now have the user data set up for your tile definitions, and you’re ready to write the code to read that data.

Setting up the Physics Body Code

Setting up the user data is only the first step in getting physics bodies added to the tile map. The next step is to write some code to read that data. While there are many ways to read the data and write the code, you’ll create a new SKTileMapNode extension to keep things clean and reusable.

Create a new file (N) using the iOS Swift File template. Name the new file SKTileMapNode+Physics.swift and replace its contents with the following code:

 import​ ​SpriteKit
 
 extension​ ​SKTileMapNode​ {
 
 }

This sets up a new extension for the SKTileMapNode class. Inside this new extension, you’ll add two new methods:

 func​ ​setupEdgeLoop​() {
 let​ mapPoint = ​CGPoint​(x: -frame.size.width/2,
  y: -frame.size.height/2)
 let​ mapSize = ​CGSize​(width: frame.size.width,
  height: frame.size.height)
 let​ edgeLoopRect = ​CGRect​(origin: mapPoint, size: mapSize)
 
 // Set up physics body
  physicsBody = ​SKPhysicsBody​(edgeLoopFrom: edgeLoopRect)
 // Adjust default values
  physicsBody?.affectedByGravity = ​false
  physicsBody?.allowsRotation = ​false
  physicsBody?.isDynamic = ​false
 
  physicsBody?.categoryBitMask = 2
 }

This first extension method, when called, creates an edge-loop physics body around the tile map node using a value of 2 for the categoryBitMask property. You first learned about physics categories and masks in Configure Physics Categories. The difference here is that instead of using an enum, you’re setting this property using a raw value of 2.

Hardcoding the Default Category Value

images/aside-icons/warning.png

For simplicity, the categoryBitMask property is using a hardcoded default value of 2.

Below the method you just added, add the following code:

 func​ ​setupMapPhysics​() {
 let​ halfWidth = ​CGFloat​(numberOfColumns) / 2.0 * tileSize.width
 let​ halfHeight = ​CGFloat​(numberOfRows) / 2.0 * tileSize.height
 
 for​ col ​in​ 0..<numberOfColumns {
 for​ row ​in​ 0..<numberOfRows {
 if​ ​let​ td = ​tileDefinition​(atColumn: col, row: row) {
 
 if​ ​let​ bodySizeValue = td.userData?.​value​(forKey: ​"bodySize"​)
 as?​ ​String​ {
 
 let​ x = ​CGFloat​(col) *
  tileSize.width - halfWidth + (tileSize.width/2)
 let​ y = ​CGFloat​(row) *
  tileSize.height - halfHeight + (tileSize.height/2)
 
 let​ bodySize = ​NSCoder​.​cgPoint​(for: bodySizeValue) ​// {x,y}
 let​ pSize = ​CGSize​(width: bodySize.x, height: bodySize.y)
 
 let​ tileNode = ​SKNode​()
  tileNode.position = ​CGPoint​(x: x, y: y)
 
 if​ ​let​ bodyOffsetValue = td.userData?.​value​(forKey: ​"bodyOffset"​)
 as?​ ​String​ {
 
 let​ bodyOffset = ​NSCoder​.​cgPoint​(for: bodyOffsetValue)
  tileNode.physicsBody =
 SKPhysicsBody​(rectangleOf: pSize,
  center: ​CGPoint​(x: bodyOffset.x, y: bodyOffset.y))
  } ​else​ {
  tileNode.physicsBody = ​SKPhysicsBody​(rectangleOf: pSize)
  }
 
 // Adjust default values
  tileNode.physicsBody?.affectedByGravity = ​false
  tileNode.physicsBody?.allowsRotation = ​false
  tileNode.physicsBody?.isDynamic = ​false
 
  tileNode.physicsBody?.categoryBitMask = 2
 
 // Add node
 addChild​(tileNode)
  }
  }
  }
  }
 }

There’s a lot going on with this code, so take a moment to read through it. It’s okay if you don’t understand it fully. The key takeaways include:

  • Looping through the tile definitions using the numberOfColumns and numberOfRows properties of the SKTileMapNode class.

  • Reading the values stored in the userData property of the tileDefinition at the specified row and column.

  • Using the bodySize and bodyOffset values to set the physics body properties.
    • If the bodySize value is not present, no action is taken.
    • If the bodySize value is present, the bodyOffset and bodyCategory values are read, along with the bodySize value, and all three are used to create the physics body.

Now that you have the extension methods in place, you’re ready to use them to read the user data for each tile definition and set up the corresponding physics body.

In the Project Navigator, select the GameScene.swift file. Jump to the didMove(to:) method and add the following code:

 let​ grassMapNode = ​childNode​(withName: ​"Grass Tile Map"​) ​as?​ ​SKTileMapNode
 grassMapNode?.​setupEdgeLoop​()
 
 let​ dungeonMapNode = ​childNode​(withName: ​"Dungeon Tile Map"​) ​as?​ ​SKTileMapNode
 dungeonMapNode?.​setupMapPhysics​()

This code uses optional chaining to execute the extension methods on the two tile map nodes.

The last thing to do is update the player node’s collision mask so that it reacts when hitting one of the wall bodies.

Updating the Player’s Collision Mask

Open the GameScene.sks and select the player node. In the Attributes Inspector, set the Collision Mask value to 2, which is the value you used for the categoryBitMask property of your tile map node bodies.

Build and run the project, and notice how the player reacts to the dungeon’s walls. Also notice the animated flames that you added earlier.

images/ExtendingYourGameWorldWithTileMaps/scene-editor-tile-map-build_3.png

Thanks to tile maps, you now have a bigger world for your player to explore—and thanks to user data, you have a way to identify tiles and set up physics bodies, making it possible for the player to step right up to the edge of a wall but not go through it. Pretty neat, right?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset