位掩码冲突的间歇性错误
Intermittent errors with Bitmask Collisions
我正在 Swift
中使用 SpriteKit
制作 2D 滚动射击游戏。我已经设置 SKPhysicsBody
并使用位掩码进行碰撞。我不断收到间歇性错误,碰撞会正常工作然后停止工作。我得到的错误是 Fatal error: Unexpectedly found nil while unwrapping an Optional value。我不明白为什么有时我得到零值,而其他时候却得到值。我在游戏中有几个不同的精灵,在进行了大量测试以查看碰撞是否有任何差异之后,我似乎找不到问题所在。例如,玩了几次,我用激光射击了一颗小行星,效果很好。第二天,完全相同的事情使游戏崩溃。另一个示例小行星撞击玩家头部并且工作正常,小行星从侧面撞击玩家导致游戏崩溃但第二天可以正常工作。我不知道问题是否出在我为每个精灵设置 PhysicsBody 的方式上,因为我已经尝试更改它但仍然有问题,或者我的 SKPhysicsContact
设置完全错误。非常感谢任何帮助,谢谢。
我的代码的精简版
import SpriteKit
import GameplayKit
import CoreMotion
@objcMembers
class GameScene: SKScene, SKPhysicsContactDelegate {
//Player Image
let player = SKSpriteNode(imageNamed: "Player.png")
//Timer to spawn enemies
var gameTimer:Timer!
//Array for different astroids
var astroidArray = ["astroid1", "astroid2"]
//Array for differnet enemy ships
var enemyArray = ["Enemy1"]
//For collision
let playerCategory:UInt32 = 0x1 << 1
let playerLaserCategory:UInt32 = 0x1 << 2
let astroidCategory:UInt32 = 0x1 << 3
let enemyCategory:UInt32 = 0x1 << 4
let bossCategory:UInt32 = 0x1 << 5
override func didMove(to view: SKView) {
//Position Player
player.position.y = -400
player.zPosition = 1
addChild(player)
//Player Physics for collision
//player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.physicsBody?.isDynamic = true
player.physicsBody?.categoryBitMask = playerCategory
player.physicsBody?.contactTestBitMask = astroidCategory | enemyCategory | bossCategory
//avoid any unwanted collisions
//player.physicsBody?.collisionBitMask = 0
//Physics for World
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
physicsWorld.contactDelegate = self
//Timer to spawn astroids
gameTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(addAstroid), userInfo: nil, repeats: true)
//Timer to spawn enemy
gameTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(addEnemy), userInfo: nil, repeats: true)
}
func addAstroid() {
astroidArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: astroidArray) as! [String]
//Select astroid from array
let astroid = SKSpriteNode(imageNamed: astroidArray[0])
//GameplayKit randomization services to spawn different astroids
let randomAstroidPosition = GKRandomDistribution(lowestValue: -350, highestValue: 350)
//Randomly spawn astroid in different positions
let position = CGFloat(randomAstroidPosition.nextInt())
astroid.position = CGPoint(x: position, y: self.frame.size.height + astroid.size.height)
astroid.zPosition = 1
//Astroid Physics for collision
astroid.physicsBody = SKPhysicsBody(circleOfRadius: astroid.size.width / 2)
astroid.physicsBody?.isDynamic = true
astroid.physicsBody?.categoryBitMask = astroidCategory
astroid.physicsBody?.contactTestBitMask = playerLaserCategory | playerCategory
//avoid any unwanted collisions
//astroid.physicsBody?.collisionBitMask = 0
addChild(astroid)
//Astroid speed
let animationDuration:TimeInterval = 6
//Clean up, remove astroids once reached a certain distance
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: position, y: -700), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
astroid.run(SKAction.sequence(actionArray))
}
func addEnemy() {
enemyArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: enemyArray) as! [String]
//Select enemy from array
let enemy = SKSpriteNode(imageNamed: enemyArray[0])
//GameplayKit randomization services to spawn different enemies
let randomEnemyPosition = GKRandomDistribution(lowestValue: -350, highestValue: 350)
//Randomly spawn enemy in different positions
let position = CGFloat(randomEnemyPosition.nextInt())
enemy.position = CGPoint(x: position, y: self.frame.size.height + enemy.size.height)
enemy.zPosition = 1
//Enemy Physics for collision
enemy.physicsBody = SKPhysicsBody(circleOfRadius: enemy.size.width / 2)
enemy.physicsBody?.isDynamic = true
enemy.physicsBody?.categoryBitMask = enemyCategory
enemy.physicsBody?.contactTestBitMask = playerLaserCategory | playerCategory
//avoid any unwanted collisions
//enemy.physicsBody?.collisionBitMask = 0
if score >= 20 {
addChild(enemy)
}
//Enemy speed
let animationDuration:TimeInterval = 6
//Clean up, remove enemy once reached a certain distance
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: position, y: -700), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
enemy.run(SKAction.sequence(actionArray))
}
func fireLaser() {
//Sound effect
self.run(SKAction.playSoundFileNamed("laser.wav", waitForCompletion: false))
//Create and position laser
let playerLaser = SKSpriteNode(imageNamed: "laser")
playerLaser.position = player.position
playerLaser.position.y += 65
//Laser Physics
playerLaser.physicsBody = SKPhysicsBody(circleOfRadius: playerLaser.size.width / 2)
playerLaser.physicsBody?.isDynamic = true
playerLaser.physicsBody?.categoryBitMask = playerLaserCategory
playerLaser.physicsBody?.contactTestBitMask = astroidCategory | enemyCategory
//avoid any unwanted collisions
//playerLaser.physicsBody?.collisionBitMask = 0
playerLaser.physicsBody?.usesPreciseCollisionDetection = true
addChild(playerLaser)
//Animation for laser firing
let animationDuration:TimeInterval = 0.3
//Clean up, removes laser blast from game
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: player.position.x, y: self.frame.size.height), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
playerLaser.run(SKAction.sequence(actionArray))
}
//Function for physics to know what object hit what
func didBegin(_ contact: SKPhysicsContact) {
var A:SKPhysicsBody
var B:SKPhysicsBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
A = contact.bodyA
B = contact.bodyB
} else {
A = contact.bodyB
B = contact.bodyA
}
//PlayerLaser is A and Astroid is B
if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerLaserHitAstroid(laserNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//PlayerLaser is A and Enemy is B
else if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerLaserHitEnemy(laserNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
//Player is A and Astroid is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerHitAstroid(playerNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//Player is A and Enemy is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerHitEnemy(playerNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
}
//Function for playerLaser to destroy Astroid
func playerLaserHitAstroid (laserNode:SKSpriteNode, astroidNode:SKSpriteNode) {
//Create explosion effect
let explosion = SKEmitterNode(fileNamed: "Explosion")!
explosion.position = astroidNode.position
addChild(explosion)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
laserNode.removeFromParent()
astroidNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosion.removeFromParent()
}
print("laser hit astroid")
//Add score
score += 5
}
//Function for playerLaser to destroy Enemy
func playerLaserHitEnemy (laserNode:SKSpriteNode, enemyNode:SKSpriteNode) {
//Create explosion effect
let explosion = SKEmitterNode(fileNamed: "Explosion")!
explosion.position = enemyNode.position
addChild(explosion)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
laserNode.removeFromParent()
enemyNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosion.removeFromParent()
}
print("laser hit enemy")
//Add score
score += 10
}
//Function for when player and astroid collide
func playerHitAstroid(playerNode:SKSpriteNode, astroidNode:SKSpriteNode) {
let explosionA = SKEmitterNode(fileNamed: "Explosion")!
explosionA.position = astroidNode.position
explosionA.zPosition = 3
addChild(explosionA)
print("Player hit astroid")
// let explosionB = SKEmitterNode(fileNamed: "Explosion")!
// explosionB.position = playerNode.position
// explosionB.zPosition = 3
// addChild(explosionB)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
//playerNode.removeFromParent()
astroidNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosionA.removeFromParent()
//explosionB.removeFromParent()
}
//Removes a life when hit
if livesArray.count > 0 {
let lifeNode = livesArray.first
lifeNode?.removeFromParent()
livesArray.removeFirst()
}
//Remove player when all lives are gone
if livesArray.count == 0 {
playerNode.removeFromParent()
let transition = SKTransition.flipHorizontal(withDuration: 0.5)
let gameOver = GameOverScene(fileNamed: "GameOverScene")!
gameOver.score = self.score
gameOver.scaleMode = scaleMode
self.view?.presentScene(gameOver, transition: transition)
}
}
//Function for when player and enemy collide
func playerHitEnemy(playerNode:SKSpriteNode, enemyNode:SKSpriteNode) {
let explosionA = SKEmitterNode(fileNamed: "Explosion")!
explosionA.position = enemyNode.position
explosionA.zPosition = 3
addChild(explosionA)
print("Player hit enemy")
// let explosionB = SKEmitterNode(fileNamed: "Explosion")!
// explosionB.position = playerNode.position
// explosionB.zPosition = 3
// addChild(explosionB)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
//playerNode.removeFromParent()
enemyNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosionA.removeFromParent()
//explosionB.removeFromParent()
}
//Removes a life when hit
if livesArray.count > 0 {
let lifeNode = livesArray.first
lifeNode?.removeFromParent()
livesArray.removeFirst()
}
//Remove player when all lives are gone
if livesArray.count == 0 {
playerNode.removeFromParent()
let transition = SKTransition.flipHorizontal(withDuration: 0.5)
let gameOver = GameOverScene(fileNamed: "GameOverScene")!
gameOver.score = self.score
gameOver.scaleMode = scaleMode
self.view?.presentScene(gameOver, transition: transition)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
fireLaser()
}
}
我想我已经修好了。我会在这里 post 解决方案,以防将来有人遇到与我相同的问题。我有这样的玩家 physicsBody
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
像这样以一种奇怪的方式工作,正面碰撞效果很好,但侧面的任何撞击都会使游戏崩溃。所以我把它改回原来的方式
player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
我之前更改过这个,因为当一颗小行星靠近但没有被击中时,它会将精灵拖出位置。我通过设置
解决了这个问题
player.physicsBody?.collisionBitMask = 0
现在,碰撞正在按预期进行,或者它们现在正在进行。
我不确定问题是否已解决。当我尝试您的代码时,它崩溃了,因为其中一个接触体节点为零。当从纹理、矩形和圆形创建物理体时,我能够产生崩溃。没关系...问题不在于:)
这是因为您在物理模拟完成之前删除了节点。
看看一帧的样子:
所以发生的事情是你在物理模拟完成之前删除了你的节点,所以物理引擎保留物理体导致它需要完成计算,但是节点被删除了。
因此 didBegin 中为 nil。所以解决方案是创建一个变量来保存要删除的节点:
private var trash:[SKNode] = []
然后在每个有物理体节点的地方执行此操作:
(比如你的 playerHitAsteroid 方法)
trash.append(laserNode)
trash.append(astroidNode)
self.run(SKAction.wait(forDuration: 2)) {[weak self] in
guard let `self` = self else {return}
self.trash.append(explosion)
}
您在应用程序中几乎没有更多地方可以更改此设置。也看看这部分:
if livesArray.count == 0 {
trash.append(playerNode)
print("Game over")
}
还有更多。但是当你像这样在所有地方修复它时,你就可以通过覆盖 didSimulatePhysics
来实现实际删除
override func didSimulatePhysics() {
//first go through every node and remove it from parent
trash.map { node in
node.run(SKAction.sequence([SKAction.fadeOut(withDuration: 0.25), SKAction.removeFromParent()]))
}
trash.removeAll() // then empty thrash array before next frame
}
最后你可以像这样改变didBegin,只是为了立即捕获这个错误。如果您遵循以下策略,就不会发生这种情况:
- 在 didSimulatePhysics 中删除带有物理实体的节点
- 正确设置位掩码(看起来,你做对了)
也许值得一提的是要小心 Timer。检查 this。这是很久以前的事了,也许有些事情已经改变了,我最近没有测试它,但我仍然更喜欢 update
方法或 SKAction
来进行我游戏中与时间相关的操作。
所以,像这样更改 didBegin
:
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
fatalError("Physics body without its node detected!")
}
let A = contact.bodyA
let B = contact.bodyB
//PlayerLaser is A and Astroid is B
if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerLaserHitAstroid(laserNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//PlayerLaser is A and Enemy is B
else if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerLaserHitEnemy(laserNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
//Player is A and Astroid is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerHitAstroid(playerNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//Player is A and Enemy is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerHitEnemy(playerNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
}
不需要,但我觉得这种实现接触检测的方式(通过切换掩码)有点多readable,如果你想看一下
一个与此节点删除无关的建议...不要使用那么多强制解包! :D
我正在 Swift
中使用 SpriteKit
制作 2D 滚动射击游戏。我已经设置 SKPhysicsBody
并使用位掩码进行碰撞。我不断收到间歇性错误,碰撞会正常工作然后停止工作。我得到的错误是 Fatal error: Unexpectedly found nil while unwrapping an Optional value。我不明白为什么有时我得到零值,而其他时候却得到值。我在游戏中有几个不同的精灵,在进行了大量测试以查看碰撞是否有任何差异之后,我似乎找不到问题所在。例如,玩了几次,我用激光射击了一颗小行星,效果很好。第二天,完全相同的事情使游戏崩溃。另一个示例小行星撞击玩家头部并且工作正常,小行星从侧面撞击玩家导致游戏崩溃但第二天可以正常工作。我不知道问题是否出在我为每个精灵设置 PhysicsBody 的方式上,因为我已经尝试更改它但仍然有问题,或者我的 SKPhysicsContact
设置完全错误。非常感谢任何帮助,谢谢。
我的代码的精简版
import SpriteKit
import GameplayKit
import CoreMotion
@objcMembers
class GameScene: SKScene, SKPhysicsContactDelegate {
//Player Image
let player = SKSpriteNode(imageNamed: "Player.png")
//Timer to spawn enemies
var gameTimer:Timer!
//Array for different astroids
var astroidArray = ["astroid1", "astroid2"]
//Array for differnet enemy ships
var enemyArray = ["Enemy1"]
//For collision
let playerCategory:UInt32 = 0x1 << 1
let playerLaserCategory:UInt32 = 0x1 << 2
let astroidCategory:UInt32 = 0x1 << 3
let enemyCategory:UInt32 = 0x1 << 4
let bossCategory:UInt32 = 0x1 << 5
override func didMove(to view: SKView) {
//Position Player
player.position.y = -400
player.zPosition = 1
addChild(player)
//Player Physics for collision
//player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.physicsBody?.isDynamic = true
player.physicsBody?.categoryBitMask = playerCategory
player.physicsBody?.contactTestBitMask = astroidCategory | enemyCategory | bossCategory
//avoid any unwanted collisions
//player.physicsBody?.collisionBitMask = 0
//Physics for World
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
physicsWorld.contactDelegate = self
//Timer to spawn astroids
gameTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(addAstroid), userInfo: nil, repeats: true)
//Timer to spawn enemy
gameTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(addEnemy), userInfo: nil, repeats: true)
}
func addAstroid() {
astroidArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: astroidArray) as! [String]
//Select astroid from array
let astroid = SKSpriteNode(imageNamed: astroidArray[0])
//GameplayKit randomization services to spawn different astroids
let randomAstroidPosition = GKRandomDistribution(lowestValue: -350, highestValue: 350)
//Randomly spawn astroid in different positions
let position = CGFloat(randomAstroidPosition.nextInt())
astroid.position = CGPoint(x: position, y: self.frame.size.height + astroid.size.height)
astroid.zPosition = 1
//Astroid Physics for collision
astroid.physicsBody = SKPhysicsBody(circleOfRadius: astroid.size.width / 2)
astroid.physicsBody?.isDynamic = true
astroid.physicsBody?.categoryBitMask = astroidCategory
astroid.physicsBody?.contactTestBitMask = playerLaserCategory | playerCategory
//avoid any unwanted collisions
//astroid.physicsBody?.collisionBitMask = 0
addChild(astroid)
//Astroid speed
let animationDuration:TimeInterval = 6
//Clean up, remove astroids once reached a certain distance
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: position, y: -700), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
astroid.run(SKAction.sequence(actionArray))
}
func addEnemy() {
enemyArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: enemyArray) as! [String]
//Select enemy from array
let enemy = SKSpriteNode(imageNamed: enemyArray[0])
//GameplayKit randomization services to spawn different enemies
let randomEnemyPosition = GKRandomDistribution(lowestValue: -350, highestValue: 350)
//Randomly spawn enemy in different positions
let position = CGFloat(randomEnemyPosition.nextInt())
enemy.position = CGPoint(x: position, y: self.frame.size.height + enemy.size.height)
enemy.zPosition = 1
//Enemy Physics for collision
enemy.physicsBody = SKPhysicsBody(circleOfRadius: enemy.size.width / 2)
enemy.physicsBody?.isDynamic = true
enemy.physicsBody?.categoryBitMask = enemyCategory
enemy.physicsBody?.contactTestBitMask = playerLaserCategory | playerCategory
//avoid any unwanted collisions
//enemy.physicsBody?.collisionBitMask = 0
if score >= 20 {
addChild(enemy)
}
//Enemy speed
let animationDuration:TimeInterval = 6
//Clean up, remove enemy once reached a certain distance
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: position, y: -700), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
enemy.run(SKAction.sequence(actionArray))
}
func fireLaser() {
//Sound effect
self.run(SKAction.playSoundFileNamed("laser.wav", waitForCompletion: false))
//Create and position laser
let playerLaser = SKSpriteNode(imageNamed: "laser")
playerLaser.position = player.position
playerLaser.position.y += 65
//Laser Physics
playerLaser.physicsBody = SKPhysicsBody(circleOfRadius: playerLaser.size.width / 2)
playerLaser.physicsBody?.isDynamic = true
playerLaser.physicsBody?.categoryBitMask = playerLaserCategory
playerLaser.physicsBody?.contactTestBitMask = astroidCategory | enemyCategory
//avoid any unwanted collisions
//playerLaser.physicsBody?.collisionBitMask = 0
playerLaser.physicsBody?.usesPreciseCollisionDetection = true
addChild(playerLaser)
//Animation for laser firing
let animationDuration:TimeInterval = 0.3
//Clean up, removes laser blast from game
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: player.position.x, y: self.frame.size.height), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
playerLaser.run(SKAction.sequence(actionArray))
}
//Function for physics to know what object hit what
func didBegin(_ contact: SKPhysicsContact) {
var A:SKPhysicsBody
var B:SKPhysicsBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
A = contact.bodyA
B = contact.bodyB
} else {
A = contact.bodyB
B = contact.bodyA
}
//PlayerLaser is A and Astroid is B
if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerLaserHitAstroid(laserNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//PlayerLaser is A and Enemy is B
else if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerLaserHitEnemy(laserNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
//Player is A and Astroid is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerHitAstroid(playerNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//Player is A and Enemy is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerHitEnemy(playerNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
}
//Function for playerLaser to destroy Astroid
func playerLaserHitAstroid (laserNode:SKSpriteNode, astroidNode:SKSpriteNode) {
//Create explosion effect
let explosion = SKEmitterNode(fileNamed: "Explosion")!
explosion.position = astroidNode.position
addChild(explosion)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
laserNode.removeFromParent()
astroidNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosion.removeFromParent()
}
print("laser hit astroid")
//Add score
score += 5
}
//Function for playerLaser to destroy Enemy
func playerLaserHitEnemy (laserNode:SKSpriteNode, enemyNode:SKSpriteNode) {
//Create explosion effect
let explosion = SKEmitterNode(fileNamed: "Explosion")!
explosion.position = enemyNode.position
addChild(explosion)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
laserNode.removeFromParent()
enemyNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosion.removeFromParent()
}
print("laser hit enemy")
//Add score
score += 10
}
//Function for when player and astroid collide
func playerHitAstroid(playerNode:SKSpriteNode, astroidNode:SKSpriteNode) {
let explosionA = SKEmitterNode(fileNamed: "Explosion")!
explosionA.position = astroidNode.position
explosionA.zPosition = 3
addChild(explosionA)
print("Player hit astroid")
// let explosionB = SKEmitterNode(fileNamed: "Explosion")!
// explosionB.position = playerNode.position
// explosionB.zPosition = 3
// addChild(explosionB)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
//playerNode.removeFromParent()
astroidNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosionA.removeFromParent()
//explosionB.removeFromParent()
}
//Removes a life when hit
if livesArray.count > 0 {
let lifeNode = livesArray.first
lifeNode?.removeFromParent()
livesArray.removeFirst()
}
//Remove player when all lives are gone
if livesArray.count == 0 {
playerNode.removeFromParent()
let transition = SKTransition.flipHorizontal(withDuration: 0.5)
let gameOver = GameOverScene(fileNamed: "GameOverScene")!
gameOver.score = self.score
gameOver.scaleMode = scaleMode
self.view?.presentScene(gameOver, transition: transition)
}
}
//Function for when player and enemy collide
func playerHitEnemy(playerNode:SKSpriteNode, enemyNode:SKSpriteNode) {
let explosionA = SKEmitterNode(fileNamed: "Explosion")!
explosionA.position = enemyNode.position
explosionA.zPosition = 3
addChild(explosionA)
print("Player hit enemy")
// let explosionB = SKEmitterNode(fileNamed: "Explosion")!
// explosionB.position = playerNode.position
// explosionB.zPosition = 3
// addChild(explosionB)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
//playerNode.removeFromParent()
enemyNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosionA.removeFromParent()
//explosionB.removeFromParent()
}
//Removes a life when hit
if livesArray.count > 0 {
let lifeNode = livesArray.first
lifeNode?.removeFromParent()
livesArray.removeFirst()
}
//Remove player when all lives are gone
if livesArray.count == 0 {
playerNode.removeFromParent()
let transition = SKTransition.flipHorizontal(withDuration: 0.5)
let gameOver = GameOverScene(fileNamed: "GameOverScene")!
gameOver.score = self.score
gameOver.scaleMode = scaleMode
self.view?.presentScene(gameOver, transition: transition)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
fireLaser()
}
}
我想我已经修好了。我会在这里 post 解决方案,以防将来有人遇到与我相同的问题。我有这样的玩家 physicsBody
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
像这样以一种奇怪的方式工作,正面碰撞效果很好,但侧面的任何撞击都会使游戏崩溃。所以我把它改回原来的方式
player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
我之前更改过这个,因为当一颗小行星靠近但没有被击中时,它会将精灵拖出位置。我通过设置
解决了这个问题player.physicsBody?.collisionBitMask = 0
现在,碰撞正在按预期进行,或者它们现在正在进行。
我不确定问题是否已解决。当我尝试您的代码时,它崩溃了,因为其中一个接触体节点为零。当从纹理、矩形和圆形创建物理体时,我能够产生崩溃。没关系...问题不在于:)
这是因为您在物理模拟完成之前删除了节点。
看看一帧的样子:
所以发生的事情是你在物理模拟完成之前删除了你的节点,所以物理引擎保留物理体导致它需要完成计算,但是节点被删除了。
因此 didBegin 中为 nil。所以解决方案是创建一个变量来保存要删除的节点:
private var trash:[SKNode] = []
然后在每个有物理体节点的地方执行此操作:
(比如你的 playerHitAsteroid 方法)
trash.append(laserNode)
trash.append(astroidNode)
self.run(SKAction.wait(forDuration: 2)) {[weak self] in
guard let `self` = self else {return}
self.trash.append(explosion)
}
您在应用程序中几乎没有更多地方可以更改此设置。也看看这部分:
if livesArray.count == 0 {
trash.append(playerNode)
print("Game over")
}
还有更多。但是当你像这样在所有地方修复它时,你就可以通过覆盖 didSimulatePhysics
override func didSimulatePhysics() {
//first go through every node and remove it from parent
trash.map { node in
node.run(SKAction.sequence([SKAction.fadeOut(withDuration: 0.25), SKAction.removeFromParent()]))
}
trash.removeAll() // then empty thrash array before next frame
}
最后你可以像这样改变didBegin,只是为了立即捕获这个错误。如果您遵循以下策略,就不会发生这种情况:
- 在 didSimulatePhysics 中删除带有物理实体的节点
- 正确设置位掩码(看起来,你做对了)
也许值得一提的是要小心 Timer。检查 this。这是很久以前的事了,也许有些事情已经改变了,我最近没有测试它,但我仍然更喜欢 update
方法或 SKAction
来进行我游戏中与时间相关的操作。
所以,像这样更改 didBegin
:
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
fatalError("Physics body without its node detected!")
}
let A = contact.bodyA
let B = contact.bodyB
//PlayerLaser is A and Astroid is B
if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerLaserHitAstroid(laserNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//PlayerLaser is A and Enemy is B
else if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerLaserHitEnemy(laserNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
//Player is A and Astroid is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerHitAstroid(playerNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//Player is A and Enemy is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerHitEnemy(playerNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
}
不需要,但我觉得这种实现接触检测的方式(通过切换掩码)有点多readable,如果你想看一下
一个与此节点删除无关的建议...不要使用那么多强制解包! :D