How can I get the position of a moving SKSpriteNode? - ios

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
}
}

Related

Is it acceptable to create an SKShapeNode in update delegate?

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.

Adding child outside of didMove scope

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!!
}

Keep track of first node in line of nodes

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.

How to change the duration of a movement with count- Swift SpriteKit?

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.

Gradually increasing speed of scrolling background in SpriteKit

I am making a simple game in SpriteKit, and I have a scrolling background. What simply happens is that a few background images are placed adjacent to each other when the game scene is loaded, and then the image is moved horizontally when it scrolls out of the screen. Here is the code for that, from my game scene's didMoveToView method.
// self.gameSpeed is 1.0 and gradually increases during the game
let backgroundTexture = SKTexture(imageNamed: "Background")
var moveBackground = SKAction.moveByX(-self.frame.size.width, y: 0, duration: (20 / self.gameSpeed))
var replaceBackground = SKAction.moveByX(self.frame.size.width, y: 0, duration: 0)
var moveBackgroundForever = SKAction.repeatActionForever(SKAction.sequence([moveBackground, replaceBackground]))
for var i:CGFloat = 0; i < 2; i++ {
var background = SKSpriteNode(texture: backgroundTexture)
background.position = CGPoint(x: self.frame.size.width / 2 + self.frame.size.width * i, y: CGRectGetMidY(self.frame))
background.size = self.frame.size
background.zPosition = -100
background.runAction(moveBackgroundForever)
self.addChild(background)
}
Now I want to increase the speed of the scrolling background at certain points of the game. You can see that the duration of the background's horizontal scroll is set to (20 / self.gameSpeed). Obviously this does not work, because this code is only run once, and therefore the movement speed is never updated to account for a new value of the self.gameSpeed variable.
So, my question is simply: how do I increase the speed (reduce the duration) of my background images' movements according to the self.gameSpeed variable?
Thanks!
You could use the gameSpeed variable to set the velocity of the background. For this to work, firstly, you need to have a reference to your two background pieces (or more if you so wanted):
class GameScene: SKScene {
lazy var backgroundPieces: [SKSpriteNode] = [SKSpriteNode(imageNamed: "Background"),
SKSpriteNode(imageNamed: "Background")]
// ...
}
Now you need your gameSpeed variable:
var gameSpeed: CGFloat = 0.0 {
// Using a property observer means you can easily update the speed of the
// background just by setting gameSpeed.
didSet {
for background in backgroundPieces {
// Minus, because the background is moving from left to right.
background.physicsBody!.velocity.dx = -gameSpeed
}
}
}
Then position each piece correctly in didMoveToView. Also, for this method to work each background piece needs a physics body so you can easily change its velocity.
override func didMoveToView(view: SKView) {
for (index, background) in enumerate(backgroundPieces) {
// Setup the position, zPosition, size, etc...
background.physicsBody = SKPhysicsBody(rectangleOfSize: background.size)
background.physicsBody!.affectedByGravity = false
background.physicsBody!.linearDamping = 0
background.physicsBody!.friction = 0
self.addChild(background)
}
// If you wanted to give the background and initial speed,
// here's the place to do it.
gameSpeed = 1.0
}
You could update gameSpeed in update for example with gameSpeed += 0.5.
Finally, in update you need to check if a background piece has gone offscreen (to the left). If it has it needs to be moved to the end of the chain of background pieces:
override func update(currentTime: CFTimeInterval) {
for background in backgroundPieces {
if background.frame.maxX <= 0 {
let maxX = maxElement(backgroundPieces.map { $0.frame.maxX })
// I'm assuming the anchor of the background is (0.5, 0.5)
background.position.x = maxX + background.size.width / 2
}
}
}
You could make use of something like this
SKAction.waitforDuration(a certain amount of period to check for the updated values)
SKAction.repeatActionForever(the action above)
runAction(your action)
{ // this is the completion block, do whatever you want here, check the values and adjust them accordly
}

Resources