SpriteKit Refer to a Node that's Outside of a Function - swift

I'm currently working on a game in SpriteKit that uses a functions to spawn a spike and I call that function every few seconds to spawn another spike. Here is that function
func summonSpike() {
let spike = SKSpriteNode(imageNamed: "Spike")
let randomizer = Int.random(in: 1..<2)
// 1 = Left
// 2 = Right
if randomizer == 1 {
spike.position = CGPoint(x: -70, y: 300)
spike.zRotation = -1.5708
} else {
spike.position = CGPoint(x: 70, y: 300)
spike.zRotation = 1.5708
}
spike.name = "spike"
spike.size = CGSize(width: 30, height: 60)
spike.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "Spike"), size: CGSize(width: 30, height: 60))
spike.physicsBody?.affectedByGravity = false
spike.physicsBody?.allowsRotation = false
if gameStarted == true && gameEnded == false {
addChild(spike)
}
}
I need to refer to each spike node that exists outside of the function. What's the best way to do this?, I tried putting the let spike = SKSpriteNode(imageNamed: "Spike") outside of the function but then it doesn't let me spawn multiple spikes. How do I refer to a node outside of the function?

there are different ways to do this. as Whirlwind mentioned, you can save your sprites in an array. however i would recommend simply relying on the node structure itself as your storage. once you call addChild(node) you've effectively saved that node for later. here are two ways to access those nodes again, first by filtering on the node's class, and second by using the node's assigned name.
//get all child SKSpriteNodes by class
let nodes_by_class = self.children.compactMap({ $0 as? SKSpriteNode })
//get all children with a particular name
let nodes_by_name = self.children.filter({ $0.name == "spike" })
you can also use childNode(withName:) to grab a single child based on name

Related

I want to reduce the code in methods that are basically identical and pass in the parameters but don't know where to begin?

