How do I constrain an SKShapeNode to the device edges in SpriteKit? - ios

Just to test things out I have created a blue square and placed it at the center of the screen like this:
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myPoint = CGPoint(x: UIScreen.main.nativeBounds.midX, y: UIScreen.main.nativeBounds.midY)
mySquare.position.x = 0
mySquare.position.y = 0
self.addChild(mySquare)
Works great. Now, I would like to use constraints and set up the square constraints to the edges of the device screen. I have tried this, but the blue square doesn't appear, so I think I have the wrong idea on how to capture the CGPoint of the screen edges.
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myPoint = CGPoint(x: UIScreen.main.nativeBounds.maxX, y: UIScreen.main.nativeBounds.maxY)
let range = SKRange(lowerLimit: 10.0, upperLimit: 10.0)
let myConstraints = SKConstraint.distance(range, to: myPoint)
mySquare.constraints = [myConstraints]
self.addChild(mySquare)
How do I capture the screen edges and constrain the square to those?

SKConstraint doesn't work equally as UIKit Constraints.
SKConstraint functionality is really specific:
Please take a look here: https://developer.apple.com/documentation/spritekit/skconstraint
Anyway, can give you some recommendations:
Transform screen position to scene position:
self.view?.convert(myPoint, to: self)
You can start with this example and get node on a corner
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myScreenPoint = CGPoint(x: UIScreen.main.bounds.maxX, y: UIScreen.main.bounds.maxY)
if let myScenePoint = self.view?.convert(myScreenPoint, to: self) {
mySquare.position = myScenePoint
}
self.addChild(mySquare)
With this logic, you can get each side of the screen and decrease or increase margin and make each 4 sides; or 1 constraint for the center.

Related

Place view in the bottom right, hiding part, programmatically swift 5

I want to move the view as shown in the image programmatically, to obtain an effect similar to 1/4 of roulette.
But I only get it to show below as indicated in the code for one screen.
let X_Position:CGFloat? = 150.0 //use your X position here
let Y_Position:CGFloat? = 300.0 //use your Y position here
circle.frame = CGRect(x: X_Position ?? 0,y: Y_Position ?? 0,width:
circle.frame.width,height: circle.frame.height)
How can I get this position for all screens?
Swift 4
Without Animation:
let screen = UIScreen.main.bounds
let circleViewRect = circlemenu.frame
circlemenu.frame = CGRect.init(x: screen.width - circleViewRect.width/2, y: screen.height - circleViewRect.height/2, width: circleViewRect.width, height: circleViewRect.height)
With Animation:
let screen = UIScreen.main.bounds
let circleViewRect = circlemenu.frame
UIView.animate(withDuration: 0.3) {
circlemenu.frame = CGRect.init(x: screen.width - circleViewRect.width/2, y: screen.height - circleViewRect.height/2, width: circleViewRect.width, height: circleViewRect.height)
}
I believe you are using an old version of swift, so feel free to change the functions accordingly

Endless Runner reposition player for equal difficulty regardless of aspect ratio

