iOS Apprentice中文版-從0開(kāi)始學(xué)iOS開(kāi)發(fā)-第七課

完善這個(gè)游戲

你已經(jīng)有了一個(gè)基本能玩的游戲app了。游戲的規(guī)則都執(zhí)行的不錯(cuò),也沒(méi)有什么邏輯上的重大缺陷。我能告訴你的就是,眼下沒(méi)有BUG。但是這里仍舊有許多我們要改進(jìn)的地方。

顯然,目前的游戲界面看起來(lái)既不3D也不華麗,我們后面會(huì)給它整容一下。但是眼下,我們有其他一些地方需要微調(diào)一下。

我們就從如何表現(xiàn)玩家的得分情況開(kāi)始吧。

如果玩家將滑條正好放到了目標(biāo)值的位置,讓提醒窗口顯示“Perfect”,如果非常接近目標(biāo)值,就顯示“You almost had it”,如果偏離的比較遠(yuǎn)則顯示“Not even close”,這可以給玩家得分一個(gè)比較良好的反饋。

練習(xí):想想實(shí)現(xiàn)方法。這些判斷邏輯應(yīng)該放在什么地方,并且你應(yīng)該如何編程實(shí)現(xiàn)它?線索:我們?cè)趧偛藕孟裼玫搅撕芏噙@樣的詞,“如果”。

放置這些判斷邏輯的正確位置是showAlert(),因?yàn)槟阍谶@里創(chuàng)建了UIAlertController的對(duì)象,用于給玩家顯示一個(gè)提示窗口。你已經(jīng)對(duì)message的文本做了一些處理,現(xiàn)在你要用類(lèi)似的方法來(lái)處理title的文本。

這里是改進(jìn)后的方法的代碼:

@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)  //這里改動(dòng)一下
        let action = UIAlertAction(title: "OK", style: .default, handler: nil)
        alert.addAction(action)
        present(alert,animated: true,completion: nil)
        startNewRound()
        updateLabels()
    }

你創(chuàng)建了一個(gè)名為title的String型局部變量,用來(lái)存放在提醒窗口頂部顯示的文本。最初,變量title沒(méi)有任何值。

你使用滑條和目標(biāo)值的差值difference來(lái)判斷應(yīng)該顯示哪一條文本:

如果difference等于0,說(shuō)明玩家非常牛X,于是你將“perfect”放入title。

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

如果difference小于10則顯示“Pretty good”

如果difference大于等于10,則認(rèn)為玩家表現(xiàn)不佳,顯示“Not even close”

你能理解這段代碼的邏輯了嗎?它僅僅是一堆if語(yǔ)句用于判斷difference變量的值,并且選一條對(duì)應(yīng)的文本顯示。

你用title變量的文本替換了你在創(chuàng)建這個(gè)UIAlertController對(duì)象時(shí),使用的一條固定文本(Hello World)。

運(yùn)行app,然后玩幾局。你將看到title文本根據(jù)你的得分情況在不斷變化。這就是if語(yǔ)句的作用。

新的title文本

練習(xí):當(dāng)玩家得到一個(gè)perfect時(shí),給玩家額外的加100分作為獎(jiǎng)勵(lì)。并且在非常接近100分的時(shí)候,比如98,97分時(shí)也給予一定的獎(jiǎng)勵(lì)(說(shuō)不定在獎(jiǎng)勵(lì)之下,有人會(huì)給你充值哦_)。

現(xiàn)在對(duì)玩家挑戰(zhàn)高難度低分時(shí),我們有相應(yīng)的激勵(lì)機(jī)制了,一個(gè)perfect不僅僅是100分而是200分。并且在非常接近100時(shí),我們也給一個(gè)50分的獎(jiǎng)勵(lì)。

這里我如何實(shí)現(xiàn)這一目的的代碼(注意看注釋部分):

@IBAction func showAlert() {
        let difference = abs(targetValue - currentValue)
        var points = 100 - difference //將let改為var,points從常量改變?yōu)樽兞?        let title: String
        if difference == 0 {
            title = "Perfect"
            points += 100   //添加這一行
        } else if difference < 5 {
            title = "You almost had it!"
            if difference == 1 {
                points += 50
            }   //添加這個(gè)if語(yǔ)句
        } else if difference < 10 {
            title = "Pretty good!"
        } else {
            title = "Not even close..."
        }
        
        score += points  //這一行原來(lái)在上面,把它移到下面
...
}

