Can’t run SKAction.runBlock in a Subclass - ios

I am new to Swift but I am having trouble with classes and inheritance. I have a class called Bunny which holds some code including an SKAction. When I place the code found in Bunny into my GameScene class, it works fine, however, when I try running it, while it is in the Bunny class, from my GameScene class it does not work.
Here is the Bunny class:
import SpriteKit
class Bunny : SKSpriteNode {
var bunny = SKSpriteNode()
var moveAndRemove = SKAction()
var textureAtlas = SKTextureAtlas()
var textureArray = [SKTexture]()
func spawnBunnies() {
let spawn = SKAction.runBlock({
() in
self.spawnBunny()
print("CALLEDBunniesSpawned") // THIS PART IS NOT GETTING CALLED
})
let delay = SKAction.waitForDuration(3)
let spawnDelay = SKAction.sequence([spawn, delay])
let spawnDelayForever = SKAction.repeatActionForever(spawnDelay)
self.runAction(spawnDelayForever)
let distance = CGFloat(self.frame.width + bunny.frame.width)
let movePipes = SKAction.moveByX(-distance - 200, y: 0, duration: NSTimeInterval(0.009 * distance)) // Speed up pipes
let removePipes = SKAction.removeFromParent()
moveAndRemove = SKAction.sequence([movePipes, removePipes])
print("BunniesSpawned") // THIS PART HERE RUNS
}
func spawnBunny() { // I NEED TO TRIGGER THIS PART VIA THE SPAWN = SKAction.runBlock
print("BunniesSpawned2")
let randomYGen = CGFloat(arc4random_uniform(UInt32(self.frame.height - 80)))
textureAtlas = SKTextureAtlas(named: "Bunnies")
for i in 1...textureAtlas.textureNames.count {
var name = "Bunny\(i).png"
textureArray.append(SKTexture(imageNamed: name))
}
bunny = SKSpriteNode(imageNamed: textureAtlas.textureNames[5] as! String)
bunny.position = CGPoint(x: self.frame.width + bunny.frame.width / 2, y: randomYGen)
bunny.setScale(0.5)
bunny.runAction(moveAndRemove)
self.addChild(bunny)
bunny.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(textureArray, timePerFrame: 0.1)))
}
}
Here is my GameScene class where I am trying to call the function from Bunny:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
if gameStarted == false {
gameStarted = true
var bun = Bunny()
bun.spawnBunnies()
} else {
}

