Introduction to Sprite Kit Physics

Login to Access Code

Sprite Kit animations will only take you so far when creating iOS games. To remake the platform, shooter, or side scrolling games that have great success you’ll need to interact with game physics. In many systems and platforms (especially in the past) game physics were a real beast to handle. They often required a math wizard dedicated to inventing and perfecting the delicate formulas that behave predictably while also allowing our eyes to believe and enjoy the transitions.Sprite Kit, however, makes physics extremely simple with built in support for collision detection, gravitational pull, velocity, momentum, and much more.

In this part, we are going to do some cleaning and some very rough initial physics to support making our hero jump whenever the screen is tapped. You should already be familiar with Sprite Kit and for better context to this tutorial, you may want to skim over the Sprite Kit animation tutorial.

Sprites vs Atlas

In our last tutorial we dropped in multiple single-image assets and called each image as a frame for our running hero. While this worked flawlessly, it’s not the most efficient approach to using sprites. In order for Xcode to make fewer calls and faster caches, an atlas is used to combine multiple image assets into a single optimized OpenGL image. This image is then mapped with a plist file for easy reference and high performance.

An atlas is created simply by naming a folder of images with a .atlas extension and dropping it into your project.

screenshot

Xcode will automatically generate an optimized, combined sprite for each build, and you can now call upon those image assets from a texture:

let heroAtlas = SKTextureAtlas(named: "hero.atlas")
var hero = SKSpriteNode(texture: heroAtlas.textureNamed("running1"))

Using Physics in a Sprite Kit Game

First, our game needs gravity. We’ve already declared our parent class “GameScene” to inherit the SKPhysicsContactDelegate prototype giving us the ability to give our overall parent scene physics properties. We’ll set a default gravity for the entire scene that pulls downward on y-axis at two times the force of Earth’s gravity (at least I’ve been told it’s based on g’s, I have yet to verify that anywhere).

self.physicsWorld.gravity = CGVectorMake(0.0, -2)

Now let’s give our hero a physics body. There are a number of ways to define our hero’s physical edges, but a simple method is to simply wrap the object in a Physics Body using a circle based upon the object’s size.

// Enable physics around our hero using a circle to draw our radius
hero.physicsBody = SKPhysicsBody(circleOfRadius: hero.size.height / 2.75)
hero.physicsBody.dynamic = true

If we run the app with just gravity applied to our character. we’ll see our character load and then quickly fall off the screen. This is due to the fact that we enabled the dynamic option on our hero, but we’ve given no physical body to the ground. Let’s enable physics on our ground:

let sprite = SKSpriteNode(texture: groundTexture)
let square = CGSize(width: groundTexture.size().width, height: groundTexture.size().height/4)
sprite.physicsBody = SKPhysicsBody(rectangleOfSize: square)
sprite.physicsBody.dynamic = false

Now our hero is back to running in the same place as before, but with gravity. We’re ready for a jump action which we’ll drop in touchesBegan method. I’ll skip the animation actions and show the physics only at this point. We’ll set the velocity to zero initially and then apply a “push” or impulse on only the y-axis:

hero.physicsBody.velocity = CGVectorMake(0, 0)
hero.physicsBody.applyImpulse(CGVectorMake(0, 280))

Here’s the full updated game. There is still some bad code, but we’ll fix that in the future.

import SpriteKit

class GameScene: SKScene, SKPhysicsContactDelegate
{
  // Our main scene. Everything is added to this for the playable game
  var moving: SKNode!

  // Our running man! Defaults to a stand still position
  let heroAtlas = SKTextureAtlas(named: "hero.atlas")
  var hero: SKSpriteNode!

  override func didMoveToView(view: SKView)
  {
    // setup physics
    self.physicsWorld.gravity = CGVectorMake(0.0, -2)

    moving = SKNode()
    self.addChild(moving)

    createSky()
    createGround()

    hero = SKSpriteNode(texture: heroAtlas.textureNamed("running1"))
    hero.xScale = 0.5
    hero.yScale = 0.5
    hero.position = CGPointMake(frame.width / 2.5, frame.height / 2.75)

    // Enable physics around our hero using a circle to draw our radius
    hero.physicsBody = SKPhysicsBody(circleOfRadius: hero.size.height / 2.75)
    hero.physicsBody.dynamic = true

    self.addChild(hero)
    runForward()
  }

  override func touchesBegan(touches: NSSet, withEvent event: UIEvent)
  {
    for touch: AnyObject in touches
    {
      // Do jump
      let hero_jump_anim = SKAction.animateWithTextures([
        heroAtlas.textureNamed("running4"),
        heroAtlas.textureNamed("running5"),
        heroAtlas.textureNamed("running6"),
        heroAtlas.textureNamed("jumping1"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping2"),
        heroAtlas.textureNamed("jumping3")
        ], timePerFrame: 0.06)

      let jump = SKAction.repeatAction(hero_jump_anim, count: 1)

      if !hero.actionForKey("jumping")
      {
        hero.runAction(jump, withKey: "jumping")
        hero.physicsBody.velocity = CGVectorMake(0, 0)
        hero.physicsBody.applyImpulse(CGVectorMake(0, 280))
      }
    }
  }

  func runForward()
  {
    let hero_run_anim = SKAction.animateWithTextures([
      heroAtlas.textureNamed("running1"),
      heroAtlas.textureNamed("running2"),
      heroAtlas.textureNamed("running3"),
      heroAtlas.textureNamed("running4"),
      heroAtlas.textureNamed("running5"),
      heroAtlas.textureNamed("running6"),
      heroAtlas.textureNamed("running7"),
      heroAtlas.textureNamed("running8"),
      heroAtlas.textureNamed("running9"),
      heroAtlas.textureNamed("running10"),
      heroAtlas.textureNamed("running11"),
      heroAtlas.textureNamed("running12"),
      heroAtlas.textureNamed("running13"),
      heroAtlas.textureNamed("running14")
      ], timePerFrame: 0.06)

    let run = SKAction.repeatActionForever(hero_run_anim)

    hero.runAction(run, withKey: "running")
  }

  func createGround()
  {
    let groundTexture = SKTexture(imageNamed: "ground")
    groundTexture.filteringMode = .Nearest

    let moveGroundSprite = SKAction.moveByX(-groundTexture.size().width * 2.0, y: 0, duration: NSTimeInterval(0.01 * groundTexture.size().width * 2.0))
    let resetGroundSprite = SKAction.moveByX(groundTexture.size().width * 2.0, y: 0, duration: 0.0)
    let moveGroundSpritesForever = SKAction.repeatActionForever(SKAction.sequence([moveGroundSprite, resetGroundSprite]))

    for var i:CGFloat = 0; i < 2.0 + self.frame.size.width / (groundTexture.size().width * 2.0); ++i
    {
      let sprite = SKSpriteNode(texture: groundTexture)
      sprite.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: groundTexture.size().width, height: groundTexture.size().height/4))
      sprite.physicsBody.dynamic = false
      sprite.setScale(2.0)
      sprite.position = CGPointMake(i * sprite.size.width, sprite.size.height / 2.0)
      sprite.runAction(moveGroundSpritesForever)
      moving.addChild(sprite)
    }
  }

  func createSky()
  {
    let skyTexture = SKSpriteNode(color: UIColor(red: 71/255, green: 140/255, blue: 183/255, alpha: 1.0), size: frame.size)
    skyTexture.position = CGPointMake(frame.width / 2, frame.height / 2)
    moving.addChild(skyTexture)
  }

}