Is the SpriteKit broken in iOS13.3?

Hello there


I began coding for iOS after a few year and, though I like how it works in Xcode, I can't help but getting suspicious at how something doesn't seem to work properly, for exemple, in this snippet, the tiles will go were I expect them to, but won't fade away by a half.:


                addChild(shape)
                let placeIt = SKAction.group(
                    [
                    SKAction.fadeAlpha(by: 0.5, duration: 2),
                    SKAction.move(to: CGPoint(x: CGFloat(x) * (SIZE + SPACE) + dX, y: CGFloat(y) * (SIZE + SPACE) + dY), duration: 2)])
                placeIt.timingMode = SKActionTimingMode.easeInEaseOut
               
                NSLog("moving %@", shape.name!)
                shape.run(placeIt, withKey: "placeIt \(shape.name!)")
                NSLog("%@ moved", shape.name!)

Even stranger, I invoke a function just after this loops which targets 2 specific shapes in order to fade them back in, then rotate them. Of course, they've not been faded, but they won't rotate and even the NSLog statements won't be anywhere to see.


Later in the game it works if I rotate once (on touchEnd), but If I want to also rotate the surrounding, it does stop to rotate aven once by further attempts.


These shapes use Physics. I know the bitmasks are right but there never seem to have a contact even though it should happen. Show physics is relevant: there's a border when expected and rotating the shapes should trigger a contact which used to happen, but not anymore. I also never got the scaling to get properly.


Seriously, I don't understand what is going here 😟

Should the SKActions only be run in a specific context?


Please help!

Mirx

Replies

There are a few earlier threads on this forum and various threads on Stack Overflow discussing issues with physics bodies from textures in iOS 13.*. I'm not sure that those are all fixed yet (I worked around them back when the latest was 13.2 and haven't gone back to see the story in 13.3), so perhaps that has something to do with your contact issues. On the other hand, when I was having problems, showsPhysics would display obviously-broken physics body outlines.


I've had no other issues though, despite working extensively with a large portion of the SpriteKit API over the last half year. When stuff like logging messages don't show up, it seems like something much odder is going on.

Thanks, I am not sure this does help, except making me consider a shameful switch to the dreaded Unity Engine.


Do you know where I could find some doc/examples about where to actually perform my SKActions? I thought these could all be performed from the SKScene.didMove method as they'd be sequentially performed in a single thread?

The actions will get run sequentially on your main thread, but sticking stuff in didMove may not be what you want. The only thing I ever used there was an initial action to happen on scene appearance (e.g., to wait for a couple of seconds, then start a new game). And things like background nodes that have something like an infinitely repeating animation could be placed there, though I usually put those in the scene creation. But once the scene gets going then the actions are all scheduled from other places. E.g., in response to contacts between nodes, or from the update function, or triggered as a result of user interaction, or based on completions of other actions, etc.


Maybe you need to provide more context, because it's really not clear how you have things structured.

Thanks. I really value your friendly help.


I want to:


1) begin by placing a bunch of differently colored SKShapeNode objects on the SKScene. I'd cram there as much 32x32 CGSize as possible.


2) The game will then begin by a pair of automatic turns:

a) having my (lower-left corner shape) initialized (changing its color and actually turning around in order to contact the surrounding similarly colored shapes using SKPhysicsContactDelegate.didBegin() ) and take possession of them

b) same for the computer opponent (upper right corner)


3) At this point, the game may begin for real: either I or the computer play(s) and choose a color which cannot be the opponent's in order to chain-reaction all of his shapes to absorb even more similarly colored, unonwned surrounding ones.


But then I am currently stuck because the inital tour doesn't quite take place: only my tile NSLogs me it has rotated but it actually doesn't.


