[iOS] #3 排球是永遠向上看之計分板

APP主要分成兩個畫面:選擇隊伍、比賽對戰。選擇隊伍之後按下PLAY,自動進入比賽對戰畫面,比賽結束產生優勝隊伍之後,可回到選擇隊伍畫面,或是點擊比賽對戰畫面上的Restart按鈕,隨時返回選擇隊伍畫面。

選擇隊伍

畫面的左右各有一個ImageView顯示隊伍照片,加上Gesture手勢功能,讓使用者透過左右滑動切換不同隊伍。上方的隊伍名稱Label及下方的Page Control會隨著隊伍的切換連動。

    var indexA = 0
var indexB = 0
let teams = ["烏野","音駒","青葉城西","伊達工業","梟谷學園","白鳥澤","稻荷崎"]

//左邊ImageView向右滑 >> 上一筆
@IBAction func preATeam(_ sender: Any) {
indexA = (indexA - 1 + teams.count) % teams.count
selectATeam()
}
//左邊ImageView向左滑 >> 下一筆
@IBAction func nextATeam(_ sender: Any) {
indexA = (indexA + 1) % teams.count
selectATeam()
}
//左邊Page Control 點選
@IBAction func ATeamChangePage(_ sender: Any) {
indexA = ATeamPageControl.currentPage
selectATeam()
}

//右邊ImageView向右滑 >> 上一筆
@IBAction func preBTeam(_ sender: Any) {
indexB = (indexB - 1 + teams.count) % teams.count
selectBTeam()
}
//右邊ImageView向左滑 >> 下一筆
@IBAction func nextBTeam(_ sender: Any) {
indexB = (indexB + 1) % teams.count
selectBTeam()
}
//右邊Page Control 點選
@IBAction func BTeamChangePage(_ sender: Any) {
indexB = BTeamPageControl.currentPage
selectBTeam()
}

//更新左邊隊伍內容 update when A Team selected
func selectATeam(){
ATeamLabel.text = teams[indexA]
ATeamImageView.image = UIImage(named: teams[indexA])
ATeamPageControl.currentPage = indexA
}
//更新右邊隊伍內容 update when B Team selected
func selectBTeam(){
BTeamLabel.text = teams[indexB]
BTeamImageView.image = UIImage(named: teams[indexB])
BTeamPageControl.currentPage = indexB
}

兩隊不可選擇相同隊伍

