I am making a sprite based game. While applying physics to my coin sprites, I have begun to encounter this error. Any suggestions on how to avoid it ?
#objc func spawnEnemy(){
enemy = childNode(withName: "enemy") as? SKSpriteNode
coin = childNode(withName: "coin") as? SKSpriteNode
self.physicsWorld.contactDelegate = self
and later
for coin in sprites{
if coin.name == "coin"{
coin.physicsBody = SKPhysicsBody(rectangleOf: enemy.size)
coin.physicsBody?.categoryBitMask = PhysicsCategory.coin
coin.physicsBody?.contactTestBitMask = PhysicsCategory.player
coin.physicsBody?.affectedByGravity = false
coin.physicsBody?.isDynamic = true
}
}
You should always safely unwrap optional values by using "if let" or "guard let"
by using if let :
if let coin = coin , let name = coin.name {
// you can use "coin" or "name" safely here
}else{
// "coin" or "name" is nil
}
by using guard let :
guard let coin = coin , let name = coin.name else {
// "coin" or "name" is nil
return
}
//you can use "coin" or "name" safely here
Related
I'm currently developing an iOS app that uses ARKit and SceneKit for the augmented reality.
I'm having a problem while loading an .usdz model into the scene.
I can load it correctly into the scene, but, when I try to get the node name (in which I loaded the .usdz model) after tapping on it, it returns the .usdz name and not the name I gave to it.
The code I use to load the .usdz model is:
let mdlAsset = MDLAsset(url: urlPath)
mdlAsset.loadTextures()
let asset = mdlAsset.object(at: 0) // extract first object
var assetNode = SCNNode(mdlObject: asset)
assetNode = SCNNode(mdlObject: asset)
assetNode.name = "Node-2"
sceneView.scene.rootNode.addChildNode(assetNode)
To capture the tap on the node, the code is:
#objc func handleTap(recognizer: UITapGestureRecognizer){
let location = recognizer.location(in: sceneView)
let results = sceneView.hitTest(location, options: nil)
guard recognizer.state == .ended else { return }
if results.count > 0 {
let result = results[0] as SCNHitTestResult
let node = result.node
print(node.name)
}
}
As I mentioned before, when I tap on the object it prints
Optional("Sphere_0")
value that can be found in the top right corner, in the model details page.
The correct value that I expected was "Node-2".
The name of your USDZ model isn't overridden. It's the peculiarities of SceneKit hit-testing and scene hierarchy. When you perform a hit-test search, SceneKit looks for SCNGeometry objects (not a main node) along the ray you specify. So, all you need to do, once the hit-test is completed, is to find the corresponding parent nodes.
Try this code:
import SceneKit.ModelIO
class GameViewController: UIViewController {
var sceneView: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView = (self.view as! SCNView)
let scene = SCNScene(named: "art.scnassets/ship.scn")!
sceneView.scene = SCNScene()
sceneView.backgroundColor = .black
let recog = UITapGestureRecognizer(target: self, action: #selector(tap))
sceneView.addGestureRecognizer(recog)
// ASSET
let mdlAsset = MDLAsset(scnScene: scene)
let asset = mdlAsset.object(at: 0)
let node = SCNNode(mdlObject: asset.children[0])
node.name = "Main-Node-Name" // former "ship"
node.childNodes[0].name = "SubNode-Name" // former "shipMesh"
node.childNodes[0].childNodes[0].name = "Geo-Name" // former "Scrap_MeshShape"
sceneView.scene?.rootNode.addChildNode(node)
}
}
And your hit-testing method:
extension GameViewController {
#objc func tap(recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: sceneView)
let results = sceneView.hitTest(location)
guard recognizer.state == .ended else { return }
if results.count > 0 {
let result = results[0] as SCNHitTestResult
let node = result.node
print(node.name!) // Geo-Name
print(node.parent!.name!) // SubNode-Name
print(node.parent!.parent!.name!) // Main-Node-Name
}
}
}
I am getting the node attached to a contact body (SKPhysicsBody), in a function called while detecting a contact between to SKSpriteNode. Sometimes, I get an error because it can not get the node attached to the contact body, so I test if there's a node to avoid that type of error. But a warning tells me it will never return false. I've not had anymore error like this since I test that but I'm not sure if it works well or if it can still happen. Do you have any idea?
//The actual code
func didBegin(_ contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == enemyCategory) && (contact.bodyB.categoryBitMask == shootCategory){ //test if it is the contact with I want to catch
let shootNode = contact.bodyB.node, shootNode != nil { // I test if there's a node attached to the enemyShootNode but it's supposed never to return false
let enemyNode = contact.bodyA.node, enemyNode != nil { // I test if there's a node attached to the shootNode but it's supposed never to return false
let enemySKNode = enemyNode as? SKSpriteNode
let shootSKNode = shootNode as? SKSpriteNode
// code
}
}
}
}
// The code with error
func didBegin(_ contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == enemyCategory) && (contact.bodyB.categoryBitMask == shootCategory){
let shootNode = contact.bodyB.node!
let enemyNode = contact.bodyA.node! // The error occurs here : "fatal error: unexpectedly found nil while unwrapping an Optional value"
let enemySKNode = enemyNode as? SKSpriteNode
let shootSKNode = shootNode as? SKSpriteNode
}
}
Thanks for your advices, the error indeed comes from the SKPhysics property (the didBegin function is called several times) but I didn't manage to fix.
Although I corrected the way I was checking if a sprite was here, and there are no more warnings or errors so I suppose it works well :
func didBegin(_ contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == enemyCategory) && (contact.bodyB.categoryBitMask == shootCategory) || (contact.bodyA.categoryBitMask == shootCategory) && (contact.bodyB.categoryBitMask == enemyCategory){
if var shootNode = contact.bodyB.node{
if var enemyNode = contact.bodyA.node{
// If the enemyNode and the shootNode don't respectively correspond to
if contact.bodyA.categoryBitMask == shootCategory {
shootNode = contact.bodyA.node!
enemyNode = contact.bodyB.node!
}
let enemySKNode = enemyNode as? SKSpriteNode
let shootSKNode = shootNode as? SKSpriteNode
}
}
}
}
I have been stuck in this part, I have been trying to removeFromParent but it doesnt work. In my game when an enemy colide 3 times with the player the game is over, the problem is that the enemies continue coming for some seconds and then dissapear but continue makeing damage to the player.
func didBeginContact(contact: SKPhysicsContact) {
let body1 = contact.bodyA.node as! SKSpriteNode
let body2 = contact.bodyB.node as! SKSpriteNode
if ((body1.name == "circuloPrincipal") && (body2.name == "enemigo")) {
colisionPrincipal(body2)
}else {
((body1.name == "enemigo") && (body2.name == "circuloPrincipal"))
colisionPrincipal(body1)
}
}
func colisionPrincipal(enemigo: SKSpriteNode) {
if hits < 2 && circuloPrincipal.color != enemigo.color{
shakeFrame(scene!)
circuloPrincipal.runAction(SKAction.scaleBy(1.5, duration:0.5))
enemigo.removeFromParent()
let particula = SKEmitterNode(fileNamed: "particulas.sks")
particula?.position = enemigo.position
particula?.hidden = false
particula?.runAction(SKAction.fadeOutWithDuration(0.8))
self.addChild(particula!)
hits += 1
}else if circuloPrincipal.color == enemigo.color {
enemigo.physicsBody?.affectedByGravity = false
enemigo.physicsBody?.dynamic = true
enemigo.removeFromParent()
score += 1
scoreLabel.text = "\(score)"
}else {
shakeFrame(scene!)
gameStarted = false
enemigo.removeFromParent()
enemigoTimer.invalidate()
highscoreLabel.runAction(SKAction.fadeInWithDuration(0.5))
if score > highscore {
let highscoreDefault = NSUserDefaults.standardUserDefaults()
highscore = score
highscoreDefault.setInteger(highscore, forKey: "highscore")
highscoreLabel.text = "Best: \(highscore)"
}
}
}
OK, so I don't have all that much information about your game to go by, but if you don't want the enemies to keep damaging the player the quick and dirty fix would be to add a guard to the top of your didBeginContact function:
func didBeginContact(contact: SKPhysicsContact) {
guard hits < 3 else {
return
}
let body1 = contact.bodyA.node as! SKSpriteNode
let body2 = contact.bodyB.node as! SKSpriteNode
if ((body1.name == "circuloPrincipal") && (body2.name == "enemigo")) {
colisionPrincipal(body2)
}else {
((body1.name == "enemigo") && (body2.name == "circuloPrincipal"))
colisionPrincipal(body1)
}
}
This should at least prevent anything to get called based on contacts after the required number of hits have occurred. As for cleaning up the sprites, this sounds rather simple (removeFromParent at the appropriate time) but I haven't got a clue about how you handle your game-states so it's hard to comment any further.
Personally I'd trigger it from the update() function if the required state has happened...
I'm trying to have a value be saved onto a sprite's user data and be able to read it off.
Heres what I have, for some reason the value never gets saved and stays as nil.
import SpriteKit
let frame = SKScene()
func createSprite() {
let sprite = SKSpriteNode()
sprite.userData?.setValue("100", forKeyPath: "key")
frame.addChild(sprite)
}
createSprite()
for child in frame.children where child is SKSpriteNode {
print(child.userData?.valueForKey("key") as? String)
//prints the value saved in the childs user data
if child.userData?.valueForKey("key") as? String == "100" {
print("It was a Success!")
}
if child.userData?.valueForKey("key") as? String == nil {
print("There was a problem :(")
}
}
I believe your issue is that the userData starts out as nil. Try this...
func createSprite() {
let sprite = SKSpriteNode()
//init NSMutableDictionary
sprite.userData = NSMutableDictionary()
sprite.userData?.setValue("100", forKeyPath: "key")
frame.addChild(sprite)
}
Hopefully that helps.
I have a problem with my func contact because the second contact don't work (contact with Bonus and Vaisseau) but the two others contact don't work.
So this is my code :
The function :
func didBeginContact(contact: SKPhysicsContact) {
let PremierBody : SKPhysicsBody = contact.bodyA
let SecondBody : SKPhysicsBody = contact.bodyB
if ((PremierBody.categoryBitMask == PhysicsCategories.Meteorites) && (SecondBody.categoryBitMask == PhysicsCategories.Meteorites)) {
contactEntreMeteorites(PremierBody.node as! SKSpriteNode, Meteorites2: SecondBody.node as! SKSpriteNode)
}
else if ((PremierBody.categoryBitMask == PhysicsCategories.Bonus) && (SecondBody.categoryBitMask == PhysicsCategories.Vaisseau) ||
(PremierBody.categoryBitMask == PhysicsCategories.Vaisseau) && (SecondBody.categoryBitMask == PhysicsCategories.Bonus)){
gameOver(PremierBody.node as! SKSpriteNode, Vaisseau: SecondBody.node as! SKSpriteNode)
print("CONTACT")
}
else if ((PremierBody.categoryBitMask == PhysicsCategories.Meteorites) && (SecondBody.categoryBitMask == PhysicsCategories.Vaisseau) ||
(PremierBody.categoryBitMask == PhysicsCategories.Vaisseau) && (SecondBody.categoryBitMask == PhysicsCategories.Meteorites)){
gameOver(PremierBody.node as! SKSpriteNode, Vaisseau: SecondBody.node as! SKSpriteNode)
print("Couco")
}
}
And this is my Physics categories :
struct PhysicsCategories {
static let Meteorites : UInt32 = 1
static let Bonus : UInt32 = 2
static let Vaisseau : UInt32 = 5
}
This in my func DidMoveToView :
Vaisseau = SKSpriteNode(texture: Vaisseau1)
Vaisseau.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
Vaisseau.physicsBody = SKPhysicsBody(rectangleOfSize: Vaisseau.size)
Vaisseau.physicsBody?.affectedByGravity = false
Vaisseau.physicsBody?.categoryBitMask = PhysicsCategories.Vaisseau
Vaisseau.physicsBody?.contactTestBitMask = PhysicsCategories.Bonus
Vaisseau.physicsBody?.contactTestBitMask = PhysicsCategories.Meteorites
Vaisseau.physicsBody?.dynamic = false
Vaisseau.setScale(0.08)
self.addChild(Vaisseau)
And i create my SpriteNode Bonus in this function :
func ApparitionBonus() {
let BonusSprite = SKSpriteNode(imageNamed: "Bonus.png")
var BonusApparitionX = UInt32(self.frame.size.width)
var BonusApparitionY = UInt32(self.frame.size.height)
BonusApparitionX = arc4random() % BonusApparitionX
BonusApparitionY = arc4random() % BonusApparitionY
BonusSprite.position = CGPointMake(CGFloat(BonusApparitionX),CGFloat(BonusApparitionY))
BonusSprite.setScale(0.8)
BonusSprite.physicsBody = SKPhysicsBody(circleOfRadius: 20)
BonusSprite.physicsBody?.affectedByGravity = false
BonusSprite.physicsBody?.categoryBitMask = PhysicsCategories.Bonus
BonusSprite.physicsBody?.contactTestBitMask = PhysicsCategories.Vaisseau
BonusSprite.physicsBody?.dynamic = false
self.addChild(BonusSprite)
let RotationBonus = SKAction.rotateByAngle(CGFloat(M_PI), duration: 3)
let wait = SKAction.waitForDuration(3)
let actionFini = SKAction.removeFromParent()
BonusSprite.runAction(SKAction.sequence([RotationBonus, wait, actionFini]))
BonusSprite.runAction(SKAction.repeatActionForever(RotationBonus))
}
In your didMoveToView, when you call:
Vaisseau.physicsBody?.contactTestBitMask = PhysicsCategories.Bonus
Vaisseau.physicsBody?.contactTestBitMask = PhysicsCategories.Meteorites
You set the contactTestBitMask to one category, and then another. You need to bitwise or them together. Something like:
Vaisseau.physicsBody?.contactTestBitMask = PhysicsCategories.Bonus | PhysicsCategories.Meteorites
This will combine the two bit masks, which will allow it to contact both the Bonus and Meteorites.
If you don't want your sprites to collide, set the collisionBitMask to 0x0 (or however you want to represent 0):
Vaisseau.physicsBody?.collisionBitMask = 0x0