iOS Apprentice中文版-从0开始学iOS开发-第七课

完善这个游戏

你已经有了一个基本能玩的游戏app了。游戏的规则都执行的不错,也没有什么逻辑上的重大缺陷。我能告诉你的就是,眼下没有BUG。但是这里仍旧有许多我们要改进的地方。

显然,目前的游戏界面看起来既不3D也不华丽,我们后面会给它整容一下。但是眼下,我们有其他一些地方需要微调一下。

我们就从如何表现玩家的得分情况开始吧。

如果玩家将滑条正好放到了目标值的位置,让提醒窗口显示“Perfect”,如果非常接近目标值,就显示“You almost had it”,如果偏离的比较远则显示“Not even close”,这可以给玩家得分一个比较良好的反馈。

练习:想想实现方法。这些判断逻辑应该放在什么地方,并且你应该如何编程实现它?线索:我们在刚才好像用到了很多这样的词,“如果”。

放置这些判断逻辑的正确位置是showAlert(),因为你在这里创建了UIAlertController的对象,用于给玩家显示一个提示窗口。你已经对message的文本做了一些处理,现在你要用类似的方法来处理title的文本。

这里是改进后的方法的代码:

@IBAction func showAlert() {
        let difference = abs(targetValue - currentValue)
        let points = 100 - difference
        score += points
        
       //添加下面这一段
        let title: String
        if difference == 0 {
            title = "Perfect"
        } else if difference < 5 {
            title = "You almost had it!"
        } else if difference < 10 {
            title = "Pretty good!"
        } else {
            title = "Not even close..."
        }
        
        let message = "Your scored \(points) points"
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)  //这里改动一下
        let action = UIAlertAction(title: "OK", style: .default, handler: nil)
        alert.addAction(action)
        present(alert,animated: true,completion: nil)
        startNewRound()
        updateLabels()
    }

你创建了一个名为title的String型局部变量,用来存放在提醒窗口顶部显示的文本。最初,变量title没有任何值。

你使用滑条和目标值的差值difference来判断应该显示哪一条文本:

如果difference等于0,说明玩家非常牛X,于是你将“perfect”放入title。

如果difference小于5,你将“You almost had it”放入title。

如果difference小于10则显示“Pretty good”

如果difference大于等于10,则认为玩家表现不佳,显示“Not even close”

你能理解这段代码的逻辑了吗?它仅仅是一堆if语句用于判断difference变量的值,并且选一条对应的文本显示。

你用title变量的文本替换了你在创建这个UIAlertController对象时,使用的一条固定文本(Hello World)。

运行app,然后玩几局。你将看到title文本根据你的得分情况在不断变化。这就是if语句的作用。

新的title文本

练习:当玩家得到一个perfect时,给玩家额外的加100分作为奖励。并且在非常接近100分的时候,比如98,97分时也给予一定的奖励(说不定在奖励之下,有人会给你充值哦_)。

现在对玩家挑战高难度低分时,我们有相应的激励机制了,一个perfect不仅仅是100分而是200分。并且在非常接近100时,我们也给一个50分的奖励。

这里我如何实现这一目的的代码(注意看注释部分):

@IBAction func showAlert() {
        let difference = abs(targetValue - currentValue)
        var points = 100 - difference //将let改为var,points从常量改变为变量
        let title: String
        if difference == 0 {
            title = "Perfect"
            points += 100   //添加这一行
        } else if difference < 5 {
            title = "You almost had it!"
            if difference == 1 {
                points += 50
            }   //添加这个if语句
        } else if difference < 10 {
            title = "Pretty good!"
        } else {
            title = "Not even close..."
        }
        
        score += points  //这一行原来在上面,把它移到下面
...
}

你应该注意以下几个事情:

在第一个if后面的花括号内,你看到了一行新的语句。当difference等于0时,你不仅使title显示为“Perfect”,而且额外的给points加了100分。

第二个if也改了。它的内部出现了一个新的if语句。这样做并没有问题。你想要单独处理difference等于1的情况,当等于1时,额外的加50分上去,这就是这个新的if的作用。

