Sky Painting

Day and Night Setup.

While the SunSprite class possesses methods to adjust the appearance and position, relative to GameTime.currentTime, for the sun and moon sprites, the rest of the scene's elements must also respond to the change in time of day. Most notably in need of dynamic updating is the sky, across which the sun and moon transit throughout the day.

We'll break the day into two twelve hour periods of day and night.

Of course, the actual amounts of light and dark within a normal day fluctuate depending on time of year and distance from the equator, but that kind of verisimilitude can wait until we've got this up and running.

For certain at least, the sky must be dark during nightTime and light during dayTime. And the transition between these states should conceal, as much as possible, the difference between the two; day's transformation into night and back again must be subtle enough, or rather slow enough, that the operation approaches imperceptibility.

Starting with another gradient, this one from blue to black, and an artboard twice the height of the view's frame, we can set up a background which will slide back and forth during the day.

The largest screen size we'll be dealing with is the iPad retina, 2048x1536, so our extra-tall background is 2048x3072.

For the majority of any twenty-four hour period, the position of dayNight@2x.png will be either completely up (night) or completely down (day). With the default anchorPoint of (0.5, 0.5), the daytime position would be at the frame's highest point and halfway across horizontally, while at nighttime it would descend to the lowest point in the frame.

The SkyBackground class will have CGPoint's to hold these.

class SkyBackground: SKNode {  
  var nightPosition: CGPoint?
  var dayPosition: CGPoint?
  let sprite = SKSpriteNode(imageNamed: "dayNight")

And when instantiating a skyBackground, we'll pass in our scene so it knows where to place its .sprite.

  init(scene: GameScene) {
    nightPosition = CGPoint(x: scene.frame.width / 2, y: 0.0)
    dayPosition = CGPoint(x: scene.frame.width / 2, y: scene.frame.height)

That's enough to test the two extremes by creating an SKAction to run in touchesBegan.

    skySprite = SkySprite(scene: self)
    skySprite.position = skySprite.dayPosition!
  override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
    let moveAction = SKAction.moveTo(skySprite.nightPosition!, duration: 2)

Since most of the day will not require any movement from day to night or night to day, the actual transition will take place over a smaller section of time.

6-8AM: Sunrise
6-8PM: Sunset

Over the course of two hours, skyBackground will need to increment or decrement its position.y 768 points, the height of the frame, depending on what portion of the 120 minutes we're in.

Whilst ugly, the computed variable shouldUpdate will let the skyBackground know whether or not its update() should fire.

  var now: NSDate { return NSDate() }
  var shouldUpdate: Bool {
    switch now.hour {
    case 6: return true
    case 7: return true
    case 18: return true
    case 19: return true
    default: return false

If the scene's update(currentTime: CFTimeInterval) calls skyBackground.update(), then it can check and set its position.y. Since 768 / 120 = 6.4, any minute we're at can be multiplied by 6.4 to get what point the background should be at.

  func update() {
    if shouldUpdate {
      var currentMinutes: Int = 0
      switch now.hour {
      case 6: currentMinutes = now.minute
      case 7: currentMinutes = now.minute + 60
      case 18: currentMinutes = now.minute
      case 19: currentMinutes = now.minute + 60
      default: return
      let newYPosition: CGFloat = CGFloat(currentMinutes) * 6.4
      switch now.hour {
      case 6...7: self.position.y = newYPosition
      case 18...19: self.position.y = 768 - newYPosition
      default: return
    } else {
      // Handling the rest of the day
      switch now.hour {
      case 0...5: self.position = nightPosition!
      case 8...17: self.position = dayPosition!
      case 20...24: self.position = nightPosition!
      default: return

With that clunky implementation, we can see that the sky stays dark when it should and lightens/darkens as the 120 minutes go by.