Spritekit Camera Node scale but pin the bottom of the scene - ios

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.

Related

Trying to animate a SKNode inside a UIView

I have programmatically created a SKView inside of my UIView. Now I want to animate a new SKNodes in all directions, starting in the middle (this works) depending on the view that is passed through. All works fine except for the fact that the ending position of the SKNode is weird. Instead of shooting in all directions, it is going inside of a corner, way out of the view's boundaries. It should never go out of the view's boundaries. I am converting the CGPoint to my scene from the View.
This is my code:
func animateExplosion(sender: UIButton){
let star = SKSpriteNode(imageNamed: "ExplodingStar")
let starHeight = sender.frame.height / 3
star.size = CGSize(width: starHeight, height: starHeight)
var position = sender.frame.origin
position = self.sceneScene.convertPoint(fromView: position)
star.position = position
let minimumDuration = 1
let maximumDuration = 2
let randomDuration = TimeInterval(RandomInt(min: minimumDuration * 100, max: maximumDuration * 100) / 100)
let fireAtWill = SKAction.move(to: getRandomPosition(view: sender), duration: randomDuration)
let rotation = SKAction.rotate(byAngle: CGFloat(randomAngle()), duration: Double(randomDuration))
let fadeOut = SKAction.fadeOut(withDuration: randomDuration)
let scaleTo = SKAction.scale(to: starHeight * 2, duration: randomDuration)
let group = SKAction.group([fireAtWill, rotation]) //for testing no fade out or remove parent
let sequence = SKAction.sequence([group])
if randomAmountOfExplodingStars > 0{
randomAmountOfExplodingStars -= 1
sceneScene.addChild(star)
star.run(sequence)
animateExplosion(sender: sender)
}
}
the getRandomPosition where the bug properly is:
func getRandomPosition(view: UIView) -> CGPoint{
let direction = RandomInt(min: 1, max: 4)
var randomX = Int()
var randomY = Int()
if direction == 1{
randomX = Int(view.frame.width / 2)
randomY = RandomInt(min: -Int(view.frame.height / 2), max: Int(view.frame.height / 2))
}
if direction == 2{
randomX = RandomInt(min: -Int(view.frame.width / 2), max: Int(view.frame.width / 2))
randomY = Int(view.frame.height / 2)
}
if direction == 3{
randomX = -Int(view.frame.width / 2)
randomY = RandomInt(min: -Int(view.frame.height / 2), max: Int(view.frame.height / 2))
}
if direction == 4{
randomX = RandomInt(min: -Int(view.frame.width / 2), max: Int(view.frame.width / 2))
randomY = -Int(view.frame.height / 2)
}
var randomPosition = CGPoint(x: randomX, y: randomY)
//randomPosition = self.sceneScene.convertPoint(fromView: randomPosition)
return randomPosition
}
I know that code looks awful, but it should do the trick right? The passed through view is a UIButton inside of a UIView. The SKView shares exactly the same constrains as that UIView. The animation should start in the middle and end somewhere to the boundaries of the passed view.
Yay got it to work finally. So dumb I did not notice before. The sender would be always a child inside of a UIView which is smaller. The return function was great, but the SKNode should not move to the returned function, but moved by.
Updated code:
let randomPositionOfSender = getRandomPosition(view: sender)
let fireAtWill = SKAction.moveBy(x: randomPositionOfSender.x, y: randomPositionOfSender.y, duration: randomDuration)

Adding AnchorPoint to SKNode breaks SKScene positioning

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?

How do I add a button at every x degrees? [duplicate]

