Sprite Kit

如何制作自己的游戏音乐


原文地址:http://www.raywenderlich.com/71872/how-to-make-game-music-for-beginners
泰然翻译组:Leo。校对:glory。

当你开发了一款画面精美,玩法有趣,故事新颖的游戏时,似乎一切已经大功告成了。你还能要求些什么呢?

可尽管如此,如果一款游戏里没有音乐,总让人觉得缺了点什么。

雇佣专业的游戏音效师当然是最佳选择,可如果你预算不够,那也没关系。通过一些强大的ipad app,你也能够轻松地制作属于自己的游戏音乐。

就个人而言,我非常喜欢使用 Korg Gadget 这个款ipad app。它集成了多达15款的电子合成器和鼓机,并且使用起来简单,非常容易上手。

你唯一需要做的就是通过点击,拖拽来合成一首音乐。制作好音乐后,很容易就可以从Korg Dadget导出音频文件,并应用到你的app中。

在这篇教程中我会指导你用Korg Gadget使用多种乐器制作一首简单的音乐,导出音频文件,并最终添加到你的app中。

准备好去发现潜藏在你体内的音乐才能了吗?来吧!:]

前期工作

从app store中下载 Korg Gadget。 Korg Gadget并不便宜,在app store上的售价是38.99美元,但偶尔会有折扣价。

用ipad打开Korg Gadget,点击左上角的文件图标来创建一个新项目,如下所示:

点击 New, 输入 RW Game Track 作为新项目名字, 然后点击 OK。

接着就能看到一系列可供选择的音序器,在app中我们习惯称之为 Gadgets。选择 London 鼓机,如下所示:

app接着就会自动跳转至 混音 界面,用户在此界面合成音乐。界面上半部分默认显示音轨列表,如下所示:

我们点击 Track 1 进入编辑界面:

在屏幕的左上方你能看到 Draw 图标和 Select 图标。 选择不同图标以来回切换 "drawing" 模式和 "selecting" 模式。

点击屏幕底部八个蓝色 面板 中的任意一个,就能实时听到伦敦鼓机的声效。 每个面板蓝色显示屏中都标明着该面板的鼓声音色。你可以点击蓝色显示屏来试听相应面板的音色。

点击控制台左边的 Drum Kit 屏,从打开弹出列表菜单中,浏览所有可用的爵士鼓。我们滑到列表下面,并选择 042 Lockeroom。

想要给你的音乐添加音符很方便,你想要在哪个地方添加音符,只需相应点击该处就行。作为你的第一次尝试,可用照下图所示添加一些音符。

如果你点错把音符加到了其他地方,只需再点击一次该音符就可以移除它了。

接着点击工具条下方的 Play 图标,就可以听到编排的音乐效果了。随着进度条播放,相应的音符就播放出来了。

循环 图标默认是绿色的选中状态,如果不手动点击暂停图标音乐就会不断循环下去。取消循环,音乐就会在播放完后自动停止。

如果你想把所有音符从屏幕中清除掉,按住 Hold to erase 按钮,音符就会在播放完后立即从界面中移除。

我把我制作的版本上传上了SoundCloud,你可以拿你的跟我的进行下比较:

下面再试着保存当前的项目。先点击 Back 图标,回到混音界面,再点击 Document 图标,最后点击 Save 图标就可以保存项目了。

基本的爵士鼓音轨就弄好了,接着我们需要为我们的音轨添加一些低音,可以尝试下a la Blade Runner或者是Miami Vice。

添加简单的低音

做完前面的操作,你现在应该返回到混音界面了。点击屏幕虚线矩形内的+号按钮以创建一个新的gadget,如下所示:

从列表中选择 Chicago Gadget ,并点击 Chicago 图标进入编辑界面。

你可以留意到这个音序器的面板比之前的伦敦合成器少些(这个有7个,伦敦合成器则有8个)。同时,这个音序器的面板不再是用于调节不同乐器声音,而是用于控制音高。第一、第四、第七个这三个面板带有黑点,表示这三个面板的 八度音程 不同于其他几个面板。

注意: 在西方音乐中,一个八度通常包含八个音符,即人们常说的Do,Re,Mi,Fa,So,La,Ti ,Do。Chicago gadget就涵盖了这些音符。而第一,第三和第五音符,或者说Do,Mi和So。这几个音符比其他音符高了一个八度。

第一个键代表middle C音高,88键钢琴的中间键也是这个音高。这个音高对于游戏音乐的低音部分而言还是显得太高了。为了降低音高,可以把左边旋钮调小,直到音高降到 G1

这一回,我们不再只是通过单纯点击屏幕放置音符,而是 点击屏幕并拖动 至右侧来创建音符,如下所示:这个音符就会持续一整个bar的时间了:

点击 Sound Program 并选择 028 Downer Arp — 这可以改变音符的音色。通过这样预设,你就能得到一种近似于用Roland Arp合成器和自动琶音器所合成的音色:

注意:每次播放一根弦上的音符,这样的话,第一、第三、和第五个音符彼此就有间隔--这样就发出了一个 琶音。 下行琶音以音高最高的音符作为开始,随后的音符音高依次降低,上行琶音则反之。

你可以尽情实验:尝试不同类型的琶音: 上行上行/下行2度 and 1度 等等,看看音乐的变化。你也通过点击琶音控制器中的 On 按钮来关掉琶音音效。

当你尝试完后,记得把按钮调到 FullDown 的位置。

你可以听听我的版本来比较一下:

至此为此,你的游戏音乐已经有基本音轨了。但绝大部分音乐都还需要一个旋律。

如果你觉得这很复杂,那我告诉你:大部分流行歌曲的主旋律部分都只是由4到6个音符组成而已。 :]

为你的音乐添加旋律

点击 Back 按钮返回混音界面,并点击 + 按钮以增加第三个Gadget。选择 Wolfburg Gadget 并点击进入编辑界面。

通过图中红框中的左右箭头,选择名称为 020 Syn. Trumpet 的乐器。基本操作流程还是像之前合成伦敦鼓音效时相仿。

尝试做出跟下图一样的样式:

在使用伦敦合成器合成音效时,每一行代表一种乐器声音;而在这里中,每一行代表一个不同的音符。

我用了 A#3 音符来有节奏性地打拍子,发挥类似贝斯鼓的作用。我还添加了一些 C4D4 音符,有些踩在拍子上,有些则不然。在第四个拍子上稍微做了些强化效果,会同时播放 F4, A#3G3 三个音符。

这就是目前的成果。]

一首音乐马上就因此而变得丰满起来,但它仍旧需要一些其他元素来把这些音符更有机地整合在一起。为了达到这一点,你需要再加些低音来填满歌曲结尾。

再添加一些低音

点击 Back 按钮回到混音界面,并点击 + 按钮再添加一个Gadget。这次我们选择 Berlin 合成器,进入编辑界面并通过点选 +- 按钮来选择 005 Natural Lead 音色。

正如之前操作的那样,同样点击并拖动 音符来使 G1 音符一直持续,如下所示:

这就是歌曲现在的效果:

你现在完成了一首有4个bar的音乐了,现在应该让这首音乐彻底地充实起来了。

分解你的音乐

当制作一首音乐时,我通常将有着相同音乐节拍的 场景 放在一起。我一点点地建立起场景,并逐步简化它。因为我知道游戏音乐会不断地循环,所以我会尝试将音乐结尾的节奏做得跟音乐开头相仿,使得音乐重复循环起来显得尽可能自然平缓。

至此为止,你的音乐里已经有了一层场景了。为了充实你的音乐并加长音乐时长,你需要把已经制好的音轨复制一份,并把它插到原音轨的中间,直到音乐结尾。为了达到这种效果,你需要点击 Function 按钮来复制音轨,并去除歌曲中的多余元素。

进行到现在,你保存过你的项目吗?如果没还保存,返回混音界面,点击Document图标,并点击 Save 图标保存项目吧。

在调音界面点击处于左下角的 Function 按钮,会弹出一层菜单,并显示当前界面的可选命令,如果你的弹出菜单的命令看起来跟上面的截图不同,那你可能还处在编辑界面。

在顶部的功能栏,连续点击 Duplicate 按钮两次来创建场景备份,如下所示:

点击 Function 按钮则可退出这个功能。

你现在应该可以看到三个独立的场景了,它们分别被标以 1, 23 ,如下所示:

首先要做的,通过点击绿色状的循环图标以取消循环,可以看到循环图标会变灰。接着点击播放图标,播放音乐。

你现在的工作就是把剥除你音乐头尾部分的的一些元素,使得音乐听起来更加动态。

点击 Function 按钮弹出编辑菜单,在 场景1 中,我们点击 音轨4Clear 按钮,以清除低音,如下所示:

点击Sure以执行操作,再以同样方法清除场景2中的低音。

再次点击 Function 按钮,你将看到场景1,场景2的低音已经被移除*,只剩下场景3有低声了。

这时再点击 Play 按钮,就可以听听现在的音乐效果了。以下是此时我所制作的音乐效果:

这是你肯定可以听到音乐的效果了,但是声音变化得太快了,我们在之后会处理这一点。

点击 Function 再次进入编辑模式。 点击场景1音轨3 Clear 按钮清除旋律。

点击 Play 看看现在的效果。以下是我制作的版本:

这是你肯定可以听到音乐的效果了,但是声音变化得太快了,我们在之后会处理这一点。

在我们继续下一步前,我们最好再 保存 一次项目。

改变场景的节拍数

在一个场景中的任一种乐器都可以有它自己的节拍数。至今为止,你的音乐里的每一条音轨都只有一拍长度--通过添加拍子,你可以延长你的场景,并使得音乐时长更长。

点击场景1中的 音轨4 ,进入音轨编辑。当前场景的长度显示在屏幕的上方--1拍。

点击 Function 按钮,选择 4 个节拍片段来改变音轨的长度,像这样:

点击 Function 按钮退出。

点击 Back 按钮返回至调音界面;你能看到音轨4已经被分成了16个节拍。我们先点击 loop 按钮,再点击 Play 按钮,听听现在跟之前的变化。

重复上面的步骤,使得每个场景都是每节4拍。

当你回到混音界面时,你会发现现在低音只有此前的1/4长度了,如下所示:

你希望低音出现在所有4个拍子里,你可以使用复制功能来进行修正。

点击场景3的 音轨4 ,它默认打开节拍1。点击节拍的顶部来查看节拍2,节拍3,节拍4的内容。先点击Function再点击弹出菜单中的灰色Copy按钮,按住1 Bar来复制其中的音符,然后根据界面显示,选择复制音符的目的地,如下所示:

点击 2 Bar ,就可以将节拍1的内容复制到节拍2中。我们依法炮制,将节拍1的内容同样复制到节拍3,节拍4。

点击 Back 按钮返回到调音界面,点击 Play 按钮就能听到,所有4个节拍都有低音了,如下所示:

这时迄今为止我所制作出来的效果:

这首音乐现在更为缓和,感觉更好了。现在你需要混合一些元素,来使得音乐听起来不那么单调。

点击调音界面的 Function 按钮,并点击场景3中的 Duplicate 按钮两次,最后再点击 Function 按钮。

接着点击场景4的 音轨4 来编辑音序,把低音音高从 G1 拖 至 A#1 。通过如此调高低音的音高使得这部分音乐更为激昂一些。对节拍2同样依法炮制(或者直接将节拍1的内容复制到节拍2中)

点击 Function ,再点击 2 ,使得场景缩短至2个节拍。点击 Back 返回调音界面。同样将场景5缩短至两个音节。

这时的弹出菜单应该如同下面截图一样了:

点击 Play 再次听听现在音乐效果。点击绿色的循环图标以关掉场景循环。

以下是我制作出的音乐效果:

现在音轨已经制作得相同不错了。你现在可以分解音乐结尾部分,使得其循环播放起来自然平缓。

分解你的音乐

用之前所学的技巧将场景5复制两份。如果现在屏幕看起来感觉太拥挤了,你可以点击expand图标,将乐器调音器面板隐藏起来。

使用 Function 按钮来清除场景6,场景7中 音轨4 的低音。同样也要清除场景7中的旋律音轨。试试看你能否在不看下面指导的情况下完成整个操作。

提示:点击 Function 按钮以打开编辑菜单,再点击场景6,场景7中的音轨4的 Clear 按钮清除低音。再点击场景7的音轨3的 Clear 按钮清除旋律。

通过复制场景7来创建场景8。删除场景8中除去鼓机音轨的其他音轨。同时记得将场景6,场景7调整为4个音节。完成了这些后,你的成品看起来应该跟下图截图一样:

将你完成的杰作从头播放一遍,听起来你已经大功告成了!

现在你已经有了一首不错的音乐了,接着就等着把它导出成音频文件,并把它应用到你的app中了。

导出音频文件

要把你的音乐导出成音频文件,你需要点击 Document 图标并点击 Export 按钮。你可以把你的歌曲导出至 GadgetCloud 中。除此之外,你还可以用iTunes export,DropBox或者AudioCopy,将其导出成 音频文件 ,并把它应用到app中。

将其导出成音频文件至所选位置,现在你就有了一个可用的音频文件了,你可以进行一些简单的修剪编辑,在正式应用进你的游戏中。

用QuickTime Plaer修剪你的音乐

因为在文件导出过程中,音频结尾会加入几秒的空白时间,所以你很可能需要修剪编辑一下你的音频文件。 — 否则音乐循环起来就不那么自然了!

幸运的是,通过QuickTime Player我们可以很容易修剪音频文件。

QuickTime Player 打开文件,并在 View 菜单项中将其设置为 Loop 。接着选择从 Edit 菜单中选择 Trim 选项,并调整音乐的长度。当音乐循环时仔细听清楚,试着将你的音乐修剪得在循环播放时没有明显断点或明显变调。

调试到你觉得满意了,你就可以 导出 修剪后的音频文件。从 File 菜单中选择 Export ,选择 Audio Only 并将其保存为m4a文件。

如果你想看看我是怎么修剪我的文件的,你可以点击这里:RW Game Track

剩余的工作就是把音乐应用到你的app中了!

添加音乐到Sprite Kit App

注意:这一部分内容是可选的;Sprite Kit的粉丝可能会对这部分内容很感兴趣,对于其他人而言到上一章为止,本教程已经结束了!

下载完整的 SpriteKit Space Invaders app;如果你想要为你的app加一些背景音乐,你可以读读以下由Joel Shapiro所写的教程

用Xcode打开 GameScene.m ,在文件上方import如下的包:

@import AVFoundation;

接着定义一个私有实例变量,用于存储music player实例,如下所示:

@interface GameScene () {
    AVAudioPlayer *_backgroundMusicPlayer;
}

添加如下面所示的方法,用于播放背景音乐:

(void)playBackgroundMusic:(NSString *)filename{
    NSError *error;
    NSURL *backgroundMusicURL = [[NSBundle mainBundle] URLForResource:filename withExtension:nil];
    _backgroundMusicPlayer = [[AVAudioPlayer alloc]
        initWithContentsOfURL:backgroundMusicURL error:&error]; 
    _backgroundMusicPlayer.numberOfLoops = -1;
    [_backgroundMusicPlayer prepareToPlay];
    [_backgroundMusicPlayer play];
}

最后,在调用完 setupHud 方法后,在 GameScene.m 添加如下内容以嵌入音频(记得将双引号的内容改成你自己的音频文件名字):

[self playBackgroundMusic:@"RW Game Track.m4a"];
注意: 对于使用基于标准Sprite Kit模板开发的app,可以使用 initWithSize: 这个方法替代。 

编译并运营app;你就可以听到你的游戏有背景音乐了。真令人充满成就感啊,不是吗? :]

一旦游戏结束,随着Sprite Kit场景变换,音乐就停止了。

如何进一步改良?

这就完成后的项目文件 RW Game Track.gdproj。你可以用iTunes sharing把它添加进你的Korg Gadget app中。

想要进一步改良你的音乐,你可以对每个场景内的音符进行微调,强化音乐表现。你可以尝试在不同地方添加音符和低音。更建议你使用Amsterdam gadget来添加一些有趣的音效。最重要的是发挥创意,并尽情享受这个过程!

如果你有任何的建议、分享,请在下面留言讨论!

在Swift中用Sprite Kit制作的卡牌游戏机制


原文地址:http://www.raywenderlich.com/76718/card-game-mechanics-sprite-kit-swift
泰然翻译组:柳比。校对:glory。

Example card image

学习如何实现基本的卡牌游戏机制和动画。

在过去的20年里,人们玩过收集类卡牌游戏(CCGs)。维基百科提供了比较详尽的这些游戏演变的过程,它似乎启发了角色扮演游戏(RPG)像龙与地下城。万智牌是一个现代化的CCG的一个例子。

其核心,CCGs是一组自定义卡片代表了人物,位置,能力,事件等。玩游戏前,玩家首先必须构建他们自己的牌组,然后他们才能使用他们独特的牌组来玩游戏。大多数玩家的牌组都会突出某种派别,生物或能力。

在本教程中,你将在CCG应用程序中使用Sprite Kit操纵图片卡。你会在屏幕上移动卡牌,移动它们来看哪些卡牌是有效的,翻转它们并放大它们来看卡牌上的文字 — 或欣赏艺术作品。

如果你还不熟悉SpriteKit,你可以阅读初学者的教程iOS游戏教程。如果你还不熟悉Swift,确认你已经了解了Swift快速入门系列.

开始

由于这是一个卡牌游戏,最好的开始就是看实际的卡牌。下载启动项目,它提供了一个iPad上横向模式下的SpriteKit项目,以及所有的图像,字体和声音文件,和你需要创建的一个功能性的示例游戏。

花一点时间来熟悉项目的文件结构和内容。你应该能看到如下的项目目录:

  1. System: 包含设置一个SpriteKit项目的基础文件。它包含了AppDelegate.swiftGameViewController.swiftMain.storyboard

  2. Scenes: 包含管理游戏内容的一个空主场景文件GameScene.swift

  3. Card: 包含管理卡牌的一个空的Card.swift文件。

  4. Supporting Files: 包含你将在本教程中使用的所有图像、字体和声音文件。

这个游戏没有美工不会显得很酷,所以我想特别感谢gameartguppy.com的Vicki的美丽的卡牌插图!

一个优雅的开始

既然我们需要使用卡牌玩卡牌游戏,我们将创建一个卡牌类来代表它们。目前Card.swift是一个空的Swift文件,所以找到它并添加:

import Foundation

import SpriteKit



class Card : SKSpriteNode {



  required init(coder aDecoder: NSCoder!) {

    fatalError("NSCoding not supported")

  }



  init(imageNamed: String) {

    let cardTexture = SKTexture(imageNamed: imageNamed)

    super.init(texture: cardTexture, color: nil, size: cardTexture.size())

  }

}

你声明了CardSKSpriteNode的子类。

要根据图片创建一个简单的精灵,你可以使用SKSpriteNode(imageNamed:)。为了保持这种行为,你需要使用继承的初始化函数,它将调用父类的指定的初始化函数init(texture:color:size:)。在本游戏中你不支持NSCoding

要把精灵放到屏幕上,打开GameScene.swift然后添加下列代码到didMoveToView()中:

let wolf = Card(imageNamed: "card_creature_wolf.png")

wolf.position = CGPointMake(100,200)

addChild(wolf)



let bear = Card(imageNamed: "card_creature_bear.png")

bear.position = CGPointMake(300, 200)

addChild(bear)

构建和运行项目,并花一点时间来欣赏儿狼和熊。

Card Images on iPad Screen

一个好的开始…

规则 #1 创建卡牌游戏:先有创意和富有想象力的美工。看起来你的应用程序塑造的不错!

注: 根据屏幕的大小,你可能需要缩放模拟器的窗口,使用Window\Scale\50%来适应屏幕。我也推荐使用iPad 2模拟器。

看这一对卡牌非常有趣,但如果你能真正的移动卡片UI将会更酷。你将在下面做到这些!

我想移动它,移动它…

无论美工的质量如何,卡牌静坐在屏幕上不会让你的应用程序获得任何的好评,因为你需要像你在玩真实的纸质卡牌一样能拖动它们。实现它的最简单的办法是在场景中处理触摸事件。

依旧在GameScene.swift中,添加这个新的函数到类中:

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

  for touch in touches {

    let location = touch.locationInNode(self)

    let touchedNode = nodeAtPoint(location)

    touchedNode.position = location

  }

}

构建和运行项目,然后试着拖动这两个卡牌。

Cards move, but sometimes slide under other cards

卡牌现在可以移动了,但有时卡牌会被遮住。继续下面的阅读来解决这个问题。

你玩过之后,将注意到两个主要的问题:

  1. 首先,由于精灵都在相同的zPosition,所以它们在被添加到场景中时被布置成相同的顺序。这意味着熊卡牌会在狼的卡牌“上面”。如果你拖动狼,它似乎被熊遮住了。

  2. 第二,nodeAtPoint()返回这个点上的最上面的精灵。所以当你拖动熊下面的狼时,nodeAtPoint()返回熊精灵然后开始改变它的位置,所以你可能会发现你拖动狼时熊会被拖动。

这个效果虽然很神奇,但它并不是你在最终的应用程序中想要的!

为了解决这个问题,你将需要在拖动时修改卡牌的zPosition。你的第一反应可能是在touchesMoved中修改精灵的zPosition,但如果你想在后面把它改回来,这不是一种好的办法。

使用开始和结束函数是一个更好的策略。依旧在GameScene.swift中,添加下列函数:

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

  for touch in touches {

    let location = touch.locationInNode(self)

    let touchedNode = nodeAtPoint(location)

    touchedNode.zPosition = 15

  }

}



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

  for touch in touches {

    let location = touch.locationInNode(self)

    let touchedNode = nodeAtPoint(location)

    touchedNode.zPosition = 0

  }

}

再次构建和运行项目,你将看到卡牌像我们期望的那样滑动。

Cards now correctly move over each other

卡片现在正确的移动了,但看起来有些平淡。你将在后面解决这个问题。

确保你挑选了大于其他卡牌的一个zPosition值。在本教程的示例游戏中,有一些重叠的元素的zPosition为20。数字19确保了重叠的元素显示在卡牌之上。

现在卡牌已经正确的移动了,但你需要添加一些令人满意的内容 — 一个可视化的指示表明卡牌被举起了。

是时候让你的卡牌跳舞了!

卡牌动画

依旧在GameScene.swift中,添加下面的代码到touchesBegan()函数中for循环的结尾

let liftUp = SKAction.scaleTo(1.2, duration: 0.2)

touchedNode.runAction(liftUp, withKey: "pickup")

touchesEnded()中添加类似的内容

let dropDown = SKAction.scaleTo(1.0, duration: 0.2)

touchedNode.runAction(dropDown, withKey: "drop")

在这里当你点击卡牌时,使用了SKActionscaleTo(scale:duration:)函数来增加卡牌的宽度和高度为它原来的1.2倍,当你松开时变回原来的大小。

构建和运行项目来查看效果。

Moving cards with pickup and drop down animation.

这个简单的动画像是把卡牌拿起来和放下去。有时候最简单的动画也是最有效的。

调整你scale和duration的值来找到最适合你的。如果你设置拿起和放下的durations为不同的值,你可以让它看起来像拿起的时候比较慢,放下的时候比较快。

摆动,摆动,摆动

拖动卡牌现在工作得已经很好了,但你应该做得更好。让卡牌围绕y轴拍翅膀当然是很棒的。

由于SpriteKit是一个纯2D框架,似乎没有办法做到精灵的部分转动效果。然而你可以这么做,修改xScale属性来制造转动的假象。

你将添加代码到touchesBegan()touchesEnded()函数中。在touchesBegan()中添加如下代码到for循环的最后:

let wiggleIn = SKAction.scaleXTo(1.0, duration: 0.2)

let wiggleOut = SKAction.scaleXTo(1.2, duration: 0.2)

let wiggle = SKAction.sequence([wiggleIn, wiggleOut])

let wiggleRepeat = SKAction.repeatActionForever(wiggle)



touchedNode.runAction(wiggleRepeat, withKey: "wiggle")

touchesEnded()中添加类似的代码:

touchedNode.removeActionForKey("wiggle")

这些代码让卡片回来转动 — 只是一点点 — 当它来回移动时。这个效果利用了reaction(action:, withKey:)函数添加了一个字符串名称到这个动作,这样你可以在后面取消它。

对于这种做法有一个小小的警告:当你删除动画时,无论它在不在动画的生命周期中,它会离开精灵。

你已经有一个动作使卡牌返回它的初始缩放值1.0。由于缩放同时设置x和y缩放,所以这部分不用担心,但如果你使用其他的属性,记得在touchesEnded函数中返回到初始值。

构建和运行项目,你会看到当你拖动他们时,卡牌会拍翅膀。

Card with scaling animation to fake 3d rotation.

一个简单的动画来显示这个卡牌是当前活动的。

挑战:在本教程最后的额外的示例游戏中,你将了解如何使用zRotation来让卡牌来回摇晃。

试着用rotateBy更换scaleXTo动作,以“摇晃”动画取代“摆动”动画。记得使其循环,这意味着它需要在重复之前返回它的出发点。

Card rotates slightly back and forth.

尝试重现这个摇摆动画的效果。

解决方法

touchesBegan中替换下列摆动代码:

let rotR = SKAction.rotateByAngle(0.15, duration: 0.2)

let rotL = SKAction.rotateByAngle(-0.15, duration: 0.2)

let cycle = SKAction.sequence([rotR, rotL, rotL, rotR])

let wiggle = SKAction.repeatActionForever(cycle)

touchedNode.runAction(wiggle, withKey: "wiggle")

这给你的卡牌添加了令人满意的小摆动,但是仍然有一个问题。试着拖动卡牌完成半个摆动周期。它的旋转是不是错误的?是的,这就是你下一步需要解决的问题,添加下面这行代码到touchesEndedfor循环的结尾:

runAction(SKAction.rotateToAngle(0, duration: 0.2), withKey:"rotate")

