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

计算玩家的分数

现在你已经实现了生成一个随机数作为目标,以及读取滑条的值,现在你应该来计算玩家的得分了。

玩家将滑条的位置拖到离目标值越近的位置,就应该得越高的分,越偏离目标则的分应越低。

计算每一回合的分值,你需要观察滑条的值与目标值之间的差值(也就是它们在数轴上的距离)相差多少。

计算滑条位置和目标值的差

一个简单的方法是对滑条的值和目标值进行相减的运算。

不幸的是,这样可能会得到一个负值当滑条位置的值小于目标值时。

你需要一些方法将这些可能出现的负值转换为正值,或者直接将负值加到用户的总分里去,介于后者会导致玩家秒删app,所以我们还是来完成这个转换吧。

只是用滑条的值(currentValue)减去目标值(targetValue)的话,是解决不了这个问题的,因为targetValue总是有比currentValue大的时候。

嗯~看来我们遇到点麻烦了

练习:如果我要求你解决这个问题,仅仅是用语言描述不需要写代码,你会提出什么样的方案呢?不要担心电脑能不能听懂你说的话,就用口语描述吧,你现在是说给我听。

如果是我的话,我会这样做:
1、如果滑条的值大于目标值,这时就用滑条的值减去目标值。
2、如果滑条的值小于目标值,这时就用目标值减去滑条的值。
3、如果两者相等,那么差值就为0。

这样做就不会有负数了,因为被减数总是大于等于减数。

来算一算:

如果滑条位置的值是60并且目标值是40,那么60-40=20.

如果滑条位置的值是10并且目标值是30,那么30-10同样等于20.

算法:

刚才你想出的这种东西就叫做算法,算法就是关于一种解决一系列计算问题的机械步骤的术语。虽然我们刚才的算法极其简单,但是这是属于你在这方面踏出的第一步。

目前世界上有许多著名的算法,比如快速排序(quicksort)用于将一些列内容进行排序,还有二进制搜索(binary search)用于将搜索这种已经排序好的列表。你可以在自己的程序中直接使用这些其他人已经发明好的算法,以便节省大量自己的脑细胞。

然而,在我们这个游戏app中恐怕你得自己想出一些算法了。其中既有像刚才那样简单的,也有相当困难使你绝望的。不过不用担心,这正是编程的乐趣所在。

计算机的学术领域主要就是学习算法并且简化它们。

你可以用自己的口语描述任何算法,它们仅仅是你执行某些运算的一系列步骤。就像你用手在纸上计算什么是一样的,就像上面我们算60-40一样。但是有些复杂的算法用手永远也算不完,所以某些情况下你需要把它们写成代码,让计算机去算。

我的建议是:如果你不知道如何让你的程序去计算,你可以先拿一张纸,在纸上写出计算的步骤,口语化的写就可以,先把电脑放在一边不要管。然后想一想这些步骤,用手算的话应该怎么去做?

一旦你适应了这种思路,那么你在程序中为自己写一个算法就是小菜一碟了。

你可以能会有多种方法解决我们刚才的问题,我在这里会给你展示两种,但是这次让我们用代码来表示它们:

var difference: Int
if currentValue > targetValue {
difference = currentValue - targetValue
} else if targetValue > currentValue {
difference  = targetValue - currentValue
} else {
difference  = 0
}

"if"结构是一个新出现的东西。它允许你的代码做出选择,其意思和英语中的if基本相同。大体上讲,它的模式如下:

if something is true {
then do this
} else if something else is true {
then do that instead
} else {
do something when neither of above are true
}

if关键字后面跟的就是所谓的逻辑条件。如果这个条件被判断为真,例如currentValue大于targetValue,那么在两个花括号之间的代码就会被执行。

然而,如果这个条件不是真的,那么电脑就会去看else if的条件是不是为真。也许这里会有多个else if,那么电脑就会从上到下的逐一的去判断每个else if的条件,直到遇到一个条件为真的为止。

如果所有的条件都是假的,那么最后一个else括号中的代码会被执行。

在这个简单的算法中,你首次创建了一个名为difference的局部变量用于存储计算结果。这个值只能是正整数或者0,所以它应该是Int(整数)型的:

var difference: Int

然后你对currentValue和targetValue进行减法运算。首先你决定currentValue大于targetValue的情况:

if currentValue > targetValue {

‘>’这个左开口的三角表示大于操作符。如果currentValue变量中的值确实大于targetVlaue变量中的值,那么currentValue > targetValue这个条件就被判定为真,那么下面这行代码就会被执行:

difference = currentValue - targetValue

这里你用较大的currentValue减去较小的targetValue并且把差值存储到变量difference中。

注意一下我是如何选择变量名称的,尽量选择那些能够清晰描述这个变量作用的词语。你经常会见到以下这种代码:

a = b - c

这看上去完全不知道它的意图,除了能看出是在做减法以外。从变量a、b、c的名称上得不到任何线索表明它们的作用。

回到我们的Swift语句。如果currentValue小于或等于targetValue,那么上面的条件就为假了并且程序会跳过条件后面花括号内的代码,进入到下一个条件判断:

} else if targetValue > currentValue {

这里发生的事情和之前一样,除了targetValue和currentValue的位置调换了。只有当targetValue的值大于currentValue的值时,电脑才执行后面花括号内的代码:

difference = targetValue - currentValue

这一次你用targetValue减去currentValue并且将结果存储到difference变量中。

到现在为止只剩一种情况你没有处理了,就是targetValue和currentValue相等的时候。当玩家正好将滑条拖到等于目标值当位置的时候,就会出现这种情况,完美的得分。在这种情况下difference的值为0。

} else {
difference = 0
}

此时,这两个数谁也不比谁大,谁也不比谁小,只给你留个一个选择,它们一定相等。

让我们把这个算法放到动作里去。在showAlert()的顶部添加如下代码:

@IBAction func showAlert() {
        var difference: Int
        if currentValue > targetValue {
            difference = currentValue - targetValue
        } else if targetValue > currentValue {
            difference = targetValue - currentValue
        } else {
            difference = 0
        }
        
        let message = "The value of the slider is: \(currentValue)" + "\nThe traget value is: \(targetValue)" + "\nThe difference is:\(difference)"
        . . .
    }

为了观察这个算法的效果,你将difference的值显示在提醒窗口的消息里。

运行app并且观察一下:

提醒窗口显示difference的值

另一种计算difference的方法

我之前提到过,这里有其他的方法计算currentValue和targetValue的差值,并且保证其结果为正数。刚才的算法虽然工作的不错,但是它有好几行代码。我想我们应该用一些简单的方法,减少几行代码。

新的算法思路如下:

1、用滑条的值减去目标值(currentValue - targetValue)

2、如果结果是负数,那么就让它乘以-1,这样把它变成正数。

你不在费劲避免出现负数了,只是当电脑计算出负数的时候,你就将它转换为正数。

练习:自己改造一下代码实现这个目的。线索:刚才我们的思路中包含了“如果”和“那么”这两个词,这是一个相当不错的指示,你应该在代码里使用if语句。

你应该会得到类似下面的语句:

var difference = currentValue - targetValue
if difference < 0 {
difference = difference * -1
}

这就是将新的算法简洁了当的翻译为代码的结果。

首先你对这两个数进行相减,然后将结果存储到difference变量里。当difference小于0时,乘以-1,将它转换为正数。

注意一下,你在一行代码里完成了创建difference变量并且将一个结算结果分配给它。完全不需要把它们写成两行,比如:

var difference: Int
difference = currentValue - targetValue

并且在一行版本里,你也并没有告诉编译器difference是Int(整数)类型的数据。因为currentValue和targetValue都是Int型的,所以我们聪明的Swift可以自动判断currentValue-targetValue也是Int型的,那么difference当然也是Int型的。

这个特点被称作‘类型推断’,这只是Swift众多优点中的一点。

当你有了计算结果以后,你使用了一个if语句判断difference是否是负数(小于0)。如果是,则将这个结果乘以-1,从而得到一个正数。让我们回到difference变量。

当你写到:

difference = difference * -1

这时电脑先将difference的值乘以-1,然后再将这个计算结果放回到difference里。所以我们看到的效果就是,通过这一运算,difference中的负值被一个正数覆盖掉了。

这是一种常见的运算,你可以将它简写为:

difference *= -1
*=操作符结合了*和=两个独立的操作符,他们的计算结果是一样的

其实你也可以写成下面这个样子:

var difference = currentValue - targetValue
if difference < 0 {
difference = -difference
}

用负号操作符代替乘以-1,也可以保证difference绝对是一个正数,因为负负得正(如果你不相信的话,可以去问问数学专家)。

试一试我们新的算法。将showAlert()改成下面这个样子:

@IBAction func showAlert() {
        var difference = currentValue - targetValue
        if difference < 0 {
            difference = difference * -1
        }
      . . .
    }

保存并运行一下这个新的版本,它的表现应该和之前的没有差别。电脑的计算结果并未改变,只是你的算法稍有不同。

最后我们在推荐一个算法,使用一个函数来完成这个功能。

