Is there a way to make a responsive layer node? - swift

I'm new to SpriteKit, I've searched for the answer to this but can't seem to find one.
Is there a way to make a node that will expand to fit whatever device you're on?
Right now I'm creating layers with either a concrete dimension or a ratio. What I'd like to do is create an expandable rectangle, attach a physics body to it and have that be the boundary for my game.
what I'm currently doing:
gamescene.swift
override func didMoveToView(view: SKView) {
let maxAspectRatio: CGFloat = 16.0 / 9.0
let maxAspectRatioheight = size.width / maxAspectRatio
let playableMargin: CGFloat = (size.height - maxAspectRatioheight) / 2
let playableRect = CGRect(x: 0, y: playableMargin, width: size.width, height: size.height - playableMargin * 2)
physicsBody = SKPhysicsBody(edgeLoopFromRect: playableRect)
}
but I've experimented with using a background as well:
// declared with the other fields outside method
var background = SKSpriteNode(imageNamed: "background")
//inside didMoveToView
let bounds = SKNode()
bounds.physicsBody = SKPhysicsBody(edgeLoopFromRect:
CGRect(x: 0, y: 0,
width: background.size.width,
height: background.size.height))
bounds.physicsBody!.categoryBitMask = PhysicsCategory.Boundary
bounds.physicsBody!.friction = 0
worldNode.addChild(bounds)
Any tips would be greatly appreciated!

Related

Programmatically drawing custom oval shape on screen swift

How can i draw custom oval shape like in below image in swift (not swiftUI).
Thank you in advance
I have tried to clone similar view using UIView and was able to create similar UI as you have stated on screenshot
And here is my code snippet
let width: CGFloat = view.frame.size.width
let height: CGFloat = view.frame.size.height
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height), cornerRadius: 0)
let rect = CGRect(x: width / 2 - 150, y: height / 2.5 - 100, width: 300, height: 400)
let circlePath = UIBezierPath(ovalIn: rect)
path.append(circlePath)
path.usesEvenOddFillRule = true
let fillLayer = CAShapeLayer()
fillLayer.path = path.cgPath
fillLayer.fillRule = .evenOdd
fillLayer.fillColor = UIColor.red.cgColor
fillLayer.opacity = 0.5
view.layer.addSublayer(fillLayer)
Hope this helps you. Good day.
You can draw oval path like this
class CustomOval: UView {
override func drawRect(rect: CGRect) {
var ovalPath = UIBezierPath(ovalIn: rect)
UIColor.gray.setFill()
ovalPath.fill()
}
}

Recreate CGAffineTransform of UIImageView inside a CGContext

