澳门新葡萄京官网注册 13

澳门新葡萄京官网注册Sprite Kit Swift Tutorial for Beginners

之前用SpriteKit做过一个叫做ColorAtom的小游戏,用了访问者模式处理碰撞检测,还用了SpriteKit中的粒子系统、连接体、力场和动画等,可以说是一个学习SpriteKit比较不错的Demo,随着Swift的火热,我也用Swift和SpriteKit写了一个更为简单的小游戏Spiral

Translate form http://www.raywenderlich.com/84434/sprite-kit-swift-tutorial-beginners

小贴士:这是从之前的一个很受欢迎的教程用Swift重写出来的,并作为iOS8盛宴的一部分发布。</br>
就像众多的超级英雄结合在一起一样,Sprite
Kit和Swift是一个令人惊讶的组合。</br>

  • Sprite Kit
    是制作iOS游戏的最好方式之一,它容易上手,强大而且完全受Apple官方支持。
  • Swift
    是一个非常简单的语言,尤其对于那些新进入iOS的人来说,这是非常容易上手的。

在这个教程里,你会学到如何使用Apple官方的2D游戏引擎来创建2D游戏,我们会使用Sprite
Kit和Swift!</br>
你可以跟着这个教程学习,也可以直接跳过教程去看最后的项目代码。当然,我们的主题是忍者。

Note:这个教程在我心中有些特别,因为这个教程之前的版本是我们这个网站最早发布的教程之一。它使用一个完全不同的语言(Objective-C)和另一个游戏引擎(Cocos2d)编写的。时间过得真是快!

先放最终效果图:

附上Spiral的动图:

Sprite Kit 对比 Unity

有一个能够替代Sprite
Kit而且很受欢迎的游戏引擎叫做Unity,Unity最早之前是一个3D引擎,但是最近也开始在内部支持2D。

所以,在你开始学习之前,我建议你先想好Sprite
Kit和Unity中哪个会是你的项目的最好选择。

澳门新葡萄京官网注册 1剪断更新01/20/2017:由Kevin
Colligan更新为iOS 10,Xcode 8和Swift 3。原文由Tammy
Coron发表,最近由Nick Lockwood更新。原文链接

澳门新葡萄京官网注册 2

Sprite Kit的优点
  • 它直接编译在iOS原生框架上
    它没有必要去下载额外的库或者产生外部依赖。你可以在不依靠额外的插件的情况下无缝的使用其他比如像iAdIn-App Purchases等等之类的iOS框架。
  • 它依赖你现有的技能:如果已经了解Swift和iOS的开发,你可以马上学会Sprite
    Kit。
  • 这是Apple官方提供的:它给你了你迁移到所有Apple新产品支持的优势。
  • 这是免费的:这可能是最重要的原因了!你可以在不花费一分钱的情况下获得到所有的Sprite
    Kit的功能,Unity有一个免费的版本,但是它不包括Pro版本的所有功能(而且比如你如果要避免Unity的弹出界面,你需要升级它)。

Cut The
Rope
是一种受欢迎的物理驱动游戏,玩家通过剪切挂起糖果的绳索喂养一只名叫Om
Nom的怪兽。在正确的时间和地点切断绳索,Om Nom将获得美味佳肴。

游戏规则是:玩家是五角星小球,小球自动沿着陀螺线向外运动,当玩家点击屏幕时五角星小球会跳跃到内层螺旋,当五角星小球碰到红色旋风或滚动到螺旋线终点时游戏结束。玩家吃掉绿色旋风来得2分,吃到紫色三角得一分并获得保护罩,保护罩用来抵挡一次红色旋风。随着分数的增加游戏会升级,速度加快。游戏结束后可以截屏分享到社交网络,也可以选择重玩。

Unity的优点
  • 跨平台 这是最大的一点了,如果你使用Sprite
    Kit。你就定死在iOS环境下,使用Unity,你可以随意到处你的游戏到Android,Windows等等。
  • 虚拟场景设计 Unity在设计场景的时候非常简单,Sprite
    Kit在iOS8也有一个场景设计工具,但是这对于Unity提供的来说非常基础。
  • 资源商店
    Unity内建了一个资源商店,你可以为你的游戏在这个商店里面购买各种各样的资源,有很多组件资源能够节省你不少的开发时间。
  • 更加强大 Unity比Sprite Kit含有更多地功能。

在对Om
Nom充分尊重的情况下,游戏的真正主角是模拟物理学:绳索摆动,重力拉动,让糖果按照您在现实生活中期望那样下落。

以下是本文内容:

我应该选择哪个?

这看过上面的之后应该在想:”那么我应该选择哪个2D框架引擎呢?“

这个答案取决你想要的目标是什么,这里是我的答案:

  • 如果你是一个编程新手,并且想要专注学习iOS:使用Sprite
    Kit——它是内建的,学起来简单,你可以很好的完成。
  • 如果你需要跨平台开发或者开发一个复杂的游戏:使用Unity——它非常强大而且灵活。

如果你觉得Unity适合你,可以看看我们的Unity教程或者我们的Unity视频教程;

否则的话,继续阅读并且开始学习Sprite Kit!

您可以使用Apple的2D游戏框架SpriteKit的物理引擎构建类似的体验。在本教程中,您将会做一个名为Snip
The Vine
的游戏。

  1. 准备工作
  2. 绘制基本界面
  3. Swift中用访问者模式处理碰撞
  4. 界面数据显示
  5. 按钮的绘制和截图分享

你好 Sprite Kit!

我们从使用Sprite Kit游戏模板创建一个Hello
World例子开始,我们使用Xcode6创建。打开Xcode
选择FileNewProject,选择iOSApplicationGame template, 点击继续。

澳门新葡萄京官网注册 3

输入项目名称为SpriteKitSimpleGame, 语言为Swift,
使用框架为SpriteKit,设备为iPhone,然后点击继续

澳门新葡萄京官网注册 4

在你的硬盘上选择一个地方来保存你的项目,然后点击创建,学则iPhone6模拟器,然后点击运行按钮,在载入界面结束后:

澳门新葡萄京官网注册 5

