I am creating a space exploration game using SpriteKit and Swift. I am generating a space environment using Core Graphics. Since the player can control a spaceship which can move in every direction, I am creating 9 different backgrounds and positioning them around the users ship like this.
7 8 9
4 5 6
1 2 3
Player start in layer 5. My game uses 2048x1536 screen with aspectFill so I can support all resolutions.
I generate background with this code.
UIGraphicsBeginImageContext(CGSizeMake(width, height))
let ctx = UIGraphicsGetCurrentContext();
let max = width * height / CGFloat(starCountModifier)
for i in 0...Int(max)
{
let x = arc4random_uniform(UInt32(width))
let y = arc4random_uniform(UInt32(height))
let alpha = CGFloat(Float(arc4random()) / Float(UINT32_MAX))
CGContextSetFillColorWithColor(ctx, SKColor(red: 1.0, green: 1.0, blue: 1.0, alpha: alpha * alpha * alpha).CGColor)
CGContextFillRect(ctx, CGRect(x: CGFloat(x), y: CGFloat(y), width: 2, height: 2))
}
let textureImage: UIImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext()
let texture = SKTexture(CGImage: textureImage.CGImage)
let sprite = SKSpriteNode(texture: texture)
addChild(sprite)
It works fine with one problem. Every background I create costs about 20-25mb of memory!
Even if I dont draw stars, it alone allocates this much memory by just creating SKSpriteNode out of that context.
Is there any way to optimize this texture data?
Thanks.
Related
Ok we have draw several things in a MTKView We can move and turn around them using Metal, MetalKit functions but we are unable to get pixel information of a given point in 3D (x,y,z). We searched several hours and could not find any solution for that.
impossible to get color from 3D model space, but possible to get color from 2D view space.
func getColor(x: CGFloat, y: CGFloat) -> UIColor? {
if let curDrawable = self.currentDrawable {
var pixel: [CUnsignedChar] = [0, 0, 0, 0] // bgra
let textureScale = CGFloat(curDrawable.texture.width) / self.bounds.width
let bytesPerRow = curDrawable.texture.width * 4
let y = self.bounds.height - y
curDrawable.texture.getBytes(&pixel, bytesPerRow: bytesPerRow, from: MTLRegionMake2D(Int(x * textureScale), Int(y * textureScale), 1, 1), mipmapLevel: 0)
let red: CGFloat = CGFloat(pixel[2]) / 255.0
let green: CGFloat = CGFloat(pixel[1]) / 255.0
let blue: CGFloat = CGFloat(pixel[0]) / 255.0
let alpha: CGFloat = CGFloat(pixel[3]) / 255.0
let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)
return color
}
return nil
}
This seems like a classic case of trying to consult your view (in the Model-View-Controller paradigm sense of the term) for information held by the model. Consult your model directly.
A 3D rendering technology like Metal is all about flattening 3D information to 2D and efficiently throwing away information not relevant to that 2D representation. Neither Metal nor the MTKView has any information about points in 3D by the end of the render.
Also note, your model probably doesn't have information for arbitrary 3D points. Most likely all of your 3D models (in the other sense of the term) are shells. They have no interiors, just surfaces. So, unless a 3D point falls exactly on the surface and not at all inside nor outside of the model, it has no color.
I have an SKSpiteNode:
private var btnSound = SKSpriteNode(imageNamed: "btnSound")
Now I made this image in Adobe Illustrator with a size of 2048x2048 pixels (overkill really), so it has good resolution. My problem is when I set the size of it the image, the lines in it go serrated or jagged...not smooth.
This is how I size it:
btnSound.position = CGPoint(x: self.frame.width * 1 / 5 , y: self.frame.height * (5.2 / 8))
btnSound.size.width = self.frame.width * 1 / 7
btnSound.size.height = btnSound.size.width
btnSound.zPosition = 1
self.addChild(btnSound)
This is the image when in Illustrator (screenshot)and this is the image in the app (screenshot)
Things I have tried:
Making the image PDF
Making the image PNG
Making the PNG 72 DPI, making it 300 DPI
Run on simulator / device (iPhone7)
btnSound.setScale(preDetermineScale)
Using the following function, though I am not familiar with the UIGraphicsBeginImageContext method. The image just comes out blurry with this. Heres the code and the resulting image:
func resizeImage(image: UIImage, newWidth: CGFloat) -> UIImage? {
let scale = newWidth / image.size.width
let newHeight = image.size.height * scale
UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
image.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
func setup() {
let btnSoundImg = UIImage(named: "btnSound")
let resizeBtnSoundImage = resizeImage(image: btnSoundImg!, newWidth: self.frame.width * 1 / 7)
let btnSoundTexture = SKTexture(image: resizeBtnSoundImage!)
btnSound.texture = btnSoundTexture
btnSound.position = CGPoint(x: self.frame.width * 1 / 5 , y: self.frame.height * (5.2 / 8))
btnSound.size.width = self.frame.width * 1 / 7
btnSound.size.height = btnSound.size.width
btnSound.zPosition = 1
self.addChild(btnSound)
}
I am self taught and haven't done a whole lot of programming so I'd love to learn how to do this correctly as I'm only finding solutions for resizing UIImageViews.
Another thought I had was maybe it shouldn't be a spriteNode as its just used for a button?
First up, there's some primitive rules to follow, to get the best results.
Only scale by factors of 2. ie 50%, 25%, 12.5% 6.25% etc.
This way, any four pixels in your original image become 1 pixel in your scaled image, for each step down in scale size.
Make your original image a square of an exponent of 2 in size. So: 128x128, 256x256, 512x512, etc. You've covered this already with your 2048x2048 sizing.
Turn on mipmapping. This is off, by default, in SpriteKit, so you have to switch it on: https://developer.apple.com/reference/spritekit/sktexture/1519960-usesmipmaps
Play with the different filtering modes to get the best reductions of noise and banding in your image: https://developer.apple.com/reference/spritekit/sktexture/1519659-filteringmode hint, linear will probably be better.
As has always been the case, judicious use of Photoshop for manually scaling will give you the best results and least flexibility
I have a relatively straight forward implementation of a progress view set up with CALayer objects. The progress view itself is a subview of UIView.
Here is the code that sets up the progress ring:
self.progressRingLayer = CAShapeLayer()
let innerRect = CGRectInset(bounds, CGFloat(self.lineWidth) / 2, CGFloat(self.lineWidth) / 2)
let innerPath = UIBezierPath(ovalInRect: innerRect)
self.progressRingLayer.path = innerPath.CGPath
self.progressRingLayer.fillColor = UIColor.clearColor().CGColor
self.progressRingLayer.strokeColor = kProgressColor.CGColor
self.progressRingLayer.anchorPoint = CGPointMake(0.5, 0.5)
self.progressRingLayer.transform = CATransform3DRotate(self.progressRingLayer.transform, (CGFloat(M_PI))*1, 0, 0, 1)
self.progressRingLayer.lineCap = kCALineCapRound
self.progressRingLayer.lineWidth = CGFloat(self.lineWidth)
self.layer.addSublayer(self.progressRingLayer)
What I am trying to do now is add a gradient to the progressRingLayer that follows (or bends with) the path. I have been successful in adding a linear gradient to the fill, but not to just the path.
Here is an example of what effect I want:
So far everything I have found requires a bunch of additional steps with CoreGraphics and CGContext that don't quite fit with my implementation. Any help would be great, thanks!
What I would do is draw a gradient layer, then draw on top of that a layer that is black with the arc erased.
Here's my attempt at roughly the image you provided (I omitted the white label in the center, but that's trivial):
And here's the code that generated it:
let r = CGRectMake(100,100,130,100)
let g = CAGradientLayer()
g.frame = r
let c1 = UIColor(
red: 151.0/255.0, green: 81.0/255.0, blue: 227.0/255.0, alpha: 1)
let c2 = UIColor(
red: 36.0/255.0, green: 176.0/255.0, blue: 233.0/255.0, alpha: 1)
g.colors = [c1.CGColor as AnyObject, c2.CGColor as AnyObject];
self.view.layer.addSublayer(g)
let percent = CGFloat(0.64) // percentage of circle
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let con = UIGraphicsGetCurrentContext()
CGContextFillRect(con, CGRect(origin: CGPoint(), size: r.size))
CGContextSetLineWidth(con, 5)
CGContextSetLineCap(con, kCGLineCapRound)
CGContextSetBlendMode(con, kCGBlendModeClear)
let pi = CGFloat(M_PI)
CGContextAddArc(con, r.size.width/2.0, r.size.height/2.0, 30,
-pi/2.0, -pi/2.0 + percent*pi*2.0, 0)
CGContextStrokePath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let b = CALayer()
b.frame = r
b.contents = im.CGImage
self.view.layer.addSublayer(b)
The gradient layer (the first part of the code) is just a "serving suggestion". If that is not the gradient you want, you can design your own. You could draw it in Photoshop and use an image as the content of the gradient layer. Or you could make an "angular" layer in code, using third-party code such as https://github.com/paiv/AngleGradientLayer. The point of the example is merely to show how it is possible to "erase" an arc in a black layer so as to reveal the gradient concealed behind it, and thus appear to paint with a gradient.
I have a relatively straight forward implementation of a progress view set up with CALayer objects. The progress view itself is a subview of UIView.
Here is the code that sets up the progress ring:
self.progressRingLayer = CAShapeLayer()
let innerRect = CGRectInset(bounds, CGFloat(self.lineWidth) / 2, CGFloat(self.lineWidth) / 2)
let innerPath = UIBezierPath(ovalInRect: innerRect)
self.progressRingLayer.path = innerPath.CGPath
self.progressRingLayer.fillColor = UIColor.clearColor().CGColor
self.progressRingLayer.strokeColor = kProgressColor.CGColor
self.progressRingLayer.anchorPoint = CGPointMake(0.5, 0.5)
self.progressRingLayer.transform = CATransform3DRotate(self.progressRingLayer.transform, (CGFloat(M_PI))*1, 0, 0, 1)
self.progressRingLayer.lineCap = kCALineCapRound
self.progressRingLayer.lineWidth = CGFloat(self.lineWidth)
self.layer.addSublayer(self.progressRingLayer)
What I am trying to do now is add a gradient to the progressRingLayer that follows (or bends with) the path. I have been successful in adding a linear gradient to the fill, but not to just the path.
Here is an example of what effect I want:
So far everything I have found requires a bunch of additional steps with CoreGraphics and CGContext that don't quite fit with my implementation. Any help would be great, thanks!
What I would do is draw a gradient layer, then draw on top of that a layer that is black with the arc erased.
Here's my attempt at roughly the image you provided (I omitted the white label in the center, but that's trivial):
And here's the code that generated it:
let r = CGRectMake(100,100,130,100)
let g = CAGradientLayer()
g.frame = r
let c1 = UIColor(
red: 151.0/255.0, green: 81.0/255.0, blue: 227.0/255.0, alpha: 1)
let c2 = UIColor(
red: 36.0/255.0, green: 176.0/255.0, blue: 233.0/255.0, alpha: 1)
g.colors = [c1.CGColor as AnyObject, c2.CGColor as AnyObject];
self.view.layer.addSublayer(g)
let percent = CGFloat(0.64) // percentage of circle
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let con = UIGraphicsGetCurrentContext()
CGContextFillRect(con, CGRect(origin: CGPoint(), size: r.size))
CGContextSetLineWidth(con, 5)
CGContextSetLineCap(con, kCGLineCapRound)
CGContextSetBlendMode(con, kCGBlendModeClear)
let pi = CGFloat(M_PI)
CGContextAddArc(con, r.size.width/2.0, r.size.height/2.0, 30,
-pi/2.0, -pi/2.0 + percent*pi*2.0, 0)
CGContextStrokePath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let b = CALayer()
b.frame = r
b.contents = im.CGImage
self.view.layer.addSublayer(b)
The gradient layer (the first part of the code) is just a "serving suggestion". If that is not the gradient you want, you can design your own. You could draw it in Photoshop and use an image as the content of the gradient layer. Or you could make an "angular" layer in code, using third-party code such as https://github.com/paiv/AngleGradientLayer. The point of the example is merely to show how it is possible to "erase" an arc in a black layer so as to reveal the gradient concealed behind it, and thus appear to paint with a gradient.
I am simply trying to draw a rectangle and placing it in the bottom left hand side of the screen
var rectW:CGFloat = CGFloat(200.0)
var rectH:CGFloat = ceil(self.frame.height * 0.15)
var rect = SKShapeNode(rectOfSize: CGSize(width: rectW, height: rectH));
let posX:CGFloat = 0.0 + (rect.frame.width / 2)
let posY:CGFloat = self.frame.height - (self.frame.height - rect.frame.height)
rect.position = CGPointMake(posX,posY)
rect.fillColor = SKColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
rect.lineWidth = 1
self.addChild(rect)
The issue is that even though i believe all the maths is correct the rectangle is about 38 points/pixels that are under the screen, as you can see from the image below (the white border shows the limit).
For your information here is an output of the positions, heights and widths.
PosX: 100.5
PosY: 117.0
Rect Width: 201.0
Rect Height: 117.0
Frame Height: 768.0
Frame Weight: 1024.0
If i simple take this line but add 38 points/pixels it works, but why? Is there something I am missing?
let posY:CGFloat = self.frame.height - (self.frame.height - rect.frame.height) + 38
I found the answer via the following two answers:
SpriteKit coordinate system messed up
Problems understanding coordinate system SpriteKit using Swift
The problem turns out to be with GameScene.sks having it's size set differently.
Open up GameScene.sks and go to 'Show SKNodeInspector' should be the third box in the top right hand side of the screen.
Then set the dimensions to 320 x 568
Save then run. Should be working.