计算玩家的分数
现在你已经实现了生成一个随机数作为目标,以及读取滑条的值,现在你应该来计算玩家的得分了。
玩家将滑条的位置拖到离目标值越近的位置,就应该得越高的分,越偏离目标则的分应越低。
计算每一回合的分值,你需要观察滑条的值与目标值之间的差值(也就是它们在数轴上的距离)相差多少。
一个简单的方法是对滑条的值和目标值进行相减的运算。
不幸的是,这样可能会得到一个负值当滑条位置的值小于目标值时。
你需要一些方法将这些可能出现的负值转换为正值,或者直接将负值加到用户的总分里去,介于后者会导致玩家秒删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的方法
我之前提到过,这里有其他的方法计算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时每一回合的分数都会加到总分中。
关于回合:
说到回合,当玩家开始新的一个回合的时候,你也需要将回合数进行累加。
练习:跟踪目前回合数的值(起始为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,并且显示在屏幕上。
你可以在04-Rounds and Score中找到源代码,如果你做出来的效果不是这样,你可以对照我的代码看看你漏掉了什么。附件请支持正版_
希望我们的文章对你的求学之路提供了切实的帮助,由于我是个人翻译,所以进度无法太快,但是我可以保证每周至少更新一节课,同时也希望大家能心疼我一下,点击一下下方的打赏_,读者的认可,就是我最大的动力。