I have some very simple Swift code which places an SKSpriteNode on screen at position x:214, y:200. In the update method, I print the current position of said SKSpriteNode. If I omit all of the SKPhysicsBody code, then the position of my SKSpriteNode is shown in the debug window as (214.0, 200.0) which is as I would expect it. If, however, I run the program with the SKPhysicsBody code present, then the first print returns correct with (214.0, 200.0), but subsequent calls return (214.000015258789, 200.0).
This looks to me like something in the physics world is affecting (albeit in a very small way) the position of the SKSpriteNode. What I cannot fathom for the life of me is what that could be. My code is exactly as shown below, nothing more and nothing less. I think I have disabled everything that could possibly have any effect on the position. I am just hoping that I might have missed something silly.
The problem may seem trivial as the values are only fractionally off, however when I attempt to move the sprite smoothly across the screen (incrementing it's x position + 1 on each update) then it will occasionally miss an entire position, going from 289.0 to 289.99, then 291.0.
Any help would be much appreciated. It feels like a bug to me, but as I am fairly new to the physics side of SpriteKit, I would like to be certain.
import SpriteKit
class GameScene: SKScene {
private var playerSprite : SKSpriteNode!
override func didMoveToView(view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)//borderBody
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
playerSprite = SKSpriteNode(color: UIColor.yellowColor(), size: CGSize(width: 100, height: 100))
playerSprite.position = CGPoint(x: 214, y: 200)
playerSprite.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 100, height: 100))
if let playerSpritePhysicsObject = playerSprite.physicsBody {
playerSpritePhysicsObject.usesPreciseCollisionDetection = true
playerSpritePhysicsObject.affectedByGravity = false
playerSpritePhysicsObject.allowsRotation = false
playerSpritePhysicsObject.dynamic = false
playerSpritePhysicsObject.angularVelocity = 0.0
playerSpritePhysicsObject.friction = 0.0
playerSpritePhysicsObject.linearDamping = 0.0
playerSpritePhysicsObject.angularDamping = 0.0
playerSpritePhysicsObject.velocity = CGVectorMake(0, 0)
playerSpritePhysicsObject.restitution = 0.0
}
self.addChild(playerSprite)
}
override func update(currentTime: NSTimeInterval) {
print("playerSprite Position = \(playerSprite.position)")
}
}
Here is the debug window output:
playerSprite Position = (214.0, 200.0)
2017-06-19 20:39:47.682 PhysicsTest[9193:2340365] <SKMetalLayer: 0x14fe6e760>: calling -display has no effect.
playerSprite Position = (214.000015258789, 200.0)
playerSprite Position = (214.000030517578, 200.0)
playerSprite Position = (214.000030517578, 200.0)
playerSprite Position = (214.000030517578, 200.0)
playerSprite Position = (214.000030517578, 200.0)
After some time agonising over this problem, I think I may have found a work around. It's nasty but does the trick. Simply reset the position of the SKSpriteNode in the didFinishUpdate() function which occurs after the didSimulatePhysics() function has complete.
override func didFinishUpdate() {
playerSprite.position.x = 214
}
Having tested further, I found this didn't work for iOS 7.1. In order for it work on older devices, you must place the code in the didSimulatePhysics() function instead:
override func didSimulatePhysics() {
playerSprite.position.x = 214
}
Related
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
I have a simple SpriteKit App with walls and a ball. Both setup with a SKPhysicsBody.
When I apply a force in one direction, I expect the ball to reflect at the wall with the same angle when it collide but in opposite direction.
But sometimes I see the angle is weird. I played a lot with all the physicsBody properties, but was not able to fix it. Sometimes the first reflections look good, but then the third or sixth and sometimes the first reflection is at wrong angle.
I read from different posts, people are kinda self-calculating the "correct direction". But I can't imagine SpriteKits physic engine is not capable to do so.
Check my attached image to understand what I mean. Can anybody help on this? I don't want to start playing around with Box2d for Swift, since this looks like it will be too hard for me to integrate into Swift.
This is the physicsWorld init on my GameScene.swift file where all my elements are added to:
self.physicsWorld.gravity = CGVectorMake(0, 0)
Adding all my code would be way too much, so I add the important pieces. Hope its enough for analyze. All elements like the ball, walls are SKSpriteNode's
This is the physicsBody code for the ball:
self.physicsBody = SKPhysicsBody(circleOfRadius: Constants.Config.playersize+self.node.lineWidth)
self.physicsBody?.restitution = 1
self.physicsBody?.friction = 0
self.physicsBody?.linearDamping = 1
self.physicsBody?.allowsRotation = false
self.physicsBody?.categoryBitMask = Constants.Config.PhysicsCategory.Player
self.physicsBody?.collisionBitMask = Constants.Config.PhysicsCategory.Wall | Constants.Config.PhysicsCategory.Enemy
self.physicsBody?.contactTestBitMask = Constants.Config.PhysicsCategory.Wall | Constants.Config.PhysicsCategory.Enemy
This is the physicsBody for the walls:
el = SKSpriteNode(color: UIColor.blueColor(), size: CGSize(width: Constants.Config.wallsize, height: Constants.Config.wallsize))
el.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: Constants.Config.wallsize, height: Constants.Config.wallsize))
el.physicsBody?.dynamic = false
el.physicsBody?.categoryBitMask = Constants.Config.PhysicsCategory.Wall
el.physicsBody?.collisionBitMask = Constants.Config.PhysicsCategory.Player
el.physicsBody?.contactTestBitMask = Constants.Config.PhysicsCategory.Player
At the end I just call the applyImpulse function on the balls physicsBody to make it moving in the physics simulation.
Also check my attached second image/gif. It shows the edge collision problem with a simple skphysics app without any special parameterization. just a rectangle with a ball in it and one vector applied as impulse.
Heres my full non-working code using the solution from appzYourLife.
class GameScene: SKScene {
private var ball:SKShapeNode!
override func didMoveToView(view: SKView) {
let screen = UIScreen.mainScreen().bounds
let p = UIBezierPath()
p.moveToPoint(CGPointMake(0,0))
p.addLineToPoint(CGPointMake(screen.width,0))
p.addLineToPoint(CGPointMake(screen.width, screen.height))
p.addLineToPoint(CGPointMake(0,screen.height))
p.closePath()
let shape = SKShapeNode(path: p.CGPath)
shape.physicsBody = SKPhysicsBody(edgeLoopFromPath: p.CGPath)
shape.physicsBody?.affectedByGravity = false
shape.physicsBody?.dynamic = false
shape.strokeColor = UIColor.blackColor()
self.addChild(shape)
ball = SKShapeNode(circleOfRadius: 17)
ball.name = "player"
ball.position = CGPoint(x: 20, y: 20)
ball.fillColor = UIColor.yellowColor()
ball.physicsBody = SKPhysicsBody(circleOfRadius: 17)
ball.physicsBody?.angularDamping = 0
ball.physicsBody?.linearDamping = 0
ball.physicsBody?.restitution = 1
ball.physicsBody?.friction = 0
ball.physicsBody?.allowsRotation = false
self.addChild(ball)
self.ball.physicsBody?.applyImpulse(CGVector(dx: 2.4, dy: 9.7))
}
I just realized the mass and density of my ball sprite is nil. Why that? Even setting it keeps in nil.
Since i cannot imagine this is some unknown bug in SpriteKits physics engine,
i very hope someone can help me here as this is a full stopper for me.
It is a (known) bug in SpriteKits physic engine. But sometimes just playing with the values will make this bug disappear - for example, try changing the ball's speed (even by small value) every time it's hitting the wall.
Another solutions are listed here:
SpriteKit physics in Swift - Ball slides against wall instead of reflecting
(The same problem as yours, already from 2014, still not fixed..)
SpriteKit is working fine
I created a simple project and it is working fine.
The problem
As #Sulthan already suggested in a comment, I think the problem in your code is this line
el.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: Constants.Config.wallsize, height: Constants.Config.wallsize))
Here instead of creating the physical border for your scene, you are creating a rectangular physics body which overlap the physics body of the Ball producing unrealistic behaviours.
To create the physics border of my scene I simply used this
physicsBody = SKPhysicsBody(edgeLoopFromRect: frame)
Full code
By the way, this is the full code for my project
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let ball = Ball()
ball.position = CGPoint(x:frame.midX, y:frame.midY)
addChild(ball)
physicsBody = SKPhysicsBody(edgeLoopFromRect: frame)
ball.physicsBody!.applyImpulse(CGVector(dx:50, dy:70))
physicsWorld.gravity = CGVector(dx:0, dy:0)
}
}
class Ball: SKSpriteNode {
init() {
let texture = SKTexture(imageNamed: "ball")
super.init(texture: texture, color: .clearColor(), size: texture.size())
let physicsBody = SKPhysicsBody(circleOfRadius: texture.size().width/2)
physicsBody.restitution = 1
physicsBody.friction = 0
physicsBody.linearDamping = 0
physicsBody.allowsRotation = false
self.physicsBody = physicsBody
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I also used scene.scaleMode = .ResizeFill inside GameViewController.swift.
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
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.
I appologise for the confusing question title, but I wasn't quite sure how to phrase my question. My ultimate goal is to remove an SKSpriteNode a certain amount of time after it's creation. However, there is a small complication. Using the following code
func remove() {
laser.removeFromParent()
}
runAction(
SKAction.sequence([
SKAction.waitForDuration(5.0),
SKAction.runBlock(remove)
])
)
I'm able to remove the SKSpriteNode I named 'laser'. When I call my function once, fireLasers(), everything runs smoothly and the laser disappears after 5 seconds. The problem is when I call it twice within a period of 5 seconds. When this happens, the first laser will stick around indefinitely and the second disappears earlier than intended. I understand why this happens, but would like to know if there's a way around it. Here's the code for the GameScene which creates the world, background image, player Sprite, and defines the fireLasers function. There's a lot of stuff that goes on in the ViewController that works with the velocities and directions of the sprites, but I hope this is sufficient to find a solution.
import SpriteKit
class GameScene: SKScene {
var player = SKSpriteNode()
var world = SKShapeNode()
var worldTexture = SKSpriteNode()
var laser = SKShapeNode()
override func didMoveToView(view: SKView) {
self.anchorPoint = CGPointMake(0.5, 0.5)
self.size = CGSizeMake(view.bounds.size.width, view.bounds.size.height)
// Add world
world = SKShapeNode(rectOfSize: CGSizeMake(7000, 7000))
world.physicsBody = SKPhysicsBody(edgeLoopFromPath: world.path!)
world.fillColor = SKColor.blackColor()
self.addChild(world)
// Add Background Image
worldTexture = SKSpriteNode(imageNamed: "grid")
worldTexture.size = CGSize(width: 7000, height: 7000)
world.addChild(worldTexture)
// Add player
player = SKSpriteNode(imageNamed: "Spaceship")
player.size = CGSize(width: 50, height: 50)
player.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 50, height: 50))
player.physicsBody?.affectedByGravity = false
player.physicsBody?.dynamic = true
world.addChild(player)
}
override func update(currentTime: CFTimeInterval) {
world.position.x = -player.position.x
world.position.y = -player.position.y
}
func fireLasers() {
laser = SKShapeNode(rectOfSize: CGSizeMake(10, 50))
laser.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 10, height: 50))
laser.position.x = player.position.x
laser.position.y = player.position.y + CGFloat(50)
laser.fillColor = SKColor.redColor()
laser.physicsBody?.dynamic = true
laser.physicsBody?.affectedByGravity = false
world.addChild(laser)
func remove() {
laser.removeFromParent()
}
runAction(
SKAction.sequence([
SKAction.waitForDuration(5.0),
SKAction.runBlock(remove)
])
)
}
}
Instead of keeping a reference to the laser (which will, as you noticed, force you to only have one around at a time), why not run an action on the created laser nodes themselves? This will allow you to use the handy SKAction class method removeFromParent():
let newLaser = makeNewLaser()
let laserAction = SKAction.sequence([SKAction.waitForDuration(5), SKAction.removeFromParent()])
newLaser.runAction(laserAction)