Thursday, August 11, 2016

SpriteKit From Scratch: Advanced Techniques and Optimizations_part1

Introduction

In this tutorial, the fifth and final installment of the SpriteKit From Scratch series, we look at some advanced techniques you can use to optimize your SpriteKit-based games to improve performance and user experience.

This tutorial requires that you are running Xcode 7.3 or higher, which includes Swift 2.2 and the iOS 9.3, tvOS 9.2, and OS X 10.11.4 SDKs. To follow along, you can either use the project you created in the previous tutorial or download a fresh copy from GitHub.

The graphics used for the game in this series can be found on GraphicRiver. GraphicRiver is a great source for finding artwork and graphics for your games.

1. Texture Atlases

In order to optimize the memory usage of your game, SpriteKit provides the functionality of texture atlases in the form of the SKTextureAtlas class. These atlases effectively combine the textures you specify into a single, large texture that takes up less memory than the individual textures on their own.

Luckily, Xcode can create texture atlases very easily for you. This is done in the same asset catalogs that are used for other images and resources in your games. Open your project and navigate to the Assets.xcassets asset catalog. At the bottom of the left sidebar, click the + button and select the New Sprite Atlas option.

As a result, a new folder is added to the asset catalog. Click the folder once to select it and click again to rename it. Name it Obstacles. Next, drag the Obstacle 1 and Obstacle 2 resources into this folder. You can also delete the blank Sprite asset that Xcode generates if you want to, but that is not required. When completed, your expanded Obstacles texture atlas should look like this:


It's now time to use the texture atlas in code. Open MainScene.swift and add the following property to the MainScene class. We initialize a texture atlas using the name we entered in our asset catalog.
  1. let obstaclesAtlas = SKTextureAtlas(named: "Obstacles")
While not required, you can preload the data of a texture atlas into memory before it is used. This allows your game to eliminate any lag that might occur when loading the texture atlas and retrieving the first texture from it. Preloading a texture atlas is done with a single method and you can also run a custom code block once the loading has completed.

In the MainScene class, add the following code at the end of the didMoveToView(_:) method:
  1. override func didMoveToView(view: SKView) {     
  2.     ...     
  3.     obstaclesAtlas.preloadWithCompletionHandler { 
  4.         // Do something once texture atlas has loaded
  5.     }
  6. }
To retrieve a texture from a texture atlas, you use the textureNamed(_:) method with the name you specified in the asset catalog as a parameter. Let's update the spawnObstacle(_:) method in the MainScene class to use the texture atlas we created a moment ago. We fetch the texture from the texture atlas and use it to create a sprite node.
  1. func spawnObstacle(timer: NSTimer) {
  2.     if player.hidden {
  3.         timer.invalidate()
  4.         return
  5.     }
  6.     let spriteGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 2)
  7.     let texture = obstaclesAtlas.textureNamed("Obstacle \(spriteGenerator)")
  8.     let obstacle = SKSpriteNode(texture: texture)
  9.     obstacle.xScale = 0.3
  10.     obstacle.yScale = 0.3 
  11.     let physicsBody = SKPhysicsBody(circleOfRadius: 15)
  12.     physicsBody.contactTestBitMask = 0x00000001
  13.     physicsBody.pinned = true
  14.     physicsBody.allowsRotation = false
  15.     obstacle.physicsBody = physicsBody 
  16.     let center = size.width/2.0, difference = CGFloat(85.0)
  17.     var x: CGFloat = 0
  18.     let laneGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 3)
  19.     switch laneGenerator.nextInt() {
  20.     case 1:
  21.         x = center - difference
  22.     case 2:
  23.         x = center
  24.     case 3:
  25.         x = center + difference
  26.     default:
  27.         fatalError("Number outside of [1, 3] generated")
  28.     }     
  29.     obstacle.position = CGPoint(x: x, y: (player.position.y + 800))
  30.     addChild(obstacle)     
  31.     obstacle.lightingBitMask = 0xFFFFFFFF
  32.     obstacle.shadowCastBitMask = 0xFFFFFFFF
  33. }
Note that, if your game takes advantage of On-Demand Resources (ODR), you can easily specify one or more tags for each texture atlas. Once you have successfully accessed the correct resource tag(s) with the ODR APIs, you can then use your texture atlas just like we did in the spawnObstacle(_:) method. You can read more about On-Demand Resources in another tutorial of mine.

2. Saving and Loading Scenes

SpriteKit also offers you the ability to easily save and load scenes to and from persistent storage. This allows players to quit your game, have it relaunched at a later time, and still be up to the same point in your game as they were before.

The saving and loading of your game is handled by the NSCoding protocol, which the SKScene class already conforms to. SpriteKit's implementation of the methods required by this protocol automatically allow for all of the details in your scene to be saved and loaded very easily. If you want to, you can also override these methods to save some custom data along with your scene.

