SpriteKit addChild order is different after removing items and readding - ios

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.

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.

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.

Swift and spritekit, performance

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)

Loop activated on mouse movement

I'm just going to explain the context so it is clearer.
I made this menu : my menu
I am looking to make an improved and more advanced version of the same menu.
I made an animation of waves on the cofee's surface and am looking to make it loop when the mouse is moving and to stop looping when it's not.
Sorry for the lack of specifications as I am quite new to actionscript, but I hope somebody will be able to help me. :)
Thanks,
Mathieu
Well, you said it - leverage MouseEvent.MOUSE_MOVE to set a conditional in your looping routine.
private var _isMoving:Boolean = false;
stage.addEventListener(MouseEvent.MOUSE_MOVE, checkMouse);
this.addEventListener(Event.ENTER_FRAME, doLoop);
private function checkMouse(e:MouseEvent):void
{
_isMoving = true;
}
private function doLoop(e:Event):void
{
trace("moving =" + _isMoving);
if(_isMoving)
{
// loop animation
}
_isMoving = false;
}
depending on how you want it to work I would do this as follows:
create an animation of wavy coffee
ensure the animation loops
note that clips loop by default, so all you have to do is match the first and last frames!
place the clip at the edge of your current coffee graphic
double click on the graphic to edit it
drag an instance of the looping animation from the library onto the "edge" of the graphic
OR just replace your entire light brown graphic with an animated one that loops
when the mouse is moving, call play on the animated loop clip
when the mouse stops, call stop on the animated loop clip
Some example code would be along the lines of:
public function init():void {
menuClip.addEventListener(MouseEvent.MOUSE_OVER, onMenuRollOver);
menuClip.addEventListener(MouseEvent.MOUSE_OUT, onMenuRollOut);
}
public function onMenuRollOver(event:MouseEvent):void {
stage.addEventListener(MouseEvent.MOUSE_MOVE, onMove);
/* do the stuff you're currently doing to animate the clip here.
something like: coffee graphic height = ease to mouseHeight */
}
public function onMenuRollOut(event:MouseEvent):void {
/* do the stuff you're currently doing to stop the clip here. */
stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMove);
coffeeClip.stop();
}
public function onMove(event:MouseEvent):void {
resetTimer();
coffeeClip.play(); //note: play has no effect when movie is playing (that's ideal in this case)
}
public function resetTimer():void {
if(mouseMovementTimer == null) createTimer();
mouseMovementTimer.reset();
mouseMovementTimer.start();
}
public function createTimer():Timer {
mouseMovementTimer = new Timer(DELAY, 1); //fiddle with the delay variable. Try 500, at first
mouseMovementTimer.addEventListener(TimerEvent.TIMER, stopAnimationLoop);
}
public function stopAnimationLoop(event:TimerEvent):void {
mouseMovementTimer.removeEventListener(TimerEvent.TIMER, stopAnimationLoop); //optional but recommended
mouseMovementTimer = null;
coffeClip.stop();
}
Of course, you would need to do things like call init() and import flash.utils.Timer and initialize variables like mouseMovementTimer, menuClip, coffeeClip and DELAY.
Warning: This code is off the top of my head and untested. So there's likely to be small bugs in it but you should get the general idea:
add a mouse listener when the user mouses over the menu
remove that listener if the user mouses out of the menu
have that listener play the looping movie clip
trigger an event that will stop the looping clip if movement hasn't been detected in a while
once the trigger goes of, stop the clip
The key is in detecting when the mouse stops moving. Flash detects interaction well but does not detect NON-INTERACTION for obvious reasons. One easy way to solve that is to trigger a timer that will go off once too much time has elapsed since the last activity. Then, when the timer triggers, you know action has stopped!
I think that's the key piece to solving your problem. I hope that helps someone in some way.
~gmale

Resources