Rotate a child node in spritekit - ios

I have a node that I need to rotate, however as it is a child node whenever I rotate it changes size. This is because it is created with a y scale value of 0.7693. I have tried creating a algorithm to manipulate the x and y scale based on the z rotation, but nothing works. Is their anyway to do this?
edit: I have found that statically changing the x and y scales does not work here is my code for that:
var ScaleDir: CGFloat = 1
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
let turnAngle:Double = 1
startWheel.zRotation = startWheel.zRotation + CGFloat(turnAngle / (180/Double.pi))
if startWheel.zRotation*(180/CGFloat.pi) >= 360{
startWheel.zRotation = startWheel.zRotation - (360/(180/CGFloat.pi))
}
startWheel.yScale = startWheel.yScale + (CGFloat((1 - 0.7693) / Double(90/turnAngle)) * ScaleDir)
startWheel.xScale = startWheel.xScale - (CGFloat((1 - 0.7693) / Double(90/turnAngle)) * ScaleDir)
if startWheel.yScale >= 1 || startWheel.xScale >= 1{
ScaleDir = ScaleDir * -1
}
}
I have realized the problem. The automatically assigned Yscale moves with the rotating node however the ovular shape that is created by the spriteKit scaling system does not and stays vertical.
Here is what my problem looks like:

If the scale is applied after the rotation, then you want to use sin on the angle to figure out the Y component and cos to figure out the X.
But, I would try giving this node a dummy parent node and rotate that.

