一個小清新 Swift 游戲的開發全過程(Part 1)

ClaudetteEh 8年前發布 | 13K 次閱讀 Swift Apple Swift開發

來自: 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,其他的交互放在下一篇。

這里涉及的類有三個:

  1. 圓的 model 就是 Circle
  2. 圓對應的 view 就是 CircleView
  3. 還有一個單例的工廠類 CircleFactory
  4. 最后是負責展示并處理操作的 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>

 本文由用戶 ClaudetteEh 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!