现在当你释放卡牌时有了一个正确旋转的漂亮的摇摆动画!

追踪伤害

在很多卡牌收集游戏中,像这些怪物会有关联的伤害值,可以与其他卡牌进行战斗。

要实现这一点,你需要在卡牌的顶端添加一个标签,这样用户可以追踪每个生物造成的伤害。依旧在GameScene.swift中,添加如下新方法:

func newDamageLabel() -> SKLabelNode {

  let damageLabel = SKLabelNode(fontNamed: "OpenSans-Bold")

  damageLabel.name = "damageLabel"

  damageLabel.fontSize = 12

  damageLabel.fontColor = UIColor(red: 0.47, green: 0.0, blue: 0.0, alpha: 1.0)

  damageLabel.text = "0"

  damageLabel.position = CGPointMake(25, 40)



  return damageLabel

}

这个辅助方法创建了一个新的SKLabelNode,它将显示每个卡牌造成的伤害。它使用了启动项目中正确的info.plist设置中包含的自定义字体。

注:关于安装自定义字体的更多信息,请查看iOS游戏教程中的第7章, “标签”.

你想知道示例中的位置是怎么工作的吗?

由于标签是卡牌精灵的子节点,位置默认是相对于精灵中心的锚点。通常经过一些尝试就可以得到你想要的标签位置。

添加如下代码到didMoveToView()结尾,来给每个卡牌添加一个伤害标签:

wolf.addChild(newDamageLabel())

bear.addChild(newDamageLabel())

构建和运行项目。你现在应该在每个卡牌中看到一个红色的“0”。

Card with a label for damage taken.

卡牌现在有一个显示受到多少伤害的标签了。

试着拖动卡牌,但是点击该标签拖动时拖动的是标签而不是卡牌本身。注意标签飞到了什么地方 — 也许是一个神奇的国度在那里可以肆无忌惮的移动?

不,其他它不是那么神秘。;]

这里的问题是当你调用nodeAtPoint时,它返回SKNode最上层的任何类型的节点,在这种情况下是SKLabelNode。当你改变节点的位置,移动的是标签而不是卡牌。呃。。。是的,这是合乎逻辑的解释。

Dragging on top of the damage label causes problems.

触摸顶部伤害标签的结果。哎呀。(改变背景为白色,使标签更明显)

场景触摸处理的利弊

在继续之前,让我们考虑考虑场景级别的触摸处理的优点和缺点。

在项目中,场景级别的触摸处理是一个好的出发点,因为它是最简单,最直接的办法。事实上,如果你的精灵有透明区域需要被忽略,比如六角格,这可能是唯一合理的解决方案。

但是,当你有复合的精灵时它就开始显出缺点了。例如,这些可能包含多个图像、标签或甚至血条。如果你有不同的规则对应不同的精灵,它可以是很笨重和复杂的。

一个好的办法是你使用nodeAtPoint,它总是返回一个节点。

如果你拖动到一个卡牌精灵之外会发生什么?因为SKSceneSKNode的一个子类,如果触摸位置相交没有其他节点,则场景本身会返回一个SKNode

当你修改了位置并在之前做了动画,你可能不知道但你实际上应该检查touchedNode是不是场景本身,但因为现在是一个学习阶段,所以也没有关系。

你会很高兴的知道这里有一个更好的解决方案…

处理那些触摸!处理它们!

你能做什么替代它呢?好吧,你可以使卡牌类负责处理它自己的触摸事件。这种方法的逻辑是相当明确的。打开Card.swift然后添加下列内容到init(imageNamed:)中:

userInteractionEnabled = true

这使得卡牌类拦截了触摸事件而不是传递它们到场景里。SpriteKit将根据这个属性设置发送触摸事件到最上层的实例。

接下来,你需要删除这三个触摸处理函数,GameScene.swift中的touchesBegan()touchesMoved()touchesEnded()然后把它们添加到Card.swift

原来的代码不能像拿过来就直接用,所以它需要一些变化来与节点工作。

作为一个挑战,让我们看看你能不能不看答案做出适当的改变!

提示:由于触摸事件直接发送到了正确的精灵,所以你不需要指出需要修改的精灵。

解决方法

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

  for touch in touches {

    // note: removed references to touchedNode

    // 'self' in most cases is not required in Swift

    zPosition = 15

    let liftUp = SKAction.scaleTo(1.2, duration: 0.2)

    runAction(liftUp, withKey: "pickup")



    let wiggleIn = SKAction.scaleXTo(1.0, duration: 0.2)

    let wiggleOut = SKAction.scaleXTo(1.2, duration: 0.2)

    let wiggle = SKAction.sequence([wiggleIn, wiggleOut])

    let wiggleRepeat = SKAction.repeatActionForever(wiggle)



    // again, since this is the touched sprite

    // run the action on self (implied)

    runAction(wiggleRepeat, withKey: "wiggle")

  }

}



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

  for touch in touches {

    let location = touch.locationInNode(scene) // make sure this is scene, not self

    let touchedNode = nodeAtPoint(location)

    touchedNode.position = location

  }

}



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

  for touch in touches {

    zPosition = 0

    let dropDown = SKAction.scaleTo(1.0, duration: 0.2)

    runAction(dropDown, withKey: "drop")

    removeActionForKey("wiggle")

  }

}

从本质上讲,这个复制了在他们当前的状态的触摸的处理函数到卡牌的实现里。主要的区别是你不再需要搜索节点树来找对应的节点。

SpriteKit调用了正确实例的函数,所以你只需要直接修改属性。

构建和运行项目,你会注意到它解决了之前乱飞的标签的问题。

Card can be moved the same as before

卡牌可以像以前一样移动。

两面性

现在花一点时间来研究卡牌节点是如何初始化的。目前,我们简单的使用了字符串名称来创建纹理,然后发送它到父类的初始化函数。

为了添加属性,如攻击和防御值,或神秘的魔法效果,你需要基于指定的卡牌数据设置属性和配置它们。你应该用枚举代替使用字符串来定义卡牌,因为字符串定义卡牌容易出现错别字。打开Card.swift 然后添加如下内容到import行和类定义中间:

enum CardName: Int {

    case CreatureWolf = 0,

    CreatureBear,       // 1

    CreatureDragon,     // 2

    Energy,             // 3

    SpellDeathRay,      // 4

    SpellRabid,         // 5

    SpellSleep,         // 6

    SpellStoneskin      // 7

}

这定义了CardName为一个新的类型,你可以使用它来识别卡牌。整数值作为引用在一套牌里将会很有帮助。

接下来,你需要为去Card类定义一些自定义属性。添加下列代码到类声明和init中间:

let frontTexture: SKTexture

let backTexture: SKTexture

var largeTexture: SKTexture?

let largeTextureFilename: String

替换Card.swift中的init(imageNamed:)

init(cardNamed: CardName) {



  // initialize properties

  backTexture = SKTexture(imageNamed: "card_back.png")



  switch cardNamed {

  case .CreatureWolf:

    frontTexture = SKTexture(imageNamed: "card_creature_wolf.png")

    largeTextureFilename = "card_creature_wolf_large.png"



  case .CreatureBear:

    frontTexture = SKTexture(imageNamed: "card_creature_bear.png")

    largeTextureFilename = "Card_creature_bear_large.png"



  default:

    frontTexture = SKTexture(imageNamed: "card_back.png")

    largeTextureFilename = "card_back_large.png"

  }



  // call designated initializer on super

  super.init(texture: frontTexture, color: nil, size: frontTexture.size())





  // set properties defined in super

  userInteractionEnabled = true

}

最后,打开GameScene.swift然后修改didMoveToView()来使用新的枚举替换字符串文件名称:

let wolf = Card(cardNamed: .CreatureWolf)

wolf.position = CGPointMake(100,200)

addChild(wolf)



let bear = Card(cardNamed: .CreatureBear)

bear.position = CGPointMake(300, 200)

addChild(bear)

下面是修改的内容:

  • 首先,你添加了一个新的叫做CardName的类型,这种枚举类型的优点是编译器知道所有可能的值并会在你输入错误的时候警告你。另外,Xcode的自动完成功能可以在你输入名称的开始几个字符时提示你。

  • 接下来,你在Card.swift中创建了四个新的属性来存储每个SKTexture的值,它将基于卡牌的状态使用。每个卡牌需要一个字体图片,背景图片和大的前面图片。largeTextureFilename使图片需要使用时再加载以节省内存,防止载入大图片时内存溢出。

  • 接下来你更新了init方法来接受一个CardName而不是一个字符串,然后根据Card的类型设置了每个新创建的属性。这利用了Swift新的switch语句。这里switch的cases不会自动失败。另外,你需要提供一个default case,或者覆盖所有可能的值。一旦你有自定义的属性,比如攻击或防御,你可以分配这些值到switch语句中。

    当初始化swift对象时有一个特定的顺序你必须遵循。



    • 首先,确保类定义的所有属性有默认的值。

    • 其次,调用父类的designated initializer

    • 第三,设置所有父类中定义的属性,然后调用你需要的对象的任意函数。

  • 最后,你更新了GameScene.swift中的代码来使用新的Card的init函数。

构建和运行项目,确保一切像之前一样工作。

注:因为你只操作了七个卡牌,所以不需要复杂的初始化卡片。当你有几十或几百个卡牌时这种特殊的策略可能不太好用。到那时,你需要系统的存储所有卡牌的属性到一个配置文件里,比如一个.json文件。你还需要设置初始化系统从配置文件中抽出数据做为字典来构建卡牌。

挑战:

通过为其他卡牌添加正确的图片来完成Card,比如凶猛的龙。你将会在Supporting Files中的cards文件夹中找到图片。

Image of Dragon creature card

Dun Dun Dun

触发翻转

最后,添加一些卡牌类动作来使游戏更逼真。由于基本前提是两个玩家共享一个iPad,所以卡牌需要正面朝下来使其他玩家无法看到它们。

一种实现的简单方法是当双击它时翻转卡牌。但是,你需要一个属性来追踪卡牌的状态。

打开Card.swift然后添加下列属性到其他属性的下面:

var faceUp = true

接下来,添加一个交换的纹理使卡牌出现翻转效果:

func flip() {

  if faceUp {

    self.texture = self.backTexture

    if let damageLabel = self.childNodeWithName("damageLabel") {

      damageLabel.hidden = true

    }

    self.faceUp = false

  } else {

    self.texture = self.frontTexture

    if let damageLabel = self.childNodeWithName("damageLabel") {

      damageLabel.hidden = false

    }

    self.faceUp = true

  }

}

最后,添加如下代码到touchesBegan的开始,在for-in循环中:

if touch.tapCount > 1 {

  flip()

}

现在你应该明白了为什么之前我们保存了卡牌的正面和背面的图片为纹理 — 它让翻转卡牌如此简单。你还需要隐藏damageLabel,这样卡牌朝下时数字才不会显示。

构建和运行项目然后翻转那些卡牌。

Card flip

通过交换纹理来实现卡牌的翻转。第一次点击卡牌时会触发一个小的弹跳效果。

注:在这里,伤害标签在卡牌初始化时作为属性初始化是完美的。作为保持本教程简洁的目的,它在这里还是一个子节点。试着把它从GameScene放到Card中。

效果还不错,但你还可以做得更好。一个技巧是使用scaleToX动画来使它看下来像真的翻转了。

用如下代码替换flip

func flip() {

  let firstHalfFlip = SKAction.scaleXTo(0.0, duration: 0.4)

  let secondHalfFlip = SKAction.scaleXTo(1.0, duration: 0.4)



  setScale(1.0)



  if faceUp {

    runAction(firstHalfFlip) {

      self.texture = self.backTexture

      if let damageLabel = self.childNodeWithName("damageLabel") {

        damageLabel.hidden = true

      }

      self.faceUp = false

      self.runAction(secondHalfFlip)

    }

  } else {

    runAction(firstHalfFlip) {

      self.texture = self.frontTexture

      if let damageLabel = self.childNodeWithName("damageLabel") {

        damageLabel.hidden = false

      }

      self.faceUp = true

      self.runAction(secondHalfFlip)

    }

  }

}

scaleXTo只收缩了水平方向,制造了一个非常酷的2D翻转动画。动画分成两半,这样你可以分别互换它们的纹理。setScale函数确保其他缩放动画不会影响到翻转。

构建和运行项目来看看新的“翻转”效果。

Card flip with animation

现在你有了看起来很不错的翻转动画。

看起来还不错,但卡牌这么小,你还不能完全理解熊的傻笑。所以你需要能放大选中的卡牌来看它的细节…

放大时刻

你将在本教程学习的最后一个效果是修改双击动作来放大卡牌。添加这两个属性到Card.swift的最开始的其他属性的前面:

var enlarged = false

var savedPosition = CGPointZero

添加如下方法来执行放大动作:

func enlarge() {

  if enlarged {

    enlarged = false

    zPosition = 0

    position = savedPosition

    setScale(1.0)

  } else {

    enlarged = true

    savedPosition = position

    zPosition = 20

    position = CGPointMake(CGRectGetMidX(parent.frame), CGRectGetMidY(parent.frame))

    removeAllActions()

    setScale(5.0)

  }

}

记得更新touchesBegan()来调用新的函数替换flip()

if touch.tapCount > 1 {

  enlarge()

}



if enlarged { return }

最后,给touchesMoved()touchesEnded做一点小修改,添加如下代码到每个for-in循环前面:

if enlarged { return }

你需要添加额外的属性savedPosition,这样卡牌才能移回它原来的位置。正如前面提到的,这是触摸处理逻辑比较棘手的问题。

在函数开始处的tapCount检查防止了卡牌已经被放大了再次双击的错误。没有提前退出,大图片会收缩并开始摆动动画。

移动放大的图片也是没有意义的,所以触摸结束后没有什么可做的,所以当卡牌放大时所有函数都提前退出了。

构建和运行应用程序,查看卡牌放大到填充屏幕。

Basic card enlarging.

基础卡牌放大效果。动画看起来好多了,放大的图片是模糊的。

但是为什么它是像素化的?Vicki的美工在这种情况下可应该是很漂亮的。你放大成这样是因为你没有使用Supporting Filescards_large文件夹里的放大版本的图片。

因为一开始就加载所有卡牌的大图片会浪费内存,所以最好在用户不需要它们的时候不加载它们。

放大函数的最终版本如下:

func enlarge() {

  if enlarged {

    let slide = SKAction.moveTo(savedPosition, duration:0.3)

    let scaleDown = SKAction.scaleTo(1.0, duration:0.3)

    runAction(SKAction.group([slide, scaleDown])) {

      self.enlarged = false

      self.zPosition = 0

    }

  } else {

    enlarged = true

    savedPosition = position



    if largeTexture != nil {

      texture = largeTexture

    } else {

      largeTexture = SKTexture(imageNamed: largeTextureFilename)

      texture = largeTexture

    }



    zPosition = 20



    let newPosition = CGPointMake(CGRectGetMidX(parent.frame), CGRectGetMidY(parent.frame))

    removeAllActions()



    let slide = SKAction.moveTo(newPosition, duration:0.3)

    let scaleUp = SKAction.scaleTo(5.0, duration:0.3)

    runAction(SKAction.group([slide, scaleUp]))

  }

}

动画现在非常明确了。

卡牌的位置在运行动画之前保存了,所以它返回了它原来的位置。为了防止它放大时被拿起和放下动画打断,你添加了removeAllActions()函数。

当缩小动画运行时,enlarged和zPosition属性没有设置直到动画完成。如果这些值在完成前被修改了,被放大的卡牌后面的卡牌将会显示出来,它也会返回它之前的位置。

由于largeTexture被定义为optional,它的值可以为nill,或“没有值”。if语句测试了它,来看它是否有值,如果它没有则加载纹理。

注: Optional是学习Swift的一个核心部分。特别是它不同于Objective-C中的nil值。

再次构建和运行应用程序。你现在应该看到一个从初始位置到最终的放大的位置漂亮的,平滑的动画。你也会看到卡牌是非像素化的、干净的、填充的。

Card enlargement with animation.

把卡牌变大,然后换成大图片让它看起来更好。

最终挑战:音效是任何游戏的一个重要部分,启动项目中包含了很多声音文件。看看你能不能使用SKAction.playSoundFileNamed(soundFile:, waitForCompletion:)来添加音效到卡牌翻转中和放大的动作中。

下一步?

你可以在这里找到本教程的最终项目。

到了这里,你已经理解了一些可以在你自己的卡牌游戏中使用的基础的 — 和一些不那么基础的 — 卡牌机制。

这个示例项目中有很多微秒的动画你可以调整,所以确保你已经试过不同的值来找到你喜欢的和适合你的。

一旦你对动画满意了,这里还有板区域,牌组,攻击和很多其他特性,内容很多所以不能简单像这篇一样的一篇文章中全部讲完。你可以查看用Objective-CSwift完成的示例游戏,了解更多有关游戏开发的其他元素。

请使用论坛在下面评论、问问题或分享你对Swift的卡牌动画的想法。感谢你抽出宝贵的时间学习本教程!

如何将你的Sprite Kit游戏从ios移植到Mac OS X平台


原文地址:http://www.raywenderlich.com/70837/how-to-port-your-sprite-kit-game-from-ios-to-os-x
泰然翻译组:阳光新鲜。校对:glory。

Mou icon

你可能想过将自己的Sprite Kit游戏移植到Mac OS X平台?事实上,这比你想像的还简单。

苹果开发Sprite Kit的目的是让ios和Mac OS X开发尽可能的一样,甚至你可以使用相同的工程环境同时完成两个平台的开发。

这篇引导将告诉你怎样获取一个ios Sprite Kit项目-Sprite Kit Tutorial for Beginners-然后将他修改成Mac OS X版本。

你将学习到如何在同一个XCode工程中维护两个版本并且学习如何在不拷贝一大堆代码的情况下保证两个版本同步。

如果你选择完成这个教程,那么你最好有一定的Mac OS X基础。尽管如此,Sprite Kit的一个优美的特性就是你不需要有比较丰富的经验就可以做出一个漂亮的App。

如果想学习更多的关于Mac开发的知识,请查看这里three part tutorial on making a simple Mac app

让我们开始吧

这里下载我们教程需要的原始工程,并在你的iPhone上调试运行。

这个实例工程和原始的有一些不同,这是为了将ios和Mac OS X版的游戏区分开来。

水平仪支持

Space Game Starter Kit项目使用了水平仪,这样可以让玩家可以通过倾斜设备来上下移动精灵,像这样:

Mou icon

更具毁灭性的力量

忍者星正在遭受魔法忍者力量史诗般的毁灭性攻击!

Mou icon

好吧,也许不是史诗般的,但是这个版本使用了非常不错的粒子效果以给忍者一些视觉冲击。

一些技术上的改进

Vicki Wnderlich 发挥自己的才能给游戏添加了新的背景以使游戏看起来更具美感。当然,状态栏是隐藏的,并且游戏支持iPad的分辨率。

使用项目和目标开发

一个项目文件里包含你的工作用到的所有文件,其中 target 决定了你的工程该如何编译。

一个项目可以包含多个目标。这种方式可以让你用多种方式编译你的项目,选择目标和哪些特定的文件关联,和一些特殊的目标设置。这些也许能给你一些关于将这个项目用Mac OS X编译的启示。

让我们看一下项目的目标。打开 SpriteKitSimpleGame 项目,在界面的左上角,选择你的项目打开项目设置。然后在项目窗口中选择 SpriteKitSimpleGame

好,现在这里有两个目标,一个是ios另一个是ios单元测试,如下:

Mou icon

二选一,在项目和目标的列表都展开后你可以选择 SpriteKitSimpleGame

Mou icon

当你构建应用时,你的target必须支持当前设备。查看当前设备是否被目标支持,点击左上角停止按钮右边的 SpriteKitSimpleGame 下拉按钮,显示如下:

Mou icon

这里也是你要创建Mac OS X目标的地方。

创建一个MacOSX目标

确保你的Xcode窗口保持激活状态,按下图的方式选择 File/New/Target

Mou icon

Xcode提示你为目标选择一个模板。

选择 OS X\Application\SpriteKit Game 模板然后点下一步,如下所示:

Mou icon

最后在 product name 中输入 SpriteKitSimpleGameMac 然后点击完成,像这样:

Mou icon

注意:如果你打算将应用上传到苹果商店,你的 bundleidentifier 必须在 developer.apple.com 分别注册。Profiles和provisioning文件分别用于ios和Mac OS X开发中。

尝试在Mac OS X上运行你的app,在列表中选择 SpriteKitSimpleGameMac,然后选择 My Mac 64-bit,如下:

Mou icon

你认为当你使用新的目标运行你的项目时会发生什么?

  • A - 游戏完美运行 - 教程到此结束!
  • B - 编译中发生数个错误。
  • C - 游戏可以编译,但运行时跳出。
  • D - 游戏可以运行,但运行结果不是你预期的。

如果你猜D那么你猜对了,就像一个新工程运行的很好但是使用的是标准模板。

Mou icon

不用着急,这个工程马上就回变为令人惊讶的忍者游戏,但首先你需要清空工程已让我们能更方便的开始工作。

将你的文件整理成多个目标

现在,你有了一个Mac OS X目标,你将对他进行修改已使你的app能运行在osx下。随着时间的推移,跟踪这些文件将变得越来越困难,所以最好先设置系统以更好的组织所有文件。

最小化你所有的组然后新建一个叫 SharedResources 的组,如下所示:

Mou icon

这个组将包含可以让Mac OS X和ios目标公用的资源。

好了,现在创建一个叫 Testing 的组,然后将单元测试项目移动到里面,像这样:

Mou icon

将用于单元测试的项目移入特定的文件夹可以避免看起来杂乱。

现在你在项目里为文件组织了一个新的结构,你现在要做的是移动文件到指定的位置。

展开 SharedResources 和 SpriteKitSimpleGame 文件夹。从 SpriteKitSimpleGame 拖动 Particles 和 Sounds组到 SharedResources.

下一步,将 sprites.atlas folder, MyScene.h, MyScene.m, GameOverScene.h and GameOverScene.m 文件也拖动过来。你的文件夹结构看上去应该如下所示:

Mou icon

删除 Spaceship.png 文件 - 你将不再需要他了。这只是你在创建 sprite Kit 模板时自动生成的样板文件。

所有共享的资源都已经放到 SharedResources 组了。所有留在 SpriteKitSimpleGame 的文件都与 ios 游戏的启动与管理有关。

展开 SpriteKitSimpleGameMac 组。在开始下一步之前,你需要删除这个文件夹的实例文件。

从 SpriteKitSimpleGameMac 中删除 MyScene.h, MyScene.m and Spaceship.png 文件,选择移动到垃圾箱。然后你的文件列表应该像这样:

Mou icon

注意:你的Mac目标的组中不能含有ViewController类;取代它的应该是AppDelegate文件。 UIViewController 类是 UIKit 的一部分,他在Mac os x上是不起作用的。取代他的是AppDelegate,他将创建一个NSWindow实例以呈现Sprite Kit场景。

最后检查一下,你的完整的展开列表应该和下面的一样:

Mou icon

你已经删除了所有不必要的项目,并且重新组织了一下。现在,是时候修改目标编译时所指向的文件了。

添加目标成员

展开 FrameWorks 组选择 UIKit.framework,像这样:

Mou icon

展开右边的 Utilities Panel 选择 File Inspector,像这样:

Mou icon

在File Inspector的下半部分你会看到Target Membership区域。这里显示的是你的目标将要显示的文件。

UIKit 只是用在ios平台上,所以将Mac OS X的目标去掉,如下所示:

Mou icon

Cocoa Framework 只能在mac os x上使用,所以确保他在mac目标上被选中,在ios目标上被取消:

Mou icon

Sprite Kit Framework 在ios和mac上都可以使用,所以像这样设置:

Mou icon

每个文件都有他自己的Membership除了atlases纹理文件以外。Atlas文件是你唯一需要自己设置Membership的文件,其他的包括纹理都是自动设置的。

尽管如此,类在这方面有一些特别。你不能给.h文件设置Membership,但你必须给.m文件设置Membership。

带着你的新发现加深对Target Membership的理解,完成你的SharedResources组,确保SpriteKitSimpleGame 和 SpriteKitSimpleGameMac所有的文件都设置完成。总体上来讲,你需要8次设置。

下一步,完成SpriteKitSimpleGame组的文件,并确保SpriteKitSimpleGame的文件和两个目标都有关联 - 他们应该已经被设置好了,但是这里最好再设置一下。

最后,完成SpriteKitSimpleGameMac组,确保SpriteKitSimpleGameMac组的文件全都标记过。同样,你什么改变都不需要左,但是检查一下也无妨。

现在你的工程已经正确设置了ios和mac的目标,你接下来终于可以做你最擅长的事情 - 写程序!

构建与运行游戏

基于之前的工作,你的项目应该构建并且在ios上正确运行。之前做的修改应该不对实际的游戏效果产生影响。尽管去次,如果你编译OS的目标时,你将看到一连串的错误。这是因为你为对ios与osx代码之间差异给予说明。

构建和运行你的项目使用SpriteKitSimpleGameMac目标;你将会看到什么?

你将会遇到Module ‘CoreMotion’ not found的错误。Mac OS X没有CoreMotion和与他等价的类;你将围绕这个错误并且使用键盘控制主角移动。尽管如此,你的初级目标就是使项目能够构建,具体的实现细节放在后面。

但是我们如何修复它呢?你不能直视移除CoreMotion,否则ios版本就不能用了。你不能使用if语句解决,因为编译器会检查每一行代码并对他不认识的东西抛出错误。

打开MyScene.m然后替换:

@import CoreMotion;

替换成下面的代码:

#if TARGET_OS_IPHONE
    @import CoreMotion;
#endif

不同于if语句的规则,#if是在预处理中执行。TARGET_OS_IPHONE如果是ios平台则返回true。

