2018-01-08

游戏逻辑框架

和上一个游戏不同,这次用中文编写代码,可以让我这个初学者,更好的理解框架逻辑的组成方式。首先在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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容