原文: Scene Kit Tutorial with Swift Part 2: Nodes
注:这是 《3D iOS 游戏教程》 这本书中一个章节的缩写版,来让您可以来领略一下这本书的内容。希望您喜欢!
这一教程将向你展示如何使用苹果最新的游戏框架-SceneKit,来创建你的第一个 3D 游戏。
在这个一共 5 节的系列教程中,你将会创建你的第一个 SceneKit 游戏: 几何忍者(GeometryFighter) !这是一个类似于 水果忍者 那样的游戏,你来摧毁不断涌上屏幕的几何体来满足自己内心的破坏欲:]
下面是本系列教程文章的导航及简介:
第 1 节,你将会学习到如何新建一个空白的 SceneKit 工程做为一个好的开始。
第 2 节,开始编写游戏,并学习有关 SceneKit 节点的知识。
第 3 节(待更新),学习如何使用 SceneKit 内置的物理引擎来移动你的几何体。
第 4 节(待更新),学习有关 SceneKit 中有关渲染循环的知识,并让你的几何体不断重生。
在最后的第 5 节(待更新)中,你将学习如何将一些非常酷的粒子效果添加到游戏中。
在上一节的学习中,我们了解了如何新建一个空白的 SceneKit 工程做为一个好的开始。
在本节的教程里,我们将开始编写游戏,在这个过程中学习有关 节点(Node) 的知识。
本篇教程从上一节教程结束的地方开始。如果你没有跟上,不要着急 ———— 你可以从这里下载本次教程的 开始程序包 。
Scene Kit 中把所有游戏中的元素以一种 场景图(scene graph) 的方式组织起来。
每一个游戏中的元素(比如灯光、摄像头、粒子发射器等)都被称为一个个的 节点(node) 存储在这种树形结构中。
为了说明它的工作原理,我们来看看下面这张图:
为了进行类比,我们把上面这个图中每一个骨头都想象成一个 节点(node) 。最大的骨头是脊柱,我们认为它是所有骨头的根源,所以它就是 根节点(root node) 。肩骨是连接在脊柱上的,所以我们认为肩骨是脊柱的 子(child) 节点,而脊柱则是肩骨的 父(parent) 节点。
同理可知,大臂的骨头是肩骨的子节点,小臂的骨头是大臂骨头的子节点,手指的骨头是小臂的骨头的子节点。
每一个节点的位置都是相对于它的父节点的。比如,如图中右边所示,要想挥动骷髅的左边的手臂,我们只需要像蓝色箭头这样旋转他的大臂的骨头,所有的子节点(小臂、手指)都会跟着一起旋转了。
恭喜你!你刚刚学习完了人体解剖学的入门课程!:]
从技术的角度来看,一个节点的类型是 SCNNode ,它代表了在三维空间中相对于父节点的位置。节点本身并不包含任何可以显示的内容,在渲染到场景上之后也会消失不见。为了创建可以显示的内容,你需要将其他的组件,比如灯光、摄像机或者几何体(比如骨头)添加到节点上。
场景图中有一个特殊的节点形成了整个节点层级结构的基础,它就是 根节点(root node) 。为了构建游戏场景,我们把节点做为子节点要么加入到根节点中,要么就加入到根节点的其它子节点中。
如果有一天你变成了成功又有钱的 3D 游戏设计者,你可以自己雇佣图形设计师以及音效工程师,这样你就可以把时间全部用来专心致志地写游戏代码了。:] Scene Kit 中的 资源目录(Asset Catalog) 就是设计来专门帮你处理这些代码之外的游戏资源的。
资源目录(Asset Catalog)让我们可以一个单独的文件夹里管理所有的游戏资源;使用它只需要我们在工程中新建一个以 .scnassets 为扩展名的文件夹,然后把所有的游戏资源文件放在那个文件夹里就可以了。Xcode 在编译的时候会自动把所有这个目录下的文件拷贝程序包里,并且保留你文件夹里的层级结构。
这样做的好处在于,你只需要把资源目录文件夹共享给设计师,如果有什么需要修改的问题(比如某只 不-是-那-么-凶-的-重-眼-怪),他们直接在上面修改之后,效果就会出现在你下一次编译的时候了,不需要来回拷来拷去的。
现在你应该明白资源目录的作用是什么了,很快你就要在几何忍者中添加一个这样的目录。
把教程资源文件包中的 GeometryFighter.scnassets 文件夹拖拽到 Xcode 的游戏工程里。在弹出的窗口中,确保 Copy items if needed 、 Create Groups ,以及 Add to targets 中的 GeometryFighter 是选中状态,然后点击 Finish 。
在项目的导航中选择 GeometryFighter.scnassets 这个文件夹,你会注意到在右边的面板中出现了一些独有的选项。展开GeometryFighter.scnassets 及它的子文件夹来查看更多细节:
你会看到在资源目录下有两个文件夹: Sounds (声音)文件夹包含了游戏所需的声音文件, Textures (纹理)文件夹包含了所需的图像。你可以尽情地看看都是些什么内容。
我们刚刚添加好了资源目录,现在需要做一些整理工作,并添加一张合适的图片做为启动屏幕。
首先,在项目的导航栏中单击 Assets.xcassets ,把 GeometryFighter.scnassets/Textures/Logo_Diffuse.png 这个文件拖进资源里,就放在 AppIcon 的下面。
下一步,点击导航栏中的 LaunchScreen.storyboard 。选中 View ,在右边的属性设置栏中,把 Background (背景)这个属性设置为深蓝色(或者任何你喜欢的颜色),如下图:
下一步,将 Logo_Diffuse 这张图片从 Media Library (媒体库)拖到视图的正中。把这张图片的 Mode (拉伸模式)属性设置为 Aspect Fit (等比例缩放),如下图:
干的漂亮,就还差一点点就完成了。现在我们需要添加一些约束来保证图片在所有 iOS 设备上的正常显示。在底部点击 Pin 这个按钮,把四个边的约束都点亮,然后点击底部的 Add 4 Constraints 的按钮,如图所示:
这样我们就添加好了启动屏幕!编译并运行程序;你会看到一个崭新的启动屏幕:
炫酷的启动画面一消失,你就又被扔回到了命运的黑色大门前。现在是时候添加一个漂亮的背景了,这样你就不会感觉自己在盯着一个黑洞了。
为了完成这个,打开 GameViewController.swift 这个文件,将下面这行代码添加到 配置场景()
这个方法的底部:
scn场景.background.contents = "GeometryFighter.scnassets/Textures/Background_Diffuse.png"
这行代码告诉我们的场景去加载资源目录中一个名为 Background_Diffuse.png 的图片,使用它来做我们的背景。
编译并运行;游戏运行后你将看到一个蓝色的背景图:
你现在已经完成了基本的清理工作。你的游戏现在有了一个闪亮的图标、一个耀眼的启动图,外加一个非常漂亮的背景!接下来我们要准备向上面添加并显示 节点(Node) 了。
在开始添加节点之前,你首先需要理解 Scene Kit 的坐标系的有关知识,这样你才能把节点放到想要的位置上。
在诸如 UIKit 和 Sprite Kit 这样的 2D 系统中,你使用一个包含 X 坐标和 Y 坐标的点来描述一个视图(view)或者一个精灵(sprite)的位置。而在三维的空间里,为了确定一个物体的位置,你还需要一个 Z 轴的坐标来描述物体的深度。
来看下面这个示意图:
Scene Kit 使用这种三个轴的坐标系来描述一个物体在三维空间中的位置。红色的方块放置在 X 轴上,绿色的方块放置在了 Y 轴,蓝色的方块放置在了 Z 轴。正中间的灰色的方块代表了 原点(origin) ,坐标为 (x:0, y:0, z:0) 。
Scene Kit 使用 SCNVetor3 这种包含三个向量的数据类型来代表三维空间中的坐标。在代码中我们这样写:
let 位置 = SCNVector3(x:0, y:5, z:10)
这里,我们使用向量 (x:0, y:5, z:10) 来声明了一个变量 位置
。可以使用下面这种方法来轻松地得到每个坐标的数值:
let x = 位置.x let y = 位置.y let z = 位置.z
如果你以前使用过 CGPoint
,那么你应该可以很容易地看出 SCNVetor3
和它的类似之处。
注:节点(Node)加入到场景的时候有一个默认的位置是 (x:0, y:0, z:0) ,记住这里的位置都是指相对于 父节点 而言的。为了把一个节点放置在指定的位置上,我们需要调整其相对于它的父节点(亦称为本地坐标)的位置,而不是相对于原点(世界坐标)的位置。
理解了如何在 Scene Kit 中放置节点之后,你可能很好奇如何真正把内容 显示 在屏幕上。想一下第 1 章中关于拍电影的类比:拍电影的时候,场景上有各种演员和道具,然后我们会放置一个摄像机来拍摄场景,所以最终出来的影像什么样取决于摄像机的位置,我们看到的也是摄像机的视角。
Scene Kit 的工作原理也是类似的。包含 摄像机(camera) 的那个节点的位置决定了我们看场景的视角。
下面这个示意图说明了摄像机是如何工作的:
图中有几个关键术语需要解释一下:
Scene Kit 中 SCNCamera
这个类就代表了摄像头,它有几个属性,其中 xPov
和 yPov
用来调整视野, zNear
和 zFar
用来调整视锥体。
另外,有一点很关键要记住,那就是摄像头只有附属于某个节点时才发挥作用,只有其本身不会做任何事情。
现在我们来试试摄像头。打开 GameViewController.swift 这个文件,在 scn场景
的下面再添加一个属性:
var 摄像头节点: SCNNode!
接下来,在 配置场景()
方法的下面再添加一个新方法:
func 配置摄像头() { // 1 摄像头节点 = SCNNode() // 2 摄像头节点.camera = SCNCamera() // 3 摄像头节点.position = SCNVector3(x: 0, y: 0, z: 10) // 4 scn场景.rootNode.addChildNode(摄像头节点) }
我们来看一下这些代码:
SCNNode
并把它赋值给了变量 摄像头节点
。 SCNCamera
对象并把它赋值给了 摄像头节点
的 camera
这个属性。 摄像头节点
的位置为 (x:0, y:0, z:10) 。 摄像头节点
作为子节点加入到场景的根节点中。 然后我们在 viewDidLoad()
这个方法中调用刚刚添加的这个新方法,在 配置场景
这行的下面再添加一行代码:
配置摄像头()
完成这一步之后并不需要立即运行,因为这时候你看不到什么变化。现在我们的场景中的确添加了摄像头,但是并没有什么东西可供我们拍摄。接下来,为了解决这个问题,让我们添加一些演员到场景来。
为了创建可见的内容,我们需要把几何体添加到节点中。一个几何体代表了一个三维立体的形状,由无数个 交点(Vertices) 构成。
另外,一个几何体可以使用不同的材料来装饰它的表面。 材料(Material) 可以让我们指定物体表面的颜色、材质、对光的敏感度等一系列的视觉效果。一系列交点和材料的组合被称为一个 模型(Model) 或是一个 网(Mesh) 。
Scene Kit 内置了一系列的几何体,也被称为 原型(Primitives) :
前排从左到右是:圆锥体(cone)、圆环体(torus)、胶囊体(capsule)、水管体(tube)。后排从左到右分别是:锥体(pyramid)、立方体(box)、球体(sphere)和圆柱体(cylinder)。
你可以提供自定义的几何体,在之后的章节中将会教你如何去做。
在把几何体添加进场景之前,我们需要创建一个新的 Swift 文件来声明一个名为 形状
的枚举用来定义游戏中用到的各种不同的几何体形状。
右键单击 GeometryFighter 这个组,然后选择 New File ……选择 iOS/Source/Swift File 这个模版,然后单击 Next :
把这个文件命名为 形状.swift ,确保其包含在工程文件里,然后单击 Create 。
文件创建好了之后,打开 形状.swift ,把文件里的内容替换为下列代码:
import Foundation // 1 public enum 形状:Int { case 立方体 = 0 case 球体 case 锥体 case 圆环体 case 胶囊体 case 圆柱体 case 圆锥体 case 水管体 // 2 static func 随机() -> 形状 { let 最大值 = 水管体.rawValue let 随机 = arc4random_uniform(UInt32(最大值+1)) return 形状(rawValue: Int(随机))! } }
上述代码的意思相当直白:
形状
代表不同的几何体类型。 随机()
用来生成随机的形状。这个方法之后会很有用。 下面的任务就是创建一个方法来随机生成 形状 这个枚举中所定义的几何体类型。
在下面的方法添加到 GameViewController.swift 中,就放到 配置摄像头()
的下面:
func 生成几何体() { // 1 var 几何体: SCNGeometry // 2 switch 形状.随机() { default: // 3 几何体 = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0) } // 4 let 几何体节点 = SCNNode(geometry: 几何体) // 5 scn场景.rootNode.addChildNode(几何体节点) }
下面按照注释的顺序一个个地来解释:
几何体
备用。 形状.随机()
这个方法的返回值。现在这里的代码还不完整,只会生成一个正方体。在文章结尾的挑战中,需要你把它来补充完整。 SCNBox
立方体的对象并把它存储在 几何体
中。这里要指定它的宽、高、长,以及斜面角度(也就是所谓的圆角,只不过换了个更高大上的说法)。 SCNNode
的节点的对象并把它命名为 几何体节点
,这次我们使用的是带 geometry
参数的初始化方法,这样生成的节点会自动跟我们提供的几何体链接起来。 现在,我们需要在程序的某处来调用这个方法。在 viewDidLoad()
中,紧挨着 配置摄像头()
这一行的下面,添加下面的代码:
生成几何体()
编译并运行;你会发现屏幕中显示了一个白色的方块。如下图所示:
这里我们可以看到几点:
生成几何体
生成的默认的形状,在场景中位于 (x:0, y:0, z:0) 的位置。 摄像头节点
所在的位置观察这个场景。由于摄像头节点的位置在 (x:0, y:0, z:10) ,所以这个立方体刚好就处在我们摄像头视野的正中间。 没错,我知道这看起来一点也不令人兴奋(而且看上去也不像是 3D 的),但是别着急——下一节会改变一切。
SCNView
内置了一些可以直接拿来使用的功能来让我们的工作轻松一些。
来到 GameViewController.swift 这个文件,在 配置视图()
这个方法的最后,增加以下的代码:
// 1 scn视图.showStatistics = true // 2 scn视图.allowsCameraControl = true // 3 scn视图.autoenablesDefaultLighting = true
下面来解释一下上面的代码在干嘛:
编译并运行;这次事情开始变得有趣起来了!
你可以使用以下的手势来控制场景中活动的摄像机:
在本节的最后,我们留了一个挑战任务来供你实践学习到的知识:完善 生成几何体()
中的 switch 语句,来处理其它的几何体类型。
在苹果官方的 Scene Kit 文档 中查看更多关于不同几何体的说明。同时看看 形状
这个枚举中的各种类型的名字,应该可以给你足够的提示来开始。
不用太担心尺寸的问题;试试让它们看起来跟之前的正方体差不多大小就可以。
如果你完成了挑战,那么恭喜你!你已经牢牢掌握了 Scene Kit 中最基本的一些概念了!
这里是 完成挑战后的代码(链接待更新) 。
就目前而言,您应该继续阅读本系列教程的第三节(待更新),在第三节中,我们将学习如何使用物理引擎让几何体动起来。
本节视频教程地址(链接待更新)
免费直播教程:
译者注:如果您刚开始学习 iOS 开发,希望系统地学习并得到及时的指导,可以考虑来购买我的视频教程: 「从零开始」学习 3D iOS游戏编程
这一教程基于本文提及的 《3D iOS 游戏教程》 这本书,将会教给你制作 3D iOS 游戏中所需要知道的一切知识,在这个过程中你将从头开始制作一系列类似于水果忍者、经典的敲砖块,甚至苹果 WWDC 上和 Apple TV 一起首发的游戏 —— Crossy Road 。
于此同时,如果您有任何问题,欢迎在微博上@方一雄 给我留言或私信。