Augmented Reality and ARKit Tutorial

Learn how to work with augmented reality in this SpriteKit and ARKit tutorial! By Caroline Begbie.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Light Estimation

If you’re in a darkened room, then your bug will be lit up like a firefly. Bring your hand to gradually cover the camera and see the bug shine. Luckily, ARKit has light estimation so that your bug can lurk creepily in dark corners.

In GameScene.swift, add this to the end of update(_:):

// 1
guard let currentFrame = sceneView.session.currentFrame,
  let lightEstimate = currentFrame.lightEstimate else {
    return
}
    
// 2
let neutralIntensity: CGFloat = 1000
let ambientIntensity = min(lightEstimate.ambientIntensity,
                           neutralIntensity)
let blendFactor = 1 - ambientIntensity / neutralIntensity

// 3
for node in children {
  if let bug = node as? SKSpriteNode {
    bug.color = .black
    bug.colorBlendFactor = blendFactor
  }
}

Here’s the breakdown of this code:

  1. You retrieve the light estimate from the session’s current frame.
  2. The measure of light is lumens, and 1000 lumens is a fairly bright light. Using the light estimate’s intensity of ambient light in the scene, you calculate a blend factor between 0 and 1, where 0 will be the brightest.
  3. Using this blend factor, you calculate how much black should tint the bugs.

As you pan about the room, the device will calculate available light. When there’s not much light, the bug will be shaded. Test this by holding your hand in front of the camera at different distances.

Shooting Bugs

Unless you’re an acrobat, these bugs are a little difficult to stomp on. You’re going to set up your game as a first-person shooter, very similar to the original Doom.

In GameScene.swift, add a new property to GameScene:

var sight: SKSpriteNode!

Override didMove(to:):

override func didMove(to view: SKView) {
  sight = SKSpriteNode(imageNamed: "sight")
  addChild(sight)
}

This adds a sight to the center of the screen so you can aim at the bugs.

You’ll fire by touching the screen. Still in GameScene, override touchesBegan(_:with:) as follows:

override func touchesBegan(_ touches: Set<UITouch>,
                           with event: UIEvent?) {
  let location = sight.position
  let hitNodes = nodes(at: location)
}

Here you retrieve an array of all the nodes that intersect the same xy location as the sight. Although ARAnchors are in 3D, SKNodes are still in 2D. ARKit very cleverly calculates a 2D position and scale for the SKNode from the 3D information.

You’ll now find out if any of these nodes are a bug, and if they are, retrieve the first one. Add this to the end of touchesBegan(_:with:)

var hitBug: SKNode?
for node in hitNodes {
  if node.name == "bug" {
    hitBug = node
    break
  }
}

Here you cycle through the hitNodes array and find out if any of the nodes in the array are bugs. hitBug now contains the first bug hit, if any.

You’ll need a couple of sounds to make the experience more realistic. The sounds are defined in Sounds in Types.swift; they are all ready for you to use.

Continue by adding this code to the end of the same method:

run(Sounds.fire)
if let hitBug = hitBug,
  let anchor = sceneView.anchor(for: hitBug) {
  let action = SKAction.run {
    self.sceneView.session.remove(anchor: anchor)
  }
  let group = SKAction.group([Sounds.hit, action])
  let sequence = [SKAction.wait(forDuration: 0.3), group]
  hitBug.run(SKAction.sequence(sequence))
}

You play a sound to indicate you’ve fired your weapon. If you do hit a bug, then play the hit sound after a short delay to indicate the bug is some distance away. Then remove the anchor for the node, which will also remove the bug node itself.

Build and run, and kill your first bug!

Level Design

Of course, a game with one bug in it isn’t much of a game. In Pest Control, you edited tile maps in the scene editor to specify the positions of your bugs. You’ll be doing something similar here, but you’ll directly add SKSpriteNodes to a scene in the scene editor.

