Giving physics to tiles of SKTileMapNode in Xcode 8
Asked Answered
G

4

7

I am learning Swift, and as a project I am working on a tile based 2D game similar to super mario where my character will walk and jump on tiles.

The latest version of Xcode and Sprite Kit give the ability to create a Tile Map directly in Xcode.

In the presentation of the new Xcode and Sprite kit, the guy demonstrates a game similar to what i am working on.

https://developer.apple.com/videos/play/wwdc2016/610/ (around the 20th minute).

He mentions giving Tiles user data properties which i did, and in code we search through all the tiles which have that user data and give them some physics properties so that the character can collide or interact with them (in my case, my character not falling or walking through the tiles).

so basically, the idea is giving those tiles a physics Body, but this can't be done using SKphysicsBody. So there must be another way, and since i am new to Swift i am missing it.

if anyone knows this, i would very much appreciate the help.

If the question is unclear let me know because i am also new to stack overflow.

Grekin answered 4/10, 2016 at 2:25 Comment(0)
S
6

Apple staff member Bobjt says here that "the right approach" is to add user data to your SKTileDefinition objects and use that to identify and add physics bodies to your tiles.

So you would add a user data value to a tile definition either programmatically or in the editor, like so:

enter image description here

Then in code you would check each tile definition to see if it has the user data value. If so, then you need to calculate the tile's position and add a physics body on a new node, parenting it to your tile map. Here is the code for this which Bobjt referred to as "the correct approach":

self.tileMap = self.childNode(withName: "Tile Map") as? SKTileMapNode  
guard let tileMap = self.tileMap else { fatalError("Missing tile map for the level") }  

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 {  
        let tileDefinition = tileMap.tileDefinition(atColumn: col, row: row)  
        let isEdgeTile = tileDefinition?.userData?["edgeTile"] as? Bool  
        if (isEdgeTile ?? false) {  
            let x = CGFloat(col) * tileSize.width - halfWidth  
            let y = CGFloat(row) * tileSize.height - halfHeight  
            let rect = CGRect(x: 0, y: 0, width: tileSize.width, height: tileSize.height)  
            let tileNode = SKShapeNode(rect: rect)  
            tileNode.position = CGPoint(x: x, y: y)  
            tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0))  
            tileNode.physicsBody?.isDynamic = false  
            tileNode.physicsBody?.collisionBitMask = playerCollisionMask | wallCollisionMask  
            tileNode.physicsBody?.categoryBitMask = wallCollisionMask  
            tileMap.addChild(tileNode)  
        }  
    }  
}  

Personally, I think this approach is too fussy. I'm going to try a different approach in my game and if it works I'll post it here. What I'd really like is for Apple to enhance the tile map API so that we can add physics bodies directly to individual tiles. Maybe in the process they could optimize the engine so that physics bodies on individual tiles would be automatically merged to form larger, more optimal shapes in order to improve system performance.

Update: I filed a request with Apple about this issue, for whatever good might come of it.

Smitt answered 15/3, 2018 at 1:14 Comment(3)
WELL DONE!!! Great research, and congrats on getting information out of an Apple-insider. I can't thank you enough. The lack of transparency into the thinking and design directions of the game kits is unbelievably off-putting. As are the lack of meaningful efforts to fix bugs and bring any sense of cohesion, ease of use and elegance to the processes and paradigms of these things. They should be a source of constant embarrassment to a company with this degree of power and resources.Androgyne
Good luck in your trials and tribulations on the path to making yet more workarounds for the clusterfark that is SK.Androgyne
@Androgyne Thanks for the kudos and good wishes! :) I will definitely post a better solution if I find one.Smitt
A
2

I'm not sure there's a surefire way to do this yet... but here's two ways to think about how to try apply physics to a tilemap.

Option 1: Apply SKNodes to each positions of each tile on your map, and apply an appropriate physicsbody to that SKNode based on the content and state of that tile.

Option 2: Use the position information of each tile to add an array of physicsbodies to the SKTileMapNode, and position each of them accordingly.

I'm imagining a gravity down, Mario style platformer, with this kind of terrain in need of physics bodies for the ground:

enter image description here

Lifted transcript from Apple's WWDC about tiles and physics:

