0
\$\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's following the player, moves out of the bounds of the scene:

enter image description here

I've found answers at Stack Overflow using SKConstraints yet the code is using a WorldLayer that I don't have and don't know what nodes to add to it, and 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 now? If so, what is it? Otherwise how could I implement this using constraints to my game?

\$\endgroup\$
0
\$\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\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.