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