In Swift Playgrounds for iPad, I am having difficulty constraining CAEmitterLayer instance to the center permanently. For example, it stays centered until the device is rotated, so I need to find some way to constrain it to the center for all device orientations.
Here is what I have so far, keep in mind I’ve tried adding observers to device orientation with no avail. But this could just be an error on my part. Thanks!
emitterLayer.emitterPosition = CGPoint(x: view.center.x, y: view.center.y)
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 30
cell.velocity = 250
cell.velocityRange = 250
cell.spinRange = 5
cell.scale = 0.04
cell.scaleRange = 0.03
cell.alphaSpeed = -0.15
cell.emissionRange = CGFloat.pi * 2
cell.contents = UIImage(named: "RainbowApple")?.cgImage
emitterLayer.emitterCells = [cell]
emitterLayer.beginTime = CACurrentMediaTime()
emitterLayer.zPosition = -1
view.layer.addSublayer(emitterLayer)
emitterLayer.emitterSize = CGSize(width: 320, height: 1)
emitterLayer.renderMode = CAEmitterLayerRenderMode.oldestLast
CALayer does not support constraints. It looks like you want to do this in a UIViewController so move the line emitterLayer.emitterPosition = CGPoint(x: view.center.x, y: view.center.y) into viewDidLayoutSubviews which gets called every time your views bounds change (ie during rotation). This will always keep your emitter in the center, even if the center changes for another reason, such as split screen multitasking on iPad.
Related
I'm having the issue on a simple but fast-paced SpriteKit game, but I've reduced my code just to a bouncing ball and still get the issue to a lesser extent:
override func didMove(to view: SKView) {
super.didMove(to: view)
physicsWorld.contactDelegate = self
physicsWorld.speed = 1
physicsWorld.gravity = CGVector(dx: 0.0, dy: 0.0)
let borderBody = SKPhysicsBody(edgeLoopFrom: self.frame)
borderBody.friction = 0
self.physicsBody = borderBody
borderBody.contactTestBitMask = BallCategory
addBall()
}
func addBall() {
let size = CGSize(width: 20, height: 20)
let position = CGPoint(x: frame.width / 2, y: 50)
let texture = SKTexture(image: #imageLiteral(resourceName: "whiteCircle"))
let ball = SKSpriteNode(texture: texture, size: size)
ball.position = position
ball.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
ball.fillColor = .white
ball.lineWidth = 0
addStandardProperties(node: ball, name: "ball", z: 5, contactTest: 0, category: BallCategory)
ball.physicsBody?.isDynamic = true
addChild(ball)
launchBall()
}
func addStandardProperties(node: SKNode, name: String, z: CGFloat, contactTest: UInt32, category: UInt32) {
node.name = name
node.zPosition = z
node.physicsBody?.isDynamic = false
node.physicsBody?.affectedByGravity = false
node.physicsBody?.mass = 0
node.physicsBody?.restitution = 1
node.physicsBody?.friction = 0
node.physicsBody?.linearDamping = 0
node.physicsBody?.angularDamping = 0
node.physicsBody?.angularVelocity = 0
node.physicsBody?.contactTestBitMask = contactTest
node.physicsBody?.categoryBitMask = category
}
func launchBall() {
let ball = childNode(withName: "ball")!
ball.physicsBody?.velocity = CGVector(dx: 0, dy: 500)
}
This code results in a ball (SKSpriteNode) bouncing up and down. When I run this, CPU usage starts at around 10% on my iPhone 6s and then after increases to around 25-30% after maybe 30-60 seconds (no idea why it's increasing). Throughout all of this, the frame rate stays very close to 60 FPS, usually going no lower than 58 FPS (it's the same way when I run the full game).
Almost any time an alert pops up (e.g., text messages, logging into Game Center, etc.), the lag shows up and shows up at random times when I'm running the full game.
I've also tried deleting and re-running the app, cleaning the project, deleting derived data and running in Release mode. None of these worked permanently.
Should I give up on SpriteKit and try another framework? If so, which? Cocos2D?
Any help is appreciated.
This is the result of Apple prioritising system calls over just about everything else.
When the system wants to know something, check something or otherwise do its thing it does so at the mercy of everything else.
No other engine will be able to help with this, there's no way to silence the system's constant activities through code.
You can get a slight improvement by putting on Flight Mode and turning off WIFI and Bluetooth. The system seems to be somewhat aware that it's in a quieter mode and does less because it's got no 4G or other connectivity it can go communicating with.
Further, there's been some pretty big changes to palm rejection in iOS 11 that's played havoc with the first round of iPad Pro models and creative software, creating multi-second rejection of all touch input. When this kind of thing can make it through to a GM you can be pretty sure they're slipping other messiness through.
Here's some complaints about iOS 11 performance: https://www.macrumors.com/2017/09/25/ios-11-app-slowdowns-performance-issues/
Turns out I had 2 SKViews in my view controller. By default, when you start a project as a SpriteKit game, Xcode sets the view controller root/superview of the GameViewController as an SKView. At some point, I had added a second SKView because I didn't intend for the scene to take up the entire screen and I apparently didn't realize that the VC root view was still set as an SKView. So every time GameViewController loaded, it was loading two SKViews, which is why I saw 120 FPS in Xcode.
I fixed the issue by simply removing the SKView class designation from the VC root view.
Ok, so I've been trying to position a line on my iPhone 6s+ and my iPad 2. The line was added in an SKCamera (I don't know if that effects it or not). Here is the code for the line:
var leftWall = SKSpriteNode()
leftWall.size = CGSize(width: 1, height: 10000)
leftWall.position = CGPoint(x: 0, y: 0)
leftWall.color = UIColor.red
leftWall.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: leftWall.size.width, height: leftWall.size.height))
leftWall.physicsBody?.affectedByGravity = false
leftWall.physicsBody?.isDynamic = false
leftWall.physicsBody?.categoryBitMask = groundGroup
leftWall.physicsBody?.collisionBitMask = ballGroup
leftWall.physicsBody?.contactTestBitMask = 0
theCamera.addChild(leftWall)
Okay, so the x position is 0,0 right? Here is how it looks on my iPhone 6s+:
As you see, the line is in the middle?
Here is how it looks on my iPad 2:
I just don't understand why the position of the line is completely different.. Please help me!
You are not taking into account the cropping that happens on the iPad, or your scaleMode is all wrong
What you need to do is set all nodes anchor points (Including the scene) to (0.5,0.5), make sure your GameScene has a static size and not based on view.frame (750,1334 is a good one) and that your scale mode is set to .aspectFill. This makes (0,0) the center of the screen, and you will notice everything line up on all devices
I have an image named "Ghost" and it moves throughout the screen. I want to have the SpriteNode constantly turning without being pressed on.
Ghost = SKSpriteNode(imageNamed: "Ghost1")
Ghost.size = CGSize(width: 50, height: 50)
Ghost.position = CGPoint(x: self.frame.width / 2 - Ghost.frame.width, y: self.frame.height / 2)
Ghost.physicsBody = SKPhysicsBody(circleOfRadius: Ghost.frame.height / 1.4)
Ghost.physicsBody?.categoryBitMask = PhysicsCatagory.Ghost
Ghost.physicsBody?.collisionBitMask = PhysicsCatagory.Ground | PhysicsCatagory.Wall
Ghost.physicsBody?.contactTestBitMask = PhysicsCatagory.Ground | PhysicsCatagory.Wall | PhysicsCatagory.Score
Ghost.physicsBody?.affectedByGravity = false
Ghost.physicsBody?.isDynamic = true
Ghost.zPosition = 2
self.addChild(Ghost)
I have looked for some answers on Stack Overflow but I can't seem to find any one with this same question, please help.
To have the SKSpriteNode constantly rotating, add this to wherever you want to start the rotating:
let rotate = SKAction.rotate(byAngle: CGFloat.pi * 2.0, duration: 2)
Ghost.run(SKAction.repeatForever(rotate), withKey: "rotateGhost")
To rotate in the opposite direction, change CGFloat.pi to -CGFloat.pi
You can fidget with the duration to change the duration for a complete rotation.
If you wish to remove the action, call:
removeAction(forKey: "rotateGhost")
Or you can just pause the action instead.
You could apply a force of an angular type, so as to rotate the body, and turn off damping so it continues to rotate forever:
https://developer.apple.com/reference/spritekit/skaction/1417775-applyangularimpulse
Or, apply torque:
Firstly, make sure it’s able to rotate by setting this to true:
https://developer.apple.com/reference/spritekit/skphysicsbody/1519986-allowsrotation
Make sure there’s no angular damping so that it doesn’t slow in rotation rate:
https://developer.apple.com/reference/spritekit/skphysicsbody/1519913-angulardamping
Now make it spin by applying a torque:
https://developer.apple.com/reference/spritekit/skphysicsbody/1519588-applytorque
Or, set a spin rate:
https://developer.apple.com/reference/spritekit/skphysicsbody/1519766-angularvelocity
I'm having problems opening my game in different divices , in the 6s iphone plus looks much bigger the circle the center, also the small circle that is on the line changes position , I would like that the center circle was the same size and that the small circle always this half on the line.
import SpriteKit
struct Circle {
var position:CGPoint
var radius:CGFloat
}
class GameScene: SKScene {
let node = SKNode()
let sprite = SKShapeNode(circleOfRadius: 6)
var rotation:CGFloat = CGFloat(M_PI)
var circles:[Circle] = []
var circuloFondo = SKSpriteNode()
var orbita = SKSpriteNode()
let padding2:CGFloat = 26.0
let padding3:CGFloat = 33.5
let padding5:CGFloat = 285.5
var circulo = SKSpriteNode()
override func didMoveToView(view: SKView) {
scaleMode = .ResizeFill
backgroundColor = UIColor(red: 0.3, green: 0.65, blue: 0.9, alpha: 1)
orbita = SKSpriteNode(imageNamed: "orbita2")
orbita.size = CGSize(width:view.frame.size.width - padding2 , height: view.frame.size.width - padding2)
orbita.color = UIColor.whiteColor()
orbita.colorBlendFactor = 1
orbita.alpha = 1
orbita.position = view.center
self.addChild(orbita)
orbita.zPosition = 3
circuloFondo = SKSpriteNode(imageNamed: "circuloFondo")
circuloFondo.size = CGSize(width:view.frame.size.width - padding5 , height: view.frame.size.width - padding5)
circuloFondo.color = UIColor.whiteColor()
circuloFondo.alpha = 1
circuloFondo.position = view.center
self.addChild(circuloFondo)
circuloFondo.zPosition = 0
let radius1:CGFloat = (view.frame.size.width - padding3)/2 - 1
let radius2:CGFloat = (view.frame.size.width - padding5)/2 + 6.5
circles.append(Circle(position: view.center, radius: radius1))
circles.append(Circle(position: view.center, radius: radius2))
addChild(node)
node.addChild(sprite)
if let circle = nextCircle() {
node.position = circle.position
sprite.fillColor = SKColor.whiteColor()
sprite.zPosition = 4.0
sprite.position = CGPoint(x:circle.radius, y:0)
rotate()
}
You can get the width of the screen like this:
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
and then set elements in your UI to be a proportion of the screenWidth. For instance:
let radius1:CGFloat = screenWidth/4
//this would always give you a radius that is one quarter of the screen width
I've used this method a few times with success, hope it works for you.
Your main problem is your scene mode is set to ResizeFill. This will cause you so many headaches as you will have to do all the scaling yourself, hence the circles are different sizes on different devices. Having scale mode resizeFill will also affect things such as the physics engine or fontSizes which you will need to adjust for on every device.
I would recommend you use scene scale mode .AspectFill with the default scene size of 1024*768 (landscape) or 768*1024 (portrait). This is the same as the xCode default game template.
This way everything will look exactly the same on all iPhones. On iPads there will be slightly more screen space at the top and bottom which you simply cover with your background. The main trick is that you position your stuff from the center.
Furthermore you can use the universal assets in the asset catalogue and everything will look great and not blurry.
The only thing that you might have to adjust for this way is that on iPads you might need to move some buttons up/down if you want them on the top/bottom edge.
I strongly recommend you consider this as I can talk from experience that using scale mode ResizeFill is really bad. I have been through this pain with 2 games before I rewrote them because they were so inconsistent on all devices causing me so many bugs in the process. Lets not talk about the time I wasted testing on all devices, adjusting values until it felt right.
Hope this helps.
I originally created my game with iOS 8 and tested on my iPhone 6S. The game looks fine in the 5, 5S, 6, 6 Plus, 6S, and 6S Plus (since all devices have the same ratio of 16:9). As you can see from the image, the music button is offset from the top right corner. The image is offset by this code:
muteButton.position = CGPoint(x: CGRectGetMidX(self.frame) + 920, y: CGRectGetMidY(self.frame) + 480)
The problem I have is if someone tried this game on an iPad, it will display this. As you can see, the bottom graphic and the mute button are offset from the sides by a lot.
I want to make it so that the objects will always stay close to the sides of the frame/view. Making the app "universal" on xCode does not fix it either. Or do I just make a completely new project built for the iPad?
Don't forget about the 4s, you will get the same problems as iPad. SpriteKit does not have constraints like in the UI builder, so you are going to have to accommodate for the 4:3 and the 16:9 devices by applying some math, or force the 4:3 to be 16:9 with black borders using the .AspectFit scaling method.
Now I am not sure where 920, and 480 are coming from, but those numbers may have to be tweaked in this code when detecting the device. Simplest way to determine your aspect ration is to do UIScreen.mainScreen().bounds.width/UIScreen.mainScreen().bounds.height, then work from there.
Solution! I figured it out! For those who come from the future and also might need help with this. This works with landscape orientation and portrait orientation.
Note: You must have your scene.scaleMode set 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.
I have declared the following variable on the top of my class.
class StartScene: SKScene {
let playableArea: CGRect
}
Then, I have the following code inside the override init() function.
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)
}
From here, I then use the variable playableArea to position my objects.
titleChild.position = CGPoint(x: CGRectGetMidX(playableArea), y: CGRectGetMaxY(playableArea) - (titleChild.size.height * 0.90))
Works amazing. Looks good in the iPhone 4S, 5, 5S, 6, 6 Plus, 6S, 6S Plus, and iPads.
If you want to see the box in the app to make sure you did it right, use the following function.
func drawPlayableArea() {
let shape = SKShapeNode()
let path = CGPathCreateMutable()
CGPathAddRect(path, nil, playableArea)
shape.path = path
shape.strokeColor = SKColor.redColor()
shape.lineWidth = 8
addChild(shape)
}
Then just call the function in the didMoveToView() function to view the red frame to make sure you did the code right. This will create a red frame the size of the view that is viewable to the user. Now that you have playableArea to hold the frame that the user can see, you can use it for other things such as making sure objects don't or can't leave the bounds, etc. For this screenshot, I use it to prevent the user from moving the spaceship outside the device.