iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
Mobile Development

Swift iOS UIKit 初學者系列:從零開始開發互動式應用系列 第 29

【Day 29】iOS 開發實戰 - 快艇骰子遊戲App:精簡 Set 與 map 資料處理,動態動畫提升遊戲體驗。

  • 分享至 

  • xImage
  •  

導言

昨天我們介紹了使用 SpriteKit 來製作打磚塊遊戲,利用了物理引擎的特性來創造逼真的遊戲體驗。不過,不是所有遊戲都需要如此複雜的物理引擎,許多遊戲只需使用 UIKit 的動畫特效就能提供良好的遊戲效果。今天,我們將探討如何利用 UIKit 製作一個簡單但有趣的快艇骰子 Yahtzee 遊戲。

Yahtzee 快艇骰子是一個將骰子遊戲與撲克牌常見的計分規則相結合的遊戲,讓運氣和策略同時挑戰玩家的技巧。這篇文章將帶領你構建一個基本的 Yahtzee 遊戲應用,並介紹如何運用 Swift 的集合 (Set) 與陣列轉換 (map) 來簡化計算邏輯。

學習目標

  • 理解 Set 集合的特性:學會如何使用集合來處理不重複元素,這在計算骰子遊戲得分時非常有用。
  • 掌握 map 方法的應用:理解如何透過 map 來將一個陣列轉換為另一個包含所需資料的陣列,這是一種處理資料的高效方式。
  • 骰子遊戲的邏輯實作:實作 Yahtzee 快艇骰子的計分邏輯,包括 Three of a Kind、Four of a Kind、Full House 等不同得分組合的判斷與計算。
  • 使用 UIKit 製作簡單動畫:學習如何使用 UIKit 中的旋轉、縮放、移動等簡單動畫效果來增強遊戲的互動性。

設計與介面

這次的介面設計比較簡潔,使用了 Storyboard 將基本的 UI 元素排布好,以藍白色為主調,畫面風格稍顯陽春,但我們專注於遊戲功能的實現。整個遊戲包含骰子顯示區、計分板以及簡單的操作按鍵。

操作影片與截圖

操作影片:展示骰子滾動、計分的完整過程。
Yes

截圖:展示了遊戲開始後的畫面,骰子與計分板的佈局。

計分規則

Yahtzee 的計分規則分為上半部和下半部。讓我們簡單回顧一下這些規則。

上半部:相同數字的總和

在上半部,玩家需要計算五個骰子中相同數字的總和。例如,如果擲出了 1、2、3、3、6,且玩家選擇計算「3」,則得分為 6(兩個 3)。

加分條件:如果 1-6 的總分大於等於 63 分,玩家可獲得額外 35 分。

下半部:特定組合

這部分則根據撲克牌的組合計分:

Three of a Kind:三個骰子相同,計算所有骰子的總和。
Four of a Kind:四個骰子相同,計算所有骰子的總和。
Full House:三個相同 + 兩個相同,固定得 25 分。
Small Straight:四個連續的數字,固定得 30 分。
Large Straight:五個連續的數字,固定得 40 分。
Yahtzee:五個骰子相同,得 50 分。
Chance:任意組合,計算所有骰子的總和。

紙本的計分表參考

遊戲性與進階功能

目前這個版本只設計了單人玩法,未來可以加入挑戰分數的機制,即玩家需超過某個預設分數才算勝利。另外,我們還可以進一步創造一個「快艇」概念:分數即為燃料,得分越高,快艇的燃料越多,最終能跑得更遠。

程式實作與技巧

這次的實作重點是運用 Swift 的集合 (Set) 與陣列轉換 (map) 來簡化邏輯運算。

定義骰子類別

class Die {
    var value: Int
    var isHeld: Bool
    
    init() {
        value = Int.random(in: 1...6)
        isHeld = false
    }
    
    func roll() {
        if !isHeld {
            value = Int.random(in: 1...6)
        }
    }
}

集合 Set、 陣列轉換map

使用 map 方法將骰子的數值轉換為一個陣列,並用 Set 來過濾重複數字:

var dice: [Die]
let uniqueValues = Set(dice.map { $0.value })

當我們調用 map 方法時,我們正在對陣列中的每個元素應用一個轉換,並返回一個包含轉換後結果的新陣列。在這種情況下,dice 是一個 Die 對象的陣列,$0.value 是一個閉包,它返回 Die 對象的 value 屬性的值。所以 dice.map { $0.value } 這行代碼將返回一個包含了所有 Die 對象的 value 屬性值的陣列。

接著,我們將這個陣列傳遞給 Set 初始化器,它將這個陣列中的所有元素作為其內容來創建一個新的 Set 實例。Set 是一種集合類型,它包含一組唯一的元素,這意味著在 uniqueValues 中將不會包含重複的元素,每個元素只會出現一次。

計算 Three of a Kind 的得分

    private func calculateThreeOfAKindScore() -> Int {
        let diceValues = dice.map { $0.value }
        let counts = Set(diceValues).map { value in diceValues.filter { $0 == value }.count }
        if counts.contains(where: { $0 >= 3 }) {
            return diceValues.reduce(0, +)
        } else {
            return 0
        }
    }