你之前已经见过几次函数了:当你生成一个随机数时用到的arc4random_uniform()以及用来给滑条的值取整的lroundf()。

为了确保计算结果为正数,你可以使用abs()这个函数。

如果你在学校里学过数学,那么也许你依稀记得一个术语叫做‘绝对值’,就是一个不需要关心它正负号的数值。

这正是你所需要的,并且标准函数库里给你提供了一个现成的函数,使用它你就可以将最终的解决方案缩减到一行代码里。

let difference = abs(targetValue - currentValue)

你用谁减去谁已经不在重要了。如果结果为负值,那么abs()函数会将其转换为正数。这是需要记住的一个便利的函数,你会经常用到它。

改一下showAlert(),并且运行app试试效果:

@IBAction func showAlert() {
        let difference = abs(targetValue - currentValue)
        
        let message = . . .
    }

已经改的简单的不能再简单了。

练习:我们还改了其他一些小地方,你注意到了吗?

答案:你用let difference代替了var difference。

变量(variables)和常量(constants)是有区别的,和变量不同,常量的值不可以发生变化(看名字就知道了,constants)。

在常量这种盒子里,你只能放一次东西进去,并且不能在之后用其他东西去替换它。

变量用关键字var定义,常量用关键字let定义。现在difference已经是一个常量了,不再是变量。

在之前几个版本的算法里,difference的值可能会发生改变。如果为负值的话,你就要将它转换为正值。这就需要difference必须是一个变量,因为只有变量才能被分配新的值替代旧的值。

现在你在一行代码内计算了全部所需内容,difference在得到一个值以后再也不会发生变化,所以此时最好将它定义为一个常量。(这样可以使你的意图更加清晰,并且使Swift的编译器更好的理解你的代码)

出于同样原因,message,alert以及action都是常量(并且始终独立存在)。现在你知道为什么声明这些对象时用的都是let关键字了,因为一旦它们被赋了一个值,就再也不需要改变了。

常量在Swift中使用非常频繁。你经常只需要暂时的保存一个值,如果在此期间这个值不需要发生变化,那么它虽好被声明为常量而不是变量。

如何计算玩家的得分?

现在你已经知道了滑条位置和目标值的差值,这样计算玩家的分数就简单多了。

将showAlert()修改为下面这样:

@IBAction func showAlert() {
        let difference = abs(targetValue - currentValue)
        let points = 100 - difference
        
        let message = "Your scored \(points) points"
        . . . 
    }

你可以得到的最高分数为100,如果你正好将滑条拖到和目标值一样的位置时,此时difference为0。而里目标值越远则得分越低。

运行app试试看你能得多少分?

提醒窗口展示玩家当前回合的得分

练习:因为滑条的最大值为100,最小值为1,最大差值为100 - 1 = 99。这意味着你能得到的最低分为1分。试着解释下这是为什么(这需要点数学常识)。

累计玩家的总分

在这个游戏里,你需要在屏幕上展示玩家得到的总分。在每一回合结束后,这个app应该添加最后一次得分到总分里并且之后更新得分的标签(score label)。

在ViewController.swift中添加一个新的实例变量score:

class ViewController: UIViewController {
    
    @IBOutlet weak var slider: UISlider!
    @IBOutlet weak var targetLabel: UILabel!
    
    var currentValue: Int = 50
    var targetValue: Int = 0
    var score = 0 //添加这一行

这是怎么回事?和之前的两个变量不一样,你没有指定score的类型为Int。

如果你没有指定数据类型,Swift就会使用类型推断来定位它的数据类型。因为0是一个整数,Swift就会推定score应该是Int类型的数据,并且自动将score设置为Int(整数)型。

实际上,你也不需要指定前两个变量的数据类型:

var currentValue = 50
var targetValue = 0

按照上图改一下这两行代码。

感谢类型推断,你仅需要在变量没有初始值的时候指出变量的数据类型。但是大多数时候,你可以安全的让Swift自己去猜变量的类型。

我觉得Swift的类型推断功能相当亲民!它可以明显的减少你的打字工作。

现在来修改showAlert(),让它可以记录总分:

@IBAction func showAlert() {
        let difference = abs(targetValue - currentValue)
        let points = 100 - difference
        score += points  //添加这一行
        
        let message = "Your scored \(points) points"

这里没有啥新东西,你只是添加了这样一行:

score += points

其作用是将玩家每一回的的得分加到总分里去,你也可以写成下面这个样子:

score = score + points

个人而言,我喜欢+=这个版本,但是后一种版本也没问题。它们完成的工作是一样的。

将总分显示在屏幕上

你要做的事情和你在target label上做的一模一样:链接score label到outlet并且将score的值放到这个label的文本中。

练习:看看在没有我的帮助下你自己能不能完成这个工作。你以前已经对target label操作过一次了,所以你应该能够在score label标签上重复一遍这个步骤。

以下步骤都应该是你熟悉的,首先在ViewController.swift中添加这么一行:

@IBOutlet weak var scoreLabel: UILabel!

然后你打开storyboard并且将这个标签链接到这个新的scoreLabel outlet(就是写着999999的那个标签)。

不确定如何连接到outlet?这里有好几种方法用于连接用户接口对象(user interfae objects)到view controller的outlet。

注意:下面的1,2,3不是步骤,而是三种方法。

1、按住ctrl点击标签(999999的那个)然后会弹出一个菜单。然后在弹出菜单上拖拽New referencing Outlet到View Controller,然后在弹出的小菜单上选择score label(我们对slider就是这样做的)。

2、打开链接检查器标签。然后拖拽New referencing Outlet到View Controller,然后在弹出的小菜单上选择score label(我们对target label就是这样做的)。

3、按住ctrl从View Controller(黄色图标的那个)往标签上拖(我们这次试试这种新方法);注意:直接在标签上按住ctrl拖拽到view controller是没有用的,不要弄反了。

看到了吗,我们有多种方法用于连接outlet。

现在scoreLabel的outlet已经有了,非常棒,你可以往这个标签的文本里写值了现在。我们应该把相关的代码写在什么地方呢?当然是updateLabels()里面了。

回到ViewController.swift,将updateLabels()改为下面这个样子:

func updateLabels() {
        targetLabel.text = String(targetValue)
        scoreLabel.text = String(score)
    }

这里没有任何新东西,你将score,一个Int型的值转换为String型的,然后将它存储到这个标签的文本中。

运行app并且确认无论何时你点击Hit Me时每一回合的分数都会加到总分中。

score标签记录了玩家的总分

关于回合:

说到回合,当玩家开始新的一个回合的时候,你也需要将回合数进行累加。

练习:跟踪目前回合数的值(起始为1),并且每一新回合开始时,对它进行+1,并且将这个数显示到屏幕上对应的标签里。也许我应该在这里多讲一些,再带带你,但是假如你已经理解并吸收了之前的内容的话,你已经有了足够多手段来实现这个目的,祝你好运!

如果你已经想到这里要用一个新的实例变量,那么你已经接近成功了。你应该在源代码中添加下面这一行:

var round = 0

如果你想把数据类型的名称也加进去,也是可以的,虽然并没有必要这样做:

var round: Int = 0

再来一个outlet给这个标签:

@IBOutlet weak var roundLabel: UILabel!

和之前一样,你需要连接这个标签到Interface Builder的outlet。

⚠️:不要忘记这些连接
忘记这些连接是新手常犯的一个错误,特别是对现在的你而言。
我常常在为一个button写好outlet以及处理用户点击后的代码以后,在测试app时发现写好的代码都没有生效,然后我不得不花时间去努力的检查问题,最终发现是我忘记了连接这些buttom到outlet或者action method(动作方法)。

最终,updateLabels()应该是这样的:

func updateLabels() {
        roundLabel.text = String(round)
        targetLabel.text = String(targetValue)
        scoreLabel.text = String(score)
    }

同时你能指出应该在哪里对round变量进行累加了吗?

要我说的话,startNewRound()就是个理想的地方。毕竟无论何时,玩家开始新一回合的时候,你都要调用它。所以我们应该在这里对round变量进行累加。

将startNewRound()改成下面这个样子:

func startNewRound() {
        round += 1 \\添加这一行
        targetValue = 1 + Int(arc4random_uniform(100))
        currentValue = 50
        slider.value = Float(currentValue)
    }

注意一下,你在定义round变量的时候,它的默认值为0。因此,当app启动时,它的值初始化为0。当你第一次调用startNewRound()时(在viewDidLoad()中),它被加1,这样你在屏幕上就看到第一回合的round值是1了。

运行app试一试,无论何时你点击Hit Me按钮后,round的值都会被加1,并且显示在屏幕上。

round标签显示当前的回合数

你可以在04-Rounds and Score中找到源代码,如果你做出来的效果不是这样,你可以对照我的代码看看你漏掉了什么。附件请支持正版_

希望我们的文章对你的求学之路提供了切实的帮助,由于我是个人翻译,所以进度无法太快,但是我可以保证每周至少更新一节课,同时也希望大家能心疼我一下,点击一下下方的打赏_,读者的认可,就是我最大的动力。

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

推荐阅读更多精彩内容