I have a transformed UIImage inside a UIVIew with clipsToBounds = true and I want to recreate (redraw) it exactly using a CGContext. It's hard to explain, here is the code:
class ViewController: UIViewController {
#IBOutlet weak var topView: UIView!
#IBOutlet weak var topImageView: UIImageView!
#IBOutlet weak var bottomImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// the example image
let image = UIImage(named: "image")!
// topImageView is embedded inside topView so that the image is being clipped
topView.clipsToBounds = true
topImageView.image = image
topImageView.contentMode = .center // I set this to center because it seems to be easier to draw it then
// The transformation data
let size = CGSize(width: 800, height: 200)
let scale: CGFloat = 1.5
let offset: (x: CGFloat, y: CGFloat) = (x: 0.0, y: 0.0)
// transform the imageView
topImageView.transform = CGAffineTransform(translationX: offset.x, y: offset.y).scaledBy(x: scale, y: scale)
// Now try to recreate the transform by using a transformed CGContext
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
let context = UIGraphicsGetCurrentContext()!
// transform the context
// Almost the same as above, but I additionally move the context so that the middle of the image is in the middle of the size defined above
context.concatenate(CGAffineTransform(translationX: (-image.size.width / 2 + size.width / 2) + offset.x, y: (-image.size.height / 2 + size.height / 2) + offset.y).scaledBy(x: scale, y: scale))
image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
bottomImageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
This is working only when I have scale set to 1.0 (so no scale). Otherwise, I don't get the correct image. Here's a screenshot so that you see it:
It's not the same. It's hard to figure out how to correctly transform the context, I don't entirely understand it. Any ideas?
Thanks!
It's a mystery. I've literally spent hours trying to get this right. 5 minutes after posting this question, I solved it. It's actually not that hard if you draw it (I used photoshop to visualize the coordinate system and played around a little bit. Should have done this earlier..)
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
let context = UIGraphicsGetCurrentContext()!
context.concatenate(CGAffineTransform.identity.scaledBy(x: scale, y: scale).translatedBy(x: size.width / (2 * scale), y: size.height / (2 * scale)).translatedBy(x: offset.x / scale, y: offset.y / scale))
image.draw(in: CGRect(x: -image.size.width / 2, y: -image.size.height / 2, width: image.size.width, height: image.size.height))
bottomImageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Rounded rectangle with SKShapeNode

I have the following code that rounds an already existing rectangle from the scene builder...
btOk.path = UIBezierPath(roundedRect: CGRect(x: -62.5,y:-25,width: 125, height: 50), cornerRadius: 10).cgPath
However, if i try to use the same init to round the corners of another rectangle that is much larger, it does not even come close to working. It just makes the width HUUUUGE (imagine Trump).
scene.enumerateChildNodes(withName: "//*"){
node,stop in
if let name = node.name{
if name.contains("round"){
if let shapeNode = node as? SKShapeNode{
print(shapeNode.frame.width) //500
shapeNode.path = UIBezierPath(roundedRect: CGRect(x: -250,y:-100,width: 500, height: 200), cornerRadius: 50).cgPath
print(shapeNode.frame.width) //5735
}
}
}
}
btOk is a SKShapeNode as well. What am i missing between the two that is so different? One thing to note is i am enumerating through the children of the scene like this because this scene is in a SKReferenceNode. Perhaps that has something to do with it?
EDIT
Taking direction from #Ron Myschuk, i've solved this and since it's such a PITA, i also created an extension. So now i can round the corners of any SKShapeNode very easily when needed. Even if it was created in the scene editor. Note, this should only be used if there are no children of the shape node. Otherwise those children will be removed also.
extension SKShapeNode {
func roundCorners(topLeft:Bool,topRight:Bool,bottomLeft:Bool,bottomRight:Bool,radius: CGFloat,parent: SKNode){
let newNode = SKShapeNode(rect: self.frame)
newNode.fillColor = self.fillColor
newNode.lineWidth = self.lineWidth
newNode.position = self.position
newNode.name = self.name
self.removeFromParent()
parent.addChild(newNode)
var corners = UIRectCorner()
if topLeft { corners = corners.union(.bottomLeft) }
if topRight { corners = corners.union(.bottomRight) }
if bottomLeft { corners = corners.union(.topLeft) }
if bottomRight { corners = corners.union(.topRight) }
newNode.path = UIBezierPath(roundedRect: CGRect(x: -(newNode.frame.width / 2),y:-(newNode.frame.height / 2),width: newNode.frame.width, height: newNode.frame.height),byRoundingCorners: corners, cornerRadii: CGSize(width:radius,height:radius)).cgPath
}
}
And use it like so...
aShapeNode.roundCorners(topLeft: true, topRight: true, bottomLeft: false, bottomRight: false, radius: 5,parent: popup)
Not what you're going to want to hear but it's because you cannot set the width of an SKShapeNode in the Scene editor (To my knowledge). In order to get that ShapeNode to have a width of 500 you would have had to adjust the xScale. The xScale then reapplies itself to the path when you adjust it (kind of growing exponentially). If you create the SKShapeNode in code there is no problem adjust the rounded corners
let round = SKShapeNode(rect: CGRect(x: 0, y: -150, width: 500, height: 200))
round.fillColor = .red
addChild(round)
print(round.frame.width)
round.path = UIBezierPath(roundedRect: CGRect(x: -250, y: -100, width: 500, height: 200), cornerRadius: 50).cgPath
print(round.frame.width)
Edit
If you have your heart set on using the Scene editor you can place your ShapeNode and stretch it to where you want it then you could just do a small conversion in code to get the results that you want
if let round = self.childNode(withName: "biground") as? SKShapeNode {
let biground = SKShapeNode(rect: round.frame)
biground.fillColor = round.fillColor
biground.position = round.position
addChild(biground)
round.removeFromParent()
print(biground.frame.width)
biground.path = UIBezierPath(roundedRect: CGRect(x: -250, y: -100, width: 500, height: 200), cornerRadius: 50).cgPath
print(biground.frame.width)
}
this just recreates the shape in code based on what you outlined in the Scene editor and rounds the edges perfectly
edit 2
I've always been under the impression that SKShapeNodes are really inefficient (i'm pretty sure they leaked memory as well). So i always setup my round rectangles as so.
let outsideTexture = SKTexture(imageNamed: "round_square_white")
let outside = SKSpriteNode(texture: outsideTexture)
let insetX: CGFloat = 20
let insetY: CGFloat = 20
let cellSize = CGSize(width: 500.0, height: 500.0)
outside.centerRect = CGRect(x: CGFloat(insetX / outside.size.width), y: CGFloat(insetY / outside.size.height), width: CGFloat((outside.size.width - insetX * 2) / outside.size.width), height: CGFloat((outside.size.height - insetY * 2) / outside.size.height))
//outside.position = CGPointMake(gameModel.gameWidth / 2, (outside.size.height) / 2);
outside.xScale = cellSize.width / outside.size.width
outside.yScale = cellSize.height / outside.size.height
outside.zPosition = 10
outside.position = CGPoint(x: CGFloat(0), y: CGFloat(-0.35 * self.size.height / 2))
self.addChild(outside)
worth noting that this lays out a rounded square/rectangle perfectly however similar to the scale issues from the scene editor you have to place an empty cell over this to add children to, otherwise they scale to the rounded square.