As crashoverride777 said, the original Bunny you created in your touchesBegan is never added to the scene. If it is not part of a scene, it will never run any of the actions you are setting up.
var bun = Bunny()
self.addChild(bun)
bun.spawnBunnies()
Additionally, in spawnBunnies you are adding the newly spawned Bunny as a child of the Bunny that is spawning it. This bunny gets removed from the scene so the newly spawned bunny (which is it's child) also get's removed.
self.addChild(bunny) should be parent?.addChild(bunny). It might also need to be moved above the line bunny.runAction(moveAndRemove).

Unless I am missing something you are not adding the Bunny subclass to the scene.
Try this
var bun = Bunny()
addChild(bun)
bun.spawnBunnies()

Related

Adding nodes to GameScene from another class

I have here a struct that's used to pass a function over to GameScene in which I can call whenever needed.
The problem is the line GameScene.addChild(explosionFirst) generates the following error:
'SKSpriteNode' is not convertible to 'GameScene'
How am I able to add the node from another class to GameScene?
import Foundation
import SpriteKit
struct Explosion {
var explosions = [SKTexture]()
mutating func spawnExplosion(spawnPosition: CGPoint) {
for i in 0...79 {
let explosion = SKTexture(imageNamed: String(i))
explosions.append(explosion)
}
let explosionFirst = SKSpriteNode(texture: explosions[0])
explosionFirst.position = spawnPosition
explosionFirst.zPosition = 1
explosionFirst.setScale(5)
GameScene.addChild(explosionFirst)
let animation = SKAction.animate(with: explosions, timePerFrame: 0.1)
let scaleIn = SKAction.scale(to: 10, duration: 2.3)
let fadeOut = SKAction.fadeOut(withDuration: 1.0)
let delete = SKAction.removeFromParent()
let explosionSequence = SKAction.sequence([scaleIn, fadeOut, delete])
explosionFirst.run(animation)
explosionFirst.run(explosionSequence)
}
}
the compiler is seeing self.GameScene.addChild and yo don't have a reference to your GameScene object so create one
var gameScene : GameScene!
now use:
gameScene.addChild(explosionFirst)
Note: by default your initialiser will now have gameScene: GameScene so you will have to inject the Gamescene dependancy on init

SpriteKit reference nodes from level editor

I'm using the scene editor in SpriteKit to place color sprites and assign them textures using the Attributes Inspector. My problem is trying to figure out how to reference those sprites from my GameScene file. For example, I'd like to know when a sprite is a certain distance from my main character.
Edit - code added
I'm adding the code because for some reason, appzYourLife's answer worked great in a simple test project, but not in my code. I was able to use Ron Myschuk's answer which I also included in the code below for reference. (Though, as I look at it now I think the array of tuples was overkill on my part.) As you can see, I have a Satellite class with some simple animations. There's a LevelManager class that replaces the nodes from the scene editor with the correct objects. And finally, everything gets added to the world node in GameScene.swift.
Satellite Class
func spawn(parentNode:SKNode, position: CGPoint, size: CGSize = CGSize(width: 50, height: 50)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.name = "satellite"
self.runAction(satAnimation)
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
self.physicsBody?.affectedByGravity = false
self.physicsBody?.categoryBitMask = PhysicsCategory.satellite.rawValue
self.physicsBody?.contactTestBitMask = PhysicsCategory.laser.rawValue
self.physicsBody?.collisionBitMask = 0
}
func createAnimations() {
let flyFrames:[SKTexture] = [textureAtlas.textureNamed("sat1.png"),
textureAtlas.textureNamed("sat2.png")]
let flyAction = SKAction.animateWithTextures(flyFrames, timePerFrame: 0.14)
satAnimation = SKAction.repeatActionForever(flyAction)
let warningFrames:[SKTexture] = [textureAtlas.textureNamed("sat8.png"),
textureAtlas.textureNamed("sat1.png")]
let warningAction = SKAction.animateWithTextures(warningFrames, timePerFrame: 0.14)
warningAnimation = SKAction.repeatActionForever(warningAction)
}
func warning() {
self.runAction(warningAnimation)
}
Level Manager Class
import SpriteKit
class LevelManager
{
let levelNames:[String] = ["Level1"]
var levels:[SKNode] = []
init()
{
for levelFileName in levelNames {
let level = SKNode()
if let levelScene = SKScene(fileNamed: levelFileName) {
for node in levelScene.children {
switch node.name! {
case "satellite":
let satellite = Satellite()
satellite.spawn(level, position: node.position)
default: print("Name error: \(node.name)")
}
}
}
levels.append(level)
}
}
func addLevelsToWorld(world: SKNode)
{
for index in 0...levels.count - 1 {
levels[index].position = CGPoint(x: -2000, y: index * 1000)
world.addChild(levels[index])
}
}
}
GameScene.swift - didMoveToView
world = SKNode()
world.name = "world"
addChild(world)
physicsWorld.contactDelegate = self
levelManager.addLevelsToWorld(self.world)
levelManager.levels[0].position = CGPoint(x:0, y: 0)
//This does not find the satellite nodes
let satellites = children.flatMap { $0 as? Satellite }
//This does work
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "satellite") {
self.satTuple.0 = node.position
self.satTuple.1 = (node as? SKSpriteNode)!
self.currentSatellite.append(self.satTuple)
}
}
The Obstacle class
First of all you should create an Obstacle class like this.
class Obstacle: SKSpriteNode { }
Now into the scene editor associate the Obstacle class to your obstacles images
The Player class
Do the same for Player, create a class
class Player: SKSpriteNode { }
and associate it to your player sprite.
Checking for collisions
Now into GameScene.swift change the updated method like this
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
let obstacleNearSprite = obstacles.contains { (obstacle) -> Bool in
let distance = hypotf(Float(player.position.x) - Float(obstacle.position.x), Float(player.position.y) - Float(obstacle.position.y))
return distance < 100
}
if obstacleNearSprite {
print("Oh boy!")
}
}
What does it do?
The first line retrieves all your obstacles into the scene.
the second line retrieves the player (and does crash if it's not present).
Next it put into the obstacleNearSprite constant the true value if there is at least one Obstacle at no more then 100 points from Player.
And finally use the obstacleNearSprite to print something.
Optimizations
The updated method gets called 60 times per second. We put these 2 lines into it
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
in order to retrieves the sprites we need. With the modern hardware it is not a problem but you should save references to Obstacle and Player instead then searching for them in every frame.
Build a nice game ;)
you will have to loop through the children of the scene and assign them to local objects to use in your code
assuming your objects in your SKS file were named Obstacle1, Obstacle2, Obstacle3
Once in local objects you can check and do whatever you want with them
let obstacle1 = SKSpriteNode()
let obstacle2 = SKSpriteNode()
let obstacle3 = SKSpriteNode()
let obstacle3Location = CGPointZero
func setUpScene() {
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "Obstacle1") {
self.obstacle1 = node
}
else if (node.name == "Obstacle2") {
self.obstacle2 = node
}
else if (node.name == "Obstacle3") {
self.obstacle3Location = node.position
}
}
}