Although ARniegeddon is fully immersive, you’ll design the level as top down. You’ll lay out the bugs in the scene as if you are a god-like being in the sky, looking down at your earthly body. When you come to play the game, you’ll be at the center of the world, and the bugs will be all around you.

Create a new SpriteKit Scene named Level1.sks. Change the scene size to Width: 400, Height: 400. The size of the scene is largely irrelevant, as you will calculate the real world position of nodes in the scene using a gameSize property which defines the size of the physical space around you. You’ll be at the center of this “scene”.

Place three Color Sprites in the scene and set their properties as follows:

  1. Name: bug, Texture: bug, Position: (-140, 50)
  2. Name: bug, Texture: bug, Position: (0, 150)
  3. Name: firebug, Texture: firebug, Position: (160, 120)

Imagine you’re at the center of the scene. You’ll have one bug on your left, one straight in front of you and the firebug on your right.

2D Design to 3D World

You’ve laid out the bugs in a 2D scene, but you need to position them in a 3D perspective. From a top view, looking down on the 2D scene, the 2D x-axis maps to the 3D x-axis. However, the 2D y-axis maps to the 3D z-axis — this determines how far away the bugs are from you. There is no mapping for the 3D y-axis — you’ll simply randomize this value.

In GameScene.swift, set up a game size constant to determine the real world area that you’ll play the game in. This will be a 2-meter by 2-meter space with you in the middle. In this example, you’ll be setting the game size to be a small area so you can test the game indoors. If you play outside, you’ll be able to set the game size larger:

let gameSize = CGSize(width: 2, height: 2) 

Replace setUpWorld() with the following code:

private func setUpWorld() {
  guard let currentFrame = sceneView.session.currentFrame,
    // 1
    let scene = SKScene(fileNamed: "Level1")
    else { return }
    
  for node in scene.children {
    if let node = node as? SKSpriteNode {
      var translation = matrix_identity_float4x4
      // 2
      let positionX = node.position.x / scene.size.width
      let positionY = node.position.y / scene.size.height
      translation.columns.3.x = 
              Float(positionX * gameSize.width)
      translation.columns.3.z = 
              -Float(positionY * gameSize.height)
      let transform = 
             currentFrame.camera.transform * translation
      let anchor = ARAnchor(transform: transform)
      sceneView.session.add(anchor: anchor)
    }
  }
  isWorldSetUp = true
}

Taking each numbered comment in turn:

  1. Here you load the scene, complete with bugs from Level1.sks.
  2. You calculate the position of the node relative to the size of the scene. ARKit translations are measured in meters. Turning 2D into 3D, you use the y-coordinate of the 2D scene as the z-coordinate in 3D space. Using these values, you create the anchor and the view’s delegate will add the SKSpriteNode bug for each anchor as before.

The 3D y value — that’s the up and down axis — will be zero. That means the node will be added at the same vertical position as the camera. Later you’ll randomize this value.

Build and run and see the bugs laid out around you.

The firebug on your right still has the orange bug texture instead of the red firebug texture. You’re creating it in ARSKViewDelegate’s view(_:nodeFor:), which currently doesn’t distinguish between different types of bug. You’ll adjust this later.

First, you’ll randomize the y-position of the bugs.

Still in setUpWorld(), before:

let transform = currentFrame.camera.transform * translation

add this:

translation.columns.3.y = Float(drand48() - 0.5)

drand48() creates a random value between 0 and 1. Here you step it down to create a random value between -0.5 and 0.5. Assigning it to the translation matrix means the bug will appear in a random position between half a meter above the position of the device and half a meter below the position of the device. For this game to work properly, it assumes the user is holding the device at least half a meter off the ground.

To get a random number, you’ll initialize drand48(); otherwise the random number will be the same every time you run it. This can be good while you’re testing, but not so good in a real game.

Add this to the end of didMove(to:) to seed the random number generator:

srand48(Int(Date.timeIntervalSinceReferenceDate))

Build and run, and your bugs should show themselves in the same location, but at a different height.

Note: This was a lucky shot — you probably won’t see the scary alien ray.