SKPhysicsBody scale with parent scaling - ios

I have a node, A, in a parent node, B.
I'm using setScale on node B to give the illusion of zooming, but when I do that, node A and all of its siblings do not have the expected physics bodies.
Even though A appears smaller and smaller, its physics body stays the same size. This leads to wacky behavior.
How can I "zoom out" and still have sensible physics?

SKPhysicsBody cannot be scaled. As a hack you could destroy the current physics body and add a smaller one as you scale up or down but unfortunately this is not a very efficient solution.

The SKCameraNode (added in iOS9) sounds perfect for your scenario. As opposed changing the scale of every node in the scene, you would change the scale of only the camera node.
Firstly, you need to add an SKCameraNode to your scene's hierarchy (this could all be done in the Sprite Kit editor too).
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
let camera = SKCameraNode()
camera.position = CGPoint(x: size.width / 2, y: size.height / 2)
self.addChild(camera)
self.camera = camera
}
Secondly, since SKCameraNode is a node you can apply SKActions to it. For a zoom out effect you could do:
guard let camera = camera else { return }
let scale = SKAction.scaleBy(2, duration: 5)
camera.runAction(scale)
For more information have a look at the WWDC 2015 talk: What's New In Sprite Kit.

A simple example that shows that a physics body scales/behaves appropriately when its parent node is scaled. Tap to add sprites to the scene.
class GameScene: SKScene {
var toggle = false
var node = SKNode()
override func didMoveToView(view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFromRect: view.frame)
scaleMode = .ResizeFill
view.showsPhysics = true
addChild(node)
let shrink = SKAction.scaleBy(0.25, duration: 5)
let grow = SKAction.scaleBy(4.0, duration: 5)
let action = SKAction.sequence([shrink,grow])
node.runAction(SKAction.repeatActionForever(action))
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(node)
let sprite = SKSpriteNode(imageNamed:"Spaceship")
if (toggle) {
sprite.physicsBody = SKPhysicsBody(circleOfRadius: sprite.size.width/2.0)
}
else {
sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
}
sprite.xScale = 0.125
sprite.yScale = 0.125
sprite.position = location
node.addChild(sprite)
toggle = !toggle
}
}
}

Related

Swift SKNode position help - sprite position always 0,0 as it moves

Problem: I’m working in swift playground on an iPad Pro.
I’d like to get the current position coordinates of an SKSpriteNode with attached physicsBody as they move around the screen. No matter what it try, the sprite position returns 0,0, despite seeing it move across the screen.
I’ve attempted various implementations of convert(_:to:) and convert(_:from:) but the sprite position always ends up 0,0. My understanding is that this isn’t really necessary in the specific case here since the sprite is the direct child of the scene so the sprite’s position property should already be what it is in the scene coordinate system.
I’ve spent a lot of googling time attempting to figure this out without much success which makes me think I’m asking the wrong question. If so, please help me figure out what the right question to ask is!
The touch position print returns the correct position relative to the initial screen size. That’s what I’d like to get for the sprite.
Disclaimer - i don’t do this professionally so please forgive what I’m sure is ugly coding!
import SwiftUI
import SpriteKit
import PlaygroundSupport
var screenW = CGFloat(2732)
var screenH = CGFloat(2048)
struct ContentView: View {
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: 2732, height: 2048)
scene.scaleMode = .aspectFit
scene.backgroundColor = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
return scene
}
class GameScene: SKScene {
var ship1 = SKSpriteNode()
public override func didMove(to view: SKView) {
// gravity edge around screen for convenience
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
//turn off default 1g gravity
physicsWorld.gravity = .zero
//Add a ship sprite
let ship1 = SKSpriteNode(imageNamed: "ship.PNG")
ship1.setScale(0.2)
ship1.position = CGPoint(x: screenW * 0.2, y: screenH * 0.2)
ship1.physicsBody = SKPhysicsBody(rectangleOf: ship1.size)
ship1.physicsBody?.velocity = CGVector(dx: 0, dy: 100)
addChild(ship1)
}
//touch handling begin
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
//
let location = touch.location(in: self)
print(location)
print(ship1.position)
}
}
var body: some View {
SpriteView(scene: scene)
.ignoresSafeArea()
}
}
//star the view
let currentView = ContentView()
//Playground view setup
PlaygroundPage.current.setLiveView(currentView)
PlaygroundPage.current.wantsFullScreenLiveView = true
PlaygroundPage.current.needsIndefiniteExecution = true

