I create an SKShapeNode in SpriteKit's update delegate and it works exactly as I want it to. My only concern is that update is called every tenth of a second. Does that mean that my code is also run that often? That doesn't seem very performance friendly. Is there a way around it? Or is it fine having the code on update?
Why is the code in update in the first place you might ask. Well if I put it in didMove or sceneDidLoad then when I rotate the device the node doesn't follow along instantly, it remains in its old place for half a second before relocating to its new position, making it look like it jumps. Bad. Update solves this. Here's the code:
override func update(_ currentTime: TimeInterval) {
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
mySquare.position = CGPoint(x: 0, y: 0)
self.addChild(mySquare)
}
}
Yes you can create. the only issue you have is it's creating a new node each time update is called. At the moment you can not see multiple nodes because you're positioning them on top of each other if you try to change the position of mySquare on each update call you will see that you have multiple nodes. created which is not effective soon you will run out of frame rates. as you don't have any animations on going you don't see the difference now.
override func update(_ currentTime: TimeInterval) {
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
mySquare.position = CGPoint(x: randomX, y: randomY)
self.addChild(mySquare)
}
changing the position of the X and Y will give you the chance to see the nodes being added to different position on the screen.
if for any reason you want to create your mySquare node in the update then you can keep a reference for the node or search the node in the parent node if it's available then don't add any
var mySquare: SKShapeNode
Then in the update check if your Square node is not nil or has the same
override func update(_ currentTime: TimeInterval) {
if mySquire == nil {
mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquire.name = "your square name"
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
mySquare.position = CGPoint(x: randomX, y: randomY)
self.addChild(mySquare)
}
}
Note: you can even check for the name like
if mySquare.name == "your square name"
then don't add it any more
Doing anything on the update func is a bad idea
Apple Documentation:
Do not call this method directly; it is called by the system exactly once per frame, so long as the scene is presented in a view and is not paused. This is the first method called when animating the scene, before any actions are evaluated and before any physics are simulated.
If you want to create objects and control them, you should use didMove func for example. And you can create an action like this for example:
self.run(
SKAction.repeatForever(
SKAction.customAction(withDuration: 0.0, actionBlock: { (_, _) in
// Create and add new node...
})
)
)
But probably create a several nodes infinitely is a bad idea.
Related
I have an SKSpriteNode that moves back and forth.
I need to get it's position as it moves (so it changes all the time) and get different positions as it moves across the screen.
When I try and receive the position it comes as the original position.
Object movement:
let right = SKAction.moveBy(x: self.frame.width, y: 0, duration: 3)
let left = SKAction.moveBy(x: -self.frame.width, y: 0, duration: 3)
And this is what I'm trying to do to get it's position printed as it goes.
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
pos = String(describing: blockposition.x)
print(pos)
}
Is it possible to get one that updates as it moves?
in your "class"
1_ type var sprite(the name of your sprite) = SKSpriteNode()
2_ then add your synod to the scene
3_ then in the update func call print(sprite(the name of your sprite).position.x or y) as you want hope it worked for you
You can't do it this way.
From https://developer.apple.com/documentation/spritekit/skaction?changes=_1
Observing Changes to Node Properties
Generally, actions do not call public methods on nodes. For example, if you wanted to subclass SKNode to respond to a move(to:duration:) action, you may consider overriding its position property to add a didSet observer (see Overriding Property Observers).
class MovingNode: SKSpriteNode {
override var position: CGPoint {
didSet {
// code to react to position change
}
}
}
However, because a move action running on an instance of MovingNode doesn't set its position, the observer isn't invoked and your code to react to a position change is never executed.
In this example, the solution is to use SKSceneDelegate. By comparing a node's position in the delegate's update(_:for:) method - which is called at the beginning of each frame - to its position in the delegate's didEvaluateActions(for:) method - which is called after any actions have been evaluated - you can check if it has moved and react accordingly.
Listing 7 shows an example of how you can implement this solution.
Listing 7
Responding to a position change during a running action
let node = SKNode()
var nodePosition = CGPoint()
func update(_ currentTime: TimeInterval, for scene: SKScene) {
nodePosition = node.position
}
func didEvaluateActions(for scene: SKScene) {
let distance = hypot(node.position.x - nodePosition.x,
node.position.y - nodePosition.y)
if distance > 0 {
// code to react to position change
}
}
I have came across a very unusual problem with - to my understand - swift 3. I am create a SpriteKit iOS game. The problem is with a SKSpriteNode. I create the node by
var gameOverBackground : SKSpriteNode!
This is located just inside the class. I then initialize the node in the didMove() function like so
override func didMove(to view: SKView) {
gameOverBackground = SKSpriteNode.init(texture: SKTexture(image: #imageLiteral(resourceName: "UIbg")), size: CGSize(width: (self.scene?.size.width)! / 1.5, height: (self.scene?.size.height)! / 1.5))
gameOverBackground.position = CGPoint(x: 0, y: 0)
gameOverBackground.zPosition = 10
gameOverBackground.name = "GameOverBackground"
}
Now the problem arises when I later try and add the node as a child to the scene. I am doing this in a contact function like so
func didBegin(_ contact: SKPhysicsContact) {
if (contact with many nodes) {
self.addChild(self.gameOverBackground)
}
}
I keep getting the error that the node is equal to nil. I have found a work around by just simply adding the child node in didMove() and making it hidden. Then just making it unhidden when contact happens; but I was more curious on why this problem is happening and if i'm doing something wrong.
Thanks in advance!
I have found a work around by just simply adding the child node in didMove() and making it hidden. Then just making it unhidden when contact happens; but I was more curious on why this problem is happening and if i'm doing something wrong.
This means that your node is notnil in didMove, but then somewhere along the way has become nil by the time of your contact delegate.
Here is an example of your code, only simplified a bit:
var node: SKSpriteNode!
override func didMove(to view: SKView) {
node = SKSpriteNode(color: .blue, size: CGSize(width: 25, height: 25))
}
// Touchesbegan on ios:
override func mouseDown(with event: NSEvent) {
addChild(node)
}
The code runs fine and when you click the screen the node appears.
Touchesbegan / didBegin won't make a difference.
The issue is that somewhere in your code the node is getting set to nil.
Here is an example of how that could happen:
// Touchesended on ios:
override func mouseUp(with event: NSEvent) {
node.removeFromParent() // or, node = nil
addChild(node) // crash!!
}
PLEASE HELP!!! I have been trying to figure this out for along time. I have searched the internet and i cannot find anything that will help me.
I am currently making a game in which you are a space ship in the middle and enemy ships are moving towards you and you have to shoot them. some enemies have different lives. for example: a red ship takes one shot to explode, the blue ship takes 3, etc. I have everything to work only the lives. for example: whenever a blue ship is called on to the screen i shoot it once so its life goes down to 2. but whenever a another blue ship is called the first blue ship has its life reset back to 3 again. Is there anyway I can make it so that whenever a ship looses lives it remains that way even if other ships are called ?
this is my ship function that gets called and adds enemy space ships onto the screen:
func VillainRight(){
let TooMuch = self.size.width
let point = UInt32(TooMuch)
life = 3
let VillainR = SKSpriteNode(imageNamed: "BlueVillain")
VillainR.zPosition = 2
VillainR.position = CGPoint(x: self.frame.minX,y: CGFloat(arc4random_uniform(point)))
//This code makes the villain's Zposition point towards the SpaceShip
let angle = atan2(SpaceShip.position.y - VillainR.position.y, SpaceShip.position.x - VillainR.position.x)
VillainR.zRotation = angle - CGFloat(M_PI_2)
let MoveToCenter = SKAction.move(to: CGPoint(x: self.frame.midX, y: self.frame.midY), duration: 15)
//Physics World
VillainR.physicsBody = SKPhysicsBody(rectangleOf: VillainR.size)
VillainR.physicsBody?.categoryBitMask = NumberingPhysics.RightV
VillainR.physicsBody?.contactTestBitMask = NumberingPhysics.Laser | NumberingPhysics.SpaceShip
VillainR.physicsBody?.affectedByGravity = false
VillainR.physicsBody?.isDynamic = true
VillainR.run(MoveToCenter)
addChild(VillainR)
}
This is the code that calls this function:
_ = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(Level1.VillainRight), userInfo: nil, repeats: true)
I am using the spritekit in Swift.
Thank You Very Much in advance!
That is happening because life variable is declared as a property of a scene and it is not local to a specific node (enemy ship). You can solve this in a few ways... First way would be using node's userData property:
import SpriteKit
let kEnergyKey = "kEnergyKey"
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
let blueShip = getShip(energy: 3)
let greenShip = getShip(energy: 2)
let redShip = getShip(energy: 1)
if let blueShipEnergy = blueShip.userData?.value(forKey: kEnergyKey) as? Int {
print("Blue ship has \(blueShipEnergy) lives left")
//hit the ship
blueShip.userData?.setValue(blueShipEnergy-1, forKey: kEnergyKey)
if let energyAfterBeingHit = blueShip.userData?.value(forKey: kEnergyKey) as? Int {
print("Blue ship has \(energyAfterBeingHit) lives left")
}
}
}
func getShip(energy:Int)->SKSpriteNode{
//determine which texture to load here based on energy value
let ship = SKSpriteNode(color: .purple, size: CGSize(width: 50, height: 50))
ship.userData = [kEnergyKey:energy]
return ship
}
}
This is what the docs say about userData property:
You use this property to store your own data in a node. For example,
you might store game-specific data about each node to use inside your
game logic. This can be a useful alternative to creating your own node
subclasses to hold game data.
As you can see, an alternative to this is subclassing of a node (SKSpriteNode):
class Enemy:SKSpriteNode {
private var energy:Int
//do initialization here
}
I've set up my game to have the same node repeatedly added from the bottom with different texture to move up to the top of the screen. I need a method to keep track of the node always leading this line of nodes. So if the original first node gets removed, the one behind it takes its place as the first node and I'm able to keep track of it. This is the code I've used to create my line of nodes. Any help would be appreciated.
EDIT: I tried making an SKNode and placing it above the ball to check its name but it just returns nil.
class GameScene: SKScene {
var aboveNode = SKNode()
override func didMoveToView(view: SKView) {
var create = SKAction.runBlock({() in self.createBottomTargets()})
var wait = SKAction.waitForDuration(2)
var createAndWait = SKAction.repeatActionForever(SKAction.sequence([create, wait]))
self.runAction(createAndWait, withKey: "firstBottom")
}
override func update(currentTime: NSTimeInterval) {
println("\(aboveNode.name)")
}
func createBottomTargets() {
ball = SKSpriteNode(imageNamed: "blue1")
ball.position = CGPoint(x: frame.size.width / 2, y: -50)
ball.texture = textures[random]
self.addChild(ball)
let move = SKAction.moveByX(0, y: 1400, duration: 18)
let remove = SKAction.removeFromParent()
ball.runAction(SKAction.sequence([move, remove]))
aboveNode.position = CGPoint(x: ball.position.x, y: ball.position.y + 163)
}
}
Depending on how other parts of your game works, you could set it up so that each subsequent node is a child node of the node before it. That way the child nodes will move with their parent nodes. Imagine a vertical stack of nodes set up this way, each of which is 50px tall. Each node has a local position of [0, 50] to place it atop its parent. If you remove the bottom node and add it's child nodes back to the scene, all of the descendent nodes will appear to have moved down because their position is relative to their parent node.
In my game, there's a class for a "wall" that's moving to the left. I want to change the speed of it based on count i that I added to touchesBegan method:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent {
count++
}
func startMoving() {
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: 1 )
let move = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: 0.5)
if(count <= 10)
{
runAction(SKAction.repeatActionForever(moveLeft))
}
else
{
runAction(SKAction.repeatActionForever(move))
}
}
but it's not working. Can you help?
As I said there are a lot of changes which have to be done:
First let's change MLWall class and add a duration property which will be used in startMoving method:
var duration:NSTimeInterval = 0.5
Then still inside MLWall class change the init method:
init(duration: NSTimeInterval) {
self.duration = duration
//...
}
And change startMoving method to use this passed parameter:
func startMoving() {
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: self.duration)
runAction(SKAction.repeatActionForever(moveLeft))
}
Those are changes inside Wall class. Now let's make some changes in WallGenerator class:
First WallGenerator class should be aware of how fast walls should go. So we are adding property to store that info:
var currentDuration: NSTimeInterval = 1 // I named it duration, because SKAction takes duration as a parameter, but this actually affects on speed of a wall.
After that the first method which has to be changed is startGeneratingWallsEvery(second:) into startGeneratingWallsEvery(second: duration:
//duration parameter added
func startGeneratingWallsEvery(seconds: NSTimeInterval, duration : NSTimeInterval) {
self.currentDuration = duration
generationTimer = NSTimer.scheduledTimerWithTimeInterval(seconds, target: self, selector: "generateWall", userInfo: nil, repeats: true)
}
Here, we are making a WallGenerator aware of desired wall's speed.
And the next method which has to be changed in order to use that speed is:
//duration parameter added
func generateWall() {
//...
//Duration parameter added
let wall = MLWall(duration: self.currentDuration)
//...
}
And there is a GameScene left. There, I've added a tapCounter property:
let debugLabel = SKLabelNode(fontNamed: "Arial") //I've also added a debug label to track taps count visually
var tapCounter = 0
Here is how you can initialize label if you want to see number of tap counts:
//Setup debug label
debugLabel.text = "Tap counter : \(tapCounter)"
debugLabel.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMaxY(frame)-50.0)
debugLabel.fontColor = SKColor.purpleColor()
self.addChild(debugLabel)
First I've changed the start method:
func start() {
//...
// adding duration parameter in method call
wallGenerator.startGeneratingWallsEvery(1,duration: 1)
}
The important part is : wallGenerator.startGeneratingWallsEvery(1,duration: 1) which says start generating walls every second with one second duration(which affects on node's speed).
Next, I've modified touchesBegan of the scene into this:
if isGameOver {
restart()
} else if !isStarted {
start()
} else {
tapCounter++
debugLabel.text = "Tap counter : \(tapCounter)"
if(tapCounter > 10){
wallGenerator.stopGenerating()
wallGenerator.startGeneratingWallsEvery(0.5, duration:0.5)
}
hero.flip()
}
Then, changed restart() method in order to restart the counter when game ends:
func restart() {
tapCounter = 0
//...
}
And that's pretty much it. I guess I haven't forgot something, but at my side it works as it should. Also, note that using NSTimer like from this GitHub project is not what you want in SpriteKit. That is because NSTimer don't respect scene's , view's or node's paused state. That means it will continue with spawning walls even if you think that game is paused. SKAction would be a preferred replacement for this situation.
Hope this helps, and if you have further questions, feel free to ask, but I guess that you can understand what's happening from the code above. Basically what is done is that WallGenerator has become aware of how fast their wall nodes should go, and Wall node has become aware of how fast it should go...
EDIT:
There is another way of changing walls speed by running an moving action with key. Then, at the moment of spawning, based on tapCounter value, you can access an moving action by the key, and directly change actions's speed property...This is probably a shorter way, but still requires some changes (passing a duration parameter inside Wall class and implementing tapCounter inside scene).
Try doing it like so :
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent {
count++
startMoving()
}
func startMoving() {
removeAllActions()
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: count <= 10 ? 1.0 : 0.5)
runAction(SKAction.repeatActionForever(moveLeft))
}
As of I read comments below your question, you made a really unclear question, but nevertheless you have idealogical problems in your code.
Your touchesBegan method is implemented in the wall if I understood everything right, so it has no effect on newly generated walls. You have to move that logic to the scene and then spawn new walls with speed as a parameter, or at least make count a class var, so every wall can access that, but you still has to handle your touches in the scene, because now touch is handled when user taps directly in the wall.