Sprite
Kit是通过场景的概念组织的,场景就是游戏里有层级的一系列荧幕,比如你会有一个场景来防止游戏的主要区域,还有一个场景来放世界地图,在他们之间就有层级。
如果你看一下你的项目,你会看见模板已经在你的项目中默认创建了一个叫做GameScene的场景,打开GameScene.swift文件你会发现这包含了一些代码放置一段文字当屏幕上,并在你点击的时候加入一个宇宙飞船。

在这个教程中,你主要会和这个GameScene场景打交道,但是在开始之前,你需要做一些改变,因为这个游戏是横屏的而不是竖屏的。

注意:本教程假设您有一些SpriteKit的经验。如果您是SpriteKit的新手,请查看我们的SpriteKit
Swift初学者教程。

准备工作

SpriteKit是苹果iOS7新推出的2D游戏引擎,这里不再过多介绍。我们新建工程的时候选取iOS中的Game,然后选择SpriteKit作为游戏引擎,语言选择Swift,Xcode6会为我们自动创建一个游戏场景GameScene,它包含GameScene.swiftGameScene.sks两个文件,sks文件可以让我们可视化拖拽游戏控件到场景上,然后再代码中加载sks文件来完成场景的初始化:

extension SKNode {
    class func unarchiveFromFile(file : NSString) -> SKNode? {

        let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks")

        var sceneData = NSData.dataWithContentsOfFile(path, options: .DataReadingMappedIfSafe, error: nil)
        var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)

        archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
        let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as GameScene
        archiver.finishDecoding()
        return scene
    }
}

但我比较喜欢纯写代码的方式来搭接面,因为sks文件作为游戏场景布局还不成熟,它是iOS8新加入的功能,以前在iOS7的时候sks文件只是作为粒子系统的可视化编辑文件。

所以我们修改GameViewController.swift文件的viewDidLoad()函数,像以前那样直接用代码加载游戏场景:

override func viewDidLoad() {
        super.viewDidLoad()
        // Configure the view.
        let skView = self.view as SKView
        /* Sprite Kit applies additional optimizations to improve rendering performance */
        skView.ignoresSiblingOrder = true
        let scene = GameScene(size: skView.bounds.size)
        /* Set the scale mode to scale to fit the window */
        scene.scaleMode = .AspectFill
        skView.presentScene(scene)

    }

GameScene虽然是Xcode自动生成的,但是只是个空架子,我们需要把它生成的没用的代码删掉,比如初始化函数里内容为“HelloWorld”的SKLabelNode,还有touchesBegan(touches: NSSet, withEvent event: UIEvent)方法中绘制飞船的代码。把这些删光后,我们还需要有图片素材来绘制这四类精灵节点:Player(五角星),Killer(红色旋风),Score(绿色旋风)和Shield(紫色三角)。我是用Sketch来绘制这些矢量图形的,文件名为spiral.sketch,随同工程文件一同放到GitHub上了。当然你不需要手动导出图片到工程,直接下载工程文件就好了:

准备工作

对于这个模板项目还有两个问题,第一个问题是我们需要一个横屏的,而这个项目是竖屏的,选择SpriteKitSimpleGame的target,然后在Deployment Info这些选项中,取消竖屏,然后只剩下向左横屏和向右横屏被勾上就像下面显示的那样:

澳门新葡萄京官网注册 6

其次,删除GameScene.sks文件,在弹出的时候选择移除到垃圾桶,这个文件可以让你可视化的在场景上设置精灵和其他组件,但是对于这个游戏,这个游戏非常简单的通过代码加入元素,所以你并不需要它。

接下来,打开GameViewController.swift 然后 用下面的代码替换里面的内容:

import UIKit
import SpriteKit

class GameViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    let scene = GameScene(size: view.bounds.size)
    let skView = view as SKView
    skView.showsFPS = true
    skView.showsNodeCount = true
    skView.ignoresSiblingOrder = true
    scene.scaleMode = .ResizeFill
    skView.presentScene(scene)
  }

  override func prefersStatusBarHidden() -> Bool {
    return true
  }
}

GameViewController除了他的View是个SKView之外他就是个普通的UIViewController,这个View包含了一个Sprite Kit的场景。

在这里你在viewDidLoad()中创建了一个GameScene对象,并把它初始化为屏幕大小。

这些就是准备工作,那么接下来让我们在屏幕上画点东西吧。

Snip The
Vine
,您可以将菠萝喂给鳄鱼。在开始之前,请下载启动项目。在Xcode中打开项目,以便快速查看它的结构。

绘制基本界面

这部分的工作主要是绘制出螺旋线作为地图,并让四种精灵节点动起来。

加入精灵元素

首先,下载这个项目的资源,然后将这些资源拖拽到项目中,请确认在拖进去的时候你选择了拷贝到项目文件夹中,并选择了SpriteKitSimpleGame为Target。

然后打开GameScene.swift 然后用下面的代码代替里面的内容:

import SpriteKit

class GameScene: SKScene {

  // 1
  let player = SKSpriteNode(imageNamed: "player")

  override func didMoveToView(view: SKView) {
    // 2
    backgroundColor = SKColor.whiteColor()
    // 3
    player.position = CGPoint(x: size.width * 0.1, y: size.height * 0.5)
    // 4
    addChild(player)
  }
}

让我们一步一步来看这段代码

1、这里定义了一个叫做player的私有属性,这个属性就是一个精灵元素,你可以看到,创建一个精灵元素非常简单,只要使用这个图片的名字就能创建。

2、这里设置背景颜色就和App设置背景颜色一样简单。在这里你设置了背景色为白色。

3、你设置了这个精灵的位置为x为宽度的0.1,y为居中。

4、你必须要将精灵加入到场景中去才能看到这个精灵,就和你要将Views加入其他的Views里面一样。

Build 并 运行,忍者就出现在了屏幕上。

澳门新葡萄京官网注册 7

项目文件分割在多个文件夹中。在本教程中,您可以使用包含主代码文件的Classes文件夹。随便浏览其他文件夹,如下所示:

螺旋线的绘制

SKNode有一个子类SKShapeNode,专门用于绘制线条的,我们新建一个Map类,继承SKShapeNode。下面我们需要生成一个CGPath来赋值给Mappath属性:

