美文网首页iOS小游戏开发程序员iOS 学习地图
【胸有成竹】Bull's Eye(来自iOS Appre

【胸有成竹】Bull's Eye(来自iOS Appre

作者: sing_crystal | 来源:发表于2016-01-12 21:07 被阅读343次

    首先了解要开发的这个游戏有哪些规则和功能,游戏是如何进行的,然后才能知道怎么进行开发。也就是平时产品部门的工作。这里我们有一份简单的产品描述:

    出现我们要出现一个随机整数作为目标数值,随机整数范围在1-100之间,然后用户滑动这个Slider,这个Slider数值在1到100之间,尽可能的滑动到能够代表目标数值的位置,也就是用户凭感觉来滑动了,比如App显示目标数值是80,那用户需要凭感觉将Slider滑动到代表80的位置,点击Hit Me按钮,弹出提示框,程序读取用户滑动位置的实际数值,这个位置的实际数值越接近目标数值,那么得分越高,提示框中显示用户的表现和得分,每点击一次Hit Me,游戏就进行了一局,局数加1。点击提示框中的确定按钮,提示框消失,然后得分和局数的数值就会加入刚刚完成的这局游戏的值。还有重现开始的按钮,点击这按钮,所有的得分和局数都清零。还有一个关于我们的按钮,点击会跳转到关于我们的页面。

    刚刚完成了产品的工作,那么就需要设计师进行设计切图了,下面就是设计师给的最终效果图:

    首页最终设计效果图 关于我们最终设计效果图

    拿到切图文件和最终效果图后,我们就可以进行代码的逻辑设计。理顺一下自己的思路和步骤,一步步来,就能轻松完成开发。

    下面这个清单是作者的写的Todo清单,作者建议不管开发什么应用,在开发之前,都要写一个Todo清单,有了这个清单,能够事半功倍。一开始写的不全没有关系,但是一定要写一个。我把作者写的清单简单翻译了一下:

    • 放上按钮Hit Me,点击后出现提示框,告诉用户的表现和游戏结以及计算出来的得分。
    • 放上Label得分score和局数round,根据用户的游戏情况显示相对应的得分和局数,这两个Label的文案是可以根据情况变化的。
    • 放上滑动条Slider,滑动数字范围在1到100之间。
    • 点击Hit Me后可以读取到Slider的数值。
    • 生成一个随机数作为目标数字,显示在界面上,让用户滑动Slider尽可能接近这个目标数字。
    • 对比Slider数值和目标数字之间的差距,得出得分score,同时局数round加1,显示在提示框里。。
    • 放上一个Start Over按钮,点击清空之前的得分score和局数round。
    • 让这个App只能横屏显示。
    • 完善界面,使用设计切图达到效果图的效果,适配各个机型。

    一、Storyboard中搭建界面、基础设置

    拿到设计图或者产品部的线框图后,我们就可以根据线框图或设计图布局App的界面了。

    1.新建工程,设置App支持的方向仅限横屏(工程详细信息中General->Deployment Info->Device Orientation)。

    2.打开Storyboard,点击ViewControllerScene。
    1)把Supported Device Orientations设置为横向(选中controller->Attribute Inspector->Orientation);
    2)模拟器也改成横向显示;
    3)不勾选Use Size Classes;
    4)选中Scene设置好对应的.swift文件(Identity Inspector中的Class);
    5)放入Button、Label、Slider等各种控件,修改控件的Text属性,也就是显示文字;
    6)Slider的值范围设置为1-100,当前值是0;
    操作完成后的样子见下图:

    应用首页

    3.关于我们页面。
    1)从Object Library库中拖入一个View Controller,这个是关于我们页面;
    2)把Supported Device Orientations设置为横向(选中controller->Attribute Inspector->Orientation);
    2)放入需要的控件Text View和Button,修改Text属性改变控件显示文案,Text View不勾选Editable选项;
    3)新建文件,选择CocoaTouch类型->Subclass为UIViewController。
    4)选中Scene设置好对应的.swift文件(Identity Inspector中的Class);
    操作完成后的样子见下图:

    关于我们

    5)Control拖拽法创建Segue,Segue:modal,Transition:Flip Horizontal

    创建Segue

    4.创建Outlet和Action连接
    首先分析一下,哪些控件需要创建连接,Outlet有:目标数值,得分Score,局数Round和滑动条Slider;所有的Button控件都建立Action连接。
    打开Assistant Editor,给需要建立连接的控件建立相对应的Outlet和Action连接(Control拖拽法)。
    1)Outlet连接

      @IBOutlet weak var targetLabel: UILabel!
      @IBOutlet weak var scoreLabel: UILabel!
      @IBOutlet weak var roundLabel: UILabel!
      @IBOutlet weak var slider: UISlider!
    

    2)Action连接
    首页的Slider控件,Event选择Value Changed,Type选择UISlider:

    @IBAction func sliderMoved(sender: UISlider) {
       
    }
    

    首页的Hit Me控件,Event选择Touch Up Inside,Type选择AnyObject:

    @IBAction func showAlert(sender: AnyObject) {
    
    }
    

    首页的Start Over控件,Event选择Touch Up Inside,Type选择AnyObject:

    @IBAction func startOver(sender: AnyObject) {
    
    }
    

    关于我们页面的Close按钮选择Touch Up Inside,选择AnyObject。

    @IBAction func close(sender: AnyObject) {
    
    }
    

    二、写代码

    通过上面的步骤,我们的布局完成了,我们需要理顺一下逻辑关系,好写代码了。

    1.创建实例变量。
    根据首页上的布局,有一个目标数值Label会显示在App中,有一个ScoreLabel显示分数,有一个局数Round显示分数,这三个都需要实例变量,根据游戏规则需要获取Slider中的实际数值对比目标数值,算出得分,所以还需要一个实际数值变量,所以我们需要四个实例变量(目标数值、当前实际数值、分数、局数):

    1)一个代表目标值的变量,整型类型,初始值是0。

    var targetValue : Int = 0
    

    2)一个代表Slider当前的实际数值的变量,整型类型,初始值是0(要和Storyboard中当前值对应)。

    var currentValue : Int = 0
    

    3)一个能记录分数的变量,整型类型,初始值是0

    var score = 0
    

    4)一个能记录局数的变量,整型类型,初始值是0

    var round = 0
    

    2.显示随机整数+开启新游戏方法。

    这个游戏的开头是现有目标数值(随机整数)然后才有后面的操作,那么,我们要保证程序启动时以及开启新的一局游戏时,这个目标数值都会变化。那么,开启新的一局游戏时,除了要更新目标数值外,还需要做什么事情呢?想一想,要把Slider的值重新调回到0的位置,Round局数也要增加1。我们把这些事情都集合到一个方法中,命名startNewRound:

    func startNewRound() {
        //获取新的目标数值
        targetValue = 1 + Int(arc4random_uniform(100))
        //Slider的值调整到0的位置
        currentValue = 0
        slider.value = Float(currentValue)
        //新的局数要加1
        round += 1
    }
    

    程序员启动后,是不是也需要做这些事情呢?那么把这个方法放在viewDidLoad中。

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

    3.Slider的值。

    上一步我们设计了目标数值,用户看到了这个目标数值,接下来就是滑动Slider,滑到某个位置。程序需要获取这个位置所代表的数值,然后和目标数值对比,方能算出得分。那么,接下来就需要我们写一个获取Slider值的方法,之前在建立Action连接时,Slider的值一有变化,就会触发事件:

    @IBAction func sliderMoved(sender: UISlider) {
        currentValue = lroundf(sender.value)
    }
    

    4.点击Hit Me按钮。

    好了,目标数值有了,当前数值也有了,可以开始做对比了吧。用户滑动结束后,点击Hit Me,程序会进行对比、计算、显示结果,显示结果用弹出框表示。用户只会看到弹出框显示的结果,出结果之前的对比计算需要我们在代码中进行,但是不显示出来。同时我们在文案上可以设计一下,根据不同的得分段,在提示框中显示不同的话。用户看到结果后,弹出框有个OK按钮,点击OK按钮,此局游戏结束,开始新的一局,同时,App中的分数和局数以及目标数值,都需要更新,分数加上刚刚这局的得分,局数也增加1,显示新的目标数值。那么接下来的代码需要在点击Hit Me按钮的方法中编写,还好我们之前已经建立了Action连接showAlert方法,用户每次Touch Up Inside,都会触发事件:

    1)先写计算过程:

    @IBAction func showAlert(sender: AnyObject) {
    
        //对比差值
        let difference = abs(targetValue - currentValue)
        //计算分数,在此规则下,用户猜的再差也能得1分
        let points = 100 - difference
        //把这次的得分加入到总分中
        score += points
    
    }
    

    2)再写提示框:

    点击OK其实代表两件事情,此局游戏结束,开始新的一局。那么可以用到我们之前的方法startNewRound,但是这个方法只是让代码更新了,显示在Label上的内容没有变化,用户没有看到这个变化,所以我们写一个方法updateLabels,把所有的Label更新文案的事情都放这里面,这样当用户点击OK的时候,可以直接调用这个方法:

    func updateLabels() {
        targetLabel.text = String(targetValue)
        scoreLabel.text = String(score)
        roundLabel.text = String(round)
    }
    

    App启动时,也需要更新Label,不然会显示我们在搭建界面时胡乱输入的文案了,那么把这个方法放在viewDidLoad中。

    override func viewDidLoad() {
        super.viewDidLoad()
        startNewRound()
        updateLabels() 
    }
    

    然后我们开始写提示框代码,注意要判断一下用户的得分属于哪个阶段,对应不同的提示语:

    @IBAction func showAlert() {
        let difference = abs(targetValue - currentValue)
        let points = 100 - difference
        score += points
    
        //开始提示框代码
    
        //判断得分的不同阶段,给出不同的提示语
        let title: String
        if difference == 0 {
            title = "Perfect!"
        } else if difference < 5 {
            title = "You almost had it!"
        } else if difference < 10 {
            title = "Pretty good!"
        } else {
            title = "Not even close..."
        }
       
        let message = "You scored \(points) points"
        let alert = UIAlertController(title: title, message: message,preferredStyle: .Alert)
    
        //用户点击OK时,用了闭包语法,这样只有在用户点击OK后,这两个方法才会被调用
        let action = UIAlertAction(title: "OK", style: .Default, handler: { action in
          self.startNewRound()
          self.updateLabels()
        })
    
        alert.addAction(action)
        presentViewController(alert, animated: true, completion: nil)
    }
    

    点击Hit Me按钮写到这里就结束了。

    5.点击Start Over按钮。

    点击Start Over后,局数要清零,分数要清零,Slider的位置也要归位,各个Label上显示的内容也要清零,这些事情,都在上面两个方法startNewGame()和updateLabels()中做过了,所以我们可以直接调用这两个方法:

    @IBAction func startOver() {
        score = 0
        round = 0
        startNewGame()
        updateLabels()
      }
    

    书中写到这里时,把App第一次启动后的效果,等同于点击了Start Over,完全开启新的一轮游戏。这是因为书中没有数据持久化的教程,毕竟是入门书籍。但是,如果我们已经会了数据持久化,再来优化这个App时,App启动后,就不一定是开启新的游戏了,有可能只是开启新的一局游戏而已。所以这个viewDidLoad()里用哪些方法,还要根据产品需求来决定。一般要考虑几个方面:App启动后,App进入后台后,App被强行关闭后,这三个地方一定要考虑一下如何对持久化的数据进行处理,不然就会出现用户游戏进度(用户数据)改变或者没有保存的情况。

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        return true
      }
    func applicationWillEnterForeground(application: UIApplication) {
        // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
      }
    func applicationWillTerminate(application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
      }
    

    在AppDelegate.swift文件中,还有几种情况,也要考虑一下才好。

    6.点击Close按钮。

    App中目前就还剩下一个按钮的方法没有写了,那就是关于我们中的Close按钮。点击Close按钮,回到首页。鉴于Segue是Modal,所以我们使用 dismissViewControllerAnimated(),这个方法要写在关于我们的.swift文件中,不能写在首页的.swift文件里:

    @IBAction func close() {
        dismissViewControllerAnimated(true, completion: nil)
    }
    

    三、增加规则

    1.如果用户的猜测水平在Perfect阶段,则再加100分作为奖励,如果在You almost had it阶段,则再加50分作为奖励:

    @IBAction func showAlert() {
    
        let difference = abs(targetValue - currentValue)
        var points = 100 - difference
    
        let title: String
        if difference == 0 {
            title = "Perfect!"
            points += 100
        } else if difference < 5 {
            title = "You almost had it!"
            if difference == 1 {
                points += 50
            }
        } else if difference < 10 {
            title = "Pretty good!"
        } else {
            title = "Not even close..."
        }
        
        //因为上面points还会根据不同的情况变化,所以一定要在points不再有变化后再加到score中,所以把这行放到这个位置
        score += points
    
        let message = "You scored \(points) points"
        let alert = UIAlertController(title: title, message: message,preferredStyle: .Alert)
    
        let action = UIAlertAction(title: "OK", style: .Default, handler: { action in
          self.startNewRound()
          self.updateLabels()
        })
    
        alert.addAction(action)
        presentViewController(alert, animated: true, completion: nil)
    }
    

    四、几个疑问

    1.为什么单独写一个startNewGame()方法?
    2.startNewRound()和updateLabels()为啥要分成两个方法,能不能写在一方法里?

    这篇文章中的步骤和一些代码并没有完全按照书中的来,比如书中还有一个startNewGame()方法,我直接把这方法写在了@IBAction func startOver(sender: AnyObject)方法里了。因为我觉得没有必要写startNewGame这个方法。至于作者为什么这么写,我暂时不理解,欢迎达人指点。

    还有一处地方就是startNewRound()和updateLabels()这两个方法,为什么不写在同一个方法中?
    我的理解是一个代表旧的一局游戏结束更新Label显示出结束的信息,一个代表的开始新的一局,程序内部已经准备好新的数据了。可是书中的代码显示,都是startNewRound()在前,updateLabels()在后,所以我的这个理解也就错了。
    那么,真正的原因是什么呢?

    还好这个问题我已经写邮件直接问作者了,感动的是,当时我是2015年12月31号晚上写的邮件, 没想到作者时隔5个小时就回复了,马上就过元旦了,还要回复读者邮件,好感动。

    如果你曾经看过这本书,那么,请把你的理解告诉我,评论或者私信皆可,我会分享给你作者的回复原因。如果你没有看过这本书,那么,实在是没有必要知道原因啊,因为您都不知道我在问啥问题啊亲们!

    五、完善外表

    书中这部分,从106页一直写到结束150页,着实让我知道了开发App大部分的工作都用到了哪里,150页的书籍,1/3都在写完善界面的方法。看来要达到设计稿的效果,还需要程序员做出特别多的努力,花很多的时间。

    需要考虑的有:设计图等设计效果,动画,自动布局AutoLayout也就是适配多个设备

    看来做出一些细节优化神马的,确实比较花费时间。剩下的这部分可以略过不看了,我纯粹是整理自己的思路,没有太多干活。而且AutoLayout和AdaptiveLayout在不同的应用上差别太大。

    • 横屏时去掉status bar:

    Main.storyboard -> select the View Controller -> Attributes inspector -> Simulated Metrics -> Status Bar -> None.

    Project Settings screen and under-> Deployment Info -> Status BarStyle -> Hide status bar.

    • 设计切图导入Xcode
      只开发iPhone端App,只需要放入@2x和@3x即可。

    • 拖宅Image View控件,放入背景图片,设置属性(width568,Height320),Editor->Arrange->Send to Back(或在outline pane中拖动)。在About View Controller中进行同样的操作

    • 修改Label控件效果:Color+Shadow+Shadow Offset+Font+FontStyle+FontSize+Autoshrink+Size to Fit

    • 修改Button控件效果:
      1)
      Hit Me按钮+关于我们页面的Close按钮:
      State Config为Default时的设置:Size Inspector中的Width+Height,Attribute Inspector下的Type->Custom + Background + Font + FontSize + FontStyle + TextColor + Shadow Color
      2)
      Hit Me按钮+关于我们页面的Close按钮:
      State Config为Highlighted时的设置:Attribute Inspector下 Background + TextColor + Shadow Color + Reverses on Highlight
      3)
      Start Over按钮+i按钮(关于我们按钮):
      Type->Custom + 去掉text中文案 + Image + Background + Width + Height

    • Slider
      在ViewController.swift文件中,把代码输入到viewDidLoad()方法中:

    let thumbImageNormal = UIImage(named: "SliderThumb-Normal")
    slider.setThumbImage(thumbImageNormal, forState: .Normal)
    let thumbImageHighlighted = UIImage(named: "SliderThumb-Highlighted")
    slider.setThumbImage(thumbImageHighlighted, forState: .Highlighted)
    let insets = UIEdgeInsets(top: 0, left: 14, bottom: 0, right: 14)
    if let trackLeftImage = UIImage(named: "SliderTrackLeft") {
        let trackLeftResizable = trackLeftImage.resizableImageWithCapInsets(insets)
        slider.setMinimumTrackImage(trackLeftResizable, forState: .Normal)
    }
    if let trackRightImage = UIImage(named: "SliderTrackRight") {
        let trackRightResizable =trackRightImage.resizableImageWithCapInsets(insets)
        slider.setMaximumTrackImage(trackRightResizable, forState: .Normal)
    }
    

    在输入图片名称时,可以不写@2x和.png,只写名字即可

    • 关于我们页面使用web view控件展示HTML内容
      1)
      storyboard中删掉Text view放入web view控件然后建立Outlet连接,在Project Navigator中右键新建文件Add Files to "BullsEye",选中BullsEye.html文件点击Add完成新建。
      2)
      在AboutViewController.swift文件中,把代码输入到viewDidLoad()方法中:
    override func viewDidLoad() {
        super.viewDidLoad()
        if let htmlFile = NSBundle.mainBundle().pathForResource("BullsEye",ofType: "html") {
            if let htmlData = NSData(contentsOfFile: htmlFile) {
                let baseURL = NSURL(fileURLWithPath:NSBundle.mainBundle().bundlePath)
                webView.loadData(htmlData, MIMEType: "text/html",textEncodingName: "UTF-8", baseURL: baseURL)
            }
         }
    }
    
    • 使用Preview预览不同设备下的效果

    • 用Auto Layout适配不同的设备
      1)
      主页和关于我们页面的背景图Image view控件:
      Align ->( Horizontally in Container + Vertically in Container ) -> Update Frames Items of New Constraints -> Add...
      2)
      关于我们页面的Close按钮:
      Align -> Horizontally in Container -> Add...,Pin -> Spacing to nearest neighbor -> Check Constrain to margins-> down bar 20 -> Update Frames Items of New Constraints -> Add...
      3)
      关于我们页面的web view控件:
      Pin -> Spacing to nearest neighbor -> Uncheck Constrain to margins -> ( left bar 20 + up bar 20 + right bar 20 + down pin 20 ) -> Update Frames Items of New Constraints -> Add...

    主页支持3.5-inch和4-inch:
    选中控件(见下图)点击Editor->Embed In -> View。刚刚嵌入的View起名叫做container view。


    Embed In View

    给container view设置: Pin -> ( Width491 + Height285 ) -> Add...,Align ->( Horizontally in Container + Vertically in Container ) -> Update Frames Items of New Constraints -> Add...
    最后设置container view的Background color属性为Clear Color
    5)
    支持iPhone 6和 6 Plus:
    删除LaunchScreen.storyboard,到Project Settings->App Icons and Launch Images ->清空Lunch Screen File;按住Option键,点击Product -> Clean Build Folder -> Clean;在Project Navigator中右键新建文件Add Files to "BullsEye",选中Default@2x.png和Default-568h@2x.png文件点击Add完成新建。
    我的小疑问:这个步骤有必要吗?这样就真可以适配iPhone 6和6 Plus吗?

    • 淡入淡出效果Crossfade
      在点击Start Over按钮后,增加动画效果:
      在ViewController.swift文件中:
    import QuartzCore
    

    修改StartOver的方法:

    @IBAction func startOver() {
        startNewGame()
        updateLabels()
    
        let transition = CATransition()
        transition.type = kCATransitionFade
        transition.duration = 1
        transition.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
        view.layer.addAnimation(transition, forKey: nil)}
    
    • 增加图标icon
      虽然此应用没有适配iPad,但是并不能阻止iPad运行此应用,iPad上会出现iPhone大小的框,在框里运行此应用,所以之前的图片没有1x,但是icon最好能够适配iPad,也就是需要1x的icon。

    • 修改应用在手机上显示的名字
      Project Navigator -> Info.plist -> Editor -> Add Item -> Bundle display name -> 输入你想要的名字

    • 在真机上运行测试

    好啦,终于结束啦~

    下篇文章见~

    相关文章

      网友评论

      • 死神一护:x楼主写的胸有成竹,比很多干巴巴的翻译好太多了
        sing_crystal:@死神一护 谢谢~争取接下来几本书也这样梳理一下
      • 闭家锁:好文,看懂原文之后,行文流畅,直抒胸臆,比一字不落,照单翻译强多了
        sing_crystal:@闭家锁 谢谢~争取接下来几本书也这样梳理一下
      • 曾樑:这一篇翻译下来挺花时间吧
        sing_crystal:@曾樑 这篇不是译文哦,我把这本书看完后,在想,如果是我,我该如何开发书中介绍的这款应用,按什么思路和顺序来完成每一步?为什么需要写这一行代码?为什么要进行这个设置?然后,有了这篇文章。这做法让我想起胸有成竹这个典故了,故此标题中有了胸有成竹标注

      本文标题:【胸有成竹】Bull's Eye(来自iOS Appre

      本文链接:https://www.haomeiwen.com/subject/yvyihttx.html