SKScene: find max position of SKSpriteNode - ios

I'm writing an App for iPad and iOS 10.
My App uses only one SKScene (size is 1300 by 1000).
I also use one camera defined like this:
myCamera=[[SKCameraNode alloc] init];
myCamera.position=CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
self.camera=myCamera;
[self addChild:myCamera];
I use many SKSpriteNodes that the user can drag around (even beyond the size of the SKScene).
I also use:
[myCamera setScale:newScale];
to deal with pinch to zoom.
I need to know if there is a way to get the max X position of the node that is most distant from position 0,0 in the SKScene on the right hand side without having to read all node positions one by one.
And then do the same for the other three sides (up, left, down).

Keep four floats minX, minY, maxX, maxY. While the user is dragging a node, check if its position exceeds either max or is less than either min. If so, update the corresponding value, e.g.
EDIT use calculateAccumulatedFrame to get the full extent of the node's descendants.
// assuming you have code like this already
skNodeThatIAmDragging.position = currentDragPosition;
CGRect frame = [skNodeThatIAmDragging calculateAccumulatedFrame];
if (self.maxX < frame.origin.x) {
self.maxX = frame.origin.x;
}
if (self.minX > CGRectGetMaxX(frame)) {
self.minX = CGRectGetMaxX(frame);
}
// ... and so on

Related

iOS - How to resize elements on a screen depending on the amount of the elements