I have an array of buttons and when I append them to a view I want the to be positioned around a image view which is in the center. Based on how many objects there are in the array, I want them to be evenly spaced around the whole circle. Below is my attempt to do so. What am I doing wrong and how should I fix it? There is more than one button behind the moose.
var userbutton = [UIButton]()
var upimage = [UIImage]()
var locationpic = [AnyObject]()
func locationsSet(){
for (index, users) in upimage.enumerate() {
let userbutton = UIButton()
userbutton.addTarget(self, action: "buttonAction:", forControlEvents: .TouchUpInside)
userbutton.frame = CGRectMake(100, 100, 50, 50)
userbutton.layer.cornerRadius = userbutton.frame.size.width/2
userbutton.clipsToBounds = true
userbutton.setImage(users, forState: .Normal)
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * (radius + 40)
let pointy = (centery) + (sin(radians)) * (radius + 40)
userbutton.center.x = pointx
userbutton.center.y = pointy
self.userbutton.append(userbutton)
self.view.addSubview(userbutton)
print("x\(pointx)")
print("y\(pointy)")
}
}
How I would do this:
Create an extension to UIView to get the diagonal and radius. These are handy because we want our "satellites" to have predictable placing even when the "planet" isn't square.
extension UIView {
var diagonal : CGFloat {
return sqrt(pow(self.frame.width, 2) + pow(self.frame.height, 2))
}
var radius : CGFloat {
return diagonal / 2
}
}
This will return a point based on an angle and a distance from an origin.
It uses dreadful trigonometry.
func getPoint(fromPoint point: CGPoint, atDistance distance: CGFloat, withAngleRadians angle:CGFloat) -> CGPoint {
let x = point.x
let y = point.y
let dx = (distance * cos(angle))
let dy = (distance * sin(angle))
return CGPoint(x: (dx + x), y: (dy + y))
}
Now the real function. Generate a bunch of points in a circle pattern. I used a running sum for the angle instead of multiplying each time by the index. This just returns the centre points for the views.
func encirclePoint(point : CGPoint, distance:CGFloat, inParts parts: Int) -> [CGPoint] {
let angle = 2 * CGFloat(M_PI) / CGFloat(parts) // critical part, you need radians for trigonometry
var runningAngle : CGFloat = -(CGFloat(M_PI) / 2) // start at the top
var points : [CGPoint] = []
for _ in 0..<parts {
let circlePoint = getPoint(fromPoint: point, atDistance: distance, withAngleRadians: runningAngle)
points.append(circlePoint)
runningAngle += angle
}
return points
}
Now you can create a simple function that takes a view, a margin and an array of "satellite" views. It will set their centre and add them to the superview of the view we used to input. It makes sense not to add them to the view itself since they might not be placed inside it.
func encircleView(view : UIView, withSubViews subViews : [UIView], withMargin margin : CGFloat) {
guard !(subViews.isEmpty) else { // if there are no subviews : abort
return
}
let distance = view.radius + margin
let points = encirclePoint(view.center, distance: distance, inParts: subViews.count)
guard subViews.count == points.count, let uberView = view.superview else { // if the count is not the same or there is no superview: abort
return
}
for (point, subView) in zip(points, subViews) { subView.center = point }
}
Notice how I did nothing except for the centre calculations in these functions. Styling them goes in another function. This makes it super easy to maintain and debug.
I might even let the last function just return the subviews with updated frames and add them later.
Or negative margin :)
Gist
A full circle is 2 * pi radians. You need to divide that by the number of items you have and multiply that by the index of the item you are currently processing. Use trig to find the location on the circle:
for (index, users) in upimage.enumerate() {
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
......
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * radius
let pointy = centery + sin(radians) * radius
......
}

How do I place the objects in the array around the center image?

