如何快速做一个糖果天天爱消除糖果罐头类游戏

You are here:
浅谈Candy类三消游戏关卡设计
作者:孙瑜
GameLook报道 / 在King发布改变手机游戏历史的Candy Crush Saga(后面简称candy)之前,提到三消游戏我们大多都只会想到宝石迷阵。但是King通过近乎完美的底层设计,丰富的特殊元素以及全新的多种关卡模式,把三消游戏从一个消费模式的游戏,变成了一个可以一直玩下去的、消费内容(关卡)的游戏,而三消的关卡设计也应运而生。
从本质上讲,关卡设计是通过在关卡地图上的摆放,对游戏底层设计、特殊元素设计以及关卡模式设计的组合和延伸。
如何设计一个好的三消类游戏关卡?
一、首先,你要有一套好玩的游戏底层设计。
游戏底层的设计包含——
1)基本元素种类(candy里面糖果的颜色,以6色为主);
2)关卡地图的大小(N*N,以9*9和8*8为主);
3)以3个同色为基本消除,那么4连、T/L连、大于等于5连、一步之后多次消除这些特殊效果的处理方式;
4)如果上一步出现特殊元素,那么他们之间交换的处理方式;
以上几点的核心就是特殊消除(3中)应该给玩家怎样的正反馈,我们能给的无非是这样几个方面——(如果是Farm Heroes类的就把“消除”两字换成“加点”)
1)线性消除;
2)范围消除;
3)同色消除;
4)随机消除。
纵观从candy出世之后直到现在的三消游戏,基本设计没有跑出过利用这4个方面的框架。
二、有了游戏底层设计,就要开始进行关卡形式的设计——换句话说,玩家怎么算完成了关卡。
游戏的关卡模式设计是三消游戏的第二根地基。在进行游戏底层设计的时候,我们明确了游戏的基本类型(主流来说是candy类的还是farm类),在这个前提下,我们需要给玩家几个关卡内的目标。
在最原始的设计中,目标就是分数的积累,显然这个设计现在已经不足以让玩家们兴奋。大多数的情况下我们把这个手法的优先级降低,作为判定关卡星级的因素——玩家分数越高,则评星越高。这样既可以作为原有关卡目标的补充,又可以和其他系统结合来促进玩家主动去玩已通过关卡的积极性。
在开心消消乐中,我们看到星级的判定会影响地图的开启、道具的获得、以及好友的排行,星级因素被充分挖掘,多种手法一起来增加已过关卡的重复利用效率。
(开心消消乐星级解决方案 )
既然星级的作用只是作为一些辅助,那么影响关卡是否完成的主要因素是什么?在candy的框架下,我们可以通过这些方式来探索关卡目标的设计出发点——
1)消除果冻的关卡——鼓励玩家在一些比较难以进行消除的地方完成消除;
2)时间模式的关卡——鼓励玩家快速找到最优消除方式(已经有些古董的模式);
3)收集掉落物品的关卡——通过规划物品的入口、路径以及出口,让游戏更加有策略性;
4)收集基本元素订单的关卡——鼓励玩家在消除的时候对一些颜色的元素更加侧重,以此增加游戏的策略性。
总结来说,(1)和(2)是对原有三消关卡形式的延续,是很多关卡必不可少的基础;而(3)和(4)则是在一定程度上消弭原来三消游戏对“好的操作”的定义,短时间内消除更多的元素不再是一个最优解,玩家需要更多的策略更多的权衡来完成关卡给出的目标。
有最优解的游戏很难对玩家产生持续的吸引力,有权衡和更多策略性的游戏才能够长盛不衰。
另外,关于在candy类游戏中加入收集基本元素订单模式的关卡,我认为并不是一个好的设计。像在Farm Heroes中,一切以收集水果为导向的底层设计,才是这个关卡类型的最好归宿。没有与手机糖果数量相配套的底层设计,candy的这类关卡显得有些尴尬。
在Farm Heroes中,关卡形式虽然只有收集固定数量的目标水果,但是我们可以看到,很多令人惊叹的特殊元素的设计,硬是在只有一个关卡类型(收集固定数目元素的订单)的前提下,让玩家几乎体验到了所有在candy中所要做出的权衡,甚至更多,尤其是“水桶”和“水滴”的设计,平衡了farm底层设计中缺乏的全局性,精彩至极。
三、有了游戏底层设计和关卡形式之后就要确定特殊元素。
Candy之所以能够从宝石迷阵的框架中跳出,就是靠着特殊元素的存在。每一个特殊元素和底层设计可以有几种组合玩法,而特殊元素之间的组合就更多,每增加一个新的特殊元素,理论上能发生的组合数目都是几何倍数的增加,这也是能够支撑三消游戏不愁没有新的好玩的关卡的底气所在。
特殊元素按照玩家主观印象可以分为“好的”、“一般的”和“讨厌的”这样三种——
1)“好的”特殊元素:
这类元素在大多数的关卡都在一定程度上能够帮助玩家完成关卡,在具体到每一个关卡的时候他们的好坏属性还可能发生逆转,但大部分时候,candy里面的鱼,4、5、T连产生的特殊水果都是玩家喜闻乐见的特殊元素,比如鱼被消除后会优先消除jelly,能够帮助玩家加快对jelly的消除;
2)“一般的”特殊元素:
这类元素比较中性,比如每次move就转换一次颜色的candy,有时惊喜有时惊吓;
3)“讨厌的”特殊元素:
这类元素在candy中大量的出现,比如巧克力、炸弹,很多时候是玩家们的噩梦。
总结来说,特殊元素在关卡中要扮演一个辅助底层设计,控制玩家感受,以此完善游戏核心玩法的角色。比如在candy中,我们见到了大量的负反馈“讨厌的”特殊元素,他们有的挡路、有的限制步数,就是在起到平衡candy底层设计正反馈过强这个前提。candy里面4连、T连和5连产生的特殊元素几乎可以到达地图的每一个点,正反馈效果极强,在这样的条件下很容易产生关卡过于简单,或者只有分数等寥寥几个控制关卡难度手段的情况,玩家会在短时间的开心之后就厌倦游戏。仔细研究candy初期的特殊元素,几乎都是在对底层设计的效果进行限制,从出现概率,到产生效果,再到对关卡最终目标的影响等方面,最终构建出了一个很赞的关卡体系。
当我们去看King的另一款游戏Farm Heroes,我们会发现好坏元素比重中,“好元素”所占比重远远大于candy,而它较弱的的底层设计正反馈也一定程度上证明了我的观点。
特殊元素的另一个作用,就是做好底层设计的补充。很多特殊元素都有比较强的可替换性,可是在candy和farm里各有一个元素几乎是不可被替换的——candy里的果冻,farm里的水桶-水滴。它们和底层设计一样,是游戏的标志和灵魂。
在candy里面,底层设计正反馈极强,地图的全局性很好。缺点就是地图的设计变得尴尬,地图的区域性变得很差,玩家几乎不需要关注某个区域的得失,这是一个很容易出现最优解的设计。果冻的存在让每一个点都可以成为这个关卡的核心所在,有它的存在地图可以充分的被挖掘。玩家在游戏过程中就必须要就是在这个区域进行一般消除,还是在其他区域特殊消除之后再杀回这个区域之间做权衡,游戏的全局性和区域性才能够相得益彰。
在farm里面同样的,底层设计正反馈很差,如果游戏的话一直在某个区域消除才是最优解(可以利用上一步中为这个区域水果增加的点数),这样直接损失了地图的全局性。水桶的出现弥补了底层设计的不足,水桶中溢出的水滴是随机性的降落,这样直接打通了这个区域和全局之间的界限,玩家又开始需要在每一次涌出水滴后权衡下一次消除的选择,是延续这个区域的优势,还是通过溢出去的水滴来迅速开启另一片优质区域,整个游戏从这里开始才变得真正有趣。
特殊元素的设计:
下面是我设计的特殊元素,仅作参考:
小虫子:小虫子占据一个格子无法被摧毁,虫子可以被和其他水果进行交换,虫子每回合自动和它上面的水果交换位置,如果被ice之类的挡住则无法上浮,如果有一只虫子到达了所在列的最高处,那么它会变成一只大虫子吃光所有的水果,游戏结束。
蜘蛛:蜘蛛每一步可以按照一个固定的方向铺设一个格子,到达最边缘则会消失(9*9)。蜘蛛只有在被触发的情况下才能铺设一次,触发条件是在它旁边有两次merge。
金属分隔线: 金属分割线是每一个小格子边缘线的一个变种,和正常的边缘线一样围绕格子存在,不占据格子的位置。 4连、T连和4连T连交换都无法穿透金属分隔线,只能在分割线的一侧产生效果,金属分隔线不可移除。
云层:云层是最上层的元素,能够遮蔽所有在它下面的元素。在云层下方的元素在被遮蔽的时候不能产生效果(如炸弹不读秒,巧克力不蔓延,但传送带可以正常运转),如果云层被驱散则可以正常产生效果。云层可以通过在旁边一次merge或者特殊效果来驱散,被驱散的云朵可以被收集。
四、现在我们开始设计关卡
就像我们在文章开头所说的那样——从本质上讲,关卡设计是通过在关卡地图上的摆放,对游戏底层设计、特殊元素设计以及关卡模式的组合和延伸。
在这个前提下,我们具体到每一个关卡中,需要注意以下几点(按照我理解的优先级排列):过程感受、pazzle和节奏设定、关卡难度、前后关卡关系、道具设置、主线关卡外的关卡以及一些其他的设计习惯
1. 过程感受:
过程感受可以理解为关卡爽快感。
玩家在玩三消类游戏的时候,如果玩家在打开这个关卡之前会有一个预期,玩的过程中会有预期,在游戏结束的 时候也会有预期。
在我看来,玩家的第一个预期——
要么抑制(看起来很难);
要么惊喜(看起来造型很美);
要么好奇(玩家上来有点看不懂但略懂);
这样对玩家的过程感受在开始容易埋下一个好的伏笔。
(看起来很难)
(看起来很美)
(看着就感到好奇)
玩家在游戏进行中,如果他能领悟这个关卡在每个时期应该进行哪些消除,那每两步应该至少有一步是应该给他相当的正反馈的,即便是关卡最终失败,游戏过程中的消除感永远不要进行抑制。
玩家关卡过程感受被抑制主要来自于这样几个方面——
1)地图设置比较逼仄,玩家每次操作几乎没有选择;(可以通过消除打开地图的不算)
2)完成关卡目标主要区域没有多少消除空间,没法影响到该区域的部分却又很多可消除的机会;
3)关卡过于复杂,让人难以理解设计目的,需要试验很多次来了解这个关卡到底是怎么工作的;
4)和最近50关内的某关很相像;
5)会带来很大挫败感的设计;
6)索然无味的关卡;
7)没有步数时总是离关卡目标差很多的关卡;
8)在10步之内完成的关卡(主要针对中后期较为成熟的关卡,不考虑引导关);
游戏结束时候如果关卡通过,那么就需要通过剩余步数奖励、评价星级、好友排名等多个纬度来给他反馈,每隔四五关让玩家产生一次想再刷一遍这个关卡的冲动为宜。当然利用已过优秀关卡的手法还有很多,比如在之前提到的开心消消乐中处理星级的手法,还有就是把设计的好、关卡过程很舒服的关卡星级设置较难,让玩家重复玩的关卡都有一定的爽快感保证。
2. Pazzle感和节奏设定
Pazzle感给我的感觉国外玩家会更喜欢一点,换句话说就是让这个关卡看起来玩起来显得“很聪明”。玩家可以通过智商来完成关卡而不仅仅是长时间游戏积累的类似下棋的手感和预判。
Puzzle感的关卡如果在国内设计,那一定要注意的是,关卡可以看起来很聪明,但绝大多数的人都可以一眼看出来这个关卡的解决思路。设计了很多的特殊元素之后,我们的确可以拼出很多看起来很聪明,而且确实需要颇费一下脑力才能想明白的关卡。但是这种关卡会带来两个不好的后果——
偏离设计目的,我们是通过这些关卡来给玩家带来的是一个三消游戏,而不是解谜游戏,这类关卡玩家一般需要尝试几次之后才能充分明白设计目的,理清解决关卡的思路,很多人会没有这样的耐心去花费时间和生命值仅仅去了解一个关卡是怎么设计的。
秀智商的产品设计永远是小众的。
节奏设定是指关卡过程中,玩家不是只有一个目标,这个关卡被分成了2-4个阶段,每一个阶段他的目标是不一样的(比如先要集中精力消除炸弹,然后打通道路,最后把要收集的ingredients收集起来),这样游戏会显得紧凑又多变,好玩度肯定会提升不少。
把一个大目标分割成一个一个小目标来逐步完成,这样设计的优点不在此赘述。我们主要谈一下分割短期目标的几个常见的手法:
通过记步(moves)的道具来给玩家短期目标;
参考特殊元素:炸弹、炸弹出口
通过固定元素来分割游戏区域,让玩家先打开地图再进行下一步;
参考特殊元素:多层饼干、绳子等可以阻挡糖果下落的固定元素
通过特殊元素的特性来划分,比如大面积的冰封通过很多4连爆炸的效果一下子解开;
参考特殊元素:4连、T连、5连等能够产生大面积消除的元素
通过特殊元素的本身性质来划分,比如风车和云彩,在风车旁边消除驱散云彩再去重点关照云层遮盖的区域;
参考图书元素:风车和云彩
通过给地图人为分层来划分,这样的关卡主要针对的是收集ingredients的关卡,每个阶段的目的一样(把ingredients移到下一层),但是通过每层设置不同环境等手法,可以让玩家在每个阶段关注的完全不同。
参考特殊元素;ingredients
在具体到每一个关卡的设计上,可以从上述几个手法中选取一个作为主要手法,然后再把其他手法作为辅助点缀在每一个小阶段中。
比如下图中的关卡(Candy Crush Level 318)——
这个关卡的主要手法是要把ingredients收集到,但完成该关卡的步骤是——
1)首先玩家要集中精力打碎左侧timebomb上面的multilayer blocker;
2)然后抓紧时间消除timebomb;
3)再然后打开左侧下方阻碍ingredients掉落到右侧的multilayer blocker;
4)最后在左侧不断制造4连T连和5连等特殊元素来帮助右侧的ingredients被最终收集到。
5)玩家在过程中不断解决小问题,得到一个又一个正反馈,最后不管是否完成任务都不会很挫败或者很疲惫。
3. 关卡难度:
关卡难度主要分为第一眼难度、过程难度和最终难度。
1) 第一眼难度就是这个关卡看起来难还是简单,一般这样处理较为妥贴:
看起来简单但是暗含玄机;(玄机不是很难,而是有趣的坎坷)看起来较难但玩的时候可以势如破竹;
2) 过程难度在很大程度上是可以和过程感受的营造划等号的。在我看来,即便关卡看起来再聪明,最终难度有多简单,不爽快的过程难度都是设计失败的关卡。
3) 最终难度就是玩家最后是否可以过关。最终难度和过程难度要有配合,但实际的关系并不是很大,通过调整order和moves,很多时候就可以让玩家用完moves的时候才意识到这个关卡“好过”还是“不好过”,最终难度的设定需要更多的根据玩家的数据和游戏收入来权衡敲定,也是游戏最大的收费点。
4. 前后关卡关系:
通过接受难易程度、是否新颖、外观效果等因素来合理安排特殊元素的介绍顺序,依此来决定每个关卡中都要出现哪些特殊元素。
在设计关卡前后关系的时候要注意以下几点:
1)不要让玩家明显感觉到他玩过这个关卡;
2)除了刚开始的一些关卡,不要连续使用几个辨识度很高的特殊元素;(不包含每个新元素出现的10-20关,它们理应在这些关卡中连续出现)
3)关卡设计总有好坏之分,好坏关卡的起伏节奏要和关卡最终难度互相配合交替循环。
想要做到这一点并不是说一直连续体验关卡凭感觉来,我们需要更多图表化和可视化的手段来进行辅助。我在安排关卡顺序的时候会常开着两个文件——一个excel文件告诉我每个关卡都包含哪些特殊元素,另一个则如下图——(这个是闲时整理的Candy Crush关卡,但可以基本表达意思)
在制作关卡的很多时候都会用到这个ppt,强力推荐三消类的关卡策划们使用这个方法。
5. 道具设置
道具设置跳不出消除指定元素道具、重置全局道具、非消除类水果交换道具等的范畴。
之所以要提一下是考虑到了道具们的摆放问题。对一些特别的关卡来说,如果有一个可以消除指定元素道具(比如锤子),那么很可能会打破这个关卡的平衡性,这一个道具在这个关卡的性价比会非常之高,那么这个道具就不该在这个关卡中出现。
6. 普通关卡外的关卡
虽然我很想把题目直接叫做“日常关卡”,但是不得不承认有时候它们也会以“隐藏关卡”、“活动关卡”等形式存在。这些关卡一般会极少用到已经介绍的特殊元素,要么不用,要么做几个只属于这种关卡的元素,在普通关卡的历程之外树立起另一套关卡体系。虽然设计的手法不尽相同,但最终的目的有如下几种——
1)在玩家没有生命(活力)的时候给玩家事情做;
2)给玩家发放道具的渠道;
3)进入游戏不论先后,都可以进行好友之间的公平PK的场所;
4)玩家在被关卡卡住的时候,依然进入游戏还可以有过关的感受;
5)不会抢了关卡主线的戏。
具备了上述所有或者大多数属性的关卡外体系,都是对现有关卡不错的补充和支持。
7. 一些其他的设计习惯
虽然需要在设计的时候给玩家很多的障碍来完成最终的关卡,但是对最终目标或者阶段目标没有明确意义的设计,哪怕是一个小格子上多了一个jelly,都会给整个关卡带来不好的影响;
设计的时候需要很多“笨功夫”,自己转换思维至少10几遍玩自己设计的关卡,才能揣摩出来更好的改进方向。我设计了很多关卡,但是极少会遇到后期不需要做出多少改进的关卡;
如果玩家在某个关卡内,明明知道接下来应该去消除什么,但是有连续5步以上都没有对最终订单产生影响(即看不到自己收集到订单里的东西有数目增加),那么就需要增加jelly等收集物来进行填充,否则会对体验产生很大的负面影响;(到关卡最后10步出现这个情况可以忽略)
五、玩家数据采集和分析
设计三消类游戏是无法避开数据的,三消游戏的数据也是最直观和易于分析的。不过这个“易于”是值相对于其他游戏而言的,实际操作起来也没那么简单。在数据的帮助下,玩家在游戏过程的乐趣也确实得到了提升,所以把数据采集和分析放到游戏设计的框架下来并没有跑题。
首先我们要知道,我们要收集哪些数据?
主要包含这些数据(可能不全):
4)app名称
7)设备类型
8)设备系统版本
13)完成关卡得分(不加剩余步数转化)
14)购买步数
15)使用步数
16)剩余步数
17)完成关卡得分(加上剩余步数转化的最终得分)
通过已有的这些数据,我们可以对玩家在每个关卡中的情况进行分析,以此为参考来进行关卡moves数,以及1、2、3星分数的设置。
初步的数据提取如下图——(用来设置move数和123星的分数)
move数的设置我还在尝试和学习中,这里不作妄论。
关于1、2、3星的设置我会采取这样的处理方式——
level 1-10 star3-100%
level 11-20 star2-10% star3-90%
level 21-30 star2-20% star3-80%
level 31-40 star1-10% star2-20% star3-70%
level 41-50 star1-15% star2-25% star3-60%
level 51– star1-20% star2-30% star3-50%
Leave a Reply
& 2017 . All rights reserved.糖果萌萌消怎么玩 特殊消除规则介绍
关注右侧公众号,回复“性爱机器人”看全文如何快速做一个糖果消除类游戏(第一节)
这段时间以来,我写了一个如何用Objective-C做一个糖果消除类的游戏教程,这是一款非常受欢迎的match-3休闲类游戏。但是我选择的是Swift制作出来的版本,所以我写了这篇文章! 在这个快速开发
本文转自,英文原文:
这段时间以来,我写了一个如何用Objective-C做一个糖果消除类的游戏教程,这是一款非常受欢迎的match-3休闲类游戏。但是我选择的是Swift制作出来的版本,所以我写了这篇文章!
在这个快速开发的教程中,您将学习到如何制作一款像糖果甜点消除冒险类的游戏,听起来真的比糖果还赞!
本教程在开发过程中,你会得到一些优秀的Swift相关的,类拷贝,加标识,闭包,拓展实践和枚举类型等技术,您还将了解到关于游戏架构方面的最佳示例。
这是一个由两章节组成的文章第一节,第一章节部分中,您将会学到基础,如游戏视图,精灵和一些逻辑检测和碰撞消除甜点的规则;
在第二部分中,您将完成游戏的开发,并且加入10个关卡到这个糖果消除冒险类游戏中去。
注意:这个快速开发教程中需要您准备Sprite Kit和Swift这些工具包,如果你初次使用Sprite Kit,请到我们的网站上看一些初学者教程或者我们的书籍,还有一些通过Swift开发IOS游戏的教程,看我们Swift的教程;
本教程需要Xcode 6&2或更高版本。写这篇教程的时候,我们选用的是Xcode6,因为当时它还处于测试阶段,所以我们选择Xcode6来做截图教程,因为我们觉得他非常好用。(任何Xcode教程里面的截图,都是objective-c的版本)
我们正式开始
开始之前,下载Swift教程的资源包,和Zip压缩文,解压后会有一个文件夹,里面包含了所有你需要的图像和音效;
启动Xcode6,Go File\New\Project&,选择IOS\Application\游戏模板,然后单击Next。填写选项如下:
* Product Name: CookieCrunch
* Language: Swift
* Game Technology: SpriteKit
* Devices: iPhone
点击下一步,选择一个文件夹单击并创建您的项目。
这是有半身像的游戏,所以打开屏幕上的设置按钮,在&常规&选项卡上,勾选上有半身像功能的部分。
开始导入图形文件,去你刚才下载的资源文件中拖动那个精灵,在Xcode项目的导航栏中选择工具。确保你需要的资源能复制到对应的目录。
现在你应该有一个蓝色文件夹存在在你的项目中:
Xcode将会自动把这个文件夹的图片纹理构建到游戏中,在游戏中使用一个纹理图的集合而不是单张图片纹理将会极大提高你的游戏绘制效率。
注意:想更多的了解图片纹理和性能问题,可以去看IOS游戏的第25章教程、&Sprite Kit 的性能和纹理集合使用&
虽然有很多的图片导入,但他们不进入一个纹理集合,因为他们不是全屏大背景图像(更多保持特效的纹理集合)或者图片,然年你将会使用UIKit界面编辑控制这些图片纹理(UIKit编辑器不能再纹理集合里面访问单张图片)
从Resources/Images文件夹中,将每个单张图片拖到asset目录中:
从asset资源目录中删除Spaceship图片。这是一个模板上的例子用的图像,但是你做哪些美味的糖果是不需要任何Spaceship的图片!:]
asset资源目录之外的项导航栏,删除GameScene.sks这个游戏中不会使用到Xcode的内置编辑器。
是时候开始写代码了!
替换掉GameViewController.Swift里的内容。接下来我们会用到Swift。
import&UIKit&import&SpriteKit&&class&GameViewController:&UIViewController&{&&&var&scene:&GameScene!&&&&override&func&prefersStatusBarHidden()&-&&Bool&{&&&&&return&true&&&}&&&&override&func&shouldAutorotate()&-&&Bool&{&&&&&return&true&&&}&&&&override&func&supportedInterfaceOrientations()&-&&Int&{&&&&&return&Int(UIInterfaceOrientationMask.AllButUpsideDown.toRaw())&&&}&&&&override&func&viewDidLoad()&{&&&&&super.viewDidLoad()&&&&&&&&&&&let&skView&=&view&as&SKView&&&&&skView.multipleTouchEnabled&=&false&&&&&&&&&&&scene&=&GameScene(size:&skView.bounds.size)&&&&&scene.scaleMode&=&.AspectFill&&&&&&&&&&&skView.presentScene(scene)&&&}&}&
这是例子代码,表示在SKView中对Sprite Kit创建和显示。
最后设置,用Swift替换GameScene的内容:
import&SpriteKit&&class&GameScene:&SKScene&{&&&init(size:&CGSize)&{&&&&&super.init(size:&size)&&&&&&anchorPoint&=&CGPoint(x:&0.5,&y:&0.5)&&&&&&let&background&=&SKSpriteNode(imageNamed:&&Background&)&&&&&addChild(background)&&&}&}&
从asset资源目录中加载这个背景图像和场景。因为现在的描点anchorPoint为(0.5,0.5),在3.5寸和4寸的设备上它总是会在屏幕居中。
然后再Build一下Run起来,太好了!
我可以有一些甜点么?
这个游戏将会从一个网格上面开始玩,有9行9列。每平方的个子里都包含一个甜点。
0列,0表示所在左下角的格子,这个(0,0)坐标点在Sprite Kit坐标系中也是一样是在屏幕的左下方,是和它吻合的,但是它和UIKit坐标系却是&颠倒&的。:]
注意:想知道Sprite Kit坐标系和UIKit坐标系为什么不一样么?这是因为OpenGLES的坐标系(0,0)在左下角,而Sprite Kit坐标系是建立在OpenGl ES之上的。
想更多的了解OpenGL ES,我们有一个视频教程系列。
想实现这一点,您需要创建一个糖果对象的类,去File\New\File&, 选择iOS\Source\Swift File template,然后单击Next。命名为Cookie.Swift单击创建。
使用Cookie.Swift内容替换如下的内容:
import&SpriteKit&&enum&CookieType:&Int&{&&&case&Unknown&=&0,&Croissant,&Cupcake,&Danish,&Donut,&Macaroon,&SugarCookie&}&&class&Cookie&{&&&var&column:&Int&&&var&row:&Int&&&let&cookieType:&CookieType&&&var&sprite:&SKSpriteNode?&&&&init(column:&Int,&row:&Int,&cookieType:&CookieType)&{&&&&&self.column&=&column&&&&&self.row&=&row&&&&&self.cookieType&=&cookieType&&&}&}&
列和行在Cookie类中表示其在2D网格中的位置。
精灵的属性是可以选择的,因此勾选SKSpriteNode的问号,因为cookie对象并不一定存在它的精灵。
cookieType属性描述的是一个cookie对象的类型,是从CookieType枚举。其实这个枚举就是从1到6得数字。但是他封装到枚举类型中是让你比较容易记住的它名称而不是它的数字。
你不需要使用cookie类型未知值,因为这个值是由特殊意义的,当您了解到这部分教程末尾的时候。每个cookie类的数字号码对应一个精灵图片:
CookieType:&var&spriteName:&String&{&&&let&spriteNames&=&[&&&&&&Croissant&,&&&&&&Cupcake&,&&&&&&Danish&,&&&&&&Donut&,&&&&&&Macaroon&,&&&&&&SugarCookie&]&&&&return&spriteNames[toRaw()&-&1]&}&&var&highlightedSpriteName:&String&{&&&let&highlightedSpriteNames&=&[&&&&&&Croissant-Highlighted&,&&&&&&Cupcake-Highlighted&,&&&&&&Danish-Highlighted&,&&&&&&Donut-Highlighted&,&&&&&&Macaroon-Highlighted&,&&&&&&SugarCookie-Highlighted&]&&&&return&highlightedSpriteNames[toRaw()&-&1]&}&
spriteName属性返回的文件名中对应的sprite图片纹理集合,出了常规的cookie精灵,玩家按下后一个高亮的版本的cookie出现。
spriteName和highlightedSpriteName属性很容易明白这个就是cookie精灵的由字符组成名称,和一个高亮的名称。找到索引,你可以用toRow()
方法枚举当前的值去转换为一个整数。回想第一个有用的cookie类型,面包,从1开始,但是数组索引是从0开始的,所以你需要在数组总索引-1。
每次一个新的cookie精灵被添加到游戏重视,它将获得一个随机的cookie类型,将会让他作为一个函数添加CookieType属性,如添加如下的枚举类型:
static&func&random()&-&&CookieType&{&&&return&CookieType.fromRaw(Int(arc4random_uniform(6))&+&1)!&}&
调用arc4random_uniform()这个方法在0和5之间生成一个随机数,然后添加1,1或6之间的数字。因为Swift的语法是非常严谨的。结果从arc4random_uniform()(一个UInt34)方法返回并转换成Int类型,然后其中()可以吧这个数字转换成适当的CookieType的值。
现在,您可能想知道,为什么你不做Cookie类的SKSpriteNode的子类了,毕竟,Cookie类是你需要在屏幕上展现出来的。
如果你熟悉模型-视图-控制器(MVC)开发模式,认为cookie是一个模型对象,简单描述了cookie的数据属性,视图是一个单独的对象,存储在精灵属性中。
这种数据模型和视图之间的分离将在我们这个教程中统一使用,MVC模式在常规应用程序中使用比游戏更常见,但您将看到,它可以使帮助我们保持简洁和灵活的代码风格。
如果你使用println()打印一个Cookie,这样做并不是很好。
当你打印一个cookie时,你想要它输出什么,你可以通过使cookie类符合可打印的协议。
要做到这点,需要修改cookie类做如下的声明:
class&Cookie:&Printable&{&&Then&add&a&computed&property&named&description:&然后添加一个用于计算的命名描述:&&var&description:&String&{&&&return&&type:\(cookieType)&square:(\(column),\(row))&&}&
现在println()将打印出有用的东西:如cookie的类型,和网络格子的行和列。以后开发中将常用到这些。
让我们将CookieType的类型也可以打印,将打印协议添加到枚举定义和描述属性并返回精灵的名称,这是一个很好的cookie类型的描述:
enum&CookieType:&Int,&Printable&{&&&...&&&var&description:&String&{&&&&&return&spriteName&&&}&}&
Cookies的2维网格形式
现在你需要将cookies看成一个9*9的网络格子。这是本教程的objective-c版本,
Cookie *_cookies[9][9];
以81为基数创建一个二维数组,你可以创建一个myCookie = _cookies[3][6],让cookie在第3列,第6行。
Swift数组,他的功能不像C的数组一样,但是,你可以编写自己的类,就像一个二维数组一样,也能很方便的使用。
操作File\New\File&,选择iOS\Source\Swift File 文件模板,然后单击Next。以Array2D.Swift文件名并单击创建。
以Swift替换Array2D的内容:
class&Array2D&T&&{&&&let&columns:&Int&&&let&rows:&Int&&&let&array:&Array&T?&&&&&&&init(columns:&Int,&rows:&Int)&{&&&&&self.columns&=&columns&&&&&self.rows&=&rows&&&&&array&=&Array&T?&(count:&rows*columns,&repeatedValue:&nil)&&&}&&&&subscript(column:&Int,&row:&Int)&-&&T?&{&&&&&get&{&&&&&&&return&array[row*columns&+&column]&&&&&}&&&&&set&{&&&&&&&array[row*columns&+&column]&=&newValue&&&&&}&&&}&}&
这个Array2D & T &符号意味着这是一个泛型类,他可以保存任何类型的元素T,您可以使用使用Array2D Cookie作为存储对象,但是在本教程以后的使用中,您将使用另外一个不同类型的对象Tile。
Array2D的初始化器创建一个常规的Swift数组有X列,且将这些元素初始化为0.当你想要一个Swift为nil空时。他需要被显示为可点击选中的状态,这就是为什么数组的属性为& T ?& T & &而不是一个单纯的数组。
怎么样才能让这个类支持加下角标。如果你知道一个特定的行和列的数据项时你可以进行如下索引操作:myCookie = cookies[column, row] .真甜蜜阿!
准备,预备&
还有一个辅助类需要编写。在Xcode 6&2中,Swift并不是来自于一组本地类型,而是一个集合,就像一个数组一样,但他允许每个元素只出现一次,而且它不将元素存放在任何特定的顺序上。
为了达到我们的目的你可以使用一个NSSet,但这不利于Swift&s 绝对类型查询。但幸运的是其实这并不难写。
Go to File\New\File&,选择iOS\Source\Swift File template文件模板,并单击Next,以Set.swift为文件名,单击Create创建。
Set.swift的内容替换为以下几点:
class&Set&T:&Hashable&:&Sequence,&Printable&{&&&var&dictionary&=&Dictionary&T,&Bool&()&&&&&&func&addElement(newElement:&T)&{&&&&&dictionary[newElement]&=&true&&&}&&&&func&removeElement(element:&T)&{&&&&&dictionary[element]&=&nil&&&}&&&&func&containsElement(element:&T)&-&&Bool&{&&&&&return&dictionary[element]&!=&nil&&&}&&&&func&allElements()&-&&T[]&{&&&&&return&Array(dictionary.keys)&&&}&&&&var&count:&Int&{&&&&&return&dictionary.count&&&}&&&&func&unionSet(otherSet:&Set&T&)&-&&Set&T&&{&&&&&var&combined&=&Set&T&()&&&&&&for&obj&in&dictionary.keys&{&&&&&&&combined.dictionary[obj]&=&true&&&&&}&&&&&&for&obj&in&otherSet.dictionary.keys&{&&&&&&&combined.dictionary[obj]&=&true&&&&&}&&&&&&return&combined&&&}&&&&func&generate()&-&&IndexingGenerator&Array&T&&&{&&&&&return&allElements().generate()&&&}&&&&var&description:&String&{&&&&&return&dictionary.description&&&}&}&
这有大量的代码但是都比较简单,你可以将元素添加到,设置,删除元素,检查那些元素目前在unionSet()方法的集合中,这个方法的作用是将两个数组组合为一个新的数组。
保证每个元素只出现一次,设置使用一个字典,就像你知道的字典需要村粗键值,而且键值是唯一的,设置存储元素作为字典的键,而不是值,保证每个元素只出现一次。
设置也符合协议generate()序列化,它将返回一个&generator&的对象,你可以循环调用它,非常方便!
这是一个初始化的方式,让我们可以实例化Array2D并使用它。
操作 File\New\File&,选择 iOS\Source\Swift File template模板文件并点击Next,命名为Level.swift,并创建。
以下的内容替换Level.swift里面的内容:
import&Foundation&&let&NumColumns&=&9&let&NumRows&=&9&&class&Level&{&&&let&cookies&=&Array2D&Cookie&(columns:&NumColumns,&rows:&NumRows)&&&}&
申明两个常量NumColumns NumRows,所以你看到9无处不在。
将对象保存为cookies的二维数组,一共是81(9行9列)。不让它被定义为var 因为设置cookies的值后将不可被改变,它和Array2D是相同的实例。
cookies数组是私有private的,所以级别需要为它提供一个获得cookies对象的方法,且在将其设置在特定的网络格子中。(注意,Swift目前不支持属性标记为private的方法。)
将下面的代码添加到Level.swift中:
func&cookieAtColumn(column:&Int,&row:&Int)&-&&Cookie?&{&&&assert(column&&=&0&&&&column&&&NumColumns)&&&assert(row&&=&0&&&&row&&&NumRows)&&&return&cookies[column,&row]&}&
使用cookieAtColumn(3、row:6)你可以查询到cookie在3列,第6行,在Array2D cookie方法被访问后返回它,注意,返回类型是cookie么?是一个可选的,并不是所有方格里都有一个cookie(他们可能为Nil空)。
注意使用assert()方法来验证行和列的有效范围。
注意:assert 这个方法是,你给他一个判定条件如果判定失败,应用程序就返回崩溃日志消息。
等一下,你可能会任务为什么我要把我的程序弄崩溃?
故意把程序弄崩溃其实是一件好事,如果你有条件,你就不要指望这样的事情发生在你的程序上,assert方法将会帮助你,因为当应用程序崩溃时,会意外回溯查询到你崩溃的状况,使你更容易的解决问题的根源。
现在来填满我们的cookies数组!稍后你讲学习如何从JSON文件中读取级别的设计,来填满数组,这样有一些现实在屏幕上。
在Level.swift中添加以下两种方法:
func&shuffle()&-&&Set&Cookie&&{&&&return&createInitialCookies()&}&&func&createInitialCookies()&-&&Set&Cookie&&{&&&var&set&=&Set&Cookie&()&&&&&&&for&row&in&0..NumRows&{&&&&&for&column&in&0..NumColumns&{&&&&&&&&&&&&&&&var&cookieType&=&CookieType.random()&&&&&&&&&&&&&&&let&cookie&=&Cookie(column:&column,&row:&row,&cookieType:&cookieType)&&&&&&&cookies[column,&row]&=&cookie&&&&&&&&&&&&&&&set.addElement(cookie)&&&&&}&&&}&&&return&set&}&
&不要担心错误消息的Set,这个问题会被解决的。
用随机的方法把把这个关卡填满cookies,现在调用createInitialCookies(),他会开始工作,这就它的作用。
* 这个方法遍历行和列的二维数组。这是本教程中你会看到很多这样的东西,记住,列0,行0是二维网络的左下角。
* 选择一个随机的cookie类型的方法。使用这个random()随机函数来添加CookieType的枚举类型。
* 接下来,用该方法创建一个新的Cookie对象,并将其添加到二维数组。
* 最后,该方法将新的Cookie对象添加到一个洗牌方法的集合中,并且返回Cookie对象给调用者。
当设计代码的时候有一个主要的问题,即如何使得不同的对象彼此之间能够联系起来。在这个游戏里,你会通过遍历一个对象集合来达到目的,通常是使用集合或者数组。
在这个案例里,在创建了一个新的层对象,使用随机用cookie来填充它,这个层响应:&我刚刚添加了一个全部由新的cookie对象组成的集合。&你可以选取这个集合,举个例子,为它里面包含的所有cookie对象创建新的精灵。实际上,这就是你将在下一部分做的事。
首先,Set其实有一个错误。因为集合是用字典键来储存它的元素的,放进集合的对象必须确定符合哈希协议。这是Swift字典键的要求。但是现在,Cookie并不确定符合Hashable,这就是为什么Xcode忐忑不安的原因。
切换到Cookie.swift,改变这个类的声明使其包含Hashable:
class&Cookie:&Printable,&Hashable&{&
将以下性能添加到类里:
var&hashValue:&Int&{&&&return&row*10&+&column&}&
哈希协议需要给此对象增加哈希值这一属性。这使得将返回的整型值独一无二。在二位坐标系里它的坐标足够确认每个cookie,而且你将会使用它来产生哈希值。
将下面的功能也写在Cookie类外:
func&==(lhs:&Cookie,&rhs:&Cookie)&-&&Bool&{&&&return&lhs.column&==&rhs.column&&&&lhs.row&==&rhs.row&}&
只要你想给一个对象添加哈希协议,那么你就应该提供==运算符,以便于两个相同类型的对象能够比较。
现在Level.swift里的错误没有了。按下Command+B编译这个app,并且确认没有任何编译错误。
场景和视图控制器
在许多Sprite Kit游戏里,场景是游戏里的主要对象。而在这款糖果消除游戏里,场景控制器将作为主要对象。
为何呢?因为这款游戏将会包含UIKit元素,比如标签,而这使得是场景控制器来管理它们。你还需要一个场景对象&&从模版创建的GameScene&&但是这一对象只负责绘制精灵;它不会控制任何游戏逻辑。
糖果消除 将会使用一个你将会从非游戏的app得知的,类似于模型-视图-控制器(MVC)模式的结构。
* 这个数据结构由层,Cookie和一些其他的类构成。这些模型将会包含数据,比如cookie对象的二维坐标,同时掌控大多数的游戏操作逻辑。
*&这个场景在一方面将会是GameScene和SKSpriteNodes,而在另一方面将会是UIViews。
* 就如经典的MVC app里一样,场景控制器在这个游戏里也会充当与其相同的角色:它会在所有的模型和视图中间同时协调整个工作。
所有的对象都能相互联系。主要是通过遍历对象的数组和集合来调整。这样彼此隔开将会让每一个对象独立地进行一个工作,完全与其他(对象)分开,这将会使得代码清晰、容易掌控。
注:将游戏数据和规则放在不同的模型对象里,对单元测试是尤其有用的。这样地分离并不能将单元测试包含其中,但是对于这样一个游戏来说,为游戏规则设立一个综合的设置测试不失为一个好办法。
如果游戏逻辑和对象都已经混淆起来了,写这些测试将会十分困难,但是在这种情况下你能与其他部分分开测试层。这样的测试会让你添加新的游戏规则的同时有自信不会破坏任何已经存在的规则。
打开GameScene.swift,将以下属性加入类:
var&level:&Level!&&&let&TileWidth:&CGFloat&=&32.0&let&TileHeight:&CGFloat&=&36.0&&&let&gameLayer&=&SKNode()&let&cookiesLayer&=&SKNode()&
这个场景中有一个公共的属性来使当前层有参照。这个变量随着层被标记!因为它初始化的时候并没有被赋值。
每个二维坐标系里的方格为32*36像素,将这些值代入TileWidth和TileHeight常量中。这些常量将会使计算cookie精灵的位置变得更简单。
为了使Sprite Kit节点层次组合紧密,GameScene将使用一些层。基础层叫做gameLayer。这是其他所有层次的容器,它在屏幕的正中间。你将把cookies精灵加在cookiesLayer这一层上,cookiesLayer是gameLayer的子层。
将下面的代码用来初始化新加入的层。把这些放在创建背景节点的代码之后:
addChild(gameLayer)&&&let&layerPosition&=&CGPoint(&&&&&x:&-TileWidth&*&CGFloat(NumColumns)&/&2,&&&&&y:&-TileHeight&*&CGFloat(NumRows)&/&2)&&&cookiesLayer.position&=&layerPosition&gameLayer.addChild(cookiesLayer)&
将两个空的SKNodes作为层添加到屏幕中,你可以将它们看做能添加其他节点的透明的平面。
要记得之前你将这个场景的锚点设置为(0,0),所以这个场景的位置默认设置为(0,0)。这意味着(0,0)是在屏幕的正中间。然而,当你把这些层作为场景的孩子添加的时候,这些层的(0,0)点也被调整到了屏幕中间。
然而,因为横坐标 0,纵坐标 0 是在二维坐标的左下角,但是你所想要的这些精灵的位置是相对于cookiesLayer的左下角。所以,你需要将这一层向下移动height的一半,向左移动width的一半。
注:因为列数NumColumns和行数NumRows都是整形,但是CGPoint的x和y的域都是浮点数,你不得不通过CGFloat(NumColumns)来转换数值。你将会在Swift代码中看见许许多多这样类似的情况。
在addSpritesForCookies()里添加场景里的精灵。加上下面的话:
func&addSpritesForCookies(cookies:&Set&Cookie&)&{&&&for&cookie&in&cookies&{&&&&&let&sprite&=&SKSpriteNode(imageNamed:&cookie.cookieType.spriteName)&&&&&sprite.position&=&pointForColumn(cookie.column,&row:cookie.row)&&&&&cookiesLayer.addChild(sprite)&&&&&cookie.sprite&=&sprite&&&}&}&&func&pointForColumn(column:&Int,&row:&Int)&-&&CGPoint&{&&&return&CGPoint(&&&&&&&&&&&x:&CGFloat(column)*TileWidth&+&TileWidth/2,&&&&&&&&&&&y:&CGFloat(row)*TileHeight&+&TileHeight/2)&}&
addSpritesForCookies()遍历了cookie里的集合,添加了对应的SKSpriteNode实例到cookiesLayer里。pointForColumn(column:, row:)用到了一个朋友的方法,将cookiesLayer相关的行列数转换为CGpoint。
跳到GameViewController.swift,给这个类增加一个新的属性。
var&level:&Level!&
接下来,添加这两个新的方法:
func&beginGame()&{&&&shuffle()&}&&func&shuffle()&{&&&let&newCookies&=&level.shuffle()&&&scene.addSpritesForCookies(newCookies)&}&
beginGame()通过调用shuffle()开始游戏。这是你调用Level的shuffle()方法的地方,返回set包含一个新的Cookie对象。切记返回的cookie对象只是模型数据;它们还没有精灵。要使它们显示在屏幕上,你必须要让FameScene为这些cookies添加精灵。
确保你在viewDidLoad的最末尾调用beginGame()来设置所要进行的行为。
override&func&viewDidLoad()&{&&&&...&&&&beginGame()&}&
创建实例Level给漏掉了。这也应该加在viewDidLoad()方法里。将下面这些代码加在显示场景(的代码)之前:
level&=&Level()&scene.level&=&level&
在创建了一个新的Level实例之后,将这一层性质设置到场景中,用来联系模型和视图。
注:声明变量level性质为Level!(增加了一个感叹号)的原因是,在一个类被初始化的时候,所有的变量都需要有一个值。但是你不能在init()里给level一个值;在viewDidLoad之前那都不会发生。使用一个!你告诉Swift这个变量在接下来之前都不会有值(但是一经设定,就不能再被赋0)。
编译运行,你会看到一些甜点~:
从JSON文件中加载关卡数据
并不是所有的甜点消除游戏都是一个简单的正方形网络格子。您将从JSON文件中添加这些格子的设计。设计五个你要加载的游戏关卡,任然是使用9*9的方格,但是他们的周边有一些空白。
将教程里面的资源文件关卡Levels拖进Xcode项目中。和往常一样,确保目的文件,是否需要检查是否复制路径。这个文件夹包含5个文件:
单击Level_1.json文件看里面内容,你会看到内容结构是一个包含三个元素的字典:tiles,moves,targetScore。
tiles数组中包含了9个数组,再关卡中每个数组标识一行。如果tiles的值为它表示有一个cookie。0意味着是空的。
你会加载这个数据到关卡中去,但是你需要添加一个新的类,Tile类,代表一个Tile二维数组。请注意,Tile比cookies是不同的
&slots&,他们分别表示不同的事物。之后我们会更多的说道这一点。
新建一个文件添加到项目中。起名为Tile.swift,将文件内容替换为:
class&Tile&{&}&
现在你可以吧这个类清空,之后,我会教你如何在这个类中为游戏添加其他的功能,如:&jelly&tiles(果冻)砖块;
打开Level.swift 并且添加如下内容:
let&tiles&=&Array2D&Tile&(columns:&NumColumns,&rows:&NumRows)&&&&func&tileAtColumn(column:&Int,&row:&Int)&-&&Tile?&{&&&assert(column&&=&0&&&&column&&&NumColumns)&&&assert(row&&=&0&&&&row&&&NumRows)&&&return&tiles[column,&row]&}&
tiles变量描述了这个关卡的结构层次,这非常类似于cookies数组,而且他还可以当做是Array2D Tile 的对象。
而cookies数组会跟Cookie对象一样放进关卡中,tiles的那些简单的关卡就可以包含一个cookie:
无论tiles[a, b]是否为空,这个格子如果是空的,那就不能装载cookie
现在水平数据的实例变量,您可以开始添加代码来填写数据。JSON文件中的关卡文件是一个字典,所以是有意义的代码加载JSON文件添加到Extensions.swift的字典。
打开File\New\File&,选择iOS\Source\Swift File template点击Next。新建名为Extensions.swift,并创建它。
添加如下代码到Extensions.swift中:
extension&Dictionary&{&&&static&func&loadJSONFromBundle(filename:&String)&-&&Dictionary&String,&AnyObject&?&{&&&&&let&path&=&NSBundle.mainBundle().pathForResource(filename,&ofType:&&json&)&&&&&if&!path&{&&&&&&&println(&Could&not&find&level&file:&\(filename)&)&&&&&&&return&nil&&&&&}&&&&&&var&error:&NSError?&&&&&let&data:&NSData?&=&NSData(contentsOfFile:&path,&options:&NSDataReadingOptions(),&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&error:&&error)&&&&&if&!data&{&&&&&&&println(&Could&not&load&level&file:&\(filename),&error:&\(error!)&)&&&&&&&return&nil&&&&&}&&&&&&let&dictionary:&AnyObject!&=&NSJSONSerialization.JSONObjectWithData(data,&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&options:&NSJSONReadingOptions(),&error:&&error)&&&&&if&!dictionary&{&&&&&&&println(&Level&file&'\(filename)'&is&not&valid&JSON:&\(error!)&)&&&&&&&return&nil&&&&&}&&&&&&return&dictionary&as?&Dictionary&String,&AnyObject&&&&}&}&
使瓦片可见
为了使cookie精灵在背景上能够更加突出一点,你可以在每一个cookie下绘制一些稍稍暗的&tile&精灵。纹理图集已经包含了这一图片(Tiled.png)。这些新的地图精灵将会在它们自己的层(tilesLayer)里。
做这个之前,先在GameScene.swift里添加一个私有属性:
然后将这段代码添加到init(size:),在你添加cookiesLayer的上方:
这个需要先做,使得瓦片出现在cookie之下(Sprite Kit节点如果有相同的zPosition则会按照添加顺序依次被绘制)。
将下面的方法也添加到GameScene.swift里
这样依次遍历所有的行和列。如果在此区域内有瓦片,则就会创建出一个新的瓦块精灵,且将它添加到瓦块层。
接下来,打开GameViewController.swift。将下面的代码添加到viewDidLoad(),紧接着设置scene.level之后:
level&=&Level(filename:&&Level_1&)&
编译运行,你就会清楚地看到这些瓦片出现了:
你也可以转化为其他层来设计,只需使得viewDidLoad()里的文件名不同即可。简单地改变文件名:参数&Level_2&P, &Level_3&P or &Level_4&P,接着编译链接。Level 3有没有使你想起什么呢?:]
自由地做自己的设计!只是切记&tiles&数组需要9个数组元素(每一行都要有一个),用九个数字(一列一个)。
在甜点消除大冒险中,玩家能够通过上下左右地点击来交换两个甜点的位置
检测点击是GameScene的工作。如果玩家在屏幕上点击一个甜点,这有可能会成为有效的交换的起点。与被点击的甜点发生交换的甜点需要检测点击的方向。
为了识别点击动作,你会使用到GameScene里的touchesBegan, touchesMoved和touchesEnded方法。即使IOS有非常方便的平台和识别点击的方法,但是这些并不能满足游戏要求的准确性和可控性。
在GameScene.swift类里添加两个私有的属性:
var&swipeFromColumn:&Int?&var&swipeFromRow:&Int?&
这些属性记录了玩家开始点击行动时,最初点击cookie的行列数。
在init(size:)最后初始化这两个属性:
swipeFromColumn&=&nil&swipeFromRow&=&nil&
nil代表着这两个属性有无效值。换句话说,他们现在还未代表任何甜点。这就是为什么它们具有选择性&&用Int?代表Int&&因为在玩家还没有点击的时候它们需要赋为nil。
现在在touchesBegan()里添加一段新的方法:
override&func&touchesBegan(touches:&NSSet!,&withEvent&event:&UIEvent!)&{&&&&&&let&touch&=&touches.anyObject()&as&UITouch&&&let&location&=&touch.locationInNode(cookiesLayer)&&&&&&let&(success,&column,&row)&=&convertPoint(location)&&&if&success&{&&&&&&&&&&if&let&cookie&=&level.cookieAtColumn(column,&row:&row)&{&&&&&&&&&&&&&&swipeFromColumn&=&column&&&&&&&swipeFromRow&=&row&&&&&}&&&}&}&
注意:这个方法需要标记覆盖,因为基类SKScene touchesBegan已经包含一个版本。这是你如何告诉迅速,您想要使用自己的版本。
游戏会调用touchesBegan()当用户将手指在屏幕上。这是什么方法,一步一步:
它将触摸位置转换为点相对于cookiesLayer。
然后,它发现如果水平网格上的接触是在一个广场通过调用一个方法你会写。如果是这样,那么这可能是滑动运动的开始。此时,您还不知道这是否广场包含一个cookie,但是至少球员把手指9&9网格内的某个地方。
接下来,cookie的方法验证联系而不是一个空的广场。
最后,它的列和行刷卡记录开始,这样你就可以比较他们后来找到的方向刷。
convertPoint()方法是新的。的相反pointForColumn(列:行:),所以你可能想添加这个方法对下面pointForColumn()附近的两个方法。
描述这两个cookies是怎么交换的,你将创建一个新的类。这是另一个模型类的唯一目的,&玩家想要把cookie A和cookie B相互交换&
创建一个文件名为Swap.swift的类,用一下内容替换它:
class&Swap:&Printable&{&&&var&cookieA:&Cookie&&&var&cookieB:&Cookie&&&&init(cookieA:&Cookie,&cookieB:&Cookie)&{&&&&&self.cookieA&=&cookieA&&&&&self.cookieB&=&cookieB&&&}&&&&var&description:&String&{&&&&&return&&swap&\(cookieA)&with&\(cookieB)&&&&}&}&
现在你有一个对象,你可以尝试着描述它的交换过程,问题来了,谁将处理实际执行交换的逻辑呢?在GameScene去刷新检测逻辑的发生,但目前为止所有真正的游戏逻辑是在GameViewController里面。
这就意味着GameScene必须有一个回调方式在GameViewController刷新检测到有效的动作后,就执行交换。具体回调方法是通过一个委托协议实现,但是由于这个消息是唯一的。GameScene必须使用一个闭包方式从GameViewController返回。
在GameScene.swift的顶部添加以下属性:
var&swipeHandler:&((Swap)&-&&())?&
看起来很吓人&这个变量类型是((Swap) -& ())?。因为-&能告诉你这是一个闭包功能的函数。这个闭包函数可以接受一个交换对象作为参数,不返回任何值。问号表明swipeHandler可以为空(是一个可选状态)。
这是实际的流程处理。如果他识别到用户刷帧动作时,她将会调用存储在刷帧的关闭处理。这是如何回到GameViewController中进行通讯呢?需要进行互换的地方。
还是在GameScene.swift中添加如下代码到trySwapHorizontal得底部,取代println()语句:
if&let&handler&=&swipeHandler&{&&&let&swap&=&Swap(cookieA:&fromCookie,&cookieB:&toCookie)&&&handler(swap)&}&
这将创建一个新的交换的对象,填写两个需要交换的cookies,然后调用刷新处理程序。因为swipeHandler可能为0,你可以绑定一个有效的参数去使用。
GameViewController将决定是否交换结果是有效地,如果是,你需要消除这两个cookies。在GameScene.swift中添加以下方法来做到这一点:
func&animateSwap(swap:&Swap,&completion:&()&-&&())&{&&&let&spriteA&=&swap.cookieA.sprite!&&&let&spriteB&=&swap.cookieB.sprite!&&&&spriteA.zPosition&=&100&&&spriteB.zPosition&=&90&&&&let&Duration:&NSTimeInterval&=&0.3&&&&let&moveA&=&SKAction.moveTo(spriteB.position,&duration:&Duration)&&&moveA.timingMode&=&.EaseOut&&&spriteA.runAction(moveA,&completion:&completion)&&&&let&moveB&=&SKAction.moveTo(spriteA.position,&duration:&Duration)&&&moveB.timingMode&=&.EaseOut&&&spriteB.runAction(moveB)&}&
这是SKAction动画的基础代码:你将cookieA移动到cookieB的位置,反之一样。
交换cookieA出现在上面的动画实现看起来的效果最好,所以这个方法调整的相对zPosition两个cookie精灵来实现上面的效果。
动画完成后,调用cookieA动作完成调用者所需要继续进行的事情,对这个游戏这是一种常见的开发模式:游戏等待动画完成之后然后再继续做之后的事情。
()- &()可以简单归纳为一个闭包,它无返回参数。
现在您已经处理完视图。但还得再对model controller数据控制进行处理,打开Level.swift添加以下方法:
func&performSwap(swap:&Swap)&{&&&let&columnA&=&swap.cookieA.column&&&let&rowA&=&swap.cookieA.row&&&let&columnB&=&swap.cookieB.column&&&let&rowB&=&swap.cookieB.row&&&&cookies[columnA,&rowA]&=&swap.cookieB&&&swap.cookieB.column&=&columnA&&&swap.cookieB.row&=&rowA&&&&cookies[columnB,&rowB]&=&swap.cookieA&&&swap.cookieA.column&=&columnB&&&swap.cookieA.row&=&rowB&}&
使第一个临时复制的行和列的Cookie对象,因为他们已被赋值。交换它们并组成新的Cookie数组,并且需要同步他们Cookie对象里面的行和列。则就是它的数据层model。
到GameViewController.swift添加以下方法:
func&handleSwipe(swap:&Swap)&{&&&view.userInteractionEnabled&=&false&&&&level.performSwap(swap)&&&&scene.animateSwap(swap)&{&&&&&self.view.userInteractionEnabled&=&true&&&}&}&
你需要告诉程序第一个关卡,并且更新数据层,然后告诉执行动画进行交换,从而更新你的视图层。在本教程中,您将在其他的游戏逻辑中用到这个函数。
而动画正在执行,你不希望玩家再去触碰任何东西,所以你需要关掉userInteractionEnabled视图。你将它完成之后再传递执行animateSwap();
注:以上使用所谓的后关闭语法,关闭写在函数调用。另一种方法把它写如下:
scene.animateSwap(swap,&completion:&{&&&self.view.userInteractionEnabled&=&true&})&
也可以在之前添加以下代码viewDidLoad():
scene.swipeHandler&=&handleSwipe&
执行这个 handleSwipe GameScene swipeHandler(swap)的这个函数。现在每当GameScene调用swipeHandler(swap)时,它实际上在GameViewController调用一个函数。这样写有点奇怪,因为在Swift中可以使用这个函数进行闭包和互换的功能啊。
Build 和 Run 一下这个程序,你现在可以交换cookies了!同时,试着在隔行不通的情况下交换一下!
高亮的Cookies
在糖果消除的时候,会有一个瞬间闪亮的点。你可以实现这种效果在Cookie被消除的时候将图片缩放一下成为一个亮点。
纹理图凸显了一个更灵活,智能,饱和的cookie精灵。CookieType enum已经有一个函数来返回这个图片的名字叫:highlightedSpriteName
你会改善GameScene在现有的cookie中添加这个高亮后的图片。添加新的精灵,而不是替换现有的这些精灵的纹理。这样能个更容易让原始的图片有淡出淡入的效果。
在GameScene.swift中添加一个新的私有的类:
var&selectionSprite&=&SKSpriteNode()&
添加以下方法:
func&showSelectionIndicatorForCookie(cookie:&Cookie)&{&&&if&selectionSprite.parent&!=&nil&{&&&&&selectionSprite.removeFromParent()&&&}&&&&if&let&sprite&=&cookie.sprite&{&&&&&let&texture&=&SKTexture(imageNamed:&cookie.cookieType.highlightedSpriteName)&&&&&selectionSprite.size&=&texture.size()&&&&&selectionSprite.runAction(SKAction.setTexture(texture))&&&&&&sprite.addChild(selectionSprite)&&&&&selectionSprite.alpha&=&1.0&&&}&}&
这从Cookie中精灵的名称更形象化了。并且将相应的纹理选择为精灵。简单的设置精灵的纹理不让他执行SKAction,并且设置正确的大小。
你也可以通过设置alpha to 1来选择精灵是否可见。您添加一个精灵作为一个孩子节点的精灵,cookie的动作以及精灵交换时的动画。
相同的方法,添加hideSelectionIndicator():
func&hideSelectionIndicator()&{&&&selectionSprite.runAction(SKAction.sequence([&&&&&SKAction.fadeOutWithDuration(0.3),&&&&&SKAction.removeFromParent()]))&}&
这个方法移除了一个精灵。
它会继续调用这个方法。但是首先执行touchesBegan(),如果cookie=其中一个节点,那么就添加:
showSelectionIndicatorForCookie(cookie)&
执行调用touchesMoved(),然后再添加trySwapHorizontal():
hideSelectionIndicator()&
有一个方法叫做hideSelectionIndicator()。如果用户只是触摸在屏幕上,而不是点击,你想更加淡出淡入的现实你的精灵,那么需要在这行顶部添加touchesEnded():
if&selectionSprite.parent&!=&nil&&&&swipeFromColumn&!=&nil&{&&&hideSelectionIndicator()&}&
&Build and run, 我们可以看到一些高亮的cookies了!
以智能的方式填充数组
这个游戏的目的是使3个或3个以上相同的cookies连接在一起。但是现在当你运行程序,他们只是在屏幕上相连而已。这不是我们希望的,我们希望用户通过互换两个cookies后生改变或者生成新的cookies在屏幕上。
这是你的规则:当轮到玩家移动时,无论是在游戏开始或者结束时,不得在黑色的版面上进行匹配了。保证在这种情况下进行游戏,需要我们有更智能的方式去填满我们的cookies数组。
到Level.swift中找到createInitialCookies()方法用以下代码cookieType来替换它:
var&cookieType:&CookieType&do&{&&&cookieType&=&CookieType.random()&}&while&(column&&=&2&&&&&&&&&&&&cookies[column&-&1,&row]?.cookieType&==&cookieType&&&&&&&&&&&&cookies[column&-&2,&row]?.cookieType&==&cookieType)&&&&||&(row&&=&2&&&&&&&&&&&&cookies[column,&row&-&1]?.cookieType&==&cookieType&&&&&&&&&&&&cookies[column,&row&-&2]?.cookieType&==&cookieType)&
天哪!这些都是什么?这段逻辑是说随机去选择cookie并且去确保他们不会去创建三个或三个以上的链接数组。
在这段代码里就像下面这样:
do&{&&&generate&a&new&random&cookie&type&}&&while&there&are&already&two&cookies&of&this&type&to&the&left&&&&or&there&are&already&two&cookies&of&this&type&below&
如果新的随机生成的数连是3个连成一串,那么必须重复再执行该函数,知道找到不创建出3个连成一串的随机数链接。所以它只能判断左右侧是否存在cookies。
运行程序试一下,验证以下在游戏开始的时候不会出现任何两个相同项链的cookies。
并不是所有的格子都可以交换
你只希望通过交换两个cookies来导致这些cookies能链接成三个或者更多。
你需要添加一些游戏逻辑来检测验证是否存在交换后达相连的结果。有两种方法可以做到这一点。最简单的方法是查询目前用户所在尝试的交换。
你可以建立一个tutorial-you的列表,里面包括且或的关系,然后尝试将他们缓慢交换移动。然后你只需要检查尝试交换的这些列就可以了。
注意:建立这个可连接交换列表还可以轻松的提示玩家进行游戏,你不会在本教程中坐,但是在消除类游戏中,如果你不玩几秒钟,比赛时程序会提供给你一个可交换的点。你可以通过这个点去实现这个随机生成出来的表的动作。
在Level.swift中添加新的代码:
var&possibleSwaps&=&Set&Swap&()&&&
再一次的告诉你,如果你使用一组数组时,在这个集合中得顺序不重要,集合包含了可交换的对象。如果玩家想交换两个不同的cookies集合,那么在游戏中不会执行这个动作,动作是无效的。
Xcode警告,不能用户一组交换,因为没有实现Hashable方法。
打开Swap.swift 添加Hashable方法:
class&Swap:&Printable,&Hashable&{&
然后再这个类里面添加Hashable的属性:
var&hashValue:&Int&{&&&return&cookieA.hashValue&^&cookieB.hashValue&}&
这个简单的散列集合结合两个cookies的运算。这是iyigechangjian的运算散列值。
最后,可以在函数外添加 == 的写法:
func&==(lhs:&Swap,&rhs:&Swap)&-&&Bool&{&&&return&(lhs.cookieA&==&rhs.cookieA&&&&lhs.cookieB&==&rhs.cookieB)&||&&&&&&&&&&(lhs.cookieB&==&rhs.cookieA&&&&lhs.cookieA&==&rhs.cookieB)&}&
现在你可以在编辑器中使用交换的对象在一组数组里面。
每个回合开始的时候,你需要检测一下cookies在时候可以被玩家互换。如果要需要,那么要执行shuffle()。回到 Level.swift 中将下面代码添加进去:
func&shuffle()&-&&Set&Cookie&&{&&&var&set:&Set&Cookie&&&&do&{&&&&&set&=&createInitialCookies()&&&&&detectPossibleSwaps()&&&&&println(&possible&swaps:&\(possibleSwaps)&)&&&}&&&while&possibleSwaps.count&==&0&&&&return&set&}&
像之前一样,执行createInitialCookies来填补随机的cookie的格子。然后在它调用之后再执行一个新的方法,detectPossibleSwaps(),来填满新的possibleSwaps集合。
在非常少见的情况下,你最终分配运行任何互换的cookies,这个循环会一直执行下去,你可以测试这个小的网格,比如3*3的网格,我有一个这样的,在项目中叫做Level_4.json。
还有执行使用一个辅助方法detectPossibleSwaps(),看看cookie相连的部分,接着添加如下方法:
func&hasChainAtColumn(column:&Int,&row:&Int)&-&&Bool&{&&&let&cookieType&=&cookies[column,&row]!.cookieType&&&&var&horzLength&=&1&&&for&var&i&=&column&-&1;&i&&=&0&&&&cookies[i,&row]?.cookieType&==&cookieT&&&&&&&&&&--i,&++horzLength&{&}&&&for&var&i&=&column&+&1;&i&&&NumColumns&&&&cookies[i,&row]?.cookieType&==&cookieT&&&&&&&&&++i,&++horzLength&{&}&&&if&horzLength&&=&3&{&return&true&}&&&&var&vertLength&=&1&&&for&var&i&=&row&-&1;&i&&=&0&&&&cookies[column,&i]?.cookieType&==&cookieT&&&&&&&&&&--i,&++vertLength&{&}&&&for&var&i&=&row&+&1;&i&&&NumRows&&&&cookies[column,&i]?.cookieType&==&cookieT&&&&&&&&&++i,&++vertLength&{&}&&&return&vertLength&&=&3&}&
链表是连续三个或三个以上相同类型的cookie的行或者列,这种方法可能看起来有点奇怪,那是因为在for-statements有很多内部的逻辑。
给定一个特定的cookie在一个特定的方格网络内,看这个方法的左边,只要找到相同类型的cookie,他就会增加horzLength,简洁的表达了这行代码的意思。
for&var&i&=&column&-&1;&i&&=&0&&&&cookies[i,&row]?.cookieType&==&cookieT&--i,&++horzLength&{&}&
这个for循环有一个空的身体。这意味着所有的逻辑发生在其参数。
for&var&i&=&column&-&1;&&&&&&i&&=&0&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&cookies[i,&row]?.cookieType&==&cookieT&&&&&&--i,&&&&&&&&&&&&&&&&&&&&&&++horzLength&&&&&&&&&&&&&&{&}&&&&&&&&&&&&&&&&&&&&&
您还可以使用一段语句写出来,但是for循环运行你把所有内容都放在一行上面去写。:]有的循环是正常的上下结构;
既然你有这个方法,你可以实现detectPossibleSwaps()。这是如何运行在更高级的关卡上的。
* 它是一个具有行和列的二维网络,只有一个cookie在他旁边进行交换,一次一个。
* 如果交换这两个cookie并创建了一个3个的链,将她添加到一个新的交换possibleSwaps中去进行交换。
* 然后,它将交换的cookie回复为原来的状态,继续下一个cookie直到可交换为止。
* 它将经过上述步骤连续两次,1次检查所有的网格竖直互换一次检测所有的垂直互换。
func&detectPossibleSwaps()&{&&&var&set&=&Set&Swap&()&&&&for&row&in&0..NumRows&{&&&&&for&column&in&0..NumColumns&{&&&&&&&if&let&cookie&=&cookies[column,&row]&{&&&&&&&&&&&&&&&&&}&&&&&}&&&}&&&&possibleSwaps&=&set&}&
这是非常简单的方法:遍历行和列,并对每一个点进行检测,如果有一个cookie可以交换,而不是一个空的网格,他将执行检测的逻辑。最后,这个方法将放进possibleSwaps方法中。
检测将这两个独立的部分,但是这样是在不同方向上做同样的事情。首先你想交换左右两边的cookie,然后又想交换上下的。记住0行是底部所以你可以定制你的方法。
添加如下代码在这里检测消除逻辑:
&if&column&&&NumColumns&-&1&{&&&&&&if&let&other&=&cookies[column&+&1,&row]&{&&&&&&&&&&cookies[column,&row]&=&other&&&&&cookies[column&+&1,&row]&=&cookie&&&&&&&&&&&if&hasChainAtColumn(column&+&1,&row:&row)&||&&&&&&&&hasChainAtColumn(column,&row:&row)&{&&&&&&&set.addElement(Swap(cookieA:&cookie,&cookieB:&other))&&&&&}&&&&&&&&&&&cookies[column,&row]&=&cookie&&&&&cookies[column&+&1,&row]&=&other&&&}&}&
尝试着交换当前右边的cookie,如果存在。并且导致这一连串的cookie凑成了三个或三个以上,那将这个交换后生成的链对象添加到这个集合。
将下面以下的代码添加到上面去:
if&row&&&NumRows&-&1&{&&&if&let&other&=&cookies[column,&row&+&1]&{&&&&&cookies[column,&row]&=&other&&&&&cookies[column,&row&+&1]&=&cookie&&&&&&&&&&&if&hasChainAtColumn(column,&row:&row&+&1)&||&&&&&&&&hasChainAtColumn(column,&row:&row)&{&&&&&&&set.addElement(Swap(cookieA:&cookie,&cookieB:&other))&&&&&}&&&&&&&&&&&cookies[column,&row]&=&cookie&&&&&cookies[column,&row&+&1]&=&other&&&}&}&
这样是再做完全相同的事情,但是对上下的cookie,而不是左右的cookie。
总之这个算法执行后,将检查它时候能导致交换后相连,然后撤销,记录每一个它发现的链表。
现在运行程序,你应该会看到类似Xcode这种的调试面板:
possible&swaps:&[&swap&type:SugarCookie&square:(6,5)&with&type:Cupcake&square:(7,5):&true,&&swap&type:Croissant&square:(3,3)&with&type:Macaroon&square:(4,3):&true,&&swap&type:Danish&square:(6,0)&with&type:Macaroon&square:(6,1):&true,&&swap&type:Cupcake&square:(6,4)&with&type:SugarCookie&square:(6,5):&true,&&swap&type:Croissant&square:(4,2)&with&type:Macaroon&square:(4,3):&true,&&.&.&.&
换还是不换...
我们要充分利用这个列表举出来的例子。在Level.swift添加以下方法:
func&isPossibleSwap(swap:&Swap)&-&&Bool&{&&&return&possibleSwaps.containsElement(swap)&}&
这看起来可能交换的设置是否包含了指定的交换对象,但是稍等...当你执行刷帧时,GameScene会创建一个新的交换对象。怎么isPossibleSwap()可能会发现内部的对象列表?它可能有一个交换对象,描述完全相同,但实际实例在内存中是不相同的。
当您运行set.containsElement(object)时,设置他去调用它所包含的对象来判断是否是匹配的对象。因为你已经为交换提供了一个 == 操作符,这个自动运行操作,没事儿,交换对象实际上是不同的实例;只要两个可换的对象类型是相同的就行。
最后在GameViewController.swift 调用,并且添加handleSwipe() 这个方法:
func&handleSwipe(swap:&Swap)&{&&&view.userInteractionEnabled&=&false&&&&if&level.isPossibleSwap(swap)&{&&&&&level.performSwap(swap)&&&&&scene.animateSwap(swap)&{&&&&&&&self.view.userInteractionEnabled&=&true&&&&&}&&&}&else&{&&&&&&view.userInteractionEnabled&=&true&&&}&}&
现在这个游戏中只会执行在可换列表中认可的交换对象。
Build and run 。如果他们导致相链,那他们就能互换。
注意,执行互换后,&有效的互换&列表就成了无效的了。所以我们得在下一个部分去修改。
要对无效的互换进行又去的动画展示,所以我们在GameScene.swift添加以下方法:
func&animateInvalidSwap(swap:&Swap,&completion:&()&-&&())&{&&&let&spriteA&=&swap.cookieA.sprite!&&&let&spriteB&=&swap.cookieB.sprite!&&&&spriteA.zPosition&=&100&&&spriteB.zPosition&=&90&&&&let&Duration:&NSTimeInterval&=&0.2&&&&let&moveA&=&SKAction.moveTo(spriteB.position,&duration:&Duration)&&&moveA.timingMode&=&.EaseOut&&&&let&moveB&=&SKAction.moveTo(spriteA.position,&duration:&Duration)&&&moveB.timingMode&=&.EaseOut&&&&spriteA.runAction(SKAction.sequence([moveA,&moveB]),&completion:&completion)&&&spriteB.runAction(SKAction.sequence([moveB,&moveA]))&}&
这种方法类似于animateSwap(swap:, completion:),但在这里cookies将会获得新的位置,然后立即翻转回来。
在GameViewController.swift,改一下else-clause handleSwipe():
}&else&{&&&scene.animateInvalidSwap(swap)&{&&&&&self.view.userInteractionEnabled&=&true&&&}&}&
现在运行以下这个程序,试着做一个交换动作,不会导致相链状态。
做一些音效
在本教程第一部分结束的时候,为何不继续添加一些游戏的音效呢?打开教程资源文件里面的声音文件拖动到XCode中。
在GameScene.swift中添加一些音效的属性:
let&swapSound&=&SKAction.playSoundFileNamed(&Chomp.wav&,&waitForCompletion:&false)&let&invalidSwapSound&=&SKAction.playSoundFileNamed(&Error.wav&,&waitForCompletion:&false)&let&matchSound&=&SKAction.playSoundFileNamed(&Ka-Ching.wav&,&waitForCompletion:&false)&let&fallingCookieSound&=&SKAction.playSoundFileNamed(&Scrape.wav&,&waitForCompletion:&false)&let&addCookieSound&=&SKAction.playSoundFileNamed(&Drip.wav&,&waitForCompletion:&false)&
不是每次要播放一个声音就要建一个SKAction动作,可以让他们复用,你就可以只加载这些声音一次。
然后添加 animateSwap()到函数的底部:
runAction(swapSound)&
再添加animateInvalidSwap()到函数底部:
runAction(invalidSwapSound)&
这就是需要的制造一些音效。Chomp!:]
是从哪来到这里的呢?
下面是这个Swift中到目前为止的所有代码。
你的游戏很好,但是还有一段路还没完成。现在给自己半块cookie吧。
在下面一节的部分中,您将实游戏中的其他部分如:删除消除匹配成链的cookie。用新的cookie去取代之前的。会增加你的得分,播放大量的新的动画。
在你吃cookie时,花点时间让逛逛我们的论坛!
参与人员名单: 插画。 音乐。音效是来自中找出来的。
部分源码的灵感来自于的游戏。
CocoaChina是全球最大的苹果开发中文社区,官方微信每日定时推送各种精彩的研发教程资源和工具,介绍app推广营销经验,最新企业招聘和外包信息,以及Cocos2d引擎、Cocos Studio开发工具包的最新动态及培训信息。关注微信可以第一时间了解最新产品和服务动态,微信在手,天下我有!
请搜索微信号“CocoaChina”关注我们!
关注微信 每日推荐
扫一扫 浏览移动版}

我要回帖

更多关于 天天爱消除桃花缘糖果 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信