0

Any idea how I could implement a shop in my spriteKit game that users could buy different players with coins they have earned in game? any tutorials out there?

5
  • I'm working on an answer for you but it will take me some time.
    – Fluidity
    Commented Jun 5, 2017 at 6:57
  • Thanks m8, means a lot
    – crow
    Commented Jun 5, 2017 at 11:52
  • this is turning out to be a lot more code than I thought, and I can see how this would be a good tutorial for someone to make. It's not a crazy amount of work, but there are many different approaches to take here. I'm already up to 300 lines of code, for just one page of costumes you can put on your character... and not finished yet.
    – Fluidity
    Commented Jun 5, 2017 at 11:57
  • basically you have your Player which keep track of money and levels cleared etc, your Shop which is a model and has no UI elements (it handles the logic between player and costumes and the ui), ShopScene which presents all the buttons and sprites and such, and then your base GameScene which transitions to shopscene and back when you are done.
    – Fluidity
    Commented Jun 5, 2017 at 11:59
  • Any way I could contact you other than stack, I could show you my code
    – crow
    Commented Jun 5, 2017 at 12:00

1 Answer 1

6

This is a multi-step project that took me about 500 loc (more without using .SKS) Here is the link to github finished project: https://github.com/fluidityt/ShopScene

Note, I am using a macOS SpriteKit project because it launches much faster on my computer. Simply change mouseDown() to touchesBegan() to get this to run on iOS.

First edit your GameScene.sks to look like this: (saves a bunch of time coding labels) enter image description here

Make sure that you name everything EXACTLY as we need this to detect touch:

"entershop", "getcoins", "coinlabel", "levellabel"

This is the main "gameplay" scene and as you click coins++ you get levels and can move around. Clicking the shop will enter the shop.

Here is our GameScene.swift which matches this SKS:


import SpriteKit

class GameScene: SKScene {

  let player = Player(costume: Costume.defaultCostume)

  lazy var enterNode:  SKLabelNode = { return (self.childNode(withName: "entershop")  as! SKLabelNode) }()
  lazy var coinNode:   SKLabelNode = { return (self.childNode(withName: "getcoins" )  as! SKLabelNode) }()
  lazy var coinLabel:  SKLabelNode = { return (self.childNode(withName: "coinlabel")  as! SKLabelNode) }()
  lazy var levelLabel: SKLabelNode = { return (self.childNode(withName: "levellabel") as! SKLabelNode) }()

  override func didMove(to view: SKView) {
    player.name = "player"
    if player.scene == nil { addChild(player) }
  }

  override func mouseDown(with event: NSEvent) {

    let location = event.location(in: self)

    if let name = atPoint(location).name {

      switch name {

      case "entershop": view!.presentScene(ShopScene(previousGameScene: self))

      case "getcoins":  player.getCoins(1)

      default: ()
      }
    }

    else {
      player.run(.move(to: location, duration: 1))
    }
  }

  override func update(_ currentTime: TimeInterval) {

    func levelUp(_ level: Int) {
      player.levelsCompleted = level
      levelLabel.text = "Level: \(player.levelsCompleted)"
    }

    switch player.coins {
      case 10: levelUp(2)
      case 20: levelUp(3)
      case 30: levelUp(4)
      default: ()
    }
  }
};

Here you can see that we have a few other things going on not yet introduced: Player and Costume

Player is a spritenode subclass (it doubles as a data model and a UI element). Our player is just a colored square that gets moved around when you click the screen

The player wears something of Costume type, which is just a model that keeps track of data such as price, name, and the texture for the player to display.

Here is Costume.swift:


import SpriteKit

/// This is just a test method should be deleted when you have actual texture assets:
private func makeTestTexture() -> (SKTexture, SKTexture, SKTexture, SKTexture) {

  func texit(_ sprite: SKSpriteNode) -> SKTexture { return SKView().texture(from: sprite)! }
  let size = CGSize(width: 50, height: 50)

  return (
    texit(SKSpriteNode(color: .gray,  size: size)),
    texit(SKSpriteNode(color: .red,   size: size)),
    texit(SKSpriteNode(color: .blue,  size: size)),
    texit(SKSpriteNode(color: .green, size: size))
  )
}

