4
votes

I don't know if this is a weird bug in Xcode or there's something about the SpriteKit's coordinate system I don't understand.

The premise is that the position of a node is always relative to its parent. However, whenever I call a block that creates and positions a node with a physics body from "didBeginContact" of SKPhysicsContactDelegate, the node is always positioned relative to the scene (instead of its parent). Note that calling this same block works as intended when triggered anywhere but "didBeginContact". Another thing is that, if I remove the physics body of the said node, the block will now work as intended even when called from "didBeginContact".

I've been stuck with this issue for two days and it would be too draggy to give other details about my actual code. So I've made a very simple project demonstrating this anomaly. Just create a new project in Xcode 6 with Spritekit Template, and replace GameViewController.swift and GameSwift.swift with the codes posted below. Just run in iPad Air and everything else should be self explanatory.

Final Notes:

  1. As you press the first button and makes contact to the second button, look at the lower left corner of the screen. You will see that the boxes are being "wrongly" positioned there.
  2. Try to remove the physics body of the box in "AddBox". It will now work as intended.
  3. Please let me know if you think this is a bug or there's something in the coordinates system or physics world that I don't understand

GameViewController.swift:

import SpriteKit

class GameViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let scene = GameScene()
        scene.size = view.frame.size
        let skView = self.view as SKView
        scene.scaleMode = .AspectFill
        skView.presentScene(scene)
    }
}

GameScene.swift:

import SpriteKit

let kButtonSize = CGSize(width: 500, height: 100)
let kContainerSize = CGSize(width: 500, height: 300)
let kBoxSize = CGSize(width: 25, height: 25)

class GameScene: SKScene, SKPhysicsContactDelegate {

    override func didMoveToView(view: SKView) {
        physicsWorld.contactDelegate = self
        addFirstButton()
        addSecondButton()
        addContainer()
    }

    func addFirstButton() {
        let button = SKSpriteNode(color: SKColor.blueColor(), size: kButtonSize)
        let label = SKLabelNode(text: "Call 'addBox' from didBeginContact")
        button.name = "firstButton"
        label.name = "firstLabel"
        button.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))
        button.physicsBody = SKPhysicsBody(rectangleOfSize: button.size)
        button.physicsBody.allowsRotation = false
        button.physicsBody.affectedByGravity = false
        button.physicsBody.categoryBitMask = 0x1
        button.physicsBody.contactTestBitMask = 0x1
        button.addChild(label)
        addChild(button)
    }

    func addSecondButton() {
        let button = SKSpriteNode(color: SKColor.blueColor(), size: kButtonSize)
        let label = SKLabelNode(text: "Call 'addBox' from touchesBegan")
        button.name = "secondButton"
        label.name = "secondLabel"
        button.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame)-200)
        button.physicsBody = SKPhysicsBody(rectangleOfSize: button.size)
        button.physicsBody.dynamic = false
        button.physicsBody.categoryBitMask = 0x1
        button.physicsBody.contactTestBitMask = 0x1
        button.addChild(label)
        addChild(button)
    }

    func addContainer() {
        let container = SKSpriteNode(color: SKColor.greenColor(), size:kContainerSize)
        let label = SKLabelNode(text: "Created node should fall here")
        label.fontColor = SKColor.blackColor()
        container.name = "container"
        container.physicsBody = SKPhysicsBody(edgeLoopFromRect: container.frame)
        container.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame)+300)
        container.addChild(label)
        addChild(container)
    }

    func addBox() {
        let container = childNodeWithName("container")
        let box = SKSpriteNode(color: SKColor.blueColor(), size: kBoxSize)
        box.physicsBody = SKPhysicsBody(rectangleOfSize: box.size)
        box.position = CGPointMake(0, 100)
        container.addChild(box)
    }

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        let touch = touches.anyObject() as UITouch
        let point = touch.locationInNode(self)
        let node = nodeAtPoint(point)
        if node.name == nil {return}

        switch node.name! {
        case "firstButton", "firstLabel":
            let button = childNodeWithName("firstButton") as SKSpriteNode
            button.physicsBody.applyImpulse(CGVectorMake(0, -500))
        case "secondButton", "secondLabel":
            addBox()
        default:
            break
        }
    }

    func didBeginContact(contact: SKPhysicsContact!) {
        addBox()
    }
}
2
I know that coordinates for physics stuff are in world space, but that should (?) not be relevant here...Emiel
Would you say that this is a bug of Xcode?Donn

2 Answers

2
votes

I have submitted a ticket regardomg this issue with Apple Bug Report. I hope this helps anyone who is encountering the same issue. Here is their response:

Apple Developer Relations24-Sep-2014 04:40 AM

Engineering has determined that this is an issue for you to resolve based on the following:

You can't modify a tree which it is being simulated, and this is clearly stated in Programming Guide.

1
votes

It seems like you can't create physics bodies in the contact delegate (this is similar to other engines, eg. AndEngine). Instead of creting new body straight into the delegate, mark some bool flag there and do your creation in the didSimulatePhysics or didFinishUpdate - works for me.