用法說明:

  • dice.map { $0.value }:這行程式碼將骰子陣列 dice 中每個骰子的數值取出,生成一個單純的數字陣列 diceValues。這樣,我們可以只針對骰子的數值進行操作。
  • Set(diceValues).map { value in diceValues.filter { $0 == value }.count }:這段程式碼有兩個步驟:
    • 先將 diceValues 轉換為 Set,這樣能去除重複的骰子數值,只保留每個不同的數字。
    • 然後,對於每個不重複的數字,使用 filter 過濾出原始陣列中與該數字相同的元素,並計算它的出現次數。
  • counts.contains(where: { $0 >= 3 }):檢查是否有某個數字出現了三次或更多次,這就是 Three of a Kind 的條件。
  • return diceValues.reduce(0, +):如果條件符合,則返回所有骰子數值的總和作為得分;否則返回 0。

簡化效果:

  • 使用 Set 避免重複計算同樣的數字,讓我們只需處理每個獨特的骰子數值。
  • map 和 filter 組合可以高效地計算每個數字出現的次數,簡化了傳統的巢狀迴圈。

計算 Four of a Kind 的得分

    private func calculateFourOfAKindScore() -> Int {
        let diceValues = dice.map { $0.value }
        let counts = Set(diceValues).map { value in diceValues.filter { $0 == value }.count }
        if counts.contains(where: { $0 >= 4 }) {
            return diceValues.reduce(0, +)
        } else {
            return 0
        }
    }

用法說明:

  • 與 Three of a Kind 相似,只是將條件改為 4 來檢查是否有四個骰子數值相同。如果有,則返回所有骰子的總和作為得分。

簡化效果:

  • 同樣利用 Set 和 map 來簡化數值的處理,使我們可以快速計算每個骰子的出現次數並進行判斷,避免手動迴圈檢查。

計算 Full House 的得分

    private func calculateFullHouseScore() -> Int {
        let diceValues = dice.map { $0.value }
        let counts = Set(diceValues).map { value in diceValues.filter { $0 == value }.count }
        if counts.contains(2) && counts.contains(3) {
            return 25
        } else {
            return 0
        }
    }

用法說明:

  • counts.contains(2) && counts.contains(3):這裡判斷是否同時存在兩個骰子值相同以及三個骰子值相同的組合,這就是 Full House 的條件。
  • return 25:若符合條件,Full House 的得分是固定的 25 分,否則返回 0。

簡化效果:

  • Set 和 map 可以很簡單地找到出現次數是 2 或 3 的數值,避免繁瑣的檢查步驟。
  • 對於這種具體的組合,簡單的集合操作讓邏輯清晰易懂。

簡單的動畫增加互動性

旋轉

首先,我們透過一個簡單的旋轉動畫來模擬搖動骰子的效果。當使用者點擊「搖骰」按鈕時,所有未被選中的骰子將進行短暫的旋轉,讓骰子看起來像是在搖動。這段動畫的實現關鍵是使用 UIView.animate 函數來執行動畫,並且將旋轉角度設為 π / 1.1。

// 旋轉骰子動畫
    UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut], animations: {
    self.emptyNotSelectedDiceImage()
    for button in self.dieButtons {
        if !button.isSelected {
            button.transform = CGAffineTransform(rotationAngle: .pi / 1.1)
        }
    }
     

移動、縮放

當使用者點擊「計分」按鈕時,我們會將所有骰子按鈕移動到按鈕的中心位置,並進行縮放,模擬骰子消失的效果。隨後,骰子會回到原本位置,並恢復大小與透明度。這段動畫提升了整體的互動體驗,讓遊戲變得更加動感。

@IBAction func scoreButtonPressed(_ sender: UIButton) {
    let targetCenter = sender.superview?.convert(sender.center, to: nil) ?? CGPoint.zero

    UIView.animate(withDuration: 1.0, animations: {
        for button in self.dieButtons {
            button.center = targetCenter
            button.transform = CGAffineTransform(scaleX: 0.2, y: 0.2)
            button.alpha = 0
        }
    })

為什麼 Set 和 map 可以簡化程式?

  • 去重與計算簡化:使用 Set 可以輕鬆處理不重複的數字,避免在計算不同骰子值時重複處理,節省了處理時間。
  • 高效資料轉換:map 函數允許我們在一個步驟內將一個陣列轉換成我們需要的資料形式,而不需要多重迴圈或複雜的邏輯。這不僅使程式碼更簡潔,也提升了可讀性。
  • 減少迴圈與條件判斷:傳統上,我們可能需要多層迴圈來檢查每個數字的出現次數,但透過 map 與 filter 的結合,可以在一行程式內完成這些操作,避免大量的手動迴圈與條件判斷。

透過這些 Swift 內建的強大工具,程式碼不僅更簡潔,也變得更具可擴展性和可維護性。

結語

透過 Yahtzee 快艇骰子遊戲 App,使用 UIKit 的動畫效果如旋轉、移動及縮放等,就能使遊戲的視覺表現更加生動,這對於未來開發更具吸引力的遊戲非常有幫助。此外,透過集合 (Set) 和陣列轉換 (map),我們學會了如何以簡潔且高效的方式處理資料,這在處理遊戲邏輯如計分等方面,尤其能顯著提升程式效率和可讀性。這些技巧也可以應用於更多類似的遊戲邏輯設計中,幫助你在未來的開發中更快速地解決問題。


上一篇
【Day 28】iOS 開發實戰 - 打磚塊遊戲 App:遊戲引擎 SpriteKit 實戰
下一篇
【Day 30】iOS 開發實戰 - 總結篇:Swift 與 UIKit 的實踐與收穫
系列文
Swift iOS UIKit 初學者系列:從零開始開發互動式應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言