SpriteKit框架详细解析(七) —— 基于SpriteKit的类Cut the Rope游戏简单示例(一)

版本记录

版本号 时间
V1.0 2019.10.23 星期三

前言

SpriteKit框架使用优化的动画系统,物理模拟和事件处理支持创建基于2D精灵的游戏。接下来这几篇我们就详细的解析一下这个框架。相关代码已经传至GitHub - 刀客传奇,感兴趣的可以阅读另外几篇文章。
1. SpriteKit框架详细解析(一) —— 基本概览(一)
2. SpriteKit框架详细解析(二) —— 一个简单的动画实例(一)
3. SpriteKit框架详细解析(三) —— 创建一个简单的2D游戏(一)
4. SpriteKit框架详细解析(四) —— 创建一个简单的2D游戏(二)
5. SpriteKit框架详细解析(五) —— 基于SpriteKit的游戏编程的三角函数(一)
6. SpriteKit框架详细解析(六) —— 基于SpriteKit的游戏编程的三角函数(二)

开始

题外话:分开半个月你就定亲了,你定你的亲,我奋斗我的未来,互不打扰,这是我最后为你做的一件事!可能昨晚是最后一次想起你流泪了~~,愿我以后还会觉得人间值得!

首先看下主要内容

主要内容:本篇主要讲述了,学习如何在Swift中使用SpriteKit构建像《Cut the Rope》这样的游戏,并提供动画,声音效果和物理效果!下面是翻译地址。

下面看下写作环境

Swift 5, iOS 13, Xcode 11

Cut The Rope 是一款流行的物理驱动游戏,玩家可以通过割断悬吊糖果的绳索来喂食一个名为Om Nom的小怪物。 在适当的时间和地点切成薄片,Om Nom会得到美味的点心。

在充分考虑Om Nom的情况下,这款游戏的真正明星是其模拟物理效果:绳索摆动,重力拉动和糖果摇摇欲坠,就像您在现实生活中所期望的那样。

您可以使用Apple的2D游戏框架SpriteKit的物理引擎来构建类似的体验。 在本Cut the Rope with SpriteKit教程中,您将使用名为Snip The Vine的游戏来做到这一点。

Snip The Vine中,您将向鳄鱼喂食可爱的小菠萝。在Xcode中打开该项目,以快速了解其结构。

您会在几个文件夹中找到项目文件。 在本教程中,您将使用包含主要代码文件的Classes。 随意浏览其他文件夹,如下所示:

在整个教程中,您将使用Constants.swift中的值,因此在深入之前请花一些时间来熟悉该文件。


Adding the Crocodile to the Scene

请注意,这条鳄鱼非常活泼,请始终保持手指安全距离!

鳄鱼由SKSpriteNode展示。 您需要为您的游戏逻辑保留对鳄鱼的引用。 您还需要为鳄鱼精灵设置一个物理物体(physics body),以检测并处理与其他物体的接触。

GameScene.swift中,将以下属性添加到类的顶部:

private var crocodile: SKSpriteNode!
private var prize: SKSpriteNode!

这些属性将存储对鳄鱼和奖品(菠萝)的引用。

GameScene.swift中找到setUpCrocodile()并添加以下代码:

crocodile = SKSpriteNode(imageNamed: ImageName.crocMouthClosed)
crocodile.position = CGPoint(x: size.width * 0.75, y: size.height * 0.312)
crocodile.zPosition = Layer.crocodile
crocodile.physicsBody = SKPhysicsBody(
  texture: SKTexture(imageNamed: ImageName.crocMask),
  size: crocodile.size)
crocodile.physicsBody?.categoryBitMask = PhysicsCategory.crocodile
crocodile.physicsBody?.collisionBitMask = 0
crocodile.physicsBody?.contactTestBitMask = PhysicsCategory.prize
crocodile.physicsBody?.isDynamic = false
    
addChild(crocodile)
    
animateCrocodile()

使用此代码,您可以创建鳄鱼节点并设置其positionzPosition