毕竟,当difference大于0小于5时,它当然可以为1,但并不总是1。因此你用了一个额外的if语句来检查defference是否为1,如果是,则加50分。

因为这些新的语句添加了新的分数,所以points不能再是常量了,它现在必须是一个变量。这就是为什么我们把points前面的关键字由let更改为var。

最后,score += points这一行必须移动到所有if语句的后面。这是必须的,因为app也许会在这些if语句的内部修改points的值,并且这些额外的得分也需要加到总分score中去。

如果你自己写的版本和我的略有不同,也没什么关系,只要它能够提供同样的功能并且正常工作。在写程序的过程中,处理一个问题经常会有多种方法,只要它们的执行结果一致就没问题。

运行app,并且看看刚才的改动都生效没有。

额外的得分

回顾一下局部变量(Local variables)

我已经多次指出局部变量与实例变量的区别。作为你此刻应该知道的内容是,一个局部变量仅仅在它所属的方法被调期间才存在,而实力变量则在它所属的对象的视图控制器(view controler)存在期间一直存在。局部常量和实例常量也是如此。

在showAlert()内部,有六个局部的量(常量和变量)和三个实例的量(常量和变量):

let difference = abs(targetValue - currentValue)
var points = 100 - difference
let title = . . .
score += points
let message = . . .
let alert = . . .
let action = . . .

练习:指出哪些是局部的,哪些是实例的,哪些是变量,哪些又是常量?

答案:局部的非常好辨认,因为它们的名字前都有let或者var,说明它们是在方法内部被定义的。(不要误会我的意思,并不是说有let和var就是局部变量,let和var是定义常量和变量的关键字,有let或者var开头,说明它们在方法内部刚刚被定义,所以是局部的,我们一开头也说过,变量或常量的作用范围纯粹看它们被定义在哪里)。

let difference = . . .
var points = . . .
let title = . . .
let message = . . .
let alert = . . .
let action = . . .

这些符号(let和var)用于创建新的变量(var)或者常量(let)。因为它们在方法的内部被创建,所以它们是局部的。

这六个项目——difference, points, title, message, alert, 以及action被限制在showAlert()内部,并且在它之外并不存在。一旦showAlert()方法执行完毕,它们就被释放了。

例如:每次玩家点击Hit Me按钮后,difference都会得到一个不同的值,即使它是常量。我们前面不是说过常量的值是不可以改变的吗?

原因是这样的:每次showAlert()方法被调用的时候,这些局部的常量和变量都会被重新创建。旧的哪些早都被扔掉不要了。

具体就是当showAlert()被调用时,它会创建一个全新的difference变量,之前一次的difference已经不存在了,被释放了。而这次新创建的这个difference在showAlert()运行结束后,也被扔掉了,下一次showAlert()运行时又创建了一个全新的。所以difference是个常量,但是它的值每次都不同,因为每次你看到的都是一个全新的difference(细思极恐系列_)。

但是在showAlert()一次运行期间,difference的值,是不能发生变化的。唯一可以改变的就是points,因为它是变量(var)。

再来看实例变量,它们被定义在任何一个方法的外面。通常都把它们放在一个文件的开头,像下面这样:

class ViewController: UIViewController {
  var currentValue = 0
  var targetValue = 0
  var score = 0
  var round = 0

你可以在任何方法内部调用这些变量或者常量,不需要重新定义一次,并且它们会长期存在。

如果你像这样做:

@IBAction func showAlert() {
  let difference = abs(targetValue - currentValue)
  var points = 100 - difference
  var score = score + points       // doesn’t work!
... }

这样不会得到我们想要的结果。因为你在score前面放了一个var,这样它就是属于showAlert() 内部的一个变量了,它不会影响外面那个score的值,并且showAlert() 运行结束后,它就消失了,这样玩家的score永远不会被显示。

很明显这不是你想要的结果,幸运的是,刚才那段代码甚至不会被编译,因为Xcode知道这样做是可疑的。

⚠️为了让这两种类型的变量有所区别,以便于一看就知道它们能活多久,有些程序员会在实例变量的前面加一个下划线。
它们会将score命名为_score。这样可以减少一些麻烦,因为在变量名字前加一个下划线就不会和局部变量弄混了。这只是个人的一些习惯,Swift才不在乎你怎么给变量取名。
还有一些程序员喜欢在前面加个m(代表member)或者加个f(代表field),有些甚至在变量名称后面加个下划线。这些方法是愚蠢的,不要去学。如果你不能心知肚明的清楚每个变量的作用范围,这些前缀或者后缀只能把你带向更深的深渊。

等待提醒窗口离开

在这个游戏中,仍然有些事情困扰着我。也许你已经注意到了。。。

当你一点击Hit Me按钮,提醒窗口就弹出来了,并且与此同时滑条立即恢复到了中间位置,回合数值立即显示为加1,并且目标值也立即被一个新的随机数替换掉了。

就是说你还木有机会观察上一回合结果的时候,新一回合的数据立马被更新到屏幕上了,这让人觉得有点怪怪的。

你也许想知道为什么会这样,毕竟在showAlert()中你是在显示提醒窗口之后才调用的startNewRound()。

@IBAction func showAlert() {
        . . .
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .default, handler: nil)
        alert.addAction(action)
        present(alert,animated: true,completion: nil)
        startNewRound()
        updateLabels()
    }

和你期望的相反,present(alert. . .)并没有暂停其他方法的执行,并且等到提醒窗口消失后才执行它们。其他一些平台上的alert是被设计为如此工作的,但是在iOS上不是。

取而代之的是present(alert. . .)将提醒窗口放到屏幕上的同时立即返回结果,然后showAlert()中的剩余方法立即被执行,新的一回合甚至在提醒窗口的弹出动画尚未结束时,就被更新到屏幕上了。

用程序术语讲就是,alert(提醒窗口)是异步工作的。更多关于异步和同步的内容我们在下一个课程中讲,现在对你而言这件事,就是意味着,在alert执行结束前你不知道其中的进展情况。你只能赌showAlert()运行结束后一切正常。

所以如果你无法在弹出窗口消失前在showAlert()内部等待,那么你如何等待它的关闭呢?

答案是简单的:事件!和你之前看到的一样,大多数iOS程序都涉及等待特殊的事件发生——按钮被点击,滑条被拖动等等。这里没有什么不同,你只需要以某种事件等待alert的结束。在这段时间内,你什么都不做。

这是它工作的原理:
对于alert来说,每一次点击Hit Me按钮,你都必须提供一个UIAlertController的对象。这个对象告诉alert,按钮(这个按钮是指提弹出的提醒窗口上的那个按钮)上的文本是“OK”,以及这个按钮是什么样式的(这里我们使用的是默认样式):

let action = UIAlertAction(title: "OK", style: .default, handler: nil)

这里的第三个参数,handle,告诉alert当OK按钮被点击后应该发生什么。这就是你寻找的等待alert消失的事件(点击OK按钮后,alert就被解除了,并且会触发一个事件)。

目前handle是nil,就是说什么都不做。为了做出我们需要的更改,你需要给UIAlertAction一些代码执行,当OK按钮被点击后。当玩家最终点击OK按钮后,alert将自己从屏幕上移除并且会跳转到你的代码上。你可以在这个地方打打注意。

这种模式也被称作‘回调’,在iOS上这种模式会在多种用途中出现。之前你经常被要求创建一个新的方法用于处理事件,但是现在,你需要一点新的东西——闭包(closure)。

将showAlert()的底部稍微改动一下:

 @IBAction func showAlert() {
        . . .
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .default, handler: {
            action in
            self.startNewRound()
            self.updateLabels()
        })
        alert.addAction(action)
        present(alert,animated: true,completion: nil)
    }

这里改变了两个地方:

1、你移除了方法中底部的startNewRound()和updateLabels(),千万别漏掉这一点。

2、你将这两个方法塞到UIAlertAction的参数handle的代码块中去了。

