SKPhysicsJointFixed doesn't keep nodes together while moving one - swift

I've created 2 SKSpriteNodes, and connected them by a SKPhysicsJointFixed, to keep them stuck together. The problem is when I apply a SKAction.move(by:, duration:) to the first, it moves alone. why is that, and how can I move them together? I've searched a lot, but can't seem to find any new or useful information. Please help. Thanks in advance.
Here's the code:
import SpriteKit
class myGame: SKScene {
var node1: SKSpriteNode!
var node2: SKSpriteNode!
func createNode(_ position: CGPoint, color: UIColor) -> SKSpriteNode {
let node = SKSpriteNode(color: color, size: CGSize(width: 50, height: 50))
node.position = position
node.physicsBody = SKPhysicsBody(rectangleOf: node.size, center: position)
node.physicsBody?.affectedByGravity = false
node.physicsBody?.allowsRotation = false
return node
}
func setup() {
node1 = createNode(.zero, color: .red)
node2 = createNode(CGPoint(x: 0, y: -50), color: .green)
self.addChild(node1)
self.addChild(node2)
let anchor = CGPoint(x: node1.size.width/2, y: -node1.size.height/2)
let joint = SKPhysicsJointFixed.joint(withBodyA: node1.physicsBody!, bodyB: node2.physicsBody!, anchor: anchor)
physicsWorld.add(joint)
}
override func didMove(to view: SKView) {
setup()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
node1.run(SKAction.move(by: CGVector(dx: 0, dy: 100), duration: 2))
}
}

This is very strange indeed, but I have encountered similar things before.
Its the AllowRotation on the physicsBody that creates the problem.
fix:
node.physicsBody?.allowsRotation = true
Note:
your physics body for the second node is a bit off. I suggest you do like this instead:
node.physicsBody = SKPhysicsBody(rectangleOf: node.size)
And your joint is currently in the right corner of the nodes, Fix:
let anchor = CGPoint(x: 0, y: -node1.size.height/2)
unless you want it to be like that ofc :)
One last tip if you don't already know it. set showPhysics in your gameViewController to see an outline of your physics bodies. it does help a lot when working with physics.
view.showsPhysics = true

Related

Is there a way I can add more than one sprite in SpriteKit globally?

I am struggling with one issue. Global declaration of my sprite so that I can interact with it. In this game, I have created a local sprite called enemy featured below:
func spawnEnemy() {
let enemy = SKSpriteNode(imageNamed: "as")
let yPosition = CGFloat(frame.maxY - enemy.size.height)
let getXvalue = GKRandomDistribution(lowestValue: Int(frame.minX + enemy.size.width), highestValue: Int(frame.maxX - enemy.size.width))
let xPosition = CGFloat(getXvalue.nextInt())
enemy.position = CGPoint(x: xPosition, y: yPosition)
enemy.name = "asteroid"
enemy.zPosition = 100
addChild(enemy)
let animationDuration:TimeInterval = 6
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: xPosition, y: 0), duration: animationDuration))
actionArray.append(SKAction.self.removeFromParent())
enemy.run(SKAction.sequence(actionArray))
}
I want to tap the enemy to make it disappear from the screen. The variable is declared locally and not globally so the touchesBegan function does not "see" enemy. However, when I move the statement:
let enemy = SKSpriteNode(imageNamed: "as")
outside of local declaration and into global. It works until the code tries to spawn in another enemy and i get an error of "Tried to add an SKNode who already has a parent" This is the code I have running in my view did load:
run(SKAction.repeatForever(SKAction.sequence([SKAction.run{self.spawnEnemy()
}, SKAction.wait(forDuration: 1.0)])))
Every time it spawns a new enemy it crashes and says that the SKNode already has a parent which i understand. However, for my game to function I need the player to be able to touch the individual instance of that enemy and remove it. Hence my code for
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in:self) {
let nodesArray = self.nodes(at:location)
if nodesArray.first?.name == "asteroid" {
print("Test")
enemy.removeFromParent()
print("Test Completed")
}
}
}
Now the error says unresolved use of "enemy" because the enemy is not global. I have been going back and forth on this issue for quite some time. If anyone has any potential solution or work around I would be very grateful, and thank you for your help.
Move your enemies to their own class and handle the touch for each of those enemies in their own class. This cleans up your GameScene and keeps your code more organized. You can now add as many instances of enemy as you want.
FYI not related to this question but somethings to consider after you get this working
when game over or level change or win make sure you have a clean up function to remove all enemies
you should strongly consider recycling your objects vs creating them on the fly...better performance
try to separate as much code to your objects class as possible
class enemy: SKSpriteNode {
init() {
super.init(texture: nil, color: .clear, size: CGSize.zero)
setup()
}
func setup() {
isUserInteractionEnabled = true
name = "asteroid"
zPosition = 100
let image = SKSpriteNode(imageNamed: "as")
imagine.zPosition = 1
addChild(image)
self.size = image.size
animate()
}
func animate() {
let animationDuration: TimeInterval = 6
let move = SKAction.move(to: CGPoint(x: xPosition, y: 0), duration: animationDuration)
let remover = SKAction.self.removeFromParent()
run(SKAction.sequence(move, remover))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
removeFromParent()
}
}
class GameScene: SKScene {
override func didMove(to view: SKView) {
let sequence = SKAction.sequence([SKAction.run{ self.spawnEnemy()
}, SKAction.wait(forDuration: 1.0)])
run(SKAction.repeatForever(sequence))
}
func spawnEnemy() {
let enemy = Enemy()
let yPosition = CGFloat(frame.maxY - enemy.size.height)
let getXvalue = GKRandomDistribution(lowestValue: Int(frame.minX + enemy.size.width), highestValue: Int(frame.maxX - enemy.size.width))
let xPosition = CGFloat(getXvalue.nextInt())
enemy.position = CGPoint(x: xPosition, y: yPosition)
addChild(enemy)
}
}

Display upperLimit SKRange

So here is my code:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var ship1 = [2,1]
var ship2 = [1,2]
let jonahSpriteNode = SKSpriteNode(imageNamed: "jonah_spaceship")
let georgeSpriteNode = SKSpriteNode(imageNamed: "george_spaceship")
override func didMove(to view: SKView) {
//var jonahFrames = [SKTexture]()
jonahSpriteNode.position = CGPoint(x: 30, y: frame.midY)
jonahSpriteNode.size = CGSize(width: 100.0, height: 100.0)
addChild(jonahSpriteNode)
georgeSpriteNode.position = CGPoint(x: 628, y: frame.midY)
georgeSpriteNode.size = CGSize(width: 100.0, height: 100.0)
addChild(georgeSpriteNode)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
var touchLocation = touch.location(in: self)
var angle1 = atan2(jonahSpriteNode.position.y - touchLocation.y , jonahSpriteNode.position.x - touchLocation.x)
var angle = angle1 - CGFloat(Double.pi / 1)
makeCircularRange(to: jonahSpriteNode)
let rotate = SKAction.rotate(toAngle: angle, duration: 1.0)
let move = SKAction.move(to: CGPoint(x: touchLocation.x, y: touchLocation.y), duration: 2.5)
let sequence = SKAction.sequence([rotate, move])
jonahSpriteNode.run(sequence)
}
}
func makeCircularRange(to node: SKNode) {
let range = SKRange(lowerLimit: 0, upperLimit: 400)
let constraint = SKConstraint.distance(range, to: .zero)
node.constraints = [constraint]
}
}
I wanted to display the SKRange by showing upperLimit in a certain color. I tried making nodes around the limit and then coloring the nodes but it just showed a bunch of errors. If you have any ideas please answer.
Something like this:
The sprite node will be in the center and the circle will show where it can move.
You can make a circular shape using range.upperLimit as a radius an add to scene.
func drawCircularRangeBy(range: SKRange) {
let radius = range.upperLimit
let node = SKShapeNode(circleOfRadius: radius)
node.strokeColor = .white
addChild(node)
}
Take a look of this example: https://github.com/Maetschl/SpriteKitExamples/blob/master/CircularRange/CircularRange/GameScene.swift
If you really need dots instead of a line please see this answer: Drawing dashed line in Sprite Kit using SKShapeNode

