Adding AnchorPoint to SKNode breaks SKScene positioning - ios

I am trying to have my SKCameraNode start in the bottom left corner, and have my background anchored there as well. When I set the anchor point to CGPointZero, here is what my camera shows:
EDIT:
Interestingly, If I set my AnchorPoint to CGPoint(x:0.5, y:0.2), I get it mostly lined up. Does it have to do with the camera scale?
EDIT 2:
If I change my scene size, I can change where the background nodes show up. Usually they appear with their anchor point placed in the center of the screen, which implies the anchorPoint of the scene is in the center of the screen.
I am new to using the SKCameraNode, and so I am probably setting it's constraints incorrectly.
Here are my camera constraints: I don't have my player added yet, but I want to set my world up first before I add my player. Again I am trying to have everything anchored off CGPointZero.
//Camera Settings
func setCameraConstraints() {
guard let camera = camera else { return }
if let player = worldLayer.childNodeWithName("playerNode") as? EntityNode {
let zeroRange = SKRange(constantValue: 0.0)
let playerNode = player
let playerLocationConstraint = SKConstraint.distance(zeroRange, toNode: playerNode)
let scaledSize = CGSize(width: SKMViewSize!.width * camera.xScale, height: SKMViewSize!.height * camera.yScale)
let boardContentRect = worldFrame
let xInset = min((scaledSize.width / 2), boardContentRect.width / 2)
let yInset = min((scaledSize.height / 2), boardContentRect.height / 2)
let insetContentRect = boardContentRect.insetBy(dx: xInset, dy: yInset)
let xRange = SKRange(lowerLimit: insetContentRect.minX, upperLimit: insetContentRect.maxX)
let yRange = SKRange(lowerLimit: insetContentRect.minY, upperLimit: insetContentRect.maxY)
let levelEdgeConstraint = SKConstraint.positionX(xRange, y: yRange)
levelEdgeConstraint.referenceNode = worldLayer
camera.constraints = [playerLocationConstraint, levelEdgeConstraint]
}
}
I have been using a Udemy course to learn the SKCameraNode, and I have been trying to modify it.
Here is where I set the SKMViewSize:
convenience init(screenSize: CGSize, canvasSize: CGSize) {
self.init()
if (screenSize.height < screenSize.width) {
SKMViewSize = screenSize
}
else {
SKMViewSize = CGSize(width: screenSize.height, height: screenSize.width)
}
SKMSceneSize = canvasSize
SKMScale = (SKMViewSize!.height / SKMSceneSize!.height)
let scale:CGFloat = min( SKMSceneSize!.width/SKMViewSize!.width, SKMSceneSize!.height/SKMViewSize!.height )
SKMUIRect = CGRect(x: ((((SKMViewSize!.width * scale) - SKMSceneSize!.width) * 0.5) * -1.0), y: ((((SKMViewSize!.height * scale) - SKMSceneSize!.height) * 0.5) * -1.0), width: SKMViewSize!.width * scale, height: SKMViewSize!.height * scale)
}
How can I get both the camera to be constrained by my world, and have everything anchored to the CGPointZero?

Related

iOS Swift - how to get aspect ratio of local and remote video?

