#21 線上遊戲職業測驗App

Lou
彼得潘的 Swift iOS / Flutter App 開發教室
13 min readMay 22, 2023

以魔力寶貝這款Q版2D角色扮演類線上遊戲職業作為設計題目,遊戲內擁有多樣化職業,替選擇困難的玩家設計職業測驗app。

操作畫面
畫面設計

建立Career檔案

將所有職業建立enum (設定為字串,方便之後使用)

enum CareerType: String {
case 劍士, 戰斧鬥士, 騎士, 弓箭手, 格鬥士 // 戰鬥系
case 魔術師, 傳教士, 巫師, 咒術師 // 法術系
case 士兵, 忍者,舞者, 盜賊,爆彈師 // 準戰鬥系
case 醫生, 護士 // 準戰鬥服務系
case 封印師, 馴獸師, 飼養師 // 寵物系
case 紡織工, 獵人, 樵夫, 礦工 // 採集系
case 武器修理師, 防具修理師, 鑑定師, 偵探, 仙人 // 服務系
case 武器製造師, 防具製造師, 寵裝製造師, 藥劑師, 廚師 // 製造系
}

careerList職業清單字典 測驗結果key對照value顯示職業資訊

struct CareerInfo {
let career: CareerType
let introduction: String
}

let careerList: [CareerType : CareerInfo]
職業清單包含簡介資訊

scoreList分數清單 提供測驗計分

struct CareerScore {
let careerType: CareerType
// 初始分數都為0
var Score = 0
}

var scoreList: [CareerScore]

建立Quiz檔案

quizList測驗清單 放入測驗題目、選項、加分職業,利用enum CareerType很方便將職業類別加入也不用怕打錯字

struct Option {
let index: Int
let text: String
let careerTypes: [CareerType]
}

struct Question {
let text: String
let options: [Option]
}

var quizList = [Question]
測驗題目及選項

HomePageViewController

登入測驗頁面

最簡單的功能 按鈕進入下一頁的測驗頁面,重新測驗時Function resetScores 將職業計分歸零

override func viewDidLoad() {
super.viewDidLoad()
resetScores()

}

// 職業計分歸零
func resetScores() {
for index in 0..<scoreList.count {
scoreList[index].Score = 0
}
}

QuizViewController

測驗題目畫面
    override func viewDidLoad() {
super.viewDidLoad()
// 設定進度條動畫圖片
progressGifPlayer()
// 測驗題目順序隨機
quizList.shuffle()
// 載入測驗題目
loadQuiz(questionNum: currentQuizIndex)
}

進度條圖片gif動畫功能

可愛鼠王 努力向前走
func progressGifPlayer() {
// 設定進度條動畫圖片的位置和大小
progressImageView.frame.size.width = questionLabel.bounds.width * 0.25
progressImageView.frame.size.height =
questionLabel.bounds.height * 0.38
progressImageView.center.y = answerProgressView.center.y
progressImageView.frame.origin.x = answerProgressView.frame.origin.x - 20

// 取得動畫圖片的資料
guard let data = NSDataAsset(name: "鼠王")?.data else { return }
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { return }

// 得到動畫幀數
let frameCount = CGImageSourceGetCount(imageSource)
var animationImages: [UIImage] = []

// 將每個幀轉換成UIImage,並加入動畫圖片陣列
for index in 0..<frameCount {
guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else { continue }
let uiImage = UIImage(cgImage: cgImage)
animationImages.append(uiImage)
}

// 設定進度條動畫的相關屬性
self.progressImageView.animationImages = animationImages
self.progressImageView.animationDuration = Double(frameCount) * 0.1 // 播放速度
self.progressImageView.animationRepeatCount = 0 // 重複播放動畫
self.progressImageView.startAnimating()

}

載入題目功能

func loadQuiz(questionNum: Int) {
// 設定題目
let question = quizList[questionNum]
questionLabel.text = question.text

// 設置隨機選項
var options = question.options
options.shuffle()
// 選項文字設定到按鈕上
for i in 0..<options.count {
optionButton[i].setTitle(options[i].text, for: .normal)
optionButton[i].tag = options[i].index
}
// 更新進度條和動畫圖片位置
currentQuizIndex += 1
answerProgressView.progress = Float(currentQuizIndex) / Float(quizList.count)
progressImageView.frame.origin.x += answerProgressView.bounds.width/20

}

