例子举了一个早期DOS下的双人游戏,类似于百战天虫类型。不过有趣的是游戏中实现了可摧毁的物理场景,而且只用了很少的代码:
游戏实现起来十分巧妙和简单,利用了CoreGraphic中的clear混合模式,将香蕉炸弹以中心位置的纹理全部消除,从而实现“摧毁”效果。
游戏中为建筑物单独创建一个类,继承于SKSpriteNode,其中有一个currentImage用来存放当前楼体的纹理:
class BuildingNode: SKSpriteNode {
var currentImage:UIImage!
}
当香蕉炸弹触碰楼体时,我们根据实际接触的中心点制作出爆炸摧毁效果:
func hitAt(point:CGPoint){
let convertedPoint = CGPoint(x: point.x + size.width/2.0, y: abs(point.y - (size.height/2.0)))
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let ctx = UIGraphicsGetCurrentContext()
currentImage.draw(at: CGPoint(x: 0, y: 0))
ctx?.addEllipse(in: CGRect(x: convertedPoint.x - 32, y: convertedPoint.y - 32, width: 64, height: 64))
ctx?.setBlendMode(.clear)
ctx?.drawPath(using: .fill)
let img = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
texture = SKTexture(image: img)
currentImage = img
configurePhysics()
}
最后一句代码configurePhysics用来重建楼体的物理像素精确物理外观,实现纹理和物理边界相符。
虽然作者的构思很棒,不过游戏有一个小小的不足,就是当香蕉炸弹触碰到多于一个楼体的时候,炸弹只会消除第一个碰到的楼体外观,这显得不够真实。
所以本猫在这里就带领大家修复这个bug ;)
我们有两种思路,一是当香蕉碰到楼体时不立即将其销毁,而是给它一定耐久度,只有当耐久度为0时才把它销毁。另外一种思路是遍历所有楼体查找香蕉炸弹爆炸时半径涉及的楼体,然后依次重绘。两种方法都很简单,我们依次来看看。
增加香蕉炸弹耐久度
在创建香蕉时加入如下代码:
banana = SKSpriteNode(imageNamed: "banana")
banana.name = "banana"
banana.physicsBody = SKPhysicsBody(circleOfRadius: banana.size.width/2)
banana.physicsBody!.categoryBitMask = CollisionTypes.banana.rawValue
banana.physicsBody!.collisionBitMask = CollisionTypes.building.rawValue|CollisionTypes.player.rawValue
banana.physicsBody!.contactTestBitMask = banana.physicsBody!.collisionBitMask
banana.physicsBody!.usesPreciseCollisionDetection = true
addChild(banana)
//新加如下代码
banana.userData = ["persistence":2]
在香蕉接触到楼体的方法中加上耐久度处理的代码:
var persistence = (banana.userData as! [String:Int])["persistence"]!
persistence -= 1
banana.userData = ["persistence":persistence]
if persistence == 0{
if timer != nil{
timer.invalidate()
timer = nil
}
banana.name = ""
banana.removeFromParent()
banana = nil
changePlayer()
}
运行App试试看,效果还不错,不过有个问题,当你大力出奇迹甩出香蕉时可能啥楼体都碰不到,直接甩到天涯海角去了,这是你的游戏就会傻傻的萌呆在那里,啥都做不了啊。
所以我们要再加一个超时判断:甩出香蕉后5秒若是无事发生就强制销毁炸弹并且切换用户控制,在GameScene中添加一个属性:
var timer:Timer!
在发射出香蕉的launch方法中加上相关逻辑:
timer = Timer(fire: Date().addingTimeInterval(5.0), interval: 5.0, repeats: false){[unowned self] _ in
DispatchQueue.main.async {
print("timer done!")
self.timer.invalidate()
self.timer = nil
self.banana.name = ""
self.banana.removeFromParent()
self.banana = nil
self.changePlayer()
}
}
运行App,将香蕉甩到天边看看!咦!怎么过了许久也没反应啊!原来当前消息环并不是空闲的,它会被SpriteKit引擎所占用,你的定时器会无限挂起,所以你必须告诉消息环,爷可不是好惹的:
RunLoop.current.add(timer, forMode: .commonModes)
遍历所有爆炸半径内的楼体
我们现在来看看第二种思路,首先我们需要确定炸弹爆炸半径,为了简单其实我取的是爆炸矩形而不是圆形:
let rect = CGRect(x: point.x - 32, y: point.y - 32, width: 64, height: 64)
因为SpriteKit默认Node位置在中心,所以你必须将其转换为UIKit的坐标。
我们新建一个checkHitBuildingAt方法,该方法依次取出场景中所有楼房,计算是否处在爆炸半径内,如果是则将其摧毁 ;)
func checkHitBuildingsAt(_ point:CGPoint){
let rect = CGRect(x: point.x - 32, y: point.y - 32, width: 64, height: 64)
for building in buildings{
if building.frame.intersects(rect){
print("Find hit building : \(building.frame)")
let buildingLocation = convert(point, to: building)
building.hitAt(point: buildingLocation)
}
}
}
修改香蕉炸弹与楼房触碰的代码如下:
func bananaHitBuilding(_ building:BuildingNode,at point:CGPoint){
checkHitBuildingsAt(point)
}
运行游戏看看,是不是很有成就感呢?