SceneKit系列文章目录node
更多iOS相关知识查看github上WeekWeekUpProjectgit
在本教程中,你将会学习如何制做一个相似Stack这样的游戏. github
本教程将包含如下内容:编程
下载初始项目starter project. 在初始项目里面,你会发现SceneKit目录文件中带有音频和场景文件.另外,还有一些SCNVector3类扩展来执行简单的向量算术运算及生成渐变图片.还有App Icon也已经添加进去了!花点时间熟悉一下项目吧.swift
你将会建立一个相似于Stack的游戏.这个游戏的目标是在一块方块上叠放另外一个方块.须要当心的是:方块叠放时稍微偏一点,多余部分就会被切掉.彻底没对齐,那就game over了!app
你将从创建你的游戏场景开始.打开GameScene.scn. 编辑器
切换到Attributes Inspector并切换相机的Projection type为Orthographic. 下一步,添加灯光到场景中. 从对象库中拖拽一个新的方向光到场景中,命名为Directional Light.由于相机只看到了场景的一侧,你没必要去照亮看不见的令一侧.回到Attributes Inspector,设置位置为X: 0, Y: 0, Z: 0,旋转为X: -65, Y: 20, Z:-30: post
神奇,亮起来了!学习
如今回到塔的顶部.你须要一个基础方块来支承这个塔,来让玩家在上面建造.拖拽一个盒子到场景中,设置属性:字体
你须要添加一个动态形体到基础方块,切换到Physics Inspector中,并将物理形体改成Static.
如今让咱们配上漂亮的背景颜色!在选中基础方块的同时,切换到Scene Inspector,并拖拽文件Gradient.png到背景选择框中:
你须要一个方法来显示给玩家,他们的塔已经堆放了多高.打开Main.storyboard;看到它已经有一个SCNView.添加一个label在SCNView顶部并设置文本为0.而后添加一个约束将label对齐到中心,像这样:
添加另外一个约束将label顶部与屏幕顶部对齐.
而后切换到Attributes Inspector中,切换字体为Custom, Thonburi, Regular, 50.
而后使用assistant editor来添加一个从label到控制器的引用,命名为scoreLabel:
编译运行,看看如今有什么了.
知道怎么让塔愈来愈高么?对,建立更多方块. 建立一些属性来帮你追踪正在使用的方块.为此,打开ViewController.swift() 并在viewDidLoad() 以前添加下面变量:
//1
var direction = true
var height = 0
//2
var previousSize = SCNVector3(1, 0.2, 1)
var previousPosition = SCNVector3(0, 0.1, 0)
var currentSize = SCNVector3(1, 0.2, 1)
var currentPosition = SCNVector3Zero
//3
var offset = SCNVector3Zero
var absoluteOffset = SCNVector3Zero
var newSize = SCNVector3Zero
//4
var perfectMatches = 0
复制代码
这段代码含义:
如今,是时间添加方块到场景中了.在viewDidLoad() 底部添加下面代码:
//1
let blockNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
blockNode.position.z = -1.25
blockNode.position.y = 0.1
blockNode.name = "Block\(height)"
//2
blockNode.geometry?.firstMaterial?.diffuse.contents =
UIColor(colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
scnScene.rootNode.addChildNode(blockNode)
复制代码
代码含义:
建立并运行,会看到你的新方块出如今屏幕上!
如今已经有一条新的方块用来放置.可是,我想若是方块是移动的会更好玩. 要实现这个移动,须要设置控制器做为场景渲染代理,并实现SCNSceneRendererDelegate协议中的方法. 在类的底部添加这个扩展:
extension ViewController: SCNSceneRendererDelegate {
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
}
}
复制代码
这里咱们须要实现SCNSceneRendererDelegate协议,添加renderer(_:updateAtTime:). 在renderer(_:updateAtTime:) 里面添加下面代码:
// 1
if let currentNode = scnScene.rootNode.childNode(withName: "Block\(height)", recursively: false) {
// 2
if height % 2 == 0 {
// 3
if currentNode.position.z >= 1.25 {
direction = false
} else if currentNode.position.z <= -1.25 {
direction = true
}
// 4
switch direction {
case true:
currentNode.position.z += 0.03
case false:
currentNode.position.z -= 0.03
}
// 5
} else {
if currentNode.position.x >= 1.25 {
direction = false
} else if currentNode.position.x <= -1.25 {
direction = true
}
switch direction {
case true:
currentNode.position.x += 0.03
case false:
currentNode.position.x -= 0.03
}
}
}
复制代码
代码含义:
默认状况下,SceneKit会暂停场景.为了看到场景中物体的移动,在viewDidLoad的底部添加下面代码:
scnView.isPlaying = true
scnView.delegate = self
复制代码
这段代码中,将这个控制器设置为场景的渲染代理,这样就能执行刚才写的代理方法了. 建立运行,查看运动!
如今,咱们已经让方块移动了,还须要在玩家点击屏幕时添加一个新方块并重设老方块的尺寸.切换到Main.storyboard并添加一个tap gesture recognizer到SCNView,像这样:
如今在控制器里面用辅助编辑器建立一个动做并命名为handleTap.
切换到标准编辑区,并打开ViewController.swift,而后在handlTap(_:) 内部添加代码:
if let currentBoxNode = scnScene.rootNode.childNode(
withName: "Block\(height)", recursively: false) {
currentPosition = currentBoxNode.presentation.position
let boundsMin = currentBoxNode.boundingBox.min
let boundsMax = currentBoxNode.boundingBox.max
currentSize = boundsMax - boundsMin
offset = previousPosition - currentPosition
absoluteOffset = offset.absoluteValue()
newSize = currentSize - absoluteOffset
currentBoxNode.geometry = SCNBox(width: CGFloat(newSize.x), height: 0.2,
length: CGFloat(newSize.z), chamferRadius: 0)
currentBoxNode.position = SCNVector3Make(currentPosition.x + (offset.x/2),
currentPosition.y, currentPosition.z + (offset.z/2))
currentBoxNode.physicsBody = SCNPhysicsBody(type: .static,
shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
}
复制代码
这里咱们从场景中获得currentBoxNode.而后计算偏移及新方块的尺寸.从而改变方块的尺寸和位置,并给它一个静态物理形体.
偏移等于上一层和当前层位置的差值.经过从当前尺寸上减去差值的绝对值,就获得了新尺寸. 注意到,把当前节点设置位置到偏移处,方块的边缘完美对齐了上一个层的边缘.这创造出一种切掉方块的错觉.
下一步,你须要一个方法来建立塔上的下一个方块.在handleTap(_:) 下面添加代码:
func addNewBlock(_ currentBoxNode: SCNNode) {
let newBoxNode = SCNNode(geometry: currentBoxNode.geometry)
newBoxNode.position = SCNVector3Make(currentBoxNode.position.x,
currentPosition.y + 0.2, currentBoxNode.position.z)
newBoxNode.name = "Block\(height+1)"
newBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(
colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
if height % 2 == 0 {
newBoxNode.position.x = -1.25
} else {
newBoxNode.position.z = -1.25
}
scnScene.rootNode.addChildNode(newBoxNode)
}
复制代码
这里咱们建立了一个和上一个方块相同尺寸的新节点.放置在当前方块上方,并根据层高改变X或Z轴的位置.最后,改变漫反射颜色并将其添加到场景中.
你须要使用handleTap(_:) 来保持属性为最新.在handleTap(_:) 里的if else语句中添加代码:
addNewBlock(currentBoxNode)
if height >= 5 {
let moveUpAction = SCNAction.move(by: SCNVector3Make(0.0, 0.2, 0.0), duration: 0.2)
let mainCamera = scnScene.rootNode.childNode(withName: "Main Camera", recursively: false)!
mainCamera.runAction(moveUpAction)
}
scoreLabel.text = "\(height+1)"
previousSize = SCNVector3Make(newSize.x, 0.2, newSize.z)
previousPosition = currentBoxNode.position
height += 1
复制代码
要作的第一件事就是调用addNewBlock(_:).若是塔的尺寸大于或等于5,将相机上移.
还须要更新分数,设置前一个尺寸和位置等于当前尺寸和位置.你可使用newSize由于你设置当前节点的尺寸为newSize.而后增长高度.
建立并运行.一切看起来堆垛地很完美!
游戏正确地重设了方块尺寸,可是若是被砍掉的部分能从塔上掉落,游戏会看起来更酷.
在addNewBlock(_:) 下面定义新方法:
func addBrokenBlock(_ currentBoxNode: SCNNode) {
let brokenBoxNode = SCNNode()
brokenBoxNode.name = "Broken \(height)"
if height % 2 == 0 && absoluteOffset.z > 0 {
// 1
brokenBoxNode.geometry = SCNBox(width: CGFloat(currentSize.x),
height: 0.2, length: CGFloat(absoluteOffset.z), chamferRadius: 0)
// 2
if offset.z > 0 {
brokenBoxNode.position.z = currentBoxNode.position.z -
(offset.z/2) - ((currentSize - offset).z/2)
} else {
brokenBoxNode.position.z = currentBoxNode.position.z -
(offset.z/2) + ((currentSize + offset).z/2)
}
brokenBoxNode.position.x = currentBoxNode.position.x
brokenBoxNode.position.y = currentPosition.y
// 3
brokenBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic,
shape: SCNPhysicsShape(geometry: brokenBoxNode.geometry!, options: nil))
brokenBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(colorLiteralRed: 0.01 *
Float(height), green: 0, blue: 1, alpha: 1)
scnScene.rootNode.addChildNode(brokenBoxNode)
// 4
} else if height % 2 != 0 && absoluteOffset.x > 0 {
brokenBoxNode.geometry = SCNBox(width: CGFloat(absoluteOffset.x), height: 0.2,
length: CGFloat(currentSize.z), chamferRadius: 0)
if offset.x > 0 {
brokenBoxNode.position.x = currentBoxNode.position.x - (offset.x/2) -
((currentSize - offset).x/2)
} else {
brokenBoxNode.position.x = currentBoxNode.position.x - (offset.x/2) +
((currentSize + offset).x/2)
}
brokenBoxNode.position.y = currentPosition.y
brokenBoxNode.position.z = currentBoxNode.position.z
brokenBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic,
shape: SCNPhysicsShape(geometry: brokenBoxNode.geometry!, options: nil))
brokenBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(
colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
scnScene.rootNode.addChildNode(brokenBoxNode)
}
}
复制代码
这里你建立了一个新节点并用height来命名.你使用了if语句来肯定坐标轴,并确保偏移大于0,由于等于时不会产生一个方块碎片.
你根据当前位置偏移量的一半来获得碎片的位置.而后,根据方块位置的正负,添加或扣除当前尺寸减去偏移的一半.
在handleTap(_:) 里面的addNewBlock(_:) 以前调用该方法:
addBrokenBlock(currentBoxNode)
复制代码
当碎片节点掉落出视线时,还在不停掉落,并无销毁.在renderer(_:updateAtTime:) 里面最上方添加代码:
for node in scnScene.rootNode.childNodes {
if node.presentation.position.y <= -20 {
node.removeFromParentNode()
}
}
复制代码
这段代码会删除Y值小于-20的全部节点. 运行看看切下的方块!
如今游戏机制的核心部分已经完成了,还有一些收尾工做.当玩家完美对齐上一层时应该有奖励.还有,如今尚未输赢判断,当你失败后也没法开始一个新游戏! 游戏尚未声音,须要添加一些声音.
在处理完美对齐的状况,在addBrokenBlock(_:) 中添加下面方法:
func checkPerfectMatch(_ currentBoxNode: SCNNode) {
if height % 2 == 0 && absoluteOffset.z <= 0.03 {
currentBoxNode.position.z = previousPosition.z
currentPosition.z = previousPosition.z
perfectMatches += 1
if perfectMatches >= 7 && currentSize.z < 1 {
newSize.z += 0.05
}
offset = previousPosition - currentPosition
absoluteOffset = offset.absoluteValue()
newSize = currentSize - absoluteOffset
} else if height % 2 != 0 && absoluteOffset.x <= 0.03 {
currentBoxNode.position.x = previousPosition.x
currentPosition.x = previousPosition.x
perfectMatches += 1
if perfectMatches >= 7 && currentSize.x < 1 {
newSize.x += 0.05
}
offset = previousPosition - currentPosition
absoluteOffset = offset.absoluteValue()
newSize = currentSize - absoluteOffset
} else {
perfectMatches = 0
}
}
复制代码
若是玩家放置位置与上一块在0.03以内,就认为是完美匹配.只要偏差足够近,就设置当前方块的位置等于上一个方块的位置.
经过设置当前和上一次位置相等,让它们在数值上彻底匹配并从新计算偏移和新尺寸.在handleTap(_:) 里面计算偏移和新尺寸以后,调用这个方法:
checkPerfectMatch(currentBoxNode)
复制代码
如今已经处理了完美对齐的状况和部分对齐的状况,但你还须要处理彻底错失的状况.
在handleTap(_:) 内checkPerfectMatch(_:) 以前,添加下面代码:
if height % 2 == 0 && newSize.z <= 0 {
height += 1
currentBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic,
shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
return
} else if height % 2 != 0 && newSize.x <= 0 {
height += 1
currentBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic,
shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
return
}
复制代码
若是玩家错失了方块,计算出的新尺寸应该是负的,检查这个值就知道玩家是否错失了方块.若是玩家错失了,你将高度增长一,这样移动的代码就再也不移动移动当前方块了.而后你再添加一个动态物理形体让方块掉落.
最后,return,这样代码就再也不运行了,如checkPerfectMatch(_:),和addBrokenBlock(_:).
由于音频文件很短,能够预先加载进来.在属性声明中添加一个新的字典属性,命名为sounds:
var sounds = [String: SCNAudioSource]()
复制代码
下一步,在viewDidLoad下面添加两个方法:
func loadSound(name: String, path: String) {
if let sound = SCNAudioSource(fileNamed: path) {
sound.isPositional = false
sound.volume = 1
sound.load()
sounds[name] = sound
}
}
func playSound(sound: String, node: SCNNode) {
node.runAction(SCNAction.playAudio(sounds[sound]!, waitForCompletion: false))
}
复制代码
第一个方法从指定目录加载音频文件并储存到sounds字典中.第二个方法播放储存在sounds字典中的方法.
在viewDidload() 中间添加下面代码:
loadSound(name: "GameOver", path: "HighRise.scnassets/Audio/GameOver.wav")
loadSound(name: "PerfectFit", path: "HighRise.scnassets/Audio/PerfectFit.wav")
loadSound(name: "SliceBlock", path: "HighRise.scnassets/Audio/SliceBlock.wav")
复制代码
有好几个地方须要播放音效.在handleTap(_:) 中,在每个检查玩家是否错失方块的if语句中,添加下面的代码:
playSound(sound: "GameOver", node: currentBoxNode)
复制代码
在调用addNewBlock以后,添加一行:
playSound(sound: "SliceBlock", node: currentBoxNode)
复制代码
滚动到checkPerfectMatch(_:),在两个if语句中分支中添加一行:
playSound(sound: "PerfectFit", node: currentBoxNode)
复制代码
建立并运行---有音效的游戏更有意思了,对吧?
游戏如何结束呢?如今咱们来处理这个问题!
进入Main.storyboard,拖拽一个新的按钮到视图中.改变文本的颜色为 #FF0000,文本内容Play.而后改变字体为Custom, Helvetica Neue, 66.
下一步,设置对齐方式align为中心对齐center,并固定底边constant为100.
拖拽引线到控制器命名为playButton.而后建立一个动做命名为playGame并写入如下代码:
playButton.isHidden = true
let gameScene = SCNScene(named: "HighRise.scnassets/Scenes/GameScene.scn")!
let transition = SKTransition.fade(withDuration: 1.0)
scnScene = gameScene
let mainCamera = scnScene.rootNode.childNode(withName: "Main Camera", recursively: false)!
scnView.present(scnScene, with: transition, incomingPointOfView: mainCamera, completionHandler: nil)
height = 0
scoreLabel.text = "\(height)"
direction = true
perfectMatches = 0
previousSize = SCNVector3(1, 0.2, 1)
previousPosition = SCNVector3(0, 0.1, 0)
currentSize = SCNVector3(1, 0.2, 1)
currentPosition = SCNVector3Zero
let boxNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
boxNode.position.z = -1.25
boxNode.position.y = 0.1
boxNode.name = "Block\(height)"
boxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(colorLiteralRed: 0.01 * Float(height),
green: 0, blue: 1, alpha: 1)
scnScene.rootNode.addChildNode(boxNode)
复制代码
你注意到,你已经重置了游戏中的全部变量为默认值,并添加了第一个方块.
由于已经添加了第一块方块,移除viewDidLoad(_:) 中下面的代码,从声明blockNode到添加到场景中.
//1
let blockNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
blockNode.position.z = -1.25
blockNode.position.y = 0.1
blockNode.name = "Block\(height)"
//2
blockNode.geometry?.firstMaterial?.diffuse.contents =
UIColor(colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
scnScene.rootNode.addChildNode(blockNode)
复制代码
在刚才建立的方法下面定义一个新方法:
func gameOver() {
let mainCamera = scnScene.rootNode.childNode(
withName: "Main Camera", recursively: false)!
let fullAction = SCNAction.customAction(duration: 0.3) { _,_ in
let moveAction = SCNAction.move(to: SCNVector3Make(mainCamera.position.x,
mainCamera.position.y * (3/4), mainCamera.position.z), duration: 0.3)
mainCamera.runAction(moveAction)
if self.height <= 15 {
mainCamera.camera?.orthographicScale = 1
} else {
mainCamera.camera?.orthographicScale = Double(Float(self.height/2) /
mainCamera.position.y)
}
}
mainCamera.runAction(fullAction)
playButton.isHidden = false
}
复制代码
这里,你缩放摄像机镜头来露出整个塔.最后,设置play按钮为可见,这样玩家就能够开始一个新游戏.
在handleTap(_:) 内部,在彻底错失方块的if语句中,调用gameover(),放在return语句以前,两个if语句里面都放:
gameOver()
复制代码
编译运行.当你失败时,就能从新开始一个新游戏了.
游戏启动时会有难看的白屏.打开LaunchScreen.storyboard并拖拽进一个图像视图.四周对齐屏幕:
更改图片为Gradient.png
如今咱们已经将白屏替换为了漂亮的渐变图!
恭喜你,你已经完成了!你能够从这里下载最终完成版final project
end