SpriteKit: How do I remove objects once they leave the screen? - swift

I have a rock sprite that it is falling and every time it goes out of the screen I want to reset it back to the top and have it fall again. It should be a continuous cycle. Here's my code:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
func addRock(){
var rock = self.childNode(withName: "rock")
rock?.physicsBody?.affectedByGravity = true
//self.addChild(rock!)
}
override func sceneDidLoad() {
//bRock = self.childNode(withName: "rock")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//addRock()
var rock = self.childNode(withName: "rock")
rock?.physicsBody?.affectedByGravity = true
/*if (Int((rock?.position.y)!) < Int((self.view?.scene?.view?.bounds.minY)!)){
print("out of screen")
rock?.removeFromParent()
addRock()
}*/
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
var rock = self.childNode(withName: "rock")
//rock?.physicsBody?.affectedByGravity = true
if (!intersects(rock!)){
print("out of screen")
rock?.removeFromParent()
addRock()
}
}
}
I have the rock coming on the screen and then falling. Once it leaves the screen, it does not reset and I get an error. I tried placing the reset code in both the touchesBegan and update functions but neither work. If someone could guide me to the correct path, that would be greatly appreciated.

The problem you're having is that you're removing the rock node from the scene but the addRock method doesn't actually add a new rock, it just finds the existing node if there is one and sets a property on its physicsBody. Instead of removing the node, you should just change its position.
Assuming your scene has the default anchor point of (0,0), you can reset the position like this:
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if let rock = self.childNode(withName: "rock") {
if !intersects(rock!) {
print("out of screen")
rock.position.y = size.height/2 // divided by 2 as discussed in comments
}
}
That will reset the rock's position to the top of the scene and it should resume falling from there.

Three ways:
the same rock is newly located at the right place after leaving the screen, so you just need to set the position of it in (misnamed) addRock (resetRock should be better): rock.position = CGPoint(...). But beware that you also need to reset its velocity, etc, as it was previously moved by physical laws...
create a new rock each time one leaves the screen in addRock: let rock = SKSpriteNode(...)... (and all the initializing code for it. Much simpler as all its physical parameters will be initialized with right values by default.
create a clone of an initial rock. I suppose you have a model of it in your sks file, then don't name it rock but something like (rockModel), and just clone it in addRock with giving it the right name rock. Don't forget to remove the model from the scene at the beginning. This is the usual way to do it.

You could also think about using a series of move actions in a sequence to control the rock's behaviour. Given the move actions are already defined, in your defined sequence, pass in sequence[(falling, hide, backtotop, unhide)] - Repeat Forever.
As long as the rocks don't actually interact with anything and are just for the backdrop/background, hiding and unhiding them is a good way to give the affect they are falling.

Related

Remove SKAction and restore node state

Desired behavior is: when an action is removed from a node (with removeAction(forKey:) for instance) it stops to animate and all the changes caused by action are discarded, so the node returns back to pervious state. In other words, I want to achieve behavior similar to CAAnimation.
But when a SKAction is removed, the node remains changed. It's not good, because to restore it's state I need to know exactly what action was removed. And if I then change the action, I also will need to update the node state restoration.
Update:
The particular purpose is to show possible move in a match-3 game. When I show a move, pieces start pulsating (scale action, repeating forever). And when the user moves I want to stop showing the move, so I remove the action. As the result, pieces may remain downscaled. Later I would like to add more fancy and complicated animations, so I want to be able to edit it easily.
Thanks to the helpful comment and answer I came to my own solution. I think the state machine would be bit too heavy here. Instead I created a wrapper node, which main purpose is run the animation. It also has a state: isAimating property. But, first of all, it allows to keep startAnimating() and stopAnimating() methods close to each other, incapsulated, so it's more difficult to mess up.
class ShowMoveAnimNode: SKNode {
let animKey = "showMove"
var isAnimating: Bool = false {
didSet {
guard oldValue != isAnimating else { return }
if isAnimating {
startAnimating()
} else {
stopAnimating()
}
}
}
private func startAnimating() {
let shortPeriod = 0.2
let scaleDown = SKAction.scale(by: 0.75, duration: shortPeriod)
let seq = SKAction.sequence([scaleDown,
scaleDown.reversed(),
scaleDown,
scaleDown.reversed(),
SKAction.wait(forDuration: shortPeriod * 6)])
let repeated = SKAction.repeatForever(seq)
run(repeated, withKey: animKey)
}
private func stopAnimating() {
removeAction(forKey: animKey)
xScale = 1
yScale = 1
}
}
Usage: just add everything that should be animated to this node. Works well with simple animations, like: fade, scale and move.
As #Knight0fDragon suggested, you would be better off using the GKStateMachine functionality, I will give you an example.
First declare the states of your player/character in your scene
lazy var playerState: GKStateMachine = GKStateMachine(states: [
Idle(scene: self),
Run(scene: self)
])
Then you need to create a class for each of these states, in this example I will show you only the Idle class
import SpriteKit
import GameplayKit
class Idle: GKState {
weak var scene: GameScene?
init(scene: SKScene) {
self.scene = scene as? GameScene
super.init()
}
override func didEnter(from previousState: GKState?) {
//Here you can make changes to your character when it enters this state, for example, change his texture.
}
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass is Run.Type //This is pretty obvious by the method name, which states can the character go to from this state.
}
override func update(deltaTime seconds: TimeInterval) {
//Here is the update method for this state, lets say you have a button which controls your character velocity, then you can check if the player go over a certain velocity you make it go to the Run state.
if playerVelocity > 500 { //playerVelocity is just an example of a variable to check the player velocity.
scene?.playerState.enter(Run.self)
}
}
}
Now of course in your scene you need to do two things, first is initialize the character to a certain state or else it will remain stateless, so you can to this in the didMove method.
override func didMove(to view: SKView) {
playerState.enter(Idle.self)
}
And last but no least is make sure the scene update method calls the state update method.
override func update(_ currentTime: TimeInterval) {
playerState.update(deltaTime: currentTime)
}

