SpriteKit convertPointToView / convertPoint - swift

Question How do I get the purple square to stay anchored at the bottom left of the screen regardless of the camera position?
Details I have an SKScene named colorScene that has a camera node and two shape nodes - one blue and one purple square. The camera is constrained to the blue square which is positioned at 0,0:
Now, say I want to position the purple square at the bottom left of the screen. And I want it to stay there no matter where the camera goes. First, I would make the purple square a child of the camera node. But then what? If I position the purple square at 0,0, strangely, it's in the middle of the screen. (Shouldn't the camera node be the entire screen area?) And if I try convertPoint to go from the camera node to the scene coordinates:
purpleNode?.position = convertPoint(CGPoint(x: 0, y: 0), toNode: cameraNode!)
...things get even more mysterious:

From the Apple docs:
The scene is rendered so that the camera node’s origin is placed in the middle of the scene.
This should explain what you experienced
If I position the purple square at 0,0, strangely, it's in the middle of the screen. (Shouldn't the camera node be the entire screen area?)
Solution
Since we want to place purple on the bottom-left of the screen, we should start with the coordinate related to the view
let bottomLeftOfView = CGPoint(x: 0, y: view.frame.height)
Next we need to convert these coordinate from the view to the scene
purple.position = convertPointFromView(bottomLeftOfView)
This is the full code
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
let camera = SKCameraNode()
self.camera = camera
addChild(camera)
let purple = SKSpriteNode(imageNamed: "square")
purple.color = SKColor.purpleColor()
purple.colorBlendFactor = 1
let bottomLeftOfView = CGPoint(x: 0, y: view.frame.height)
purple.position = convertPointFromView(bottomLeftOfView)
camera.addChild(purple)
}
}
Purple and anchorpoint
Only a quarter of the purple sprite is inside the screen. This because its anchorpoint is (0.5, 0.5): the center. So we just told SpriteKit to place the center of purple to the corner of the screen.
In order to have purple entirely into the screen we just need to tell SpriteKit to use use the bottom left corner of purple as anchorpoint
purple.anchorPoint = CGPointZero
This is the final full code
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
let camera = SKCameraNode()
self.camera = camera
addChild(camera)
let purple = SKSpriteNode(imageNamed: "square")
purple.color = SKColor.purpleColor()
purple.colorBlendFactor = 1
purple.anchorPoint = CGPointZero
let bottomLeftOfView = CGPoint(x: 0, y: view.frame.height)
purple.position = convertPointFromView(bottomLeftOfView)
camera.addChild(purple)
}
}

Related

Aligning ARFaceAnchor with SpriteKit overlay

I'm trying to calculate SpriteKit overlay content position (not just overlaying visual content) over specific geometry points ARFaceGeometry/ARFaceAnchor.
I'm using SCNSceneRenderer.projectPoint from the calculated world coordinate, but the result is y inverted and not aligned to the camera image:
let vertex4 = vector_float4(0, 0, 0, 1)
let modelMatrix = faceAnchor.transform
let world_vertex4 = simd_mul(modelMatrix, vertex4)
let pt3 = SCNVector3(x: Float(world_vertex4.x),
y: Float(world_vertex4.y),
z: Float(world_vertex4.z))
let sprite_pt = renderer.projectPoint(pt3)
// To visualize sprite_pt
let dot = SKSpriteNode(imageNamed: "dot")
dot.size = CGSize(width: 7, height: 7)
dot.position = CGPoint(x: CGFloat(sprite_pt.x),
y: CGFloat(sprite_pt.y))
overlayScene.addChild(dot)
In my experience, the screen coordinates given by ARKit's projectPoint function are directly usable when drawing to, for example, a CALayer. This means they follow iOS coordinates as described here, where the origin is in the upper left and y is inverted.
SpriteKit has its own coordinate system:
The unit coordinate system places the origin at the bottom left corner of the frame and (1,1) at the top right corner of the frame. A sprite’s anchor point defaults to (0.5,0.5), which corresponds to the center of the frame.
Finally, SKNodes are placed in an SKScene which has its origin on the bottom left. You should ensure that your SKScene is the same size as your actual view, or else the origin may not be at the bottom left of the view and thus your positioning of the node from view coordinates my be incorrect. The answer to this question may help, in particular checking the AspectFit or AspectFill of your view to ensure your scene is being scaled down.
The Scene's origin is in the bottom left and depending on your scene size and scaling it may be off screen. This is where 0,0 is. So every child you add will start there and work its way right and up based on position. A SKSpriteNode has its origin in the center.
So the two basic steps to convert from view coordinates and SpriteKit coordinates would be 1) inverting the y-axis so your origin is in the bottom left, and 2) ensuring that your SKScene frame matches your view frame.
I can test this out more fully in a bit and edit if there are any issues
Found the transformation that works using camera.projectPoint instead of the renderer.projectPoint.
To scale the points correctly on the spritekit: set scaleMode=.aspectFill
I updated https://github.com/AnsonT/ARFaceSpriteKitMapping to demo this.
guard let faceAnchor = anchor as? ARFaceAnchor,
let camera = sceneView.session.currentFrame?.camera,
let sie = overlayScene?.size
else { return }
let modelMatrix = faceAnchor.transform
let vertices = faceAnchor.geometry.vertices
for vertex in vertices {
let vertex4 = vector_float4(vertex.x, vertex.y, vertex.z, 1)
let world_vertex4 = simd_mul(modelMatrix, vertex4)
let world_vector3 = simd_float3(x: world_vertex4.x, y: world_vertex4.y, z: world_vertex4.z)
let pt = camera.projectPoint(world_vector3, orientation: .portrait, viewportSize: size)
let dot = SKSpriteNode(imageNamed: "dot")
dot.size = CGSize(width: 7, height: 7)
dot.position = CGPoint(x: CGFloat(pt.x), y: size.height - CGFloat(pt.y))
overlayScene?.addChild(dot)
}