由於左右兩個隊伍清單是一樣的,為了防止使用者選擇相同隊伍比賽,在按下PLAY按鈕時,檢查是否選擇了同隊,若選擇隊伍清單同一筆(以index比較),跳出提示訊息。

    
//按下PLAY按鈕
@IBAction func Play(_ sender: Any) {
checkIfSameTeam() //檢查是否同隊
}
//檢查是否同隊
func checkIfSameTeam(){
if indexA==indexB {
sameTeamAlert()
}
}
//訊息 - 檢查是否同隊
func sameTeamAlert() {
let alertController = UIAlertController(title: "注意", message: "無法同隊比賽,請選擇其他隊伍!", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(alertAction)
present(alertController, animated: true)
}

隊伍資料

APP中所有使用到的圖片,都以隊名開頭命名,方便隨時變更圖片的需求。由於隊伍名稱清單及隊伍代表色清單是兩個畫面View Controller共用,因此將清單資料宣告寫在class ViewController 外面,兩個View Controller即可直接取用資料。

import UIKit

var indexA = 0
var indexB = 0
let teams = ["烏野","音駒","青葉城西","伊達工業","梟谷學園","白鳥澤","稻荷崎"]
let teamColors = [
UIColor(red: 220/255, green: 132/255, blue: 77/255, alpha: 1),
UIColor(red: 217/255, green: 67/255, blue: 77/255, alpha: 1),
UIColor(red: 148/255, green: 193/255, blue: 186/255, alpha: 1),
UIColor(red: 76/255, green: 112/255, blue: 111/255, alpha: 1),
UIColor(red: 156/255, green: 157/255, blue: 159/255, alpha: 1),
UIColor(red: 121/255, green: 65/255, blue: 108/255, alpha: 1),
UIColor(red: 125/255, green: 62/255, blue: 72/255, alpha: 1)
]

class ViewController: UIViewController {

}

比賽對戰

選擇隊伍之後,按下PLAY按鈕,進入比賽對戰畫面。

賽前準備

依據參賽隊伍繪製比賽場地,取用隊伍資料中的隊名,即可取得隊伍徽章圖片檔名。也透過index取得對應隊伍的代表色以設定背景色,比數也都要歸零。另外也利用Bool.random()決定由哪一隊發球(排球將會出現在該隊的場域內)

    override func viewDidLoad() {
super.viewDidLoad()
initGameUI()
}

//初始化
func initGameUI(){

//左隊
indexL = indexA
LTeamLabel.text = teams[indexL] //隊名
LTeamView.backgroundColor = teamColors[indexL] //場地顏色
LTeamLogo.image = UIImage(named: teams[indexL] + "-logo") //隊徽
LSetScore.text = "0" //局分數
LScore.text = "0" //分數

//右隊
indexR = indexB
RTeamLabel.text = teams[indexR] //隊名
RTeamView.backgroundColor = teamColors[indexR] //場地顏色
RTeamLogo.image = UIImage(named: teams[indexR] + "-logo") //隊徽
RSetScore.text = "0" //局分數
RScore.text = "0" //分數

//發球隊
let serve = Bool.random()
LTeamVollyball.isHidden = serve
RTeamVollyball.isHidden = !serve

//勝利隊伍
WinnerView.isHidden = true //隱藏

}

接發球

排球會出現在兩隊其中一邊的場域中,點擊排球圖案模擬接發球、傳球或殺球。每次點擊都會隨機決定這一球的狀況。

接球成功且觸球尚未超過三次,再隨機決定是否把球打過場

若打過場,則排球圖案將會出現在對方的場域中。

若接球失敗或是觸球超過三次,就等於失分,更新計分

    //接發球 - 右隊
func RReceive(){

//觸球+1
RTouchBallCount = RTouchBallCount + 1

//接發成功 & 觸球3次內
if Bool.random() && RTouchBallCount<3 {
ReportLabel.text = teams[indexR] + "第\(RTouchBallCount)次接球成功"
//打過網
if Bool.random(){
ReportLabel.text = teams[indexR] + "殺球過網"
RTouchBallCount = 0 //觸球歸零
//左隊接發準備
LPrepare()
}

//失敗
}else{
ReportLabel.text = teams[indexR] + "失分"
//左隊得分
AddLScore()
}
}

畫面中央的播報文字,會隨著接發球狀態改變。

按下排球按鈕,會隨機顯示該隊的隊員圖片,長按可以保持顯示圖片。

Button的Touch Down Event時顯示隊員圖片,並改變排球顏色

    //Touch Down Event
@IBAction func LTeamActionStart(_ sender: Any) {

//按下時球變成粉紅色
LTeamVollyball.tintColor = UIColor.systemPink

//按下時顯示球員
LTeamPlayer.image = UIImage(named: teams[indexL]+"-Q"+String(Int.random(in: 1...9)))
LTeamPlayer.isHidden = false
}

Button的Touch Up Event時隱藏隊員圖片,也復原排球顏色

Button的Touch Up Event時進行接發球動作

    //Touch Up Event
@IBAction func LTeamActionEnd(_ sender: Any) {

//放開時球變回黃色
LTeamVollyball.tintColor = UIColor.systemYellow

//放開時隱藏球員
LTeamPlayer.isHidden = true

//接發球動作
LReceive()
}

得分規則

每次接發球都要判斷回合是否結束,以及比賽是否結束。因此分別寫成function以利重複呼叫。

正式的排球計分規則為三戰二勝或五戰三勝,每一回合則以先取得25分的隊伍得勝,若比分到達24:24進入duce階段,則要連勝2分才能取勝,但最多不超過30分。在此選擇三戰二勝,由於要玩很久才能到達25分,因此我自行改為15分就能贏得回合^^|||。

    //判斷回合勝隊
func setWinner() -> Bool {
var isSetOver:Bool = true
let lScore:Int = Int(LScore.text ?? "0") ?? 0
let rScore:Int = Int(RScore.text ?? "0") ?? 0

//左隊 取得回合
if (lScore==15 && rScore<=13) //左隊是否15分內取勝
|| (lScore>15 && lScore-rScore==2) //左隊是否duce領先2分取勝
|| lScore==20 { //左隊是否duce達到20分取勝

AddLSetScore()

//右隊 取得回合
}else if (rScore==15 && lScore<=13) //右隊是否15分內取勝
|| (rScore>15 && rScore-lScore==2) //右隊是否duce領先2分取勝
|| rScore==20 { //右隊是否duce達到20分取勝

AddRSetScore()

//回合繼續
}else{
isSetOver = false
}

return isSetOver
}

//判斷賽末勝隊
func finalWinner() -> Bool {
var isGameOver:Bool = true
let lSetScore:Int = Int(LSetScore.text ?? "0") ?? 0
let rSetScore:Int = Int(RSetScore.text ?? "0") ?? 0

//左隊 兩戰兩勝 三戰二勝
if lSetScore==2 && rSetScore<=1{
theWinnerIs(index: indexL)

//右隊 兩戰兩勝 三戰二勝
}else if rSetScore==2 && lSetScore<=1{
theWinnerIs(index: indexR)

//下一回合
}else{
isGameOver = false
}

return isGameOver
}

回合結束

當其中一隊先達到15分或是duce連得贏2分,取得回合勝利,回合成績加1,進入下一回合,下一場開始前要先互換場地,且兩隊得分必須歸零。

    //換場
func switchCourt(){
//左隊 -> temp
let indexTemp:Int = indexL
let scoreTemp:String = LScore.text ?? "0"
let setScoreTemp:String = LSetScore.text ?? "0"

//右隊 -> 左隊
indexL = indexR
LScore.text = RScore.text
LSetScore.text = RSetScore.text

//temp -> 右隊
indexR = indexTemp
RScore.text = scoreTemp
RSetScore.text = setScoreTemp

//隊名及場地
LTeamLabel.text = teams[indexL]
LTeamView.backgroundColor = teamColors[indexL]
RTeamLabel.text = teams[indexR]
RTeamView.backgroundColor = teamColors[indexR]

//發球隊
let serve = Bool.random()
LTeamVollyball.isHidden = serve
RTeamVollyball.isHidden = !serve

ReportLabel.text = ""
}

比賽結束

當回合分數達到三戰二勝或連兩勝,比賽結束!畫面上將會顯示勝利隊伍照片,且宣告最終回合成績。勝利隊伍照片是以View蓋在所有元件最上方,對戰時先隱藏 .isHidden = true,直到比賽結束,帶入勝利隊伍對應的照片,再將View解除隱藏 .isHidden = false。

    //恭喜勝利隊伍
func theWinnerIs(index:Int){
WinnerImageView.image = UIImage(named: "\(teams[index])-win")
WinnerLLabel.text = LTeamLabel.text
WinnerRLabel.text = RTeamLabel.text
WinnerLSetScore.text = LSetScore.text
WinnerRSetScore.text = RSetScore.text
if teams[index]==WinnerLLabel.text {
CrownL.isHidden = false
CrownR.isHidden = true
}else{
CrownL.isHidden = true
CrownR.isHidden = false
}
WinnerView.isHidden = false
}

返回選擇隊伍畫面

比賽對戰畫面的Restart按鈕,及恭喜勝利隊伍畫面的X按鈕,都呼叫同一個function,回到選擇隊伍畫面

Restart返回
右上角X返回
    //back to team
@IBAction func backToTeam(_ sender: Any) {
dismiss(animated: true, completion: nil)
}

心得

排球計分板,原本以為就是 “點擊畫面讓比數增加” 這麼簡單,實作之後才發現其中的規則判斷和邏輯計算,以及畫面上元件的安排和連動,有很多細節需要考量周全,才能讓比賽順利進行XD。另外也覺得程式還有很多重複冗長的地方,目前兩隊的判斷規則都個別寫一次,之後希望可以進一步整合得更精簡一些。

GitHub連結

--

--