How to Make a Waiting Game Like Farmville with SpriteKit and Swift

In this SpriteKit tutorial, you’ll learn how to make your very own waiting game — just like Farmville — with SpriteKit and Swift. By Kevin Colligan.

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

Switching States

Add the following method to StockItem.swift:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
  switch state {
  case .empty:
    let bought = gameDelegate.updateMoney(by: -stockingPrice * maxAmount)
    if bought {
      switchTo(state: .stocking)
    } else {
      let playSound = SKAction.playSoundFileNamed("hit.wav", waitForCompletion: true)
      run(playSound)
      
      let rotateLeft = SKAction.rotate(byAngle: 0.2, duration: 0.1)
      let rotateRight = rotateLeft.reversed()
      let shakeAction = SKAction.sequence([rotateLeft, rotateRight])
      let repeatAction = SKAction.repeat(shakeAction, count: 3)
      priceTag.run(repeatAction)
    }
  case .stocked:
    switchTo(state: .selling)
  default:
    break
  }
}

This method operates on the two states that allow user interaction: empty and stocked.

When empty, you first attempt to update the player’s money through gameDelegate. If the player has enough money to make the purchase, you then call switchTo(state:) to change the item’s state to stocking. If the player is short on funds, you let the player know by playing a sound effect and shaking the price tag.

To handle stocked, you simply call switchTo(state:) with the selling state. There are no additional conditions that need to be met for this transition, and this puts the item in a state where it will update over time.

Updating States Over Time

To update an item over time, you’ll first need to know the last time the state changed to calculate how much time has passed and how far along the stocking or selling process should be.

Add the following property to StockItem.swift, right after the state property:

private var lastStateSwitchTime: CFAbsoluteTime

You’ll use CFAbsoluteTime to refer to a specific point in time. Even if the player restarts the game you still need to know exactly when that event happened in order to update stock properly.

Add the following line to init(stockItemData:stockItemConfiguration:gameDelegate:) just before super.init(), to load the time of the last state change:

lastStateSwitchTime = stockItemData["lastStateSwitchTime"] as AnyObject? as! CFAbsoluteTime

And add the following line to data(), right before the return statement:

data["lastStateSwitchTime"] = lastStateSwitchTime

This line adds an entry for the last state-switch time to the data dictionary stored in gamedata.plist.

Now you need to make sure that lastStateSwitchTime is assigned the proper value while the game is running.

Add the following line of code to the beginning of switchTo(state:):

if self.state != state {
  lastStateSwitchTime = CFAbsoluteTimeGetCurrent()
}

This ensures that you’ve actually changed states. If so, then update lastStateSwitchTime to the current time. You can always get the current time using the ever-helpful CFAbsoluteTimeGetCurrent().

Stocking Your Items

You can use the absolute time of the last state-switch to show some progress indicators to your player. Start by updating the countdown that shows the player how long they need to wait for a purchased item to complete stocking.

Add the following method to StockItem.swift:

func updateStockingTimerText() {
  let stockingTimeTotal = CFTimeInterval(Float(maxAmount) * stockingSpeed)
  let currentTime = CFAbsoluteTimeGetCurrent()
  let timePassed = currentTime - lastStateSwitchTime
  let stockingTimeLeft = stockingTimeTotal - timePassed
  stockingTimer.text = String(format: "%.0f", stockingTimeLeft)
}

In this method, you set the text of the stockingTimer to the time remaining until stocking is complete. To get this value, you first calculate the amount of time it takes to fully stock the item. You do so by multiplying stockingSpeed and the maximal amount of the stock item and then cast it to CFTimeInterval. Next, you store the current time in a temporary variable to calculate how much time has passed since the last state change.

The time to restock the item is now simply the total time minus the time that has passed to this point:

This time bar shows you the different intervals and absolute times you are using.

This time bar shows you the different intervals and absolute times you are using.

Finally, you set the text to the remaining time, so the user can see when the item will be fully stocked. Since you only want to display whole seconds to your player you use the format specifier %.0f, which tells Swift to display a float variable with zero digits after the decimal.

Add the following method to StockItem.swift to update the display of the item during stocking and selling:

func update() {
  let currentTimeAbsolute = CFAbsoluteTimeGetCurrent()
  let timePassed = currentTimeAbsolute - lastStateSwitchTime
  switch state {
  case .stocking:
    updateStockingTimerText()
    amount = min(Int(Float(timePassed) / stockingSpeed), maxAmount)
    if amount == maxAmount {
      switchTo(state: .stocked)
    }
  default:
    break
  }
}

First, calculate how much time has passed since the last state-switch. If the item’s current state is stocking, you call the helper method updateStockingTimerText().

Next, you update the item amount which is simply the time elapsed divided by the stocking speed. Of course, the player can never stock more items than maxAmount, so you use min to limit the amount to maxAmount. Finally, you check whether the new amount is equal to maxAmount. If so, then change the state to stocked.

The only thing left to do is call update() for every stock item.

Add the following method override in GameScene.swift at the bottom of GameScene class as follows:

override func update(_ currentTime: TimeInterval) {
  for stockItem in stockItems {
    stockItem.update()
  }
}

Build and run your project. Tap on a stock item and you’ll see the timer count down to zero. Then the item switches to the stocked state. That coin in front of the cookies indicates that they are ready to be sold.

Countdown

Selling Items

As soon as an item is fully stocked, players can start selling it. Add the following code to update() in StockItem.swift, inside the case statement right before the default case:

case .selling:
  let previousAmount = amount
  amount = maxAmount - min(maxAmount, Int(Float(timePassed) / sellingSpeed))
  let amountSold = previousAmount - amount
  if amountSold >= 1 {
    let _ = gameDelegate.updateMoney(by: sellingPrice * amountSold)
    progressBar.setProgress(percentage: Float(amount) / Float(maxAmount))
    if amount <= 0 {
      switchTo(state: .empty)
    }
  }

First, you store the current amount of the item in previousAmount. You then calculate the new amount by subtracting the quotient of timePassed and sellingSpeed from maxAmount. Again, you need to limit the number of items that can be sold to maxAmount. Now, the number of items sold is simply the difference between the previous amount and the new amount.

In order to limit the number of calls to progressBar and gameDelegate, you check whether at least one item has been sold since the last call to update. If so, notify gameDelegate about the change in the player's funds, then set the progress bar to the value of the amount sold divided by the maximum amount available. This change in money will always be positive, so you can ignore the result of updateMoney(by:) here.

Finally, you check whether the stock item sold out by comparing the amount remaining to 0. When the item is sold out, set the state back to empty. Your state machine is now complete.

Build, run and buy some cookies! Click on the coin to start selling. You'll see your cookies sell over time, fattening your wallet:

w00t I sold a cookie — I'm rich!

w00t I sold a cookie — I'm rich!
Kevin Colligan

Contributors

Kevin Colligan

Author

Kyle Gorlick

Tech Editor

Chris Language

Final Pass Editor

Tammy Coron

Team Lead

Over 300 content creators. Join our team.