I have a spriteKit project where I have many characters across several scenes.
As a beginner I just built each one individually for each scene - which makes for a ton of extra code.
I know I could clean this up with a "Build character class" or something like that...
I am just not sure where to begin.
Here is code from two of the characters in one scene...but imagine 5-10 characters per scene?
Also is there a way a property list could be useful for storing these type of properties?
//BEAR
func buildBear() {
let bearAnimatedAtlas = SKTextureAtlas(named: "Bear")
var bearFrames: [SKTexture] = []
let numImages = bearAnimatedAtlas.textureNames.count
for i in 1...numImages {
let bearTextureName = "bear\(i)"
bearFrames.append(bearAnimatedAtlas.textureNamed(bearTextureName))
}
animatedBear = bearFrames
let firstFrameTexture = animatedBear[0]
bear = SKSpriteNode(texture: firstFrameTexture)
bear.size.height = 370
bear.size.width = 370
bear.position = CGPoint(x: 295, y: 25)
bear.zPosition = 1
bear.name = "bear"
isUserInteractionEnabled = true
addChild(bear)
}
//CAT
func buildCat() {
let catAnimatedAtlas = SKTextureAtlas(named: "Cat")
var catFrames: [SKTexture] = []
let numImages = catAnimatedAtlas.textureNames.count
for i in 1...numImages {
let catTextureName = "cat\(i)"
catFrames.append(catAnimatedAtlas.textureNamed(catTextureName))
}
animatedCat = catFrames
let firstFrameTexture = animatedCat[0]
cat = SKSpriteNode(texture: firstFrameTexture)
cat.size.height = 240
cat.size.width = 240
cat.position = CGPoint(x: 134, y: -38)
cat.zPosition = 2
cat.name = "cat"
isUserInteractionEnabled = true
addChild(cat)
}
How could I clean up something like this - I need different position/size per scene but I imagine I could just override that per scene?
I know I know how to do this! - just not where to start?
Gimme a nudge please!
One of the confusing things about the existence of so many languages is that they each have their own jargon, and their own conventions. The root of your problem, however, has nothing to do with Swift or Sprite Kit. When I read your question, I see code that could use some Abstract Data Types. In Java, you would create an Interface, in C++ you would create a "pure virtual" class. Well a rose by any other name still gets the job done. I recommend creating a Protocol, perhaps called Spritable, to define the types of objects that you intend to build into sprites. It would probably be as simple as this:
protocol Spritable {
var species: String { get }
var height: Int { get }
var width: Int { get }
}
The only other thing that differs between your two functions appears to be the starting position. Since this is not inherent in the meaning of a Spritable object, I would package that data separately. A tuple should do the job. With these revisions, your two functions can be merged into one:
func buildSprite(of creature: Spritable, at position: (x: Int, y: Int, z: Int)) {
let spriteAnimatedAtlas = SKTextureAtlas(named: creature.species)
var spriteFrames: [SKTexture] = []
let numImages = spriteAnimatedAtlas.textureNames.count
for i in 1...numImages {
let spriteTextureName = "\(creature.species.lowercased())\(i)"
spriteFrames.append(spriteAnimatedAtlas.textureNamed(spriteTextureName))
}
animatedSprite = spriteFrames
let firstFrameTexture = animatedSprite[0]
sprite = SKSpriteNode(texture: firstFrameTexture)
sprite.size.height = creature.height
sprite.size.width = creature.width
sprite.position = CGPoint(x: position.x, y: position.y)
sprite.zPosition = position.z
sprite.name = creature.species
isUserInteractionEnabled = true
addChild(sprite)
}
To build a bear, aside from a workshop, you will need to define a struct that implements Spritable:
struct Bear: Spritable {
var species: String { return "Bear" }
var height: Int
var width: Int
init(height: Int, width: Int) {
self.height = height
self.width = width
}
}
Then here would be your function call:
buildSprite(of: Bear(height: 370, width: 370), at: (295, 25, 1))
This is a pretty simple example, and could be solved in a few simpler ways than this. However, I find that the larger a project gets, the greater the benefits of organizing code around Abstract Data Types become, so it's worth taking that approach even in a simple case like this.
I ended up not using a protocol on this.
I simply built a method to construct the sprites - similar to what #TallChuck suggested.
func buildCharacter(name:String, height: CGFloat, width: CGFloat, position: CGPoint, zPosition: CGFloat) {
let animatedAtlas = SKTextureAtlas(named: name)
var animationFrames: [SKTexture] = []
let numImages = animatedAtlas.textureNames.count
for i in 1...numImages {
let textureName = "\(name)\(i)"
animationFrames.append(animatedAtlas.textureNamed(textureName))
}
animatedCharacter = animationFrames
let firstFrameTexture = animatedCharacter[0]
builtCharacter = SKSpriteNode(texture: firstFrameTexture)
builtCharacter.size.height = height
builtCharacter.size.width = width
builtCharacter.position = position
builtCharacter.zPosition = zPosition
builtCharacter.name = name
isUserInteractionEnabled = true
addChild(builtCharacter)
}
It works perfect for building and adding to the scenes - had some issues accessing the nodes names for touch detection but got it sorted. Now trying to figure out how to call actions on the nodes - its all different than the normal way. So I will prolly ask that question next. But overall reduced a ton of repeated code! Thanks for the help.
None of this has to be done in code. With Sprite Kit, you create your bear and your cat via the sprite kit editor, and then you load in the sks file by using the constructor that loads by filename.
This is a similar behavior to how game objects work in unity.

Spawn SKSpriteNode objects at random times

