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"){...}
Related
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.
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.
Here's my code:
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let origin = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
let delay = SKAction.waitForDuration(2.0)
for (var i = 0; i < 6; i++) {
//generate a random point on a circle centered at origin with radius of 500
let point = randomPointOnCircle(500, center: origin)
let chanceToSpawn = arc4random_uniform(100)
switch chanceToSpawn {
case 0...60:
self.runAction(delay){
let smallBall = SKSpriteNode(imageNamed: "smallBall”)
self.addChild(smallBall)
smallBall.xScale = 0.05
smallBall.yScale = 0.05
smallBall.position = point
let finalDest = CGPointMake(origin.x, origin.y)
let moveAction = SKAction.moveTo(finalDest, duration: 5.0)
smallBall.runAction(SKAction.sequence([SKAction.waitForDuration(0.1), moveAction, SKAction.runBlock({
smallBall.removeFromParent()
})]))
delay
}
break
case 61...80:
//same as case 0…60, smallBall changed to “mediumBall”
break
case 81...99:
//same as case 0…60, smallBall changed to “largeBall”
break
default:
break
} // ends switch statement
}//ends for loop
}//end didMoveToView
Basically I have balls that spawn on a random point on a circle and move towards the center. But the problem I am having is that no matter how I rearrange things (or rewrite things, i.e., trying different loops or defining functions for spawning the ball and the movement of the ball and simply calling them from inside the switch statement) all of the balls spawn at the same time. I am trying to make it so they spawn one after the other (meaning a ball spawns, the thread waits a few seconds, then spawns another ball.), and I can't seem to figure out why each ball is spawning at the same time as all the other balls.
I'm fairly new to Swift/Spritekit programming and I feel like I'm overlooking some small detail and this is really bugging me. Any help appreciated!
For loops are executed faster than you think! Your for loop is probably executed instantly.
"Why is that? I have already called self.runAction(delay) to delay the for loop!" you asked. Well, the delay action doesn't actually delay the for loop, unfortunately. It just delays the other actions you run on self.
To fix this, try delaying different amounts, dependind on the for loop counter:
self.runAction(SKAction.waitForDuration(i * 2 + 2)) { ... }
This way, balls will appear every two seconds.
try this, may be due to same time u are adding the actions for each balls,
smallBall.runAction(SKAction.sequence([SKAction.waitForDuration(0.1 + (i * 0.35/2)), moveAction, SKAction.runBlock({
smallBall.removeFromParent()
})])) // delay added (0.1 + (increment the daly))
try this
Your problem is you are calling runAction on each iteration of your for loop. This means that all your actions will run concurrently.
What you want to do is create an array of actions. then after the for loop, run a sequence of actions.
(Note: I took the liberty of cleaning up your code)
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let origin = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
let delay = SKAction.waitForDuration(2.0)
var sequence = [SKAction]()
for (var i = 0; i < 6; i++) {
//generate a random point on a circle centered at origin with radius of 500
let point = randomPointOnCircle(500, center: origin)
let chanceToSpawn = arc4random_uniform(100)
var ballname = "smallBall”
switch chanceToSpawn {
case 61...80:
//same as case 0…60, smallBall changed to “mediumBall”
ballname = "mediumBall”
case 81...99:
//same as case 0…60, smallBall changed to “largeBall”
ballname = "largeBall”
default: ()
} // ends switch statement
sequence.append(delay)
sequence.append(
SKAction.run()
{
[unowned self] in
let ball = SKSpriteNode(imageNamed: ballName)
self.addChild(ball)
ball.xScale = 0.05
ball.yScale = 0.05
ball.position = point
let finalDest = CGPointMake(origin.x, origin.y)
let moveAction = SKAction.moveTo(finalDest, duration: 5.0)
let briefWait = SKAction.waitForDuration(0.1)
let remove = SKAction.removeFromParent()
let moveSeq = SKAction.sequence([briefWait ,moveAction,remove])
ball.runAction(moveSeq)
}
)
}//ends for loop
self.run(SKAction.sequence(sequence))
}//end didMoveToView
In my game I want an enemy to spawn every 10 seconds. I attempt to accomplish this by, in the GameViewController, writing
var secondEnemyTimer = NSTimer.scheduledTimerWithTimeInterval(10.0, target: self, selector: "secondEnemyFunction", userInfo: nil, repeats: false)
in the viewWillLayoutSubviews method. Then in the secondEnemyFunction I write:
let skView = self.view as! SKView
let gameScene = GameScene(size: skView.bounds.size)
gameScene.enemy2Function()
Then in the enemy2Function in the GameScene class I write:
println("Called!")
enemy2.name = enemyCategoryName
enemy2.size.width = 57
enemy2.size.height = 57
let randomX = randomInRange(Int(CGRectGetMinX(self.frame)), hi: Int(CGRectGetMaxX(self.frame)))
let randomY = randomInRange(Int(CGRectGetMinY(self.frame)), hi: Int(CGRectGetMaxY(self.frame)))
let randomPoint = CGPoint(x: randomX, y: randomY)
enemy2.position = randomPoint
self.addChild(enemy2)
enemy2.physicsBody = SKPhysicsBody(circleOfRadius: enemy1.size.width / 2)
enemy2.physicsBody?.friction = 0
enemy2.physicsBody?.restitution = 1
enemy2.physicsBody?.linearDamping = 0
enemy2.physicsBody?.allowsRotation = false
enemy2.physicsBody?.applyImpulse(CGVectorMake(50, -50))
enemy2.physicsBody?.categoryBitMask = enemyCategory
In the log "Called!" appear yet the enemy is not spawned. Just so you know I did create the enemy at the top of the class by doing:
let enemy2 = SKSpriteNode(imageNamed: "enemy")
Does anyone know how I can spawn my second enemy? Thank you in advance!
-Vinny
You should keep things simple and just do everything inside GameScene. Another thing is to drop NSTimer and use SKAction to spawn enemies. NSTimer don't respect scene's paused state, so you can get into some trouble eventually. This is how you can spawn enemies using SKAction:
GameScene.swift:
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
generateEnemies()
}
func stopGeneratingCoins(){
if(self.actionForKey("spawning") != nil){removeActionForKey("spawning")}
}
func generateEnemies(){
if(self.actionForKey("spawning") != nil){return}
let timer = SKAction.waitForDuration(10)
//let timer = SKAction.waitForDuration(10, withRange: 3)//you can use withRange to randomize duration
let spawnNode = SKAction.runBlock {
var enemy = SKSpriteNode(color: SKColor.greenColor(), size:CGSize(width: 40, height:40))
enemy.name = "enemy" // name it, so you can access all enemies at once.
//spawn enemies inside view's bounds
let spawnLocation = CGPoint(x:Int(arc4random() % UInt32(self.frame.size.width - enemy.size.width/2) ),
y:Int(arc4random() % UInt32(self.frame.size.height - enemy.size.width/2)))
enemy.position = spawnLocation
self.addChild(enemy)
println(spawnLocation)
}
let sequence = SKAction.sequence([timer, spawnNode])
self.runAction(SKAction.repeatActionForever(sequence) , withKey: "spawning") // run action with key so you can remove it later
}
}
When it comes to positioning, I assumed that your scene already has the correct size. If scene is not correctly initialized and has different size (or more precisely, different aspect ratio) than a view, it could happen that enemy get off-screen position when spawned. Read more here on how to initialize the scene size properly.
I have tried to find a way to detect if a SKSpriteNode left the screen (I would like to call a Game Over function).
I have declared the node within a function (It is not global if I get that right?) and made and SKAction that moves the Node out of the screen and removes it afterwards.
This is what I came up with:
var node = SKSpriteNode()
let nodeTexture = SKTexture(imageNamed: "node")
nodeTexture.filteringMode = .Nearest
node = SKSpriteNode(texture: nodeTexture)
let nodeFalling = SKAction.moveToY(-70, duration: 1.6)
let nodeRemoving = SKAction.removeFromParent()
node.runAction(SKAction.sequence([nodeFalling, nodeRemoving]))
self.addChild(node)
Now what I need is to call GameOver() if the node left the screen.
I am very thankful for every answer!
In your scene you have to remember the reference for node you want to check and then in update method you just do the following:
if node.position.y < -node.size.height/2.0 {
node.removeFromParent()
gameOver()
}
Edit:
class MyScene: SKScene {
// ....
//here is the list of nodes which you want to check
var nodesToCheck = [SKSpriteNode]()
//here is your spawn function
func spawnNewNode() {
var node = SKSpriteNode()
let nodeTexture = SKTexture(imageNamed: "node")
nodeTexture.filteringMode = .Nearest
node = SKSpriteNode(texture: nodeTexture)
let nodeFalling = SKAction.moveToY(-70, duration: 1.6)
let nodeRemoving = SKAction.removeFromParent()
node.runAction(SKAction.sequence([nodeFalling, nodeRemoving]))
self.addChild(node)
nodesToCheck.append(node)
}
//and here is the update method
override func update(currentTime: NSTimeInterval) {
super.update(currentTime)
// ... every other update logic
for node in nodesToCheck {
if node.position.y < -node.size.height/2.0 {
node.removeFromParent()
gameOver()
}
}
}
func gameOver() {
println("Damn!")
}
}
Dont forget to remove your node from nodeToCheck array when they are no longer scene members.