import UIKit
import SpriteKit
class Map: SKShapeNode {
    let spacing:CGFloat = 35
    var points:[CGPoint] = []
    convenience init(origin:CGPoint,layer:CGFloat){

        var x:CGFloat = origin.x
        var y:CGFloat = origin.y
        var path = CGPathCreateMutable()
        self.init()
        CGPathMoveToPoint(path, nil, x, y)
        points.append(CGPointMake(x, y))
        for index in 1..<layer{
            y-=spacing*(2*index-1)
            CGPathAddLineToPoint(path, nil , x, y)
            points.append(CGPointMake(x, y))
            x-=spacing*(2*index-1)
            CGPathAddLineToPoint(path, nil , x, y)
            points.append(CGPointMake(x, y))
            y+=spacing*2*index
            CGPathAddLineToPoint(path, nil , x, y)
            points.append(CGPointMake(x, y))
            x+=spacing*2*index
            CGPathAddLineToPoint(path, nil , x, y)
            points.append(CGPointMake(x, y))
        }
        self.path = path
        self.glowWidth = 1
        self.antialiased = true
        CGPathGetCurrentPoint(path)
    }
}

算法很简单,就是顺时针计算点坐标然后画线,这里把每一步的坐标都存入了points数组里,是为了以后计算其他数据时方便。因为这部分算法不难而且不是我们的重点,这里不过多介绍了。

移动的怪物

接下来你就要加入一些怪物到你的场景中来和你的忍者战斗,为了让事情变得有趣些,你需要让怪物动起来,不然的话就没有什么挑战性了!所以,让我们在屏幕的右边创建一些怪物,并给他加上移动到左边的动作。

将下面的代码加入到GameScene.swift

func random() -> CGFloat {
  return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}

func random(#min: CGFloat, max: CGFloat) -> CGFloat {
  return random() * (max - min) + min
}

