1

I describe as StackOverflow standards the following issue.

Summarize the problem

I have issue about colliding two nodes. One is composed by a crowd and each people is a single item of my crowd defined in the same way (included in a while just to be clear and using index "i" and "j" to create the row of the crowd). I wanted to make disappear once arrive a node (as civilian) to the bottom and going to the top and the crowd remove the civilian spawned along the path. Actually I have this thing and the func tells me that the colliding happens but it didn't 'cause nothing happens and the civilian node captured by crowd it didn't disappear or removed with the removefromparent(). I've got no error messages with my compiler, it works for him. My scope is : detecting node civilian during the path by the crowd and remove this one from the path.

What I've tried I tried many things to fix this. The first thing is following a lot of tutorials about how Collision Masks etc.. work. I know what they do. But what I've tried it was to make a invisible line for the last line of crowd of people just to see if the problem is the crowd itself and making that if the civilian node arrives to collide the invisible line is like he was in contact with the crowd but it didin't this effect. I followed a lot of tutorial such as HackingWithswift, Youtube tutorials but the procedure for me it's clear but nothing happens (sorry for being repetitive).

Show code My problem is about this GameScene.sks because it it just one file with all the functions.

import SpriteKit
import GameplayKit

enum CategoryMask: UInt32 {
    case civilian_value = 1
    case crowd_value = 2
    case background_value = 0 
}

enum GameState {
    case ready
    case playing
    case dead
}

var gameState = GameState.ready {
    didSet {
        print(gameState)
    }
}

