四子棋小遊戲 — Connect 4 Game App

ytchao
海大 SwiftUI iOS / Flutter App 程式設計
9 min readMar 19, 2022

GitHub 連結

Demo 影片

0:03 PVP 遊戲、示範拖曳手勢效果
0:13 遊戲結果訊息動畫、計時器暫停、記分板變化
0:17 PVP 第二回合、遊戲內調整音量 (按鈕可開關)
0:32 點擊遊戲內重新開始按鈕、重整棋盤、剩餘步數及計時器 (不包含記分板)
0:43 點擊首頁按鈕,開始新遊戲,包含記分板清空
0:48 選擇 PVE 模式,點選設定按鈕、設定玩家顏色及音量
1:02 開始 PVE 遊戲,Player 2 改為 Environment、因自動下棋方式為隨機,因此示範故意使電腦勝利

功能需求

  • App Icon、App 名稱
  • Launch Screen
  • 判斷遊戲勝負及和局並標示成功連線的棋子、顯示雙方剩餘步數、顯示目前輪到誰 ( PVP模式:玩家對戰玩家 )
1. Player 1 勝利 (橫)、2. Player 2 勝利 (豎)、3. Player 1 勝利 (反斜線)
4. Player 2 勝利 (正斜線)、5. 和局、6.倒計時結束的和局

加分功能

  • 實作兩種模式,跟電腦 PK 和跟朋友 PK,玩家可選擇要玩哪一種模式。(基本功能已有 PVP 模式,故以下為 PVE 模式:玩家對戰電腦)
function oneTurn 會進行丟下棋子以及判斷勝負的動作,並回傳一個 Bool 值表示此次下子是否成功 (若欲下子的 column 內棋子已滿便會無法下子)若玩家成功下子而未使遊戲結束(未達成連線),且目前為 PVE 模式,則 0.5 秒後於已記錄的空餘 column 中隨機擇一下子。
  • 玩家可選擇棋子的顏色或圖片。
  • 動畫。
  1. 棋子掉落
GridView 中的第一個 Circle 為要掉落的棋子,以改變其 offset 來進行掉落。function oneTurn 會進行丟下棋子以及判斷勝負的動作,並回傳一個 Bool 值表示此次下子是否成功 (若欲下子的 column 內棋子已滿便會無法下子)。在丟下棋子時,會先將目標棋子上移 270,並在經過 0.1 秒後恢復至原位。透過 withAnimation(spring(dampingFraction: 0.8)) 產生掉落動畫,並在掉落後出現輕微擺盪效果。

2. 顯示勝負訊息

JudgeAlert 在物件生成時將 offset 設為 200,使得訊息 Button 初始位置偏下。
在 Button.onAppear 中會將 offset 恢復原位,並使用 .animation 產生上移效果。
  • 音效。
  • 背景音樂。
將背景音樂及音效的檔案放入 project。背景音樂需要循環播放,因此選擇使用 AVQueuePlayer 及 AVPlayerLooper。
因為此物件會在最一開始就生成,且只會生成一次,所以直接在生成時開始播放。
因音效須可以與背景音樂同時播放,所以要再宣告一個 player2。
音效不需要重複播放,因此我選擇使用 AVPlayer。
function oneTurn 中,在棋子的 offset 被調回 0,也就是棋子下降後,使用 .seek(to: .zero) 以及 .play() 讓音效從頭播放一次。
  • 計時。
在 Game property 中宣告需要的變數:
- timer: 計時器
- timeUp: 用來顯示 Alert
- countDownTime: 欲倒數的時間 (120秒:2分鐘,GIF 中設為 10 秒)
- timerLabel: 剩餘時間轉換成字串來顯示 (2分鐘,GIF 中設為 10 秒)
- formatter: 轉換字串的 Formatter ("HH:mm:ss")
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in
// 此處的程式將每過 1 秒執行一次
// 因此若仍有剩餘時間且遊戲未結束,則每秒將剩餘時間減 1 秒,並重新轉換字串
// 反之則將計時器停止
// 如果時間結束而遊戲未結束,則宣布遊戲結果為因時間結束而平手
}

自主添加功能

  • 鏤空圖形 (製作方型中間鏤空圓型的圖案:HoleShapeMask,並拼出棋盤。製作此種圖形才能實現棋子從棋盤後方掉落的效果)。
Color.blue
.frame(height: 45)
.mask(HoleShapeMask().fill(style: FillStyle(eoFill: true)))

struct HoleShapeMask: Shape {
func path(in rect: CGRect) -> Path {
var shape = Rectangle().path(in: rect)
shape.addPath(
Circle()
.path(in: CGRect(
x: rect.minX,
y: rect.midY / 2.65,
width: rect.width, height: rect.height*0.65
))
)
return shape
}
}
  • 手勢
對棋盤中的每一格添加拖曳手勢,並傳入格子所在 column 及 column 寬度 (= 棋盤寬度 / column 數)每次拖曳幅度改變時,計算目前觸摸位置的 column
(= 目前 column + 四捨五入(移動幅度的 x值 / column 寬度),後者為計算目前觸摸位置距離初始觸摸位置 向左(x為負) 或 向右(x為正) 移動了多少 columns)
若計算後的 column 數值合理 (未超過棋盤範圍),則更改 Game property 中記錄的目標 column,以利後續計算 preview 的位置。
  • 顯示目標 column (移動的倒三角形)
因棋盤使用 LazyVGrid 製作,故製作一個單列但與棋盤等寬 (相同 column 數)的 LazyVGrid。在此 LazyVGrid 內擺滿倒三角圖形,而除了參數 col 以外,其他的 column 中的圖形都進行 hidden,便能使圖形仍佔據空間卻不顯示,只顯示參數 col 中的倒三角形。此參數 col 將傳入 Game property 中的目標 Column,因上層中 Game 為 StateObject,且其 property 為 Published,因此雖然參數 col 不是 State 變數也仍會改變畫面。
  • 顯示目標 preview
在手勢中設定好 Game property 的目標 Column 後,就可以計算 preview 的位置了。如果目標 Column 仍有空餘的位置才能進行 preview。
第一步須先清空上次的 preview,接著計算本次的目標位置 (我使用的棋盤為一維陣列)。
最後根據目前下棋玩家來設定 preview 的對象。
  • 漸層背景
// 使用玩家的顏色來製作遊戲結果訊息的漸層背景.background(LinearGradient(colors: [setting.playerColor[0], setting.playerColor[1]], startPoint: .topLeading, endPoint: .bottomTrailing))
  • 自訂字型 (用於首頁)

--

--