Align SpriteNode with surface on collision SpriteKit Swift3

随声附和 提交于 2019-12-24 07:30:13

问题


Im trying to align a SKSpriteNode to match the "hit surface" of a PhysicsBody on collision.

What I'm doing is to shoot the SpriteNode at a Cube. I have setup the collision and the node attachment (fixed joint). Everything works but I need to find a way to rotate the spriteNode to match the hit surface as you can see below:

Note that the Cube can rotate etc so we don't always have a fixed rotation value on the Cube.

Any Ideas how to solve this?

Thanks in advance /Magnus


回答1:


Here is most of the answer that I think will work. I will update it later.

Basically, the goal is to place two sticky nodes to the magnet and to the cube respectively at the contacted point.

Then, you match the magnet's zRotation to the cube's rotation, and then align the magnet to the cube based on the position of the two sticky nodes.

I haven't done the rotation matching or sticky alignment, but everything else is here if you want to finish it until I get to later:

// Props:
class GameScene: SKScene, SKPhysicsContactDelegate {

  struct Category {
    static let
    border = UInt32(2),
    cube = UInt32(4),
    magnet = UInt32(8)
  }

  let names = (border: "border", cube: "cube", magnet: "magnet", stickyPoint: "stickyPoint")

  var flag_hitThisSimulation = false
}


// Setup:
extension GameScene {

  private func setupNodes() {
    border: do {
      let pb = SKPhysicsBody(edgeLoopFrom: frame)
      pb.categoryBitMask = Category.border
      physicsBody = pb
    }
    cube: do {
      let cubeNode = SKSpriteNode(color: .blue, size: CGSize(width: 150, height: 150))
      let pb = SKPhysicsBody(rectangleOf: cubeNode.size)
      pb.affectedByGravity = false
      pb.isDynamic = false
      pb.categoryBitMask = Category.cube
      pb.contactTestBitMask = Category.magnet
      cubeNode.physicsBody = pb
      cubeNode.position.y += 200
      cubeNode.name = names.cube
      cubeNode.run(.repeatForever(.rotate(byAngle: 3.14, duration: 3)))
      addChild(cubeNode)
    }
    magnet: do {
      let magnetNode = SKSpriteNode(color: .green, size: CGSize(width: 50, height: 12))
      let pb = SKPhysicsBody(rectangleOf: magnetNode.size)
      pb.categoryBitMask = Category.magnet
      pb.affectedByGravity = false
      magnetNode.physicsBody = pb
      magnetNode.name = names.magnet
      addChild(magnetNode)
    }
  }

  override func didMove(to view: SKView) {

    removeAllChildren()
    physicsWorld.contactDelegate = self
    setupNodes()

  }
}

// Physics:
extension GameScene {

  private func assignNodeOfName(_ name: String, contact: SKPhysicsContact) -> SKNode? {
    guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else { fatalError("how are there no nodes?") }
    if      nodeA.name == name { return nodeA }
    else if nodeB.name == name { return nodeB }
    else                       { return nil   }
  }

  private func addNodeToPoint(_ point: CGPoint, parent: SKNode) {
    let node = SKSpriteNode(color: .orange, size: CGSize(width: 5, height: 5))
    node.position = point
    node.name = names.stickyPoint
    parent.addChild(node)
  }

  func alignMagnetToCube() {

  }

  func didBegin(_ contact: SKPhysicsContact) {
    defer { flag_hitThisSimulation = true }
    if flag_hitThisSimulation { return }

    let contactedNodes = contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask

    switch contactedNodes {

    case Category.magnet + Category.cube:
      // Place the two sticky nodes:
      let cube   = assignNodeOfName(names.cube,   contact: contact)!
      let magnet = assignNodeOfName(names.magnet, contact: contact)!

      let cubePoint   = convert(contact.contactPoint, to: cube)
      let magnetPoint = convert(contact.contactPoint, to: magnet)

      addNodeToPoint(cubePoint,   parent: cube)
      addNodeToPoint(magnetPoint, parent: magnet)

      // Set the magnet's zRotation to the cube's zRotation, then align the two stickyNodes:
      // fluidity.SLEEPY(for: now)
      // finish.later()

    default: ()
    }
  }
}

// Game loop:
extension GameScene {