class GameScene: SKScene, SKPhysicsContactDelegate {

let player = SKSpriteNode(imageNamed: "player1")
let textureA = SKTexture(imageNamed: "player1")
let textureB = SKTexture(imageNamed: "player2")
let pause = SKSpriteNode(imageNamed: "pause-button")
let resume = SKSpriteNode(imageNamed: "pause-button")

var civilian = SKSpriteNode()

let pauseLayer = SKNode()
let gameLayer = SKNode()

weak var sceneDelegate: GameSceneDelegate?

//main
override func didMove(to view: SKView) {
    self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    
    self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
    self.physicsWorld.contactDelegate = self
    physicsBody = SKPhysicsBody(edgeLoopFrom: frame)

    //func for dynamic background
    moveBackground(image: ["background1", "background2", "background3", "background1"], x: 0, z: -3, duration: 5, size: CGSize(width: 0.5, height: 1.0))
    
    character(player: player)
    
    run(SKAction.repeatForever(
        SKAction.sequence([
            SKAction.run(civilians),
            SKAction.wait(forDuration: 3.0)])))
    
    run(SKAction.run(crowdSpawn))
    
    pause.name="pause"
    pause.position = CGPoint(x: frame.minX/1.3, y: frame.minY/1.15)
    pause.size=CGSize(width: 0.1, height: 0.1)
    pause.zPosition = 4
    addChild(pause)
    
    if self.scene?.isPaused == true {
        resume.name="resume"
        resume.position = CGPoint(x: frame.minX/1.5, y: frame.minY/1.15)
        resume.size=CGSize(width: 0.1, height: 0.1)
        resume.zPosition = 12
        addChild(resume)
    }

}

func pauseGame() {
    sceneDelegate?.gameWasPaused()

    let barr = SKSpriteNode()
    let barrbehind = SKSpriteNode()
    let buttonresume = SKSpriteNode(imageNamed: "back")
    
    
    barrbehind.name = "barrbehind"
    barrbehind.zPosition = 9
    barrbehind.color = SKColor.black
    barrbehind.size = CGSize(width: frame.width, height: frame.height)
    barrbehind.alpha = 0.5
    self.addChild(barradietro)

    barr.name = "bar"
    barr.size = CGSize(width: 0.4, height: 0.5)
    barr.color = SKColor.white
    barr.zPosition = 10
    self.addChild(barr)

    buttonresume.name = "resume"
    buttonresume.zPosition = 11
    buttonresume.color = SKColor.black
    buttonresume.size = CGSize(width: 0.1, height: 0.1)
    buttonresume.alpha = 0.5
    self.addChild(buttonresume)
    
    self.scene?.isPaused = true

}

//random func (it helps for generate randomly civilians along the path
func random() -> CGFloat {
    return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}

func random(min: CGFloat, max: CGFloat) -> CGFloat {
    return random() * (max - min) + min
}

//func to define civilians
func civilians() {
    let civilian = SKSpriteNode(imageNamed: "PV")
    civilian.name = "civilian"
    //posiziono il civile
    civilian.position = CGPoint(x: frame.size.width/8.0 * random(min: -1.5, max: 1.5), y: -frame.size.height * 0.45)
    
    civilian.physicsBody = SKPhysicsBody(rectangleOf: civilian.size)
    civilian.zPosition = 3
    
    civilian.physicsBody?.categoryBitMask = CategoryMask.civilian_value.rawValue
    
    civilian.physicsBody?.collisionBitMask = CategoryMask.crowd_value.rawValue
    civilian.physicsBody?.contactTestBitMask = CategoryMask.crowd_value.rawValue
    
    civilian.physicsBody?.isDynamic = true
    
    //civilian size
    civilian.size=CGSize(width: 0.2, height: 0.2)
    //civilian movement
    
    civilian.run(
        SKAction.moveBy(x: 0.0, y: frame.size.height + civilian.size.height,duration: TimeInterval(1.77)))
 
    addChild(civilian)
   
}

//func for the main character
func character(player: SKSpriteNode){
    player.position = CGPoint(x: 0, y: 0)
    player.size = CGSize(width: 0.2, height: 0.2)
    let animation = SKAction.animate(with: [textureB,textureA], timePerFrame:0.2)
    player.position = CGPoint(x: frame.midX, y: frame.midY)
    addChild(player)
    player.run(SKAction.repeatForever(animation))
}

//func for generate the crowd
func crowdSpawn(){
    
    var i = 0.0
    var j = 0.25
    var crowdRaw : Bool = true
    while crowdRaw {
        
        if i <= 1 {
            let crowd = SKSpriteNode(imageNamed: "player1")
            crowd.name = "crowd"
            //posiziono il civile
            crowd.size=CGSize(width: 0.15, height: 0.15)
            crowd.position = CGPoint(x: -frame.size.width / 3.6 + CGFloat(i)/2 * crowd.size.width , y: frame.size.height / 2 + (CGFloat(j)*2) * -crowd.size.height)
            crowd.zPosition = 3
            
            let animation = SKAction.animate(with: [textureB,textureA], timePerFrame:0.25)
            crowd.run(SKAction.repeatForever(animation))
            crowd.run(SKAction.moveBy(x: frame.size.width / 16.0 + CGFloat(i) * crowd.size.width, y: 0, duration: 0))
            
            
            let infectedCollision = SKSpriteNode(color: UIColor.red,
                                                 size: CGSize(width: 1, height: 0.1))
            infectedCollision.physicsBody = SKPhysicsBody(rectangleOf: infectedCollision.size)
            infectedCollision.physicsBody?.categoryBitMask = CategoryMask.crowd_value.rawValue
            //collisionBitMask : qui la linea della folla non può collidere con il civilian
            infectedCollision.physicsBody?.collisionBitMask = CategoryMask.civilian_value.rawValue
            infectedCollision.physicsBody?.contactTestBitMask = CategoryMask.civilian_value.rawValue
            infectedCollision.physicsBody?.isDynamic = true
            
            infectedCollision.name = "infectedCollision"
            
            infectedCollision.position = crowd.position
            addChild(crowd)

            addChild(infectedCollision)

            i += 0.25
        } else {
            j += 0.25
            i = 0.0
        }
        
        if j == 1 {
            crowdRaw = false
        }
    }
    
}

func didBegin(_ contact: SKPhysicsContact) {

    if contact.bodyA.node?.position == contact.bodyB.node?.position {
        let actionMoveDone = SKAction.removeFromParent()
        civilian.run(SKAction.sequence([actionMoveDone]))
    }
}


//func about the touches
func touchDown(atPoint pos : CGPoint) {
    let action = SKAction.move(to: pos, duration: 1.0)
    // playerSprite is a SpriteKit sprite node.
    player.run(action)
}


//func about the touches 
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    switch gameState {
    case .ready:
        gameState = .playing
        
    case .playing:
        for t in touches {
            let location = t.location(in: self)
            
            player.position.x = location.x/2
            
            for node in self.nodes(at: location){
                
                if node.name == "civilian" {
                    
                    let explode = SKAction.colorize(with: UIColor.systemBlue,colorBlendFactor: 5.0, duration: 2)
                    let vanish = SKAction.fadeOut(withDuration: 2.0)
                    node.run(explode , completion: {
                        node.run(vanish) {
                            node.removeFromParent()
                            
                        }
                    })
                }else if node.name == "pause" {
                    pauseGame()
                    
                }else if node.name == "resume" {
                    self.scene?.isPaused = false
                }
            }
        }
        
    case .dead:
        print("dead")
    }
    
    
    
}

