一個小清新 Swift 游戲的開發全過程(Part 1)
來自: http://vulgur.me/2016/01/23/last-circle-part1/
Last Circle 是我上架的第一個獨立開發的 App(App Store 地址),針對這個 App 的開發全過程,我準備寫三到四篇 blog 來介紹詳細的實現,1元的定價就當是這一系列 blog 的辛苦費吧。
這個游戲并不是我原創的,而是根據多年前偶然看到的一個 Flash 小游戲改造的,源地址已不可考……。自打我學習移動開發以后,一直都拿這個小游戲練手,一開始是用 Android 實現了一個簡單的 Demo,后來學習 iOS 開發又用 Objective-C 實現了一次,再后來為了學習 Swift 又用 Swift 重寫了一遍,并增加了許多交互,在多個朋友的幫助下,最終提交到 App Store,圓了自己一個上架夢。
# 游戲理念(玩法)
Last Circle 的定位是一個極簡的小游戲,也有點虐心+虐眼……。游戲的玩法非常簡單,但是為了保持極簡的風格,App 中我沒有加入游戲說明和新手引導(我其實是寫在了 App Store 的介紹里,但是估計是沒有人仔細看了),導致了很多人一開始都不會玩。Last Circle 是一個測試并鍛煉短時記憶力的小游戲,每一回合都會新產生一個圓(位置上),正確點擊就會進入下一回合,所以需要的就是在所有的圓中找出最新的一個。原版游戲是黑底白圓,為了增加難度我把所有的圓變成了隨機的彩色,并且在顯示順序上也是隨機的。說起來可能很簡單,但是玩起來還是比較困難的,黑白模式下我可以玩到27回合,但是彩色模式下我自己最多玩到21回合。
#基礎建設
九層之臺,起于累土。這個游戲做得最多的就是畫圓,所以要先把這部分搞定。用 Objective-C 寫的時候,我是用 UIView 的 - (void)drawRect:(CGRect)rect 來畫圓的,后來考慮到針對每個圓要有動畫的交互,所以重寫的時候就改為用一個 UIView 來表示一個圓。
本篇主要就是講如何設計一個圓的 model,并根據 model 來布局 UI,其他的交互放在下一篇。
這里涉及的類有三個:
- 圓的 model 就是 Circle
- 圓對應的 view 就是 CircleView
- 還有一個單例的工廠類 CircleFactory
- 最后是負責展示并處理操作的 CircleViewController
#Circle
Circle 類的作用是代表一個圓的實體信息,包括三個屬性:顏色,圓心坐標和半徑。還有一個 class function 是用來生成一個隨機的圓,首先計算一個隨機的半徑(在一定的范圍內,不同的屏幕尺寸下這個范圍是不一樣的),然后在合理的屏幕區域內隨機一個點作為圓心。代碼如下:
class Circle { // MARK: Properties static var minRadius: Int { switch ScreenUtils.screenWidthModel() { case .Width320, .Width375, .Width414, .Other: return 20 case .Width768: return 40 case .Width1024: return 60 } } static var maxRadius: Int { switch ScreenUtils.screenWidthModel() { case .Width320, .Width375, .Width414, .Other: return 50 case .Width768: return 100 case .Width1024: return 150 } } var color: UIColor let radius: Int let center: CGPoint init(color:UIColor, radius:Int, center:CGPoint){ self.color = color self.radius = radius self.center = center } class func randomCircle() -> Circle { let screenRect = UIScreen.mainScreen().bounds let screenWidth:CGFloat = screenRect.width let screenHeight:CGFloat = screenRect.height let randomRadius = minRadius + Int(arc4random_uniform(UInt32(maxRadius - minRadius + 1))) let areaWidth = Int(screenWidth) - (randomRadius << 1); let areaHeight = Int(screenHeight) - (randomRadius << 1) - 20; let x = randomRadius + Int(arc4random_uniform(100000)) % areaWidth let y = 20 + randomRadius + Int(arc4random_uniform(100000)) % areaHeight // below the status bar let randomPoint = CGPoint(x: x, y: y) let randomColor = ColorUtils.randomColor() let circle = Circle(color: randomColor, radius: randomRadius, center: randomPoint) return circle } }
</div>
#CircleView
CircleView 類是 Circle 對象在 UI 上的體現,實現上只是增加了一個自定義的初始化方法,參數是一個 Circle 對象,根據其信息設置 view 的大小、位置和顏色,并且通過 layer 將 view 設置成圓形。代碼如下:
class CircleView: UIView { // MARK: Init override init(frame: CGRect) { super.init(frame: frame) } init(circle: Circle) { let frame = CGRectMake(0, 0, CGFloat(circle.radius*2), CGFloat(circle.radius*2)) super.init(frame: frame) self.backgroundColor = circle.color self.center = circle.center self.layer.cornerRadius = CGFloat(circle.radius) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
</div>
#CircleFactory
CircleFactory 類是一個單例工廠類,維持一個 Circle 的數組,在每次游戲重新開始的時候會清空這個數組,并且通過 addCircle 向這個數組中添加新的 Circle 對象。添加新圓的邏輯很簡單,就是一個無限循環,直到算出來一個合格的圓為止。判斷圓是否可用的方法是 isCircleAvailable ,就是判斷兩個圓是否相交。但是屏幕區域畢竟是有限的,不可能會一直找到一個可用的圓,經過我的測試,圓的個數大于40的時候每次計算的耗時就會顯著增加,界面也會因為計算而卡住,好在這個游戲我還沒發現有那個超級大腦可以通過30回合(我朋友靠作弊才到26),所以游戲目前來說還不會遇到上述的極端情況。有個常量 MaxCircleCount 我設置成了40,期初的想法是游戲的終結就是第40回合,不過目前還沒有用到。代碼如下:
class CircleFactory: NSObject{ // MARK: Properties static let MaxCircleCount = 40 static let sharedCircleFactory = CircleFactory() let RadiusGap:Float = 10 var circles = [Circle]() private override init() { self.circles.removeAll() } func addCircle() { while true { let aCircle = Circle.randomCircle() if isCircleAvailable(aCircle) { self.circles.append(aCircle) break } continue } } func isCircleAvailable(aCircle: Circle) -> Bool { for circle in self.circles { let distance = hypotf( Float(aCircle.center.x - circle.center.x), Float(aCircle.center.y - circle.center.y)) let radiusLength = Float(aCircle.radius + circle.radius) if distance <= radiusLength + RadiusGap { return false } } return true } }
</div>
#CircleViewController
CircleViewController 在加載的時候遍歷 CircleFactory 的 Circle 數組,生成相對應的 CircleView ,并加入到 view hierarchy 中。同時, CircleViewController 有一個保存 CircleView 的數組 circleViews ,這個數組的用處主要是給所有的 circle view 添加交互的動畫。 CircleViewController 還有一個 lastCircleView 用來保存最后一個 circle view。這里的代碼有一個 Swift 相較于 Objective-C 的新操作符 === ,它比較的就是兩個對象的引用是否相等,其相反的操作符是 !== 。
override func viewDidLoad() { super.viewDidLoad() ... for circle in CircleFactory.sharedCircleFactory.circles { let color = ColorUtils.randomColor() circle.color = color let cv = CircleView(circle: circle) if circle === CircleFactory.sharedCircleFactory.circles.last! { self.lastCircleView = cv } self.view.addSubview(cv) circleViews.append(cv) ... } }
</div>
判斷是否正確點擊了最新的圓,就是在 func touchesEnded(_ touches: Set<UITouch>, withEvent event: UIEvent?) 判斷點擊是否在 lastCircleView 中,同時我也做了一定的容錯,畢竟手指不是鼠標。這里為了防止多次點擊,用了一個 bool 變量 touched 來判斷是否已經點擊過。
// MARK: Touch override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { ... if touched { let touch = touches.first! let point = touch.locationInView(self.view) let sizeSide = CGFloat(CircleFactory.sharedCircleFactory.circles.last!.radius) + tolerance var roundedRect = lastCircleView.frame roundedRect.origin.x -= tolerance roundedRect.origin.y -= tolerance roundedRect.size.width += 2*tolerance roundedRect.size.height += 2*tolerance let maskPath = UIBezierPath(roundedRect: roundedRect, byRoundingCorners: UIRectCorner.AllCorners, cornerRadii: CGSizeMake(sizeSide, sizeSide)) if maskPath.containsPoint(point) { // go to next round } else { // go to game over } touched = false } }
</div>
#后記
這篇 blog 主要是介紹了整個游戲的核心模型,代碼貼的比文字還多,可能比較枯燥。下一篇就不會這樣了,我準備在 Part 2 著力介紹這個游戲的各種動畫效果,肯定是圖文并茂,敬請期待!
</div>