Here's some simple code I was using in a playground to try to duplicate what you described in your question. I tried manipulating the yScale and rotation of various elements in the setup but nothing seemed to reproduce the undesired behavior. Have I reasonably replicated your setup, or am I missing something?
import SpriteKit
import PlaygroundSupport
public class GameScene: SKScene {
let node: SKNode
init(nodeToRotate: SKNode) {
self.node = nodeToRotate
super.init(size: CGSize(width: 500, height: 400))
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func update(_ currentTime: TimeInterval) {
let turnAngle = 9.0
node.zRotation = node.zRotation + CGFloat(turnAngle / (180.0/Double.pi))
}
}
let square = SKShapeNode(rectOf: CGSize(width: 80, height: 80))
square.fillColor = .purple
square.strokeColor = .purple
square.position = CGPoint(x: 250, y: 200)
let circle = SKShapeNode(circleOfRadius: 30)
circle.fillColor = .orange
circle.strokeColor = .orange
circle.position = CGPoint(x: 0, y: 0)
circle.yScale = 0.7
square.addChild(circle)
let scene = GameScene(nodeToRotate: square)
scene.addChild(square)
let view = SKView(frame: CGRect(origin: .zero, size: CGSize(width: 500, height: 400)))
view.presentScene(scene)
PlaygroundPage.current.liveView = view

The problem was that the parent node had a y scale and this was causing all of the problems, as soon as I removed the y-scale and changed it back to 1 everything started to work again.

Related

SpriteKit scrolling background image showing line between images

I'm learning how to make games in iOS, so I decided to replicate the first level of Mario Bros using my own assets, just to learn how to make them as well.
The issue I'm having right now is that, when creating the scrolling background, using this formula I found on Hacking with Swift, I keep getting a line in between my images.
I've checked that my images have no border in the AI file. (The images are the ones above)
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var player = SKSpriteNode()
private var bg = SKSpriteNode()
override func didMove(to view: SKView) {
let playerTexture = SKTexture(imageNamed: "player")
player = SKSpriteNode(texture: playerTexture)
player.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
addBackground()
self.addChild(player)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
// MARK: UTILITY FUNCTIONS
func addBackground() {
let bgTexture = SKTexture(imageNamed: "bg")
for i in 0 ... 1 {
bg = SKSpriteNode(texture: bgTexture)
//bg.position = CGPoint(x: (i == 0 ? bgTexture.size().width : bgTexture.size().width - 1) * CGFloat(i), y: self.frame.midY)
bg.position = CGPoint(x: (bgTexture.size().width * CGFloat(i)) - CGFloat(1 * i), y: 0)
bg.size.height = self.frame.height
//bg.anchorPoint = CGPoint.zero
bg.zPosition = -10
self.addChild(bg)
let moveLeft = SKAction.moveBy(x: -bgTexture.size().width, y: 0, duration: 5)
let moveReset = SKAction.moveBy(x: bgTexture.size().width, y: 0, duration: 0)
let moveLoop = SKAction.sequence([moveLeft, moveReset])
let moveForever = SKAction.repeatForever(moveLoop)
bg.run(moveForever)
}
}
}
Just create a new project, copy-paste it and you should see it running
I also changed my GameScene.sks size to: 2688 x 1242
And one last change I made to make the background appear in full screen was to set the LaunchScreen as stated in this answer which seems to be an issue in Xcode 12
I understand that the formula from Hacking with Swift post is making the images overlap by 1px, I've tried removing the 1 * i part from it, yet results are not different.
Other thing I did was to verify the images were "joining" perfectly together, and they do, the lines you can see in the image below are from the AI canvas, both images join in between "cactuses".
I also tried running it into a device in case it might be a bug in the simulator:
Your image has a thin black border on the left handside and along the top, 1 pixel wide. It's black. Remove that and try again.

How to move a character back and forth across horizontally in swift

Currently trying to get a monster character to move across the screen horizontal back and forth. I'm extremely new to Swift as I just started learning last week and also haven't exactly master OOP. So how do I properly call the enemyStaysWithinPlayableArea func while it calls the addEnemy func. Heres what I have so far:
import SpriteKit
class GameScene: SKScene {
var monster = SKSpriteNode()
var monsterMove = SKAction()
var background = SKSpriteNode()
var gameArea: CGRect
// MARK: - Set area where user can play
override init(size: CGSize) {
// iphones screens have an aspect ratio of 16:9
let maxAspectRatio: CGFloat = 16.0/9.0
let playableWidth = size.height/maxAspectRatio
let margin = (size.width - playableWidth)/2
gameArea = CGRect(x: margin, y: 0, width: playableWidth, height: size.height)
super.init(size: size)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(to view: SKView) {
// I want to run addEnemy() while it checks for enemyStayWithinPlayableArea()
// run(SKAction.repeatForever(...)) ???
enemyStayWithinPlayableArea()
// MARK: - Setting the background
background = SKSpriteNode(imageNamed: "background")
background.size = self.size
// Set background at center of screen
background.position = CGPoint(x:self.size.width/2 ,y:self.size.height/2)
// Layering so everything in the game will be in front of the background
background.zPosition = 0
self.addChild(background)
}
func addEnemy() {
// MARK: - Set up enemy and its movements
monster = SKSpriteNode(imageNamed: "monster")
monster.zPosition = 5
// this is where the enemy starts
monster.position = CGPoint(x: 0, y: 300)
self.addChild(monster)
// I want it to move to the end of the screen within 1 sec
monsterMove = SKAction.moveTo(x: gameArea.maxX, duration: 1.0)
monster.run(monsterMove)
}
func enemyStayWithinPlayableArea(){
addEnemy()
// when the monster reaches the right most corner of the screen
// turn it back around and make it go the other way
if monster.position.x == gameArea.maxX{
monsterMove = SKAction.moveTo(x: gameArea.minX, duration: 1.0)
monster.run(monsterMove)
}
// when the monster reaches the left most corner of the screen
// same idea as above
if monster.position.x == gameArea.minX{
monsterMove = SKAction.moveTo(x: gameArea.maxX, duration: 1.0)
monster.run(monsterMove)
}
}
}
So far the enemy moves across the screen to the right edge of the screen, so that part works perfectly. I just need help making sure it continues in a loop (perhaps using the SKAction.repeatForever method but not sure how).
To understand movement of image in spritekit check this example.
let moveLeft = SKAction.moveTo(CGPoint(x: xpos, y: ypos), duration: duration)
let moveRight = SKAction.moveTo(CGPoint(x: xpos, y: ypos), duration: duration)
sprite.runAction(SKAction.repeatActionForever(SKAction.sequence([moveLeft, moveRight])))
for horizontal movement y position never been changed it should be same as in move left and move right.to stop repeatforever action use this.
sprite.removeAllActions()
Make two functions moveToMax and moveToMin with respective SKActions.And run action with completion handler.
func moveToMin(){
monsterMove = SKAction.moveTo(x: gameArea.minX, duration: 1.0)
monster.run(monsterMove, completion: {
moveToMax()
})
}

SKSpriteNode not scalling properly

I'm developing a small game using Swift & SpriteKit. When I add a SKSpriteNode for Restart button, it doesn't scale properly.
The size of Restart button is 100px in height and width. If I don't set scale , it covers the whole screen and make the screen appear white. I figured out that if I setScale to 0.005 only than it appears on screen, but not in proper size.
import Foundation
import SpriteKit
class EndScene: SKScene {
var restartBtn = SKSpriteNode()
override func didMoveToView(view: SKView) {
background()
restartGame()
}
func restartGame() {
restartBtn = SKSpriteNode(imageNamed: "restartBtn")
restartBtn.setScale(0.005)
restartBtn.position = CGPoint(x: self.size.width / 2, y: self.size.height / 4)
restartBtn.zPosition = 1
self.addChild(restartBtn)
}
func background() {
let bkg = SKSpriteNode(imageNamed: "Background")
bkg.size = self.frame.size
bkg.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
bkg.zPosition = -2
self.addChild(bkg)
}
}
Here is the output of this code,
Restart Button Output
UPDATE
I put scene!.scaleMode = .AspectFill right inside didMoveToView function and it helped in rendering the shape of the SpriteNode properly. But still I have to setScale(0.001) to make the size of Restart button fit in the screen. Can anyone assist me what line of code I'm still missing?
Instead of using .setScale try using restartBtn.size = CGSize(Width: 50, Height: 50)
This uses the Sprite Kit's resize formula.
I ran into this as well. It happened when I forgot to specify the size of the scene. Instead of calling initWithSize I just wrote init and the scene started to scale down all nodes by x1000. Nodes were visible, but they required a scale of 0.001

Sprites show up in simulator, not on device

This simple project inserts a square sprite in the center of the field when you press the button, which calls the add() function below. In the simulator, when you add multiple sprites, it pushes the others out of the way, so when you have pressed it a bunch of times you get...
screen shot from simulator, iphone 6, iOS 9.2, and this is the behavior I want.
But the same code running on my iphone, after adding the same number of sprites yields this... screen show from physical iphone 6, iOS 9.2
Here is the code from GameScene.swift:
import SpriteKit
class GameScene: SKScene {
override init(size: CGSize) {
super.init(size:size)
self.physicsWorld.gravity = CGVectorMake(0, -1.0)
let worldBorder = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsBody = worldBorder
self.physicsBody?.friction = 0.5
}
func add()
{
let sprite = SKSpriteNode(color: UIColor.blueColor(), size: CGSize(width: 10, height: 10))
sprite.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
sprite.physicsBody = SKPhysicsBody(circleOfRadius: 8)
sprite.physicsBody?.friction = 0.0
sprite.physicsBody?.affectedByGravity = false
sprite.physicsBody?.restitution = 0.5
sprite.physicsBody?.linearDamping = 0.5
addChild(sprite)
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
}
Where am I going wrong? And how to I get the behavior I want on the real iPhone?
I'm not sure if which one is the desired behavior, but it does make sense that they wouldn't slide if you are stacking them literally right on top of each other. That being said, there is a way to get around this.
Keep in mind that nodes use floating point positioning, but of course they can only actually visibly be positioned by the pixel.
1 pixel would be 0.5 points on a 2x screen, and 0.33 points on 3x screen. With that in mind, you can use an offset of < 0.33 to get the physics bodies offset without it being visible to the user. Here's a little example:
class GameScene: SKScene {
var xOffset: CGFloat = 0.05
var yOffset: CGFloat = 0.3
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
override init(size: CGSize) {
super.init(size:size)
self.physicsWorld.gravity = CGVectorMake(0, -1.0)
let worldBorder = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsBody = worldBorder
self.physicsBody?.friction = 0.5
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
add()
updateOffsets()
}
func add()
{
let sprite = SKSpriteNode(color: UIColor.blueColor(), size: CGSize(width: 10, height: 10))
sprite.position = CGPointMake((self.frame.size.width / 2) + xOffset, (self.frame.size.height / 2) + yOffset)
sprite.physicsBody = SKPhysicsBody(circleOfRadius: 8)
sprite.physicsBody?.friction = 0.0
sprite.physicsBody?.affectedByGravity = false
sprite.physicsBody?.restitution = 0.5
sprite.physicsBody?.linearDamping = 0.5
addChild(sprite)
}
private func updateOffsets() {
xOffset = -xOffset
yOffset = -yOffset
}
}
Switching the offsets is the important part. If you don't do that, you'll have the same problem of stacking exactly on top of each other. The offsets I used get pretty close to the simulator behavior, but a few taps in you'll notice it's a little different. Hopefully you don't care about matching the exact same pattern you're getting in simulator. It's a very similar one with this code. If you do care, you'll notice changing the offsets will change the pattern.

iPhone 4, 5, 5s and 6+ simulators misaligning nodes

Apologies if I've already asked this, but Im very stuck; the first question was very unclear. Created a game in SpriteKit on the iPhone 6 simulator and when run the other sized simulators, everything is thrown out of alignment. Im not using any .sks, .xib or storyboarding. Is there a way to programmatically resize the game so that all screen sizes have the correct positioning? Will post code if necessary.
Edit: GameViewController:
import UIKit
import SpriteKit
var sceneSize = UIScreen.mainScreen().bounds
var screenWidth = sceneSize.width
var screenHeight = sceneSize.height
class GameViewController: UIViewController {
var scene: GameScene!
override func viewDidLayoutSubviews() {
super.viewWillLayoutSubviews()
let skView = self.view as! SKView
skView.multipleTouchEnabled = false
scene = GameScene(size: skView.bounds.size)
//scene.scaleMode = .AspectFill
scene.scaleMode = SKSceneScaleMode.ResizeFill
skView.presentScene(scene)
scene.anchorPoint = CGPointMake(0, 0)
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
let completionBlock: () -> Void = {
}
let errorBlock: (NSError!) -> Void = { error in
print("error")
}
RevMobAds.startSessionWithAppID("", withSuccessHandler: completionBlock, andFailHandler: errorBlock);
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
Edit 2 GameScene:
import Foundation
import SpriteKit
import UIKit
var sceneSize = UIScreen.mainScreen().bounds
var screenWidth = sceneSize.width
var screenHeight = sceneSize.height
class GameScene: SKScene {
player.position = CGPointMake(screenWidth / 5, screenHeight / 3)
}
First, set the GameScene SKSceneScalode (this is found pre-programmed into the GameViewController to .ResizeFill
Then copy the following code into GameScene:
Import UIKit
var screenSize = UIScreen.mainscreen().bounds
var screenWidth = screenSize.width
var screenHeight = screenSize.height
This will store the width and height of the screen in the variables screenWidth and screenHeight. Now when you create your spriteNodes and other objects, use these two variables as your basis. Here's an example
someNode.position = CGPoint(x: screenWidth/5 y: screenHeight/3)
Are you the same person that posted on the Swift section of Reddit? Here's my code that works for all iPhone devices, as I answered it well on the Reddit post the same way. Here's the more profound answer.
In your View Controller, enter the following code inside the viewDidLoad().
//Create a scene object with the max size of an iPhone 6 Plus at 1920 x 1080
//That means it can easily scale down to the 6, 5, and 4S.
let mainScene = GameScene(size: CGSize(width: 1920, height: 1080))
//To fill the scene and fit for all sizes
mainScene.scaleMode = .AspectFill
//Prepare the view
let mainView = self.view as! SKView
mainView.showsFPS = true
mainView.showsNodeCount = true
mainView.ignoresSiblingOrder = true
//Move to the required scene
mainView.presentScene(mainScene, transition: SKTransition.fadeWithDuration(1))
In the class scene, add the following variables at the top.
//Will be used to get the ratio of the device
let deviceWidth = UIScreen.mainScreen().bounds.width
let deviceHeight = UIScreen.mainScreen().bounds.height
let maxAspectRatio: CGFloat
let playableArea: CGRect
If you don't already have an override init inside the class scene that the view is moving to, add the following inside the class.
//Overrides the superclasses' SKScene init
override init(size: CGSize) {
//Initializes the maxAspectRatio variable
maxAspectRatio = deviceHeight / deviceWidth
print("The ratio of the device is: \(maxAspectRatio)")
//Prepares the rectangle view that is viewable by the user (ONLY FOR PORTRAIT MODE)
let playableWidth = size.height / maxAspectRatio
let playableMargin = (size.width - playableWidth) / 2.0
playableArea = CGRect(x: playableMargin, y: 0, width: playableWidth, height: size.height)
/*USE THIS CODE IF YOUR APP IS IN LANDSCAPE MODE*****************************************
let playableHeight = size.width / maxAspectRatio
let playableMargin = (size.height - playableHeight) / 2.0
playableArea = CGRect(x: 0, y: playableMargin, width: size.width, height: playableHeight)
*///*************************************************************************************
//Find out what kind of device the user is using
if maxAspectRatio == (4 / 3) {
//The iPhone's 4S ratio is 4:3
print("The user is using an iPhone 4S")
} else {
//That means the ratio is not 4:3, so it probably is 16:9
print("The user is using an iPhone 5, 6, 6S, or 6 Plus.")
}
//Assigns the size of the scene to the superclass init of SKScene
//Must be called after all variables are initialed in this class
super.init(size: size)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Add the following function inside the scene class too so you can see the viewable area that is seen by the user. It draws a red rectangle. Then, just call this function inside the didMoveToView() function. Now to position objects, all you have to do is use CGRectGet... properties on the playableArea to position the objects correctly.
//Used to draw the rectangle or the view of the phone
func drawPlayableArea() {
let shape = SKShapeNode()
let path = CGPathCreateMutable()
CGPathAddRect(path, nil, playableArea)
shape.path = path
shape.strokeColor = SKColor.redColor()
shape.lineWidth = 8
addChild(shape)
}
override func didMoveToView(view: SKView) {
drawPlayableArea()
//Examples used to position objects on a screen related to the playable area or viewable area
//This works for all iPhone devices as well as iPods.
let label = SKLabelNode(fontNamed: "Arial")
label.fontColor = SKColor.whiteColor()
label.text = "Hello, Redditor!"
label.position = CGPoint(x: CGRectGetMidX(playableArea), y: CGRectGetMidY(playableArea))
addChild(label)
let score = SKLabelNode(fontNamed: "Arial")
score.verticalAlignmentMode = .Top
score.horizontalAlignmentMode = .Left
score.fontColor = SKColor.whiteColor()
score.text = "Score: 100"
score.position = CGPoint(x: CGRectGetMinX(playableArea) + 20, y: CGRectGetMaxY(playableArea) - 20)
addChild(score)
backgroundColor = SKColor.blackColor()
}
Here are the github files if you need to understand the code more.
Screenshots of positions in an 4S and a 6 Plus. Notice how the objects stay positioned relative to the scene size.
OK I had time to reread this, and I think I know what is going on, I will leave my old answer in here as well.
New Answer:
What is going on here is you are setting your scene size at the time of creation of view, before any auto constraints get applied. So your view size is not the same as your screen size. Thus, your scene becomes the same size regardless of device, then you apply the math based on your screen, and everything gets thrown off. What you need to do is 1) Verify that my theory is right, and that at the time of creating your scene, the size is not the same as screen size, then 2) create the scene at the time the view does get resized for the device.
Old Answer:
The reason everything is thrown out of alignment is because your game scene is being resized based on device. You need to set a single constant size for doing things the way you are doing
let scene = GameScene(size:CGSizeMake(400,300))
Where 400 x 300 can be changed based on your needs. this will give you a play area of fixed size to work with, so your nodes will be placed the same. Then, to handle the screen size, you set your scenes scale mode to aspect fill or aspect fit, depending on if you want to lose information, but have no black bars, or have black bars and show all information. (resize fill will distort your image by stretching if that is what you want). to do this it is just scene.scaleMode = .AspectFill
Now you will have to tweak numbers and images if you are getting undesirable results, but the same logic should be applied to all devices. If you start grabbing things like the screen size again, things will start misaligning.
Inside your scene code, use the scenes width and height to manipulate your nodes, not the screen
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
player.position = CGPointMake(scene.frame.width / 5, scene.frame.height / 3)
}
Best way to think about it is the Game scene gets treated as its own screen, and in the end you want to take this screen and scale it to meet the various device settings.

Resources