Bits of code:


    override func didMove(to view: SKView) {
        /* Set the scale mode to scale to fit the window */
        scaleMode = .resizeFill
        // Set physics
        physicsWorld.gravity = .zero
        physicsWorld.contactDelegate = self
       
        backgroundColor = SKColor.darkGray
       
        label = SKLabelNode(fontNamed: "Chalkduster")
        label.text = "Color ZooZ 2"
        label.horizontalAlignmentMode = .center
        label.verticalAlignmentMode = .center
        label.color = SKColor.systemPink
        label.fontSize = 48
        label.position = CGPoint(x: size.width/2, y: size.height/2)
        addChild(label)
        installShapes()
    }
   
    func installShapes() {
        X = size.width / (SIZE + SPACE) - 1
        Y = size.height / (SIZE + SPACE) - 1
        dX = (size.width - (X * (SIZE + SPACE))) / 1
        dY = (size.height - (Y * (SIZE + SPACE))) / 1
        NSLog("MIRKO display: %0.0f x %0.0f & Nb: %0.0f x %0.0f & dX: %0.0f / dY: %0.0f", size.width, size.height, X, Y, dX, dY)
       
        self.run(SKAction.run {
            for x in 0...Int8(self.X - 1) {
                for y in 0...Int8(self.Y - 1) {
                    self.addShape(atX: x, andY: y)
                    }
                }
            }, completion: {
                NSLog("End of installShapes, about to rename the player shapes")
                self.renameShape(from: "\(0).\(0)", to: "me")
                self.renameShape(from: "\(self.X-1).\(self.Y-1)", to: "him")
        })
    }
   
    func renameShape(from oldName : String, to newName : String) {
        if let s = shapeByName(name: oldName) as SKShapeNode? {
            s.strokeColor = s.fillColor
            s.physicsBody?.categoryBitMask = PhysicsCategory.mine
            s.physicsBody?.contactTestBitMask = PhysicsCategory.unset
            s.physicsBody?.collisionBitMask = PhysicsCategory.none
            s.name = newName
            s.alpha = 1.0
            propagate(shape: s)
        }
    }
   
    func addShape(atX x: Int8, andY y : Int8) {
        let shape = SKShapeNode(rectOf: CGSize(width: SIZE, height: SIZE), cornerRadius: SIZE / 4)
        shape.name = "\(x).\(y)"
        shape.position = CGPoint(x: 0, y: 0)
        let generator = ColorGenerator()
        shape.fillColor = generator.random()
        shape.strokeColor = UIColor.lightGray
        shape.lineWidth = BORDER

        shape.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: SIZE, height: SIZE))
        shape.physicsBody?.isDynamic = false
        shape.physicsBody?.categoryBitMask = PhysicsCategory.unset
        shape.physicsBody?.contactTestBitMask = PhysicsCategory.mine | PhysicsCategory.his
        shape.physicsBody?.collisionBitMask = PhysicsCategory.none
        shape.physicsBody?.usesPreciseCollisionDetection = true
        shape.isPaused = false

        addChild(shape)
        let placeIt = SKAction.group(
            [SKAction.fadeAlpha(by: 0.5, duration: 2),
            SKAction.move(to: CGPoint(x: CGFloat(x) * (SIZE + SPACE) + dX, y: CGFloat(y) * (SIZE + SPACE) + dY), duration: 2),
            SKAction.wait(forDuration: 0.1)])
        placeIt.timingMode = SKActionTimingMode.easeInEaseOut
       
        NSLog("moving %@", shape.name!)
        shape.run(placeIt, withKey: "placeIt \(shape.name!)")
        NSLog("%@ moved", shape.name!)
    }

    func shapeAt(x mX : Int8, y mY : Int8) -> SKShapeNode? {
        let allNodes = nodes(at: CGPoint(x: CGFloat(mX) * (SIZE + SPACE) + dX, y: CGFloat(mY) * (SIZE + SPACE) + dY))
        for node in allNodes {
            if let o = node as? SKShapeNode {
                return o
            }
        }
        return nil
    }
   
    func shapeByName(name s:String) -> SKShapeNode? {
        if let c = childNode(withName: s) as? SKShapeNode {
            return c
        }
        return nil
    }
   
    func propagate(shape s : SKShapeNode) {
        NSLog("Propagating...")
        if s.isPaused { s.isPaused = false }
        let rotate = SKAction.rotate(toAngle: .pi, duration: 0.5)
        rotate.timingMode = SKActionTimingMode.easeInEaseOut
        NSLog("rotating")
        s.run(rotate, withKey: "rotate \(s.name!)")
        NSLog("rotated")
    }