注意:如果你计划使用#if检查当前的平台是否为Mac,如果是就执行一系列方法,那么你应该使用TARGET_OS_IPHONE。
TARGET_OS_MAC看起来也可以 - 但问题是他在ios下也返回true。
这看起来很糟糕,但苹果在他们的实例中使用!TARGET_OS_IPHONE来表示包含多平台的目标,所以这是一个小故障,看起来他们并不想修改。

现在你需要找到和CoreMotion有关的代码,并给他加上#if。

在MyScene.m中的实例变量中找下列代码:

CMMotionManager *_motionManager;

然后将他替换成:

#if TARGET_OS_IPHONE
    CMMotionManager *_motionManager;
#endif

滚动到int方法,寻找下列代码:

_motionManager = [[CMMotionManager alloc] init];
_motionManager.accelerometerUpdateInterval = 0.05;
[_motionManager startAccelerometerUpdates];

替换成如下代码:

#if TARGET_OS_IPHONE
    _motionManager = [[CMMotionManager alloc] init];
    _motionManager.accelerometerUpdateInterval = 0.05;
    [_motionManager startAccelerometerUpdates];
#endif

现在,找到如下代码

[self updatePlayerWithTimeSinceLastUpdate:timeSinceLast];

替换成如下代码:

#if TARGET_OS_IPHONE
    [self updatePlayerWithTimeSinceLastUpdate:timeSinceLast];
#endif

最后,当然这并不是最不重要的。找到updatePlayerWithTimeSinceLastUpdate方法:用下面的代码包裹整个方法:

#if TARGET_OS_IPHONE
    - (void)updatePlayerWithTimeSinceLastUpdate:> (CFTimeInterval)timeSinceLast 
    .
    .
    .
    }
#endif

如果你使用ios目标编译,所有的#if将返回TRUE,所以app会像以前一样编译。相反的,如果使用mac os x目标进行编译,所有的#if将返回FALSE,所有的程序块将不会被编译。

仔细查看touchesEnded:withEvents方法在MyScene.m。Mac版本不支持触屏,所以这个方法已经失效了。Mac版将使用鼠标作为屏幕触控的完美替代品。

为了避免给代码增加一个新分支,你将创建一个继承自SKScene类以帮助用来控制屏幕触控和鼠标点击!

添加事件操作

选择你的SharedResources组。

在菜单栏上选择File \ New \ File…,如下所示:

Mou icon

选择Objective-C Class不管你是ios还是os策略,点下一步。

Mou icon

命名为SKMScene继承于SKScene。

Mou icon

将文件直接置于工程文件夹下,确保ios和mac目标都被选中。

Mou icon

打开SKMScene.h替换代码:

@import SpriteKit;

@interface SKMScene : SKScene

//Screen Interactions
-(void)screenInteractionStartedAtLocation:(CGPoint)location;
-(void)screenInteractionEndedAtLocation:(CGPoint)location;

@end

你将用SKMScene重写上面的两个方法。

将下面的代码直接加在SKMScene.m文件的@implementation SKMScene:行下:

#if TARGET_OS_IPHONE
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  UITouch *touch = [touches anyObject];
  CGPoint positionInScene = [touch locationInNode:self];
  [self screenInteractionStartedAtLocation:positionInScene];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
  UITouch *touch = [touches anyObject];
  CGPoint positionInScene = [touch locationInNode:self];
  [self screenInteractionEndedAtLocation:positionInScene];
}

- (void)touchesCancelled:(NSSet *)touches
   withEvent:(UIEvent *)event
{
  UITouch *touch = [touches anyObject];
  CGPoint positionInScene = [touch locationInNode:self];
  [self screenInteractionEndedAtLocation:positionInScene];
}
#else
-(void)mouseDown:(NSEvent *)theEvent {
  CGPoint positionInScene = [theEvent locationInNode:self];
  [self screenInteractionStartedAtLocation:positionInScene];
}

- (void)mouseUp:(NSEvent *)theEvent
{
  CGPoint positionInScene = [theEvent locationInNode:self];
  [self screenInteractionEndedAtLocation:positionInScene];
}

- (void)mouseExited:(NSEvent *)theEvent
{
  CGPoint positionInScene = [theEvent locationInNode:self];
  [self screenInteractionEndedAtLocation:positionInScene];
}
#endif

-(void)screenInteractionStartedAtLocation:(CGPoint)location {
  /* Overridden by Subclass */
}

-(void)screenInteractionEndedAtLocation:(CGPoint)location {
  /* Overridden by Subclass */
}

这确实是一串很长的代码,但如果你从头到尾读下来,还是能找到其中的道理的。触摸屏幕和返回的方法在TARGET_OS_IPHONE程序块里。然后你创建一个包含位置信息的CGPoint触摸点并且调用screenInteraction相关的方法。

点击或释放鼠标将会调用#else处的方法。和上面一样,你创建一个包含点击信息的CGPoint并且调用和screenInteraction有关的方法。

使用这个子类的好处是触摸和点击都调用screenInteraction方法。screenInteraction方法没有代码,所以你需要在你的子类中重写。

打开MyScene.h并且在#import下添加如下类的声明:

#import "SKMScene.h"

然后修改父类为如下内容:

@interface MyScene : SKMScene

这确保你的游戏场景继承于SKMScene子类。你可以替代你子类中的触摸事件。

在MyScene.m寻找如下行:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

用下面的代码替换他:

-(void)screenInteractionEndedAtLocation:(CGPoint)location {

接下了,删除以下几行你已经不需要的方法:

UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];

使用Mac目标构建和运行你的项目;他应该可以编译通过,并且没有错误:

Mou icon

恭喜你 - 你已经成功的在Mac上运行了你的Sprite Kit game!你得注意这里有一些bug:

  1. 在一些苹果设备上,首次点击游戏会有短暂的卡顿。
  2. 在一些苹果上粒子效果可能显示不正常。
  3. 有些屏幕的尺寸可能有问题。
  4. 背景音乐没声音。

接下来我将带您改正所有的这些bug - 你在这个过程中将学到一些在做跨平台应用时经常遇到的问题。

纠正预加载错误

这个bug不会在所有的系统上出现,但一旦出现就会有性能问题。“First-time-through”通常源于资源加载。

Texture atlases是经常被使用的资源,但是我们的应用不包换动画和大型混合图片,所以问题出在别的地方。

音效更有可能出问题,当用户点击屏幕时,音效还没有加载。

修复这个问题,添加如下变量在 MyScene.m:

SKAction *_playPewPew;

下一步,在initWithSize的if语句中添加如下代码:

_playPewPew = [SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO];

这个修改后你的应用在场景初始化时预加载声音。

在screenInteractionEndedAtLocation寻找下列代码:

[self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]];

替换成:

[self runAction:_playPewPew];

运行应用;点击鼠标确保延迟消失了。

如果你的系统没有表现出这个问题,那么你也应该修改一下防止在其他系统上运行时发生。

纠正SKS错误。

Mou icon

在写这篇文档时我发现在xcode5上运行,粒子效果会出现问题。你需要在Sks文件中重写引用的纹理文件。

技术上,你的Sks文件没有发生任何错误 - 你不会在所有的系统中体验到这种错误 - 但你需要尽量避免他出现。

在MyScene.m的projectile:dideCollideWithMonster中寻找下面的代码:

SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:@"SmallExplosion" ofType:@"sks"]];

将下面的代码直接添加到你找的的代码下面:

emitter.particleTexture = [SKTexture textureWithImageNamed:@"spark"];

你之前所做的事情是为了告诉XCode哪里寻找粒子纹理。

创建你的应用;现在你可以欣赏你史诗般无障碍的粒子效果。

修复图片大小错误

打开SpriteKitSimpleGameMac组然后选择AppDelegate.m。查看你在applicationDidFinishLaunching中设置的尺寸。

1024 * 768 - 这是非视网膜屏的ipad分辨率。

现在查看一下sprites.atlas的内容。不出所料,所有的ipad版本图片都有~ipad后缀,这样你的应用在ipad上运行时就知道该使用哪种图片。

不幸的事,这里没有~mac后缀;取代他的是,你需要创建一个为mac使用的texture atlas纹理。

为了保证你的工程尽量的小,你应该使你的应用的分辨率尽可能的小。

右键sprites.atla选择Show in Finder。

复制sprites.atlas然后删除所有不带~ipad后缀的文件。

Mou icon

下一步,删除~ipad后缀文件,但是留下@2x后缀文件。

注意:必须留下@2x文件以支持配备视网膜屏的Macbook Pro。

重命名文件夹为spritesMac.atlas然后拖动重命名的文件夹到你的工程。

在Choose options for adding these files对话框中,确保只有SpriteKitSimpleGameMac目标被选中,如下所示:

Mou icon

点击完成。现在文件夹已经被导入了,选择sprites.atlas,在Membership关闭mac的目标。这可以保证每一个texture atlas都和其他的区分开。

让纹理文件夹的组织和原则一致,移动ios纹理到ios组mac纹理到mac组,如下:

Mou icon

下一步,选择Project\Clean。这将删除所有的旧文件从你的构建目录(如果你忘了做这一步,sprites.atlas会仍然存在)。

运行你的应用;你应该看到所有的纹理都按适当的尺寸载入,如下:

Mou icon

现在你的应用已经支持iPhone, iPad 和 Mac OS X的系统分辨率 - 也兼容视网膜屏。

修复音轨问题

最后,你需要处理音轨的问题。

查看SpriteKitSimpleGame组的ViewController.m文件。viewWillLayoutSubviews有一小部分代码是AVAudioPlayer的实例,设置他永远循环。

NSError *error;
NSURL *backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
self.backgroundMusicPlayer.numberOfLoops = -1;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];

啊哈-你的mac工程里没有ViewController。因此,你需要在AppDelegate调用这些代码来替代。

在SpriteKitSimpleGameMac组的AppDelegate.m文件中寻找如下代码:

@implementation AppDelegate

替换:

@import AVFoundation;

@implementation AppDelegate {
    AVAudioPlayer *backgroundMusicPlayer;
}

下一步,在applicationDidFinishLaunching顶部添加如下代码:

NSError *error;
NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
backgroundMusicPlayer.numberOfLoops = -1;
[backgroundMusicPlayer prepareToPlay];
[backgroundMusicPlayer play];

构建应用;音乐开始播放了!

你已经解决了mac版本上所有的bug,但你还有一个游戏控制的问题没有解决。

使用键盘

ios版的忍者移动依靠的是倾斜设备。这种工作是依靠CoreMotion类处理的,游戏的主循环调用updatePlayerWithTimeSinceLastUpdate:以计算主角当前帧的位置。

我们现在需要一个不同的方法来监听键盘事件。

将下面代码添加到MyScene.m文件的updatePlayerWithTimeSinceLastUpdate:方法的#endif语句之前:

#else
-(void)keyDown:(NSEvent *)theEvent {


}

这个方法用于响应键盘操作。注意有个keyUp方法响应键盘的抬起事件,事件是键盘从按下到抬起的时间。

你不想处理响应所有的键盘事件;你可以在NSEvent中找到你需要处理的按键。

在keyDown的花括号中添加如下代码:

-(void)keyDown:(NSEvent *)theEvent {
  NSString *keyPressed = [theEvent charactersIgnoringModifiers];
if ([keyPressed length] == 1) {
      NSLog(@"Key: %c",[keyPressed characterAtIndex:0]);
  }
}

这里你可以提取你按下的字母,没有任何修饰键。这意味着组合键如Command + S将被忽略.同样的,你只检查按下的一个字母,这样可以过滤掉你不想要的字符事件。你将吧按压事件输出到控制台。

运行工程;你输入的按键将输出到debug区域,如下:

Mou icon

你可以使用上下键来移动你的精灵,试着按一些键看控制台输出什么:

Mou icon

恩,这看起来有点糟糕。方向键是功能键的一部分,所以他没有合适的字符可以代表。但这没关系:我这里有一个简单的方法可以检测功能键的按压。

NSEvent可以帮助你很好的管理mac上鼠标和键盘的操作。这个引导教程只介绍了NSEvent的一部分;强烈推荐你仔细查看NSEvent类的参考手册

现在,快速浏览一下NSEvent文档中关于按键时间枚举的部分。按键和NSUpArrowFunctionKey与NSDownArrowFunctionKey有关。

返回MyScene.m找到keyDown:添加代码。

注释掉NSLog语句并且立即粘贴如下代码:

unichar charPressed = [keyPressed characterAtIndex:0];
switch (charPressed) {
    case NSUpArrowFunctionKey:
        [_player runAction:[SKAction moveByX:0.0f y:50.0f duration:0.3]];
        break;
        case NSDownArrowFunctionKey:
        [_player runAction:[SKAction moveByX:0.0f y:-50.0f duration:0.3]];
        break;
    default:
        break;
}

在这里你将按Unicode的方式比较上下按键。然后使用SKAction上下移动角色。

运行工程;按上下键你将看到角色上下移动如下图:

Mou icon

你需要花更多的时间修改mac版游戏,但你需要确保你没有影响到ios版本部分的运行效果!

运行你的ios目标并且充分的玩每一个部分确保你之前的更改没有影响到ios部分的功能。

未来要做的事?

你可以从在这里得到完整的工程。

现在,你应该已经还好的理解了将ios工程转换成mac/ios工程所需的工作,你一定希望让创建的Sprite Kit游戏一开始就具有跨平台功能。我在github上放了一个Sprite Kit跨平台的模板,这对你一定有用的。

想学习更多的ios和osx游戏的知识(尤其是和场景尺寸,图片大小,横纵比,和UIKit对比Cocoa Touch的坐标系统相关的),看看我们即将出版的
iOS Games by Tutorials,这里你可以找到更多详细的内容。

如果你有一些注释或问题,在下面踊跃发言哦!

如何使用Sprite Kit制作像切绳子这样的游戏


原文地址:http://www.raywenderlich.com/72146/create-game-like-cut-rope-using-sprite-kit
泰然翻译组:志创大侠。校对:glory。

在ios7之前,如果你想要去做一个游戏,需要知道一些关于OPENGL这种深奥难懂的知识,或者要借助第三方的库做一些复杂困难的工作。当你有了一个图像引擎,你往往需要加上一个物理层,并且加上一个额外的库去模拟真实世界的行为(比如跑,跳)。这样经常会导致一些附属事物的增加,而且会要求程序员掌握其它的程序语言(比如C或者C++).

在采用IOS7的时候,苹果公司也彻底改变了这一局面。就在那个时候,开发者们一下子就得到了2D图像引擎和物理引擎的接口,所有的一切都可以使用Objective-C来操作。开发者们可以集中精力去做一个游戏而不是纠结于管理整个架构体系。

这个游戏框架就是SpriteKit。在这个教程中,你将会学习制作一个和切绳子差不多的游戏(切绳子是一个备受赞誉的基于物理规则的解密游戏)。你将会学到的有:

  • 在场景中加入对象

  • 制作序列动画

  • 加入音乐

  • 最终要的是学会如何在Sprite Kit的物理系统里让游戏运转起来

在教程的最后,你应该就能在自己的项目中很熟练地使用Sprite Kit了。

要清楚的是这篇教程不是入门级的,如果你连SKNodeSKAction都不是很了解,那么出门右拐,看一下我们的Sprite Kit基础教程吧。该教程能使你快速地学会用Sprite Kit制作游戏,然后你就可以运行游戏,给鳄鱼喂菠萝了,哈哈!

然后呢?继续看吧

开始

在这个教程,你将会做一个叫《Cut the Verlet》的游戏。这个游戏模仿的是切绳子的游戏设定,就是让你切掉一根绑着一颗糖的绳子,让糖掉到一只饥饿而且非常烦躁的生物的嘴中。每一关你都会遇到一些新的挑战,比如会有蜘蛛和锯片。这个游戏在Cocos2D框架下也被实现过,但是在这个教程中,你用的就是Sprite Kit了。

那什么是Verlet呢?verletverlet integration的缩写。而verlet integration是一种用来模拟运动粒子轨道和绳子物理特性的方法。写这个游戏的Cocos2D版本的作者Gustavo Ambrozio对verlet进行了很详细的介绍并且说明了把这个方法应用到游戏当中的思路。在读这篇教程前先浏览一下链接中的文章,还是很有必要读的。

首先,先下载一下启动项目。把它放在合适的地方然后用XCode打开看一下它的构成。

项目包括4个主文件,如下所示:

  • Classes包括一些主文件,比如主视图的控制器,场景以及绳子对象的类。你将会在阅读教程的过程中在里面加入这些类。

  • Helpers包括一些储存游戏数据的文件,它的数据将贯穿这个游戏程序。

  • Resources/Sounds包含了项目的声音文件。

  • Resources/Other包含了一些用于在场景中加入粒子的文件。

  • Other Resources包含了从第三方得到的所有资源。这篇教程将会使用一个iOS Game by Tutorials制作的插件,叫SKUtils。

  • Images.xcassets包含了图片资源。

另外,我已经加入了所有必要的#import语句到启动项目。这些#import语句都被包含在 CutTheVerlet-Prefix.pch中。

注释:A.pch就是预先编译好的头文件。预编译头文件是用来加快文件编译速度的文件。他们使在你的项目中被使用的那些文件不需要被导入。只要记住预编译头文件能隐藏在类中使用到的附属物。

关掉ResourcesOther Resources ,你不需要在这些地方进行修改的。你会在ClassesHelpers中直接使用到这些文件。

终于要开始了

加一些常数

常数就是那些一旦被设定了值就永远不会改变的数。常数能让你的代码更容易管理。全局常数有时候还能让你的代码更易读更易维护。

在这个项目中,你将会加入一些全局常量,用来定义精灵的纹理图片名称,音效文件名称,精灵节点名称,精灵的Z-order或者zPosition以及为每一个精灵定义的目录,这些目录是用来做碰撞检测的。

打开TLC ShareConstains.h并且加入如下代码在@interface上面:

typedef NS_ENUM(int, Layer)

{

    LayerBackground,

    LayerForeground,

    LayerCrocodile,

    LayerRope,

    LayerPrize

};

 

typedef NS_OPTIONS(int, EntityCategory)

{

    EntityCategoryCrocodile = 1 << 0,

    EntityCategoryRopeAttachment = 1 << 1,

    EntityCategoryRope = 1 << 2,

    EntityCategoryPrize = 1 << 3,

    EntityCategoryGround = 1 << 4

};

 

extern NSString *const kImageNameForRopeHolder;

extern NSString *const kImageNameForRopeTexture;

 

extern NSString *const kImageNameForCrocodileBaseImage;

extern NSString *const kImageNameForCrocodileMouthOpen;

extern NSString *const kImageNameForCrocodileMouthClosed;

 

extern NSString *const kSoundFileNameForCutAction;

extern NSString *const kSoundFileNameForSplashAction;

extern NSString *const kSoundFileNameForBiteAction;

 

extern NSString *const kSoundFileNameForBackgroundMusic;

 

extern NSString *const kImageNameForPrize;

extern NSString *const kNodeNameForPrize;

上面的代码声明了两个类型分别为Layerint:EntityCategorytypedef变量。当你把它们加入到场景中时,你将会使用它们去定义碰撞目录和精灵的zPosition。当然,它们的作用不只于此。

代码又用const定义了一组类型为NSString的常量。写上extern是为了让你去做一些清楚的声明,就是让你在这声明了变量并且能在任何地方设置变量的值。

问题:为什么程序猿总是在声明常量的时候在常量名上加上'k'这个前缀

对于我们为什么用k有着一个小争论,但是普遍上的解释是它起源于Hungarian notation,那时候k指代常量。或者是说k是指konstant?

什么叫在这声明一个常量并且在其他地方设定常量的值?其实在这个项目中,其他地方 就是指TLC SharedConstants.m文件。

打开TLC ShareConstaints.m并且加上在@implementation上面如下代码

NSString *const kImageNameForRopeHolder = @"ropeHolder";

NSString *const kImageNameForRopeTexture = @"ropeTexture";

 

NSString *const kImageNameForCrocodileBaseImage = @"croc";

NSString *const kImageNameForCrocodileMouthOpen = @"croc01";

NSString *const kImageNameForCrocodileMouthClosed = @"croc00";

 

NSString *const kSoundFileNameForCutAction = @"cut.caf";

NSString *const kSoundFileNameForSplashAction = @"splash.caf";

NSString *const kSoundFileNameForBiteAction = @"bite.caf";

 

NSString *const kSoundFileNameForBackgroundMusic = @"CheeZeeJungle.caf";

 

NSString *const kImageNameForPrize = @"pineapple";

NSString *const kNodeNameForPrize = @"pineapple";

你将会在这里使用字符串型的常量去设定图片和声音文件的名字。如果你玩过《切绳子》游戏,你大概就能猜出这些名字代表着什么。你也可以把用于碰撞检测的精灵节点的名字设在字符串常量里。碰撞检测在本文的碰撞检测章节中会有介绍。

注解:一开始就加入这么多东西可能有些草率,让你不知所措。但是你最好先把这些定义好,因为你将会在阅读教程的时候使用它们并且了解它们。

加入背景和前景

刚建立好的新项目提供了项目方法的初始版本,你需要在这个基础上加入自己的代码。第一步要做的就是初始化场景和加入背景。

打开TLCMyScene.m并在界面声明中加入如下代码,就是一些properties (接口),下面的properties就都先叫做接口吧。


@property (nonatomic, strong) SKNode *worldNode;

@property (nonatomic, strong) SKSpriteNode *background;

@property (nonatomic, strong) SKSpriteNode *ground;

 

@property (nonatomic, strong) SKSpriteNode *crocodile;

 

@property (nonatomic, strong) SKSpriteNode *treeLeft;

@property (nonatomic, strong) SKSpriteNode *treeRight;

你在这里定义properties是为了在场景中引用不同的节点。

现在在initWithSize中加入代码块,就是在注释/ add setup here /下面。

self.worldNode = [SKNode node];

[self addChild:self.worldNode];

 

[self setupBackground];

[self setupTrees];

上面的代码创建了一个SKNode的对象并且将该对象赋给了world接口。然后又用了addChild函数把这个节点加入到场景中。

这里有调用了两种方法,一种是为了建立背景的,另一种是为了建立树。因为这两种方法是差不多的,所以当你加入它们的时候,我会把它们放一起解释。

首先,找到setupBackground并加入如下代码:

self.background = [SKSpriteNode spriteNodeWithImageNamed:@"background"];

self.background.anchorPoint = CGPointMake(0.5, 1);

self.background.position = CGPointMake(self.size.width/2, self.size.height);

self.background.zPosition = LayerBackground;

 

[self.worldNode addChild:self.background];

 

self.ground = [SKSpriteNode spriteNodeWithImageNamed:@"ground"];

self.ground.anchorPoint = CGPointMake(0.5, 1);

self.ground.position = CGPointMake(self.size.width/2, self.background.frame.origin.y);

self.ground.zPosition = LayerBackground;

 

[self.worldNode addChild:self.ground];

 

SKSpriteNode *water = [SKSpriteNode spriteNodeWithImageNamed:@"water"];

water.anchorPoint = CGPointMake(0.5, 1);

water.position = CGPointMake(self.size.width/2, self.ground.frame.origin.y + 10);

water.zPosition = LayerBackground;

 

[self.worldNode addChild:water];

然后找到setupTrees并且加入下面的代码:


self.treeLeft = [SKSpriteNode spriteNodeWithImageNamed:@"treeLeft"];

self.treeLeft.anchorPoint = CGPointMake(0.5, 1);

self.treeLeft.position = CGPointMake(self.size.width * .20, self.size.height);

self.treeLeft.zPosition = LayerForeground;

 

[self.worldNode addChild:self.treeLeft];

 

self.treeRight = [SKSpriteNode spriteNodeWithImageNamed:@"treeRight"];

self.treeRight.anchorPoint = CGPointMake(0.5, 1);

self.treeRight.position = CGPointMake(self.size.width * .86, self.size.height);

self.treeRight.zPosition = LayerForeground;

 

[self.worldNode addChild:self.treeRight];

现在所有的事情都已就位,我要解释一下了。

setupBackgroundsetupTrees 中,你创建了一个SKSpriteNode 并且使用 spriteNodeWithImageNamed 方法把它初始化了,从方法的名字就可以看出,是通过图片的名字来初始化节点。

然后你就把每一个锚点从默认值(0.5,0.5)改到(0.5,1)。

注释:想要了解关于这个集成系统的更多信息,请查阅苹果的Sprite Kit 程序指导中的Woring with Sprites章节。

你也需要设置精灵的postion(位置)和zPosition(深度)。很多时候,当你要设置精灵的位置时,你仅仅需要设置屏幕的宽度和高度到合适的值。

然而ground精灵必须要被直接放到背景的底端。你可以通过使用self.background.frame.origin.y获得背景框的大小数据的方法来实现这个目的。

同样的,当你想要让water这个精灵处在 ground 下面, water 的下端和 ground 重合,上端和 ground 有一定距离,具体看下图。你可以使用 self。ground.frame.orign.y+10 来实现。哦, water 这个精灵是没有使用预先声明的变量的。

回到TLCSharedConstants.h,那里你定义了一些用于精灵的 zPosition 的常量。你在代码中使用了它们当中的两个量,就是 LayerBackgroundLayerForeground。由于 SKSpriteNode 是继承于SKNode的,所以你可以使用 SKNode 的所有接口,包括 zPosition

最后,你就把所有刚创建的精灵加入到你的世界节点中。

然而精灵是可以直接加入到场景的,增加一个世界节点来包含这些精灵能使项目的长期开发地更顺利,尤其是当你要在游戏世界中加入物理特征的时候。

你的第一次编译和运行可是有官方的支持的。因此,干嘛不先试试?

编译运行后,如果你做的都是对的话,你将会在屏幕中看到如下画面:

真是个漂亮的场景。我们把鳄鱼做出来吧。

在鳄鱼节点中增加动画

增加鳄鱼节点和增加背景和前景没什么区别。

TLCMyScene.m 中找到setupCrocodie,并且加入如下代码:

self.crocodile = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForCrocodileMouthOpen];