Scenario:
I'm building a WebRTC view inside an app
The container for videos will always have a height of 160.
In the center of the container there should be displayed the remote video with a max height of 160, width should be scaled to respect the aspect ratio of the video. Width also cannot be bigger than the view width, in that case the width will be equal to view width and the height should be adapted to aspect ratio.
In top right corner there should be displayed the local video from front camera with a max width of 100 and the height should be adapted to respect the aspect ratio of local video
my code so far:
func createPeerConnection () {
// some other code
self.localStream = self.factory.mediaStream(withStreamId: "stream")
let videoSource = self.factory.videoSource()
let devices = RTCCameraVideoCapturer.captureDevices()
if let camera = devices.last,
let format = RTCCameraVideoCapturer.supportedFormats(for: camera).last,
let fps = format.videoSupportedFrameRateRanges.first?.maxFrameRate {
let intFps = Int(fps)
self.capturer = RTCCameraVideoCapturer(delegate: videoSource)
self.capturer?.startCapture(with: camera, format: format, fps: intFps)
videoSource.adaptOutputFormat(toWidth: 100, height: 160, fps: Int32(fps))
}
let videoTrack = self.factory.videoTrack(with: videoSource, trackId: "video")
self.localStream.addVideoTrack(videoTrack)
DispatchQueue.main.async {
if self.localView == nil {
let videoView = RTCEAGLVideoView(frame: CGRect(x: self.view.frame.size.width - 105, y: 5, width: 100, height: 160))
videoView.backgroundColor = UIColor.red
self.view.addSubview(videoView)
self.localView = videoView
}
videoTrack.add(self.localView!)
}
}
func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
self.remoteStream = stream
if let videoTrack = stream.videoTracks.first {
DispatchQueue.main.async {
if self.remoteView == nil {
let videoView = RTCEAGLVideoView(frame: CGRect(x: self.view.frame.size.width - 50, y: 0, width: 100, height: 160))
videoView.backgroundColor = UIColor.green
if let local = self.localView {
self.view.insertSubview(videoView, belowSubview: local)
} else {
self.view.addSubview(videoView)
}
self.remoteView = videoView
}
videoTrack.add(self.remoteView!)
}
}
}
I don't know how to get the aspect ratio of either of the videos, local or remote. If i had that, i could compute the appropriate width and heights for each of them
// Edit with solution:
I did not find a way to get the exact size but I found a way to render the video at scale
All I had to do was:
let videoView = RTCEAGLVideoView(frame: CGRect(x: self.view.frame.size.width - 105, y: 5, width: 100, height: 134))
videoView.contentMode = .scaleAspectFill
Now the video scales itself based on the container size
You can use the AVURLAsset and CGSize to get the resolution for video
private func resolutionForLocalVideo(url: URL) -> CGSize? {
guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaTypeVideo).first else { return nil }
let size = track.naturalSize.applying(track.preferredTransform)
return CGSize(width: fabs(size.width), height: fabs(size.height))
}
Now, Use natural size and preferredTransform
var mediaAspectRatio: Double! // <- here the aspect ratio for video with url will be set
func initAspectRatioOfVideo(with fileURL: URL) {
let resolution = resolutionForLocalVideo(url: fileURL)
guard let width = resolution?.width, let height = resolution?.height else {
return
}
mediaAspectRatio = Double(height / width)
}
Also, you can find the scale factor
float xScale = destination.size.width / imageSize.width; //destination is the max image drawing area.
float yScale = destination.size.height / imageSize.height;
float scaleFactor = xScale < yScale ? xScale : yScale;
This can also be achieved by GPUImageMovie, GPUImageCropFilter and GPUImageMovieWriter

issue while dropping pin to imageview on different devices

i am dropping pin image on imageview with coordinates so pin set correctly but there is minor position difference in various devices i am sharing my code below
func convertTapToImg(_ point: CGPoint) -> CGPoint? {
let xRatio = imgView.frame.width / (img?.size.width)!
let yRatio = imgView.frame.height / (img?.size.height)!
let ratio = min(xRatio, yRatio)
let imgWidth = (img?.size.width)! * ratio
let imgHeight = (img?.size.height)! * ratio
var tap = point
var borderWidth: CGFloat = 0
var borderHeight: CGFloat = 0
// detect border
if ratio == yRatio {
// border is left and right
borderWidth = (imgView.frame.size.width - imgWidth) / 2
if point.x < borderWidth || point.x > borderWidth + imgWidth {
return nil
}
tap.x -= borderWidth
} else {
// border is top and bottom
borderHeight = (imgView.frame.size.height - imgHeight) / 2
if point.y < borderHeight || point.y > borderHeight + imgHeight {
return nil
}
tap.y -= borderHeight
}
let xScale = tap.x / (imgView.frame.width - 2 * borderWidth)
let yScale = tap.y / (imgView.frame.height - 2 * borderHeight)
let pixelX = (img?.size.width)! * xScale
let pixelY = (img?.size.height)! * yScale
return CGPoint(x: pixelX, y: pixelY)
}
then with tap-gesture i have fetched one x y coordinates as below
#objc func tapGesture(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: imgView)
let imgPoint = convertTapToImg(point)
print("tap: \(point) -> img \(imgPoint)")
}
then after i am set up pin like below with coordinates
var xCo = 83.0
var yCo = 404.0
let imageView = UIImageView()
imageView.image = #imageLiteral(resourceName: "ic_Pin")
imageView.frame = CGRect(x: xCo - 20, y: yCo - 20, width: 22, height: 22)
imgView.addSubview(imageView)
now i am sharing screen shot of various devices output there is just minor difference with pin position
This is iPhone 5s Screen Shot
This is iPhone 6 Screen Shot
This is iPhone XR Screen Shot
please check screen shot and please help that how to set pin position same on all devices

Spritekit Camera Node scale but pin the bottom of the scene