鳄鱼有一个SKPhysicsBody,这意味着它可以与世界上其他对象进行交互。当您要检测菠萝何时落入嘴中时,这将在以后有用。

您不希望鳄鱼被撞倒或从屏幕底部掉下!为避免这种情况,请将isDynamic设置为false,以防止物理力影响它。

categoryBitMask定义了物体所属的物理类别-在这种情况下为PhysicsCategory.crocodile。您将collaringBitMask设置为0,是因为您不希望鳄鱼从其他物体反弹。您需要知道的是“奖励”物体何时与鳄鱼接触,因此您可以相应地设置contactTestBitMask

您可能会注意到,鳄鱼的物体主体是使用SKTexture对象初始化的。您可以简单地将.crocMouthOpen用作物体纹理,但是该图像包括鳄鱼的整个身体,而蒙版纹理仅包括鳄鱼的头和嘴。鳄鱼的尾巴不能吃菠萝!

现在,您将向鳄鱼添加“等待”动画。找到animateCrocodile()并添加以下代码:

let duration = Double.random(in: 2...4)
let open = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthOpen))
let wait = SKAction.wait(forDuration: duration)
let close = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthClosed))
let sequence = SKAction.sequence([wait, open, wait, close])
    
crocodile.run(.repeatForever(sequence))

除了使小鳄鱼非常焦虑外,此代码还创建了一些动作来更改鳄鱼节点的纹理,以便在闭合的嘴和张开的嘴之间交替。

SKAction.sequence(_ :)构造函数从数组创建一系列动作。 在这种情况下,纹理动作与2到4秒之间的随机选择的延迟时间顺序结合。

sequence action封装在repeatForever(_ :)中,因此它将在关卡持续时间内重复执行。 然后,鳄鱼节点运行它。

就这些! 建立并运行,然后看到这个凶猛的爬行动物咬死了他的下巴!

您有风景,也有鳄鱼-现在您需要可爱的小动物菠萝。


Adding the Prize

打开GameScene.swift并找到setUpPrize()。 添加以下内容:

prize = SKSpriteNode(imageNamed: ImageName.prize)
prize.position = CGPoint(x: size.width * 0.5, y: size.height * 0.7)
prize.zPosition = Layer.prize
prize.physicsBody = SKPhysicsBody(circleOfRadius: prize.size.height / 2)
prize.physicsBody?.categoryBitMask = PhysicsCategory.prize
prize.physicsBody?.collisionBitMask = 0
prize.physicsBody?.density = 0.5

addChild(prize)

类似于鳄鱼,菠萝节点也使用physics body。 最大的不同是,菠萝应该掉下来并弹跳,而鳄鱼只是坐在那里,不耐烦地等待。 因此,将isDynamic设置为其默认值true。 您还可以降低菠萝的density,使其更自由地摆动。


Working With Physics

在开始坠落菠萝之前,最好先配置物理世界physics world。 在GameScene.swift中找到setUpPhysics()并添加以下三行:

physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector(dx: 0.0, dy: -9.8)
physicsWorld.speed = 1.0

这将设置物理世界physics worldcontactDelegategravityspeed。 重力指定了应用于世界物理物体的重力加速度,而速度则调节了模拟执行的速度。 您希望在此处将两个属性都设置为其默认值。

您会发现您不必遵循SKPhysicsContactDelegate,因为GameScene底部已经有一个扩展程序来实现它。 暂时不要管它; 您稍后再使用。

构建并再次运行。 您应该看到菠萝驶过鳄鱼,掉入水中。

是时候添加藤蔓了。


Adding Vines

SpriteKit的物理物体模拟了刚性物体……但是藤蔓弯曲了。为了解决这个问题,您需要将每个藤蔓实施为具有柔性接头的分段阵列,类似于链。

每个藤蔓具有三个重要属性:

  • anchorPointCGPoint,指示藤蔓末端连接到树的位置。
  • length:一个Int,代表藤蔓中的节数。
  • name:一个String,用于标识给定段属于哪个藤。