self.crocodile.anchorPoint = CGPointMake(0.5, 1);

self.crocodile.position = CGPointMake(self.size.width * .75, self.background.frame.origin.y + (self.crocodile.size.height - 5));

self.crocodile.zPosition = LayerCrocodile;

 

[self.worldNode addChild:self.crocodile];

 

[self animateCrocodile];

上面的代码使用了你设定的两个常量: kImageNameForCrocodileMouthClosedLayerCrocodile 。程序使用背景节点的 frame.origin.y 获得第一个位置信息。和鳄鱼的大小相加得到鳄鱼位置的y值,x值是屏幕宽度的0.75,最后得到鳄鱼的位置。

就在之前的一个操作,你通过设置 zPosition 来使鳄鱼在背景层和前景层的最前面。默认的时候,Sprite Kit 都会根据精灵加入的顺序渲染画面。你可以通过赋予不同的 zPosition 来选择节点的深度。

现在该做动画了

找到 animateCrocodile 并加入如下代码:

NSMutableArray *textures = [NSMutableArray arrayWithCapacity:1];

 

for (int i = 0; i <= 1; i++) {

    NSString *textureName = [NSString stringWithFormat:@"%@0%d", kImageNameForCrocodileBaseImage, i];

    SKTexture *texture = [SKTexture textureWithImageNamed:textureName];

    [textures addObject:texture];

}

 

CGFloat duration = RandomFloatRange(2, 4);

 

SKAction *move = [SKAction animateWithTextures:textures timePerFrame:0.25];

SKAction *wait = [SKAction waitForDuration:duration];

SKAction *rest = [SKAction setTexture:[textures objectAtIndex:0]];

 

SKAction *animateCrocodile = [SKAction sequence:@[wait, move, wait, rest]];

[self.crocodile runAction: [SKAction repeatActionForever:animateCrocodile]];

以前的代码会创造一个叫SKTexture的数组对象。你可以在里面使用SKAction对象制作动画。

你也可以使用常量去设置一个基础的图片名称并且在你的动画里用for 这个循环的方法设置图片的编号。这里只有两张图片,分别为croc00croc01 。最终,你将使用一系列的SKAction 对象去制作鳄鱼的动画。

SKAction sequence让你设置多个动作并且依次连续地运行,就是把很多动画连接起来。

一旦你定义了这个动画序列,你就可以使用 runAction 方法去在节点上运行这个动作序列。在上面的代码中,你使用repeatActionForever: 去使这个节点重复循环的永远把动画序列运行下去。

最后就是调用setupCrocodile 来加入和运行这个鳄鱼动画。你可以在initWithSize 中做这件事。

TLCMyScene.m 的最上方,找到initWithSize ,在[self setupTrees] 后加入下面的代码。

[self setupCrocodile];

好了。你可以看到目光狰狞的鳄鱼张合它的下颚想要吃掉旁边的任何东西。

编译运行这段看这段野蛮粗暴的鳄鱼动画吧。

看上去好恐怖额。。作为玩家,你需要用菠萝去满足这只鳄鱼。当然菠萝是鳄鱼最爱吃的东西。

如果你的屏幕上的东西不是和上面的图片一模一样,那么你可能已经错过了某一步或者做错了什么。

你已经得到了这个画面并且有了玩家,那么是时候制定运动规则让游戏开始吧,也就是物理运动规则。

在你的游戏世界里增加物理原则

SpriteKit 利用的就是IOS的物理引擎包。这个引擎的底层就是BOX2D。如果你使用过CoCos-2D ,那么你可能已经使用过BOX2D 来管理你的物理层面的规则。在SpriteKit 中使用Box2D 最大的区别就是你不需要用C++去使用它,因为苹果公司已经把这个库封装在了Objective C的包装类中了。

首先,在TLCMyScene.m中找到initWithSize并在[self add child:self.worldNode]下加上三行代码:

self.worldNode.scene.physicsWorld.contactDelegate = self;

self.worldNode.scene.physicsWorld.gravity = CGVectorMake(0.0,-9.8);

self.worldNode.scene.physicsWorld.speed = 1.0;

然后在@interface末尾加上下面的代码,也就是文件上端的位置:

上面的代码设置了世界节点的碰撞代理,重力和速度。要记住,这个节点包括了你所有的其它节点,因此应该把物理设置加到这里。

这里的重力和速度的值是默认的,有各自默认的接口。这个系统是先在物体上指定了重力加速度,然后模拟确定速度。由于它们是默认的值,所以你不需要指定它们,但是知道这件事情总是好的,因为你可能会需要调节你的物理规律。

你可以在SKPhysicsWorld Class Reference中找到这些接口。

准备好绳子

到现在你应该已经知道要处理一下绳子的问题了,哦,我应该说是verlets

在这个工程中,我们使用TLCGameData 类来设定绳子。在开发环境中,你会用PLIST 或者其他数据储存文件来配置水平面。

然后,你就将创建一个含有TLCGameData 对象的数组来表示你储存数据的信息。

@property (nonatomic, assign) int name;

 

@property (nonatomic, assign) CGPoint ropeLocation;

@property (nonatomic, assign) int ropeLength;

 

@property (nonatomic, assign) BOOL isPrimaryRope;

这些就将作为你的数据模型。在开发环境中,你将知道使用PLIST会比用程序创造数据更好。

回到TLCMyScene.m 并在#import 后加入下列代码:

#define lengthOfRope1 24

#define lengthOfRope2 18

#define lengthOfRope3 15

然后在其他的接口后面加入下面的两个接口来控制奖品和绳子层级的数据:

@property (nonatomic, strong) SKSpriteNode *prize;

 

@property (nonatomic, strong) NSMutableArray *ropes;

当你完成了这些东西,那就去找到setupGameData 并加入下列代码:

self.ropes = [NSMutableArray array];

 

TLCGameData *rope1 = [[TLCGameData alloc] init];

rope1.name = 0;

rope1.ropeLocation = CGPointMake(self.size.width *.12, self.size.height * .94);

rope1.ropeLength = lengthOfRope1;

rope1.isPrimaryRope = YES;

[self.ropes addObject:rope1];

 

TLCGameData *rope2 = [[TLCGameData alloc] init];

rope2.name = 1;

rope2.ropeLocation = CGPointMake(self.size.width *.85, self.size.height * .90);

rope2.ropeLength = lengthOfRope2;

rope2.isPrimaryRope = NO;

[self.ropes addObject:rope2];

 

TLCGameData *rope3 = [[TLCGameData alloc] init];

rope3.name = 2;

rope3.ropeLocation = CGPointMake(self.size.width *.86, self.size.height * .76);

rope3.ropeLength = lengthOfRope3;

rope3.isPrimaryRope = NO;

[self.ropes addObject:rope3];

上面这些代码给你绳子定义了基础的参数。最重要的就是isPrimaryRope 接口,因为它决定了绳子是怎么连接到奖品(就是菠萝)上的。当你创造你的绳子时,只有一个对象的isPrimaryRope 属性是Yes。

最后,增加两个函数:initWithSize:[self setupGameData][self setupRopes] 。当你做好了,initWithSize 下就会像下面这样:

if (self = [super initWithSize:size]) {

    /* Setup your scene here */

 

    self.worldNode = [SKNode node];

    [self addChild:self.worldNode];

 

    self.worldNode.scene.physicsWorld.contactDelegate = self;

    self.worldNode.scene.physicsWorld.gravity = CGVectorMake(0.0,-9.8);

    self.worldNode.scene.physicsWorld.speed = 1.0;

 

    [self setupSounds];

    [self setupGameData];

 

    [self setupBackground];

    [self setupTrees];

    [self setupCrocodile];

 

    [self setupRopes];

}

 

return self;

现在你可以建造绳子了。

绳子类

在这节,你将会学习创建处理绳子的类。

打开TLCRope.h,你将将在在文件中加入两端代码。在@interface 前加入第一段代码,就是代理的协议。

@protocol TLCRopeDelegate

- (void)addJoint:(SKPhysicsJointPin *)joint;

@end

@interface 下加入第二段代码,包括了对自定义init方法的声明。

@property (strong, nonatomic) id delegate;

 

- (instancetype)initWithLength:(int)length usingAttachmentPoint:(CGPoint)point toNode:(SKNode*)node withName:(NSString *)name withDelegate:(id)delegate;

- (void)addRopePhysics;

- (NSUInteger)getRopeLength;

- (NSMutableArray *)getRopeNodes;

代理要一个对象定义一个包括方法的协议。而代理将回应这些方法。代理类必须声明以表示它将依据协议运行需要的方法。

注释:然而你不需要绳子对象的代理正确运行,这个教程包含着一个代理来说明如何去运行一个。最后的项目将包含一行解释性代码来说明一个在代理之前的可代替的方法。

当你完成了头文件,打开TLCRope.m 并在@interface 中加入下面的接口:


@property (nonatomic, strong) NSString *name;

 

@property (nonatomic, strong) NSMutableArray *ropeNodes;

 

@property (nonatomic, strong) SKNode *attachmentNode;

@property (nonatomic, assign) CGPoint attachmentPoint;

 

@property (nonatomic, assign) int length;

你接下来要做的就是在自定义的init中加入代码。找到 #pragma mark Init Method 并加入如下代码:

- (instancetype)initWithLength:(int)length usingAttachmentPoint:(CGPoint)point toNode:(SKNode*)node withName:(NSString *)name withDelegate:(id)delegate;

{

    self = [super init];

    if (self)

    {

        self.delegate = delegate;

 

        self.name = name;

 

        self.attachmentNode = node;

        self.attachmentPoint = point;

 

        self.ropeNodes = [NSMutableArray arrayWithCapacity:length];

 

        self.length = length;

    }

    return self;

}

很简单了:你把数值放到init里面并用它们来设置类中的私有接口。

下面要加的方法就是getRopeLengthgetRopeNodes

找到 #pragma mark Helper Methods 并加入下列代码:


- (NSUInteger)getRopeLength

{

    return self.ropeNodes.count;

}

 

- (NSMutableArray *)getRopeNodes

{

    return self.ropeNodes;

}

这两个方法是用来读取私有接口值的。

你可能已经注意到上面的代码引用的是rope nodes,是复数,也就是有很多个绳子节点。因为在这个游戏,每个绳子都是由很多个节点组成,主要是为了让它看上去和表现得是像是活动的,就是和真的绳子一样。我们来看看在实际运用中这是怎么工作的。

创造绳子的每个部分

尽管下面你写的几个方法将会是不完整的。这是有理由的。你可以去彻底地去理解发生了什么,一步一步地去做事总是很重要。

仍然在TLCRope.m 下操作,找到#pragma mark Setup Physics 并加入如下方法:


- (void)addRopePhysics

{

    // 记录追踪现在绳子子节点的位置

    CGPoint currentPosition = self.attachmentPoint;

 

    // 加入绳子所有的子部分

    for (int i = 0; i < self.length; i++) {

        SKSpriteNode *ropePart = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForRopeTexture];

        ropePart.name = self.name;

        ropePart.position = currentPosition;

        ropePart.anchorPoint = CGPointMake(0.5, 0.5);

 

        [self addChild:ropePart];

        [self.ropeNodes addObject:ropePart];

 

        /* TODO - 在这加入控制物理特性的代码 */

 

        // set the next rope part position

        currentPosition = CGPointMake(currentPosition.x, currentPosition.y - ropePart.size.height);

    }

}

在上面的代码中,你在对象初始化后调用这个函数,你创建了每个绳子的部分并把它们加到ropNode 数组中。为了以后可以引用它们,你也给每一个绳子部分命名。最后,你使用addChild 把它们以子节点的身份加到TLCRope 对象中。

马上,你就将用一些代码替换上面的TODO注释,为了给这些绳子的部分自身的物理实体。

现在你让一切就绪了。你几乎可以通过编译运行来看的你的绳子了。最后的步骤就是把绳子了绑好的奖品加到主场景中。

把绳子和奖品加到场景中

由于项目给TLCRope 用了代理模式,你将要声明它在任何充当代理的类中。在这个例子里,这个类就是TLCMyScene

打开TLCMyScene.m 找到@interface 这行。把它改成下面那样:

@interface TLCMyScene() 

你将用3中方法把绳子加到场景中,并且他们都是内部连接的,这3个方法就是:setupRopes,addRopeAtPosition:withLength:withNamesetupPrizeUsingPrimaryRope

先做这步吧,找到setupRope 并加入如下代码:


// 得到绳子数据

    for (int i = 0; i < [self.ropes count]; i++) {

        TLCGameData *currentRecord = [self.ropes objectAtIndex:i];

 

        // 1

        TLCRope *rope = [self addRopeAtPosition:currentRecord.ropeLocation withLength:currentRecord.ropeLength withName:[NSString stringWithFormat:@"%i", i]];

 

        // 2

        [self.worldNode addChild:rope];

        [rope addRopePhysics];

 

        // 3

        if (currentRecord.isPrimaryRope) {

            [self setupPrizeUsingPrimaryRope:rope];

        }

    }

 

    self.prize.position = CGPointMake(self.size.width * .50, self.size.height * .80);



我先解释下:

  1. 首先你创建新的绳子时往里面传递了位置和长度。等一下你将会写这个方法。

  2. 下一步就是把绳子加入到世界中并设置物理特性。

  3. 最后一步就是当绳子是主要的那个,就把奖品设置物理特性。

找到addRopeAtPosition:withLength:withName 并用下面的代码代替里面的内容。


SKSpriteNode *ropeHolder = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForRopeHolder];

 

ropeHolder.position = location;

ropeHolder.zPosition = LayerRope;

 

[self.worldNode addChild:ropeHolder];

 

CGPoint ropeAttachPos = CGPointMake(ropeHolder.position.x, ropeHolder.position.y -8);

 

TLCRope *rope = [[TLCRope alloc] initWithLength:length usingAttachmentPoint:ropeAttachPos toNode:ropeHolder withName:name withDelegate:self];

rope.zPosition = LayerRope;

rope.name = name;

 

return rope;

其实你就是用了这个方法创造了单独的绳子并把它们显示在屏幕上。

你曾经看过这段内容。首先你使用常量代表图片名称,用图片名称初始化SKSpriteNode ,然后用常量设置它的positionzPosition。这个SKSpriteNode将成为绳子的固定点。

代码继续初始化你的绳子对象并设置它的zPositionname

最后,最后一个难题就只剩下弄好你的奖品了。是不是很聪明?

找到setupPrizeUsingPrimaryRope并加入如下代码:


self.prize = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForPrize];

self.prize.name = kNodeNameForPrize;

self.prize.zPosition = LayerPrize;

 

self.prize.anchorPoint = CGPointMake(0.5, 1);

 

SKNode *positionOfLastNode = [[rope getRopeNodes] lastObject];

self.prize.position = CGPointMake(positionOfLastNode.position.x, positionOfLastNode.position.y + self.prize.size.height * .30);

 

[self.worldNode addChild:self.prize];

你可能已经注意到了一个存在于setupRopes和游戏的绳子对象中的为变量isPrimaryRope而设的接口,这个接口让你的循环贯穿数据并选择合适的绳子作为主绳来绑上这个奖品。

当你把isPrimaryRope设置为YES,上面的代码会执行和找到传入绳子对象的末端。可以通过得到绳子的数组对象ropeNodes内部的末尾对象来找到绳子的末端。可以通过使用在TLCRope中的方法getRopeNodes来执行这个操作。

注释:如果你想知道为什么奖品的位置会移动到TLCRope对象的最后一个节点。因为在Sprite Kit中有一个bug。当你设置物理实体时,如果节点的位置在先前未被设置,那么实体的行为将无法预测。以前的代码使用你选择的绳子的最后的那个部分作为奖品的初始位置。

现在你还等什么,快开始编译运行你的项目吧。

等一下。。。为什么菠萝没被系在绳子上?为什么这些东西看上去这么蛋疼。

不要担心,解决方案就是设置更多的物理特性。

在绳子中增加物理实体

还记得你早先加的TODO的注释吗?是时候用物理实体代替注释来使东西移动了。

打开TLCRope.m 并找到addRopePhysics 。用如下代码代替TODO的注释:


CGFloat offsetX = ropePart.frame.size.width * ropePart.anchorPoint.x;

CGFloat offsetY = ropePart.frame.size.height * ropePart.anchorPoint.y;

 

CGMutablePathRef path = CGPathCreateMutable();

 

CGPathMoveToPoint(path, NULL, 0 - offsetX, 7 - offsetY);

CGPathAddLineToPoint(path, NULL, 7 - offsetX, 7 - offsetY);

CGPathAddLineToPoint(path, NULL, 7 - offsetX, 0 - offsetY);

CGPathAddLineToPoint(path, NULL, 0 - offsetX, 0 - offsetY);

 

CGPathCloseSubpath(path);

 

ropePart.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

ropePart.physicsBody.allowsRotation = YES;

ropePart.physicsBody.affectedByGravity = YES;

 

ropePart.physicsBody.categoryBitMask = EntityCategoryRope;

ropePart.physicsBody.collisionBitMask = EntityCategoryRopeAttachment;

ropePart.physicsBody.contactTestBitMask =  EntityCategoryPrize;

 

[ropePart skt_attachDebugFrameFromPath:path color:[SKColor redColor]];

CGPathRelease(path);

上面的代码给你每个绳子的部分创建了物理实体,让你为每个节点设置一系列的物理特性。比如形状,大小,密度,重力以及摩擦力。

使用类方法SKPhysicsBody bodyWithPolygonFromPath:来创建物理实体。这种方法使用了一个参数,那就是path。可以用 SKPhysicsBody Path Generator这个在线方便的工具得到这个path(物体轮廓路径)。

注释想要知道在你的项目中使用物理实体的更多方法,可以参考 SKPhysicsBody Class Reference

另外,把物理实体设置给每个节点,上面的代码也设置了一些关键的接口来处理碰撞,也就是categoryBitMask, collisionBitMask和contactTestBitMask。每一个的值都被赋予一个你之前定义的常数值。这个教程等一下将把这些接口包括到物体的深度信息中。

如果你现在运行你的app,那么每一个绳子的部分都将掉到你的屏幕底部。那是因为你已经给绳子的每个部分并且还没有把它们连接在一起。

为了融合你的绳子,你将使用SKPhysicsJoint,在 addRopePhysics下加入如下的方法:

- (void)addRopeJoints

{

    // 给初始化的绑定点设置关节 

    SKNode *nodeA = self.attachmentNode;

    SKSpriteNode *nodeB = [self.ropeNodes objectAtIndex:0];

 

    SKPhysicsJointPin *joint = [SKPhysicsJointPin jointWithBodyA: nodeA.physicsBody

                                                           bodyB: nodeB.physicsBody

                                                          anchor: self.attachmentPoint];

 

    // 让绑定点僵硬化

    joint.shouldEnableLimits = YES;

    joint.upperAngleLimit = 0;

    joint.lowerAngleLimit = 0;

 

    [self.delegate addJoint:joint];

 

    // 为剩下的绳子子节点设置关节

    for (int i = 1; i < self.length; i++) {

        SKSpriteNode *nodeA = [self.ropeNodes objectAtIndex:i-1];

        SKSpriteNode *nodeB = [self.ropeNodes objectAtIndex:i];

        SKPhysicsJointPin *joint = [SKPhysicsJointPin jointWithBodyA: nodeA.physicsBody

                                                               bodyB: nodeB.physicsBody

                                                              anchor: CGPointMake(CGRectGetMidX(nodeA.frame),

                                                                                  CGRectGetMinY(nodeA.frame))];

        // 让关节自由转动

        joint.shouldEnableLimits = NO;

        joint.upperAngleLimit = 0;

        joint.lowerAngleLimit = 0;

 

        [self.delegate addJoint:joint];

    }

}

这些方法通过 SKPhysicsJoint类连接了所有的绳子部分。这个类能使两个连接好的实体能独自以锚点为中心旋转,这使游戏中的绳子和真的绳子一样。

你将绳子的第一个部分在attachmentPoint上连接(固定)到attachmentNode然后把其它的子序列节点加到前面的节点上。

注释: 在上面的代码中,有用调用代理的方式把关节加到场景当中。正如我先前已经提到的,这种函数的调用不是必要的。你可以简单地调用[self.scene.physicsWorld addJoint:joint]来完成这样的事。

现在把这句函数调用加到刚写好的方法的addRopePhysics下面。

[self addRopeJoints];

编译运行,开吧!

当你有了这些活动的绳子,但它们只是从屏幕上落下来。这是因为你没有在TLCScene.m给节点赋上物理实体。是时候给绳子的固定点和奖品加上物理实体了。

固定绳子的末端

打开TLCMyScene.m 找到addRopeAtPosition:withLength:withName:,在[self.worldNode addChild:ropeHolder]下加入如下的代码:

CGFloat offsetX = ropeHolder.frame.size.width * ropeHolder.anchorPoint.x;

CGFloat offsetY = ropeHolder.frame.size.height * ropeHolder.anchorPoint.y;

 

CGMutablePathRef path = CGPathCreateMutable();

 

CGPathMoveToPoint(path, NULL, 0 - offsetX, 6 - offsetY);

CGPathAddLineToPoint(path, NULL, 6 - offsetX, 6 - offsetY);

CGPathAddLineToPoint(path, NULL, 6 - offsetX, 0 - offsetY);

CGPathAddLineToPoint(path, NULL, 0 - offsetX, 0 - offsetY);

 

CGPathCloseSubpath(path);

 

ropeHolder.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

ropeHolder.physicsBody.affectedByGravity = NO;

ropeHolder.physicsBody.dynamic = NO;

 

ropeHolder.physicsBody.categoryBitMask = EntityCategoryRopeAttachment;

ropeHolder.physicsBody.collisionBitMask = 0;

ropeHolder.physicsBody.contactTestBitMask =  EntityCategoryPrize;

 

[ropeHolder skt_attachDebugFrameFromPath:path color:[SKColor redColor]];

CGPathRelease(path);

这里你给每一个绳子的固定点设置了SKPhysicsBody以及用于检测碰撞的接口。你要让这些固定处充当结实的稳固点,可以通过关闭它们的affectedByGravitydynamic接口来实现这个功能。

下一步就是找到代理的方法addJoint,加入下面这行:

[self.worldNode.scene.physicsWorld addJoint:joint];

上面的方法就是将你在TLCRope.m里创建的节点加入到场景中。这行代码就将绳子的每个部分连接在一起。

下一步就是把物理实体加入到奖品中并设置它的碰撞接口。

找到 setupPrizeUsingPrimaryRope:,在[self.worldNode addChild:self.prize]之前加入下列代码:

CGFloat offsetX = self.prize.frame.size.width * self.prize.anchorPoint.x;

CGFloat offsetY = self.prize.frame.size.height * self.prize.anchorPoint.y;

 

CGMutablePathRef path = CGPathCreateMutable();

 

CGPathMoveToPoint(path, NULL, 18 - offsetX, 75 - offsetY);

CGPathAddLineToPoint(path, NULL, 5 - offsetX, 65 - offsetY);

CGPathAddLineToPoint(path, NULL, 3 - offsetX, 55 - offsetY);

CGPathAddLineToPoint(path, NULL, 4 - offsetX, 34 - offsetY);

CGPathAddLineToPoint(path, NULL, 8 - offsetX, 7 - offsetY);

CGPathAddLineToPoint(path, NULL, 21 - offsetX, 2 - offsetY);

CGPathAddLineToPoint(path, NULL, 33 - offsetX, 4 - offsetY);

CGPathAddLineToPoint(path, NULL, 38 - offsetX, 20 - offsetY);

CGPathAddLineToPoint(path, NULL, 34 - offsetX, 53 - offsetY);

CGPathAddLineToPoint(path, NULL, 36 - offsetX, 62 - offsetY);

 

CGPathCloseSubpath(path);

 

self.prize.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

self.prize.physicsBody.allowsRotation = YES;

self.prize.physicsBody.affectedByGravity = YES;

self.prize.physicsBody.density = 1;

self.prize.physicsBody.dynamic = NO;

 

self.prize.physicsBody.categoryBitMask = EntityCategoryPrize;

self.prize.physicsBody.collisionBitMask = 0;

self.prize.physicsBody.contactTestBitMask = EntityCategoryRope;

 

[self.prize skt_attachDebugFrameFromPath:path color:[SKColor redColor]];

CGPathRelease(path);

就像之前一样,你设置了物理实体和碰撞检测接口。

为了把奖品连接到绳子的末端,你需要做两件事情。

第一,就是找到setupRopes。在for循环的末端,加入如下代码:

//把绳子的另一端接到奖品上

[self attachNode:self.prize toRope:rope];

然后,找到attachNode:toRope并加入如下代码:

SKNode *previous = [[rope getRopeNodes] lastObject];

node.position = CGPointMake(previous.position.x, previous.position.y + node.size.height * .40);

 

SKSpriteNode *nodeAA = [[rope getRopeNodes] lastObject];

 

SKPhysicsJointPin *jointB = [SKPhysicsJointPin jointWithBodyA: previous.physicsBody

                                                        bodyB: node.physicsBody

                                                       anchor: CGPointMake(CGRectGetMidX(nodeAA.frame), CGRectGetMinY(nodeAA.frame))];

 

 

[self.worldNode.scene.physicsWorld addJoint:jointB];

上面的代码得到了绳子对象的最后一个节点并为了连接奖品创建了一个新的SKPhysicsJointPin

编译运行项目。如果你的连接点和节点都设置正确了,你应该能在屏幕上看到这样的图片:

看起来很棒吧。额。。。可能还是有点呆板。可能这就是你在游戏里想要的效果吧。如果不是,你可以让绳子看上去更加灵活柔软一些。

TLCMyScene.m的顶端并在你的#define下加入如下代码:

#define prizeIsDynamicsOnStart YES

然后找到setupRopes并改变最后两句代码:

// 重设你的奖品位置,根据你的游戏需要设置是否要是动态的

self.prize.position = CGPointMake(self.size.width * .50, self.size.height * .80);