I am creating and endless runner for iphones in landscape mode using SpriteKit. I set up the scene as such:
override func viewDidLayoutSubviews() {
let scene = GameScene(size: CGSize(width: 812, height: 375))
let skView = view as! SKView
scene.backgroundColor = UIColor.red
scene.scaleMode = .aspectFill
skView.presentScene(scene)
skView.showsFPS = true
skView.showsNodeCount = true
print("Screen Size: \(GlobalProperties.screenSize.width) x \(GlobalProperties.screenSize.height)")
print("Scene Size: \(scene.size.width) x \(scene.size.height)")
}
I would like to position the player so that there is the same amount of pixels between the edge of the player and the right edge of the screen regardless of aspect ratio. Is this a reasonable practice for maintaining difficulty between devices? I have the layout setup as such:
override func didMove(to view: SKView) {
let player = SKSpriteNode(texture: SKTexture(imageNamed: "CuteMelon"))
player.anchorPoint = CGPoint(x: 0.5, y: 0.5)
player.position = CGPoint(x: frame.width - 600, y: frame.midY)
self.addChild(player)
let rect = SKSpriteNode(color: .orange, size: CGSize(width: 550, height: 200))
rect.anchorPoint = CGPoint(x: 1, y: 0.5)
rect.position = CGPoint(x: frame.width-50, y: frame.height/2)
self.addChild(rect)
}
I added the rectangle to see if in both cases the player was 600 pixels from the right (leaving a 50px gap to ensure it wasnt running off the edge)
The result is as follows:
iPhone XR: https://imgur.com/y6OYHkK which is working as intended
iPhone 8: https://imgur.com/nkrx5By which is not placing the rectangle 50 pixels from the right bound of the frame.
What do I have to do to fix this issue or should I go about solving it a different way entirely? Thank you
Simply figure out the difference between the scene size and the screen size, and shift the camera over by half that distance. The formula abs((sceneWidth - screenWidth * sceneHeight/screenHeight)/2) will get you that. What this does is scale the screen to whatever height the scene is, then subtract the two differences from the width and return half that value.
override func didMove(to view: SKView) {
let widthPadding = abs((self.frame.width - (UIScreen.main.bounds.width * self.frame.height / UIScreen.main.bounds.height )) / 2)
self.camera = self.camera ?? SKCameraNode()
self.camera.position.x += widthPadding
let player = SKSpriteNode(texture: SKTexture(imageNamed: "CuteMelon"))
player.anchorPoint = CGPoint(x: 0.5, y: 0.5)
player.position = CGPoint(x: frame.width - 600, y: frame.midY)
self.addChild(player)
let rect = SKSpriteNode(color: .orange, size: CGSize(width: 550, height: 200))
rect.anchorPoint = CGPoint(x: 1, y: 0.5)
rect.position = CGPoint(x: frame.width-50, y: frame.height/2)
self.addChild(rect)
}

How to create a ruler / scale slider with Swift 3?

I'm trying to recreate a temperature ruler similar to the Coinbase app for an app in Swift 3. Unfortunately, I'm not sure if I follow the right approach.
In my current experiment, I used a UIScrollView element and placed / drawn lines at a certain distance (with a loop and UIBezierPath). In the next step I want to read the user input. With the current approach I have to use the X-position of the UIScrollView to convert things to read the current temperature. That seems to me a relatively inaccurate thing to be? My result looks like this.
// UI SCROLL VIEW
var scrollView: UIScrollView!
scrollView = UIScrollView(frame: CGRect(x: 0, y: 120, width: 400, height: 100))
scrollView.contentSize = CGSize(width: 2000, height: 100)
scrollView.showsHorizontalScrollIndicator = false
let minTemp = 0.0
let maxTemp = 36.8
let interval = 0.1
// LINES
let lines = UIBezierPath()
// DRAW TEMP OTHER LINES
for temp in stride(from: minTemp, to: maxTemp, by: interval)
{
let isInteger = floor(temp) == temp
let height = (isInteger) ? 20.0 : 10.0
let oneLine = UIBezierPath()
oneLine.move(to: CGPoint(x: temp*50, y: 0))
oneLine.addLine(to: CGPoint(x: temp*50, y: height))
lines.append(oneLine)
// INDICATOR TEXT
if(isInteger)
{
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 40, height: 21))
label.center = CGPoint(x: temp*50, y: height+15)
label.font = UIFont(name: "HelveticaNeue",
size: 10.0)
label.textAlignment = .center
label.text = "\(temp) °C"
scrollView.addSubview(label)
}
}
// DESIGN LINES IN LAYER
let shapeLayer = CAShapeLayer()
shapeLayer.path = lines.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 1
// ADD LINES IN LAYER
scrollView.layer.addSublayer(shapeLayer)
view.addSubview(scrollView)
self.view = view
The Coinbase app also struck me that there is some kind of adaptive feedback (at least on the iPhone X) when I move the slider. The iPhone vibrates easily when you come across a line, similar to the UIPickerView. I do not have that with my approach and I strongly doubt that the developer has programmed it in manually on Coinbase... So maybe there's a smarter, more up-to-date approach to how to recreate such a ruler natively in Swift?
you can using this pod
this will allow you to using that as horizontal or vertical
https://github.com/farshidce/RKMultiUnitRuler

