一个小清新 Swift 游戏的开发全过程(Part 1)

ClaudetteEh 6年前

来自: 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>