self.prize.physicsBody.dynamic = prizeIsDynamicsOnStart;

再次编译运行这个项目。

现在能看到你的绳子是多么柔软灵活了吧。当然,如果你想要另一种形态,只要把prizeIsDynamicsOnStart改为No。毕竟这是你的游戏,随便啦,做爱做的事吧。

再增加一点物理实体

由于你已经知道这些物理实体了,那就应该给玩家和水节点设置这些实体。当你把这些东西都配置好,你最好也把物理碰撞检测也做好。

TLCMyScene.m,找到setupCrocodile并在[self.worldNode addChild:self.crocodile];前加入如下代码:

CGFloat offsetX = self.crocodile.frame.size.width * self.crocodile.anchorPoint.x;

CGFloat offsetY = self.crocodile.frame.size.height * self.crocodile.anchorPoint.y;

 

CGMutablePathRef path = CGPathCreateMutable();

CGPathMoveToPoint(path, NULL, 47 - offsetX, 77 - offsetY);

CGPathAddLineToPoint(path, NULL, 5 - offsetX, 51 - offsetY);

CGPathAddLineToPoint(path, NULL, 7 - offsetX, 2 - offsetY);

CGPathAddLineToPoint(path, NULL, 78 - offsetX, 2 - offsetY);

CGPathAddLineToPoint(path, NULL, 102 - offsetX, 21 - offsetY);

 

CGPathCloseSubpath(path);

 

self.crocodile.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

 

self.crocodile.physicsBody.categoryBitMask = EntityCategoryCrocodile;

self.crocodile.physicsBody.collisionBitMask = 0;

self.crocodile.physicsBody.contactTestBitMask =  EntityCategoryPrize;

 

self.crocodile.physicsBody.dynamic = NO;

 

[self.crocodile skt_attachDebugFrameFromPath:path color:[SKColor redColor]];

CGPathRelease(path);

就和绳子节点一样,你要给你的玩家节点上的物理实体确定轮廓路径并设置它的碰撞检测接口,这些我等一下就会逐一解释。

最后的但并不是不重要的,水也需要物理实体这样当奖品是落在水中而不是鳄鱼的嘴里,你就能判断出来。

找到setupBackground,在[self.worldNode addChild:water];上,加入如下代码:

// 让整个尺寸看上去短一些,为了让奖品看上去是真的落到了水里

CGSize bodySize = CGSizeMake(water.frame.size.width, water.frame.size.height -100);

 

water.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:bodySize];

water.physicsBody.dynamic = NO;

 

water.physicsBody.categoryBitMask = EntityCategoryGround;

water.physicsBody.collisionBitMask = EntityCategoryPrize;

water.physicsBody.contactTestBitMask =  EntityCategoryPrize;

再一次,你加了一个物理实体,但是这次你用的是bodyWithRectangleOfSize:。你也要设置碰撞检测接口。

你已经注意到你把EntityCategoryGround赋给了水对象的categoryBitMask(类识别掩码)。事实上EntityCategoryGround代表着你接受水果失败了,水果撞上地面的情况。如果你想加一些其他的陷阱比如旋转锯,你将会把EntityCategoryGround赋给它的类识别掩码。

注释:你可能注意到在大多数物理实体的设置中,调用了skt_attachDebugFrameFromPath:。这是一个从SKNode+SKTDebugDraw来的方法。它是Sprite Kit 工具集中的一部分。而这个工具集是Razeware做的。这个特殊的方法在调试物理实体的过程中起作用。为了把它打开,可以打开SKNode+SKTDebugDraw.m并把BOOL SKTDebugDrawEnabledno改成yes。它会画出一个形状来代表你的物理实体。当你把事情完成了,不要忘记把它关掉。

把切绳子的功能做出来

如果说你的verlets不能被切,那么切Verlet这个功能就不存在了。

在这节,你将学习如何使用触摸方法去让你的玩家切绳子。第一步就是第一一些基础的变量。

仍然实在TLCMyScene.m中操作,在@interface下加入下面的接口:

@property (nonatomic, assign) CGPoint touchStartPoint;

@property (nonatomic, assign) CGPoint touchEndPoint;

@property (nonatomic, assign) BOOL touchMoving;

你将在追踪玩家的触摸位置中使用它们。

然后在TLCMyScene.m上方加入你最后的定义

#define canCutMultipleRopesAtOnce NO

如果你想对游戏的功能做一些改变,你将需要它。

IOS包括一些处理触摸事件的方法。你将使用3种方法,分别是touchesBegan:withEvent:, touchesEnded:withEvent:touchesMoved:withEvent:

找到touchesBegan:withEvent:并加入如下代码:

self.touchMoving = NO;

 

for (UITouch *touch in touches) {

    self.touchStartPoint = [touch locationInNode:self];

}

上面的代码设置了基于玩家触摸位置的值。

下一步,找到touchesEnded:withEvent:并加入下列代码:

for (UITouch *touch in touches) {

    if (touches.count == 1 && self.touchMoving) {

        self.touchEndPoint = [touch locationInNode:self];

 

        if (canCutMultipleRopesAtOnce) {

            /* 允许几根绳子被切 */

 

            [self.worldNode.scene.physicsWorld enumerateBodiesAlongRayStart:self.touchStartPoint end:self.touchEndPoint usingBlock:^(SKPhysicsBody *body, CGPoint point, CGVector normal, BOOL *stop)

             {

                 [self checkRopeCutWithBody:body];

             }];

        }

        else {

            /* 允许只有一根绳子被切 */

 

            SKPhysicsBody *body = [self.worldNode.scene.physicsWorld bodyAlongRayStart:self.touchStartPoint end:self.touchEndPoint];

            [self checkRopeCutWithBody:body];

        }

    }

}

 

self.touchMoving = NO;

代码做了下面几件事。首先,它确保了玩家正用一个手指触摸屏幕,然后它判断了玩家是否正在移动手指。最后,它得到数据并将其赋给了touchEndPoint。有了这个数据,你可以基于你是允许在一次划动过程中是让一根绳子被切还是让多根绳子被切来执行相应的动作。

当要切多根绳子,你使用SKPhysicsWorldenumerateBodiesAlongRayStart:end:usingBlock:方法去得到多个触摸点。当要切单根绳子的时候,你使用bodyAlongRayStart:end:方法去得到第一个触摸点。然后你就把信息传给自制的方法:checkRopeCutWithBody:

最后,找到touchesMoved:withEvent:,并加入如下代码:

if (touches.count == 1) {

    for (UITouch *touch in touches) {

        NSString *particlePath = [[NSBundle mainBundle] pathForResource:@"TLCParticle" ofType:@"sks"];

        SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:particlePath];

        emitter.position = [touch locationInNode:self];

        emitter.zPosition = LayerRope;

        emitter.name = @"emitter";

 

        [self.worldNode addChild:emitter];

 

        self.touchMoving = YES;

    }

}

从技术上来说,上面的绝大多数代码你并不需要,但是它可以提供很多帅酷霸气吊的效果当你划过屏幕的时候。并且你需要把touchMoving接口设为YES。正如你早些时候看到的,你设置这个值来决定玩家是否在移动手指。

那么剩下的代码是干嘛的?

这里使用了SKEmitterNode去自动把帅气的绿色粒子显示在屏幕上当玩家划动的时候。

上面的代码载入了粒子文件并把它加到worldNode。但粒子反射器的介绍不在本次的教程范围内。额。。。你还是知道这个东西的存在的吧。好吧,你还有其他事情要做。

触摸事件的相关函数完成后,你应该去完成在touchesEnded:withEvent:里调用的方法。

找到checkRopeCutWithBody:并加入下列代码:

SKNode *node = body.node;

if (body) {

    self.prize.physicsBody.affectedByGravity = YES;

    self.prize.physicsBody.dynamic = YES;

 

    [self.worldNode enumerateChildNodesWithName:node.name usingBlock:^(SKNode *node, BOOL *stop)

     {

         for (SKPhysicsJoint *joint in body.joints) {

             [self.worldNode.scene.physicsWorld removeJoint:joint];

         }

 

         SKSpriteNode *ropePart = (SKSpriteNode *)node;

 

         SKAction *fadeAway = [SKAction fadeOutWithDuration:0.25];

         SKAction *removeNode = [SKAction removeFromParent];

 

         SKAction *sequence = [SKAction sequence:@[fadeAway, removeNode]];

         [ropePart runAction: sequence];

     }];

}

上面代码枚举了worldNode所有的子节点,而且如果它得到了相应的绳子关节,它把这些绳子关节删掉了。当然不会是突然就删掉它们,代码使用了SKAction的序列让节点先淡出,然后删除。

编译运行项目,你应该能划动屏幕并切掉这三根绳子和奖品(目前是这样的)。切换canCutMultipleRopesAtOnce的设置来看整个行为有什么不一样。还有,这些产生的粒子看上去不是很好看吗?

碰撞检测

你几乎已经把碰撞检测做好了。你完成了划动时有的效果,物理实体,而且你已经设置了所有的碰撞接口,但是这些碰撞接口是干什么的?它们怎么运作的?更重要的是它们之间是怎么运作的?

这里有一些重要的事要提:

  1. 你需要让TLCMyScene充当一个contact delegate(触点代理):SKPhysicsContactDelegate

  2. 你需要给世界节点设置代理:self.worldNode.scene.physicsWorld.contactDelegate = self

  3. 你需要设置一个categoryBitMask, 一个collisionBitMask 和一个contactTestBitMask

  4. 你需要运行这个代理方法。

你在给worldNode设置物理的时候做了前两件事情。当你给节点们设置了物理实体时做了第三件事情。既然这些都被你在之前的操作中做了,那你还是很有远见的。

那么就是剩下第四件事了:运行这些方法。在做这些是之前,你要创建一些接口。

TLCMyScene.m下的@implementation中,加入下列代码:


@property (nonatomic, assign) BOOL scoredPoint;

@property (nonatomic, assign) BOOL hitGround;

现在你可以修改代理方法了。

找到didBeginContact:并加入如下代码:

SKPhysicsBody *other = (contact.bodyA.categoryBitMask == EntityCategoryPrize ? contact.bodyB : contact.bodyA);

if (other.categoryBitMask == EntityCategoryCrocodile) {

    if (!self.hitGround) {

        NSLog(@"scoredPoint");

        self.scoredPoint = YES;

    }

 

    return;

}

else if (other.categoryBitMask == EntityCategoryGround) {

    if (!self.scoredPoint) {

        NSLog(@"hitGround");

        self.hitGround = YES;

        return;

    }

}

上面的代码在任何时间都会不断执行场景的physicsWorld(物理世界)的碰撞检测。它检测实体的categoryBitMask并基于这个值,得一分并注册一个地面碰撞。

三种设置:categoryBitMask, collisionBitMaskcontactTestBitMask都是互相工作的。

  • categoryBitMask定义了这个精灵是属于哪个类别的。

  • collisionBitMask设置了可以与之碰撞的精灵。

  • contactTestBitMask设置了那些类别可以触发通知发给代理。

浏览SKPhysicsBody Class Reference以学到更多。

这个游戏用了五个类别,在TLCSharedConstants类中定义了的。打开TLCSharedConstants.m来看一下。你将看到一些在之前设置了的碰撞类别。


EntityCategoryCrocodile = 1 << 0,

EntityCategoryRopeAttachment = 1 << 1,

EntityCategoryRope = 1 << 2,

EntityCategoryPrize = 1 << 3,

EntityCategoryGround = 1 << 4

你得去检测奖品和鳄鱼碰撞的情况以及奖品和水碰撞的情况。你不是基于绳子的接触点来奖励分数或者结束游戏,而是基于为绳子的附属点设置的类别。而这些附属点就是为了让绳子在这些点上看上去更为真实。

注释:这里有一些你在本教程不需要的代理方法。你可以阅读SKPhysicsContact Class ReferenceSKPhysicsContactDelegate Protocol Reference来学习它们。

编译运行工程。当你切绳子的时候,当奖品落地,你将会看到记录被打出来。

奖励动画!

当你做出游戏的大致样子,玩家们是不会看着命令窗口来知道他们是不是赢了。而且,当你切了正确的绳子,水果会掉到鳄鱼的嘴里并且鳄鱼会吃水果。玩家会想让鳄鱼吃掉菠萝的。

是时候用动画实现这个功能了。你可以通过修改nomnomnomActionWithDelay:.来达成。

[self.crocodile removeAllActions];

 

SKAction *openMouth = [SKAction setTexture:[SKTexture textureWithImageNamed:kImageNameForCrocodileMouthOpen]];

SKAction *wait = [SKAction waitForDuration:duration];

SKAction *closeMouth = [SKAction setTexture:[SKTexture textureWithImageNamed:kImageNameForCrocodileMouthClosed]];

 

SKAction *nomnomnomAnimation = [SKAction sequence:@[openMouth, wait, closeMouth]];

 

[self.crocodile runAction: [SKAction repeatAction:nomnomnomAnimation count:1]];

 

if (!self.scoredPoint) {

    [self animateCrocodile];

}

上面的代码使用removeAllActions移除了现在运行在鳄鱼节点上的所有动画。然后它加了鳄鱼张开嘴和不关闭嘴的动画并把它加到动画序列中,把序列在鳄鱼节点上运行出来。在这时,如果玩家没有得分,它将运行animateCrocodile来重设鳄鱼的开关下颚的动作。

接下来就是找到checkRopeCutWithBody:并且在self.prize.physicsBody.dynamic = YES;后加入下列代码:

[self nomnomnomActionWithDelay:1];

这段代码在玩家每次切绳子的时候执行。它运行了你刚刚创建的方法。然后这段动画就会给你一种错觉让你感觉鳄鱼好像是在张开嘴期待美味掉入它的嘴里。

你也需要运行didBeginContact的方法,为了让奖品落到鳄鱼嘴中的时候,鳄鱼能张嘴吃它。

didBeginContact:中,在self.scoredPoint = YES;后加入如下代码:

[self nomnomnomActionWithDelay:.15];

就像之前那样,你运行了nomnomnomActionWithDelay,,只是在这次,你是在当奖品接触到鳄鱼的时候才运行它。这让鳄鱼看上去在吃奖品。

编译运行

食物掉下的时候就从鳄鱼上穿过去了。只要做一些小小的改变,你就可以解决这个问题。

找到checkForScore并加入如下代码:


if (self.scoredPoint) {

    self.scoredPoint = NO;

 

    SKAction *shrink = [SKAction scaleTo:0 duration:0.08];

    SKAction *removeNode = [SKAction removeFromParent];

 

    SKAction *sequence = [SKAction sequence:@[shrink, removeNode]];

    [self.prize runAction: sequence];

}

上面的代码解决了scoredPoint的接口。如果被设为YES,代码就将值设为NO,使用SKAction sequence播放nomnomnom(这是个拟声词,和喵喵喵的性质一样)的声音并将奖品从场景中去掉。

如果你想让代码继续执行并且能追踪更新你的数值,你需要改变update函数。

找到update并加入如下代码:

[self checkForScore];

update:执行在每一帧动画播放之前。你可以在这里调用方法来检查是否玩家得到了分数。

你要做的下一件事情就是检查一下地面碰撞。找到checkForGroundHit并加入下列代码:

if (self.hitGround) {

    self.hitGround = NO;

 

    SKAction *shrink = [SKAction scaleTo:0 duration:0.08];

    SKAction *removeNode = [SKAction removeFromParent];

 

    SKAction *sequence = [SKAction sequence:@[shrink, removeNode]];

    [self.prize runAction: sequence];

}

几乎就像checkForScore一样,这段代码检查了hitGround的值。如果这个值为YES,代码就把它重置为NO,通过SKAction sequence.播放

Sprite Kit教程:动画和纹理图集

原文地址:http://www.raywenderlich.com/45152/sprite-kit-tutorial-animations-and-texture-atlases 翻译:破船

从本文,可以学习到如何使用iOS 7中的Sprite Kit框架创建一个简单的动画:在屏幕上行走的熊。

另外还可以学习到如何使用纹理图集来制作动画效果,如何在触摸事件发生时让熊移动,以及改变熊运动的方向。

学习本文之前,最好先看看下面的文章: Sprite Kit教程:初窥门径

英文原文在这里:Sprite Kit Tutorial for Beginners

下面我们就开始吧。

创建一个工程

我们先创建好一个工程架子——选择File\New Project…,在iOS Application中选择Sprite Kit Game,如下图所示:

4

选择Next,并将工程命名为AnimatedBear,把Class Prefix中的内容清除掉,并将Devices选择为iPad,如下图所示:

5

接着选择Next,将工程保存到磁盘中。

现在编译并运行程序的话,当点击屏幕时,可以看到在屏幕中有一个自动旋转的飞船。如下图所示:

6

这样工程架子就准备好了,下面我们去寻找一些熊的动画资源——从这里下载即可:BearImages Art。如下图所示:

7

上面下载到的图片有所需要的最大分辨率——iPad retina显示(2X)和non-retina版本(1x)。这些文件的命名方式为bear1..n@2x~ipad和bear1..n~ipad.png。

在这里,构建一个动画,你可以只需要将这些图片直接添加到Sprite Kit工程中即可。不过,还有另外一种更加方便的方法来构建动画——使用纹理图集。

纹理图集和熊

如果之前你没使用过纹理图集,那你可以把它想象为一副很大的图片,其中包括动画中需要使用到的各种图片。这个图集可以看做是一个文件,它指定了每个sprite的边界范围,当在代码中需要使用时,可以将这些sprite取出来。

使用纹理图集是因为Sprite Kit和图形引擎会对其做相应的优化处理。后面这段话暂时不知道什么意思:

If you use sprites within a texture atlas properly, rather than making one OpenGL ES draw call per sprite it just makes one per texture atlas sheet.

简而言之——使用纹理图集会非常的快,特别是有大量sprite的时候!

Xcode会自动的生成这个纹理图集文件,并指定好每个sprite的边界范围,这样当在代码中需要用到某个sprite的时候,可以方便取出来。这一切都会自动处理,开发者不用亲力为之。

注意:当纹理图集有问题时(例如错误的图片等),那么建议clean一下工程(Product\Clean)——这样可以强制让纹理图集重新构建。

为纹理图集创建一个文件夹,并将图片文件放置到该文件夹中,然后在文件夹名称尾部添加.atlas。这样Xcode就能识别出.atlas扩展名,进而自动的将图片合并为一个纹理图集。

之前下载的图片资源中有一个名为BearImages.atlas的文件夹,里面包含了各种分辨率的图片(是其它两个文件夹中的图片合集)。

将名为BearImages.atlas的文件夹拖拽到程序中,如下图所示:

8

当释放鼠标时,会看到如下图片中的对话框:是关于如何添加到工程中的。确保选中这三项:Copy items into destination group’s folder, Create groups for any added folder, 和 the AnimatedBear,然后点击Finish

9

在Xcode中展开这个文件夹BearImages.atlas,会看到如下内容:

10

下面,是时候让熊动起来了!

一个简单的动画

这里我们先把熊显示在屏幕中间,并开启永久循环动画。

此处主要都是在MyScene.m中写代码。打开这个文件,并用下面的代码替换之:

#import 
#import "MyScene.h"

@implementation MyScene
{

    SKSpriteNode *_bear;
    NSArray *_bearWalkingFrames;

}

-(id)initWithSize:(CGSize)size
{
    if (self = [super initWithSize:size]) {
        /* Setup your scene here */

        self.backgroundColor = [SKColor blackColor];

        // TODO...

    }
    return self;
}

-(void)update:(CFTimeInterval)currentTime {
    /* Called before each frame is rendered */
}

@end

上面的代码很简单,只是定义了几个稍后会用到的变量。编译并运行一下,确保没有错误——会看到屏幕是黑色的。

接下来要让熊动起来,有5步需要处理,我们就来看看吧。

记得将下面的代码添加到initWithSize方法的TODO位置。

1) 构建一个用于保存行走帧(walking frame)

NSMutableArray *walkFrames = [NSMutableArray array];

2) 加载纹理图集

SKTextureAtlas *bearAnimatedAtlas = [SKTextureAtlas atlasNamed:@"BearImages"];

上面的代码会从程序bundle的数据区中创建一个图集。Sprite Kit会根据设备的寻找对应分辨率的图片文件,在iPad retina上会使用BearImages@2x~ipad.png。

3) 构建帧列表

int numImages = bearAnimatedAtlas.textureNames.count;
for (int i=1; i <= numImages/2; i++) {
    NSString *textureName = [NSString stringWithFormat:@"bear%d", i];
    SKTexture *temp = [bearAnimatedAtlas textureNamed:textureName];
    [walkFrames addObject:temp];
}
_bearWalkingFrames = walkFrames;

上面的代码根据图片名称从图集中循环获取到一个帧列表(这些图片的命名为bear1.png->bear8.png),注意到numImages这个变量了吗?它为啥要除以2呢?

这是因为:纹理图集包含了所有分辨率的图片文件(non-retina和retina)。共有16个文件,每种分辨率有8个文件。要想加载某种分辨率的图片,就需要除以2。这样通过名称和计数器,就能获取到正确的分辨率图片。

4) 创建sprite,并将其位置设置为屏幕中间,然后将其添加到场景中

SKTexture *temp = _bearWalkingFrames[0];
_bear = [SKSpriteNode spriteNodeWithTexture:temp];
_bear.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
[self addChild:_bear];
[self walkingBear];

利用帧列表的第一帧构建一个sprite,然后将其放置到屏幕正中间。最后调用walkingBear方法,让熊开始走动。

5) 在initWithSize方法后面添加一个新的方法walkingBear

-(void)walkingBear
{
    //This is our general runAction method to make our bear walk.
    [_bear runAction:[SKAction repeatActionForever:
                      [SKAction animateWithTextures:_bearWalkingFrames
                                       timePerFrame:0.1f
                                             resize:NO
                                            restore:YES]] withKey:@"walkingInPlaceBear"];
    return;
}

上面的这个action会以0.1秒的间隔开始播放各帧。如果你的代码再次调用这个方法使动画重新开始的话,walkingInPlaceBear这个key会强制移除动画。这对于确保动画不相互干扰非常重要。withKey参数还提供了一个钟方法对动画进行检查,来判断其是否通过名称运行的。

这个action是永久重复的,内部的actionan imateWithTextures会按顺序动画播放帧列表中的图片。

完工!

现在编译并运行程序,一切正常的话,会在屏幕中看到一个会动的熊,如下图所示:

11

改变动画运动的方向

看起来不错哦!下面我们就来看看如何通过触摸屏幕上的点来控制熊的运动方向。在MyScene.m文件中做如下改动:

// Add these new methods
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{

    CGPoint location = [[touches anyObject] locationInNode:self];
    CGFloat multiplierForDirection;

    if (location.x <= CGRectGetMidX(self.frame)) {
        //walk left
        multiplierForDirection = 1;
    } else {
        //walk right
        multiplierForDirection = -1;
    }

    _bear.xScale = fabs(_bear.xScale) * multiplierForDirection;
    [self walkingBear];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
}

上面的代码会根据tap的位置,让touchesEnded方法判断tap处于屏幕正中间的左边还是右边。通过该方法,决定熊的朝向。熊的方向是通过Sprite Kit来改变的(通过负值乘以xScale就可以让熊朝向左边。)

编译并运行程序,一切正常的话,当你在屏幕上点击时,会发现熊的朝向发生了改变。

12

在屏幕上让熊移动

下面我们让熊可以移动到屏幕的各个位置。

MyScene.m文件中做如下改动:

// Comment out the call to start the bear walking
//[self walkingBear];

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    //Stuff from below!
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
}

//add this method
-(void)bearMoveEnded
{
    [_bear removeAllActions];
}

如上所示,移除了touchesEnded方法中的所有代码。下面我们一步一步的往里面添加代码。

当想要停止动画的时可以调用方法bearMoveEnded

下面就从touchesEnded方法开始吧:

1) 确定触摸的位置并定义一个变量代表熊的朝向

CGPoint location = [[touches anyObject] locationInNode:self];
CGFloat multiplierForDirection;

如上代码,利用常见的一个方法将触摸的位置转换为node坐标系中的位置。

2) 设置速度

CGSize screenSize = self.frame.size;
float bearVelocity = screenSize.width / 3.0;

如上,定义了熊移动的速度。可知熊从移动长度为屏幕宽度这么长时,需要3秒钟。由于不同设备的屏幕宽度可能会不同,所以在这里使用了self.frame.size,所以熊的速度应该是屏幕宽度/3秒。

3) 计算出熊在X和Y轴中移动的量

CGPoint moveDifference = CGPointMake(location.x - _bear.position.x, location.y - _bear.position.y);

通过简单的利用触摸位置减去熊的位置,计算出熊在X和Y轴上应该移动的距离。

4) 计算出实际的移动距离

float distanceToMove = sqrtf(moveDifference.x * moveDifference.x + moveDifference.y * moveDifference.y);

上面的代码是计算出熊实际移动的直线距离(一个直角三角形的斜边:熊当前的位置和触摸位置)。关于游戏中涉及到的数学知识可以看看这本书:Trigonometry for Game Programming

5) 计算出移动实际距离所需要花费的时间

float moveDuration = distanceToMove / bearVelocity;

通过移动的实际距离除以移动速度计算出需要花费的时间。

6) 需要的话对动画做翻转(Flip)处理

if (moveDifference.x < 0) {
    multiplierForDirection = 1;
} else {
    multiplierForDirection = -1;
}
_bear.xScale = fabs(_bear.xScale) * multiplierForDirection;

上面的代码:确定熊往左还是往右移动。如果小于0,则往左移动,否则往右移动。

在这里,你的第一直觉可能是利用图片编辑器创建并使用对应另一个方向的图片。不过,之前我们学习过了如果通过乘法来改变sprite的xScale,进而改变sprite的方向。

7) 运行一些action

if ([_bear actionForKey:@"bearMoving"]) {
    //stop just the moving to a new location, but leave the walking legs movement running
    [_bear removeActionForKey:@"bearMoving"];
} //1