SKSpriteNode Stuck with init(edgeLoopFrom:) - SKPhysicsBody

I have a separate class called "Floor" with below.
class Floor: SKNode {
override init() {
super.init()
//let edgeFrame = CGRect(origin: CGPoint(x: 1,y: 1), size: CGSize(width: 1078, height: 1950))
//self.physicsBody = SKPhysicsBody(edgeLoopFrom: edgeFrame)
let borderBody = SKPhysicsBody(edgeLoopFrom: self.frame)
borderBody.friction = 0
self.physicsBody = borderBody
// Apply a physics body to the node
// self.physicsBody?.isDynamic = false
// Set the bit mask properties
self.physicsBody?.categoryBitMask = floorCategory
self.physicsBody?.contactTestBitMask = nailDropCategory
self.physicsBody?.collisionBitMask = balloonCategory
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemted")
}
}
I basically have falling SKSpriteNode's that start at the top of the screen and goes to the bottom where it touches the "Floor" and removes itself then restarts from top again. Issue I'm having is all my SKSpriteNode keeps getting stuck on top and not falling through the border around the frame of the screen. How can I tell my application to ignore those specific nodes and let them in? Appreciate any help!
Here is the object that is moving left and right on the screen but it just falls off the side of the screen without the edgeLoop
if let accelerometerData = motionManager.accelerometerData {
if UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft {
balloon.physicsBody?.velocity = CGVector(dx: accelerometerData.acceleration.x * -500.0, dy: 0)
} else {
balloon.physicsBody?.velocity = CGVector(dx: accelerometerData.acceleration.x * 500.0, dy: 0)
}
}
}
What is happening is that your objects are hitting the top boundary of the edge loop and not being able to make it into the scene.
there is several ways you can do you this, if you absolutely needed the edge loop on the sides I would suggest elongating the loop to be higher than the scene and creating the objects inside the loop but above the visible area. However since you haven't given any indication that you actually need the loop on the sides all I would do is get rid of the edge loop detection.
create a box that is the width of the scene and say a 100px high then put a physics body on it of type floorCategory. then put this box 50 px below the bottom of the screen. Assuming that your floor sprite box has an anchorPoint of 0.5, 0.5 this will hide the box below the screen and the top of the box will sit flush with the bottom of the screen.
Now you will be able to detect when your objects hit the bottom of the screen and you will no longer have to worry about them passing through the edge loop at the top.
OR
an example of elongating the loop would be...
You create a rectangle taller than the screen (green border in the image) to apply the edge loop to, not to the screen size itself

Change an SKScene centerpoint

