Swift and spritekit, performance - ios

I'm having some issues getting my game working in Swift/SpriteKit, and can't find answers elsewhere so I was hoping I'd get help here.
I have a screen filled with sksprite nodes, around 1800 of them, and when my finger moves over them they disappear.
I originally had this working using physics, by dragging a hidden node around under my finger, however I've just loaded it on my phone and the performance isn't up to scratch.
I then redesigned it to use 'containsPoint'.
Now the performance is better when at rest (60FPS), however whenever I touch the screen is hangs, and CPU goes up.
This is because it's having to loop through the location of each tile on every move, which is over 1800 calculations per move, and compare it to my current touch.
Please can someone suggest a more elegant solution? I've tried using a dictionary however it complains that CGPOINT isn't hashable.
I'll paste the relevant code below, many thanks!
func addTiles() {
let rowsReq=56, colsReq=32
for i in 0..< rowsReq*colsReq {
...
let tileNode:SKSpriteNode = SKSpriteNode()
tilePosArray.append(tileNode.position)
...
}
}
func moveMarker(location: CGPoint){
markerLayer.position=location
for i in 0..<tilePosArray.count {
if (markerLayer.containsPoint(tilePosArray[i])) {
tileTouched(i)
}
}
}
func tileTouched(tileIdInt: Int) {
tileArray[tileIdInt].alpha=0
checkComplete()
}

You could use:
self.nodeAtPoint(point: CGPoint)
or
self.nodesAtPoint(point: CGPoint)
with the touch location to find the node(s) that are being touched, and then the alpha can be changed for the node(s)

Related

How to display the number of objects in an array as Sprites on screen in SpriteKit

In my game I have the number of lives saved in an array, the lives are then displayed in the top right of the game view during gameplay. The way I have it done this is working but I hit problems when moving to the next level.
This works fine in the first level, I can lose lives when hit but I did hit a problem when I tried to have extras life pickups. I was able to add to the array but couldn't display the extra life on the screen. I ended up cutting this feature from the game but now hit the same problem when player advances to next level. Using the same loop will result in the player having three lives again no matter how many are lost in the previous level.
I know the problem is the loop I've used caused I've set the for in loop to be 1 to 3. I'm not great a programming and not sure what kind of loop to use for this and when Googling for a solution I'm obviously not asking the right questions to point me in the right direction.
If anyone can help it would be greatly appreciated. Thank you.
Here's my code:
class GameScene: SKScene, SKPhysicsContactDelegate {
//lives
static var livesArray:[SKSpriteNode]!
override func didMove(to view: SKView) {
//Call Lives method
addLives()
}
func addLives() {
GameScene.livesArray = [SKSpriteNode]()
for life in 1...3 {
let lifeNode = SKSpriteNode(imageNamed: "Life")
lifeNode.position = CGPoint(x: 350 - CGFloat(4 - life) * lifeNode.size.width, y: 525)
addChild(lifeNode)
GameScene.livesArray.append(lifeNode)
}
}
}

Detecting when a SKNode is tapped on Apple Watch

I'm writing an app for Apple Watch using SpriteKit, so I don't have access to functions like touchesBegan and I have to use a WKTapGestureRecognizer to detect taps, no big deal, but I have issues detecting taps on a node.
In my InterfaceController I have:
#IBAction func handleTap(tapGestureRecognizer: WKTapGestureRecognizer){
scene?.didTap(tapGesture: tapGestureRecognizer)
}
And in my Scene file I have
func didTap(tapGesture:WKTapGestureRecognizer) {
let position = tapGesture.locationInObject()
let hitNodes = self.nodes(at: position)
if hitNodes.contains(labelNode) {
labelNode.text = "tapped!"
}
Problem is the Tap Gesture Recognizer gives me the absolute coordinates of the touch point (for example 11.0, 5,0) while my node is positioned relatively to the center of the screen (so its position is -0.99,-11.29 even though is at the center of the screen) therefore the tap is hitting the node not when actually tapping it, but when I tap on the top left of the screen. I searched everywhere and it looks like this is the way to do it yet I don't find people having the same issues. The node has been added via the editor. What am I doing wrong?
So you have the right idea. You are getting this wrong because hitNodes is an array of SKNodes. Those are newly created. So when you use hitNodes.contains the addresses of the labelNode and the address of the newly created SKNode that is being compared would be completely different. Therefore it would never be tapped.
Here's what I would do. This would be in my Scene File. Your InterfaceController class is correct.
func didTap(tapGesture:WKTapGestureRecognizer) {
let position = tapGesture.locationInObject()
if labelNode.contains(position) {
labelNode.text = "tapped!"
}
}
OR another way would be this. I like this way because you only have one function which would be in the WKInterfaceControlller And you would need no functions in your Scene File.
#IBAction func tapOnScreenAct(_ sender: WKGestureRecognizer) {
if scene.labelNode.contains(sender.locationInObject()) {
scene.labelNode.text = "tapped!"
}
}
Either way, both should work. Let me know if you have any more questions or clarifications.

