转载

使用Swift和SceneKit开发一片圣诞树林

过去几年中,移动应用像风暴一样席卷世界,改变了我们的网上工作、娱乐方式。很多移动应用开发技术应运而生,而移动也开始得到开发过程重视。尽管移动已经看似无所不在,但未来才刚刚开始。新一代的移动设备,好比可穿戴设备,好比物联网的许许多多移动构件,就在我们面前。我们会发现:用于数据展示和命令接收的用户界面不断地推陈出新;越来越多的公司站在真正的移动浪潮之巅。这会在未来几年中影响软件的设计、开发、测试方式。

InfoQ 的这篇文章是快速变革的移动技术 系列 的一部分。你可以在 这里 订阅该系列的新文章通知。

今年,随着Swift编程语言及其1.0版的发布,苹果允许向AppStore提交用Swift开发的iOS应用,为iOS编写应用从未如此简单。这篇文章展示了用Swift创建iOS应用的全过程,以及如何通过SceneKit展示3D图形。

本文中代码的Swift版本为1.0版,仅供学习使用。

软件需求

创建本文相关代码之前,先要在OSX 10.9或更高版本中安装Xcode 6.1。文中的代码可以直接在模拟器中运行,想要部署到硬件设备上的话,需要激活 iOS开发者 账号。

iOS新项目创建

创建新项目,可以通过Xcode启动时的欢迎对话框,或是“文件→新建→项目”菜单。

使用Swift和SceneKit开发一片圣诞树林

出现在Xcode新窗口菜单中的新对话框包含了多种iOS或OS X应用选项。有很多缺省模版类型,用以创建不同行为的示例代码项目。

使用Swift和SceneKit开发一片圣诞树林

选择“iOS→应用→游戏”可以从众多不同类型中选择一个来创建模版:

  • SceneKit
  • SpriteKit
  • OpenGL ES
  • Metal

SceneKit 创建一个面向通用设备的名为 MerrySwiftmas 的Swift应用。(通用应用意味着该应用可以运行在iPad和iPhone/iPod上;如果应用只面向一种设备,可以在此处或接下来的 info.plist 中进行更改)。

使用Swift和SceneKit开发一片圣诞树林

运行项目

这样就根据模版创建了一个新项目。项目的用户界面可以被划分为四个独立区域:左边是导航区(可以通过快捷键⌘1-9进行切换);右上部是各种检视器(可以通过快捷键⌥⌘1-9进行切换);右下方是各个类库(可以通过快捷键^⌥⌘1-9进行切换)。

使用Swift和SceneKit开发一片圣诞树林

窗口中间是编辑区,该区域会显示导航区所选中的文件。选中最顶层项目时,编辑区会展示一系列通用信息(这些信息存储在info.plist文件中);单击选中其他文件时,编辑区会发生变化,双击文件时,则会弹出含有该文件的新窗口。

窗口顶部是运行按钮;运行意味着构建并启动缺省目标设备及应用;本例中,是一个加载应用的模拟器窗口:

使用Swift和SceneKit开发一片圣诞树林

Swift代码简介

飞船的加载、显示是靠 GameViewController.swift 文件中的 viewDidLoad 函数。

import UIKit import SceneKit  class GameViewController: UIViewController {   override func viewDidLoad() {     super.viewDidLoad()     let scene = SCNScene()     ...     let scnView = self.view as SCNView     scnView.scene = scene     scnView.autoenablesDefaultLighting = true     scnView.allowsCameraControl = true   }   ... }

正如其他C类型语言,块集成用花括号{},类成员访问用.,函数和构造器传参用括号()。参数可以是定位参数或命名参数,冒号隔开名称和值。

虽说Swift是静态编译的强类型应用,却不必声明变量类型。Swift会在编译时自动推导。 Var 用于声明变量, let 用于声明常量。

关键字 as 用于类型转换,有了它,源于 UIViewController 超类、被声明为 UIViewself.view 就可以当SceneKit SCNView 使用,进而对诸如 scene 或是 allowsCameraContro l这类SceneKit特有的字段进行访问。

替换宇宙飞船

视图控制器首次加载时, viewDidLoad() 的函数体会被调用,函数体可以用上述代码替换。运行替换后的代码会得到不含其他信息的黑色屏幕。

场景图像通过结点结构进行展示,并以 rootNode 为顶层结点。任何结点都能添加子结点;结点的翻转变换会统一影响其所有子结点。

向图中添加柱体,可以像下面这样,创建一个含有 SCNCylinderSCNNode 并添加到图中:

... scnView.allowsCameraControl = true  let rootNode = scene.rootNode let cylinder = SCNCylinder(radius:1,height:3) let tree = SCNNode(geometry: cylinder) rootNode.addChildNode(tree)

