在本教程中,您将使用Swift 3在SpriteKit中创建一个二十一点游戏。您将学习实现触摸,创建视觉动画以及许多其他概念,这些概念在构建SpriteKit游戏时会派上用场。
1.创建项目并导入资源
打开Xcode,然后选择“ 创建新的Xcode项目”,或从“ 文件”菜单中选择“ 新建”>“项目... ”。 确保选择了iOS ,然后选择“ 游戏”模板。
接下来,选择所需的产品名称 , 组织名称和组织标识符 。 确保“ 语言”设置为“ Swift” ,“ 游戏技术”设置为“ SpriteKit” ,“ 设备”设置为iPad 。
指定保存项目文件的位置,然后单击创建 。
导入助手类
下载此项目的GitHub存储库。 在其中,您将看到一个classes文件夹。 打开此文件夹,然后将所有文件拖到名称为项目名称的文件夹上,例如blackjack 。 确保选中“ 如果需要复制项目”以及目标列表中的主要目标。
导入图像
在教程GitHub存储库中也有一个名为教程图像的文件夹。 在项目导航器中,打开Assets.xcassets并将所有图像拖到侧栏中。 Xcode将自动从这些图像创建纹理图集。
2.设置项目
在项目导航器中,可以删除两个文件( Gamescene.sks和Actions.sks )。 删除这两个文件,然后选择“ 移至废纸rash” 。 这些文件由Xcode的内置场景编辑器使用,该编辑器可用于可视化地布局您的项目。 但是,我们将通过代码创建所有内容,因此不需要这些文件。
打开GameViewController.swift ,删除其内容,并将其替换为以下内容。
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size:CGSize(width: 768, height: 1024))
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = false
scene.scaleMode = .aspectFill
skView.presentScene(scene)
}
override var prefersStatusBarHidden: Bool {
return true
}
}
GameViewController
类继承自UIViewController
,并将以SKView
作为其视图。 在viewDidLoad
方法内部,我们使用as!
将view
属性向下转换为SKView
实例as!
键入强制转换运算符,然后配置视图。
如果要在重新创建该项目时运行它,则可能会注意到屏幕右下方的文本。 这就是showsFPS
和showsNodeCount
属性的用途,显示游戏正在运行的每秒帧数以及场景中可见的SKNodes
数。 我们不需要此信息,因此将它们设置为false
。
ignoreSiblingOrder
属性用于确定游戏中SKNode
的绘制顺序。 我们在此将其设置为false
是因为我们需要我们的SKNodes
绘制它们添加到场景中的顺序。
最后,我们将缩放模式设置为.aspectFill
,这将导致场景的内容缩放以填满整个屏幕。 然后,我们调用presentScene(_:)
的方法skView
呈现或“表演”的场景。
接下来,删除GameScene.swift中的所有内容并将其替换为以下内容。
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
}
现在,您可以测试项目,并且应该出现空白的黑屏。 在下一步中,我们将开始向场景添加内容。
3.变量和常量
在GameScene
继承自SKScene
位置下面的GameScene
类的开始处输入以下代码。
class GameScene: SKScene {
let moneyContainer = SKSpriteNode(color: .clear, size: CGSize(width:250, height: 150))
let dealBtn = SKSpriteNode(imageNamed: "deal_btn")
let hitBtn = SKSpriteNode(imageNamed: "hit_btn")
let standBtn = SKSpriteNode(imageNamed: "stand_btn")
let money10 = Money(moneyValue: .ten)
let money25 = Money(moneyValue: .twentyFive)
let money50 = Money(moneyValue: .fifty)
let instructionText = SKLabelNode(text: "Place your bet")
我们在这里创建许多SKSpriteNode
。 SKSpriteNode
用于创建彩色节点,或更常见的是从SKTexture
创建彩色节点, SKTexture
通常是图像。 我们使用便捷的初始化程序init(color:size:)
创建一个清晰的彩色节点moneyContainer
。 moneyContainer
将用于持有玩家下注的钱,并且在每轮结束时,我们将为向谁赢得比赛的方向设置动画。 将所有资金放置在该单个节点中,可以轻松地一次动画化所有资金。
接下来,我们创建常量dealBtn
, hitBtn
和standBtn
。 顾名思义,它们将分别在游戏中用于发动,击中和站立。 我们正在使用便捷的初始化程序init(imageNamed:)
,它以不带扩展名的图像名称作为参数。
然后,我们创建三个常量money10
, money25
和money50
,它们是Money
类型。 Money
是扩展SKSpriteNode
的自定义类,并根据作为参数传递的moneyValue
的类型创建三种不同的Money类型之一。 moneyValue
参数的类型为MoneyValue
,这是一个enum
。 看一下项目GitHub存储库中的Money
类,看看这一切如何工作。
最后,我们使用便利初始化程序init(text:)
创建一个SKLabelNode
,该初始化程序将要在标签内显示的文本作为参数。
4.实现setupTable
在didMove(to:)
函数下面添加以下内容。
func setupTable(){
let table = SKSpriteNode(imageNamed: "table")
addChild(table)
table.position = CGPoint(x: size.width/2, y: size.height/2)
table.zPosition = -1
addChild(moneyContainer)
moneyContainer.anchorPoint = CGPoint(x:0, y:0)
moneyContainer.position = CGPoint(x:size.width/2 - 125, y:size.height/2)
instructionText.fontColor = UIColor.black
addChild(instructionText)
instructionText.position = CGPoint(x: size.width/2, y: 400)
}
在这里,我们初始化一个常数table
并使用addChild(_:)
将其添加到场景中,该对象将要添加到场景中的节点作为参数。 我们设置table
在场景中的position
,并将其zPosition
设置为-1
。 zPosition
属性控制绘制节点的顺序。 首先绘制最低的数字,依次绘制更大的数字。 因为我们需要其他所有table
都在下面,所以我们将其zPosition
设置为-1
。 这样可以确保在其他任何节点之前绘制它。
我们还将moneyContainer
和instructionText
添加到场景中。 我们将instructionText
fontColor
设置为黑色(默认为白色)。
将didMove(to:)
更新为以下内容。
override func didMove(to view: SKView) {
setupTable()
}
视图呈现场景后立即调用didMove(to:)
方法。 通常,在这里您将为场景进行设置并创建资产。 如果现在进行测试,则应该看到该table
和instructionText
文本已添加到场景中。 moneyContainer
那里,但是您看不到它,因为我们用清晰的颜色创建了它。
5.实施setupMoney
在setupTable
方法下面添加以下内容。
func setupMoney(){
addChild(money10)
money10.position = CGPoint(x: 75, y: 40)
addChild(money25)
money25.position = CGPoint(x:130, y:40)
addChild(money50)
money50.position = CGPoint(x: 185, y:40)
}
在这里,我们只需添加货币实例并设置其位置即可。 在didMove(to:)
调用此方法。
override func didMove(to view: SKView) {
setupTable()
setupMoney()
}
6.实现setupButtons
将以下内容添加到在上述步骤中创建的setupMoney
方法下面。
func setupButtons(){
dealBtn.name = "dealBtn"
addChild(dealBtn)
dealBtn.position = CGPoint(x:300, y:40)
hitBtn.name = "hitBtn"
addChild(hitBtn)
hitBtn.position = CGPoint(x:450, y:40)
hitBtn.isHidden = true
standBtn.name = "standBtn"
addChild(standBtn)
standBtn.position = CGPoint(x:600, y:40)
standBtn.isHidden = true
}
就像在上一步中使用货币一样,我们添加按钮并设置它们的位置。 在这里,我们使用name
属性,以便能够通过代码识别每个按钮。 通过将isHidden
属性设置为true
,我们还可以将hitBtn
和standBtn
设置为隐藏或不可见。
现在在didMove(to:)
调用此方法。
override func didMove(to view: SKView) {
setupTable()
setupMoney()
setupButtons()
}
如果现在运行该应用程序,应该会看到货币实例和按钮已添加到场景中。
7.实施touchesBegan
我们需要实现touchesBegan(_:with:)
方法,以判断何时触摸了场景中的任何对象。 当一个或多个手指触摸屏幕时,将调用此方法。 在touchesBegan
添加以下内容。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
let touchedNode = self.atPoint(touchLocation)
if(touchedNode.name == "money"){
let money = touchedNode as! Money
bet(betAmount: money.getValue())
}
}
默认情况下,场景视图的multiTouchEnabled
属性设置为false
,这意味着视图仅接收多点触摸序列的第一次触摸。 禁用此属性后,由于触摸集中只有一个对象,因此可以使用触摸组的first
计算属性来检索触摸。
我们可以通过touch的location
属性在场景中获取touchLocation
。 然后,我们可以找出哪些节点是通过调用感动atPoint(_:)
并传入touchLocation
。
我们检查touchedNode
的name属性是否等于“ money ”,是否知道他们已经触碰到了这三个money实例之一。 我们初始化money
通过向下转型的不断touchedNode
到Money
,然后我们称之为bet
方法调用getValue()
的方法money
不变。
8.实施bet
在您在上面的步骤中创建的setupButtons
函数下输入以下内容。
func bet(betAmount: MoneyValue ){
if(betAmount.rawValue > player1.bank.getBalance()){
print("Trying to bet more than have");
return
}else{
pot.addMoney(amount: betAmount.rawValue)
let tempMoney = Money(moneyValue: betAmount)
tempMoney.anchorPoint = CGPoint(x:0, y:0)
moneyContainer.addChild(tempMoney)
tempMoney.position = CGPoint(x:CGFloat(arc4random_uniform(UInt32(moneyContainer.size.width - tempMoney.size.width))), y:CGFloat(arc4random_uniform(UInt32(moneyContainer.size.height - tempMoney.size.height))))
dealBtn.isHidden = false;
}
}
我们首先要确保玩家所下的赌注不会超过其所下的赌注,如果是的话,我们只是从函数中返回。 否则,我们将betAmount
添加到pot
,创建一个常量tempMoney
,将其anchorPoint
设置为(0,0)
,然后将其添加到moneyContainer
。 然后,通过将其isHidden
属性设置为false来设置其position
并隐藏dealBtn
。
SKSpriteNode
具有一个anchorPoint
属性,默认为(0.5,0.5)
。 坐标系将(0,0)
放在左下角,将(1,1)
放在右上角。 如果要旋转SKSpriteNode
并希望它绕不同的点旋转,则可以更改此属性的默认值。 例如,如果将anchorPoint
属性更改为(0,0)
则SKSpriteNode
将从其左下角旋转。 就像我们在这里一样,您经常会更改此属性以帮助定位。
我们需要创建Pot
和Player
类的实例,此代码才能正常工作。 将以下内容与其他常量和变量一起添加。
let pot = Pot()
let player1 = Player(hand: Hand(),bank: Bank())
如果您现在进行测试,则可以按任意一项,然后将其添加到moneyContainer
。
9.实施deal
将以下内容与其余的常量和变量一起添加。
let dealer = Dealer(hand: Hand())
var allCards = [Card]()
let dealerCardsY = 930 // Y position of dealer cards
let playerCardsY = 200 // Y position of player cards
var currentPlayerType:GenericPlayer = Player(hand: Hand(),bank: Bank())
let deck = Deck()
allCards
数组将用于容纳游戏中的所有卡。 这样可以轻松遍历它们,并一次性将它们从场景中删除。 dealerCardsY
和playerCardsY
常数是纸牌在y轴上的位置。 这将有助于我们放置新卡。 currentPlayerType
用于指示下一个交易对象。 它将等于发dealer
或player1
。
在didMove(to:)
内部,添加以下内容。
override func didMove(to view: SKView) {
setupTable()
setupMoney()
setupButtons()
currentPlayerType = player1
}
在前面的代码中,我们将currentPlayerType
初始化为Player
类的未命名实例。 在这里,我们将其设置为player1
。
在实施Deal方法之前,我们需要创建一个新的扑克牌。 在setupTable
输入以下setupTable
。
func setupTable(){
let table = SKSpriteNode(imageNamed: "table")
addChild(table)
table.position = CGPoint(x: size.width/2, y: size.height/2)
table.zPosition = -1
addChild(moneyContainer)
moneyContainer.anchorPoint = CGPoint(x:0, y:0)
moneyContainer.position = CGPoint(x:size.width/2 - 125, y:size.height/2)
instructionText.fontColor = UIColor.black
addChild(instructionText)
instructionText.position = CGPoint(x: size.width/2, y: 400)
deck.new()
}
现在我们可以实现交易功能。 在bet
方法下面添加以下内容。
func deal() {
instructionText.text = ""
money10.isHidden = true;
money25.isHidden = true;
money50.isHidden = true;
dealBtn.isHidden = true;
standBtn.isHidden = false
hitBtn.isHidden = false
let tempCard = Card(suit: "card_front", value: 0)
tempCard.position = CGPoint(x:630, y:980)
addChild(tempCard)
tempCard.zPosition = 100
let newCard = deck.getTopCard()
var whichPosition = playerCardsY
var whichHand = player1.hand
if(self.currentPlayerType is Player){
whichHand = player1.hand
whichPosition = playerCardsY;
} else {
whichHand = dealer.hand
whichPosition = dealerCardsY;
}
whichHand.addCard(card: newCard)
let xPos = 50 + (whichHand.getLength()*35)
let moveCard = SKAction.move(to: CGPoint(x:xPos, y: whichPosition),duration: 1.0)
tempCard.run(moveCard, completion: { [unowned self] in
self.player1.setCanBet(canBet: true)
if(self.currentPlayerType is Dealer && self.dealer.hand.getLength() == 1){
self.dealer.setFirstCard(card: newCard)
self.allCards.append(tempCard)
tempCard.zPosition = 0
} else {
tempCard.removeFromParent()
self.allCards.append(newCard)
self.addChild(newCard)
newCard.position = CGPoint( x: xPos, y: whichPosition)
newCard.zPosition = 100
}
if(self.dealer.hand.getLength() < 2){
if(self.currentPlayerType is Player){
self.currentPlayerType = self.dealer
}else{
self.currentPlayerType = self.player1
}
self.deal()
}else if (self.dealer.hand.getLength() == 2 && self.player1.hand.getLength() == 2) {
if(self.player1.hand.getValue() == 21 || self.dealer.hand.getValue() == 21){
self.doGameOver(hasBlackJack: true)
} else {
self.standBtn.isHidden = false;
self.hitBtn.isHidden = false;
}
}
if(self.dealer.hand.getLength() >= 3 && self.dealer.hand.getValue() < 17){
self.deal();
} else if(self.player1.isYeilding() && self.dealer.hand.getValue() >= 17){
self.standBtn.isHidden = true
self.hitBtn.isHidden = true
self.doGameOver(hasBlackJack: false)
}
if(self.player1.hand.getValue() > 21){
self.standBtn.isHidden = true;
self.hitBtn.isHidden = true;
self.doGameOver(hasBlackJack: false);
}
})
}
此方法很大,但是对于实现交易逻辑是必需的。 让我们逐步进行。 我们将tempCard
常量初始化为Card
的实例,设置其位置,然后将其添加到场景中。 我们需要在zPosition
大于0
抽取此卡,因为经销商的第一张卡必须为0
。 我们将其设置为任意数字100
就可以了。 我们还通过调用deck
的getTopCard()
方法来创建newCard
常量。
接下来,我们初始化两个变量whichPosition
和whichHand
,然后通过一些逻辑来确定它们的最终值。 然后,我们添加newCard
到适当的手(或者玩家的或经销商的)。 一旦完成动画制作, xPos
常数将确定卡片的最终x位置。
SKAction
类具有许多可用于更改节点属性(例如位置,比例和旋转)的类方法。 在这里,我们调用move(to:duration:)
方法,该方法会将节点从一个位置移动到另一个位置。 但是,要实际执行SKAction
,您必须调用节点的run(_:)
方法并将SKAction
作为参数传递。 但是,在这里,我们正在调用run(_:completion:)
方法,该方法将导致操作完成执行后,完成闭包中的代码运行。
操作完成后,我们可以通过在player1
实例上调用setCanBet(canBet:)
来允许玩家下注。 然后,我们通过调用hand.getLength()
检查currentPlayerType
是否是Dealer
的实例,并检查dealer
只有一张卡。 在这种情况下,我们设置了dealer
的第一张牌,这是我们在游戏结束时需要的。
因为发dealer
者的第一张牌始终面朝下直到比赛结束,所以我们需要参考第一张牌,以便稍后显示。 我们将此卡添加到allCards
数组中,以便稍后将其删除,然后将其zPosition
属性设置为0
因为我们需要此卡在所有其他卡下面。 (请记住其他卡的z位置为100
)
如果currentPlayerType
不是Dealer
的实例,并且手的长度不等于1
,则我们删除tempCard
并将newCard
放在相同位置,确保将其zPosition
设置为100
。
根据二十一点的规则,发牌者和玩家都会获得两张牌以开始游戏。 在这里,我们正在检查currentPlayerType
是什么,并将其更改为相反的值。 因为发牌者的卡少于两张,所以我们再次调用deal
功能。 否则,我们检查发dealer
和player1
是否都持有两张牌,如果是这种情况,我们将检查两者中是否有总值为21的牌(获胜手)。 如果其中一个有21,则游戏结束,因为其中一个已经赢得了21点。 如果两者都不是21,则我们显示standBtn
和hitBtn
,然后游戏继续进行。
二十一点规则规定dealer
必须年满17岁 。 码检查的接下来的几行,如果dealer
的手的值小于17,如果是调用deal
方法。 如果大于等于17 ,则游戏结束。 最后,如果player1
1的手牌价值大于21,则游戏已经结束,因为他们已经破产。
这是要经历的许多逻辑! 如果不清楚,请重新阅读并花一些时间来理解它。
接下来,我们需要实现gameover
方法。
我们需要能够分辨出用户何时按下了交易按钮。 将以下代码添加到touchesBegan(_:with:)
方法。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
let touchedNode = self.atPoint(touchLocation)
if(touchedNode.name == "money"){
let money = touchedNode as! Money
bet(betAmount: money.getValue())
}
if(touchedNode.name == "dealBtn"){
deal()
}
}
10.实现doGameOver
接下来,在您在上述步骤中创建的deal
方法下方输入以下内容。
func doGameOver(hasBlackJack: Bool){
hitBtn.isHidden = true
standBtn.isHidden = true
let tempCardX = allCards[1].position.x
let tempCardY = allCards[1].position.y
let tempCard = dealer.getFirstCard()
addChild(tempCard)
allCards.append(tempCard)
tempCard.position = CGPoint(x:tempCardX,y:tempCardY)
tempCard.zPosition = 0
var winner:GenericPlayer = player1
if(hasBlackJack){
if(player1.hand.getValue() > dealer.hand.getValue()){
//Add to players Bank Here (pot value * 1.5)
instructionText.text = "You Got BlackJack!";
moveMoneyContainer(position: playerCardsY)
}else{
//Subtract from players bank here
instructionText.text = "Dealer got BlackJack!";
moveMoneyContainer(position: dealerCardsY)
}
return
}
if (player1.hand.getValue() > 21){
instructionText.text = "You Busted!"
//Subtract from players bank
winner = dealer
}else if (dealer.hand.getValue() > 21){
//Add to players bank
instructionText.text = "Dealer Busts. You Win!"
winner = player1
}else if (dealer.hand.getValue() > player1.hand.getValue()){
//Subtract from players bank
instructionText.text = "You Lose!"
winner = dealer
}else if (dealer.hand.getValue() == player1.hand.getValue()){
//Subtract from players bank
instructionText.text = "Tie - Dealer Wins!"
winner = dealer
}else if (dealer.hand.getValue() < player1.hand.getValue()){
//Add to players bank
instructionText.text="You Win!";
winner = player1
}
if(winner is Player){
moveMoneyContainer(position: playerCardsY)
}else{
moveMoneyContainer(position: dealerCardsY)
}
}
我们得到allCards
数组中第一张卡的x和y位置,这是发牌人的第一张卡。 然后,通过在经销商处调用getFirstCard
实例化一个恒定的tempCard
。 还记得我们在发Card
方法中设置了这张Card
吗? 在这里,我们将其添加到场景中,使用tempCardX
和tempCardY
常量设置其位置,并将其zPosition
设置为0
所以它在其他卡的下面。
我们需要知道谁赢得了比赛,因此我们将变量winner
初始化为将其设置为player1
,尽管这可能会有所变化,具体取决于发dealer
真正赢得了比赛。
然后,我们通过一些逻辑来确定谁赢得了比赛。 如果hasBlackjack
参数为true,则我们确定谁赢了,并从函数中返回。 否则,我们将继续按照逻辑找出谁赢得了比赛。 我不会逐步讲解这个逻辑,因为应该很容易理解。 无论谁赢了,我们都将调用moveMoneyContainer(position:)
,该方法将钱moveMoneyContainer(position:)
作为参数。 这将是发dealer
人或player1
的y位置。
11.实施moveMoneyContainer
在doGameOver
方法下面输入以下代码。
func moveMoneyContainer(position: Int){
let moveMoneyContainer = SKAction.moveTo(y: CGFloat(position), duration: 3.0)
moneyContainer.run(moveMoneyContainer, completion: { [unowned self] in
self.resetMoneyContainer()
});
}
moveMoneyContainer(position:)
方法将moneyContainer
移动到赢得游戏的任何人,无论是玩家还是庄家。 SKAction
完成后,我们调用resetMoneyContainer
。
12.实现resetMoneyContainer
resetMoneyContainer
方法通过调用removeAllChildren()
方法删除所有金钱,将moneyContainer
重置为其原始位置,然后调用newGame
。
func resetMoneyContainer(){
moneyContainer.removeAllChildren()
moneyContainer.position.y = size.height/2
newGame()
}
13.实施newGame
将以下内容添加到您在上述步骤中实现的resetMoneyContainer
方法下方。
func newGame(){
currentPlayerType = player1
deck.new()
instructionText.text = "PLACE YOUR BET";
money10.isHidden = false;
money25.isHidden = false;
money50.isHidden = false;
dealBtn.isHidden = false
player1.hand.reset()
dealer.hand.reset()
player1.setYielding(yields: false)
for card in allCards{
card.removeFromParent()
}
allCards.removeAll()
}
在这里,我们通过循环allCards
数组并在每个元素上调用removeFromParent()
,重置所有必需的变量并从场景中删除所有卡。
14.实施hitBtn
和standBtn
完成游戏所需hitBtn
就是实现hitBtn
和standBtn
的接触。 在touchesBegan(_:with:)
方法中输入以下内容。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
let touchedNode = self.atPoint(touchLocation)
if(touchedNode.name == "money"){
let money = touchedNode as! Money
bet(betAmount: money.getValue())
}
if(touchedNode.name == "dealBtn"){
deal()
}
if(touchedNode.name == "hitBtn"){
hit()
}
if(touchedNode.name == "standBtn"){
stand()
}
}
现在,我们将实现事件处理程序中调用的方法。 在newGame
方法下面输入以下两种方法。
func hit(){
if(player1.getCanBet()){
currentPlayerType = player1
deal()
player1.setCanBet(canBet: false)
}
}
func stand(){
player1.setYielding(yields: true)
standBtn.isHidden = true
hitBtn.isHidden = true
if(dealer.hand.getValue() < 17){
currentPlayerType = dealer
deal();
}else{
doGameOver(hasBlackJack: false)
}
}
在hit
方法中,我们确保玩家可以下注,如果是这种情况,我们将currentPlayerType
设置为player1
,然后调用deal
方法并进一步停止玩家下注。
在stand方法中,我们在setYielding
上调用player1
,并传入true
。 然后,我们检查发dealer
者的手牌价值是否小于17 ,如果是这种情况,我们称之为交易;如果发dealer
者的手数等于或大于17 ,则表示游戏结束了。
您现在可以测试完成的游戏。
结论
这是一个很长的教程,在Deal方法中包含了很多逻辑。 我们没有实现使用底Pot
以及从玩家的银行中增加和减少钱款的方法。 您为什么不尝试将其作为完成应用程序的练习?
翻译自: https://code.tutsplus.com/tutorials/create-a-blackjack-game-in-swift-3-and-spritekit--cms-28511