And you'll note that I'm colliding with the tiles here.

And I achieve this by leveraging custom user data that we can put on each of our tiles.

Here, I'll show you in our tile set.

Select one of the variants here.

And you can see that we have some user data over here.

And I just have a value called edgeTile which is a Boolean, and I set to 1.

So, in code, I'm going through the tile map in our platform demo here, and I'm looking for all of these edge tiles.

And whenever I find one, I create some physics data to allow the player to collide with it.

And since it is just in our tile map here, say I wanted to get over this large wall here.

When I run the game, you'll see that my guy can't actually jump high enough to get over it.

And he really wants to because that red button over there looks really tempting.

I really want to push that thing.

So, since we're just generating our physics data from our tiles and our user data, all we can do is just going here and erase these tiles and build and run our game again.

The tiles are now gone and we can now move through there.

And we didn't have to change code or anything.

We just used the data that we pulled from the tile map to set up our tile.

It's that simple.


Makes it sound very simple, but takes a far greater mind than mine to figure out what is being done, and how it's being done.

Androgyne answered 4/10, 2016 at 18:35 Comment(83)
I am going to comment here instead of answering, Giving Physics Bodies to individual tiles do not make a lot of sense, you end up adding more bodies then needed, really hurting the engine. In the example here, you would need 11 bodies that have to be checked, 4 of which are a waste due to their location. I believe the speaker( I havent seen this yet) was trying to say was to use the user data to construct your own physics bodies where needed, kind of like what this person is doing. forums.developer.apple.com/thread/50043Linkboy
For a "Mario" character, is this kind of physics overhead an issue?Androgyne
absolutely, you will have tiles all over the place depending on how large the level isLinkboy
so add and remove physics nodes based on the tiles onscreen at any time?Androgyne
no, you want it to be automatic, can't work that way, the idea is to share resources. So you remove the body from 1 brick, you remove it from allLinkboy
What? These are tiles. Each is individually addressable thanks to userData, right? And each has a unique location, which is discoverable/ascertainable.Androgyne
alternatively, change the bitmasks.... for those offscreen, so they're not calculated, but I don't really understand this category masking thing, yet.Androgyne
userData is just an NSMutableDictionary, we went over that in another question. The idea is to just give it identities, I am not sure 100%, but I do not think the user data targets the individual cell, I thought it is for the shared SKNode that is used on the mapLinkboy
If I have 1 Grass tile located in 2 cells, could I give them 2 different sets of userData?Linkboy
Yeah... I'm not sure, either, if each indexable tile position can have its own custom data/identity.Androgyne
yeah so if that is the case, a body for 1 is a body for allLinkboy
The only way I can see to do this, is the use of iteration over locations: developer.apple.com/reference/spritekit/sktilemapnode/…Androgyne
At any given time, it's possible to determine which columns and rows of an SKTileMapNode are on the screen. For those not, the physics body can be removed. For those that are coming onto the screen, based on direction of travel, etc, the SKTile Definition needs to be gotten, and an appropriate physics body added/activated.Androgyne
It's quite extraordinary that no mechanism for this kind of physics handling was designed and made by Apple as part of SKTileMapNode. I think.Androgyne
eh I would think that Gameplay kit should be doing that kind of stuff anywayLinkboy
See additional content in my "answer" above... @LinkboyAndrogyne
he starts his post by saying he'd also like to know how it was done, probably meaning he doesn't think his code is in anyway similar to what he's seen done in the Apple WWDC demo. Do you think they're the same thing?Androgyne
Something very similar to itLinkboy
wow, there is no way to enumerate over all the definitions, that is lameLinkboy
That, too. I'm shocked there's no way to have each tile position have a unique ID of some sort. I mean... it's possible to make a 2D array for each tile position, I suppose, but it seems strange that that's not part of SKTileMapNode.Androgyne
The whole thing feels half baked. Like "quick, let's make tile mapping, cause that's cool, right? Right!"Androgyne
you would think that this could be used with .map, go over your Tilegroup (I am guessing this is the index on the tile map) and set features based on userData (I am guessing TileDefinition is the hacked up SKNode)Linkboy
From looking at it, I do not see anything special about this yet. They are doing almost what I do with tile engines, I am just curious if they can change the texture faster then I can (due to not having low level access)Linkboy
No, it's weird, there is no Tile location ID other than the column/row. TileGroup is the holder for TileDefinitions that only define what's possible in terms of texture for any given tile, nothing to do with location. So it's TileNode>TileSet>TileGroup>TileDefinition in which a TileGroup can contain multiple TileDefinitionsAndrogyne
It's almost as though they're waiting for someone to show them how to best address individual tile locations within the SKTileMapNode, at which point they'll copy that idea and release it in version 2, probably for iOS 11.Androgyne
trying to understand what tile set isLinkboy
ok I think I got it, tileSet I think is the texture data of the tiles tileDefinition is the SKNode data of the tiles, and tileGroup is the index location of the map. It makes sense that a tile group hold multiple definitions for animationLinkboy
tileGroup doesn't seem to have any information about location within the map. Individual tileDefinitions are applied to locations within the map. The tileGroup is only a container for definitions. I haven't been able to find an index-like feature that knows which tileDefinition is at any location of the map.Androgyne
why would it, it is an index, not a cellLinkboy
I mean to say it would seem logical that there would be some kind of index of all cells, somewhere... but there doesn't seem to be...Androgyne
huh, that doesn't make senseLinkboy
Ok. Think about this.... how do you address a tile at any given location in a tile map? How do you think that would be best done? How do you discover what tileDefinition is currently at that location, so you can add the appropriate physics body?Androgyne
if we were doing it like an array, [1,2,3,3,2,1], you poll the map for cell (2,0) that tells you to check group 2. In group 2 is where the magic happens I would thinkLinkboy
No, it's in the tileDefinition that you find what you need ie the used texture at this location. row/column, ask tileDefinition which texture it's showing. A tileDefinition can show more than one texture, and can rotate it, too.Androgyne
You get the tile definition from the groupLinkboy
if you change the definitions inside group, then in my example, BOTH 3 should changeLinkboy
You get the tile definition from the group, which you get from the Set, which you get from the Node... yes... but the bigger question, how do you get the texture that's being shown at any given location?Androgyne
mySKTileMapNode.mySKTileSet.mySKTileGroup.mySKTileDefinition.texture(at: column/row) or something like that...Androgyne
The texture is accessible from definition, same way you get it from a spritenode, the only difference is animation is given to you automatically, so it has to be textures not textureLinkboy
argh, I think I got it... a tileDefinition only really has one possible texture, not multiples. I was confused by the plurality in the docs. It's not multiple textures, it's multiple frames of a possible single texture, which would have a rate of playback, but still constitute one single texture, just with multiple frames, as per an "animation". Want a different texture? Need another tileDefinition. So any single tileDefinition only has one known "texture", but this could be a frame-sequence animation rather than just a still image.Androgyne
I'll promptly forget this. The above, but the good news is that if you know what the tileDefinition is at any location, then you know what texture is being used in this location, but may not yet know which frame, if it's a frame-sequence animated texture.Androgyne
exactly, that is why group allows multipleLinkboy
I'm going to change my username to slow-confused-forgetful-ignorantAndrogyne
I am going to guess tileset is kind of like a texture atlas, but is treated like a cached VRAM they are constantly writing to this during the animation steps, and this is how ALL nodes animate, instead of changing the reference to all nodes. Used to do this when I did Nintendo DS developmentLinkboy
I think I understand you. This makes it optimal for large numbers of frame-sequences to run at different times, perhaps. I think. Maybe. Is there something similar can be done in terms of optimising the replication of physics bodies around a scene?Androgyne
sure, but I think we are getting into tricky territory here that they may not want to endulge, and it would only reduce space in memory not speed of executionLinkboy
argh, ok. I forgot SKPhysicsBodies are probably pretty light weight, already.Androgyne
The problem with physics bodies is that 1 physics body always has to check against every other physics body, unless the bit mask means we have 32 different stacks that can hold a reference to a body, ( I want to say no) so this means we need to eliminate. Now most of this is done fast, but no matter how fast it is, if you have a lot of bodies, it will get slow. Imagine 32001 bodies on screen. that is 32000(32000+1)/2 or something like that, meaning 512,016,000 callsLinkboy
So if all physics bodies were set to not be checked against each other for collisions or contacts by setting all their category masks to say "no, don't check", they don't check? If so, changing the category mask/collision/contact masks might be much lighter/easier/faster/simplerAndrogyne
in other words, turn "on" physics checking only for the physicsbodies that represent tiles currently on the screen, and none of the ones off the screen.Androgyne
that wont help, because you need to check ALL the nodes if they are on screen or not lolLinkboy
Why so? Surely all the physics calculations have been turned off, so that's all that 512 million checks off. Only the dozen or so bodies on the screen are on, and a formula can prepare to turn on whatever bitmasks need to match for the next position of the camera???Androgyne
I know it is hard to think like a computer lol, how to you expect the system to know what to turn on and off, it has to go through every body and say are you on screen? oh you are, ok turn on physics, and lets do more elaborate checkingLinkboy
this is no different than categoryBitMasks, the question is Oh are you group 1? ok do more elaborate checkingLinkboy
The only way I know of how to reduce checking all is to have lists, Then you only need to compare what is in the list, not the entire physics body worldLinkboy
I think we have a different vision of scale. I'm thinking about a mario type game. Let's say... 8x screens of 128 tiles each (16x8) = 1024 total tiles.Androgyne
There's no need to search for tiles to come onto the screen, you can simply store all of them in an array, and know when to turn them on/off (their physics) based on position of the camera.Androgyne
you are still doing the check, maybe YOU physically aren't doing the check, but computer has to, Otherwise you are saying on every update, grab every node on the screen and throw it into a pile, and that is even more insane lolLinkboy
Camera position = tiles visible onscreen. It's maths. For position x (camera only scrolls left <--> right), the physicsNodes associated with the range of tileMap columns currently visible must be "on". Those columns are numbers directly pertinent to the camera's current .x position.Androgyne
I can't explain this low level stuff to you, you just aren't getting it and I am starting to get angry. I know it sounds simple, it just doesn't work that way, no matter what you do, you need to touch every physics body in some wayLinkboy
Calm down. Think about it logically. I'm perhaps not explaining it well. ONLY the mapTiles visible onscreen need physics on. Those mapTile positions can be calculated. That means all mapTile positions NOT onscreen can be assumed to be left turned OFF in terms of their associated physics bodies. You're assuming I need a lesson in basic computer science, when that's not the case. I'm trying to show you a relationship you've missed.Androgyne
There is a direct relationship between the columns of tileMapLocations and the current camera position. If only ever turning on associated physicsBodies for the tiles onscreen, there's no need to go hammering through the entire tilemap to determine what's onscreen. It's known.Androgyne
it doesnt work that way, you still need to check whether or not it IS on screen, there is nothing free that filters out on and off screenLinkboy
Yes, there is. The position of the camera DIRECTLY relates to columns in the tilemap.Androgyne
the position of the camera means shit, that just tells where to do the evaluation, the evaluation still has to happenLinkboy
you dont move the camera and all the physics body data categoryBitMasks magically fizzle to 0Linkboy
I know you still need to do the onscreen physics calculations. That's never going way. I'm talking about switching nodes off and on not being very expensive: In primitive english: for columns plus and minus 8 units from camera.position.x (assuming 16 tiles fit horizontally on the screen) = physicsNodeON. For columns 9 units less than current camera position and more than current camera position, turn physics off. The rest are all off.Androgyne
In this way, it's a very efficient turning on and off process, of physics, based on the camera position... no need to iterate through all tiles to determine which ones are on and off screen, each frame.Androgyne
The more advance checks dont happen, my point is no matter what, every physics body has to be touched, there are ways to reduce the speed, but we still need to touch every bodyLinkboy
switching nodes off and on means you touch every node, then every body in the node, and hit the switch. this means constantly rebuilding an array, because you need to iterate through all known bodies. This is a lot slower than building an array once and doing a comparison on an intLinkboy
I'm reasonably sure you understand what I'm suggesting but are deliberately avoiding it.Androgyne
no, I am not avoiding it, you need to think lower level, behind the scenesLinkboy
you want to use only what is on the screen, well you have 3 options, you can build a new list of physics bodies every update using what is on the view every time, you can build a list of what is on screen and add and remove what is on screen which involves a comparison to old and new, or you can put all the nodes on the list, and run through each node to determine if it is on or off the screen. How else can you build this listLinkboy
They're all there, all the time, already there. A physical recreation of the aesthetics of the tile map exists, all the time. But the "on and off" I've been talking about, this whole time, is about the masking. Turning on and off the masking.Androgyne
I don't have the right lexicon to talk about physics category masking. I don't yet fully understand how it works, nor how to even use its terminology. But I do know this: When categories are matched, they detect one another for contacts and/or collisions. Right? So it's possible to turn this behaviour off, meaning 1000 physics bodies are just in a scene, doing nothing.Androgyne
Mirror the column/row lexicon of the Tilemap in a 2D array containing all the physics bodies, and it's simply a matter of exactly telling which "physicsTiles" to turn on and off, based on the position of the camera. I'm pretty sure SKTileMapNode is doing the same exact thing in terms of the textures of tiles.Androgyne
yes, I know, how do you expect to turn on and off masking without touching every node, you need to know what got on the screen, and what got off the screenLinkboy
As I said: the camera's position.x is always pertinent (exactly) to a set of columns in the tileMap. If that set of columns is mirrored in the 2D array holding the PhysicsBodies, there's not even any maths. For any given x position you know exactly which bodies to activate.Androgyne
no you don't, you need to physically tell the memory to change, to do that, you need to touch the variableLinkboy
You really are overthinking this. Imagine a 2D array that mirrors the columns and rows of the tilemap. In that array is a physics body for each and every tile. The aesthetic representation of any given tile is matched by the shape of its physicsBody in this mirrored array. Whenever a tile comes on the screen, you goto the same exact location in the array of physicsBodies and change its bitmask category from "of to on", similarly, for those going off the screen on the other side, you turn them off.Androgyne
The "variables" accessed are whatever the number of rows are in your tilemap, times 2, because one side is going off the screen while the other comes on the screen.Androgyne
Think of it like a conveyer belt of physics bodies moving under the camera.Androgyne
Nice chat guys :) Someone from Apple staff finally chimed in at the link @Linkboy posted. They indicated some code as "the right approach" so I posted it here. It seems like adding physics bodies to tiles by searching on their user data is as good as it gets, for now.Smitt
S
2