在本教程中,游戏只有一个级别。但是在真实游戏中,您希望能够轻松地创建新的关卡布局而无需编写大量代码。做到这一点的一种好方法是独立于游戏逻辑指定关卡数据,也许是通过将其存储在带有属性列表或JSON的数据文件中来进行。

由于您将从文件中加载藤蔓数据,因此代表它的自然结构是可编码Codable值的数组,可以使用PlistDecoder从属性列表中轻松读取该值。属性列表中的每个字典将代表一个VineData实例,该实例已在入门项目中定义。

GameScene.swift中,找到setUpVines()并添加以下代码:

let decoder = PropertyListDecoder()
guard
  let dataFile = Bundle.main.url(
    forResource: GameConfiguration.vineDataFile,
    withExtension: nil),
  let data = try? Data(contentsOf: dataFile),
  let vines = try? decoder.decode([VineData].self, from: data)
  else {
    return
}

从属性列表文件加载vine数据。 看一下Resources / Data中的VineData.plist。 您应该看到该文件包含一个字典数组,每个字典包含一个relAnchorPointlength

接下来,将以下代码添加到方法中:

// 1 add vines
for (i, vineData) in vines.enumerated() {
  let anchorPoint = CGPoint(
    x: vineData.relAnchorPoint.x * size.width,
    y: vineData.relAnchorPoint.y * size.height)
  let vine = VineNode(
    length: vineData.length, 
    anchorPoint: anchorPoint, 
    name: "\(i)")

  // 2 add to scene
  vine.addToScene(self)

  // 3 connect the other end of the vine to the prize
  vine.attachToPrize(prize)
}

