美文网首页
2018-01-08

2018-01-08

作者: Ericoool | 来源:发表于2018-01-08 14:18 被阅读0次

    游戏逻辑框架

    和上一个游戏不同,这次用中文编写代码,可以让我这个初学者,更好的理解框架逻辑的组成方式。首先在GameViewController.swift中创建场景,只用到两个方法,一个定义了场景的基本参数,并传入SKView;另一个是隐藏手机顶部状态栏。

    class GameViewController: UIViewController {
      override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
            
        if let sk视图 = self.view as? SKView {
          if sk视图.scene == nil {
          //  创建场景
            let 长宽比 = sk视图.bounds.size.height / sk视图.bounds.size.width
            let 场景 = GameScene(size:CGSize(width: 320, height: 320 * 长宽比))
            sk视图.showsFPS = true
            sk视图.showsNodeCount = true          //显示场景中节点数量,也就是元素数量
            k视图.showsPhysics = false           //显示物理模型的轮廓
            sk视图.ignoresSiblingOrder = true     //忽略加入场景的元素的先后顺序
                    
            场景.scaleMode = .aspectFill          //等比咧缩放
                    
            sk视图.presentScene(场景)              //加入视图
            }
          }
      }
        
      override func prefersHomeIndicatorAutoHidden() -> Bool {           //手机顶部的状态栏是否隐藏?
        return true
        }
    }
    

    后来我发现,最新的Xcode9.0在系统提供的方法中,好像已经基本预设好了。在GameScene.swift中也预设了很多方法,不过,还是先全部删除了,自己慢慢手打一遍。好了,剩下的代码会全部在GameScene.swift中完成。所有的执行代码都在一个类(class)内实现,执行的默认代理SKScene和后加入的SKPhysicsContactDelegate(物理碰撞代理):

    class GameScene: SKScene, SKPhysicsContactDelegate { }
    

    GameScenc类里面,在缺省的几个方法下面:override func didMove(程序启动时)、override func touchesBegan(点击屏幕时)、override func update(程序运行时),要放入相应的执行方法来实现。

    1.在程序启动时,需要调用切换主菜单()方法:
    let 世界单位 = SKNode()
    override func didMove(to view: SKView) {
      //关掉重力
      physicsWorld.gravity = CGVector(dx: 0, dy: 0)
      //设置碰撞代理
      physicsWorld.contactDelegate = self
            
      addChild(世界单位)
      切换到主菜单()
      }
    

    而在切换主菜单()中继续调用其它几个方法:

    func 切换到主菜单() {
      当前的游戏状态 = .主菜单
      设置背景()
      设置前景()
      设置主菜单()
      }
    
    2.在点击屏幕时,用到了switch判断语句,在设定的当前游戏状态中,分别执行不同的动作,很强大简洁:
    var 当前的游戏状态: 游戏状态 = .游戏
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      guard let 点击 = touches.first else {
      return
      }
      let 点击位置 = 点击.location(in: self)
            
      switch 当前的游戏状态 {
        case .主菜单:
          if 点击位置.y < size.height * 0.15 {
            去学习()
            } else if 点击位置.x < size.width/2 {
            切换到教程状态()
            } else {
            去评价()
            }
          break
        case .教程:
          切换到游戏状态()
          break
        case .游戏:
          主角飞一下()
          break
        case .跌落:
          break
        case .显示分数:
          break
        case .结束:
          切换到新游戏()
          break
        }
      }
    

    就是在不同状态下,当你点击屏幕,你希望能发生的所有动作,用switch判断语句还能添加更多的状态。

    3.在程序运行过程中,同样也用了switch
    var 上一次更新时间: TimeInterval = 0
    var dt: TimeInterval = 0
    override func update(_ 当前时间: TimeInterval) {
      if 上一次更新时间 > 0 {
        dt = 当前时间 - 上一次更新时间
        } else {
        dt = 0
        }
      上一次更新时间 = 当前时间
            
      switch 当前的游戏状态 {
        case .主菜单:
          break
        case .教程:
          break
        case .游戏:
          更新主角()
          更新前景()
          撞击障碍物检查()
          撞击地面检查()
          更新得分()
          break
        case .跌落:
          更新主角()
          撞击地面检查()
          break
        case .显示分数:
          break
        case .结束:
          break
        }
      }
    

    这就是游戏的大框架,里面调用的所有的方法,同样写在class大类中,但在class之外,先要定义二个enum(枚举)和一个struck(结构体):

    enum 图层: CGFloat {
        case 背景
        case 障碍物
        case 前景
        case 游戏角色
        case UI
    }
    
    enum 游戏状态 {
        case 主菜单
        case 教程
        case 游戏
        case 跌落
        case 显示分数
        case 结束
    }
    
    struct 物理层 {
        static let 无: UInt32 = 0            //0二进制
        static let 游戏角色: UInt32 = 0b1   //1
        static let 障碍物: UInt32 = 0b10   //2
        static let 地面: UInt32 = 0b100   //4
    }
    

    图层枚举里,系统默认由小到大区分前后顺序,象ps中的图层一样,背景在最下面,上面是障碍物、前景和游戏角色。定义好了就可以在下面的方法中给背景z坐标赋值:

        背景.zPosition = 图层.背景.rawValue
    
    4.然后在class类中需要定义的常量和变量,用于给方法中参数赋值,当然,也可以在方法中定义,但集中写在一起,方便阅读和修改数值。
    let k前景地面数 = 2
    let k地面移动速度:CGFloat = -150.0
    let k重力: CGFloat = -1000.0
    let k上冲速度: CGFloat = 300.0
    var 速度 = CGPoint.zero
        
    let k底部障碍最小乘数: CGFloat = 0.1
    let k底部障碍最大乘数: CGFloat = 0.6
    let k缺口乘数: CGFloat = 4.0
    let k首次生成障碍延迟: TimeInterval = 1.75
    let k每次重生障碍延迟: TimeInterval = 1.5
    let k动画延迟 = 0.3
        
    let k顶部留白: CGFloat = 20.0
    let k字体名字 = "AmericanTypewriter-Bold"
    var 得分标签: SKLabelNode!
    var 当前分数 = 0
        
    var 撞击了地面 = false
    var 撞击了障碍物 = false
    var 当前的游戏状态: 游戏状态 = .游戏
        
    let 世界单位 = SKNode()
    var 游戏区域起始点: CGFloat = 0
    var 游戏区域的高度: CGFloat = 0
    let 主角 = SKSpriteNode(imageNamed: "Bird0")
    var 上一次更新时间: TimeInterval = 0
    var dt: TimeInterval = 0
        
    //  创建音效
    let 拍打的音效 = SKAction.playSoundFileNamed("flapping.wav", waitForCompletion: false)
    let 撞击地面的音效 = SKAction.playSoundFileNamed("hitGround.wav", waitForCompletion: false)
    let 摔倒的音效 = SKAction.playSoundFileNamed("whack.wav", waitForCompletion: false)
    let 下落的音效 = SKAction.playSoundFileNamed("falling.wav", waitForCompletion: false)
    let 得分的音效 = SKAction.playSoundFileNamed("coin.wav", waitForCompletion: false)
    let 乒的音效 = SKAction.playSoundFileNamed("pop.wav", waitForCompletion: false)
    let 叮的音效 = SKAction.playSoundFileNamed("ding.wav", waitForCompletion: false)
    

    比如,在点击屏幕时,游戏状态下调用的让主角飞一下()方法,用到了变量:var 速度 = CGPoint.zero、常量:let k上冲速度: CGFloat = 300.0和常量:let 拍打的音效 =...

    func 主角飞一下() {
      速度 = CGPoint(x: 0, y: k上冲速度)
      run(拍打的音效)
      }
    

    剩下的工作流程就是添加场景元素,让场景循环移动,造成游戏主角在向前飞行的视觉假象,下面是关于不断生成障碍物的三个方法,第一个先创建障碍物并设置物理属性,第二个是在场景里生成障碍,位置、间距,第三个是让障碍无限重生:

    func 创建障碍物(图片名: String) -> SKSpriteNode{
      let 障碍物 = SKSpriteNode(imageNamed: 图片名)
      障碍物.zPosition = 图层.障碍物.rawValue
      障碍物.userData = NSMutableDictionary()        //初始化用户数据
            
      障碍物.physicsBody = SKPhysicsBody(rectangleOf: 障碍物.size)
      障碍物.physicsBody?.categoryBitMask = 物理层.障碍物
      障碍物.physicsBody?.collisionBitMask = 0                    //关闭系统提供的碰撞处理
      障碍物.physicsBody?.contactTestBitMask = 物理层.游戏角色       //打开碰撞检测
    
      return 障碍物
      }
        
    func 生成障碍() {
      let 底部障碍 = 创建障碍物(图片名: "CactusBottom")
      let 起始X坐标 = size.width + 底部障碍.size.width/2
      let Y坐标最小值 = (游戏区域起始点 - 底部障碍.size.height/2) + 游戏区域的高度 * k底部障碍最小乘数
      let Y坐标最大值 = (游戏区域起始点 - 底部障碍.size.height/2) + 游戏区域的高度 * k底部障碍最大乘数
      底部障碍.position = CGPoint(x: 起始X坐标, y: CGFloat.random(min: Y坐标最小值, max: Y坐标最大值))
      底部障碍.name = "底部障碍"
      世界单位.addChild(底部障碍)
            
      let 顶部障碍 = 创建障碍物(图片名: "CactusTop")
      顶部障碍.zRotation = CGFloat(180).degreesToRadians()
      顶部障碍.position = CGPoint(x: 起始X坐标, y: 底部障碍.position.y + 底部障碍.size.height/2 + 顶部障碍.size.height/2 + 主角.size.height * k缺口乘数)
      顶部障碍.name = "顶部障碍"
      世界单位.addChild(顶部障碍)
            
      let X轴移动距离 = -(size.width + 底部障碍.size.width)
      let 移动持续时间 = X轴移动距离 / k地面移动速度
      let 移动的动作队列 = SKAction.sequence([
        SKAction.moveBy(x: X轴移动距离, y: 0, duration: TimeInterval(移动持续时间)),
        SKAction.removeFromParent()
        ])
      顶部障碍.run(移动的动作队列)
      底部障碍.run(移动的动作队列)
      }
        
    func 无限重生障碍() {
      let 首次延迟 = SKAction.wait(forDuration: k首次生成障碍延迟)
      let 重生障碍 = SKAction.run(生成障碍)
      let 每次重生间隔 = SKAction.wait(forDuration: k每次重生障碍延迟)
      let 重生的动作队列 = SKAction.sequence([重生障碍, 每次重生间隔])
      let 无限重生 = SKAction.repeatForever(重生的动作队列)
      let 总的动作队列 = SKAction.sequence([首次延迟, 无限重生])
      run(总的动作队列, withKey: "重生")
      }
    

    在第二个生成障碍的方法中,先放置底部障碍,它的y坐标需要随机产生底部障碍.position = CGPoint(x: 起始X坐标, y: CGFloat.random(min: Y坐标最小值, max: Y坐标最大值)),这段代码用到教程中事先写好的模版代码,因为教程的编译是swift2.0版的,在swift4.0下大量报错,我就先把模版删除了,结果,这段random(min: Y坐标最小值, max: Y坐标最大值)不出意外的报错,提示没有.random的方法,在网上查了半天,才突然想起被删除的模版,又只有尴尬的找回模版,慢慢的修改了80多个版本升级后的报错,才找到这段代码:

    // Returns a random floating point number in the range min...max, inclusive.
    public static func random(min: CGFloat, max: CGFloat) -> CGFloat {
      assert(min < max)
      return CGFloat.random() * (max - min) + min
      }
    

    目的是将随机结果转换成CGFloat。好了,成功的随机生成了障碍。
    后面顶部障碍.zRotation = CGFloat(180).degreesToRadians()还用到了一段解决将图形旋转180的方法,也是模版提供的:

    //Converts an angle in degrees to radians.
    public func degreesToRadians() -> CGFloat {
      return π * self / 180.0
      }
    

    网上也有其它旋转图片的方法,好像这个比较简单,可以直接输入角度参数。


    关于分数的存储,提供一段固定代码,用于写入磁盘,这样程序重启后也能保存最高分:

    func 最高分() -> Int {
      return UserDefaults.standard.integer(forKey: "最高分")
      }
    func 设置最高分(最高分: Int) {
      UserDefaults.standard.set(最高分, forKey: "最高分")
      UserDefaults.standard.synchronize()
      }
    

    还有一个很方便移除元素的方法,比如:在加载了教程的一些元素在场景中,之后要开始游戏,就要移除教程元素,可以先给所有教程元素都命名为“教程”,然后在移除的时候,通过全局匹配名字,同时移除。

    func 设置教程() {
      let 教程 = SKSpriteNode(imageNamed: "Tutorial")
      教程.position = CGPoint(x: size.width * 0.5, y: 游戏区域的高度 * 0.4 + 游戏区域起始点)
      教程.name = "教程"
      教程.zPosition = 图层.UI.rawValue
      世界单位.addChild(教程)
            
      let 准备 = SKSpriteNode(imageNamed: "Ready")
      准备.position = CGPoint(x: size.width * 0.5, y: 游戏区域的高度 * 0.7 + 游戏区域起始点)
      准备.name = "教程"
      准备.zPosition = 图层.UI.rawValue
      世界单位.addChild(准备)
      }
    
    func 切换到游戏状态() {
      当前的游戏状态 = .游戏
      世界单位.enumerateChildNodes(withName: "教程", using: {匹配单位, _ in
        匹配单位.run(SKAction.sequence([
        SKAction.fadeOut(withDuration: 0.05),
        SKAction.removeFromParent()
        ]))})
      无限重生障碍()
      主角飞一下()
      }
    

    世界单位.enumerateChildNodes(withName: "教程", using: {匹配单位, _ in 匹配单位.run(SKAction.sequence([ SKAction.fadeOut(withDuration: 0.05), SKAction.removeFromParent()]))})......好长啊,这是一个block,找到“教程”这个匹配单位,就执行动作:淡出fadeOut(0.05秒),从父视图中移除。


    最后,有个实现图片在屏幕上闪烁的动画效果,其实就是让如片先放大1.02,再缩小0.98:

     // 学习按钮的动画
    let 放大动画 = SKAction.scale(to: 1.02, duration: 0.75)
      放大动画.timingMode = .easeInEaseOut
            
    let 缩小动画 = SKAction.scale(to: 0.98, duration: 0.75)
      缩小动画.timingMode = .easeInEaseOut
            
    学习.run(SKAction.repeatForever(SKAction.sequence([
      放大动画,缩小动画
      ])))
    

    学习过程还真充满乐趣,每次实现一个效果,解决一个问题,总是令人开心,“加油吧,少年!”(my son's pet phrase)

    相关文章

      网友评论

          本文标题:2018-01-08

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