首页 > 代码库 > SpriteKit可摧毁物理场景的进一步完善

SpriteKit可摧毁物理场景的进一步完善

例子举了一个早期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 = http://www.mamicode.com/["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)
}

运行游戏看看,是不是很有成就感呢?

技术分享

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    SpriteKit可摧毁物理场景的进一步完善