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!!
}
Related
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.
I'm working on an iOS game based on SpriteKit/Swift using xCode, currently experimenting around with animations.
I've created a class PlayerSprite as a subclass of SKSpriteNode and defined a method moveRight running the following action:
run(
SKAction.moveBy(x: 32.0, y: 0.0, duration: 0.25), completion: {
debugPrint("Action completed.")
}
)
There's only one player instance of class PlayerSprite which is part of the node tree (SKScene -> SKTileMap -> PlayerSprite). The point is, that the mentioned action is not executed reliably:
When I start the app "for the first time" it's executed.
When I stop the app and start it again it's NOT executed.
When I press "pause" in the debugger and than "play" again, the action is executed!
This holds for the simulator as well as for starting the app on a connected iPhone. Stopping the app refers to pressing the stop button in xCode and starting to running it again from xCode.
The update loop is part of the SKScene subclass (called LevelSceneView in this case) and looks like this (just playing around so far):
override func update(_ currentTime: TimeInterval) {
debugPrint("Update called")
if !player.hasActions() {
player.moveRight()
}
}
The update loop is called correctly, but the action is not executed (according to the log console).
Has anyone experienced such a behavior yet? Any ideas would be very much appreciated. I hope I've described the issue adequately. In case of any questions don't hesitate to ask.
So If I understand correctly, the animation doesn't always run? I did run into a problem like this a while ago using xCode 9. The way I fixed it was to toggle the isPaused properties of the scene. So at the end of my update call I have two lines of code that read like so:
self.isPaused = true
self.isPaused = false
This way each time update() is called the scene is paused then paused. Using this I have never had a problem with running animations since. Hopefully, that helps get around the problem.
Where is your animation located?
If it's in didMove function then your action is going to run only when your app is loaded/loading.
I think the problem could be that when you stop your app it's going to stay in phone memory for some time.
That will probably cause the problem, I would recommend to do not put any kinds of actions in didMove function if it's possible.
I use the simple subclass from the SKSpriteNode and found no issue. Maybe you can restart from here.
class MySpriteNode: SKSpriteNode {
func moveRight(){
run(
SKAction.moveBy(x: 3, y: 0.0, duration: 0.25), completion: {
debugPrint("Action completed.")
}
)
}
}
import SpriteKit
import GameplayKit
class GameScene: SKScene {
// private var label : SKLabelNode?
// private var spinnyNode : SKShapeNode?
private var myNode : MySpriteNode?
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
debugPrint("Update called")
if !(myNode?.hasActions())! {
myNode?.moveRight()
}
}
override func didMove(to view: SKView) {
myNode = MySpriteNode.init(texture: nil, color: UIColor.red, size: CGSize.init(width: 200, height: 200))
self.addChild(myNode!)
}}
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 am building a game in SpriteKit. The game will include 5 players and instead of coding SKActions five times for every player, I want to consolidate all characters into one and code one time for each SKAction, but the code below doesn't seem to work. Am I missing something?
import SpriteKit
var player1 = SKSpriteNode()
var player2 = SKSpriteNode()
var player3 = SKSpriteNode()
var player4 = SKSpriteNode()
var player5 = SKSpriteNode()
var mainPlayer = SKSpriteNode()
// in view did load
player1 = mainPlayer
player2 = mainPlayer
player3 = mainPlayer
player4 = mainPlayer
player5 = mainPlayer
//in touches began
let rightMoveAction = SKAction.moveByX(50, y: 0, duration: 0.1)
mainPlayer.runAction(SKAction.repeatActionForever(rightMoveAction))
I'm assuming your motivation is to make the code shorter and because mainPlayer is probably a complex node that you don't want to replicate the code 5 times. You don't show where you add the nodes to the scene, but the way you currently have it will crash when you try to add player2 because it is already added. It looks like you are trying to simply make 5 of the same character that can perform the same action.
Here is a general strategy I would use to try to accomplish what you're after. Since your mainPlayer node is probably some complex character that you will be reusing (possibly even in other scenes), then you might consider making it into its own class (and put the code in a separate .swift file). You can then add the functions it will use to move and perform other SKActions
For example:
import SpriteKit
class Character : SKSpriteNode {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(texture: SKTexture?, color: UIColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
//setup your node here
self.userInteractionEnabled = true//you need this to detect touches
//log so you know it was created - take this out at some point
print("character created")
}
//create an example action that will move the node to the right by 50
func moveRight() {
print("moving right")
self.runAction(SKAction.moveByX(50, y: 0, duration: 0.1))
}
//you can also override the touch functions so you'll know when the node was touched
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
print("charater touched")
}
}
Then in your scene, when you want to create and use a new character, you can do this:
//create the character
let newPlayer = Character.init(texture: nil, color: UIColor.blueColor(), size: CGSizeMake(50, 50))
//position the character where you want
newPlayer.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
//add the character to the scene
self.addChild(newPlayer)
/move the character to the right
newPlayer.moveRight()
Making things more "modular" like this is great for re-usability of code. Now you can reuse the Character class whenever and wherever you need. Even in an entirely different game.
In terms of making all characters run the same action with one function call, I don't know of anything that would allow this. Although you could write your own function for it by perhaps placing all of the character nodes into an array and passing the array as an argument to a custom function, but personally I would just call the moveRight function on each of the nodes after you instantiated them.
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.