Detecting touch events on SKSpriteNodes

so there are some similar questions on here, but they don't really answer the questions/problem I am having.
The following explanation of my project may or may not help, I am adding it here just in case...
I am trying to build my first iOS Game, and I have built a scene using the scene editor. The scene contains a SKNode named "World". There is then a backgroundNode and a foregroundNode that are children of the world node. Everything in the scene right now, all SKSpriteNodes, are children of the backgroundNode.
In my GameScene.swift file I have variables attached to the background and foreground nodes so that I can add children to these nodes as the game progresses.
Hopefully this is clear so far...
Now I have added 5 other *.sks files to my project. These file contain scenes that I have made that will be added as children of the foreground node through code in my GameScene.swift file. The SKSpriteNodes in these scene files are placed in the foreground, but their z-position is less than the z-position of one of the background child nodes. This is because I want to have a box appear behind a light beam (the light beam is apart of the background and the box is added to the foreground). Here is a picture in case I caused any confusion
My problem is this...
I want to tap on the screen using gesture recognizers so that when I tap on the box I can then do some stuff. Trouble is that since the light beam has a greater z-position (cause of the effect I want), every time I use the atPoint(_ p: CGPoint) -> SKNode method to determine what node I tapped on I get returned the light beam node and not the box node.
How do I tap on just the box? I have tried changing the isUserInteractionEnabled property for the lights to false already, and I have tried using touchesBegan as shown in many of the other responses to similar question. I have also tried reading the swift developer documents provided by Apple, but I can't figure this out.
The code for my gesture Recognizers is below:
//variables for gesture recognition
let tapGesture = UITapGestureRecognizer()
let swipeUpGesture = UISwipeGestureRecognizer()
//set up the tap gesture recognizer
tapGesture.addTarget(self, action: #selector(GameScene.tappedBox(_:)))
self.view!.addGestureRecognizer(tapGesture)
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
//handles the tap event on the screen
#objc func tappedBox(_ recognizer: UITapGestureRecognizer) {
//gets the location of the touch
let touchLocation = recognizer.location(in: recognizer.view)
if TESTING_TAP {
print("The touched location is: \(touchLocation)")
}
//enumerates through the scenes children to see if the box I am trying to tap on can be detected (It can be detected using this just don't know how to actually detect it by tapping on the box)
enumerateChildNodes(withName: "//Box1", using: { node, _ in
print("We have found a single box node")
})
//tells me what node is returned at the tapped location
let touchedNode = atPoint(touchLocation)
print("The node touched was: \(String(describing: touchedNode.name))")
//chooses which animation to run based on the game and player states
if gameState == .waitingForTap && playerState == .idle{
startRunning() //starts the running animation
unPauseAnimation()
}else if gameState == .waitingForTap && playerState == .running {
standStill() //makes the player stand still
unPauseAnimation()
}
}
Hopefully this is enough for you guys... If you need some more code from me, or need me to clarify anything please let me know
Please read notes in the code. You can get what you want easily in spriteKit. Hope you get the answer.
#objc func tappedBox(_ recognizer: UITapGestureRecognizer) {
let touchLocation = recognizer.location(in: recognizer.view)
// Before you check the position, you need to convert the location from view to Scene. That's the right location.
let point = (recognizer.view as! SKView).convert(touchLocation, to: self)
print ( getNodesatPoint(point, withName: "whatever Node name" ) )
}
// 2 : This function gives you all nodes with the name you assign. If you node has a unique name, you got it.
/// You can change name to other properties and find out.
private func getNodesatPoint(_ point : CGPoint , withName name: String) -> [SKNode] {
return self.nodes(at: point).filter{ $0.name == name}
}
You want to use nodesAtPoint to do a hit test and get all of your nodes that is associated with the point. Then filter the list to find the box you are looking for.
If you have a lot of layers going on, you also may want to just add an invisible layer on top of your graphics that handles nodes being touched
Your other option is to turn off isUserInteractionEnabled on everything except the boxes and then override the touch events for your boxes, but that would mean you can't use gestures like you are doing now.