I am making a game where I would like to spawn obstacles at random times. The obstacles are going to make contact with the player.
Here the code that I have:
var obstacle : SKSpriteNode?
func createObstacles() {
let obstaclesArray = ["obstacle_1", "obstacle_2", "obstacle_3", "obstacle_4", "obstacle_5"]
let randomObs = obstaclesArray.randomElement()
let selectedTexture = SKTexture(imageNamed: randomObs!)
obstacle = SKSpriteNode(imageNamed: randomObs!)
obstacle?.physicsBody = SKPhysicsBody(texture: selectedTexture, size: selectedTexture.size())
obstacle?.position = CGPoint(x: scene!.size.width/2 + selectedTexture.size().width, y: -120)
obstacle?.zPosition = -15
if let obs = obstacle?.physicsBody {
obs.affectedByGravity = true
obs.allowsRotation = false
obs.isDynamic = true
obs.restitution = 0
obs.categoryBitMask = obstacleCategory
obs.collisionBitMask = floorCategory
obs.contactTestBitMask = heroCategory
}
addChild(obstacle!)
}
func spawnObstacles() {
let wait = SKAction.wait(forDuration: 1, withRange: 0.4)
let spawn = SKAction.run {
createObstacles()
}
let moveLeft = SKAction.moveBy(x: -scene!.size.width - obstacle.size.width - 10, y: 0, duration: 2)
let sequence = SKAction.sequence([wait, spawn, moveLeft, SKAction.removeFromParent()])
self.run(SKAction.repeatForever(sequence))
}
I read some a similar questions but the response is my same code but it is not working.
Spawning a Spritekit node at a random time
I also tried other way:
var randDelay = Double.random(in: 0.7 ..< 1.4)
DispatchQueue.main.asyncAfter(deadline: randDelay, execute: {
if self.canCreateObstacle == true {
self.spawnObstacle()
}})
But it is not working and everytime I restart the game it seems like the function is being called two times, if I restart the game a third time it is called 3 times and so on.
Anyone with a good and clean solution to spawn objects at random times?
Do not use DispatchQueue Set up a repeating sequential action using wait(forDuration:withRange:) like you previously had.
https://developer.apple.com/documentation/spritekit/skaction/1417760-wait
First, create a generic node used to spawn obstacles, then attach this generic node to the scene.
Finally assign the repeating sequential action to this node.
Boom, you are done.
The reason why you want to assign it to a random node is because you want to be able to give your game the opportunity to stop generating obstacles, plus you alter the speed property to make the node generate nodes faster or slower.
You also want to detach the spawning/waiting from the moving/destroying, because as of right now, your code is confused. You are saying move the scene left for 2 seconds, then wait a random amount of time to spawn the next enemy, but I think you are trying to just spawn enemies on a time interval and move the enemy to the left.
Your scene code should look something like this
class GameScene : SKScene{
let obstacleGenerator = SKNode()
func didMove(to: view){
let wait = SKAction.wait(forDuration: 1, withRange: 0.4)
let spawn = SKAction.run({createObstacles()})
let sequence = SKAction.sequence([wait, spawn])
obstacleGenerator.run(SKAction.repeatForever(sequence))
addChild(obstacleGenerator)
}
func createObstacles() {
let obstaclesArray = ["obstacle_1", "obstacle_2", "obstacle_3", "obstacle_4", "obstacle_5"]
let randomObs = obstaclesArray.randomElement()
let selectedTexture = SKTexture(imageNamed: randomObs!)
obstacle = SKSpriteNode(imageNamed: randomObs!)
obstacle.position = CGPoint(x: scene!.size.width/2 + selectedTexture.size().width, y: -120)
obstacle.zPosition = -15
let body = SKPhysicsBody(texture: selectedTexture, size: selectedTexture.size())
body.affectedByGravity = true
body.allowsRotation = false
body.isDynamic = true
body.restitution = 0
body.categoryBitMask = obstacleCategory
body.collisionBitMask = floorCategory
body.contactTestBitMask = heroCategory
obstacle.physicsBody = body
addChild(obstacle!)
let moveLeft = SKAction.moveBy(x: -scene!.size.width - obstacle.size.width - 10, y: 0, duration: 2)
let seq = SKAction.sequence([moveLeft,SKAction.removeFromParent()])
obstacle.run(seq)
}
}
Now as for your spawning increasing with each reset, you never post how you are resetting. I am going to assume you never remove the previous action upon reset, and this is why your rate increases. It is always better to just create a new GameScene instance when doing a reset.

