How to drag a SKSpriteNode without touching it with Swift - swift

I'm trying to move SKSpriteNode horizontally by dragging. To get the idea of what I try to achieve you can watch this. I want player to be able to drag sprite without touching it. But I don't really know how to implement it correctly.
I tried to do something like:
override func didMoveToView(view: SKView) {
/* Setup your scene here */
capLeft.size = self.capLeft.size
self.capLeft.position = CGPointMake(CGRectGetMinX(self.frame) + self.capLeft.size.height * 2, CGRectGetMinY(self.frame) + self.capLeft.size.height * 1.5)
capLeft.zPosition = 1
self.addChild(capLeft)
let panLeftCap: UIPanGestureRecognizer = UIPanGestureRecognizer(target: capLeft, action: Selector("moveLeftCap:"))
And when I'm setting a moveLeftCap function, code that I've found for UIPanGestureRecognizer is requiring "View" and gives me an error. I also wanted to limit min and max positions of a sprite through which it shouldn't go.
Any ideas how to implement that?

You probably get that error because you can't just access the view from any node in the tree. You could to refer to it as scene!.view or you handle the gesture within you scene instead which is preferable if you want to keep things simple.
I gave it a try and came up with this basic scene:
import SpriteKit
class GameScene: SKScene {
var shape:SKNode!
override func didMoveToView(view: SKView) {
//creates the shape to be moved
shape = SKShapeNode(circleOfRadius: 30.0)
shape.position = CGPointMake(frame.midX, frame.midY)
addChild(shape)
//sets up gesture recognizer
let pan = UIPanGestureRecognizer(target: self, action: "panned:")
view.addGestureRecognizer(pan)
}
var previousTranslateX:CGFloat = 0.0
func panned (sender:UIPanGestureRecognizer) {
//retrieve pan movement along the x-axis of the view since the gesture began
let currentTranslateX = sender.translationInView(view!).x
//calculate translation since last measurement
let translateX = currentTranslateX - previousTranslateX
//move shape within frame boundaries
let newShapeX = shape.position.x + translateX
if newShapeX < frame.maxX && newShapeX > frame.minX {
shape.position = CGPointMake(shape.position.x + translateX, shape.position.y)
}
//(re-)set previous measurement
if sender.state == .Ended {
previousTranslateX = 0
} else {
previousTranslateX = currentTranslateX
}
}
}
when you move you finger across the screen, the circle gets moves along the x-axis accordingly.
if you want to move the sprite in both x and y directions, remember to invert the y-values from the view (up in view is down in scene).

Related

Swift: Strange Collision Behavior

I'm building a Breakout game (#cs193p) and I've got the beginnings of the general working set up: The bricks, ball, and paddle all draw as they should. The collisions sort of work as they should, except the collision boundaries appear to be sort of wrong. They're generally larger than the paths I've constructed them with, but not always.
I've set the elasticity of the ball to zero, so that it rests on the paddle, so that the discrepancy is clear. This screenshot shows the ball resting on the incorrect collision boundary of the paddle.
The bricks and the black area at the bottom respond a little differently. For the bricks, the ball seems to be colliding with the bottom row of bricks when it visually reaches the next row. I've got the bricks disappearing "correctly" so the collision is doubly confirmed by the bricks disappearing. The black area at the bottom (the space for panning to move the paddle) has a similar issue: the ball dips a little into this area before bouncing.
Here is, well, a bunch of code, because I don't know where the problem might lie.
From my BreakoutBehavior class (I'm leaving out the gravity section because it doesn't seem to be a part of the problem):
let collider: UICollisionBehavior = {
let collider = UICollisionBehavior()
collider.translatesReferenceBoundsIntoBoundary = true
return collider
}()
private let ballBehavior: UIDynamicItemBehavior = {
let behavior = UIDynamicItemBehavior()
behavior.allowsRotation = true
behavior.elasticity = 1.25
return behavior
}()
internal func addColliderBoundary(path: UIBezierPath, named name: String) {
collider.removeBoundary(withIdentifier: name as NSCopying)
collider.addBoundary(withIdentifier: name as NSCopying, for: path)
}
override init() {
super.init()
addChildBehavior(gravity)
addChildBehavior(collider)
addChildBehavior(ballBehavior)
}
internal func addItem (_ item: UIDynamicItem) {
gravity.addItem(item)
collider.addItem(item)
ballBehavior.addItem(item)
}
And here's some code from my BreakoutView class:
I'm giving you the paddle as just one example of a boundary problem, rather than also giving the bricks and pan area, to be more efficient. So of course some of the variables I refer to won't be present in my excerpts. Note that I've left out some things like adding the behavior to the animator, but know that everything is indeed working, I'm just having these boundary problems.
private var paddleSize: CGSize {
return CGSize(width: brickSize.width * 3, height: brickSize.height)
}
private var paddleOrigin: CGPoint {
let x = frame.midX - paddleSize.width / 2
let y = panOrigin.y - paddleSize.height
return CGPoint(x: x, y: y)
}
private func addBallAndPaddle () {
let paddleFrame = CGRect(origin: paddleOrigin, size: paddleSize)
let paddleView = UIView(frame: paddleFrame)
paddleView.backgroundColor = UIColor.white
paddleView.layer.borderWidth = 0.25
addSubview(paddleView)
paddle = paddleView
behavior.addColliderBoundary(path: UIBezierPath(rect: paddleFrame), named: Boundaries.paddle)
let ballFrame = CGRect(origin: ballOrigin, size: ballSize)
let ballView = UIView(frame: ballFrame)
ballView.backgroundColor = UIColor.white
ballView.layer.borderWidth = 0.25
addSubview(ballView)
behavior.addItem(ballView)
ball = ballView
}
private lazy var animator: UIDynamicAnimator = {
let animator = UIDynamicAnimator(referenceView: self.superview!)
animator.delegate = self
return animator
}()
private lazy var behavior: BreakoutBehavior = {
let behavior = BreakoutBehavior()
behavior.collider.collisionDelegate = self
return behavior
}()
And here's code from the BreakoutViewController. configureGameView() is called in viewDidLayoutSubviews
private func configureGameView () {
gameView.frame = topView.frame // topView is the view in IB
gameView.backgroundColor = UIColor.lightGray
gameView.addViews() // adds bricks, ball, paddle, pan area
firstTap.numberOfTapsRequired = 1
gameView.addGestureRecognizer(firstTap) // DOESN'T WORK
if let panView = gameView.panner {
panView.addGestureRecognizer(UIPanGestureRecognizer(target: gameView, action: #selector(gameView.movePaddle(_:))))
panView.addGestureRecognizer(firstTap) // DOESN'T WORK
}
}
Thanks!

Creating endless background without image

I'm creating a simple game with Swift and SpriteKit.
I want to add an endless background (vertically), I only found answers for background with images but I need to do it without image, only background color.
I thought about checking if the player is in the frame.maxY, if so, to move it back to the starting point, but I was wondering if there is a better idea.
//Does not matter which ring we chose as all ring's 'y' position is the same.
func moveBackgroundUp(){
if ((mPlayer.position.y >= self.frame.maxY) || (mRingOne.position.y >= self.frame.maxY)) {
mPlayer.position.y = 150 //mPlayers original starting point.
for ring in mRings {
ring.position.y = 350
}
}
}
Thanks in advance!
Don't just move a background up the screen, that' really isn't the way to go about it. What you should do is detect the position of the camera (assuming it moves with the player), and when it's position is about to occupy space outside of the occupied space of your current background sprite, add a new background sprite to the scene where the last one left off. Here is an example of how to do that with just a red sprite:
First add a property to the scene to track level position:
// To track the y-position of the level
var levelPositionY: CGFloat = 0.0
Now create a method to update your background:
func updateBackground() {
let cameraPos = camera!.position
if cameraPos.y > levelPositionY - (size.height * 0.55) {
createBackground()
}
}
func createBackground() {
// Create a new sprite the size of your scene
let backgroundSprite = SKSpriteNode(color: .red, size: size)
backgroundSprite.anchorPoint = CGPoint(x: 0.5, y: 0)
backgroundSprite.position = CGPoint(x: 0, y: levelPositionY)
// Replace backgroundNode with the name of your backgroundNode to add the sprite to
backgroundNode.addChild(backgroundSprite)
levelPositionY += backgroundSprite.size.height
}
Now you want to call updateBackground inside your overridden update(_:) method:
override func update(_ currentTime: TimeInterval) {
// All your other update code
updateBackground()
}
Also, make sure to create an initial background when you first create the scene:
override func didMove(to view: SKView) {
createBackground()
}
NOTE! - It's important to set the custom anchor point for the background sprite for this code to work properly. An anchor of (0.5, 0) allows the background sprite to be anchored in the middle of the scene on the x-axis, but at the bottom of the scene on the y-axis. This allows you to easily stack one on top of the other.
EDIT - I forgot to mention that it's also a good idea to conserve resources and remove any background nodes that are outside the viewable area and won't be coming back in (i.e. a continuous scrolling game where you can't go backwards). You could do that by updating your updateBackground method above:
func updateBackground() {
let cameraPos = camera!.position
if cameraPos.y > levelPositionY - (size.height * 0.55) {
createBackground()
}
// Make sure to change 'backgroundNode' to whatever the name of your backgroundNode is.
for bgChild in backgroundNode.children {
// This will convert the node's coordinates to scene's coordinates. See below for this function
let nodePos = fgNode.convert(fgChild.position, to: self)
if !isNodeVisible(bgChild, positionY: nodePos.y) {
// Remove from it's parent node
bgChild.removeFromParent()
}
}
}
func isNodeVisible(_ node: SKNode, positionY: CGFloat) -> Bool {
if !camera!.contains(node) {
if positionY < camera!.position.y - size.height * 2.0 {
return false
}
}
return true
}
So above you just loop through all the children inside your background node and detect if they are out of view, and if so remove them from the parent. Make sure to change my generic backgroundNode to whatever the name of your background node is.

Sprite Kit - Sometimes ball disappearing from the screen?

I'm new with sprite kit. I have tried simple ball bouncing game with 2 player, another is tracking the ball slowly. But I have discovered a problem. When I move the line to ball (with edge) ball disappearing from the screen. Another times not a problem, ball bouncing. What is the problem?
I have one GameScene, sks and ViewController. My sprite nodes coming from sks. If someone explain this case. It would be better. I have attached what I did below.
My GameScene:
class GameScene: SKScene {
var ball = SKSpriteNode()
var enemy = SKSpriteNode()
var main = SKSpriteNode()
override func didMove(to view: SKView) {
ball = self.childNode(withName: "ball") as! SKSpriteNode
enemy = self.childNode(withName: "enemy") as! SKSpriteNode
main = self.childNode(withName: "main") as! SKSpriteNode
ball.physicsBody?.applyImpulse(CGVector(dx: -20, dy: -20))
ball.physicsBody?.linearDamping = 0
ball.physicsBody?.angularDamping = 0
let border = SKPhysicsBody(edgeLoopFrom: self.frame)
border.friction = 0
border.restitution = 1
self.physicsBody = border
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
main.run(SKAction.moveTo(x: location.x, duration: 0.2))
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
main.run(SKAction.moveTo(x: location.x, duration: 0.2))
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
enemy.run(SKAction.moveTo(x: ball.position.x, duration: 0.5))
}
View controller:
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
}
}
override var prefersStatusBarHidden: Bool {
return true
}
Pad settings:
Ball settings:
Some updates
I have tried some messages in update function, then encountered with same case ball goes outside from left side of the device (using iPhone 6S)
2016-12-08 14:27:54.436485 Pong[14261:3102941] fatal error: ball out of left bounds: file
You're pinching the ball against the wall, with the enemy. This means that the force is eventually enough to create enough speed of ball movement/force to overcome the physics system, so it pops through the wall. If you make your enemy stop before it pinces the ball against the wall, you should be fine.
This 'pincing' is occurring because of this line of code:
enemy.run(SKAction.moveTo(x: ball.position.x, duration: 0.5))
This is making the enemy chase the ball, which is a good idea for a ball game, but for the way it's being moved is wrong. Using an Action means the enemy has infinite force applied to it, and is aiming for the middle of the ball.
So when the ball gets to the wall, it's stopped against a physics object with infinite static force, then this enemy comes along and applies infinite force from the other side... and the ball either pops inside the bounds of the enemy, or over the other side of the wall, because it's being crushed by infinite forces.
So you either need to take very good care of how you control the enemy with Actions, or use forces to control the enemy, as these won't be infinite, and the physics system will be able to push back on the enemy.
How easy is it to reproduce the problem? In update(), print the ball's position to see where it is when it has 'disappeared'. (this will produce a lot of output, so be warned).
From what you've posted, it doesn't look like the ball is set to collide with the border, meaning the ball will not react (i.e. bounce off) the border and the border itself is immobile (as it's an edge-based physics body). This, combined with a high ball velocity (from a hard hit) might make it possible that you have hit the ball so hard with the 'main' sprite that it's gone through the border - using preciseCollisionDetection=true might resolve this but give the border a category first and add this to the ball's collisionBitMask.
here is an example of what Steve is saying (in your .update())
if ball.position.x > frame.maxX { fatalError(" ball out of right bounds") }
if ball.position.x < frame.minX { fatalError(" ball out of left bounds") }
if ball.position.y > frame.maxY { fatalError(" ball out of top bounds") }
if ball.position.y < frame.minY { fatalError(" ball out of bottom bounds) }
you could also just spam your debug window:
print(ball.position)
This will help you to find out what is going on--if your ball is flying through the boundary, or if it's getting destroyed somewhere, or some other possible bug.
As a workaround (for now) I would just replace the above "fatalError" with "ball.position = CGPoint(x: 0, y: 0)" or some other position to "reset" the ball in case of it getting lost.
You could even store it's last position in a variable, then restore it to that should the above if-statements trigger.
var lastBallLocation = CGPoint(x: 0, y: 0) // Just to initialize
override func update( prams ) {
if ball.position.x > frame.maxX { ball.position = lastBallLocation }
// .. copy the other three cases
lastBallLocation = ball.position // update only on successful position
Or, you could try making the walls thicker (use a shape node or spritenode and lay them on the outside of the frame such as the walls of a house, and your view on screen is the "room")
each wall also has a physics body for bouncing:

What is the best way to zoom and deplace nodes?

I'm working with Swift 3, SpriteKit and Xcode.
So I have a node named backgroundNode, and I attach every nodes of my game to this backgroundNode.
Now I would like to be able to zoom on my game with a pinch gesture, and when I'm zoomed in to navigate in my game.
I see 2 possibilities to do this :
deplace the background node and change its scale to zoom in and zoom out,
use SKCameraNode
What do you think is the best option ?
I already tried the first option but the zoom gesture is quite complex as if I scale the backgroundNode up when I want to zoom, the anchor point is in 0;0 and not 0.5;0.5 so it doesnt zoom where the pinch gesture is detected, but from the bottom right corner, I don't know if you see what I mean.
And for the second option, I can't manage to move the camera without having a glitchy effect, maybe my code is wrong but it really seems correct.
Can you help me ?
Edit : So I got it working using SKCameraNode and UIPanGestureRecognizer, here is the code :
var cam: SKCameraNode!
let panGesture = UIPanGestureRecognizer()
override func didMove(to view: SKView)
{
cam = SKCameraNode()
camera = cam
cam.position = CGPoint(x: playableRect.midWidth, y: playableRect.midHeight)
addChild(cam)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(GameScene.panFunction))
view.addGestureRecognizer(panGesture)
}
func panFunction(pan : UIPanGestureRecognizer)
{
let deltaX = pan.translation(in: view).x
let deltaY = pan.translation(in: view).y
cam.position.x -= deltaX
cam.position.y += deltaY
pan.setTranslation(CGPoint(x: 0, y: 0), in: view)
}
Now I'm struggling with the Zoom. I tried using UIPinchGestureRecognizer but it doesn't work as good as the pan gesture, here is what I tried :
var firstPinch: CGFloat = 0
var pinchGesture = UIPinchGestureRecognizer()
let panGesture = UIPanGestureRecognizer()
var cam: SKCameraNode!
override func didMove(to view: SKView)
{
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(GameScene.pinchFunction))
view.addGestureRecognizer(pinchGesture)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(GameScene.panFunction))
view.addGestureRecognizer(panGesture)
}
func pinchFunction(pinch : UIPinchGestureRecognizer)
{
if UIGestureRecognizerState.began == pinch.state
{
firstPinch = pinch.scale
}
actualPinch = pinch.scale
cam.xScale -= actualPinch - firstPinch
cam.yScale -= actualPinch - firstPinch
}
How would you do it ?
You need to post your code. I helped someone with this in another forum. Their code + my answer should give a general idea of what to do:
https://forums.developer.apple.com/message/192823#192823
Basically it involves UIPanGestureRecognizer etc, and then a bit of delta-scale logic to adjust for the new bounds of the camera.

Keeping an image on screen in Swift

I have a game where I use a UISwipeGestureRecognizer to move the screen. I am trying to figure out how to make it so that the image that I am swiping cannot move off the screen. I have looked around the internet and have not been able to find anything.
Here is my swipe method:
func swipedRight(sender: UISwipeGestureRecognizer) {
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("moveBackground"), userInfo: nil, repeats: true)
if imageView.frame.origin.x > 5000 {
imageView.removeFromSuperview()
}
}
Here is my moveBackground method:
func moveBackground() {
self.imageView.frame = CGRect(x: imageView.frame.origin.x - 100, y: imageView.frame.origin.y, width: 5500, height: UIScreen.mainScreen().bounds.height)
}
The only things I have tried so far is to check if the view is in a certain position and remove it if it is but that hasn't worked. I added that code to the swipe method.
It will be very helpful if you post your class code here, so we can be more specific helping you.
By the way, you should check the object position each time the function is called, before move it, taking in mind its size.
Lets say you have a playing cards game and you are swiping a card along the board.
To avoid swiping the card off the board (off the screen) you will have to do something like this:
// I declare the node for the card with an image
var card = SKSpriteNode(imageNamed: "card")
// I take its size from the image size itself
card.physicsBody = SKPhysicsBody(rectangleOfSize: card.size)
// I set its initial position to the middle of the screen and add it to the scene
card.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2)
self.addChild(card)
// I set the min and max positions for the card in both X and Y axis regarding the whole scene size.
// Fit it to your own bounds if different
let minX = card.size.width/2
let minY = card.size.height/2
let maxX = self.frame.size.width - card.size.height/2
let maxY = self.frame.size.width - card.size.height/2
// HERE GOES YOUR SWIPE FUNCTION
func swipedRight(sender: UISwipeGestureRecognizer) {
// First we check the current position for the card
if (card.position.x <= minX || card.position.x >= maxX || card.position.y <= minY || card.position.y >= maxY) {
return
}
else {
// ...
// YOUR CODE TO MOVE THE ELEMENT
// ...
}
}
I hope this helps!
Regards.