Why does calculateAccumulatedFrame give an unexpected CGRect?

I'm trying to determine the right scaling factor for my node tree to make it fit exactly in my presentation rectangle, so I'm trying to find the smallest bounding rectangle around all my nodes. Apple's docs say that calculateAccumulatedFrame "Calculates a rectangle in the parent’s coordinate system that contains the content of the node and all of its descendants." That sounds like what I need, but it's not giving me the tight fit that I expect. My complete playground code is:
import SpriteKit
import PlaygroundSupport
let view:SKView = SKView(frame: CGRect(x: 0, y: 0, width: 800, height: 800))
PlaygroundPage.current.liveView = view
let scene:SKScene = SKScene(size: CGSize(width: 1000, height: 800))
scene.scaleMode = SKSceneScaleMode.aspectFill
view.presentScene(scene)
let yellowBox = SKSpriteNode(color: .yellow, size:CGSize(width: 300, height: 300))
yellowBox.position = CGPoint(x: 400, y: 500)
yellowBox.zRotation = CGFloat.pi / 10
scene.addChild(yellowBox)
let greenCircle = SKShapeNode(circleOfRadius: 100)
greenCircle.fillColor = .green
greenCircle.position = CGPoint(x: 300, y: 50)
greenCircle.frame
yellowBox.addChild(greenCircle)
let uberFrame = yellowBox.calculateAccumulatedFrame()
let blueBox = SKShapeNode(rect: uberFrame)
blueBox.strokeColor = .blue
blueBox.lineWidth = 2
scene.addChild(blueBox)
And the results are:
The left and bottom edges of the blue rectangle look good, but why are there gaps between the blue rectangle and the green circle on the top and right?
The notion "frame" does funny things when you add a transform. The bounding box around the box and circle is a rectangle. You have rotated that rectangle. Therefore its corners stick out. The accumulated frame embraces that rotated rectangle, including the sticking-out corners. It does not magically hug the drawn appearance of the nodes (e.g. the circle).

Change Node Positioning Depending on Screen Size Swift/Sprite Kit

