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

后使用快捷导航没有帐号?
iOS游戏开发3个新框架全解
查看: 9595|
评论: 0|原作者: 子龙山人|来自:
摘要: GamePlayKit给游戏开发者带来了全新的游戏架构(“实体组件系统”)和一些通用模式(比如:状态机,Goal-agent-behavior系统等),同时它还提供了大量游戏算法,比如寻路算法,模糊逻辑和规则系统等。
苹果非凡的技术整合和持续创新能力体现在其涵盖领域的任何方面,而对于游戏开发,未来苹果除了继续完善已有的2D游戏框架SpritKit, 3D游戏框架SceneKit和全新的图形渲染API Metal 外,还将增加GamePlayKit, ReplayKit和Model I/O三个新框架:1. GamePlayKit给游戏开发者带来了全新的游戏架构(“实体组件系统”)和一些通用模式(比如:状态机,Goal-agent-behavior系统等),同时它还提供了大量游戏,比如寻路算法,模糊逻辑和规则系统等。2. ReplayKit可以让玩家在游戏中录制游戏视频,并且可以添加语音评论,然后通过社交网络分享出去。3. Model I/O框架,主要是用来处理3D资源和数据,同时可以给一些资源做离线处理,提高游戏运行时性能。SpriteKit新功能SpriteKit是苹果在2013年推出来的2D游戏开发框架,它最初的设计很多来自Cocos2D-iPhone项目,特别是API方面,几乎和Cocos2D-iPhone没有差别。得益于此,Cocos2D用户可以用很小的成本转到SpriteKit上面来。 去年苹果推出了一系列重磅级功能特性:物理集成,XCode资源打包、实时编辑器,SceneKit 3D框架,Metal图形API等,这让SpriteKit成为了开发iOS/OS X平台上的游戏的推荐游戏框架。 今年,苹果继续发力,添加了3个新的节点类型,同时还完善了整个游戏开发流程和工具链支持。SKCameraNode此类模拟游戏里面的照相机,它可以定义游戏的可见视图,同时它也是一个node,可以对它进行缩放,旋转和平移等操作。此类是开发者继去年WWDC后最希望实现的功能,对于实现一些大地图的冒险类游戏和RPG游戏非常有帮助,可以给主角添加一个SKCameraNode子节点,然后游戏视角就会跟随主角的移动而移动了。另外,游戏里面可以支持多个SKCameraNode.SKAudioNode此类可以实现基于位置的音效,环境音效和沉浸性音效。可以想像一下,一颗导弹向你的主角飞过来,声音由远及近,当导弹离你的主角很近时,甚至可以听到导弹尾部喷气的声音,这种游戏音效会给玩家带来非凡的听觉感受。SKReferenceNode此类是SpriteKit的一个亮点,它可以实现已有组件的序列化,支持数据驱动式的游戏开发。 配合XCode,序列化的组件可以被当作游戏资源在各个不同的工程和游戏里面重用。 而且它会自动Cache,同样的文件不会被加载多次。对于一些复杂的场景和组合动作序列,当需要重用它们的时候,SKReferenceNode是个大杀器。Metal支持目前iOS 9全面支持Metal,已有的SpriteKit游戏跑在iOS 9上面,不需要做任何修改即可享受Metal带来的性能提升。如果你的设备支持Metal,那么它就会使用Metal,反之则切换到OpenGL ES.&SpriteKit 相关工具一个好的游戏框架离不开一个好用的工具支持,游戏开发者可以通过工具快速地进行迭代式开发。苹果一直致力于完善SpriteKit的工作流,让开发者可以更快速地开发游戏,同时让玩家可以不用关心技术细节,只需要专心游戏玩法即可。接下来SpriteKit相关工具支持方面主要会有以下几点:1. 资源分类(Asset catalog)支持,开发者可以更较精确地控制不同设备使用的资源分辨率,最重要的是,玩家在下载游戏到设备里面时,只会下载与其设备分辨率匹配的资源。2. 按需加载资源,通过给不同的资源设备Tag,可以指定某些场景和关卡只加载特定Tag的资源。3. XCode快速预览:支持在Playgound里面使用Swift编写一些测试代码,然后快速预览效果。不需要额外的编译,立马就可以看到API的使用效果,这对于测试一些API和验证一些想法非常有帮助。4. 2D时间轴动作编辑器:这个应该算是SpriteKit的大杀器了,它是一个功能完善的2D动作编辑器,可以支持实时预览动作效果,并且同时支持2D和3D。5. 数据驱动:SKReferenceNode可以让可重用的游戏组件数据化,让玩家可以用更少的代码来完成游戏开发。小结整个SpriteKit在性能,工具和易用性方面都有了较大的提升,另外,配合SceneKit,开发者可以更容易地开发3D休闲类游戏了。SceneKit新功能SceneKit是苹果为了让游戏开发者更方便地开发3D游戏而推出来的,它支持iOS和MacOS X。 同时,它还完美支持粒子系统,物理集成和SpriteKit。今年,苹果改进了场景编辑器,同时对Metal的支持也更加完善了。新的场景编辑器新的场景编辑器的功能总结如下:1. 支持更多的3D文件格式: DAE,OBJ,Alembic, STL和PLY文件格式;2. 新的原生文件格式,使用NSKeyedArchiver来存取;3. Shader修改器和环境光遮蔽(Ambient Occlusoni);4. 天空盒(Skybox)和几何修改器;5. SCNTechnique支持;6. 支持声音节点;7. 支持Model I/O(后面会具体介绍Model I/O是什么);除了这7点以外,还有诸如compute shader, relfective cub map等功能。Metal支持和SKTransitions新的SceneKit可以根据设备类型自动匹配是否使用OpenGL或Metal作为渲染后端,同时它支持SpriteKit的切换场景切换,制作SceneKit场景切换效果更容易了。Metal 新功能及性能优化今年WWDC游戏相关视频中,苹果用了3个视频介绍了Metal的新特性以及性能优化方法,同时SpriteKit和SceneKit在iOS 9中都是默认使用Metal作为渲染API。Metal作为苹果生态圈的高性能图形渲染API,大有可能在将来某一天取代OpenGL的趋势。新功能介绍这里简单给大家介绍一下Metal的新功能,具体的细节还需要查看相关文档和视频:1. 新的内存模型:它一共支持3种内存模型,分别是共享内存模型,私有内存模型和托管内存模型;2. 新的纹理压缩格式(ASTC);3. 设备GPU 类型判断:自动根据设备的GPU类型来判断支持的特性集合;4. MetalKit框架:让Metal应用开发更加简单方便;5. Metal performance shader 框架:提供了一系列数据并行算法;6. Metal 调试工具:更方面地调试Metal图形应用;7. Metal应用程序瘦身:这一点和上面介绍的SpriteKit瘦身原理是一样的。MetalKit框架MetalKit对于一些常见的应用场景提供了高效的实现,开发者可以用更少地代码来开发应用,并且开发出来的应用性能和稳定性会更高。 MetalKit提供了一个MTKView类,它统一处理了渲染一个Metal场景所需要的设置代码,另外,它提供了一个纹理加载器,它可以从一个图片文件中创建一个Metal纹理。最后,MetalKit完美支持Model I/O,让3D模型的处理更加简单。Metal performance shader框架Metal performance shader是一个为GPU提供数据并行算法的一个框架,目前只支持A8处理器。 它定义了一些类似操作CPU风格的API,这样可以让GPU编程变得更加简单。它提供了很多内置的并行算法,比如兰索斯重新取样(Lanczos resampling),卷积函数(比如高斯滤镜等),直方图等。Metal调试工具和较佳实践众所周知,图形应用是非常难以调试的,而Metal在设计之初就考虑了多线程,而多线程的调试也是很困难的。因此,如果没有好用的工具支持,调试Metal应用将会变得非常困难。 此次,苹果围绕Metal提供了一系列好用的工具。比如:一个可视化的帧调试器,一个资源和状态查看器,shader性能分析工具,集成的离线Metal编译器等。 读者可以根据WWDC中关于Metal性能优化的视频去了解更多详细的信息。 另外,苹果给我们定义了一些较佳实践:要尽早分析和经常分析代码;在最合适的时机去获得Drawable对象;尽可能基于GPU去考虑多线程渲染;提前创建好一些耗时的对象并尽可能地重用这些对象,提前编译好着色器程序。小结Metal已经成了苹果在3D图形渲染的一张王牌,它的性能远超OpenGL,开发者应该全面拥抱它,利用它来开发出性能更好的图形应用和游戏。同时,使用Metal,也可以用更少的代码来编写图形应用,另外,它也会让你的程序会更容易维护。GamePlayKit介绍GamePlayKit是今年WWDC游戏开发方面较大的亮点,它提供的实体组件系统将直接改变之前以继承为主的开发游戏方式。另外,还为游戏开发提供了大量实用的算法与设计模式,iOS游戏开发从未如此简单。实体组件系统实体组件系统是一种非常流行和先进的组织游戏逻辑的方式,采用它编写的游戏代码,更容易维护,更容易扩展,同时也更加灵活。1. GKEntity 在实体组件系统里面,所有的游戏对象都被组织成游戏实体(GKEntity),它只是一个组件(GKComponent)容器,可以在游戏过程中动态地添加和删除游戏组件。并且可以通过组件类型来访问组件并更新所有组件。2. GKComponent GKComponent是所有游戏组件的基类,它负责存储游戏数据,同时会随着实体的Update方法来更新自身的状态。所有的游戏逻辑被放置在updateWithDeltaTime方法中。3. GKComponentSystem组件系统是处理组件的容器,它包含了所有同种类型的组件,并通过统一的逻辑来处理这些组件。组件系统并不关心实体,它会透明地对象所有同种类型的组件并更新其状态。寻路算法寻路算法可以让你指定一些节点生成一个双向连接图,然后可以通过简单的API调用找到任意两个点之间的最优路径。所有的有向图的基类是GKGraph,它是一个抽象类。它提供一些共用的逻辑,比如动态地添加和删除节点,连接这些节点并找到任意两个节点之间的路径。 此外,GamePlayKit还提供了两种特殊类型的图节点:GKGridGraph和GKObstacleGraph。GKGridGraph非常适合2D游戏里面的路径查找。而GKObstacleGraph则可以设置一些障碍物,节点在寻路的时候,这些障碍物是无法穿越的。SKNode的边界,物理刚体和纹理都可以当作障碍物。状态机状态机是游戏玩法的骨架,游戏本身就是由一系列的状态机组成的。比如主角动画,AI状态,UI切换,关卡切换等,这些都可以使用状态机巧妙地解决。 GamePlayKit通过封装了GKStateMachine和GKState类,把一些共用的逻辑全部抽象出来了,开发者再也不用为每一个游戏去开发单独的状态机了。Goal, Agent和BehaviorAgent指的是会自已移动的游戏实体,它们通过Goal和Behavior来驱动自身的逻辑。Behavior是由一系列的Goal组成的,每一个Goal都包含有相应的权重。Agent在与玩家进行游戏交互的时候,可以表现得更像人类,因为它们也会做决策。 以往只有在一些很复杂的游戏里面才会看到的AI,如今通过GamePlayKit,我们可以非常轻松地给自己的游戏也添加了。通过给游戏里面的怪物添加一些AI,能够极大地增强游戏的趣味和粘性。AI 策略(MinMax AI)MinMax AI主要用于一些回合类游戏,比如象棋,围棋,Tic-Tac-Toe等。它还能给人类玩家提供一些建议,比如告诉你下一步棋该怎么下。另外,通过计算时间的设置,AI可以定义不同的难度等级。规则系统规则系统可以让你制作更加复杂的游戏AI,游戏世界的交互不再是“黑即是黑,白即是白”,而是以事先定义好的一定的规则去交互。游戏AI可以实现所谓的模糊逻辑,游戏怪物的活动可以根据定义好的规则系统去动态地调整和评估自己的行为。随机数发生器(Random Sources)每个游戏都有一些特别的随机数生成需求,而rand()函数只能生成一些伪随机数,并且可能跟平台实现还有一些出入。作为开发者,我们需要一个与具体平台无关的随机数生成器,另外这个随机数生成器还能产生符合一定数学分布的随机数。Demobots示例这是一个非常有价值的游戏demo,里面几乎涉及了GamePlayKit里面的大部分技术,是一个值得深入学习的游戏示例。这里是 下载链接小结实体组件系统不是苹果的原创,但是这一次GamePlayKit巧妙地整合了已有的游戏开发框架,iOS游戏开发变得从未如此简单和有效。配合强大的Swift和Xcode支持,更加如虎添翼。ReplayKit和 Game CenterReplayKit可以让玩家在游戏中录制游戏视频,并且可以给这些视频添加语音评论并且通过社交网络分享出去。另外Game Center允许访客模式,并且整合了产品服务器和沙盒服务器,这样测试和开发基于Game Center的游戏就变得更加简单了。ReplayKit只有在iOS 9才能使用,另外开发者可以定制是自动在某些游戏场景开启视频录制还是让玩家自己选择录制时机。录制完的视频还可以被编辑和预览,从此玩游戏不再孤单。另外,访客模式是不需要取得认证即可参与游戏的,访客不会取得任何游戏成就,也不可以发布游戏得分。开启访客模式的游戏,玩家只能与其它运行iOS 9的玩家进行游戏。Model I/O模块介绍Model I/O是用处理3D资源和数据的框架,我们可以通过Model I/O快速地加载和导出3D资源文件。另外,它集成在Xcode里面并且和可以非常容易地与GameKit API配合. 它的主要功能特性如下:1. 支持多种3D文件格式的;2. 可以对资源进行修改和Bake;3. 支持3D模型三维像素化(Voxels);4. 集成Playground, Swift和Finder,并且能够在Finder的Quick look里面预览资源;5. 支持基于物理的材质和光照。总结从本次苹果对游戏开发所做的改进来看,休闲社交游戏仍然是其主推的游戏类型。不过有了Metal的支持,在MacOS X开发AAA游戏也是完全有可能的。另外,主流的游戏厂商Unity和Unreal都声称直接了Metal,相信以后苹果生态圈里面会出现更多高品质的游戏。 另外,随着虚拟现实和现实增强的火爆,相信在不久的将来,苹果也可能推出自家的VR设备。到时候,这些游戏开发框架和工具必将是您开发苹果VR游戏的推荐。
上一篇:下一篇:本视频教程是由Lynda机构出品的SceneKit制作IOS游戏实例训练视频教程,时长:1小时26分,大小:250 MB,MP4高清视频格式,教程使用软件:iOS, SceneKit,作者:Craig Barr,共19个章节,语言:英语。SceneKit苹果IOS平台游戏开发框架,SceneKit是用来构建3D场景的框架,且可以与Core Animation和SpriteKit无缝交互。在SceneKit中可以直接引入COLLADA行业标准文件制作好的3D模型或场景。与SpriteKit一样,SceneKit通过场景(SCNScene)来显示物体,场景包涵在SCNView。场景内同样是以节点的结构来呈现物体。琳达(Lynda) 是全球最知名的在线教育机构,每年可以吸引到4500万独立访客,让它成为教育界的长青网站。这家在线视频教学网站,提供了数以千计的现在最流行的软件技术视频教程。有大约1百万人都愿意支付每月25美元的使用费来访问它,它已经成为一个网上帝国。它的创始人琳达·温曼(LyndaWeinman)认为,她的教程在网民中大受欢迎的程度会鞭策那些认为网络只是让人变笨变懒的人。根据该公司的报告,Lynda 现在每年可以吸引到4500万独立访客,大部份的独立订户会订阅内容1至3年。公司现在雇有200名全职员工,风险投资人还不停地要扔钱过来,但温曼说,她比较喜欢看到公司自然平稳地增长并通过口碑营销打开市场。网站上的教育录像质量很高,课程划分细致,学习的人可以根据自己的目标随时切换内容。Lynda Building a 3D Game in SceneKitDo you have a good idea for a 3D game, but find some of the technical aspects of 3D game creation to be a bit daunting? SceneKit—a 3D graphics API used in iOS programming—can help simplify this process, and make it easier to bring your game to life. In this course, learn how to leverage SceneKit to build a simple 3D game for iOS. Mike Wong helps to familiarize you with the basics of this high-level framework, covering how to build your first SceneKit game project, and work with scenes, nodes, physics, and particles.
-----------------------------------------------------------------
资源名称: SceneKit制作IOS游戏实例训练视频教程
本站编号:&&ZH3551
百度网盘3:百度网盘5:城通网盘:
解压密码:&&
更多的信息: 待补充
如果下载地址失效,请发邮件到IOS 2D游戏开发框架--SpriteKit - OpenABC
>> IOS 2D游戏开发框架--SpriteKit
16:12:33 . 已浏览598次
最近发现Xcode自带的2D游戏开发框架SpriteKit可以直接引入到APP中进行混合开发,这就是说可以开发出既带业务应用又带游戏的苹果APP,咋怎么觉得这是一个自己的小发现,查了下其实人家早有人这样做了,发现这功能我当然很开了,所以下了两个案例准备学学。以前业余时间也学过一下cocos2d-x这样的跨平台游戏框架,也做过小案例,所以感觉这个框架并不麻烦,而且比cocos2d-x简单。并且这框架我不应该像C2D-X那样学了就扔哪里不管了,因为IOS开发正是我当下工作。。也许以后我做的APP都会隐藏几个小游戏在里面,等app审核过了再放出来,多么惬意的想法。
今天是第一次笔记,我直接创建一个xcode自带的spritekit小案例来说下我对这的理解。
2D游戏涉及到使用类其实很少的,主要围绕精灵、场景两个核心的类在转,精灵就是游戏中的角色、玩家操作的角色、游戏里面的怪物角色,场景就是游戏的背景了, spriteKit里面这些类都继承与SkNode。下面有一截图你可以看下截图
如果你把上面几个类搞透测了,做个小游戏应该对你来说不麻烦, 如果要做复杂的游戏,那当然是不行了,这框架现在好像还能写OpenGL做角色渲染了,那复杂得很,复杂度不小于c语言。
下面我贴代码,我在原本官方自带案例基础上,增加了纹理类使用, 场景背景的设置,节点层的设置
纹理就是将整个动作分割为若干个图片,然后放到一个数组中去,然后将需要使用这个
纹理的精灵增加一个行为,将上面的纹理数组增加到行为中, 然后一个连贯的动作就出来了。
其实在ios开发中也可以给一个视图绑定一个由若干图片组合而成的动画。
我这里没有整套行为的图片,这里只是实现这个功能而已
SKScene:场景初始化时执行的代码实现这个功能。
- (instancetype)initWithSize:(CGSize)size
if (self = [super initWithSize:size]) {
/*首先创建第一个精灵节点,设置精灵节点的背景图片,
&&&&&&&&这里设置精灵节点的图片方式很多,我这里也是用的纹理,
&&&&&&&&你也可以直接用.backgroundimage,是一样的
&&&&&&&&*/
// SKSpriteNode *
FirstSkNodeX =
&&&&//[SKSpriteNode spriteNodeWithImageNamed:
&&&&&&&&&&&
//&#(nonnull NSString *)#&];
*BackgourndTextureImge=[UIImage
&&&&&&&&& imageNamed:@"planeBack"];
SKTexture *BackgourndTexture = [SKTexture
&&&&&&&&& extureWithImage:BackgourndTextureImge];
SKSpriteNode *
FirstSkNode = [SKSpriteNode
spriteNodeWithTexture:BackgourndTexture size:self.size];
FirstSkNode.position = CGPointMake(
&&&&&&&& CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
FirstSkNode.name = @"Backgournd";
FirstSkNode.zPosition = 0;
&&&&&& /*将该精灵设置的位置设置到0层,0=最底层,
&&&&&&&&这属性=同html中的 zindex,精灵中的层一定要记得设置,
&&&&&&&&否则初始化或则经过一个动作后你看不到有可能*/
[self addChild:FirstSkNode];
/*下面是行为动画纹理
BearImages.atlas=代表一个文件夹,
&&&&&&&&记得文件夹后面加 .atlas,然后将整套行为切分图片放进去
SKTextureAtlas *atlas = [SKTextureAtlas
&&&&&&&&&&&&atlasNamed:@"BearImages.atlas"];
SKTexture *f1 = [atlas textureNamed:@"planeBack"];
SKTexture *f2 = [atlas textureNamed:@"planeBack1"];
NSArray *monsterWalkTextures = @[f1,f2];
/*下面是一个循环执行的Action,这里实现的效果是,
&&&&&&&&精灵不停切换monsterWalkTextures 里面的纹理图片*/
SKAction *walkAnimation =
&&&&&&&&[SKAction animateWithTextures:monsterWalkTextures
&&&&&&&&&&&&timePerFrame:0.1];
[FirstSkNode runAction:[SKAction
&&&&&&&&&&&&repeatActionForever:walkAnimation]];
} -(void)didMoveToView:(SKView *)view {
} /*下面这个是官方原本有的方法,
你自己通过xcode创建一个案例就知道了,
点一下就增加一个飞机*/
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *) &&&&event {
/* Called when a touch begins */
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
SKSpriteNode *sprite = [SKSpriteNode
&&&&&&&&&&&&spriteNodeWithImageNamed:@"Spaceship"];
sprite.zPosition=1;//场景中的层
sprite.xScale = 0.15;//精灵大小
sprite.yScale = 0.15;//精灵大小
sprite.position =//精灵所在位置
///每秒转动一圈
SKAction *action = [SKAction rotateByAngle:M_PI
&&&&&&&&duration:1];
///重复行为
[sprite runAction:[SKAction repeatActionForever:action]];
[self addChild:sprite];
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
调用地方也有小改动
- (void)viewDidLoad
[super viewDidLoad];
// Configure the view.
SKView * skView = (SKView *)self.
skView.showsFPS = YES;
skView.showsNodeCount = YES;
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
GameScene *scene = [[GameScene alloc]initWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectF
[skView presentScene:scene];
好文0钦佩0笑抽0泪奔0无聊0气炸
?微信扫一扫 关注我们?教你用 SpriteKit 做一个自己的”割绳子“游戏(Swift 3) - 简书
教你用 SpriteKit 做一个自己的”割绳子“游戏(Swift 3)
本文翻译自
日更新:由 Kevin Colligan 更新至 iOS 10,Xcode 8 和 Swift 3。
,上一次更新是 Nick Lockwood 。
给鳄鱼喂过菠萝吗?这篇教程会教你!
是一款流行的物理驱动游戏,玩家通过剪断挂着糖果的绳索来喂养一只名叫 Om Nom 的小怪兽。只要在正确的时间和位置切断,Om Nom 就会得到一份美味佳肴。
虽然对 Om Nom 有着满满的敬意,可我还是要说,这个游戏真正的明星是它方针的物理学:绳索摆动、重力牵引、糖果按照真实世界中那样落下。
我们可以用苹果的 2D 游戏框架,SpriteKit,借助它的物理引擎创建相似的游戏体验。在本教程中,我们会一起做一个名为 Snip The Vine 的游戏。
注意:本教程假设你对 SpriteKit 有一些经验。如果还不了解 SpriteKit,看看这篇
在 Snip The Vine 中,玩家可以把 可爱的小动物 菠萝喂给鳄鱼。从
开始。在 Xcode 中打开项目,快速浏览一下结构。
项目文件分散在多个文件夹中。本教程中,我们需要处理 Classes 文件夹,它包含了主要的代码文件。再随便看看其它文件夹,如下所示:
常量可以让代码可读性更强,避免重复硬编码的字符串和”魔法数字(magic numbers)“。
打开 Constants.swift 然后添加如下代码:
struct ImageName {
static let Background = "Background"
static let Ground = "Ground"
static let Water = "Water"
static let VineTexture = "VineTexture"
static let VineHolder = "VineHolder"
static let CrocMouthClosed = "CrocMouthClosed"
static let CrocMouthOpen = "CrocMouthOpen"
static let CrocMask = "CrocMask"
static let Prize = "Pineapple"
static let PrizeMask = "PineappleMask"
struct SoundFile {
static let BackgroundMusic = "CheeZeeJungle.caf"
static let Slice = "Slice.caf"
static let Splash = "Splash.caf"
static let NomNom = "NomNom.caf"
上面的代码为 sprite 图片名和声音文件这些东西定义了常量。
紧接着上面,添加如下代码:
struct Layer {
static let Background: CGFloat = 0
static let Crocodile: CGFloat = 1
static let Vine: CGFloat = 1
static let Prize: CGFloat = 2
static let Foreground: CGFloat = 3
struct PhysicsCategory {
static let Crocodile: UInt32 = 1
static let VineHolder: UInt32 = 2
static let Vine: UInt32 = 4
static let Prize: UInt32 = 8
这段代码又定义了两个结构体,Layer 和 PhysicsCategory,每个都包含了很多 CGFloat 和 UInt32 属性。在我们添加东西到场景中时,会用它们来指定 sprite 的 zPostion 和物理类别。
最后,再添加一个结构体:
struct GameConfiguration {
static let VineDataFile = "VineData.plist"
static let CanCutMultipleVinesAtOnce = false
VineDataFile 定义了文件名,用于确定葡萄藤放置的位置。
CanCutMultipleVinesAtOnce 是一种简单的修改游戏参数的方式。会让游戏更有意思的决策并不是显而易见的。像这样的常量就提供了一种简单的方式,让我们在各种方法之间切换,以便我们在后面修改游戏。
现在可以开始为我们的场景添加节点了。
为场景添加背景子画面(Sprites)
打开 GameScene.swift 然后将如下代码添加到 setUpScenery():
let background = SKSpriteNode(imageNamed: ImageName.Background)
background.anchorPoint = CGPoint(x: 0, y: 0)
background.position = CGPoint(x: 0, y: 0)
background.zPosition = Layer.Background
background.size = CGSize(width: size.width, height: size.height)
addChild(background)
let water = SKSpriteNode(imageNamed: ImageName.Water)
water.anchorPoint = CGPoint(x: 0, y: 0)
water.position = CGPoint(x: 0, y: 0)
water.zPosition = Layer.Foreground
water.size = CGSize(width: size.width, height: size.height * 0.2139)
addChild(water)
setUpScenery() 方法是从 didMove() 中调用的。在这个方法里,我们创建了一组 SKSpriteNode,并且使用 SKSpriteNode(imageNamed:) 将它们初始化了。要处理多种屏幕尺寸的话,要明确规定背景图片的尺寸。
我们已经将这两个节点的 anchorPoint 从 (0.5, 0.5) 更改到 (0, 0)。这意味着节点的定位是现对于左下角的,而不是中心,这样就可以轻松地将背景和水置于场景中,并且让它们底部对齐。
注意: anchorPoint 属性使用了 unit 坐标系,(0,0) 表示子画面图片的左下角,(1,1) 表示右上角。因为量度总是为 0 到 1,所以这些坐标与图像尺寸和纵横比无关。
我们还设置了子画面的 zPosition,控制了 SpriteKit 在屏幕上绘制节点的顺序。
回想一下在 Constants.swift 中,我们指定了一些值,用于子画面的 zPosition。这里(Layer)用到了其中两个。Background 和 Layer.Foreground —— 确保背景将保持在其它子画面的后面,前景则始终在最前面绘制。
构建并运行项目。如果没做错的话,就可以看到下面的画面:
把鳄鱼加进场景
提前警告一下,这只鳄鱼很喜欢咬人,注意手指要一直和它保持距离!:]
就像背景布景一样,鳄鱼使用 SKSpriteNode 来表示。但有几个重要的区别:为了游戏逻辑,我们需要保留对鳄鱼的引用;我们还需要为鳄鱼子画面设置物理身体,以检测和处理与其他身体的接触。
还是在 GameScene.swift 里,把如下属性加到类的最上面:
private var crocodile: SKSpriteNode!
private var prize: SKSpriteNode!
这些属性用于保存对鳄鱼和奖励(菠萝)的引用。我们把它们定义为私有的,因为它们不会在 GameScene 之外被访问。
这些属性的类型已经被定义为 SKSpriteNode!。! 表示它们是被隐式拆包的可选值,告诉 Swift 自己并不需要立刻被初始化。只有在你百分百确信访问它们的时候,它们不会是 nil 的情况下才这么使用……否则 app 将会崩溃。
找到 GameScene.swift 里面的 setUpCrocodile() 方法,然后添加如下代码:
crocodile = SKSpriteNode(imageNamed: ImageName.CrocMouthClosed)
crocodile.position = CGPoint(x: size.width * 0.75, y: size.height * 0.312)
crocodile.zPosition = Layer.Crocodile
crocodile.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: ImageName.CrocMask), size: crocodile.size)
crocodile.physicsBody?.categoryBitMask = PhysicsCategory.Crocodile
crocodile.physicsBody?.collisionBitMask = 0
crocodile.physicsBody?.contactTestBitMask = PhysicsCategory.Prize
crocodile.physicsBody?.isDynamic = false
addChild(crocodile)
animateCrocodile()
这段代码创建了鳄鱼节点,并设置了它的 position 和 zPosition。
与背景布景不同,鳄鱼有 SKPhysicsBody,意味着它可以与世界上其他物体进行物理交互。在后面检测菠萝是否落到它嘴巴里的时候很有用处。我们不希望鳄鱼被打翻、或是从屏幕底下掉出来,所以把 isDynamic 设置为 false,从而防止它受到物理受力的影响。
categoryBitMask 定义了身体所属的物理类别 —— PhysicsCategory。在这里就是鳄鱼。我们把 collisionBitMask 设置为 0 因为我们不希望鳄鱼把其它身体弹飞。我们需要知道的就是何时”奖励“身体会接触到鳄鱼,所以我们设置了响应的 contactTestBitMask。
你可能注意到了,鳄鱼的物理身体使用了 SKTexture 进行初始化。其实简单点的话,我们可以直接在身体纹理上复用 CrocMouthOpen ,但那个图片包括了鳄鱼的整个身体,而 mask 纹理只包含鳄鱼的头和嘴。鳄鱼可不能用尾巴吃菠萝!
现在我们会为鳄鱼添加一个”等待“动画。找到 animateCrocodile() 方法,添加如下代码:
let duration = 2.0 + drand48() * 2.0
let open = SKAction.setTexture(SKTexture(imageNamed: ImageName.CrocMouthOpen))
let wait = SKAction.wait(forDuration: duration)
let close = SKAction.setTexture(SKTexture(imageNamed: ImageName.CrocMouthClosed))
let sequence = SKAction.sequence([wait, open, wait, close])
crocodile.run(SKAction.repeatForever(sequence))
除了要让小鳄鱼显得很焦虑外,这段代码还创建了一些改变鳄鱼节点的纹理的动作,使其在闭嘴和张嘴之间交替。
SKAction.sequence() 构造函数从数组中创建了一个动作序列。在这种情况下,纹理动作按照序列进行组合,并且有2到4秒不定的随机延迟时间。
序列动作被包装在一个 repeatActionForever() 动作中,所以它在那段时间内会一直重复。然后由鳄鱼节点运行这个动作。
搞定!构建并运行,看看这只可怕的爬行动作撕咬它的死亡之颚!
我们现在有了布景,我们也有了一只鳄鱼——现在需要 可爱的小动物一个菠萝。
打开 GameScene.swift 然后找到 setUpPrize() 方法。添加如下代码:
prize = SKSpriteNode(imageNamed: ImageName.Prize)
prize.position = CGPoint(x: size.width * 0.5, y: size.height * 0.7)
prize.zPosition = Layer.Prize
prize.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: ImageName.Prize), size: prize.size)
prize.physicsBody?.categoryBitMask = PhysicsCategory.Prize
prize.physicsBody?.collisionBitMask = 0
prize.physicsBody?.density = 0.5
addChild(prize)
与鳄鱼类似,菠萝节点也用了物理身体。最大的区别是菠萝会掉落然后弹来弹去,而鳄鱼只是坐在那里,焦急的等待。所以我们没有设置 isDynamic ,让它保留默认值,true。我们还减少了菠萝的密度,这样它就可以更自由的摇摆。
使用物理学
在让菠萝掉下之前,最好能配置一下物理世界。找到 GameScene.swift 中的 setUpPhysics() 方法,然后添加下面的三行代码:
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector(dx: 0.0, dy: -9.8)
physicsWorld.speed = 1.0
这样就建立了物理世界的 contactDelegate、重力(gravity)和速度(speed)。重力决定了物理世界中身体的重力加速度,速度决定了模拟执行的速度。(这两个属性都被设置为默认值。)
由于我们把 self 指定为 contact delegate,所以会在第一行出现一个编译器错误,因为 GameScene 还不符合 SKPhysicsContactDelegate 协议。在类定义中添加这个协议可以修复,像这样:
class GameScene: SKScene, SKPhysicsContactDelegate {
再次构建并运行 app。应该能看见菠萝穿过鳄鱼,掉进水里(实际上是在水的后面)。是时候添加葡萄藤了。
添加葡萄藤
SpriteKit 的物理身体旨在模拟刚性物理。但葡萄藤是弯的。所以我们会把每条葡萄藤实现为,具有一段段灵活接头的数组,类似链条。
每条葡萄藤有三个重要的属性:
anchorPoint: CGPoint,表示藤的末端,连接到树的位置
length:Int,表示葡萄藤中有多少段
name:String,用于标识给定段所属的葡萄藤
在本教程中,游戏只有一关。但在真正的游戏中,我们会希望能够轻松创建新的关卡布局,而无需编写大量代码。一种实现的好方法是独立于游戏逻辑指定关卡数据,比如借助 property list 或 JSON 以将其存储在数据文件中。
因为我们会从文件中加载葡萄藤数据,因此表示葡萄藤数据的自然结构是 NSDictionary 对象的 NSArray,可以使用初始化方法 NSArray(contentsOfFile:) 从 property list 中轻易读取出来。每个 dictionary 都表示一条葡萄藤。
在 GameScene.swift 中,找到 setUpVines() 然后添加如下代码:
// 1 加载葡萄藤数据
let dataFile = Bundle.main.path(forResource: GameConfiguration.VineDataFile, ofType: nil)
let vines = NSArray(contentsOfFile: dataFile!) as! [NSDictionary]
// 2 添加葡萄藤
for i in 0..&vines.count {
// 3 创建葡萄藤
let vineData = vines[i]
let length = Int(vineData["length"] as! NSNumber)
let relAnchorPoint = CGPointFromString(vineData["relAnchorPoint"] as! String)
let anchorPoint = CGPoint(x: relAnchorPoint.x * size.width,
y: relAnchorPoint.y * size.height)
let vine = VineNode(length: length, anchorPoint: anchorPoint, name: "\(i)")
// 4 添加到创建中
vine.addToScene(self)
// 5 将葡萄藤的另一端连接到奖励
vine.attachToPrize(prize)
使用上面的代码,我们:
从 property list 文件中加载了葡萄藤数据。可以看看 Resources/Data 中的 VineData.plist 文件,可以看到该文件包含了一个字典数组,每个字典包括 relAnchorPoint 和 length:
for 循环遍历了数组的索引。遍历索引,而不是遍历数组对象的原因是我们需要该索引值以为每条葡萄藤生成唯一的名字字符串。这在后面会相当重要。
对于每个葡萄藤字典,都要取出 length 和 relAnchorPoint,用于初始化新的 VineNode 对象。length 指定了葡萄藤的段数。relAnchorPoint 用于确定葡萄藤相对于场景的尺寸的锚点位置。
最后,使用 addToScene() 把 VineNode 附到 场景中。
然后用 attachToPrize() 将其附加到奖励上。
下面我们会在 VineNode 中实现这些方法。
定义葡萄藤类
打开 VineNode.swift。VineNode
是一个自定义类,继承自 SKNode。它本身没有任何视觉外观,而是作为表示容纳葡萄藤段的 SKSpriteNodes 集合。
在类定义中添加如下属性:
private let length: Int
private let anchorPoint: CGPoint
private var vineSegments: [SKNode] = []
会出现几个错误,因为 length 和 anchorPoint 还没有被初始化。我们把它们声明为非可选值,但却没有分配值。用如下代码替换 init(length:anchorPoint:name:) 方法的实现部分即可修复:
self.length = length
self.anchorPoint = anchorPoint
super.init()
self.name = name
相当简单,但由于某些原因还是有错误。有第二个初始化方法,init(coder:) ——我们没有在任何地方调用它,所以它是干嘛用的?
因为 SKNode 实现了 NSCoding 协议,所以它继承了必要初始化方法 init(coder:),表示我们必须初始化非可选值属性,即使我们没有用到它。
现在就干。用以下代码替换掉 init(coder:) 的内容:
length = aDecoder.decodeInteger(forKey: "length")
anchorPoint = aDecoder.decodeCGPoint(forKey: "anchorPoint")
super.init(coder: aDecoder)
下一步,我们需要实现 addToScene() 方法。这是一个复杂的方法,所以我们要分阶段来写。首先,找到 addToScene() 并添加以下代码:
// 把葡萄藤加到场景中
zPosition = Layer.Vine
scene.addChild(self)
我们把葡萄藤加到了场景中,并设置了它的 zPosition。接下来,把这个代码块添加到同样的方法中:
// 创建葡萄藤架
let vineHolder = SKSpriteNode(imageNamed: ImageName.VineHolder)
vineHolder.position = anchorPoint
vineHolder.zPosition = 1
addChild(vineHolder)
vineHolder.physicsBody = SKPhysicsBody(circleOfRadius: vineHolder.size.width / 2)
vineHolder.physicsBody?.isDynamic = false
vineHolder.physicsBody?.categoryBitMask = PhysicsCategory.VineHolder
vineHolder.physicsBody?.collisionBitMask = 0
这样就创建了葡萄藤架,就像用于葡萄藤悬挂的钉子。和鳄鱼一样,这个身体不是动态(dynamic)的,不会与其它身体碰撞。
藤架是圆形的,所以用 SKPhysicsBody(circleOfRadius:) 构造函数。藤架的位置就是我们创建 VineModel 时指定的 anchorPoint。
接下来,我们要创建葡萄藤。还是那个方法,把下面的代码加到底部:
// 添加葡萄藤的各个部分
for i in 0..&length {
let vineSegment = SKSpriteNode(imageNamed: ImageName.VineTexture)
let offset = vineSegment.size.height * CGFloat(i + 1)
vineSegment.position = CGPoint(x: anchorPoint.x, y: anchorPoint.y - offset)
vineSegment.name = name
vineSegments.append(vineSegment)
addChild(vineSegment)
vineSegment.physicsBody = SKPhysicsBody(rectangleOf: vineSegment.size)
vineSegment.physicsBody?.categoryBitMask = PhysicsCategory.Vine
vineSegment.physicsBody?.collisionBitMask = PhysicsCategory.VineHolder
此循环创建了葡萄藤段的数组,数量与创建 VineModel 时指定的 length 相等。每一段都是拥有自己物理身体的子画面。这些分段是矩形的,因此我们用 SKPhysicsBody(rectangleOfSize:) 来指定物理身体的形状。
和藤架不同,葡萄藤节点是动态的,所以它们四处移动,也会受到重力的影响。
构建并运行 app,看看我们的进展。
我的天呐!葡萄藤段就像切碎的意大利面一样从屏幕上掉了下来!
添加葡萄藤的接头(Joint)
现在的问题是没有把葡萄糖段接在一起。要修复这个问题,我们需要在 addToScene() 的底部添加最后一段代码:
// 为藤架设置接头
let joint = SKPhysicsJointPin.joint(withBodyA: vineHolder.physicsBody!,
bodyB: vineSegments[0].physicsBody!,
anchor: CGPoint(x: vineHolder.frame.midX, y: vineHolder.frame.midY))
scene.physicsWorld.add(joint)
// 在葡萄藤分段间增加接头
for i in 1..&length {
let nodeA = vineSegments[i - 1]
let nodeB = vineSegments[i]
let joint = SKPhysicsJointPin.joint(withBodyA: nodeA.physicsBody!, bodyB: nodeB.physicsBody!,
anchor: CGPoint(x: nodeA.frame.midX, y: nodeA.frame.minY))
scene.physicsWorld.add(joint)
这段代码设置了分段间的物理接头,把分段连接在了一起。我们用的接头类型是 SKPhysicsJointPin,它表现的就像用锤子把两个节点钉在一起,这两个节点可以绕着钉子转动,但是不能彼此靠近或远离。
再次构建并运行。我们的葡萄藤应该已经逼真的挂在树上了。
最后一步是把葡萄藤附到菠萝上。还是在 VineNode.swift 里面,滚动到 attachToPrize()。添加如下代码:
// 连接奖励和葡萄藤的最后一段
let lastNode = vineSegments.last!
lastNode.position = CGPoint(x: prize.position.x, y: prize.position.y + prize.size.height * 0.1)
// 设置连接接头
let joint = SKPhysicsJointPin.joint(withBodyA: lastNode.physicsBody!,
bodyB: prize.physicsBody!, anchor: lastNode.position)
prize.scene?.physicsWorld.add(joint)
这段代码获取了葡萄藤的追后一个分段,并将其置于略高于奖励中心的位置。(这里用这种附加方式,实际上就把奖励挂起来了。如果死板的用中心位置,奖励的重量会被均匀分布,而且还可能会绕着轴线旋转。)我们还钉了另一个接头,把葡萄藤段附加到奖励上。
构建并运行项目。如果所有接头和节点都设置正确了,应该会看到下面这样的屏幕:
棒棒!一只挂着的菠萝——到底是谁把菠萝挂到树上的?:]
你应该已经发现了,我们还不能剪断这些葡萄藤?下面我们来解决这个小问题。
在本节中,我们会用触摸方法,让玩家可以剪断那些悬着的葡萄藤。回到 GameScene.swift,找到 touchesMoved() 然后添加如下代码:
for touch in touches {
let startPoint = touch.location(in: self)
let endPoint = touch.previousLocation(in: self)
// 检查是否切割葡萄藤
scene?.physicsWorld.enumerateBodies(alongRayStart: startPoint, end: endPoint,
using: { (body, point, normal, stop) in
self.checkIfVineCutWithBody(body)
// 产生一些好看的颗粒
showMoveParticles(touchPosition: startPoint)
这段代码的工作原理如下:对于每次触摸,都会获得它的当前和前一个位置。接下来,使用 SKScene 非常便捷的方法 enumerateBodies(alongRayStart:end:using:),遍历循环这两点间的场景中所有的身体。对于遇到的每个身体,都会调用 checkIfVineCutWithBody(),我们马上就会写这个方法。
最后,代码调用了一个方法,从 Particle.sks 文件加载并创建了 SKEmitterNode,并将其添加到场景中用户触摸的位置。这样只要拖动手指就会产生很好看的绿色烟雾踪迹(相当的秀色可餐!)
向下滚动到 checkIfVineCutWithBody() 方法,添加这段代码到方法体内:
let node = body.node!
// 如果有 name,就必然是葡萄藤节点
if let name = node.name {
// 切断葡萄藤
node.removeFromParent()
// 让所有名字匹配的节点淡出
enumerateChildNodes(withName: name, using: { (node, stop) in
let fadeAway = SKAction.fadeOut(withDuration: 0.25)
let removeNode = SKAction.removeFromParent()
let sequence = SKAction.sequence([fadeAway, removeNode])
node.run(sequence)
上面的代码首先检查连接到物理身体的节点是否有名字。记住场景里除了葡萄藤段外,还有其它节点,我们肯定不想随意一挥就不小心切断了鳄鱼和菠萝!因为我们只为葡萄藤节点命名了,所以如果节点有名字,就可以确定它是某段葡萄藤。
下一步,从场景中删除节点。删除节点还会删除它的 physicsBody,并销毁与其连接的所有接头。葡萄藤现在正式被剪断了!
最后,使用 scene 的 enumerateChildNodes(withName:using:)
遍历场景中所有与被剪断的节点名称相匹配的节点。只有相同葡萄藤中的其它段的节点会匹配,所以我们其实就是遍历被剪断的葡萄藤的分段。
对于每个节点,我们都创建了一个 SKAction 序列,首先淡出节点,然后将其从场景中删除。效果就是每个葡萄糖被切断后都会消失。
构建并运行项目。试着剪断这些葡萄藤——我们现在应该可以滑动切掉全部三个葡萄藤,然后看着奖励掉下来。漂亮的菠萝!:]
处理身体间的接触
在我们写 setUpPhysics() 方法时,把 GameScene 指定为 physicsWorld 的 contactDelegate。我们还配置了 croc 的 contactTestBitMask,以便它与奖励相交时可以收到通知。这太有远见了!
现在我们需要实现 SKPhysicsContactDelegate 的
didBegin(),当检测到两个适当的 mask body 相交时就会触发。这个方法已经有一个空壳——向下滑动找到它,然后添加如下代码:
if (contact.bodyA.node == crocodile && contact.bodyB.node == prize)
|| (contact.bodyA.node == prize && contact.bodyB.node == crocodile) {
// 把菠萝缩小出去
let shrink = SKAction.scale(to: 0, duration: 0.08)
let removeNode = SKAction.removeFromParent()
let sequence = SKAction.sequence([shrink, removeNode])
prize.run(sequence)
这段代码检查两个相交的身体是否属于鳄鱼和奖励(我们也不知道被列出的节点的顺序,所以两种组合都要检查)。如果检查通过,我们会触发一个简单的动画序列,把奖励缩小到没有,然后将其从场景中删除。
鳄鱼咀嚼动画
当鳄鱼抓住菠萝时,我们希望它能够咀嚼。在我们刚刚触发菠萝缩小动画的 if 语句中,再添加下面这行:
runNomNomAnimationWithDelay(0.15)
现在找到 runNomNomAnimationWithDelay() 并添加这段代码:
crocodile.removeAllActions()
let closeMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.CrocMouthClosed))
let wait = SKAction.wait(forDuration: delay)
let openMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.CrocMouthOpen))
let sequence = SKAction.sequence([closeMouth, wait, openMouth, wait, closeMouth])
crocodile.run(sequence)
上面的代码用 removeAllActions() 删除了当前在鳄鱼节点上的所有动画。然后创建了一个新的动画序列,张开和合上鳄鱼的嘴巴,然后让 crocodile 运行这个序列。
这个新的动画会在奖励落在鳄鱼嘴里时触发,给人一种鳄鱼正在咀嚼的印象。
把下面的代码添加到 checkIfVineCutWithBody() 的 if 语句中:
crocodile.removeAllActions()
crocodile.texture = SKTexture(imageNamed: ImageName.CrocMouthOpen)
animateCrocodile()
这样可以确保剪葡萄藤时鳄鱼的嘴是张开额,并且让它有机会掉进鳄鱼嘴里。
构建并运行。
如果菠萝落在鳄鱼的嘴里,她就会很开心的咀嚼。但如果真的发生了这种情况,游戏就会被一直挂在那里。
在 GameScene.swift 中,找到 switchToNewGameWithTransition(),添加如下代码:
let delay = SKAction.wait(forDuration: 1)
let sceneChange = SKAction.run({
let scene = GameScene(size: self.size)
self.view?.presentScene(scene, transition: transition)
run(SKAction.sequence([delay, sceneChange]))
上面的代码使用了
的 presentScene(_:transition:) 方法来呈现下一个场景。
在这种情况下,我们要切换的场景是相同的 GameScene 类的新的实例。我们还使用了 SKTransition 类传递转换效果。该转换被指定为这个方法的参数,以便我们可以根据游戏的效果使用不同的转换效果。
回滚到 didBegin(),在 if 语句里面,缩小奖励和 nomnom 动画中的后面,添加以下内容:
// 转到下一关
switchToNewGameWithTransition(SKTransition.doorway(withDuration: 1.0))
这段代码使用 SKTransition.doorway(withDuration:) 初始化方法创建了一个 doorway 转换,供 switchToNewGameWithTransition() 调用。这样就会用一种类似开门的效果显示下一关。 很简洁吧?
也许你想再给水添加一个物理身体,这样就能检测奖励是否击中了它,但如果菠萝飞到了屏幕的侧面,这就没用了。更简单、更友好的方式就是检测菠萝是否已经移动到屏幕底部,然后结束游戏。
SKScene 提供了一个 update() 方法,每帧都会调用一次。找到那个方法,添加下面的逻辑:
if prize.position.y &= 0 {
switchToNewGameWithTransition(SKTransition.fade(withDuration: 1.0))
if 语句检测奖励的 y 坐标是不是小于 0(屏幕底部)。如果是,就调用 switchToNewGameWithTransition() 再开一关,这次使用了 SKTransition.fade(withDuration:)。
构建并运行项目。
现在玩家不论成功与否,都会看到场景过渡到新场景中。
添加音效和音乐
选择了一首好听的丛林之歌,然后从 freesound.org 选了一些音效。
SpriteKit 会为我们处理音效。但是我们会用 AVAudioPlayer 在关卡转换间不间断的播放背景音乐。
给 GameScene.swift 添加另一个属性:
private static var backgroundMusicPlayer: AVAudioPlayer!
这样就声明了一个类型属性,GameScene 所有实例就都可以访问到相同的 backgroundMusicPlayer 了。找到 setUpAudio() 方法然后添加如下代码:
if GameScene.backgroundMusicPlayer == nil {
let backgroundMusicURL = Bundle.main.url(forResource: SoundFile.BackgroundMusic, withExtension: nil)
let theme = try AVAudioPlayer(contentsOf: backgroundMusicURL!)
GameScene.backgroundMusicPlayer = theme
// 无法加载文件 :[
GameScene.backgroundMusicPlayer.numberOfLoops = -1
上面的代码检查 backgroundMusicPlayer 是否已经被创建。如果没有,就用我们之前添加到 Constants.swift 的
BackgroundMusic 常量(被转化为 URL)初始化一个新的 AVAudioPlayer ,然后将其分配给属性。numberOfLoops 被设置为 -1,表示音乐会无限循环。
下一步,在 setUpAudio() 方法的底部,添加这段代码:
if !GameScene.backgroundMusicPlayer.isPlaying {
GameScene.backgroundMusicPlayer.play()
这将在场景首次加载时开始播放背景音乐(会一直播放直到 app 退出或另一个方法调用了 player 的 stop())。我们可以不用先检查 player 是否正在播放再调用 play(),但这样的话如果关卡开始时已经在播放了,音乐不会被跳过或重新开始。
现在我们还要设置一下后面会用到的音效。和音乐不同,我们不想立马播放音效。相反,我们会创建一些可复用的 SKActions,可用于稍后播放音效。
回到 GameScene 类定义的顶部,添加如下属性:
private var sliceSoundAction: SKAction!
private var splashSoundAction: SKAction!
private var nomNomSoundAction: SKAction!
现在回到 setUpAudio() 然后在方法底部添加下面几行代码:
sliceSoundAction = SKAction.playSoundFileNamed(SoundFile.Slice, waitForCompletion: false)
splashSoundAction = SKAction.playSoundFileNamed(SoundFile.Splash, waitForCompletion: false)
nomNomSoundAction = SKAction.playSoundFileNamed(SoundFile.NomNom, waitForCompletion: false)
这段代码使用 SKAction 的 playSoundFileNamed(_:waitForCompletion:) 初始化了声音动作。现在是时候播放音效了。
向上滚动到 update() 然后在 if 语句中,switchToNewGameWithTransition() 调用上方添加下面这行代码:
run(splashSoundAction)
当菠萝落在水里时,会发出溅水的声音。接下来,找到 didBegin() 然后在 runNomNomAnimationWithDelay(0.15) 这行的下方添加下面这行代码:
run(nomNomSoundAction)
当鳄鱼抓住奖励时,会发出咔嚓咔嚓的声音。最后,找到 checkIfVineCutWithBody() 然后在 if 语句中添加下面这行代码:
run(sliceSoundAction)
这样当玩家剪断葡萄藤时,就会发出挥击的声音。
构建并运行项目。
有没有发现一个 bug?如果没有击中鳄鱼,溅水的声音会播放好多次。这是因为“完成关卡”逻辑在游戏过渡到下一场景前被重复触发了。要改正的话,在类的顶部添加一个新的状态属性:
private var levelOver = false
现在修改 update() 和 didBegin(),在每个顶部添加如下代码:
if levelOver {
最后,还是在这两个方法的 if
语句中,添加一些代码以将 levelOver 状态设置为 true:
levelOver = true
现在如果游戏检测到 levelOver 标记已被设置(要么因为菠萝掉到了地上,要么因为鳄鱼迟到了东西),就会停止检查游戏的成功/失败情况,并且不会反复尝试播放这些音效。构建并运行。再也没有尴尬的音效了!
添加触觉反馈
iPhone 7 配备了一个新的 taptic 引擎,为用户提供触摸反馈。最著名的就是在手机全新的 home 键(没有可移动的部件)上模拟“点击”。但感谢 UIFeedbackGenerator 类,开发者就只要用几行代码也可以实现这个效果。
我们会用 UIImpactFeedbackGenerator 子类为 sprite 碰撞添加一些抖动。这个类有三种设置:light、medium 和 heavy。如果鳄鱼在咀嚼菠萝,我们会添加 heavy 效果。如果菠萝飞出了屏幕,会添加 light 效果。
首先,实例化反馈生成器。在 GameScene.swift 中,在 didMove() 之前添加如下属性:
let chomp = UIImpactFeedbackGenerator(style: .heavy)
let splash = UIImpactFeedbackGenerator(style: .light)
下一步,使用 impactOccurred() 方法触发反馈。滚动到 update 然后直接在 run(splashSoundAction) 下面添加如下代码:
splash.impactOccurred()
下一步,找到 didBegin() 然后在 run(nomNomSoundAction) 行下方,添加如下代码:
chomp.impactOccurred()
构建,然后在 iPhone 7 上运行一下我们的游戏,感受 haptic 反馈。
如果想更多了解 haptic 反馈,看看这个简短的视频教程,
玩了几轮后,游戏似乎显得太简单了。玩家很快就能找到合适的时间一下切断三条葡萄藤来给鳄鱼喂食。
使用之前我们设置的常量,CanCutMultipleVinesAtOnce,来让游戏变得更加棘手。
在 GameScene.swift 中,GameScene 类定义的顶部添加最后一个属性:
private var vineCut = false
现在找到 checkIfVineCutWithBody()
方法,在方法顶部添加下面的 if 语句:
if vineCut && !GameConfiguration.CanCutMultipleVinesAtOnce {
还是这个方法,在底部添加这行代码:
vineCut = true
找到 touchesMoved(),在上方添加这个方法:
override func touchesBegan(_ touches: Set&UITouch&, with event: UIEvent?) {
vineCut = false
这样当用户触摸屏幕时,就会重置 vineCut 标志。
再次构建并运行游戏。现在应该可以看到,每次滑动时只能剪断一条葡萄藤。要剪掉另外一条,需要抬起手指然后再滑一次。
在这里下载
但不要就此打住!尝试添加新的关卡,不同的葡萄藤,或者增加一个 HUD 来显示分数和时间。
如果你想多了解 SpriteKit,一定要看看这本书,
如果有任何疑问或评论,直接在下方参与讨论!

我要回帖

更多关于 使用dd 制作系统镜像 的文章

 

随机推荐