游戏中的SpriteKit Shop场景

时间:2017-06-04 19:05:57

标签: ios swift swift3 sprite-kit

任何想法如何在我的spriteKit游戏中实现一个商店,用户可以用他们在游戏中获得的硬币购买不同的玩家?那里有任何教程吗?

1 个答案:

答案 0 :(得分:6)

这是一个多步项目,花了我大约500个loc(更多没有使用.SKS)这里是github完成项目的链接:https://github.com/fluidityt/ShopScene

注意,我使用的是macOS SpriteKit项目,因为它在我的计算机上启动速度更快。只需将mouseDown()更改为touchesBegan()即可在iOS上运行。

首先编辑你的GameScene.sks看起来像这样:(保存一堆时间编码标签) enter image description here

确保您完全按照我们的需要命名一切来检测触摸:

"进入商店"," getcoins"," coinlabel"," levellabel"

这是主要的游戏玩法"场景,当你点击硬币++,你得到水平,可以四处走动。点击商店将进入商店。

这是我们与本SKS匹配的GameScene.swift:

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: ()
    }
  }
};

您可以在此处看到我们还有其他一些内容尚未介绍:PlayerCostume

Player是一个spritenode子类(它兼作数据模型和UI元素)。我们的播放器只是一个彩色方块,当您点击屏幕时会移动它

玩家佩戴Costume类型的东西,这只是一个跟踪玩家要显示的价格,名称和纹理等数据的模型。

这是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 }
}

这个结构的设计是双重的。首先是作为服装对象的蓝图(它包含服装的名称,价格和纹理),然后它作为所有服装的存储库通过硬编码的静态主列表属性。

顶部makeTestTextures()的功能只是这个项目的一个例子。我这样做只是为了你可以复制和粘贴,而不必下载要使用的图像文件。

这是Player.swift,它可以穿着列表中的服装:

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() }
};

播放器有很多功能,但它们都可以在代码中的其他地方处理。我只是去做这个设计决定,但不要觉得你需要用2行方法加载你的课程。

现在我们已经开始研究更多细节了,因为我们设置了:

  • 基础场景
  • 服装清单
  • 玩家对象

我们真正需要的最后两件事是: 1.跟踪库存的商店模型 2.商店场景,显示库存,UI元素,并处理您是否可以购买物品的逻辑

这是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") }
};

我的想法是让第四套服装只在某个级别上可用,但我已经没时间实现这个功能了,但大多数支持方法都在那里(你只需要实现逻辑) )。

此外,Shop几乎可以只是一个结构,但我觉得它现在作为一个类更灵活。

现在,在进入我们最大的文件ShopScene之前,让我告诉你一些设计决定。

首先,我使用node.name来处理触摸/点击。这使我可以快速轻松地使用.SKS和常规SKNode类型。通常,我喜欢子类化SKNodes,然后覆盖他们自己的touchesBegan方法来处理点击。你可以这样做。

现在,在ShopScene中,您可以按下&#34;购买&#34;,&#34;退出&#34;我用作普通的SKLabelNodes;但是对于显示服装的实际节点,我创建了一个名为CostumeNode的子类。

我制作了CostumeNode,以便它可以处理节点以显示服装的名称,价格和做一些动画。 CostumeNode只是一个可视元素(与Player不同)。

这是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") }
};

最后我们有ShopScene,这是庞然大物的文件。它处理数据和逻辑,不仅可以显示UI元素,还可以更新商店和玩家模型。

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: ()
      }
  }
};

这里有很多要消化的东西,但几乎所有事情都发生在mouseDown()(或者触摸iOS版)。我不需要update()或其他每帧方法。

那我是怎么做到的?第一步是计划,我知道有几个设计决策要做(可能不是最好的)。

我知道我的播放器和商店库存需要一组特定的数据,这两件事也需要UI元素。

我选择将Player的数据+ UI组合成一个Sprite子类。

对于商店,我知道数据和UI元素会非常激烈,所以我将它们分开(Shop.swift处理库存,Costume.swift是蓝图,CostumeNode.swift处理大部分UI)< / p>

然后,我需要将数据链接到UI元素,这意味着我需要很多逻辑,所以我决定创建一个全新的场景来处理与进入商店和与商店交互相关的逻辑(它处理一些图形的东西也。)

这一切都是这样的:

  • 玩家有服装和硬币
  • GameScene是您收集新硬币(和级别)的地方
  • ShopScene处理大部分逻辑,用于确定要显示的UI元素,而CostumeNode具有动画UI的功能。
  • ShopScene还提供了通过商店更新玩家纹理(服装)和硬币的逻辑。
  • Shop只管理播放器广告资源,并具有用于填充更多CostumeNodes
  • 的数据
  • 当您完成商店后,您的GameScene实例会在您进入之前立即恢复到您停止的位置

所以你可能有的问题是,&#34;我如何在游戏中使用它?&#34;

嗯,你不能复制并粘贴它。很可能需要进行大量的重构。这里要介绍的是学习创建,展示和与商店互动所需的不同类型数据,逻辑和操作的基本系统。

这是github: https://github.com/fluidityt/ShopScene