Centering SKSpriteNode in landscape mode by programing. Swift - ios

I am trying to create a game that is only in landscape mode and I have an SKSpriteNode that I am trying to center it to middle of the screen no matter what size of device the person is using. I have tried many things but it does not seem to be working well.
Thank you for your help.

The best option which will place a node at the center of the screen (which will actually place the node at the center of the scene) is something like this:
let sprite = SKSpriteNode(color: .purpleColor(), size: CGSize(width: 50, height: 50))
sprite.position = CGPoint(x:frame.midX, y:frame.midY)
addChild(sprite)
This will place your node at the center of the screen no matter how your scene is sized (if it is bigger than a view, or smaller) or what scaleMode you actually use.

Related

Swapped width and height values

Explanation
For some reason, width and height values are being swapped in my game (as you can see below) that is set to be in landscape orientation. This way, the sprite that should be centered in the screen, is totally off the right position.
Code
You can download it here.
GameScene
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
print("width:", self.frame.width)
print("height:", self.frame.height)
//Sprite
let sprite = SKSpriteNode (imageNamed: "sprite")
sprite.anchorPoint = CGPointZero
sprite.position = CGPoint(x: self.frame.width / 2 - sprite.frame.width / 2, y: self.frame.height / 2 - sprite.frame.height / 2)
addChild(sprite)
}
}
GameViewController
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Set view size.
let scene = GameScene(size: view.bounds.size)
// Configure the view.
let skView = view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .ResizeFill
skView.presentScene(scene)
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .Landscape
} else {
return .Landscape
}
}
}
Thanks in advance,
Luiz.
If you go to targets-General in your xCode project (where you set version number, bundle ID etc) did you untick portrait? You also might have to check your info.plist to see if all portrait entries are removed for both devices.
You approach unfortunately is bad practice a lot of tutorials teach you and I would not continue like this.
If you use "scaleMode ResizeFill" or set the scene size to "view.bounds" your game will never look consistent on all devices. Furthermore all your values (sprite sizes, font sizes, physics values etc) will also not be the same on devices different to the one you are testing on.
Basically you will have to adjust for all of this on like 5-6 devices and its madness, especially using xCode simulator. Trust me I have been there before with 2 games and it nearly killed me. Its a game of game of "yeah this looks about right".
Dont go through this, so what you should do is
1) Change your scene size to the default scene size used by xCode
GameScene(size: CGSize(width: 1024, height: 768)) // swap if portrait
Note: Check update at bottom
If you dont do this, and leave it at view.bounds.size, point 2 will not work.
2) Change your scene scale mode to .AspectFill (also default xCode settings).
This way all your stuff will look great on all iPhones. Your values will scale correctly and you save yourself a lot of work.
On iPads you will have some extra space at the top and bottom (landscape) or left and right (portrait) which you usually just cover with some more background and have as a non playable area
Your scene basically looks like this now.
The red area is your whole scene size (iPad) but if you run on iPhones you will only see the green bit. In Portrait mode that red area would be on the left and right side.
Thats why y positioning is usually done from the centre because if you use frame.minY or frame.maxY you would be in the red zone and won't see the sprite on iPhones. That red zone you just cover with some more background (your background full screen images should be iPad size).
This also make your game more balanced, because if your would just stretch up your game than it will be easier on iPads because you have more space.
So design your game within the green zone. Than on iPads the only thing you might have to do is move up some UI, like pause button or score label, when you want to show them at the top edge. I do it this way.
// Some button
...
someButton.position = CGPoint(x: frame.midX, y: frame.midY + 200)
addChild(someButton)
if UIDevice.current.userInterfaceIdiom == .pad {
// set for iPad
}
// Some label
someLabel.positon = CGPoint(x: frame.midX - 100, someButton.position.y) // because its y position is the same as someButton I only have to adjust someButton.position.y on iPads.
addChild(someLabel)
I would not continue with your approach, even if it means redoing a lot of work.
UPDATE:
It seems with xCode 8 apple changed the default scene size from 1024x768 (768x1024) to 1334x750 (750x1334). I have only played around with those settings for a bit and they seem confusing for a universal game.
So you scene would now look like this and stuff on the xAxis on iPads is offscreen.
That makes no sense as the iPad is clearly not more widescreen than an iPhone.
I actually opened a bug report to see what apple says about this so for now I would probably keep it at 1024x768.
Hope this helps
Override viewWillLayoutSubviews in your GameViewController to ensure you get the proper width/height of the view. The implication with this also is that any objects you may instantiate would need to wait until this method has been called.
So you would want to do something like
class GameViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// NOTE: Add code to do this only once!
// Set view size.
let scene = GameScene(size: view.bounds.size)
// Configure the view.
let skView = view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .ResizeFill
skView.presentScene(scene)
}
Note I didn't try and compile this and it is missing a trailing }. Additionally, you would want to do this only once, I haven't added the code to do that. In my game, my scene is a property so I just check to see if it is nil and if it is, then I create the scene and present.
Following your code and according to how do you want to construct your sprite and position in GameScene you simply must change the scene.scaleMode to AspectFill or AspectFit (depend by what do you want to achieve):
sources:
#available(iOS 7.0, *)
public enum SKSceneScaleMode : Int {
case Fill /* Scale the SKScene to fill the entire SKView. */
case AspectFill /* Scale the SKScene to fill the SKView while preserving the scene's aspect ratio. Some cropping may occur if the view has a different aspect ratio. */
case AspectFit /* Scale the SKScene to fit within the SKView while preserving the scene's aspect ratio. Some letterboxing may occur if the view has a different aspect ratio. */
case ResizeFill /* Modify the SKScene's actual size to exactly match the SKView. */
}