/// The items that are for sale in our shop:
struct Costume {

  static var allCostumes: [Costume] = []

  let name:    String
  let texture: SKTexture
  let price:   Int

  init(name: String, texture: SKTexture, price: Int) { self.name = name; self.texture = texture; self.price = price
    // This init simply adds all costumes to a master list for easy sorting later on.
    Costume.allCostumes.append(self)
  }

  private static let (tex1, tex2, tex3, tex4) = makeTestTexture()  // Just a test needed to be deleted when you have actual assets.

  static let list = (
    // Hard-code any new costumes you create here (this is a "master list" of costumes)
    // (make sure all of your costumes have a unique name, or the program will not work properly)
    gray:  Costume(name: "Gray Shirt",  texture: tex1 /*SKTexture(imageNamed: "grayshirt")*/,  price:  0),
    red:   Costume(name: "Red Shirt",   texture: tex2 /*SKTexture(imageNamed: "redshirt")*/,   price: 5),
    blue:  Costume(name: "Blue Shirt",  texture: tex3 /*SKTexture(imageNamed: "blueshirt")*/,  price: 25),
    green: Costume(name: "Green Shirt", texture: tex4 /*SKTexture(imageNamed: "greenshirt")*/, price: 50)
  )

  static let defaultCostume = list.gray
};

func == (lhs: Costume, rhs: Costume) -> Bool {
  // The reason why you need unique names:
  if lhs.name == rhs.name { return true }
  else { return false }
}

The design of this struct is twofold.. first is to be a blueprint for a Costume object (which holds the name, price, and texture of a costume), and second it serves as a repository for all of your costumes via a hard-coded static master list property.

The function at the top makeTestTextures() is just an example for this project. I did this just so that way you can copy and paste instead of having to download image files to use.

Here is the Player.swift, which can wear the costumes in the list:


final class Player: SKSpriteNode {

  var coins = 0
  var costume: Costume
  var levelsCompleted = 0

  var ownedCostumes: [Costume] = [Costume.list.gray]      // FIXME: This should be a Set, but too lazy to do Hashable.

  init(costume: Costume) {
    self.costume = costume
    super.init(texture: costume.texture, color: .clear, size: costume.texture.size())
  }

  func getCoins(_ amount: Int) {
    guard let scene = self.scene as? GameScene else {     // This is very specific code just for this example.
      fatalError("only call this func after scene has been set up")
    }

    coins += amount
    scene.coinLabel.text = "Coins: \(coins)"
  }

  func loseCoins(_ amount: Int) {
    guard let scene = self.scene as? GameScene else {     // This is very specific code just for this example.
      fatalError("only call this func after scene has been set up")
    }

    coins -= amount
    scene.coinLabel.text = "Coins: \(coins)"
  }

  func hasCostume(_ costume: Costume) -> Bool {
    if ownedCostumes.contains(where: {$0.name == costume.name}) { return true }
    else { return false }
  }

  func getCostume(_ costume: Costume) {
    if hasCostume(costume) { fatalError("trying to get costume already owned") }
    else { ownedCostumes.append(costume) }
  }

  func wearCostume(_ costume: Costume) {
    guard hasCostume(costume) else { fatalError("trying to wear a costume you don't own") }
    self.costume = costume
    self.texture = costume.texture
  }

  required init?(coder aDecoder: NSCoder) { fatalError() }
};

Player has a lot of functions, but they all could be handled elsewhere in the code. I just went for this design decision, but don't feel like you need to load up your classes with 2 line methods.

Now we are getting to the more nitty-gritty stuff, since we have set up our:

  • Base scene
  • Costume list
  • Player object

The last two things we really need are: 1. A shop model to keep track of inventory 2. A shop scene to display inventory, UI elements, and handle the logic of whether or not you can buy items

Here is Shop.swift:


/// Our model class to be used inside of our ShopScene:
final class Shop {

  weak private(set) var scene: ShopScene!     // The scene in which this shop will be called from.

  var player: Player { return scene.player }

  var availableCostumes: [Costume] = [Costume.list.red, Costume.list.blue]   // (The green shirt wont become available until the player has cleared 2 levels).