这样的代码块就叫做闭包(closure)。你可以把它想象做一个没有名称的方法。这些代码不会被立即执行,之后点击OK按钮后才会执行。这个闭包在alert被解除后才告诉app开始新的一回合以及更新标签的内容。

运行app并且观察一下效果。我想你的游戏效果应该比刚才好些了。

⚠️:self
你也许想知道为什么在handle的代码块里你用self.startNewRound()取代了startNewRound()。
self关键字允许view controller指向自己。这个概念对你而言不应该太陌生。当你说:“我需要一个冰淇淋”时,你用‘我’这个词指代了自己。类似的,程序中的对象也可以用一个代称来讨论它们自己。
通常你不需要这个self向view controller传递消息。这里是个例外:在闭包中,你必须使用关键字self来指向view controller。
这是Swift的语法规则。如果你在闭包中忘记了self,Xcode会创建app失败(亲自试试)。之所以存在这个规则是因为闭包可以‘捕获’变量,这会带来一些意外的结果,基本上不是好的结果。你会在另外的课程中学习这些内容,在本课程中,我们就讨论到这里。

重新开始

我不是指删掉你之前的所用东西,如果你已经这么做了,那么恭喜你多了一次复习的机会。我说的是这个游戏app中的“Start Over”按钮。这个按钮用于将你的得分和回合数重制为默认值。

Start Over按钮的作用之一是用来和其他玩家一较高下。比如第一个玩家玩十个回合,然后重置分数让第二个玩家玩十回合,看看谁的得分更高。

练习:试着自己完成Start Over按钮的功能。你已经见识过了按下一个按钮后,view controller是如何进行响应的,并且你应该可以实现如何改变score和round变量的值。

你会如何做呢?如果你卡住了的话,跟着我的讲解往下做。

首先,在ViewController.swift中添加一个新的方法,用于开始一次新的游戏。我建议你将这个方法放在startNewRound()的附近,因为这两个方法的概念差不多。

添加新的方法:

func startNewGame() {
        score = 0
        round = 0
        startNewRound()
    }

这个方法重置了score和round的值,并且同时开始新的一个回合。

注意一下,这里你设置round的值为0而不是1。是因为在startNewRound()中已经设置了对round加1。

如果你将round设置为1,那么startNewRound()中再被加1,那么第一回合你看到的回合数就是2了。

所以这里设置为0,让startNewRound()在第一局开始前对它进行加1操作。

(这些代码比我的说明更能解释,为什么我们不用平常的语言去编程,而要用专门的编程语言)

你同时需要一个action方法处理点击Start Over按钮后触发的动作:

将下面的action方法添加到ViewController.swift中:

@IBAction func startOver() {
        startNewGame()
        updateLabels()
    }

这个方法放在代码中的哪个位置并不重要,但是放在其他action方法等下面是一个不错的选择。

当Start Over按钮被点击后,startOver()这个action方法首先调用startNewGame()用于开始新的一次游戏。(看到了吗,如果你选用合适的方法名称,那么你的代码目的也就一目了然了)

因为startNewGame()改变了score与round这两个实例变量的值,所以你需要调用updateLabels()来更新这些标签的文本。

作为最终的完善,你应该将viewDidLoad()中的startNewRound()替换为startNewGame()。因为当app刚开始运行时分数和回合数都应该是0,这一改动不会对app的运行效果有任何影响,只是使你的代码更加合理了。

override func viewDidLoad() {
        super.viewDidLoad()
        startNewGame()  //改动这一行
        updateLabels()
    }

最后,将Start Over按钮和action方法连接起来。

打开storyboard,按住ctrl并且拖动start over按钮到view controller。放开鼠标后在弹出的窗口上选择startOver。这样按钮的Touch Inside event就和你刚才定义的动作连接起来了。

运行app,并且玩几局。然后点击start over按钮看看有没有生效。

小帖士:如果你丢失了某个按钮或者标签的连接,不知道它们是连接到哪个方法,你可以右击storyboard中黄色图标的那个view controller,就可以看到你操作过的所有连接。

view controller到所有对象的连接

你可以在05-polish中找到本节课的相关代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容