diff --git a/Beginning-Unity-3d-For-ios-Part1.md b/Beginning-Unity-3d-For-ios-Part1.md index fadfbea..4b0c010 100644 --- a/Beginning-Unity-3d-For-ios-Part1.md +++ b/Beginning-Unity-3d-For-ios-Part1.md @@ -1,4 +1,4 @@ -#Unity 3D中级教程:iOS篇-第1/3篇 +# Unity 3D中级教程:iOS篇-第1/3篇 本教程由“We Make Play”创始人[Joshua Newnham](http://www.raywenderlich.com/about#joshuanewnham)编写。“We Make Play”是一家专为新兴平台打造创造性游戏的独立工作室。 @@ -19,7 +19,7 @@ Unity可以说是iOS上最流行的3D游戏引擎,有很多优点。 让我们动手吧~:] -#设计App +# 设计App 和任何app一样,在开始编码前首先要确定你要制作什么以及为什么要做这个!要明确你的目标用户,你的创意,为何创意能吸引他们,以及app拥有的特性。 首先来解决上面的问题。 @@ -44,19 +44,19 @@ Unity可以说是iOS上最流行的3D游戏引擎,有很多优点。 嗯,看起来不错。是时候来完成需要的功能和组件了。 -##玩法/互动: +## 玩法/互动: * 目标:在限定的时间内尽可能的得分 * 玩家用手指点屏幕,点的时间越长,投篮的力量就越大。但是点的时间太长的话就犯规了。 -##特点: +## 特点: * 视觉效果丰富,引人入胜,吸引玩家 * 简单的支持按钮(开始游戏的按钮浮在游戏场景上) * 逼真的篮板球效果 * 随着时间的流逝,通过更频繁的移动球场周围的化身来增加难度,玩家也更适应游戏。 -##资源及其特点: +## 资源及其特点: * 环境 @@ -73,7 +73,7 @@ Unity可以说是iOS上最流行的3D游戏引擎,有很多优点。 好了,app的基本设定已经完成,开始创作吧!:] -#Unity 3D简介 +# Unity 3D简介 如果你已经安装了Unity,可以跳过本节。 @@ -92,7 +92,7 @@ Unity可以说是iOS上最流行的3D游戏引擎,有很多优点。 在开始之前,快速浏览一下Unity的UI,它是所有Unity工程的命令中心。 -#Unity接口 +# Unity接口 来快速浏览一下Unity接口。如果你已经很熟悉了,可以直接就跳过本节。 @@ -140,7 +140,7 @@ Unity的UI由5个独立的面板组成,彼此联系紧密,又从不同的角 到目前为止,你对UI有了基本的认识。下一节我们将试着创建Unity场景。 -#游戏资源 +# 游戏资源 本节将介绍如何倒入资源到Unity,Unity如何处理资源,并快速介绍材质以及其作用。 @@ -186,7 +186,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: 如果模型是灰色的,很有可能是材质和纹理没有关联上。下一节-材质和纹理-我将会解释关联是如何工作的,并教会你如何解决没关联上的问题。 -#材质和纹理 +# 材质和纹理 模型和材质是一体的。为材质指定着色器,决定Unity如何根据光线、常规映射(normal mapping)以及像素数据来渲染图片(着色器是图形流水中的小程序,判决模型顶点位置,以及模式如何光栅化到2D屏幕)。有些着色器可能是处理器密集型的,因此最好为材质指定移动设备专用的着色器。 @@ -210,7 +210,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: 有些自元素不太一样,这取决于3D建模工具。在Blender中,BPlayer和BPlayerSkeleton是对象,BPlayer(BPlayerSkeleton下面)是描述球员地理位置的网格数据,剩下的是动作帧,承载球员的更多信息。 -#设置场景 +# 设置场景 本节中我们将可视化的设置场景。目标是积累一些Unity场景环境的经验,了解Unity提供的跟多的**Components**,并最终完成场景。 @@ -244,7 +244,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: 现在你完成了场景布局,是时候来配置计分牌了。 -##从场景中分离计分牌 +## 从场景中分离计分牌 目前,计分牌还是背景的一部分,但是我们希望它能分离出来。在结构面板视图中,将它从场景中拖到结构面的根节点中。出现下面的对话框-点击Continue。 @@ -276,7 +276,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: ![controla panel](http://cdn3.raywenderlich.com/wp-content/uploads/2012/09/Screen-Shot-2012-09-25-at-7.48.54-PM.png) -##打开光源 +## 打开光源 有没有注意到场景很暗啊?这是因为场景没有光源,那就来给它光源! @@ -290,7 +290,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: ![directional ligth](http://cdn3.raywenderlich.com/wp-content/uploads/2012/09/Setting-up-the-Scene-Directional-Light.png) -##摄像机位置 +## 摄像机位置 现在聚焦(绝对双关语义)到摄像机的位置上。这不是一门科学,而是艺术。像个导演一样,利用场景面板的移动和旋转工具拖拽摄像机到合适的位置。 @@ -302,7 +302,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: 现在场景看起来相当不错!是不是像个真导演?:] -#Unity物理:碰撞和身体 +# Unity物理:碰撞和身体 现在为**GameObjets**添加**Components**,这样他们就能够彼此交互。 @@ -324,7 +324,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: 使用物理的最好方法是实践-赋予篮球弹性吧。 -#让篮球弹起来 +# 让篮球弹起来 选中篮球,选择**Component > Physics > Rigidbody**为其添加一个刚体。然后点击播放按钮,位于Unity的上面中间位置,进行预览-你可以看到篮球穿过了地板。 @@ -371,7 +371,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: >注意:选择碰撞器的对象,然后按下Shift,会出现碰撞器的控制柄,这样就可以用鼠标可视化的改变碰撞器大小。 -#约见球队 +# 约见球队 游戏中,我们希望球员能运球,而球不会穿过球员的身体。这很简单:为球员**GameObject**添加一个**Capsule Colldier**。 @@ -393,7 +393,7 @@ Unity能够自动检测倒入的资源的类型,并设置一些缺省属性: ![add box collider](http://cdn2.raywenderlich.com/wp-content/uploads/2012/09/Screen-Shot-2012-10-06-at-1.25.00-PM.png) -#预制件(Prefabs)-及如何用好预制件 +# 预制件(Prefabs)-及如何用好预制件 本节不是教程的必须内容,可能以后对你会有用。如果你要休息一下,可以跳过本节。 @@ -407,7 +407,7 @@ Unity为我们提供了**预制件**。它允许我们创建一个对象的主 >注意:要更新**预制件**,随便找一个这种类型的**预制件**,进行更新,然后从工具栏中选择**Game Object -> Apply Changes To Prefab**。修改会自动的更新到所有关联的对象! -#何去何从 +# 何去何从 恭喜你!你刚完成了最困难的部分-作为新手适应Unity GUI。从今往后将会一帆风顺! diff --git a/Beginning-Unity-3d-For-ios-Part2.md b/Beginning-Unity-3d-For-ios-Part2.md index 70d801e..d9697c8 100644 --- a/Beginning-Unity-3d-For-ios-Part2.md +++ b/Beginning-Unity-3d-For-ios-Part2.md @@ -1,4 +1,4 @@ -#Unity 3D中级教程:iOS篇-第2/3篇 +# Unity 3D中级教程:iOS篇-第2/3篇 欢迎回到我们的Unity 3D iOS中级系列教程。 @@ -20,7 +20,7 @@ 让我们开始吧! -##确保所有人都很好的跟上节奏 +## 确保所有人都很好的跟上节奏 在你深入研究代码之前,先很快的过一眼下面的图表,它标出了你将要加入到游戏中的每一个部件的功能与职责,以及这些部件之间的关系: ![Class diagram for app](res/class-diagram-1-678x500.png) @@ -32,7 +32,7 @@ 最后,你的游戏没有**Ball**怎么行呢?这个对象负责在**Ball**穿过篮网以及落在地面时触发特定事件,表明玩家当前回合结束。 -##脚本,脚本,脚本 +## 脚本,脚本,脚本 Unity提供了若干不同脚本语言可供选择;包括Boo(不,我不是在吓你,这[真是一种语言](http://boo.codehaus.org/)),Javascript(也就是UnityScript)以及C#。通常来说,如果你之前是做前端网页开发出身,那么UnityScript也许是最好的选择。 不过,如果你更熟悉C++,Java,Objective-C或者C#,那么C#对你的脚本编写任务而言就更合适。考虑到本网站的大部分读者都有Objective-C的背景,在本教程中你将用C#来写脚本。 @@ -73,7 +73,7 @@ public class DummyScript : MonoBehaviour { 注意:MonoBehaviour还有一个更新方法叫做FixedUpdate()。这个方法是被物理引擎调用的,并且应该只被用来更新刚体或者其他基于物理的属性。它被称为FixedUpdate是因为它保证在固定间隔时间后调用,而不像Update()方法那样,因为是每一个tick都被调用,两次调用中间相差的时间可能会不同。 ``` -##ScoreBoard +## ScoreBoard 我们先从最简单的**ScoreBoard**脚本开始。你已经创建了脚本文件,所以只用将它改名成**ScoreBoard**,然后双击打开即可。 啊哈,我打赌你还不知道Unity自带MonoDevelop! @@ -129,7 +129,7 @@ public class ScoreBoard : MonoBehaviour 这样就把**3D文本**子对象与脚本属性联系了起来。非常简单不是吗? -##测试时间 +## 测试时间 在继续下去之前,让我们确保所有的一切都正常工作。为了做到这一点,我们要创建一个新的脚本,用来更新你的记分牌上的时间和分数。创建一个叫**ScoreboardTest**的新脚本,并将以下代码复制进去: ``` using UnityEngine; @@ -154,7 +154,7 @@ public class ScoreBoardTest : MonoBehaviour 可以工作了——你应该看到记分牌上的文本变成像上面这张截图一样。如果没有,从头检查每一个步骤看看哪里可能出了问题。 -##控制碰撞 +## 控制碰撞 现在是时候来看看Unity是怎么处理对象碰撞的了。 回忆一下,Ball对象是负责在自己入网并且/或者落地时通知**GameController**的。这个Ball对象是绑在一个**球状碰撞体(SphereCollider)**以及**刚体(Rigidbody)**上的,这让你可以检测碰撞并对此作出反应。在你的脚本里,你可以监听这些碰撞事件,并正确的通知**Game Controller**。 @@ -309,7 +309,7 @@ public void OnTriggerEnter( Collider collider ) { 球相关的部分就告一段落——但是现在Player需要开始在这个游戏里出场了:)但在这以前,先让我们确认一切都进展顺利。 -##测试时间 +## 测试时间 首先,你需要创建一个Ball脚本依赖的GameController脚本的占位符,这样才能让所有代码跑起来。创建一个新脚本取名为**GameController**,并将以下代码替换到内容里: ``` using UnityEngine; @@ -387,7 +387,7 @@ public class BallTest : MonoBehaviour { 现在你知道碰撞在正常工作了,可以继续设置Player了! -##Player框架 +## Player框架 现在你会实现一个Player的占位符代码。稍后**GameController**完成后再回来继续完善。 创建一个新脚本并命名为**Player**,在MonoDevelop里面输入以下内容: @@ -494,7 +494,7 @@ public float MyProperty{ Player对象目前就到这里,这个占位符的实现非常简单,我们可以昵称player为“Stubby”:)让我们先去继续完善GameController的其它内容! -##GameController +## GameController GameController负责协调游戏的各种活动,并处理用户输入。 “协调活动”是什么意思呢?游戏通常内部是以状态机的形式运转。游戏的当前状态决定了当前需要跑哪部分代码,用户输入如何中断,以及台前幕后发生哪些事情。 @@ -503,7 +503,7 @@ GameController负责协调游戏的各种活动,并处理用户输入。 你已经创建了**GameController**的起始脚本——让我们进一步充实它。 -#变量 +# 变量 首先要声明需要的变量。因为GameController的主要工作是协调游戏里的所有实体,你需要通过变量对其中大部分对象进行引用,还需要通过变量来管理游戏数据(例如当前分数,剩余时间等)。 加入以下代码来声明变量(作用在代码注释中标出): @@ -524,7 +524,7 @@ private Vector3 _orgPlayerPosition; 将gameSessionTime(玩家需要在此时限内得分)以及throwRadius(你的篮球玩家可以从他的当前起始位置移动多远)暴露成公共变量意味着你可以在做测试时很方便的对他们的值进行调整。 -#游戏状态 +# 游戏状态 你已经给你的Player对象增加了一些状态,现在给游戏也增加一些状态: ``` public enum GameStateEnum @@ -583,7 +583,7 @@ public GameStateEnum State { ``` 如上所示,在属性里存放状态使你可以轻松的感知状态变化,并根据需要执行相关必要逻辑。 -#辅助方法与属性 +# 辅助方法与属性 接下来你将增加一些辅助方法与属性。 首先如下增加StartNewGame(开始新游戏)方法: @@ -644,7 +644,7 @@ public float TimeRemaining { 辅助方法和属性就完成了,是时候搞定大块头Update了! -#让一切实时更新 +# 让一切实时更新 现在你需要将注意力转移到Update方法及其附属方法,是它们保证了GameController像钟表一样走时。在GameController中增加如下代码: ``` void Update () { @@ -730,7 +730,7 @@ if( (player.State == Player.PlayerStateEnum.Miss || player.State == Player.Playe 然后检查是否还有剩余时间。如果没有,则更新状态为GameOver,否则让篮球玩家移到一个新位置继续投篮。 -#测试时间 +# 测试时间 你已经在Hierarchy中创建了一个GameController对象,并在上面挂上了GameController脚本,所以你已经准备好了。 在Hierarchy面板中选择GameController对象,注意GameController现在有player,scoreboard和basketball的公共属性。在Inspector里通过拖拽把这些属性设置为相应的对象。 @@ -739,7 +739,7 @@ if( (player.State == Player.PlayerStateEnum.Miss || player.State == Player.Playe 现在当你点击play按钮,就可以看到随着GameController每秒递减时间,记分牌上的数值也在相应更新! ![game controller test](res/unity3d-game-controller-test-1.png) -#处理用户输入 +# 处理用户输入 在很多时候,越是接近项目完成,你就越会发现你忙于在桌面电脑开发以及移植到真实设备这两个状态之间切换。所以你需要同时处理两种输入:设备触摸屏,以及鼠标键盘。 要做到这一点,首先在GameController里面加入一个帮助方法来检测app是否在移动设备上运行: @@ -802,7 +802,7 @@ TouchCount和TouchDownCount的唯一区别是TouchCount返回的是正接触着 对于Unity的Input类的整体概述,可以参考[Unity官方站](http://docs.unity3d.com/Documentation/ScriptReference/Input.html)。 -#球的处理:和消息打交道 +# 球的处理:和消息打交道 回忆Ball对象会在两种情况下给GameController发送消息:球过网时,以及球触地时。 重写OnBallCollisionEnter方法来处理球触地的情况: @@ -843,7 +843,7 @@ public void HandleBasketBallOnNet(){ } ``` -#处理来自Player部件的消息 +# 处理来自Player部件的消息 另一个和GameController交互的部件是Player。到目前为止这里还只有占位符,但你要在GameController里实现消息和事件的处理。Player在动画播完的时候抛出事件,GameController的游戏状态的update随之触发。 在Start()的结束处增加如下代码来注册事件: @@ -865,7 +865,7 @@ public void HandlePlayerOnPlayerAnimationFinished (string animationName) 教程的下一部分会将这些事件都联系起来并让你最终进行若干次投篮! -##Player:不再是占位符! +## Player:不再是占位符! 让我们快速回顾下Player的任务以及需要的功能: - 在Idle的时候,Player需要运球 - 在Play的游戏状态,Player需要对用户输入进行反应,在这里当用户手指接触屏幕时,Player应该上好发条准备投篮。 @@ -876,7 +876,7 @@ public void HandlePlayerOnPlayerAnimationFinished (string animationName) 回过头来打开Player脚本,让我们跟着代码慢慢前进。 -#角色动画 +# 角色动画 Unity提供了丰富的类来处理3D动画的导入与使用。当你导入这个用Blender创建的player,就是导入了一套动画打成的包。如果在编辑器选择Player对象的Animation部件,能看到如下画面: ![player animations](res/player-animations.png) @@ -981,7 +981,7 @@ private void OnAnimationFinished () 最后,OnAnimationFinished()负责抛出相关事件,从而通知GameController动画已完成,使其了解到Player GameObject的当前状态和动作。 -#测试时间 +# 测试时间 让我们确保你的动画都正确的设置好并且可运行。为了做到这一点,在你的Player的start方法后面加上这一行: ``` CurrentAnimation = animPrepareThrow; @@ -996,7 +996,7 @@ CurrentAnimation = animPrepareThrow; 注意:在继续之前,记得再次启用GameController,并且移除测试代码。 ``` -#管理状态 +# 管理状态 是时候充实你之前创建的State属性了;但是在此之前先让我们写好要用到的方法的占位符。 ``` private void AttachAndHoldBall(){ @@ -1059,7 +1059,7 @@ CancelInvoke("OnAnimationFinished"); 下一段有意思的代码是关于PlayerStateEnum.Walking的;在这段代码里,你根据目标(投球)位置与你当前位置的比较来判断玩家需要前进还是后退从而确定动画。 -#测试时间 +# 测试时间 与上面类似,让我们进行个快速测试来检查你的状态和动画是否协调正确工作。在你的Player类的Start方法增加如下代码: ``` State = PlayerStateEnum.Score; @@ -1072,7 +1072,7 @@ State = PlayerStateEnum.Score; 注意:在继续之前记得再次启用你的GameController并且移走测试代码。 ``` -#运球 +# 运球 篮球player的职责之一是在等待用户输入时运球。在这部分我们将看到如何实现这个功能。 首先在你的Player类的开头声明以下变量: @@ -1234,7 +1234,7 @@ public class PlayerBallHand : MonoBehaviour 最后,选中BallPhyMat并将bounciness设成1,以保证篮球弹起时有足够的动力。 -#测试时间 +# 测试时间 你已经写了很多代码,现在是时候测试是否一切都在正常工作了。就像以前做过的那样,如下修改Start方法对状态进行设置以测试弹球: ``` State = PlayerStateEnum.BouncingBall; @@ -1250,7 +1250,7 @@ State = PlayerStateEnum.BouncingBall; 注意:在继续之前记得重新启用GameController并且移除测试代码。 ``` -#投球 +# 投球 首先在Player类的开头声明以下变量: ``` public float maxThrowForce = 5000f; @@ -1297,7 +1297,7 @@ if (_state == PlayerStateEnum.Throwing ) { 当播放完毕(没投中并且球落地或者入网并且球落地),GameController随机选择一个新的投球位置,并通知玩家移动到那个位置。 -#请就位 +# 请就位 增加以下变量到Player类的开头: ``` public float walkSpeed = 5.0f; @@ -1362,7 +1362,7 @@ if (_state == PlayerStateEnum.Walking) { 就是这样——你终于都写完了,是时候测起来了:) -#测起来! +# 测起来! 终于可以全跑起来了!点击play按钮来开始你的游戏。有好几个细节需要根据你的设置进行调整: - 你可以通过在游戏区域按下手指并保持,然后放手来投篮。如果效果不对,可以修改Player的ThrowDirection变量——我的调整为了X=1,Y=0.75,Z=0。 @@ -1378,7 +1378,7 @@ if (_state == PlayerStateEnum.Walking) { 再花点时间来看代码——这个教程的目的是帮助你对Unity的脚本入门,并了解事件处理机制。 -#接下来? +# 接下来? 这里有本教程到此完成的[示例项目](http://cdn3.raywenderlich.com/downloads/NBN_Part2.zip)。菜单选择File\Open Project,点击Open Other,然后通过文件夹定位,就可以在Unity打开。注意场景不会被默认加载——需要选择Scenes\GameScene来打开它。 请关注教程的第三部分,你将学到如何给主菜单做简单的UI! diff --git a/Beginning-Unity-3d-For-ios-Part3.md b/Beginning-Unity-3d-For-ios-Part3.md index fc6f6b9..b3a3c6d 100644 --- a/Beginning-Unity-3d-For-ios-Part3.md +++ b/Beginning-Unity-3d-For-ios-Part3.md @@ -1,547 +1,547 @@ -# 中级IOS unity 3d开发:第三部分 # - -这是一篇由 Joshua Newnham 提供的指导文档,它是一个让我们更有效,更独立,更专业的创建数字产品的新兴平台。 - -欢迎回到中级Unity 3D ios开发指引! - -在第一部分当中,你已经学会如何使用Unity来布局游戏场景,并添加物理碰撞检测。 -在第二部分当中,你已经学会如何使用Unity来编写脚本,创建游戏逻辑,比如射击或者投篮。 - -在最后这部分中,你将学会如何创建游戏用户界面系统和游戏控制系统。 - -现在让我们开始吧! - -## 上机实验 ## - -到目前为止,你已经尝试过unity内置的模拟器。但现在是时候去真机上测试你的这些功能了。 - -首先通过点击File->Build settings 打开创建窗口.你将会看到如下窗口: -![](http://i.imgur.com/Rz3S9fr.png) - -请确保你已经正确选择了IOS平台(IOS的后面会有一个Unity的图标显示,如果没有的话点击IOS的图标,点击switch platform按钮) - -选择“Player Settings”来打开检查面板 - - -![](http://i.imgur.com/xJN9632.png) - -在这个面板中,有很多的选项,但到目前为止,你只需要关心以下内容。 - -在“Resolution and Presentation”面板中,选择Landscape-Left项,其它的都使用默认的选项,然后点击Other Settings. - -在这里,你需要选择你的开发环境相关内容,(就像使用XCODE一样) 其它的使用默认。 - -## 关键步骤 ## - -之前你已经设定好相关内容,回到Build Settings对话框开始创建。 - -Unity将会提示你为项目选择目标平台。如果你之前选择的是XCODE,Unity将会使你的项目可以在XCode中编译并在设备上运行。 - -提示:不要在模拟器中运行游戏因为Unity中的库只能运行在IOS设备中。在IOS设备上运行Unity项目需要证书,app帐号和许可证件。请在[http://answers.unity3d.com/questions/20775/the-game-exporting-proccess-for-iphone.html](http://answers.unity3d.com/questions/20775/the-game-exporting-proccess-for-iphone.html "这里")查看更多相关内容。 - - -![](http://i.imgur.com/9q4EZhB.png) - - - -这一些都设定好之后,你需要继续为游戏来创建一个简单的菜单。 - - -## 继续 ## - -首先[cdn3.raywenderlich.com/downloads/NBNResources.zip](cdn3.raywenderlich.com/downloads/NBNResources.zip "资源")下载该资源,这里包括了一些支撑游戏的脚本文件。解压主该 zip 文件,并把.cs文件托到Unity的script文件夹中。 -这些脚本文件可能已经超出了该教程的范围,所以你需要一个实验来知道如何使用它。 -在你的游戏场景中创建一个新的GameObject,将LeaderboardController.cs与该对象关联起来。 -创建一个新的脚本文件,命名为LeaderboardControllerTest,并将它与一个新的GameObject对象进行关联。你将执行实现存储分数增减一的例子。 -首先,引用LeaderboardController中的内容,添加到你的LeaderBoardControllerTest类中,内容如下: - - using UnityEngine; - using System.Collections.Generic; - using System.Collections; - public class LeaderboardControllerTest : MonoBehaviour { - public LeaderboardController leaderboard; - - void Start () { - } - - void Update () { - } - } - -提示:请注意一下usingSystem.Collections.Generic; -在类的上方,这句话包括了 System.Collections.Generic的包引用,因为你可以使用Generic specific colections。关于Generic更详细的解释请看这里。[http://www.codethinked.com/an-overview-of-system_collections_generic](http://www.codethinked.com/an-overview-of-system_collections_generic) - -使用LeaderboardController中的AddPlayersScore方法来添加玩家的分数: - - void Start () { - leaderboard.AddPlayersScore( 100 ); - leaderboard.AddPlayersScore( 200 ); - } - -当你把游戏关闭后,玩家的分数需存储到本地磁盘。所以你需要在LeaderboardControllers中添加OnScoresLoaded事件,并实现这个事件处理,代码如下: - -另外,由于异步调用的原因,你可以不在一开始就调用。 - - void Start () { - leaderboard.OnScoresLoaded += Handle_OnScoresLoaded; - - leaderboard.AddPlayersScore( 100 ); - leaderboard.AddPlayersScore( 200 ); - - leaderboard.FetchScores(); - } - - public void Handle_OnScoresLoaded( List scores ){ - foreach( ScoreData score in scores ){ - Debug.Log ( score.points ); - } - } - -函数的参数是一个ScoreData类型的list对象表,ScoreData是一个简单的数据结构,用来存储分数记录。 - -Handle_OnScoresLoaded方法会遍历所有你记录的分数。 - -好了!现在执行如下几步: - -1. 创建一个新的GameObject,命名为LeaderboardController,并将脚本LeaderboardController与它关联。 - -2. 选择LeaderboardControllerTest对象,并将LeaderboardController对象与分数榜的属性关联。 - -3. 点击开始,你应当在控制台看到分数的输出日志。 - -## 创建简单的菜单 ## - -现在到了最让人兴奋的步骤了-你将要学习如何为游戏创建菜单! - -这是一张最张的效果图: - -![](http://i.imgur.com/pnPUPKV.png) - -在Unity中一共有三种方式可以用来实现用户界面。每一种都有它自己的优点和缺点。下面是每一种方式的详细说明。 - -1:GUI -Unity提共了预定义用户控制集合,可以通过使用GUI Component 中 MonoBehaviour的OnGUI方法。 -在Unity中使用Skins也可支持自定义界面。 - -对于场景来说,这不是一个结点,预先提供丰富的控制集是种完美的解决方案。然而,出于表现效果考虑,在游戏中不建议使用。 - -2:GUITexture and GUIText -Unity提供了两个组件,GUITexture和GUIText.这些组件使你可以在屏幕上绘制出2D图像和文字。你可以很轻易的扩展并创建出与GUI形成对比的的用户界面。 - -3:3D Planes/Texture Altas -如果你正在创建平视显示游戏(简称HUD,比如在游戏中显示的菜单),那么该方式是一个不错的选择;虽然你可能需要付出更多!但是一但你创建好这样的类,你可以将他们用于任何一个新的工程中。 - -3D Planes使用 3D 平面和纹理集来实现HUD显示模式,一个纹理集收集了许多张分离的图像在一张大图中。就像cocos2d中精灵一样。 - -这些纹理是可以共用的元素,通常只需要调用一次来绘制到屏幕。在更多的情况下,你需要设定一个专用的镜头,以达到预期的效果。 - -在本教程中你将要使用的是第一种方式-GUI。不管怎样,它有许多特性,使本教程更简单一些。 - -现在将通过SKINS创建主菜单,然后你将通过代码控制菜单绘制,最后将他们通过GameController拼合到一起。 - -听起来不错吧?是时候开始Skins了。 - -## Skins ## - -Unity提供了一个叫做Skin的来实现自定义 GUI 元素。 -它可以被看做成一个使用HTML标签相联的样式表。 - -我创建了两个skin文件,一个大小为480X320,另一个大小为960X640。下面是一张480X320 skin属性面板的截图。 - -![skin.png](http://cdn4.raywenderlich.com/wp-content/uploads/2012/08/skin.png) - -在Skin的属性显示界面暴露出了许多可以让你创建出独一无二风格样式的属性。在本工程中你只需要设定字体。 - -打开选择GUI下的GameMenuSmall,将scoreboard字体拖拽到字体属性中,并把字号大小设置成16。打开选择GUI下的GameMenuNormal,将scoreboard字体拖拽到字体属性中,并把字号大小设置成32。 - -下一步,是实现真正的菜单。 - -##Main Menu - -这节将会提供GameMenuController的源代码,它用来绘制菜单界面并响应用户的输入。你将会看到该代码的重要部分,并很快将它应用到你的游戏中! - -创建新的脚本文件命名为GameMenuController,并在 其中添加如下内容: - using UnityEngine; - using System.Collections; - - [RequireComponent (typeof (LeaderboardController))] - public class GameMenuController : MonoBehaviour { - - public Texture2D backgroundTex; - public Texture2D playButtonTex; - public Texture2D resumeButtonTex; - public Texture2D restartButtonTex; - public Texture2D titleTex; - public Texture2D leaderboardBgTex; - public Texture2D loginCopyTex; - public Texture2D fbButtonTex; - public Texture2D instructionsTex; - - public GUISkin gameMenuGUISkinForSmall; - public GUISkin gameMenuGUISkinForNormal; - - public float fadeSpeed = 1.0f; - private float _globalTintAlpha = 0.0f; - - private GameController _gameController; - private LeaderboardController _leaderboardController; - private List _scores = null; - - public const float kDesignWidth = 960f; - public const float kDesignHeight = 640f; - - private float _scale = 1.0f; - private Vector2 _scaleOffset = Vector2.one; - - private bool _showInstructions = false; - private int _gamesPlayedThisSession = 0; - } - -在代码的开始,定义了一些从变量的名字就可以看出其用意的公共变量,它们是用来绘制出GameMenuController菜单的基本元素。之后 ,你需要引用定义上一节创建的Skins。 - -以上就是你在主菜单中需要用到的所有变量了。 - -我们还引用 了GameController和LeaderboardController以及用来存储玩家分数的ScoreData。 - -定义了这些之后,你还需要一个衡量玩家界面大小的变量,比如不同大小屏幕,Iphone3GS是480X320 ,Iphone4是960X640。 - -最后定义了于些用来管理GameMenuController组件状态的变量 。 - -下一步将Awake()方法和Start()方法添加进来,代码如下: - - void Awake(){ - _gameController = GetComponent(); - _leaderboardController = GetComponent(); - } - - void Start(){ - _scaleOffset.x = Screen.width / kDesignWidth; - _scaleOffset.y = Screen.height / kDesignHeight; - _scale = Mathf.Max( _scaleOffset.x, _scaleOffset.y ); - - _leaderboardController.OnScoresLoaded += HandleLeaderboardControllerOnScoresLoaded; - - _leaderboardController.FetchScores(); - } - -在Start()方法中,通过LeaderboardController请求了玩家的分数。当然,一些图形图像的比率也被计算出了自适应的大小。 - -代码中的测量偏移变量用来确保界面元素能够适当的显示出来。比如,如果游戏菜单被设计成在屏幕大小为960X640上显示,但当前屏幕的大小是480X320,你就需要把当前的界面缩小50%;代码中的变量scaleOffset就是0.5。如果只是简单的界面,这样做将会节省大量的成本,并且对设备多样化有实质性的意义。 - -当分数被加载后,本地缓存中的分数将会被绘制到界面当中。 - - public void HandleLeaderboardControllerOnScoresLoaded( List scores ){ - _scores = scores; - } - -##测试一下 - -让我们先暂停一下,并测试一下现有内容。 - -将下面的代码添加到GameMenuController当中: - - void OnGUI () { - GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex ); - if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ - Debug.Log( "Click" ); - } - } - -上面的代码中有一个叫做OnGUI的方法。这个方法类的调用类似于Update()方法,是Gui Component的入口。GUI Component提供了一套实现用户界面控制的静态方法,点击[这里]([http://docs.unity3d.com/ScriptReference/MonoBehaviour.OnGUI.html](http://docs.unity3d.com/ScriptReference/MonoBehaviour.OnGUI.html))可以从Unity网站中获得更多关于OnGUI和GUI相关内容。 - -通过调用OnGUI方法你可以使用下面代码通过缓存来在整个屏幕进行绘制: - - GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex ); -下面是一句使用GUI..Button方法的条件语句,GUI.Button方法会在给定位置绘制一个按钮(使用你之前计算出来 的偏移变量)。该方法为boolean类型,如果用户点击就会返回true,否则返回false。 - - if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ - Debug.Log( "Click" ); - } - -在这句代码中,如果用户点击了该按钮,就会在控制台输出“Click”。 - -测试前,需要将GameMenuController脚本和GameController脚本关联到游戏对象上,然后将所有内容关联到适当的对象 -和属性中,就像下图这样: - -![skin.png](http://cdn5.raywenderlich.com/wp-content/uploads/2012/10/unity3d-gui-properties.png) - -现在运行游戏,点击开始,你将会看到一个按钮出现。点击它,在控制台会看到一条消息。 - -![unity3d-ongui-test.png](http://cdn4.raywenderlich.com/wp-content/uploads/2012/10/unity3d-ongui-test.png) - -还不错吧?制作主菜单第一步,完成! - -##使用自定义界面 - -现在你已经有了正确的方向,让我们继续设计出一个适合屏幕大小的自定义界面。 - -使用下面的代码替换OnGUI方法: - - if( _scale < 1 ){ - GUI.skin = gameMenuGUISkinForSmall; - } else{ - GUI.skin = gameMenuGUISkinForNormal; - } -上面的代码可以确保你使用的是正确的字体大小(依懒于屏幕大小);你之前计算的scale大小,决定了你使用哪一种自定义界面。如果该值小于1.0,那么 将会使用较小的,否则就会使用正常的。 - -##显示和隐藏 - -收到用户请求后,与其立刻生硬的做出响应,不如让他逐渐的来显示 。为了达到该效果,你需要有技巧的处理这些颜色变量(这个会影响到所有GUI类的绘制)。 - -为了达到这个效果,你需要以像素为单位,将透明度从0到1,并将这个值赋值给GUI中的颜色变量。 - -将以下代码添加到你的OnGUI方法中: - - _globalTintAlpha = Mathf.Min( 1.0f, Mathf.Lerp( _globalTintAlpha, 1.0f, Time.deltaTime * fadeSpeed ) ); - - Color c = GUI.contentColor; - c.a = _globalTintAlpha; - GUI.contentColor = c; - -你还需要一种实现菜单显示或隐藏的方法,所以创建2个公共的方法,命名为Show()和Hide(),代码如下: - - public void Show(){ - // ignore if you are already enabled - if( this.enabled ){ - return; - } - _globalTintAlpha = 0.0f; - _leaderboardController.FetchScores(); - this.enabled = true; - } - - public void Hide(){ - this.enabled = false; - } - -没有什么 不可能实现的!你需要一批新的分数,以防玩家出现新的分数。所以你需要将全局色彩值设置成0,来隐藏./显示开始/暂停按钮在OnGUI调用。(比如,如果该按钮被隐藏,那么所有的更新方法,比如update,FixedUpdate,OnGUI都不会再被调用)。 - -你的菜单显示什么 取决于你当前游戏的状态, 比如,游戏结束状态会与游戏暂停状态绘制完全不同的内容。 - -在OnGUI方法中添加如下代码: - -GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex ); - - if( _gameController.State == GameController.GameStateEnum.Paused ){ - if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ - _gameController.ResumeGame(); - } - - if (GUI.Button( new Rect ( 229 * _scaleOffset.x, 357 * _scaleOffset.y, 100 * _scale, 100 * _scale ), restartButtonTex, GUIStyle.none) ){ - _gameController.StartNewGame(); - } - } else{ - if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), playButtonTex, GUIStyle.none) ) - { - if( _showInstructions || _gamesPlayedThisSession > 0 ){ - _showInstructions = false; - _gamesPlayedThisSession++; - _gameController.StartNewGame(); - } else{ - _showInstructions = true; - } - } - } -这些对你来说看起来比较熟悉,你绘制的所有图像纹理和按钮都取决于GameController是什么状态。 - -当游戏暂停的时候,游戏界面会显示出两个按钮-回到游戏,重新开始: - - if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ - _gameController.ResumeGame(); - } - - if (GUI.Button( new Rect ( 229 * _scaleOffset.x, 357 * _scaleOffset.y, 100 * _scale, 100 * _scale ), restartButtonTex, GUIStyle.none) ){ - _gameController.StartNewGame(); - } - -提示:如何更好的获得这些做标点和大小?我是很费力的在创建界面的时候从绘图程序中拷贝来的。 - -另外一个游戏状态就是游戏结束,就是当你显示play按钮的时候。 - -提示:也行你对_showInstructions 和 _gamesPlayedThisSession这2个变量有疑问。 - -_gamesPlayedThisSession 是用来决定你可是第几次游戏,如果你是初次打开该游戏,那么_showInstructions 在游戏开始前会被设置为真。这时游戏在开始前会展示一些新手教程。 - -##测试时间 - -在你完成GameMenuController前,让我们确定一下每一部分都能按预期效果工作。如果是一步一步走下来的话,开始游戏后会看到类似如下效果: -![unity3d-ongui-test-2.png](http://cdn4.raywenderlich.com/wp-content/uploads/2012/10/unity3d-ongui-test-2.png) - -##完成GameMenuController - -最后没有的就差标题(title),指令(instructions)和分数(score). - -画标题还是新手教程取决于instruction的标识,在OnGUI方法的后面添加如下代码: - - if( _showInstructions ){ - GUI.DrawTexture( new Rect( 67 * _scaleOffset.x, 80 * _scaleOffset.y, 510 * _scale, 309 * _scale ), instructionsTex ); - } else{ - GUI.DrawTexture( new Rect( 67 * _scaleOffset.x, 188 * _scaleOffset.y, 447 * _scale, 113 * _scale ), titleTex ); - } -注意 ;最后一步是分数面板。OnGUI提示了 分组归类,分组归类允许你把这些东西按钮垂直或者水平进行布局。下面的代码 画出了leaderboard和一些仿facebook/Twritter的按钮,并将他们添加到了组中。在OnGUI方法中添加下面的代码: - - GUI.BeginGroup( new Rect( Screen.width - (214 + 10) * _scale, (Screen.height - (603 * _scale)) / 2, 215 * _scale, 603 * _scale ) ); - - GUI.DrawTexture( new Rect( 0, 0, 215 * _scale, 603 * _scale ), leaderboardBgTex ); - - Rect leaderboardTable = new Rect( 17 * _scaleOffset.x, 50 * _scaleOffset.y, 180 * _scale, 534 * _scale ); - if( _leaderboardController.IsFacebookAvailable && !_leaderboardController.IsLoggedIn ){ - leaderboardTable = new Rect( 17 * _scaleOffset.x, 50 * _scaleOffset.y, 180 * _scale, 410 * _scale ); - GUI.DrawTexture( new Rect( 29* _scaleOffset.x, 477* _scaleOffset.y, 156 * _scale, 42 * _scale ), loginCopyTex ); - if (GUI.Button( new Rect ( 41 * _scaleOffset.x, 529 * _scaleOffset.y, 135 * _scale, 50 * _scale ), fbButtonTex, GUIStyle.none) ) - { - _leaderboardController.LoginToFacebook(); - } - } - GUI.BeginGroup( leaderboardTable ); - if( _scores != null ){ - for( int i=0; i<_scores.Count; i++ ){ - Rect nameRect = new Rect( 5 * _scaleOffset.x, (20 * _scaleOffset.y) + i * 35 * _scale, 109 * _scale, 35 * _scale ); - Rect scoreRect = new Rect( 139 * _scaleOffset.x, (20 * _scaleOffset.y) + i * 35 * _scale, 52 * _scale, 35 * _scale ); - - GUI.Label( nameRect, _scores[i].name ); - GUI.Label( scoreRect, _scores[i].points.ToString() ); - } - } - GUI.EndGroup(); - GUI.EndGroup(); - - } -这就是GameMenuController,完成并将它添加到GameController类中(控制界面显示或者隐藏),打开GameController并按照下面的方法使用它。 - -打开GameController并在开始声明如下变量: - - private GameMenuController _menuController; - private LeaderboardController _leaderboardController; - public Alerter alerter; - -在Awake()方法中h引用他们: - - void Awake() { - _instance = this; - _menuController = GetComponent(); - _leaderboardController = GetComponent(); - } -最有重要意义的是状态游戏状态,用下面的代码替换UpdateStatePlaye方法中的代码,然后 我们将进行一个总结: - - public GameStateEnum State{ - get{ - return _state; - } - set{ - _state = value; - - // MENU - if( _state == GameStateEnum.Menu ){ - player.State = Player.PlayerStateEnum.BouncingBall; - _menuController.Show(); - } - - // PAUSED - else if( _state == GameStateEnum.Paused ){ - Time.timeScale = 0.0f; - _menuController.Show(); - } - - // PLAY - else if( _state == GameStateEnum.Play ){ - Time.timeScale = 1.0f; - _menuController.Hide(); - - // notify user - alerter.Show( "GAME ON", 0.2f, 2.0f ); - } - - // GAME OVER - else if( _state == GameStateEnum.GameOver ){ - // add score - if( _gamePoints > 0 ){ - _leaderboardController.AddPlayersScore( _gamePoints ); - } - - // notify user - alerter.Show( "GAME OVER", 0.2f, 2.0f ); - } - } - } -以上代码很容易阅读和理解;当游戏的状态处于菜单或者暂停状态的时候,你可以通过已经实现的GameMenuController中的Show方法。如果游戏处于游戏中的状态,你可以调用GameMenuController的Hide方法来隐藏,最后 如果游戏处于结束状态,就会将玩家的分数显示到分数面板上。 - -需要注意的是,该代码是基于单例模式的,为了正常工作,需要创建一个新的对象,把它设置成单独的脚本,然后 把对象拖到GameController中的对象面板。 - -##编译运行 - - -完成以上工作后,打开 Build对象框,打开File下的Build Setting,点击编译按钮完成游戏创建并运行! - -![build-dialog.png](http://cdn5.raywenderlich.com/wp-content/uploads/2012/08/build-dialog.png) - - -编译并运行项目,你应该可以在设备上看到下面的画面! - - -![photo-11-700x394.png](http://cdn1.raywenderlich.com/wp-content/uploads/2012/10/photo-11-700x394.png) - - -很好,你已经完成了一个简单的Unity3D游戏。 - - -##优化: - - -这里讲一些优化的内容!虽然你可能认为现在的设备性能已经大大提升了,但考虑仍有大量较老的pad和iphone 3g设备在使用。你需要更努力的来优化游戏,不能让使用较老设备的人认为你的游戏太不好! - -下面是一些游戏开发中常用 的优化方法: - -1:尽量减少调用绘制方法-你应当尽可能的减少绘画方法的调用次数。为了实现这个,可以把图像纹理或者其它资源共享使用,尽量避免透明,可以使用填充黑色。限制灯光的使用数量 ,在高清设备上可以使用一张纹理图集。 - -2:在复杂的场景中要留神-使用最最优化的模型,就是几何图形较少的那种。为了减少几何图形,你可以一起使用同一种效果代替 更多的纹理图形和灯光。记住,用户只能看到屏幕内的东西,所以诸多东西他们并发现不到。 - -3:使用模型仿阴影-动态阴影在IOS设备中是不被支持的,但是幻灯机可以模仿 -阴影。唯一可以确定的就是,幻灯机会给你的绘画调用次数明显增多,所以,如果可能的话,使用简单的一张纹理来模仿阴影。 - -4:小心update/Fixedupdate方法中的第一个地方 ---理想的情况下,update和fixedupdate方法每秒被调用 30到60次最佳。因此 ,需要确保你的计算或者引用的每一样内容在此前要完成。当然也要注意逻辑和添加的模型,特别是那些和物理属性相关的。 - -5:关掉所有你不使用的内容--如果你不需要运行某一个脚本,那么就禁用它。不管它多少的小,或者出现的很少,但每一个处理都需要占用时间。 - -6:尽可能的使用简单组件---如果你不需求功能较多的组件,那么就自己去实现它避免一起使用大量系统组件。比如,CharacterController是一个很废资源的组件,那么最好使用刚体来 定义自己的解决方案。 - -7:使用真实设备进行调试开发---当运行游戏的时候,把控制台打开,你就可以看到什么最占用时间周期。在XCODE中找到并打开iPhone_Profiler.h文件,把ENABLE_INTERNAL_PROFILER调协成1.然后,系统会以较高的优先级告知程序运行状况。你 果你用Unity 3d许可,那么你可以利用这一点对脚本代码中的每个时间方法进行运行评估。输出的结果应该类似于这样: - -![unity3d-internal-profiler.png](http://cdn5.raywenderlich.com/wp-content/uploads/2012/08/unity3d-internal-profiler.png) - -8:帧数 - -它是衡量游戏运行速度的标准,默认情况下会被设置成30或者60.游戏运行的平均值应该介于2者之间。 - -9:draw-call 给出了当前绘制方法调用 了多少次,之前提到过,应当通过共用纹理图像或资源来尽可能的保持最低调用次数, - -10:verts 给出了当前有多少个顶点绘制。 - -11:player-detail 很清晰的给出了游戏中每一个组件处理的时间。 - -##附录 - -[这是](cdn1.raywenderlich.com/downloads/NBN_Part3.zip)本教程的一个简单工程样例。使用Unity打开 ,点击File下的Open Project,点击Open Other 打开文件中的工程文件。游戏的场景默认不会被加载,要打开场景请选择Scenes\GameScene. - -目前为止你做的很好,但是并没有到此结束。希望你继续努力,成为一名Unity高手;下面列出了一些目前为止需要掌握的技能! - -这里有一些建议,来扩展一下游戏: - -1:添加音效。音效是满足交互很重要的一部分,所以花一些时间找些音效和音乐,添加到游戏中。 -2:添加FaceBook的支持,玩家就可以和朋友一起分享了。 - -3:添加多人游戏模式,玩家之前可以在同一设备轮流出手,进行对抗。 - -4:添加新的特性,让游戏更人性化。 - -5:添加手势识别,可以根据手势的不同做不出事的投篮动作。 - -6:添加特殊的篮球,比如每个篮球的重力各不相同。 - -7:添加新的等级 ,等级越高,挑战难度越高。 - -这些已经够你喝一壶的了! - -我希望大家会喜欢这篇文章,并学会了一些关于Unity的知识。希望将来看到你们创建出一些优秀的Unityp游戏。 - +# 中级IOS unity 3d开发:第三部分 # + +这是一篇由 Joshua Newnham 提供的指导文档,它是一个让我们更有效,更独立,更专业的创建数字产品的新兴平台。 + +欢迎回到中级Unity 3D ios开发指引! + +在第一部分当中,你已经学会如何使用Unity来布局游戏场景,并添加物理碰撞检测。 +在第二部分当中,你已经学会如何使用Unity来编写脚本,创建游戏逻辑,比如射击或者投篮。 + +在最后这部分中,你将学会如何创建游戏用户界面系统和游戏控制系统。 + +现在让我们开始吧! + +## 上机实验 ## + +到目前为止,你已经尝试过unity内置的模拟器。但现在是时候去真机上测试你的这些功能了。 + +首先通过点击File->Build settings 打开创建窗口.你将会看到如下窗口: +![](http://i.imgur.com/Rz3S9fr.png) + +请确保你已经正确选择了IOS平台(IOS的后面会有一个Unity的图标显示,如果没有的话点击IOS的图标,点击switch platform按钮) + +选择“Player Settings”来打开检查面板 + + +![](http://i.imgur.com/xJN9632.png) + +在这个面板中,有很多的选项,但到目前为止,你只需要关心以下内容。 + +在“Resolution and Presentation”面板中,选择Landscape-Left项,其它的都使用默认的选项,然后点击Other Settings. + +在这里,你需要选择你的开发环境相关内容,(就像使用XCODE一样) 其它的使用默认。 + +## 关键步骤 ## + +之前你已经设定好相关内容,回到Build Settings对话框开始创建。 + +Unity将会提示你为项目选择目标平台。如果你之前选择的是XCODE,Unity将会使你的项目可以在XCode中编译并在设备上运行。 + +提示:不要在模拟器中运行游戏因为Unity中的库只能运行在IOS设备中。在IOS设备上运行Unity项目需要证书,app帐号和许可证件。请在[http://answers.unity3d.com/questions/20775/the-game-exporting-proccess-for-iphone.html](http://answers.unity3d.com/questions/20775/the-game-exporting-proccess-for-iphone.html "这里")查看更多相关内容。 + + +![](http://i.imgur.com/9q4EZhB.png) + + + +这一些都设定好之后,你需要继续为游戏来创建一个简单的菜单。 + + +## 继续 ## + +首先[cdn3.raywenderlich.com/downloads/NBNResources.zip](cdn3.raywenderlich.com/downloads/NBNResources.zip "资源")下载该资源,这里包括了一些支撑游戏的脚本文件。解压主该 zip 文件,并把.cs文件托到Unity的script文件夹中。 +这些脚本文件可能已经超出了该教程的范围,所以你需要一个实验来知道如何使用它。 +在你的游戏场景中创建一个新的GameObject,将LeaderboardController.cs与该对象关联起来。 +创建一个新的脚本文件,命名为LeaderboardControllerTest,并将它与一个新的GameObject对象进行关联。你将执行实现存储分数增减一的例子。 +首先,引用LeaderboardController中的内容,添加到你的LeaderBoardControllerTest类中,内容如下: + + using UnityEngine; + using System.Collections.Generic; + using System.Collections; + public class LeaderboardControllerTest : MonoBehaviour { + public LeaderboardController leaderboard; + + void Start () { + } + + void Update () { + } + } + +提示:请注意一下usingSystem.Collections.Generic; +在类的上方,这句话包括了 System.Collections.Generic的包引用,因为你可以使用Generic specific colections。关于Generic更详细的解释请看这里。[http://www.codethinked.com/an-overview-of-system_collections_generic](http://www.codethinked.com/an-overview-of-system_collections_generic) + +使用LeaderboardController中的AddPlayersScore方法来添加玩家的分数: + + void Start () { + leaderboard.AddPlayersScore( 100 ); + leaderboard.AddPlayersScore( 200 ); + } + +当你把游戏关闭后,玩家的分数需存储到本地磁盘。所以你需要在LeaderboardControllers中添加OnScoresLoaded事件,并实现这个事件处理,代码如下: + +另外,由于异步调用的原因,你可以不在一开始就调用。 + + void Start () { + leaderboard.OnScoresLoaded += Handle_OnScoresLoaded; + + leaderboard.AddPlayersScore( 100 ); + leaderboard.AddPlayersScore( 200 ); + + leaderboard.FetchScores(); + } + + public void Handle_OnScoresLoaded( List scores ){ + foreach( ScoreData score in scores ){ + Debug.Log ( score.points ); + } + } + +函数的参数是一个ScoreData类型的list对象表,ScoreData是一个简单的数据结构,用来存储分数记录。 + +Handle_OnScoresLoaded方法会遍历所有你记录的分数。 + +好了!现在执行如下几步: + +1. 创建一个新的GameObject,命名为LeaderboardController,并将脚本LeaderboardController与它关联。 + +2. 选择LeaderboardControllerTest对象,并将LeaderboardController对象与分数榜的属性关联。 + +3. 点击开始,你应当在控制台看到分数的输出日志。 + +## 创建简单的菜单 ## + +现在到了最让人兴奋的步骤了-你将要学习如何为游戏创建菜单! + +这是一张最张的效果图: + +![](http://i.imgur.com/pnPUPKV.png) + +在Unity中一共有三种方式可以用来实现用户界面。每一种都有它自己的优点和缺点。下面是每一种方式的详细说明。 + +1:GUI +Unity提共了预定义用户控制集合,可以通过使用GUI Component 中 MonoBehaviour的OnGUI方法。 +在Unity中使用Skins也可支持自定义界面。 + +对于场景来说,这不是一个结点,预先提供丰富的控制集是种完美的解决方案。然而,出于表现效果考虑,在游戏中不建议使用。 + +2:GUITexture and GUIText +Unity提供了两个组件,GUITexture和GUIText.这些组件使你可以在屏幕上绘制出2D图像和文字。你可以很轻易的扩展并创建出与GUI形成对比的的用户界面。 + +3:3D Planes/Texture Altas +如果你正在创建平视显示游戏(简称HUD,比如在游戏中显示的菜单),那么该方式是一个不错的选择;虽然你可能需要付出更多!但是一但你创建好这样的类,你可以将他们用于任何一个新的工程中。 + +3D Planes使用 3D 平面和纹理集来实现HUD显示模式,一个纹理集收集了许多张分离的图像在一张大图中。就像cocos2d中精灵一样。 + +这些纹理是可以共用的元素,通常只需要调用一次来绘制到屏幕。在更多的情况下,你需要设定一个专用的镜头,以达到预期的效果。 + +在本教程中你将要使用的是第一种方式-GUI。不管怎样,它有许多特性,使本教程更简单一些。 + +现在将通过SKINS创建主菜单,然后你将通过代码控制菜单绘制,最后将他们通过GameController拼合到一起。 + +听起来不错吧?是时候开始Skins了。 + +## Skins ## + +Unity提供了一个叫做Skin的来实现自定义 GUI 元素。 +它可以被看做成一个使用HTML标签相联的样式表。 + +我创建了两个skin文件,一个大小为480X320,另一个大小为960X640。下面是一张480X320 skin属性面板的截图。 + +![skin.png](http://cdn4.raywenderlich.com/wp-content/uploads/2012/08/skin.png) + +在Skin的属性显示界面暴露出了许多可以让你创建出独一无二风格样式的属性。在本工程中你只需要设定字体。 + +打开选择GUI下的GameMenuSmall,将scoreboard字体拖拽到字体属性中,并把字号大小设置成16。打开选择GUI下的GameMenuNormal,将scoreboard字体拖拽到字体属性中,并把字号大小设置成32。 + +下一步,是实现真正的菜单。 + +## Main Menu + +这节将会提供GameMenuController的源代码,它用来绘制菜单界面并响应用户的输入。你将会看到该代码的重要部分,并很快将它应用到你的游戏中! + +创建新的脚本文件命名为GameMenuController,并在 其中添加如下内容: + using UnityEngine; + using System.Collections; + + [RequireComponent (typeof (LeaderboardController))] + public class GameMenuController : MonoBehaviour { + + public Texture2D backgroundTex; + public Texture2D playButtonTex; + public Texture2D resumeButtonTex; + public Texture2D restartButtonTex; + public Texture2D titleTex; + public Texture2D leaderboardBgTex; + public Texture2D loginCopyTex; + public Texture2D fbButtonTex; + public Texture2D instructionsTex; + + public GUISkin gameMenuGUISkinForSmall; + public GUISkin gameMenuGUISkinForNormal; + + public float fadeSpeed = 1.0f; + private float _globalTintAlpha = 0.0f; + + private GameController _gameController; + private LeaderboardController _leaderboardController; + private List _scores = null; + + public const float kDesignWidth = 960f; + public const float kDesignHeight = 640f; + + private float _scale = 1.0f; + private Vector2 _scaleOffset = Vector2.one; + + private bool _showInstructions = false; + private int _gamesPlayedThisSession = 0; + } + +在代码的开始,定义了一些从变量的名字就可以看出其用意的公共变量,它们是用来绘制出GameMenuController菜单的基本元素。之后 ,你需要引用定义上一节创建的Skins。 + +以上就是你在主菜单中需要用到的所有变量了。 + +我们还引用 了GameController和LeaderboardController以及用来存储玩家分数的ScoreData。 + +定义了这些之后,你还需要一个衡量玩家界面大小的变量,比如不同大小屏幕,Iphone3GS是480X320 ,Iphone4是960X640。 + +最后定义了于些用来管理GameMenuController组件状态的变量 。 + +下一步将Awake()方法和Start()方法添加进来,代码如下: + + void Awake(){ + _gameController = GetComponent(); + _leaderboardController = GetComponent(); + } + + void Start(){ + _scaleOffset.x = Screen.width / kDesignWidth; + _scaleOffset.y = Screen.height / kDesignHeight; + _scale = Mathf.Max( _scaleOffset.x, _scaleOffset.y ); + + _leaderboardController.OnScoresLoaded += HandleLeaderboardControllerOnScoresLoaded; + + _leaderboardController.FetchScores(); + } + +在Start()方法中,通过LeaderboardController请求了玩家的分数。当然,一些图形图像的比率也被计算出了自适应的大小。 + +代码中的测量偏移变量用来确保界面元素能够适当的显示出来。比如,如果游戏菜单被设计成在屏幕大小为960X640上显示,但当前屏幕的大小是480X320,你就需要把当前的界面缩小50%;代码中的变量scaleOffset就是0.5。如果只是简单的界面,这样做将会节省大量的成本,并且对设备多样化有实质性的意义。 + +当分数被加载后,本地缓存中的分数将会被绘制到界面当中。 + + public void HandleLeaderboardControllerOnScoresLoaded( List scores ){ + _scores = scores; + } + +## 测试一下 + +让我们先暂停一下,并测试一下现有内容。 + +将下面的代码添加到GameMenuController当中: + + void OnGUI () { + GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex ); + if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ + Debug.Log( "Click" ); + } + } + +上面的代码中有一个叫做OnGUI的方法。这个方法类的调用类似于Update()方法,是Gui Component的入口。GUI Component提供了一套实现用户界面控制的静态方法,点击[这里]([http://docs.unity3d.com/ScriptReference/MonoBehaviour.OnGUI.html](http://docs.unity3d.com/ScriptReference/MonoBehaviour.OnGUI.html))可以从Unity网站中获得更多关于OnGUI和GUI相关内容。 + +通过调用OnGUI方法你可以使用下面代码通过缓存来在整个屏幕进行绘制: + + GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex ); +下面是一句使用GUI..Button方法的条件语句,GUI.Button方法会在给定位置绘制一个按钮(使用你之前计算出来 的偏移变量)。该方法为boolean类型,如果用户点击就会返回true,否则返回false。 + + if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ + Debug.Log( "Click" ); + } + +在这句代码中,如果用户点击了该按钮,就会在控制台输出“Click”。 + +测试前,需要将GameMenuController脚本和GameController脚本关联到游戏对象上,然后将所有内容关联到适当的对象 +和属性中,就像下图这样: + +![skin.png](http://cdn5.raywenderlich.com/wp-content/uploads/2012/10/unity3d-gui-properties.png) + +现在运行游戏,点击开始,你将会看到一个按钮出现。点击它,在控制台会看到一条消息。 + +![unity3d-ongui-test.png](http://cdn4.raywenderlich.com/wp-content/uploads/2012/10/unity3d-ongui-test.png) + +还不错吧?制作主菜单第一步,完成! + +## 使用自定义界面 + +现在你已经有了正确的方向,让我们继续设计出一个适合屏幕大小的自定义界面。 + +使用下面的代码替换OnGUI方法: + + if( _scale < 1 ){ + GUI.skin = gameMenuGUISkinForSmall; + } else{ + GUI.skin = gameMenuGUISkinForNormal; + } +上面的代码可以确保你使用的是正确的字体大小(依懒于屏幕大小);你之前计算的scale大小,决定了你使用哪一种自定义界面。如果该值小于1.0,那么 将会使用较小的,否则就会使用正常的。 + +## 显示和隐藏 + +收到用户请求后,与其立刻生硬的做出响应,不如让他逐渐的来显示 。为了达到该效果,你需要有技巧的处理这些颜色变量(这个会影响到所有GUI类的绘制)。 + +为了达到这个效果,你需要以像素为单位,将透明度从0到1,并将这个值赋值给GUI中的颜色变量。 + +将以下代码添加到你的OnGUI方法中: + + _globalTintAlpha = Mathf.Min( 1.0f, Mathf.Lerp( _globalTintAlpha, 1.0f, Time.deltaTime * fadeSpeed ) ); + + Color c = GUI.contentColor; + c.a = _globalTintAlpha; + GUI.contentColor = c; + +你还需要一种实现菜单显示或隐藏的方法,所以创建2个公共的方法,命名为Show()和Hide(),代码如下: + + public void Show(){ + // ignore if you are already enabled + if( this.enabled ){ + return; + } + _globalTintAlpha = 0.0f; + _leaderboardController.FetchScores(); + this.enabled = true; + } + + public void Hide(){ + this.enabled = false; + } + +没有什么 不可能实现的!你需要一批新的分数,以防玩家出现新的分数。所以你需要将全局色彩值设置成0,来隐藏./显示开始/暂停按钮在OnGUI调用。(比如,如果该按钮被隐藏,那么所有的更新方法,比如update,FixedUpdate,OnGUI都不会再被调用)。 + +你的菜单显示什么 取决于你当前游戏的状态, 比如,游戏结束状态会与游戏暂停状态绘制完全不同的内容。 + +在OnGUI方法中添加如下代码: + +GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex ); + + if( _gameController.State == GameController.GameStateEnum.Paused ){ + if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ + _gameController.ResumeGame(); + } + + if (GUI.Button( new Rect ( 229 * _scaleOffset.x, 357 * _scaleOffset.y, 100 * _scale, 100 * _scale ), restartButtonTex, GUIStyle.none) ){ + _gameController.StartNewGame(); + } + } else{ + if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), playButtonTex, GUIStyle.none) ) + { + if( _showInstructions || _gamesPlayedThisSession > 0 ){ + _showInstructions = false; + _gamesPlayedThisSession++; + _gameController.StartNewGame(); + } else{ + _showInstructions = true; + } + } + } +这些对你来说看起来比较熟悉,你绘制的所有图像纹理和按钮都取决于GameController是什么状态。 + +当游戏暂停的时候,游戏界面会显示出两个按钮-回到游戏,重新开始: + + if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ + _gameController.ResumeGame(); + } + + if (GUI.Button( new Rect ( 229 * _scaleOffset.x, 357 * _scaleOffset.y, 100 * _scale, 100 * _scale ), restartButtonTex, GUIStyle.none) ){ + _gameController.StartNewGame(); + } + +提示:如何更好的获得这些做标点和大小?我是很费力的在创建界面的时候从绘图程序中拷贝来的。 + +另外一个游戏状态就是游戏结束,就是当你显示play按钮的时候。 + +提示:也行你对_showInstructions 和 _gamesPlayedThisSession这2个变量有疑问。 + +_gamesPlayedThisSession 是用来决定你可是第几次游戏,如果你是初次打开该游戏,那么_showInstructions 在游戏开始前会被设置为真。这时游戏在开始前会展示一些新手教程。 + +## 测试时间 + +在你完成GameMenuController前,让我们确定一下每一部分都能按预期效果工作。如果是一步一步走下来的话,开始游戏后会看到类似如下效果: +![unity3d-ongui-test-2.png](http://cdn4.raywenderlich.com/wp-content/uploads/2012/10/unity3d-ongui-test-2.png) + +## 完成GameMenuController + +最后没有的就差标题(title),指令(instructions)和分数(score). + +画标题还是新手教程取决于instruction的标识,在OnGUI方法的后面添加如下代码: + + if( _showInstructions ){ + GUI.DrawTexture( new Rect( 67 * _scaleOffset.x, 80 * _scaleOffset.y, 510 * _scale, 309 * _scale ), instructionsTex ); + } else{ + GUI.DrawTexture( new Rect( 67 * _scaleOffset.x, 188 * _scaleOffset.y, 447 * _scale, 113 * _scale ), titleTex ); + } +注意 ;最后一步是分数面板。OnGUI提示了 分组归类,分组归类允许你把这些东西按钮垂直或者水平进行布局。下面的代码 画出了leaderboard和一些仿facebook/Twritter的按钮,并将他们添加到了组中。在OnGUI方法中添加下面的代码: + + GUI.BeginGroup( new Rect( Screen.width - (214 + 10) * _scale, (Screen.height - (603 * _scale)) / 2, 215 * _scale, 603 * _scale ) ); + + GUI.DrawTexture( new Rect( 0, 0, 215 * _scale, 603 * _scale ), leaderboardBgTex ); + + Rect leaderboardTable = new Rect( 17 * _scaleOffset.x, 50 * _scaleOffset.y, 180 * _scale, 534 * _scale ); + if( _leaderboardController.IsFacebookAvailable && !_leaderboardController.IsLoggedIn ){ + leaderboardTable = new Rect( 17 * _scaleOffset.x, 50 * _scaleOffset.y, 180 * _scale, 410 * _scale ); + GUI.DrawTexture( new Rect( 29* _scaleOffset.x, 477* _scaleOffset.y, 156 * _scale, 42 * _scale ), loginCopyTex ); + if (GUI.Button( new Rect ( 41 * _scaleOffset.x, 529 * _scaleOffset.y, 135 * _scale, 50 * _scale ), fbButtonTex, GUIStyle.none) ) + { + _leaderboardController.LoginToFacebook(); + } + } + GUI.BeginGroup( leaderboardTable ); + if( _scores != null ){ + for( int i=0; i<_scores.Count; i++ ){ + Rect nameRect = new Rect( 5 * _scaleOffset.x, (20 * _scaleOffset.y) + i * 35 * _scale, 109 * _scale, 35 * _scale ); + Rect scoreRect = new Rect( 139 * _scaleOffset.x, (20 * _scaleOffset.y) + i * 35 * _scale, 52 * _scale, 35 * _scale ); + + GUI.Label( nameRect, _scores[i].name ); + GUI.Label( scoreRect, _scores[i].points.ToString() ); + } + } + GUI.EndGroup(); + GUI.EndGroup(); + + } +这就是GameMenuController,完成并将它添加到GameController类中(控制界面显示或者隐藏),打开GameController并按照下面的方法使用它。 + +打开GameController并在开始声明如下变量: + + private GameMenuController _menuController; + private LeaderboardController _leaderboardController; + public Alerter alerter; + +在Awake()方法中h引用他们: + + void Awake() { + _instance = this; + _menuController = GetComponent(); + _leaderboardController = GetComponent(); + } +最有重要意义的是状态游戏状态,用下面的代码替换UpdateStatePlaye方法中的代码,然后 我们将进行一个总结: + + public GameStateEnum State{ + get{ + return _state; + } + set{ + _state = value; + + // MENU + if( _state == GameStateEnum.Menu ){ + player.State = Player.PlayerStateEnum.BouncingBall; + _menuController.Show(); + } + + // PAUSED + else if( _state == GameStateEnum.Paused ){ + Time.timeScale = 0.0f; + _menuController.Show(); + } + + // PLAY + else if( _state == GameStateEnum.Play ){ + Time.timeScale = 1.0f; + _menuController.Hide(); + + // notify user + alerter.Show( "GAME ON", 0.2f, 2.0f ); + } + + // GAME OVER + else if( _state == GameStateEnum.GameOver ){ + // add score + if( _gamePoints > 0 ){ + _leaderboardController.AddPlayersScore( _gamePoints ); + } + + // notify user + alerter.Show( "GAME OVER", 0.2f, 2.0f ); + } + } + } +以上代码很容易阅读和理解;当游戏的状态处于菜单或者暂停状态的时候,你可以通过已经实现的GameMenuController中的Show方法。如果游戏处于游戏中的状态,你可以调用GameMenuController的Hide方法来隐藏,最后 如果游戏处于结束状态,就会将玩家的分数显示到分数面板上。 + +需要注意的是,该代码是基于单例模式的,为了正常工作,需要创建一个新的对象,把它设置成单独的脚本,然后 把对象拖到GameController中的对象面板。 + +## 编译运行 + + +完成以上工作后,打开 Build对象框,打开File下的Build Setting,点击编译按钮完成游戏创建并运行! + +![build-dialog.png](http://cdn5.raywenderlich.com/wp-content/uploads/2012/08/build-dialog.png) + + +编译并运行项目,你应该可以在设备上看到下面的画面! + + +![photo-11-700x394.png](http://cdn1.raywenderlich.com/wp-content/uploads/2012/10/photo-11-700x394.png) + + +很好,你已经完成了一个简单的Unity3D游戏。 + + +## 优化: + + +这里讲一些优化的内容!虽然你可能认为现在的设备性能已经大大提升了,但考虑仍有大量较老的pad和iphone 3g设备在使用。你需要更努力的来优化游戏,不能让使用较老设备的人认为你的游戏太不好! + +下面是一些游戏开发中常用 的优化方法: + +1:尽量减少调用绘制方法-你应当尽可能的减少绘画方法的调用次数。为了实现这个,可以把图像纹理或者其它资源共享使用,尽量避免透明,可以使用填充黑色。限制灯光的使用数量 ,在高清设备上可以使用一张纹理图集。 + +2:在复杂的场景中要留神-使用最最优化的模型,就是几何图形较少的那种。为了减少几何图形,你可以一起使用同一种效果代替 更多的纹理图形和灯光。记住,用户只能看到屏幕内的东西,所以诸多东西他们并发现不到。 + +3:使用模型仿阴影-动态阴影在IOS设备中是不被支持的,但是幻灯机可以模仿 +阴影。唯一可以确定的就是,幻灯机会给你的绘画调用次数明显增多,所以,如果可能的话,使用简单的一张纹理来模仿阴影。 + +4:小心update/Fixedupdate方法中的第一个地方 ---理想的情况下,update和fixedupdate方法每秒被调用 30到60次最佳。因此 ,需要确保你的计算或者引用的每一样内容在此前要完成。当然也要注意逻辑和添加的模型,特别是那些和物理属性相关的。 + +5:关掉所有你不使用的内容--如果你不需要运行某一个脚本,那么就禁用它。不管它多少的小,或者出现的很少,但每一个处理都需要占用时间。 + +6:尽可能的使用简单组件---如果你不需求功能较多的组件,那么就自己去实现它避免一起使用大量系统组件。比如,CharacterController是一个很废资源的组件,那么最好使用刚体来 定义自己的解决方案。 + +7:使用真实设备进行调试开发---当运行游戏的时候,把控制台打开,你就可以看到什么最占用时间周期。在XCODE中找到并打开iPhone_Profiler.h文件,把ENABLE_INTERNAL_PROFILER调协成1.然后,系统会以较高的优先级告知程序运行状况。你 果你用Unity 3d许可,那么你可以利用这一点对脚本代码中的每个时间方法进行运行评估。输出的结果应该类似于这样: + +![unity3d-internal-profiler.png](http://cdn5.raywenderlich.com/wp-content/uploads/2012/08/unity3d-internal-profiler.png) + +8:帧数 + +它是衡量游戏运行速度的标准,默认情况下会被设置成30或者60.游戏运行的平均值应该介于2者之间。 + +9:draw-call 给出了当前绘制方法调用 了多少次,之前提到过,应当通过共用纹理图像或资源来尽可能的保持最低调用次数, + +10:verts 给出了当前有多少个顶点绘制。 + +11:player-detail 很清晰的给出了游戏中每一个组件处理的时间。 + +## 附录 + +[这是](cdn1.raywenderlich.com/downloads/NBN_Part3.zip)本教程的一个简单工程样例。使用Unity打开 ,点击File下的Open Project,点击Open Other 打开文件中的工程文件。游戏的场景默认不会被加载,要打开场景请选择Scenes\GameScene. + +目前为止你做的很好,但是并没有到此结束。希望你继续努力,成为一名Unity高手;下面列出了一些目前为止需要掌握的技能! + +这里有一些建议,来扩展一下游戏: + +1:添加音效。音效是满足交互很重要的一部分,所以花一些时间找些音效和音乐,添加到游戏中。 +2:添加FaceBook的支持,玩家就可以和朋友一起分享了。 + +3:添加多人游戏模式,玩家之前可以在同一设备轮流出手,进行对抗。 + +4:添加新的特性,让游戏更人性化。 + +5:添加手势识别,可以根据手势的不同做不出事的投篮动作。 + +6:添加特殊的篮球,比如每个篮球的重力各不相同。 + +7:添加新的等级 ,等级越高,挑战难度越高。 + +这些已经够你喝一壶的了! + +我希望大家会喜欢这篇文章,并学会了一些关于Unity的知识。希望将来看到你们创建出一些优秀的Unityp游戏。 + 如果你对该文章或者Unity有任何问题或者意见,请加入到我们的论坛在下面进行留言。 \ No newline at end of file diff --git "a/How to Make a Game Like Jetpack Joyride in Unity 2D \342\200\223 Part 3.md" "b/How to Make a Game Like Jetpack Joyride in Unity 2D \342\200\223 Part 3.md" index ec1e580..312c155 100644 --- "a/How to Make a Game Like Jetpack Joyride in Unity 2D \342\200\223 Part 3.md" +++ "b/How to Make a Game Like Jetpack Joyride in Unity 2D \342\200\223 Part 3.md" @@ -1,4 +1,4 @@ -#如何用Unity 2D制作一个像疯狂喷气机的游戏——第三部分 +# 如何用Unity 2D制作一个像疯狂喷气机的游戏——第三部分 这是关于如何用Unity 2D制作一个像疯狂喷气机的游戏的系列教程的最后一个部分。如果你落下了教程之前的部分,你最好先去完成之前教程的学习。它们分别是:第一部分和第二部分。 @@ -6,7 +6,7 @@ 在这个部分你将添加激光,硬币,音效,音乐甚至是视差滚动。好了,聊了够多了,让我们开始享受乐趣吧! -##开始 +## 开始 你可以继续使用你在第二部分创建的工程或者你也可以下载这个部分的初始工程。它们几乎是相同的。 @@ -14,13 +14,13 @@ 当你已经准备好打开**RocketMouse.unity**场景,就让我们开始吧! -##添加激光 +## 添加激光 老鼠飞过房间是很棒的,但是这个游戏的挑战在哪里呢?是时候添加一些障碍物了,还有什么比激光更酷的呢?:) 激光将被随机生成,以一个与生成房间的相同方式,所以你需要创建一个活动房屋。你还需要创建一个小脚本来控制激光。 -##创建激光 +## 创建激光 这里是创建一个激光对象所需的步骤: @@ -54,7 +54,7 @@ ![img](http://cdn5.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_p3_40-552x500.png "rocket_mouse_unity_p3_40") -###通过脚本控制激光的开关 +### 通过脚本控制激光的开关 在**MonoDevelop**上打开**LaserScript**并且加上以下实例变量: @@ -143,7 +143,7 @@ void FixedUpdate () { 注意:你可以仅仅通过设置rotationSpeed为零来让旋转不可用。 -###设置激光脚本参数 +### 设置激光脚本参数 切回到Unity并且在**Hierarchy**中选择**laser**。确保激光脚本组成是可见的。 @@ -169,7 +169,7 @@ void FixedUpdate () { ![img](http://cdn3.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_p3_44.png "rocket_mouse_unity_p3_44") -##杀死老鼠 +## 杀死老鼠 现在老鼠可以很轻松地在可用的激光中穿过,当没有很多弯曲的胡须的时候。这不是小孩的一个好榜样。小孩可以看到玩激光的结果。:) @@ -247,7 +247,7 @@ void FixedUpdate () 由于游戏不能这样结束,你需要添加一些状态来显示老鼠已经死亡。 -###添加掉落和死掉的老鼠动画 +### 添加掉落和死掉的老鼠动画 在**Hierarchy**中选择**老鼠**GameObject,并且打开**动画**视图。 @@ -289,7 +289,7 @@ void FixedUpdate () ![img](http://cdn3.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_p3_51.png "rocket_mouse_unity_p3_51") -###变换到Fall和Die动画 +### 变换到Fall和Die动画 在创建动画之后,你需要适时将Animator切换到对应的动画。为了达到这个效果,你可以通过一个特殊的状态叫做**Any State**来进行变换,因为当老鼠触碰到激光时候它当前是处在什么状态是没有关系的。 @@ -337,7 +337,7 @@ animator.SetBool("dead", true); 就像你看到的,当老鼠触到激光,脚本设置**dead**参数为**true**并且老鼠切换到fall状态(因为grounded还是false)。不管怎样,当老鼠到达地面时,脚本设置grounded参数为**true**。现在,所有条件指向切换到**die**状态。 -###使用Trigger来让老鼠死亡一次 +### 使用Trigger来让老鼠死亡一次 古语有云,每个动物都只有一条命,但是这里老鼠却一直在死去。你可以通过在老鼠死亡后审查Animator视图来自己检查这个错误。 @@ -369,11 +369,11 @@ animator.SetBool("dead", true); 这次老鼠在死亡后躺在地板上了。 -##添加硬币 +## 添加硬币 当致命的激光有趣地被实现,加上一些硬币给老鼠收集怎么样。 -###创建硬币活动房屋 +### 创建硬币活动房屋 创建一个硬币活动房屋是很简单的,就和创建激光一样,所以你应该尝试自己动手。只需要使用硬币精灵并且跟着这些步骤来做: @@ -421,7 +421,7 @@ animator.SetBool("dead", true); 不是的,硬币是正常的。老鼠死掉是因为**MouseController**脚本里的代码操作任何一个碰撞机就像一个含有激光的碰撞机。 -###用Tags来区分硬币和激光 +### 用Tags来区分硬币和激光 你可以使用**Tags**来区分硬币和激光,**Tags**专门为了这个目的而产生的。 @@ -439,7 +439,7 @@ animator.SetBool("dead", true); 当然,仅仅只是设置Tag属性不能让脚本区分硬币和激光,你仍然需要修改一些代码。 -###更新MouseController脚本来使用Tags +### 更新MouseController脚本来使用Tags 打开**MouseController**脚本并且添加一个**coins**计数器变量: @@ -484,13 +484,13 @@ void OnTriggerEnter2D(Collider2D collider) 现在好多了。老鼠收集硬币并且在它触碰到激光的时候死亡。看起来你已经准备好用脚本生成激光和硬币。 -##生成硬币和激光 +## 生成硬币和激光 生成硬币和激光和生成房间的做法类似。算法基本相同,但是在写这个代码之前,你需要改善下硬币的生成以便为玩家提供更多乐趣。 现在你有一个只有一个硬币组成的活动房屋,所以如果你写生成代码你将在这个水平面的各个地方只简单地生成一个硬币。这是很无趣的!何不从硬币上创建不同的轮廓并且一下子生成一堆硬币呢? -##创建一堆硬币的活动房屋 +## 创建一堆硬币的活动房屋 在Project视图中打开**Prefabs**文件夹并且用硬币活动房屋在场景中创建**9个硬币**。它应该看起来像这样: @@ -516,7 +516,7 @@ void OnTriggerEnter2D(Collider2D collider) 你已经完成了。现在从场景中移除所有的硬币和激光因为他们将在脚本中生成。 -##在生成脚本中添加新的参数 +## 在生成脚本中添加新的参数 打开**GeneratorScript**并且添加以下实例变量: @@ -542,7 +542,7 @@ public float objectsMaxRotation = 45.0f; 通过使用**objectsMinY**和**objectsMaxY**你可以在放置的每一个对象中配置最大高度和最小高度,并且通过使用**objectsMinRotation**和**objectsMaxRotation**你可以配置旋转角度。 -##添加方法到新添加的对象中 +## 添加方法到新添加的对象中 新对象被添加到**AddObject**,和添加房间的方法相似。 diff --git a/How-to-Make-a-Game-Like-Candy-Crush-with-Swift-Tutorial-Part2.md b/How-to-Make-a-Game-Like-Candy-Crush-with-Swift-Tutorial-Part2.md index 76909ff..a56b545 100644 --- a/How-to-Make-a-Game-Like-Candy-Crush-with-Swift-Tutorial-Part2.md +++ b/How-to-Make-a-Game-Like-Candy-Crush-with-Swift-Tutorial-Part2.md @@ -1,1063 +1,1063 @@ -#如何使用Swift指南制作一个像Candy Crush的游戏:第二部分 - -欢迎回到我们的关于如何使用Swift指南制作一个像Candy Crush的游戏的系列教程。 - - -这是教你如何制作一个像Candy Crush Saga或Bejeweled等三消类游戏系列教程的第二部分。游戏的名字是Cookie Crunch Adventure,并且使用的是美味的cookie哦! - -在[指南的第一部分](How-to-Make-a-Game-Like-Candy-Crush-with-Swift-Tutorial-Part1.md)中,你从JSON文件夹中加载了标准的形状,把cookie显示到了屏幕上,实现了监测点击和交换cookie的逻辑。 - -在第二部分(也是最后部分)中,你将会实现游戏剩下的内容,添加加载动画并把Cookie Crunch Adventure美化达到排名前十的质量。想想就感觉特别美。 - -这部分Swift指南是前一部分内容的继续。如果你还没有学习过,这里是到目前为止所有的源代码。你还需要里面的资源文件(和第一部分中的是相同的文件)。 - -让我们碾碎cookie吧! - -###开始### - -你原来所做的事情是允许玩家交换cookie。下面,就需要去处理交换之后的结果了。 - -交换通常会形成一个有三个或者更多相匹配的cookie的链表。下面要做的就是从屏幕上消除这些相同的cookie,然后给玩家一些积分奖励。 - -这是这些事件的顺序: - - -你已经完成了前三步:用cookie填充关卡,计算可能的交换,等待玩家交换。在Swift指南的这部分内容中,你将会完成剩下的步骤。 - -###找到链表### - -这个时候,玩家一般已经移动并交换了两个cookie。如果交换之后会形成一个有三个或者更多相同类型的cookie的链表--至少有一个,也可能有其他的链表,则游戏只允许玩家交换一次。 - -在你从屏幕上消除这些相同的cookie之前,你需要先找到满足条件的所有链表。这就是我们接下来要做的事情。 - -首先,生成一个描述链表的类。找到File\New\File...,选择IOS\Source\Swift File模板,然后点击下一步,把文件命名为Chain.swift,然后点击创建。 - -用下面的内容替换Chain.swift中的内容: - - - class Chain: Hashable, Printable { - var cookies = Array() // private - - enum ChainType: Printable { - case Horizontal - case Vertical - - var description: String { - switch self { - case .Horizontal: return "Horizontal" - case .Vertical: return "Vertical" - } - } - } - - var chainType: ChainType - - init(chainType: ChainType) { - self.chainType = chainType - } - - func addCookie(cookie: Cookie) { - cookies.append(cookie) - } - - func firstCookie() -> Cookie { - return cookies[0] - } - - func lastCookie() -> Cookie { - return cookies[cookies.count - 1] - } - - var length: Int { - return cookies.count - } - - var description: String { - return "type:\(chainType) cookies:\(cookies)" - } - - var hashValue: Int { - return reduce(cookies, 0) { $0.hashValue ^ $1.hashValue } - } - } - - func ==(lhs: Chain, rhs: Chain) -> Bool { - return lhs.cookies == rhs.cookies - } - -Chain类有一个存储cookie对象的数组和一个表示水平(行)或垂直(列)的属性。这个属性被定义为枚举类型;因为它和Chain是成对出现的,因此它嵌套在Chain类的内部。如果你喜欢挑战,你也可以添加更加复杂的链表类型,比如L-和T-shapes。 - -这里使用Array而不是Set来存储cookie对象是有原因的:这样更方便记住cookie对象的顺序,使你知道哪些cookie在链表的尾部。使把多个链表结合到一个链表中来检测那些L-或T-shapes更加简单。 - - - 注意:chain类实现了Hashable,所以可以把它放进Set中。 - hashValue的代码看起来有点奇怪,但是它仅仅完成了把链表 - 中所有cookie的值进行异或的运算。 - reduce()函数是Swift更多高级的功能性编程特性的一个体现。 - -为了好好的利用这些chain对象,需要打开Level.swift。然后添加一个名字为removeMatches()的函数,但是在这之前,你需要一些协助函数来完成找到满足条件的链表的繁重工作。 - -为了找到满足条件的链表,你需要一对for循环来遍历这个关卡网格的每一个方块。 - - -当遍历水平方向的一行中的cookie时,你想要找到满足链表条件的第一个cookie。 - -如果有一个满足条件的链表,那么第一个cookie的右边必须有两个紧邻的且类型相同的的cookie。然后你就可以跳过这些相同类型的cookie直到你找到一个不满足链表条件的cookie。你重复这个动作直到你考虑了所有的可能。 - -把下面的函数添加到Level.swift中,来检查水平方向cookie的匹配情况: - - func detectHorizontalMatches() -> Set { - // 1 - let set = Set() - // 2 - for row in 0..NumRows { - for var column = 0; column < NumColumns - 2 ; { - // 3 - if let cookie = cookies[column, row] { - let matchType = cookie.cookieType - // 4 - if cookies[column + 1, row]?.cookieType == matchType && - cookies[column + 2, row]?.cookieType == matchType { - // 5 - let chain = Chain(chainType: .Horizontal) - do { - chain.addCookie(cookies[column, row]!) - ++column - } - while column < NumColumns && cookies[column, row]?.cookieType == matchType - - set.addElement(chain) - continue - } - } - // 6 - ++column - } - } - return set - } - -下面是函数的工作原理: - -1.你创建一个Set保存水平方向的链表(Chain对象)。然后,你把这些链表中的cookie从游戏中删除。 - -2.循环这些行和列。注意:你并不需要去检查最后两列,因为这些cookie并不会形成一个新的链表。还要注意内层的for循环并不会增加循环计数,只有在循环体中满足一定的条件时才会有增加。 - -3.在关卡设计中你可以跳过任何的缺口。 - -4.你检查是否接下来的两列有相同类型的cookie。通常,你需要注意不要在做`cookies[column + 2, row]`类似的运算的时候超过数组的边界,但是这里并不会发生错误。这就是for循环的限定条件为`NumColumns - 2`。注意使用问号的可选性链接。 - -5.这个时候,会有一个至少有三个cookie的链表。这一步遍历所有的可以匹配的cookie直到找到一个不满足链表的cookie或者是到达网格的末端。然后把所有匹配的cookie添加到一个Chain对象中。每匹配一次增加一列。 - -6.如果接下来的两个cookie和当前的不匹配或者是有一个空格,那就没有链表了,你就可以直接的跳过当前的cookie。 - - - 注意:如果在网格中有一个空格,使用可选的链接-- `cookies[column, row]?`后面的问号--确保while循环在满足这个条件的时候终止。上面的逻辑对于水平方向上有空格的行也是适用的。漂亮! - -其次,添加下面的函数来检查垂直方向的cookie匹配情况: - - func detectVerticalMatches() -> Set { - let set = Set() - - for column in 0..NumColumns { - for var row = 0; row < NumRows - 2; { - if let cookie = cookies[column, row] { - let matchType = cookie.cookieType - - if cookies[column, row + 1]?.cookieType == matchType &&cookies[column, row + 2]?.cookieType == matchType { - - let chain = Chain(chainType: .Vertical) - do { - chain.addCookie(cookies[column, row]!) - ++row - } - while row < NumRows && cookies[column, row]?.cookieType == matchType - - set.addElement(chain) - continue - } - } - ++row - } - } - return set - } -垂直方向上有相同的逻辑,但是需要把列放在外层的for循环上,行放在内层的for循环上。 - -你也许会疑惑当我们检测到他们满足链表条件的时候为什么不直接的把他们从关卡中移除。是因为某个cookie可能同时在两个链表中:一个水平方向的、一个垂直方向的。因此在你检查了水平和垂直方向的两个选择之前你并不像直接的把它移除。 - -既然两个检测函数都已经就绪了,添加下面的`removeMatches()`的具体实现: - - func removeMatches() -> Set { - let horizontalChains = detectHorizontalMatches() - let verticalChains = detectVerticalMatches() - - println("Horizontal matches: \(horizontalChains)") - println("Vertical matches: \(verticalChains)") - - return horizontalChains.unionSet(verticalChains) - } -这个函数调用上面的两个协助函数,然后把结果结合起来放到一个`set`中。然后,你将要在这个函数中添加更多的逻辑处理,但是现在你只对找到这些匹配的cookie并返回`set`感兴趣。 - -你仍然需要在`GameViewController.swift`中调用`removeMatches()`。添加下面的协助函数: - - func handleMatches() { - let chains = level.removeMatches() - // TODO: do something with the chains set - } -然后,你就会发现这个函数会移除cookie链表并把其他的cookie放到空格里。在函数`handleSwipe()`中,把`scene.animateSwap()`的调用改变成下面的形式: - - scene.animateSwap(swap, completion: handleMatches) -回想一下,闭包和函数在Swift中是相同的。因此你可以给函数`animateSwap()`传递一个函数名来代替闭包块。 - -生成并运行,然后交换两个cookie形成链表。你在Xcode的debug窗口中应该可以看到下面的图形: - - -###移除链表### -到目前为止,函数`removeMatches()`只能检测匹配的链表。现在你将会播放一个漂亮的动画并把满足条件的cookie从游戏中移除。 - -首先,你需要更新一个数据模型--就是把cookie对象从二维网格数组中移除。这些完成之后,你可以告诉`GameScene`播放这些存在的cookie精灵的动画。 - -从模型中移除这些cookie是很简单的。在`Level.swift`中添加下面的函数: - - func removeCookies(chains: Set) { - for chain in chains { - for cookie in chain.cookies { - cookies[cookie.column, cookie.row] = nil - } - } - } -每一个链表都有一串cookie对象,每个cookie对象都知道它在网格中的行号与列号。因此,你可以简单的把数组中的元素置为`nil`来从数据模型中移除这些cookie对象。 - - 注意:现在,Chain对象只是Cookie对象的所有者。当这些链表被释放的时候,这些cookie对象也会被释放。 -在函数`removeMatches()`中,用下面的内容替换`println()`的声明: - - removeCookies(horizontalChains) - removeCookies(verticalChains) -注意数据模型。现在切换到`GameScene.swift`,并添加下面的函数: - - func animateMatchedCookies(chains: Set, completion: () -> ()) { - for chain in chains { - for cookie in chain.cookies { - if let sprite = cookie.sprite { - if sprite.actionForKey("removing") == nil { - let scaleAction = SKAction.scaleTo(0.1, duration: 0.3) - scaleAction.timingMode = .EaseOut - sprite.runAction(SKAction.sequence([scaleAction, SKAction.removeFromParent()]),withKey:"removing") - } - } - } - } - runAction(matchSound) - runAction(SKAction.waitForDuration(0.3), completion: completion) - } -这个函数循环所有的链表和每个链表中所有的元素,然后播放动画。 - -因为同一个cookie可能同时在两个链表中(一个水平的和一个垂直的),你要确保只给精灵播放一个动画,而不是两个。这就是动作被添加给那些有“移除”关键字的精灵的原因。如果这样的动作已经存在,你不要再给精灵添加一个新的动画。 - -当缩小动画播放完成后,已经把精灵从cookie面板上移除。这个函数结尾处的`waitForDuration()`动作是确保游戏的其余部分只能在动画结束后才能继续。 - -打开**`GameViewController.swift`**,改变函数`handleMatches()`来调用新的动画: - - func handleMatches() { - let chains = level.removeMatches() - - scene.animateMatchedCookies(chains) { - self.view.userInteractionEnabled = true - } - } -试一下。点击生成和运行,然后形成一些匹配的cookie: - -注意:当移除链表的动画正在播放的时候,你并不希望玩家能够点击任何东西。因此,在处理点击程序中,第一件事就是使`userInteractionEnabled`无效,当动画播放完成后,再次使它生效。 - -###把cookie降到空格里### -把链表中的cookie移除之后会在网格中留下空格。其他的cookie应该落下来填补这些空格。我们再一次分成两部来处理: - -1.更新模型。 - -2.播放精灵动画。 - -在 **Level.swift**中添加新的函数: - - func fillHoles() -> Array> { - var columns = Array>() - // 1 - for column in 0..NumColumns { - var array = Array() - for row in 0..NumRows { - // 2 - if tiles[column, row] != nil && cookies[column, row] == nil { - // 3 - for lookup in (row + 1)..NumRows { - if let cookie = cookies[column, lookup] { - // 4 - cookies[column, lookup] = nil - cookies[column, row] = cookie - cookie.row = row - // 5 - array.append(cookie) - // 6 - break - } - } - } - } - // 7 - if !array.isEmpty { - columns.append(array) - } - } - return columns - } -这个函数检测哪里有空格然后把cookie落进空格中。它从底部开始,然后向上扫描。如果它发现一个空格,然后就会在它上面找到最近的一个cookie并把它移到空格中去。 - -下面是它的工作原理: - -1.你由下往上依次检查各行。 - -2.如果有一个空格,那么就会有一个空穴。记住:`tiles`数组描述这个关卡的形状。 - -3.你向上查找正好适合这个空穴的cookie。注意,这个空穴可能比一个方格要大(例如,可能是垂直方向的空链)并且网格中也可能存在多个空穴。 - -4.如果你发现另一个cookie,把它移动空穴中去。这样可以非常有效率的把cookie移下来。 - -5.你把cookie添加到数组中。每一列都有它自己的数组,并且cookie在屏幕上的位置越低,其在数组中的位置就越靠前。保持这个顺序的是非常重要的,以使动画代码可以应用正确的延迟时间。块的位置越远,动画开始前的延迟时间就越长。 - -6.一旦你找到一个cookie,你就不需要继续查找然后可以直接跳出内层循环。 - -7.如果一列没有任何空穴,那就没有必要把它添加到最终的数组中。 - -最后,这个函数返回一个包含各列中被移下来的cookie的数组。 - - - 注意:函数`fillHoles()` 返回值的类型是**Array>**(一个存储以“cookie为元素的数组”的数组),你也可以写成这种形式:`Cookie[][]`。 - -你已经用新的位置更新了存储cookie的数据模型,下面就是对精灵的处理。**GameScene** 将会播放精灵动画,而`GameViewController `是协调模板(**Level**)和视图(**GameScene**)的中间对象。 - -切换到**GameScene.swift**并添加一个新的动画函数: - - func animateFallingCookies(columns: Array>, completion: () -> ()) { - // 1 - var longestDuration: NSTimeInterval = 0 - for array in columns { - for (idx, cookie) in enumerate(array) { - let newPosition = pointForColumn(cookie.column, row: cookie.row) - // 2 - let delay = 0.05 + 0.15*NSTimeInterval(idx) - // 3 - let sprite = cookie.sprite! - let duration = NSTimeInterval(((sprite.position.y - newPosition.y) / TileHeight) * 0.1) - // 4 - longestDuration = max(longestDuration, duration + delay) - // 5 - let moveAction = SKAction.moveTo(newPosition, duration: duration) - moveAction.timingMode = .EaseOut - sprite.runAction( - SKAction.sequence([ - SKAction.waitForDuration(delay), - SKAction.group([moveAction, fallingCookieSound])])) - } - } - // 6 - runAction(SKAction.waitForDuration(longestDuration), completion: completion) - } -下面是工作原理: - -1.和其他的动画函数一样,你只能在所有的动画播放完后调用这个实现模块。因为下落的cookie数可能改变,你不能硬编码这个总的持续时间,而是要计算它。 - -2.越往上的cookie,动画的延迟时间就越长。这样比同时下落所有的cookie看起来更有动感。这个计算能够有效地前提是函数`fillHoles()`确保了越低的cookie在数组中的位置越靠前。 - -3.同样,动画的持续时间基于cookie下落的距离(每个方格0.1S)。你可以稍微调整这个这些数字来改变动画的播放效果。 - -4.计算出最长的动画时间。这也是游戏继续的钱必须等待的时间。 - -5.完成动画(包括延迟时间、动作、音效)。 - -6.在游戏继续前,你要等待所有的cookie全部落下来。 - -你现在可以把它整理到一起了。打开**GameViewController.swift**。用下面的内容替换函数`handleMatches()`中的内容: - - func handleMatches() { - let chains = level.removeMatches() - scene.animateMatchedCookies(chains) { - let columns = self.level.fillHoles() - self.scene.animateFallingCookies(columns) { - self.view.userInteractionEnabled = true - } - } - } -现在这个函数调用`fillHoles()`来更新模板,其中函数`fillHoles()`返回记录下落的cookie的数组。同时函数`handleMatches()`把返回的数组传递给场景,让场景播放动画并把精灵放到他们的新位置上去。 - - 注意:在Objective-C中访问一个属性或者调用一个方法,通常需要使用self。在Swift语言中,除了在闭包中,你是不需要这样做的。这就是你在函数`handleMatches()`中看到很多self的原因。Swift坚持这样做表明闭包确实通过强引用获取了self的值。实际上,如果你在比保重如果不指明self,Swift编译器会报错。 -试一下! - -cookie正在下落!注意一下,cookie可以正确的掠过关卡设计中的缺口。 - -###添加新的cookie### -还需要再做一件事情来完成游戏循环。下落的cookie会在每列的顶部留出空穴。 - - -你需要用新的cookie填满这些列。在 **Level.swift**中添加一个新函数: - - func topUpCookies() -> Array> { - var columns = Array>() - var cookieType: CookieType = .Unknown - - for column in 0..NumColumns { - var array = Array() - // 1 - for var row = NumRows - 1; row >= 0 && cookies[column, row] == nil; --row { - // 2 - if tiles[column, row] != nil { - // 3 - var newCookieType: CookieType - do { - newCookieType = CookieType.random() - } while newCookieType == cookieType - cookieType = newCookieType - // 4 - let cookie = Cookie(column: column, row: row, cookieType: cookieType) - cookies[column, row] = cookie - array.append(cookie) - } - } - // 5 - if !array.isEmpty { - columns.append(array) - } - } - return columns - } -这个函数在需要的地方添加cookie来填满一列。它返回一个存储着在有空格的列中添加的新cookie对象的数组。 - -如果某列有X个空格,那么它就需要X个新的cookie。现在的空穴全部在列的上端,因此你可以简单的从上往下扫描知道你找到一个cookie。 - -下面是它的工作原理: - -1.你从上往下扫描一列。这个for循环的终止条件是`cookies[column, row]`不为`nil`时---也就是找到一个cookie。 - -2.你可以忽略水平方向上的空缺,因为你只需填满网格中有瓷砖的方块。 - -3.你随机的创建一个新的cookie类型。但是它不能喝最后一个新的cookie类型相同,因为这样的话就会有太多可以直接消除的cookie。 - -4.创建一个新的cookie对象,然后添加到该列的数组中。 - -5.和以前一样,如果某列没有空穴,你就不需要把它添加到最终的数组中。 - - 函数`topUpCookies()`返回的数组中包括有空穴的各列形成的子数组。这些数组中的cookie对象时从上到下按顺序排列的。这个顺序对于下面的动画是非常重要的。 - -切换到**GameScene.swift**,并添加新的动画函数: - func animateNewCookies(columns: [[Cookie]], completion: () -> ()) { - // 1 - var longestDuration: NSTimeInterval = 0 - - for array in columns { - // 2 - let startRow = array[0].row + 1 - - for (idx, cookie) in enumerate(array) { - // 3 - let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName) - sprite.position = pointForColumn(cookie.column, row: startRow) - cookiesLayer.addChild(sprite) - cookie.sprite = sprite - // 4 - let delay = 0.1 + 0.2 * NSTimeInterval(array.count - idx - 1) - // 5 - let duration = NSTimeInterval(startRow - cookie.row) * 0.1 - longestDuration = max(longestDuration, duration + delay) - // 6 - let newPosition = pointForColumn(cookie.column, row: cookie.row) - let moveAction = SKAction.moveTo(newPosition, duration: duration) - moveAction.timingMode = .EaseOut - sprite.alpha = 0 - sprite.runAction( - SKAction.sequence([ - SKAction.waitForDuration(delay), - SKAction.group([ - SKAction.fadeInWithDuration(0.05), - moveAction, - addCookieSound]) - ])) - } - } - // 7 - runAction(SKAction.waitForDuration(longestDuration), completion: completion) - } -这个和下落cookie的动画类似。主要的区别是数组中cookie对象的按相反的顺序排序(从上到下),下面是这个函数的功能: - -1.动画结束之前,游戏是不能继续的。因此你可以出计算最长的动画时间,在第7步中要用到。 - -2.新的cookie精灵应该在一列中第一个的上面开始。找出瓷砖的行号的简单方法是查看数组中第一个cookie的行号,数组中的第一个cookie也经常是一列中最上面的一个。 - -3.创建一个新的cookie精灵。 - -4.cookie的位置越高,延迟的时间就越长,因此这些cookie看起来就像是一个接着一个的下落。 - -5.根据将要的下落的cookie的最远位置计算出动画的持续时间。 - -6.播放精灵下落的动画然后渐渐的显示出来。这样cookie的出现就不会显得那么的突兀。 - -7.动画结束之后,继续游戏。 - -最后,在 **GameViewController.swift**中,用下面的内容替换掉函数`handleMatches()`中的实现模块: - - func handleMatches() { - let chains = level.removeMatches() - scene.animateMatchedCookies(chains) { - let columns = self.level.fillHoles() - self.scene.animateFallingCookies(columns) { - let columns = self.level.topUpCookies() - self.scene.animateNewCookies(columns) { - self.view.userInteractionEnabled = true - } - } - } - } -试试看!!! - - -###不断下落的cookie### - -玩了一段时间之后你可能注意到有一些奇怪的问题。cookie从落下到空格里,新的cookie从顶部落下来,这些动作会形成新的有三个或者多个的满足条件的链表。但是然后会发生什么事情呢? - -你需要移除这些匹配的链表,让其他的cookie取代它们的位置。应该一直这样知道屏幕上没有匹配的cookie。到这个时候才能让玩家从新操作。 - -处理这些可能的不断下落的情况可能会很棘手,但是你已经写好了这个功能的代码!然后你只需要又满足条件的链表时不断的调用函数`handleMatches()`。 - -在 **GameViewController.swift**的`handleMatches()`函数中,把设置`userInteractionEnabled` 的那行改成:`self.handleMatches()`。就是`handleMatches()`调用它自身。 -这就是递归,也是非常有用的编程技术。使用递归的时候,你只需要注意一件事:你需要在某一时刻结束递归调用,否则的话,这个程序就会无限循环直至崩溃。 - -因为上面的原因,在函数`handleMatches()`顶部调用函数`removeMatches()`的后面添加下面的内容: - - if chains.count == 0 { - beginNextTurn() - return - } -如果没有可以匹配的cookie了,这时候就需要玩家去移动,并且为了防止再次的递归调用,这个函数就会退出。 - -最后,添加`beginNextTurn()`函数: - - func beginNextTurn() { - view.userInteractionEnabled = true - } -试试看!!!如果移除一个链表的时候在其他的地方形成了一条新的链表,游戏就会继续移除那个链表: - - -还有一个问题:玩了一段时间之后,游戏不去响应本来应该是有效地移动。这是有原因的,你能猜一下吗? - - 解答:玩家每移动一次,存储可能移动的列表就会超时。这时,你需要在玩家再次移动前重新计算这个列表。 -这部分内容的逻辑在**Level.swift**的`detectPossibleSwaps()`函数中。你需要在**GameViewController.swift**中的 `beginNextTurn()`函数中调用这个函数: - - func beginNextTurn() { - level.detectPossibleSwaps() - view.userInteractionEnabled = true - } -漂亮!现在完成了游戏的循环部分,并且可以补充无数个cookie! - -###得分### - -在游戏 Cookie Crunch Adventure中,玩家的目标是用尽可能少的交换次数取得一个可观的分数。这些值都来自于 JSON文件。游戏中,应该把这些数字显示在屏幕上,让玩家知道自己玩的有多好。 - -首先,在**GameViewController.swift**中添加下面的属性: - - var movesLeft: Int = 0 - var score: Int = 0 - - @IBOutlet var targetLabel: UILabel - @IBOutlet var movesLabel: UILabel - @IBOutlet var scoreLabel: UILabel -其中`movesLeft`和`score`变量记录玩家水平,`outlets`属性把这些显示到屏幕中。 - -打开**Main.storyboard**,并在视图中添加下面的这些标签。把视图控制器设计成下面这样: - -(这是Xcode 5的截图,但是看起来和在Xcode 6中的一样) - -确保取消选中文件检查器中的 **Use Auto Layout**(自动布局),就是右边的第一个选项卡。在显示的窗口中,选择**Disable Size Classes**。就像上面的图片中显示的一样,这样会使场景变成iPhone屏幕的尺寸。 - -可以给主视图选一个灰色的背景,来让标签更清楚。字体改成**Gill Sans Bold**,其中数字的大小改成20.0,文字的大小改成14.0。您还可以设置一个轻微的阴影标签,使他们更容易看到。 - -如果你把数字设成居中看起来会更好。把这三个数字和他们各自的outlets属性联系起来。 - -因为得分和移动的最大次数被存储在JSON文件中,你应该把他们加载到关卡中。在Level.swift中添加下面的属性: - - let targetScore: Int! - let maximumMoves: Int! -这些属性会存放从JSON中加载的数据。他们用!标记,是因为他们可能没有得到任何值(如果因为某种原因加载关卡失败)。 - -在Level.swift的init(filename:)的底部添加下面两行: - - init(filename: String) { - ... - if let tilesArray: AnyObject = dictionary["tiles"] { - ... - // Add these two lines: - targetScore = (dictionary["targetScore"] as NSNumber).integerValue - maximumMoves = (dictionary["moves"] as NSNumber).integerValue - } - } - } -到目前为止,你已经把JSON解析到词典(dictionary)中,因此你可以获取两个值,并把他们存储起来。 - -切换到 **GameViewController.swift**,添加下面的函数: - - func updateLabels() { - targetLabel.text = NSString(format: "%ld", level.targetScore) - movesLabel.text = NSString(format: "%ld", movesLeft) - scoreLabel.text = NSString(format: "%ld", score) - } -每次更新标签中的文本之后,你都会调用这个函数。 - -在beginGame()的顶部shuffle()的调用之前添加下面的内容: - - movesLeft = level.maximumMoves - score = 0 - updateLabels() -上面的内容会重置所有的数据。点击生成并运行,你的显示画面应该和下面的差不多: - - -###计算分值### - -积分规则很简单: - -
    -
  • 三个可以消除的连在一起是60分
  • -
  • 三个以上每多一个就多60分
  • -
