Scene editor与代码挂钩
-
在编辑器中拖相应的组件并设定属性值,步骤如下图所示:
-
挂钩后,编辑器中的木头都属于WoodNode类的子节点,然后在GameScene中创建了他们对应的实例对象:
然后通过节点名称将节点赋值于对应的对象
childNode(withName: String) as! WoodNode
然后就可以为所欲为地操作相应的节点啦(比如上图所示的woodNodeH1.setScale(1.5))
学到这里的时候遇到一个很有用的函数
enumerateChildNodes(withName: "//woodV*", using: {node, _ in
print("\(node.name)")
})
其作用是可以通过节点名称遍历并统一进行相应的操作,这里加上//的意思是从根目录从上到下依次遍历所有的树形结构节点,如果以后这个树形结构非常庞大,这样搜索的效率肯定会很低,不过现在作为练习项目就先这样吧,//woodV* 后面有一个*号是指可以通过数字増序依次搜索woodV1、woodV2...
因为这四块木头具有很多的共同属性,所以我将他们都列入WoodNode中,不仅仅如此,其实他们还单独存在于一个sks文件中,然后通过reference的方式添加到GameScene.sks中。设置reference的视觉化操作很简单,就是拖一个reference的控件,然后将reference属性设置为之前编辑的sks文件即可
物理碰撞检测
使木头自由下落及设置GameScene的边框物理属性
然后我将这几块木头的isDynamic属性值打开,他们就会收到重力影响自由下落,但是默认他们会掉出屏幕之外,所以我需要在func didMove(to view: SKView)中将GameScene边框的physicsBody设置好,他们就能妥妥地掉在屏幕下边框上了:
添加physics body
有以下三种方法添加physics body
- Creating simple bodies in the scene editor
-
Creating simple bodies from code
-
Creating custom bodies
第三种方式就是解决物理外形不一定是texture外形的问题。比如一直猫猫,可能他的物理有效碰撞区域只是身体的那一部分,头和尾巴不是有效的物理碰撞,所以我们可以运用一张纯色的texture并设置为猫猫的有效物理碰撞区域
好了,现在我们设置一个小需求并予以实现,如下图所示的一组木块,默认状态在屏幕上方。开始时,他们会自由下落到屏幕的下边框。
- 现在我们需要检测woodNodeS分别于woodNodeH1、Edge的碰撞情况并打印日志
- 点击木块后,木块都会从屏幕中消失
-
所有木块消失2秒后,游戏回到初始化的状态
检测碰撞
检测碰撞的步骤如下:
- 将碰撞体分类:虽然游戏中的四块木头都属于WoodNode,但是在检测碰撞时它们是四个独立的个体,所以我们需要将他们设置成四个独立的category。
首先通过一个struct建好碰撞体的目录结构如下:
struct PhysicsBodyCategory {
static let WH1: UInt32 = 0b1 //1
static let WS : UInt32 = 0b10 //2
static let WV1: UInt32 = 0b100 //4
static let WV2: UInt32 = 0b1000 //8
static let Edge: UInt32 = 0b10000 //16
}
-
设置category bit mask
然后将四个木块及场景归属到对应的碰撞体结构,可通过编辑器及代码的方式实现。
编辑器:默认的category mask为极大的值,比如我要将woodH1分类到刚刚建好的WH1中,我只需将category mask值设置为2即可
代码:其实更偏向于通过代码来统一管理,既然之前已经通过struct定义好碰撞体的category,那么我只需要将各个木块的category赋值即可,这样可能会更好管理吧?
woodNodeH1 = childNode(withName: "//woodH1") as! WoodNode
woodNodeH1.physicsBody!.categoryBitMask = PhysicsBodyCategory.WH1
// woodNodeH1.setScale(1.5)
woodNodeS = childNode(withName: "//woodS") as! WoodNode
woodNodeS.physicsBody!.categoryBitMask = PhysicsBodyCategory.WS
woodNodeV1 = childNode(withName: "//woodV1") as! WoodNode
woodNodeV1.physicsBody!.categoryBitMask = PhysicsBodyCategory.WV1
woodNodeV2 = childNode(withName: "//woodV2") as! WoodNode
woodNodeV2.physicsBody!.categoryBitMask = PhysicsBodyCategory.WV2
physicsBody!.categoryBitMask = PhysicsBodyCategory.Edge
- 设置collision bit mask
默认的category bit mask 和collision都是4294967295,换做二进制数为11111111111111111111111111111111(32个1),也就是节点之间默认加上物理体后,彼此是有效碰撞的,除非你指定想让某节点与具体的一些节点才会有效碰撞,默认是不需要设置此参数,比如,我想让woodNodeS只与woodNodeH1有效碰撞,那么我设置如下
woodNodeS.physicsBody!.collisionBitMask = PhysicsBodyCategory.WH1
一旦将woodNodeH1消除掉,woodNodeS将掉出屏幕以外。
- 检测碰撞并回调didBegin
我现在是需要在woodNodeS与woodNodeH1、Edge碰撞时检测碰撞并回调相应函数,那么首先我就需要将woodNodeS的contactTestBitMask设置为Edge | WH1,如下所示:
woodNodeS.physicsBody!.contactTestBitMask = PhysicsBodyCategory.Edge | PhysicsBodyCategory.WH1
让GameScene遵从SKPhysicsContactDelegate协议,这样在碰撞时才会调用didBegin函数
class GameScene: SKScene,SKPhysicsContactDelegate
physicsWorld.contactDelegate = self
最后在didBegin中检测碰撞
func didBegin(_ contact: SKPhysicsContact) {
let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if collision == PhysicsBodyCategory.WS | PhysicsBodyCategory.Edge {
print("square hits the floor")
} else if collision == PhysicsBodyCategory.WS | PhysicsBodyCategory.WH1 {
print("sqaure hits the wh1")
}
}
运行代码后,woodNodeS掉落下来后默认就会和woodNodeH1进行碰撞,碰撞检测后的日志也打印出来了
点击木块后,木块都会从屏幕中消失
然后我们就来做点击消除木块的功能,教程中的原话是这么描述的:
To distinguish nodes you can tap on from nodes that are just static decoration you will add a new protocol. Open GameScene.swift and add under the existing protocol declaration for EventListenerNode:
protocol InteractiveNode {
func interact()
}
大概的意思是加了一个InteractiveNode的协议让WoodNode中的实例化对象来遵守,这样就可以区分出你是点的哪个node了。作为新手的我来说,看到这个有点一脸懵逼(即使查了protocol的用法以后也有点懵逼,protocol里面的变量如果不是optional,在实例化的时候比如赋值。但是func呢?为什么我自己另外建了demo发现func不会自动执行呢?留个大大的问号?),先暂时就局限于知其然吧,反正后面会频繁地用到点击事件,在后面的探索中希望能知其所以然。
在WoodNode中设置了以下的点击事件,点击事件会触发将这个node从父节点移除
override func touchesEnded(_ touches: Set, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
interact()
}
func interact() {
print("touches take action")
self.removeFromParent()
}
所有木块消失2秒后,游戏回到初始化的状态
所有木块都属于同一个parent,所以我可以通过判断parent.children是否为空来判断师傅所有木块都消失了,
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if woodNodeS.parent?.children == nil {
//DO SOMETHING HERE
}
}
然后定义初始化场景的函数并在//DO SOMETHING HERE的位置
通过run(SKAction.sequence)的方法等2秒后执行初始化场景的操作
func newGame() {
let scene = GameScene(fileNamed: "GameScene")
scene!.scaleMode = scaleMode
view!.presentScene(scene)
}
run(SKAction.sequence([SKAction.wait(forDuration: 2.0),SKAction.run(newGame)]))
差不多跌跌撞撞就这样了,中间有一些坑,比如判断子节点是否为空我放在了update函数里,这样多多少少会牺牲一些效率吧?正确的做法应该是在减少children的时候interact()再判断一下children是否已经为nil。以事件为导向处理肯定比每一帧调用时来处理效率高吧?但是interact()方法我又放到WoodNode中的,所以暂时不知道怎么去解决这个问题,先留个坑,等“日”后再来填吧