Alternatively, you can apply a line sweep algorithm to the tiles you want to give a SKPhysicsBody.

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.

Screenshot without visuals for comparison

Screenshot without visuals showing the physicsbodies

See my answer in a similar post on how to implement this.

Sileas answered 14/6, 2019 at 19:14 Comment(0)
S
1

I spent two days trying different ideas but could find nothing better than what I posted in my other answer. As mentioned there, it is the way recommended by an Apple staff member and as far as I know it's the most efficient way to have SpriteKit automatically add physics bodies to all your tiles. I've tested it and it works. (Although I'm still holding my breath for Apple to add a straightfoward way of putting physics bodies on tiles).

But there are other considerations. If you are having performance issues because there are too many physics bodies in your scene, you might want to try one of these other approaches. However, they are both more time-consuming than the approach described above. The only reason that may justify using one of these more labor-intensive approaches is if you need to reduce the number of physics bodies in your scene due to performance issues. Otherwise, I think the "automatic" approach mentioned above is the best option we have.

So I won't go into detail here because I think the automatic option is the best. These are just ideas for alternate approaches if your game needs to be extra stingy with system resources.

Alternate Approach #1 - Use Fewer Physics Bodies

Create your tile map in the editor. 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 had used the automatic approach mentioned above.

Alternate Approach #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.

Smitt answered 16/3, 2018 at 11:10 Comment(2)
I don't think Apple will add physics to tiles any soon .. I'sn't the aim of tiles is to be highly optimized and keep the power for what's on front?Gann
You are probably right about Apple but one can hope :) I’m just speculating but seems like adding physics bodies could be done in such a way that doesn’t add too much overhead. I understand SKPhysicsBody is fairly well optimized.Smitt

© 2022 - 2024 — McMap. All rights reserved.