Search code examples
iosswiftsprite-kitarc4random

Randomize Space Game PowerUp spawns


I have a small Space Game that I have been working on for a month or so now while learning SpriteKit and Swift. I got everything working just as I want it except for one thing, no matter what I try I either get errors or an close but not desired effect.

My issues is with spawning 1 random Power every XX seconds. The code I have right now works, but doesn't spawn 1, it spawns 1 or 2 or even 3 at the same time at a set interval between 30 and 90 seconds.

I tried several different arc4random ways, but I can't figure out how to do it. Could someone please guide me into the right direction. This is my last hurdle before the game is finished and it has been bugging me for weeks now.

The code I use solar, while working, but not he desired effect:

    func startNewLevel(){

    levelNumber += 1

    if self.action(forKey: "spawningEnemies") != nil{
        self.removeAction(forKey: "spawningEnemies")
    }

    if self.action(forKey: "spawningUfos") != nil{
        self.removeAction(forKey: "spawningUfos")
    }

    if self.action(forKey: "spawningLifePowerUps") != nil{
        self.removeAction(forKey: "spawningLifePowerUps")
    }

    if self.action(forKey: "spawningShieldPowerUps") != nil{
        self.removeAction(forKey: "spawningShieldPowerUps")
    }

    if self.action(forKey: "spawningRapidFirePowerUps") != nil{
        self.removeAction(forKey: "spawningRapidFirePowerUps")
    }

    var levelDuration = TimeInterval()
    // Change the levelDuration = X.X to what ever you want to determin your game diffculty. The higher the number the slow the game and vice versa
    switch levelNumber {
    case 1: levelDuration = 3.0 //Easy
    case 2: levelDuration = 2.8
    case 3: levelDuration = 2.6
    case 4: levelDuration = 2.4
    case 5: levelDuration = 2.2
    case 6: levelDuration = 2.0 //Medium
    case 7: levelDuration = 1.8
    case 8: levelDuration = 1.6
    case 9: levelDuration = 1.4
    case 10: levelDuration = 1.2
    case 11: levelDuration = 1.0 //Hard
    case 12: levelDuration = 0.8
    case 13: levelDuration = 0.6
    case 14: levelDuration = 0.4
    case 15: levelDuration = 0.2 //Insane
    default:
        levelDuration = 0.2
        print("Cannot find levelinfo")
    }

    let upperBonuslimit : UInt32 = 90 // Default 120 seconds
    let lowerBonuslimit : UInt32 = 60 // Default 60 seconds

    let upperlimit : UInt32 = 90 // Default 90 seconds
    let lowerlimit : UInt32 = 30 // Default 30 seconds

    let spawn = SKAction.run(spawnEnemy)
    let waitToSpawn = SKAction.wait(forDuration: levelDuration)
    let spawnSequence = SKAction.sequence([waitToSpawn, spawn])
    let spawnForever = SKAction.repeatForever(spawnSequence)
    self.run(spawnForever, withKey: "spawningEnemies")

    let spawnUfoEnemy = SKAction.run(spawnUfo)
    let waitToSpawnUfo = TimeInterval(CGFloat(arc4random() % ((upperBonuslimit - lowerBonuslimit) * 10) + lowerBonuslimit * 10)/10.0)
    let randomWaitUfo = SKAction.wait(forDuration: waitToSpawnUfo)
    let spawnUfoSequence = SKAction.sequence([randomWaitUfo, spawnUfoEnemy])
    let spawnUfoForever = SKAction.repeatForever(spawnUfoSequence)
    self.run(spawnUfoForever, withKey: "spawningUfos")

    let spawnLife = SKAction.run(spawnALifePowerUp)
    let waitToSpawnLife = TimeInterval(CGFloat(arc4random() % ((upperlimit - lowerlimit) * 10) + lowerlimit * 10)/10.0)
    let randomWait = SKAction.wait(forDuration: waitToSpawnLife)
    let spawnSequenceLife = SKAction.sequence([randomWait, spawnLife])
    let spawnForeverLife = SKAction.repeatForever(spawnSequenceLife)
    self.run(spawnForeverLife, withKey: "spawningLifePowerUps")

    let spawnShield = SKAction.run(spawnAShieldPowerUp)
    let waitToSpawnShield = TimeInterval(CGFloat(arc4random() % ((upperlimit - lowerlimit) * 10) + lowerlimit * 10)/10.0)
    let randomWait2 = SKAction.wait(forDuration: waitToSpawnShield)
    let spawnSequenceShield = SKAction.sequence([randomWait2, spawnShield])
    let spawnForeverShield = SKAction.repeatForever(spawnSequenceShield)
    self.run(spawnForeverShield, withKey: "spawningShieldPowerUps")

    let spawnRapidFire = SKAction.run(spawnARapidFirePowerUp)
    let waitToSpawnRapidFire = TimeInterval(CGFloat(arc4random() % ((upperlimit - lowerlimit) * 10) + lowerlimit * 10)/10.0)
    let randomWait3 = SKAction.wait(forDuration: waitToSpawnRapidFire)
    let spawnSequenceRapidFire = SKAction.sequence([randomWait3, spawnRapidFire])
    let spawnForeverRapidFire = SKAction.repeatForever(spawnSequenceRapidFire)
    self.run(spawnForeverRapidFire, withKey: "spawningRapidFirePowerUps")
}

Please let me know if there is anything I need to explain better. Or if I need to add any code.

