计算玩家的分数
现在你已经实现了生成一个随机数作为目标,以及读取滑条的值,现在你应该来计算玩家的得分了。
玩家将滑条的位置拖到离目标值越近的位置,就应该得越高的分,越偏离目标则的分应越低。
计算每一回合的分值,你需要观察滑条的值与目标值之间的差值(也就是它们在数轴上的距离)相差多少。
计算滑条位置和目标值的差一个简单的方法是对滑条的值和目标值进行相减的运算。
不幸的是,这样可能会得到一个负值当滑条位置的值小于目标值时。
你需要一些方法将这些可能出现的负值转换为正值,或者直接将负值加到用户的总分里去,介于后者会导致玩家秒删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中找到源代码,如果你做出来的效果不是这样,你可以对照我的代码看看你漏掉了什么。附件请支持正版_
希望我们的文章对你的求学之路提供了切实的帮助,由于我是个人翻译,所以进度无法太快,但是我可以保证每周至少更新一节课,同时也希望大家能心疼我一下,点击一下下方的打赏_,读者的认可,就是我最大的动力。
网友评论
右键点击View Controller的黄色小方块,把带黄色感叹号的链接删除重新连一次就好
这节没讲什么新东西: 算法的概念.计算总分数,求绝对值....
如果你有其他方面的编程经验.这些可以跳过了