func addMonster() {

  // Create sprite
  let monster = SKSpriteNode(imageNamed: "monster")

  // Determine where to spawn the monster along the Y axis
  let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)

  // Position the monster slightly off-screen along the right edge,
  // and along a random position along the Y axis as calculated above
  monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)

  // Add the monster to the scene
  addChild(monster)

  // Determine speed of the monster
  let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))

  // Create the actions
  let actionMove = SKAction.moveTo(CGPoint(x: -monster.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
  let actionMoveDone = SKAction.removeFromParent()
  monster.runAction(SKAction.sequence([actionMove, actionMoveDone]))
}

我已经加上了很多的注释来让大家更好地理解,首先我们要向之前讨论那样建立对象,你需要做一些简单的计算来确定我们在哪里创建这个怪物对象,设置它的位置,然后就像你加入忍者一样的加入怪物。

唯一不同的是我们这次要加入一些动作,Sprite
Kit提供了很多极其简便的内建方式来帮助你改变精灵元素的状态,比如移动动作,旋转动作,渐变动作,动画动作等等,在这里你会对怪物使用三种动作。

  • SKAction.moveTo(_:duration:):你使用这个方法让对象横跨屏幕到屏幕的左边,注意你可以动过设置duration来确定这大哥移动动画的时间,我们这里使用一个2~4的随机值。
  • SKAction.removeFromParent():Sprite
    Kit提供一个简便的方法来将精灵节点从他的父节点移除,就是把它从场景移除的意思,我们使用这个方法将怪物从场景中移除,这样这个怪物就消失了,这点非常重要,因为如果你不提供让怪物对象消亡的方法,你的设备的内存会被消耗光。
  • SKAction.sequence(_:)sequence将我们想要执行的方法序列化,使用这个东西可以先执行“移动”的动作,等到这个完成了,再执行“从父节点删除”的动作。

在这些动作之前,你必须要先调用创建怪物对象的方法。为了让这些更加有趣些,我们需要让怪物连续不断的出现在屏幕上。我们只需要在didMoveToView()的最后加入以下代码:

runAction(SKAction.repeatActionForever(
  SKAction.sequence([
    SKAction.runBlock(addMonster),
    SKAction.waitForDuration(1.0)
  ])
))

这样你就可以有序的调用在代码块中的动作(多亏了Swift的强大,你可以无缝的在addMaster()中加入动作代码),在完成后等待一秒钟之后,你可以虚幻这个动作的序列。

好了!运行这个项目,现在你可以欣喜的看见怪物在屏幕上移动。

澳门新葡萄京官网注册 8

澳门新葡萄京官网注册 9文件结构

四种精灵的绘制

因为四种精灵都是沿着Map类的路径来顺时针运动,它们的动画绘制是相似的,所以我建立了一个Shape类作为基类来绘制动画,它继承于SKSpriteKit类,并拥有半径(radius)、移动速度(moveSpeed)和线段计数(lineNum)这三个属性。其中lineNum是用于标记精灵在螺旋线第几条线段上的,这样比较方便计算动画的参数。

class Shape: SKSpriteNode {
    let radius:CGFloat = 10
    var moveSpeed:CGFloat = 50
    var lineNum = 0
    init(name:String,imageName:String){
        super.init(texture: SKTexture(imageNamed: imageName),color:SKColor.clearColor(), size: CGSizeMake(radius*2, radius*2))
        self.physicsBody = SKPhysicsBody(circleOfRadius: radius)
        self.physicsBody.usesPreciseCollisionDetection = true
        self.physicsBody.collisionBitMask = 0
        self.physicsBody.contactTestBitMask = playerCategory|killerCategory|scoreCategory
        moveSpeed += CGFloat(Data.speedScale) * self.moveSpeed
        self.name = name
        self.physicsBody.angularDamping = 0

    }
}

构造函数中设定了Shape类的一些物理参数,比如物理体的形状大小,碰撞检测掩码等。这里设定usesPreciseCollisionDetectiontrue是为了增加碰撞检测的精度,常用于体积小速度快的物体。collisionBitMask属性标记了需要模拟物理碰撞的类别,contactTestBitMask属性标记了需要检测到碰撞的类别。这里说的“类别”指的是物体的类别:

let playerCategory:UInt32      =  0x1 << 0;
let killerCategory:UInt32      =  0x1 << 1;
let scoreCategory:UInt32       =  0x1 << 2;
let shieldCategory:UInt32      =  0x1 << 3;

这种用位运算来判断和存储物体类别的方式很常用,上面这段代码写在了NodeCategories.swift文件中。

为了描述Shape的速度随着游戏等级上升而增加,这里速度的计算公式含有Data.speedScale作为参数,关于Data“类”在后面会讲到。

为了让精灵动起来,需要知道动画的移动目的地是什么。虽然SKActionfollowPath(path: CGPath?, speed: CGFloat)方法,但是在这里并不实用,因为Player会经常改变路线,所以我写了一个runInMap(map:Map)方法让精灵每次只移动到路径上的下一个节点(之前Map类存储的points属性用到了吧!嘿嘿)

func runInMap(map:Map){
        let distance = calDistanceInMap(map)
        let duration = distance/moveSpeed
        let rotate = SKAction.rotateByAngle(distance/10, duration: duration)
        let move = SKAction.moveTo(map.points[lineNum+1], duration: duration)
        let group = SKAction.group([rotate,move])
        self.runAction(group, completion: {
            self.lineNum++
            if self.lineNum==map.points.count-1 {
                if self is Player{
                    Data.gameOver = true
                }
                if self is Killer{
                    self.removeFromParent()
                }
                if self is Score{
                    self.removeFromParent()
                }
                if self is Shield{
                    self.removeFromParent()
                }
            }
            else {
                self.runInMap(map)
            }
            })
    }

上面的代码先是调用calDistanceInMap(map:Map)->CGFloat方法计算精灵距离下一个节点的距离(也就是需要移动的距离),然后计算精灵需要旋转动画时间和移动动画时间,最后将两个动画作为一个group来运行,在动画运行结束后判断精灵是否运行到了最后一个节点,也就是螺旋线的终点:如果到终点了则移除精灵,否则开始递归调用方法,来开始下一段动画(奔向下一个节点)。

计算距离的calDistanceInMap(map:Map)->CGFloat方法代码如下:

func calDistanceInMap(map:Map)->CGFloat{
        if self.lineNum==map.points.count {
            return 0
        }
        switch lineNum%4{
        case 0:
            return position.y-map.points[lineNum+1].y
        case 1:
            return position.x-map.points[lineNum+1].x
        case 2:
            return map.points[lineNum+1].y-position.y
        case 3:
            return map.points[lineNum+1].x-position.x
        default:
            return 0
        }
    }

到此为止Shape类完成了,KillerScoreShield类比较简单,继承Shape类并设置自身纹理和类别即可:

class Killer: Shape {
    convenience init() {
        self.init(name:"Killer",imageName:"killer")
        self.physicsBody.categoryBitMask = killerCategory
    }
}
class Score: Shape {
    convenience init() {
        self.init(name:"Score",imageName:"score")
        self.physicsBody.categoryBitMask = scoreCategory
    }
}
class Shield: Shape {
    convenience init() {
        self.init(name:"Shield",imageName:"shield")
        self.physicsBody.categoryBitMask = shieldCategory
    }
}

Player因为有护盾状态并可以在螺旋线上跳跃到内层,所以稍微复杂些:

class Player: Shape {
    var jump = false
    var shield:Bool = false {
    willSet{
        if newValue{
            self.texture = SKTexture(imageNamed: "player0")
        }
        else{
            self.texture = SKTexture(imageNamed: "player")
        }
    }
    }
    convenience init() {
        self.init(name:"Player",imageName:"player")
        self.physicsBody.categoryBitMask = playerCategory
        self.moveSpeed = 70
        self.lineNum = 3
    }
    func restart(map:Map) {
        self.alpha = 1
        self.removeAllActions()
        self.lineNum = 3
        self.moveSpeed = 70
        self.jump = false
        self.shield = false
        self.position = map.points[self.lineNum]
        self.runInMap(map)
    }
}

Player类的初始位置是螺旋线第四个节点,而且移动速度要略快于其他三种精灵,所以在这里设置为70(Shape默认速度50)。jumpshield是用来标记Player当前状态的属性,其中shield属性还定义了属性监察器,这是Swift中存储属性具有的响应机制,类似于KVO。在shield状态改变时也同时改变Player的纹理。需要注意的是构造器中对属性的改变并不会调用属性检查器,在willSetdidSet中改变自身属性也不会调用属性检查器,因为那样会造成死循环。

restart(map:Map)方法用于在游戏重新开始时重置Player的相关数据。

发射飞镖!

这个时候,忍者正在等着你给他下指令呢!那么让我们发射飞镖吧!有很多方式可以实现发射飞镖,但是在这个游戏中,我们需要完成的效果是当用户点击屏幕的时候,忍者会向用户点击的方向发射飞镖。

对于一个初学者来说,我们会使用“移动”动作来实现,但是要使用“移动“来实现我们需要做一些数学计算。

因为”移动“动作需要你告诉他一个飞镖的目的地,但是你点击的位置只是指明一个方向,而不是飞镖的目的地,你要做的是让飞镖沿着手点击的方向一直移动,直到这个飞镖飞出屏幕。

下面这张图说明了这种情况:

澳门新葡萄京官网注册 10

你可以发现,你在原始点和点击的点的之间建立了一个三角形,你需要建立一个等比的三角形,然后你会直到这个点将会从屏幕的哪里飞出。

如果你对向量计算有所了解的话,你使用起这些计算会比较得心应手,但是Srpite
Kit没有默认的这些方法,需要自己去实现。

非常幸运,多亏了强的Swift的运算符重载功能,我们能够非常简单的实现这些功能。将这些方法加入到文件的顶部,就在GameScene之前。

func + (left: CGPoint, right: CGPoint) -> CGPoint {
  return CGPoint(x: left.x + right.x, y: left.y + right.y)
}

func - (left: CGPoint, right: CGPoint) -> CGPoint {
  return CGPoint(x: left.x - right.x, y: left.y - right.y)
}

func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
  return CGPoint(x: point.x * scalar, y: point.y * scalar)
}

func / (point: CGPoint, scalar: CGFloat) -> CGPoint {
  return CGPoint(x: point.x / scalar, y: point.y / scalar)
}

#if !(arch(x86_64) || arch(arm64))
func sqrt(a: CGFloat) -> CGFloat {
  return CGFloat(sqrtf(Float(a)))
}
#endif

extension CGPoint {
  func length() -> CGFloat {
    return sqrt(x*x + y*y)
  }

  func normalized() -> CGPoint {
    return self / length()
  }
}