I have a camera node that is scaled at 1. When I run the game, I want it to scale it down (i.e. zoom out) but keep the "floor" at the bottom. How would I go about pinning the camera node to the bottom of the scene and effectively zooming "up" (difficult to explain). So the bottom of the scene stays at the bottom but the rest zooms out.
I have had a go with SKConstraints but not having any luck (I'm quite new at SpriteKit)
func setConstraints(with scene: SKScene, and frame: CGRect, to node: SKNode?) {
let scaledSize = CGSize(width: scene.size.width * xScale, height: scene.size.height * yScale)
let boardContentRect = frame
let xInset = min((scaledSize.width / 2), boardContentRect.width / 2)
let yInset = min((scaledSize.height / 2), boardContentRect.height / 2)
let insetContentRect = boardContentRect.insetBy(dx: xInset, dy: yInset)
let xRange = SKRange(lowerLimit: insetContentRect.minX, upperLimit: insetContentRect.maxX)
let yRange = SKRange(lowerLimit: insetContentRect.minY, upperLimit: insetContentRect.maxY)
let levelEdgeConstraint = SKConstraint.positionX(xRange, y: yRange)
if let node = node {
let zeroRange = SKRange(constantValue: 0.0)
let positionConstraint = SKConstraint.distance(zeroRange, to: node)
constraints = [positionConstraint, levelEdgeConstraint]
} else {
constraints = [levelEdgeConstraint]
}
}
then calling the function with:
gameCamera.setConstraints(with: self, and: scene!.frame, to: nil)
(This was code from a tutorial I was following) The "setConstraints" function is an extension of SKCameraNode
I'm not sure this will give me the correct output, but when I run the code to scale, it just zooms from the middle and shows the surrounding area of the scene .sks file.
gameCamera.run(SKAction.scale(to: 0.2, duration: 100))
This is the code to scale the gameCamera
EDIT: Answer below is nearly what I was looking for, this is my updated answer:
let scaleTo = 0.2
let duration = 100
let scaleTop = SKAction.customAction(withDuration:duration){
(node, elapsedTime) in
let newScale = 1 - ((elapsedTime/duration) * (1-scaleTo))
let currentScaleY = node.yScale
let currentHeight = node.scene!.size.height * currentScaleY
let newHeight = node.scene!.size.height * newScale
let heightDiff = newHeight - currentHeight
let yOffset = heightDiff / 2
node.setScale(newScale)
node.position.y += yOffset
}
You cannot use a constraint because your scale size is dynamic.
Instead you need to move your camera position to give the illusion it is only scaling in 3 directions.
To do this, I would recommend creating a custom action.
let scaleTo = 2.0
let duration = 1.0
let currentNodeScale = 0.0
let scaleTop = SKCustomAction(withDuration:duration){
(node, elapsedTime) in
if elapsedTime == 0 {currentNodeScale = node.scale}
let newScale = currentNodeScale - ((elapsedTime/duration) * (currentNodeScale-scaleTo))
let currentYScale = node.yScale
let currentHeight = node.scene.size.height * currentYScale
let newHeight = node.scene.size.height * newScale
let heightDiff = newHeight - currentHeight
let yOffset = heightDiff / 2
node.scale(to:newScale)
node.position.y += yOffset
}
What this is doing is comparing the new height of your camera with the old height, and moving it 1/2 the distance.
So if your current height is 1, this means your camera sees [-1/2 to 1/2] on the y axis. If you new scale height is 2, then your camera sees [-1 to 1] on the y axis. We need to move the camera up so that the camera sees [-1/2 to 3/2], meaning we need to add 1/2. So we do 2 - 1, which is 1, then go 1/2 that distance. This makes our yOffset 1/2, which you add to the camera.

how to get multiple spriteNodes to jump when screen is tapped

I have this line of code, to make 3 spriteNodes, i would like them to jump up the screen when I tap anywhere on the screen, but not to follow where the screen is tapped, i can get a single spriteNode to do it, using
applyImpluse in touchBegan, i will be using the accelerometer to move x axis, any help will be greatly appreciated,
let numberOfBalls = 3
let ballWidth = SKSpriteNode(imageNamed: "ball6").size.width
let totalBallWidth = ballWidth * CGFloat(numberOfBalls)
ball6.size = CGSize(width: 50, height: 50)
let xOffset = (CGRectGetWidth(frame) - totalBallWidth)/2
for i in 0..<numberOfBalls{
let ball6 = SKSpriteNode(imageNamed: "ball6")
ball6.position = CGPoint(x: xOffset + CGFloat(CGFloat(i) / 2) * ballWidth, y: CGRectGetHeight(frame) / 2)
self.addChild(ball6)
ball6.physicsBody = SKPhysicsBody(circleOfRadius: ball6.frame.size.width/2)
ball6.physicsBody?.friction = 0.5
ball6.physicsBody?.restitution = 0.8
ball6.physicsBody?.mass = 0.2
ball6.physicsBody?.allowsRotation = true
ball6.physicsBody?.dynamic = true
ball6.physicsBody?.affectedByGravity = true

Spawning a circle in a random spot on screen

I've been racking my brain and searching here and all over to try to find out how to generate a random position on screen to spawn a circle. I'm hoping someone here can help me because I'm completely stumped. Basically, I'm trying to create a shape that always spawns in a random spot on screen when the user touches.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenHeight = screenSize.height
let screenWidth = screenSize.width
let currentBall = SKShapeNode(circleOfRadius: 100)
currentBall.position = CGPointMake(CGFloat(arc4random_uniform(UInt32(Float(screenWidth)))), CGFloat(arc4random_uniform(UInt32(Float(screenHeight)))))
self.removeAllChildren()
self.addChild(currentBall)
}
If you all need more of my code, there really isn't any more. But thank you for whatever help you can give! (Just to reiterate, this code kind of works... But a majority of the spawned balls seem to spawn offscreen)
The problem there is that you scene is bigger than your screen bounds
let viewMidX = view!.bounds.midX
let viewMidY = view!.bounds.midY
print(viewMidX)
print(viewMidY)
let sceneHeight = view!.scene!.frame.height
let sceneWidth = view!.scene!.frame.width
print(sceneWidth)
print(sceneHeight)
let currentBall = SKShapeNode(circleOfRadius: 100)
currentBall.fillColor = .green
let x = view!.scene!.frame.midX - viewMidX + CGFloat(arc4random_uniform(UInt32(viewMidX*2)))
let y = view!.scene!.frame.midY - viewMidY + CGFloat(arc4random_uniform(UInt32(viewMidY*2)))
print(x)
print(y)
currentBall.position = CGPoint(x: x, y: y)
view?.scene?.addChild(currentBall)
self.removeAllChildren()
self.addChild(currentBall)
First: Determine the area that will be valid. It might not be the frame of the superview because perhaps the ball (let's call it ballView) might be cut off. The area will likely be (in pseudocode):
CGSize( Width of the superview - width of ballView , Height of the superview - height of ballView)
Once you have a view of that size, just place it on screen with the origin 0, 0.
Secondly: Now you have a range of valid coordinates. Just use a random function (like the one you are using) to select one of them.
Create a swift file with the following:
extension Int
{
static func random(range: Range<Int>) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}
And now you can specify a random number as follows:
Int.random(1...1000) //generate a random number integer somewhere from 1 to 1000.
You can generate the values for the x and y coordinates now using this function.
Given the following random generators:
public extension CGFloat {
public static var random: CGFloat { return CGFloat(arc4random()) / CGFloat(UInt32.max) }
public static func random(between x: CGFloat, and y: CGFloat) -> CGFloat {
let (start, end) = x < y ? (x, y) : (y, x)
return start + CGFloat.random * (end - start)
}
}
public extension CGRect {
public var randomPoint: CGPoint {
var point = CGPoint()
point.x = CGFloat.random(between: origin.x, and: origin.x + width)
point.y = CGFloat.random(between: origin.y, and: origin.y + height)
return point
}
}
You can paste the following into a playground:
import XCPlayground
import SpriteKit
let view = SKView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
XCPShowView("game", view)
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let wait = SKAction.waitForDuration(0.5)
let popIn = SKAction.scaleTo(1, duration: 0.25)
let popOut = SKAction.scaleTo(0, duration: 0.25)
let remove = SKAction.removeFromParent()
let popInAndOut = SKAction.sequence([popIn, wait, popOut, remove])
let addBall = SKAction.runBlock { [unowned scene] in
let ballRadius: CGFloat = 25
let ball = SKShapeNode(circleOfRadius: ballRadius)
var popInArea = scene.frame
popInArea.inset(dx: ballRadius, dy: ballRadius)
ball.position = popInArea.randomPoint
ball.xScale = 0
ball.yScale = 0
ball.runAction(popInAndOut)
scene.addChild(ball)
}
scene.runAction(SKAction.repeatActionForever(SKAction.sequence([addBall, wait])))
(Just make sure to also paste in the random generators, too, or to copy them to the playground's Sources, as well as to open the assistant editor so you can see the animation.)

Resources