Here is one of the Powerup function codes just incase it's needed swell to understand my issue:

 func spawnALifePowerUp(){
    if livesNumber < 5 {
    let randomXStart = random(min: gameArea.minX, max: gameArea.maxX)
    let randomXEnd = random(min: gameArea.minX, max: gameArea.maxX)

    let startPoint = CGPoint(x: randomXStart, y: self.size.height * 1.2)
    let endPoint = CGPoint(x: randomXEnd, y: -self.size.height * 0.2)

    let lifePower = SKSpriteNode(imageNamed: "lifePowerUp")
    lifePower.name = "LifePu"
    lifePower.setScale(1.2)
    lifePower.position = startPoint
    lifePower.zPosition = 3
    lifePower.physicsBody = SKPhysicsBody(rectangleOf: lifePower.size)
    lifePower.physicsBody!.affectedByGravity = false
    lifePower.physicsBody!.categoryBitMask = PhysicsCategories.LifePu
    lifePower.physicsBody!.collisionBitMask = PhysicsCategories.None
    lifePower.physicsBody!.contactTestBitMask = PhysicsCategories.Player | PhysicsCategories.ShieldActive
    lifePower.physicsBody!.isDynamic = true
    self.addChild(lifePower)

    let lifeRotation:SKAction = SKAction.rotate(byAngle: CGFloat.pi * 2, duration: 5)
    let repeatLifeRotation:SKAction = SKAction.repeatForever(lifeRotation)

    let moveLifeHeart = SKAction.move(to: endPoint, duration: 3.5) // Life Power Up move Speed
    let deleteLifeHeart = SKAction.removeFromParent()
    let lifeHeartSequence = SKAction.sequence([moveLifeHeart, deleteLifeHeart])

    if currentGameState == gameState.inGame{
        lifePower.run(lifeHeartSequence)
        lifePower.run(repeatLifeRotation)
    }
    let dx = endPoint.x - startPoint.x
    let dy = endPoint.y - startPoint.y
    let amoutToRotate = atan2(dy, dx)
    lifePower.zRotation = amoutToRotate
    } else {
        print("Life meter FULL!")
    }
}

Solution

  • ASSUMPTION: you only want one of the power ups to fire at a random interval between 30-90 seconds and then reset the interval and pick a different power and repeat.

    There is 2 problems with how you have your code set up

    1. you are setting random intervals for all 3 objects and it's entirely possible that they end up with a same value and fire at the same time.
    2. you are finding a random interval and putting into an SKAction and repeating that action. That action takes a snapshot of that value and repeats the snapshot. If your random interval generates 35secs. it will always fire on 35secs. it will not rerun the randomizer every time the SKAction repeats.

    There are a ton of ways to accomplish this, here is a pretty basic example that can get you on your way. It uses recursion and calls itself that way it keeps firing forever.

    //somewhere in your setup code
    spawnPowerup()
    

    ...

    func spawnPowerup() {
    
        let upperlimit: UInt32 = 90
        let lowerlimit: UInt32 = 30
    
        let waitToSpawnLife = TimeInterval(CGFloat(arc4random() % ((upperlimit - lowerlimit) * 10) + lowerlimit * 10) / 10.0)
        print("waitToSpawnLife \(waitToSpawnLife)")
    
        self.run(.wait(forDuration: waitToSpawnLife)) {
    
            let powerup = Int(arc4random_uniform(UInt32(3)))
            print("powerup \(powerup)")
    
            var spawnPowerUp: SKAction!
    
            if powerup == 0 {
                spawnPowerUp = SKAction.run(self.spawnALifePowerUp)
                self.run(spawnPowerUp, withKey: "spawningLifePowerUps")
            }
            else if powerup == 1 {
                spawnPowerUp = SKAction.run(self.spawnAShieldPowerUp)
                self.run(spawnPowerUp, withKey: "spawningShieldPowerUps")
            }
            else if powerup == 2 {
                spawnPowerUp = SKAction.run(self.spawnARapidFirePowerUp)
                self.run(spawnPowerUp, withKey: "spawningRapidFirePowerUps")
            }
    
            self.spawnPowerup()
        }
    }
    

    this code will pick a random interval to wait between 30-90 seconds and then fire one of the power funcs. It then calls itself and re-picks a new interval between 30-90 seconds and picks a new random power up to fire and repeats, and repeats and repeats...

    EDIT

    Not related to the question...

    I realize you are just starting out with Spritekit, but you are probably at the stage where you should start thinking about optimizing some of your code. The easiest way to start optimizing is to look for code that is the same for multiple objects and rerun multiple times and try to consolidate it. Much like how I consolidated your generate power up Actions.

    for example...

    if self.action(forKey: "spawningEnemies") != nil{
        self.removeAction(forKey: "spawningEnemies")
    }
    
    if self.action(forKey: "spawningUfos") != nil{
        self.removeAction(forKey: "spawningUfos")
    }
    
    if self.action(forKey: "spawningLifePowerUps") != nil{
        self.removeAction(forKey: "spawningLifePowerUps")
    }
    
    if self.action(forKey: "spawningShieldPowerUps") != nil{
        self.removeAction(forKey: "spawningShieldPowerUps")
    }
    
    if self.action(forKey: "spawningRapidFirePowerUps") != nil{
        self.removeAction(forKey: "spawningRapidFirePowerUps")
    }
    

    could become...

    removeRunningActionBy(key: "spawningEnemies")
    removeRunningActionBy(key: "spawningUfos")
    removeRunningActionBy(key: "spawningLifePowerUps")
    removeRunningActionBy(key: "spawningShieldPowerUps")
    removeRunningActionBy(key: "spawningRapidFirePowerUps")
    
    func removeRunningActionBy(key: String) {
        if self.action(forKey: key) != nil{
            self.removeAction(forKey: key)
        }
    }