Nodes position within scene size

In my SKScene, I am making a simple space shooter game. How can I make sure that my enemies always appear within the screen size regardless of which iphone the game is played at?
In other words words, how do I calculate the max and min X-coordinates of the scene size and more importantly how do i know what's the current scene size depending on which iphone the game is run at?
Don't resize your scene depend by iPhone model, leave Sprite-kit do this kind of job:
scene.scaleMode = SKSceneScaleMode.ResizeFill
The scene is not scaled to match the view. Instead, the scene is
automatically resized so that its dimensions always matches those of
the view.
About the size, when a scene is first initialized, its size property is configured by the designated initializer. The size of the scene specifies the size of the visible portion of the scene in points. This is only used to specify the visible portion of the scene.
Update to help you in positioning:
If you want to set your positions instead to use scaleMode you can
set your scene.scaleMode to .AspectFill for this to work on all scenes and the scene size has to be 2048x1536 or 1536x2048. This will make it scaleable for iPad too.
class StartScene: SKScene {
let playableArea: CGRect!
}
override init(size: CGSize) {
//1. Get the aspect ratio of the device
let deviceWidth = UIScreen.mainScreen().bounds.width
let deviceHeight = UIScreen.mainScreen().bounds.height
let maxAspectRatio: CGFloat = deviceWidth / deviceHeight
//2. For landscape orientation, use this
let playableHeight = size.width / maxAspectRatio
let playableMargin = (size.height - playableHeight) / 2.0
playableArea = CGRect(x: 0, y: playableMargin, width: size.width, height: playableHeight)
//3. For portrait orientation, use this
let playableWidth = size.height / maxAspectRatio
let playableMargin = (size.width - playableWidth) / 2.0
playableArea = CGRect(x: playableMargin, y: 0, width: playableWidth, height: size.height)
super.init(size: size)
}
So you can positioning your object with:
ball.position = CGPoint(x: CGRectGetMidX(playableArea), y: CGRectGetMaxY(playableArea) - (ball.size.height * 0.90))
This code works in the iPhone 4S, 5, 5S, 6, 6 Plus, 6S, 6S Plus, and iPads.
If you want to see your borders (for debug or not):
func drawWorkArea() {
let shape = SKShapeNode()
let path = CGPathCreateMutable()
CGPathAddRect(path, nil, workArea)
shape.path = path
shape.strokeColor = SKColor.redColor()
shape.lineWidth = 8
addChild(shape)
}

Couldn't add NSButton to the Sprite-Kit scene

I'm a newbie to OS X programming. I'm building a simple game for Mac with Sprite-Kit, and stuck with the problem of adding NSButton (and other NSObjects) to the view. It just doesn't work - no button shows up.
My code:
class LaunchScene: SKScene {
override func didMoveToView(view: SKView) {
let background = SKSpriteNode(imageNamed: "Wall")
background.size = CGSize(width: 1600, height: 1200)
background.position = CGPoint(x: CGRectGetMidX(self.frame),
y: CGRectGetMidY(self.frame))
self.addChild(background)
let button = NSButton(frame: NSRect(x: self.frame.width/2,
y: self.frame.height/2, width: 50, height: 25))
self.view!.addSubview(button) //button doesn't show up
How could I fix it?
P.S. I tried to add a layer to my view, as some bloggers recommended, in this situation the button actually does show up but the screen turns completely black or renders only glitches.
let layer = CALayer()
self.view!.layer = layer
self.view!.wantsLayer = true
OK, finally I figured out how to do that (don't need to assign a layer to your view if self.view!.wantsLayer is equal to "true"):
class LaunchScene: SKScene {
override func didMoveToView(view: SKView) {
let background = SKSpriteNode(imageNamed: "Wall")
background.size = CGSize(width: 1600, height: 1200)
background.position = CGPoint(x: CGRectGetMidX(self.frame),
y: CGRectGetMidY(self.frame))
self.addChild(background)
self.view!.wantsLayer = true
let button = NSButton(frame: NSRect(x: self.frame.width/2,
y: self.frame.height/2, width: 50, height: 25))
self.view!.addSubview(button)