3

I am using SpriteKit to write a game for iOS in Swift. I am still fairly new to SpriteKit.

I would like to support both orientations for both iPhone and iPad, and have found this: multiple orientations in SpriteKit

This works as expected in the simulator and device, however on device I notice some SKSpriteNodes distort to their new size slightly before the device rotation animation.

This is very noticeable especially with SKLabelNodes where the text distorts either slightly squashed or stretched depending on the orientation change.

I have an idea why the distortion is happening, but confirmation and a fix would be fantastic.

This occurs on device with the code described in the link, but I have updated for swift 3

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let scene = GameScene(size:self.view.bounds.size)
        scene.scaleMode = .resizeFill
        (self.view as! SKView).presentScene(scene)
    }

    override var shouldAutorotate: Bool {
        return true
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return .allButUpsideDown
        } else {
            return .all
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Release any cached data, images, etc that aren't in use.
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }
}

class GameScene: SKScene {
    var currentNode: CustomNode!

    override func didMove(to view: SKView) {
        self.backgroundColor = SKColor.white
        transitionToScene(sceneType: .Menu)
    }
    override func didChangeSize(_ oldSize: CGSize) {
        currentNode?.layout()
    }
    func transitionToScene(sceneType: SceneTransition) {
        switch sceneType {
        case .Menu:
            currentNode?.dismissWithAnimation(animation: .Right)
            currentNode = MenuNode(gameScene: self)
            currentNode.presentWithAnimation(animation: .Right)

        case .Scores:
            currentNode?.dismissWithAnimation(animation: .Left)
            currentNode = ScoresNode(gameScene: self)
            currentNode.presentWithAnimation(animation: .Left)

        default: fatalError("Unknown scene transition.")
        }
    }
}

class CustomNode: SKNode {
    weak var gameScene: GameScene!

    init(gameScene: GameScene) {
        self.gameScene = gameScene
        super.init()
    }
    func layout() {}
    func presentWithAnimation(animation:Animation) {
        layout()
        let invert: CGFloat = animation == .Left ? 1 : -1
        self.position = CGPoint(x: invert*gameScene.size.width, y: 0)
        gameScene.addChild(self)
        let action = SKAction.move(to: CGPoint(x: 0, y: 0), duration: 0.3)
        action.timingMode = SKActionTimingMode.easeInEaseOut
        self.run(action)
    }
    func dismissWithAnimation(animation:Animation) {
        let invert: CGFloat = animation == .Left ? 1 : -1
        self.position = CGPoint(x: 0, y: 0)
        let action = SKAction.move(to: CGPoint(x: invert*(-gameScene.size.width), y: 0), duration: 0.3)
        action.timingMode = SKActionTimingMode.easeInEaseOut
        self.run(action, completion: {self.removeFromParent()})
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}


class MenuNode: CustomNode {
    var label: SKLabelNode
    var container: SKSpriteNode

