版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.20 星期六 |
前言
SceneKit
使用高级场景描述创建3D游戏并将3D内容添加到应用程序。 轻松添加动画,物理模拟,粒子效果和逼真的基于物理性的渲染。接下来这几篇我们就详细的解析一下这个框架。感兴趣的看下面几篇文章。
1. SceneKit框架详细解析(一) —— 基本概览(一)
2. SceneKit框架详细解析(二) —— 基于SceneKit的简单游戏示例的实现(一)
3. SceneKit框架详细解析(三) —— 基于SceneKit的简单游戏示例的实现(二)
4. SceneKit框架详细解析(四) —— 基于SceneKit的简单游戏示例的实现(三)
开始
在这部分中,您将学习如何通过Scene Kit
渲染循环使几何体逐渐产生。
在上一篇中,您为生成的对象启用了基本物理,并施加了冲击以将其踢到空中。最终,由于重力的模拟效应,物体倒下并消失。
尽管效果很好,但是产生多个彼此碰撞的物体会更加cool。这肯定会让兴奋因素上升一个档次!
现在,你的游戏只调用一次spawnShape()
。要生成多个对象,您需要重复调用spawnShape()
。
正如您在之前的教程中所了解到的,SceneKit使用SCNView
对象渲染场景的内容。 SCNView有一个delegate
属性,您可以将其设置为符合SCNSceneRendererDelegate
协议的对象;然后,当每个帧的动画和渲染过程中发生某些事件时,SCNView
将调用该委托上的方法。
通过这种方式,您可以轻松进入SceneKit渲染场景的每个帧时所采用的步骤。这些渲染步骤构成了渲染循环。
那么 - 这些步骤究竟是什么?好吧,这是渲染循环的快速细分:
这是命运之轮吗? 不,它只是渲染循环的九个步骤的描述。在一个以60 fps运行的游戏中,所有这些步骤都会按顺序运行 - 你猜对了 - 每秒60次。
这些步骤始终按以下顺序执行,这使您可以将游戏逻辑准确地注入所需的位置:
- 1) Update - 更新:视图在其委托上调用
renderer(_: updateAtTime:)
。这是放置基本场景更新逻辑的好地方。 - 2) Execute Actions & Animations - 执行动作和动画:SceneKit执行所有动作并执行所有附加动画到场景图中的节点。
- 3) Did Apply Animations - 应用动画:视图调用其委托的
renderer(_: didApplyAnimationsAtTime:)
。此时,场景中的所有节点都根据应用的动作和动画完成了一个帧的动画。 - 4) Simulates Physics - 模拟物理:SceneKit将物理模拟的一个步骤应用于场景中的所有物理实体。
- 5) Did Simulate Physics - 完成模拟物理:视图在其委托上调用
renderer(_: didSimulatePhysicsAtTime:)
。此时,物理模拟步骤已完成,您可以添加任何依赖于上面应用的物理的逻辑。 - 6) Evaluates Constraints - 评估约束:SceneKit评估并应用约束,这些约束是您可以配置的规则,以使SceneKit自动调整节点的转换。
- 7) Will Render Scene - 将渲染场景:视图在其委托上调用
renderer(_: willRenderScene: atTime:)
。此时,视图即将渲染场景,因此应在此处执行任何最后一分钟的更改。 - 8) Renders Scene In View - 在视图中渲染场景:SceneKit在视图中渲染场景。
- 9) Did Render Scene - 完成渲染场景:最后一步是视图调用其委托的
renderer(_: didRenderScene: atTime:)
。这标志着渲染循环的一个循环的结束;你可以把任何游戏逻辑放在这里,需要在进程重新开始之前执行。
因为渲染循环是一个循环,所以它是调用spawnShape()
的最佳位置。你的工作是决定在哪里注入spawn
逻辑。
The Renderer Delegate - 渲染代理
现在是时候将这个很酷的功能用于游戏中。
首先,通过将以下内容添加到GameViewController.swift
的底部,使GameViewController
类符合SCNSceneRendererDelegate
协议:
// 1
extension GameViewController: SCNSceneRendererDelegate {
// 2
func renderer(_ renderer: SCNSceneRenderer,
updateAtTime time: TimeInterval) {
// 3
spawnShape()
}
}
细分上面的代码:
- 1) 这为
GameViewController
添加了一个扩展,遵循协议,并允许您在单独的代码块中维护协议方法。 - 2) 这增加了
renderer(_: updateAtTime:)
协议方法的实现。 - 3) 最后,调用
spawnShape()
在委托方法中创建一个新形状。
这将为您提供第一次挂钩到SceneKit
的渲染循环。 在视图可以调用此委托方法之前,首先需要知道GameViewController
将充当视图的委托。
通过在setupView()
的底部添加以下行来完成此操作:
scnView.delegate = self
这会将SceneKit
视图的代理设置为self
。 现在,视图可以在渲染循环运行时调用您在GameViewController
中实现的委托方法。
最后,通过删除viewDidLoad()
中对spawnShape()
的单个调用,稍微清理一下代码;因为你现在在渲染循环中调用方法,所以不再需要它了。
构建并运行
游戏开始并产生了大量的物体,导致了碰撞物体的撞击。
那么这里发生了什么? 由于您在渲染循环的每个更新步骤中调用spawnShape()
,因此您每秒会生成60个对象 - 如果您运行的设备可以以60 fps支持您的游戏。 但是功能较弱的设备(包括模拟器)无法支持该帧速率。
随着游戏的运行,您会注意到帧速率的快速下降。 图形处理器不仅必须处理越来越多的几何体,物理引擎必须处理越来越多的碰撞,这也会对帧速率产生负面影响。
目前情况有点失控,因为你的游戏在所有设备上的表现都不尽如人意。
Spawn Timers - 产生定时器
要使设备之间的游戏体验保持一致,您需要利用时间。 不,我不是说花更多的时间来写你的游戏!相反,你需要使用时间的推移作为设备间的一个常数;这使您可以以一致的速率设置动画,无论设备可以支持的帧速率如何。
定时器是许多游戏中的常用技术。 还记得传递给update delegate
方法的updateAtTime
参数吗? 该参数表示当前系统时间。 如果您监视此参数,则可以计算游戏的已用时间等内容,或者每三秒生成一个新对象,而不是尽可能快无限的生产。
Geometry Fighter
将使用一个简单的计时器以任意处理器应该能够处理的随机时间间隔生成对象。
将以下属性添加到GameViewController
中cameraNode
下面:
var spawnTime: TimeInterval = 0
您将使用它来确定生成另一个形状之前的时间间隔。
要修复连续生成,请使用以下内容替换整个renderer(_: updateAtTime:)
:
// 1
if time > spawnTime {
spawnShape()
// 2
spawnTime = time + TimeInterval(Float.random(min: 0.2, max: 1.5))
}
下面进行细分:
- 1) 您检查
time
(当前系统时间)是否大于spawnTime
。 如果是这样,产生一个新的形状;否则,什么也不做。 - 2) 生成对象后,下次更新
spawnTime
以生成新对象。 下一个生成时间只是当前时间增量随机量。 由于TimeInterval
以秒为单位,因此您会在当前时间之后的0.2秒到1.5秒之间生成下一个对象。
构建并运行,检查你的计时器的差异:
事情看起来更容易管理,形状随机产生。 但是,你难道不是很好奇所有这些物体掉出视线后会发生什么?
Removing Child Nodes - 删除子节点
spawnShape()
不断地将新的子节点添加到场景中 - 但是它们永远不会被移除,即使它们不在视线之外。 SceneKit做了很棒的工作,让事情尽可能长时间保持平稳运行,但这并不意味着你可以忘记你的子节点。
要以最佳性能级别和帧速率运行,您必须删除看不见的对象。 还有什么比这更好的地方 - 渲染循环! 很好的处理地方,不是吗?
一旦对象达到其边界的极限,您应该将其从场景中删除。
将以下内容添加到GameViewController
类的末尾,就在spawnShape()
下面:
func cleanScene() {
// 1
for node in scnScene.rootNode.childNodes {
// 2
if node.presentation.position.y < -2 {
// 3
node.removeFromParentNode()
}
}
}
这是上面代码中所做的事情:
- 1) 首先,您只需创建一个
for
循环,逐步遍历场景根节点中的所有可用子节点。 - 2) 由于此时物理模拟正在进行中,您不能简单地查看对象的位置,因为这反映了动画开始前的位置。 SceneKit在动画期间维护对象的副本并进行,直到动画完成。 一开始理解这是一个奇怪的概念,但你不久就会看到它是如何工作的。 要在动画制作动画时获取对象的实际位置,请使用
presentationNode
属性。 这纯粹是只读的 - 不要试图修改此属性的任何值! - 3) 这行代码使一个对象不存在。
要使用上面的方法,在renderer(_: updatedAtTime:)
内部if语句之后调用cleanScene()
:
cleanScene()
还有最后一件事要补充。 默认情况下,如果没有要播放的动画,SceneKit会进入“ paused”
状态。 要防止这种情况发生,您必须在SCNView
实例上启用playing
属性。
将以下代码行添加到setupView()
的底部:
scnView.isPlaying = true
这会强制SceneKit
视图进入无限play的模式。
构建并运行;随着你的物体开始下降,捏合缩小,看看它们在哪里消失:
落在较低y边界上的对象(在上面的屏幕截图中用红线表示)将从场景中删除。 这比将所有物体放在设备的暗凹处更好。
后记
本篇主要讲述了基于SceneKit的简单游戏示例的实现,感兴趣的给个赞或者关注~~~