if (![_bear actionForKey:@"walkingInPlaceBear"]) {
    //if legs are not moving go ahead and start them
    [self walkingBear];  //start the bear walking
} //2

SKAction *moveAction = [SKAction moveTo:location duration:moveDuration];  //3
SKAction *doneAction = [SKAction runBlock:(dispatch_block_t)^() {
        NSLog(@"Animation Completed");
        [self bearMoveEnded];
}]; //4

SKAction *moveActionWithDone = [SKAction sequence:@[moveAction,doneAction]]; //5

[_bear runAction:moveActionWithDone withKey:@"bearMoving"]; //6

1. 停止已有的移动action(因为要准备告诉熊移动到别的地方)。这里使用的key可以开始和停止以此命名的动画的运行。

2. 如果熊还没有准备移动腿,那么就让熊的腿开始移动,否则它该如何走到新的位置呢。这里使用了我们之前使用过的方法,这个方法可以确保不启动一个已经运行着的动画(以key命名)。

3. 创建一个移动action,并制定移动到何处,以及需要花费的时间。

4. 创建一个done action,当熊到达目的地后,该action利用一个block调用一个方法来停止动画。

5. 将上面的两个action设置为一个顺序action链,就是说让这两个action按照先后顺序运行(第一个运行完之后,再运行第二个)。

6. 让熊开始运行action,并制定一个key为:”bearMoving”。记住,这里的key用来判断熊是否需要移动到新的位置。

注意:Sprite Kit支持两种action:sequentialgroupedsequentialaction表示action按照顺序执行。如果想要action同时运行,那么就使用grouped

当然,也可以在sequential action中包含grouped action,反之亦然。更多相关内容请看Sprite Kit Programming Guide中的Adding Actions to Nodes章节。

当动画执行完毕之后,bearMoveEnded会被调用,所有的动画都将被停止,并等待下一个移动方位。

搞定了!

现在编译并运行程序,一切正常的话,那么当点击屏幕时,熊会跟着移动。

13

何去何从?

这里是本文涉及到的工程示例

下面这些想法可以让动画更加有趣:

尝试在方法walkingBear中增加或者减慢运动的速度,看看效果

试着在屏幕上同时显示多个熊。提示:创建多个sprite node,并赋予action。

至此,你应该已经知道如何使用动画了。

如果你希望学习更多相关Sprite Kit内容,可以看看这本书:iOS Games by Tutorials。本书会告诉你需要知道的内容——从物理特性,到磁贴地图,以及粒子系统,甚至是制作自己的关卡编辑器。

Sprite Kit教程:初窥门径

注:本文转自破船《Sprite Kit教程:初学者》

在iOS 7中内置了一个新的Sprite Kit框架,该框架主要用来开发2D游戏。目前已经支持的内容包括:精灵、很酷的特效(例如视频、滤镜和遮罩),并且还集成了物理库等许多东西。

iOS 7中附带了一个非常棒的Sprite Kit示例工程,名字叫做Adventure。不过这个示例工程稍微有点复杂,不太适合初学者。本文的目的就是做一个关于Sprite Kit使用的初级教程。

通过本文,你可以从头到尾的学习到如何为你的iPhone创建一个简单又有趣的2D游戏。如果你看过我们之前的教程:Simple Cocos2D game教程,你会发现非常的相似。

在开始之前,请确保已经安装了最新版本的Xcode(5.X),里面支持Sprite Kit以及iOS 7。

Sprite Kit的优点和缺点

首先,我想指出在iOS中开发2D游戏Sprite Kit并不是唯一的选择,下面我们先来看看Sprite Kit的一些优点和缺点。

Sprite Kit的优点:

1、它是内置到iOS中的,因此并不需要下载额外的库或者其它一些外部依赖。并且它是由苹果开发的,所以对于它的支持和更新我们可以放心。

2、它内置的工具支持纹理和粒子。

3、它可以让你做一些其它框架很难做到的事情,例如把视频当做精灵一样处理,或者使用很酷的图形效果和遮罩。

Sprite Kit的缺点:

1、如果使用了Sprite Kit,那么你将被iOS生态圈所绑架,导致你无法很容易对你开发的游戏移植到Android上。

2、Sprite Kit现在还处于初始阶段,此时提供的功能还没有别的框架丰富,例如Cocos2D。最缺的东西应该是暂不支持写自定义的OpenGL代码。

Sprite Kit vs Cocos2D-iPhone vs Cocos2D-X vs Unity

此时,你可能在想“我该选择使用哪个2D框架呢?”

这取决于你的实际情况,下面是我的一些想法:

1、如果你是一个初学者,并且只关注于iOS,那么就使用内置的Sprite Kit吧,它非常容易学习,并且完全可以把工作做好。

2、如果需要写自己的OpenGL代码,那么还是使用Cocos2D,或者其它框架吧,目前Sprite Kit并不支持自定义OpenGL代码。

3、如果要进行跨平台开发,那么选择Cocos2D-X或者Unity。Cocos2D-X非常出色,可以用它来构建2D游戏。Unity则更加的灵活(例如,如果有需要的话,你可以在游戏中添加一些3D效果)。

看到这里,如果你还想要继续了解Sprite Kit的话,请继续往下读吧。

Hello,Sprite Kit!

下面我们就开始利用Xcode 5内置的Sprite Kit模板来构建一个简单的Hello World工程吧。

启动Xcode,选择File\New\Project,接着选中iOS\Application\SpriteKit Game模板,然后单击Next

7

输入Product Name为SpriteKitSimpleGame,Devices选择iPhone,接着单击Next

8

选择工程保存的路径,然后点击Create。然后点击Xcode中的播放按钮来运行工程。稍等片刻,可以看到如下运行画面:

9

跟Cocos2D类似,Sprite Kit也是按照场景(scenes)来构建的,这相当于游戏中的”levels”和”screens”。例如,你的游戏中可能会有一个主游戏区的场景,以及一个世界地图的一个场景。

如果你观察一下创建好的工程,会发现SpriteKit Game模板已经创建好了一个默认的场景MyScene。现在打开MyScene.m,里面已经包含了一些代码,其中将一个lable放到屏幕中,并且添加了:当tap屏幕时,会在屏幕上新增一个旋转的飞船。

在本教程中,我们主要在MyScene中写代码。不过在开始写代码之前,需要进行一个小调整——让程序以横屏的方式运行。

横屏显示

首先,在Project Navigator中单击SpriteKitSimpleGame工程以打开target设置,选中SpriteKitSimpleGame target。然后在Deployment Info中,不要勾选Portrait,只选中LandscapeLandscape Right,如下所示:

11

编译并运行工程,会看到如下运行画面:

12

下面我们试着添加一个忍者(ninja)。

首先,下载此工程的资源文件,并将其拖拽到Xcode工程中。确保勾选上“Copy items into destination group’s folder (if needed)”SpriteKitSimpleGame target

接着,打开MyScene.m,并用下面的内容替换之:

#import "MyScene.h"

// 1
@interface MyScene ()
@property (nonatomic) SKSpriteNode * player;
@end

@implementation MyScene

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {

        // 2
        NSLog(@"Size: %@", NSStringFromCGSize(size));

        // 3
        self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];

        // 4
        self.player = [SKSpriteNode spriteNodeWithImageNamed:@"player"];
        self.player.position = CGPointMake(100, 100);
        [self addChild:self.player];

    }
    return self;
}

@end

我们来看看上面的代码。

1. 为了给player(例如忍者)声明一个私有变量,在这里创建了一个私有的interface,之后可以把这个私有变量添加到场景中。

2. 在这里打印出了场景的size,至于什么原因很快你就会看到了。

3. 在Sprite Kit中设置一个场景的背景色非常简单——只需要设置backgroundColor属性,在这里将其设置位白色。

4. 在Sprite Kit场景中添加一个精灵同样非常简单,只需要使用spriteNodeWithImageNamed方法,并把一副图片的名称传递进去就可以创建一个精灵。接着设置一下精灵的位置,然后调用addChild方法将该精灵添加到场景中。在代码中将忍者的位置设置为(100, 100),该位置是从屏幕的左下角到右上角计算的。

编译并运行,看看效果如何…

13

呀!屏幕是白色的,并没有看到忍者。这是为什么呢?你可能在想设计之初就是这样的,实际上这里有一个问题。

如果你观察一下控制台输出的内容,会看到如下内容

SpriteKitSimpleGame[3139:907] Size: {320, 568}

scene认为自己的宽度是320,高度则是568——实际上刚好相反!

我们来看看具体发生了什么:定位到ViewController.mviewDidLoad方法:

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Configure the view.
    SKView * skView = (SKView *)self.view;
    skView.showsFPS = YES;
    skView.showsNodeCount = YES;

    // Create and configure the scene.
    SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
    scene.scaleMode = SKSceneScaleModeAspectFill;

    // Present the scene.
    [skView presentScene:scene];
}

上面的代码中利用view的边界size创建了场景。不过请注意,当viewDidLoad被调用的时候,view还没被添加到view层级结构中,因此它还没有响应出布局的改变。所以view的边界可能还不正确,进而在viewDidLoad中并不是开启场景的最佳时机。

提醒:要想了解更多相关内容,请看由Rob Mayoff带来的最佳解释。

解决方法就是将开启场景代码的过程再靠后一点。用下面的代码替换viewDidLoad:

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];

    // Configure the view.
    SKView * skView = (SKView *)self.view;
    if (!skView.scene) {
      skView.showsFPS = YES;
      skView.showsNodeCount = YES;

      // Create and configure the scene.
      SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
      scene.scaleMode = SKSceneScaleModeAspectFill;

      // Present the scene.
      [skView presentScene:scene];
    }
}

编译并运行程序,可以看到,忍者已经显示在屏幕中了!

14

如上图所示,可以看到坐标系已经正确了,如果想要把忍者的位置设置为其中间靠左,那么在MyScene.m中用下面的代码来替换设置忍者位置相关的代码:

self.player.position = CGPointMake(self.player.size.width/2, self.frame.size.height/2);

移动怪兽

接下来,我们希望在场景中添加一些怪兽,让忍者进行攻击。为了让游戏更有趣一点,希望怪兽能够移动——否则没有太大的挑战!OK,我们就在屏幕的右边,离屏的方式创建怪兽,并给怪兽设置一个动作:告诉它们往左边移动。

将下面这个方法添加到MyScene.m中:

- (void)addMonster {

    // Create sprite
    SKSpriteNode * monster = [SKSpriteNode spriteNodeWithImageNamed:@"monster"];

    // Determine where to spawn the monster along the Y axis
    int minY = monster.size.height / 2;
    int maxY = self.frame.size.height - monster.size.height / 2;
    int rangeY = maxY - minY;
    int actualY = (arc4random() % rangeY) + minY;

    // Create the monster slightly off-screen along the right edge,
    // and along a random position along the Y axis as calculated above
    monster.position = CGPointMake(self.frame.size.width + monster.size.width/2, actualY);
    [self addChild:monster];

    // Determine speed of the monster
    int minDuration = 2.0;
    int maxDuration = 4.0;
    int rangeDuration = maxDuration - minDuration;
    int actualDuration = (arc4random() % rangeDuration) + minDuration;

    // Create the actions
    SKAction * actionMove = [SKAction moveTo:CGPointMake(-monster.size.width/2, actualY) duration:actualDuration];
    SKAction * actionMoveDone = [SKAction removeFromParent];
    [monster runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];

}

在上面,我尽量让代码看起来容易理解。首先是通过一个简单的计算,确定怪兽出现的位置,并将该位置设置给怪兽,然后将其添加到场景中。

接着是添加动作(actions)。跟Cocos2D一样,Sprite Kit同样提供了很多方便的内置动作,例如移动动作、旋转动作、淡入淡出动作、动画动作等。在这里我们只需要在怪兽上使用3中动作即可:

1. moveTo:duration:使用这个动作可以把怪兽从屏幕外边移动到左边。移动过程中,我们可以指定移动持续的时间,上面的代码中,指定为2-4秒之间的一个随机数。

2. removeFromParent:在Sprite Kit中,可以使用该方法,方便的将某个node从parent中移除,能有效的从场景中删除某个对象。此处,将不再需要显示的怪兽从场景中移除。这个功能非常的重要,否则当有源源不断的怪兽出现在场景中时,会耗尽设备的所有资源。

3. sequence:sequence动作可以一次性就把一系列动作串联起来按照一定顺序执行。通过该方法我们就能让moveTo:方法先执行,当完成之后,在执行removeFromParent:动作。

最后,我们需要做的事情就是调用上面这个方法addMonster,以实际的创建出怪兽!为了更加好玩,下面我们来让怪兽随着时间持续的出现在屏幕中。

在Sprite Kit中,并不能像Cocos2D一样,可以配置每隔X秒就回调一下update方法。同样也不支持将从上次更新到目前为止的时间差传入方法中。(非常令人吃惊!)。

不过,我们可以通过一小段代码来仿造这种行为。首先在MyScene.m的private interface中添加如下属性:

@property (nonatomic) NSTimeInterval lastSpawnTimeInterval;
@property (nonatomic) NSTimeInterval lastUpdateTimeInterval;

通过lastSpawnTimeInterval可以记录着最近出现怪兽时的时间,而lastUpdateTimeInterval可以记录着上次更新时的时间。

接着,我们写一个方法,该方法在画面每一帧更新的时候都会被调用。记住,该方法不会被自动调用——需要另外写一个方法来调用它:

- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)timeSinceLast {

    self.lastSpawnTimeInterval += timeSinceLast;
    if (self.lastSpawnTimeInterval > 1) {
        self.lastSpawnTimeInterval = 0;
        [self addMonster];
    }
}

上面的代码中简单的将上次更新(update调用)的时间追加到self.lastSpawnTimeInterval中。一旦该时间大于1秒,就在场景中新增一个怪兽,并将lastSpawnTimeInterval重置。

最后,添加如下方法来调用上面的方法:

- (void)update:(NSTimeInterval)currentTime {
    // Handle time delta.
    // If we drop below 60fps, we still want everything to move the same distance.
    CFTimeInterval timeSinceLast = currentTime - self.lastUpdateTimeInterval;
    self.lastUpdateTimeInterval = currentTime;
    if (timeSinceLast > 1) { // more than a second since last update
        timeSinceLast = 1.0 / 60.0;
        self.lastUpdateTimeInterval = currentTime;
    }

    [self updateWithTimeSinceLastUpdate:timeSinceLast];

}

Sprite Kit在显示每帧时都会调用上面的update:方法。

上面的代码其实是来自苹果提供的Adventure示例中。该方法会传入当前的时间,在其中,会做一些计算,以确定出上一帧更新的时间。注意,在代码中做了一些合理性的检查,以避免从上一帧更新到现在已经过去了大量时间,并且将间隔重置为1/60秒,避免出现奇怪的行为。

现在编译并运行程序,可以看到许多怪兽从左边移动到屏幕右边并消失。

15

发射炮弹

现在我们开始给忍者添加一些动作,首先从发射炮弹开始!实际上有多种方法来实现炮弹的发射,不过,在这里要实现的方法时当用户tap屏幕时,从忍者的方位到tap的方位发射一颗炮弹。

由于本文是针对初级开发者,所以在这里我使用moveTo:动作来实现,不过这需要做一点点的数学运算——因为moveTo:方法需要指定炮弹的目的地,但是又不能直接使用touch point(因为touch point仅仅代表需要发射的方向)。实际上我们需要让炮弹穿过touch point,直到炮弹在屏幕中消失。

如下图,演示了上面的相关内容:

16

如图所示,我们可以通过origin point到touch point得到一个小的三角形。我们要做的就是根据这个小三角形的比例创建出一个大的三角形——而你知道你想要的一个端点是离开屏幕的地方。

为了做这个计算,如果有一些基本的矢量方法可供调用(例如矢量的加减法),那么会非常有帮助,但很不幸的时Sprite Kit并没有提供相关方法,所以,我们必须自己实现。

不过很幸运的时这非常容易实现。将下面的方法添加到文件的顶部(implementation之前):

static inline CGPoint rwAdd(CGPoint a, CGPoint b) {
    return CGPointMake(a.x + b.x, a.y + b.y);
}

static inline CGPoint rwSub(CGPoint a, CGPoint b) {
    return CGPointMake(a.x - b.x, a.y - b.y);
}

static inline CGPoint rwMult(CGPoint a, float b) {
    return CGPointMake(a.x * b, a.y * b);
}

static inline float rwLength(CGPoint a) {
    return sqrtf(a.x * a.x + a.y * a.y);
}

// Makes a vector have a length of 1
static inline CGPoint rwNormalize(CGPoint a) {
    float length = rwLength(a);
    return CGPointMake(a.x / length, a.y / length);
}

上面实现了一些标准的矢量函数。如果你看得不是太明白,请看这里关于矢量方法的解释

接着,在文件中添加一个新的方法:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

    // 1 - Choose one of the touches to work with
    UITouch * touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];

    // 2 - Set up initial location of projectile
    SKSpriteNode * projectile = [SKSpriteNode spriteNodeWithImageNamed:@"projectile"];
    projectile.position = self.player.position;

    // 3- Determine offset of location to projectile
    CGPoint offset = rwSub(location, projectile.position);

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

    // 5 - OK to add now - we've double checked position
    [self addChild:projectile];

    // 6 - Get the direction of where to shoot
    CGPoint direction = rwNormalize(offset);

    // 7 - Make it shoot far enough to be guaranteed off screen
    CGPoint shootAmount = rwMult(direction, 1000);

    // 8 - Add the shoot amount to the current position       
    CGPoint realDest = rwAdd(shootAmount, projectile.position);

    // 9 - Create the actions
    float velocity = 480.0/1.0;
    float realMoveDuration = self.size.width / velocity;
    SKAction * actionMove = [SKAction moveTo:realDest duration:realMoveDuration];
    SKAction * actionMoveDone = [SKAction removeFromParent];
    [projectile runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];

}

上面的代码中做了很多事情,我们来详细看看。

1. SpriteKit为我们做了很棒的一件事情就是它提供了一个UITouch的category,该category中有locationInNode:和previousLocationInNode:方法。这两个方法可以帮助我们定位到在SKNode内部坐标系中touch的坐标位置。这样一来,我们就可以寻得到在场景坐标系中touch的位置。

2. 然后创建一个炮弹,并将其放置到忍者的地方,以当做其开始位置。注意,现在还没有将其添加到场景中,因为还需要先做一个合理性的检查——该游戏不允许忍者向后发射。

3. 接着利用touch位置减去炮弹的当前位置,这样就能获得一个从当前位置到touch位置的矢量。

4. 如果X值小于0,就意味着忍者将要向后发射,由于在这里的游戏中是不允许的(真实中的忍者是不回头的!),所以就return。

5. 否则,将可以将炮弹添加到场景中。

6. 调用方法rwNormalize,将offset转换为一个单位矢量(长度为1)。这样做可以让在相同方向上,根据确定的长度来构建一个矢量更加容易(因为1 * length = length)。

7. 在单位矢量的方向上乘以1000。为什么是1000呢?因为着肯定足够超过屏幕边缘了 :]

8. 将上一步中计算得到的位置与炮弹的位置相加,以获得炮弹最终结束的位置。

9. 最后,参照之前构建怪物时的方法,创建moveTo:和removeFromParent:两个actions。

编译并运行程序,现在忍者可以发射炮弹了!

17

碰撞检测和物理特性: 概述

至此我们已经可以让炮弹任意的发射了——现在我们要让忍者利用炮弹来消灭这些怪物。下面就添加一些代码来给炮弹与怪物相交做检测。

Sprite Kit内置了一个物理引擎,这非常的棒!该物理引擎不仅可以模拟现实运动,还能进行碰撞检测。

下面我们就在游戏中使用Sprite Kit的物理引擎来检测炮弹与怪物的碰撞。首先,我们来看看需要做些神马事情:

1. 物理世界的配置。物理世界是一个模拟的空间,用来进行物理计算。默认情况下,在场景(scene)中已经创建好了一个,我们可以对其做一些属性配置,例如重力感应。

2. 为精灵(sprite)创建对应的物体(physics bodies)。在Sprite Kit中,为了碰撞检测,我们可以为每个精灵创建一个相应的形状,并设置一些属性,这就称为物体(physics body)。注意:图文的形状不一定跟精灵的外形一模一样。一般情况,这个形状都是简单的、大概的(而不用精确到像素级别)——毕竟这已经足以够大多数游戏使用了。

3. 将精灵分类。在物体(physics body)上可以设置的一个属性是category,该属性是一个位掩码(bitmask)。通过该属性可以将精灵分类。在本文的游戏中,有两个类别——一类是炮弹,另一类则是怪物。设置之后,当两种物体相互碰撞时,就可以很容易的通过类别对精灵做出相应的处理。

设置一个contact(触点) delegate。还记得上面提到的物理世界吗?我们可以在物理世界上设置一个contact delegate,通过该delegate,当两个物体碰撞时,可以收到通知。收到通知后,我们可以通过代码检查物体的类别,如果是怪物和炮弹,那么就做出相应的动作!

上面大致介绍了一下游戏策略,下面就来看看如何实现!

碰撞检测和物理特性: 实现

首先在MyScene.m文件顶部添加如下两个常量:

static const uint32_t projectileCategory     =  0x1 << 0;
static const uint32_t monsterCategory        =  0x1 << 1;

上面设置了两个类别,记住需要用位(bit)的方式表达——一个用于炮弹,另一个则是怪物。

注意:看到上面的语法你可能感到奇怪。在Sprite Kit中category是一个32位整数,当做一个位掩码(bitmask)。这种表达方法比较奇特:在一个32位整数中的每一位表示一种类别(因此最多也就只能有32类)。在这里,第一位表示炮弹,下一位表示怪兽。

接着,在initWithSize中,将下面的代码添加到位置:添加player到场景涉及代码的后面。

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

上面的代码将物理世界的重力感应设置为0,并将场景设置位物理世界的代理(当有两个物体碰撞时,会受到通知)。

addMonster方法中,将如下代码添加创建怪兽相关代码后面:

monster.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:monster.size]; // 1
monster.physicsBody.dynamic = YES; // 2
monster.physicsBody.categoryBitMask = monsterCategory; // 3
monster.physicsBody.contactTestBitMask = projectileCategory; // 4
monster.physicsBody.collisionBitMask = 0; // 5

来看看上面代码意思:

1. 为怪兽创建一个对应的物体。此处,物体被定义为一个与怪兽相同尺寸的矩形(这样与怪兽形状比较接近)。

2. 将怪兽设置位dynamic。这意味着物理引擎将不再控制这个怪兽的运动——我们自己已经写好相关运动的代码了。

3. 将categoryBitMask设置为之前定义好的monsterCategory

4. contactTestBitMask表示与什么类型对象碰撞时,应该通知contact代理。在这里选择炮弹类型。

5. collisionBitMask表示物理引擎需要处理的碰撞事件。在此处我们不希望炮弹和怪物被相互弹开——所以再次将其设置为0。

接着在touchesEnded:withEvent:方法中设置炮弹位置的代码后面添加如下代码。

projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:projectile.size.width/2];
projectile.physicsBody.dynamic = YES;
projectile.physicsBody.categoryBitMask = projectileCategory;
projectile.physicsBody.contactTestBitMask = monsterCategory;
projectile.physicsBody.collisionBitMask = 0;
projectile.physicsBody.usesPreciseCollisionDetection = YES;

在上面的代码中跟之前的类似,只不过有些不同,我们来看看: 1. 为了更好的效果,炮弹的形状是圆形的。 2. usesPreciseCollisionDetection属性设置为YES。这对于快速移动的物体非常重要(例如炮弹),如果不这样设置的话,有可能快速移动的两个物体会直接相互穿过去,而不会检测到碰撞的发生。

接着,添加如下方法,当炮弹与怪物发生碰撞时,会被调用。注意这个方法是不会被自动调用,稍后会看到我们如何调用它。

- (void)projectile:(SKSpriteNode *)projectile didCollideWithMonster:(SKSpriteNode *)monster {
    NSLog(@"Hit");
    [projectile removeFromParent];
    [monster removeFromParent];
}

当怪物和炮弹发生碰撞,上面的代码会将他们从场景中移除。很简单吧!

下面该实现contact delegate方法了。将如下方法添加到文件中:

- (void)didBeginContact:(SKPhysicsContact *)contact
{
    // 1
    SKPhysicsBody *firstBody, *secondBody;

    if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
    {
        firstBody = contact.bodyA;
        secondBody = contact.bodyB;
    }
    else
    {
        firstBody = contact.bodyB;
        secondBody = contact.bodyA;
    }

    // 2
    if ((firstBody.categoryBitMask & projectileCategory) != 0 &&
        (secondBody.categoryBitMask & monsterCategory) != 0)
    {
        [self projectile:(SKSpriteNode *) firstBody.node didCollideWithMonster:(SKSpriteNode *) secondBody.node];
    }
}

还记得之前给物理世界设置的contactDelegate吗?当两个物体发生碰撞之后,就会调用上面的方法。

在上面的方法中,可以分为两部分来理解:

1. 该方法会传递给你发生碰撞的两个物体,但是并不一定符合特定的顺序(如炮弹在前,或者炮弹在后)。所以这里的代码是通过物体的category bit mask来对其进行排序,以便后续做出正确的判断。注意,这里的代码来自苹果提供的Adventure示例。

2. 最后,检测一下这两个碰撞的物体是否就是炮弹和怪物,如果是的话就调用之前的方法。

最后一步,为了编译器没有警告,确保private interface 中添加一下SKPhysicsContactDelegate

@interface MyScene () 

现在编译并运行程序,可以发现,当炮弹与怪物接触时,他们就会消失!

收尾

现在,本文的游戏快完成了。接下来我们就来为游戏添加音效和音乐,以及一些简单的游戏逻辑吧。

