本系列文章是对<3D Apple Games by Tutorials>一书的学习记录和体会node
此书对应的代码地址git
SceneKit系列文章目录github
更多iOS相关知识查看github上WeekWeekUpProjectswift
在Xcode主菜单中选择File > New > Project.数组
选择iOS/Application/Game模板,点击Next app
输入项目名GeometryFighter,选择Swift语言, SceneKit游戏技术,Universal设备类型, 去掉单元测试的勾,点击Next: dom
下一步,清理不须要的文件. 删除art.scnassets文件夹. 清理GameViewController.swift文件中的内容:ide
import UIKit
import SceneKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
} }
复制代码
而后在viewDidLoad()
前面添加:工具
var scnView: SCNView!
复制代码
再在prefersStatusBarHidden()
下方添加:oop
func setupView() {
scnView = self.view as! SCNView
}
复制代码
并在Main.storyboard中将view类型设置为SCNView.
继续添加属性:
var scnScene: SCNScene!
复制代码
在setupView()
下方接着写:
func setupScene() {
scnScene = SCNScene()
scnView.scene = scnScene
}
复制代码
在viewDidLoad()
中调用这些方法:
setupView()
setupScene()
复制代码
从Resources中找到游戏图标,拖放到Assets.xcassets中
此时运行游戏,看到的是黑屏.
从resources文件夹中拖放GeometryFighter.scnassets到咱们的项目中,选中Copy items if needed, Create Groups还有个人项目GeometryFighter,点击Finish.
在项目中选中素材文件,能够查看详情
先点击Assets.xcassets,拖放GeometryFighter.scnassets/Textures/Logo_Diffuse.png到AppIcon下面.
再点击LaunchScreen.storyboard,选中view,设置背景为深蓝色:
从右下的媒体库中,拖放Logo_Diffuse到view中,设置Content Mode为Aspect Fit:
添加约束:
运行一下:
在GameViewController.swift中setupScene()
方法的底部添加:
scnScene.background.contents = "GeometryFighter.scnassets/Textures/
Background_Diffuse.png"
复制代码
运行一下
打开GameViewController.swift,在scnScene
下方添加新属性:
var cameraNode: SCNNode!
复制代码
并在setupScene()
方法下方添加:
func setupCamera() {
// 1
cameraNode = SCNNode()
// 2
cameraNode.camera = SCNCamera()
// 3
cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
// 4
scnScene.rootNode.addChildNode(cameraNode)
}
复制代码
其中:
cameraNode
.cameraNode
的camera
属性.(x:0, y:0, z:10)
.cameraNode
到场景中,做为场景根节点的一个子节点.完成后,在viewDidLoad()
方法中,setupScene()
方法后面调用:
setupCamera()
复制代码
添加一个新文件,命名为setupCamera()
打开并更改内容以下:
import Foundation
// 1
enum ShapeType:Int {
case box = 0
case sphere
case pyramid
case torus
case capsule
case cylinder
case cone
case tube
// 2
static func random() -> ShapeType {
let maxValue = tube.rawValue
let rand = arc4random_uniform(UInt32(maxValue+1))
return ShapeType(rawValue: Int(rand))!
} }
复制代码
代码含义:
ShapeType
,用来表示各类不一样形状.random()
,用来产生随机的ShapeType
.在GameViewController.swift中,setupCamera()
方法下面,添加:
func spawnShape() {
// 1
var geometry:SCNGeometry
// 2
switch ShapeType.random() {
default:
// 3
geometry = SCNBox(width: 1.0, height: 1.0, length: 1.0,
chamferRadius: 0.0)
}
// 4
let geometryNode = SCNNode(geometry: geometry)
// 5
scnScene.rootNode.addChildNode(geometryNode)
}
复制代码
代码含义:
switch
语句来处理ShapeType.random()
中返回的形状.暂时咱们只添加一个立方体形状,其余的稍后添加.SCNBox
对象并储存在geometry
中.SCNNode
实例,命名为geometryNode
.构造器使用geometry
参数来自动建立一个节点并将几何体附加在上面.还须要在viewDidLoad()
中调用一下,放在setupCamera()
后面:
spawnShape()
复制代码
运行一下,看到一个白方块:
由于立方体节点是从spwnSpape()
建立的,会位于场景的(x:0, y:0, z:0)
.咱们又是从cameraNode
节点来观察场景的,摄像机节点位置是在(x:0, y:0: z:10)
,因此正好立方体正好出如今屏幕中间.
为了更方便观察,咱们能够打开视图的内置属性,给GameViewController.swift中的setupView()
方法再添加几行:
// 1
scnView.showsStatistics = true
// 2
scnView.allowsCameraControl = true
// 3
scnView.autoenablesDefaultLighting = true
复制代码
代码含义:
showStatistics
会在屏幕底部启动一个实时的统计面板.allowsCameraControl
能让你用手势(单指轻扫,双指轻扫,双指捏合,双击)控制摄像机的位置.autoenablesDefaultLighting
则建立一个泛光灯来照亮你的场景.运行一下,看起来好多了!
拖放GameUtils文件夹到咱们的项目中,点击Finish:
打开GameViewController.swift,在spawnShape()
中的建立geometryNode
代码以后添加一行:
geometryNode.physicsBody =
SCNPhysicsBody(type: .dynamic, shape: nil)
复制代码
shape传nil,会自动根据显示的形状建立一个物理形体.
运行一下,会看到随机产生的几何体,自动掉落下去了,这是由于SceneKit的场景会自动打开重力:
在spawnShape()
中的建立geometryNode
代码以后添加一行:
// 1
let randomX = Float.random(min: -2, max: 2)
let randomY = Float.random(min: 10, max: 18)
// 2
let force = SCNVector3(x: randomX, y: randomY , z: 0)
// 3
let position = SCNVector3(x: 0.05, y: 0.05, z: 0.05)
// 4
geometryNode.physicsBody?.applyForce(force,
at: position, asImpulse: true)
复制代码
代码含义:
applyForce(direction: at: asImpulse:)
方法将力应用到geometryNode
的物理形体上.运行一下,物体凭空出现后,受到力的做用被抛向空中,飞翔以后,最终受到重力影响下落.
如今物体是在屏幕中间凭空出现,效果很很差,咱们只须要修改摄像机的位置就能够改善.在setupCamera()
中更改位置:
cameraNode.position = SCNVector3(x: 0, y: 5, z: 10)
复制代码
下面,还能够给几何体添加一些随机颜色.在spawnShape()
方法中添加一行,在建立geometry
以后中, 建立geometryNode
以前:
geometry.materials.first?.diffuse.contents = UIColor.random()
复制代码
运行一下,物体就有了漂亮的颜色:
在GameViewController.swift中,添加SCNSceneRendererDelegate
协议,并实现协议方法:
// 1
extension GameViewController: SCNSceneRendererDelegate {
// 2
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// 3
spawnShape()
} }
复制代码
在此以前,还要先成为视图的代理.在setupView()
方法的末尾添加一行:
scnView.delegate = self
复制代码
此时,已经能够删除viewDidLoad()
中对spawnShape()
的调用了.运行一下:
能够发现,建立的太多了,场面几乎失控了.咱们须要控制一下建立几何体的时间间隔.
在cameraNode
下方添加一个新属性:
var spawnTime: TimeInterval = 0
复制代码
而后替换renderer(_:updateAtTime:)
方法中的内容:
// 1
if time > spawnTime {
spawnShape()
// 2
spawnTime = time + TimeInterval(Float.random(min: 0.2, max: 1.5))
}
复制代码
代码含义:
time
(当前系统的时间),若是大于spawnTime
就产生一个新的形状,不然,什么也不作.spawnTime
来决定下一次建立的时机.下一次建立时间应该是在当前时间上增长一个随机量.运行一下.
spawnShape()
方法一直不停地建立新的节点并添加到场景中,可是却没有移除,仅仅是掉落出视线而已.虽然SceneKit有些优化能让场景继续运行下去不卡顿,但咱们仍然须要将不要的节点移除掉.
在spawnShape()
下方,添加几行:
func cleanScene() {
// 1
for node in scnScene.rootNode.childNodes {
// 2
if node.presentation.position.y < -2 {
// 3
node.removeFromParentNode()
}
} }
复制代码
代码含义:
position
来表示它的真实位置,此时的position
反应的是动画开始前的位置.SceneKit在动画期间保存了对象的副本,并用副原本执行动画.要想获得动画进行过程当中的实际位置,须要使用presentationNode
属性.在renderer(_: updatedAtTime:)
方法中调用cleanScene()
方法:
cleanScene()
复制代码
还有一个问题须要处理.默认状况下,SceneKit在没有动画时会进入"暂停"状态.咱们能够启用SCNView
实例的playing
属性来阻止它.
在setupView()
的最后,添加下面的代码:
scnView.isPlaying = true
复制代码
运行一下,旋转看看物体下落到哪里消失的.
建立一个新分组
命名为Particles,右击分组选择New File,选择iOS/Resource/SceneKit Particle System模板,点击Next继续:
接下来,在Particle system template中选择Fire类型,点击Next.保存为Tail.scnp并点击Create.而后你会看到这样的场景:
注:Xcode 11 中,粒子系统建立方式有变化,在.scn 场景右上角的“+”号中。
![]()
在右侧配置粒子系统的属性以下:
配置完成后的最终效果以下,若是你看到的不同,试着旋转一下摄像机:
在GameViewController.swift类中添加下面的代码:
// 1
func createTrail(color: UIColor, geometry: SCNGeometry) ->
SCNParticleSystem {
// 2
let trail = SCNParticleSystem(named: "Trail.scnp", inDirectory: nil)!
// 3
trail.particleColor = color
// 4
trail.emitterShape = geometry
// 5
return trail
}
复制代码
代码含义:
createTrail(_: geometry:)
接收color
和geometry
参数来建立粒子系统.进入spawnShape()
中,找到设置材质颜色的代码,用常量保存起来:
let color = UIColor.random()
geometry.materials.first?.diffuse.contents = color
复制代码
下一步,在spawnShape()
中,在添加力到geometryNode
的物理形体上以后,添加下面的代码:
let trailEmitter = createTrail(color: color, geometry: geometry)
geometryNode.addParticleSystem(trailEmitter)
复制代码
运行一下:
给GameViewController.swift中添加一个新属性,放在spawnTime
后面:
var game = GameHelper.sharedInstance
复制代码
在GameViewController
最底部,createTail()
方法后面,添加下面的方法:
func setupHUD() {
game.hudNode.position = SCNVector3(x: 0.0, y: 10.0, z: 0.0)
scnScene.rootNode.addChildNode(game.hudNode)
}
复制代码
其中咱们是从帮助文件库中调用的game.hudNode.
下一步,咱们须要调用setupHUD()
.在viewDidLoad()
方法的底部添加一行:
setupHUD()
复制代码
咱们还须要不断更新显示的内容.在renderer(_: updateAtTime:)
方法底部,调用game.updateHUD()
:
game.updateHUD()
复制代码
运行一下,屏幕上方就出现了抬头显示面板:
在咱们处理触摸事件以前,咱们须要标识出每一个物体.最简单的方法就是给他们起个名字.
在spawnShape()
中添加下面的代码,放在添加粒子系统以后:
if color == UIColor.black {
geometryNode.name = "BAD"
} else {
geometryNode.name = "GOOD"
}
复制代码
下一步,在GameViewController
中, setupHUD()
以后,添加下列方法:
func handleTouchFor(node: SCNNode) {
if node.name == "GOOD" {
game.score += 1
node.removeFromParentNode()
} else if node.name == "BAD" {
game.lives -= 1
node.removeFromParentNode()
}
}
复制代码
下一步,在GameViewController
中, handleTouchFor(_:)
以后,添加下列方法:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
// 1
let touch = touches.first!
// 2
let location = touch.location(in: scnView)
// 3
let hitResults = scnView.hitTest(location, options: nil)
// 4
if let result = hitResults.first {
// 5
handleTouchFor(node: result.node)
}
}
复制代码
代码含义:
scnView
的坐标.hitTest(_: options:)
返回一个SCNHitTestResult
对象数组,表明着从用户触摸点发出的射线碰到的全部物体.最后一步,须要禁用摄像机控制:
scnView.allowsCameraControl = false
复制代码
运行一下,用手指触摸就会毁灭!
再建立一个粒子效果,命名为Explode.scnp.尝试着本身配置一下,让它看起来像这样:
能够用下面的图片做为参考:
能够在projects/challenge/ GeometryFighter文件夹中找到已经完成的Explode.scnp文件.
接着还须要将这个效果用起来.在GameViewController
中, touchesBegan(_: withEvent)
方法后面,添加下面的代码:
// 1
func createExplosion(geometry: SCNGeometry, position: SCNVector3, rotation: SCNVector4) {
// 2
let explosion =
SCNParticleSystem(named: "Explode.scnp", inDirectory:
nil)!
explosion.emitterShape = geometry
explosion.birthLocation = .surface
// 3
let rotationMatrix =
SCNMatrix4MakeRotation(rotation.w, rotation.x,
rotation.y, rotation.z)
let translationMatrix =
SCNMatrix4MakeTranslation(position.x, position.y,
position.z)
let transformMatrix =
SCNMatrix4Mult(rotationMatrix, translationMatrix)
// 4
scnScene.addParticleSystem(explosion, transform: transformMatrix)
}
复制代码
代码含义:
createExplosion(_: position: rotation:)
接收三个参数:geometry
定义了粒子效果的形状,position
和rotation
帮助放置爆炸效果到场景中.geometry
做为emitterShape
,这样粒子就能够从形状的表面发射出来.addParticleSystem(_: wtihTransform)
将爆炸效果添加到场景中.在handleTouchFor(_:)
中添加两次下面的代码-"good"分支一次,"bad"分支一次.添加在移除节点以前:
createExplosion(geometry: node.geometry!, position: node.presentation.position,rotation: node.presentation.rotation)
复制代码
这里,咱们又使用了
presentation
,由于物理效果模拟正在移动节点.
运行一下,点击爆炸!
这个效果能够在projects/ challenge/GeometryFighter文件夹中找到.
为了让游戏更好玩,还能够添加不少彩蛋效果,好比:
这些效果均可以在projects/juiced/GeometryFighter文件夹中找到最终完成品.打开尝试一下吧.