-因此,4个相同的cookie就是120分,5个相同的cookie就是180,以此类推。 - -把分值存进Chain对象中是最简单的,这样每个可以消除的链表都知道自己是多少分。 - -在Chain.swift中添加下面的内容: - - var score: Int = 0 -得分是模型数据,因此需要**Level**重新计算。在Level.swift中添加下面的函数: - - func calculateScores(chains: Set) { - // 3-chain is 60 pts, 4-chain is 120, 5-chain is 180, and so on - for chain in chains { - chain.score = 60 * (chain.length - 2) - } - } -现在在函数 `removeMatches()`中的return语句之前调用这个函数: - - calculateScores(horizontalChains) - calculateScores(verticalChains) -因为有水平方向和竖直方向两套chain对象,因此你需要调用这个函数两次。 - -既然,关卡知道怎么样计算得分和怎么样把他们存进Chain对象,你可以刷新玩家的得分,并把它们显示到屏幕上。 - -这些都是在**GameViewController.swift**中完成的。在函数`handleMatches()`中,在`self.level.fillHoles()`调用之前添加下面的内容: - - for chain in chains { - self.score += chain.score - } - self.updateLabels() -上面的内容只是简单的遍历链表,把他们的分值添加到玩家的总得分上去,然后更新标签。 - -试试看!!!交换cookie,观察你增加的得分: - - -###分值动画### - -如果每一个链表的得分都会显示一个特别漂亮的动画会是很有趣的事情。在 **GameScene.swift**中,添加一个新的函数: - - func animateScoreForChain(chain: Chain) { - // Figure out what the midpoint of the chain is. - let firstSprite = chain.firstCookie().sprite! - let lastSprite = chain.lastCookie().sprite! - let centerPosition = CGPoint( - x: (firstSprite.position.x + lastSprite.position.x)/2, - y: (firstSprite.position.y + lastSprite.position.y)/2 - 8) - - // Add a label for the score that slowly floats up. - let scoreLabel = SKLabelNode(fontNamed: "GillSans-BoldItalic") - scoreLabel.fontSize = 16 - scoreLabel.text = NSString(format: "%ld", chain.score) - scoreLabel.position = centerPosition - scoreLabel.zPosition = 300 - cookiesLayer.addChild(scoreLabel) - - let moveAction = SKAction.moveBy(CGVector(dx: 0, dy: 3), duration: 0.7) - moveAction.timingMode = .EaseOut - scoreLabel.runAction(SKAction.sequence([moveAction, SKAction.removeFromParent()])) - } -上面的函数用得分和链表中间的位置创建一个新的变量`SKLabelNode` ,分值的数字在消失之前会向上浮动几个像素。 - -在函数 `animateMatchedCookies()`中的两个for循环之间调用这个新函数: - - for chain in chains { - - // Add this line: - animateScoreForChain(chain) - - for cookie in chain.cookies { -当使用`SKLabelNode`时,Sprite Kit需要加载字体,然后把它转换成一个纹理。这个过程只发生一次,但是还是会有很小的延迟,因此在游戏开始的提前加载字体是一个很明智的方式。 - -在**GameScene**中的 `init()`函数的底部添加下面的内容: - - SKLabelNode(fontNamed: "GillSans-BoldItalic") -现在试试看!!!点击生成和运行,然后获取一些得分。 - - -###连击### - -使 **Candy Crush Saga**特别有趣的是有连击的功能,或者是一行中有多个可以匹配的cookie。 - -在玩家点出连击的时候,你应该给他额外的得分。为了达到这样的效果,你需要添加一个连击的倍乘因子,第一次是正常得分,第二次是得双倍的分,第三次是得三倍的分,以此类推。 - -在Level.swift中添加下面的私有属性: - - var comboMultiplier: Int = 0 // private -把`calculateScores()`更新成下面的样子: - - func calculateScores(chains: Set) { - // 3-chain is 60 pts, 4-chain is 120, 5-chain is 180, and so on - for chain in chains { - chain.score = 60 * (chain.length - 2) * comboMultiplier - ++comboMultiplier - } - } -这个函数使链表的分值乘上连击倍乘因子,然后在下一个链表的时候增加倍乘因子。 - -你也需要一个在下一轮可以充值倍乘因子的函数。在Level.swift中添加下面的函数: - - func resetComboMultiplier() { - comboMultiplier = 1 - } -打开 GameViewController.swift,并找到beginGame()。然后在调用‘shuffle()前面添加下面的一行内容: - - level.resetComboMultiplier() -在`beginNextTurn()`的顶部添加一行相同的内容。 - -现在你就有连击功能了。试试看!!! - - - 问题:你应该怎么样检测L型的链表,并使每行的值加倍? - 解答:L型的链表由两个链表组成,一个水平的和一个竖直的,它们公用一个顶角上的cookie。你可以遍历水平方向的链表并查看该链表的第一个或者是最后一个cookie是不是也在其他的竖直方向的链表中。如果是的话,移除这两个链表,并把它们结合成一个新类型的链表。 - -###输赢### - -玩家只有有限的移动次数来取得目标分数。如果没有实现的话,那么游戏结束。这部分的逻辑并不难添加。 - -在**GameViewController.swift**中添加一个新的函数: - - func decrementMoves() { - --movesLeft - updateLabels() - } -这个函数只是简单的实现移动次数的减少,并把它更新到屏幕的标签中。 - -在beginNextTurn()函数中的底部调用上面的函数: - - decrementMoves() -点击生成和运行,然后查看数值的变化。每交换一次,游戏就会移除匹配的cookie,并减少剩余的可移动次数。 - -当然,你仍然需要检查玩家用完移动次数(游戏结束)或者是达到目标分值(胜利),然后根据具体的情况作出响应。 - -首先,故事板需要做一些工作。 - -###胜利或失败的界面### - -打开Main.storyboard,然后往视图中拖进去一张图片。图片的大小为320×150像素,并且竖直方向居中。 - - -图片视图上面会显示失败或者胜利的信息。 - -切换到Size inspector(尺寸检查器)并使图像视图的自动调整大小的蒙片看起来像下面这样: - - -不管屏幕的尺寸是多大,图片都会一直居中。 - -现在在GameViewController.swift 中把这个图像视图和一个新的输出变量gameOverPanel连接起来。 - - @IBOutlet var gameOverPanel: UIImageView -现在为手势识别也添加一个属性: - - var tapGestureRecognizer: UITapGestureRecognizer! -在函数`viewDidLoad()`中,在你显示场景之前,要确保这个图像视图是隐藏的: - - gameOverPanel.hidden = true -现在添加一个新的函数来显示游戏结束的界面: - - func showGameOver() { - gameOverPanel.hidden = false - scene.userInteractionEnabled = false - - tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideGameOver") - view.addGestureRecognizer(tapGestureRecognizer) - } -这个函数使图像视图显示出来,禁用在场景中的触摸来阻止玩家交换cookie并且添加一个可以重新开始游戏的点击的手势识别。 - -添加下面的函数: - - func hideGameOver() { - view.removeGestureRecognizer(tapGestureRecognizer) - tapGestureRecognizer = nil - - gameOverPanel.hidden = true - scene.userInteractionEnabled = true - - beginGame() - } -这个函数讲隐藏游戏结束的界面并且重新开始游戏。 - -检测什么时间显示游戏结束界面的逻辑在函数`decrementMoves()`中。在那个函数的底部添加下面的内容: - - if score >= level.targetScore { - gameOverPanel.image = UIImage(named: "LevelComplete") - showGameOver() - } else if movesLeft == 0 { - gameOverPanel.image = UIImage(named: "GameOver") - showGameOver() - } -如果当前的分数大于等于目标分数,那么玩家就取得了胜利!如果剩余移动次数是0,玩家就输掉了游戏。 - -不论哪种情况,函数都会加载适当的图片并且调用函数 `showGameOver()`来显示的屏幕上。 - -试试看!!!如果你赢了,你就会看到下面的内容: - -同样,如果你的剩余移动次数为0,你就会看到游戏结束的信息。 - -###转换动画### - -在cookie的上面显示标题看起来就会有点混乱,因此也让我们添加一点动画。在GameScene.swift中添加下面的两个函数: - - func animateGameOver(completion: () -> ()) { - let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3) - action.timingMode = .EaseIn - gameLayer.runAction(action, completion: completion) - } - - func animateBeginGame(completion: () -> ()) { - gameLayer.hidden = false - gameLayer.position = CGPoint(x: 0, y: size.height) - let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3) - action.timingMode = .EaseOut - gameLayer.runAction(action, completion: completion) - } -函数`animateGameOver()`会播放一个使`gameLayer`慢慢消失的动画。 `animateBeginGame()` 函数却正好相反,使`gameLayer`慢慢的从屏幕的顶部滑到屏幕中。 - -游戏刚开始的时候,你也想调用函数`animateBeginGame()`来播放相同的动画。在动画开始前,如果游戏图层是隐藏的看起来可能更好,因此往GameScene.swift中的 init(size:)中创建gameLayer节点 的后面添加下面的内容: - - gameLayer.hidden = true - -现在打开`GameViewController.swift`,并且在函数`showGameOver()`中调用函数`animateGameOver()`: - - func showGameOver() { - gameOverPanel.hidden = false - scene.userInteractionEnabled = false - - scene.animateGameOver() { - self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideGameOver") - self.view.addGestureRecognizer(self.tapGestureRecognizer) - } - } -注意:在动画播放完后,添加响应点击的手势识别。这样可以防止玩家在播放动画的时候点击。 - -最后,在GameViewController.swift中的`beginGame()`中,在调用函数`shuffle()`之前,调用函数`animateBeginGame()`: - - scene.animateBeginGame() { } - -现在这个动画的实现模块是空的,但是你很快就可以添加内容了。 - -现在,游戏结束后,当你点击屏幕的时候,应该下拉屏幕,是cookie显示在他们开始的位置。Nice!、 - - -Whoops!出问题了,你好像没有把老的cookie精灵移除。 - -在GameScene.swift 中添加下面的函数,完成清除的功能: - - func removeAllCookieSprites() { - cookiesLayer.removeAllChildren() - } -在 GameViewController.swift中的函数`shuffle()`中第一件事就是调用上面的函数: - - scene.removeAllCookieSprites() -问题解决了!点击生成和运行,现在你的游戏开始干净利落的重新启动了。 - -###“手动洗牌”### - -还有一种情况:可能会发生--尽管很少见--就是没有可以移动的cookie了。那样的话,玩家就被困住了。 - -这种情况都很多种处理方法。例如,Candy Crush Saga自动的重排cookie。但是在Cookie Crunch中,将会把这个权利交给玩家。你可以让玩家在任何时候点击按钮来重排cookie,但是代价是把这个点击算作移动了一次。 - -在GameViewController.swift中添加一个输出属性: - - @IBOutlet var shuffleButton: UIButton -添加下面的动作函数: - - @IBAction func shuffleButtonPressed(AnyObject) { - shuffle() - decrementMoves() - } -点击使cookie重排算是移动了一次,因此会调用函数`decrementMoves()`。 - -在函数`showGameOver()`中,添加下面的一行代码是重排的按钮隐藏起来: - - shuffleButton.hidden = true - -在函数` viewDidLoad()`中也要这样做,使按钮在游戏开始的时候是隐藏的。 - -在函数`beginGame()`中,在动画的实现模块,把按钮重新显示在屏幕上: - - scene.animateBeginGame() { - self.shuffleButton.hidden = false - } -现在打开Main.storyboard,并在屏幕的底部添加一个按钮: - -设置按钮的标题为“重排”并且使按钮的大小为100x36点。为了使按钮看起来美观,把字体改成Gill Sans Bold,10pt。使文字的颜色为白色并有50%不透明度的黑色阴影。背景可以选择第一部分中你添加到资源目录中名字为“按钮”的图片。 - -使按钮紧靠屏幕的底部,并设置自动调整大小使其在3.5英寸的手机上也能正常显示。 - - -最后,把输出属性shuffleButton 和按钮连接起来,把它的Touch Up Inside事件和shuffleButtonPressed:动作联系起来。 - -试试看!!! - - - 注意:当我们洗牌的时候,我们拿着实实在在的牌,改变他们的顺序,然后处理同一副但是顺序不同的牌。但是,在这个游戏中,你获取的是随机的cookie。因此找到一个至少允许移动一次的相同的cookie的分配方式是无法计算的,毕竟,这是一个随机性的游戏。 - -直接重排会显得和突然,因此让我们为新的cookie添加一个漂亮的动画。在GameScene.swift中找到函数`addSpritesForCookies()`并在for循环的内部,现存代码的后面添加下面的内容: - - // 给每个cookie一个短暂的延迟,然后把他们渐渐的显现出来. - sprite.alpha = 0 - sprite.xScale = 0.5 - sprite.yScale = 0.5 - - sprite.runAction( - SKAction.sequence([ - SKAction.waitForDuration(0.25, withRange: 0.5), - SKAction.group([ - SKAction.fadeInWithDuration(0.25), - SKAction.scaleTo(1.0, duration: 0.25) - ]) - ])) - -上面的内容给每个cookie一个短暂的、随机性延时,然后把他们渐渐的显示到屏幕中。效果看起来像下面这样: - - -###音乐### - -当玩家碾碎cookie的时候,我们可以播放一些舒缓的、放松的音乐。在GameViewController.swift 的顶部添加下面的内容来包含AVFoundation框架: - - import AVFoundation -并添加下面的属性: - - var backgroundMusic: AVAudioPlayer! -在函数`viewDidLoad()`中调用`beginGame()`的前面添加下面的内容: - - // 加载并播放背景音乐. - let url = NSBundle.mainBundle().URLForResource("Mining by Moonlight", withExtension: "mp3") - backgroundMusic = AVAudioPlayer(contentsOfURL: url, error: nil) - backgroundMusic.numberOfLoops = -1 - backgroundMusic.play() -上面的内容会加载背景音乐,并且循环播放。这样就给游戏添加了很多的旋律。 - -###绘制更加漂亮的瓷砖### - -如果你把你的游戏和Candy Crush Saga仔细的对比,你就会注意到绘制的瓷砖有些许的不一样。Candy Crush中的边界画的更好一点。 - - -还有,如果cookie在下降的时候通过一个缺口,你的游戏是直接的背景的上面绘制,但是Candy Crush中却是在背景的后面绘制。 - - -要创建这样的效果是不难的,但是你需要一些新的cookie精灵。你可以在文件夹Grid.atlas下面找到此指南使用的全部资源。把这个文件夹拖进你的Xcode项目中。这样会用这些图片创建一个新的纹理集。 - -在 GameScene.swift中,添加两个新的属性: - - let cropLayer = SKCropNode() - let maskLayer = SKNode() - -在函数`init(size:)`中,在创建tilesLayer的代码的后面添加下面的内容: - - gameLayer.addChild(cropLayer) - - maskLayer.position = layerPosition - cropLayer.maskNode = maskLayer - -这样会创建两个新的图层:cropLayer--它是一种被称作SKCropNode的特殊类型的节点,还有一个蒙版图层。裁剪节点只绘制蒙版中有像素的子节点。这样你就可以在有瓷砖的地方绘制cookie,而不会在背景上绘制。 - -用下面的内容: - - cropLayer.addChild(cookiesLayer) -替换 - - gameLayer.addChild(cookiesLayer) - -现在,你把cookiesLayer 添加到这个新的cropLayer中,而不是直接的添加到gameLayer中。 - -为了填充剪裁图层的蒙版区域,按下面的内容修改函数`addTiles()`: - -
    -
  • 用"MaskTile"替换 "Tile"
  • -
  • 用maskLayer替换tilesLayer
  • -
-无论哪里有瓷砖,这个函数现在都是往图层中绘制特殊的蒙版瓷砖(功能和SKCropNode的蒙版一样)。蒙版瓷砖比一般正常的瓷砖大一点。 - -点击生成并运行。注意当cookie下落通过缺口的时候是怎么样被裁剪的。 - - - 提示:如果你想看看蒙版图层是什么样的,可以在`init(size:)`中添加下面的内容: - cropLayer.addChild(maskLayer) - 当你结束的时候,千万不要忘记移除它! -最后一步,在addTiles()的底部添加下面的代码: - - for row in 0...NumRows { - for column in 0...NumColumns { - let topLeft = (column > 0) && (row < NumRows) - && level.tileAtColumn(column - 1, row: row) - let bottomLeft = (column > 0) && (row > 0) - && level.tileAtColumn(column - 1, row: row - 1) - let topRight= (column < NumColumns) && (row < NumRows) - && level.tileAtColumn(column, row: row) - let bottomRight = (column < NumColumns) && (row > 0) - && level.tileAtColumn(column, row: row - 1) - - // The tiles are named from 0 to 15, according to the bitmask that is - // made by combining these four values. - let value = Int(topLeft) | Int(topRight) << 1 | Int(bottomLeft) << 2 | Int(bottomRight) << 3 - - // Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn. - if value != 0 && value != 6 && value != 9 { - let name = String(format: "Tile_%ld", value) - let tileNode = SKSpriteNode(imageNamed: name) - var point = pointForColumn(column, row: row) - point.x -= TileWidth/2 - point.y -= TileHeight/2 - tileNode.position = point - tilesLayer.addChild(tileNode) - } - } - } -上面的内容会在水平的瓷砖之间画一个特定的类型的边界。你可以挑战一下,自己去破解它的工作原理.:) - -解答: -假设把一个瓷砖分成四个象限。四个波尔类型的变量来表示这个瓷砖有什么类型的边界。例如,在一个正方形关卡中,右下角的瓷砖需要一个背景去覆盖左上角的(查看Tile_1.png)。那种四周都有相邻瓷砖的瓷砖就会有一个完整的背景(查看Tile_15.png)。 - -点击生成并运行,你现在应该有一个看起来和玩起来都和 Candy Crush Saga类型的游戏! - - -###何去何从### - -祝贺你完成了这部分内容!这是一个很长的Swift指南,现在你用了编写自己三消类游戏的全部基础模块。 - -你可以在这里下载最终的Xcode项目。 - -下面是一些你可以添加的其他的特性: - -
    -
  • 当玩家匹配成指定的形状的时候可以出现特殊的cookie。例如,当你在一行中匹配到四个可以消除的cookie的时候,Candy Crush Saga就会给出一个可以消除整行的特殊cookie。
  • -
  • 检测特殊的链表,比如L型和T型,这时可以奖励玩家更多的积分或者是特殊的物品。
  • -
  • 玩家可以随时使用的物品。例如,一个可以一下子移除屏幕上同一种类型cookie的物品。
  • -
  • 果冻关卡:在这些关卡里,某些瓷砖上显示果冻。你有X步来移除这些果冻。这时瓷砖类就派上用场了。你可以添加一个BOOL类型的果冻属性,如果玩家在这个瓷砖上匹配到cookie,就把这个果冻属性设为NO,然后移除果冻。
  • -
  • 提示:如果玩家两秒内没有移动的话,就加亮显示互换之后可以消除的两个cookie
  • -
  • 如果玩家完成当前关,就自动的进入下一关。
  • -
  • 如果没有可以移动的cookie的时候自动重排所有的cookie。
  • -
- -你看,仍然有很多我们可以做的。好好享受哦! - -小组成员: Vicki Wenderlich的原图, Kevin MacLeod的音乐,音效是基于 freesound.org的样品。 - -源代码中使用的一些技术是基于 a blog post by Emanuele Feronato的。 +# 如何使用Swift指南制作一个像Candy Crush的游戏:第二部分 + +欢迎回到我们的关于如何使用Swift指南制作一个像Candy Crush的游戏的系列教程。 + + +这是教你如何制作一个像Candy Crush Saga或Bejeweled等三消类游戏系列教程的第二部分。游戏的名字是Cookie Crunch Adventure,并且使用的是美味的cookie哦! + +在[指南的第一部分](How-to-Make-a-Game-Like-Candy-Crush-with-Swift-Tutorial-Part1.md)中,你从JSON文件夹中加载了标准的形状,把cookie显示到了屏幕上,实现了监测点击和交换cookie的逻辑。 + +在第二部分(也是最后部分)中,你将会实现游戏剩下的内容,添加加载动画并把Cookie Crunch Adventure美化达到排名前十的质量。想想就感觉特别美。 + +这部分Swift指南是前一部分内容的继续。如果你还没有学习过,这里是到目前为止所有的源代码。你还需要里面的资源文件(和第一部分中的是相同的文件)。 + +让我们碾碎cookie吧! + +### 开始 ### + +你原来所做的事情是允许玩家交换cookie。下面,就需要去处理交换之后的结果了。 + +交换通常会形成一个有三个或者更多相匹配的cookie的链表。下面要做的就是从屏幕上消除这些相同的cookie,然后给玩家一些积分奖励。 + +这是这些事件的顺序: + + +你已经完成了前三步:用cookie填充关卡,计算可能的交换,等待玩家交换。在Swift指南的这部分内容中,你将会完成剩下的步骤。 + +### 找到链表 ### + +这个时候,玩家一般已经移动并交换了两个cookie。如果交换之后会形成一个有三个或者更多相同类型的cookie的链表--至少有一个,也可能有其他的链表,则游戏只允许玩家交换一次。 + +在你从屏幕上消除这些相同的cookie之前,你需要先找到满足条件的所有链表。这就是我们接下来要做的事情。 + +首先,生成一个描述链表的类。找到File\New\File...,选择IOS\Source\Swift File模板,然后点击下一步,把文件命名为Chain.swift,然后点击创建。 + +用下面的内容替换Chain.swift中的内容: + + + class Chain: Hashable, Printable { + var cookies = Array() // private + + enum ChainType: Printable { + case Horizontal + case Vertical + + var description: String { + switch self { + case .Horizontal: return "Horizontal" + case .Vertical: return "Vertical" + } + } + } + + var chainType: ChainType + + init(chainType: ChainType) { + self.chainType = chainType + } + + func addCookie(cookie: Cookie) { + cookies.append(cookie) + } + + func firstCookie() -> Cookie { + return cookies[0] + } + + func lastCookie() -> Cookie { + return cookies[cookies.count - 1] + } + + var length: Int { + return cookies.count + } + + var description: String { + return "type:\(chainType) cookies:\(cookies)" + } + + var hashValue: Int { + return reduce(cookies, 0) { $0.hashValue ^ $1.hashValue } + } + } + + func ==(lhs: Chain, rhs: Chain) -> Bool { + return lhs.cookies == rhs.cookies + } + +Chain类有一个存储cookie对象的数组和一个表示水平(行)或垂直(列)的属性。这个属性被定义为枚举类型;因为它和Chain是成对出现的,因此它嵌套在Chain类的内部。如果你喜欢挑战,你也可以添加更加复杂的链表类型,比如L-和T-shapes。 + +这里使用Array而不是Set来存储cookie对象是有原因的:这样更方便记住cookie对象的顺序,使你知道哪些cookie在链表的尾部。使把多个链表结合到一个链表中来检测那些L-或T-shapes更加简单。 + + + 注意:chain类实现了Hashable,所以可以把它放进Set中。 + hashValue的代码看起来有点奇怪,但是它仅仅完成了把链表 + 中所有cookie的值进行异或的运算。 + reduce()函数是Swift更多高级的功能性编程特性的一个体现。 + +为了好好的利用这些chain对象,需要打开Level.swift。然后添加一个名字为removeMatches()的函数,但是在这之前,你需要一些协助函数来完成找到满足条件的链表的繁重工作。 + +为了找到满足条件的链表,你需要一对for循环来遍历这个关卡网格的每一个方块。 + + +当遍历水平方向的一行中的cookie时,你想要找到满足链表条件的第一个cookie。 + +如果有一个满足条件的链表,那么第一个cookie的右边必须有两个紧邻的且类型相同的的cookie。然后你就可以跳过这些相同类型的cookie直到你找到一个不满足链表条件的cookie。你重复这个动作直到你考虑了所有的可能。 + +把下面的函数添加到Level.swift中,来检查水平方向cookie的匹配情况: + + func detectHorizontalMatches() -> Set { + // 1 + let set = Set() + // 2 + for row in 0..NumRows { + for var column = 0; column < NumColumns - 2 ; { + // 3 + if let cookie = cookies[column, row] { + let matchType = cookie.cookieType + // 4 + if cookies[column + 1, row]?.cookieType == matchType && + cookies[column + 2, row]?.cookieType == matchType { + // 5 + let chain = Chain(chainType: .Horizontal) + do { + chain.addCookie(cookies[column, row]!) + ++column + } + while column < NumColumns && cookies[column, row]?.cookieType == matchType + + set.addElement(chain) + continue + } + } + // 6 + ++column + } + } + return set + } + +下面是函数的工作原理: + +1.你创建一个Set保存水平方向的链表(Chain对象)。然后,你把这些链表中的cookie从游戏中删除。 + +2.循环这些行和列。注意:你并不需要去检查最后两列,因为这些cookie并不会形成一个新的链表。还要注意内层的for循环并不会增加循环计数,只有在循环体中满足一定的条件时才会有增加。 + +3.在关卡设计中你可以跳过任何的缺口。 + +4.你检查是否接下来的两列有相同类型的cookie。通常,你需要注意不要在做`cookies[column + 2, row]`类似的运算的时候超过数组的边界,但是这里并不会发生错误。这就是for循环的限定条件为`NumColumns - 2`。注意使用问号的可选性链接。 + +5.这个时候,会有一个至少有三个cookie的链表。这一步遍历所有的可以匹配的cookie直到找到一个不满足链表的cookie或者是到达网格的末端。然后把所有匹配的cookie添加到一个Chain对象中。每匹配一次增加一列。 + +6.如果接下来的两个cookie和当前的不匹配或者是有一个空格,那就没有链表了,你就可以直接的跳过当前的cookie。 + + + 注意:如果在网格中有一个空格,使用可选的链接-- `cookies[column, row]?`后面的问号--确保while循环在满足这个条件的时候终止。上面的逻辑对于水平方向上有空格的行也是适用的。漂亮! + +其次,添加下面的函数来检查垂直方向的cookie匹配情况: + + func detectVerticalMatches() -> Set { + let set = Set() + + for column in 0..NumColumns { + for var row = 0; row < NumRows - 2; { + if let cookie = cookies[column, row] { + let matchType = cookie.cookieType + + if cookies[column, row + 1]?.cookieType == matchType &&cookies[column, row + 2]?.cookieType == matchType { + + let chain = Chain(chainType: .Vertical) + do { + chain.addCookie(cookies[column, row]!) + ++row + } + while row < NumRows && cookies[column, row]?.cookieType == matchType + + set.addElement(chain) + continue + } + } + ++row + } + } + return set + } +垂直方向上有相同的逻辑,但是需要把列放在外层的for循环上,行放在内层的for循环上。 + +你也许会疑惑当我们检测到他们满足链表条件的时候为什么不直接的把他们从关卡中移除。是因为某个cookie可能同时在两个链表中:一个水平方向的、一个垂直方向的。因此在你检查了水平和垂直方向的两个选择之前你并不像直接的把它移除。 + +既然两个检测函数都已经就绪了,添加下面的`removeMatches()`的具体实现: + + func removeMatches() -> Set { + let horizontalChains = detectHorizontalMatches() + let verticalChains = detectVerticalMatches() + + println("Horizontal matches: \(horizontalChains)") + println("Vertical matches: \(verticalChains)") + + return horizontalChains.unionSet(verticalChains) + } +这个函数调用上面的两个协助函数,然后把结果结合起来放到一个`set`中。然后,你将要在这个函数中添加更多的逻辑处理,但是现在你只对找到这些匹配的cookie并返回`set`感兴趣。 + +你仍然需要在`GameViewController.swift`中调用`removeMatches()`。添加下面的协助函数: + + func handleMatches() { + let chains = level.removeMatches() + // TODO: do something with the chains set + } +然后,你就会发现这个函数会移除cookie链表并把其他的cookie放到空格里。在函数`handleSwipe()`中,把`scene.animateSwap()`的调用改变成下面的形式: + + scene.animateSwap(swap, completion: handleMatches) +回想一下,闭包和函数在Swift中是相同的。因此你可以给函数`animateSwap()`传递一个函数名来代替闭包块。 + +生成并运行,然后交换两个cookie形成链表。你在Xcode的debug窗口中应该可以看到下面的图形: + + +### 移除链表 ### +到目前为止,函数`removeMatches()`只能检测匹配的链表。现在你将会播放一个漂亮的动画并把满足条件的cookie从游戏中移除。 + +首先,你需要更新一个数据模型--就是把cookie对象从二维网格数组中移除。这些完成之后,你可以告诉`GameScene`播放这些存在的cookie精灵的动画。 + +从模型中移除这些cookie是很简单的。在`Level.swift`中添加下面的函数: + + func removeCookies(chains: Set) { + for chain in chains { + for cookie in chain.cookies { + cookies[cookie.column, cookie.row] = nil + } + } + } +每一个链表都有一串cookie对象,每个cookie对象都知道它在网格中的行号与列号。因此,你可以简单的把数组中的元素置为`nil`来从数据模型中移除这些cookie对象。 + + 注意:现在,Chain对象只是Cookie对象的所有者。当这些链表被释放的时候,这些cookie对象也会被释放。 +在函数`removeMatches()`中,用下面的内容替换`println()`的声明: + + removeCookies(horizontalChains) + removeCookies(verticalChains) +注意数据模型。现在切换到`GameScene.swift`,并添加下面的函数: + + func animateMatchedCookies(chains: Set, completion: () -> ()) { + for chain in chains { + for cookie in chain.cookies { + if let sprite = cookie.sprite { + if sprite.actionForKey("removing") == nil { + let scaleAction = SKAction.scaleTo(0.1, duration: 0.3) + scaleAction.timingMode = .EaseOut + sprite.runAction(SKAction.sequence([scaleAction, SKAction.removeFromParent()]),withKey:"removing") + } + } + } + } + runAction(matchSound) + runAction(SKAction.waitForDuration(0.3), completion: completion) + } +这个函数循环所有的链表和每个链表中所有的元素,然后播放动画。 + +因为同一个cookie可能同时在两个链表中(一个水平的和一个垂直的),你要确保只给精灵播放一个动画,而不是两个。这就是动作被添加给那些有“移除”关键字的精灵的原因。如果这样的动作已经存在,你不要再给精灵添加一个新的动画。 + +当缩小动画播放完成后,已经把精灵从cookie面板上移除。这个函数结尾处的`waitForDuration()`动作是确保游戏的其余部分只能在动画结束后才能继续。 + +打开**`GameViewController.swift`**,改变函数`handleMatches()`来调用新的动画: + + func handleMatches() { + let chains = level.removeMatches() + + scene.animateMatchedCookies(chains) { + self.view.userInteractionEnabled = true + } + } +试一下。点击生成和运行,然后形成一些匹配的cookie: + +注意:当移除链表的动画正在播放的时候,你并不希望玩家能够点击任何东西。因此,在处理点击程序中,第一件事就是使`userInteractionEnabled`无效,当动画播放完成后,再次使它生效。 + +### 把cookie降到空格里 ### +把链表中的cookie移除之后会在网格中留下空格。其他的cookie应该落下来填补这些空格。我们再一次分成两部来处理: + +1.更新模型。 + +2.播放精灵动画。 + +在 **Level.swift**中添加新的函数: + + func fillHoles() -> Array> { + var columns = Array>() + // 1 + for column in 0..NumColumns { + var array = Array() + for row in 0..NumRows { + // 2 + if tiles[column, row] != nil && cookies[column, row] == nil { + // 3 + for lookup in (row + 1)..NumRows { + if let cookie = cookies[column, lookup] { + // 4 + cookies[column, lookup] = nil + cookies[column, row] = cookie + cookie.row = row + // 5 + array.append(cookie) + // 6 + break + } + } + } + } + // 7 + if !array.isEmpty { + columns.append(array) + } + } + return columns + } +这个函数检测哪里有空格然后把cookie落进空格中。它从底部开始,然后向上扫描。如果它发现一个空格,然后就会在它上面找到最近的一个cookie并把它移到空格中去。 + +下面是它的工作原理: + +1.你由下往上依次检查各行。 + +2.如果有一个空格,那么就会有一个空穴。记住:`tiles`数组描述这个关卡的形状。 + +3.你向上查找正好适合这个空穴的cookie。注意,这个空穴可能比一个方格要大(例如,可能是垂直方向的空链)并且网格中也可能存在多个空穴。 + +4.如果你发现另一个cookie,把它移动空穴中去。这样可以非常有效率的把cookie移下来。 + +5.你把cookie添加到数组中。每一列都有它自己的数组,并且cookie在屏幕上的位置越低,其在数组中的位置就越靠前。保持这个顺序的是非常重要的,以使动画代码可以应用正确的延迟时间。块的位置越远,动画开始前的延迟时间就越长。 + +6.一旦你找到一个cookie,你就不需要继续查找然后可以直接跳出内层循环。 + +7.如果一列没有任何空穴,那就没有必要把它添加到最终的数组中。 + +最后,这个函数返回一个包含各列中被移下来的cookie的数组。 + + + 注意:函数`fillHoles()` 返回值的类型是**Array>**(一个存储以“cookie为元素的数组”的数组),你也可以写成这种形式:`Cookie[][]`。 + +你已经用新的位置更新了存储cookie的数据模型,下面就是对精灵的处理。**GameScene** 将会播放精灵动画,而`GameViewController `是协调模板(**Level**)和视图(**GameScene**)的中间对象。 + +切换到**GameScene.swift**并添加一个新的动画函数: + + func animateFallingCookies(columns: Array>, completion: () -> ()) { + // 1 + var longestDuration: NSTimeInterval = 0 + for array in columns { + for (idx, cookie) in enumerate(array) { + let newPosition = pointForColumn(cookie.column, row: cookie.row) + // 2 + let delay = 0.05 + 0.15*NSTimeInterval(idx) + // 3 + let sprite = cookie.sprite! + let duration = NSTimeInterval(((sprite.position.y - newPosition.y) / TileHeight) * 0.1) + // 4 + longestDuration = max(longestDuration, duration + delay) + // 5 + let moveAction = SKAction.moveTo(newPosition, duration: duration) + moveAction.timingMode = .EaseOut + sprite.runAction( + SKAction.sequence([ + SKAction.waitForDuration(delay), + SKAction.group([moveAction, fallingCookieSound])])) + } + } + // 6 + runAction(SKAction.waitForDuration(longestDuration), completion: completion) + } +下面是工作原理: + +1.和其他的动画函数一样,你只能在所有的动画播放完后调用这个实现模块。因为下落的cookie数可能改变,你不能硬编码这个总的持续时间,而是要计算它。 + +2.越往上的cookie,动画的延迟时间就越长。这样比同时下落所有的cookie看起来更有动感。这个计算能够有效地前提是函数`fillHoles()`确保了越低的cookie在数组中的位置越靠前。 + +3.同样,动画的持续时间基于cookie下落的距离(每个方格0.1S)。你可以稍微调整这个这些数字来改变动画的播放效果。 + +4.计算出最长的动画时间。这也是游戏继续的钱必须等待的时间。 + +5.完成动画(包括延迟时间、动作、音效)。 + +6.在游戏继续前,你要等待所有的cookie全部落下来。 + +你现在可以把它整理到一起了。打开**GameViewController.swift**。用下面的内容替换函数`handleMatches()`中的内容: + + func handleMatches() { + let chains = level.removeMatches() + scene.animateMatchedCookies(chains) { + let columns = self.level.fillHoles() + self.scene.animateFallingCookies(columns) { + self.view.userInteractionEnabled = true + } + } + } +现在这个函数调用`fillHoles()`来更新模板,其中函数`fillHoles()`返回记录下落的cookie的数组。同时函数`handleMatches()`把返回的数组传递给场景,让场景播放动画并把精灵放到他们的新位置上去。 + + 注意:在Objective-C中访问一个属性或者调用一个方法,通常需要使用self。在Swift语言中,除了在闭包中,你是不需要这样做的。这就是你在函数`handleMatches()`中看到很多self的原因。Swift坚持这样做表明闭包确实通过强引用获取了self的值。实际上,如果你在比保重如果不指明self,Swift编译器会报错。 +试一下! + +cookie正在下落!注意一下,cookie可以正确的掠过关卡设计中的缺口。 + +### 添加新的cookie ### +还需要再做一件事情来完成游戏循环。下落的cookie会在每列的顶部留出空穴。 + + +你需要用新的cookie填满这些列。在 **Level.swift**中添加一个新函数: + + func topUpCookies() -> Array> { + var columns = Array>() + var cookieType: CookieType = .Unknown + + for column in 0..NumColumns { + var array = Array() + // 1 + for var row = NumRows - 1; row >= 0 && cookies[column, row] == nil; --row { + // 2 + if tiles[column, row] != nil { + // 3 + var newCookieType: CookieType + do { + newCookieType = CookieType.random() + } while newCookieType == cookieType + cookieType = newCookieType + // 4 + let cookie = Cookie(column: column, row: row, cookieType: cookieType) + cookies[column, row] = cookie + array.append(cookie) + } + } + // 5 + if !array.isEmpty { + columns.append(array) + } + } + return columns + } +这个函数在需要的地方添加cookie来填满一列。它返回一个存储着在有空格的列中添加的新cookie对象的数组。 + +如果某列有X个空格,那么它就需要X个新的cookie。现在的空穴全部在列的上端,因此你可以简单的从上往下扫描知道你找到一个cookie。 + +下面是它的工作原理: + +1.你从上往下扫描一列。这个for循环的终止条件是`cookies[column, row]`不为`nil`时---也就是找到一个cookie。 + +2.你可以忽略水平方向上的空缺,因为你只需填满网格中有瓷砖的方块。 + +3.你随机的创建一个新的cookie类型。但是它不能喝最后一个新的cookie类型相同,因为这样的话就会有太多可以直接消除的cookie。 + +4.创建一个新的cookie对象,然后添加到该列的数组中。 + +5.和以前一样,如果某列没有空穴,你就不需要把它添加到最终的数组中。 + + 函数`topUpCookies()`返回的数组中包括有空穴的各列形成的子数组。这些数组中的cookie对象时从上到下按顺序排列的。这个顺序对于下面的动画是非常重要的。 + +切换到**GameScene.swift**,并添加新的动画函数: + func animateNewCookies(columns: [[Cookie]], completion: () -> ()) { + // 1 + var longestDuration: NSTimeInterval = 0 + + for array in columns { + // 2 + let startRow = array[0].row + 1 + + for (idx, cookie) in enumerate(array) { + // 3 + let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName) + sprite.position = pointForColumn(cookie.column, row: startRow) + cookiesLayer.addChild(sprite) + cookie.sprite = sprite + // 4 + let delay = 0.1 + 0.2 * NSTimeInterval(array.count - idx - 1) + // 5 + let duration = NSTimeInterval(startRow - cookie.row) * 0.1 + longestDuration = max(longestDuration, duration + delay) + // 6 + let newPosition = pointForColumn(cookie.column, row: cookie.row) + let moveAction = SKAction.moveTo(newPosition, duration: duration) + moveAction.timingMode = .EaseOut + sprite.alpha = 0 + sprite.runAction( + SKAction.sequence([ + SKAction.waitForDuration(delay), + SKAction.group([ + SKAction.fadeInWithDuration(0.05), + moveAction, + addCookieSound]) + ])) + } + } + // 7 + runAction(SKAction.waitForDuration(longestDuration), completion: completion) + } +这个和下落cookie的动画类似。主要的区别是数组中cookie对象的按相反的顺序排序(从上到下),下面是这个函数的功能: + +1.动画结束之前,游戏是不能继续的。因此你可以出计算最长的动画时间,在第7步中要用到。 + +2.新的cookie精灵应该在一列中第一个的上面开始。找出瓷砖的行号的简单方法是查看数组中第一个cookie的行号,数组中的第一个cookie也经常是一列中最上面的一个。 + +3.创建一个新的cookie精灵。 + +4.cookie的位置越高,延迟的时间就越长,因此这些cookie看起来就像是一个接着一个的下落。 + +5.根据将要的下落的cookie的最远位置计算出动画的持续时间。 + +6.播放精灵下落的动画然后渐渐的显示出来。这样cookie的出现就不会显得那么的突兀。 + +7.动画结束之后,继续游戏。 + +最后,在 **GameViewController.swift**中,用下面的内容替换掉函数`handleMatches()`中的实现模块: + + func handleMatches() { + let chains = level.removeMatches() + scene.animateMatchedCookies(chains) { + let columns = self.level.fillHoles() + self.scene.animateFallingCookies(columns) { + let columns = self.level.topUpCookies() + self.scene.animateNewCookies(columns) { + self.view.userInteractionEnabled = true + } + } + } + } +试试看!!! + + +### 不断下落的cookie ### + +玩了一段时间之后你可能注意到有一些奇怪的问题。cookie从落下到空格里,新的cookie从顶部落下来,这些动作会形成新的有三个或者多个的满足条件的链表。但是然后会发生什么事情呢? + +你需要移除这些匹配的链表,让其他的cookie取代它们的位置。应该一直这样知道屏幕上没有匹配的cookie。到这个时候才能让玩家从新操作。 + +处理这些可能的不断下落的情况可能会很棘手,但是你已经写好了这个功能的代码!然后你只需要又满足条件的链表时不断的调用函数`handleMatches()`。 + +在 **GameViewController.swift**的`handleMatches()`函数中,把设置`userInteractionEnabled` 的那行改成:`self.handleMatches()`。就是`handleMatches()`调用它自身。 +这就是递归,也是非常有用的编程技术。使用递归的时候,你只需要注意一件事:你需要在某一时刻结束递归调用,否则的话,这个程序就会无限循环直至崩溃。 + +因为上面的原因,在函数`handleMatches()`顶部调用函数`removeMatches()`的后面添加下面的内容: + + if chains.count == 0 { + beginNextTurn() + return + } +如果没有可以匹配的cookie了,这时候就需要玩家去移动,并且为了防止再次的递归调用,这个函数就会退出。 + +最后,添加`beginNextTurn()`函数: + + func beginNextTurn() { + view.userInteractionEnabled = true + } +试试看!!!如果移除一个链表的时候在其他的地方形成了一条新的链表,游戏就会继续移除那个链表: + + +还有一个问题:玩了一段时间之后,游戏不去响应本来应该是有效地移动。这是有原因的,你能猜一下吗? + + 解答:玩家每移动一次,存储可能移动的列表就会超时。这时,你需要在玩家再次移动前重新计算这个列表。 +这部分内容的逻辑在**Level.swift**的`detectPossibleSwaps()`函数中。你需要在**GameViewController.swift**中的 `beginNextTurn()`函数中调用这个函数: + + func beginNextTurn() { + level.detectPossibleSwaps() + view.userInteractionEnabled = true + } +漂亮!现在完成了游戏的循环部分,并且可以补充无数个cookie! + +### 得分 ### + +在游戏 Cookie Crunch Adventure中,玩家的目标是用尽可能少的交换次数取得一个可观的分数。这些值都来自于 JSON文件。游戏中,应该把这些数字显示在屏幕上,让玩家知道自己玩的有多好。 + +首先,在**GameViewController.swift**中添加下面的属性: + + var movesLeft: Int = 0 + var score: Int = 0 + + @IBOutlet var targetLabel: UILabel + @IBOutlet var movesLabel: UILabel + @IBOutlet var scoreLabel: UILabel +其中`movesLeft`和`score`变量记录玩家水平,`outlets`属性把这些显示到屏幕中。 + +打开**Main.storyboard**,并在视图中添加下面的这些标签。把视图控制器设计成下面这样: + +(这是Xcode 5的截图,但是看起来和在Xcode 6中的一样) + +确保取消选中文件检查器中的 **Use Auto Layout**(自动布局),就是右边的第一个选项卡。在显示的窗口中,选择**Disable Size Classes**。就像上面的图片中显示的一样,这样会使场景变成iPhone屏幕的尺寸。 + +可以给主视图选一个灰色的背景,来让标签更清楚。字体改成**Gill Sans Bold**,其中数字的大小改成20.0,文字的大小改成14.0。您还可以设置一个轻微的阴影标签,使他们更容易看到。 + +如果你把数字设成居中看起来会更好。把这三个数字和他们各自的outlets属性联系起来。 + +因为得分和移动的最大次数被存储在JSON文件中,你应该把他们加载到关卡中。在Level.swift中添加下面的属性: + + let targetScore: Int! + let maximumMoves: Int! +这些属性会存放从JSON中加载的数据。他们用!标记,是因为他们可能没有得到任何值(如果因为某种原因加载关卡失败)。 + +在Level.swift的init(filename:)的底部添加下面两行: + + init(filename: String) { + ... + if let tilesArray: AnyObject = dictionary["tiles"] { + ... + // Add these two lines: + targetScore = (dictionary["targetScore"] as NSNumber).integerValue + maximumMoves = (dictionary["moves"] as NSNumber).integerValue + } + } + } +到目前为止,你已经把JSON解析到词典(dictionary)中,因此你可以获取两个值,并把他们存储起来。 + +切换到 **GameViewController.swift**,添加下面的函数: + + func updateLabels() { + targetLabel.text = NSString(format: "%ld", level.targetScore) + movesLabel.text = NSString(format: "%ld", movesLeft) + scoreLabel.text = NSString(format: "%ld", score) + } +每次更新标签中的文本之后,你都会调用这个函数。 + +在beginGame()的顶部shuffle()的调用之前添加下面的内容: + + movesLeft = level.maximumMoves + score = 0 + updateLabels() +上面的内容会重置所有的数据。点击生成并运行,你的显示画面应该和下面的差不多: + + +### 计算分值 ### + +积分规则很简单: + +
    +
  • 三个可以消除的连在一起是60分
  • +
  • 三个以上每多一个就多60分
  • +
+因此,4个相同的cookie就是120分,5个相同的cookie就是180,以此类推。 + +把分值存进Chain对象中是最简单的,这样每个可以消除的链表都知道自己是多少分。 + +在Chain.swift中添加下面的内容: + + var score: Int = 0 +得分是模型数据,因此需要**Level**重新计算。在Level.swift中添加下面的函数: + + func calculateScores(chains: Set) { + // 3-chain is 60 pts, 4-chain is 120, 5-chain is 180, and so on + for chain in chains { + chain.score = 60 * (chain.length - 2) + } + } +现在在函数 `removeMatches()`中的return语句之前调用这个函数: + + calculateScores(horizontalChains) + calculateScores(verticalChains) +因为有水平方向和竖直方向两套chain对象,因此你需要调用这个函数两次。 + +既然,关卡知道怎么样计算得分和怎么样把他们存进Chain对象,你可以刷新玩家的得分,并把它们显示到屏幕上。 + +这些都是在**GameViewController.swift**中完成的。在函数`handleMatches()`中,在`self.level.fillHoles()`调用之前添加下面的内容: + + for chain in chains { + self.score += chain.score + } + self.updateLabels() +上面的内容只是简单的遍历链表,把他们的分值添加到玩家的总得分上去,然后更新标签。 + +试试看!!!交换cookie,观察你增加的得分: + + +### 分值动画 ### + +如果每一个链表的得分都会显示一个特别漂亮的动画会是很有趣的事情。在 **GameScene.swift**中,添加一个新的函数: + + func animateScoreForChain(chain: Chain) { + // Figure out what the midpoint of the chain is. + let firstSprite = chain.firstCookie().sprite! + let lastSprite = chain.lastCookie().sprite! + let centerPosition = CGPoint( + x: (firstSprite.position.x + lastSprite.position.x)/2, + y: (firstSprite.position.y + lastSprite.position.y)/2 - 8) + + // Add a label for the score that slowly floats up. + let scoreLabel = SKLabelNode(fontNamed: "GillSans-BoldItalic") + scoreLabel.fontSize = 16 + scoreLabel.text = NSString(format: "%ld", chain.score) + scoreLabel.position = centerPosition + scoreLabel.zPosition = 300 + cookiesLayer.addChild(scoreLabel) + + let moveAction = SKAction.moveBy(CGVector(dx: 0, dy: 3), duration: 0.7) + moveAction.timingMode = .EaseOut + scoreLabel.runAction(SKAction.sequence([moveAction, SKAction.removeFromParent()])) + } +上面的函数用得分和链表中间的位置创建一个新的变量`SKLabelNode` ,分值的数字在消失之前会向上浮动几个像素。 + +在函数 `animateMatchedCookies()`中的两个for循环之间调用这个新函数: + + for chain in chains { + + // Add this line: + animateScoreForChain(chain) + + for cookie in chain.cookies { +当使用`SKLabelNode`时,Sprite Kit需要加载字体,然后把它转换成一个纹理。这个过程只发生一次,但是还是会有很小的延迟,因此在游戏开始的提前加载字体是一个很明智的方式。 + +在**GameScene**中的 `init()`函数的底部添加下面的内容: + + SKLabelNode(fontNamed: "GillSans-BoldItalic") +现在试试看!!!点击生成和运行,然后获取一些得分。 + + +### 连击 ### + +使 **Candy Crush Saga**特别有趣的是有连击的功能,或者是一行中有多个可以匹配的cookie。 + +在玩家点出连击的时候,你应该给他额外的得分。为了达到这样的效果,你需要添加一个连击的倍乘因子,第一次是正常得分,第二次是得双倍的分,第三次是得三倍的分,以此类推。 + +在Level.swift中添加下面的私有属性: + + var comboMultiplier: Int = 0 // private +把`calculateScores()`更新成下面的样子: + + func calculateScores(chains: Set) { + // 3-chain is 60 pts, 4-chain is 120, 5-chain is 180, and so on + for chain in chains { + chain.score = 60 * (chain.length - 2) * comboMultiplier + ++comboMultiplier + } + } +这个函数使链表的分值乘上连击倍乘因子,然后在下一个链表的时候增加倍乘因子。 + +你也需要一个在下一轮可以充值倍乘因子的函数。在Level.swift中添加下面的函数: + + func resetComboMultiplier() { + comboMultiplier = 1 + } +打开 GameViewController.swift,并找到beginGame()。然后在调用‘shuffle()前面添加下面的一行内容: + + level.resetComboMultiplier() +在`beginNextTurn()`的顶部添加一行相同的内容。 + +现在你就有连击功能了。试试看!!! + + + 问题:你应该怎么样检测L型的链表,并使每行的值加倍? + 解答:L型的链表由两个链表组成,一个水平的和一个竖直的,它们公用一个顶角上的cookie。你可以遍历水平方向的链表并查看该链表的第一个或者是最后一个cookie是不是也在其他的竖直方向的链表中。如果是的话,移除这两个链表,并把它们结合成一个新类型的链表。 + +### 输赢 ### + +玩家只有有限的移动次数来取得目标分数。如果没有实现的话,那么游戏结束。这部分的逻辑并不难添加。 + +在**GameViewController.swift**中添加一个新的函数: + + func decrementMoves() { + --movesLeft + updateLabels() + } +这个函数只是简单的实现移动次数的减少,并把它更新到屏幕的标签中。 + +在beginNextTurn()函数中的底部调用上面的函数: + + decrementMoves() +点击生成和运行,然后查看数值的变化。每交换一次,游戏就会移除匹配的cookie,并减少剩余的可移动次数。 + +当然,你仍然需要检查玩家用完移动次数(游戏结束)或者是达到目标分值(胜利),然后根据具体的情况作出响应。 + +首先,故事板需要做一些工作。 + +### 胜利或失败的界面 ### + +打开Main.storyboard,然后往视图中拖进去一张图片。图片的大小为320×150像素,并且竖直方向居中。 + + +图片视图上面会显示失败或者胜利的信息。 + +切换到Size inspector(尺寸检查器)并使图像视图的自动调整大小的蒙片看起来像下面这样: + + +不管屏幕的尺寸是多大,图片都会一直居中。 + +现在在GameViewController.swift 中把这个图像视图和一个新的输出变量gameOverPanel连接起来。 + + @IBOutlet var gameOverPanel: UIImageView +现在为手势识别也添加一个属性: + + var tapGestureRecognizer: UITapGestureRecognizer! +在函数`viewDidLoad()`中,在你显示场景之前,要确保这个图像视图是隐藏的: + + gameOverPanel.hidden = true +现在添加一个新的函数来显示游戏结束的界面: + + func showGameOver() { + gameOverPanel.hidden = false + scene.userInteractionEnabled = false + + tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideGameOver") + view.addGestureRecognizer(tapGestureRecognizer) + } +这个函数使图像视图显示出来,禁用在场景中的触摸来阻止玩家交换cookie并且添加一个可以重新开始游戏的点击的手势识别。 + +添加下面的函数: + + func hideGameOver() { + view.removeGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer = nil + + gameOverPanel.hidden = true + scene.userInteractionEnabled = true + + beginGame() + } +这个函数讲隐藏游戏结束的界面并且重新开始游戏。 + +检测什么时间显示游戏结束界面的逻辑在函数`decrementMoves()`中。在那个函数的底部添加下面的内容: + + if score >= level.targetScore { + gameOverPanel.image = UIImage(named: "LevelComplete") + showGameOver() + } else if movesLeft == 0 { + gameOverPanel.image = UIImage(named: "GameOver") + showGameOver() + } +如果当前的分数大于等于目标分数,那么玩家就取得了胜利!如果剩余移动次数是0,玩家就输掉了游戏。 + +不论哪种情况,函数都会加载适当的图片并且调用函数 `showGameOver()`来显示的屏幕上。 + +试试看!!!如果你赢了,你就会看到下面的内容: + +同样,如果你的剩余移动次数为0,你就会看到游戏结束的信息。 + +### 转换动画 ### + +在cookie的上面显示标题看起来就会有点混乱,因此也让我们添加一点动画。在GameScene.swift中添加下面的两个函数: + + func animateGameOver(completion: () -> ()) { + let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3) + action.timingMode = .EaseIn + gameLayer.runAction(action, completion: completion) + } + + func animateBeginGame(completion: () -> ()) { + gameLayer.hidden = false + gameLayer.position = CGPoint(x: 0, y: size.height) + let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3) + action.timingMode = .EaseOut + gameLayer.runAction(action, completion: completion) + } +函数`animateGameOver()`会播放一个使`gameLayer`慢慢消失的动画。 `animateBeginGame()` 函数却正好相反,使`gameLayer`慢慢的从屏幕的顶部滑到屏幕中。 + +游戏刚开始的时候,你也想调用函数`animateBeginGame()`来播放相同的动画。在动画开始前,如果游戏图层是隐藏的看起来可能更好,因此往GameScene.swift中的 init(size:)中创建gameLayer节点 的后面添加下面的内容: + + gameLayer.hidden = true + +现在打开`GameViewController.swift`,并且在函数`showGameOver()`中调用函数`animateGameOver()`: + + func showGameOver() { + gameOverPanel.hidden = false + scene.userInteractionEnabled = false + + scene.animateGameOver() { + self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideGameOver") + self.view.addGestureRecognizer(self.tapGestureRecognizer) + } + } +注意:在动画播放完后,添加响应点击的手势识别。这样可以防止玩家在播放动画的时候点击。 + +最后,在GameViewController.swift中的`beginGame()`中,在调用函数`shuffle()`之前,调用函数`animateBeginGame()`: + + scene.animateBeginGame() { } + +现在这个动画的实现模块是空的,但是你很快就可以添加内容了。 + +现在,游戏结束后,当你点击屏幕的时候,应该下拉屏幕,是cookie显示在他们开始的位置。Nice!、 + + +Whoops!出问题了,你好像没有把老的cookie精灵移除。 + +在GameScene.swift 中添加下面的函数,完成清除的功能: + + func removeAllCookieSprites() { + cookiesLayer.removeAllChildren() + } +在 GameViewController.swift中的函数`shuffle()`中第一件事就是调用上面的函数: + + scene.removeAllCookieSprites() +问题解决了!点击生成和运行,现在你的游戏开始干净利落的重新启动了。 + +### “手动洗牌” ### + +还有一种情况:可能会发生--尽管很少见--就是没有可以移动的cookie了。那样的话,玩家就被困住了。 + +这种情况都很多种处理方法。例如,Candy Crush Saga自动的重排cookie。但是在Cookie Crunch中,将会把这个权利交给玩家。你可以让玩家在任何时候点击按钮来重排cookie,但是代价是把这个点击算作移动了一次。 + +在GameViewController.swift中添加一个输出属性: + + @IBOutlet var shuffleButton: UIButton +添加下面的动作函数: + + @IBAction func shuffleButtonPressed(AnyObject) { + shuffle() + decrementMoves() + } +点击使cookie重排算是移动了一次,因此会调用函数`decrementMoves()`。 + +在函数`showGameOver()`中,添加下面的一行代码是重排的按钮隐藏起来: + + shuffleButton.hidden = true + +在函数` viewDidLoad()`中也要这样做,使按钮在游戏开始的时候是隐藏的。 + +在函数`beginGame()`中,在动画的实现模块,把按钮重新显示在屏幕上: + + scene.animateBeginGame() { + self.shuffleButton.hidden = false + } +现在打开Main.storyboard,并在屏幕的底部添加一个按钮: + +设置按钮的标题为“重排”并且使按钮的大小为100x36点。为了使按钮看起来美观,把字体改成Gill Sans Bold,10pt。使文字的颜色为白色并有50%不透明度的黑色阴影。背景可以选择第一部分中你添加到资源目录中名字为“按钮”的图片。 + +使按钮紧靠屏幕的底部,并设置自动调整大小使其在3.5英寸的手机上也能正常显示。 + + +最后,把输出属性shuffleButton 和按钮连接起来,把它的Touch Up Inside事件和shuffleButtonPressed:动作联系起来。 + +试试看!!! + + + 注意:当我们洗牌的时候,我们拿着实实在在的牌,改变他们的顺序,然后处理同一副但是顺序不同的牌。但是,在这个游戏中,你获取的是随机的cookie。因此找到一个至少允许移动一次的相同的cookie的分配方式是无法计算的,毕竟,这是一个随机性的游戏。 + +直接重排会显得和突然,因此让我们为新的cookie添加一个漂亮的动画。在GameScene.swift中找到函数`addSpritesForCookies()`并在for循环的内部,现存代码的后面添加下面的内容: + + // 给每个cookie一个短暂的延迟,然后把他们渐渐的显现出来. + sprite.alpha = 0 + sprite.xScale = 0.5 + sprite.yScale = 0.5 + + sprite.runAction( + SKAction.sequence([ + SKAction.waitForDuration(0.25, withRange: 0.5), + SKAction.group([ + SKAction.fadeInWithDuration(0.25), + SKAction.scaleTo(1.0, duration: 0.25) + ]) + ])) + +上面的内容给每个cookie一个短暂的、随机性延时,然后把他们渐渐的显示到屏幕中。效果看起来像下面这样: + + +### 音乐 ### + +当玩家碾碎cookie的时候,我们可以播放一些舒缓的、放松的音乐。在GameViewController.swift 的顶部添加下面的内容来包含AVFoundation框架: + + import AVFoundation +并添加下面的属性: + + var backgroundMusic: AVAudioPlayer! +在函数`viewDidLoad()`中调用`beginGame()`的前面添加下面的内容: + + // 加载并播放背景音乐. + let url = NSBundle.mainBundle().URLForResource("Mining by Moonlight", withExtension: "mp3") + backgroundMusic = AVAudioPlayer(contentsOfURL: url, error: nil) + backgroundMusic.numberOfLoops = -1 + backgroundMusic.play() +上面的内容会加载背景音乐,并且循环播放。这样就给游戏添加了很多的旋律。 + +### 绘制更加漂亮的瓷砖 ### + +如果你把你的游戏和Candy Crush Saga仔细的对比,你就会注意到绘制的瓷砖有些许的不一样。Candy Crush中的边界画的更好一点。 + + +还有,如果cookie在下降的时候通过一个缺口,你的游戏是直接的背景的上面绘制,但是Candy Crush中却是在背景的后面绘制。 + + +要创建这样的效果是不难的,但是你需要一些新的cookie精灵。你可以在文件夹Grid.atlas下面找到此指南使用的全部资源。把这个文件夹拖进你的Xcode项目中。这样会用这些图片创建一个新的纹理集。 + +在 GameScene.swift中,添加两个新的属性: + + let cropLayer = SKCropNode() + let maskLayer = SKNode() + +在函数`init(size:)`中,在创建tilesLayer的代码的后面添加下面的内容: + + gameLayer.addChild(cropLayer) + + maskLayer.position = layerPosition + cropLayer.maskNode = maskLayer + +这样会创建两个新的图层:cropLayer--它是一种被称作SKCropNode的特殊类型的节点,还有一个蒙版图层。裁剪节点只绘制蒙版中有像素的子节点。这样你就可以在有瓷砖的地方绘制cookie,而不会在背景上绘制。 + +用下面的内容: + + cropLayer.addChild(cookiesLayer) +替换 + + gameLayer.addChild(cookiesLayer) + +现在,你把cookiesLayer 添加到这个新的cropLayer中,而不是直接的添加到gameLayer中。 + +为了填充剪裁图层的蒙版区域,按下面的内容修改函数`addTiles()`: + +
    +
  • 用"MaskTile"替换 "Tile"
  • +
  • 用maskLayer替换tilesLayer
  • +
+无论哪里有瓷砖,这个函数现在都是往图层中绘制特殊的蒙版瓷砖(功能和SKCropNode的蒙版一样)。蒙版瓷砖比一般正常的瓷砖大一点。 + +点击生成并运行。注意当cookie下落通过缺口的时候是怎么样被裁剪的。 + + + 提示:如果你想看看蒙版图层是什么样的,可以在`init(size:)`中添加下面的内容: + cropLayer.addChild(maskLayer) + 当你结束的时候,千万不要忘记移除它! +最后一步,在addTiles()的底部添加下面的代码: + + for row in 0...NumRows { + for column in 0...NumColumns { + let topLeft = (column > 0) && (row < NumRows) + && level.tileAtColumn(column - 1, row: row) + let bottomLeft = (column > 0) && (row > 0) + && level.tileAtColumn(column - 1, row: row - 1) + let topRight= (column < NumColumns) && (row < NumRows) + && level.tileAtColumn(column, row: row) + let bottomRight = (column < NumColumns) && (row > 0) + && level.tileAtColumn(column, row: row - 1) + + // The tiles are named from 0 to 15, according to the bitmask that is + // made by combining these four values. + let value = Int(topLeft) | Int(topRight) << 1 | Int(bottomLeft) << 2 | Int(bottomRight) << 3 + + // Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn. + if value != 0 && value != 6 && value != 9 { + let name = String(format: "Tile_%ld", value) + let tileNode = SKSpriteNode(imageNamed: name) + var point = pointForColumn(column, row: row) + point.x -= TileWidth/2 + point.y -= TileHeight/2 + tileNode.position = point + tilesLayer.addChild(tileNode) + } + } + } +上面的内容会在水平的瓷砖之间画一个特定的类型的边界。你可以挑战一下,自己去破解它的工作原理.:) + +解答: +假设把一个瓷砖分成四个象限。四个波尔类型的变量来表示这个瓷砖有什么类型的边界。例如,在一个正方形关卡中,右下角的瓷砖需要一个背景去覆盖左上角的(查看Tile_1.png)。那种四周都有相邻瓷砖的瓷砖就会有一个完整的背景(查看Tile_15.png)。 + +点击生成并运行,你现在应该有一个看起来和玩起来都和 Candy Crush Saga类型的游戏! + + +### 何去何从 ### + +祝贺你完成了这部分内容!这是一个很长的Swift指南,现在你用了编写自己三消类游戏的全部基础模块。 + +你可以在这里下载最终的Xcode项目。 + +下面是一些你可以添加的其他的特性: + +
    +
  • 当玩家匹配成指定的形状的时候可以出现特殊的cookie。例如,当你在一行中匹配到四个可以消除的cookie的时候,Candy Crush Saga就会给出一个可以消除整行的特殊cookie。
  • +
  • 检测特殊的链表,比如L型和T型,这时可以奖励玩家更多的积分或者是特殊的物品。
  • +
  • 玩家可以随时使用的物品。例如,一个可以一下子移除屏幕上同一种类型cookie的物品。
  • +
  • 果冻关卡:在这些关卡里,某些瓷砖上显示果冻。你有X步来移除这些果冻。这时瓷砖类就派上用场了。你可以添加一个BOOL类型的果冻属性,如果玩家在这个瓷砖上匹配到cookie,就把这个果冻属性设为NO,然后移除果冻。
  • +
  • 提示:如果玩家两秒内没有移动的话,就加亮显示互换之后可以消除的两个cookie
  • +
  • 如果玩家完成当前关,就自动的进入下一关。
  • +
  • 如果没有可以移动的cookie的时候自动重排所有的cookie。
  • +
+ +你看,仍然有很多我们可以做的。好好享受哦! + +小组成员: Vicki Wenderlich的原图, Kevin MacLeod的音乐,音效是基于 freesound.org的样品。 + +源代码中使用的一些技术是基于 a blog post by Emanuele Feronato的。 diff --git "a/How-to-Make-a-Game-Like-Jetpack-Joyride-in-Unity-2D\342\200\223Part1.md" "b/How-to-Make-a-Game-Like-Jetpack-Joyride-in-Unity-2D\342\200\223Part1.md" index 1b3ec57..8833f9a 100644 --- "a/How-to-Make-a-Game-Like-Jetpack-Joyride-in-Unity-2D\342\200\223Part1.md" +++ "b/How-to-Make-a-Game-Like-Jetpack-Joyride-in-Unity-2D\342\200\223Part1.md" @@ -1,4 +1,4 @@ -#如何用Unity 2D做一个类似于疯狂喷气机(Jetpack Joyride)的游戏 - 第一部分 +# 如何用Unity 2D做一个类似于疯狂喷气机(Jetpack Joyride)的游戏 - 第一部分 Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己的解决方案来构建2D游戏了。现在可以使用一个即视即用的2D工具集来进行开发。结合这些标准统一的工具,开发Unity2D游戏不再是一个痛苦的过程,变成一个富有乐趣的工作了:] @@ -21,11 +21,11 @@ Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己 *是否对使用其他库来做这个游戏感兴趣呢?这里有一些如何使用Cocos2D 2.x 和 Corona来制作 这个游戏的教程。如果你对这些教程感兴趣你可以从这里得到它:[Cocos2D 2.x 版本](http://www.raywenderlich.com/28713/how-to-make-a-game-like-jetpack-joyride-using-latest-levelhelper-spritehelper-cocos2d-edition-part-1)和[Corona 版本](http://www.raywenderlich.com/9050/how-to-make-a-game-like-jetpack-joyride-using-levelhelper-spritehelper-and-corona-sdk-part-1)。但是我们都知道这些教程是老古董了,酷小孩只用Unity,我说的是对呢,还是对呢?* -##起步指导 +## 起步指导 在开始前你需要准备一些游戏美术资源,声音效果,还有游戏音乐。我已将所有需要的材料都准备好并打包,你可以从这里下载得到:[火箭鼠Unity资源](http://cdn4.raywenderlich.com/wp-content/uploads/2014/03/RocketMouse_Unity_Resources.zip). -#####内部解决方案,详见包内容: +##### 内部解决方案,详见包内容: 资源包包含: * 游戏美术资源由Vicki Wenderlich创建,原本是为第一版的如何用关卡助手(LevelHelper)和精灵助手(SpriteHelper)做一个类疯狂喷气机游戏教程所做。 @@ -46,7 +46,7 @@ Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己 *还有一点,我用的是Unity的**OS X**版本,但是由于Unity在OS X和Windows下运行基本是相同的,所以你在Windows环境下完成这篇教程的学习是没有问题的。* -##创建和配置项目 +## 创建和配置项目 打开Unity,然后选择**File\New Project…**创建一个新项目。 @@ -64,7 +64,7 @@ Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己 虽然说在创建新的项目时,设置默认值为2D可以说是切换Unity为2D模式时你唯一需要修改的地方,但是有时候这个操作会不起作用。嗯,至少对于我来说有几回就是这样,也包括这一次。因此最好能确认当前是在2D模式。 -###2D模式确认 +### 2D模式确认 下面是你需要检查所有设置都已切换为2D项目模式的列表: @@ -82,7 +82,7 @@ Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己 ![save scene](http://cdn4.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_07_save_scene.gif) -###设置游戏视图 +### 设置游戏视图 切换到游戏视图,然后设置游戏视图为固定分辨率大小**1136×640**,如果你在列表中没有看到这个选项,那就新创建一个,然后命名为**iPhone Landscape**。 ![game view](http://cdn1.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_08_game_view.gif) @@ -93,7 +93,7 @@ Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己 **保存**场景,对比项目创建Unity窗口没有什么大的变化,但是刚才所做的配置步骤非常重要,没有这些配置游戏就会朝着非预期运行。 -##添加游戏角色 +## 添加游戏角色 在本节教程中,你将会添加玩家角色,这个角色是一个背着飞行背包的酷鼠,来吧! @@ -101,7 +101,7 @@ Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己 开始之前,确认你已经下载好了本游戏的资源包。解压缩资源包后你会发现里面包含两个目录:精灵(Sprites)和声音(Audio)。本部分教程中将会大量使用精灵目录中的文件,声音目录中的文件将会在另一部分教程中使用。现在只要记住你有这些文件就行了。 -###导入游戏素材 +### 导入游戏素材 添加所有资源时,只需要简单的**选择****Sprites**和**Audio**文件夹,然后拖到**项目浏览器**中,放置到之前所创建的**Scenes**文件夹旁边 @@ -113,7 +113,7 @@ Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己 这样你就完成了对所有必须的资源文件的添加。此时你可能会看到似乎有很多奇怪的文件,不要担心,大部分图片只是装饰和背景。除过这些文件就是小飞鼠角色,还有激光和硬币的图片文件。 -###在场景中添加玩家角色 +### 在场景中添加玩家角色 现在开始实际的往场景中添加东西。在**项目浏览器**中打开**Sprites**文件夹,找到名为**mouse_fly**的精灵文件,**拖**到**场景**中。 ![add_mouse](http://cdn4.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_13_add_mouse_go-611x500.png) @@ -156,7 +156,7 @@ Unity 4.3发布后,开发者不再需要使用第三方库或者创建自己 等一下!为什么小飞鼠一直往下掉?你没有给小飞鼠添加任何重力效果...或者还是你添加了?实际上,当你添加了一个Rigidbody 2D组件,它被默认赋予一个scale为1的重力效果。这就告诉了系统使用物理引擎的默认重力来使角色下跌。 -##创建脚本控制喷气背包 +## 创建脚本控制喷气背包 不要让小飞鼠掉进深渊。这不是我想改变的:] @@ -219,7 +219,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 ![mouse_unity_22](http://cdn4.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_22.gif) -###调整重力效果 +### 调整重力效果 喷气背包可用了,但是你可以很直观的看到几个问题。第一,从你的角度出发也可以发现,喷气背包的动力有点太强;或者是重力太弱了。很容易就会让小飞鼠飞过屏幕上方,而且再也看不见了。与其修改喷气背包的动力,还不如修改整个项目的重力设置。通过改变全局的重力设置,你可以为比较小的iPhone屏幕设置一个更智能的 默认值。再说,谁不喜欢控制重力的想法:] @@ -234,7 +234,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 对于控制小飞鼠在屏幕内,如果你仍然有困难也不要太担心。试着将你的游戏视图扩大,或者调整喷气背包动力或者再对重力进行设置。这个地方建议值是当你在iPhone上能运行良好。当然添加天花板和地板会将小飞鼠保持在视野内,下一步你就会做这部分内容。 -###添加地板和天花板 +### 添加地板和天花板 正如你可能想到的那样,添加天花板和地板相对来说是一个比较简单的练习;你所需要的是小飞鼠在场景顶部和底部发生碰撞的对象。在之前创建小飞鼠对象时,由于是用图像创建的,所以用户可以在游戏过程中很直观的查看和跟踪。而天花板和地板可以用空的游戏对象来代替,因为它们从来不移动,而且它们的位置相对于用户来说也是很明显的。 @@ -261,7 +261,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 我可以确定你能自己添加天花板。设置其**Position**为**(0, 3.7, 0)**,不要忘了将它重命名为**ceiling**。 -####内部解决方案:添加天花板 +#### 内部解决方案:添加天花板 选择**GameObject\Create Empty**创建对象。在层次结构中选择这个对象,然后在**检测面板**中执行下面操作: @@ -279,13 +279,13 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 ![rocketmouse](http://cdn2.raywenderlich.com/wp-content/uploads/2014/06/rocketmouse.gif) -##使用粒子系统创建喷气背包的火焰 +## 使用粒子系统创建喷气背包的火焰 现在你可以让小飞鼠按照用户的任何意愿来移动了,是时候添加一些火焰了。在这一节中,你将会制作效果,在小飞鼠上升的时候喷气背包发射火焰。为什么是火焰?因为任何东西加上火焰效果会更好! 喷气背包喷射出火焰的方式有很多种,但我个人最喜欢的是使用粒子系统。粒子系统是用来创建大量的小颗粒和诸如火焰、爆炸、烟雾等的模拟效果;所有这些都基于你如何配置这个系统。 -###创建粒子系统 +### 创建粒子系统 通过选择**GameObject\Create Other\Particle System**来添加一个粒子系统到场景中。你会立即注意到场景的变化;当对象被选择的时候粒子系统会在场景中显示出其默认行为。 @@ -315,7 +315,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 ![mouse_unity_29](http://cdn1.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_29-205x500.png) -###改进火焰效果 +### 改进火焰效果 现在火焰看起来挺不错的,但是你会发现火焰颗粒停止的很突然,就好像在粒子发射器的末端撞上了一堵无形的墙。可以根据粒子从喷气背包下降的越来越远,改变粒子的颜色,从而解决这个问题。 @@ -339,7 +339,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 ![mouse_unity_34](http://cdn2.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_34.gif) -##创建关卡选择 +## 创建关卡选择 需要记住的是,小飞鼠要在一个无止境的房间里滑翔,要躲避激光射击并且收集硬币。然而,如果手动添加每一件东西并创建一个无止境的房间的话,这是一个杳无尽头的工作:]你是在开玩笑,对吧? @@ -359,7 +359,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 * 添加地板和天花板 * 添加装饰物(书架,老鼠洞,等等) -###添加房间背景 +### 添加房间背景 确认**场景**视图和**项目浏览器**是可见的。在**项目浏览器**中打开**Sprites**文件夹,然后将**bg_window**精灵拖到场景中。不需要把它放置到一个精确的位置,你要在1分钟内完成这个操作。 @@ -379,7 +379,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 *注:你可能想知道为什么要围绕**(0, 0, 0)**点来建立房间。这个是必需的,因为你将要往一个空的游戏对象中添加所有的房间,因此你需要确保你知道房间的中心点在哪里。在你开始创建产生关卡的时候这种方式会让你很清楚。* -###使用顶点捕捉 +### 使用顶点捕捉 对于屏幕中的每个背景元素,你可以基于每一个元素的大小来进行定位,但是移动对象的时候,如果要一直计算这些值的话就不是很方便了。 @@ -389,7 +389,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 使用顶点捕捉的时候,你只需要在选择后按住V键,但是要记住是在移动游戏对象之前。 -####内部解决方案:使用定点捕捉的更多细节 +#### 内部解决方案:使用定点捕捉的更多细节 选择你要移动的房间背景对象,不要忘了释放鼠标键,然后按住V键,移动小飞鼠到你需要使用为支点的角落。 @@ -403,7 +403,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 ![mouse_unity_p2_7](http://cdn3.raywenderlich.com/wp-content/uploads/2014/03/rocket_mouse_unity_p2_7.png) -###使用图层排序 +### 使用图层排序 为了使小飞鼠在房间背景的顶部出现,你需要使用一个叫图层排序的特性。这里只需要一点时间来设置一切。 @@ -442,13 +442,13 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 你可以试着去检测面板中寻找喷气背包火焰粒子系统相关的图层排序属性,但是你不会找到。这里有个小提醒,Unity最初是一个3D游戏引擎,在2D方面它的一些对象没有得到完全更新。 -##固定粒子系统排序顺序 +## 固定粒子系统排序顺序 即使在检测面板中没有喷气背包火焰方面的图层排序属性,但是你仍然可以访问并用脚本来进行控制。 首先,**新建一个C# 脚本**,命名为**ParticleSortingLayerFix**,把其**附加**到**jetpackFlames**对象上。接下来怎么做?看看下面的解决方案: -####内部解决方案:创建ParticleSortingLayerFix脚本 +#### 内部解决方案:创建ParticleSortingLayerFix脚本 首先确保在项目浏览器中**Scripts**文件夹被选中,然后点击**Assets\Create\C# Script**。将脚本命名为**ParticleSortingLayerFix**。 @@ -478,11 +478,11 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 然而,如果停止游戏然后切换到场景视图,粒子系统还是会显示在背景后面。不幸的是,这个问题还得忍受下去。这是由于火焰的**图层排序**是在代码中设置,所以在场景的UI中就不会展示出来。也许在Unity后面的更新中会解决这个问题。 -###装饰房间 +### 装饰房间 在项目浏览器中选择Sprites文件夹,从这个文件夹中可以选择任意数量的书架和老鼠洞素材来装饰房间。你可以在任何位置来摆放,只是不要忘了设置它们的**图层排序**为**Decorations**。 -####内部解决方案:室内设计帮助 +#### 内部解决方案:室内设计帮助 在**项目浏览器**中选择名为**object_bookcase_short1**的图片。像之前操作房间背景那样把它拖到场景中,添加到场景中就行,不需要精确摆放位置。 @@ -502,7 +502,7 @@ Unity会以一个固定的时间步长调用`FixedUpdate`方法,所有与物 到现在为止,这看起来像一个真正的游戏了! -##后续导读 +## 后续导读 看到这里我希望你能够喜欢这个教程。现在你可以在这个基础背景中操控自己的英雄小飞鼠飞上飞下了,在教程第二部分中,小飞鼠将可以向前移动并在随机生成的房间中穿梭。你甚至可以添加一些动画,让游戏更有趣,更有吸引力。 diff --git a/Image Processing in iOS Part 2-Core Graphics, Core Image, and GPUImage.md b/Image Processing in iOS Part 2-Core Graphics, Core Image, and GPUImage.md index 9beeba7..11e6cec 100644 --- a/Image Processing in iOS Part 2-Core Graphics, Core Image, and GPUImage.md +++ b/Image Processing in iOS Part 2-Core Graphics, Core Image, and GPUImage.md @@ -12,7 +12,7 @@ 如果你在第一节中表现得很好,你要好好享受这一节!既然你理解了工作原理,你将充分理解这些库进行图像处理是多么的简单。 -##超级SpookCam之Core Graphics版本 +## 超级SpookCam之Core Graphics版本 **Core Graphics**是Apple基于Quartz 2D绘图引擎的绘图API。它提供了底层API,如果你熟悉OpenGL可能会觉得它们很相似。 如果你曾经重写过视图的**-drawRect**:函数,你其实已经与Core Graphics交互过了,它提供了很多绘制对象、斜度和其他很酷的东西到你的视图中的函数。 @@ -93,7 +93,7 @@ 这个函数内容可真够多的,让我们一点一点分析它。 -###1) 计算Ghosty的位置 +### 1) 计算Ghosty的位置 UIImage * ghostImage = [UIImage imageNamed:@"ghost.png"]; CGFloat ghostImageAspectRatio = ghostImage.size.width / ghostImage.size.height; @@ -112,7 +112,7 @@ 如果你直接在这个context中绘制**UIImage**,你不需要执行变换,坐标系统将会自动匹配。设置这个context的变换将影响所有你后面绘制的图像。 -###2) 把你的图像绘制到context中。 +### 2) 把你的图像绘制到context中。 UIGraphicsBeginImageContext(input.size); CGContextRef context = UIGraphicsGetCurrentContext(); @@ -134,7 +134,7 @@ 这里为context设置混合模式是为了让它使用之前的相同的alpha混合公式。在设置完这些参数之后,翻转幽灵的坐标然后把它绘制在图像中。 -###3) 取回你处理的图像 +### 3) 取回你处理的图像 UIImage * imageWithGhost = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); @@ -143,7 +143,7 @@ 因为你使用**CGBitmapContextCreate()**来创建了这个context,坐标则是以左下角为原点,你不需要翻转它来绘制**CGImage**。 -###4) 绘制你的图像到一个灰度(grayscale)context中 +### 4) 绘制你的图像到一个灰度(grayscale)context中 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); context = CGBitmapContextCreate(nil, inputWidth, inputHeight, @@ -160,7 +160,7 @@ 因此,你需要自己创建它。你需要使用**CGBitmapContextCreateImage()**来渲染context中的图像。 -###5) 清理。 +### 5) 清理。 CGColorSpaceRelease(colorSpace); CGContextRelease(context); @@ -196,7 +196,7 @@ 介绍完了两种方法,下面还有两种方法。下一个:**Core Image**! -##超超SpookCam之Core Image版本 +## 超超SpookCam之Core Image版本 这个网站也已经有大量好的Core Image教程,比如IOS 6中的[这个](http://www.raywenderlich.com/22167/beginning-core-image-in-ios-6)。我们也在我们的[iOS教程系列](http://www.raywenderlich.com/store/ios-by-tutorials-bundle)中有很多关于Core Image的章节。 在本教程中,你将看到有很多关于Core Image与其他几种方法对比的讨论。 @@ -268,7 +268,7 @@ Apple还提供了巨大的预先制作的滤镜库。在OSX中,你甚至可以 这个方法使用了一个叫做**-createPaddedGhostImageWithSize:**的帮助函数,它使用Core Graphics创建了输入图像25%大小缩小版的填充的幽灵。你自己能实现这个函数吗? 自己试一下。如果你被卡住了,请看下面的解决方案: -###解决方案 +### 解决方案 - (UIImage *)createPaddedGhostImageWithSize:(CGSize)inputSize { UIImage * ghostImage = [UIImage imageNamed:@"ghost.png"]; CGFloat ghostImageAspectRatio = ghostImage.size.width / ghostImage.size.height; @@ -311,7 +311,7 @@ Apple还提供了巨大的预先制作的滤镜库。在OSX中,你甚至可以 现在到了最后一个解决方案,也是本教程中附带的唯一的第三方选项:**GPUImage**。 -##大型超超SpookCam之GPUImage版本 +## 大型超超SpookCam之GPUImage版本 **GPUImage**是一个活跃的iOS上基于GPU的图像处理库。它在这个网站中的[十佳iOS库](http://www.raywenderlich.com/21987/top-10-most-useful-ios-libraries-to-know-and-love)中赢得了一席之地! GPUImage隐藏了在iOS中所有需要使用OpenGL ES的复杂的代码,并用极其简单的接口以很快的速度处理图像。GPUImage的性能甚至在很多时候击败了Core Image,但是Core Image仍然在很多函数中有优势。 @@ -408,7 +408,7 @@ GPUImage隐藏了在iOS中所有需要使用OpenGL ES的复杂的代码,并用 在[这里](http://cdn3.raywenderlich.com/wp-content/uploads/2014/03/SpookCam-GPUImage.zip)下载本节项目中的所有代码。 -##下一步? +## 下一步? 恭喜!你已经用四种不同方式实现了SpookCam。这里是所有的下载链接: diff --git a/Programming Challenge Are You a Swift Ninja-Part 1.md b/Programming Challenge Are You a Swift Ninja-Part 1.md index 3d11c9e..899c4f3 100644 --- a/Programming Challenge Are You a Swift Ninja-Part 1.md +++ b/Programming Challenge Are You a Swift Ninja-Part 1.md @@ -1,4 +1,4 @@ -#编程挑战:你是Swift忍者吗?第一节 +# 编程挑战:你是Swift忍者吗?第一节 ![Are you a Swift Ninja?](http://cdn4.raywenderlich.com/wp-content/uploads/2014/07/ninja_swift-250x250.png) *你是Swift忍者吗?* @@ -27,14 +27,14 @@ >**注**: 这篇文章是为有Swift语言经验的程序员准备的。如果你感觉不那么轻松的话,查看[我们其余的Swift教程](http://www.raywenderlich.com/tutorials#swift). -##挑战 +## 挑战 这个系列跟我们之前在这个网站发表的文章风格上有一点的不同。它将提出一系列问题来不断增加复杂度。这里的许多问题用到了之前部分的技术,所以掌握它们是你成功的基本条件。 每一个问题突出至少一个Swift的语言特征,奇怪的语法,或者聪明的Hack。 不用担心,你不会被扔到狼堆里 – 这里会有提示。每一部分都有两个等级的提示,当然还有Apple的Swift和书,和你的好朋友Stack Overflow能帮助你。 -###如何对待每一个问题 +### 如何对待每一个问题 每个问题的开始定义了你需要什么去完成代码,哪些Swift特性你是能用的,哪些是不能用的。我建议你使用Playground来完成每一个挑战。 如果你遇到了困难就打开提示部分。虽然提示没有解决你当前的问题,但它们指明了方向。 @@ -55,8 +55,8 @@ 别添加你的得分来欺骗自己。这不是高尚的忍者的行事方法。即使你没有收集到每一个飞镖,在文章的最后你也会开阔你的思维,大胆得走向未来。 -##Swift忍者挑战 -###挑战 #1 +## Swift忍者挑战 +### 挑战 #1 在Apple的Swift书中,有许多函数的例子来交换两个变量的值。里面的代码总是使用额外的变量来存储的“经典”的方案。但是你可以比它做得更好。 你的第一个挑战是写一个具有两个参数(任何类型)的函数,它交换了两个变量的值。 @@ -66,12 +66,12 @@ 如果你没有打开*提示*或*教程*部分,给你自己![3shurikens](http://cdn5.raywenderlich.com/wp-content/uploads/2014/07/3shurikens.png) -####解决方法:提示 +#### 解决方法:提示 Swift元组非常强大 – 你可以存储任何类型的变量到元组中。此外,如果它们是元组,你可以一次给很多变量赋值。一个元组,两个元组!:] 记得你偷看了提示,现在你只能在这次的挑战中得到![2shurikens](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/2shurikens.png)。 -####解决方法:教程 +#### 解决方法:教程 作为一个Swift忍者应该知道,Swift新特性中的**元组**可以存储变量。语法也很简单 – 用括号括住变量列表(或常量、表达式等) var a = "Marin" @@ -109,7 +109,7 @@ Swift元组非常强大 – 你可以存储任何类型的变量到元组中。 如果你在Playground中运行了代码,学会了如何用元组交换值,给你自己另外一个![1shuriken](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/1shuriken.png)! -###挑战 #2 +### 挑战 #2 Swift函数是非常灵活的 — 它们可以接受可变数量的参数,返回一个或多个值,返回其他的函数等等。 在这次的挑战中,将测试你对Swift函数的语法的理解。写一个满足下列要求的,命名为**flexStrings**的函数: @@ -127,12 +127,12 @@ Swift函数是非常灵活的 — 它们可以接受可变数量的参数,返 解决问题将获得![3shurikens](http://cdn5.raywenderlich.com/wp-content/uploads/2014/07/3shurikens.png),如果你用一行代码完成了它并且完成了上面4个要求,你将获得一个额外的飞镖。 -####解决方法:提示 +#### 解决方法:提示 Swift函数参数可以有默认值,因此你调用函数时,可以省略参数。 记得现在在这次挑战中你只能获得![2shurikens](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/2shurikens.png)了 — 而且一行代码解决问题也没有额外的飞镖了! -####解决方法:教程 +#### 解决方法:教程 Swift函数参数可以有一个默认值,这是旧的Objective-C和Swift之间的一个差别。当参数有默认值时,你可以用它的名称调用这个函数。 好处就在于如果你喜欢用它的默认值,你可以忽略参数。真棒! @@ -155,7 +155,7 @@ Swift函数参数可以有一个默认值,这是旧的Objective-C和Swift之 试着在Playground中使用不同的参数调用这个函数 — 确定你理解了它是如何工作的。做完后给你自己一个![1shuriken](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/1shuriken.png)。 -###挑战 #3 +### 挑战 #3 你已经在前面的挑战中掌握了使用可选参数的函数。相当有趣吧! 但是根据之前的方法,你只能有一个固定最大数量的参数。如果你想*真的*得到一个可变数量的输入参数的函数,这还有一个更好的方法。 @@ -179,7 +179,7 @@ Swift函数参数可以有一个默认值,这是旧的Objective-C和Swift之 ![3shurikens](http://cdn5.raywenderlich.com/wp-content/uploads/2014/07/3shurikens.png) -####解决方法:提示 +#### 解决方法:提示 你可以把函数的最后一个参数定义为**name: Type...**,这样就定义一个接受可变数量参数的函数。然后,你就可以用一个普通的**Array**来操作**name**了。 你可以用**Array.map((T)->(T))**来一个一个处理其中的元素。也可以用**Array.reduce(T, (T)->(T))**换算数组中的单个值。 @@ -188,7 +188,7 @@ Swift函数参数可以有一个默认值,这是旧的Objective-C和Swift之 ![2shurikens](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/2shurikens.png) -####解决方法:教程 +#### 解决方法:教程 这个问题涉及到了很多Swift内建的语言特性,所以在看最终的解决方案之前,让我们先来浏览一些概念。 首先来看一下如何定义一个接受可变数量参数的函数: @@ -283,7 +283,7 @@ Swift函数参数可以有一个默认值,这是旧的Objective-C和Swift之 ![1shuriken](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/1shuriken.png) -###挑战 #4 +### 挑战 #4 写一个命名为**countFrom(from:Int, #to:Int)**的函数,它将输出(比如通过**print()**或**println()**)从**from**到**to**的数值。你不可以使用任何循环、变量或者任何内建的数组函数。假定**from**的值小于**to**(输入是有效的)。 下面是调用的示例和它的输出: @@ -292,12 +292,12 @@ Swift函数参数可以有一个默认值,这是旧的Objective-C和Swift之 ![3shurikens](http://cdn5.raywenderlich.com/wp-content/uploads/2014/07/3shurikens.png) -####解决方法:提示 +#### 解决方法:提示 使用递归函数,每次调用增加**from**的值,直到它与**to**参数的值相等。 ![2shurikens](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/2shurikens.png) -####解决方法:教程 +#### 解决方法:教程 本问题的解决方案将涉及递归。对于每一个从**from**开始的数值,你将递归调用**countFrom**同时每次给**from**的值增加1。当**from**与**to**相等时,你将停止递归。这将有效的把函数转换成一个简单的循环。 下面我们来看一下完成的解决方案: @@ -321,7 +321,7 @@ Swift函数参数可以有一个默认值,这是旧的Objective-C和Swift之 为你学习了如何在Swift中使用递归给你自己![1shuriken](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/1shuriken.png)。 -##下一步我们将去哪? +## 下一步我们将去哪? ![Get your revenge in part 2!](http://cdn1.raywenderlich.com/wp-content/uploads/2014/07/Ninja_Swift2-250x250.png) *在第2节中得到复仇!* diff --git a/README.md b/README.md index df9b96d..5925408 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ -#泰然翻译教程索引 +# 泰然翻译教程索引 -##Swift +## Swift -###入门 +### 入门 - [Swift教程第一部分:快速指南](Swift-Tutorial-A-Quick-Start.md)-译者:Verky - [Swift教程第二部分: 一个简单的iOS应用](Swift-Tutorial-Part2-A-Simple-iOS-App.md)-译者:永远的微笑 - [Swift教程第三部分:元组,协议,委托和表格视图](./Swift-Tutorial-Part3-Tuples-Protocols-Delegates-and-Table-Views.md)-译者:小荄 - [Swift常见问题解答](Swift-Language-FAQ.md)-译者:紫秀青衣, 数羊, NickYang -###系列教程 +### 系列教程 - [如何快速做一个糖果消除类游戏:第一部分](How-to-Make-a-Game-Like-Candy-Crush-with-Swift-Tutorial-Part1.md)-译者:紫秀青衣 - [如何快速做一个糖果消除类游戏:第二部分](How-to-Make-a-Game-Like-Candy-Crush-with-Swift-Tutorial-Part2.md)-译者:Stroustrup_Lee -##Unity +## Unity -###系列教程 +### 系列教程 - Unity 2D diff --git a/Swift-Language-FAQ.md b/Swift-Language-FAQ.md index 1403d00..2369b25 100644 --- a/Swift-Language-FAQ.md +++ b/Swift-Language-FAQ.md @@ -1,4 +1,4 @@ -#Swift常见问题解答 +# Swift常见问题解答 Swift 是Apple发布的一种全新的、现代化的、类型安全的编程语言,它针对Cocoa开发。Swift已经被研发了4年之久,在今年刚刚过去的WWDC大会上发布。 @@ -12,9 +12,9 @@ Swift的语言更加简单和简洁,降低了iOS开发的门槛,而且让开 注意这里面有些答案只是一些意见或者推测,因此对它们要持怀疑态度。我们很欢迎看到你自己的一些想法或者意见,并且基于你的反馈我们将对应的来更新这个FAQ。 -##基础知识 +## 基础知识 -###我是一个初学者,我应该学Objective-C,还是Swift,或者两者都学? +### 我是一个初学者,我应该学Objective-C,还是Swift,或者两者都学? 在我们看来,这取决于你打算做一个独立开发者,还是去另一家iOS公司去工作。 @@ -24,26 +24,26 @@ Swift的语言更加简单和简洁,降低了iOS开发的门槛,而且让开 多年后随着前景的发展和Swift接受度的增长,这些答案可能会改变。最后,知道Objective-C可能就类似于知道COBOL(可能意味Objective-C会像COBOL一样变成一门古老的语言)。 -###我已经做了多年的Objective-C开发。我现在要重新开始学么? +### 我已经做了多年的Objective-C开发。我现在要重新开始学么? 是和不是,如果你一直在Apple平台发展,你仍然有着巨大的优势,因为你已经熟悉了Xcode和Cocoa/Cocoa Touch相关的API。跟学习Swift相比,学习Xcode和数量庞大的Cocoa/Cocoa Touch 相关API要耗费更多的时间,因此你应该在处于比较良好的状态。 长话短说,一旦你写Swift代码熟练了,你回感觉到很舒适自在—而且对于你来说,应该会很快的就学会Swift。 -###iOS和OS X Yosemite应用程序会只使用Swift开发么? +### iOS和OS X Yosemite应用程序会只使用Swift开发么? 不会,Apple已经将Swift进行了构造,让Swift可以和Objective-C流利的进行互操作,反之亦然!对于Objective-C的API和框架,Apple还未完全将其转换成Swift,但是仍然可以在你自己的Swift代码中使用。 这些只有时间来证明,但是在Swift慢慢被采纳的过程中,可能许多iOS和OS X应用商店还会持续的依赖Objective-C很多年。 -###Swift可以在其它版本的iOS和OS X下运行么? +### Swift可以在其它版本的iOS和OS X下运行么? 是的!Xcode 6可以编译Swift代码并在iOS 7及以上或者OS X 10.9及以上部署生成目标。Apple其实已经用Swift开发了一个WWDC app,你现在就可以从App Store下载试用! 不过要记住Apple不允许从Xcode的测试版本构建的app发布到App Store。因此你需要等到Xcode 6的最终版本发布之后,再将你用Swift写得app提交到App Store。 -###Swift是要取代Objective-C,还是对Objective-C的补充? +### Swift是要取代Objective-C,还是对Objective-C的补充? 引述Apple的话,“Objective-C不会消失,Swift和Objective-C两者都是做Cocoa和Cocoa Touch开发的最好工具”。 @@ -52,13 +52,13 @@ Swift的语言更加简单和简洁,降低了iOS开发的门槛,而且让开 然而这完全是推测,大家都在猜测在将来的框架和API开发中,Apple会逐渐减少Objective-C的使用,甚至有一天Objective-C可能会被废弃。因此,赶紧跳上Swift的火车加入raywenderlich.com的团队来吧:) -###什么是playground? +### 什么是playground? playground是一个你可以进行实时代码效果预览的文件,这对于学习Swift或者新的API,编写原型代码或者开发算法是非常棒的功能。 当你要说你将要去playground的时候小心你的孩子。就像Chris LaPollo所学到的,由于你没带孩子们去公园,当你坐到电脑前的时候,孩子们可能会在你身后大哭! -###开发者保密协议(NDA)解除了么? +### 开发者保密协议(NDA)解除了么? 现在还不清楚这方面的情况。最近iOS开发者协议条款中有了这样的更新: @@ -74,7 +74,7 @@ playground是一个你可以进行实时代码效果预览的文件,这对于 我们正试着从Apple得到一些解释,然后弄清楚这些,如果/当我们查清楚我们会更新这个帖子。 -###怎样学Swift? +### 怎样学Swift? 这里已经有学习Swift的一些非常好的资源: @@ -88,7 +88,7 @@ playground是一个你可以进行实时代码效果预览的文件,这对于 很快我们也会发布大量的额外资源 – 有关这方面请查看下一个问题! -###你们将来的书或者教程是否会使用Swift? +### 你们将来的书或者教程是否会使用Swift? 简短回答—是的!我们正尽最大力帮助大家过渡到Swift。 @@ -104,9 +104,9 @@ playground是一个你可以进行实时代码效果预览的文件,这对于 - **一些惊喜…**:我们还有一些惊喜来吸引你 – 敬请关注!:) -##马上行动 +## 马上行动 -###Swift是否能做一些Objective-C做不了的事情,又或者Objective-C能做的Swift做不了? +### Swift是否能做一些Objective-C做不了的事情,又或者Objective-C能做的Swift做不了? 答案是肯定的!Swift是一门提供了许多Objective-C并不支持的特性的现代语言。其中一些大的特性包括命名空间(namespacing), 可选择(optionals), 元组(tuples),泛型(generic),类型推导(type inference)等。 @@ -114,7 +114,7 @@ Objective-C也拥有一些特性是Swift并不支持的,例如给一个nil值 在看完这帖子之后,读者如果有兴趣获取更多细节的话可以阅读由苹果公司提供的指南:[Using Swift with Cocoa and Objective-C Guide](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/index.html#//apple_ref/doc/uid/TP40014216-CH2-XID_0) -###是否有些库接口(APIs)是Swift不支持的? +### 是否有些库接口(APIs)是Swift不支持的? 在写这个帖子的时候,我还没有发现有不支持的。 但是,在Objective-C和Swift的接口之间进行移植的时候需要注意一些问题,例如: @@ -147,13 +147,13 @@ for fruit in fruits as String[] { - 嵌套的类型 - 柯里化函数 -###println()结果显示在PlayGround的什么地方? +### println()结果显示在PlayGround的什么地方? 为了能看到控制台的输出,你必须打开Assistant Editor。 你可以通过选择**View > Assistant Editor > Show Assistant Editor** 或者按 **Option + Command + Return**键来打开Assistant Editor。 感谢[Chris LaPollo](http://www.raywenderlich.com/u/clapollo) 提供了关于这方面的信息 -###在Playgrounds如何查看那些很酷的值图 +### 在Playgrounds如何查看那些很酷的值图 你可以在Playgrounds上图形化显示一个值在不同时间的结果,这对算法的可视化十分便利。你可以在PlayGround里面通过输入一些代码实现在不同时间产生一些值,譬如: @@ -165,7 +165,7 @@ for x in 1..10 { 在边栏上,你可以看到一些内容,例如“9 times”。 把鼠标移动到这一行上面,将会有一个“+”按钮出现。点击这个按钮(保证你的Assistant Editor是打开的),然后你就可以看到这个图了 -###如何运行REPL(Read-Eval-Print-Loop: 读验证打印循环)? +### 如何运行REPL(Read-Eval-Print-Loop: 读验证打印循环)? 在终端上执行以下命令来让使用Xcode 6的命令行工具 @@ -181,14 +181,14 @@ xcrun swift 当你想退出REPL的时候你可以输入 `:exit` 或者 `:quit`来退出。你也可以使用快捷键 **CTRL+D** -###你能使用Swift来调用你自己的Objective-C代码或者第三方库么?如果可以,该怎么实现? +### 你能使用Swift来调用你自己的Objective-C代码或者第三方库么?如果可以,该怎么实现? 可以的!当你添加你的第一个.swift文件到你的Xcode工程的时候你将收到让Xcode创建一个桥接头文件的提示。在那个头文件里面你可以导入要被你的Swift代码访问的Objective-C头文件。 接着,你不必再进行import,就可以在你的Swift代码使用这些类。你可以用Swift调用系统类同样的语法来使用你的自定义Objective-C代码。 -###那么数组只能包含一种对象类型?那如果我想要包含多个对象类型,该怎么做? +### 那么数组只能包含一种对象类型?那如果我想要包含多个对象类型,该怎么做? 在Swift中,鼓励开发者使用只包含一种对象类型的强类型数组,语句可以想这样: @@ -204,7 +204,7 @@ var goodArray: String[] = ["foo", "bar"] var brokenArray: AnyObject[] = ["foo", 1, 12.23, true] ``` -###对于字典类型(dictionaries)是否也是这样?字典类型是否也应该是强类型? +### 对于字典类型(dictionaries)是否也是这样?字典类型是否也应该是强类型? 是的!但是同样你可以使用‘AnyObject’来避免它。对于字典类型,它常常可能并不是所有的值都是同一种类型。想想由服务端发送过来的一个以字典类型表现的Json格式的响应: @@ -214,13 +214,13 @@ var employee : Dictionary = ["FirstName" : "Larry", "LastName 这个字典包含了两个‘String’类型的键值和一个‘Double’类型的键值。虽然这种方式实现没有问题,但是你应该选择创建第一类模型对象(first classs model objects)来表示你的数据,而尽量不依赖字典。 -##追根溯源 +## 追根溯源 -###Swift是否有等价于id的东西? +### Swift是否有等价于id的东西? 是的。正如前文所说,Objective-C API返回id,在Swift中用AnyObject代替。AnyObject类型可以代替任意类类型的实例。同样Any可以代替任意类型的实例(除了函数类型)。 -###Swift的内省机制是怎样的?(例如与if ([obj isKindOfClass:[Foo class]]) { … }等价的)? +### Swift的内省机制是怎样的?(例如与if ([obj isKindOfClass:[Foo class]]) { … }等价的)? 你可以用 is关键字来检查变量和常量的类型。编译器足够聪明能让你知道使用 is是否多此一举。这都要归功于Swift的类型安全机制让这一切成为可能,已经不再能够将不同的类型分配给同一个引用。 @@ -257,7 +257,7 @@ Playground execution failed: error: :7:14: error: 'is' test is always true if someValue is String { ``` -###在Swift里怎样把位移操作结果放入一个枚举中?(例如:MyVal = 1<<5) +### 在Swift里怎样把位移操作结果放入一个枚举中?(例如:MyVal = 1<<5) 不幸的是这里有一点让人糊涂的地方,而且毫无疑问没有c的变种那么简单明了。你将使用如下展示的一个struct而不是enum。 @@ -285,7 +285,7 @@ func ^ (lhs: MyOptions, rhs: MyOptions) -> MyOptions { return MyOptions(lhs.valu [Nate Cook](http://stackoverflow.com/users/59541/nate-cook)授权,需要更多细节请查看[his answer on Stack Overflow](http://stackoverflow.com/a/24066171) -###Swift怎么进行GCD? +### Swift怎么进行GCD? 相同的方式,你可以使用C API正如你在Objective-C中那样。 @@ -297,7 +297,7 @@ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), 你同样可以使用更高级的苹果公司处理并发的NSOperationQueue。 -###那么Objective-C本地化的宏定义呢? +### 那么Objective-C本地化的宏定义呢? 类似Objective-C中的NSLocalizedString宏集合,在Swift中你可以使用 NSLocalizedString(key:tableName:bundle:value:comment:)为本地化做好准备。 tableName, bundle, value这些参数都有默认值。如果你过去习惯使用NSLocalizedString那么你可以像下面这样写: @@ -306,12 +306,12 @@ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), NSLocalizedString("Hello", comment: "standard greeting") ``` -###我是否还需要为循环引用而担心? +### 我是否还需要为循环引用而担心? 当然!当两个对象彼此强引用的时候还是有可能创建出一个retain的循环。打破这个循环你还是需要使用类似Objective-C中的方法。有三个关键字来声明如下所述引用类型;弱引用和无主引用可以解决循环引用的问题。 -###我什么时候使用强引用,弱引用和无主引用? +### 我什么时候使用强引用,弱引用和无主引用? - **strong**: 在你拥有这个东西的时候可以使用强引用。 强引用使得ARC保存retain的实例直到不再需要这些实例为止。当所有的强引用都被移除的时候,被引用的实例也会被释放。 @@ -331,7 +331,7 @@ NSLocalizedString("Hello", comment: "standard greeting") 无主引用的行为类似于Objective-C中的 `unsafe_unretained`。你有责任确保当你释放掉被引用的对象后,不再访问该对象,否则你的程序会崩溃。无主引用必须是不可选并且不能被设置为空的。无主引用也是隐式解析的。 -###分号在哪里?! +### 分号在哪里?! 分号在Swift中是可选的,为了可读性,苹果建议你不要再使用它们。 @@ -341,21 +341,21 @@ NSLocalizedString("Hello", comment: "standard greeting") for var index = 0; index < 3; ++index { ... } ``` -##下一步是什么? +## 下一步是什么? -###Swift下一步有何打算? +### Swift下一步有何打算? 当前是版本1,对于这门语言苹果的意图遍历他们是明确简洁的。 所以一定要[报告错误信息并请求特征](http://bugreport.apple.com/)!在正式版本1发布我们看到有很多地方得到改进。 -###CocoaPods响应swift速度如何? +### CocoaPods响应swift速度如何? 几乎一样的使用方法。Swift项目仍然使用xcode开发并且支持多个targets。在自定义模块和框架时它还有很多潜在提高的地方。 为了使用此特性CocosPods被重写是可行的。用CocoaPods与[Swift项目](https://medium.com/swift-programming/cocoapods-with-swift-e6f8ba8f0afc)工作的人和少部分使用CocoaPods工作的人已经[讨论这个问题](https://github.com/CocoaPods/CocoaPods/issues/2218)了。 -##更多问题? +## 更多问题? 如果你有任何问题请提交你们疑问。我会挑选一些好的问题并更新问题和回复。-- 和给你回馈! diff --git a/Swift-Tutorial-Part2-A-Simple-iOS-App.md b/Swift-Tutorial-Part2-A-Simple-iOS-App.md index cf696ee..b69255c 100644 --- a/Swift-Tutorial-Part2-A-Simple-iOS-App.md +++ b/Swift-Tutorial-Part2-A-Simple-iOS-App.md @@ -1,411 +1,411 @@ -#Swift教程第二部分: 一个简单的iOS应用 - +# Swift教程第二部分: 一个简单的iOS应用 + 欢迎回到我们的Swift教程系列 在这个Swift教程中创建一个简单的iOS app。 - -在第一个Swift教程中,你们学习了Swift语言的基础语法,并创建了自己的小费计算器类。 - -在第二个Swift教程,你将会学习怎样去创建一个简单的iOS app。具体来说,你将要为上次开发的小费计算器类创建一个用户界面。 - -我会以写教程的方式,这样有助于初学和经验丰富的iOS开发者都能迅速过渡到SWift开发。 - -在这个Swift教程中,你需要用于最新的Xcode副本(在写这篇教程时最新的Xcode6-Beta版本)。你不需要像任何的Swift或者Objective-C编程经验,但是如果你有这方面的编辑经验会帮助你学习得更快。 - - -注意:在写本教程的时候,[我们不能发布Xcode6的截图](http://www.raywenderlich.com/74138/swift-language-faq),因为它仍在测试阶段。因此,我们禁止在本教程截图直到我们知道它是允许的。 - - -#开始 - -启动Xcode,接着步骤File\New\Project。选择iOS\Application\Single View Application,点击下一步。 - -输入产品名称为TipCalculator,设置语言为Swift,设备为iPhone。确认Use Core Data没有勾选,点击一下步。 - -选择一个保存目录,并点击Create。 - -我们看到Xcode下面为你创建工程。在Xcode的左上角,选择iPhone5模拟器并点击Play测试您的应用程序。 - -您应该看到一个空白屏幕出现。Xcode在你的应用程序创造了一个空白的屏幕,在本教程中你将把这个空白屏幕填充内容! - -#创建你的模板 - -首先第一件事-在你为你的app创建用户界面之前,你要先创建你app的模板。一个模板就是一个类(或者几个类组成)描述你的类的数据,并且完成你的app的数据操作。 - -在这个教程,你的app模板在[第一个SWift教程](http://www.raywenderlich.com/74438/swift-tutorial-a-quick-start)中简单地命名为TipCalculator,现在你要将他改名为TipCalculatorModel - -让我们把这个类添加到你的项目中。接着下面步骤,File\New\File,然后选择iOS\Source\Swift File,文件名填写TipCalculatorModel.swift,点击创建。 - -``` -注意:你不能从你的app中调用Playground文件。因为Playground文件只是用于测试和原型设计的代码;如果你想从你的app中调用Playground文件,你把它转换成为一个Swift文件,就好像你接下来要做的那样。 + +在第一个Swift教程中,你们学习了Swift语言的基础语法,并创建了自己的小费计算器类。 + +在第二个Swift教程,你将会学习怎样去创建一个简单的iOS app。具体来说,你将要为上次开发的小费计算器类创建一个用户界面。 + +我会以写教程的方式,这样有助于初学和经验丰富的iOS开发者都能迅速过渡到SWift开发。 + +在这个Swift教程中,你需要用于最新的Xcode副本(在写这篇教程时最新的Xcode6-Beta版本)。你不需要像任何的Swift或者Objective-C编程经验,但是如果你有这方面的编辑经验会帮助你学习得更快。 + + +注意:在写本教程的时候,[我们不能发布Xcode6的截图](http://www.raywenderlich.com/74138/swift-language-faq),因为它仍在测试阶段。因此,我们禁止在本教程截图直到我们知道它是允许的。 + + +# 开始 + +启动Xcode,接着步骤File\New\Project。选择iOS\Application\Single View Application,点击下一步。 + +输入产品名称为TipCalculator,设置语言为Swift,设备为iPhone。确认Use Core Data没有勾选,点击一下步。 + +选择一个保存目录,并点击Create。 + +我们看到Xcode下面为你创建工程。在Xcode的左上角,选择iPhone5模拟器并点击Play测试您的应用程序。 + +您应该看到一个空白屏幕出现。Xcode在你的应用程序创造了一个空白的屏幕,在本教程中你将把这个空白屏幕填充内容! + +# 创建你的模板 + +首先第一件事-在你为你的app创建用户界面之前,你要先创建你app的模板。一个模板就是一个类(或者几个类组成)描述你的类的数据,并且完成你的app的数据操作。 + +在这个教程,你的app模板在[第一个SWift教程](http://www.raywenderlich.com/74438/swift-tutorial-a-quick-start)中简单地命名为TipCalculator,现在你要将他改名为TipCalculatorModel + +让我们把这个类添加到你的项目中。接着下面步骤,File\New\File,然后选择iOS\Source\Swift File,文件名填写TipCalculatorModel.swift,点击创建。 + +``` +注意:你不能从你的app中调用Playground文件。因为Playground文件只是用于测试和原型设计的代码;如果你想从你的app中调用Playground文件,你把它转换成为一个Swift文件,就好像你接下来要做的那样。 +``` + +打开TipCalculator.swift,并把TipCalculator类从上一个项目中复制过来,跟着做下面这些操作: + +1.把类重命名为TipCalculatorModel + +2.把常量total和taxPct改为变量(因为用户运行app的时候将要改变他们的值) + +3.因为这些,你要把subtotal变为一个computed property。subtotal属性替换为以下几个点: + +```Swift +var subtotal: Double { + get { + return total / (taxPct + 1) + } +} ``` - -打开TipCalculator.swift,并把TipCalculator类从上一个项目中复制过来,跟着做下面这些操作: - -1.把类重命名为TipCalculatorModel - -2.把常量total和taxPct改为变量(因为用户运行app的时候将要改变他们的值) - -3.因为这些,你要把subtotal变为一个computed property。subtotal属性替换为以下几个点: - -```Swift -var subtotal: Double { - get { - return total / (taxPct + 1) - } -} -``` - -一个计算小计没有实际的储存值。相反地,它每次都是根据其他值计算出来的。在这里,你小计部分都是由total和taxPct现在的值计算出来的。 - - + +一个计算小计没有实际的储存值。相反地,它每次都是根据其他值计算出来的。在这里,你小计部分都是由total和taxPct现在的值计算出来的。 + + 注意:如果你想,还可以提供一个setter方法给计算小计方法,语法如下: - -```Swift -var subtotal: Double { - get { - return total / (taxPct + 1) - } - set(newSubtotal) { - //... - } -} -``` - -你的setter方法将会更新它的备份属性(i.e. 根据newSubtotal设置total和taxPct,但是这对app是没有意义的,所以在这里你不用实现。 - - -4.在init中删除设置subtotal的行。 - -5.当你完成后,删除一些注释,文件内容应该如下: -```Swift -import Foundation - -class TipCalculatorModel { - - var total: Double - var taxPct: Double - var subtotal: Double { - get { - return total / (taxPct + 1) - } - } - - init(total:Double, taxPct:Double) { - self.total = total - self.taxPct = taxPct - } - - func calcTipWithTipPct(tipPct:Double) -> Double { - return subtotal * tipPct - } - - func returnPossibleTips() -> Dictionary { - - let possibleTipsInferred = [0.15, 0.18, 0.20] - let possibleTipsExplicit:Double[] = [0.15, 0.18, 0.20] - - var retval = Dictionary() - for possibleTip in possibleTipsInferred { - let intPct = Int(possibleTip*100) - retval[intPct] = calcTipWithTipPct(possibleTip) - } - return retval - - } - -} -``` - -现在你已经为你的app界面准备好模板了。 - -#介绍 Storyboards和Interface Builder -``` + +```Swift +var subtotal: Double { + get { + return total / (taxPct + 1) + } + set(newSubtotal) { + //... + } +} +``` + +你的setter方法将会更新它的备份属性(i.e. 根据newSubtotal设置total和taxPct,但是这对app是没有意义的,所以在这里你不用实现。 + + +4.在init中删除设置subtotal的行。 + +5.当你完成后,删除一些注释,文件内容应该如下: +```Swift +import Foundation + +class TipCalculatorModel { + + var total: Double + var taxPct: Double + var subtotal: Double { + get { + return total / (taxPct + 1) + } + } + + init(total:Double, taxPct:Double) { + self.total = total + self.taxPct = taxPct + } + + func calcTipWithTipPct(tipPct:Double) -> Double { + return subtotal * tipPct + } + + func returnPossibleTips() -> Dictionary { + + let possibleTipsInferred = [0.15, 0.18, 0.20] + let possibleTipsExplicit:Double[] = [0.15, 0.18, 0.20] + + var retval = Dictionary() + for possibleTip in possibleTipsInferred { + let intPct = Int(possibleTip*100) + retval[intPct] = calcTipWithTipPct(possibleTip) + } + return retval + + } + +} +``` + +现在你已经为你的app界面准备好模板了。 + +# 介绍 Storyboards和Interface Builder +``` 注意:如果你是一个经验丰富的iOS开发者,这一节和下一节可能是你的复习。为了加快速度,你可以想直接跳到一个视图控制器之旅。我们将有一个创建了用户界面的启动项目等着你。 -``` - -你用来创建iOS app用户界面的叫Storyboard。Xcode内置有一个叫Interface Builder的构建工具,可以让你可视化地编辑Storyboards。 - -使用Interface Builder,你可以在你的app(称作视图)简单地拖放来布局全部的button,text fild,label和其他控件。 - -继续进行,在Xcode的左边点击Main.storyboard来在Interface Builder中显示Storyboard。 - -在这里我们有很多东西讨论,所以让我们重温一次这个屏幕的每一章节。 - -1.最左边是项目导航栏,在这里你可以看到你的项目文件。 - -2.在Interface Builder的左边是你的文档大纲,你可以对你的app(视图控制器)每一个“屏幕”的视图都一目了然。请务必点击“向下”箭头来完全展开第个项目的文档大纲。 - -现在你的app只有空白视图一个视图控制器。很快你将会往里面添加内容。 - -3.你的视图控制器左侧有一个箭头。表明这是一个初始视图控制器,或者app启动后最先显示的视图控制器。你可以通过拖动箭头到另一个视图控制器来改变,或者在不同的视图控制器点击"Is Initial View Controller"属性(后面有详细介绍)。 - -4.在Interface Builder的底部你可以看见写关w..、h..的。这意味着,你在编辑app的布局时,可以使用不同大小的用户界面。你可以使用自动布局。点击该区域,你可以选择指定大小的屏幕来编辑你的布局。在后面的教程你将了解自适应用户界面和自动布局。 - -5.在视图控制器的顶部,你可以看到三个小图标,这代表视图控制器的本身和其他两个选项:响应和退出。如果你使用Xcode开发一会儿,你会发现已经移动了(它们是视图控制器的下文)。在教程中,你不会使用这些,所以不用担心它们的出现。 - -6.在界面生成器的右下方是自动布局的四个图标。同样,在后面的教程你将了解更多关于这些。 - -7.在界面生成器的右上角的是你选择文档大纲的检查器。如果你没有看到检检查器,按下面步骤显:View\Utilities\Show Utilities。 -``` -注意:检查器有多个选项卡,在本教程中,你将会为你的app视图添加很多配置。 -``` -8.界面生成器的右下方是Libraries。这个有一个不同类型的视图或者视图控制器的库,你可以添加到你的app中。很快你就会从控件库中拖动控件到你的视图控制器来布局你的app。 - - -#创建你的视图 - -记住,你的TipCalculatorModel类有两个输入参数:total(总数)和tax percentage(税收的百分比)。 - -如果用户输入total(总数)的是使用的是一个数字输入键盘,这对于用户来说是非常友好的,所以最适合的是一个文本输入域。对于tax percentage(税收的百分比),通常被限制在一个小范围内的值,所以你将使用一个滑动控件来代替。 - -除了文字哉和滑动控件,你要为它们都添加一个标签,还要添加一个导航栏,用来显示应用程序的名称,一个按钮,点击按钮的就进行计算,以及一个显示结果的文字域。 - -现在让我们来创建用户界面。 - -1.Navigation bar。现在添加一个Navigation bar,选择你的视图控制器接着下面步骤Editor\Embed In\Navigation Controller。这样就在你的视图控制器中设置一个导航栏。双击导航栏(在视图控制器内),并设置Tip Calculator的文本。 - -2.Labels。从对象库中拖动一个Labels到你的视图控制器。双击标签设置文字为Bill Total (Post-Tax):。选择标签,并在检查器的第五个选项卡(the Size Inspector)设置X=33,Y=81。重复步骤设置另一个标签,不同的是文字为Tax Percentage (0%):,X=20,Y=120。 - -3.Text Field。从对象库中拖动一个Text Field到你的视图控制器。在属性检查器中,设置Keyboard Type=Decimal Pad。在尺寸检查器中,设置X=192,Y=72,Width=268。 - -4.Slider。从对象库中拖动一个Slider到您的视图控制器。在属性检查器中,设置Minimum Value=0, Maximum Value=10, and Current Value=6。在尺寸检查器中,设置X=190,Y=111,Width=272。 - -5.Button。从对象库中拖动一个Button到您的视图控制器。双击按钮,把文本设置为Calculate。在尺寸检查器中,设置X=208 Y=149。 - -6.Text View。从对象库中拖动一个Text View到您的视图控制器。双击文本视图,并删除占位符文本。在属性检查器,确保可编辑和可选择不检查。在尺寸检查器中,设置X=20,Y=187,Width=440,Height=288。 - -7.Tap Gesture Recognizer。从对象库中拖动一个Tap Gesture Recognizer到你的主视图。这将用于当用户点击该视图时隐藏键盘。 - -8.Auto Layout。 Interface Builder会自动地为你的自动布局做大量的合理自动布局的设置。要做到这一点,在Interface Builder左下角点击第三个按钮(看起来像一个战斗机),然后选择Add Missing Constraints - -构建并在您的iPhone5的模拟器运行,你应该可以看到一个基本的用户界面了。 - -#视图控制器之旅 - -注意:如果你想跳过本节,这里是本节项目知识点的[项目压缩包](http://cdn4.raywenderlich.com/wp-content/uploads/2014/06/TipCalculatorStarter11.zip)。 - -到目前为止,你已经创建了app的模板和视图,是时候为重点转到视图控制器上了。 - -打开ViewController.swift。这是你app中单页面视图控制器("屏幕")的Swift代码。它负责管理你app中模板和视图的通信。 - -你会发现类文件里面已经有如下代码: - -```Swift -// 1 -import UIKit - -// 2 -class ViewController: UIViewController { - - // 3 - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } - - // 4 - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - -} -``` -在Swift中你有一些新的元素是没有学习的,所以我现在开始学习这些新元素。 - -1.iOS分成了很多个框架,每一个都由不同的代码集组成。在你app中使用框架之前要把它导入到项目中。UIKit包含组成视图控制器的基类,例如button和text field等各种控件。 - -2.这是你在第一个练习中看到类的另一个类的子类。在这里,你声明了一个新的类ViewController,并且该类继承了苹果的UIViewController类。 - - -注意:经验丰富的iOS开发者-你不用为了避免了命令空间冲突,像Objective-C那样在类名字添加类的前缀(ie.你不能命令这个RWTViewController)。因为Swift支持命令空间,而且你在项目里创建的每一个类都有自己的命令空间。 +``` + +你用来创建iOS app用户界面的叫Storyboard。Xcode内置有一个叫Interface Builder的构建工具,可以让你可视化地编辑Storyboards。 + +使用Interface Builder,你可以在你的app(称作视图)简单地拖放来布局全部的button,text fild,label和其他控件。 + +继续进行,在Xcode的左边点击Main.storyboard来在Interface Builder中显示Storyboard。 + +在这里我们有很多东西讨论,所以让我们重温一次这个屏幕的每一章节。 + +1.最左边是项目导航栏,在这里你可以看到你的项目文件。 + +2.在Interface Builder的左边是你的文档大纲,你可以对你的app(视图控制器)每一个“屏幕”的视图都一目了然。请务必点击“向下”箭头来完全展开第个项目的文档大纲。 + +现在你的app只有空白视图一个视图控制器。很快你将会往里面添加内容。 + +3.你的视图控制器左侧有一个箭头。表明这是一个初始视图控制器,或者app启动后最先显示的视图控制器。你可以通过拖动箭头到另一个视图控制器来改变,或者在不同的视图控制器点击"Is Initial View Controller"属性(后面有详细介绍)。 + +4.在Interface Builder的底部你可以看见写关w..、h..的。这意味着,你在编辑app的布局时,可以使用不同大小的用户界面。你可以使用自动布局。点击该区域,你可以选择指定大小的屏幕来编辑你的布局。在后面的教程你将了解自适应用户界面和自动布局。 + +5.在视图控制器的顶部,你可以看到三个小图标,这代表视图控制器的本身和其他两个选项:响应和退出。如果你使用Xcode开发一会儿,你会发现已经移动了(它们是视图控制器的下文)。在教程中,你不会使用这些,所以不用担心它们的出现。 + +6.在界面生成器的右下方是自动布局的四个图标。同样,在后面的教程你将了解更多关于这些。 + +7.在界面生成器的右上角的是你选择文档大纲的检查器。如果你没有看到检检查器,按下面步骤显:View\Utilities\Show Utilities。 +``` +注意:检查器有多个选项卡,在本教程中,你将会为你的app视图添加很多配置。 +``` +8.界面生成器的右下方是Libraries。这个有一个不同类型的视图或者视图控制器的库,你可以添加到你的app中。很快你就会从控件库中拖动控件到你的视图控制器来布局你的app。 + + +# 创建你的视图 + +记住,你的TipCalculatorModel类有两个输入参数:total(总数)和tax percentage(税收的百分比)。 + +如果用户输入total(总数)的是使用的是一个数字输入键盘,这对于用户来说是非常友好的,所以最适合的是一个文本输入域。对于tax percentage(税收的百分比),通常被限制在一个小范围内的值,所以你将使用一个滑动控件来代替。 + +除了文字哉和滑动控件,你要为它们都添加一个标签,还要添加一个导航栏,用来显示应用程序的名称,一个按钮,点击按钮的就进行计算,以及一个显示结果的文字域。 + +现在让我们来创建用户界面。 + +1.Navigation bar。现在添加一个Navigation bar,选择你的视图控制器接着下面步骤Editor\Embed In\Navigation Controller。这样就在你的视图控制器中设置一个导航栏。双击导航栏(在视图控制器内),并设置Tip Calculator的文本。 + +2.Labels。从对象库中拖动一个Labels到你的视图控制器。双击标签设置文字为Bill Total (Post-Tax):。选择标签,并在检查器的第五个选项卡(the Size Inspector)设置X=33,Y=81。重复步骤设置另一个标签,不同的是文字为Tax Percentage (0%):,X=20,Y=120。 + +3.Text Field。从对象库中拖动一个Text Field到你的视图控制器。在属性检查器中,设置Keyboard Type=Decimal Pad。在尺寸检查器中,设置X=192,Y=72,Width=268。 + +4.Slider。从对象库中拖动一个Slider到您的视图控制器。在属性检查器中,设置Minimum Value=0, Maximum Value=10, and Current Value=6。在尺寸检查器中,设置X=190,Y=111,Width=272。 + +5.Button。从对象库中拖动一个Button到您的视图控制器。双击按钮,把文本设置为Calculate。在尺寸检查器中,设置X=208 Y=149。 + +6.Text View。从对象库中拖动一个Text View到您的视图控制器。双击文本视图,并删除占位符文本。在属性检查器,确保可编辑和可选择不检查。在尺寸检查器中,设置X=20,Y=187,Width=440,Height=288。 + +7.Tap Gesture Recognizer。从对象库中拖动一个Tap Gesture Recognizer到你的主视图。这将用于当用户点击该视图时隐藏键盘。 + +8.Auto Layout。 Interface Builder会自动地为你的自动布局做大量的合理自动布局的设置。要做到这一点,在Interface Builder左下角点击第三个按钮(看起来像一个战斗机),然后选择Add Missing Constraints + +构建并在您的iPhone5的模拟器运行,你应该可以看到一个基本的用户界面了。 + +# 视图控制器之旅 + +注意:如果你想跳过本节,这里是本节项目知识点的[项目压缩包](http://cdn4.raywenderlich.com/wp-content/uploads/2014/06/TipCalculatorStarter11.zip)。 + +到目前为止,你已经创建了app的模板和视图,是时候为重点转到视图控制器上了。 + +打开ViewController.swift。这是你app中单页面视图控制器("屏幕")的Swift代码。它负责管理你app中模板和视图的通信。 + +你会发现类文件里面已经有如下代码: + +```Swift +// 1 +import UIKit + +// 2 +class ViewController: UIViewController { + + // 3 + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + } + + // 4 + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + +} +``` +在Swift中你有一些新的元素是没有学习的,所以我现在开始学习这些新元素。 + +1.iOS分成了很多个框架,每一个都由不同的代码集组成。在你app中使用框架之前要把它导入到项目中。UIKit包含组成视图控制器的基类,例如button和text field等各种控件。 + +2.这是你在第一个练习中看到类的另一个类的子类。在这里,你声明了一个新的类ViewController,并且该类继承了苹果的UIViewController类。 + + +注意:经验丰富的iOS开发者-你不用为了避免了命令空间冲突,像Objective-C那样在类名字添加类的前缀(ie.你不能命令这个RWTViewController)。因为Swift支持命令空间,而且你在项目里创建的每一个类都有自己的命令空间。 理解我的意思,替换类声明如下: - -```Swift -class UIViewController { -} - -class ViewController: UIKit.UIViewController { + +```Swift +class UIViewController { +} + +class ViewController: UIKit.UIViewController { +``` + +在这里UIKit.UIViewController指的是UIViewController类在UIKit的命令空间。同样,TipCalculator.UIViewController指的是你项目中的UIViewController类。 + +3.这个视图控制器的根视图第一次访问的方法。当你在Swift中重写方法时,要使用override关键字标识。 + +4.当设备运行时内存不足就调用此方法。这是清理一些不再使用的资源的好地方。 + +# 连接视图控制器和视图 + +现在你对已经比较了解你的视图控制器类了,让我们为它的子视图添加一些属,并在Interface Builder中勾起来。 + +要做的是,把下列有属性添加一你的ViewController类(viewDidLoad的右前方) + +```Swift +@IBOutlet var totalTextField : UITextField +@IBOutlet var taxPctSlider : UISlider +@IBOutlet var taxPctLabel : UILabel +@IBOutlet var resultsTextView : UITextView +``` + +这里声明了四个变量,就像你学习的[第一个Swift教程](http://www.raywenderlich.com/74438/swift-tutorial-a-quick-start)那样-一个UITextField,一个UISlider,一个UILabel和一个UITextView。 + +这里只有一个区别:你要在变量的前面加上前缀@IBOutlet关键字。Interface Builder扫描你的代码在寻找你的视图控制器前缀是这个关键字相关的属性。如果发现相关的,你就可以把它连接到视图。 + +让我们试一下这个。打开Main.storyboard并在文档大纲中选择你的视图控制器。打开Connections Inspector(第6个选项卡),你会在Outlets 中看到所以属性都列出来。 + +你会发现在resultsTextView的右边有一个小圆圈。按着Control键并从按钮开始按下拖动到计算按钮下面的文本视图中,然后释放就可以把你的Swif变量连接到你的视图中。 + +现在重复步骤设置其它三个变量,把每个变量分别连接应用的UI元素。 +``` +注意:还有另一个更简单的方法使你的变量和视图控制器连接起来。 + +Main.storyboard打开,你将会打开你的辅助编辑器(View\Assistant Editor\Show Assistant Editor)并且确保你的辅助编辑器是设置了显示你视图控制器的Swift代码。 + +然后你可以把你的视图从在viewDidLoad右前方拖动到辅助编辑器中。在弹出窗口中,你可以输入变量名称,然后单击连接。 + +这一步就可以在Interface Builder中创建你的变量并连接到你的视图控制器。很方便,不是吗? + +这两种方法,你可以在你们的项目中选择你喜欢的方式。 ``` - -在这里UIKit.UIViewController指的是UIViewController类在UIKit的命令空间。同样,TipCalculator.UIViewController指的是你项目中的UIViewController类。 - -3.这个视图控制器的根视图第一次访问的方法。当你在Swift中重写方法时,要使用override关键字标识。 - -4.当设备运行时内存不足就调用此方法。这是清理一些不再使用的资源的好地方。 - -#连接视图控制器和视图 - -现在你对已经比较了解你的视图控制器类了,让我们为它的子视图添加一些属,并在Interface Builder中勾起来。 - -要做的是,把下列有属性添加一你的ViewController类(viewDidLoad的右前方) - -```Swift -@IBOutlet var totalTextField : UITextField -@IBOutlet var taxPctSlider : UISlider -@IBOutlet var taxPctLabel : UILabel -@IBOutlet var resultsTextView : UITextView -``` - -这里声明了四个变量,就像你学习的[第一个Swift教程](http://www.raywenderlich.com/74438/swift-tutorial-a-quick-start)那样-一个UITextField,一个UISlider,一个UILabel和一个UITextView。 - -这里只有一个区别:你要在变量的前面加上前缀@IBOutlet关键字。Interface Builder扫描你的代码在寻找你的视图控制器前缀是这个关键字相关的属性。如果发现相关的,你就可以把它连接到视图。 - -让我们试一下这个。打开Main.storyboard并在文档大纲中选择你的视图控制器。打开Connections Inspector(第6个选项卡),你会在Outlets 中看到所以属性都列出来。 - -你会发现在resultsTextView的右边有一个小圆圈。按着Control键并从按钮开始按下拖动到计算按钮下面的文本视图中,然后释放就可以把你的Swif变量连接到你的视图中。 - -现在重复步骤设置其它三个变量,把每个变量分别连接应用的UI元素。 -``` -注意:还有另一个更简单的方法使你的变量和视图控制器连接起来。 - -Main.storyboard打开,你将会打开你的辅助编辑器(View\Assistant Editor\Show Assistant Editor)并且确保你的辅助编辑器是设置了显示你视图控制器的Swift代码。 - -然后你可以把你的视图从在viewDidLoad右前方拖动到辅助编辑器中。在弹出窗口中,你可以输入变量名称,然后单击连接。 - -这一步就可以在Interface Builder中创建你的变量并连接到你的视图控制器。很方便,不是吗? - -这两种方法,你可以在你们的项目中选择你喜欢的方式。 -``` -#连接动作和视图控制器 - -就好像你在视图控制器中连接你的变量和视图那样,你想在视图控制器中连接某一个动作到你的视图(如button的单击事件)方法。 - -这样做,打开ViewController.swift并在你的类中添加这三个方法: - -```Swift -@IBAction func calculateTapped(sender : AnyObject) { -} -@IBAction func taxPercentageChanged(sender : AnyObject) { -} -@IBAction func viewTapped(sender : AnyObject) { -} -``` - -当你在视图中声明动作的回调方法,它们需要一个相同名字的方法-一个没有返回值的方法,需要一个AnyObject类型的参数,它代表一个类的任意类型。 - +# 连接动作和视图控制器 + +就好像你在视图控制器中连接你的变量和视图那样,你想在视图控制器中连接某一个动作到你的视图(如button的单击事件)方法。 + +这样做,打开ViewController.swift并在你的类中添加这三个方法: + +```Swift +@IBAction func calculateTapped(sender : AnyObject) { +} +@IBAction func taxPercentageChanged(sender : AnyObject) { +} +@IBAction func viewTapped(sender : AnyObject) { +} +``` + +当你在视图中声明动作的回调方法,它们需要一个相同名字的方法-一个没有返回值的方法,需要一个AnyObject类型的参数,它代表一个类的任意类型。 + 注意:AnyObject相当于在Ojbective-C中的id。如果想学习更多关于AnyObject的内容,可以到[Swift Language FAQ](http://www.raywenderlich.com/74138/swift-language-faq)。 - - - -为了使Interface Builder知道你的新方法,你的方法必须加上@IBAction关键字(就像你用@IBOutlet关键字标识变量那样)。 - -接着,回到Main.storyboard并在视图控制器中选择文档大纲。确保连接检查器是打开的(第6个标签)和你在Received Actions中看到你的新方法。 - -在calculateTapped:的右边找到圆圈,并在圆圈内拖出一条线连接到Calculate的按钮。 - -在弹出的窗口选择Touch Up Inside: - -这实际上就是说:“当用户从屏幕按钮的上方释放手指时,就调用名为calculateTapped:的方法。 - -现在复习另外两个方法: - -*从taxPercentageChanged:拖动到你的slider,并把它连接到不同值的动作,这就是用户每次移动的slider。 - -*从viewTapped:拖动到文档大纲的Tap Gesture Recognize。没有选择手势识别器的动作;你的方法会被称为触发识别器。 -``` -注意:就像属性一样,这是使用Interface Builder的一个连接操作的快捷方法。 - -你可以按下Control键拖拽控件来辅助编辑你视图控制器的Swift代码的动作(如button)。在弹出的窗口,你要选择Action,给方法一个名字。 - -这只要一步就能将在你的Swift文件中创建一个方法和把动作连接到你的方法。同样,这两种个工作,它只是为了方便你! -``` -#连接你的视图控制器和模板 - -你要做的差不多完成了-现在你要把你的视图控制器和模板连接起来。 - -打开ViewController.swift并为你的模板类添加一个属性和一个刷新UI的方法。 - -```Swift -let tipCalc = TipCalculatorModel(total: 33.25, taxPct: 0.06) - -func refreshUI() { - // 1 - totalTextField.text = String(tipCalc.total) - // 2 - taxPctSlider.value = Float(tipCalc.taxPct) * 100.0 - // 3 - taxPctLabel.text = "Tax Percentage (\(Int(taxPctSlider.value))%)" - // 4 - resultsTextView.text = "" -} -``` - -现在让我们来复习一次refreshUI: - - -1.在Swift中,转换类型的时候必须明确。现在你要把tipCalc.total从Double转换成String。 - -2.你想要税收百分比显示为整数(i.e. 0%-10%)而不是一个小数(好像0.06)。所以你要把结果乘以100. - -注意:计算是必须的,因为taxPctSlider.value变量是一个浮点型数据。 - -3.在这里,你在税收百分比的基础上使用字符串插值更新标签。 - -4.当用户点击计算按钮的时候你要清除文本视图的计算结果。 - -接着,在viewDidLoad:的底部添加调用refreshUI的方法 。 - -```Swift -refreshUI() -``` - -taxPercentagechanged和viewTapped的实现如下: - -```Swift -@IBAction func taxPercentageChanged(sender : AnyObject) { - tipCalc.taxPct = Double(taxPctSlider.value) / 100.0 - refreshUI() -} -@IBAction func viewTapped(sender : AnyObject) { - totalTextField.resignFirstResponder() -} -``` - -taxPercentageChanged简单地做了"乘以100"的相反操作,当视图监听里viewTapped在totalTextField调用了resignFirstResponder(有隐藏键盘的效果)。 - -实现了一个方法。现在实现calculateTapped如下: - -```Swift -@IBAction func calculateTapped(sender : AnyObject) { - // 1 - tipCalc.total = Double(totalTextField.text.bridgeToObjectiveC().doubleValue) - // 2 - let possibleTips = tipCalc.returnPossibleTips() - var results = "" - // 3 - for (tipPct, tipValue) in possibleTips { - // 4 - results += "\(tipPct)%: \(tipValue)\n" - } - // 5 - resultsTextView.text = results -} -``` - -让我们跟着下面的步骤做: - -1.在这里你需要把string转换成Double。这是一个隐式操作;然后Swift在未来会有更简单的方法。 - -注意:这是就是你想知道他是如何工作的。 - -写这篇文章的时候,Swift的string类没有在每个方法中使用,而NSString有(NSString是在Foundation框架中的字符串类)。特别的,Swift的string类没有一个转换成double的方法;而NSString有。 - -你可以调用bridgeToObjectiveC()方法把Swiftstring转换成NSString。然后你就可以调用NSString的部分方法了,例如转换成double的方法。 - -如果想学习更多关于NSString,这点击[NSString类参考](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/Reference/NSString.html)。 - -2.你在tipCalc模板中调用returnPossibleTips方法,它返回一个可能百分比映射的提示值的字典。 - -3.这是你在Swift如何通过键和值的字典枚举。非常方便,不是吗? - -4.在这里,你用字符串插值建立字符串文本提交结果。\n是换行符。 - -5.最后你把结果设置到你创建你字符串中。 - -就是这样!编译并运行,并享受自己编写的小费计算器! + + + +为了使Interface Builder知道你的新方法,你的方法必须加上@IBAction关键字(就像你用@IBOutlet关键字标识变量那样)。 + +接着,回到Main.storyboard并在视图控制器中选择文档大纲。确保连接检查器是打开的(第6个标签)和你在Received Actions中看到你的新方法。 + +在calculateTapped:的右边找到圆圈,并在圆圈内拖出一条线连接到Calculate的按钮。 + +在弹出的窗口选择Touch Up Inside: + +这实际上就是说:“当用户从屏幕按钮的上方释放手指时,就调用名为calculateTapped:的方法。 + +现在复习另外两个方法: + +*从taxPercentageChanged:拖动到你的slider,并把它连接到不同值的动作,这就是用户每次移动的slider。 + +*从viewTapped:拖动到文档大纲的Tap Gesture Recognize。没有选择手势识别器的动作;你的方法会被称为触发识别器。 +``` +注意:就像属性一样,这是使用Interface Builder的一个连接操作的快捷方法。 + +你可以按下Control键拖拽控件来辅助编辑你视图控制器的Swift代码的动作(如button)。在弹出的窗口,你要选择Action,给方法一个名字。 + +这只要一步就能将在你的Swift文件中创建一个方法和把动作连接到你的方法。同样,这两种个工作,它只是为了方便你! +``` +# 连接你的视图控制器和模板 + +你要做的差不多完成了-现在你要把你的视图控制器和模板连接起来。 + +打开ViewController.swift并为你的模板类添加一个属性和一个刷新UI的方法。 + +```Swift +let tipCalc = TipCalculatorModel(total: 33.25, taxPct: 0.06) + +func refreshUI() { + // 1 + totalTextField.text = String(tipCalc.total) + // 2 + taxPctSlider.value = Float(tipCalc.taxPct) * 100.0 + // 3 + taxPctLabel.text = "Tax Percentage (\(Int(taxPctSlider.value))%)" + // 4 + resultsTextView.text = "" +} +``` + +现在让我们来复习一次refreshUI: + + +1.在Swift中,转换类型的时候必须明确。现在你要把tipCalc.total从Double转换成String。 + +2.你想要税收百分比显示为整数(i.e. 0%-10%)而不是一个小数(好像0.06)。所以你要把结果乘以100. + +注意:计算是必须的,因为taxPctSlider.value变量是一个浮点型数据。 + +3.在这里,你在税收百分比的基础上使用字符串插值更新标签。 + +4.当用户点击计算按钮的时候你要清除文本视图的计算结果。 + +接着,在viewDidLoad:的底部添加调用refreshUI的方法 。 + +```Swift +refreshUI() +``` + +taxPercentagechanged和viewTapped的实现如下: + +```Swift +@IBAction func taxPercentageChanged(sender : AnyObject) { + tipCalc.taxPct = Double(taxPctSlider.value) / 100.0 + refreshUI() +} +@IBAction func viewTapped(sender : AnyObject) { + totalTextField.resignFirstResponder() +} +``` + +taxPercentageChanged简单地做了"乘以100"的相反操作,当视图监听里viewTapped在totalTextField调用了resignFirstResponder(有隐藏键盘的效果)。 + +实现了一个方法。现在实现calculateTapped如下: + +```Swift +@IBAction func calculateTapped(sender : AnyObject) { + // 1 + tipCalc.total = Double(totalTextField.text.bridgeToObjectiveC().doubleValue) + // 2 + let possibleTips = tipCalc.returnPossibleTips() + var results = "" + // 3 + for (tipPct, tipValue) in possibleTips { + // 4 + results += "\(tipPct)%: \(tipValue)\n" + } + // 5 + resultsTextView.text = results +} +``` + +让我们跟着下面的步骤做: + +1.在这里你需要把string转换成Double。这是一个隐式操作;然后Swift在未来会有更简单的方法。 + +注意:这是就是你想知道他是如何工作的。 + +写这篇文章的时候,Swift的string类没有在每个方法中使用,而NSString有(NSString是在Foundation框架中的字符串类)。特别的,Swift的string类没有一个转换成double的方法;而NSString有。 + +你可以调用bridgeToObjectiveC()方法把Swiftstring转换成NSString。然后你就可以调用NSString的部分方法了,例如转换成double的方法。 + +如果想学习更多关于NSString,这点击[NSString类参考](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/Reference/NSString.html)。 + +2.你在tipCalc模板中调用returnPossibleTips方法,它返回一个可能百分比映射的提示值的字典。 + +3.这是你在Swift如何通过键和值的字典枚举。非常方便,不是吗? + +4.在这里,你用字符串插值建立字符串文本提交结果。\n是换行符。 + +5.最后你把结果设置到你创建你字符串中。 + +就是这样!编译并运行,并享受自己编写的小费计算器! 注意:@BBK在论坛止问到,如果用百分比给结果排序。我认为这个问题很重要,所以在这里给出了答案。 @@ -430,7 +430,7 @@ for tipPct in keys { 我希望这对你有帮助!:] -#接着要到哪里 +# 接着要到哪里 这是这个Swift教程包含了所有代码的[最终Xcode项目](http://cdn4.raywenderlich.com/wp-content/uploads/2014/06/TipCalculatorFinished11.zip)。 想要学习更多内容?继续阅读[下一章节](http://www.raywenderlich.com/75289/swift-tutorial-part-3-tuples-protocols-delegates-table-views),你将会学习tuples,protocols和table views-或者看看我们的[Swift新书](http://www.raywenderlich.com/store/swift-tutorials-bundle) diff --git a/Swift-Tutorial-Part3-Tuples-Protocols-Delegates-and-Table-Views.md b/Swift-Tutorial-Part3-Tuples-Protocols-Delegates-and-Table-Views.md index 64d2ebc..6770301 100644 --- a/Swift-Tutorial-Part3-Tuples-Protocols-Delegates-and-Table-Views.md +++ b/Swift-Tutorial-Part3-Tuples-Protocols-Delegates-and-Table-Views.md @@ -1,4 +1,4 @@ -#Swift教程第三部分:元组,协议,委托和表格视图 +# Swift教程第三部分:元组,协议,委托和表格视图 **更新于2014年7月7日:**为Xcode6-beta 3而更新。 @@ -16,7 +16,7 @@ `注意:` 在写这篇教程的时候,我们的理解是我们不能张贴Xcode 6相关的截图,因为它还处于Beta阶段。因此,我们在确保不会引起相关问题之前,将不会提供截屏。 -##开始 +## 开始 到现在为止,我们的小费计算器为每个小费百分比提供了一个参考。然而,一旦你选择了你要付的小费,你必须在你脑中将小费加到账单总额——这有点儿打败了这个点! @@ -26,7 +26,7 @@ 现在我们开始耍耍元组感受一下它是如何运作的。在Xcode中创建一个Playground(或者如果你采纳我在[第一篇Swift教程](./Swift-Tutorial-A-Quick-Start.md)中的建议,就只要点击已经保存到dock上的Playground)。在你的Playground中删除所有东西以便我们从一个干净的板块下开始。 -##未命名元组 +## 未命名元组 让我们从创建一个叫做**未命名元组**的东西开始。在Playground中输入以下内容: @@ -53,7 +53,7 @@ theTotal 这种语法允许你用一个特殊的名字创建一个新的常量来代表元组中的每个元素。 -##命名元组 +## 命名元组 未命名元组可以使用,但是就像你看到的那样,它需要一些额外的代码来通过名字访问每一项。 @@ -75,7 +75,7 @@ let tipAndTotalNamed:(tipAmt:Double, total:Double) = (4.00, 25.19) 注意当你使用显式语法的时候,在右手边命名变量是可选的。 -##返回元组 +## 返回元组 现在我们了解了元组的基础知识,现在我们看看在小费计算器中如何使用它们来返回两个值。 @@ -97,7 +97,7 @@ calcTipWithTipPct(0.20) 这里是到目前为止的[playground文件](http://cdn1.raywenderlich.com/wp-content/uploads/2014/06/Prototype1.playground.zip)。现在清空playground来开始一个新的板块。 -##一个完整的原型 +## 一个完整的原型 这时候,你已经准备好掌握你所学到的知识并且把它整合到**TipCalculatorModel**类中。 @@ -157,7 +157,7 @@ tipCalc.returnPossibleTips() 这时候,保存这个文件并且重新启动一个新的空的playground。我们接下来将转到这个playground。 -##协议 +## 协议 下一步是为你的应用建立一个表格视图的原型。但是在做这件事之前,你需要理解**协议**和**委托**的概念。让我们从协议开始学习。 @@ -205,7 +205,7 @@ class Dog : Animal, Speaker { 在这个例子中,**Dog**继承自**Animal**,所以当你声明**Dog**类时候你在后面加了一个**:**,然后它继承了那个类,然后列出所有的协议。在Swift中你可以只继承一个类,但是你可以遵守任何数量的协议。 -##可选的协议 +## 可选的协议 你可以在协议中标记一个方法是否是可选的。通过以下代码替换**Speaker**协议来尝试一下: @@ -254,7 +254,7 @@ class Ray: Speaker { 问:寿司A对寿司B说了什么?答:芥末! 问:怎样的面向对象方法能让类变得更丰富?答:继承! -##使用协议 +## 使用协议 现在我们创建了一个协议和一些类并且实现了它们,让我们尝试使用它们。在你的playground中添加以下几行代码: @@ -286,7 +286,7 @@ speaker.TellJoke?() 可选链接是一个有用的技术,它帮助你在任何时候你想要在使用一个可选的值前验证它是否存在,作为我们之前讨论的**if let**(可选绑定)语法的替代。在余下的系列和网络上其它Swift教程中我们将更经常使用它。 -##委托 +## 委托 委托是一个简单的遵守协议的变量,是一个典型的用来通知事件或执行各种子任务的类。想象它像一个老板给他的下属状态更新,或者告诉他/她要做些什么事! @@ -390,7 +390,7 @@ class DateSimulator { 现在,保存这个文件并且回到在这个教程先前保存的包含了新的改善了的**TipCalculatorModel**类的playground文件中。现在是时候把它放到一个表格视图中了! -##表格视图,委托和数据源 +## 表格视图,委托和数据源 现在我们理解了协议和委托的概念,可以准备好在应用中使用表格视图。 @@ -501,7 +501,7 @@ tableView.reloadData() 你现在已经完成了为新的改善的**TipCalculatorModel**和新表格视图创建原型。是时候整合这些代码到你的主工程了! -##收尾 +## 收尾 打开你的**TipCalculator**工程,拷贝新的改善的**TipCalculatorModel**,它优于你已存在的实现。 @@ -614,7 +614,7 @@ class ViewController: UIKit.UIViewController, UITableViewDataSource { 编译并且运行,然后享受你的小费计算器的新的外观! -##接下来还有什么呢? +## 接下来还有什么呢? 这里是目前为止这个教程系列的[已经完成的Xcode工程](http://cdn1.raywenderlich.com/wp-content/uploads/2014/06/TipCalculatorFinished21.zip)。 diff --git a/UIKit Dynamics Tutorial in Swift.md b/UIKit Dynamics Tutorial in Swift.md index 52f334f..2795e3e 100644 --- a/UIKit Dynamics Tutorial in Swift.md +++ b/UIKit Dynamics Tutorial in Swift.md @@ -1,4 +1,4 @@ -###UIKit Dynamics(UIKit动力学)在Swift中的使用指南### +### UIKit Dynamics(UIKit动力学)在Swift中的使用指南 ### **更新提示:**该指南是Colin Eberhardt写的IOS 7教程中的一章的缩写版,是由James Frost更新到IOS 8和Swift中的。 @@ -17,7 +17,7 @@ IOS的设计目标是鼓励你去创造可以响应触摸、手势、定位等 注意:写这篇指南的时候,据我们了解[IOS 8上面还不能发布截图](http://www.raywenderlich.com/74138/swift-language-faq),因为还在测试阶段。所有的截图都来自IOS 7,但是应该和在IOS 8上面看起来效果是一样的。 -###开始### +### 开始 ### UIKit dynamics(UIKit动力学)是很有趣的;开始学习的最好方式是用一些跳动的小示例。 @@ -33,7 +33,7 @@ UIKit dynamics(UIKit动力学)是很有趣的;开始学习的最好方式 如果你在实际的设备上运行这个app,可以试着倾斜、倒置、或是晃动你的手机。发生了什么?什么都没发生?这就对了,所有的事情都在按事先设计好的方式运行。当你在界面上添加一个视图的时候,你就希望它能牢牢的呆在框架所规定的地方--直到你往你的界面中添加一些动态实现。 -###添加重力### +### 添加重力 ### 仍然打开**ViewController.swift**,在`viewDidLoad`前面添加下面的属性: @@ -64,7 +64,7 @@ UIKit dynamics(UIKit动力学)是很有趣的;开始学习的最好方式 你真的需要了解这些吗?不一定;你所需要直到的就是g的值越大,物体掉落的就越快,但是这并不难理解底层的数学内容。 -###设置边界### +### 设置边界 ### 尽管你看不到,但这个正方形从你屏幕的底部消失之后还是会继续掉。为了使它一直在你的屏幕范围内,你需要定义一个边界。 @@ -85,7 +85,7 @@ UIKit dynamics(UIKit动力学)是很有趣的;开始学习的最好方式 这真是一个相当漂亮的行为,尤其是当我们想到我们仅仅添加了少量的代码。 -###处理碰撞### +### 处理碰撞 ### 下一步,你将会添加一个会和正方形碰撞并相互作用的固定的障碍物。在`viewDidLoad`往视图中添加正方形的后面添加下面的代码: @@ -105,7 +105,7 @@ UIDynamicAnimator 和提供坐标系统的参考系有关联。然后你可以 你当前的代码中并没有和障碍物相关的行为,因此在与下面的动力学引擎关联之前,这个障碍物可以说是不存在的。 -###使对象响应碰撞### +### 使对象响应碰撞 ### 为了使正方形和障碍物发生碰撞,找到初始化碰撞行为的那行代码,然后用下面的代码替换: @@ -126,7 +126,7 @@ UIDynamicAnimator 和提供坐标系统的参考系有关联。然后你可以 看起来你好像需要另一种方法来出来这个问题。因为障碍物是不能移动的,那么这个动力学引擎就没有必要直到它的存在。但是应该怎么样检测碰撞呢? -###隐形的边界和碰撞### +### 隐形的边界和碰撞 ### 把碰撞行为的初始化部分改回原来的样子,这样它就仅仅知道正方形的存在了: @@ -144,7 +144,7 @@ UIDynamicAnimator 和提供坐标系统的参考系有关联。然后你可以 到目前位置,UIKit Dynamics(UIKit 动力学)的能力已经很清晰了:你可以用很少的代码来实现很多的功能。但是引擎的底部却有很多的内容,下面的部分将会给你展示你程序中的对象与动力学引擎发生相互作用的一些实现的细节 。 -###碰撞场景的幕后### +### 碰撞场景的幕后 ### 每一个动态行为都有一个动作属性,通过这个属性你可以提供动画执行的每一步的代码块。在`viewDidLoad`中添加下面的代码: @@ -181,7 +181,7 @@ UIDynamicAnimator 和提供坐标系统的参考系有关联。然后你可以 这个协议也意味着动力学引擎并不是和UIView紧紧相连的,实际上还有一个不是视图的UIKit类:`UICollectionViewLayoutAttributes`,但是也采用这个协议。这允许动力学引擎在集合视图内播放项目动画。 -###碰撞通知### +### 碰撞通知 ### 到目前为止,你已经添加了几个视图和行为,下面就让动力学引擎接管吧。接下来的一步,你将会看到当项目发生碰撞的时候怎么样去接收通知。 @@ -226,7 +226,7 @@ UIDynamicAnimator 和提供坐标系统的参考系有关联。然后你可以 到目前为止, UIKit Dynamics已经通过基于你的项目边界的计算来自动的设置你项目的物理属性,比如质量和弹性。下面你将会看到如何通过`UIDynamicItemBehavior`类来控制这些物理属性。 -###配置项目属性### +### 配置项目属性 ### 在`viewDidLoad`的最后面的部分添加下面的内容: @@ -267,7 +267,7 @@ UIDynamicAnimator 和提供坐标系统的参考系有关联。然后你可以
  • allowsRotation (是否允许旋转):这是一个特别有趣的属性,它并不适用于现实世界中的物理特性。把这个属性设为NO的话,不管怎样的旋转力作用于它,这个对象都不会发生旋转。
  • -###动态的添加行为### +### 动态的添加行为 ### 当前情况下,你的应用程序设置所有的系统行为,然后让动力学引擎处理系统的所有物理特性。下面的内容,你将会看到怎么样动态的添加和移除行为。 @@ -297,7 +297,7 @@ UIDynamicAnimator 和提供坐标系统的参考系有关联。然后你可以 两个正方形之间好像连在一起,但是因为屏幕上什么都没有画,所以你实际上并不能看到像一条线或者弹簧一样的连接。 -###用户交互### +### 用户交互 ### 就像你刚刚看到,在你的物理系统已经在运动的时候,你可以动态的添加和移除行为。在最后的这部分内容中,你将会在用户什么时候点击屏幕的时候添加另一种类型的动力学引擎的行为--`UISnapBehavior`。`UISnapBehavior` 会使对象像弹簧一样跳到指定的位置。 @@ -330,7 +330,7 @@ UIDynamicAnimator 和提供坐标系统的参考系有关联。然后你可以 点击生成并运行你的程序。试着点击周围,当你点击的时候,这个正方形应该迅速的移动的你触摸的地方。 -###何去何从### +### 何去何从 ### 现在你应该对UIKit Dynamics(UIKit动力学)有一个深刻的理解。你可以从这篇指南中下载DynamicsDemo的最终版,以方便日后的学习。 diff --git a/Unity-2d-Tutorial-Scrolling-Scenes-And-Sounds.md b/Unity-2d-Tutorial-Scrolling-Scenes-And-Sounds.md index 29b9e9d..846ff44 100644 --- a/Unity-2d-Tutorial-Scrolling-Scenes-And-Sounds.md +++ b/Unity-2d-Tutorial-Scrolling-Scenes-And-Sounds.md @@ -1,4 +1,4 @@ -#Unity 4.3 2D 教程: 滚动,场景和音效 +# Unity 4.3 2D 教程: 滚动,场景和音效 欢迎回到我们的 Unity 4.3 2D 系列教程。 @@ -19,7 +19,7 @@ 你已经拿到了Zombie Conga项目的一部分内容,所以现在你需要做的就是许多充满抱负的游戏开发者最头疼的事情:完成游戏! -##开始吧!## +## 开始吧! ## Zombie Conga 是一个横向卷轴(side-scrolling)游戏,但是到目前为止,你的僵尸仅仅是停留在海滩上的一小部分。现在是时候让他到其他的地方去走走了。 @@ -136,7 +136,7 @@ Zombie Conga 是一个横向卷轴(side-scrolling)游戏,但是到目前 在播放这个场景的时候,一个很突出的问题是这个沙滩上完全没有猫咪的踪影。我不知道你们的习惯是怎样的,反正我每次去砂染的时候,都会带上我的小猫咪。 -##产生猫咪 +## 产生猫咪 你会需要新的猫咪不停地出现在沙滩上直到玩家获胜或是输掉游戏。为了处理这个问题,你需要创建一个全新的脚本并添加一个空的GameObject。 @@ -185,7 +185,7 @@ Zombie Conga 是一个横向卷轴(side-scrolling)游戏,但是到目前 这样你的 Kitten Factory 就可以按照规划工作了,你需要让它产生(原文使用spit out)一些猫咪。为了达到这一目的,你需要使用Unity 最强大的特性之一:Prefabs。 -##Prefabs +## Prefabs Prefabs 现在在你的项目中,而不是在你场景的 Hierarchy 中。你可以将 Prefabs 用作在你的场景中创建对象的模板。 但是,这些实例并不仅仅是原始 Prefabs 的副本。取而代之的是,Prefab 定义了一个对象的默认值,接下来你可以自由地修改你的场景中的特定实例,而不会影响其他同样通过这个 Prefab 创建的对象。 @@ -303,7 +303,7 @@ Kitten Factory 现在在监视器(Inspector)中看来是这个样子的: OK,你现在已经得到了一片满是猫咪的沙滩,并且沙滩上还有一个到处闲逛寻找陪伴的僵尸。我觉得你应该已经知道我们做了什么以及接下来该做什么了。 -##Conga时刻! +## Conga时刻! 如果你从这个系列教程的第一部分开始就是忠实的读者,你或许已经开始怀疑为什么这个游戏被叫做Zombie Conga。 @@ -313,7 +313,7 @@ OK,你现在已经得到了一片满是猫咪的沙滩,并且沙滩上还有 当这个僵尸撞到一个猫咪,你将把这个猫咪加入到 conga 线中(译者注:就是猫咪会一直跟着僵尸运动)。但是,你会想要当僵尸撞到敌人时能有不同的效果。为了区别撞击到的是猫咪还是敌人,你需要为它们分配不同的标签(tags)。 -###用标签(tags)来分辨对象 +### 用标签(tags)来分辨对象 Unity 允许你向任何 GameObject 分配一个字符串,这个字符串被称作标签(tag)。新创建的项目包含了一些默认的标签(tag),就像主摄影机(Main Camera)和玩家(Player),但是你可以自由地添加任何你希望添加的标签(tag)。 @@ -374,7 +374,7 @@ Unity 允许你向任何 GameObject 分配一个字符串,这个字符串被 现在你知道你的碰撞测试已经被正确设置了。是时候让它们为你做点什么了! -##从脚本触发动画 +## 从脚本触发动画 还记得你在本系列的第二、三部分中制作的动画吗?猫咪在快乐地上下摆动,就像这样 @@ -417,7 +417,7 @@ Unity 允许你向任何 GameObject 分配一个字符串,这个字符串被 没有人希望看到一大堆猫咪零散地分布在沙滩上。你所希望的是它们加入到你僵尸坚持不懈的舞蹈当中,你需要教会这些猫咪怎么样区跟随它们的僵尸老大。 -###Conga 运动 +### Conga 运动 你需要一个 List 来追踪哪些猫咪已经加入到 conga 线了(译者注:就是指哪些猫咪已经在跟随着僵尸运动了)。 @@ -573,7 +573,7 @@ congaLine 会为 conga 线中的猫咪储存 Transform 对象。你正在储存 ![Alt text](http://cdn5.raywenderlich.com/wp-content/uploads/2015/04/better_conga_line.gif) -###修正 conga 动画 +### 修正 conga 动画 要使得猫咪看起来像是在跳着舞享受它们的僵尸生活,你需要稍微改变一下逻辑。每一只猫咪会在超过一个 CatConga 动画周期中选择一个点然后跳向那个点,而不是在每一帧计算目标位置。然后猫咪会选择另一点而后跳向那个点,依此类推。 @@ -668,7 +668,7 @@ congaLine 会为 conga 线中的猫咪储存 Transform 对象。你正在储存 啊哈哈哈哈,我是骗你的!这里有一个解释加一个解决方案! -###让动画和脚本完美地一起运行 +### 让动画和脚本完美地一起运行 为什么被设置了动画的 GameObject 不肯被创造它们的脚本改变呢?这是一个普遍的问题,所以花些时间找到一个好的解决方案是很有价值的。 @@ -872,7 +872,7 @@ congaLine 会为 conga 线中的猫咪储存 Transform 对象。你正在储存 现在僵尸可以往它的 conga 线上招募猫咪了,但是这些老太太却还是没有办法阻止这场让她不爽的不死狂欢。是时候给老太太们放手一搏的机会了! -##处理与敌人的接触 +## 处理与敌人的接触 在 Zombie Conga 中,玩家需要在撞到一定数目的敌人之前招募到足够数量的猫咪。或者说,这是你完成这个系列教程以后的目标。 @@ -1024,7 +1024,7 @@ congaLine 会为 conga 线中的猫咪储存 Transform 对象。你正在储存 好的,conga 线已经做好了,但是玩家该怎么赢,该怎么输呢?所以到现在为止这还不能算是一个游戏(的确是这样的,我觉得,如果无法判定玩家的输赢,这显然不是一个游戏)。是时候解决这个问题了。 -##赢和输 +## 赢和输 当 Zombie Conga 玩家建立了一条足够长的 conga 线的时候,他这把游戏就算赢了。你在 ZombieController.cs 中维护 conga 线,所以现在在 MonoDevelop 中打开这个文件吧。 @@ -1076,7 +1076,7 @@ congaLine 会为 conga 线中的猫咪储存 Transform 对象。你正在储存 就是这样!就算这个 Zombie Conga 看起来有些粗糙,但是它已经可以正常运行了!接下来你会做一些收尾的工作,包括额外的场景(输、赢的场景等),一些背景音乐和一些音效。 -##额外的场景 +## 额外的场景 为了完成这个游戏,你还需要在 Zombie Conga 中添加如下三个场景: @@ -1246,7 +1246,7 @@ congaLine 会为 conga 线中的猫咪储存 Transform 对象。你正在储存 现在你的场景都设置好了,是时候给弄点音乐了。 -##音频 +## 音频 找到你刚才下载的资源文件中的名叫 Audio 的文件夹。这个文件夹包含了 Vinnie Prabhu 为我们的书籍 —— [iOS Games by Tutorials](http://www.raywenderlich.com/store/ios-games-by-tutorials) 所制作的音乐和音效。 @@ -1361,7 +1361,7 @@ Audio Source 的默认设置是好的。你不用在这上面设置一个音频 ![Alt text](http://cdn5.raywenderlich.com/wp-content/uploads/2015/04/ZombieConga-YouWin.png) -##接下来我该何去何从? +## 接下来我该何去何从? 如果你坚持看完了整个系列攻略,恭喜你!你已经在 Unity 中制作了一个属于自己的游戏,并且通过这个方式你学到了很多 Unity 的全新的 2D 特性。 diff --git a/swift-how-to-make-custom-keyboard-ios-8-using-swift.md b/swift-how-to-make-custom-keyboard-ios-8-using-swift.md index 3c1c620..5b1e1eb 100644 --- a/swift-how-to-make-custom-keyboard-ios-8-using-swift.md +++ b/swift-how-to-make-custom-keyboard-ios-8-using-swift.md @@ -1,10 +1,10 @@ -#如何在iOS 8下使用Swift设计一个自定义的输入法 +# 如何在iOS 8下使用Swift设计一个自定义的输入法 by Andrei Puni 我会复习一下有关键盘扩展的内容,然后通过使用iOS 8中的新应用扩展API的设计一个摩斯码的输入法。完成这个教程大约需要花费20分钟。[完整代码](https://github.com/WeHeartSwift/MorseCode) -##概览 +## 概览 通过使用自定义输入法替换系统输入法,用户可以实现一些特别的功能。例如一个特别新颖的输入方式,或输入iOS原生并不支持的语言。自定义输入法的基本功能很简单:通过点击、手势,或者其他输入事件,然后通过一个未分类的 `NSString` 对象在当前文本输入对象的文本插入点插入文字。 @@ -23,7 +23,7 @@ by Andrei Puni 同样,自定义输入法不能在顶行之外显示任何内容(就像系统键盘当你在后面的行上长按一个键时)。 -##沙盒 +## 沙盒 默认情况下,自定义输入法并没有网络访问,也不能和容纳它的应用共享文件。如果想实现这些功能,必须在`Info.plist`文件中将`RequestOpenAccess`布尔值至`YES`。做了这个之后,会扩展自定义输入法的沙盒,就像在[建立和维护用户信任](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/Keyboard.html#//apple_ref/doc/uid/TP40014214-CH16-SW3)提到的那样。 diff --git a/swift-table-view-animations-tutorial-drop-cards.md b/swift-table-view-animations-tutorial-drop-cards.md index 9450aea..6313bb3 100644 --- a/swift-table-view-animations-tutorial-drop-cards.md +++ b/swift-table-view-animations-tutorial-drop-cards.md @@ -10,7 +10,7 @@ >在出版时,因为iOS 8还处在beta阶段,我们考虑到我们不能发iOS 8的截图。所以下面的所有截图都出自于iOS 7,这将接近于你在iOS 8中看到的。 -##开始 +## 开始 下载[开始项目](http://cdn4.raywenderlich.com/wp-content/uploads/2014/08/CardTilt-swift-starter.zip)然后在Xcode 6中打开它。你将找到一个简单的storyboard项目,包含**UITableViewController**的子类(**MainViewController**)和一个显示团队成员的自定义**UITableViewCell** (**CardTableViewCell**)。你也将看到一个叫做**Member**的model类,包含了团队成员的所有信息,并且它知道如何从存储在本地的JSON文件中获取信息。 生成并在模拟器中运行项目;你将看到如下所示: @@ -21,7 +21,7 @@ 该应用程序是一个好的开始,但它还可以做得更好。那将是你需要做的;你将使用一些Core Animation技巧来给你的卡片加动画。 -##定义一个最简单的动画 +## 定义一个最简单的动画 你将由创建一个超级简单的淡入动画辅助类来开始接触基本的应用程序结构。转到**File\New\File…**选择类型**iOS\Source\Swift File**来创建一个空的Swift文件。点击**Next**,把文件命名为**TipInCellAnimator.swift**,然后点击**Create**。 把文件内容替换如下: @@ -50,7 +50,7 @@ 现在你已经有了动画的代码,你需要表格视图控制器在卡片出现时触发这个新的动画。 -##触发动画 +## 触发动画 要触发你的动画,打开**MainViewController.swift**然后添加如下方法到类中: override func tableView(tableView: UITableView!, willDisplayCell cell: UITableViewCell!, @@ -64,7 +64,7 @@ ![SwiftDrop-InFadeAnimation](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/SwiftDrop-InFadeAnimation-281x500.png) -##酷炫的旋转 +## 酷炫的旋转 现在是时候让应用程序加入一些旋转动画来显得更酷炫了。这部分和淡入动画使用的是同种方式,除非你指定了开始和结束的变换。 打开**TipInCellAnimator.swift**,替换它的内容如下: @@ -120,7 +120,7 @@ 生成并运行你的应用程序。查看卡片出现时是如何倾斜的进入视图的! ![Rotation of card cell.](http://cdn4.raywenderlich.com/wp-content/uploads/2013/10/TransformationMontage.png) -##Swift重构 +## Swift重构 本教程的Objective-C原版中只在开始处计算了变换一次。在上述的代码版本中,它每次调用**animate()**计算一次。如何在Swift中像原版那样做? 一种方法是通过调用闭包计算一个不变的存储属性。替换**TipInCellAnimator.swift**的内容如下: @@ -159,7 +159,7 @@ >**注:**如果把**TipInCellAnimatorStartTransform**设为一个TipInCellAnimator的类属性就更好了。但在撰写本文时,Swift的类属性尚未实现。 -##给你的变换添加一些界限 +## 给你的变换添加一些界限 虽然动画效果是整洁的,但你也要有节制地使用它。如果你曾经经历了有过度的声音效果和动画效果的演示文稿,那么你就应该知道效果过度的感觉! 在你的项目中,你只想要动画在卡片出现时运行一次 — 当它从底部滚动进入屏幕时。当你向顶部滚动时,卡片应该滚动而没有动画。 @@ -190,7 +190,7 @@ ![Drop-In-UpDownScroll](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/Drop-In-UpDownScroll-563x500.png) -##下一步? +## 下一步? 在本教程中,你给标准的视图控制器添加了动画。实现细节远离了**MainViewController**类,放进了一个小的,集中的,动画辅助类中。保持类的职责集中,特别是视图控制器,是iOS开发中一个主要的挑战。 你可以从[这里](http://cdn3.raywenderlich.com/wp-content/uploads/2014/08/CardTilt-Swift-Final.zip)下载本教程的最终项目。 diff --git a/swift-tutorial-a-quick-start.md b/swift-tutorial-a-quick-start.md index 3dc9725..22ccfa4 100644 --- a/swift-tutorial-a-quick-start.md +++ b/swift-tutorial-a-quick-start.md @@ -1,4 +1,4 @@ -#Swift教程:快速指南 +# Swift教程:快速指南 Swift是苹果今年在WWDC发布的一门新语言。 @@ -15,7 +15,7 @@ Swift是苹果今年在WWDC发布的一门新语言。 注意: 在撰写这篇指南以前,我们的理解是我们不能张贴Xcode相关的截图,因为它还处于Beta阶段。因此,我们在确保不会引起相关问题之前,将不会提供截屏。 -##有关Playgrounds的介绍 +## 有关Playgrounds的介绍 启动**Xcode6**,然后进入**File\New\Fil**e。选择**iOS\Source\Playground**,然后点击下一步。 @@ -37,7 +37,7 @@ Playgrounds是了解Swfit的一种很好的方式(就像你在这份Swift的 注意:在这个阶段,我同样推荐,你把你的playground文件(SwiftTurorial.playground)拖到你的OS X Dock上。 这样,你就可以在任何你想要尝试一些Swift代码的时候,随时把这个文件当做一个快速的实验场地。 -##Swift中的变量和常量 +## Swift中的变量和常量 尝试把下面这行加到你playground的最后一行: `totalTeam +=1` @@ -54,7 +54,7 @@ Playgrounds是了解Swfit的一种很好的方式(就像你在这份Swift的 嗯,最佳实践是在所有适用的地方,都用let去声明,因为这样就可以让编译器更好的去做一些优化。所以记住:尽可能适用let。 -##显式与隐式类型 +## 显式与隐式类型 目前为止,因为编译器有足够的信息来推测出数据的类型,你可能还没有显式的设置任何常量或者变量的类型。 @@ -70,7 +70,7 @@ Playgrounds是了解Swfit的一种很好的方式(就像你在这份Swift的 `let tutorialTeam = 56` -##Swift中的基本数据类型和流控制 +## Swift中的基本数据类型和流控制 到目前为止,你已经看了一个Int类型的例子,在Swift中,这是用来存储整型的值,但是还有很多其他类型。 **浮点数和双精度浮点数** @@ -111,7 +111,7 @@ if语句和字符串生成 这里是目前为止这份教程中涉及到的[playground文件](http://cdn3.raywenderlich.com/wp-content/uploads/2014/06/SwiftTutorial-Demo1.playground.zip) -##类和方法 +## 类和方法 一个你在Swift开发过程中最常做的,就是创建类以及他的方法,所以我们现在就开始吧。 @@ -234,7 +234,7 @@ if语句和字符串生成 你可以通过检查Assistant Editor来确认一下结果。 -##数组和For循环 +## 数组和For循环 当前,上面的代码存在一些重复,因为你调用calcTipWithTotal了很多次,以计算不同的小费比例。你可以在这里通过使用数组来降低重复。 @@ -264,7 +264,7 @@ if语句和字符串生成 数组有一个count属性,用来保存数组内的成员个数。同样你也可以通过arrayName[index]的格式来访问数组成员的内容。 -##词典 +## 词典 我们来对小费计算器做最后一点改进。不用只是打印出小费数字,你可以返回一个包含结果的词典。这会使得在应用界面中可以很方便的显示这一类数据结果。 @@ -303,7 +303,7 @@ if语句和字符串生成 就这些---恭喜你!你已经有一个Swift代码构建的小费计算器。 -##接下来还有什么? +## 接下来还有什么? 这里是[完整的playground文件](http://cdn3.raywenderlich.com/wp-content/uploads/2014/06/SwiftTutorial-Demo2.playground2.zip)包括所有教程中提到的Swift代码。 diff --git a/unity-2d-tutorial-animations.md b/unity-2d-tutorial-animations.md index d2e400c..bfe9e0d 100644 --- a/unity-2d-tutorial-animations.md +++ b/unity-2d-tutorial-animations.md @@ -1,4 +1,4 @@ -#Unity 4.3 2D 教程: 动画 +# Unity 4.3 2D 教程: 动画 欢迎回到我们的Unity 4.3 2D系列教程! 在[系列的第一部分](http://www.raywenderlich.com/61532/unity-2d-tutorial-getting-started)中,我们已经开始制作一个叫做Zombie Conga的好玩的的游戏。你已经学会如何添加精灵,如何使用精灵表,配置游戏视图以及用脚本移动精灵和做精灵动画。 @@ -7,7 +7,7 @@ 当你完成时,你将会很好的理解Unity的强大的动画系统,Zombie Conga也将得到改进! -##开始 +## 开始 首先,我们需要下载这个[启动项目](http://cdn5.raywenderlich.com/wp-content/uploads/2015/02/ZombieConga-Animations-Part1-Start.zip),它包含了本系列教程第一部分的所有东西,[Unity 4.3 2D 教程: 开始](http://www.raywenderlich.com/61532/unity-2d-tutorial-getting-started). 如果你愿意,你也可以继续使用你的旧项目,但是使用这个启动项目会更好的让你跟着本教程。 解压这个文件然后双击打开**ZombieConga/Assets/Scenes/CongaScene.unity**: @@ -22,7 +22,7 @@ >**注**: 如果你的文件夹显示为图标而不是小的文件夹,你可以拖动窗口右下角的滑块改变你看到的视图。为了方便,我将在本教程中的图标视图和压缩视图之间来回切换。 -###方法提示: 想学习怎样自己创建文件夹? +### 方法提示: 想学习怎样自己创建文件夹? 在Unity工程里的文件夹只不过是你计算机磁盘上的目录。 @@ -48,7 +48,7 @@ - **僵尸会走出屏幕吗?** 很烦,但的确是。我们会在系列的后面部分修复这一问题。 - **猫坐在沙滩上,甚至比僵尸都更像死了吗?** 完全无法接受啊! 是时候给猫加点动画了。 -##精灵动画 +## 精灵动画 在系列的第一部分,你通过脚本**ZombieAnimator.cs**使僵尸循环走动。这是为了演示如何在你的脚本访问*SpriteRenderer*,它有时是很有用的。然而,现在你需要使用Unity内置的动画支持系统来代替之前的脚本。 @@ -90,7 +90,7 @@ >术语“usually”是早期使用的,因为你如果在Project浏览器中选择了某种资源,例如Prefab,Animation视图控制器会禁用它们而不是让你继续操作你的动画。 -###介绍动画视图 +### 介绍动画视图 在创建动画之前,我们需要理解下面的三个术语: - **动画剪辑**: 定义了一个特定的动画的片断的Unity资源。动画剪辑能定义简单的动画,比如一个闪烁的灯光,或复杂的动画,比如一个九头蛇怪的攻击动画。之后你会学到如何合并动画剪辑来制作更复杂的动画。 @@ -241,7 +241,7 @@ Samples字段定义了一个动画剪辑的帧的速率,它默认值是60FPS 好了,你已经在Unity中的动画部分学习了很多,但是仍然有很多剩余的需要去学。幸运的是,你已经有一个完美的试验猫坐在那里了。 -##动画的其他属性 +## 动画的其他属性 Unity可以给除了精灵以外的东西添加动画。具体来说,Unity可以让下面的任意类型都动起来: - Bool @@ -283,7 +283,7 @@ Unity可以给除了精灵以外的东西添加动画。具体来说,Unity可 你将会在没有其他的任何的美工内容的情况下制作这些动画。另外,你将会设置猫的缩放,旋转和颜色的属性。 -###创建猫的动画剪辑 +### 创建猫的动画剪辑 在**Hierarchy**中选择**cat**。记住,Unity根据最近选择动画剪辑决定了新的动画剪辑的位置。 在**Animation**视图中,在控制栏的下拉菜单中选择**[Create New Clip]**,如下图所示: @@ -304,7 +304,7 @@ Unity可以给除了精灵以外的东西添加动画。具体来说,Unity可 >当然,如果你在读这段注释之前,因为愚蠢的原因创建了剪辑,像是因为这些说明在这个注释之前,那我们之中的一个人都在操作的顺序上犯了一个严重的错误,不是吗? -###自动添加曲线 +### 自动添加曲线 本教程会展示设置动画剪辑的不同的技术。对于僵尸的循环走动,你拖动精灵到Animation视图,然后Unity自动创建了所有需要的组件。这次,你将开始编辑一个空的剪辑,然后让Unity为你添加曲线。 在**Hierarchy**中选择**cat**,然后在**Animation**视图的剪辑下拉菜单中选择 **CatSpawn**,如下图所示: @@ -385,7 +385,7 @@ Animation视图的控制栏包含一个当前帧字段,表明了scrubber在的 你应该也想让它们引起僵尸的注意吧,没有什么比一个扭动的猫更能引起僵尸的注意,来让猫扭动起来吧。 -###手动添加曲线 +### 手动添加曲线 在录制时让Unity基本你的修改为你添加必要的曲线是很有用的,但有时候你会想在Animation视图明确的添加一个曲线。 在**Hierarchy**中选择**cat**,然后在**Animation**视图中的剪辑下拉菜单中 选择 **CatWiggle**。 @@ -434,7 +434,7 @@ Animation视图的控制栏包含一个当前帧字段,表明了scrubber在的 鉴于刚刚的练习,你对CatWiggle添加**Scale**曲线应该没问题,现在就做吧。 -####方法提示:还记得在怎样添加曲线吗? +#### 方法提示:还记得在怎样添加曲线吗? 你可以用下面两种方法添加一个Scale曲线: 1. 在**Animation**视图中点击**Add Curve**,展开出现菜单中的**Transform**,然后点击**Scale**右边的**+**按钮。 @@ -447,7 +447,7 @@ Animation视图的控制栏包含一个当前帧字段,表明了scrubber在的 你已经学会了两种不同的设置值的方法,所以自己动手试试吧。 -####方法提示:需要复习一下设置关键帧? +#### 方法提示:需要复习一下设置关键帧? 你可以用下列两种已经学过的方法设置这些值: 1. 在**Animation**视图中修改曲线列表中的字段的值。 @@ -470,10 +470,10 @@ Animation视图的控制栏包含一个当前帧字段,表明了scrubber在的 问题是不止一个动画剪辑和这只猫有关,所以你要确保那只猫在恰当的时间做相应的动作。为此,你就要配置这只猫的动画控制器,这些内容你将在本教程的下一部分学习怎么去做! -##附赠:动画视图的补充内容 +## 附赠:动画视图的补充内容 以下是一些关于Animation视图的,可能对你有所帮助的补充内容。 -###曲线模式 +### 曲线模式 虽然本教程没有覆盖Animation视图的曲线模式,你可能发现在某些时候需要用它来调整你的动画。 在曲线模式中,时间轴显示了选中的曲线。下图展示了在曲线模式中同时选择了**Rotation.z**和**Scale.x**曲线: @@ -492,12 +492,12 @@ Animation视图的控制栏包含一个当前帧字段,表明了scrubber在的 直接对曲线进行调整,很复杂也很容易出错;当然这也不够精确。但是如果你对一个动画的时间控制不满意时,你可以试一试这些选项,可能会对你有帮助。 -###预览模式编辑 +### 预览模式编辑 你可能已经知道如果你在编辑器中对运行画面中的游戏对象做出改变时,当你停止场景时,这些修改会丢失。但是,在你使用Animation视图的Play按钮预览动画剪辑,就不会出现这样的情况了。 也就是说,你可以在预览模式中循环播放时调整剪辑的值,直到你觉得满意。这个功能可以在曲线模式调整曲线或移动关键帧来调整剪辑的时间的时候派上用场。 -##接下来学习什么? +## 接下来学习什么? 在这部分的教程中你学到了怎么使用Unity的Animation视图去为你的2D游戏创建动画剪辑。你可以在[这里](http://cdn4.raywenderlich.com/wp-content/uploads/2015/02/ZombieConga-Animations-Part1-Complete.zip)找到一份项目的备份。 虽然你可以通过所学的这些知识开始制作动画,但是,它还涉及很多细节问题。在本教程的[下一部分](http://www.raywenderlich.com/66523/unity-2d-tutorial-animation-controllers)你将会得到更多的练习制作动画剪辑,并学习使用动画控制器过渡不同的剪辑。在本系列教程的[最终部分](http://www.raywenderlich.com/70344/unity-2d-tutorial-physics-and-screen-sizes) 中,你将完成Zombie Conga和学习一些Unity的2D物理引擎和脚本的知识。