Swift - Jump Only When Landed

swift - Jump only when landed

I recently had this problem. This is how I fixed it. In the game scene create this variable:

    var ableToJump = true

Then in the update method put this code:

    if cat.physicsBody?.velocity.dy == 0 {
ableToJump = true
}
else {
ableToJump = false
}

The above code will work because the update method is ran every frame of the game. Every frame it checks if your cat is moving on the y axis. If you do not have an update method, just type override func update and it should autocomplete.

Now the last step is put this code in the touchesBegan:

    if ableToJump == true {
cat.physicsBody!.applyImpulse(CGVectorMake(0,40))
}

Note: You may have to tinker with the impulse to get desired jump height

How to make a player jump only once in Swift?

Here's how I did it:

EDIT:

    class MainScene: SKScene, SKPhysicsContactDelegate {

let colorChanger = ColorChanger()
let ground = SKSpriteNode(imageNamed: "Ground")
let player = SKSpriteNode(imageNamed: "Player")
let block1 = SKSpriteNode(imageNamed: "Block2")
let block2 = SKSpriteNode(imageNamed: "Block2")
let scoreLabel = SKLabelNode(fontNamed: "Chalkboard SE")
let highScoreLabel = SKLabelNode(fontNamed: "Chalkboard SE")
let life = SKSpriteNode(imageNamed: "Player")
let life2 = SKSpriteNode(imageNamed: "Player")
let life3 = SKSpriteNode(imageNamed: "Player")
var firstTime = Int()

var blockMaxX = CGFloat(0)
var origBlockPositionX = CGFloat(0)
var score = Int()
var highScore = Int()
var lives = Int()

var groundPositionX = CGFloat(0)
var maxBarX = CGFloat(0)
var groundSpeed = 5
var playerBaseLine = CGFloat(0)
var more = CGFloat(0)

var onGround = true
var velocityY = CGFloat(0)
let gravity = CGFloat(1.3)

enum Collision:UInt32{
case Player = 1
case Block = 2
}

override func didMoveToView(view: SKView) {
self.physicsWorld.contactDelegate = self
let color1 = colorChanger.randomColor()
runAction(SKAction.colorizeWithColor(color1, colorBlendFactor: 1, duration: 0.2))
self.ground.anchorPoint = CGPointMake(0, 1)
self.ground.position = CGPointMake(CGRectGetMinX(self.frame),CGRectGetMidY(self.frame)-23)
self.ground.size = CGSize(width: 1100, height: 4000)

self.groundPositionX = self.ground.position.x
self.maxBarX = self.ground.size.width - self.frame.size.width - 100
self.maxBarX *= -1

self.player.size = CGSize(width: 50, height: 50)
self.playerBaseLine = self.frame.height / 2
self.player.position = CGPointMake(CGRectGetMinX(self.frame) + self.player.size.width / 1.5 , self.playerBaseLine)
self.player.physicsBody = SKPhysicsBody(circleOfRadius: CGFloat(self.player.size.width / 2))
self.player.physicsBody!.affectedByGravity = false
self.player.physicsBody!.categoryBitMask = Collision.Player.rawValue
self.player.physicsBody!.contactTestBitMask = Collision.Block.rawValue
self.player.physicsBody!.collisionBitMask = Collision.Block.rawValue

override func update(currentTime: NSTimeInterval) {

self.velocityY += self.gravity
self.player.position.y -= velocityY

if self.player.position.y < self.playerBaseLine {
self.player.position.y = self.playerBaseLine
velocityY = 0.0
onGround = true
}
}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if self.onGround{
self.velocityY = -18.0
self.onGround = false
}
}

You need to play with gravity ;)

Trying to make platforms that I can jump through from underneath but land on top of. having trouble fine tuning the logic

Here is a link to the project that I made for macOS and iOS targets:

https://github.com/fluidityt/JumpUnderPlatform

Basically, this all has to do with

  1. Detecting collision of a platform
  2. Then determining if your player is under the platform
  3. Allow your player to go through the platform (and subsequently land on it)

--