Which SKScene member funcion should I override for which part of the game init/turn, etc?


Once again, thanks!

It's hard to say without being able to try it and see what happens, but my impression is that you're essentially setting up a bunch of nodes in one corner, scheduling actions to have them move (in parallel) into position, renaming two nodes, and then scheduling actions to have them do some rotation (also in parallel with everything else). I suspect you want things to happen in a more sequential order:


1. The shapes start in a corner and spread out to form an array (perhaps in parallel, perhaps one-by-one).

2. One corner node to do some rotating.

3. The second corner node to do some rotating.


Possibly you want 2 and 3 to happen in parallel, but in any case, from your description it sounds like 1 should definitely happen before 2 and/or 3. That's not what you've written though.


In particular, lines 28-38 add a bunch of nodes, schedule actions for those nodes, and then immediately (before the actions run) do the renameShape stuff.


The completion of the run action means the completion of adding the nodes and scheduling their actions, but it does not mean the completion of those scheduled actions.


There are various ways of sequencing things. E.g., you could pass a completion block that would do the rotations and have addShape schedule that after the completion of the actions for one of the nodes (assuming they all move into position simultaneously). Or you could just have another action that would wait(forDuration:) a bit longer than what it takes the nodes to move into position and then runs a block to do the rotations.


As a side note, when you're writing SKAction.group, those are happening in parallel. So in a group with actions that have duration 2, an action that is a delay of 0.1 is having no effect at all. Maybe you meant SKAction.sequence, or .sequence([.group([...]), .wait(..)])

Oh, also sequences like NSLog, run, NSLog are just going to do two log messages in quick succession. It's not waiting for the run to finish. If you want that effect, you need to have the second NSLog in a completion block for the run.

This is the intro of the game:

1. The shapes start in a corner and spread out to form an array (perhaps in parallel, perhaps one-by-one).

They all go simultaneously to their definite place. They however don't fadeAlpha which is kinda bugging me.


Then for the first turn only, I want these to happen in parallel:

2. One corner node to do some rotating.

3. The second corner node to do some rotating.


Once this is set the game can go on with one of the corners absorbing its neighbours by taking their colors, then the opponent and so on until one reaches more than 50%.


I kicked off my SpriteKit approach using a Ray Wenderlich tutorial but it doesn't quite seem to work at me... many SKaction, even grouped don't work (even though they NSLog so)...


I fixed the short wait which I indeed wanted to sequence after the group, but then it doesn't work because it just means that it starts after the others were started, but not completed.


Do you have a typical working link to something like:


