Tuesday, November 11, 2014

How to implement a space shooter with SpriteKit and SWIFT - Part 2

Adding enemies, bullets and shooting with SKAction and SKConstraint





Tutorial Overview:

  • Part 1: Initial project setup, sprite creation and movement using SKAction and SKConstraint
  • Part 2: Adding enemies, bullets and shooting with SKAction and SKConstraint
  • Part 3Adding a HUD with SKLabelNode and SKSpriteNode
  • Part 4Adding basic game logic and collision detection
  • Part 5: Adding particles and sound 
  • Part 6: GameCenter integration


Add the enemies:

I'll add several enemy sprites. These will automatically follow and orient to the hero sprite.  You can download the code from Part 1 here.





1. Add a new class EnemySpriteController:




2. Import SpriteKit, define the class and add an array which stores all enemies:


import SpriteKit

// Controller class for:
// - creating/destroying enemies, 
// - shooting
// - animitaions
class EnemySpriteController {
    var enemySprites: [SKSpriteNode] = []

}

3. Add a new method spawnEnemy to EnemySpriteController:

Nothing magic here. Just create a SKSpriteNode and add it to the enemy collection. Targeting and orientation behavior is implemented with SKConstraints. For details check my post: HowTo: Implement targeting or follow behavior for sprites with SpriteKit and SKConstraint.

    // Return a new enemy sprite which follows the targetSprite node
    func spawnEnemy(targetSprite: SKNode) -> SKSpriteNode {

        // create a new enemy sprite
        let newEnemy = SKSpriteNode(imageNamed:"Spaceship")
        enemySprites.append(newEnemy)
        newEnemy.xScale = 0.08
        newEnemy.yScale = 0.08
        newEnemy.color = UIColor.redColor()
        newEnemy.colorBlendFactor=0.4
        
        // position new sprite at a random position on the screen
        var sizeRect = UIScreen.mainScreen().applicationFrame;
        var posX = arc4random_uniform(UInt32(sizeRect.size.width))
        var posY = arc4random_uniform(UInt32(sizeRect.size.height))
        newEnemy.position = CGPoint(x: CGFloat(posX), y: CGFloat(posY))
        
        // Define Constraints for orientation/targeting behavior
        let i = enemySprites.count-1
        let rangeForOrientation = SKRange(constantValue:CGFloat(M_2_PI*7))
        let orientConstraint = SKConstraint.orientToNode(targetSprite, offset: rangeForOrientation)
        let rangeToSprite = SKRange(lowerLimit: 80, upperLimit: 90)
        var distanceConstraint: SKConstraint
  
        // First enemy has to follow spriteToFollow, second enemy has to follow first enemy, ...
        if enemySprites.count-1 == 0 {
            distanceConstraint = SKConstraint.distance(rangeToSprite, toNode: targetSprite)
        } else {
            distanceConstraint = SKConstraint.distance(rangeToSprite, toNode: enemySprites[i-1])
        }
        newEnemy.constraints = [orientConstraint, distanceConstraint]
        
        return newEnemy

    }

4. Create a property for the EnemySpriteController object inside GameScene.swift:

var enemySprites = EnemySpriteController()

5. Create some enemies at the end of didMoveToView method inside GameScene.swift:

// Add enemy sprites
for(var i=0; i<3;i++){
  self.addChild(enemySprites.spawnEnemy(heroSprite))

}

Result are three red enemy sprites which will follow the white spaceship. Next steps are adding bullets and shooting.





6. Add a shoot method inside EnemySpriteController.swift

The shoot method iterates over each enemy sprite, creates a bullet, determines a vector to the target object and starts a SKAction which moves the bullet.


// Shoot in direction of spriteToShoot
func shoot(targetSprite: SKNode) {
        
  for enemy in enemySprites {
            
    // Create the bullet sprite
    let bullet = SKSpriteNode()
    bullet.color = UIColor.greenColor()
    bullet.size = CGSize(width: 5,height: 5)
    bullet.position = CGPointMake(enemy.position.x, enemy.position.y)
    targetSprite.parent?.addChild(bullet)
            
    // Determine vector to targetSprite
    let vector = CGVectorMake((targetSprite.position.x-enemy.position.x), targetSprite.position.y-enemy.position.y)
            
    // Create the action to move the bullet. Don't forget to remove the bullet!
    let bulletAction = SKAction.sequence([SKAction.repeatAction(SKAction.moveBy(vector, duration: 1), count: 10) ,  SKAction.waitForDuration(30.0/60.0), SKAction.removeFromParent()])
    bullet.runAction(bulletAction)
            
  }
}

7. Call shoot inside the update method of GameScene.swift

SpriteKit cannot guarantee in which time intervals the update method is called. To ensure that the enemies shoot every second, I'll store the time interval when shoot was called in a global property.  

var _dLastShootTime: CFTimeInterval = 1

override func update(currentTime: CFTimeInterval) {
  /* Called before each frame is rendered */
            
  if currentTime - _dLastShootTime >= 1 {
    enemySprites.shoot(heroSprite)
    _dLastShootTime=currentTime
  }
}


That's all for today. In my next part I'll add a HUD, implement a basic game loging and add collision detection.
You can download the code from GitHub: Part 2 or the latest version here.

You can also download my prototyping App for this tutorial series:



Cheers, Stefan

8 comments:

  1. Hi Stefan. Awesome Blog. I was wondering why you chose to use:
    let rangeForOrientation = SKRange(constantValue:CGFloat(M_2_PI*7))
    instead of just editing your spaceship image so that the nose of the ship points to the right. This way rangeForOrientation can have a value of 0 for an offset, and the enemy sprite will point in the correct direction. Is M_2_PI*7 a "Magic Number"? Is it supposed to represent 270 degrees in radians? For the version of your project that has the sprite image facing upwards instead of to the right like I rotated it, I used:
    let rangeForOrientation = SKRange(constantValue:CGFloat(-M_PI_2))

    This will point the enemy ship in the proper direction also. Thanks again for the awesome blog.

    Ransak


    ReplyDelete
    Replies
    1. Hi Ransak, good point. I just used the default spaceship image which was provided by the game template. Using a rotated image would habe been much simpler, especially for a tutorial. Thank you for your comment.
      Stefan

      Delete
  2. Hello, Step 7 says to modify the update method in EnemySpriteController.swift, but it should be GameScene.swift.
    Thanks

    ReplyDelete
  3. Hello Stefan, this blog was incredibly helpful! Although i have ran into a small problem i hope you can help me with.
    on the line in which i write var enemySprites = EnemySpriteController() i am given an error in Xcode with the following prompt: " 'EnemySpriteController' cannot be constructed because it has no accessible initializers ". Can you please help me with this? Thank you very much

    ReplyDelete
  4. Hi Tariq,

    can you upload your code for example to GitHub? Then I can have a look.

    Cheers,
    Stefan

    ReplyDelete
  5. Hello awesome guide ;)
    My spaceship keeps loosing its direction after end of movement...
    Idea for change or problem ?

    ReplyDelete
    Replies
    1. Thanks :-)

      You can stop the movement a little bit before reaching the invisible node.

      Delete