写点什么

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

  • 2015-05-01
  • 本文字数:5172 字

    阅读完需:约 17 分钟

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

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

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

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

软件需求

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

iOS 新项目创建

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

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

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

  • SceneKit
  • SpriteKit
  • OpenGL ES
  • Metal

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

运行项目

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

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

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

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或是allowsCameraControl 这类 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)

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

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

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

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

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

复制代码
...
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 个单元,但仍保持和柱体中心对称。?.是可选访问器:如果值为空,那么一切都不变;如果值非空,那么计算表达式的剩余部分。

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

完成树

由于要在顶部添加更多锥体,锥体的创建代码会被重复多次。也可以用含有这部分代码的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强制转换成浮点型以便根据树根计算相对位置。

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

添加礼物

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)
}

重构为类

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

用“文件→新建→文件”创建一个名为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 网格中。

所有树看上去都很相似,部分原因在于所有礼物的整齐排列。加入树的旋转后,会不再那么相似。旋转用弧度衡量:圆的弧度为 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 支持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 是一种很容易上手的语言,因为它有自己的 REPL(swift,简单交互式编程环境,源于 Lisp,译者注);只通过几行代码就可以用 SceneKit 创建交互图形应用。最后,祝你们圣诞快乐!

Alex__ 的 __GitHub__ 空间可以找到该项目的源代码。

关于作者

二十年前,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 )关注我们,并与我们的编辑和其他读者朋友交流。

2015-05-01 12:583738

评论

发布
暂无评论
发现更多内容

Stack Memory vs Heap Memory in Java

Mahipal_Nehra

Java heap memory Stack memory Java development

Hire Remote Developers

Mahipal_Nehra

Java angular blockchain React app development

数据监控预警系统,实现不同端信息推送

葡萄城技术团队

前端 数据可视化

10分钟为你全面解答HDFS的SecondaryNamenode的作用

好程序员IT教育

大数据 hdfs

专利解析|多维建模结合AI识别商品特征的方法

元年技术洞察

AI 数字化转型

帮助中心:培养客户自助服务意识的实用工具

Baklib

MatrixOne从入门到实战04——MatrixOne的连接和建表

MatrixOrigin

数据库 分布式 MatrixOrigin MatrixOne

如何通过Java 合并和取消合并 Excel 单元格

在下毛毛雨

Java Excel 合并单元格

阿里 CTO 程立:Severless 化正加速重塑阿里应用架构和研发模式

阿里巴巴云原生

阿里云 Serverless 云原生

ONE 2.0应用场景解读 | 如何通过时序拓扑直观还原故障传导链路?

博睿数据

可观测性 应用场景 智能运维 博睿数据 ONE平台

学历低可以参加大数据培训吗

小谷哥

MatrixOne从入门到实践05——数据类型介绍

MatrixOrigin

数据库 分布式 MatrixOrigin MatrixOne

5步法助力自动化转型

FunTester

年终最重磅!云原生实时数仓 SelectDB 首次产品发布等你来约!

SelectDB

数据库 云计算 大数据 实时计算

Amazon Braket 与量子计算

亚马逊云科技 (Amazon Web Services)

量子计算 Hero 专栏 Amazon Braket

关于平台工程的开发者工具链,你还想加点啥?

阿里巴巴云原生

阿里云 微服务 云原生 EDAS

从手动测试到自动测试,企业该如何选择?

SoFlu-JavaAI开发助手

折叠屏“世界杯”开哨,荣耀Magic Vs踢出关键一球

脑极体

Chrome 103支持使用本地字体,纯前端导出PDF优化

葡萄城技术团队

chrome 前端 HTTP PDF

Zebec流支付生态,开启多链布局的“两手准备”

鳄鱼视界

DevData Talks | 知乎艾辉:从工具建设到运营,千人团队研发提效最佳实践

思码逸研发效能

研发管理 研发效能

《数据》杂志 | 浅析《网络安全法》修改对数据合规与隐私计算的影响

洞见科技

游戏品类加速回暖,文娱内容持续火热——2022年IAA行业品类发展洞察系列报告·第三期

易观分析

游戏 报告 文娱

低代码开发是未来软件开发的主流模式

元年技术洞察

低代码 方舟PaaS

容器服务 ACK 结合 MSE Ingress,让集群入口流量管理更丰富、更容易

阿里巴巴云原生

阿里云 云原生 容器服务

第三章 TCP/IP ip地址概念与应用

我叫于豆豆吖.

11月月更

Serverless Devs 社区联合信通院邀请您参加 2022 中国 Serverless 用户调查

阿里巴巴云原生

阿里云 Serverless 云原生

阿里云云原生加速器成员企业袋鼠云创始人陈吉平:深耕国产自研数字化技术与服务,持续为客户创造价值

阿里巴巴云原生

阿里云 云原生

分布式存储之 etcd 的集群管理

焱融科技

云计算 分布式系统 etcd 高性能 分布式存储

开源让这位00后逆袭成为各类大奖收割者

OpenI启智社区

开源 OpenI启智社区 免费算力

如何用JavaScripte和HTML 实现一整套的考试答题卡和成绩表

葡萄城技术团队

使用Swift和SceneKit开发一片圣诞树林_移动_Alex Blewitt_InfoQ精选文章