SK Physics makes this a little complicated:


  1. On collision detection, your player's .position.y or .velocity.dy
    may already have changed to a "false" state in reference to satisfying the #2 check from above (meaning #3 will never happen). Also, your player will bounce off the platform on first contact.

  2. There is no "automatic" way to determine when your player has finished passing through the object (thus to allow player to land on the platform)

--

So to get everything working, a bit of creativity and ingenuity must be used!


1: Detecting collision of a platform:

So, to tackle 1 is the simplest: we just need to use the built in didBegin(contact:)

We are going to be relying heavily on the 3 big bitMasks, contact, category, and collision:

(fyi, I don't like using enums and bitmath for physics because I'm a rebel idiot):

struct BitMasks {
static let playerCategory = UInt32(2)
static let jupCategory = UInt32(4) // JUP = JumpUnderPlatform
}

override func didBegin(_ contact: SKPhysicsContact) {

// Crappy way to do "bit-math":
let contactedSum = contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask

switch contactedSum {

case BitMasks.jupCategory + BitMasks.playerCategory:
// ...
}

--

Now, you said that you wanted to use the SKSEditor, so I have accommodated you:

Sample Image
Sample Image
Sample Image
Sample Image

// Do all the fancy stuff you want here...
class JumpUnderPlatform: SKSpriteNode {

var pb: SKPhysicsBody { return self.physicsBody! } // If you see this on a crash, then WHY DOES JUP NOT HAVE A PB??

// NOTE: I could not properly configure any SKNode properties here..
// it's like they all get RESET if you put them in here...
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
}

--

Now for the player:

class Player: SKSpriteNode {

// If you see this on a crash, then WHY DOES PLAYER NOT HAVE A PB??
var pb: SKPhysicsBody { return self.physicsBody! }

static func makePlayer() -> Player {

let newPlayer = Player(color: .blue, size: CGSize(width: 50, height: 50))
let newPB = SKPhysicsBody(rectangleOf: newPlayer.size)

newPB.categoryBitMask = BitMasks.playerCategory
newPB.usesPreciseCollisionDetection = true

newPlayer.physicsBody = newPB
newPlayer.position.y -= 200 // For demo purposes.

return newPlayer
}
}


2. (and dealing with #4): Determining if under platform on contact:

There are many ways to do this, but I chose to use the player.pb.velocity.dy approach as mentioned by KOD to keep track of the player's position... if your dy is over 0, then you are jumping (under a platform) if not, then you are either standing still or falling (need to make contact with the platform and stick to it).

To accomplish this we have to get a bit more technical, because again, the physics system and the way SK works in its loop doesn't always mesh 100% with how we think it should work.

Basically, I had to make an initialDY property for Player that is constantly updated each frame in update

This initialDY will give us the correct data that we need for the first contact with the platform, allowing us to tell us to change the collision mask, and also to reset our player's CURRENT dy to the initial dy (so the player doesn't bounce off).


3. (and dealing with #5): Allow player to go through platform

To go through the platform, we need to play around with the collisionBitMasks. I chose to make the player's collision mask = the player's categoryMask, which is probably not the right way to do it, but it works for this demo.

You end up with magic like this in didBegin:

  // Check if jumping; if not, then just land on platform normally.
guard player.initialDY > 0 else { return }

// Gives us the ability to pass through the platform!
player.pb.collisionBitMask = BitMasks.playerCategory

Now, dealing with #5 is going to require us to add another piece of state to our player class.. we need to temporarily store the contacted platform so we can check if the player has successfully finished passing through the platform (so we can reset the collision mask)

Then we just check in didFinishUpdate if the player's frame is above that platform, and if so, we reset the masks.

Here are all of the files , and again a link to the github:

https://github.com/fluidityt/JumpUnderPlatform



Player.swift:

class Player: SKSpriteNode {

// If you see this on a crash, then WHY DOES PLAYER NOT HAVE A PB??
var pb: SKPhysicsBody { return self.physicsBody! }

// This is set when we detect contact with a platform, but are underneath it (jumping up)
weak var platformToPassThrough: JumpUnderPlatform?

// For use inside of gamescene's didBeginContact (because current DY is altered by the time we need it)
var initialDY = CGFloat(0)
}

// MARK: - Funkys:
extension Player {
static func makePlayer() -> Player {

let newPlayer = Player(color: .blue, size: CGSize(width: 50, height: 50))
let newPB = SKPhysicsBody(rectangleOf: newPlayer.size)

newPB.categoryBitMask = BitMasks.playerCategory
newPB.usesPreciseCollisionDetection = true

newPlayer.physicsBody = newPB
newPlayer.position.y -= 200 // For demo purposes.

return newPlayer
}

func isAbovePlatform() -> Bool {
guard let platform = platformToPassThrough else { fatalError("wtf is the platform!") }

if frame.minY > platform.frame.maxY { return true }
else { return false }
}

func landOnPlatform() {
print("resetting stuff!")
platformToPassThrough = nil
pb.collisionBitMask = BitMasks.jupCategory
}
}

// MARK: - Player GameLoop:
extension Player {

func _update() {
// We have to keep track of this for proper detection of when to pass-through platform
initialDY = pb.velocity.dy
}

func _didFinishUpdate() {

// Check if we need to reset our collision mask (allow us to land on platform again)
if platformToPassThrough != nil {
if isAbovePlatform() { landOnPlatform() }
}
}
}


JumpUnderPlatform & BitMasks.swift (respectively:)

// Do all the fancy stuff you want here...
class JumpUnderPlatform: SKSpriteNode {

var pb: SKPhysicsBody { return self.physicsBody! } // If you see this on a crash, then WHY DOES JUP NOT HAVE A PB??

required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }

}

struct BitMasks {
static let playerCategory = UInt32(2)
static let jupCategory = UInt32(4)
}


GameScene.swift:

-

MAKE SURE YOU HAVE THE TWO NODES IN YOUR SKS EDITOR:
Sample Image
Sample Image

-

// MARK: - Props:
class GameScene: SKScene, SKPhysicsContactDelegate {

// Because I hate crashes related to spelling errors.
let names = (jup: "jup", resetLabel: "resetLabel")

let player = Player.makePlayer()
}

// MARK: - Physics handling:
extension GameScene {

private func findJup(contact: SKPhysicsContact) -> JumpUnderPlatform? {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else { fatalError("how did this happne!!??") }

if nodeA.name == names.jup { return (nodeA as! JumpUnderPlatform) }
else if nodeB.name == names.jup { return (nodeB as! JumpUnderPlatform) }
else { return nil }
}

// Player is 2, platform is 4:
private func doContactPlayer_X_Jup(platform: JumpUnderPlatform) {

// Check if jumping; if not, then just land on platform normally.
guard player.initialDY > 0 else { return }

// Gives us the ability to pass through the platform!
player.physicsBody!.collisionBitMask = BitMasks.playerCategory

// Will push the player through the platform (instead of bouncing off) on first hit
if player.platformToPassThrough == nil { player.pb.velocity.dy = player.initialDY }
player.platformToPassThrough = platform
}

func _didBegin(_ contact: SKPhysicsContact) {

// Crappy way to do bit-math:
let contactedSum = contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask

switch contactedSum {

case BitMasks.jupCategory + BitMasks.playerCategory:
guard let platform = findJup(contact: contact) else { fatalError("must be platform!") }
doContactPlayer_X_Jup(platform: platform)

// Put your other contact cases here...
// case BitMasks.xx + BitMasks.yy:

default: ()
}
}
}

// MARK: - Game loop:
extension GameScene {

// Scene setup:
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
addChild(player)
}

// Touch handling: (convert to touchesBegan for iOS):
override func mouseDown(with event: NSEvent) {
// Make player jump:
player.pb.applyImpulse(CGVector(dx: 0, dy: 50))

// Reset player on label click (from sks file):
if nodes(at: event.location(in: self)).first?.name == names.resetLabel {
player.position.y = frame.minY + player.size.width/2 + CGFloat(1)
}
}

override func update(_ currentTime: TimeInterval) {
player._update()
}

func didBegin(_ contact: SKPhysicsContact) {
self._didBegin(contact)
}

override func didFinishUpdate() {
player._didFinishUpdate()
}
}