//function to have different backgrounds in scrolling (3 backgrounds in a loop)
func moveBackground(image: [String], x: CGFloat, z:CGFloat, duration: Double, size: CGSize) {
    for i in 0...3 {
        
        let background = SKSpriteNode(imageNamed: image[i])
        
        background.position = CGPoint(x: x, y: size.height * CGFloat(i))
        background.size = size
        background.zPosition = z
        
        let move = SKAction.moveBy(x: 0, y: -background.size.height*3, duration: 0)
        let back = SKAction.moveBy(x: 0, y: background.size.height*3, duration: duration)
        
        let sequence = SKAction.sequence([move,back])
        let repeatAction = SKAction.repeatForever(sequence)
        
        addChild(background)
        background.run(repeatAction)  
    }
}

}
Whirlwind
  • 14,286
  • 11
  • 68
  • 157
  • I guess something like [this](https://stackoverflow.com/questions/44176202/moving-objects-random-across-the-screen-with-a-certain-speed/44207679#44207679) would be helpful to You. – Whirlwind Feb 22 '22 at 22:50
  • Few suggestions, name your methods better. Like `civilians()` can be renamed to `addCivilian(with: SpriteConfig)`. Mark properties and methods that are private, with `private` keyword. These tiny thingies help a reader to easier understand the code. Be consistent, either use `self` everywhere, or just where it is needed (You are using it in the places where is not needed). Etc. Just saying, if you try to write neat, self-explanatory and readable code, you do good for yourself and for others (which is me in this case :D ). – Whirlwind Feb 22 '22 at 22:52

1 Answers1

1

Ok, it was fun to recall how all this SpriteKit stuff works :D

First problem you have is node/sprite creation. The solution could be some kind of Factory pattern with more or less abstraction. GameScene doesn't have to know how nodes are initialized/configured. Scene could know only which type of nodes exist, and thats enough to get them ready for use.

    //MARK: - Factory
protocol AbstractFactory {
    func getNode()-> SKNode
    func getNodeConfig()->SpriteConfig
}

class CivilianFactory : AbstractFactory {
    
    // Local Constants
    private struct K {
        static let size = CGSize(width: 32, height: 32)
        static let name = "civilian"
        static let color = UIColor.yellow
    }
    
    // Here we get Civilian sprite config
    func getNodeConfig() -> SpriteConfig {
        
        let physics = SpritePhysicsConfig(categoryMask: Collider.civilian, contactMask: Collider.player | Collider.wall, collisionMask: Collider.none)
        
        return  SpriteConfig(name: K.name, size: K.size, color: K.color, physics: physics)
    }
    
    func getNode() -> SKNode {
        
        let config = getNodeConfig()
        
        let sprite = SKSpriteNode(color: config.color, size: config.size)
        sprite.color = config.color
        sprite.name = config.name
        sprite.zPosition = 1
        
        if let physics = config.physics {
            sprite.physicsBody = SKPhysicsBody(rectangleOf: config.size)
            sprite.physicsBody?.isDynamic = physics.isDynamic
            sprite.physicsBody?.affectedByGravity = physics.isAffectedByGravity
            sprite.physicsBody?.categoryBitMask = physics.categoryMask
            sprite.physicsBody?.contactTestBitMask = physics.contactMask
            sprite.physicsBody?.collisionBitMask = physics.collisionMask
        }
    }
  
        
        return sprite
    }
}