How do I make my enemySprite respawn as new bad guy once its been killed?

I am having trouble with my game, I've managed to have my enemySprites all shoot at the hero in a synchronized matter and I've gotten them to play the "killed" animation once they've been hit. Although I've run into a rather small matter which I was really hoping you guys could help me with. The problem I have is that when my badGuy is killed they move off the screen and I don't know how I can program it so that a 'new' badGuy appears forever until the Hero sprite is killed.
This is my function to spawn my enemy:
func spawnEnemy(targetSprite: SKNode) -> SKSpriteNode {
if !gamePaused{
// create a new enemy sprite
let main = GameScene()
newEnemy = SKSpriteNode(imageNamed:"BNG1_1.png")
enemySprite.append(newEnemy)//ADD TO THE LIBRARY OF BADGUYS
newEnemy.xScale = 1.2
newEnemy.yScale = 0.6
newEnemy.physicsBody?.dynamic = true
newEnemy.physicsBody = SKPhysicsBody(texture: newEnemy.texture, size: newEnemy.size)
newEnemy.physicsBody?.affectedByGravity = false
newEnemy.physicsBody?.categoryBitMask = BodyType.badguyCollision.rawValue
newEnemy.physicsBody?.contactTestBitMask = BodyType.beamCollison.rawValue
newEnemy.physicsBody?.collisionBitMask = 0x0
let muv : UInt32 = (200 + (arc4random()%500))
let actualDuration = NSTimeInterval(random(min: CGFloat(3.0), max: CGFloat(4.0)))
let randomNum = CGPoint(x:Int (muv), y:Int (arc4random()%500))
// Create the actions
var actionMove = SKAction.moveTo(randomNum, duration: NSTimeInterval(actualDuration))
newEnemy.runAction(SKAction.sequence([actionMove]))
// position new sprite at a random position on the screen
var posX = arc4random_uniform(UInt32(sizeRect.size.width))
var posY = arc4random_uniform(UInt32(sizeRect.size.height))
newEnemy.position = CGPoint(x: screenSize.width*2 + newEnemy.size.width, y: random(min: newEnemy.size.height, max: screenSize.height - newEnemy.size.height))
let atlas = SKTextureAtlas(named: "BG1.atlas")
let anime = SKAction.animateWithTextures([atlas.textureNamed("BG1_1.png"), atlas.textureNamed("BG1_2.png"),
atlas.textureNamed("BG1_3.png"),
atlas.textureNamed("BG1_2.png"),
atlas.textureNamed("BG1_1.png")], timePerFrame: 0.1)
dinoRun = SKAction.repeatActionForever(anime)
newEnemy.runAction(dinoRun)
}
return newEnemy
}
And this is my function for once they are hit (this function is called when the collisionTest between my hero's laser and the badguy is recognized):
func deadBadGuy(){
//animation
var dinoRun:SKAction
var newdes = CGPoint(x: Int(arc4random()%500), y:0)
var actionMoov = SKAction.moveTo(newdes, duration: 3)
var goaway = SKAction.removeFromParent()
let aTlas = SKTextureAtlas(named: "dedBG.atlas")
let anime = SKAction.animateWithTextures([aTlas.textureNamed("deadBG1.png"), aTlas.textureNamed("deadBG2.png"),
aTlas.textureNamed("deadBG3.png"),
aTlas.textureNamed("deadBG2.png"),
aTlas.textureNamed("deadBG1.png")], timePerFrame: 0.1)
dinoRun = SKAction.repeatActionForever(anime)
newEnemy.runAction(dinoRun)
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]))
score++
self.scoreNode.text = String(score)
enemySprites.newPlace(neewEnemy)
dead = true //i created this Boolean because the sprite keeps shooting even if its dead
}
This is the way I called for the collisionTest in my program in case you need it for more information:
func didBeginContact(contact:SKPhysicsContact){
if !gamePaused {
let firstNode = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch(firstNode){
case BodyType.beamCollison.rawValue | BodyType.badguyCollision.rawValue:
deadBadGuy()
//enemySprites.spawnEnemy(sprite)
default:
print("hit")
}
Note: I tried having the func spawnEnemy being called during the collisionTest but that results in the BadGuy staying off screen shooting at my hero.
Update : I found out how to add a new enemy sprite once the other is dead all i had to do was
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]), completion: {
self.addChild(self.enemySprites.spawnEnemy(sprite))
})
in the deadBadGuy function(the spawnEnemy function is in another class which I named enemySprites as a variable). However now I've run into a new issue and that is that it adds 10 enemySprites instead of one. How can I change that?
Update 2 : I figured out that issue too, I just needed to remove to dead Boolean method in the deadBadGuy function.
I found my answer and all I had to do was add this line to my deadBadGuy function and remove the dead boolean methods
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]), completion: {
self.addChild(self.enemySprites.spawnEnemy(sprite))
})

