iOS小游戏开发程序员iOS 学习地图

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

2016-01-12  本文已影响343人  sing_crystal

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

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

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

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

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

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

一、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在不同的应用上差别太大。

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.

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,只写名字即可

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)
        }
     }
}

主页支持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吗?

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)}

好啦,终于结束啦~

下篇文章见~

上一篇下一篇

猜你喜欢

热点阅读