Same as this, You will make other "factories" as needed (just copy the factory and change visual/physics data setup). For this example I will make PlayerFactory.

and with next method I will create my nodes:

 private func getNode(factory:AbstractFactory)->SKNode{
        return factory.getNode()
 }

and then just use it like this:

let node = getNode(factory: self.civiliansFactory) // or self.whateverFactory

Here you just provide a factory you want (can be anything that conforms to AbstractFactory), and in return, You get a desired node (You can return here anything that is SKNode). This way, we have hid initialization process, dependencies etc. from outside world (GameScene), and put everything in one place.

So, quite flexible, plus removes a bunch of repeating code from your scene.

And here are config structs for sprites creation:

//MARK: - Sprite Config
struct SpriteConfig {
    
    let name:String
    let size:CGSize
    let color:UIColor
    let physics:SpritePhysicsConfig? // lets make this optional
}

struct SpritePhysicsConfig {
    
    let categoryMask: UInt32
    let contactMask: UInt32
    let collisionMask:UInt32
    let isDynamic:Bool
    let isAffectedByGravity:Bool
    
    init(categoryMask:UInt32, contactMask:UInt32, collisionMask:UInt32, isDynamic:Bool = true, isAffectedByGravity:Bool = false){
        self.categoryMask = categoryMask
        self.contactMask = contactMask
        self.collisionMask = collisionMask
        self.isDynamic = isDynamic
        self.isAffectedByGravity = isAffectedByGravity
    }
}

Now some useful extensions that I needed:

//MARK: - Extensions

//Extension borrowed from here : https://stackoverflow.com/a/37760551
extension CGRect {
    func randomPoint(x:CGFloat? = nil, y:CGFloat? = nil) -> CGPoint {
        let origin = self.origin
        return CGPoint(x: x == nil ? CGFloat(arc4random_uniform(UInt32(self.width))) + origin.x : x!,
                       y: y == nil ? CGFloat(arc4random_uniform(UInt32(self.height))) + origin.y : y!)
    }
}

//Extension borrowed from here:  https://stackoverflow.com/a/33292919

extension CGPoint {
    func distance(point: CGPoint) -> CGFloat {
        return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
    }
}

And the GameScene:

//MARK: - Game Scene
class GameScene: SKScene {
    
    //MARK: - Local Constants
    
    // It's always good to have some kind of local constants per file, so that you have all variables in one place when it comes to changing/tuning
    private struct K {
        
        struct Actions {
            static let civilianSpawningKey = "civilian.spawning"
            static let playerMovingKey = "player.moving"
            static let spawningDuration:TimeInterval = 0.7
            static let spawningRange = 0.2
            static let fadeOutDuration:TimeInterval = 0.35
        }
        
        struct General {
            static let playerSpeed:CGFloat = 350
        }
        
    }
    
    //MARK: - Private Properties
    private var player:SKSpriteNode?
    
    // Just in case, nodes are removed after physics simulation is done (in didSimulatePhysics which is called in each frame)
    // Frame-Cycle Events : https://developer.apple.com/documentation/spritekit/skscene/responding_to_frame-cycle_events
    private var trash:[SKNode] = []
    
    private let civilianFactory = CivilianFactory()
    private let playerFactory = PlayerFactory()
    
    //MARK: - Scene lifecycle
    override func sceneDidLoad() {
        
        physicsWorld.contactDelegate = self
        spawnCivilians()
    }
    
    //MARK: - Creating & Spawning sprites
    
