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:
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
- Detecting collision of a platform
- Then determining if your player is under the platform
- Allow your player to go through the platform (and subsequently land on it)
--
SK Physics makes this a little complicated:
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.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:
// 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:
-
// 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:
The two using max/min height never got below 20
, the original method you're using often hit 0
.
Related Topics
How to Print Out a Property's Contents Using Xcode Debugger
iOS Web App: Showing Content Only If the Application Is Standalone
App Crashes in Background While Changing Permission - Swift
How to Register Undomanager in Swift
Uipageviewcontroller Gesture Is Calling Viewcontrollerafter: But Doesn't Animate
How to Ignore Certain Uitouch Points in Multitouch Sequence
Removing iPad Support from App
Multiple Uialertcontrollers in iOS
iOS Fix Search Bar on Top of the Uitableviewcontroller
Storing [Nsnull Null] Values in Nsuserdefaults, from JSON Serialization, Causes Unwanted Exceptions
iOS Library to Convert Powerpoint Presentations into Images
Crashlytics Does Not Show Crashes
Xcode 9 Simulator Error Unable to Boot the Simulator Launchd Failed to Respond MAC Os Sierra
Uilabel and Uitextview Line Breaks Don't Match
Why Don't My Views Show Up in Storyboards and Are Greyed Out in the Left Pane