你應(yīng)該注意以下幾個(gè)事情:

在第一個(gè)if后面的花括號(hào)內(nèi),你看到了一行新的語(yǔ)句。當(dāng)difference等于0時(shí),你不僅使title顯示為“Perfect”,而且額外的給points加了100分。

第二個(gè)if也改了。它的內(nèi)部出現(xiàn)了一個(gè)新的if語(yǔ)句。這樣做并沒(méi)有問(wèn)題。你想要單獨(dú)處理difference等于1的情況,當(dāng)?shù)扔?時(shí),額外的加50分上去,這就是這個(gè)新的if的作用。

畢竟,當(dāng)difference大于0小于5時(shí),它當(dāng)然可以為1,但并不總是1。因此你用了一個(gè)額外的if語(yǔ)句來(lái)檢查defference是否為1,如果是,則加50分。

因?yàn)檫@些新的語(yǔ)句添加了新的分?jǐn)?shù),所以points不能再是常量了,它現(xiàn)在必須是一個(gè)變量。這就是為什么我們把points前面的關(guān)鍵字由let更改為var。

最后,score += points這一行必須移動(dòng)到所有if語(yǔ)句的后面。這是必須的,因?yàn)閍pp也許會(huì)在這些if語(yǔ)句的內(nèi)部修改points的值,并且這些額外的得分也需要加到總分score中去。

如果你自己寫(xiě)的版本和我的略有不同,也沒(méi)什么關(guān)系,只要它能夠提供同樣的功能并且正常工作。在寫(xiě)程序的過(guò)程中,處理一個(gè)問(wèn)題經(jīng)常會(huì)有多種方法,只要它們的執(zhí)行結(jié)果一致就沒(méi)問(wèn)題。

運(yùn)行app,并且看看剛才的改動(dòng)都生效沒(méi)有。

額外的得分

回顧一下局部變量(Local variables)

我已經(jīng)多次指出局部變量與實(shí)例變量的區(qū)別。作為你此刻應(yīng)該知道的內(nèi)容是,一個(gè)局部變量?jī)H僅在它所屬的方法被調(diào)期間才存在,而實(shí)力變量則在它所屬的對(duì)象的視圖控制器(view controler)存在期間一直存在。局部常量和實(shí)例常量也是如此。

在showAlert()內(nèi)部,有六個(gè)局部的量(常量和變量)和三個(gè)實(shí)例的量(常量和變量):

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

練習(xí):指出哪些是局部的,哪些是實(shí)例的,哪些是變量,哪些又是常量?

答案:局部的非常好辨認(rèn),因?yàn)樗鼈兊拿智岸加衛(wèi)et或者var,說(shuō)明它們是在方法內(nèi)部被定義的。(不要誤會(huì)我的意思,并不是說(shuō)有l(wèi)et和var就是局部變量,let和var是定義常量和變量的關(guān)鍵字,有l(wèi)et或者var開(kāi)頭,說(shuō)明它們?cè)诜椒▋?nèi)部剛剛被定義,所以是局部的,我們一開(kāi)頭也說(shuō)過(guò),變量或常量的作用范圍純粹看它們被定義在哪里)。

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

這些符號(hào)(let和var)用于創(chuàng)建新的變量(var)或者常量(let)。因?yàn)樗鼈冊(cè)诜椒ǖ膬?nèi)部被創(chuàng)建,所以它們是局部的。

這六個(gè)項(xiàng)目——difference, points, title, message, alert, 以及action被限制在showAlert()內(nèi)部,并且在它之外并不存在。一旦showAlert()方法執(zhí)行完畢,它們就被釋放了。

例如:每次玩家點(diǎn)擊Hit Me按鈕后,difference都會(huì)得到一個(gè)不同的值,即使它是常量。我們前面不是說(shuō)過(guò)常量的值是不可以改變的嗎?

原因是這樣的:每次showAlert()方法被調(diào)用的時(shí)候,這些局部的常量和變量都會(huì)被重新創(chuàng)建。舊的哪些早都被扔掉不要了。

具體就是當(dāng)showAlert()被調(diào)用時(shí),它會(huì)創(chuàng)建一個(gè)全新的difference變量,之前一次的difference已經(jīng)不存在了,被釋放了。而這次新創(chuàng)建的這個(gè)difference在showAlert()運(yùn)行結(jié)束后,也被扔掉了,下一次showAlert()運(yùn)行時(shí)又創(chuàng)建了一個(gè)全新的。所以difference是個(gè)常量,但是它的值每次都不同,因?yàn)槊看文憧吹降亩际且粋€(gè)全新的difference(細(xì)思極恐系列_)。

但是在showAlert()一次運(yùn)行期間,difference的值,是不能發(fā)生變化的。唯一可以改變的就是points,因?yàn)樗亲兞浚╲ar)。

再來(lái)看實(shí)例變量,它們被定義在任何一個(gè)方法的外面。通常都把它們放在一個(gè)文件的開(kāi)頭,像下面這樣:

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

你可以在任何方法內(nèi)部調(diào)用這些變量或者常量,不需要重新定義一次,并且它們會(huì)長(zhǎng)期存在。

如果你像這樣做:

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

這樣不會(huì)得到我們想要的結(jié)果。因?yàn)槟阍趕core前面放了一個(gè)var,這樣它就是屬于showAlert() 內(nèi)部的一個(gè)變量了,它不會(huì)影響外面那個(gè)score的值,并且showAlert() 運(yùn)行結(jié)束后,它就消失了,這樣玩家的score永遠(yuǎn)不會(huì)被顯示。

很明顯這不是你想要的結(jié)果,幸運(yùn)的是,剛才那段代碼甚至不會(huì)被編譯,因?yàn)閄code知道這樣做是可疑的。

??為了讓這兩種類(lèi)型的變量有所區(qū)別,以便于一看就知道它們能活多久,有些程序員會(huì)在實(shí)例變量的前面加一個(gè)下劃線。
它們會(huì)將score命名為_(kāi)score。這樣可以減少一些麻煩,因?yàn)樵谧兞棵智凹右粋€(gè)下劃線就不會(huì)和局部變量弄混了。這只是個(gè)人的一些習(xí)慣,Swift才不在乎你怎么給變量取名。
還有一些程序員喜歡在前面加個(gè)m(代表member)或者加個(gè)f(代表field),有些甚至在變量名稱(chēng)后面加個(gè)下劃線。這些方法是愚蠢的,不要去學(xué)。如果你不能心知肚明的清楚每個(gè)變量的作用范圍,這些前綴或者后綴只能把你帶向更深的深淵。

等待提醒窗口離開(kāi)

在這個(gè)游戲中,仍然有些事情困擾著我。也許你已經(jīng)注意到了。。。

當(dāng)你一點(diǎn)擊Hit Me按鈕,提醒窗口就彈出來(lái)了,并且與此同時(shí)滑條立即恢復(fù)到了中間位置,回合數(shù)值立即顯示為加1,并且目標(biāo)值也立即被一個(gè)新的隨機(jī)數(shù)替換掉了。

就是說(shuō)你還木有機(jī)會(huì)觀察上一回合結(jié)果的時(shí)候,新一回合的數(shù)據(jù)立馬被更新到屏幕上了,這讓人覺(jué)得有點(diǎn)怪怪的。

你也許想知道為什么會(huì)這樣,畢竟在showAlert()中你是在顯示提醒窗口之后才調(diào)用的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. . .)并沒(méi)有暫停其他方法的執(zhí)行,并且等到提醒窗口消失后才執(zhí)行它們。其他一些平臺(tái)上的alert是被設(shè)計(jì)為如此工作的,但是在iOS上不是。

取而代之的是present(alert. . .)將提醒窗口放到屏幕上的同時(shí)立即返回結(jié)果,然后showAlert()中的剩余方法立即被執(zhí)行,新的一回合甚至在提醒窗口的彈出動(dòng)畫(huà)尚未結(jié)束時(shí),就被更新到屏幕上了。

用程序術(shù)語(yǔ)講就是,alert(提醒窗口)是異步工作的。更多關(guān)于異步和同步的內(nèi)容我們?cè)谙乱粋€(gè)課程中講,現(xiàn)在對(duì)你而言這件事,就是意味著,在alert執(zhí)行結(jié)束前你不知道其中的進(jìn)展情況。你只能賭showAlert()運(yùn)行結(jié)束后一切正常。

所以如果你無(wú)法在彈出窗口消失前在showAlert()內(nèi)部等待,那么你如何等待它的關(guān)閉呢?

答案是簡(jiǎn)單的:事件!和你之前看到的一樣,大多數(shù)iOS程序都涉及等待特殊的事件發(fā)生——按鈕被點(diǎn)擊,滑條被拖動(dòng)等等。這里沒(méi)有什么不同,你只需要以某種事件等待alert的結(jié)束。在這段時(shí)間內(nèi),你什么都不做。

這是它工作的原理:
對(duì)于alert來(lái)說(shuō),每一次點(diǎn)擊Hit Me按鈕,你都必須提供一個(gè)UIAlertController的對(duì)象。這個(gè)對(duì)象告訴alert,按鈕(這個(gè)按鈕是指提彈出的提醒窗口上的那個(gè)按鈕)上的文本是“OK”,以及這個(gè)按鈕是什么樣式的(這里我們使用的是默認(rèn)樣式):

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

這里的第三個(gè)參數(shù),handle,告訴alert當(dāng)OK按鈕被點(diǎn)擊后應(yīng)該發(fā)生什么。這就是你尋找的等待alert消失的事件(點(diǎn)擊OK按鈕后,alert就被解除了,并且會(huì)觸發(fā)一個(gè)事件)。

目前handle是nil,就是說(shuō)什么都不做。為了做出我們需要的更改,你需要給UIAlertAction一些代碼執(zhí)行,當(dāng)OK按鈕被點(diǎn)擊后。當(dāng)玩家最終點(diǎn)擊OK按鈕后,alert將自己從屏幕上移除并且會(huì)跳轉(zhuǎn)到你的代碼上。你可以在這個(gè)地方打打注意。

這種模式也被稱(chēng)作‘回調(diào)’,在iOS上這種模式會(huì)在多種用途中出現(xiàn)。之前你經(jīng)常被要求創(chuàng)建一個(gè)新的方法用于處理事件,但是現(xiàn)在,你需要一點(diǎn)新的東西——閉包(closure)。

將showAlert()的底部稍微改動(dòng)一下:

 @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)
    }

這里改變了兩個(gè)地方:

1、你移除了方法中底部的startNewRound()和updateLabels(),千萬(wàn)別漏掉這一點(diǎn)。

2、你將這兩個(gè)方法塞到UIAlertAction的參數(shù)handle的代碼塊中去了。

這樣的代碼塊就叫做閉包(closure)。你可以把它想象做一個(gè)沒(méi)有名稱(chēng)的方法。這些代碼不會(huì)被立即執(zhí)行,之后點(diǎn)擊OK按鈕后才會(huì)執(zhí)行。這個(gè)閉包在alert被解除后才告訴app開(kāi)始新的一回合以及更新標(biāo)簽的內(nèi)容。

運(yùn)行app并且觀察一下效果。我想你的游戲效果應(yīng)該比剛才好些了。

??:self
你也許想知道為什么在handle的代碼塊里你用self.startNewRound()取代了startNewRound()。
self關(guān)鍵字允許view controller指向自己。這個(gè)概念對(duì)你而言不應(yīng)該太陌生。當(dāng)你說(shuō):“我需要一個(gè)冰淇淋”時(shí),你用‘我’這個(gè)詞指代了自己。類(lèi)似的,程序中的對(duì)象也可以用一個(gè)代稱(chēng)來(lái)討論它們自己。
通常你不需要這個(gè)self向view controller傳遞消息。這里是個(gè)例外:在閉包中,你必須使用關(guān)鍵字self來(lái)指向view controller。
這是Swift的語(yǔ)法規(guī)則。如果你在閉包中忘記了self,Xcode會(huì)創(chuàng)建app失敗(親自試試)。之所以存在這個(gè)規(guī)則是因?yàn)殚]包可以‘捕獲’變量,這會(huì)帶來(lái)一些意外的結(jié)果,基本上不是好的結(jié)果。你會(huì)在另外的課程中學(xué)習(xí)這些內(nèi)容,在本課程中,我們就討論到這里。

重新開(kāi)始

我不是指刪掉你之前的所用東西,如果你已經(jīng)這么做了,那么恭喜你多了一次復(fù)習(xí)的機(jī)會(huì)。我說(shuō)的是這個(gè)游戲app中的“Start Over”按鈕。這個(gè)按鈕用于將你的得分和回合數(shù)重制為默認(rèn)值。

Start Over按鈕的作用之一是用來(lái)和其他玩家一較高下。比如第一個(gè)玩家玩十個(gè)回合,然后重置分?jǐn)?shù)讓第二個(gè)玩家玩十回合,看看誰(shuí)的得分更高。

練習(xí):試著自己完成Start Over按鈕的功能。你已經(jīng)見(jiàn)識(shí)過(guò)了按下一個(gè)按鈕后,view controller是如何進(jìn)行響應(yīng)的,并且你應(yīng)該可以實(shí)現(xiàn)如何改變score和round變量的值。

你會(huì)如何做呢?如果你卡住了的話,跟著我的講解往下做。

首先,在ViewController.swift中添加一個(gè)新的方法,用于開(kāi)始一次新的游戲。我建議你將這個(gè)方法放在startNewRound()的附近,因?yàn)檫@兩個(gè)方法的概念差不多。

添加新的方法:

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

這個(gè)方法重置了score和round的值,并且同時(shí)開(kāi)始新的一個(gè)回合。

注意一下,這里你設(shè)置round的值為0而不是1。是因?yàn)樵趕tartNewRound()中已經(jīng)設(shè)置了對(duì)round加1。

如果你將round設(shè)置為1,那么startNewRound()中再被加1,那么第一回合你看到的回合數(shù)就是2了。

所以這里設(shè)置為0,讓startNewRound()在第一局開(kāi)始前對(duì)它進(jìn)行加1操作。

(這些代碼比我的說(shuō)明更能解釋?zhuān)瑸槭裁次覀儾挥闷匠5恼Z(yǔ)言去編程,而要用專(zhuān)門(mén)的編程語(yǔ)言)

你同時(shí)需要一個(gè)action方法處理點(diǎn)擊Start Over按鈕后觸發(fā)的動(dòng)作:

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

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

這個(gè)方法放在代碼中的哪個(gè)位置并不重要,但是放在其他action方法等下面是一個(gè)不錯(cuò)的選擇。

當(dāng)Start Over按鈕被點(diǎn)擊后,startOver()這個(gè)action方法首先調(diào)用startNewGame()用于開(kāi)始新的一次游戲。(看到了嗎,如果你選用合適的方法名稱(chēng),那么你的代碼目的也就一目了然了)

因?yàn)閟tartNewGame()改變了score與round這兩個(gè)實(shí)例變量的值,所以你需要調(diào)用updateLabels()來(lái)更新這些標(biāo)簽的文本。

作為最終的完善,你應(yīng)該將viewDidLoad()中的startNewRound()替換為startNewGame()。因?yàn)楫?dāng)app剛開(kāi)始運(yùn)行時(shí)分?jǐn)?shù)和回合數(shù)都應(yīng)該是0,這一改動(dòng)不會(huì)對(duì)app的運(yùn)行效果有任何影響,只是使你的代碼更加合理了。

override func viewDidLoad() {
        super.viewDidLoad()
        startNewGame()  //改動(dòng)這一行
        updateLabels()
    }

最后,將Start Over按鈕和action方法連接起來(lái)。

打開(kāi)storyboard,按住ctrl并且拖動(dòng)start over按鈕到view controller。放開(kāi)鼠標(biāo)后在彈出的窗口上選擇startOver。這樣按鈕的Touch Inside event就和你剛才定義的動(dòng)作連接起來(lái)了。

運(yùn)行app,并且玩幾局。然后點(diǎn)擊start over按鈕看看有沒(méi)有生效。

小帖士:如果你丟失了某個(gè)按鈕或者標(biāo)簽的連接,不知道它們是連接到哪個(gè)方法,你可以右擊storyboard中黃色圖標(biāo)的那個(gè)view controller,就可以看到你操作過(guò)的所有連接。

view controller到所有對(duì)象的連接

你可以在05-polish中找到本節(jié)課的相關(guān)代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容