Detect touches on SKSpriteNode with oddly shaped image(s) running animations

238 Views Asked by At

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.

2

There are 2 best solutions below

5
Confused On

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.

0
Knight0fDragon On

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
}