2017-06-04 531 views
0

任何想法如何在我的spriteKit遊戲中實現一家商店,用戶可以用他們在遊戲中獲得的硬幣購買不同的玩家?那裏有任何教程?SpriteKit遊戲中的店鋪場景

+0

我正在給你一個答案,但它會帶我一段時間。 – Fluidity

+0

感謝m8,意味着很多 – ccronheimer

+0

這是比我想象的要多得多的代碼,我可以看到這將是一個很好的教程,供人們使用。這不是一個瘋狂的工作量,但有很多不同的方法可以採取。我已經達到了300行代碼,只有一頁服飾可以放在你的角色上......還沒有完成。 – Fluidity

回答

3

這是一個多步驟的項目,我花了大約500 LOC(更不用.SKS)這裏是GitHub上完成的項目鏈接:https://github.com/fluidityt/ShopScene

請注意,我用的是MacOS的SpriteKit項目,因爲它啓動在我的電腦上快得多。只需將mouseDown()更改爲touchesBegan()即可使其在iOS上運行。

首先編寫GameScene.sks看起來像這樣:(節省了一堆時間編碼標籤) enter image description here

確保你的名字的一切正是因爲我們需要這個來檢測觸摸:

「 「buycoins」,「getcoins」,「coinlabel」,「levellabel」

這是主要的「遊戲玩法」場景,當你點擊硬幣++時,你會得到關卡並可以四處移動。點擊商店將進入商店。

這裏是我們GameScene.swift此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:() 
    } 
    } 
}; 

在這裏你可以看到,我們有一些其他的東西怎麼回事尚未出臺:PlayerCostume

播放器是一個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行方法加載你的類。

現在我們正在向更多的基本事實的東西,因爲我們已經建立了我們:

  • 基地現場
  • 服裝名單
  • Player對象

最後兩我們真正需要的東西是: 1.跟蹤庫存的商店模型 2.顯示庫存,UI元素和處理邏輯的商店場景是否或n加時賽,你可以買到的物品

這裏是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中,您可以使用「buy」,「exit」按鈕,我只用它作爲常規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元素,還用於更新Shop和Player模型。


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()(或的touchesBegan iOS設備)。我不需要update()或其他每幀方法。

那麼我是怎麼做到的呢?第一步是規劃,我知道有幾個設計決策要做(這可能不是最好的)。

我知道我需要一些用於播放器和商店庫存的數據,而且這兩件事情也需要UI元素。

我選擇將Player +的數據+用戶界面組合成一個Sprite子類。我知道數據和UI元素會非常強烈,所以我把它們分開了(Shop.swift處理庫存,Costume.swift是一個藍圖,CostumeNode.swift處理大部分UI)然後,我需要將數據鏈接到UI元素,這意味着我需要很多邏輯,所以我決定創建一個全新的場景來處理僅僅進入和與店鋪互動的邏輯(它處理一些圖形的東西)。

這一切工作在一起,就像這樣:

  • 播放器有一個服裝和硬幣
  • GameScene是你收集新幣(和級別)
  • ShopScene處理大部分的邏輯決定哪些UI元素來顯示,而CostumeNode具有動畫UI的功能。
  • ShopScene還提供了通過Shop更新玩家紋理(服裝)和硬幣的邏輯。
  • 店鋪只能勉強播放器庫存,並具有用於填充更多CostumeNodes
  • 當你與店做,你GameScene實例立即恢復,你進入

之前離開該數據所以你可能會遇到的問題是,「我怎麼在我的遊戲中使用它?」

那麼,你不能複製和粘貼它。很可能需要很多重構。這裏的要點是要學習您需要創建,呈現和與商店互動所需的不同類型的數據,邏輯和操作的基本系統。

這裏是github上再次: https://github.com/fluidityt/ShopScene