How to move sprite just left or right using CoreMotion?

For this game I am creating, I want the Sprite "Character" to either move left or right by me tilting the device either left or right.
I have looked into it and changed the code, but it seems like that it does not work.
When I run it, the "Character" moves in only one direction and still goes either up or down.
I would just like the "Character" to move only left or right, not up or down.
Any help on this?
Here is a look at the GameScene.swift file:
import SpriteKit
import CoreMotion
class GameScene: SKScene {
let motionManager = CMMotionManager()
var Ground = SKSpriteNode()
var Character = SKSpriteNode()
override func didMoveToView(view: SKView) {
/* Setup your scene here */
Character = SKSpriteNode(imageNamed: "NinjaGhost")
Character.physicsBody = SKPhysicsBody(circleOfRadius: Character.size.height)
Character.size = CGSize(width: 200, height: 200)
Character.physicsBody?.allowsRotation = false
Character.zPosition = 2
Character.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
self.addChild(Character)
Ground = SKSpriteNode(imageNamed: "Dirt Background")
Ground.size = CGSize(width: 1920, height: 1080)
Ground.zPosition = 1
Ground.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
self.addChild(Ground)
motionManager.startAccelerometerUpdates()
motionManager.accelerometerUpdateInterval = 0.1
motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue() ) {
(data, error) in
self.physicsWorld.gravity = CGVectorMake(CGFloat((data?.acceleration.x)!) * 1, 0)
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
You should follow the swift guidelines, your properties should not start with capital letters, only classes, structs, enums and protocols should. It makes code harder to read on stack overflow (code is marked blue but shouldn't) and in general its not good practice.
Change your code to this because there was a few errors.
character = SKSpriteNode(imageNamed: "NinjaGhost")
character.size = CGSize(width: 200, height: 200) // Better to call this before positioning
character.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) // Give pos before physics body
character.physicsBody = SKPhysicsBody(circleOfRadius: character.size.width / 2) // You have to divide by 2 when using circleOfRadius to get proper size in relation to the sprite
character.physicsBody?.allowsRotation = false
character.physicsBody?.affectedByGravity = false // Stop falling
character.zPosition = 2
self.addChild(character)
In your motion queue code you are manipulating the scene itself, so that will not work.
Try this code instead which uses the physics body to move the sprite. This is my preferred way of doing it because it gives the sprite more natural movement, especially when changing direction, compared to directly manipulating the sprites position property (If you want to manipulate the sprite position property directly than read this article. http://www.ioscreator.com/tutorials/move-sprites-accelerometer-spritekit-swift)
if let error = error { // Might as well handle the optional error as well
print(error.localizedDescription)
return
}
guard let data = motionManager.accelerometerData else { return }
if UIDevice.currentDevice().orientation == UIDeviceOrientation.LandscapeLeft {
character.physicsBody?.applyForce(CGVectorMake(-100 * CGFloat(data.acceleration.y), 0))
} else {
character.physicsBody?.applyForce(CGVectorMake(100 * CGFloat(data.acceleration.y), 0))
}
This will make the sprite move on the x axis. I am using data.acceleration.y because in this example the game is in landscape so you have to use the Y data to move it on the x axis.
I am also checking in which landscape orientation the device is so that the motion works in both orientations.
Adjust the 100 here to get the desired force depending on the size of the sprite (higher is more force). Big sprites properly need a few 1000s to move.

Balancing a sprite

So my problem regards the rotation of a sprite. I want it look like it's balancing. When you tap left - the sprite moves left, when you tap right - the sprite moves right. To get a better idea, put a pencil on it's end and try balance it... Yeah... that (but only in x axis).
Currently I am rotating a sprite either clockwise or anti-clockwise dependent upon whether I tap the left or right side of the screen. This is all done using SKActions.
The problem with this is that it results in a very 'jerky' and not particularly realistic motion.
I assume that I want to use physics body and something similar to velocity but my questions are:
the use of velocity is right... isn't it?
would i better off with a volume based sprite (and get it to take into account it's volume and mass) or just a simple edge based sprite?
Thanks in advance!
-- Code -
This is how I'm currently rotating and moving my sprite:
import SpriteKit
enum rotationDirection{
case clockwise
case counterClockwise
case none
}
// Creates GameScene and initialises Sprites in Scene //
class GameScene: SKScene, SKPhysicsContactDelegate {
// Rotation direction variable for ship motion (rotation and movement) //
var currentRotationDirection = rotationDirection.none
var xVelocity: CGFloat = 0
var sprite = SKSpriteNode()
// Setup Scene here //
override func didMoveToView(view: SKView) {
// Background colour //
self.backgroundColor = SKColor.whiteColor()
// sprite Physics + add's sprite //
sprite.physicsBody = SKPhysicsBody(rectangleOfSize: ship.size)
sprite.physicsBody?.affectedByGravity = true
sprite.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addSprite()
// Initialises sprite node and it's properties //
func addSprite() {
// Sprite dimension properities //
sprite.name = "sprite"
sprite = SKSpriteNode(imageNamed: "sprite")
sprite.setScale(0.5)
sprite.position = CGPointMake(frame.midX, 220)
sprite.anchorPoint = CGPoint(x: 0.5, y: 0.25)
sprite.zPosition = 1;
// sprite Physics properties //
sprite.physicsBody = SKPhysicsBody(rectangleOfSize: ship.size)
sprite.physicsBody?.categoryBitMask = UInt32(shipCategory)
sprite.physicsBody?.dynamic = true
sprite.physicsBody?.contactTestBitMask = UInt32(obstacleCategory)
sprite.physicsBody?.collisionBitMask = 0
self.addChild(sprite)
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
// Defines UI Touch //
let touch = touches.first as UITouch!
let touchPosition = touch.locationInNode(self)
// Sets up Inital Rotation Direction //
let newRotationDirection : rotationDirection = touchPosition.x < CGRectGetMidX(self.frame) ? .clockwise : .counterClockwise
// Left or Right movement based on touch //
if touchPosition.x < CGRectGetMidX(self.frame) {xVelocity = -75}
else {xVelocity = 75}
// Clockwise or anticlockwise rotation based on touch //
if currentRotationDirection != newRotationDirection && currentRotationDirection != .none {
reverseRotation()
currentRotationDirection = newRotationDirection
}
else if (currentRotationDirection == .none) {
setupRotationWith(direction: newRotationDirection)
currentRotationDirection = newRotationDirection
}
}
func reverseRotation() {
let oldRotateAction = sprite.actionForKey("rotate")
let newRotateAction = SKAction.reversedAction(oldRotateAction!)
sprite.runAction(newRotateAction(), withKey: "rotate")
}
func stopRotation() {
sprite.removeActionForKey("rotate")
}
func setupRotationWith(direction direction: rotationDirection){
let angle : CGFloat = (direction == .clockwise) ? CGFloat(M_PI) : -CGFloat(M_PI)
let rotate = SKAction.rotateByAngle(angle, duration: 2)
let repeatAction = SKAction.repeatActionForever(rotate)
sprite.runAction(repeatAction, withKey: "rotate")
}
override func update(currentTime: CFTimeInterval) {
let rate: CGFloat = 0.3; //Controls rate of motion. 1.0 instantaneous, 0.0 none.
let relativeVelocity: CGVector = CGVector(dx:xVelocity-sprite.physicsBody!.velocity.dx, dy:0);
sprite.physicsBody!.velocity=CGVector(dx:sprite.physicsBody!.velocity.dx+relativeVelocity.dx*rate, dy:0);
}
I think what your are going to want to look into is acceleration. Right now you are adjusting verlocityX with a static amount using:
if touchPosition.x < CGRectGetMidX(self.frame) {
xVelocity = -75
}
else {
xVelocity = 75
}
Doing this will result in your jerky motion. Which is actually just linear. Using acceleration you can avoid this.
For this you'll need two more variables for maxVelocity and acceleration. You want a maxVelocity probably to limit the velocity. Each frame you'll need to increase the velocity with the acceleration amount.
I suggest you try something like this:
velocity = 0 // No movement
acceleration = 2 // Increase velocity with 2 each frame
maxVelocity = 75 // The maximum velocity
if touchPosition.x < CGRectGetMidX(self.frame) {
xVelocity += acceleration
if xVelocity <= -maxVelocity {
xVelocity = -maxVelocity
}
}
else {
xVelocity -= acceleration
if xVelocity >= maxSpeed {
xVelocity = maxSpeed
}
}
Increasing your velocity like this will result with a rotation speed of 2 in the first frame, than 4 in the second and 6 in the thirds etc. So it will have an increasing effect. While at the same time, when trying to reverse it you wont have a jerky effect either. Say the velocity is at 50, you first decrease it to 48, 46, 44, etc. Not until the 0 point you will actually start rotating the other way.

Locking scene to a sprite node swift xcode

Basically, I want to move the character (player) and have the “camera” or scene follow them in the world. The problem is Apple/xcode does not have a built in camera node and if I try to move the scene, I get an error saying something to the effect of “SKScene cannot be moved.” I don’t want to just move the background around and lock the player in place because I want to be able to apply forces to the player node. I have gone to Apple’s website to the Advanced Scene Processing page and followed the example but it doesn’t work. I just need a nudge in the right direction. Thanks!
Code:
(I have deleted pieces of code that are irrelevant)
class PlayScene: SKScene, SKPhysicsContactDelegate {
let world = SKNode()
let player = SKShapeNode(circleOfRadius: 25)
var cannonRotation = 0.0
var fired = false
override func didMoveToView(view: SKView) {
physicsWorld.contactDelegate = self
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.addChild(world)
//Adds the background to the world
//Adds the elements to the scene
player.fillColor = UIColor.whiteColor()
player.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMinY(self.frame)+65+25)
player.zPosition = 2
player.physicsBody = SKPhysicsBody(circleOfRadius: 25)
player.physicsBody?.dynamic = true
player.physicsBody?.restitution = 0.9
player.physicsBody?.categoryBitMask = BodyType.player.rawValue
//Adds the physics properties to GameScene
let physicsGround = SKPhysicsBody(edgeFromPoint: CGPoint(x: CGRectGetMinX(self.frame), y: CGRectGetMinY(self.frame)+65), toPoint: CGPoint(x: CGRectGetMaxX(self.frame), y: CGRectGetMinY(self.frame)+65))
self.physicsBody = physicsGround
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
powerLevel()
totalPower = powerReached * playCannon.powerMultiplier
self.addChild(player)
player.physicsBody?.applyImpulse(CGVector(dx: CGFloat(Float(totalPower)*cosf(Float(cannonRotation))), dy: CGFloat(Float(totalPower)*sinf(Float(cannonRotation)))))
fired = true
}
Override didsimulatePhysics, And in that function call this
self.centerOnNode(player);
What that does, It calles a function we will later create and it sendes a parameter of an SKNode and in your case it is the player node. Next create a method called centerOnNede and add the following parameters
func centerOnNode(node: SKNode) {
}
In the centerOnNode function add this following line of code.
var cameraPosition: CGPoint = node.scene?.convertPoint(node.position, fromNode: node.parent!)
cameraPosition.x = 0
let posX = node.parent?.position.x
let posY = node.parent?.position.y
node.parent?.position = CGPointMake(posX! - cameraPosition.x, posY! - cameraPosition.y)
That will center the camera on to the node and update it's position. Code converted from here. How to make camera follow SKNode in Sprite Kit?
There is a detailed example of how to do this in the Apple Adventure reference game. It has both Swift and Objective-C examples.
In summary what they recommend is that you
Put all nodes on a background node (SKNode) container.
implement didSimulatePhysics and move the world in the opposite direction of the player. That will keep the player in the middle of the screen. Special logic will be needed if you want the camera to bump into the edge of your world.
A camera node would be very helpful, but has not been implemented into SpriteKit.
As of iOS 9 there is now an SKCameraNode that does all the heavy lifting for you.
Simply add these three lines of code in your didMoveToView function:
let cameraNode = SKCameraNode()
self.addChild(cameraNode)
self.camera = cameraNode
Then in your didFinishUpdate function just set the cameraNode's position to whatever SKSpriteNode you want it to follow.

How to rotate sprite in sprite kit with swift

Hi my problem is regarding the rotation of two sprites. When I touch the right half of the screen the rotation starts and moving sprite2 and sprite3. If I touch the left half of the screen the rotation stops because velocity-velocity = 0. If I touch the left half again the rotation begins.
However, if I touch the half of the screen corresponding with the current rotational-direction the velocity is duplicated. I want to be able to change the direction of the rotation, but for the speed to remain constant.
Video demonstrating the problem: http://youtu.be/HxLwl1QZiNM
import SpriteKit
class GameScene: SKScene {
let sprite = SKSpriteNode(imageNamed:"bWhite")
let sprite2 = SKSpriteNode(imageNamed:"bBlue")
let sprite3 = SKSpriteNode(imageNamed:"bRed")
let circle = SKSpriteNode(imageNamed:"bCircle")
override func didMoveToView(view: SKView) {
/* Setup your scene here */
backColor = SKColor(red: 0, green: 0, blue: 0, alpha: 1)
self.backgroundColor = backColor
sprite.setScale(1.25)
sprite2.setScale(1)
sprite3.setScale(1)
sprite.position = CGPointMake(self.frame.size.width/2, (self.frame.size.height/2)-200);
circle.position = CGPointMake(self.frame.size.width/2, (self.frame.size.height/2)-200);
sprite3.zRotation = 172.77
sprite2.anchorPoint = CGPointMake(-1.10, 0.5);
sprite3.anchorPoint = CGPointMake(-1.10, 0.5);
self.addChild(sprite)
self.addChild(circle)
sprite.addChild(sprite2)
sprite.addChild(sprite3)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let action = SKAction.rotateByAngle(CGFloat(M_PI), duration:1)
let action2 = SKAction.rotateByAngle(CGFloat(-M_PI), duration:1)
if (location.x > self.frame.size.width / 2)
{
sprite2.runAction(SKAction.repeatActionForever(action2))
sprite3.runAction(SKAction.repeatActionForever(action2))
} else {
sprite2.runAction(SKAction.repeatActionForever(action))
sprite3.runAction(SKAction.repeatActionForever(action))
}
}
}
OK, take three. I'm not 100% sure about the specifics of when the rotation should end etc. but all the pieces should be in place with the below code. It will:
start rotation clockwise or counterclockwise based on the position of the first touch
stop the rotation if the user touches for the same direction again
switch the rotation if the user touches on the other half of the screen
import SpriteKit
enum rotationDirection{
case clockwise
case counterClockwise
case none
}
class GameScene: SKScene {
var currentRotationDirection = rotationDirection.none
let sprite = SKSpriteNode(color: UIColor.yellowColor(), size: CGSizeMake(100, 100))
override func didMoveToView(view: SKView) {
sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
sprite.physicsBody.affectedByGravity = false
sprite.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addChild(sprite)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let touch : UITouch = touches.anyObject() as UITouch
let touchPosition = touch.locationInNode(self)
let newRotationDirection : rotationDirection = touchPosition.x < CGRectGetMidX(self.frame) ? .clockwise : .counterClockwise
if currentRotationDirection != newRotationDirection && currentRotationDirection != .none{
reverseRotation()
currentRotationDirection = newRotationDirection
} else if currentRotationDirection == newRotationDirection{
stopRotation()
currentRotationDirection = .none
} else if (currentRotationDirection == .none){
setupRotationWith(direction: newRotationDirection)
currentRotationDirection = newRotationDirection
}
}
func reverseRotation(){
let oldRotateAction = sprite.actionForKey("rotate")
let newRotateAction = SKAction.reversedAction(oldRotateAction)
sprite.runAction(newRotateAction(), withKey: "rotate")
}
func stopRotation(){
sprite.removeActionForKey("rotate")
}
func setupRotationWith(#direction: rotationDirection){
let angle : Float = (direction == .clockwise) ? Float(M_PI) : -Float(M_PI)
let rotate = SKAction.rotateByAngle(angle, duration: 1)
let repeatAction = SKAction.repeatActionForever(rotate)
sprite.runAction(repeatAction, withKey: "rotate")
}
}
Edit: Changed example to cater the specific needs in the question. Something odd with the code formatting not quite sure what's going on there.
I suggest removing all current actions before adding new, since they may conflict. I'm still not sure what you are trying to achieve, can you explain more?
Have you just tired removing the actions at touchesEnded? I had a similar problem before where I wanted my object to move left to right based on which half of the screen the user touched. I had a similar issue where if I touched right and right again my velocity would double, and if I touched left my object would just stop.
I fixed this issue by adding the touchesEnded method and just removed the actions whenever I let go. This essentially reset my move action so the object would stop when I let go and would go to the opposite direction right away instead of having to tap it twice.

Resources