  // var soldCostumes: [Costume] = [Costume.defaultCostume] // Implement something with this if you want to exclude previously bought items from the store.

  func canSellCostume(_ costume: Costume) -> Bool {
    if player.coins < costume.price                { return false }
    else if player.hasCostume(costume)             { return false }
    else if player.costume == costume              { return false }
    else                                           { return true  }
  }

  /// Only call this after checking canBuyCostume(), or you likely will have errors:
  func sellCostume(_ costume: Costume) {
    player.loseCoins(costume.price)
    player.getCostume(costume)
    player.wearCostume(costume)
  }

  func newCostumeBecomesAvailable(_ costume: Costume) {
    if availableCostumes.contains(where: {$0.name == costume.name}) /*|| soldCostumes.contains(costume)*/ {
      fatalError("trying to add a costume that is already available (or sold!)")
    }
    else { availableCostumes.append(costume) }
  }

  init(shopScene: ShopScene) {
    self.scene = shopScene
  }

  deinit { print("shop: if you don't see this message when exiting shop then you have a retain cycle") }
};

The idea was to have the fourth costume only be available at a certain level, but I've run out of time to implement this feature, but most of the supporting methods are there (you just need to implement the logic).

Also, Shop can pretty much just be a struct, but I feel that it's more flexible as a class for now.

Now, before jumping into ShopScene, our biggest file, let me tell you about a couple of design decisions.

First, I'm using node.name to handle touches / clicks. This lets me use the .SKS and the regular SKNode types quickly and easily. Normally, I like to subclass SKNodes and then override their own touchesBegan method to handle clicks. You can do it either way.

Now, in ShopScene you have buttons for "buy", "exit" which I have used as just regular SKLabelNodes; but for the actual nodes that display the costume, I have created a subclass called CostumeNode.

I made CostumeNode so that way it could handle nodes for displaying the costume's name, price, and doing some animations. CostumeNode is just a visual element (unlike Player).

Here is CostumeNode.swift:


/// Just a UI representation, does not manipulate any models.
final class CostumeNode: SKSpriteNode {

  let costume:   Costume

  weak private(set) var player: Player!

  private(set) var
  backgroundNode = SKSpriteNode(),
  nameNode       = SKLabelNode(),
  priceNode      = SKLabelNode()

  private func label(text: String, size: CGSize) -> SKLabelNode {
    let label = SKLabelNode(text: text)
    label.fontName = "Chalkduster"
    // FIXME: deform label to fit size and offset
    return label
  }

  init(costume: Costume, player: Player) {

     func setupNodes(with size: CGSize) {

      let circle = SKShapeNode(circleOfRadius: size.width)
      circle.fillColor = .yellow
      let bkg = SKSpriteNode(texture: SKView().texture(from: circle))
      bkg.zPosition -= 1

      let name = label(text: "\(costume.name)", size: size)
      name.position.y = frame.maxY + name.frame.size.height

      let price = label(text: "\(costume.price)", size: size)
      price.position.y = frame.minY - price.frame.size.height

      addChildrenBehind([bkg, name, price])
      (backgroundNode, nameNode, priceNode) = (bkg, name, price)
    }

    self.player = player
    self.costume = costume

    let size = costume.texture.size()
    super.init(texture: costume.texture, color: .clear, size: size)

    name = costume.name   // Name is needed for sorting and detecting touches.

    setupNodes(with: size)
    becomesUnselected()
  }

  private func setPriceText() { // Updates the color and text of price labels

    func playerCanAfford() {
      priceNode.text = "\(costume.price)"
      priceNode.fontColor = .white
    }

    func playerCantAfford() {
      priceNode.text = "\(costume.price)"
      priceNode.fontColor = .red
    }

    func playerOwns() {
      priceNode.text = ""
      priceNode.fontColor = .white
    }

    if player.hasCostume(self.costume)         { playerOwns()       }
    else if player.coins < self.costume.price  { playerCantAfford() }
    else if player.coins >= self.costume.price { playerCanAfford()  }
    else                                       { fatalError()       }
  }

