1

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Frakcool
  • 10,915
  • 9
  • 50
  • 89
  • I believe you are using an impulse for the jump, but left and right is done by adding or subtracting an x value to the position. When you release either left or right button, you are no longer moving (basically player.position.x + 0), there is no momentum as you are just using hard coded values and not physics. Whereas jump is using an impulse to move, then gravity is used to bring your character back down. If you used an impulse to move the character left and right, it would act more like mario. – JohnL Mar 17 '21 at 20:28
  • In this case should I use the impulse? Or velocity from the `physicsbody`? @JohnL – Frakcool Mar 17 '21 at 21:49
  • Does [this](https://stackoverflow.com/questions/49410187/add-velocity-to-skphysicsbody-while-moving-its-skspritenode) answer help? – JohnL Mar 17 '21 at 21:58
  • It somewhat helped @JohnL, I edited my question – Frakcool Mar 18 '21 at 03:51
  • Nevermind, I changed the floor to be a solid node rather than a collection / array of multiple nodes and it worked. It seems to be an issue where it got stuck between nodes. – Frakcool Mar 18 '21 at 04:09
  • 1
    Ok, glad you have it working. FYI, in your latest video your player bounces a little, use player.restitution = 0 to stop the bounciness. – JohnL Mar 18 '21 at 12:38
  • Awesome! Thanks that helps as well, I'll give it a try later on with the old floor with multiple blocks – Frakcool Mar 18 '21 at 13:17

1 Answers1

0

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.

Frakcool
  • 10,915
  • 9
  • 50
  • 89