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!)
}}
Related
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!!
}
I'm building my first game in Swift and I wanted to know how to go about handling multiple on screen sprites at once. My game pushes sprites on to screen with addChild continuously, so there are many active at once. I realized that I didn't have a proper way of simultaneously affecting all of them- like if I wanted to affect a physics property of all enemy sprites at once. So far I created an empty array var enemySprites = [enemyType1]() at the begining of GameScene and have been appending the sprite instances to it instead of using addChild to draw them directly to the scene. However, I'm not able to simply loop through and draw them to screen with:
for enemy in enemySprites{
addChild(enemy)
}
this bit of code is in the override func update(currentTime: CFTimeInterval) function, so maybe I'm just misplacing it? Any help on how to go about this would be great!
Sam,
Here's some sample code to update enemies when your lives reach 0:
First, we set a property observer on the lives property so we can call a function when you lose all lives:
var lives = 3 {
didSet {
if lives == 0 {
updateEnemies()
}
}
And then a function to enumerate over all the enemies and change each one's velocity to (0, 0):
func update enemies() {
enumerateChildNodesWithName("type1") {
node, stop in
let enemy = node as! SKSpriteNode
enemy.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
}
}
Instead of use update method, you could use a timer. From sources:
public class func scheduledTimerWithTimeInterval(ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
So if you follow Apple guide, it will be for example:
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("spawnAlien:"), userInfo: myParameter, repeats: true)
func spawnAlien(timer : NSTimer) {
if let myUserInfo = timer.userInfo {
print(myUserInfo) // a parameters passed to help you to the alien creation
}
timer.invalidate()
}
BUT according to Whirlwind I agree with him and with LearnCocos2d work, sprite-kit don't work well with timers (as explained in the link by LearnCocos2d) and the better way, especially as you say you develop your first game, it's to use SKAction, a combination of actions to achieve the similar behavior obtained by NSTimer.
I've think about a function or an extension, let me know if it's work as expected:
extension SKAction {
class func scheduledTimerWithTimeInterval(time:NSTimeInterval, selector: Selector, repeats:Bool)->SKAction {
let call = SKAction.customActionWithDuration(0.0) { node, _ in
node.performSelector(selector)
}
let wait = SKAction.waitForDuration(time)
let seq = SKAction.sequence([wait,call])
let callSelector = repeats ? SKAction.repeatActionForever(seq) : seq
return callSelector
}
}
Usage:
let spawn = SKAction.scheduledTimerWithTimeInterval(time, selector: #selector(GenericArea.spawnAlien), repeats: true)
self.runAction(spawn,withKey: "spawnAlien")
When returning to my App after closing it the applicationDidBecomeActive(application: UIApplication) automatically fires in AppDelegate.swift.
This fires a method that handles the paused status of the app:
GameViewController().pause(true)
The method looks like this:
func pause(paused: Bool) {
if paused == true {
scene?.paused = true
print("paused")
} else if paused == false {
scene?.paused = false
print("unparsed")
}
}
When first launching the app the Game is automatically paused which is exactly what should happen. When returning to the app it unpauses though. Still, the Console prints "paused".
I have also tried using scene?.view?.paused instead of scene?.paused. This does work, but leads to lag in the animations running on the scene.
Any help would be highly appreciated
EDIT
I managed to solve the problem by calling the pause() method in the update(currentTime: NSTimeInterval) function but I don't like this solution as it means the method is called once per frame. Other solutions would be highly appreciated
This code makes no sense
GameViewController().pause(true)
because you are creating a new instance of GameViewController rather than accessing the current one.
Rather than pausing the whole scene you should just pause the nodes that you would liked paused. Usually you create some kind of worldNode in your game scene (Apple also does this in DemoBots)
class GameScene: SKScene {
let worldNode = SKNode()
// state machine, simple bool example in this case
var isPaused = false
....
}
than add it to the scene in DidMoveToView
override func didMoveToView(view: SKView) {
addChild(worldNode)
}
Than all nodes that you need paused you add to the worldNode
worldNode.addChild(YOURNODE1)
worldNode.addChild(YOURNODE2)
Than your pause function should look like this
func pause() {
worldNode.paused = true
physicsWorld.speed = 0
isPaused = true
}
and resume like this
func resume() {
worldNode.paused = false
physicsWorld.speed = 1
isPaused = false
}
Lastly to make sure the game is always paused when in paused add this to your update method. This ensures that you game does not resume by accident e.g due to app delegate, iOS alerts etc.
override func update(currentTime: CFTimeInterval) {
if isPaused {
worldNode.paused = true
physicsWord.speed = 0
return
}
// Your update code
...
}
To call these from your AppDelegate you should use delegation or NSNotificationCenter as has been mentioned in one of the comments.
In gameScene create the NSNotifcationObserver in didMoveToView
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(pause), name: "Pause", object: nil) // in your app put the name string into a global constant property to avoid typos when posting
and in appDelegate post it at the correct spot
NSNotificationCenter.defaultCenter().postNotificationName("Pause", object: nil)
The main benefit with the worldNode approach is that you can easily add pause menu sprites etc while the actual game is paused. You also have more control over your game, e.g having the background still be animated while game is paused.
Hope this helps.
I am trying to transition from the default, root scene to a new scene with SpriteKit. However, whenever I press the Start button, it grays out the old scene (although it remains visible) and the Drawing Board label shows up. The scene remains greyed out. All the buttons from the old scene can still be pressed but do not perform their associated actions. A UIButton triggers this func:
startButton.addTarget(self, action: "goToDrawingBoard:", forControlEvents: UIControlEvents.TouchUpInside)
The func:
#objc func goToDrawingBoard(sender: UIButton){
let drawingBoardScene = DrawingBoardScene(size: self.size)
self.scene?.view?.presentScene(drawingBoardScene, transition: SKTransition.crossFadeWithDuration(1.0))
}
And the DrawingBoardScene.swift file:
import Foundation
import SpriteKit
import UIKit
class DrawingBoardScene: SKScene {
let titleLabel = SKLabelNode(text: "DRAWING BOARD")
override func didMoveToView(view: SKView) {
/*LABEL: Displays title*/
titleLabel.fontColor = UIColor.blackColor()
titleLabel.fontSize = 60
titleLabel.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(titleLabel)
}
}
Looks like you are presenting the scene incorrectly, try the following:
#objc func goToDrawingBoard(sender: UIButton){
let drawingBoardScene = DrawingBoardScene(size: self.size)
self.view?.presentScene(drawingBoardScene, transition: SKTransition.crossFadeWithDuration(1.0))
}
There is no reason to add the new scene as a child to the old scene, and who knows why your scene has a scene object.
As a personal note, presenting your scene in this matter is not a good way to present scenes. It is the views job to be presenting scenes, so what you should be doing is when it comes time for the scene to be removes, send a notification in some way to the view that the scene is done working and is waiting for it to be removed, and have the view then present the scene. This will allow the view to properly remove the old scene without having any retainers holding it back. One method to do this is threw delegation
My friend and I spent a couple nights working on various solutions and the one we finally came up with is this:
override func willMoveFromView(view: SKView) {
self.removeAllChildren()
delete(startButton)
}
override func delete(sender: AnyObject?) {
let subviews = (self.view?.subviews)! as [UIView]
for v in subviews {
if let button = v as? UIButton {
button.removeFromSuperview()
}
}
}
The only problem with this is that when the scene shifts the buttons can take a split second longer to disappear, giving it a kind of glitchy feel. It does work though, so for a short term solution it is great.
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.