Sprite Kit Set Min. and Max. for Jump

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 - 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

Sprite Kit stop Impulse

1.Create ‘Game’ from Xcode template based on SpriteKit

2.Copy paste listed code to GameScene class

import SpriteKit

class GameScene: SKScene, SKPhysicsContactDelegate {

var location = CGPoint()
var floorSize = CGSize()
var floorColor = UIColor()
var player = SKSpriteNode()

override func didMoveToView(view: SKView) {

view.showsFPS = true;
view.showsNodeCount = true;
view.showsDrawCount = true;

self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsBody?.categoryBitMask = 1
self.physicsBody?.contactTestBitMask = 1
self.physicsWorld.gravity = CGVectorMake(0, 0)
self.physicsWorld.contactDelegate = self;

location = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))

player = SKSpriteNode(imageNamed:"Spaceship")
player.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 320, height: 320))
player.physicsBody?.categoryBitMask = 1
player.physicsBody?.collisionBitMask = 1
player.physicsBody?.contactTestBitMask = 1
player.physicsBody?.linearDamping = 0;
player.xScale = 1
player.yScale = 1
player.position = location
self.addChild(player)

}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.physicsWorld.gravity = CGVectorMake(0, 0)
let direction = Float(1.5708)//Float(player.zRotation) + Float(M_PI_2)
player.physicsBody?.applyForce(CGVector(dx: 150000*CGFloat(cosf(direction)), dy: 150000*CGFloat(sinf(direction))))
}

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.physicsWorld.gravity = CGVectorMake(0, -7.9)
}

}

3.Run the app

This should give you start point for you 'Jump' game :)

SpriteKit Issue with Double Jump only working some of the time

Have you tried simply delaying setting the "on ground" flag by a fraction of a second?

let waitAction = SKAction.wait(forDuration: 0.5)
scene.runAction(waitAction, completion: {
self.onGround = false

})

Obviously you could experiment with the exact duration to make sure it gives the "bounce" just enough time to end.

Increase jump height on longer touch until maximum height

You can disable the body's velocity on the y axis as you lift your finger and if you want to cap the maximum jump height use an optional variable to store the initial y position just before the jump and check the delta between your current y poistion to the initial y position:

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
if onGround && !gameOver {
if self.initialJumpY == nil {
self.initialJumpY = self.character.position.y
}

if self.character.position.y - self.initialJumpY < <Your cap value> {
self.character.physicsBody?.applyImpulse(CGVectorMake(0, 75))
} else {
self.character.physicsBody?.velocity.dy = 0.0
}

self.onGround = false
}

and reset it when done:

    override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
self.character.physicsBody?.velocity.dy = 0.0
self.initialJumpY = nil
}

declare the initialJumpY in your node class where the touch methods are defined:

class MyNode: SKNode {

var initialJumpY: Float?

}

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!

Setting limits to sprite vertical location with arc4Random

You could set a minimum and maximum height:

var height_max = UInt32( self.frame.size.height )
var height_min = UInt32( 20 )
var bird_range = arc4random_uniform(height_max - height_min + 1) + height_min;

Alternate method:

var bird_range = (arc4random() % (height_max - height_min) + 1) + height_min;

Methods Graphed:

Sample Image

The two using max/min height never got below 20, the original method you're using often hit 0.



Related Topics



Leave a reply



Submit