Because our game is very basic, we are going to use a simple Bool value to indicate whether the car has crashed. This shows you how to save and load custom data that is tied to a scene. Add the following two methods of the NSCoding protocol to the MainScene class.
  1. // MARK: - NSCoding Protocol 
  2. required init?(coder aDecoder: NSCoder) {
  3.     super.init(coder: aDecoder)
  4.     let carHasCrashed = aDecoder.decodeBoolForKey("carCrashed") 
  5.     print("car crashed: \(carHasCrashed)")
  6. } 
  7. override func encodeWithCoder(aCoder: NSCoder) {
  8.     super.encodeWithCoder(aCoder) 
  9.     let carHasCrashed = player.hidden
  10.     aCoder.encodeBool(carHasCrashed, forKey: "carCrashed")
  11. }
If you are unfamiliar with the NSCoding protocol, the encodeWithCoder(_:) method handles the saving of your scene and the initializer with a single NSCoder parameter handles the loading.

Next, add the following method to the MainScene class. The saveScene() method creates an NSData representation of the scene, using the NSKeyedArchiver class. To keep things simple, we store the data in NSUserDefaults.
  1. func saveScene() {
  2.     let sceneData = NSKeyedArchiver.archivedDataWithRootObject(self)
  3.     NSUserDefaults.standardUserDefaults().setObject(sceneData, forKey: "currentScene")
  4. }
Next, replace the implementation of didBeginContactMethod(_:) in the MainScene class with the following:
  1. func didBeginContact(contact: SKPhysicsContact) {
  2.     if contact.bodyA.node == player || contact.bodyB.node == player {
  3.         if let explosionPath = NSBundle.mainBundle().pathForResource("Explosion", ofType: "sks"),
  4.             let smokePath = NSBundle.mainBundle().pathForResource("Smoke", ofType: "sks"),
  5.             let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile(explosionPath) as? SKEmitterNode,
  6.             let smoke = NSKeyedUnarchiver.unarchiveObjectWithFile(smokePath) as? SKEmitterNode {
  7.              
  8.             player.removeAllActions()
  9.             player.hidden = true
  10.             player.physicsBody?.categoryBitMask = 0
  11.             camera?.removeAllActions()
  12.              
  13.             explosion.position = player.position
  14.             smoke.position = player.position
  15.              
  16.             addChild(smoke)
  17.             addChild(explosion)
  18.              
  19.             saveScene()
  20.         }
  21.     }
  22. }
The first change made to this method is editing the player node's categoryBitMask rather than removing it from the scene entirely. This ensures that upon reloading the scene, the player node is still there, even though it is not visible, but that duplicate collisions are not detected. The other change made is calling the saveScene() method we defined earlier once the custom explosion logic has been run.

Finally, open ViewController.swift and replace the viewDidLoad() method with the following implementation:
  1. override func viewDidLoad() {
  2.     super.viewDidLoad() 
  3.     let skView = SKView(frame: view.frame) 
  4.     var scene: MainScene?
  5.     if let savedSceneData = NSUserDefaults.standardUserDefaults().objectForKey("currentScene") as? NSData,
  6.        let savedScene = NSKeyedUnarchiver.unarchiveObjectWithData(savedSceneData) as? MainScene {
  7.         scene = savedScene 
  8.     } else if let url = NSBundle.mainBundle().URLForResource("MainScene", withExtension: "sks"),
  9.               let newSceneData = NSData(contentsOfURL: url),
  10.               let newScene = NSKeyedUnarchiver.unarchiveObjectWithData(newSceneData) as? MainScene {
  11.         scene = newScene
  12.     } 
  13.     skView.presentScene(scene)
  14.     view.insertSubview(skView, atIndex: 0) 
  15.     let left = LeftLane(player: scene!.player)
  16.     let middle = MiddleLane(player: scene!.player)
  17.     let right = RightLane(player: scene!.player) 
  18.     stateMachine = LaneStateMachine(states: [left, middle, right])
  19.     stateMachine?.enterState(MiddleLane)
  20. }
When loading the scene, we first check to see if there is saved data in the standard NSUserDefaults. If so, we retrieve this data and recreate the MainScene object using the NSKeyedUnarchiver class. If not, we get the URL for the scene file we created in Xcode and load the data from it in a similar manner.

Run your app and run into an obstacle with your car. At this stage, you don't see a difference. Run your app again, though, and you should see that your scene has been restored to exactly how it was when you just crashed the car.
Written  by Davis Allie

If you found this post interesting, follow and support us.
Suggest for you:

iOS 10 Projects: Build Amazing Apps with Apple's Newest iOS

iOS 10 & Swift 3: From Beginner to Paid Professional

Complete Beginners Guide to iOS Development - Build 10 Apps

The Complete iOS 10 Developer - Build Real Apps with Swift 3

Swift 3 and iOS 10 The Final Course Learn to Code like a Pro

No comments:

Post a Comment