I am creating a 2D macOS game using SpriteKit and I want a continuous scrolling background from left to right.
I have a background image that is the same size as my frame. The image is duplicated side-by-side: initially the left one is centred and the right is off-screen. Both images (SKSpriteNode) are animated to slide across the screen and then reset their positions once moved by a full frame-width.
Here is the relevant code:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var background = SKSpriteNode()
func makeBackground() {
let texture = SKTexture(imageNamed: "bg-empty")
let scroll = SKAction.move(by: CGVector(dx: -texture.size().width, dy: 0), duration: 30)
let reset = SKAction.move(by: CGVector(dx: texture.size().width, dy: 0), duration: 0)
let animation = SKAction.repeatForever(SKAction.sequence([scroll, reset]))
for idx in 0...1 {
background = SKSpriteNode(texture: texture)
background.position = CGPoint(x: CGFloat(idx)*texture.size().width, y: self.frame.midY)
background.zPosition = -1
background.run(animation)
self.addChild(background)
}
}
override func didMove(to view: SKView) {
makeBackground()
}
}
Although this works, I notice a black (~1 pixel vertical strip) flicker that appears ad-hoc at the seam of the connection.
What is causing this flicker and how do I get rid of it?
You are encountering floating point rounding errors. This is going to lead to a situation where your first BG rounds down, and your second BG rounds up, giving you a 1 pixel gap.
instead, try the following code
class GameScene: SKScene {
private var background = SKNode()
func makeBackground() {
let texture = SKTexture(imageNamed: "bg-empty")
let scroll = SKAction.move(by: CGVector(dx: -texture.size().width, dy: 0), duration: 30)
let reset = SKAction.move(by: CGVector(dx: texture.size().width, dy: 0), duration: 0)
let animation = SKAction.repeatForever(SKAction.sequence([scroll, reset]))
for idx in 0...1 {
let subNode = SKSpriteNode(texture: texture)
subNode.position = CGPoint(x: CGFloat(idx)*texture.size().width, y: self.frame.midY)
background.addChild(subNode)
}
background.zPosition = -1
background.run(animation)
self.addChild(background)
}
override func didMove(to view: SKView) {
makeBackground()
}
}
Now you can avoid the gap because you are not running 2 different actions.
If you want to be able to scroll both ways. then place a texture to the left of your bg.
This will place a background node before and after your main background, and essentially turn it into 1 big node, with only the middle texture showing in its entirety.
If you have multiple background images, then just place the last frame of your background also to the left
Ive been trying to move a cube forward when the user long presses the screen. I can't get it to move smoothly. Is there any way I can move it only forward (in the negative direction) and update the position on each frame the scene?
Here is what I have so far:
var carLocation = SCNVector3(x:0, y: 0, z:-0.01)
func setupScene() {
sceneView = self.view as? SCNView
scene = SCNScene(named: "art.scnassets/Map.scn")
sceneView.scene = scene
let holdRecognizer = UILongPressGestureRecognizer()
holdRecognizer.addTarget(self, action: #selector(GameViewController.hold(recognizer:)))
sceneView.addGestureRecognizer(holdRecognizer)
}
#objc func hold(recognizer: UILongPressGestureRecognizer) {
print("Held down!")
let position = recognizer.location(in: sceneView)
carLocation = SCNVector3(x:0, y: 0, z:-0.01)
carNode.physicsBody?.velocity += carLocation
}
I put a count in your print, just so you could see that LongPress is going to repeat itself... 'a lot', which is probably not what you want long term. So, assuming you created something like:
let p = SCNPhysicsBody(type: .dynamic, shape: nil)
p.isAffectedByGravity = false
shipNode.physicsBody = p
Then this will move your object forward and backward smoothly.
#objc func hold(recognizer: UILongPressGestureRecognizer) {
print("Held down! \(count)")
count += 1
shipNode.physicsBody?.velocity = SCNVector3Make(0.0, 0, 0.51)
}
#objc func handleTap(_ gestureRecognize: UIGestureRecognizer) {
shipNode.physicsBody?.velocity = SCNVector3Make(0.0, 0, -0.51)
}
Unless you have a ton of objects in there, I can't see any reason for it to be slow. Set scnView.showsStatistics = true to see your FPS. Hopefully, you are not trying to run scenekit in the simulator?
I want to detect whether my node was tapped or not. I am using UIGestureRecognizer:
let longPress = UILongPressGestureRecognizer()
longPress.minimumPressDuration = CFTimeInterval(0.0)
longPress.addTarget(self, action: #selector(self.longPressGesture(longpressGest:)))
self.view?.addGestureRecognizer(longPress)
And the function that is called:
#objc func longPressGesture(longpressGest: UIGestureRecognizer) {
let touchPos = longpressGest.location(in: self.view)
if atPoint(touchPos).name == "jump" {
print("jump")
}
}
My button which I want to be detected when it is tapped:
let jump = SKSpriteNode(imageNamed: "Any")
jump = CGSize(width: self.size.width*0.06, height: self.size.height*0.08)
jump = CGPoint(x: self.size.width*0.05 - self.size.width/2, y: self.size.height*0.1 - self.size.height/2)
jump.zPosition = 2
jump.name = "jump"
cameraNode.addChild(jump)
Importend: jump is a child node from my cameraNode
My cameraNode:
self.camera = cameraNode
self.addChild(cameraNode)
let cameraNode = SKCameraNode()
let range = SKRange(constantValue: 0)
let cameraConstraint = SKConstraint.distance(range, to: player)
cameraNode.constraints = [cameraConstraint]
With this code "jump" isn't printed. I think I have to convert the touchPos to the same coordinate system like the cameraNodes or jump buttons system. My question: How can I convert view coordinates to my cameraNodes coordinate system?
P.S. I already tried the whole convert functions which didn't work. Maybe I just did it wrong.
SKNodes have a method called convertPoint
#objc func longPressGesture(longpressGest: UIGestureRecognizer) {
let touchPosinView = longpressGest.location(in: self.view)
let touchPos = self.convertPoint(fromView:touchPosinView )
if atPoint(touchPos).name == "jump" {
print("jump")
}
}
I've implemented a Navigation controller to incorporate an rotating-disc type of layout (imagine each VC laid out in a circle, that rotates as a whole, into view sequentially. The navigation controller is configured to use a custom transition class, as below :-
import UIKit
class RotaryTransition: NSObject, UIViewControllerAnimatedTransitioning {
let isPresenting :Bool
let duration :TimeInterval = 0.5
let animationDuration: TimeInterval = 0.7
let delay: TimeInterval = 0
let damping: CGFloat = 1.4
let spring: CGFloat = 6.0
init(isPresenting: Bool) {
self.isPresenting = isPresenting
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//Get references to the view hierarchy
let fromViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let toViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let sourceRect: CGRect = transitionContext.initialFrame(for: fromViewController)
let containerView: UIView = transitionContext.containerView
if self.isPresenting { // Push
//1. Settings for the fromVC ............................
// fromViewController.view.frame = sourceRect
fromViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3);
fromViewController.view.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3);
//2. Setup toVC view...........................
containerView.insertSubview(toViewController.view, belowSubview:fromViewController.view)
toViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3);
toViewController.view.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3);
toViewController.view.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI / 180));
//3. Perform the animation...............................
UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: {
fromViewController.view.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI / 180));
toViewController.view.transform = CGAffineTransform(rotationAngle: 0);
}, completion: {
(animated: Bool) -> () in transitionContext.completeTransition(true)
})
} else { // Pop
//1. Settings for the fromVC ............................
fromViewController.view.frame = sourceRect
fromViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3);
fromViewController.view.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3);
//2. Setup toVC view...........................
// toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
toViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3);
toViewController.view.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3);
toViewController.view.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI / 180));
containerView.insertSubview(toViewController.view, belowSubview:fromViewController.view)
//3. Perform the animation...............................
UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: {
fromViewController.view.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI / 180));
toViewController.view.transform = CGAffineTransform(rotationAngle: 0);
}, completion: {
//When the animation is completed call completeTransition
(animated: Bool) -> () in transitionContext.completeTransition(true)
})
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration;
}
}
A representation of how the views move is show in the illustration below... The two red areas are the problems, as explained later.
The presenting (push) translation works fine - 2 moves to 1 and 3 moves to 2. However, the dismissing (pop) translation does not, whereby the dismissing VC moves out of view seemingly correctly (2 moving to 3), but the presenting (previous) VC arrives either in the wrong place, or with an incorrectly sized frame...
With the class as-is, the translation results in 2 moving to 3 (correctly) but 1 then moves to 4, the view is correctly sized, yet seems offset, by a seemingly arbitrary distance, from the intended location. I have since tried a variety of different solutions.
In the pop section I tried adding the following line (commented in the code) :-
toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
...but the VC now ends up being shrunk (1 moves to 5). I hope someone can see the likely stupid error I'm making. I tried simply duplicating the push section to the pop section (and reversing everything), but it just doesn't work!
FYI... Those needing to know how to hookup the transition to a UINavigationController - Add the UINavigationControllerDelegate to your nav controller, along with the following function...
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let transition: SwingTransition = SwingTransition.init(isPresenting: ( operation == .push ? true : false ))
return transition;
}
The diagram below shows how all views would share the same originating point (for the translation). The objective is to give the illusion of a revolver barrel moving each VC into view. The top centre view represents the viewing window, showing the third view in the stack. Apologies for the poor visuals...
The problem is that one of the properties in the restored view controller's view isn't getting reset properly. I'd suggest resetting it when the animation is done (you probably don't want to keep the non-standard transform and anchorPoint in case you do other animations later that presume the view is not transformed). So, in the completion block of the animation, reset the position, anchorPoint and transform of the views.
class RotaryTransition: NSObject, UIViewControllerAnimatedTransitioning {
let isPresenting: Bool
let duration: TimeInterval = 0.5
let delay: TimeInterval = 0
let damping: CGFloat = 1.4
let spring: CGFloat = 6
init(isPresenting: Bool) {
self.isPresenting = isPresenting
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewController(forKey: .from)!
let to = transitionContext.viewController(forKey: .to)!
let frame = transitionContext.initialFrame(for: from)
let height = frame.size.height
let width = frame.size.width
let angle: CGFloat = 15.0 * .pi / 180.0
let rotationCenterOffset: CGFloat = width / 2 / tan(angle / 2) / height + 1 // use fixed value, e.g. 3, if you want, or use this to ensure that the corners of the two views just touch, but don't overlap
let rightTransform = CATransform3DMakeRotation(angle, 0, 0, 1)
let leftTransform = CATransform3DMakeRotation(-angle, 0, 0, 1)
transitionContext.containerView.insertSubview(to.view, aboveSubview: from.view)
// save the anchor and position
let anchorPoint = from.view.layer.anchorPoint
let position = from.view.layer.position
// prepare `to` layer for rotation
to.view.layer.anchorPoint = CGPoint(x: 0.5, y: rotationCenterOffset)
to.view.layer.position = CGPoint(x: width / 2, y: height * rotationCenterOffset)
to.view.layer.transform = self.isPresenting ? rightTransform : leftTransform
//to.view.layer.opacity = 0
// prepare `from` layer for rotation
from.view.layer.anchorPoint = CGPoint(x: 0.5, y: rotationCenterOffset)
from.view.layer.position = CGPoint(x: width / 2, y: height * rotationCenterOffset)
// rotate
UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, animations: {
from.view.layer.transform = self.isPresenting ? leftTransform : rightTransform
to.view.layer.transform = CATransform3DIdentity
//to.view.layer.opacity = 1
//from.view.layer.opacity = 0
}, completion: { finished in
// restore the layers to their default configuration
for view in [to.view, from.view] {
view?.layer.transform = CATransform3DIdentity
view?.layer.anchorPoint = anchorPoint
view?.layer.position = position
//view?.layer.opacity = 1
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
}
I did a few other sundry changes, while I was here:
eliminated the semicolons;
eliminated one of the duration properties;
fixed the name of the parameter of the completion closure of animate method to finished rather than animated to more accurately reflect what it's real purpose is ... you could use _, too;
set the completeTransition based upon whether the animation was canceled or not (because if you ever make this interactive/cancelable, you don't want to always use true);
use .pi rather than M_PI;
I commented out my adjustments of opacity, but I generally do that to give the effect a touch more polish and to ensure that if you tweak angles so the views overlap, you don't get any weird artifacts of the other view just as the animation starts or just as its finishing; I've actually calculated the parameters so there's no overlapping, regardless of screen dimensions, so that wasn't necessary and I commented out the opacity lines, but you might consider using them, depending upon the desired effect.
Previously I showed how to simplify the process a bit, but the resulting effect wasn't exactly what you were looking for, but see the previous rendition of this answer if you're interested.
Your problem is a common one that happens when you do custom view controller transitions. I know this because I've done it a lot :)
You're looking for a problem in the pop transition, but the actual problem is in the push. If you inspect the view of the first controller in the stack after the transition, you'll see that it has an unusual frame, because you've messed about with its transform and anchor point and layer position and so forth. Really, you need to clean all that up before you end the transition, otherwise it bites you later on, as you're seeing in the pop.
A much simpler and safer way to do your custom transitions is to add a "canvas" view, then to that canvas add snapshots of your outgoing and incoming views instead and manipulate those. This means you have no cleanup at the end of the transition - just remove the canvas view. I've written about this technique here. For your case, I added the following convenience method:
extension UIView {
func snapshot(view: UIView, afterUpdates: Bool) -> UIView? {
guard let snapshot = view.snapshotView(afterScreenUpdates: afterUpdates) else { return nil }
self.addSubview(snapshot)
snapshot.frame = convert(view.bounds, from: view)
return snapshot
}
}
Then updated your transition code to move the snapshots around on a canvas view instead:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//Get references to the view hierarchy
let fromViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let toViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let sourceRect: CGRect = transitionContext.initialFrame(for: fromViewController)
let containerView: UIView = transitionContext.containerView
// The canvas is used for all animation and discarded at the end
let canvas = UIView(frame: containerView.bounds)
containerView.addSubview(canvas)
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
toView.frame = transitionContext.finalFrame(for: toViewController)
toView.layoutIfNeeded()
let toSnap = canvas.snapshot(view: toView, afterUpdates: true)!
if self.isPresenting { // Push
//1. Settings for the fromVC ............................
// fromViewController.view.frame = sourceRect
let fromSnap = canvas.snapshot(view: fromView, afterUpdates: false)!
fromView.removeFromSuperview()
fromSnap.layer.anchorPoint = CGPoint(x: 0.5, y: 3);
fromSnap.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3);
//2. Setup toVC view...........................
toSnap.layer.anchorPoint = CGPoint(x: 0.5, y: 3);
toSnap.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3);
toSnap.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI / 180));
//3. Perform the animation...............................
UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: {
fromSnap.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI / 180));
toSnap.transform = CGAffineTransform(rotationAngle: 0);
}, completion: {
(animated: Bool) -> () in
containerView.insertSubview(toViewController.view, belowSubview:canvas)
canvas.removeFromSuperview()
transitionContext.completeTransition(true)
})
} else { // Pop
//1. Settings for the fromVC ............................
fromViewController.view.frame = sourceRect
fromViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3);
fromViewController.view.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3);
//2. Setup toVC view...........................
let toSnap = canvas.snapshot(view: toView, afterUpdates: true)!
toSnap.layer.anchorPoint = CGPoint(x: 0.5, y: 3);
toSnap.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3);
toSnap.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI / 180));
//3. Perform the animation...............................
UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: {
fromViewController.view.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI / 180));
toSnap.transform = CGAffineTransform(rotationAngle: 0);
}, completion: {
//When the animation is completed call completeTransition
(animated: Bool) -> () in
containerView.insertSubview(toViewController.view, belowSubview: canvas)
canvas.removeFromSuperview()
transitionContext.completeTransition(true)
})
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration;
}
}
This particular transition is pretty simple so it's not too difficult to reset the properties of the view frames, but if you do anything more complex then the canvas and snapshot approach works really well, so I tend to just use it everywhere.
Right now, I am creating a new game that will involve a ball that moves up and down the screen by itself without touching a button or the screen. I have been looking everywhere for a solution, but I haven't found one. I know how to make the ball move up and down in objective-c, but not in swift or spritekit. I don't know if the following code will help or not. Any solution is accepted.
class GamePlayScene: SKScene, SKPhysicsContactDelegate {
var ball = SKSpriteNode(imageNamed: "green ball.png")
override func didMoveToView(view: SKView) {
//setup scene
physicsWorld.gravity = CGVector.zeroVector
self.scene?.backgroundColor = UIColor.blackColor()
self.scene?.size = CGSize(width: 640, height: 1136)
ball.position = CGPoint (x: self.size.width * 0.5, y: self.size.width * 0.9)
ball.size = CGSize (width: 80, height: 82)
let moveBallUp = SKAction.moveToY(600, duration: 0.4)
let moveBallDown = SKAction.moveToY(293, duration: 0.4)
self.addChild(ball)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let moveBallUp = SKAction.moveToY(600, duration: 0.4)
let moveBallDown = SKAction.moveToY(293, duration: 0.4)
if ball.position.y == 600 {
self.ball.runAction(moveBallUp)
} else {
self.ball.runAction(moveBallDown)
}
}
}
I suggest you use repeatActionForever. If you start your ball position from the bottom of the screen this sequence should work. If you want to start the ball from the top then switch the positions of the actions in moveUpAndDown with each other.
func moveSprite() {
let moveBallUp = SKAction.moveToY(600, duration: 0.4)
let moveBallDown = SKAction.moveToY(293, duration: 0.4)
let moveUpAndDown = SKAction.sequence([moveBallUp, moveBallDown])
let moveUpAndDownForever = SKAction.repeatActionForever(moveUpAndDown)
ball.runAction(moveUpAndDownForever)
}