苹果提供的Sprite Kit里面并没有音频引擎(Cocos2D中是有的),不过我们可以通过action来播放音效,并且可以使用AVFoundation播放后台音乐。

在工程中我已经准备好了一些音效和很酷的后台音乐,在本文开头已经将resources添加到工程中了,现在只需要播放它们即可!

首先在ViewController.m文件顶部添加如下import:

@import AVFoundation;

上面的语法是iOS 7中新的modules功能 —— 只需要使用新的关键字@import,就可以框架的头文件和库文件添加到工程中,这功能非常方便。要了解更多相关内容,请看到iOS 7 by Tutorials中的第十章内容中的:What’s New with Objective-C and Foundation。

接着添加一个新的属性和private interface:

@interface ViewController ()
@property (nonatomic) AVAudioPlayer * backgroundMusicPlayer;
@end

接着将下面的代码添加到viewWillLayoutSubviews方法中(在[super viewWillLayoutSubviews]后面):

NSError *error;
NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
self.backgroundMusicPlayer.numberOfLoops = -1;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];

上面的代码会开始无限循环的播放后台音乐。

下面我们来看看如何处理音效。切换到MyScene.m文件中,并将下面这行代码添加到touchesEnded:withEvent:方法的顶部:

[self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]];

如上,一行代码就可以播放音效了,很简单吧!

下面,我们创建一个新的创建和layer,用来显示你赢了(You Win)你输了(You Lose)。用模板iOS\Cocoa Touch\Objective-C class创建一个新的文件,将其命名为GameOverScene,并让其继承自SKScene,然后点击NextCreate

接着用如下代码替换GameOverScene.h中的内容:

#import 

@interface GameOverScene : SKScene

-(id)initWithSize:(CGSize)size won:(BOOL)won;

@end

在上面的代码中导入了Sprite Kit头文件,并声明了一个特定的初始化方法,该方法的第一个参数用来定位显示的位置,第二个参数won用来判断用户是否赢了。

接着用下面的代码替换GameOverLayer.m中的内容:

#import "GameOverScene.h"
#import "MyScene.h"

@implementation GameOverScene

-(id)initWithSize:(CGSize)size won:(BOOL)won {
    if (self = [super initWithSize:size]) {

        // 1
        self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];

        // 2
        NSString * message;
        if (won) {
            message = @"You Won!";
        } else {
            message = @"You Lose :[";
        }

        // 3
        SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        label.text = message;
        label.fontSize = 40;
        label.fontColor = [SKColor blackColor];
        label.position = CGPointMake(self.size.width/2, self.size.height/2);
        [self addChild:label];

        // 4
        [self runAction:
            [SKAction sequence:@[
                [SKAction waitForDuration:3.0],
                [SKAction runBlock:^{
                    // 5
                    SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
                    SKScene * myScene = [[MyScene alloc] initWithSize:self.size];
                    [self.view presentScene:myScene transition: reveal];
                }]
            ]]
        ];

    }
    return self;
}

@end

上面的代码可以分为4部分内容,我们来分别看看:

1. 将背景色设置为白色(与主场景一样颜色)。

2. 根据won参数,将信息设置为”You Won”或”You Lose”。

3. 这里的代码是利用Sprite Kit将一个文本标签显示到屏幕中。如代码所示,只需要选择一个字体,并设置少量的参数即可,也非常简单。

4. 设置并运行有个有两个action的sequence。为了看起来方便,此处我将它们放到一块(而不是为每个action创建单独的一个变量)。首先是等待3秒,然后是利用runBlockaction来运行一些代码。

5. 演示了在Sprite Kit中如何过渡到新的场景。首先可以选择任意的一种不同的动画过渡效果,用于场景的显示,在这里选择了翻转效果(持续0.5秒)。然后是创建一个想要显示的场景,接着使用self.view的方法presentScene:transition:来显示出场景。

OK,万事俱备,只欠东风了!现在只需要在主场景中,适当的情况下加载game over scene就可以了。

首先,在MyScene.m中导入新的场景:

#import "GameOverScene.h"

然后,在addMonster中,用下面的代码替换最后一行在怪物上运行action的代码:

SKAction * loseAction = [SKAction runBlock:^{
    SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
    SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:NO];
    [self.view presentScene:gameOverScene transition: reveal];
}];
[monster runAction:[SKAction sequence:@[actionMove, loseAction, actionMoveDone]]];

上面创建了一个”lose action”,当怪物离开屏幕时,显示game over场景。

在这里为什么loseAction要在actionMoveDone之前运行呢? 原因在于如果将一个精灵从场景中移除了,那么它就不在处于场景的层次结构中了,也就不会有action了。所以需要过渡到lose场景之后,才能将精灵移除。不过,实际上actionMoveDone永远都不会被调用——因为此时已经过渡到新的场景中了,留在这里就是为了达到教学的目的。

现在,需要处理一下赢了的情况。在private interface中添加一个新的属性:

@property (nonatomic) int monstersDestroyed;

然后将如下代码添加到projectile:didCollideWithMonster:的底部:

self.monstersDestroyed++;
if (self.monstersDestroyed > 30) {
    SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
    SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:YES];
    [self.view presentScene:gameOverScene transition: reveal];
}

编译并运行程序,尝试一下赢了和输了会看到的画面!

何去何从?

至此Sprite Kit教程:初学者结束!这里可以下到完整的代码

希望本文能帮助你学习Sprite Kit,并写出你自己的游戏!

如果你希望学习更多相关Sprite Kit内容,可以看看这本书:iOS Games by Tutorials。本书会告诉你需要知道的内容——从物理特性,到磁贴地图,以及粒子系统,甚至是制作自己的关卡编辑器。

【译】Sprite Kit编程指南(2):使用精灵

精灵是用于创建大部分场景内容的基石,所以在转到其他Sprite Kit节点类之前先了解精灵是有用的。精灵用SKSpriteNode对象表现。一个SKSpriteNode对象,要么绘制成一个由纹理映射(mapped)的矩形,要么绘制成一个彩色无纹理的矩形。纹理精灵更常见,因为它们代表了你把自定义插图引进场景的主要方式。这个自定义插图可能代表你的游戏的人物角色、背景元素甚至是用户界面元素。但基本的策略是一致的。一个美工创建图像,然后你的游戏加载它们作为纹理。然后你用那些纹理创建精灵,并把它们添加到场景中。

 

创建纹理精灵

 

创建一个纹理精灵的最简单方法是让Sprite Kit为你创建的纹理和精灵。你把插图存储在应用程序bundle中,然后在运行时加载它。清单2-1展示了这个代码是多么的简单。

清单2-1  从存储在bundle中的图像创建一个纹理的精灵

SKSpriteNode *spaceship = [SKSpriteNode spriteNodeWithImageNamed:@“rocket.png”];
spaceship.position = CGPointMake(100,100);
[self addChild:spaceship];

当你以这种方式创建一个精灵,你可以免费得到很多的默认行为:

  • 精灵以匹配纹理尺寸的frame来创建。
  • 精灵以它的位置为中心来渲染。精灵的frame属性指定的矩形定义了它所涵盖的面积。
  • 精灵纹理在帧缓冲区(framebuffer)中是半透明的(alpha-blended)。
  • 一个SKTexture对象被创建并附加到精灵上。此纹理对象每当精灵节点在场景中时自动加载纹理数据,它是可见的,而且对渲染场景是必要的。稍后,如果精灵从场景中移除或不再可见,如果需要那些内存用于其他用途,Sprite Kit可以删除纹理数据。这种自动内存管理简化但并不能消除在管理你游戏中的美术资产(art assets)方面你需要做的工作。

默认的行为给你一个有用的基础来创建一个基于精灵的游戏。你已经懂得了足够的知识去添加插图到你的游戏,创建精灵,并运行这些精灵的动作来做一些有趣的事情。随着精灵屏幕内外移动,Sprite Kit尽可能有效地管理纹理和绘制动画的帧。如果这对你已经足够,就花点时间去探索你能对精灵做些什么。或者继续阅读SKSpriteNode类得到更深入的理解。一路上,你将获得其功能以及如何与美工和设计师交流这些功能的深入理解。并且你会学到更高级的使用纹理的方式以及如何提高基于纹理的精灵的性能。

 

定制纹理精灵

 

你可以使用精灵的每个属性独立配置四个不同的渲染阶段:

通常情况下,配置精灵执行定位、调整尺寸、着色、混合这四个步骤要根据用于创建精灵纹理的插图。这意味着你很少脱离插图设置属性值。你与美工合作以确保你的游戏配置精灵与插图匹配。

下面是一些也行你可以遵循的策略:

  • 在你的项目中用硬编码值创建精灵。这是最快但在长期最不理想的方法,因为这意味着每当美术资产变动时必须更改代码。
  • 使用Sprite Kit创建自己的工具,让你可以微调精灵的属性值。一旦你有一个你想要的方式配置的精灵,保存精灵到归档中。你的游戏在运行时使用归档创建精灵。
  • 在存储在你的应用程序bundle的属性列表中存储配置数据。当精灵加载时,加载属性列表并使用它的值来配置精灵。这允许美工提供正确的各个值并在不改变代码的情况下进行更改。

使用锚点移动精灵的frame

默认情况下,精灵的frame及其纹理的中心定位在精灵的位置上。然而,你可能想纹理的不同部分出现在节点的位置。经常要作出这样的决定因为纹理描绘的游戏元素不是纹理图像的中心。

精灵节点的的anchorPoint属性决定frame的哪一点定位在精灵的位置上。锚点在单位坐标系(unit coordinate system)中指定,如图2-1所示。单位坐标系的原点位于frame的左下角,而(1,1)位于frame的右上角。精灵的锚点默认为(0.5,0.5,对应于frame的中心。

2-1   单位坐标系

虽然你想要移动frame,你这样做是因为你想纹理的相应部分处于位置的中点。图2-2展示了一对纹理图像。在第一个图像中,默认的锚点在纹理位置的中心。第二个相反地,选择了图像的顶部一个点。你可以看到,当精灵旋转时纹理图像会围绕这一点旋转。

2-2   改变精灵的锚点

清单2-2展示了如何将火箭的锚点放在前锥体处。通常,你在精灵初始化时设置锚点,因为它与插图对应。然而,你可以在任何时候设置此属性。frame会被立即更新,并且屏幕上的精灵会在场景下一次渲染时更新。

清单2-2   设定精灵的锚点

rocket.anchorPoint:= CGPointMake(0.5,1.0);

调整精灵的尺寸

精灵的frame属性的尺寸是由其他三个属性的值决定的:

  • 精灵的size属性指定精灵基准(无缩放)尺寸。当一个精灵使用代码清单2-1初始化时,这个属性的值被初始化为于精灵的纹理的尺寸相等。
  • 然后基准尺寸根据精灵从SKNode类继承来的xScale与yScale属性进行缩放。

例如,如果精灵的基准尺寸是32 × 32像素,而它的xScale的值为1.0且yScale的值为2.0,精灵的frame的尺寸是32 × 64像素。

注:场景中精灵的祖先的缩放值也用于缩放精灵。这将改变精灵的有效尺寸,而不改变它的实际frame的值。请参阅“节点的很多属性适用于其后代”

当精灵的frame大于它的纹理时,纹理被拉伸以覆盖frame。一般情况下,纹理会在整个frame中被均匀地拉伸,如在图2-3中所示。

2-3   纹理位伸以覆盖精灵的frame

然而,有时你想使用精灵构建用户界面元素,如按钮或健康指示器。通常,这些元素包含固定尺寸的元素,如结束端点(end caps),它不应该被拉伸。在这种情况下,使部分的纹理不拉伸,然后拉伸纹理frame剩下的其余部分。

精灵的centerRect属性控制缩放行为,该属性在纹理的单位坐标中指定。默认值是一个覆盖整个纹理的矩形,这就是为什么整个纹理被拉伸到整个frame的原因。如果指定了一个只涵盖了部分的纹理的矩形,你就创建了一个3x3网格。在网格中的每个盒子有其自己的缩放行为:

  • 网格的四个角中的纹理绘制的部分不进行任何缩放。
  • 网格的中心在两个方向缩放。
  • 中间的上下部分仅水平缩放。
  • 中间的左右部分仅垂直缩放。

图2-4展示了一个纹理的特写视图,你可能会用它来绘制用户界面按钮。实际元素是28点×28点。四个角是12×12像素而中心是4×4像素。

2-4   可伸缩的按钮纹理

清单2-3展示了这个按键精灵将如何初始化。centerRect属性根据纹理的中心矩形来计算。

清单2-3   设置中心矩形以调整拉伸行为

SKSpriteNode *button = [SKSpriteNode spriteWithImageNamed:@”stretchable_button.png”];
button.centerRect = CGRectMake(12.0/28.0,12.0/28.0,4.0/28.0,4.0/28.0);
....

图2-5展示了即使在该按钮以不同的尺寸绘制时四个角仍保持不变。

2-5   对不同尺寸的按钮应用按钮纹理

对精灵着色

在把纹理应用到精灵之前,你可以使用colorcolorBlendFactor属性对它着色。默认情况下的颜色混合因子为0.0,这表明纹理未经更改地使用。当你增加这个数字,更多的纹理颜色就会被混合颜色替换。例如,在你的游戏中的怪物受到伤害时,你可能要添加一个红色的色调(tint)给角色。清单2-4展示了如何将色调应用于精灵。

清单2-4   着色精灵的颜色

monsterSprite.color = [SKColor redColor];
monsterSprite.colorBlendFactor = 0.5;

2-6   上色调整纹理的颜色

你也可以使用动作让颜色和颜色混合因子成为动画。清单2-5展示了如何短暂地给精灵调色,然后让它恢复正常。

清单2-5   颜色变化的动画

SKAction *pulseRed= [SKAction sequence:@[
                        [SKAction colorizeWithColor:[SKColor redColor] colorBlendFacto:1.0 duration:0.15],
                        [SKAction waitForDuration:0.1],
                        [SKAction colorizeWithColorBlendFactor:0.0 duration:0.15]] 
[monsterSprite runAction:pulseRed];

混合精灵到帧缓冲区

渲染的最终阶段是把精灵的纹理混合(blend)到其目标帧缓冲区。默认行为使用纹理的alpha值混合纹理与目标像素。但是,当你想添加其他的特效到场景时你可以使用其他混合模式。

你可以使用BlendMode属性来控制精灵的混合行为。例如,附加混合模式在把多个精灵结合在一起时很有用,比如开枪(fire)或发光(lighting)。清单2-6展示了如何使用附加混合改变混合模式。

清单2-6   使用附加混合模式模拟发光

lightFlareSprite.blendMode = SKBlendModeAdd;

 

使用纹理对象

 

虽然Sprite Kit可以在你创建一个精灵时为你创建纹理,但在一些更复杂的游戏中,你需要对纹理有更多的控制。例如,你可能需要做以下任何一项:

  • 多个精灵之间共享一个纹理。
  • 在精灵创建后更改它的纹理。
  • 通过一系列的纹理让精灵动起来。
  • 用不直接存储在应用程序bundle中的数据创建纹理。
  • 把节点树渲染成纹理。例如,你可能要对游戏进行截屏,在玩家完成了关卡(level)后展示给他或她。

你通过直接使用SKTexture对象可以做所有这些事情。纹理对象是可应用于精灵的可复用的图像。你可以创建纹理对象独立于创造精灵。然后,你可以使用纹理对象来创建新的的精灵或改变现有精灵的纹理。它跟Sprite Kit为你创建纹理相似,但你对过程有更多的控制权。

 

从存储在App Bundle的图像创建纹理

清单2-7展示了一个类似清单2-1中展示的例子,但使它用纹理对象。在这种情况下,代码一次创建了多支火箭,全部来自相同的纹理。通常情况下,你会加载一次纹理,并保持对它的强引用,以便每次需要创建一个新的精灵时都可以使用它。

清单2-7bundle中加载纹理

SKTexture *rocketTexture = [SKTexture textureWithImageNamed:@“rocket.png”];
for(int i = 0; i<10; i++)
{
    SKSpriteNode *rocket = [SKSpriteNode spriteNodeWithTexture:rocketTexture];
    rocket.position = [self randomRocketLocation];
    [self addChild:rocket];
}

纹理对象本身只是实际的纹理数据的一个占位符。纹理数据占用(intensive)更多的资源,所以当使用它的精灵在屏幕上且可见时,Sprite Kit只保存它在内存中。

 

使用纹理图册收集相关的美术资产

通常情况下,存储在你的应用程序bundle的美术资产是不相干的图像,却是一起用于相同精灵 的图像的集合。例如,下面是一些常见的美术资产的集合:

  • 一个角色的动画帧
  • 用来创建游戏关卡或者迷宫的地形瓦片(terrain tiles)
  • 用于用户界面控件的图像,如按钮、开关和滑块

如果你把这些逻辑分组看成单独的纹理,Sprite Kit和图形硬件必须运行得更加艰难来渲染场景,而且游戏的性能可能会受到影响。所以,Sprite Kit使用纹理图册把相关的图像收集起来。你指定哪些资产一起使用,然后Xcode会自动构建纹理图册。然后在你的游戏加载纹理图册时,Sprite Kit可以更好地管理性能和内存使用。

 

创建一个纹理图册

Xcode可以自动为你从图像集合构建纹理图册。欲了解更多信息,请参阅

纹理图册帮助



在创建一个纹理图册时,在收集太多的纹理与太少的纹理到图册之间,有一个平衡的做法。如果你使用的项目数量不足,那么纹理之间切换的开销可能仍然太大。如果你把太多的图像放在一个单一的图册中,那更多的纹理数据会存储在内存中。因为Xcode为你构建图册,它可以相对容易地在不同的图册配置之间切换。对你的纹理图册不同的配置做实验,并选择为你提供最佳性能的结合。

 

加载纹理纹理图册

清单2-7中的代码,也可以用来从纹理图册中加载纹理。Sprite Kit首先搜索指定的文件名​​的图像文件,但如果它没有找到,那么它会在内置到应用程序bundle里面任何纹理图册内部搜索。这意味着,在你的游戏中你不必作出任何编码的更改来支持它。此设计还为美工提供了这样的能力,试验新的纹理而不需要重新构建(rebuild)你的游戏。美工把纹理拖放到应用程序bundle中,就可以自动发现它们(覆盖任何之前内置到纹理图册的版本)。一旦美工对纹理满意了,然后你就可以将它们添加到项目中且合并到你的纹理图册中。

如果你想显式使用纹理图册,你可以使用SKTextureAtlas类。首先,你使用图册的名称创建一个纹理图册对象。然后,使用图册中存储的图像文件的名字查看各自的纹理。清单2-8展示了一个这样的例子。它采用了纹理图册装截一个角色的多个动画帧。代码加载这些帧,并将它们存储在一个数组中。

清单2-8   加载散步动画的纹理

SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:@“monster.atlas”];
SKTexture *f1 = [atlas textureNamed:@”master-walk1.png”];
SKTexture *f2 = [atlas textureNamed:@”master-walk2.png”];
SKTexture *f3 = [atlas textureNamed:@”master-walk3.png”];
SKTexture *f4 = [atlas textureNamed:@”master-walk4.png”];
NSArray *monsterWalkTextures = @[f1,f2,f3,f4];

从纹理的小部分创建纹理

如果你已经有一个SKTexture对象,你可以创建新的纹理引用它的一部分。这是非常有效的,因为新的纹理对象引用内存中相同的纹理数据。这个功能跟纹理图册是类似的。通常情况下,如果你的游戏已经有了自己的自定义纹理图册格式,你就可以这样使用。在这种情况下,你负责存储这些存储在自定义纹理图册中的各个图像的坐标。

清单2-9展示了如何提取部分的纹理。矩形的坐标在单位坐标空间中。

代码清单2-9   使用纹理的一部分

SKTexture *bottomLeftTexture = [SKTexture textureWithRect:CGRectMake(0.0,0.0,0.5,0.5) inTexture:cornerTextures];

其他创建纹理的方法

除了从应用程序bundle加载纹理,你还可以从其他来源创建纹理:

  • 使用SKTexture初始化方法通过内存中正确格式化的像素数据、核心图像或对现有的纹理应用一个Core Image滤镜来创建纹理。
  • SKView类的textureFromNode方法可以把一个节点树的内容渲染成纹理。纹理被指定好尺寸,以便它可以包含节点的内容和所有它的可见后代节点。

当你从应用程序bundle中的文件之外的其他来源创建一个纹理时,纹理数据不能被清除,因为Sprite Kit不保留用于生成纹理的原始数据的引用。基于这个原因,你应该有节制地使用这些纹理。一旦不再需要它们,马上移除对它们的强引用。

 

更改精灵的纹理

精灵的texture属性指向它当前的纹理。你可以将此属性更改为指向一个新的纹理。下一次场景渲染一个新的帧时,它会用新的纹理来渲染。每次你更改纹理时,为了与新的纹理一致,你可能还需要更改其他的精灵属性,如size、anchorPoint和centerRect。一般,确保所有的插图都一致会更好,这样相同的值可用于所有的纹理会。也就是说,纹理应该有一个一致的尺寸和锚点定位,让你的游戏并不需要更新纹理以外的其他任何东西。

因为动画是一个非常常见的任务,你可以使用动作让一个精灵的一系列纹理都动起来。清单2-10中的代码展示了如何使用清单2-8创建的动画帧数组让精灵的纹理动起来。

清单2-10   通过一系列的纹理形成动画

SKAction *walkAnimation = [SKAction animateWithTextures:monsterWalkTextures timePerFrame:0.1]
[monster runAction:walkAnimation];
/ /在这里插入其他代码来移动怪物。

Sprite Kit提供了渠道(plumbing),让你活动或改变精灵的纹理。它不利用你的动画系统的特定设计。但是,这意味着你需要决定精灵可能会需要什么样的动画,并设计自己的动画系统来让这些动画在运行时切换。例如,一个怪物可能有步行,战斗,停顿(idle)和死亡的动画序列。你的代码来决定何时在这些序列之间切换。

 

预加载纹理来提高帧率

使用Sprite Kit的一个主要优点是它自动为你执行了大量的内存管理。Sprite Kit从图像文件加载纹理,将这些数据转换成图形硬件可以使用的格式,并将其上传到图形硬件。Sprite Kit很擅长于确定当前帧纹理是否需要渲染。如果纹理不在内存中,它会加载纹理。如果纹理在内存中并且有一段时间没有使用,纹理数据会被丢弃,以便可以加载其他需要的纹理。

如果一次有太多没加载纹理的精灵变为可见,它可能无法在一个单一的动画帧内加载所有这些纹理。纹理加载的延迟可能会导致帧速率突然丢失,这是对用户可见的。Sprite Kit提供了在精灵变为可见之前预加载纹理的选项。因为你非常熟悉你的游戏的设计,你往往更清楚地知道什么时候即将要使用一套新的纹理。例如,在一个滚动的游戏中,当用户在宇宙间移动时,你知道玩家即将进入宇宙的哪一部分。然后你可以在动画的每一帧加载三两个纹理,这样当玩家到达那里时纹理已经在内存中了。清单2-11展示了如何加载纹理。

清单2-11   预加载纹理

[newSectionTexture preload];

预加载代码的正确设计要依赖于你的游戏的引擎。这里有两种可能设计要考虑:

  • 当玩家开始一个新的关卡,预加载这个关卡的所有纹理。游戏被划分成各个关卡,每个关卡能保持所有纹理资产同时在内存中。这保证了所有纹理在游戏开始前就加载好,消除任何纹理加载的延迟。
  • 如果一个游戏需要比可以适合内存更多的纹理,你需要动态地预加载纹理。通常,这意味着当你能确定它很快就需要会才预加载纹理。例如,在赛车游戏中,玩家总是在在同一方向移动,所以你预加载玩家即将看到的部分赛道的纹理。纹理在后台加载,取代赛道中最旧的纹理。在一个允许玩家时刻控制的冒险游戏中,你可能必须临时加载更多的纹理。

创建彩色精灵

 

虽然纹理精灵是使用SKSpriteNode类的最常见的方式,你也可以不用精灵创建精灵节点。类的在精灵缺乏纹理时发生变化:

  • 没有纹理可拉伸,所以centerRect参数被忽略。
  • 没有着色步骤,color属性用作精灵的颜色。
  • 颜色的alpha分量被用来确定精灵如何混合到缓冲区。

其他属性(size、anchorPoint和blendMode)照旧不变。

 

试试这个!

现在你对精灵知道更多了,请尝试以下一些活动:

你可以在

Sprite Tour

示例中找到一些有用的代码。

 

【译】Sprite Kit编程指南(1):深入Sprite Kit

深入Sprite Kit

学习Sprite Kit最好的方法是在实践中观察它。此示例创建一对场景和各自的动画内容。通过这个例子,你将学习使用Sprite Kit内容的一些基础技术,包括:

·      使用场景组织一个基于Sprite Kit的游戏。

·      如何组织节点树来绘制内容。

·      使用动作让场景内容动起来。

·      如何添加交互到场景。

·      场景之间的过渡。

·      在一个场景里模拟物理。

一旦你完成这个项目,你可以用它来试验其他Sprite Kit概念。你可以在这个例子的结尾找到一些建议。

你应该已经熟悉创建iOS应用程序之前通过这个项目工作。欲了解更多信息,请参阅

今天开始开发iOS应用程序的

。大多数Sprite Kit在这个例子中的代码是相同的OS X。

 

让我们开始吧

本次练习需要Xcode 5.0。使用的单一视图的应用程序模板创建一个新的iOS应用程序的Xcode项目。

在创建项目时,请使用以下值:

·      产品名称:SpriteWalkthrough

·      ClassPrefix:Sprite

·      设备:iPad

添加Sprite Kit框架到项目中。

 

创建你的第一个场景

Sprite Kit内容被放置在一个窗口中,就像其他可视化内容那样。Sprite Kit内容由SKView类渲染呈现。SKView对象渲染的内容称为一个场景,它是一个SKScene对象。场景参与响应链,还有其他使它们适合于游戏的功能。

因为Sprite Kit内容由视图对象渲染,你可以在视图层次组合这个视图与其他视图。例如,你可以使用标准的按钮控件,并把它们放在你的Sprite Kit视图上面。或者,你可以添加交互到精灵来实现自己的按钮,选择权在你。在这个例子中,稍候你会看到如何实现场景交互。

 

配置视图控制器来使用Sprite Kit

1.    打开项目的storyboard。它有一个单一的视图控制器(SpriteViewController)。选择视图控制器的view对象并把它的类改成SKView

2.    在视图控制器的实现文件添加一个导入行。

#import <SpriteKit/SpriteKit.h>

3.    实现视图控制器的viewDidLoad方法来配置视图。

 - (void)viewWillAppear:(BOOL)animated
{
    HelloScene *hello = [[HelloScene alloc] initWithSize:CGSizeMake(768,1024)];
    SKView *spriteView =(SKView *)self.view;
    [spriteView presentScene:hello];
}

4.    代码开启了描述场景如何渲染视图的诊断信息。最重要的一块信息是帧率(spriteView.showsFPS),你希望你的游戏尽可能在一个恒定的帧率下运行。其他行展示了在视图中显示了多少个节点,以及使用多少绘画传递来渲染内容(越少越好)的详情。

接下来,添加第一个场景。

 

创建Hello场景 

1.    创建一个名为HelloScene新类并让它作为SKScene类的子类。

2.    在你的视图控制器导入场景的头文件。

#import “HelloScene.h”

3.    修改视图控制器来创建场景,并在视图中呈现场景。

 - (void)viewWillAppear:(BOOL)animated
{
    HelloScene *hello = [[HelloScene alloc] initWithSize:CGSizeMake(768,1024)];
    SKView *spriteView =(SKView *)self.view;
    [spriteView presentScene:hello];
}

现在,构建并运行项目。该应用程序应该启动并显示一个只有诊断信息的空白屏幕。

 

将内容添加到场景

当设计一个基于Sprite Kit的游戏,你要为你的游戏界面各主要大块(chuck)设计不同的场景类。例如,你可以为主菜单创建一个场景而为游戏设置创建另一个单独的场景。在这里,你会遵循类​​似的设计。这第一个场景显示了传统的“Hello World”文本。

大多数情况下,你可以配置一个场景在它被视图首次呈现时的内容。这跟视图控制器只在视图属性被引用时加载他们的视图的方式是类似的。在这个例子中,代码在didMoveToView:方法内部,每当场景在视图中显示时该方法会被调用。

 

在场景中显示Hello文本

1.    添加一个新的属性到场景的实现文件中来跟踪场景是否已创建其内容。

@interface HelloScene()
@property BOOL contentCreated;
@end

该属性跟踪并不需要向客户端公开的状态,所以,在实现文件中它一个私有接口声明里实现。

2.    实现场景的didMoveToView:方法。

- (self)didMoveToView:(SKView *)view
{
    if(!self.contentCreated)
    {
        [self createSceneContents];
        self.contentCreated = YES;
    }
}

每当视图呈现场景时,didMoveToView:方法都会被调用。但是,在这种情况下,场景的内容应只在场景第一次呈现时进行配置。因此,这段代码使用先前定义的属性(contentCreated)来跟踪场景的内容是否已经被初始化。

3.    实现场景的createSceneContents方法。

 - (void)createSceneContents
{
    self.backgroundColor = [SKColor blueColor];
    self.scaleMode = SKSceneScaleModeAspectFit;
    [self AddChild:[self newHelloNode];
}

场景在绘制它的子元素之前用背景色绘制视图的区域。注意使用SKColor类创建color对象。事实上,SKColor不是一个类,它是一个宏,在iOS上映射为UIColor而在OS X上它映射为NSColor它的存在是为了使创建跨平台的代码更容易。

场景的缩放(scale)模式决定如何进行缩放以适应视图。在这个例子中,代码缩放视图,以便你可以看到场景的所有内容,如果需要使用宽屏(letterboxing)。

4.    实现场景的newHelloNode方法。

- (SKLabelNode *)newHelloNode
{
    SKLabelNode * helloNode = [SKLabelNode labelNodeWithFontNamed:@“Chalkduster”];
    @helloNode.text =“Hello, World!”
    helloNode.fontSize = 42;
helloNode.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
    return helloNode;
}

你永远不用编写显式执行绘图命令的代码,而如果你使用OpenGL ES或Quartz 2D你就需要。在Sprite Kit中,你通过创建节点对象并把它们添加到场景中来添加内容。所有绘制必须由Sprite Kit中提供的类来执行。你可以自定义这些类的行为来产生许多不同的图形效果。然而,通过控制所有的绘图,Sprite Kit可以对如何进行绘图应用许多优化。

现在构建并运行该项目。你现在应该看到一个蓝色屏幕上面有“Hello, World!”。现在,你已经学会了绘制Sprite Kit内容的所有基础知识。

 

使用动作让场景动起来

静态文本很友好,但如果文字可以动起来,它会更有趣。大多数的时候,你通过执行动作(action)移动场景周围的东西。Sprite Kit中的大多数动作对一个节点应用变化。创建action对象来描述你想要的改变,然后告诉一个节点来运行它。然后,当渲染场景时,动作被执行,在几个帧上发生变化直到它完成。

当用户触摸场景内容,文字动起来然后淡出。

 

让文本动起来 

1.    添加以下代码到newHelloNode方法:

helloName.name = @“helloNode”;

所有节点都有一个名称属性,你可以设置它来描述节点。当你想能够在稍后找到它,或当你想构建基于节点名称的行为时,你应该命名一个节点。稍后,你可以搜索树中与名称相匹配的节点。

在这个例子中,你给标签的一个名称以便稍后可以找到它。在实际的游戏中,你可能会得给呈现相同类型的内容的任何节点以相同的名称。例如,如果你的游戏把每个怪物呈现为一个节点,你可能会命名节点为monster

2.    重载场景类的touchesBegan:withEvent方法。当场景接收到触摸事件,它查找名为helloNode节点,并告诉它要运行一个简短的动画。

所有节点对象都是iOS上UIResponder 或OS X上NSResponder 的子类。这意味着你可以创建Sprite Kit节点类的子类来添加交互到场景中的任何一个节点。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    SKNode *helloNode = [self childNodeWithName:@“helloNode”];
    If(helloNode != nil)
    {
        helloNode.name = nil;
        SKAction *moveUp = [SKAction moveByX:0 y:100.0 duration:0.5];
        SKAction *zoom = [SKAction scaleTo:2.0 duration:0.25];
        SKAction *pause = [SKAction waitForDuration:0.5];
        SKAction *fadeAway = SKAction fadeWithDuration:0.25];
        SKAction *remove = [SKAction removeFromParent];
        SKAction * moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove];
        [helloNode runAction:moveSequence];
    }
}