这些都是一些非常基础的向量计算的实现,如果你对这里为什么会这么做或者你是第一次接触向量计算的话,可以看一些这个网站迅速学期一下。

接下来,在文件中加入这个方法:

override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {

  // 1 - Choose one of the touches to work with
  let touch = touches.anyObject() as UITouch
  let touchLocation = touch.locationInNode(self)

  // 2 - Set up initial location of projectile
  let projectile = SKSpriteNode(imageNamed: "projectile")
  projectile.position = player.position

  // 3 - Determine offset of location to projectile
  let offset = touchLocation - projectile.position

  // 4 - Bail out if you are shooting down or backwards
  if (offset.x < 0) { return }

  // 5 - OK to add now - you've double checked position
  addChild(projectile)

  // 6 - Get the direction of where to shoot
  let direction = offset.normalized()

  // 7 - Make it shoot far enough to be guaranteed off screen
  let shootAmount = direction * 1000

  // 8 - Add the shoot amount to the current position
  let realDest = shootAmount + projectile.position

  // 9 - Create the actions
  let actionMove = SKAction.moveTo(realDest, duration: 2.0)
  let actionMoveDone = SKAction.removeFromParent()
  projectile.runAction(SKAction.sequence([actionMove, actionMoveDone]))
}

这里有很多知识点,我们还是一步一步来看代码。

1、一个非常酷的事情就是SpriteKit包含了UITouch的一些方法比如locationInNode(_:)previousLocationInNode(_:),这些方法能让你找到你在SKNode系统中的点击事件,通过这点,你可以找到在SKNode的点击位置。
2、然后你在忍者的位置上面创建一个飞镖对象,这时候你还不需要将这个飞镖加到场景中去,因为你需要先进行检测,因为我们的游戏不允许忍者向后发射飞镖。
3、然后通过运算计算出点击位置和忍者位置的向量。
4、如果X值小于或者等于0那么,这说明忍者将要向后发射,我们就直接return不做任何操作。
5、否则的话,将飞镖加入到场景中。
6、将获得的向量转换为长度为1的单位向量,这样我们可以用这个向量更容易的得到我们所要的长度的向量,因为1
* 长度 = 长度。
7、我们将这个单位向量乘以1000,为什么是1000?因为这样会有足够的长度让飞镖飞出屏幕。
8、加入当前的位置的数据,这样我们就能知道飞镖什么时候飞出屏幕。
9、最后像创建怪物对象一样使用moveTo(_:, duration:)方法和removeFromParent()方法。

运行代码,现在你的忍者就能够向着成群飞来的怪物发射了!

澳门新葡萄京官网注册 11

常数通过避免重复使用含义不明确的字符串或数字,使您的代码更易于阅读和维护。

Swift中用访问者模式处理碰撞

访问者模式是双分派(Double
Dispatch)模式的一种实现,关于双分派模式的详细解释,参考我的另一篇文章:Double
Dispatch模式及其在iOS开发中实践,里面包含了C++,Java和Obje-C的实现,这次我们用Swift实现访问者模式。

因为SpriteKit中物理碰撞检测到的都是SKPhysicsBody,所以我们的被访问者需要包含一个SKPhysicsBody对象:

class VisitablePhysicsBody{
    let body:SKPhysicsBody
    init(body:SKPhysicsBody){
        self.body = body
    }
    func acceptVisitor(visitor:ContactVisitor){
        visitor.visitBody(body)
    }
}

acceptVisitor方法传入的是一个ContactVisitor类,它是访问者的基类(也相当于接口),访问者的visitBody(body:SKPhysicsBody)方法会根据传入的body实例来推断出被访问者的真实类别,然后调用对应的方法来处理碰撞:

func visitBody(body:SKPhysicsBody){
        //第二次dispatch,通过构造方法名来执行对应方法
        // 生成方法名,比如"visitPlayer"
        var contactSelectorString = "visit" + body.node.name + ":"
        let selector = NSSelectorFromString(contactSelectorString)
        if self.respondsToSelector(selector){
            dispatch_after(0, dispatch_get_main_queue(), {
                NSThread.detachNewThreadSelector(selector, toTarget:self, withObject: body)
                })
        }

    }

Swift废弃了performSelector方法,所以这里耍了个小聪明来将消息传给具体的访问者。有关Swift中替代performSelector的方案,参见这里

下面让GameScene实现SKPhysicsContactDelegate协议:

func didBeginContact(contact:SKPhysicsContact){
        //A->B
        let visitorA = ContactVisitor.contactVisitorWithBody(contact.bodyA, forContact: contact)
        let visitableBodyB = VisitablePhysicsBody(body: contact.bodyB)
        visitableBodyB.acceptVisitor(visitorA)
        //B->A
        let visitorB = ContactVisitor.contactVisitorWithBody(contact.bodyB, forContact: contact)
        let visitableBodyA = VisitablePhysicsBody(body: contact.bodyA)
        visitableBodyA.acceptVisitor(visitorB)
    }

跟Objective-C中实现访问者模式类似,也是通过ContactVisitor类的工厂方法返回一个对应的子类实例来作为访问者,然后实例化一个被访问者,被访问者接受访问者的访问。A访问B和B访问A在大多数场合是相同的,但是你不知道谁是A谁是B,所以需要两种情况都调用。下面是ContactVisitor类的工厂方法和构造器:

class ContactVisitor:NSObject{
    let body:SKPhysicsBody!
    let contact:SKPhysicsContact!
    class func contactVisitorWithBody(body:SKPhysicsBody,forContact contact:SKPhysicsContact)->ContactVisitor!{
        //第一次dispatch,通过node类别返回对应的实例
        if 0 != body.categoryBitMask&playerCategory {
            return PlayerContactVisitor(body: body, forContact: contact)
        }
        if 0 != body.categoryBitMask&killerCategory {
            return KillerContactVisitor(body: body, forContact: contact)
        }
        if 0 != body.categoryBitMask&scoreCategory {
            return ScoreContactVisitor(body: body, forContact: contact)
        }
        if 0 != body.categoryBitMask&shieldCategory {
            return ShieldContactVisitor(body: body, forContact: contact)
        }
        return nil

    }
    init(body:SKPhysicsBody, forContact contact:SKPhysicsContact){
        self.body = body
        self.contact = contact
        super.init()

    }
}

