Wednesday, November 5, 2014

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

Initial project setup, sprite creation and movement using 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 3: Adding a HUD with SKLabelNode and SKSpriteNode
  • Part 4: Adding basic game logic and collision detection
  • Part 5: Adding particles and sound 
  • Part 6: GameCenter integration


1. Create a new universal project (template: game; language: Swift)






2. Remove the GameScene.sks file

I'll not use the internal level editor, so this file is obsolete and can be deleted:



Inside GameController.swift remove this code block:

extension SKNode {
    class func unarchiveFromFile(file : NSString) -> SKNode? {
        if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
            var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)!
            var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)
            
            archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
            let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as GameScene
            archiver.finishDecoding()
            return scene
        } else {
            return nil
        }
    }

}

Replace the  ViewDidLoad method with this code to create a scene in fullscreen mode for iPad and iPhone:

    override func viewDidLoad() {
        super.viewDidLoad()

        // Detect the screensize
        var sizeRect = UIScreen.mainScreen().applicationFrame
        var width = sizeRect.size.width * UIScreen.mainScreen().scale
        var height = sizeRect.size.height * UIScreen.mainScreen().scale
        
        // Scene should be shown in fullscreen mode
        let scene = GameScene(size: CGSizeMake(width, height))
        
        
        // Configure the view.
        let skView = self.view as SKView
        skView.showsFPS = true
        skView.showsNodeCount = true
        
        /* Sprite Kit applies additional optimizations to improve rendering performance */
        skView.ignoresSiblingOrder = true
            
        /* Set the scale mode to scale to fit the window */
        scene.scaleMode = .AspectFill
        
        skView.presentScene(scene)

    }


Limit the supported screen orientations to LandscapeLeft:


    override func supportedInterfaceOrientations() -> Int {
        return Int(UIInterfaceOrientationMask.LandscapeLeft.rawValue)
    }

3. Add the space ship:

I'll add a spaceship sprite. The sprite will automatically orient and move to a touch location on the screen:




The automated orienting of the spaceship is implemented with a little trick. I'll add a SKConstraint to an invisible sprite. I case of a touch event, the invisible sprite is moved to the touch location and the spaceship is automatically oriented to this invisible sprite/touch location.


3.1. Open the GameScene.swift file and remove the code for creating a label in didMoveToView:

    override func didMoveToView(view: SKView) {
        /* Setup your scene here */

    }

3.2. Create two global Sprite properties:


    var heroSprite = SKSpriteNode(imageNamed:"Spaceship")
    var invisibleControllerSprite = SKSpriteNode()
    
    override func didMoveToView(view: SKView) {
        /* Setup your scene here */

    }

3.3. Create a sprite inside didMoveToView:

    self.backgroundColor = UIColor.blackColor()
        
    // Create the hero sprite and place it in the middle of the screen
    heroSprite.xScale = 0.15
    heroSprite.yScale = 0.15
    heroSprite.position = CGPointMake(self.frame.width/2, self.frame.height/2)
    self.addChild(heroSprite)
        

3.4. Create an invisible sprite to implement the orientation behavior inside didMoveToView:

    // Define invisible sprite for rotating and steering behavior without trigonometry
    invisibleControllerSprite.size = CGSizeMake(0, 0)
    self.addChild(invisibleControllerSprite)
        
    // Define a constraint for the orientation behavior
    let rangeForOrientation = SKRange(constantValue: CGFloat(M_2_PI*7))

 heroSprite.constraints = [SKConstraint.orientToNode(invisibleControllerSprite, offset: rangeForOrientation)]


3.5. Replace the code inside the touchesBegan method to implement the sprite movement:

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        /* Called when a touch begins */
        
        for touch: AnyObject in touches {
            

            // Determine the new position for the invisible sprite:
            // The calculation is needed to ensure the positions of both sprites 
            // are nearly the same, but different. Otherwise the hero sprite rotates
            // back to it's original orientation after reaching the location of 
            // the invisible sprite
            var xOffset:CGFloat = 1.0
            var yOffset:CGFloat = 1.0
            var location = touch.locationInNode(self)
            if location.x>heroSprite.position.x {
                xOffset = -1.0
            }
            if location.y>heroSprite.position.y {
                yOffset = -1.0
            }
            
            // Create an action to move the invisibleControllerSprite. 
            // This will cause automatic orientation changes for the hero sprite
         let actionMoveInvisibleNode = SKAction.moveTo(CGPointMake(location.x - xOffset, location.y - yOffset), duration: 0.2)
            invisibleControllerSprite.runAction(actionMoveInvisibleNode)
            
            // Create an action to move the hero sprite to the touch location
            let actionMove = SKAction.moveTo(location, duration: 1)
            heroSprite.runAction(actionMove)
            
        }

    }


That's all for today. In my next part I'll add some enemies and bullets.
You can download the code from GitHub: Part 1 or the latest version here.

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



Cheers,
Stefan

6 comments:

  1. Hi, there seems to be a stray line of code:
    followerSprite.constraints = [orientConstraint, distanceConstraint]
    Thanks

    ReplyDelete
    Replies
    1. Hi,
      yes, you are right. I've removed it.
      Thanks
      Stefan

      Delete
  2. Hi Stefan, Awesome tutorial you go here, I have used some of your ideas to implement my own space shooter game. I have a question for you regarding the star field emitter. What would I need to do to make the stars generate horizontally instead of vertically? I've tried a few things but haven't been able to figure it out.

    Thanks,
    Daniel

    ReplyDelete
    Replies
    1. Hi Daniel

      just rotate the emitter nodes:
      emitterNode.zRotation = CGFloat(M_PI_2)

      Cheers,
      Stefan

      Delete
    2. Thank you so much for this Stefan, keep up the great work!

      Daniel

      Delete
  3. This comment has been removed by the author.

    ReplyDelete