  // Change to touchesBegan for iOS:
  override func mouseDown(with event: NSEvent) {

    let magnet = childNode(withName: names.magnet) as! SKSpriteNode

    // Start simulation:
    magnet.removeAllActions()
    magnet.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 25))

    // End simulation:
    magnet.run(.wait(forDuration: 2)) {
      print("resetting simulation! \(event.timestamp)")
      magnet.physicsBody!.velocity = CGVector.zero
      magnet.zRotation = 0 // FIXME: This isn't working..
      magnet.position = CGPoint.zero
      self.flag_hitThisSimulation = false

      // Remove sticky nodes:
      for node in self.children {
        for childNode in node.children {
          if childNode.name == self.names.stickyPoint { childNode.removeFromParent() }
        }
      }
    }
  }
}



回答2:


Okay so I found a solution that works good. This is how I did it:

Since the first step of collision handling is a bit out side of this scope lets just say that we pass 3 variables from the didBegin(_ contact: SKPhysicsContact) function to our own function we name magnetHitBlock. This is where we do the rest.

  1. The Magnet as a sprite node
  2. The cube or block as a sprite node.
  3. The contactPoint (this is the point were the collision was triggered)

called from didBegin(_ contact: SKPhysicsContact)

let magnet = contact.bodyA.node as! SKSpriteNode
let block = contact.bodyB.node as! SKSpriteNode

magnetHitBlock(magnet: magnet, attachTo: block, contactPoint: contact.contactPoint)

our main function doing the work

func magnetHitBlock(magnet:SKSpriteNode, attachTo block:SKSpriteNode, contactPoint:CGPoint){

.....

}

So basically what we want is to find the angle of the contact surface of the cube(or other rectangular shape). And then we want to rotate our magnet to match that angle. We don't really care about the cube rotation, we just want the angle of the contact Surface in space.

Since each surface of the cube consists of 2 points we could get the angle of the surface by using the atan2 function with these 2 points as argument.

So first of all we need to map out all corners of the block geometry and convert these points into the scene coordinate space. Then we save them in an array.

let topLeft = convert(CGPoint(x: -block.size.width/2, y: block.size.height/2), from: block)
let bottomLeft = convert(CGPoint(x: -block.size.width/2, y: -block.size.width/2), from: block)
let topRight = convert(CGPoint(x: block.size.width/2, y: block.size.height/2), from: block)
let bottomRight = convert(CGPoint(x: block.size.width/2, y: -block.size.width/2), from: block)

let referencePoints = [topLeft,topRight,bottomRight,bottomLeft]

When we have the position of all corners we have to figure out which 2 points that makes up the contact surface that the magnet did hit. And since we have the hit location stored in our contactPoint variable we can do some measurements.

The first point is easy to find. We simply check the distance from our contactPoint to all of our referencePoints (corners). And the closest reference point is what we need because its always going to be one of the contact surface points.

(If we were only working with square geometry as a cube. the second closest point would be our second surface point. but this is not always true for a rectangle with different height/width ratio. so we need to do some more things)