  func becomesSelected() {    // For animation / sound purposes (could also just be handled by the ShopScene).
    backgroundNode.run(.fadeAlpha(to: 0.75, duration: 0.25))
    setPriceText()
    // insert sound if desired.
  }

  func becomesUnselected() {
    backgroundNode.run(.fadeAlpha(to: 0, duration: 0.10))
    setPriceText()
    // insert sound if desired.
  }

  required init?(coder aDecoder: NSCoder) { fatalError() }

  deinit { print("costumenode: if you don't see this then you have a retain cycle") }
};

Finally we have ShopScene, which is the behemoth file. It handles the data and logic for not only showing UI elements, but also for updating the Shop and Player models.


import SpriteKit

// Helpers:
extension SKNode {
  func addChildren(_ nodes: [SKNode]) { for node in nodes { addChild(node) } }

  func addChildrenBehind(_ nodes: [SKNode]) { for node in nodes {
    node.zPosition -= 2
    addChild(node)
    }
  }
}
 func halfHeight(_ node: SKNode) -> CGFloat { return node.frame.size.height/2 }
 func halfWidth (_ node: SKNode) -> CGFloat { return node.frame.size.width/2 }


// MARK: -
/// The scene in which we can interact with our shop and player:
class ShopScene: SKScene {

  lazy private(set) var shop: Shop = { return Shop(shopScene: self) }()

  let previousGameScene: GameScene

  var player: Player { return self.previousGameScene.player }    // The player is actually still in the other scene, not this one.

  private var costumeNodes = [CostumeNode]()                   // All costume textures will be node-ified here.

  lazy private(set) var selectedNode: CostumeNode? = {
    return self.costumeNodes.first!
  }()

  private let
  buyNode  = SKLabelNode(fontNamed: "Chalkduster"),
  coinNode = SKLabelNode(fontNamed: "Chalkduster"),
  exitNode = SKLabelNode(fontNamed: "Chalkduster")

  // MARK: - Node setup:
  private func setUpNodes() {

    buyNode.text = "Buy Costume"
    buyNode.name = "buynode"
    buyNode.position.y = frame.minY + halfHeight(buyNode)

    coinNode.text = "Coins: \(player.coins)"
    coinNode.name = "coinnode"
    coinNode.position = CGPoint(x: frame.minX + halfWidth(coinNode), y: frame.minY + halfHeight(coinNode))

    exitNode.text = "Leave Shop"
    exitNode.name = "exitnode"
    exitNode.position.y = frame.maxY - buyNode.frame.height

    setupCostumeNodes: do {
      guard Costume.allCostumes.count > 1 else {
        fatalError("must have at least two costumes (for while loop)")
      }
      for costume in Costume.allCostumes {
        costumeNodes.append(CostumeNode(costume: costume, player: player))
      }
      guard costumeNodes.count == Costume.allCostumes.count else {
        fatalError("duplicate nodes found, or nodes are missing")
      }

      let offset = CGFloat(150)

      func findStartingPosition(offset: CGFloat, yPos: CGFloat) -> CGPoint {   // Find the correct position to have all costumes centered on screen.
        let
        count = CGFloat(costumeNodes.count),
        totalOffsets = (count - 1) * offset,
        textureWidth = Costume.list.gray.texture.size().width,                 // All textures must be same width for centering to work.
        totalWidth = (textureWidth * count) + totalOffsets

        let measurementNode = SKShapeNode(rectOf: CGSize(width: totalWidth, height: 0))

        return CGPoint(x: measurementNode.frame.minX + textureWidth/2, y: yPos)
      }

      costumeNodes.first!.position = findStartingPosition(offset: offset, yPos: self.frame.midY)

      var counter = 1
      let finalIndex = costumeNodes.count - 1
      // Place nodes from left to right:
      while counter <= finalIndex {
        let thisNode = costumeNodes[counter]
        let prevNode = costumeNodes[counter - 1]

        thisNode.position.x = prevNode.frame.maxX + halfWidth(thisNode) + offset
        counter += 1
      }
    }

    addChildren(costumeNodes)
    addChildren([buyNode, coinNode, exitNode])
  }

  // MARK: - Init:
  init(previousGameScene: GameScene) {
    self.previousGameScene = previousGameScene
    super.init(size: previousGameScene.size)
  }