So I am developing a game using Spritekit that uses a pyramid of Sprites (let's say circles for a simple instance). The user can choose the amount of rows of sprites they would like to have in the game. The sprites are to form a pyramid, so if you have 1 row, you have 1 sprite node. It increases by 2 the farther down you go (the more rows you choose) - creating the pyramid shape. So if a user picked 3 rows, the game board would look like this:
O
O O O
O O O O O
However, when it gets to 5 rows, it loses its pyramid shape because the screen is only so wide and it has to fit all the elements onto the screen (elements are more smushed together in rows further down).
My question is, to fix this issue, what would I have to do to make the pyramid resize and change its spacing between elements depending on how many rows are chosen? Would I have to multiply the spacing by a certain factor? I have also heard of people adding layers onto the screen - maybe drawing the sprites in some sort of container so that it always resizes the pyramid to fit the screen without skewing the pyramid shape?
Your idea is correct! Make a SKNode container, then update it's .size property, or do .setScale.
(not at xcode right now, pardon if not 100%)
// Say that our scene's size is 400x400:
let bkg = SKShapeNode(rectangleOfSize: self.size)
bkg.addChild(firstSprite)
bkg.addChild(secondSprite) // And so on...
// Find the farthest point in bkg:
var farthestX = CGFloat(0)
for node in bkg.children {
if node.position.x + node.frame.size.width / 2 > farthestX {
farthestX = node.position.x + node.frame.size.width / 2
}
}
// Do math to resize the bkg:
if self.size.width < farthestX {
let scaler = self.size.width / farthestX
bkg.setScale(scaler)
}
This should work, or at least the general idea should work... You would want to check for Y values and Height as well.
You can easily compute a symmetrical size proportional to the number of rows and resize your sprites accordingly. This is my idea in pseudocode:
let computedSize = deviceWidth/(2*(rows-1) + 1)
for sprite in sprites {
sprite.size.width = computedSize
sprite.size.height = computedSize
}

Swift: Positioning Children of the SKCameraNode

Context:
there is a cursor (like your mouse) SKSpriteNode
cam is a SKCameraNode and is a child to the cursor (i.e. wherever your cursor goes, so follows the camera).
cam is purposely not centered on the cursor; rather, it is offset so the cursor appears at the top of the view, and there remains empty space below
A simple schematic is given below
Goal:
The goal is two add to sprites to the lower left and lower right corners of the camera's view. The sprites will be children of the camera, so that they always stay in view.
Question
How can I position a sprite in the corner of a camera, especially given that the SKSpriteNode does not have an anchorPoint attribute (as an SKSpriteNode typically has, which let me offset the camera as a child to the cursor)?
Note: One can position the SKSpriteNodes on the GameScene and then call .move(toParent: SKNode), which gets you closers but also messes with the position and scale of the SKSpriteNodes
var cam: SKCameraNode!
let cursor = SKSpriteNode(imageNamed: "cursor")
override func didMove(to view: SKView) {
// Set up the cursor
cursor.setScale(spriteScale)
cursor.position = CGPoint(x: self.frame.midX, y: raisedPositioning)
cursor.anchorPoint = CGPoint(x:0.5, y:0.5)
cursor.zPosition = CGFloat(10)
addChild(cursor)
// Set up the camera
cam = SKCameraNode()
self.camera = cam
cam.setScale(15.0)
// Camera is child of Cursor so that the camera follows the cursor
cam.position = CGPoint(x: cursor.size.width/2, y: -(cursor.size.height * 4))
cursor.addChild(cam)
// Add another sprite here and make it child to cursor
...
the cameraNode has no size, but you can get the current screen size with the frame property
frame.size
then you can position your node accordingly, for example if you want to position the center of yournode in the left corner you set the position as this:
yournode.position.x = 0
yournode.position.y = frame.size.height
This is best solved with a "dummy node" that acts as the camera's screen space coordinates system.
Place this dummy node at the exact centre of the view of the camera, at a zPosition you're happy with, as a child of the camera.
...from SKCameraNode docs page:
The scene is rendered so that the camera node’s origin is placed in
the middle of the scene.
Attach all the HUD elements and other pieces of graphics and objects you want to stay in place, relative to the camera, to this dummy object, in a coordinate system that makes sense relative to the camera's "angle of view", which is its frame of view.
...from a little further down the SKCameraNode docs page:
The camera’s viewport is the same size as the scene’s viewport
(determined by the scene’s size property) and the scene is still
scaled by its scaleMode property when it is rendered into the view.
Whenever the camera moves, it moves the dummy object, and all the children of the dummy object move with the dummy object.
The biggest advantage of this approach is that you can shake or otherwise move the dummy object to create visual effects indicative of motion and explosions. But also a neat system for removal from view, too.

Swift sprite kit vertical background infinite image

I have 3 images:
topBg.png
midBg.png
botBg.png
I want to set topBg.png at top scene and height = 200
middleBg.png should be infinite scale or repeat vertically
botBg.png - should be in bottom and height = 200
i have next code:
override func didMove(to view: SKView) {
self.bgTopSpriteNode = self.childNode(withName: "//bgTopNode") as? SKSpriteNode
self.bgMiddleSpriteNode = self.childNode(withName: "//bgMiddleNode") as? SKSpriteNode
self.bgBottomSpriteNode = self.childNode(withName: "//bgBottomNode") as? SKSpriteNode
if let bgTopSpriteNode = self.bgTopSpriteNode,
let bgMiddleSpriteNode = self.bgMiddleSpriteNode,
let bgBottomSpriteNode = self.bgBottomSpriteNode {
bgTopSpriteNode.size.width = self.frame.width
bgTopSpriteNode.size.height = 200
bgTopSpriteNode.position.x = 0
bgMiddleSpriteNode.size.width = self.frame.width
bgMiddleSpriteNode.size.height = self.frame.height-400
bgMiddleSpriteNode.position.x = 0
bgBottomSpriteNode.size.width = self.frame.width
bgBottomSpriteNode.size.height = 200
bgBottomSpriteNode.position.x = 0
}
}
But how to set Y position of images. Because coordinates begin from center of screen, not from left top and i don't know how to convert them.
There are a couple of different ways to achieve what you're looking to do.
First, you can compute the y position of the top and the bottom of the screen using simply size.height / 2 if you have the anchorPoint of your scene at (0.5,0.5). (Don't use frame - use size. That way, you take into account the scaleMode of the scene.)
It sounds like you are frustrated that the origin of the scene is in the center. If you'd like to move it to the corner, you can easily do so by setting the scene's anchorPoint property, say, to (0.0, 0.0) for the lower left corner. Then, your y-values are 0 and size.height. If you are using the .sks editor, this is exposed in the interface - you can just set it there. Otherwise, you can set it programmatically.
Finally, you can set the scaleMode of your scene to something like .aspectFill, set the size of the scene directly (say, to 1024x768 for an iPad), and just place the images wherever they need to go. This approach works particularly well with .sks files, if you are using them; when you load up a scene, you can set the size of the scene based on the aspect ratio of the view it's in to accommodate different aspect ratios. For instance, you could adopt a 320x480 "reference size" for your iPhone scenes. Whenever you load up the scene, you could set the size of the scene to be 320 points wide and however many points tall to match the aspect ratio of the device. Then, all your graphics would be produced at 320pt wide, and you could slide them up or down proportionally across the scene's size for layout. This is a little more complicated, but it's a lot easier than trying to deal with separate layout considerations for multiple devices.
I should also point out a couple of things.
You can use the anchorPoint property of a sprite to dictate where the sprite's coordinates are measured from. This is handy for cases where you want images to be flush up against something. For instance, if you want an image flush against the left side of the screen, set its position to be exactly the left side of the screen, and then set its anchorPoint.x to 0.0; this will put the left edge of the sprite against the left edge of the screen. This also works for scenes, as you encountered - moving the anchorPoint of the scene moves everything in the scene relative to its size.
You don't need three images for what you're describing. You can use a single sprite and just set its centerRect property to tell it to use the top and bottom of an image and stretch the center part vertically. You have to do a little math to set the right xScale and yScale (not width and height, IIRC), but then you can draw all of that with one sprite instead of three. This would be really handy in your case, because you could just leave the sprite at (0,0), set its scale to match the size of the entire scene, and set the centerRect property - you wouldn't have to do any positioning math at all.

Check if node is visible on the screen

I currently have a large map that goes off the screen, because of this its coordinate system is very different from my other nodes. This has led me to a problem, because I'm needing to generate a random CGPoint within the bounds of this map, and then if that point is frame/on-screen I place a visible node there. However the check on wether or not the node is on screen continuously fails.
I'm checking if the node is in frame with the following code: CGRectContainsPoint(self.frame, values) (With values being the random CGPoint I generated). Now this is where my problem comes in, the coordinate system of the frame is completely different from the coordinate system of the map.
For example, in the picture below the ball with the arrows pointing to it is at coordinates (479, 402) in the scene's coordinates, but they are actually at (9691, 9753) in the map's coordinates.
I determined the coordinates using the touchesBegan event for those who are wondering. So basically, how do I convert that map coordinate system to one that will work for the frame?
Because as seen below, the dot is obviously in the frame however the CGRectContainsPoint always fails. I've tried doing scene.convertPoint(position, fromNode: map) but it didn't work.
Edit: (to clarify some things)
My view hierarchy looks something like this:
The map node goes off screen and is about 10,000x10,000 for size. (I have it as a scrolling type map). The origin (Or 0,0) for this node is in the bottom left corner, where the map starts, meaning the origin is offscreen. In the picture above, I'm near the top right part of the map. I'm generating a random CGPoint with the following code (Passing it the maps frame) as an extension to CGPoint:
static func randPoint(within: CGRect) -> CGPoint {
var point = within.origin
point.x += CGFloat(arc4random() % UInt32(within.size.width))
point.y += CGFloat(arc4random() % UInt32(within.size.height))
return point;
}
I then have the following code (Called in didMoveToView, note that I'm applying this to nodes I'm generating - I just left that code out). Where values is the random position.
let values = CGPoint.randPoint(map.totalFrame)
if !CGRectContainsPoint(self.frame, convertPointToView(scene!.convertPoint(values, fromNode: map))) {
color = UIColor.clearColor()
}
To make nodes that are off screen be invisible. (Since the user can scroll the map background). This always passes as true, making all nodes invisible, even though nodes are indeed within the frame (As seen in the picture above, where I commented out the clear color code).
If I understand your question correctly, you have an SKScene that contains an SKSpriteNode that is larger than the scene's view, and that you are randomly generating coordinates within that sprite's coordinate system that you want to map to the view.
You're on the right track with SKNode's convertPoint(_:fromNode:) (where your scene is the SKNode and your map is the fromNode). That should get you from the generated map coordinate to the scene coordinate. Next, convert that coordinate to the view's coordinate system using your scene's convertPointToView(_:). The point will be out of bounds if it is not in view.
Using a worldNode which includes a playerNode and having the camera center on this node, you can check on/off with this code:
float left = player.position.x - 700;
float right = player.position.x + 700;
float up = player.position.y + 450;
float down = player.position.y - 450;
if((object.position.x > left) && (object.position.x < right) && (object.position.y > down) && (object.position.y < up)) {
if((object.parent == nil) && (object.dead == false)) {
[worldNode addChild:object];
}
} else {
if(object.parent != nil) {
[object removeFromParent];
}
}
The numbers I used above are static. You can also make them dynamic:
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenRect.size.width;
CGFloat screenHeight = screenRect.size.height;
Diving the screenWidth by 2 for left and right. Same for screenHeight.

Confusion about coordinates, frames & child nodes in SpriteKit on iOS?

I'm still playing around with learning SpriteKit in iOS & have been doing lots of reading & lots of experimenting. I'm confused by something else I've found* regarding coordinates, frames & child nodes.
Consider this snippet of code, in which I'm trying to draw a green box around my spaceship sprite for debugging purposes:
func addSpaceship()
{
let spaceship = SKSpriteNode.init(imageNamed: "rocketship.png")
spaceship.name = "spaceship"
// VERSION 1
spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
let debugFrame = SKShapeNode(rect: spaceship.frame)
debugFrame.strokeColor = SKColor.greenColor()
spaceship.addChild(debugFrame)
// VERSION 2
// spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
spaceship.setScale(0.50)
self.addChild(spaceship)
}
If I add the set the spaceship sprite with the line marked "VERSION 1" above, I get this:
which is clearly wrong. But if I comment out the line marked "VERSION 1" above, and instead use the line marked "VERSION 2", I get what I want:
Notice that the actual code for the lines marked Version 1 & Version 2 is identical:
spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
So why does it matter when I set the position of the spaceship sprite?
To my way of thinking, the position of the spaceship sprite is irrelevant to the placement of the the debugFrame, because the debugFrame is a child of the spaceship sprite & thus, it's coordinates should be relative to the spaceship sprite's frame - right?
Thanks
WM
*This is somewhat related to a question I asked yesterday:
In SpriteKit on iOS, scaling a textured sprite produces an incorrect frame?
but a) I understand that one now, and b) this is a different enough that it deserves its own question.
UPDATE:
Hmmm - thanks, guys for the ideas below, but I still don't get it & maybe this will help.
I modified my code to print out the relavant positions & frames:
func addSpaceship()
{
let spaceship = SKSpriteNode.init(imageNamed: "rocketship.png")
spaceship.name = "spaceship"
println("Spaceship0 Pos \(spaceship.position) Frame = \(spaceship.frame)")
// VERSION 1 (WRONG)
spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
println("Spaceship1 Pos \(spaceship.position) Frame = \(spaceship.frame)")
let debugFrame = SKShapeNode(rect: spaceship.frame)
println("DEBUG Pos \(debugFrame.position) Frame = \(debugFrame.frame)")
debugFrame.strokeColor = SKColor.greenColor()
spaceship.addChild(debugFrame)
// VERSION 2 (RIGHT)
// spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
// println("Spaceship2 Pos \(spaceship.position) Frame = \(spaceship.frame)")
spaceship.setScale(0.50)
self.addChild(spaceship)
}
Then, I ran it both ways got these results. Since I understand Version 2, let's start there.
Running with the "VERSION 2 (RIGHT)" code, I got:
Spaceship0 Pos (0.0,0.0) Frame = (-159.0,-300.0,318.0,600.0)
DEBUG Pos (0.0,0.0) Frame = (-159.5,-300.5,319.0,601.0)
Spaceship2 Pos (384.0,512.0) Frame = (225.0,212.0,318.0,600.0)
The spaceship node starts, by default, with its position at the bottom left of the screen (Spaceship0). Its frame is also expressed in terms of its anchor point (center) being set in the bottom left of the screen, hence the negative numbers for the origin of its frame rect.
The debug frame is then created with its position set to 0, 0 by default & its frame set to be the same as the spaceship's.
The code (Spaceship2) then moves the spaceship node to a position in the view's coordinates (384.0,512.0), and its frame's origin is moved by adding the new position to the old origin (i.e. 384 + -159 = 225).
All is well.
Unfortunately, I still don't get Version 1.
When I run with the "VERSION 1 (WRONG)," code, I get
Spaceship0 Pos (0.0,0.0) Frame = (-159.0,-300.0,318.0,600.0)
Spaceship1 Pos (384.0,512.0) Frame = (225.0,212.0,318.0,600.0)
DEBUG Pos (0.0,0.0) Frame = (0.0,0.0,543.5,812.5)
As above, the spaceship node starts, by default, with its position at the bottom left of the screen (Spaceship0). Its frame is also expressed in terms of its anchor point (center) being set in the bottom left of the screen, hence the negative numbers for the origin of its frame rect.
The code (Spaceship1) then moves the spaceship node to a position in the view's coordinates (384.0,512.0), and its frame's origin is moved by adding the new position to the old origin (i.e. 384 + -159 = 225).
The debug frame is then created with its position set to 0, 0 by default & its frame set to have a strange width (543.5) & a strange height (812.5). Since I'm initializing the debugFrame.frame with spaceship.frame (i think that's what the default initializer does), I would expect the new debugFrame.frame to be the same as the spaceship's frame - but it isn't! The debug frame width & height values apparently come from adding the actual width & height to the origin of the spaceship node frame (543.5 = 225 + 318.5). But if that is the case, why is t's frame rect origin still 0, 0 & not the same adding (225.0 + 0 = 225.0)???
I don't get it.
You are creating the shape node using the sprite's frame, which is in scene coordinates. Because the shape will be a child of the sprite, you should create the node in the sprite's coordinates. For example, if you create the SKShapeNode with spaceship's size
let debugFrame = SKShapeNode(rectOfSize: spaceship.size)
instead of using the spaceship's frame, debugFrame will be centered on the spaceship regardless of when you set the ship's position. Also, debugFrame will scale/move appropriately with the ship.
In response to your 'I don't get it'.
Your code is ok but has a logical problem. The 'frame' is counted relative to the parent coordinates. Please realize that the parent of the spaceship and the parent of the debug window are different in your code.
Two ways to resolve your problem:
Add a zero offset for the debug window and use the spaceship as the parent.
The advantage of this is that the debug window will move, scale with the spaceship:
let rectDebug = CGRectMake( 0, 0, spaceship.frame.size.width, spaceship.frame.size.height)
let debugFrame = SKShapeNode(rect:rectDebug)
spaceship.addChild(debugFrame)
Add the debug window with the spaceship 'frame' coordinates to the parent of the spaceship (which is 'self'). The disadvantage of this, is that you have to move, scale the debug window yourself in the code, since it will not be attached to the spaceship.:
let debugFrame = SKShapeNode(rect:spaceship.frame)
self.addChild(debugFrame)
Both solutions are widely used. You should chose whichever is better for you in your case.
Three other problems might come up:
1.There might be code errors in my code, I just typed these into the web window directly without xcode syntax checking.
2.The anchor points of the two objects could be different. So you might need alignment in your code for this.
3.The zPosition of the objects should also be taken into consideration, so these objects will not be hidden under some other objects.
In response to the problem you are trying to solve, perhaps showPhysics would help:
skView.showsFPS = YES;
skView.showsNodeCount = YES;
skView.showsPhysics = YES;
This is almost the same problem as in the other question you mentioned. frame is a property that contains a position and a size. Both of them are subject to scaling of their ancestor node. Read section "A Node Applies Many of Its Properties to Its Descendants" in https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Nodes/Nodes.html#//apple_ref/doc/uid/TP40013043-CH3-SW13
Again : never apply scaling to a node, never move a node before having fully constructed its hierarchy, except if you want some special or weird effect.

Resources