I HOPE THIS HELPS SOME!

Sprite Kit set Min. and Max. for Jump

Here is an working example on how to make something like:

  • long pressed jump based on duration of press
  • short (one tap jump)
  • restrict character to jump while in the air
  • keep character jumping while finger is on screen

Code (Swift 4.x)

import SpriteKit

struct Constants {
static let minimumJumpForce:CGFloat = 15.0
static let maximumJumpForce:CGFloat = 30.0
static let characterSideSpeed:CGFloat = 18.0
}

class GameScene: SKScene,SKPhysicsContactDelegate
{
let CharacterCategory : UInt32 = 0x1 << 1
let PlatformCategory : UInt32 = 0x1 << 2
let WallCategory : UInt32 = 0x1 << 3

var force: CGFloat = 16.0 //Initial force
var pressed = false
var isCharacterOnGround = false // Use this to prevent jumping while in the air
let character = SKSpriteNode(color: .green, size: CGSize(width: 30, height:30))
let debugLabel = SKLabelNode(fontNamed: "Geneva")

override func didMove(to view: SKView)
{
//Setup contact delegate so we can use didBeginContact and didEndContact methods
physicsWorld.contactDelegate = self
physicsWorld.speed = 0.5
//Setup borders so character can't escape from us :-)
self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
self.physicsBody?.categoryBitMask = WallCategory
self.physicsBody?.collisionBitMask = CharacterCategory

//Setup character
character.position = CGPoint(x: 150, y: 150)
character.physicsBody = SKPhysicsBody(rectangleOf: character.size)
character.physicsBody?.categoryBitMask = CharacterCategory
character.physicsBody?.contactTestBitMask = PlatformCategory
character.physicsBody?.collisionBitMask = PlatformCategory | WallCategory
character.physicsBody?.allowsRotation = false
character.physicsBody?.isDynamic = true
character.physicsBody?.restitution = 0.1

self.addChild(character)

generatePlatforms()

debugLabel.text = " DEBUG: "
debugLabel.fontColor = .white
debugLabel.fontSize = 12.0
debugLabel.position = CGPoint(x: frame.midX, y: frame.midY+100)
self.addChild(debugLabel)
}

func generatePlatforms(){
for i in 1...4
{
let position = CGPoint(x: frame.midX, y: CGFloat(i)*140.0 - 100)
let platform = createPlatformAtPosition(position: position)
self.addChild(platform)
}
}

func createPlatformAtPosition(position : CGPoint)->SKSpriteNode{

let platform = SKSpriteNode(color: .green, size: CGSize(width: frame.size.width, height:20))

platform.position = position

platform.physicsBody = SKPhysicsBody(
edgeFrom: CGPoint(x: -platform.size.width/2.0, y:platform.size.height/2.0),
to:CGPoint(x: platform.size.width/2.0, y: platform.size.height/2.0))

platform.physicsBody?.categoryBitMask = PlatformCategory
platform.physicsBody?.contactTestBitMask = CharacterCategory
platform.physicsBody?.collisionBitMask = CharacterCategory
platform.physicsBody?.allowsRotation = false
platform.name = "platform"
platform.physicsBody?.isDynamic = false
platform.physicsBody?.restitution = 0.0

return platform
}

func jump(force : CGFloat){
if(self.isCharacterOnGround){
self.character.physicsBody?.applyImpulse(CGVector(dx: 0, dy: force))
self.character.physicsBody?.collisionBitMask = WallCategory
self.isCharacterOnGround = false
}
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.pressed = true

let timerAction = SKAction.wait(forDuration: 0.05)

let update = SKAction.run({
if(self.force < Constants.maximumJumpForce){
self.force += 2.0
}else{
self.jump(force: Constants.maximumJumpForce)
self.force = Constants.maximumJumpForce
}
})

let sequence = SKAction.sequence([timerAction, update])
let repeat_seq = SKAction.repeatForever(sequence)
self.run(repeat_seq, withKey:"repeatAction")
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

self.removeAction(forKey: "repeatAction")
self.jump(force: self.force)
self.force = Constants.minimumJumpForce
self.pressed = false

}

override func update(_ currentTime: TimeInterval) {

debugLabel.text = "DEBUG: onTheGround : \(isCharacterOnGround), force \(force)"

if(character.position.x <= character.size.width/2.0 + 5.0 && character.physicsBody!.velocity.dx < 0.0 ){
character.physicsBody?.applyForce(CGVector(dx: Constants.characterSideSpeed, dy: 0.0))
}else if((character.position.x >= self.frame.size.width - character.size.width/2.0 - 5.0) && character.physicsBody!.velocity.dx >= 0.0){
character.physicsBody?.applyForce(CGVector(dx: -Constants.characterSideSpeed, dy: 0.0))
}else if(character.physicsBody!.velocity.dx > 0.0){
character.physicsBody!.applyForce(CGVector(dx: Constants.characterSideSpeed, dy: 0.0))
}else{
character.physicsBody!.applyForce(CGVector(dx: -Constants.characterSideSpeed, dy: 0.0))
}
}

func didBegin(_ contact: SKPhysicsContact) {

var firstBody, secondBody: SKPhysicsBody

if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}

if ((firstBody.categoryBitMask & CharacterCategory) != 0 &&
(secondBody.categoryBitMask & PlatformCategory != 0)) {

let platform = secondBody.node! as! SKSpriteNode
// platform.color = UIColor.redColor()
let platformSurfaceYPos = platform.position.y + platform.size.height/2.0

let player = contact.bodyB.node! as! SKSpriteNode
let playerLegsYPos = player.position.y - player.size.height/2.0

if((platformSurfaceYPos <= playerLegsYPos)){
character.physicsBody?.collisionBitMask = PlatformCategory | WallCategory
self.isCharacterOnGround = true

if(self.pressed){
let characterDx = character.physicsBody?.velocity.dx
character.physicsBody?.velocity = CGVector(dx: characterDx!, dy: 0.0)
self.jump(force: Constants.maximumJumpForce)
}
}
}
}