  required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented")}

  deinit { print("shopscene: if you don't see this message when exiting shop then you have a retain cycle") }

  // MARK: - Game loop:
  override func didMove(to view: SKView) {
    anchorPoint = CGPoint(x: 0.5, y: 0.5)
    setUpNodes()

    select(costumeNodes.first!)                           // Default selection.
    for node in costumeNodes {
      if node.costume == player.costume { select(node) }
    }
  }

  // MARK: - Touch / Click handling:
  private func unselect(_ costumeNode: CostumeNode) {
    selectedNode = nil
    costumeNode.becomesUnselected()
  }

  private func select(_ costumeNode: CostumeNode) {
    unselect(selectedNode!)
    selectedNode = costumeNode
    costumeNode.becomesSelected()

    if player.hasCostume(costumeNode.costume) {      // Wear selected costume if owned.
      player.costume = costumeNode.costume
      buyNode.text = "Bought Costume"
      buyNode.alpha = 1
    }

    else if player.coins < costumeNode.costume.price { // Can't afford costume.
      buyNode.text = "Buy Costume"
      buyNode.alpha = 0.5
    }

    else {                                            // Player can buy costume.
      buyNode.text = "Buy Costume"
      buyNode.alpha = 1
      }
  }

  // I'm choosing to have the buttons activated by searching for name here. You can also
  // subclass a node and have them do actions on their own when clicked.
  override func mouseDown(with event: NSEvent) {

    guard let selectedNode = selectedNode else { fatalError() }
    let location    = event.location(in: self)
    let clickedNode = atPoint(location)

    switch clickedNode {

      // Clicked empty space:
      case is ShopScene:
        return

      // Clicked Buy / Leave:
      case is SKLabelNode:
        if clickedNode.name == "exitnode" { view!.presentScene(previousGameScene) }

        if clickedNode.name == "buynode"  {
          // guard let shop = shop else { fatalError("where did the shop go?") }
          if shop.canSellCostume(selectedNode.costume) {
            shop.sellCostume(selectedNode.costume)
            coinNode.text = "Coins: \(player.coins)"
            buyNode.text = "Bought"
          }
        }

      // Clicked a costume:
      case let clickedCostume as CostumeNode:
        for node in costumeNodes {
          if node.name == clickedCostume.name {
            select(clickedCostume)
          }
        }

      default: ()
      }
  }
};

There's a lot to digest here, but pretty much everything happens in mouseDown() (or touchesBegan for iOS). I had no need for update() or other every-frame methods.

So how did I make this? The first step was planning, and I knew there were several design decisions to make (which may not have been the best ones).

I knew that I needed a certain set of data for my player and shop inventory, and that those two things would also need UI elements.

I chose to combine the data + UI for Player by making it a Sprite subclass.

For the shop, I knew that the data and UI elements would be pretty intense, so I separated them (Shop.swift handling the inventory, Costume.swift being a blueprint, and CostumeNode.swift handling most of the UI)

Then, I needed to link the data to the UI elements, which meant that I needed a lot of logic, so I decided to make a whole new scene to handle logic pertaining just to entering and interacting with the shop (it handles some graphics stuff too).

This all works together like this:

  • Player has a costume and coins
  • GameScene is where you collect new coins (and levels)
  • ShopScene handles most of the logic for determining which UI elements to display, while CostumeNode has the functions for animating the UI.
  • ShopScene also provides the logic for updating the Player's texture (costume) and coins through Shop.
  • Shop just manages the player inventory, and has the data with which to populate more CostumeNodes
  • When you are done with the shop, your GameScene instance is immediately resumed where you left off prior to entering

So the question you may have is, "how do I use this in my game??"

Well, you aren't going to be able to just copy and paste it. A lot of refactoring will likely be needed. The takeaway here is to learn the basic system of the different types of data, logic, and actions that you will need to create, present, and interact with a shop.

Here is the github again: https://github.com/fluidityt/ShopScene

1
  • 1
    Well done! You really went above and beyond on this one.
    – peacetype
    Commented Mar 16, 2018 at 9:57

Not the answer you're looking for? Browse other questions tagged or ask your own question.