Moving SKSpriteNode with finger only if it is of type Fruit (subclass of SKSpriteNode)

I am creating a game where the user can move some fruits around in the scene. I want to user to be able to move only the fruits and not any other SKSpriteNode in the scene, so I wrote the code below to implement it. However the code doesn't work properly as I can't seem to be able to drag any of my sprites around, but rather they change position only when I stop touching the screen and they don't move by much anyway.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in: self){
let nodesTouched = nodes(at: location)
for node in (nodesTouched) {
if node is Fruit{
for t in touches {
let locationMoved = t.location(in: self)
node.position.x = locationMoved.x
node.position.y = locationMoved.y
}
}
}
}
}
Anyone knows what's wrong with it?
Thanks in advance!
I found a solution to this, which was to basically set the physicsBody.affectedByGravity property to false for that specific Fruit instance every time I touch it, and then set it back to true as soon as I stop touching it. that makes it possible to drag all the fruits everywhere I want.

how to detect touch on node

I have an app thats spawn ball on the screen every 1 second. now, I want the user to touch those balls what make them disappear (removeFromParent()). as I understand I have to set the touch function via touchesBegan and I do so, here is my code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches{
let positionOfTouch = touch.location(in: self)
enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in
if positionOfTouch == node.position {
print("just touched the ball")
}
else{
print("error")
}
}
}
the problem is that when I touch the screen/ ball the console print error instead of just touched the ball, which mean that my code doesn't work. moreover, the console print the error message as the number of the balls in my view. i don't relay understand what I am doing wrong and how to really set this function.
here is my createBall function which implement from my BallNode class (type SKShapeNode):
func createBall(){
let ball = BallNode(radius: 65)
print(ball.Name)
print(ball._subName!)
ball.position.y = ((frame.size.height) - 200)
let ballXPosition = arc4random_uniform(UInt32(frame.size.width)) // set the ball a randon position from the top of the screen
ball.position.x = CGFloat(ballXPosition)
ball.physicsBody?.categoryBitMask = PhysicsCategory.ball // ball's category bitMask
ball.physicsBody?.collisionBitMask = PhysicsCategory.ball // prevent objects from intersecting
ball.physicsBody?.contactTestBitMask = PhysicsCategory.topBorder // when need to know if two objects touch each other
addChild(ball)
}
can you help me with that? because I am quit new for swift I also would like to get some explanation about this touch detection (and touches in general - the apple doc is poor).
every time you touch the screen you are cycling through all balls to see if you're touching one of them. if you have 50 balls on the screen it goes through them all to see if you are touching 1. that's not an efficient way of figuring out if you are touching 1.
There are many ways you can do this but what I would do is handle the touches inside of the Ball class. That way you don't have to figure out if you are touching a ball and which one it might be.
Explanation of protocol (to the best of my ability) this may seem a little much right now, but the faster you learn and understand protocols that better off you will be (IMO).
In this example we will use a protocol to setup a delegate of the
BallNode class. A protocol is a set user defined "rules" that must be
followed by any class that you designate compliant to that protocol.
In my example I state that for a class to be compliant to the
BallNodeDelegate protocol it must contain the didClick func. When you
add the BallNodeDelegate after GameScene you are stating that this
class will be compliant to that protocol. So if in GameScene you did
not have the didClick func it will cause an error. All this is put in
place so that you have an easy way to communicate between your
BallNode instances and your GameScene class (without having to pass
around references to your GameScene to each BallNode). Each BallNode
then has a delegate (GameScene) which you can pass back the
information to.
inside your BallNode class make sure you have isUserInteraction = true
outside of your BallNode class create a protocol that will send the touch info back to the GameScene
protocol BallNodeDelegate: class {
func didClick(ball: BallNode)
}
create a delegate variable in your BallNode class
weak var delegate: BallNodeDelegate!
move the touches began to you BallNode class
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.delegate?.didClick(ball: self)
}
in GameScene add the compliance to the BallNode protocol
class GameScene: SKScene, BallNodeDelegate
in GameScene when you create a Ball make sure you set it's delegate
let ball = BallNode()
ball.delegate = self
in GameScene add the nest. func to handle the clicks
func didClick(ball: BallNode) {
print("clicked ball")
}
You are comparing the exact touch point with the exact position of the node, which are very unlikely to ever be the same.
if positionOfTouch == node.position {
Instead, you'll need to test to see if the user's touch is close enough to the position of the ball.
One option is to use SKNode's contains function, which will handle this for you.
if node.contains(positionOfTouch) {
Side note: You'll probably want to use SKSpriteNode instead of SKShapeNode, as SKShapeNode has poor performance in SpriteKit.
Take a look at nodes(at:CGPoint) defined at SKNode to retrieve a list of the nodes at the touched position. You'll need to convert in between view coordinates and scene coordinates, though, using convertPoint(fromView). Documentation here and here.

SKSpriteNode Automated movement

I'm making my first game, it will be similar to Mario. (using swift)
Have started to make some enemies they will need to move left and right and respond to collisions, I can do this for my player node as I call player.update() function from the SKScene update function,
override func update(currentTime: NSTimeInterval)
{
player.update()
}
My question is:
For enemyX , how can I get the node to update since SKSpriteNode does not have a default update function that can be overridden and from my game scene
I do not want really write some thing like:
override func update(currentTime: NSTimeInterval)
{
player.update()
enemyX.update()
enemyY.update()
enemy001.update()
...
enemy100.update
}
Do you see what I mean, how do I get around this, does SKSpriteNode need to be a different class?
Maybe actions?
let moveLeft = SKAction.moveByX( 200, y:0, duration: 5)
self.runAction(moveLeft, completion: {
print("----> move left Complete")
})
Thanks.
you subclass SKSpriteNode and and an update method inside that class. then in your update method in your scene you do exactly what you said
player.update()
enemy.update()
etc.
Don't recommend using SKAction unless the behavior of your enemies is extremely simple.

touchesMoved not functioning properly

I am trying to implement an overriden touchesMoved func to enable SKSpriteNodes to be moved around by the user. However, I have to move the node very slowly for it to follow my touches when I drag. In addition, there are three SKSpriteNodes in the background (which you will see I explicitly set to .userInteractionEnabled = false) and these nodes will occasionally respond to the touches. Any help would be greatly appreciated. Let me know if there are any other parts of the code you need.
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
var positionInScene = CGPoint(x: 375.0, y: 400.0) //sets a default position
titleLabel.userInteractionEnabled = false
drawingBoard.userInteractionEnabled = false
sideBar.userInteractionEnabled = false
for touch in touches {
positionInScene = touch.locationInNode(self)
if self.nodeAtPoint(positionInScene) is SKSpriteNode {
if (self.nodeAtPoint(positionInScene)).name == movableNodeName { //movableNodeName is the name assigned to all SKSpriteNodes that should be draggable
//I know this might be a strange way of doing it
(self.nodeAtPoint(touch.previousLocationInNode(self))).position = positionInScene
}
}
}
}
I've managed to fix this issue by just setting the coordinates of the node that shouldn't move back to what they were originally whenever it is moved. This seems to work as I haven't been able to replicate the bug again in testing. If anyone could come up with a better solution though, I would love to hear it.
Your problem is you are using touch.locationInNode(self), where self is your SKSpriteNode. This means that your touch move code will only respond to what is going on inside of your SKSpriteNode. What you need to do, is use either the parent of the sprite, or the scene, based on how you want to apply this logic.