Moving SpriteNode Texture but not PhysicBody

Working on a game right now, I've faced a problem regarding management of SkSpriteNodes. I have a SpriteNode whose texture size is lower than physicsBody size assigned to it. It is possible to move, thanks to something similar to an SKAction, only the texture of a node and not its physicsBody?
To explain the problem in other terms, I will give you a graphic example:
As you can see, what I want to achieve is not to modify physicsBody proprieties(in order to not affect collision or having problems with continuos reassigning of PhysicsBody entities), but changing its texture length and adjusting its position. How can I achieve this programmatically?
A bit of code for context, which is just illustrative of the problem:
let node = SKSpriteNode(color: .red, size: CGSize(width: 8, height: 60)
node.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 8, height: 60)
self.addChild(node)
//what I've tried is something like that
//It causes glitches in visualisation... and I need to move the object since resizing is towards the center.
let resize = SKAction.scaleY(to: 0.5, duration: 5)
let move = SKAction.move(to: node.position - CGPoint(x:0, y:node.size.height*0.5), duration: 5)
let group = SKAction.group([resize, move])
node.run(group)
//And this is even worse if I add, in this specific example, another point fixed to the previous node
let node2 = SKSpriteNode(color: .blue, size: CGSize(width: 8, length: 8)
node2.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 8, height: 8))
node2.position = CGPoint(x: 0, y: -node.height)
self.addChild(node2)
self.physicsWorld.add(SKPhysicsJointFixed.joint(withBodyA: node.physicsBody! , bodyB: node2.physicsBody!, anchor: CGPoint(x: 0, y: -node.size.height)))
I get your problem.
let resize = SKAction.scaleY(to: 0.5, duration: 5)
This line will cause the physicsBody to scale the x and y axis uniformly. While your texture will just scale the y axis.
Its not so straight forward changing physicsBody shapes to match actions
One way to do it though would be to call a method from
override func didEvaluateActions()
Something like this:
var group1: SKAction? = nil
var group2: SKAction? = nil
var touchCnt = 0
var test = SKSpriteNode(texture: SKTexture(imageNamed: "circle"), color: .blue, size: CGSize(width: 100, height: 100))
func setActions() {
let newPosition = CGPoint(x: -200, y: 300)
let resize1 = SKAction.scaleY(to: 0.5, duration: 5)
let move1 = SKAction.move(to: newPosition, duration: 5)
group1 = SKAction.group([resize1, move1])
let resize2 = SKAction.scaleY(to: 1, duration: 5)
let move2 = SKAction.move(to: position, duration: 5)
group2 = SKAction.group([resize2, move2])
}
func newPhysics(node: SKSpriteNode) {
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.size)
node.physicsBody?.affectedByGravity = false
node.physicsBody?.allowsRotation = false
node.physicsBody?.usesPreciseCollisionDetection = false
}
override func sceneDidLoad() {
physicsWorld.contactDelegate = self
test.position = CGPoint(x: 0, y: 300)
setActions()
newPhysics(node: test)
addChild(test)
}
override func didEvaluateActions() {
newPhysics(node: test)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if touchCnt == 0 {
if !test.hasActions() {
test.run(group1!)
touchCnt += 1
}
} else {
if !test.hasActions() {
test.run(group2!)
touchCnt -= 1
}
}
}
if you put the above code in your gameScene, taking care to replace any duplicated methods and replacing the test node texture. then when you tap the screen the sprite should animate as you want while keeping the physics body is resized at the same time. There are a few performance issues with this though. As it changes the physics body on each game loop iteration.

view?.ispaused = true with skscene not working

