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
Related
How can I create a visual effect of traveling wave like this in Swift SpriteKit?
I am using an extension to the SKAction that performs the oscillatory movement in the node, but I still do not know how to create the trail. I though in creating copies but it did not worked.
extension SKAction {
static func oscillation(scene: SKScene, amplitude a: CGFloat, timePeriod t: CGFloat, midPoint: CGPoint) -> SKAction {
let action = SKAction.customAction(withDuration: Double(t)) { node, currentTime in
let displacement = a * sin(2 * .pi * currentTime / t)
node.position.y = midPoint.y + displacement
let copy = node.copy() as! SKSpriteNode
copy.position.x = node.position.x
copy.position.y = node.position.y
scene.addChild(copy)
}
return action
}
}
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let node = SKSpriteNode(color: UIColor.greenColor(), size: CGSize(width: 50, height: 50))
node.position = CGPoint(x: 25, y: size.height / 2)
self.addChild(node)
let oscillate = SKAction.oscillation(amplitude: 200, timePeriod: 1, midPoint: node.position)
node.runAction(SKAction.repeatActionForever(oscillate))
node.runAction(SKAction.moveByX(size.width, y: 0, duration: 5))
}
}
I would try a particle emitter attached to the moving node. It could be set to produce dot shaped particles quickly enough that they would overlap and make a line. The particles would move at a constant speed to the left, with no variations or fade. Set the count high enough that the oldest particles go out of view before disappearing. If you don’t need a solid line, you could cut down the emitter rate and make a dotted line showing the traveling wave. There are oodles of options in particle emitters, so you could play around with the settings to get other effects if appropriate for your project.
I'm having trouble using SKNode.contains() on scaled sprites, and I can't figure out what important concept I'm missing.
I have "gremlin" sprites that move around inside a "jail" sprite. Gremlins can go through walls sometimes, and I want to know when they've left the jail. I thought I could simply use jail.contains(gremlin) (or some variant), perhaps with a bit of math done on it to get the scale right, but no joy. I can't seem to figure out which sprite or which frame, or which size, or what kind of transform, or whatever, to use with contains().
I've read quite a bit about coordinate systems, frames and bounds, hit testing, scaling, frames, sizes, origins, anchor points, everything I've been able to find. I've read this SO question, and this, this, this, and this, and quite a few others.
I'm missing something. Here's some stripped-down code that shows the problem I'm having. This is the cleaned-up version. I've tried every permutation of convertPoint(), shrinking and growing size, shrinking and growing scale, changing who is whose parent, and various other desperate attempts to understand. No luck.
In this sample code, just to get my head around the problem, I'd like to control which of the big rectangles is to serve as the container for the small ones. The way it's set right now, I'm trying to get the little rectangles outside the red rectangle to be gray. As you can see, it seems that the red rectangle's hit-test area is the same size as that of the blue rectangle. That is, everything is green except the ones out at the very edges of the blue rectangle.
Help!
class GameScene: SKScene {
static var shared: GameScene?
var bottomSprite: SKSpriteNode!
var middleSprite: SKSpriteNode!
var setupComplete = false
var spriteTexture: SKTexture?
var textureAtlas: SKTextureAtlas?
var topSprite: SKSpriteNode!
func getMarkerColor(outerSprite: SKSpriteNode, innerSprite: SKSpriteNode) -> NSColor {
return outerSprite.frame.contains(innerSprite.frame) ? .green : .gray
}
override func didMove(to view: SKView) {
GameScene.shared = self
spriteTexture = SKTexture(imageNamed: "debugRectangle")
}
func drawContainerSprite(parent: SKNode, scale: CGFloat, color: NSColor) -> SKSpriteNode {
let sprite = SKSpriteNode(texture: spriteTexture)
sprite.color = color
sprite.colorBlendFactor = 1.0
sprite.anchorPoint = CGPoint(x: 0.5, y: 0.5)
sprite.setScale(scale)
sprite.size = scene!.size
parent.addChild(sprite)
sprite.position = CGPoint.zero
return sprite
}
func drawMarkerSprite(parent: SKNode, scale: CGFloat) -> SKSpriteNode {
let sprite = SKSpriteNode(texture: spriteTexture)
sprite.size = CGSize(width: bottomSprite.size.width * 0.05, height: bottomSprite.size.height * 0.05)
sprite.colorBlendFactor = 1.0
sprite.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let x = Int.random(in: Int(-self.bottomSprite.size.width)..<Int(self.bottomSprite.size.width))
let y = Int.random(in: Int(-self.bottomSprite.size.height)..<Int(self.bottomSprite.size.height))
sprite.position = CGPoint(x: x, y: y)
parent.addChild(sprite)
return sprite
}
override func update(_ currentTime: TimeInterval) {
if !setupComplete {
bottomSprite = drawContainerSprite(parent: self.scene!, scale: 0.5, color: .blue)
middleSprite = drawContainerSprite(parent: bottomSprite, scale: 0.5, color: .orange)
topSprite = drawContainerSprite(parent: middleSprite, scale: 1.0, color: .red)
setupComplete = true
}
let markerSprite = drawMarkerSprite(parent: self.scene!, scale: 1.0)
markerSprite.color = getMarkerColor(outerSprite: topSprite, innerSprite: markerSprite)
}
}
Here is what you're missing: SKNode.contains(_:) is not the same as CGRect.contains(_:).
SKNode.contains(_:) is not about rectangles. It's about hit-testing whether a point is inside a parent node. CGRect.contains(_:) is about rectangles, and it knows nothing about nodes, or scaling, or SpriteKit. If you're going to use a sprite's frame to check whether the gremlins are getting out of jail, you'll have to scale it yourself.
I have a scene that acts as a menu page for a game and use a UI Button to activate an skaction that moves the camera on the scene and change the menu buttons using a UIView Container.
The code simply moves the camera from the far left to the far right. At the exact same speed. And yet for some reason, the movement to the right is slower than the left?
func MoveCameraToRight () {
let CameraWidth = StartingWidth * SceneCamera.xScale
let MoveRight = SKAction.move(to: CGPoint(x: (scene?.frame.maxX)!-(CameraWidth/2), y: (camera?.position.y)!), duration: 1)
camera?.run(MoveRight)
}
func MoveCameraToLeft () {
let CameraWidth = StartingWidth * SceneCamera.xScale
let MoveLeft = SKAction.move(to: CGPoint(x: (scene?.frame.minX)!-(CameraWidth/2), y: (camera?.position.y)!), duration: 1)
camera?.run(MoveLeft)
}
As you can see, the durations are exactly the same. Yet for some reason on an iPhone XS Max it appears to be significantly slower than the move left action?
Here's the code that makes those actions run:
#IBAction func Openlevels () {
let Game = (self.view as! SKView).scene as! MainMenuScene
Game.MoveCameraToRight()
LevelsContainer.isHidden = false
}
func HideLevels () {
let Game = (self.view as! SKView).scene as! MainMenuScene
Game.MoveCameraToLeft()
UIView.animate(withDuration: 0.25, animations: {
self.LevelsContainer.alpha = 0
}) { (complete: Bool) in
self.LevelsContainer.isHidden = true
}
}
And then on the container view to dismiss (and move camera back to the left):
#IBAction func Home () {
let Parent = self.parent as! MainMenu
Parent.HideLevels()
Parent.ShowMenu()
}
Both SKActions for the camera's position are set to 1, so why would one be slower than the other?
The solution was actually very simple:
let MoveLeft = SKAction.move(to: CGPoint(x: (scene?.frame.minX)!-(CameraWidth/2), y: (camera?.position.y)!), duration: 1)
Move left should be min + camWidth/2 not minus. The problem was it was telling the camera to go further than the distance of moving to the right.
let MoveRight = SKAction.move(to: CGPoint(x: (scene?.frame.maxX)!-(CameraWidth/2), y: (camera?.position.y)!), duration: 1)
I´ve made a game with SpriteKit with different bricks falling down on a hill. When these physical bodies fall down they usually bounce off the hill and change their alignment(they spin a couple of times). If I transition to the GameOver scene and press replay(back to the GameScene) the physics bodies are still aligned like when I left the scene. But I want them to have a horizontal alignment like in the beginning.
GameScene:
import SpriteKit
import GameplayKit
let hillTexture = SKTexture(imageNamed: "HillIllustration")
let hillIllustration = SKSpriteNode(texture: hillTexture)
let brickTexture = SKTexture(imageNamed: "BrickIllustration")
let brick = SKSpriteNode(texture: brickTexture)
class GameScene: SKScene {
//Hill
hillIllustration.setScale(0.7)
hillIllustration.position = CGPoint(x: self.size.width / 2, y: self.size.height * 0.16)
hillIllustration.zPosition = 2
hillIllustration.physicsBody = SKPhysicsBody(polygonFrom: clipPath)
hillIllustration.physicsBody?.isDynamic = false
hillIllustration.physicsBody?.categoryBitMask = CollisionBitMask.Hill
hillIllustration.physicsBody?.affectedByGravity = false
self.addChild(hillIllustration)
//The brick is a child of the hill node
brick.setScale(1)
brick.position = CGPoint(x: -350, y: self.size.height * 0.5)
brick.zPosition = 1
brick.physicsBody = SKPhysicsBody(polygonFrom: clipPath2)
brick.physicsBody?.isDynamic = true
brick.physicsBody?.categoryBitMask = CollisionBitMask.Brick
brick.physicsBody?.affectedByGravity = true
hillIllustration.addChild(brick)
}
Transition to GameOver:
let transition = SKTransition.crossFade(withDuration: 0)
let gameScene = GameOver(size: self.size)
self.view?.presentScene(gameScene, transition: transition)
Transition back to GameScene:
let transition = SKTransition.crossFade(withDuration: 0)
let gameScene = GameScene(size: self.size)
self.view?.presentScene(gameScene, transition: transition)
Somehow when I transition scenes the information of how the bricks were aligned gets saved. How can I change that?
if you want that after you restart the game the brick node go back to start rotation than call:
brick.zRotation = 0
if you want that during the game the node do not rotate than you may put this code in the update func that already have in all spritekit files:
override func update(_ currentTime: TimeInterval) {
brick.zRotation = 0
}
Hope this helps
When the view is presented, I would like the image named "Player" (set) which is in my images.xcassets to show on the scene.
Currently the scene just loads up blue. Not sure why, as even when adding a color to change the image color did nothing.
import SpriteKit
class CharacterScene: SKScene {
var Circle = SKSpriteNode(imageNamed: "Player")
override func didMoveToView(view: SKView) {
backgroundColor = SKColor.blackColor()
Circle.size = CGSize(width: 40, height: 40)
Circle.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
Circle.color = SKColor.brownColor()
self.addChild(Circle)
}
}
You need to initialize the size of CharacterScene before presenting it on the screen. Here you go:
self.scene!.view!.presentScene(CharacterScene(size: self.scene!.size), transition: SKTransition.pushWithDirection(SKTransitionDirection.Up, duration: 0.3))