PS:上面的代码省略了已经提到过的visitBody(body:SKPhysicsBody)方法

因为这个游戏逻辑比较简单,所有碰撞后的逻辑都写到了PlayerContactVisitor类里:

func visitKiller(body:SKPhysicsBody){
        let thisNode = self.body.node as Player
        let otherNode = body.node
//        println(thisNode.name+"->"+otherNode.name)
        if thisNode.shield {
            otherNode.removeFromParent()
            thisNode.shield = false
        }
        else {
            Data.gameOver = true
        }
    }
    func visitScore(body:SKPhysicsBody){
        let thisNode = self.body.node
        let otherNode = body.node
//        println(thisNode.name+"->"+otherNode.name)
        otherNode.removeFromParent()
        Data.score += 2
    }
    func visitShield(body:SKPhysicsBody){
        let thisNode = self.body.node as Player
        let otherNode = body.node
        otherNode.removeFromParent()
        thisNode.shield = true
        Data.score++
        //        println(thisNode.name+"->"+otherNode.name)
    }

上面的方法都是“visit+类名”格式的,处理的是Player碰撞到其他三种精灵的逻辑。而其他三种精灵之间的碰撞不需要处理,所以KillerContactVisitorScoreContactVisitorShieldContactVisitor这三个ContactVisitor的子类很空旷,这里不再赘述。

我们设置Player碰撞到Killer游戏结束,碰撞到Score加两分,碰撞到Shield加一分并获得护甲(shield属性设为true)。可以看到这里大量用到了Data“类“”,它其实是一个存储并管理全局数据的结构体,它里面存储了一些静态的成员属性,也可看做非线程安全的单例。

碰撞检测和物理效果

我们从在文件头部加入下面这段代码开始:

struct PhysicsCategory {
  static let None      : UInt32 = 0
  static let All       : UInt32 = UInt32.max
  static let Monster   : UInt32 = 0b1       // 1
  static let Projectile: UInt32 = 0b10      // 2
}

This is setting up the constants for the physics categories you’ll need
in a bit – no pun intended! :] (这句翻译不好,各位自己理解)

注意:你会想这TM是什么语句,你会发现这个Sprite
Kit的类是一个作为位掩码的32位的Integer数据,这个方式说明数字总的每一个位可以代表一个类(所以你最多能够含有32个类)。在这里,你设置第一位指向怪物对象,第二位指向飞镖对象等等。

接下来,让GameScene实现SKPhysicsContactDelegate接口:

class GameScene: SKScene, SKPhysicsContactDelegate {

然后,在didMoveToView(_:)中在加入忍者之后加入这几行代码:

physicsWorld.gravity = CGVectorMake(0, 0)
physicsWorld.contactDelegate = self

这设置这个这个物理世界没有重力,并且能够将两个物体的碰撞事件通过delegate传递到场景中。

addMonster()方法中,在创建怪物对象的后面插入这几行代码:

monster.physicsBody = SKPhysicsBody(rectangleOfSize: monster.size) // 1
monster.physicsBody?.dynamic = true // 2
monster.physicsBody?.categoryBitMask = PhysicsCategory.Monster // 3
monster.physicsBody?.contactTestBitMask = PhysicsCategory.Projectile // 4
monster.physicsBody?.collisionBitMask = PhysicsCategory.None // 5

我们一行一行来看这些代码做了什么

1、为精灵元素创建SKPhysicsBody,在这个栗子中,SKPhysicsBody是一个和精灵大小相同的矩形,我们把这个当做这个怪物的近似形状。

2、这只精灵元素是动态的,这意味着物理引擎是不能控制这个精灵元素的移动的,你可以通过代码来设置这个精灵元素的移动。

3、设置精灵元素的bit mask为我们之前定义的monsterCategory

4、contactTestBitMask indicates what categories of objects this object
should notify the contact listener when they intersect. You choose
projectiles here.

5、The collisionBit Mask indicates what categories of objects this
object that the physics engine handle contact responses to (i.e. bounce
off of). You don’t want the monster and projectile to bounce off each
other – it’s OK for them to go right through each other in this game –
so you set this to 0.(这两段没看懂)

接下来,在touchesEnded(_:withEvent:)中,在设置飞镖的地点之后加入下面代码:

projectile.physicsBody = SKPhysicsBody(circleOfRadius: projectile.size.width/2)
projectile.physicsBody?.dynamic = true
projectile.physicsBody?.categoryBitMask = PhysicsCategory.Projectile
projectile.physicsBody?.contactTestBitMask = PhysicsCategory.Monster
projectile.physicsBody?.collisionBitMask = PhysicsCategory.None
projectile.physicsBody?.usesPreciseCollisionDetection = true

可以自己尝试着看懂这些代码,如果你还不懂,就回过头去看看之前的讲解。

第二个测试就是看看这两段代码有什么不一样的地方。

接下来,定义一个飞镖碰撞怪物之后的处理的方法,注意这个方法不会自动调用,你需要自己调用,

func projectileDidCollideWithMonster(projectile:SKSpriteNode, monster:SKSpriteNode) {
  println("Hit")
  projectile.removeFromParent()
  monster.removeFromParent()
}

你在这里只是当他们碰撞的时候从场景中移除怪物和飞镖,很简单!是吗?

现在,是时候实现delete了,将下面这个新方法加到文件中

func didBeginContact(contact: SKPhysicsContact) {

  // 1
  var firstBody: SKPhysicsBody
  var secondBody: SKPhysicsBody
  if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
    firstBody = contact.bodyA
    secondBody = contact.bodyB
  } else {
    firstBody = contact.bodyB
    secondBody = contact.bodyA
  }

  // 2
  if ((firstBody.categoryBitMask & PhysicsCategory.Monster != 0) &&
      (secondBody.categoryBitMask & PhysicsCategory.Projectile != 0)) {
    projectileDidCollideWithMonster(firstBody.node as SKSpriteNode, monster: secondBody.node as SKSpriteNode)
  }

}

因为你设置了这个场景为这个物理世界的代理,这个方法在每次两个精灵经行碰撞的时候都会调用。

这个方法有两部分:

