2

Using SpriteKit for an iOS 9.0 app/game (and no physics body).

I have a few SpriteNodes on a scene. SpriteNodes are with oddly shaped images and runs through multiple frames for animation.

What is the best way to detect touches on a SpriteNode's image content, but not on transparent area of the entire image rectangle.

I see a lot of posts on using SKCropNode / MaskImage. In my scenario, I have multiple images/frames for each SpriteNode for the animation.

Please advice on an approach or point me in the right direction. Thanks.

ZippyMind
  • 141
  • 2
  • 9
  • how many frames and how complex are the shapes? Can you give any examples of them? I can think of a few approaches to this problem, needed one of them myself. But need a little more info to determine which is best (or at all appropriate) for your situation. – Confused Oct 21 '16 at 00:49
  • Thanks, Good to know that I am not alone :-) Each Sprite has anywhere from 4 to 16 frames. Example would be animals and their actions when touched. Most examples that I see are single frame, masked with SKCropNode and moved/rotated. In my case, each of the frame for a sprite is different. – ZippyMind Oct 21 '16 at 01:25
  • I am looking at a couple of approaches. 1. Adding Physics body on the alpha channel for the texture 2. Dynamic mask creation based on the texture. On the other hand, I think, this should be a trivial issue and may have a simpler solution?! – ZippyMind Oct 21 '16 at 01:33
  • You could use this information to create a small physics body the size of the touch, for the alternative approach added to the answer: https://developer.apple.com/reference/uikit/uitouch/1618106-majorradius – Confused Oct 21 '16 at 02:51

2 Answers2

2

Based on additional info from comments above:

With only 16 shapes, from the 16 frames, one of the more efficient ways would be to draw primitive shapes that roughly approximate the outlines of your character at each frame, and convert these to CGPaths. These can then be swapped in and out for each frame or (better) plucked out appropriately when there's a touch for testing based on the frame currently being shown at time of touch.

CGPaths are very lightweight structs, so there shouldn't be any performance problems with either approach.

CGPathContainsPoint is the old name of this test, which is now a modernised API for Swift 3 and onwards:

Troubles using CGPathContainsPoint SWIFT

There is an app called PaintCode, that's about $100 USD, which translates vectors into CGPaths, amongst other things, so you could use this to import your shapes. You can copy/paste vectors into it, which I suggest, because you might want to draw in a friendly drawing program. PaintCode doesn't have the world's best drawing experience.


Additionally, here's a free, online tool for doing the creation of a polygon path from textures. Probably more painful than using a drawing app and PaintCode, but it's FREE!

Alternative: Physics Bodies from Texture, and Contact with Touch

You can automagically create physics body shapes from a texture, like so, from the docs on this page:

let texturedSpaceShip = SKSpriteNode(texture: spaceShipTexture)
texturedSpaceShip.physicsBody = SKPhysicsBody(texture: spaceShipTexture,
                                              size: CGSize(width: circularSpaceShip.size.width,
                                                           height: circularSpaceShip.size.height))

There have been reports of problems with determining if a point is within a given physics body, so it might be better to create a small physics object, place it where the touch is, and then determine if its in contact with the physics body shape appropriate for the current frame of the current game entity.

Caveat: There's no telling (nor way to control, that I know of) how complex the resultant CGPaths are for these automagically created physics shapes.

Community
  • 1
  • 1
Confused
  • 6,048
  • 6
  • 34
  • 75
  • Thanks, will check it out and keep you posted. Each of our animal sprite has 4 to 16 frames, we have 25+ animals. I wish, there is a much simpler solution/option. – ZippyMind Oct 21 '16 at 02:31
  • If you have a designer sitting around twiddling their thumbs, give them a polygon count maximum, and they should be able to do all of the outlines in less than a day. Especially if you can find one that's a user of Coreldraw. – Confused Oct 21 '16 at 02:39
  • Oh... wait. I have an idea that you could do in code... so you can do this immediately, but will be a bit less performant... cause you have no control over the complexity of the paths.. will update answer... – Confused Oct 21 '16 at 02:41
  • Thanks, I am able to use the alternate approach along with `physicsWorld.body(at: positionInScene)?.node` to detect touches. What happens when I animate SKSpriteNode? Will SpriteNode's physicsBody gets updated too? – ZippyMind Oct 21 '16 at 05:36
  • I think you'll need to premake (bake) all the shapes you need from the entire raft of textures, before the game starts, otherwise you might get stutters. I don't know how long SpriteKit takes to analyse the texture and "draw" into memory a CGPath from it, then build and attach a physics body from a texture, so probably better to do all few hundred of them before hand, and store them for swapping in and out on touch, appropriate to the state of the animation at the time of the touch. – Confused Oct 21 '16 at 05:43
0

I personally would just check the pixel of the current texture to see if is transparent or not. You can do this to get the pixel data:

(Note this code is hypothetical to get you started, I will try to work out a real example later on for you if you can't figure it out.)

(Also note, if you scale sprites, you need to handle that with converting touch location to texture)

let image = sprite.texture.cgImage()
if let dataProvider = image.dataProvider, let data = dataProvider.data
{

  let screenScale = UIScreen.main.scale //let's handle retina graphics (may need work)
  //touchedLocation is the location on the sprite, if the anchor was bottom left (it may be top left, will have to verify) 
  //So if I touch the bottom left corner of the sprite, I should get (0,0)
  let x = touchedLocation.x * screenScale
  let y = touchedLocation.y * screenScale
  let width = sprite.texture.size().width * screenScale
  let bpp = 4 // 4 bytes per pixel
  let alphaChannel = 3 //Alpha channel is usually the highest byte
  let index = bpp * (y * width + x) + alphaChannel
  let byte = data.withUnsafeBytes({[UInt8](UnsafeBufferPointer(start:$0 + index,count:1))})
  print(Alpha: \(byte))
}
else
{
  //we have no data
}
Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44