使用此代码,您:

  • 1) 对于每个藤蔓,初始化一个新的VineNodelength指定藤中的节段数。 relAnchorPoint确定相对于场景scene大小的藤蔓的锚点位置。
  • 2) 最后,使用addToScene(_ :)VineNode附加到场景`scene。
  • 3) 然后,使用attachToPrize(_ :)将其附加到奖励。

接下来,您将在VineNode中实现这些方法。


Defining the Vine Class

打开VineNode.swiftVineNode是从SKNode继承的自定义类。 它本身没有任何视觉外观,而是充当代表藤蔓片段的SKSpriteNodes集合的容器。

将以下属性添加到类定义:

private let length: Int
private let anchorPoint: CGPoint
private var vineSegments: [SKNode] = []

由于您尚未初始化lengthanchorPoint属性,因此会出现一些错误。 您已将它们声明为非可选,但尚未分配值。 通过使用以下替换init(length:anchorPoint:name :)的实现来解决此问题:

self.length = length
self.anchorPoint = anchorPoint

super.init()

self.name = name

非常简单……但是由于某些原因,仍然存在错误。 您可能会注意到,还有第二个初始化方法,init(coder :)。 您没有在任何地方调用,那是为了什么?

因为SKNode实现了NSCoding,所以它继承了required的初始化程序init(coder :)。 这意味着即使您没有使用它,也必须在其中初始化非可选属性。

现在就做。 将init(coder :)的内容替换为:

length = aDecoder.decodeInteger(forKey: "length")
anchorPoint = aDecoder.decodeCGPoint(forKey: "anchorPoint")

super.init(coder: aDecoder)

接下来,您需要实现addToScene(_ :)。 这是一种复杂的方法,因此您将分阶段进行编写。 首先,找到addToScene(_ :)并添加以下内容:

// add vine to scene
zPosition = Layer.vine
scene.addChild(self)

将藤蔓添加到场景并设置其zPosition。 接下来,将此代码块添加到相同的方法中:

// create vine holder
let vineHolder = SKSpriteNode(imageNamed: ImageName.vineHolder)
vineHolder.position = anchorPoint
vineHolder.zPosition = 1
    
addChild(vineHolder)
    
vineHolder.physicsBody = SKPhysicsBody(circleOfRadius: vineHolder.size.width / 2)
vineHolder.physicsBody?.isDynamic = false
vineHolder.physicsBody?.categoryBitMask = PhysicsCategory.vineHolder
vineHolder.physicsBody?.collisionBitMask = 0

这将创建藤蔓固定器,就像藤蔓悬挂的钉子一样。 与鳄鱼一样,该物体不是动态的,不会与其他物体碰撞。

藤蔓支架是圆形的,因此请使用SKPhysicsBody(circleOfRadius :)初始化程序。 藤蔓支架的位置与您在创建VineNode时指定的anchorPoint匹配。

接下来,您将创建藤蔓。 将以下代码再次添加到同一方法的底部:

// add each of the vine parts
for i in 0..

此循环创建一个藤蔓片段数组,其数目与创建VineNode时指定的长度相等。 每个段都是具有自己的physics bodysprite。 这些线段是矩形的,因此您可以使用SKPhysicsBody(rectangleOf :)指定物理物体的形状。

与藤蔓架不同,藤蔓藤节是动态的-它们可以移动并受到重力的影响。

构建并运行以查看进度。

哦哦! 藤蔓藤段像切碎的意大利面条一样从屏幕上掉下来!


Adding Joints to the Vines

问题是您还没有将藤蔓分段结合在一起。 要解决此问题,您需要将此最后的代码块添加到addToScene(_ :)的底部:

// set up joint for vine holder
let joint = SKPhysicsJointPin.joint(
  withBodyA: vineHolder.physicsBody!,
  bodyB: vineSegments[0].physicsBody!,
  anchor: CGPoint(
    x: vineHolder.frame.midX,
    y: vineHolder.frame.midY))

scene.physicsWorld.add(joint)

// set up joints between vine parts
for i in 1..

此代码在线段之间建立物理连接,并将它们连接在一起。 您使用的关节类型是SKPhysicsJointPin。 这种关节类型的行为就像您将销钉通过两个节点锤击一样,使它们可以围绕销钉枢转,但彼此之间的距离不能太远。

构建并再次运行。 您的藤蔓应该从树上垂下来。

最后一步是将藤蔓附着到菠萝上。 仍在VineNode.swift中,滚动到attachToPrize(_ :)。 添加以下代码:

// align last segment of vine with prize
let lastNode = vineSegments.last!
lastNode.position = CGPoint(x: prize.position.x,
                            y: prize.position.y + prize.size.height * 0.1)
    
// set up connecting joint
let joint = SKPhysicsJointPin.joint(withBodyA: lastNode.physicsBody!,
                                    bodyB: prize.physicsBody!,
                                    anchor: lastNode.position)
    
prize.scene?.physicsWorld.add(joint)

该代码获取藤蔓的最后一段,并将其定位在奖品中心的上方。 您希望将其附加在此处,以便使奖品吊着下来。 如果它是死点,则奖品将被平均加权并可能沿其轴旋转。 它还创建了另一个销钉接头,将藤蔓部分附加到奖品上。

构建并运行。 如果所有关节和节点都正确设置,则应该看到类似于以下屏幕的屏幕:

好极了! 悬空的菠萝–谁将菠萝与树绑在一起?


Snipping the Vines

您可能已经注意到,您仍然不能剪那些藤! 接下来,您将解决该小问题。

在本部分中,您将使用允许您剪断那些悬挂藤蔓的触摸方法。 返回GameScene.swift,找到touchesMoved(_:with :)并添加以下代码:

for touch in touches {
  let startPoint = touch.location(in: self)
  let endPoint = touch.previousLocation(in: self)
  
  // check if vine cut
  scene?.physicsWorld.enumerateBodies(
    alongRayStart: startPoint, 
    end: endPoint, 
    using: { body, _, _, _ in
      self.checkIfVineCut(withBody: body)
  })
  
  // produce some nice particles
  showMoveParticles(touchPosition: startPoint)
}

该代码的工作方式如下:首先,它获取每次触摸的当前位置和先前位置。 接下来,使用非常方便的SKSceneenumerateBodies(alongRayStart:end:using :)方法,循环遍历这两个点之间的所有场景。 对于遇到的每个body,它将调用checkIfVineCut(withBody :),您将在一分钟内编写它。

最后,代码调用一个方法,该方法通过从Particle.sks加载SKEmitterNode来创建一个SKEmitterNode,并将其添加到场景中用户触摸的位置。 无论您将手指拖到哪里,都会产生一条漂亮的绿色烟雾痕迹。

现在,向下滚动到checkIfVineCut(withBody :)并将此代码块添加到方法主体中:

let node = body.node!

// if it has a name it must be a vine node
if let name = node.name {
  // snip the vine
  node.removeFromParent()

  // fade out all nodes matching name
  enumerateChildNodes(withName: name, using: { node, _ in
    let fadeAway = SKAction.fadeOut(withDuration: 0.25)
    let removeNode = SKAction.removeFromParent()
    let sequence = SKAction.sequence([fadeAway, removeNode])
    node.run(sequence)
  })
}

上面的代码首先检查连接到physics body的节点是否具有名称。请记住,场景中除了藤节以外还有其他节点,您当然不希望不小心将秋千或菠萝切成薄片!但是,您仅命名了vine节点段,因此,如果该节点具有名称,则可以确定它是vine的一部分。

接下来,从场景中删除该节点。删除节点还会删除其PhysicalBody并破坏与其连接的所有关节。您现在已经正式切断了藤!

最后,使用场景的enumerateChildNodes(withName:using :)枚举场景中所有名称与扫到的节点名称匹配的节点。名称匹配的唯一节点是同一藤蔓中的其他片段,因此您只需遍历切片的任何藤蔓的片段。

对于每个节点,创建一个SKAction,先淡出该节点,然后将其从场景中删除。效果是,切成薄片后,每棵藤都会褪出。

构建并运行。尝试剪断那些藤蔓–现在,您应该可以滑动并切割所有三个藤蔓,并看到该奖品下降了。甜菠萝!


Handling Contact Between Bodies

编写setUpPhysics()时,您指定GameScene将充当PhysicsWorldcontactDelegate。 您还配置了croccontactTestBitMask,以便SpriteKit在与奖品相交时通知。 那是极好的预见!

现在,在GameScene.swift中,您需要实现SKPhysicsContactDelegatedidBegin(_ :),只要它检测到两个适当遮罩的物体之间的交集,就会触发该事件。 该方法有一个stub-向下滚动以找到它并添加以下代码:

if (contact.bodyA.node == crocodile && contact.bodyB.node == prize)
  || (contact.bodyA.node == prize && contact.bodyB.node == crocodile) {
  
  // shrink the pineapple away
  let shrink = SKAction.scale(to: 0, duration: 0.08)
  let removeNode = SKAction.removeFromParent()
  let sequence = SKAction.sequence([shrink, removeNode])
  prize.run(sequence)
}

该代码检查两个相交的物体是否属于鳄鱼和奖品。 您不知道节点的顺序,因此您要检查两个组合。 如果测试通过,您将触发一个简单的动画序列,该序列将奖赏缩减为零,然后将其从场景中删除。


Animate the Crocodile's Chomp

您还希望鳄鱼在抓到菠萝时剁碎。 在您刚刚触发了菠萝收缩动画的if语句中,添加以下额外的行:

runNomNomAnimation(withDelay: 0.15)

现在找到runNomNomAnimation(withDelay :)并添加以下代码:

crocodile.removeAllActions()

let closeMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthClosed))
let wait = SKAction.wait(forDuration: delay)
let openMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthOpen))
let sequence = SKAction.sequence([closeMouth, wait, openMouth, wait, closeMouth])

crocodile.run(sequence)

上面的代码使用removeAllActions()删除当前在鳄鱼节点上运行的所有动画。 然后,它创建一个新的动画序列,以关闭并张开鳄鱼的嘴,让鳄鱼crocodile运行该序列。

当奖品降落在鳄鱼嘴中时,将触发此新动画,给人以鳄鱼正在咀嚼它的印象。

在进行此操作时,在checkIfVineCut(withBody :)中的enumerateChildNodes调用下面添加以下行:

crocodile.removeAllActions()
crocodile.texture = SKTexture(imageNamed: ImageName.crocMouthOpen)
animateCrocodile()

这将确保您在剪藤时张开鳄鱼的嘴,这样奖品就有可能落在鳄鱼的嘴里。

构建并运行。

现在,快乐的鳄鱼如果将菠萝放在嘴里,就会嚼碎菠萝。 但是一旦发生这种情况,游戏就会挂在那里。 接下来,您将解决该问题。


Resetting the Game

接下来,当菠萝掉落或鳄鱼吃掉菠萝时,您将重置游戏。

GameScene.swift中,找到switchToNewGame(withTransition :),然后添加以下代码:

let delay = SKAction.wait(forDuration: 1)
let sceneChange = SKAction.run {
  let scene = GameScene(size: self.size)
  self.view?.presentScene(scene, transition: transition)
}

run(.sequence([delay, sceneChange]))

上面的代码使用SKViewpresentScene(_:transition :)呈现下一个场景。

在这种情况下,过渡到的场景是同一GameScene的新实例。 您还可以使用SKTransition类传递过渡效果。 您可以将过渡指定为方法的参数,以便可以根据游戏的结果使用不同的过渡效果。

滚动回到didBegin(_ :),然后在if语句内的奖品收缩和NomNom动画之后,添加以下内容:

// transition to next level
switchToNewGame(withTransition: .doorway(withDuration: 1.0))

这将使用.doorway(withDuration :)初始化程序调用switchToNewGame(withTransition :)来创建门口过渡。 这显示了具有开门效果的下一关。 构建并运行以查看效果。

很整洁吧?


Ending the Game

您可能会认为您需要在水中添加另一个physics body,以便可以检测到奖品是否撞击它,但是如果菠萝从屏幕侧面飞出,那将无济于事。

一种更简单,更好的方法是检测菠萝何时移到屏幕边缘的底部以下,然后结束游戏。

SKScene提供了update(_ :),每帧调用一次。 在GameScene.swift中找到该方法,并添加以下逻辑:

if prize.position.y <= 0 {
  switchToNewGame(withTransition: .fade(withDuration: 1.0))
}

if语句检查奖品的y坐标是否小于零(即屏幕底部)。 如果是这样,它将调用switchToNewGame(withTransition :)再次启动级别,这次使用.fade(withDuration :)

构建并运行。

每当玩家获胜或失败时,您都应该看到场景淡出并过渡到新场景。


Adding Sound and Music

现在,您将在incompetech.com上添加一首优美的丛林歌曲,并从freesound.org中添加一些音效。

SpriteKit将为您处理声音效果。 但是,您将使用AVAudioPlayer在关卡过渡之间无缝播放背景音乐。

1. Adding the Background Music

要开始添加音乐,请向GameScene.swift添加另一个属性:

private static var backgroundMusicPlayer: AVAudioPlayer!

这声明了一个type属性,因此GameScene的所有实例都将能够访问相同的backgroundMusicPlayer。 找到setUpAudio()并添加以下代码:

if GameScene.backgroundMusicPlayer == nil {
  let backgroundMusicURL = Bundle.main.url(
    forResource: SoundFile.backgroundMusic,
    withExtension: nil)
  
  do {
    let theme = try AVAudioPlayer(contentsOf: backgroundMusicURL!)
    GameScene.backgroundMusicPlayer = theme
  } catch {
    // couldn't load file :[
  }
  
  GameScene.backgroundMusicPlayer.numberOfLoops = -1
}

上面的代码检查backgroundMusicPlayer是否存在。 如果没有,它将使用您之前添加到Constants.swiftBackgroundMusic初始化一个新的AVAudioPlayer。 然后,它将其转换为URL并将其分配给属性。 numberOfLoops值设置为-1,表示歌曲应无限循环。

接下来,将此代码添加到setUpAudio()的底部:

if !GameScene.backgroundMusicPlayer.isPlaying {
  GameScene.backgroundMusicPlayer.play()
}

场景首次加载时,将启动背景音乐。 然后它将无限期播放,直到应用程序退出或其他方法在播放器上调用stop()为止。

您可以直接调用play(),而无需先检查播放器是否正在播放,但是通过这种方式,如果在关卡开始时已经在播放音乐,则音乐不会跳过或重新开始。

2. Adding the Sound Effects

在这里时,您还可以设置以后将要使用的所有声音效果。 与音乐不同,您不想立即播放声音效果。 相反,您将创建一些可重用的SKAction,这些稍后将播放声音。

返回GameScene类定义的顶部,并添加以下属性:

private var sliceSoundAction: SKAction!
private var splashSoundAction: SKAction!
private var nomNomSoundAction: SKAction!

现在返回setUpAudio()并将以下行添加到方法的底部:

sliceSoundAction = .playSoundFileNamed(
  SoundFile.slice,
  waitForCompletion: false)
splashSoundAction = .playSoundFileNamed(
  SoundFile.splash,
  waitForCompletion: false)
nomNomSoundAction = .playSoundFileNamed(
  SoundFile.nomNom,
  waitForCompletion: false)

此代码使用SKActionplaySoundFileNamed(_:waitForCompletion :)初始化声音操作。 现在,该播放音效了。

向上滚动到update(_ :)并在switchToNewGame(withTransition :)调用上方的if语句内添加以下代码:

run(splashSoundAction)

当菠萝落入水中时,会发出飞溅的声音。 接下来,找到didBegin(_ :)并在runNomNomAnimation(withDelay :)行的下方添加以下代码:

run(nomNomSoundAction)

当鳄鱼获得奖品时,它将发出刺耳的声音。 最后,找到checkIfVineCut(withBody :)并在if let语句的底部添加以下代码:

run(sliceSoundAction)

每当玩家剪断藤蔓时,就会播放轻拂的声音。

构建并运行,当鳄鱼吃菠萝时,享受那松脆的声音!

3. Getting Rid of an Awkward Sound Effect

您发现bug了吗? 如果您错过croc,则飞溅的声音会播放多次。 这是因为在游戏过渡到下一个场景之前,您反复触发“关卡完成”逻辑。 若要更正此问题,请在类顶部添加一个新的state属性:

private var isLevelOver = false

现在通过在每个代码的顶部添加以下代码来修改update(_ :)didBegin(_ :)

if isLevelOver {
  return
}

最后,在相同方法的其他if语句内,添加一些代码以将isLevelOver状态设置为true

isLevelOver = true

现在,一旦游戏检测到设置了isLevelOver,要么是因为菠萝掉到地上,要么是因为鳄鱼吃了饭,那么它将停止检查游戏的胜利/失败场景。 它不会继续反复尝试播放那些声音效果。

构建并运行。 不再有笨拙的声音效果!


Adding Some Difficulty

玩了几回合后,游戏似乎有些简单。 您将很快到达可以在三个藤蔓上用适时的单个切片喂鳄鱼的地步。

使用Constants.swiftCanCutMultipleVinesAtOnce中的值使事情变得棘手。

GameScene.swift中,在GameScene类定义的顶部添加最后一个属性:

private var didCutVine = false

现在找到checkIfVineCut(withBody :)并在顶部添加以下if语句:

if didCutVine && !GameConfiguration.canCutMultipleVinesAtOnce {
  return
}

将此代码添加到相同方法的底部的if语句中:

didCutVine = true

为了使事情保持在一起,找到touchesBegan(_:with :),然后添加以下行:

didCutVine = false

这样,只要用户触摸屏幕,就可以重置didCutVine

构建并再次运行。

您应该看到,现在每次滑动只能剪一棵藤蔓。 要剪另一根,您必须抬起手指,然后再次滑动。

后记

本篇主要讲述了基于SpriteKit的类Cut the Rope游戏简单示例,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(SpriteKit框架详细解析(七) —— 基于SpriteKit的类Cut the Rope游戏简单示例(一))