1、这个方法传递了两个碰撞的精灵元素,但是我们不能保证两个精灵元素的前后顺序,所以这些位掩码可以帮助我们识别两个精灵元素。

2、最后,判断碰撞的两个精灵元素是不是一个是怪物精灵,一个是飞镖精灵,然后调用之前定义的那个方法。

运行程序,现在,当你的飞镖撞击到目标的时候,他们就会消失。

打开Constants.swift并添加以下代码:

界面数据显示

这部分很简单,主要是将Data结构体中存储的分数和等级等数据通过SKLabelNode显示在界面上,只不过我封装了一个Display类来将所有的SKLabelNode统一管理,并让其实现我定义的DisplayData协议来让Data中的数据变化驱动界面更新:

protocol DisplayData{
    func updateData()
    func levelUp()
    func gameOver()
    func restart()
}

下面是Data结构体代码,大量使用了存储属性的监察器来响应数据变化:

struct Data{
    static var display:DisplayData?
    static var updateScore:Int = 5
    static var score:Int = 0{
    willSet{
        if newValue>=updateScore{
            updateScore+=5 * ++level
        }
    }
    didSet{
        display?.updateData()
    }
    }
    static var highScore:Int = 0
    static var gameOver:Bool = false {
    willSet{
        if newValue {
            let standardDefaults = NSUserDefaults.standardUserDefaults()
            Data.highScore = standardDefaults.integerForKey("highscore")
            if Data.highScore < Data.score {
                Data.highScore = Data.score
                standardDefaults.setInteger(Data.score, forKey: "highscore")
                standardDefaults.synchronize()
            }
            display?.gameOver()
        }
        else {
            display?.restart()
        }
    }
    didSet{

    }
    }
    static var level:Int = 1{
    willSet{
        speedScale = Float(newValue)*0.1
        if newValue != 1{
            display?.levelUp()
        }
    }
    didSet{
        display?.updateData()

    }
    }
    static var speedScale:Float = 0{
    willSet{

    }
    didSet{

    }
    }

    static func restart(){
        Data.updateScore = 5
        Data.score = 0
        Data.level = 1
        Data.speedScale = 0
    }
}

这里不得不提到一个更新界面时遇到的一个坑,当我想通过名字遍历GameScene子节点的时候,一般会用到enumerateChildNodesWithName(name: String?, usingBlock: ((SKNode!, UnsafePointer<ObjCBool>) -> Void)?)方法,但是这个方法在Xcode6Beta3更新后经常会抛异常强退,这让我很费解,恰巧遇到此问题的不只是我一个人,所以还是老老实实的自己写循环遍历加判断吧。

结束点击

现在,你离一个非常好玩(但是非常简单)的游戏已经很近了,你现在只需要一些特效和音乐(当然,哪个游戏没有音乐效果?)和一些简单的游戏逻辑。

Sprite
Kit本身没有像cocos2D一样带有一个音频引擎,但是有一个好消息就是我们有个方法来根据动作来播放音乐,你可以使用AVFoundation来播放背景音乐。

你现在已经一些我给的非常酷炫的音乐,这会对你的项目产生非常棒的效果,这些音乐就在之前你加入项目的资源文件里面,你只需要播放他们!

为了实现这些,你需要在GameScene.swift中加入这些代码:

import AVFoundation

var backgroundMusicPlayer: AVAudioPlayer!

func playBackgroundMusic(filename: String) {
  let url = NSBundle.mainBundle().URLForResource(
    filename, withExtension: nil)
  if (url == nil) {
    println("Could not find file: (filename)")
    return
  }

  var error: NSError? = nil
  backgroundMusicPlayer = 
    AVAudioPlayer(contentsOfURL: url, error: &error)
  if backgroundMusicPlayer == nil {
    println("Could not create audio player: (error!)")
    return
  }

  backgroundMusicPlayer.numberOfLoops = -1
  backgroundMusicPlayer.prepareToPlay()
  backgroundMusicPlayer.play()
}

这些是一些AVFoundation的代码。
想要试一试这些,只要在didMoveToView(_:)刚开始加入下面这些代码:

playBackgroundMusic("background-music-aac.caf")

至于特效音乐,在touchesEnded(_:withEvent:)加入下面代码:

runAction(SKAction.playSoundFileNamed("pew-pew-lei.caf", waitForCompletion: false))

非常方便,是不是?你只需要一行代码就能播放音效了。
运行项目,享受这些音乐吧!

struct ImageName { static let Background = “Background” static let Ground = “Ground” static let Water = “Water” static let VineTexture = “VineTexture” static let VineHolder = “VineHolder” static let CrocMouthClosed = “CrocMouthClosed” static let CrocMouthOpen = “ CrocMouthOpen“ static let CrocMask = ”CrocMask” static let Prize = “Pineapple” static let PrizeMask = “PineappleMask” } struct SoundFile { static let BackgroundMusic = “CheeZeeJungle.caf” static let Slice = “Slice.caf” static let Splash = “Splash.caf” static let NomNom = “NomNom.caf” }

按钮的绘制和截图分享

参考我的另外两篇文章:在游戏的SKScene中添加Button和SpriteKit截屏并分享至社交网络

在本工程中只有ShareButtonReplayButton两个按钮,Swift版本的代码很简洁,而我通过Social.Framework中的UIActivityViewController来分享得分,这部分代码写在了ShareButton.swift中:

let scene = self.scene as GameScene
        let image = scene.imageFromNode(scene)
        let text = "我在Spiral游戏中得了(Data.score)分,快来追逐我的步伐吧!"
        let activityItems = [image,text]
        let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
        (scene.view.nextResponder() as UIViewController).presentViewController(activityController, animated: true, completion: nil)

游戏结束,小伙子!

现在让我们新建一个场景来作为“获胜”或者“失败”页面,新建文件iOSSourceSwift File template,为文件命名,然后点击创建。

然后在GameOverScene.swift中用下面代码代替里面的内容:

import Foundation
import SpriteKit

class GameOverScene: SKScene {

