I'm working on a little App, where I load a tilemap ( JSTilemap ) into sprite kit.
This all works fine except that the tile map looks normal and fine in portrait but when I flip to landscape it seems to blow up. I have not added any code just imported JSTilemap header and this code initwithsize.
self.map = [JSTileMap mapNamed:#"tilemap.tmx"];
[self addChild:self.map];
I have also downloaded a simple project with a tile map and when I rotate the size stays the same, only that if i try to mimic the code my tilemap still seems to blow up when I rotate it.
Is there some setting to restrict this that I am missing?
You should use -(id)initWithSize:(CGSize)size instead of -(void)didMoveToView:(SKView *)view
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
JSTileMap* tiledMap = [JSTileMap mapNamed:#"level1.tmx"];
if (tiledMap)
[self addChild:tiledMap];
self.userInteractionEnabled = YES;
}
return self;
}
Also in GameViewController (if you are using default template classes) you should change scene initializing with sceneWithSize
/*
// Create and configure the scene.
GameScene *scene = [GameScene unarchiveFromFile:#"GameScene"];
scene.scaleMode = SKSceneScaleModeAspectFill;
*/
// Create and configure the scene.
SKScene * scene = [GameScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
I assume by "blow up" you mean stretch and/or zoom inappropriately? Try changing your scaleMode on your SKScene to be SKSceneScaleModeAspectFill.
Just so you know, when running your application the iPhone always begins in Portrait mode. This has nothing to do with Sprite Kit. It's about general iOS lifecycle & view issues. Skewing content or loosing content entirely happens because of this. When making a game usually Portrait or Landscape is used. Using them together is pretty rare. To understand why your content is being "stretched" write the following code into your SKScene's initWithSize method:
NSLog(#"In initWithSize, at %0.f wide and %0.f high", size.width, size.height);
Fire the game up & look at the NSLog message. Even though we think we started in Landscape mode (that is if you tilted your phone & waited for the game to load or did that a bit later), the NSLog message displays "In initWithSize, at 320 wide and 568 high" (I'm using an iPhone 5S). That means the scene is being rendered in Portrait mode even if we selected Landscape mode in the Targets/Deployment Info section. We want the width to be more then the height (568 wide and 320 high)! But why is all of this happening? The issue is internally. The iOS app life cycle begins EVERY application in Portrait and ONLY switches to Landscape during the loading process. So the Storyboard loads, it loads that initial View Controller and it still thinks it's in Portrait orientation. This view controller will automatically load the View object (for Sprite Kit that's the SKView). We're still in Portrait orientation. The View object loaded and that will cause viewDidLoad to be called back in the View Controller. We're still in Portrait orientation. But in the viewDidLoad method that is where we create our scene (SKScene) passing in the current height & width. Still Portrait orientation. All of this is taking a fraction of a second, but it's only after this that the application realizes it's supposed to be in Landscape and the View Controller switches to Landscape taking the View with it. Depending on your settings, what the Scene is going to do is trying to react to this by scaling up to fill the available space and we're going to loose content because internally it's still in Portrait orientation...just scaled up. How do we fix this? Our scene is being created in viewDidLoad, but when this is happening the application DOES NOT yet know we're supposed to be in Landscape mode. So we need to create our scene, but we just need to do it a bit later in the process - after the iOS application life cycle has realized we're in Landscape mode. There's a couple of different methods to do this. The one I like is viewWillLayoutSubviews. It is another built-in View Controller method. It gets called automatically after viewDidLoad and after the shift to Landscape mode. So if we post-pone our scene creation by a few milliseconds until we're in viewWillLayoutSubviews method then we can create it with the right orientation. So copy & paste all the code in viewDidLoad (apart from the call to [super viewDidLoad];) and put it into viewWillLayoutSubviews. There is only 1 more issue now. It is possible viewWillLayoutSubviews will be called a couple of times or at least more then once during the lifecycle. That's ok. We just don't want to create the scene several times. So once we have grabbed hold of the view we're going to create an if statement to check if there's a Scene object in that view. If not, we'll create it and return it. Otherwise, we don't need to do that. Here's how all of this code looks inside the View Controller that creates our Scene.
//Method can be deleted (It's called behind the scenes regardless).
-(void)viewDidLoad
{
[super viewDidLoad];
}
//Called AFTER viewDidLoad & after the view shifts to landscape (which is what we want).
-(void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews]; //Don't really need to call this method (but a good habit just in case Apple ever changes that).
//Configure the view.
_skView = (SKView *)self.view;
_skView.showsFPS = YES;
_skView.showsNodeCount = YES;
//_skView.showsPhysics = YES;
//viewWillLayoutSubviews might get called several times & that's ok. But, we don't want several GameScenes to be created. Therefore...
//Is there a scene object in that view? If there ISN'T, we'll create it.
if (!_skView.scene)
{
//Create & configure the scene.
SKScene *sceneGame = [MainMenuScene sceneWithSize:_skView.bounds.size];
sceneGame.scaleMode = SKSceneScaleModeAspectFill;
//Present the scene.
[_skView presentScene:sceneGame transition:[SKTransition fadeWithDuration:0.5]];
}
//If there is, we don't need to do that.
}
Now go ahead and run this. Everything should look good now. The NSLog message should now say "In initWithSize, at 568 wide and 320 high". Hurray!
Related
I am new to SceneKit and I could use your help with the following:
I have two SCNViews
- a large one (skView) showing the scene for the user which can be manipulated by the user with the standard allowsCameraControl option enabled
- another small one (coordinateCrossView) in the left corner which shows a coordinate cross that I would like to mirror any camera navigation the user performs in the large SCNView
I implemented it by making the ViewController which holds both SCNViews a SCNRendererDelegate and by updating the cross' orientation in the renderer: updateAtTime: method like so:
- (void)renderer:(id<SCNSceneRenderer>)renderer updateAtTime:(NSTimeInterval)time {
[self.coordinateCrossView.scene.rootNode childNodeWithName:#"cross" recursively:YES].orientation = self.skView.pointOfView.orientation;
[self.coordinateCrossView setNeedsDisplay];
}
So far so good. However, this only works when the user is manipulating the large SCNView with a finger on the screen. After as swipe gesture when the finger leaves the screen the object in the large SCNView continues to rotate a bit - but the coordinate cross is not updated.
Any ideas why this behavior is not captured by the pointOfView.orientation changes?
I found out what the problem was: the small coordinateCrossView was not continuously updated and therefore even though the coordinate cross' orientation was changed, the view didn't show it - even though setNeedsDisplay was called.
I fixed the issue by setting the isPlaying property of the coordinateCrossView which forces it to be updated.
In order to define an rectangular edge, this is the code I wrote:
-(void)didMoveToView:(SKView *)view {
/* Setup your scene here */
SKNode *edge = [SKNode node];
edge.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
[self addChild:edge];
}
I want this edge to wrap the whole screen, i.e bottom, top, left and right borders.
I wish that the objects that I add to my scene bounce on all borders. But, those objects only bounce on bottom and top part of the edge.
P.S: The same code, worked about a year ago when SpriteKit GameScene.mclass had -(id)initWithSize:(CGSize)size instead of -(void)didMoveToView:(SKView *)view.
Currently SpriteKit by default loads a scene from .sks file and by default, the scene size is set to 1024x768. That is probably why you are getting unexpected results (self.frame has different size in compare to a view).
There are few things you should keep in mind these days:
When scene is loaded from .sks file, initWithSize: is never called. initWithCoder: is called instead. If you want to use initWithSize: you should create a scene in "old" way - using sceneWithSize:
In initWithSize: the view is always nil, so all the code which requires a view, should be moved to didMoveToView.
In viewDidLoad a final size of a view may not be known yet. A proper implementation of viewWillLayoutSubviews can be used to get around this. Read more here.
What I would suggest you, is to create a scene inside viewWillLayoutSubviews using sceneWithSize: method and initialize it with view.bounds.size. After that, you can create borders like this:
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
Hint: It can be useful to enable visual physics representation. You can do this from view controller:
skView.showsPhysics = YES;
If all this doesn't help, check if your view has correct size. View can be wrongly sized if wrong launch images are supplied.
Hope this helps and make sense.
I have to ask this question because Im utterly puzzled by the mechanics of this. I am trying to check for portrait orientation, and while it works fine in viewdidload for my case, because I only need to make the call once, I still wondered what to do if I needed to check for portrait every time the device was turned back to portrait.
I have the following code
-(void)viewDidLayoutSubviews{
if(UIInterfaceOrientationIsPortrait(self.interfaceOrientation)){
self.adviceLabel.preferredMaxLayoutWidth = [[UIScreen mainScreen]bounds].size.width-25;
[self.adviceLabel setFrame:CGRectMake(0, 0,[[UIScreen mainScreen]bounds].size.width-25 , 103)];
NSLog(#"portrait");
}
First of all "portrait" gets loved 3 times, which I suppose is because I have 3 views inside my VC, duh. But then when I start to turn the screen around, it actually starts to log "portrait" consistently when I "turn to landscape" meaning it logs after I hit landscape mode. Is there any meaning behind this? I hate leaving a subject without understand. Thanks in advance!
Use following code for your goal:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
// do something before rotation
}
or
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
// do something after rotation
}
-viewDidLayoutSubviews method is called every time when view and it's subviews change their frames. This is not good practice to check orientation every time view changes it's frame.
The following is a snippet of the code being used to set up the subview layout on device rotate (I am using the same code for initial setup of the views):
(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
CGRect screenBounds = [[UIScreen mainScreen] bounds];
if(screenBounds.size.height >= 1024) {
[myButton setFrame:CGRectMake((screenBounds.size.width/3), (screenBounds.size.height/100)*81, (screenBounds.size.width/3), (screenBounds.size.height/100)*16)];
}
else {
[myButton setFrame:CGRectMake((screenBounds.size.width/3), (screenBounds.size.height/100)*81, (screenBounds.size.width/3), (screenBounds.size.height/100)*16)];
}
}
What is happening is myButton is being redrawn properly, but trying to interact with the button does nothing. If I go back to portrait, the button is able to be pressed after redraw. If I start in landscape, the button works; but if I later rotate away and then back to landscape, button no longer works. This has me stumped.
So, it's very difficult to tell what's going on in your view without being able to see your storyboard, view hierarchy, etc. However, Xcode has made troubleshooting these types of issues much easier with Live View Debugging.
The first thing you can try is turning on view frames. This will allow you to see the frames and bounds for each control. Most likely what you will see is that your button's frame is in one place, but the bounds is someplace else. You can turn on view frames via Debug -> View Debugging -> Show View Frames.
If you need a more comprehensive overview of your hierarchy -- for example, maybe another view is covering the button -- Xcode now has a really cool feature to capture the view hierarchy. You can manipulate the hierarchy in 3D, filter for certain controls and more. This will allow you to visually inspect the hierarchy at run-time and see what might be going wrong with your button. Debug -> View Debugging -> Capture View Hierarchy.
I'm having a bizarre problem here with iOS8. I've been googling and bashing my head against a wall for a couple of days with no luck. I'm not exactly sure how to even explain it, but here goes...
Apps have been running fine under iOS7, but now compiling using xCode 6/iOS8 I'm having a few strange orientation problems. I'm not using a xib file, but instead creating a window and view programatically. I'm running OpenGL in the view, and then handling everything else inside OpenGL. So, I'm collecting touches and passing them to my GL routines. All has been fine forever, til now.
Now with iOS8 it seems as if the "touch window" is rotating itself so that touches in Landscape are limited to one side of the screen, as if the display window is Landscape, but the touches are all within a Portrait window that is set to on one side of the screen.
What seems bizarre is that touches come through to the view all over the screen as normal, but the Y value stops at 320 and goes no further. The X acts normally. If I turn the device to Portrait (the view controller does not auto rotate), it all works, but in Landscape it switches to this strange behaviour.
I've tried all I can think of without any changes, and am not sure where to begin to work out why this could be happening. Any advice would be greatly appreciated.
On iOS8, UIScreen is now interface oriented. In some (or should I say all of) libraries before iOS8, some checks were done to adjust that lack and swap width and height when the application was in landscape.
You should look at some code that makes some rotation and remove it.
-[UIScreen bounds],
-[UIScreen applicationFrame],
Status bar frame and
Keyboard frame are now interface-oriented.
(cf. WWDC2014 video - view controller advancements in iOS8 # 50:41)
In our case.
Force to rotate with view's transform caused that problem.
We removed that code, rotate with
+ (void)attemptRotationToDeviceOrientation
and problem solved.
We called that method at our forceRotate method.
-(void)forceRotateLandscape {
...
UIViewController *vc = [UIViewController new];
[topVc presentViewController:vc animated:NO completion:nil];
[topVc dismissViewControllerAnimated:NO completion:nil];
supportedOrientations = UIInterfaceOrientationMaskLandscapeRight;
[UIViewController attemptRotationToDeviceOrientation];
...
}
My guess is that whatever view owns the gesture recognizer isn't getting resized during the rotation for one (or more) of the following reasons:
It uses auto layout and isn't pinned to its superview
It uses springs and struts and doesn't have the correct UIViewAutoresizing mask
It lives in a subview that isn't getting resized
If you're using auto layout, try this in your view controller:
- (void)viewDidLayoutSubviews
{
// For each subview with its own auto layout:
[self.mySubview layoutSubviews];
// ...
[super viewDidLayoutSubviews];
}
Ok, I've found the issue on my side (I'm the bounty giver).
We were probably using some kind of hack in our App to manually handle the rotation of our views, because we were returning 0 to the - (NSUInteger)supportedInterfaceOrientations in our rootViewController.
- (NSUInteger)supportedInterfaceOrientations {
return 0; // BOOOOOOO, even forbidden by documentation
}
Then we manually managed our internal views by listening to the UIDeviceRotationChanged notification, rotating them views, and asking the statusBar to rotate as well.
Returning something not 0 to this supportedInterfaceOrientations made the "touch" layer rotate along with the rest.
But now we have to make the code evolve to use iOS rotation instead of our hacky layer.
Also, after investigating more, especially on iPad, you have to use a Storyboard-based UIWindow.
Using a XIB-based window will result in its dimensions being wrong when the app is launched while in landscape. This leads to a mountain of buggy frames and messes up badly with our layout.
I had the same problem. In iOS7 all worked fine, in iOS8 it was like the "touch-view" was still in portrait.
I just replaced the main xib with a storyboard, no other change (i've tried a lot playing with screen bound and frame without any results).
Just try it out, let me know if it work also for you. Good luck.
I have the same issue with second window in landscape. It refuses to display itself on the full screen and also to receives touch events on the cut part of the screen. The only solution that I found so far is to enable support of the iPhone6/6+ native resolutions by adding required launch screens. If you do not support these resolutions in your app that might be the same problem.