问题
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.
- The Magnet as a sprite node
- The cube or block as a sprite node.
- 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.
- Distance from our closestCorner to the nextCorner
- 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