  init(size: CGSize, won:Bool) {

    super.init(size: size)

    // 1
    backgroundColor = SKColor.whiteColor()

    // 2
    var message = won ? "You Won!" : "You Lose :["

    // 3
    let label = SKLabelNode(fontNamed: "Chalkduster")
    label.text = message
    label.fontSize = 40
    label.fontColor = SKColor.blackColor()
    label.position = CGPoint(x: size.width/2, y: size.height/2)
    addChild(label)

    // 4
    runAction(SKAction.sequence([
      SKAction.waitForDuration(3.0),
      SKAction.runBlock() {
        // 5
        let reveal = SKTransition.flipHorizontalWithDuration(0.5)
        let scene = GameScene(size: size)
        self.view?.presentScene(scene, transition:reveal)
      }
    ]))

  }

  // 6
  required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

这个代码中有5个值得提及的部分:

1、设置背景为白色,就和之前在主场景做的一样。

2、通过won参数来决定是“胜利”或者“失败”。

3、这展示了应该怎么在使用Sprite
Kit在屏幕上放一个文字,正如你所见的,这非常简单,你只需要设置字体并定义一些参数。

4、最后,定义两个有序的动作,刚开始先等待两秒,然后它运行了代码块

5、这是我们切换场景的动画,你可以在多种多样的动画中选择一种方式来切换场景,在这里你选择了Flip动画,并设置动画时间为0.5秒,然后你创建了一个你想要显示的场景,然后使用self.view的presentScene(_:transition:)方法来切换。

6、如果你实现了场景的init方法,那么你必须也实现init(coder:)方法,
虽然这个方法是不会被调用的,所以你只需要随便加上fatalError(_:)之类的代码。

这下好了,现在你只需要启动你的主场景,然后在适当的时候切换到游戏结束场景就好了。

切换回到GameScene.swift, 在addMonster()中的最后加入下面的代码:

let loseAction = SKAction.runBlock() {
  let reveal = SKTransition.flipHorizontalWithDuration(0.5)
  let gameOverScene = GameOverScene(size: self.size, won: false)
  self.view?.presentScene(gameOverScene, transition: reveal)
}
monster.runAction(SKAction.sequence([actionMove, loseAction, actionMoveDone]))

这设定了当怪物走出了屏幕之后你就失败了,如果你理解了所有的代码,如果不参考教程对之前代码的解释,这里给你来个突击检测:你为什么要在actionMoveDone之前运行loseAction,如果你不知道会发生什么的话,你可以颠倒顺序试试看。

现在你需要处理胜利的逻辑了,别对你的玩家太残忍了,在GameScene的最上面加上一个新的属性,就放在Player的下面:

var monstersDestroyed = 0

并且在projectile(_:didCollideWithMonster:):最下面加上这些代码:

monstersDestroyed++
if (monstersDestroyed > 30) {
  let reveal = SKTransition.flipHorizontalWithDuration(0.5)
  let gameOverScene = GameOverScene(size: self.size, won: true)
  self.view?.presentScene(gameOverScene, transition: reveal)
}

继续运行项目,现在你就可以产生胜利和失败的条件,并在适当的时候切换到游戏结束的场景!

澳门新葡萄京官网注册 12

使用以上代码,您定义了一些常量保存sprite图像名称和声音文件名称。

看完这个该何去何从

结束了,这是这个Sprite Kit Swift Tutorial for beginners教程的全部代码

我希望你能够喜欢学习Sprite Kit并能够有兴趣自己做一个自己的游戏

如果你想逃学习更多地关于Sprite Kit的知识,你可以看一下我们的书《 iOS
Games by
Tutorials》

在下面添加以下内容:

struct Layer { static let Background: CGFloat = 0 static let Crocodile: CGFloat = 1 static let Vine: CGFloat = 1 static let Prize: CGFloat = 2 static let Foreground: CGFloat = 3 }struct PhysicsCategory { static let Crocodile: UInt32 = 1 static let VineHolder: UInt32 = 2 static let Vine: UInt32 = 4 static let Prize: UInt32 = 8 }

上面的代码声明了两个结构体:LayerPhysicsCategory,它们分别包含一些static
CGFloatUInt32的属性。当您将它们添加到场景中时,将使来指定精灵的zPosition和物理学属性。

最后再添加一个struct

struct GameConfiguration { static let VineDataFile = “VineData.plist” static let CanCutMultipleVinesAtOnce = false }

VineDataFile指定了葡萄藤所在位置文件的名称。

CanCutMultipleVinesAtOnce允许通过简单的方式修改游戏参数。怎样的游戏决策会使游戏更有趣,这总是并不明确的。这样的常数提供了一种简单的开关方式,让你可以稍后改变你的游戏。

现在,您可以开始向场景添加节点。

打开GameScene.swift并将以下内容添加到setUpScenery()

let background = SKSpriteNode(imageNamed: ImageName.Background)background.anchorPoint = CGPoint(x: 0, y: 0)background.position = CGPoint(x: 0, y: 0)background.zPosition = Layer.Backgroundbackground.size = CGSize(width: size.width, height: size.height)addChild(background) let water = SKSpriteNode(imageNamed: ImageName.Water)water.anchorPoint = CGPoint(x: 0, y: 0)water.position = CGPoint(x: 0, y: 0) water.zPosition = Layer.Foregroundwater.size = CGSize(width: size.width, height: size.height * 0.2139)addChild

setUpScenery()didMove()方法中被调用。在这个方法中,创建一些SKSpriteNode实例,并调用SKSpriteNode(imageNamed:)进行初始化。为了处理多个屏幕尺寸,您需要显式设置背景图像的大小。

您将节点的anchorPoint从默认值更改为。这意味着节点从原本的相对于中心定位变成了相对于左下角定位,这使您可以轻松地将backgroundwater与场景底部对齐。

注意:anchorPoint属性使用单位坐标系,其中表示图像的左下角,表示右上角。因为它的值总是从0到1,所以这些坐标与图像尺寸和宽高比无关。

您还可以设置Sprite的zPosition,控制节点在屏幕上的绘制顺序。

回想一下,在Constants.swift中,您指定了一些用于zPosition的值。这里使用其中的:Layer.BackgroundLayer.Foreground,确保背景总是落在另外的Sprite之后,前景将始终画在最前面。

编译并运行您的项目。如果您做的一切正确,您应该看到以下效果:

澳门新葡萄京官网注册 13背景图

下一篇文章将开始在项目中添加游戏的主角——鳄鱼。

发表评论

电子邮件地址不会被公开。 必填项已用*标注