SpriteKit layout issues (Swift)

I'm new to SpriteKit and Swift (done a lot in Obj-C in the past for iOS) but I'm struggling on something that should be very basic.. positioning sprites on the screen!
Centering a label on the screens seems easy enough:
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));
But when I try to put in the Bottom-left or top-right of the screen it isn't visible:
//Bottom-left label
myLabel.horizontalAlignmentMode = .Left
myLabel.position = CGPoint(x: 0.0, y: self.size.height)
or
//Top-right label
myLabel.horizontalAlignmentMode = .Right
myLabel.position = CGPoint(x: self.size.width, y: self.size.height)
This is with a new project, no other code. What am I doing wrong, please?
What you have to keep in mind is that a SpriteKit scene by default takes up far more than the screen. Its default size is 1024 by 768 and everything is scaled for universality unless you tell it otherwise. In your view controller you could see how the scene is initialized. You will see that it probably has a statement that says something like
scene.scaleMode = .AspectFill
This is the source of this. Apple in the documentation explains this in a straightforward manner what happens when you use AspectFill scaleMode:
The scaling factor of each dimension is calculated and the larger of the two is chosen. Each axis of the scene is scaled by the same scaling factor. This guarantees that the entire area of the view is filled but may cause parts of the scene to be cropped.
There are other scale Modes to explore, which you can try but Aspect Fill has a large advantage with its universality.
You can specify the size of an SKScene when you create it. Typically you pass the size of the containing SKView so it covers the entire view. If the device rotates, your view is resized (if your app allows the new orientation). I'm not sure if the SKScene if resized. But either way, you need to update the position of your scene contents so that for example the label is in the top-right with the new dimensions.
The view controller whose view presents the scene receives the message viewWillTransitionToSize(_ size:,withTransitionCoordinator coordinator:). You can subclass this method to send a message to a custom method of the presenting SKScene that the size is changing. Then, you can simply reposition the node using the new size since self.frame will update.
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));
Back to the core of the question! How to position a label in the top-right of the scene? Take into account that the scene origin is bottom-left. For SKLabelNode, the center is really in the center. So, offset by half of the label's dimensions.
myLabel.position = CGPoint(x: self.size.width - myLabel.size.width /2,
y: self.size.height - myLabel.size.height/2)
By the way, I have never seen an iOS game which uses a sprite based engine (such as Sprite Kit or Cocos2d) which supports device rotation/multiple screen sizes when running on the same device (iPhone/iPad).

