gpt4 book ai didi

ios - 游戏中的SpriteKit Shop场景

转载 作者:行者123 更新时间:2023-11-28 09:42:11 26 4
gpt4 key购买 nike

你知道我如何在我的spriteKit游戏中实现一个商店,用户可以用他们在游戏中赚取的硬币购买不同的玩家吗?有教程吗?

最佳答案

这是一个多步骤的项目,花了我大约500 loc(更多不使用.SKS)这里是链接到github完成的项目:https://github.com/fluidityt/ShopScene
注意,我正在使用macOS SpriteKit项目,因为它在我的计算机上启动得更快。只需将mouseDown()更改为touchesBegan()即可在iOS上运行。
首先编辑gamescope.sks如下:(节省大量时间编码标签)
enter image description here
确保您的名字与我们检测触摸所需的名称完全一致:
“entershop”、“getcoins”、“coinlabel”、“levellabel”
这是主要的“游戏”场景,当你点击coins++时,你就可以得到关卡并四处移动。点击商店将进入商店。
这是我们的gamescope.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
Player是spritenode子类(兼作数据模型和UI元素)。我们的播放器只是一个彩色的正方形,当你点击屏幕时它会移动
玩家穿的是 Costume类型的东西,这只是一个模型,它可以跟踪价格、名字和玩家要显示的纹理等数据。
这是古装。斯威夫特:
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() }
};

Player有很多函数,但它们都可以在代码的其他地方处理。我只是支持这个设计决策,但不觉得需要用两行方法加载类。
既然我们已经建立了:
基本场景
服装单
播放器对象
我们真正需要的最后两件事是:
一。跟踪库存的商店模型
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") }
};

我们的想法是让第四套服装只在某个级别上可用,但是我已经没有时间实现这个特性,但是大多数支持方法都在那里(您只需要实现逻辑)。
此外,商店几乎可以只是一个结构,但我觉得它作为一个班现在更灵活。
现在,在进入我们最大的文件ShopScene之前,让我告诉你一些设计决策。
首先,我使用 node.name来处理触摸/点击。这让我可以快速方便地使用.SKS和常规 SKNode类型。通常,我喜欢将SKNodes子类化,然后重写它们自己的 touchesBegan方法来处理单击。你两种方法都可以。
现在,在SuffField中,你有“购买”按钮,“退出”,我用它作为普通的SkabelEnter节点,但是对于显示服装的实际节点,我创建了一个名为 CostumeNode的子类。
我制作了服装节点,这样它就可以处理显示服装名称、价格和一些动画的节点。客户节点只是一个视觉元素(不像玩家)。
这里是customenode.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()(或者touchesbeartforios)中。我不需要 update()或其他每帧方法。
我是怎么做到的?第一步是计划,我知道有几个设计决策要做(可能不是最好的)。
我知道我的播放器和商店库存需要一组特定的数据,这两样东西也需要UI元素。
我选择将播放器的data+UI组合为Sprite子类。
对于商店,我知道数据和UI元素会非常密集,所以我将它们分开(shop.swift处理库存,Costume.swift作为蓝图,而costumnode.swift处理大部分UI)
然后,我需要将数据链接到UI元素,这意味着我需要很多逻辑,所以我决定创建一个全新的场景来处理与进入商店和与商店交互有关的逻辑(它也处理一些图形内容)。
这一切都是这样的:
玩家有服装和硬币
游戏场景是你收集新硬币(和等级)的地方
ShopScene处理大多数用于确定要显示哪些UI元素的逻辑,而CustomerNode具有设置UI动画的功能。
ShopScene还提供了通过Shop更新玩家纹理(服装)和硬币的逻辑。
Shop只管理玩家库存,并拥有数据来填充更多的客户节点
当你完成商店,你的游戏场景实例将立即恢复,在你进入之前离开的地方
所以你可能会问,“我该如何在游戏中使用它?”??"
好吧,你不能只是复制粘贴它。可能需要进行大量重构。这里的要点是学习创建、呈现和与商店交互所需的不同类型数据、逻辑和操作的基本系统。
这里又是github:
https://github.com/fluidityt/ShopScene

关于ios - 游戏中的SpriteKit Shop场景,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44357804/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com