func didEnd(_ contact: SKPhysicsContact) {

var firstBody, secondBody: SKPhysicsBody

if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}

if ((firstBody.categoryBitMask & CharacterCategory) != 0 &&
(secondBody.categoryBitMask & PlatformCategory != 0)) {

let platform = secondBody.node as! SKSpriteNode
let platformSurfaceYPos = platform.position.y + platform.size.height/2.0

let player = contact.bodyB.node as! SKSpriteNode
let playerLegsYPos = player.position.y - player.size.height/2.0

if((platformSurfaceYPos <= playerLegsYPos) && ((character.physicsBody?.velocity.dy)! > CGFloat(0.0))){
character.physicsBody?.collisionBitMask = WallCategory
self.isCharacterOnGround = false
}
}
}
}

Note that this is simple example, and in real application you will probably have to handle states like isOnTheGround in a different way. Right now, to determine if character is on the ground you just set isOnTheGround = true when character make a contact with platform, and set it to false in didEndContact...But there are situations when character can be in contact with platform while in the air (eg. side contact)...

EDIT:

I changed the code to let the player jump while pressed. Here is the result:

Sample Image

Important:

Actual platform implementation and contact handling is up to you and this is not tested. The only purpose of this example is to show you how to jump while pressed. Currently, physicsWorld.speed is set to 0.5 to make animation slower because its easier to debug like that, but you can change this to default value (1.0).

So, as you can see from the image, while player is on the first platform some small jumps are presented (by simple tapping, or short pressing). Then (player is still on first platform) long press has been made, and player has jumped on second platform. After that, another long press is done, but this time without releasing, and player starts jumping from one platform to another using maximum force.

This needs a lot of tweaking and proper platform & contact detection, but it can give you an idea about how to implement jumping you asked about.

swift - pause animation whilst jumping

Thank you for all your answers, I solved this by simply changing the speed of the cat in my update func.

If the cat isn't traveling on the y axis i set cat.speed = 1 and if it is traveling (or jumping), I set cat.speed = 0.

Here is how the update func looks now...

    if cat.physicsBody?.velocity.dy == 0 {
ableToJump = true
cat.speed = 1

}
else {
ableToJump = false
cat.speed = 0
}


Related Topics



Leave a reply



Submit