Why is my rectangle being drawn wrong?

I'm trying to draw a rectangle around my screen. Here's my code:
let viewRect = SKShapeNode(rect: self.view!.frame)
viewRect.strokeColor = SKColor.whiteColor()
viewRect.lineWidth = 2.0
addChild(viewRect)
I'm using self.view!.frame instead of self.frame because my scene is not the same size as my screen (the scene is stretched to fill my screen).
I would have expected the code to draw a rectangle around my screen, but it draws this instead (the white rectangle is what is drawn):
Anyone know why it's not being drawn around my screen?
Scene size: 1024x768
View size: 414x736
Scale mode: Aspect Fill
You are creating your border as a node within the scene but then setting its coordinates according to the view that is presenting the scene. The SKScene's coordinate system is not generally the same as the view presenting it so that is why the shape is different to what you expect.

Set permanent SKSpritenode image orientation

Hi I have a bunch of round SKSpriteNodes with a circle physical body. Now when these balls roll down a path I want some of these SKSpritenodes image to stay upright even when rolling. So think of an arrow pointing upwards. When the ball starts rolling the arrow spins in circles. But for some balls Id like the arrow to remain pointing up even when the ball rolls. Whats the best way of doing this?
Edit
So an answer was given but from testing it turns out it is not the correct one. Not allowing the ball to rotate affects the way it rolls down the path. So I guess what I want is rotation to be on but the image to always appear to the user like its not rotating. Thanks.
This looks like a job for SupermSKConstraint. Constraints are evaluated and applied after the physics simulation runs on each frame, so you can use them for tasks like making a node point a certain direction regardless of what physics does to it. For this, you'd want a zRotation constraint.
But there's a bit more to it than that. If you set a zero-rotation constraint on the ball:
// Swift
let constraint = SKConstraint.zRotation(SKRange(constantValue: 0))
ball.constraints = [constraint]
You'll find that SpriteKit resets the physics body's transform every frame due to the constraint, so it only sort-of behaves like it's rolling. Probably not what you want. (To get a better idea what's going on here, try adding a zero-rotation constraint to a rectangular physics body in a world without gravity, applying an angular impulse to it, and watching it try to spin in a view with showsPhysics turned on. You'll see the sprite and its physics body get out of sync and shake a bit -- probably due to accumulated rounding errors as the physics engine and the constraint engine fight it out.)
Instead, you can do a bit of what's in 0x141E's answer, but use constraints to make it less code (and run more efficiently):
Give the ball node a circular physics body. (And possibly no texture, if the only art you want for the ball is a non-rotating sprite.)
Add the arrow node as a child of the ball node. (It doesn't need its own physics body.)
Put a zero-rotation constraint on the arrow.
Wait, that doesn't work -- I told the arrow to not rotate, but it's still spinning?! Remember that child nodes are positioned (and rotated and scaled) relative to their parent node. So the arrow isn't spinning relative to the ball, but the ball is spinning. Don't worry, you can still solve this with a constraint:
Tell the constraint to operate relative to the node containing the ball (probably the scene).
Now the constraint will keep the arrow in place while allowing the ball to rotate however the physics simulation wants it to.
Here's some test code to illustrate:
// Step 1: A rectangular spinner so we can see the rotation
// more easily than with a ball
let spinner = SKSpriteNode(color: SKColor.redColor(), size: CGSize(width: 300, height: 20))
spinner.position.x = scene.frame.midX
spinner.position.y = scene.frame.midY
spinner.physicsBody = SKPhysicsBody(rectangleOfSize: spinner.size)
scene.addChild(spinner)
spinner.physicsBody?.applyAngularImpulse(0.1) // wheeeeee
// Step 2: Make the arrow a child of the spinner
let arrow = SKSpriteNode(color: SKColor.greenColor(), size: CGSize(width: 20, height: 50))
spinner.addChild(arrow)
// Step 3: Constrain the arrow's rotation...
let constraint = SKConstraint.zRotation(SKRange(constantValue: 0))
arrow.constraints = [constraint]
// Step 4: ...relative to the scene, instead of to its parent
constraint.referenceNode = scene
Here are two methods to create a ball with a physics body and an arrow:
Add an arrow as a child of a ball
Add both the ball and the arrow directly to the scene
Here's what will happen when you add the above to the SpriteKit simulation:
The arrow will rotate when the ball rotates
Both the arrow and the ball will move/rotate independently
If you want the arrow to rotate with the ball, choose Option 1. If you want the arrow to remain fixed, choose Option 2. If you choose Option 2, you will need to adjust the rotation of the arrow to ensure that it points upward. Here's an example of how to do that.
-(void)didMoveToView:(SKView *)view {
self.scaleMode = SKSceneScaleModeResizeFill;
/* Create an edge around the scene */
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:view.frame];
// Show outline of all physics bodies
self.view.showsPhysics = YES;
CGFloat radius = 16;
SKNode *balls = [SKNode node];
balls.name = #"balls";
[self addChild:balls];
// Create 5 balls with stationary arrows
for (int i = 0;i<5;i++) {
// Create a shape node with a circular physics body. If you are targeting iOS 8,
// you have other options to create circular node. You can also create an SKSpriteNode
// with a texture
SKShapeNode *ball = [SKShapeNode node];
// Create a CGPath that is centered
ball.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(-radius,-radius,radius*2,radius*2)].CGPath;
ball.fillColor = [SKColor whiteColor];
ball.position = CGPointMake(100, 100+i*radius*2);
ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:radius];
[balls addChild:ball];
// Create an arrow node
CGSize size = CGSizeMake(2, radius*2);
SKSpriteNode *arrow = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size:size];
arrow.name = #"arrow";
arrow.position = CGPointZero;
[ball addChild:arrow];
// Apply angular impulse to the ball so it spins when it hits the floor
[ball.physicsBody applyAngularImpulse:-1];
}
}
- (void) didSimulatePhysics
{
SKNode *balls = [self childNodeWithName:#"balls"];
for (SKNode *ball in balls.children) {
SKNode *arrow = [ball childNodeWithName:#"arrow"];
arrow.zRotation = -ball.zRotation;
}
}
sprite.physicsBody.allowsRotation = NO;
The allowRotation property should control exactly what you are asking.

SpritKit background image has margins

I am following a tutorial on SpriteKit and when I add my background image to the scene, the image does not fill up the entire scene (has margins).
The project is a universal project, and I am using size classes (same effect when I turn it off).
I am new to Swift and SpriteKit, so maybe I am missing something. Here is my code (from a brand new project):
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.anchorPoint = CGPointMake(0.5, 0.5)
let background = SKSpriteNode(imageNamed: "Background")
background.position = CGPointMake(0, 0)
background.size = CGSizeMake(self.view.bounds.size.width, self.view.bounds.size.height)
self.addChild(background)
}
And here is a screenshot:
I added the position and size code just to be sure. Also, frame.size is showing (1024, 768) but bounds.size shows the proper (640, 1136). Any help will be greatly appreciated!
You will find that in your ViewController.swift, the current scale mode is .AspectFill which is sometimes perfectly find to use but in your case, you need to change that to
scene.scaleMode = .ResizeFill
which will make sure that the size fits perfectly into any size screen. You then use self.view.frame.size instead of self.view.bounds.size and you are good to go! Ask if you need any more information, though the documentation explains the scaleModes quite well:
https://developer.apple.com/library/prerelease/ios/documentation/SpriteKit/Reference/SKScene_Ref/index.html#//apple_ref/c/econst/SKSceneScaleModeFill

Resources