I have an array of buttons and when I append them to a view I want the to be positioned around a image view which is in the center. Based on how many objects there are in the array, I want them to be evenly spaced around the whole circle. Below is my attempt to do so. What am I doing wrong and how should I fix it? There is more than one button behind the moose.
var userbutton = [UIButton]()
var upimage = [UIImage]()
var locationpic = [AnyObject]()
func locationsSet(){
for (index, users) in upimage.enumerate() {
let userbutton = UIButton()
userbutton.addTarget(self, action: "buttonAction:", forControlEvents: .TouchUpInside)
userbutton.frame = CGRectMake(100, 100, 50, 50)
userbutton.layer.cornerRadius = userbutton.frame.size.width/2
userbutton.clipsToBounds = true
userbutton.setImage(users, forState: .Normal)
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * (radius + 40)
let pointy = (centery) + (sin(radians)) * (radius + 40)
userbutton.center.x = pointx
userbutton.center.y = pointy
self.userbutton.append(userbutton)
self.view.addSubview(userbutton)
print("x\(pointx)")
print("y\(pointy)")
}
}
How I would do this:
Create an extension to UIView to get the diagonal and radius. These are handy because we want our "satellites" to have predictable placing even when the "planet" isn't square.
extension UIView {
var diagonal : CGFloat {
return sqrt(pow(self.frame.width, 2) + pow(self.frame.height, 2))
}
var radius : CGFloat {
return diagonal / 2
}
}
This will return a point based on an angle and a distance from an origin.
It uses dreadful trigonometry.
func getPoint(fromPoint point: CGPoint, atDistance distance: CGFloat, withAngleRadians angle:CGFloat) -> CGPoint {
let x = point.x
let y = point.y
let dx = (distance * cos(angle))
let dy = (distance * sin(angle))
return CGPoint(x: (dx + x), y: (dy + y))
}
Now the real function. Generate a bunch of points in a circle pattern. I used a running sum for the angle instead of multiplying each time by the index. This just returns the centre points for the views.
func encirclePoint(point : CGPoint, distance:CGFloat, inParts parts: Int) -> [CGPoint] {
let angle = 2 * CGFloat(M_PI) / CGFloat(parts) // critical part, you need radians for trigonometry
var runningAngle : CGFloat = -(CGFloat(M_PI) / 2) // start at the top
var points : [CGPoint] = []
for _ in 0..<parts {
let circlePoint = getPoint(fromPoint: point, atDistance: distance, withAngleRadians: runningAngle)
points.append(circlePoint)
runningAngle += angle
}
return points
}
Now you can create a simple function that takes a view, a margin and an array of "satellite" views. It will set their centre and add them to the superview of the view we used to input. It makes sense not to add them to the view itself since they might not be placed inside it.
func encircleView(view : UIView, withSubViews subViews : [UIView], withMargin margin : CGFloat) {
guard !(subViews.isEmpty) else { // if there are no subviews : abort
return
}
let distance = view.radius + margin
let points = encirclePoint(view.center, distance: distance, inParts: subViews.count)
guard subViews.count == points.count, let uberView = view.superview else { // if the count is not the same or there is no superview: abort
return
}
for (point, subView) in zip(points, subViews) { subView.center = point }
}
Notice how I did nothing except for the centre calculations in these functions. Styling them goes in another function. This makes it super easy to maintain and debug.
I might even let the last function just return the subviews with updated frames and add them later.
Or negative margin :)
Gist
A full circle is 2 * pi radians. You need to divide that by the number of items you have and multiply that by the index of the item you are currently processing. Use trig to find the location on the circle:
for (index, users) in upimage.enumerate() {
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
......
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * radius
let pointy = centery + sin(radians) * radius
......
}

Making Sure Nodes are visible

So I have this sprite kit game, which is coded in swift 2. The game includes these colored circles (Green, Red, Purple, Yellow, Blue) that fall down the screen, starting from the same height, but starting at different widths. When the circles hit the bottom of the screen, the respectable method is called. The problem I am having is, the random x position can sometimes cut half of the circle off because it is on the very side of the screen. How can I prevent the circles from clipping the side of the screen? Here are the methods that are called when the circles hit the bottom of the screen.
func changeGreen(){
Green.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Green.position.x = CGFloat(PositionX)
}
func changeRed(){
Red.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Red.position.x = CGFloat(PositionX)
}
func changeBlue() {
Blue.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Blue.position.x = CGFloat(PositionX)
}
func changeYellow() {
Yellow.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Yellow.position.x = CGFloat(PositionX)
}
func changePurple() {
Purple.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Purple.position.x = CGFloat(PositionX)
}
Assuming position.x is the center of the circle, I think something like this might work:
let maxX = 350 // this is your frame width
let radius = 50 // radius of the circle
var positionX = Int(arc4random_uniform(UInt32(maxX - radius))) // max position of x is taken care of here
let pointsFromLeft = positionX - radius
if pointsFromLeft < 0 {
positionX -= pointsFromLeft // move to the right if necessary
}
You have to offset the radius of the circle from both ends of frame so that the circles never get clipped.
let radius = 20 // radius of your circle
let positionX = radius + arc4random_uniform(UInt32(self.frame.width - 2 * radius))

Resources