    override func layout() {
        container.position = CGPoint(x: gameScene.size.width/2.0, y: gameScene.size.height/2.0)
    }
    override init(gameScene: GameScene) {
        label = SKLabelNode(text: "Menu Scene")
        label.horizontalAlignmentMode = .center
        label.verticalAlignmentMode = .center
        container = SKSpriteNode(color: UIColor.black, size: CGSize(width: 200, height: 200))
        container.addChild(label)
        super.init(gameScene: gameScene)
        self.addChild(container)
        self.isUserInteractionEnabled = true
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.gameScene.transitionToScene(sceneType: .Scores)
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class ScoresNode: CustomNode {
    var label: SKLabelNode
    var container: SKSpriteNode

    override func layout() {
        container.position = CGPoint(x: gameScene.size.width/2.0, y: gameScene.size.height/2.0)
    }
    override init(gameScene: GameScene) {
        label = SKLabelNode(text: "Scores Scene")
        label.horizontalAlignmentMode = .center
        label.verticalAlignmentMode = .center
        container = SKSpriteNode(color: UIColor.black, size: CGSize(width: 200, height: 200))
        container.addChild(label)
        super.init(gameScene: gameScene)
        self.addChild(container)
        self.isUserInteractionEnabled = true
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.gameScene.transitionToScene(sceneType: .Menu)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

enum SceneTransition{
    case Menu, Scores
}
enum Animation {
    case Left, Right, None
}

credit: Epic Byte

I have also tried using

viewWillTransitionToSize...

as this handles device orientation changes, I see that didChangeSize... is called multiple times in a device rotation therefore I prefer the viewWillTransitionToSize...

Thanks in advance.

Leon

Community
  • 1
  • 1
Itergator
  • 299
  • 3
  • 16
  • Sounds to me like you are using scaleMode .Fill, check your view controller. BTW, every node already knows its scene, there is no reason to do the gameScene stuff you are doing. – Knight0fDragon Feb 28 '17 at 14:02
  • @Knight0fDragon I have updated the code with the ViewController I am using and am seeing the results with the scaleMode set to resizeFill. Its as though the resizing is occurring before the device has rotated therefore there is a slight distortion in the text. – Itergator Feb 28 '17 at 15:38
  • with resize fill you shouldnt see any size changes, because it is at the point level, so there is no scaling done. The only thing that should happen is your center point will change, and things shift. I would just set the scene anchor point to 0.5,0.5 so that position 0,0 is the center of your scene, then you won't need to do the division. Btw, your `MenuSceneNode` and your `ScoresSceneNode` are not used as scenes, so I would make them just SKNodes and rename it to avoid confusion from other developers. – Knight0fDragon Feb 28 '17 at 16:38
  • @Knight0fDragon I will make the changes to make the code less confusing. It was a straight upgrade to Swift 3 of the code linked in the question therefore I didn't make any other changes. Thanks for some of the tips with anchor point etc. It is just that both the menu node and score node size/frame seem to change suddenly before the animation making the rotation not a smooth transition like that seen in UIKit with constraints etc. I find that with the scale mode set to .fill the rotation animation is much more smooth therefore I am going to try with this scale mode to get the same result – Itergator Feb 28 '17 at 17:21
  • .fill just makes everything fat, that is why I suggested using anchorpoint at 0. Here is what is happening, Portait mode on iphone 5 has a width of 320, so you position at 160. You then rotate. Now your width is 568, but your position is still at 160, so you are not center. Then the next frame you recalculate your center to 276, and everything is reset again – Knight0fDragon Feb 28 '17 at 18:21
  • @Knight0fDragon will give the anchor point change a go tonight. Is there another way to stop an SKLabelNode from scaling the text at all when the bounds/frame of the node are changed so that the text isn't distorted? i.e. so it performs more like a UILabel? EDIT: I have changed the anchor point and am still getting the same distortion of text in SKLabelNodes on rotation. – Itergator Feb 28 '17 at 23:55
  • Your label size should not be changing or distorting, just shifting – Knight0fDragon Mar 01 '17 at 16:12
  • unless the scene does not resize till after, I am going to have to create a test to see what is happening – Knight0fDragon Mar 01 '17 at 16:13
  • @Knight0fDragon It distorts slightly before the device rotation to the new size but either squashed on stretched depending on going from portrait to landscape or vice versa. I find that the only scale mode that animates as expected is .fill, therefore could you help in converting to this scale mode, or suggest some code whereby the 'repositioning' and distortion does not occur? – Itergator Mar 01 '17 at 23:26

2 Answers2

3

Go into your Storyboard file, Select your ViewController's view and on the right hand bar, look for the slider image, it should be the 4th one from the left. This is called the attributes inspector. Change Content Mode to Center. This will give you black bars but stop the squishing.

Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44
  • It may be possible to use a square scene with a square view to eliminate the black bar effect. – Knight0fDragon Mar 02 '17 at 14:34
  • 1
    yes, I just slapped a 1000x1000 view onto the storyboard main view, turned off auto layouts and auto sizing, set content mode to center, and was able to get the scene to rotate without having any black borders – Knight0fDragon Mar 02 '17 at 14:50
  • Can confirm that this is indeed the answer I was looking for, no more stretching or squashing of sklabelnode when rotating a device – Itergator Mar 02 '17 at 15:19
  • I am trying to get your previous comment to work in code rather than using interface builder, could you provide? – Itergator Mar 02 '17 at 18:57
  • 1
    ugh keep design and development separate LOL anyway in your scene just do `if let view = self.view { view.contentMode = .center}` – Knight0fDragon Mar 02 '17 at 19:05
  • It'd be nice if `skView.contentMode = .redraw` worked : / – Ian Jul 25 '17 at 19:54
  • 1
    @Ian, your contentMode is always on redraw mode with SKScene – Knight0fDragon Jul 25 '17 at 19:56
  • Huh... I'd expect the scene bounds to change continuously during the orientation change animation, or didChangeSize event to be called repeatedly, or something similar. What am I missing? – Ian Jul 26 '17 at 13:37
  • @Ian, you are not using resize for your scaleMode then – Knight0fDragon Jul 26 '17 at 13:38
  • 1
    unless you mean you expect the size to "morph" into landscape, in which case even IOS does not do that. On a rotate, it sets the size of the new orientation, then does the rotation animation – Knight0fDragon Jul 26 '17 at 13:40
  • Yes, I was expecting the bounds to animate from portrait to landscape. – Ian Jul 26 '17 at 13:42
  • No, iOS doesnt behave that way. You would need to do what I listed here and create a square to achieve that kind of effect – Knight0fDragon Jul 26 '17 at 13:43
  • Yea, I was happy you mentioned that approach, but I don't see how that avoids the instant scene-cropping when the orientation changes. It just changes the background color you see behind the scene. – Ian Jul 26 '17 at 13:57
  • 1
    if your view is a square, and your scene is a square, then your orientation becomes a view port (basically a camera), so rotation wont cause any distortion or stretching – Knight0fDragon Jul 26 '17 at 14:01
  • When I get time, I will provide some images to show how it works. – Knight0fDragon Jul 26 '17 at 14:02
  • Ahhh... I think I understand. I'll give it a try. – Ian Jul 26 '17 at 15:15
  • @Knight0fDragon (*Head smack*) The just-make-the-SKView-larger idea works great! I ended up with some extra cruft because of some camera stuff and it being a universal app, but it's not a big deal and orientation changes look real nice! Thanks! – Ian Jul 27 '17 at 14:50
  • @Ian, not problem, glad you got it working. If you could add some code and an image to this answer, I would appreciate it – Knight0fDragon Jul 27 '17 at 14:51
1

This was the code created from the suggestion by @Knight0fDragon, producing SKLabelNodes that do not squash or stretch when rotated with scaleMode .resizeFill. Note the size of the view in this is for an iPad with 1024x768 resolution, creating a square skview of size 1024x1024. This would be easy to replicate with the largest value either width or height when the app loads depending on the orientation.

class GameViewController: UIViewController {

var scene : GameScene!
var newView: SKView!

override func viewDidLoad() {
    super.viewDidLoad()

    self.newView = SKView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 1024, height: 1024)))
    (self.view as? SKView)?.contentMode = .center
    self.newView.contentMode = .center
    self.newView.autoresizesSubviews = false
    self.newView.autoresizingMask = [.flexibleBottomMargin, .flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin]
    (self.view as? SKView)?.addSubview(newView)
    self.newView.center = view.center
    self.scene = GameScene(size: CGSize(width: 1024, height: 1024))
    self.scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    self.scene.scaleMode = .resizeFill
    self.newView.presentScene(scene)
}

override var shouldAutorotate: Bool {
    return true
}

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    if UIDevice.current.userInterfaceIdiom == .phone {
        return .allButUpsideDown
    } else {
        return .all
    }
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Release any cached data, images, etc that aren't in use.
}

override var prefersStatusBarHidden: Bool {
    return true
}
}

class GameScene: SKScene {

var labelNode: SKLabelNode!
var container: SKSpriteNode!

override func didMove(to view: SKView) {
    self.backgroundColor = SKColor.darkGray

    container = SKSpriteNode(color: .black, size: CGSize(width: 300, height: 300))
    self.addChild(container)

    labelNode = SKLabelNode(text: "hello")
    labelNode.fontColor = .white
    container.addChild(labelNode)
}
}
Itergator
  • 299
  • 3
  • 16