PhysicsBody is nil using SpriteKit with Swift

I have a SpriteKit game I am building and I am loading a level from a multidimensional array. The loadlevel function works the first time. It does fail if I do a println of the physicsBody above the physicsBody assignment (after the initialization of the physicBody). When I remove all the tiles with removeChildrenInArray the secondtime I run load level it throws an error saying fatal error: unexpectedly found nil while unwrapping an Optional and it points to line right below the println below. And the println indicates that it is the physicsBody that is nil. In my mind there is no reason a freshly initialize PhysicsBody should ever be nil. The println prints physicsBody nil. I have no idea why the physicsBody would be nil. I am just trying to reset the level by removing all block nodes and adding new ones in the original place according to the level map.
func loadLevel() {
var levels = Levels().data
var frameSize = view.frame.size
var thisLevel = levels[currentLevel]
println("running load level")
for (rowIndex,row) in enumerate(thisLevel) {
for (colIndex,col) in enumerate(row) {
if col == 4 {
continue
}
println("COL: \(col)")
var tile = SKSpriteNode(texture: SKTexture(imageNamed: "brick_\(tileMap[col])"))
tile.name = "tile_\(rowIndex)_\(colIndex)"
tile.position.y = frameSize.height - (tile.size.height * CGFloat(rowIndex)) - (tile.size.height/2)
tile.position.x = tile.size.width * CGFloat(colIndex) + (tile.size.width/2)
var physicsBody = SKPhysicsBody(rectangleOfSize: tile.size)
tile.physicsBody = physicsBody
tile.physicsBody.affectedByGravity = false
tile.physicsBody.categoryBitMask = ColliderType.Block.toRaw()
tile.physicsBody.contactTestBitMask = ColliderType.Ball.toRaw()
tile.physicsBody.collisionBitMask = ColliderType.Ball.toRaw()
scene.addChild(tile)
tileCount++
}
}
}
Here is my ColliderType
enum ColliderType:UInt32 {
case Paddle = 1
case Block = 2
case Wall = 3
case Ball = 4
}
This is my reset function contents:
func reset() {
tileCount = 0
var removeTiles = [SKSpriteNode]()
// remove all the tiles
for child in scene.children {
var a_tile = child as SKSpriteNode
if a_tile.name.hasPrefix("tile_") {
a_tile.removeFromParent()
a_tile.name = ""
removeTiles.append(a_tile)
}
}
removeTiles.removeAll(keepCapacity: false)
ball!.position = CGPoint(x: 200, y: 200)
ballVel = CGPoint(x: 0, y: -5)
currentLevel++
loadLevel()
lost = false
won = false
}
Here is my Level structs
struct Tile {
let map = ["blue","green","purple","red"]
}
struct Levels {
let data = [
[
[4,4,0,4,0,4,0,4,0,4,0,4,0,0,4,4],
[4,4,1,4,1,4,1,4,1,4,1,4,1,1,4,4],
[4,4,2,4,2,4,2,4,2,4,2,4,2,2,4,4],
[4,4,3,4,3,4,3,4,3,4,3,4,3,3,4,4]
],
[
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[2,2,2,2,2,2,2,4,2,2,2,2,2,2,2,2],
[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]
]
]
}
If this is a bug in Swift I am trying to figure out a way around so I can just make this work.
Looks like SKPhysicsBody is instantiated with an empty size. Try to create the physics body object with an explicit size, like so:
var physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(100, 100))
Alternatively, you can set the size directly on the SKSpriteNode or use one of its constructors that take a CGSize construct like initWithTexture:color:size:

Different results between iOS 7.1 and 8 while using Swift

I am practicing swift by writing a simple game. However, I noticed something weird happening. I am getting different results when I run my app on iOS 7.1 compared to iOS 8. I am trying to downcast an SKNode to a custom class that I wrote called CircleNode, which inherits from SKSpriteNode. Here is the class:
import UIKit
import SpriteKit
class CircleNode: SKSpriteNode {
var _hasMoved = false
var _touchingCircles:NSMutableArray = NSMutableArray()
var _isTouchingObject = false
var _selectedForDeletion = false
var _isBeingTouched = false
var _xOffset:CGFloat = 0
var _yOffset:CGFloat = 0
func addTouchingCircle(touchingCircle:CircleNode) {
_touchingCircles.addObject(touchingCircle)
_isTouchingObject = true
}
func removeAllCircles(){
self._selectedForDeletion = true
for circ :CircleNode! in _touchingCircles {
if circ._selectedForDeletion == false {
circ.removeAllCircles()
}
}
self.removeFromParent()
}
}
The code that is resulting in weird results is :
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let locationInScene = touch.locationInNode(self)
var selectedNode = self.nodeAtPoint(locationInScene)
let node = selectedNode as? CircleNode
if node {
println("NODE FOUND:circle")
} else {
println("NULL")
}
}
}
In iOS 8, when I click on one of the on screen circles, it prints "NODE FOUND:circle". However, when I do the same on iOS 7.1 it prints "NULL". I have been trying to figure out why this is happening for days, but I can't seem to figure it out. It seems like in iOS 8, var selectedNode = self.nodeAtPoint(locationInScene) actually returns my CircleNode, but in iOS 7.1, it doesn't recognize the node. Any help? Thanks in advance!
There seems to be an issue with the way the XCode templates have changed between XCode 6.0 and 6.1 that causes problems when working with SpriteKit scenes in Swift.
The problem is actually with the initialization of the SKScene from your controller, not the node itself: XCode 6.1 implements the 'unarchiveFromFile' extension method which doesn't appear to work well with Swift on iOS7.1
I got round this by using the following extension:
import UIKit
import SpriteKit
extension SKNode {
class func unarchiveFromFile<T: SKScene>(file : NSString) -> T {
let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks")
var sceneData = NSData(contentsOfFile: path!, options: .DataReadingMappedIfSafe, error: nil)
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData!)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as T
archiver.finishDecoding()
return scene
}
class func safeInit<T: SKScene>(size: CGSize, _ skFile: NSString) -> T {
let currentVersion = UIDevice.currentDevice().systemVersion
let isIOS8 = currentVersion.compare("8.0", options: NSStringCompareOptions.NumericSearch) != NSComparisonResult.OrderedAscending
if isIOS8 {
let scene = T.unarchiveFromFile(skFile)
scene.size = size
return scene as T
} else {
return T(size: size)
}
}
}
And then calling this from your controller using the following code:
let skView = self.view as SKView
let scene = GameScene.safeInit(skView.bounds.size, "GameScene") as GameScene
That seems to work for me. It feels like a bit of a hack but I can't see any alternative option that actually works.

Resources