Why does overriding the position variable of my SKSpriteNode subclass slow things down so much? - swift

In Scratch there is a cool function called penDown which causes your sprite to trace a line of some color across the screen whenever it moves from A to B. I wanted to recreate this behavior by subclassing SKSpriteNode and getting notified whenever the position changes. However, this simple override is causing the whole thing to slow down a ton (FPS drops from 20 to 7 with only two sprites):
override var position : CGPoint {
get {
return super.position
}
set {
super.position = newValue
// Add this new point to the bezier path of the line so that I can trace it.
}
}
Why is this happening?

In this case you should be using property observers instead of overriding get and set.
override var position : CGPoint {
didSet {
// Add this new point to the bezier path of the line so that I can trace it.
}
}

Related

How to detect which CAShapeLayer was clicked on in macOS using Swift?

I have a view with many CAShapeLayer objects (there can be other CALayer objects as well) and I want to modify the CAShapeLayer object that the user clicks on.
I was experimenting with the below two methods but none of them works. Any tips would be great.
Approach one:
private func modifyDrawing(at point: NSPoint) {
for layer in view.layer!.sublayers! {
let s = layer.hitTest(point)
if s != nil && s is CAShapeLayer {
selectedShape = s as? CAShapeLayer
}
}
// modify some properties
selectedShape?.shadowRadius = 20
selectedShape?.shadowOpacity = 1
selectedShape?.shadowColor = CGColor.black
}
Approach two:
private func modifyDrawing(at point: NSPoint) {
let drawingsAtMouseClick: [CAShapeLayer] = view.layer!.sublayers!.compactMap{ $0 as? CAShapeLayer }
if drawingsAtMouseClick.isEmpty {
return
}
for drawing in drawingsAtMouseClick {
if drawing.contains(point) {
selectedShape = drawing
break
}
}
// modify some properties
selectedShape?.shadowRadius = 20
selectedShape?.shadowOpacity = 1
selectedShape?.shadowColor = CGColor.black
}
The point parameter passed to these functions is the NSEvent.locationInWindow. Not sure whether I should convert this with respect to the CAShapeLayer or something.
P.S.: This isn't production code so please kindly ignore Swift best practices, etc.
The CALayer.hitTest(_:) method will tell you the layer that was hit, including in a layer's sublayers.
You shouldn't need to check every sublayer. You should be able to ask your view's layer what layer was hit by asking the top-level layer.
A view's layer's coordinates are generally the same as the view's bounds. It's anchored at 0,0 in the parent layer, and the sublayers use that coordinate space. Thus, you should convert your point to view/layer coordinates before hit testing.
(I always have to go check to see which coordinate systems are flipped from the other between iOS and Mac OS and views and layers. You might need to flip coordinates. I leave that research up to you.)
Edit:
I seem to remember that CALayer.hitTest(_:) just checks that the layer's frame contains the point, not that it actually contains an opaque pixel at that position. It's more complex if you want to check to see if the point contains an opaque pixel.

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

How to check if sprite node change position

My sprite node change position on the screen. And I need to detect when sprite move to up, move to down, move to left, move to right.
Define a variable oldPosition and keep track of the sprite position in the update: method:
var oldPosition: CGPoint?
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if Int((oldPosition?.x)!) > Int((sprite?.position.x)!) {
// sprite moves to the left
} else if Int((oldPosition?.x)!) < Int((sprite?.position.x)!) {
// sprite moves to the right
}
if Int((oldPosition?.y)!) > Int((sprite?.position.y)!) {
// sprite moves down
} else if Int((oldPosition?.y)!) < Int((sprite?.position.y)!) {
// sprite moves up
}
// keep track
oldPosition = sprite?.position
}
I hope it helps.
You’ve got two options here. As you’ve not posted any code I can’t really advise which is the best:
If you’ve sub-classed SKNode, then override the frame property to add a didSet observer clause:
override var frame : CGRect {
didSet {
// compute the change in the frame’s origin
let delta : CGPoint = CGPoint(x: frame.origin.x - oldValue.origin.x,
y: frame.origin.y - oldValue.origin.y)
// etc
}
}
See SKNode docs at https://developer.apple.com/documentation/spritekit/sknode
If you don’t want to subclass, you can use NSNotificationCenter to add an observer to your node via addObserver:selector:name:object: . See the documentation at:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html
I’d probably go for option 1.
Hope that helps.

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

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.

SpriteKit: Add SKSpriteNodes To The View

I basically want to add a SKSpriteNode to the screen and I have no idea where the mistake it. So I include pictures of my three classes participating in this problem. I created a class for all the background stuff. There I use the function addChild() to add my 56 Nodes to the View but they do not appear on the screen. Theoretically (for me) everything should work.
Hopefully you can help me adding the Nodes to the screen!
Edit: Updated the pictures.
When you call this:
GameScene().addChild(image)
you every time inside for create the local GameScene object, add image to this object and when you move to the next iteration of the loop, this object is destroyed. In fact, after your loop thire is no one GameScene object.
Second, you don't need to use var image = SKSpriteNode() in your hintergrund object. You every time set this reference to new object.
You have the cross references in code, try to make somthing like this:
class GameScene: SKScene {
...
override func didMoveToView(view: UIView) {
hintergrund.setBackgroundForScene(self, width: screenWidht)
}
}
class hintergrund {
...
class func setBackgroundForScene(gameScene: GameScene, width: CGFloat) {
...
for (...) {
...
gameScene.addChild(image)
}
}
}