答題計分功能

利用UIButton.tag 得到選擇的選項按鈕,對應選項加分的職業,直到測驗完成,統計最高分的職業到結果頁面。(多個同分職業則隨機取出)

用max(by:) 函式在 scoreList 中 找到Score 值最大的 CareerScore 物件,使用closure 接收兩個 CareerScore 物件{ $0.Score < $1.Score } 根據 Score 屬性的值來進行比較。

@IBAction func selectOption(_ sender: UIButton) {

// 取得按鈕tag 對應選項index
let index = sender.tag
// 取得問題的選項清單
let optionList = quizList[currentQuizIndex-1].options
// 取得選項內對應的職業清單
let optionScoreCareerList = optionList[index].careerTypes
// 對應到的職業用迴圈加分
for career in optionScoreCareerList {
if let careerIndex = scoreList.firstIndex(where: { $0.careerType == career } ) {
scoreList[careerIndex].Score += 1
}
}
// 測驗題目完成
if currentQuizIndex == quizList.count {
// 找出最高分數
let maxScore = scoreList.max(by: { $0.Score < $1.Score })?.Score
// 找出分數相同的職業
let maxScoreCareers = scoreList.filter { $0.Score == maxScore }.map { $0.careerType }
// 隨機選擇一個職業
careerMatch = maxScoreCareers.randomElement()
// showResultView
performSegue(withIdentifier: "showResultView", sender: sender)


}else {
// 題目還有 繼續測驗
loadQuiz(questionNum: currentQuizIndex)
}
}

返回功能

回到上一題

firstIndex(where:) 是一個陣列方法,這個方法接收closure作為參數,在closure中,我們使用 $0 來表示陣列中的每個元素,並檢查 careerType 是否與 career 相等。如果相等 firstIndex(where:) 方法會回傳這個元素在陣列中的索引值。

找到在 scoreList 中的索引值後,我們使用 scoreList[careerIndex].Score -= 1 來將該職業的分數減1,恢復前一題測驗未加分的狀態。

@IBAction func backButtonPressed(_ sender: UIButton) {
// 避免第一題操作 超出陣列範圍
if currentQuizIndex > 1 {
//載入題目時Index已經先+1 所以這邊要-2才會是上一題
currentQuizIndex -= 2

// 取得上一個選擇的選項index
if let optionIndex = selectedOptionArray.last {
let optionList = quizList[currentQuizIndex].options
let optionScoreCareerList = optionList[optionIndex].careerTypes

// 對應到的職業用迴圈減分
for career in optionScoreCareerList {
if let careerIndex = scoreList.firstIndex(where: { $0.careerType == career } ) {
scoreList[careerIndex].Score -= 1
}
}

}

// 移除最後一個選擇的選項index
selectedOptionArray.removeLast()
// 重新載入上一題的問題
loadQuiz(questionNum: currentQuizIndex)
// 更新進度條動畫圖片位置
progressImageView.frame.origin.x -= answerProgressView.bounds.width/20*2
}

}

利用print檢查返回上一題功能的分數是否正常

// 印出每個職業的分數 檢查分數
for career in scoreList {
print("\(career.careerType):\(career.Score)分")
}
測驗選項加分
返回後扣分恢復

傳遞資料功能

傳遞測驗結果的職業到ResultViewController

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

let controller = segue.destination as? ResultViewController

if let careerMatch = careerMatch {
controller?.careerMatch = careerMatch
}

}

ResultViewController

測驗結果展示

接收測驗結果的職業類別對照careerList字典,利用.rawValue取得enum的資料類型,得到字串設定圖片和職業簡介。

測驗結果 職業簡介
  // 測驗結果的值
var careerMatch: CareerType!


override func viewDidLoad() {
super.viewDidLoad()
// 設定職業名稱標題
careerNameLabel.text = careerList[careerMatch]?.career.rawValue
// 設定職業圖片
careerImage.image = UIImage(named: careerList[careerMatch]!.career.rawValue)
// 設定職業介紹
introductionLabel.text = careerList[careerMatch]?.introduction

}
重新測驗

--

--