so I'm working with a game in SpriteKit and I am using this code to pause the game.
self.pauseButton.alpha = 0
self.playButton.alpha = 1
self.settingsBackground.alpha = 0.85
self.isPaused = true
self.pauseButton.alpha = 1
self.playButton.alpha = 0
self.settingsBackground.alpha = 0
The code which is run before pausing is to change the appearance of the pause and the code afterward reverts it. The problem is that the code before pausing is not getting run and instead the game is just pausing before changing the visuals around. I've tried adding delays, making the steps SKActions, and testing just the code before the pause. When I run just the first 3 lines the visuals change properly but obviously, the game doesn't pause. When I run the whole thing the game pauses but the visuals don't change. Help!
the problem is that even when a scene isPaused the code still get run. Yes you are pausing the scene, yes the visuals do get run...but the visuals after the isPaused line also get run and reset the visuals that you just changed.
here is a really simple example of how this working. there are 3 boxes on the scene; the bottom box has an action that repeatedly scales up and down, and will stop when the scene is paused. the top box will pause the game when pressed and will reveal the middle box which will unpause the game.
class GameScene: SKScene {
private var test: SKSpriteNode!
private var test2: SKSpriteNode!
private var test3: SKSpriteNode!
override func didMove(to view: SKView) {
backgroundColor = .clear
test = SKSpriteNode(color: .blue, size: CGSize(width: 200, height: 200))
test.position = CGPoint(x: 0, y: 350)
addChild(test)
test2 = SKSpriteNode(color: .red, size: CGSize(width: 200, height: 200))
test2.position = CGPoint(x: 0, y: 0)
test2.alpha = 0
addChild(test2)
test3 = SKSpriteNode(color: .yellow, size: CGSize(width: 200, height: 200))
test3.position = CGPoint(x: 0, y: -350)
addChild(test3)
let scaleDown = SKAction.scale(to: 0.1, duration: 1.0)
let scaleUp = SKAction.scale(to: 1.0, duration: 1.0)
let sequence = SKAction.sequence([scaleDown, scaleUp])
let repeater = SKAction.repeatForever(sequence)
test3.run(repeater)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch! {
let touchLocation = touch.location(in: self)
if test.contains(touchLocation) {
print("pausing")
self.test2.alpha = 1
self.test.alpha = 0
self.isPaused = true
}
if test2.contains(touchLocation) {
print("unpausing")
self.test2.alpha = 0
self.test.alpha = 1
self.isPaused = false
}
}
}
}

How Make a Sprite Node Rotate Around Another Node? (One More Line)

So, I am trying to challenge my self by recreating the popular App Store game, One More Line using SpriteKit and Swift 3; however, I am immediately having a big issue.
1)How do I get my node to rotate around another node when the user is tapping
2) I want my node to continue moving in the same direction it is facing when the user releases their finger (hope that makes sense...).
kinda like this
In my GameScene.sks I created a center node that is constantly rotating and a player. Then I did this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
player.move(toParent: center)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
player.move(toParent: self)
}
override func update(_ currentTime: TimeInterval) {
let dx = 5 * cos(player.zRotation)
let dy = 5 * sin(player.zRotation)
player.physicsBody?.applyImpulse(CGVector(dx: dx, dy: dy))
}
player moves to the right and when I tap it circles around the node, when I release it continues to move to the right.
I know that I am going about this all wrong. Any help would be very much appreciated. Thanks!
I'm not sure I completely understand what you are looking for, but if you simply want a sprite to rotate evenly around a centre sprite, add the centre sprite, another centre sprite (invisible) and add your outer sprite to the invisible centre sprite. Then rotate your two centre sprites. Adjust the outerObject's x position to bring the outerObject closer or further from the centre object.
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
anchorPoint = CGPoint(x: 0.5, y: 0.5)
let centerObject = SKSpriteNode(texture: nil, color: UIColor.blue, size: CGSize(width: 20, height: 20))
addChild(centerObject)
let invisibleParent = SKSpriteNode()
addChild(invisibleParent)
let outerObject = SKSpriteNode(texture: nil, color: UIColor.red, size: CGSize(width: 50, height: 50))
outerObject.position.x = 100
invisibleParent.addChild(outerObject)
centerObject.run(SKAction.repeatForever(SKAction.rotate(byAngle: 1, duration: 1)))
invisibleParent.run(SKAction.repeatForever(SKAction.rotate(byAngle: -1, duration: 1)))
}
}