原文: Scene Kit Tutorial with Swift Part 4: Render Loop
注:这是 《3D iOS 游戏教程》 这本书中一个章节的缩写版,来让您可以来领略一下这本书的内容。希望您喜欢!
这一教程将向你展示如何使用苹果最新的游戏框架-SceneKit,来创建你的第一个 3D 游戏。
在这个一共 5 节的系列教程中,你将会创建你的第一个 SceneKit 游戏: 几何忍者(GeometryFighter) !这是一个类似于 水果忍者 那样的游戏,你来摧毁不断涌上屏幕的几何体来满足自己内心的破坏欲:]
下面是本系列教程文章的导航及简介:
第 1 节,你将会学习到如何新建一个空白的 SceneKit 工程做为一个好的开始。
第 2 节,开始编写游戏,并学习有关 SceneKit 节点的知识。
第 3 节,学习如何使用 SceneKit 内置的物理引擎来移动你的几何体。
第 4 节,学习有关 SceneKit 中有关渲染循环的知识,并让你的几何体不断重生。
在最后的第 5 节中,你将学习如何将一些非常酷的粒子效果添加到游戏中,并最终完成这个游戏。
本节是第四节,在本节的教程中,你将学习有关 SceneKit 中有关渲染循环的知识,并让你的几何体不断重生。
本篇教程从上一节教程结束的地方开始。如果你没有跟上,不要着急 ———— 你可以从这里下载本节教程的 开始程序包 。
在上一节的教程中,我们给生成的几何体添加了物理引擎,然后给了一个冲击力把它扔到了空中。然后在模拟的重力作用下,物体最终还是慢慢落下然后消失不见。
现在这个效果看起来还不错,但是如果我们能同时生成很多的几何体,让他们一起飞向空中,还能互相碰撞,那就 更 棒了。那样的话我们这个游戏的有趣程度绝对又会上升了一个档次!
现在,我们的游戏只会调用 生成几何体()
这个方法一次。为了生成多个几何体,我们需要循环地多次调用 生成几何体()
这个方法。下面将隆重介绍…… 渲染循环(Render Loop) !
在之前的章节中我们讲过,Scene Kit 使用 SCNView 来渲染你的内容。 SCNView 中有一个 delegate(代理) 属性,这个属性设置的对象需要服从一个名为 SCNSceneRendererDelegate 的协议。设置好之后, SCNView 在之后每一帧的动画和渲染的过程中,当某一个事件发生时,就会调用 SCNSceneRendererDelegate 协议中相关的方法。
用这种方式,你可以窥见 Scene Kit 渲染一个场景中的每一个步骤。这些渲染的步骤组成了 渲染循环 。
所以 —— 到底都 有 哪些步骤?来看一下下面这张渲染循环的示意图:
这是 幸运大转盘 吗?:] 不,这只是关于渲染循环中 9 个步骤的一个简单的示意图。如果一个游戏的帧率是 60 FPS,猜猜看这些步骤将运行多少次?没错,你猜对了,一秒钟 60 次。
我们可以把游戏的逻辑代码准确地加入到需要的地方,因为这些步骤总是按照下面的顺序运行:
renderer(_: updateAtTime:)
这个方法。这里是我们放入游戏场景更新的逻辑代码的好时机。 renderer(_: didApplyAnimationsAtTime:)
这个方法。在这时,所有的节点都完成了这一帧需要完成的动画。 renderer(_: didSimulatePhysicsAtTime:)
这个方法。在这时,所有的节点都完成了这一帧需要完成的物理模拟。 renderer(_: willRenderScene: atTime:)
这个方法。在这时,视图马上要开始渲染场景了,所以任何需要放到最后的改变都应该放在这里。 renderer(_: didRenderScene: atTime:)
。这标志着一次渲染循环的结束;我们可以把任何需要在下一次循环开始前执行的代码放在这里。 由于渲染循环是一个,嗯, 循环 。所以这是我们放置 生成几何体
的完美地点 —— 接下来的任务就是要决定应该放到哪个步骤里。
现在是时候把这个炫酷的功能添加到游戏中了。
首先,我们需要让 GameViewController
这个类遵守 SCNSceneRendererDelegate
这个协议。把下面的代码添加到 GameViewController.swift 这个文件的底部:
// 1 extension GameViewController: SCNSceneRendererDelegate { // 2 func renderer(renderer: SCNSceneRenderer, updateAtTime 当前时间: NSTimeInterval) { // 3 生成几何体() } }
接下来让我们仔细看看上面的代码:
GameViewController
添加了一个扩展(extension),让它遵守一个协议,并把这部分代码单独组织在了一个代码块中。 renderer(_: updateAtTime:)
的一个实现。 生成几何体()
来生成一个新的几何形状。 这是我们侵入 Scene Kit 渲染循环的第一步。但是在视图调用这个代理方法之前,它首先要知道我们的 GameViewController
现在是视图的代理了。
在 配置视图()
方法的最后添加这样一行代码:
scn视图.delegate = self
这行代码将 Scene Kit 的视图的 delegate
设置给了 self
。现在当渲染循环开始后,视图就可以调用我们在 GameViewController
中写好的代理方法了。
最后,我们做一点清理工作:把 viewDidLoad()
中的 生成几何体()
这行代码删除掉。因为我们已经在渲染循环中生成几何体了,所以这里就没必要再写它了。
编译并运行;感受渲染循环不端生成的怒火吧!:]
游戏开始后疯狂地生成几何体,最终导致所有的几何体都撞在了一起 —— 好棒!:]
到底发生了什么?因为我们在渲染循环的每一更新时都调用了一次 生成几何体()
这个方法,所以每秒钟将生成 60 个几何体 —— 如果设备的性能足够强大以 60 FPS 来运行游戏的话。但是性能稍弱的设备(包括模拟器)都支持不了这个速度。
游戏开始运行后,你会发现底部的帧率会突然下降很多。这是因为图形处理器不止要处理不断增加的几何体,同时还要计算物体间的碰撞模拟,这都会大幅地降低帧率。
现在事情有点不受控制了,因为我们现在的游戏在很多设备上的表现都不会很好。
为了更好地提高用户体验,你需要使用好 时间 。哦,不,我不是说你要花更多的时间来写游戏的代码!:]我的意思是,你需要让时间在各个设备上都保持一致;这样无论设备本身的帧率如何,你的动画都将保持一个固定的频率。
使用 定时器 是很多游戏中都会用到的一个常用技术。还记得在更新的代理方法中传入的参数 updateAtTime 吗?这个参数代表了当前的系统时间。如果我们跟踪记录这个参数,我们就可以计算出游戏中流逝的时间,进而控制生成几何体的频率,比如每三秒钟生成一个,而不是现在这样越快越好。
几何忍者中将使用一个简单的定时器来随机生成几何体,同时这个生成时间的间隔应该大到足够任何设备的处理器能正常运行。
在 GameViewController
中增加一个新的属性,就放在 摄像头节点
的下面:
var 重生时间: NSTimeInterval = 0
我们将使用这个属性来决定下次生成几何体的时间。
为了解决当前持续生成几何体的问题,把 renderer(_: updateAtTime:)
这个方法中的内容替换成下面的:
// 1 if 当前时间 > 重生时间 { 生成几何体() // 2 重生时间 = 当前时间 + NSTimeInterval(Float.random(min: 0.2, max: 1.5)) }
按照注释的顺序来解释: 1. 首先检查 当前时间
是否大于 重生时间
。如果是,那么就生成一个新的几何体;否则,什么都不做。
2. 生成几何体之后,更新 重生时间
为 下一次 生成新几何体的时间。而下一次重生的时间就是当前的时间再加上一段随机的时间。因为 NSTimeInterval
是以秒为单位的,我们下一次生成几何体的时间就是当前时间的 0.2 秒到 1.5 秒之间。
编译并运行,来看看定时器的作用:
相当梦幻,不是吗?
事情开始变的可以控制了,同时几何体的生成还是随机的。但是你就不好奇当这些几何体都跌落并消失在视野之外以后将发生什么吗?
生成几何体()
持续地在场景中添加新的子节点 —— 但是从不删除旧的,即使是那么已经看不见的。Scene Kit 会尽可能保持游戏地平稳运行,但是那并不代表你可以忘记你的孩子们。你说你这算哪门子父母呀?!:]
为了让游戏尽可能地高效运行并保持理想的帧率,我们需要把视野之外的节点移除。想想在什么地方做这个清理工作最合适呢?啊哈,对啦,就是渲染循环!很方便,是不是?
当物体到达我们的边界时,我们就要把它移出场景。
将下面这个方法添加到 GameViewController
这个类的底部,就放在 生成几何体()
的下面:
func 清理场景() { // 1 for 节点 in scn场景.rootNode.childNodes { // 2 if 节点.presentationNode.position.y < -2 { // 3 节点.removeFromParentNode() } } }
下面来解释一下上面的代码在干什么:
presentationNode
这个属性。这个属性是只读的,不要尝试修改它! 为了使用上面这个方法,在 renderer(_: updatedAtTime:)
那个方法中, if
之后,增加这样一行代码:
清理场景()
还有一件事情:默认设置中,当没有动画需要执行时 Scene Kit 会进入一个“暂停”的状态。为了阻止它的放生,我们需要打开视图的 playing
这个属性。
在 配置视图()
这个方法的底部添加一行代码:
scn视图.playing = true
这行代码让 Scene Kit 的视图一致保持运行状态。
编译并运行;当屏幕中的物体开始下落时,拉远你的摄像头来看它们是怎么消失不见的:
下落到 Y 轴边界(图中红线)之下的物体会被清除掉。这样肯定要比让它们一直躺在你的 iPhone 的底部好多了。:]
这里是 完成后的代码 。
就目前而言,您应该继续阅读本系列教程的最后的第五节,在那里,你将学习如何将一些非常酷的粒子效果添加到游戏中,最终完成这个游戏。
本节视频教程: 地址
译者注:如果您刚开始学习 iOS 开发,希望系统地学习并得到及时的指导,可以考虑来购买我的视频教程: 「从零开始」学习 3D iOS游戏编程
这一教程基于本文提及的 《3D iOS 游戏教程》 这本书,将会教给你制作 3D iOS 游戏中所需要知道的一切知识,在这个过程中你将从头开始制作一系列类似于水果忍者、经典的敲砖块,甚至苹果 WWDC 上和 Apple TV 一起首发的游戏 —— Crossy Road 。
于此同时,如果您有任何问题,欢迎在微博上@方一雄 给我留言或私信。