    private func getNode(factory:AbstractFactory)->SKNode{
        return factory.getNode()
    }
    
    private func spawnCivilian(at position: CGPoint){
        
        let node = getNode(factory: civilianFactory)
        node.position = position
        
        addChild(node)
    }
    
    private func spawnPlayer(at position: CGPoint){
        
        // If its a first time, create player and leave it there
        guard let `player` = player else {
            
            let node = getNode(factory: playerFactory)
            node.position = position
            
            self.player = (node as? SKSpriteNode)
            
            addChild(node)
            
            return
        }
        
        // If player exists, move it around
        let distance =  player.position.distance(point: position)
        let speed = K.General.playerSpeed
        
        // To maintain same moving speed, cause if we use constant here, sprite would move faster or slower based on a given distance
        let duration = distance / speed
        let move = SKAction.move(to: position, duration:duration)
        
        // This is a good way to check if some action is running
        if player.action(forKey: K.Actions.playerMovingKey) != nil {
            player.removeAction(forKey: K.Actions.playerMovingKey)
            
        }
        player.run(move, withKey: K.Actions.playerMovingKey)
    }
    
    private func spawnCivilians(){
        
        let wait = SKAction .wait(forDuration: K.Actions.spawningDuration, withRange: K.Actions.spawningRange)
        
        let spawn = SKAction.run({[weak self] in
            guard let `self` = self else {return}
            
            self.spawnCivilian(at: self.frame.randomPoint())
        })
        
        let spawning = SKAction.sequence([wait,spawn])
        
        self.run(SKAction.repeatForever(spawning), withKey:K.Actions.civilianSpawningKey)
        
    }
    
    //MARK: - Touches Handling
    func touchDown(atPoint pos : CGPoint) {
        spawnPlayer(at: pos)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        for t in touches { self.touchDown(atPoint: t.location(in: self)) }
    }
}

So I pretty much commented everything. Here, you :

  • Start spawning civilians infinitely, immediately after the scene is loaded
  • On touch you add player to the scene
  • On every next touch player travels to the touch location (by the same speed)

And contacts:

//MARK: - Physics
struct Collider{
    static let player : UInt32 = 0x1 << 0
    static let civilian : UInt32 = 0x1 << 1
    static let wall : UInt32 = 0x1 << 2
    static let none : UInt32 = 0x0
}

extension GameScene: SKPhysicsContactDelegate{
    
    //MARK: - Removing Sprites
    override func didSimulatePhysics() {
        
        for node in trash {
              // first remove node from parent (with fadeOut)
              node.run(SKAction.sequence([SKAction.fadeOut(withDuration: K.Actions.fadeOutDuration), SKAction.removeFromParent()])) 
        }
        trash.removeAll() // then empty the trash
    }
    
    //MARK: Removing
    func didBegin(_ contact: SKPhysicsContact) {
        
        guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
            
            //Silliness like removing a node from a node tree before physics simulation is done will trigger this error
            fatalError("Physics body without its node detected!")
        }
        
        let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
        
        switch mask {
            
        // Contact between player and civilian detected
        case Collider.player | Collider.civilian:
            
            if let civilian = (contact.bodyA.categoryBitMask == Collider.civilian ? nodeA : nodeB) as? SKSpriteNode
            {
                trash.append(civilian)
            }
            
        default:
            
            break
        }
    }
}

I guess those contacts and node removal were your problem. The point is that nodes that have physics body, are safer to remove from a node tree when didSimulatePhysics method is finished. There is a link in comments that explains what happens each frame, but the bottom point is, that physics engine retains physics body cause simulation is not finished, but the node is removed and that often end up in some unexpected results.

So to try how this work, you just copy / paste it in your GameScene. Here is how it looks:

video example

You can see how nodes are really removed by observing nodes count label. (to enable these labels, you just go (in your view controller class) with (self.view as? SKView)?.showsNodeCount = true, showsFPS, showsPhysics etc).

Whirlwind
  • 14,286
  • 11
  • 68
  • 157