SpriteKit keep moving player in current direction while falling after touchesEnded

241 Views Asked by At

I'm making my own Mario Bros. replica for the first level to learn how to make games with iOS, with my own assets. So far I've managed to place three SKSpriteNodes for the controls (left, right, up), and my player node can move in those three directions, but if I make my player jump while running in either direction, as soon as I remove my finger from the "left control", the player loses all its momentum and falls right there (as if it hit a wall) instead of following the parabola.

I don't know what might be needed in this case to be an MRE, so this is basically the whole thing that can reproduce the issue, along with some attempts I've made to make it work.

Basically I tried to apply an impulse / set the velocity / change the position directly and this last one was the one with better results (yet it still makes the player node to fall as soon as I remove the finger from the direction controls).

Here's a video demonstrating the issue.

This is the GameScene

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 upArrow = SKSpriteNode()
    private var floor = [SKSpriteNode]()
    private var isLeftTouched = false
    private var isRightTouched = false
    private var selectedNodes: [UITouch:SKSpriteNode] = [:]

    override func didMove(to view: SKView) {
        addBackground()
        addFloor()
        addPlayer(xOffset: 0, yOffset: 0)
        addControls()
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 50))
        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 == "up" {
                        player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
                    } else if name == "left" {
                        isLeftTouched = true
                    } else if name == "right" {
                        isRightTouched = true
                    }
                }
            }
        }

        if let name = touchedNode.name {
            if name == "up" {
                player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
            } else if name == "left" {
                isLeftTouched = true
            } else if name == "right" {
                isRightTouched = true
            }
        }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        //let direction = ((touches.first?.location(in: self).x)! < (touches.first?.previousLocation(in: self).x)!) ? Direction.LEFT : Direction.RIGHT
        //runIn(direction: direction)
    }

    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
        if isLeftTouched {
            runIn(direction: Direction.LEFT)
        }
        if isRightTouched {
            runIn(direction: Direction.RIGHT)
        }
    }

    // MARK: INTERACTION METHODS

    func runIn(direction: Direction) {
        let x = player.position.x + (direction == Direction.RIGHT ? 5 : -5)
        let position = CGPoint(x: x, y: player.position.y)
        if position.x >= self.frame.maxX || position.x <= self.frame.minX {
            return
        }
        player.position = position
        //player.physicsBody?.velocity = CGVector(dx: direction == Direction.RIGHT ? 50 : -50, dy: 0)
        //player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 5 : -5 , dy: 0))
    }

    // MARK: UI METHODS

    func addBackground() {
        let bgTexture = SKTexture(imageNamed: "bg")

        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 addPlayer(xOffset: CGFloat, yOffset: CGFloat) {
        let playerTexture = SKTexture(imageNamed: "player")

        player = SKSpriteNode(texture: playerTexture)

        //let xPos = calculateXOffset(for: player, from: self.frame.midX, offset: xOffset)
        //let yPos = calculateXOffset(for: player, from: self.frame.midY, offset: yOffset)
        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?.isDynamic = true

        self.addChild(player)
    }

    func addFloor() {
        let blockTexture = SKTexture(imageNamed: "block")

        for i in 0 ... (Int) (self.frame.width / blockTexture.size().width) {
            let blockNode = SKSpriteNode(texture: blockTexture)

            blockNode.position = CGPoint(x: self.frame.minX + (blockNode.frame.width * CGFloat(i)),
                                         y: self.frame.minY + blockNode.frame.height / 2)

            blockNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: blockNode.frame.width, height: blockNode.frame.height))
            blockNode.physicsBody?.isDynamic = false

            floor.append(blockNode)
            self.addChild(blockNode)
        }
    }

    func addControls() {
        addLeftArrow()
        addRightArrow()
        addUpArrow()
    }

    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: 50),
                                     y: calculateXOffset(for: leftArrow, from: self.frame.minY, offset: 50))

        self.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: 150),
                                      y: calculateXOffset(for: rightArrow, from: self.frame.minY, offset: 50))

        self.addChild(rightArrow)
    }

    func addUpArrow() {
        let upTexture = SKTexture(imageNamed: "up")
        upArrow = SKSpriteNode(texture: upTexture)

        upArrow.name = "up"
        upArrow.position = CGPoint(x: calculateXOffset(for: upArrow, from: self.frame.maxX, offset: -(125 + upTexture.size().width)),
                                   y: calculateXOffset(for: upArrow, from: self.frame.minY, offset: 50))

        self.addChild(upArrow)
    }

    // 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;
    }
}

My Direction enum:

enum Direction {
    case LEFT
    case RIGHT
    case UP
    case DOWN
}

And the only change I made in GameViewController was this:

scene.scaleMode = .resizeFill

My GameScene.sks is 926 x 428, only supporting landscape. I also set the LaunchScreen to Main due to a bug in Xcode 12: Background is not filling the whole view SpriteKit

And these are all my assets:

Enter image description here Enter image description here Enter image description here Enter image description here enter image description here Enter image description here


Edit

I tried applying an impulse in my runIn method like this:

player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 , dy: 0))

This makes the player node move in the parabola but now from time to time it gets stuck and the only way to make it move is to make it jump until it happens again.

Here's a video demonstrating the issue again.

If I try to set the velocity instead, then I'm not able to jump while moving and it seems to glide when jumping and moving after.

1

There are 1 best solutions below

0
Frakcool On

I ended up following @JohnL suggestion in the comments above, to use an impulse as well to move my player node:

player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 , dy: 0))

The issue where the player node was stuck while moving was removed when changing the floor for a single asset rather than multiple blocks one next to each other.