I am working with a simple 2d game and I am trying to switch from level 1 (scene one) to level 2 (scene two). For the sake of testing, both levels contain the same content. This is my code for transitioning:
let transition = SKTransition.push(with: SKTransitionDirection.left, duration: 1)
let nextScene = Scene(size: levelBuilder.scene.size)
nextScene.scaleMode = .aspectFill
nextScene.backgroundColor = UIColor(red:0.17, green:0.24, blue:0.31, alpha:1.0)
levelBuilder.scene.view?.presentScene(nextScene, transition: transition)
nextScene.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
nextScene.physicsWorld.contactDelegate = levelBuilder.scene.physicsWorld.contactDelegate
loadLevelContents(level: 2, world: 1, borderPercentage: 30)
This works perfectly, but the issue is not the transitioning but the loading of nodes into scene two which happens here:
loadLevelContents(level: 2, world: 1, borderPercentage: 30)
The positioning goes fine, but the problem is that everything is based according to the center point, or relative (0,0), being in the middle of the screen.
Scene 2 has the center point at the lower left corner
While I need it to be like scene 1, where it is in the middle of the screen:
How can I change this relative point or center point, not sure what it is called, to the middle of the screen for all SKScenes
The point that you are looking for is called anchorPoint. In Spritekit lower left is 0,0 and upper right is 1, 1. So to put it in the center you would have to set and achorPoint of 0.5,0.5 in the didMove func
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
For SKScenes the default value is (0,0), but it's not just SKScenes that have anchorPoints, SKSpriteNodes have it as well.

SpriteKit - Nodes not adding to SKCameraNode - Swift

Im trying to pin the game pad controller to the bottom left on my camera node but when i add the node as a child of my camera it doesnt show up?
let gameCamera = SKCameraNode()
var joypadBackground : SKSpriteNode = SKSpriteNode(imageNamed: "a")
override func didMove(to view: SKView) {
//Set game camera
self.camera = gameCamera
joypadBackground.position = convert(CGPoint(x: 0, y: 0), to: gameCamera)
joypadBackground.size = CGSize(width: 50, height: 50)
joypadBackground.zPosition = 1000
gameCamera.addChild(joypadBackground)
}
I had a hard time with this same problem the first time I was working with SKCameraNode and creating a heads up display.
Basically you have to remember that there are two parts to the camera. Running its functionality and rendering its children. By setting the scene's camera to gameCamera you've setup the functionality, but your camera isn't in the node tree for rendering. So, if you ever have a camera that needs to render its children don't forget to add it to the scene as a child, then the camera's children will be displayed.
self.camera = gameCamera
self.addChild(gameCamera)
Hope that helps someone avoid a very common error with a very simple solution.
You don't need
convert(CGPoint(x: 0, y: 0), to: gameCamera)
You can just set the CGPoint position to (0,0) and it should be at that point relative to the camera's space.
Not sure if this helps, at all, but what I do is (generally) position a child node AFTER I've added it to its parent. This is mainly a mental reminder, to me, that the child's position is within the coordinate space of the parent. So I'd do something like this:
gameCamera.addChild(joypadBackground)
joypadBackground.position = CGPoint(x: 0, y: 0)
If you're using a mid screen origin in your SKScene, this should be in the middle of the screen.
Bottom left will be a negative x and negative y value, size of which is relative to your frame size.

Background image in gamescene.swift

What do I need to code in order to have an image (that is already in the assets.xcassets) displayed as the background of the GameScene.swift?
First of all you could call you scene with the scaleMode .resizeFill that modify the SKScene's actual size to exactly match the SKView :
scene.scaleMode = .resizeFill
.resizeFill – The scene is not scaled. It is simply resized so that its fits the view. Because the scene is not scaled, the images will all remain at their original size and aspect ratio. The content will all remain relative to the scene origin (lower left).
By default, a scene’s origin is placed in the lower-left corner of the view. So, a scene is initialized with a height of 1024 and a width of 768, has the origin (0,0) in the lower-left corner, and the (1024,768) coordinate in the upper-right corner. The frame property holds (0,0)-(1024,768).The default value for the anchor point is CGPointZero (so you don't need to change it), which places it at the lower-left corner.
Finally, you can use the code below to add your background image (called of example bg.jpg):
// Set background
let txt = SKTexture(imageNamed: "bg.jpg")
let backgroundNode = SKSpriteNode(texture: txt, size:size)
self.addChild(backgroundNode)
backgroundNode.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
Although this might not be the best way, but it's what I always do, and it works.
Assuming that you have an image that is exactly the same aize as your scene, you can do this:
// please declare bg as a class level variable
bg = SKSpriteNode(imageNamed: "name of your texture")
// the below two lines of code is my preference only. I want the
// background's anchor point to be the bottom left of the screen
// because IMO it's easier to add other sprites as children of the background.
bg.anchorPoint = CGPoint.zero
bg.position = CGPoint(x: self.frame.width / -2, y: self.frame.height / -2)
self.addChild(bg)
Alernatively, just do this in an sks file. It's much easier.
After that, add all your game sprites as children of bg instead of self because it is easier to manage.