如果现在运行应用,你会看到一个相当无趣的白块:

使用Swift和SceneKit开发一片圣诞树林

柱体看上去并不具备3D效果,这只是因为柱体尚未上色、场景尚未投灯光。这些都很容易搞定:可以像下面这样,将柱体的漫反射材质上色,场景投自动灯光:

... rootNode.addChildNode(tree)  cylinder.firstMaterial?.diffuse.contents = UIColor.brownColor() scnView.autoenablesDefaultLighting = true

现在再运行应用,柱体就会显示出着色后的的3D效果。因为场景允许摄像控制(采用上帝视角观察),所以可以通过手指或鼠标手势拖动场景:

使用Swift和SceneKit开发一片圣诞树林

下一步是在柱体之上添加锥体,方法与添加柱体类似。

... scnView.autoenablesDefaultLighting = true  let cone = SCNNode(geometry: SCNCone(topRadius:0, bottomRadius:3, height:3)) cone.position.y = 3 cone.geometry?.firstMaterial?.diffuse.contents = UIColor.greenColor() tree.addChildNode(cone)

为了把锥体放在柱体之上,初始位置上移了3个单元,但仍保持和柱体中心对称。 ?. 是可选访问器:如果值为空,那么一切都不变;如果值非空,那么计算表达式的剩余部分。

现在应用的运行结果如下:

使用Swift和SceneKit开发一片圣诞树林

完成树

由于要在顶部添加更多锥体,锥体的创建代码会被重复多次。也可以用含有这部分代码的 for 循环替代。

... scnView.autoenablesDefaultLighting = true  for i in 1...3 {   let cone = SCNNode(geometry: SCNCone(topRadius:0, bottomRadius:3, height:3))   cone.position.y = 2 * Float(i) + 1   cone.geometry?.firstMaterial?.diffuse.contents = UIColor.greenColor()   tree.addChildNode(cone) }

for 循环中,用 1...3 代表闭区间 [1,2,3] 。半闭合区间 [1,2] 可以用 1..<3 表示。通过堆叠锥体来生成树。

整型 i 可以通过 Float 强制转换成浮点型以便根据树根计算相对位置。

应用运行时,会显示一棵树:

使用Swift和SceneKit开发一片圣诞树林

添加礼物

SCNBox 的话,可以把礼物放在树下。柱体(即树的底部)的位置在0,0,0及其之上,盒子的位置要略低一点: y=-1Xz 方向随意。可以显式指定位置,但for循环所允许的位置只有(0,1),(1,0)以及(1,1)。

for i in 1...3 {   let present = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))   present.geometry?.firstMaterial?.diffuse.contents = UIColor.blueColor()   present.position.x = Float(i % 2) * 2   present.position.z = Float(i / 2) * 2   present.position.y = -1   tree.addChildNode(present) }

使用Swift和SceneKit开发一片圣诞树林

重构为类

把这些一股脑儿堆放在视图控制器加载的时候,并不是好的实践方案。这种情况下,代码复用和测试都会变得相对困难。实际上,创建树的相关代码可以单独成类。

用“文件→新建→文件”创建一个名为 ChristmasTree 的iOS Swift文件。文件创建时为空,需要导入 SceneKit 并声明一个名为 ChristmasTree 的类。

import SceneKit  class ChristmasTree { }

想创建 SCNNode 子类的话,要把父类名称放在子类名称之后,并用冒号隔开。实现协议(接口)用的也是同样的语法。这样做时,还要添加一系列调用init的构造器:

class ChristmasTree: SCNNode {   override init() {     super.init()   }   required init(coder: NSCoder) {     fatalError("init(coder:) has not been implemented")   } }

Swift约定:对象要有一个主要(或缺省)初始化函数;在很多UIKit对象中,这意味着要遵循NSCoder协议,也就是说,要有一个编码器相关的init函数。通过把字段属性转换成序列化结构并还原,NSCoder能将Interface Builder中的对象持久化。如果你忘了写,Xcode会报错并帮你添上。这样做的实际效果就是,用Swift重写子类时,“需要”一个构造器,“重写”其他构造器。

现在类框架写好了,也该补充内容了。把创建树的代码从 viewDidLoad 方法移动到 ChristmasTreeinit 方法中来:

override init() {   super.init()    let cylinder = SCNCylinder(radius:1,height:3)   let trunk = SCNNode(geometry: cylinder)   cylinder.firstMaterial?.diffuse.contents = UIColor.brownColor() addChildNode(trunk)    for i in 1...3 {     let cone = SCNNode(geometry: SCNCone(topRadius:0, bottomRadius:3, height:3))     cone.position.y = 2 * Float(i) + 1     cone.geometry?.firstMaterial?.diffuse.contents = UIColor.greenColor()     addChildNode(cone)   }    for i in 1...3 {     let present = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))     present.geometry?.firstMaterial?.diffuse.contents = UIColor.blueColor()     present.position.x = Float(i % 2) * 2     present.position.z = Float(i / 2) * 2     present.position.y = -1     addChildNode(present)   } }

这样就把树的添加工作留到视图控制器更新时再做:

override func viewDidLoad() {   super.viewDidLoad()   let scene = SCNScene()   let scnView = self.view as SCNView   scnView.scene = scene   scnView.allowsCameraControl = true   let rootNode = scene.rootNode   let tree = ChristmasTree()   rootNode.addChildNode(tree)   scnView.autoenablesDefaultLighting = true }

种植森林

可以拥有上千棵树,为何满足于只有一棵?现在树被抽象为一个类。种一百棵树很简单。用嵌套for循环的 position 属性安排每棵树的位置。在步长不为1的情况下,可以通过内置函数 stride 生成数字迭代器。

for x in stride(from:0, to:100, by:10) {   for z in stride(from:0, to:100, by:10) {     let tree = ChristmasTree()     tree.position.x = Float(x)     tree.position.z = Float(z)     rootNode.addChildNode(tree)   } }

这样,一百棵树就种好了,间隔10个单位,分布在2D网格中。

使用Swift和SceneKit开发一片圣诞树林

所有树看上去都很相似,部分原因在于所有礼物的整齐排列。加入树的旋转后,会不再那么相似。旋转用弧度衡量:圆的弧度为2π,因此,想得到随机位置,可以选一个180以内的随机数,除以180,再乘以π:

tree.position.x = Float(x) tree.position.z = Float(z) tree.rotation.y = 1 tree.rotation.w = Float(M_PI) * Float(arc4random_uniform(180)) / Float(180)

现在森林看上去有那么一点不一样了:

使用Swift和SceneKit开发一片圣诞树林

添加颜色

目前所有礼物都是蓝色。如果你恰巧喜欢蓝色,就很不错,只是看上去会有一点单调。还有一种选择,你可以创建颜色序列,并随机选取。

尽管Swift支持 structenum 类型的 static 成员,但直到Swift1.1,尚不支持 static 类成员。(实际上,要是你试着向Swift类中添加 static 成员,编译器会抱怨你不该用 static ,而应该用 class ,可当你用了 class ,编译器又会表示尚不支持)

值得庆幸的是,类之外也能声明变量,这样声明的全局变量正好能存储构建好的颜色序列。通过随机系数就可以选取礼物的颜色。

let colors = [   UIColor.blueColor(),   UIColor.cyanColor(),   UIColor.magentaColor(),   UIColor.orangeColor(),   UIColor.purpleColor(),   UIColor.redColor(),   UIColor.whiteColor(),   UIColor.yellowColor(), ]  class ChristmasTree: SCNNode { ...       let present = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))       present.geometry?.firstMaterial?.diffuse.contents = colors[random() % colors.count]

使用Swift和SceneKit开发一片圣诞树林

结论

Swift是一种很容易上手的语言,因为它有自己的REPL( swift ,简单交互式编程环境,源于Lisp,译者注);只通过几行代码就可以用SceneKit创建交互图形应用。最后,祝你们圣诞快乐!

Alex GitHub 空间 可以找到该项目的源代码。

关于作者

使用Swift和SceneKit开发一片圣诞树林 二十年前, Dr Alex Blewitt 第一次在NeXTstation上接触到Objective-C面向对象编程并一直使用至今。Swift的发布预示着OS X平台的未来,Alex为此写过一本书, Swift Essentials ,该书定于下月出版发行。有空时,如果天气很好,Alex会从本地的Crafield机场试飞。

过去几年中,移动应用像风暴一样席卷世界,改变了我们的网上工作、娱乐方式。很多移动应用开发技术应运而生,而移动也开始得到开发过程重视。尽管移动已经看似无所不在,但未来才刚刚开始。新一代的移动设备,好比可穿戴设备,好比物联网的许许多多移动构件,就在我们面前。我们会发现:用于数据展示和命令接收的用户界面不断推陈出新;越来越多的公司站在真正的移动浪潮之巅。这会在未来几年中影响软件的设计、开发、测试方式。

InfoQ 的这篇文章是快速变革的移动技术 系列 的一部分。你可以在 这里 订阅该系列的新文章通知。

查看英文原文: Merry Swiftmas from InfoQ

感谢夏雪对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流。

正文到此结束
Loading...