I want to change to positioning of my nodes depending on the size of the screen. For example, when I run my app on an iPhone 4s, not all of the nodes fit on the screen, since I developed the app with iPhone 6 dimensions in mind. How can I make it so the nodes reposition themselves depending on which device it's running on? I know I can achieve this with constraints normally, but I don't know how to do that in Sprite Kit. I have included a screen shot down below.
lblRocketCount.position = CGPoint(x: 100, y: 150)
lblRocketCount.text = "Bullets: 30"
self.addChild(lblRocketCount)
lblMissileCount.position = CGPoint(x: -100, y: 150)
lblMissileCount.text = "Missiles: 5"
self.addChild(lblMissileCount)
leftBorder.position = CGPoint(x: -333, y: 0)
self.addChild(leftBorder)
rightBorder.position = CGPoint(x: 333, y: 0)
self.addChild(rightBorder)
topBorder.position = CGPoint(x: 0, y: 187)
self.addChild(topBorder)
bottomBorder.position = CGPoint(x: 0, y: -187)
self.addChild(bottomBorder)
buttonDirUp.position = CGPoint(x: -200, y: -50)
buttonDirUp.setScale(2.0)
self.addChild(buttonDirUp)
ship.setScale(0.33)
shootButton.setScale(0.7)
missileButton.setScale(0.5)
buttonDirLeft.position = CGPoint(x: -250, y: -100)
buttonDirLeft.setScale(2.0)
self.addChild(buttonDirLeft)
buttonDirDown.position = CGPoint(x: -200, y: -150)
buttonDirDown.setScale(2.0)
self.addChild(buttonDirDown)
buttonDirRight.position = CGPoint(x: -150, y: -100)
buttonDirRight.setScale(2.0)
self.addChild(buttonDirRight)
self.view?.multipleTouchEnabled = true
self.backgroundColor = SKColor.blackColor()
self.anchorPoint = CGPointMake(0.5, 0.5)
self.addChild(base)
base.position = CGPointMake(200, -100)
self.addChild(ball)
ball.position = base.position
self.addChild(ship)
ship.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.addChild(shootButton)
shootButton.position = CGPoint(x: self.frame.midX, y: -100)
self.addChild(missileButton)
missileButton.position = CGPoint(x: -200, y: 50)
Screen Shot
This is a technique that I've found to be effective for dealing with multiple resolutions. You'll need to create two global variables in your GameViewController that reflect the size of the current view:
import SpriteKit
// global variables
var SKViewSize: CGSize?
var SKViewSizeRect: CGRect?
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let skView = self.view as! SKView
SKViewSize = skView.bounds.size
let scene = GameScene(size: SKViewSize!)
scene.scaleMode = .AspectFill
skView.presentScene(scene)
SKViewSizeRect = getViewSizeRect()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
SKViewSize = self.view.bounds.size
SKViewSizeRect = getViewSizeRect()
let skView = self.view as! SKView
if let scene = skView.scene {
if scene.size != self.view.bounds.size {
scene.size = self.view.bounds.size
}
}
}
func getViewSizeRect() -> CGRect {
return CGRect(x: ((SKViewSize!.width * 0.5) * -1.0), y: ((SKViewSize!.height * 0.5) * -1.0), width: SKViewSize!.width, height: SKViewSize!.height)
}
}
Then create an extension for all SKNode types that allows you to position them using a percentage of the current screen size:
public extension SKNode {
public func posByScreen(x: CGFloat, y: CGFloat) {
self.position = CGPoint(x: CGFloat((SKViewSizeRect!.width * x) + SKViewSizeRect!.origin.x), y: CGFloat((SKViewSizeRect!.height * y) + SKViewSizeRect!.origin.y))
}
}
For example, to create a sprite node and center it on any screen size, you'd use this in your GameScene:
let sprite = SKSpriteNode(color: SKColor.whiteColor(), size: CGSizeMake(32, 32))
addChild(sprite)
sprite.posByScreen(0.5, y: 0.5)
There is no easy answer to what you are looking for, you first need to decide what your game should be like at the different aspect ratios, and plan around that. In a lot of cases, you want to just give the 16:9 phones more FOV (They can see more of the game world). To do this, design your game with 2 layers: a game layer, and a hud overlay.
The game layer will utilize .AspectFill to keep your game the same "size" across devices, with the iPhone 4s folk losing the ability to see some of the screen above and below them (You can adjust where they lose some of the screen by moving the game layer nodes position)
The HUD overlayer will handle the things that need to stay at a fixed position on screen, like buttons and life bars. Now you can use the % based coordinates like some people have mentioned, or you can keep the absolute coordinates, and just add/subtract the pixel difference from your overlay nodes if your game detects a 3:2 (or 4:3 if you want iPad) ratio
Basic principle: (Do not copy verbatum)
Lets say we create a game scene that is 160x284 points
This means on an iphone 4s our resolution is 320x480, aspect fill is going to take the smallest of the distance between top/bottom or left/right and scale to that. In our case 160 is going to scale to 320, 284 is going to scale to 568. Uh oh, iphone only has 480, so you are losing out on 88 pixels, which means in the game world we are losing 44 points.
//This means that the top and bottom will lose out on 22 points each, so lets make that our iphone4s padding
class GameScene
{
let gameNode = SKNode();
let overlayNode = SKNode();
let iPhone4sPadding = 22;
func init()
{
//add all of the games nodes that you would add to the scene to gameNode instead
self.addChild(gameNode);
//add all of your overlay nodes (buttons, lifebars, etc) to the overlayNode
//do a check on uiscreen to see if the height is 480, if it is
// all nodes on the bottom add iphone4spadding to the y
// all nodes on the top subtract iphone4spadding from the y
self.addChild(overlayNode);
}
}

Resources