为了防止节点响应重复按压,代码会清除节点的名称。然后,它构建动作对象来执行各种操作。最后,它组合这些动作创建一个动作序列;序列运行时,按顺序执行每个动作。最后,它告诉标签节点执行序列动作。

运行的应用程序。你应该看到像之前那样的文字。在屏幕的底部,节点计数应该是1。现在,点击视图内部。你应该看到文字动画并淡出。在它淡出后,节点计数应该变为0,因为节点已从父节点中删除。

 

场景之间的转换

Sprite Kit让场景之间的过渡变得很容易。场景之间的过渡时,你可以坚持保留它们,或清除它们。在这个例子中,你将创建第二个场景类,来学习一些其他的游戏行为。“Hello, World!”文字从屏幕上消失时,代码创建一个新的场景并过渡到它。Hello场景过渡在后会被丢弃。

 

创建飞船场景

1.    创建一个名为SpaceshipScene新类并让它成为SKScene类的子类。

2.    实现代码来初始化飞船场景的内容。此代码类似于你为HelloScene类实现的代码。

@interface SpaceshipScene()
@property BOOL contentCreated;
@end
 
@implementation SpaceshipScene
 - (void)didMoveToView:(SKView *)view
{
    If(!self.contentCreated)
    {
        [self createSceneContents];
        self.contentCreated = YES;
    }
}
 
 - (void)createSceneContents
{
    self.backgroundColor = [SKColor blackColor];
    self.scaleMode = SKSceneScaleModeAspectFit;
}

3.    在HelloScene.m文件中导入SpaceshipScene.h头。

#import "SpaceshipScene.h"

 

4.    在touchesBegan:withEvent方法中,更改runAction:调用为新的调用runAction:completion:。实现完成处理来创建并呈现一个新的场景。

[helloNode runAction:moveSequence completion:^ {
    SKScene * spaceshipScene = [[SpaceshipScene alloc] initWithSize:self.size];
    SKTransition *doors= [SKTransition doorsOpenVerticalWithDuration:0.5];
    [self.view presentScene:spaceshipScene transition:doors];
}];

构建并运行该项目。当你触摸场景内部时,文字淡出,然后在视图过渡到新的场景。你应该看到一个黑色的屏幕。

 

使用节点构建复杂的内容

新的场景还没有任何内容,所以你准备要添加一个飞船到场景。要构建这个太空飞船,你需要使用多个SKSpriteNode对象来创造了飞船和它表面的灯光。每个精灵节点都将执行动作。

精灵节点是在一个Sprite Kit应用程序中最常见用于创建内容的类。他们可以绘制无纹理或纹理的矩形。在这个例子中,你要使用无纹理对象。稍后,这些占位符(placeholder)可以很容易地用纹理精灵进行替换,而不改变它们的行为。在实际的游戏中,你可能需要几十个或上百个节点来创建你的游戏的可视化内容。但是,从本质上说,那些精灵将使用与这个简单的例子相同​​的技术。

虽然你可以直接添加所有三个精灵到场景,但这并不是Sprite Kit的方式。闪烁的灯光是飞船的一部分!如果飞船移动,灯光应该和它一起移动。解决的办法是使飞船节点成为它们的父节点,同样地场景将是飞船的父节点。光的坐标将要相对于父节点的位置来指定,而父节点是在子精灵图像的中心。

 

添加飞船

1.    在SpaceshipScene.m,添加代码到createSceneContents方法来创建飞船。

SKSpriteNode *spaceship = [self newSpaceship];
spaceship.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame)-150);
[self addChild:spaceship];

2.    实现newSpaceship方法。

 - (SKSpriteNode *)newSpaceship
{
    SKSpriteNode *hull= [[SKSpriteNode alloc] initWithColor:[SKColor grayColor] size:CGSizeMake(64,32);
 
    SKAction *hover= [SKAction sequence:@[
                          [SKAction waitForDuration:1.0]
                          [SKAction moveByX:100 y:50.0 duration:1.0]
                          [SKAction waitForDuration:1.0]
                          [SKAction moveByX:-100.0 y:-50 duration:1.0]];
    [hull runAction:[SKAction repeatActionForever:hover];
 
    return hull;
}

此方法创建飞船的船体,并添加了一个简短的动画。需要注意的是引入了一种新的动作。一个重复的动作不断地重复的传递给它的动作。在这种情况下,序列一直重复。

现在构建并运行应用程序来看当前的行为,你应该看到一个矩形。

在建立复杂的有孩子的节点时,把用来在构造方法后面或者甚至是在子类中创建节点的代码分离出来,是一个很好的主意。这使得它更容易改变精灵的组成和行为,而无需改变使用精灵的客户端(client)。

3.    添加代码到newSpaceship方法来添加灯光。

SKSpriteNode *light1= [self newLight];
light1.position = CGPointMake(-28.0,6.0);
[hull addChild:light1];
 
SKSpriteNode *light2= [self newLight];
Light2.position = CGPointMake(28.0,6.0);
[hull addChild:light2];

4.    实现newLight方法。

- (SKSpriteNode *)newLight
{
SKSpriteNode *light = [[SKSpriteNode alloc] initWithColor:[SKColor yellowColor] size:CGSizeMake(8,8)];
 
    SKAction *blink= [SKAction sequence:@ [
                          [SKAction fadeOutWithDuration:0.25]
                          [SKAction fadeInWithDuration:0.25]];
    SKAction * blinkForever = [SKAction repeatActionForever:blink];
    [light runAction:blinkForever];
 
    return light;
}

当你运行应用程序时,你应该看到一对灯在飞船上。当飞船移动,灯光和它一起移动。这三个节点全都是连续动画。你可以添加额外的动作,让灯光在船的周围移动,它们总是相对船体移动。

 

创建能交互的节点

在实际的游戏中,你通常需要节点之间能交互。把行为添加给精灵的方法有很多,所以这个例子仅展示其中之一。你将添加新节点到场景,使用物理子系统模拟它们的运动并实现碰撞效果。

Sprite Kit提供了一个完整的物理模拟,你可以使用它添加自动行为到节点。也就是说,物理在使其移动的节点上自动模拟,而不是在节点上执行动作。当它与物理系统一部分的其他节点交互时,碰撞自动计算并执行。

 

添加物理模拟到飞船场景

 

1.    更改newSpaceship方法来添加一个物理体到飞船。

hull.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hull.size];

构建并运行应用程序。等一下!飞船垂直坠落到屏幕下方。这是因为重力施加到飞船的物理体。即使移动动作仍在运行,物理效果也被应用到飞船上。

2.    更改的newSpaceship方法来防止飞船受物理交互影响。

hull.physicsBody.dynamic = NO;

当你现在运行它时,应用程序像之前那样运行。飞船不再受重力影响。稍后,这也意味着飞船的速度将不会受到碰撞的影响,。

3.    添加代码到createSceneContents方法来生成大量岩石。

SKAction * makeRocks = [SKAction sequence:@ [
    [SKAction performSelector:@selector(addRock) onTarget:self]
    [SKAction waitForDuration:0.10 withRange:0.15]
    ]];
[self runAction:[SKAction repeatActionForever:makeRocks];

场景也是一个节点,因此它也可以运行动作。在这种情况下,自定义操作调用场景上的方法来创建岩石。序列创建一个岩石,然后等待一段随机时间。重复这个动作,场景不断产生大量新的岩石。

4.    实现addRock方法。

static inline:CGFloat skRandf() {
    return rand()/(CGFloat)RAND_MAX;
}
 
static inline CGFloat skRand(CGFloat low, CGFloat high) {
    return skRandf()*(high - low) + low;
}
 
 - (void)addRock
{
SKSpriteNode *rock = [[SKSpriteNode alloc] initWithColor:[SKColor brownColor] size:CGSizeMake(8,8)];
    rock.position = CGPointMake(skRand(0, self.size.width),self.size.height-50);
    rock.name = @“rock”;
    rock.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rock.size];
    rock.physicsBody.usesPreciseCollisionDetection = YES;
    [self addChild:rock];
}

构建并运行该项目。岩石现在应该从场景上方落下来。当一块石头击中了船,岩石从船上反弹。没有添加动作来移动岩石。岩石下落并与船碰撞完全是由于物理子系统的作用。

岩石都很小且移动速度非常快,所以代码指定精确的碰撞,以确保所有的碰撞都检测到。

如果你让应用程序运行了一段时间,帧率会开始下降,即使节点计数仍然很低。这是因为节点的代码仅显示出场景中可见的节点。然而,当岩石落下到场景的底部时,它们继续存在于场景中,这意味着物理还在对它们模拟。最终,有如此多的节点正在处理以致Sprite Kit减慢了。

5.    实现场景中的didSimulatePhysics方法来当岩石移动到屏幕之外时移除它们。

- (void)didSimulatePhysics
{
    [self enumerateChildNodesWithName:@“rock” usingBlock:^(SKNode *node, BOOL *stop){
        if (node.position.y <0)
            [node removeFromParent];
    }];
}

每次场景处理一帧,都运行动作和模拟物理。你的游戏可以挂接到这个过程中来执行其他自定义代码。在每一帧,场景将处理物理,然后移除移出屏幕底部的所有岩石。当你运行应用程序时,帧率保持不变。

在场景中,预处理及后处理与动作和物理结合的地方,就是你建立你的游戏的行为的地主。

这就是你第一次体验Sprite Kit!其它一切都是你在这里看到的基本技术的细化。

 

试试这个!

这里有一些东西,你可以尝试:

·      做一个OS X版本的这个例子。你在视图控制器写的代码,在OS X上通常是在一个应用程序委托中实现。响应代码需要改变来使用鼠标事件而不是触摸事件。但是,代码的其余部分应是相同的。

·      使用纹理精灵呈现船和岩石。(提示:“使用精灵”

·      尝试在触摸事件的响应中移动飞船。(提示:“添加动作节点”“构建场景”)。

·      添加额外的图形效果到场景(提示:“使用其他节点类型”

·      岩石与船舶碰撞时添加其他行为。例如,使岩石发生爆炸。(提示:“模拟物理”

【译】Sprite Kit编程指南(0):简介

本文翻译自Apple官方的《Sprite Kit Programming Guide》,原文在线地址 :https://developer.apple.com/library/prerelease/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Introduction/Introduction.html
初次在这里发翻译贴,请大家多多指教。欢迎各种讨论。

 

关于Sprite Kit

重要提示:  这是API或开发技术的一个初版文档。虽然本文档的技术准确性已被审阅过,但这还不是最终版本。这个苹果的机密信息仅用于适用的苹果开发者计划的注册会员。苹果公司提供这些机密信息来帮助你计划采用本文所述的技术和编程接口。此信息如有变更,根据这份文档实现的软件,应该用最终的操作系统软件和最终文档进行测试。本文档的新版本可能会与API或技术的未来种子一起提供。

 

Sprite Kit提供了一个图形渲染(rendering)和动画的基础,你可以使用它让任意纹理(textured)图像或精灵动起来。Sprite Kit采用的是传统的渲染循环,允许在渲染前处理每一帧的内容。你的游戏确定场景的内容,以及这些内容如何在每帧中变化。Sprite Kit做的工作,就是有效地利用图形硬件来渲染动画的帧。Sprite Kit优化到本质上允许对动画每一帧的任意修改。

Sprite Kit还提供了其他对游戏非常有用的功能,包括基本的声音播放支持和物理模拟。此外,Xcode中提供了内置的Sprite Kit支持,可以很容易地创建并在你的应用程序中使用复杂的特效和纹理图册(atlases)。这种框架和工具的组合,使Sprite Kit对于游戏和其他需要类似动画的应用程序是一个很好的选择。对于其他类型的用户界面动画,使用Core Animation代替。

update_loop_2x

概览

Sprite Kit可用于iOS和OS X。它使用主机设备提供的图形硬件,以高帧速率复合2D图像。Sprite Kit支持多种不同类型的内容,包括:

  • 无纹理或有纹理的矩形(精灵)
  • 文本
  • 任意基于CGPath的形状
  • 视频

Sprite Kit还提供了裁剪和其他特效的支持,允许你对全部或者部分内容应用这些效果。你可以在每一帧中活动(animate)或者改变这些元素。你也可以附加物理体到将这些元素,使他们正确地支持武装和碰撞。

通过支持丰富的渲染基础和处理所有低级别的工作来提交OpenGL的绘图命令,Sprite Kit允许你全神贯注解决更高层次的设计问题,并创造伟大的游戏。

在精灵视图内由呈现场景绘制精灵内容

动画和渲染由SKView对象执行。你在一个窗口在放置这个视图,然后渲染它的内容。因为它是一个视图,所以它的内容可以结合在视图层次里的其他视图。

你的游戏中的内容会被组织成场景(scenes,用SKScene对象代表它们。场景包含精灵和其他要渲染的内容。场景也实现了每帧的逻辑和内容处理。在任何给定的时间内,视图展示一个场景。只要一个场景被呈现出来,它的动画和每帧逻辑会自动执行。

要使用Sprite Kit创建一个游戏,你要创建一个或多个的SKScene类的子类。例如,你可能会创建单独的场景类,用来分别显示主菜单、游戏画面和游戏结束后显示的内容。你可以很容易地在你的窗口中使用一个单一的SKView对象并在不同场景之间进行过渡。

有关章节: “深入Sprite Kit”, “ 使用场景间过渡”, “ Sprite Kit最佳实践”

节点树定义出现在一个场景中的内容

SKScene类实际上是SKNode类的后代。节点是所有内容的基石,而场景对象作为一个节点对象树的根节点。场景及其后代决定哪个内容被绘制以及它渲染的方式。

每个节点的位置在它的父节点定义的坐标系中指定。节点的内容的其他属性也适用于它后代的内容。例如,当一个节点是旋转,所有它的后代也跟着旋转。你可以使用节点树构建一个复杂的图像,然后通过调整最上层节点的属性旋转、缩放并融入整个图像。

SKNode类绘制任何东西,但它对后代应用于它的属性。每一种可绘制内容 由Sprite Kit的不同子类表示。其他的节点子类不直接绘制内容,但修改它们后代的行为。例如,你可以在场景中使用一个SKEffectNode对象对整个子树应用一个Core Image滤镜。通过精确控制节点树的结构,你确定节点的渲染顺序,让你可以在一个场景中布局(layer)复杂的图形效果。

所有节点对象都是响应者(responder)对象,派生(descending)自UIResponderNSResponder,所以你可以继承任何节点类来创建接受用户输入的新类。视图类自动扩展响应链来包含场景的节点树。

相关章节: “使用精灵”, “ 构建场景”, “ 使用其他节点类型”

纹理保存可复用的图形数据

纹理是用来渲染精灵的共享图像。当你需要对多个精灵应用相同的图像时,总是使用纹理。通常你通过加载存储在你的应用程序bundle的图像文件来创建纹理。然而,Sprite Kit也可以在运行时从包括核心图形图像在内的其他来源为你创建纹理,或者甚至渲染把节点树成纹理。

Sprite Kit通过处理较低级别的代码需求来加载纹理和并让它们对图形硬件可用,来简化了纹理的管理。纹理管理由Sprite Kit自动管理。但是,如果你的游戏中使用了大量的图像,你可以通过控制部分的过程来提高性能。首先,你通过提示Sprite Kit纹理很快就需要来做这个。

纹理图册是在你的游戏中一起使用的一组相关的纹理。例如,你可以使用一个纹理图册存储让一个角色活动需要的所有纹理或渲染游戏级别的背景需要的所有瓷砖。Sprite Kit用纹理图册来提高渲染性能。

相关章节: “使用精灵”

动作在场景中由节点执行

使用动作(actions)让场景的内容动起来。每一个动作都是一个对象,由SKAction类定义。你来告诉节点执行动作。然后,当场景处理动画帧,动作就被执行。有些动作在一帧动画内完成,而另一些在完成前应用变化于多帧动画。动作最常见的用途是改变节点的属性。例如,你可以创建动作来移动、缩放或旋转节点,或使其透明。然而,动作也可以更改节点树、播放声音、甚至是执行自定义代码。

动作是非常有用的,但你也可以组合动作来创建更复杂的效果。你可以创建一

同时运行或顺序运行的动作。你可以让动作自动重复。

场景中也能执行自定义的每帧处理。覆盖你的场景子类的方法来执行额外的游戏任务。例如,如果一个节点需要每帧移动,你可能会直接每帧地调整其属性而不是使用一个动作来这样做。

相关章节: “添加动作到节点”, “ 高级场景处理”

添加物理体和联合来在场景中模拟物理

虽然你可以控制场景中的每一个节点的确切位置,你经常想这些节点互相交流、碰撞并在这个过程中告知速度的变化。你可能还需要模拟重力和其他形式的加速度,这些都不在动作系统中处理的。要做到这一点,你可以创建物理体(SKPhysicsBody),并将它们附加到你场景中的节点上。每个物理体由形状、尺寸质量和其他物理特性定义。

当场景中包含物理体,场景就在这些主体上模拟物理。一些势力(forces),如重力和摩擦力,会自动应用。你也可以对物理体调用方法来应用自己的势力。每个主体的加速度和速度会被计算,然后主体彼此碰撞。然后,模拟完成后,相应的节点的位置和旋转的被更新。

你物理体的交互拥有精确的控制。你确定哪些主体被允许相互碰撞 并单独决定哪些交互可以被你的应用程序调用。这些回调允许你勾(hook)到物理模拟中创建其他的游戏逻辑。例如,在一个物理体被另一个物理体击中时,你的游戏可能会销毁一个节点。

场景在一个附加的SKPhysicsWorld对象上定义了物理模拟的全局特性。你可以使用物理世界定义整个模拟的重力,定义模拟的速度,并在场景中查找物理体。你还可以使用物理世界通过一个联合(SKPhysicsJoint)把物理体连接在一起。连接的主体根据联合的类型模拟在一起。

相关章节: “模拟物理”

如何使用本文档

阅读“深入Sprite Kit”获得实现Sprite Kit游戏的一个概述。然后通过其他章节学习关于Sprite Kit功能的细节。一些章节包含建议的练习,以帮助你开发你对Sprite Kit的理解。学习Sprite Kit的最好方法是实践;把一些精灵放到场景中并实验它们!

最后一章,“Sprite Kit最佳实践”,进入更详细的使用Sprite Kit设计游戏。

先决条件

在试图使用Sprite Kit创建一个游戏之前,你应该对应用程序开发的基础知识相当熟悉。

  • 在iOS上,参见今天开始开发iOS应用程序
  • 在OS X上,参见今天开始开发Mac应用程序

尤其的,你应该熟悉以下概念:

  • 使用Xcode开发应用程序
  • Objective-C语言,包括块(blocks)支持
  • 视图和窗口系统

虽然本指南展示了许多有用的创建游戏的技术,它还不是一个完整的游戏设计或游戏开发指南。

参见

当你需要Sprite Kit框架的具体细节时,参见

SpriteKit框架参考



关于如何使用Xcode对Sprite Kit的内置支持的信息,参见

纹理图册帮助

粒子发射器编辑器指南



关于SKSpriteNode类的详细说明参见

Sprite Tour



要了解Sprite Kit中的物理系统,参见

SpriteKit物理碰撞



要深入了解基于Sprite Kit的游戏参见

代码:Explained Adventure

?>