Swift SpriteKit allow user to choose between player nodes

In my game, the user controls a ship which they move around.
How should I allow the user to select from a range of nodes?
For example, offering a red/blue/green ship which they can choose from or even unlock when their score = X.
Here is my code for the current player node:
let shipTexture = SKTexture(imageNamed: "ship1.png")
ship = SKSpriteNode(texture: shipTexture)
ship.position = CGPoint(x: self.frame.midX, y: -self.frame.height / 3)
ship.zPosition = 3
ship.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 30, height: 100))
ship.physicsBody!.isDynamic = false
ship.run(makeShipAnimate)
ship.physicsBody!.contactTestBitMask = ColliderType.ship.rawValue
ship.physicsBody!.categoryBitMask = ColliderType.ship.rawValue
ship.physicsBody!.collisionBitMask = ColliderType.ship.rawValue
ship.physicsBody?.affectedByGravity = false
self.addChild(ship)
Is there a way to allow the user to pick between "ship1.png" or "ship2.png" for example?
There are many ways to do this. Something like this should get you started:
import SpriteKit
class GameScene: SKScene {
var shipName = ""//create a var to hold the chosen ship
Then create some buttons or a way for the user to choose which ship and create a function to handle the selection by passing in an Int which corresponds to a particular color:
func chooseShip(ship: Int) {
switch ship {
case 0:
shipName = "blueShip.png"
break
case 1:
shipName = "greenShip.png"
break
default:
shipName = "greenShip.png"
}
}
So if the blue ship is chosen for example, that button would call the above function and pass in the selection:
chooseShip(ship: 0)
After the user has chosen you can load the image you want:
let shipTexture = SKTexture(imageNamed: shipName)
etc...

Change texture of individual nodes in didBeginContact

I've created a simple game where I have a match hover over candles (the odd description lends itself to my question) and the player scores a point when the match comes in contact with the wick. However, if it comes into contact with the anything else (like the 'wax' part of the candle), the game is over. The player controls the match by tapping on the screen.
My candle, being the wick and the coloured part, is created as follows (I have removed irrelevant parts, like the series of random textures):
func makeCandles() {
//Node properties and randomisation
let candle = SKNode()
let randomCandle = Int(arc4random_uniform(UInt32(candleTexture.count)))
let randomTexture = candleTexture[randomCandle] as SKTexture
let random = arc4random_uniform(17)
candle.position = CGPoint(x: self.frame.size.width, y: CGFloat(random * 12) - 120)
//Candle
let chosenCandle = SKSpriteNode(texture: randomTexture)
chosenCandle.position = CGPoint(x: 0, y: self.frame.size.height / 2)
chosenCandle.physicsBody = SKPhysicsBody(rectangleOfSize: chosenCandle.size)
chosenCandle.physicsBody?.dynamic = false
chosenCandle.physicsBody?.categoryBitMask = self.candleCategory
chosenCandle.physicsBody?.contactTestBitMask = self.matchCategory
chosenCandle.physicsBody?.collisionBitMask = 0
chosenCandle.physicsBody?.restitution = 0
candle.addChild(chosenCandle)
//Wick
let wickArea = SKSpriteNode(texture: wickTexture)
wickArea.name = "wickNode"
wickArea.position = CGPoint(x: 0, y: self.frame.size.height / 1.3)
wickArea.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: wickArea.size.width / 4, height: wickArea.size.height))
wickArea.physicsBody?.dynamic = false
wickArea.physicsBody?.categoryBitMask = self.wickCategory
wickArea.physicsBody?.contactTestBitMask = self.matchCategory
wickArea.physicsBody?.collisionBitMask = 0
wickArea.zPosition = 11
wickArea.physicsBody?.restitution = 0
candle.addChild(wickArea)
//Add the node and zPosition
self.partsMoving.addChild(candle)
chosenCandle.zPosition = 12
}
The candles are then created in a runBlock:
let createCandles = SKAction.runBlock({() in self.makeCandles()})
let briefPause = SKAction.waitForDuration(averageDelay, withRange: randomDelay)
let createAndPause = SKAction.sequence([createCandles, briefPause])
let createAndPauseForever = SKAction.repeatActionForever(createAndPause)
self.runAction(createAndPauseForever)
This is my function that changes the texture which is called in didBeginContact:
func updateFlame() {
if let newNode: SKNode = self.childNodeWithName("//wickNode") {
let updateTexture = SKAction.setTexture(flameTexture, resize: true)
newNode.runAction(updateTexture)
}
}
This is my didBeginContact function:
func didBeginContact(contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask == wickCategory || contact.bodyB.categoryBitMask == wickCategory {
score += 1
scoreLabel.text = "\(score)"
updateFlame()
} else {
runGameOverScene()
}
My problem is that it only changes the first node to a flame, and doesn't change any others. Even if it is the second or third wick on which contact is detected, only the first created wick is changed (the first one that comes across the screen). I know that contact is being detected on each node and that that works fine, because the score updates every time the match comes into contact with a wick.
What am I doing wrong that is stopping the texture of each node that individually comes into contact with the match from changing? Everything else is working just fine, but this part has had me beat for a week and everything I've tried doesn't work. This is the closest I've gotten.
After much trial and error, I have finally figured out how to make each node change texture when contact occurs! This is my code for that part:
func didBeginContact(contact: SKPhysicsContact) {
let collision : UInt32 = (contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask)
if collision == (matchCategory | candleCategory | cakeCategory) {
runGameOverScene()
}
if (contact.bodyA.categoryBitMask == wickCategory) {
let newWick = contact.bodyA.node
let updateTexture = SKAction.setTexture(flameTexture, resize: true)
newWick!.runAction(updateTexture)
} else if (contact.bodyB.categoryBitMask == wickCategory) {
let newWick = contact.bodyB.node
let updateTexture = SKAction.setTexture(flameTexture, resize: true)
newWick!.runAction(updateTexture)
}
}
I followed the logic of this question (even though I wanted to set the texture, not remove it) and it worked perfectly: removeFromParent() Doesn't Work in SpriteKit.

SpriteKit having trouble removing nodes from scene

I have a method that creates a object that moves across the screen, and i run this method a lot of times to produce a lot of objects, but what i can't do now is remove them when i need to. I've tried
childNodeWithName("monster")?.removeFromParent()
but that doesn't work, they still complete their action. This is the method
func spawn() {
let ran = Int(arc4random_uniform(1400));
var monster = SKSpriteNode(imageNamed: "spike")
monster = SKSpriteNode(texture: text)
monster.position = CGPoint(x: ran, y: 800);
monster.zPosition = 1;
monster.physicsBody = SKPhysicsBody(texture: text, size: text.size())
monster.physicsBody?.categoryBitMask = PhysicsCategory.Monster
monster.physicsBody?.contactTestBitMask = PhysicsCategory.Player
monster.physicsBody?.collisionBitMask = 0
monster.physicsBody?.dynamic = false
monster.name = "monster"
self.addChild(monster);
let move = SKAction.moveTo(CGPointMake(monster.position.x, -100), duration: 1.5);
let remove = SKAction.runBlock { () -> Void in
monster.removeFromParent()
self.score += 1
}
monster.runAction(SKAction.sequence([move,remove]))
}
How can i remove every "monster" node at once when i need to?
To remove every monster node at once you can use SKNode's enumerateChildNodesWithName:usingBlock: method, like this:
self.enumerateChildNodesWithName("monster") {
node, stop in
node.removeAllActions()
node.removeFromParent()
}
Here, self is a scene because you've added monsters to the scene. If you for example added monsters to some container node, then you should run this method on that node, eg. containerNode.enumerateChildNodesWithName("monster"){...}