run(SKAction.run {
   manythingsinparallel()
}, completion: {
  otherthings() 
}


...or maybe I should find ou how I could get a notification from all SKAction.group once it has completed all of its subtasks?


Pity I don't find much around as I am lacking the right jargon to perform my searches...


Thanks again.

The fadeAlpha I've never used, but probably the name is confusing you and it's adding the amount specified to alpha. Since you're starting at alpha = 1, adding 0.5 is not going to do anything. Try -0.5.


For doing actions in parallel on a bunch of nodes, then running something after all have finished, here's a couple of approaches.


// If all nodes have actions that take the same amount of time
func makeNodes(numNodes: Int, then whenDone: @escaping () -> Void) {
  let nodeAction = ...
  let node1 = makeNode()
  node1.run(nodeAction, completion: whenDone)
  for _ in 1 ..< numNodes {
    let node = makeNode()
    node.run(nodeAction)
  }
}
// If node actions may take random unknown amounts of time
class Coordinator {
  var remaining: Int
  let action: () -> Void
 
  init(numNodes: Int, whenDone action: @escaping () -> Void) {
    remaining = numNodes
    action = whenDone
  }

  func finished() {
    remaining -= 1
    if remaining == 0 {
      action()
    }
  }
}

func makeNodes(numNodes: Int, then whenDone: @escaping () -> Void) {
  let coordinator = Coordinator(numNodes: numNodes, whenDone: whenDone)
  for _ in 0 ..< numNodes {
    let node = makeNode()
    let nodeAction = ...
    node.run(.sequence([nodeAction, .run { coordinator.finished() }]))
  }
}
// If node actions may take varying times but you can compute a max
func makeNodes(numNodes: Int, then whenDone: @escaping () -> Void) {
  for _ in 0 ..< numNodes {
    let node = makeNode()
    let nodeAction = ...
    node.run(nodeAction)
  }
  let delayNode = SKNode()
  addChild(delayNode)
  delayNode.run(.sequence([.wait(forDuration: maxDelay), .removeFromParent()]), completion: whenDone)
}


(I probably have some typos in there since I'm only typing and not compiling/running, but you get the idea.)

Thanks, I will try and implement it other the week-end!


Where did you find such ideas? It's not obvious it should be done this way in the Apple Developer Doc and I have been looking for days elsewhere...

Once you experiment with actions a while, your mental model of how they work and the various combination/completion options will get filled in. I spent the last 6 months doing a lot of that.

Excellent, I went further 🙂


Now, I wonder why these Physics bodies: 2 adjacent squares do not generate an SKPhysicsContact when one rotates by .pi:


Bitmasks:

    struct PhysicsCategory {
        static let none      : UInt32 = 0
        static let all       : UInt32 = UInt32.max
        static let mine      : UInt32 = 0b001       // 1
        static let his       : UInt32 = 0b010       // 2
        static let us        : UInt32 = 0b011       // 3
        static let unset     : UInt32 = 0b100       // 4
    }


Tiles:

        shape.physicsBody?.isDynamic = true
        shape.physicsBody?.categoryBitMask = PhysicsCategory.unset
        shape.physicsBody?.contactTestBitMask = PhysicsCategory.mine | PhysicsCategory.his
        shape.physicsBody?.collisionBitMask = PhysicsCategory.none
        shape.physicsBody?.usesPreciseCollisionDetection = true
        shape.isPaused = false


Player (a Tile which is customized):

            if (newName == "me") {
                s.physicsBody?.categoryBitMask = PhysicsCategory.mine
            } else {
                s.physicsBody?.categoryBitMask = PhysicsCategory.his
            }
            s.physicsBody?.contactTestBitMask = PhysicsCategory.unset
            s.physicsBody?.collisionBitMask = PhysicsCategory.none
            s.name = newName


Whatever I do, there's nothing in the logs involving a contact???

Nothing immediately obvious from that snippet. Did you turn on showsPhysics to see the physics bodies?

Yes I did. The borders are relevant and rotate accordingly. I want to use the physics not to have to (re)code a massive matrix routine to determine which neighbours should or should not be affected...


I tried to make the "unset" shapes to react to everything but their installation triggers a huge load of contacts. I also tested that the players' shapes NSLog their category bitmasks and it's also relevant.


Strange.

Everything isDynamic? I'm not sure whether that matters or not; I read that only one shape needs to be dynamic for collisions to be flagged, but I've never experimented with it. What's didBegin look like?

extension GameScene: SKPhysicsContactDelegate {
   
    func didBegin(_ contact: SKPhysicsContact) {
        NSLog("SKPhysicsContact detected")
        if contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask {
            return
        }
        var player: SKPhysicsBody
        var that: SKPhysicsBody

        if contact.bodyA.categoryBitMask != PhysicsCategory.shape {
            player = contact.bodyA
            that = contact.bodyB
        } else {
            player = contact.bodyB
            that = contact.bodyA
        }
        if let o = that.node as? SKShapeNode, let p = player.node as? SKShapeNode {
            if (o.fillColor == p.fillColor) {
                NSLog("Collision between %@ and %@.", p.name!, o.name!)
                o.strokeColor = p.fillColor
                o.physicsBody?.categoryBitMask = p.physicsBody!.categoryBitMask
                if (p.physicsBody!.categoryBitMask == PhysicsCategory.me) {
                    mySquares.append(o)
                    for q in mySquares {
                        propagate(shape: q)
                    }
                }
                if (p.physicsBody!.categoryBitMask == PhysicsCategory.it) {
                    hisSquares.append(o)
                    for q in hisSquares {
                        propagate(shape: q)
                    }
                }
            }
        }
    }
}