Is it possible to have an SKaction repeat forever only when the sprite is in view of the camera/player?

This is for a 2D game:
I have certain quality of life SKactions repeating forever, the two big ones for me are coins rotating/bobbing up and down and water flowing.
According to Apple's documentation, SKactions are instanced. So as long as I have the action subclassed then its only running "once" regardless of how many sprites its being used on. For example, as long as I have all my coins getting their actions from the same "Coin" class, then the memory footprint being used by the coin's action is the same regardless if I have 1 or 20 coins.
All that being said, it seems like such a waste to have these actions going when they aren't even in view of the camera/player.
Is there a way to have repeating forever actions deactivate when they aren't in view of the camera? I know that defeats the purpose of "forever" but as far as I can tell its either choosing some some sort of static duration or choosing forever.
Any advice is greatly appreciated.
Is better to add node only when is visible, but if you have few nodes, you can use
func containedNodeSet() -> Set
Example
class Enemy: SKSprite {
func startAnimationForever() {
//do animation if is not already running
}
func stopAnimation() {//stop animation}
}
in your scene, suppose your cam i myCam:
//add all enemies in this var or search in the scene all enemy
var allEnemies = Set<Enemy>()
func allVisibleEnemy() -> Set<Enemy>() {
let allVisibleEnemy = myCam.containedNodeSet().enumerated().flatMap{node in
if let enemy = node as? Enemy {
return enemy
}
}
return Set(arrayLiteral: allVisibleEnemy)
}
func allInvisibleEnemy() -> Set<Enemy>() {
let allVisibleEnemy = allVisibleEnemy()
return allEnemies.substract(allVisibleEnemy)
}
override func update() {
//all your stuff
let allVisibleEnemy = getVisibleEnemy()
let allInvisibleEnemy = allInvisibleEnemy()
allVisibleEnemy.forEach{enemy in
enemy.startAnimationForever()
}
allInvisibleEnemy.forEach{enemy in
enemy.stopAnimation()
}
}
You can optimizate it if necessary
I've not the compiler, fell free to edit.
If for any reason you have nodes that are not on the screen that does need to be on the scene for anything, then you should be taking it off the scene to help improve performance. This would stop actions on the nodes from running, and stop your physics world from having to check if anything physics related needs to placed on it.
Now there are many ways to go about doing this, but a very basic principal would be to establish some kind of map that lays out your nodes (This could be an SKScene that you have not attached to the main scene) Then use the map(scene) to keep track of all of your nodes. Take your camera and find all the nodes on the map (scene) that is in the view of the camera, and move those nodes over to the main scene.

SpriteKit addChild order is different after removing items and readding

I have 3 SkSpriteNodes: A man a background and a groundType. These are all pngs and sit ontop of each other to create a scene. I add them using addChild by adding the background then the groundType and then the man. This works fine. When the user gets to the end of the screen I erase all except the man and change the background and the groundType. Similar to the way Pitfall works. When I remove them and add them back in they appear in wrong order. I call cleanScreen() then loadBackground() then loadLevelType(). Which you would think would add the background then the level type. What I see instead is leveltype below then background, so you can't see the level type. Even if I switch the order of the functions this still happens. I cannot seems to find anywhere that gives me information on how the ordering of sprites work when removed and added back in. It seems inconsistent. I have thoroughly read the docs for Obj-C and I also tried using insertChild:atIndex but that did not seem to work for me either.
...
cleanScreen()
loadBackground()
loadLevelType()
...
func cleanScreen() {
levelType.removeFromParent()
bg.removeFromParent()
}
func loadBackground() {
var bgNum = currentLevelData["bg"]! as String // resolves to "2"
bg = SKSpriteNode(imageNamed: "bg_\(bgNum)")
bg.name = "bg"
scene.addChild(bg)
}
func loadLevelType() {
var levelTypeInfo = currentLevelData["type"]! as String // resolves to "lake"
levelType = SKSpriteNode(imageNamed: levelTypeInfo)
println(levelTypeInfo)
levelType.name = "level_type"
levelType.position.y = -140
scene.addChild(levelType)
}
When I loop over scene.children the object are in the correct order. I have never played around with zPosition before. The names come back as:
man
bg
level_type
But they appear on screen as man level_type bg. I added opacity to the background and I see now for sure that they are not being rendered in the correct order now that I can see through the background.

Resources