Im sure there is a better way to write this pice of code. but for now it does the trick.

    //Varible to store the closetCorner - Default topLeft
    var closestCorner = referencePoints[0]

    //We set the prevDistance to something very large.
    var prevDistance:CGFloat = 10000000

    for corner in referencePoints{
        // We check the distance from the contactPoint to each corner.
        // If the distance is shorter then the last checked corner we update the closestCorner varible and also the prevDistance.
        let distance = hypot(corner.x - contactPoint.x, corner.y - contactPoint.y)
        if distance < prevDistance{
            prevDistance = distance
            closestCorner = corner
        }

Now we have one of the required surface points stored in closestCorner variable. We will use this to find the second surface point which can only be either: the next point or the previous point.

So now we create 2 variables, nextCorner and prevCorner, and we set these points relative to the closestCorner we already found. Note that if the closest point is the last element in the referencePoint array, our nextCorner would have to be the first element in the array. if the closest point is the first element in the array we have to set the prevCorner as the last element in the array:

    var nextCorner:CGPoint
    var prevCorner:CGPoint
    let index = referencePoints.index(of: closestCorner)

    if index == 3{
        nextCorner = referencePoints[0]
    }
    else{
        nextCorner = referencePoints[index! + 1]
    }

    if index == 0{

        prevCorner = referencePoints[3]

    }
    else{
        prevCorner = referencePoints[index! - 1]
    }

Alright so we have our closestCorner which is guaranteed to be one of our surface points. And we have the nextCorner and the PrevCroner stored. Now we have to figure out which one of these that is correct. We can do this with two messurants.

  1. Distance from our closestCorner to the nextCorner
  2. Distance from our contactPoint to the nextCorner.

if the distance from measurement 1 is greater then measurement 2, our second surface point has to be the nextCorner. else it has to be the prevCorner. And this is always the case no matter the rotation of the cube / block.

Okay so we now have our second surface point and we can use the atan2 function to get the angle of the surface and then set the magnet.Zrotation to this value. Great! But on last thing. If the second surface point turns out to be the the prevCorner instead of the nextCorner. The atan2 function arguments has to be reversed. otherwise the rotation is going to be 180 degree out of phase. in other words, the magnet will point outwards instead of inwards.

    // Distance from closestCorner to nextCorner.
    let distToNextCorner = hypot(closestCorner.x - nextCorner.x, closestCorner.y - nextCorner.y)
    // Distance from contactPoint to nextCorner
    let distFromContactPoint = hypot(contactPoint.x - nextCorner.x, contactPoint.y - nextCorner.y)

    let firstSurfacePoint = closestCorner
    var secondSurfacePoint:CGPoint

    if distToNextCorner > distFromContactPoint{

        secondSurfacePoint = nextCorner

        let angle = atan2( firstSurfacePoint.y - secondSurfacePoint.y  ,  firstSurfacePoint.x - secondSurfacePoint.x )
        magnet.zRotation = angle

    }
    else{
        secondSurfacePoint = prevCorner

        let angle = atan2(secondSurfacePoint.y - firstSurfacePoint.y , secondSurfacePoint.x - firstSurfacePoint.x )
        magnet.zRotation = angle
    }

Here is a full code example of the magnetHitBlock function:

func magnetHitBlock(magnet:SKSpriteNode, attachTo block:SKSpriteNode, contactPoint:CGPoint){


    // first move the magnet to the contact point.
    magnet.position = contactPoint


    // find the corners and convert thoes points into the scene coordinate space
    let topLeft = convert(CGPoint(x: -block.size.width/2, y: block.size.height/2), from: block)
    let bottomLeft = convert(CGPoint(x: -block.size.width/2, y: -block.size.width/2), from: block)
    let topRight = convert(CGPoint(x: block.size.width/2, y: block.size.height/2), from: block)
    let bottomRight = convert(CGPoint(x: block.size.width/2, y: -block.size.width/2), from: block)

    // Then we put these "referencePoints" into an array for easy acces.
    // Note that we go in a clockwise direction from the top left

    let referencePoints = [topLeft,topRight,bottomRight,bottomLeft]


    // Find the closest corner.

    // Varible to store the closetCorner
    var closestCorner = referencePoints[0]

    //We set the prevDistance to something very large.
    var prevDistance:CGFloat = 10000000

    for corner in referencePoints{

        // We check the distance from the contactPoint to each corner.
        // If the distance is smaler then the last checked corner we update the closestCorner varible and also the prevDistance.
        let distance = hypot(corner.x - contactPoint.x, corner.y - contactPoint.y)
        if distance < prevDistance{
            prevDistance = distance
            closestCorner = corner
        }

    }



    // Now lets find the NextCorner and prevCorner relative to the closestCorner.
    var nextCorner:CGPoint
    var prevCorner:CGPoint
    let index = referencePoints.index(of: closestCorner)

    if index == 3{
        nextCorner = referencePoints[0]
    }
    else{
        nextCorner = referencePoints[index! + 1]
    }

    if index == 0{

        prevCorner = referencePoints[3]

    }
    else{
        prevCorner = referencePoints[index! - 1]
    }



    // Distance from closestCorner to nextCorner.
    let distToNextCorner = hypot(closestCorner.x - nextCorner.x, closestCorner.y - nextCorner.y)
    // Distance from contactPoint to nextCorner
    let distFromContactPoint = hypot(contactPoint.x - nextCorner.x, contactPoint.y - nextCorner.y)



    let firstSurfacePoint = closestCorner
    var secondSurfacePoint:CGPoint

    if distToNextCorner > distFromContactPoint{

        secondSurfacePoint = nextCorner

        let angle = atan2( firstSurfacePoint.y - secondSurfacePoint.y  ,  firstSurfacePoint.x - secondSurfacePoint.x )
        magnet.zRotation = angle

    }
    else{
        secondSurfacePoint = prevCorner

        let angle = atan2(secondSurfacePoint.y - firstSurfacePoint.y , secondSurfacePoint.x - firstSurfacePoint.x )
        magnet.zRotation = angle


    }


    // currently the magnet position is centered on the block border. lets position it edge to edge with the block.

    magnet.position = convert(CGPoint(x: 0, y: -magnet.size.height/2), from: magnet)

   // Add code to attach the magnet to the block. 

}


来源:https://stackoverflow.com/questions/44986417/align-spritenode-with-surface-on-collision-spritekit-swift3

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!