1
\$\begingroup\$

I've been working on a Mario Bros clone for the first level, trying to learn how to make games with SpriteKit.

I'm trying to use an SKCameraNode so that I can move the player along a bigger world. However, when moving the player to the left or the right, the camera that follows the player moves out of the bounds of the scene:

enter image description here

I've found answers at Stack Overflow using SKConstraints. However, the code is using a WorldLayer that I don't have and don't know what nodes to add to it. Furthermore, that code is from over 5 years ago, and I've found that most of the answers from back then are now easier to implement, just like what this site (where I took the code to implement the camera) says:

In the good old days it used to take quite a bit of math to create the illusion that the player is moving around a bigger “world”. I’ve been playing around with SpriteKit recently and today I learned how easy it is to actually create that effect using the SKCameraNode.

import SpriteKit
import GameplayKit

class GameScene: SKScene {
    private var player = SKSpriteNode()
    private var bg = SKSpriteNode()
    private var leftArrow = SKSpriteNode()
    private var rightArrow = SKSpriteNode()
    private var floor = SKSpriteNode()
    private var isLeftTouched = false
    private var isRightTouched = false
    private var selectedNodes: [UITouch:SKSpriteNode] = [:]
    private var cam = SKCameraNode()
    
    override func didMove(to view: SKView) {
        self.camera = cam
        self.addChild(cam)
        self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
        
        addBackground()
        addFloor()
        addPlayer(xOffset: 0, yOffset: 0)
        addControls()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first! as UITouch
        let positionInScene = touch.location(in: self)
        let touchedNode = self.atPoint(positionInScene)
        
        for touch in touches {
            let location = touch.location(in:self)
            if let node = self.atPoint(location) as? SKSpriteNode {
                if let name = touchedNode.name {
                    selectedNodes[touch] = node
                    if name == "left" {
                        isLeftTouched = true
                    } else if name == "right" {
                        isRightTouched = true
                    }
                }
            }
        }
        
        if let name = touchedNode.name {
            if name == "left" {
                isLeftTouched = true
            } else if name == "right" {
                isRightTouched = true
            }
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            if selectedNodes[touch] != nil {
                if selectedNodes[touch]?.name == "left" {
                    isLeftTouched = false
                } else if selectedNodes[touch]?.name == "right" {
                    isRightTouched = false
                }
                selectedNodes[touch] = nil
            }
        }
    }
    
    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
        let x = player.position.x + (isRightTouched ? 5 : -5)
        let position = CGPoint(x: x, y: player.position.y)
        if position.x >= self.frame.maxX || position.x <= self.frame.minX {
            return
        }
        if isLeftTouched {
            runIn(direction: Direction.LEFT)
        }
        if isRightTouched {
            runIn(direction: Direction.RIGHT)
        }
        
        updateCameraPosition()
    }
    
    // MARK: INTERACTION METHODS
    
    func runIn(direction: Direction) {
        player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 ,
                                                  dy: 0))
    }
    
    func updateCameraPosition() {
        cam.position = CGPoint(x: player.position.x, y: self.frame.midY)
    }
    
    // MARK: UI METHODS
    
    func addBackground() {
        let bgTexture = SKTexture(imageNamed: "bg")
        //let floorTexture = SKTexture(imageNamed: "floor")
        
        bg = SKSpriteNode(texture: bgTexture)
        bg.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
        bg.size.height = self.frame.height
        bg.zPosition = -10
        
        self.addChild(bg)
    }
    
    func addFloor() {
        let floorTexture = SKTexture(imageNamed: "floor")
        floor = SKSpriteNode(texture: floorTexture)
        // Divide the height of the texture by 2 so it doesn't sink below the screen
        floor.position = CGPoint(x: self.frame.midX,
                                 y: self.frame.minY + floorTexture.size().height / 2)
        floor.zPosition = -9
        
        floor.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: floor.frame.width,
                                                              height: floor.frame.height))
        floor.physicsBody?.isDynamic = false
        
        self.addChild(floor)
    }
    
    func addPlayer(xOffset: CGFloat, yOffset: CGFloat) {
        let playerTexture = SKTexture(imageNamed: "player")
        
        player = SKSpriteNode(texture: playerTexture)
        player.position = CGPoint(x: self.frame.midX,
                                  y: self.frame.midY)
        
        player.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: player.frame.width, height: player.frame.height))
        player.physicsBody?.allowsRotation = false
        player.physicsBody?.isDynamic = true
        
        self.addChild(player)
    }
    
    func addControls() {
        addLeftArrow()
        addRightArrow()
    }
    
    func addLeftArrow() {
        let leftTexture = SKTexture(imageNamed: "left")
        leftArrow = SKSpriteNode(texture: leftTexture)
        
        leftArrow.name = "left"
        leftArrow.position = CGPoint(x: calculateXOffset(for: leftArrow, from: self.frame.minX, offset: 100),
                                     y: calculateXOffset(for: leftArrow, from: self.frame.minY, offset: 50))
        
        cam.addChild(leftArrow)
    }
    
    func addRightArrow() {
        let rightTexture = SKTexture(imageNamed: "right")
        rightArrow = SKSpriteNode(texture: rightTexture)
        
        rightArrow.name = "right"
        rightArrow.position = CGPoint(x: calculateXOffset(for: rightArrow, from: self.frame.minX, offset: 200),
                                      y: calculateXOffset(for: rightArrow, from: self.frame.minY, offset: 50))
        
        cam.addChild(rightArrow)
    }
    
    // MARK: UTILITY FUNCTIONS
    
    func calculateXOffset(for asset: SKSpriteNode, from coord: CGFloat, offset: CGFloat) -> CGFloat {
        let width = asset.frame.width
        
        return coord + offset + width;
    }
    
    func calculateYOffset(for asset: SKSpriteNode, from coord: CGFloat, offset: CGFloat) -> CGFloat {
        let height = asset.frame.height
        
        return coord + offset + height;
    }
}

Is there an easier way nowadays to deal with this problem? If so, what is it? If not, how could I implement this using constraints to my game?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

How about simply adding a condition in updateCameraPosition? You'll obviously need to define leftBoundary and rightBoundary, but you get the idea.

func updateCameraPosition() {
  if player.position.x > leftBoundary && player.position.x < rightBoundary {
      cam.position = CGPoint(x: player.position.x, y: self.frame.midY)
  }
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .