2

My goal is to set up all my platforms in the .sks file for easier design of my levels.


this is declared at the top of game scene.swift before didMove:

private var JumpThroughPlatformObject = SKSpriteNode()

and this is in DidMove:

if let JumpThroughPlatformObjectNode = self.childNode(withName: "//jumpThroughPlatform1") as? SKSpriteNode {
        JumpThroughPlatformObject = JumpThroughPlatformObjectNode} 

I reference the platform to get it's height from the .sks, since all my platforms are going to be the same height I only need to get it from one.

Below is what Im trying to use in my update method to turn off collisions until my player is totally above the platform. The main issue with only checking if my players velocity is greater than zero is: if the player is at the peak of a jump (his velocity slows to zero). if this happens and the player is inside a platform, he either instantly springs up to the top of the platform or gets launched downward.

I don't want my platforms to have to be 1 pixel high lines. I also need to have the player have a full collision box since he will be interacting with other types of environments. This leads me to believe that I somehow need to only register the top of the platform as a collision box and not the entire platform.

This if statement I wrote is supposed to take the y position of a platform and add half of its height to it, since the y position is based on the center of the sprite I figured this would put the collision for the platform on its top boundary.

I did the same for the player but in reverse. Putting the players collisions on only the bottom of his border. But its not working perfectly and I'm not sure why at this point.

if (JumpThroughPlatformObject.position.y + (JumpThroughPlatformObject.size.height / 2)) > (player.position.y - (player.size.height / 2))

The function below is giving me 3 main issues:

  1. My players jump is always dy = 80. If I'm jumping up to a platform that position.y = 90, the players peak of the jump stops in the middle of the platform, but he teleports to the top of it instead of continuing to fall to the ground.

  2. the left and right edges of the platforms still have full collision with the player if I'm falling

  3. if my player is on a platform and there is another one directly above me, the player can't jump through it.

    let zero:CGFloat = 0

        if let body = player.physicsBody {
            let dy = player.physicsBody?.velocity.dy
    
            // when I jump dy is greater than zero else I'm falling
    
           if (dy! >= zero) {
    
               if (JumpThroughPlatformObject.position.y + (JumpThroughPlatformObject.size.height / 2)) > (player.position.y - (player.size.height / 2)) {
    
                    print(" platform y: \(JumpThroughPlatformObject.position.y)")
                    print ("player position: \(player.position.y)")
    
                    // Prevent collisions if the hero is jumping
                    body.collisionBitMask = CollisionTypes.saw.rawValue | CollisionTypes.ground.rawValue
    
                }
    
           }
            else {
                // Allow collisions if the hero is falling
                body.collisionBitMask = CollisionTypes.platform.rawValue | CollisionTypes.ground.rawValue | CollisionTypes.saw.rawValue
            }
        } 
    

Any advice would be greatly appreciated. I've been tearing my hair out for a couple days now.

EDIT in didBegin and didEnd:

   func didBegin(_ contact: SKPhysicsContact) {

    if let body = player.physicsBody {
        let dy = player.physicsBody?.velocity.dy
        let platform = JumpThroughPlatformObject
        let zero:CGFloat = 0


    if contact.bodyA.node == player {
      // playerCollided(with: contact.bodyB.node!)

            if (dy! > zero || body.node!.intersects(platform)) && ((body.node?.position.y)! - player.size.height / 2 < platform.position.y + platform.size.height / 2) {

                body.collisionBitMask &= ~CollisionTypes.platform.rawValue

            }



    } else if contact.bodyB.node == player {
     //  playerCollided(with: contact.bodyA.node!)
        isPlayerOnGround = true

        if (dy! > zero || body.node!.intersects(platform)) && ((body.node?.position.y)! - player.size.height / 2 < platform.position.y + platform.size.height / 2) {

            body.collisionBitMask &= ~CollisionTypes.platform.rawValue}
        }
    }
}

func didEnd(_ contact: SKPhysicsContact) {

    if let body = player.physicsBody {
//            let dy = player.physicsBody?.velocity.dy
//            let platform = JumpThroughPlatformObject

    if contact.bodyA.node == player {


        body.collisionBitMask |= CollisionTypes.platform.rawValue

    }else if contact.bodyB.node == player {

        body.collisionBitMask |= CollisionTypes.platform.rawValue

    }
    }
}

Adding what I did, the player can no longer jump through the platform.

genericguy25
  • 602
  • 3
  • 16
  • I think you just want to remove the dy! >= zero test. That's not the right test, because you still need to disable collision when the player is falling through a platform they are partially inside. Your test for bottom of player vs top of platform should be sufficient to make sure the player doesn't fall through anything they were on top of. – Richard Byron Jul 04 '17 at 21:00
  • thanks for the response! Unfortunately after removing the initial dy > 0 check, I still have problems with 1 and 3. – genericguy25 Jul 04 '17 at 21:10
  • 1
    this is going to be a nightmare, just fyi :) – Fluidity Jul 04 '17 at 23:19
  • hah it already seems that way. Is there a way to grab the y position of the node thats currently contacting the player? – genericguy25 Jul 04 '17 at 23:20
  • i have a sample project for you but i need to clean it up. – Fluidity Jul 05 '17 at 03:04
  • Checkout this : https://stackoverflow.com/a/31454044 One way platforms wasn't a goal there, so they are pretty much untested, but maybe it could be worth of a look. – Whirlwind Jul 05 '17 at 07:50
  • @Fluidity, I would really appreciate that! – genericguy25 Jul 05 '17 at 14:41
  • sorry I took so long. I am making an answer now. Here is the project files: https://github.com/fluidityt/JumpUnderPlatform – Fluidity Jul 07 '17 at 06:01

3 Answers3

3

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:

enter image description here enter image description here enter image description here enter image description here

// 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: enter image description here enter image description here

-

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

Fluidity
  • 3,985
  • 1
  • 13
  • 34
  • I really appreciate everything you've done! I went through and attempted to implement your work into my own code. unfortunately it compiles, but i simply can't jump through the platform. I'm missing something some and I'm not sure where. Tomorrow Im going to try to start with your macOS sample project by converting it to iOS and working backwards. Can I have x number of platforms named "jup" and it will work the same for each one? – genericguy25 Jul 08 '17 at 07:11
  • @genericguy25 it should work yes. that is how I designed it. Like I told you before, this is a bit complicated for something that seems like it should be easy. You have to keep track of a bunch of state and reset things at just the right time in certain parts of the SK loop :{ – Fluidity Jul 08 '17 at 07:13
  • @genericguy25 just to emphasize for this to work properly, you really have to have 4 things... 1, the `initialDY` of your player at update, 2, the contacted platform with the `didFinishUpdate` positional check (to land) (to reset the collision mask), and 3, you have to set the player's CURRENT dy to the initialDY on first contact (or he will bounce off), and 4, you have to change the collision masks on that first contact as well – Fluidity Jul 08 '17 at 07:16
  • @genericguy25 also, I am going to push an iOS target to this project on github in about 10 minutes... so you won't have to try to do it yourself if you are unsure of what to do. – Fluidity Jul 08 '17 at 07:20
  • @genericguy25 I adde the iOS target, and added more information in the #2 section that I had forgotten to explain (but was in the code comments). – Fluidity Jul 08 '17 at 07:32
  • Thank you so much man. I will implement this today and I will report back here with the results. – genericguy25 Jul 08 '17 at 18:10
  • @genericguy25 yeah post your code or link to project if you still can't get it... I have been messing with something since last night and it was a silly copy and paste error >:[ – Fluidity Jul 08 '17 at 18:12
  • It looks like its working as intended as i slowly integrate my project into it! So thank you so much, I really appreciate all the work that went into your answer! I have a few questions about how you wrote it since I'm still a novice. you made a player class and added an extension to that class with "func _update()" does this append anything in here to the current game scene's update function? – genericguy25 Jul 10 '17 at 16:39
2

You just need a condition that let's you know if you are in a body. I also cleaned up your code to avoid accidently putting in the wrong categories

if let body = player.physicsBody,   let dy = body.velocity.dy {
   // when I am jumping or I am in a platform, then do not register
   if (dy > zero || body.node.intersects(platform) && (body.node.position.y - body.node.size.height/2 != platform.position.y + platform.size.height / 2)  {
        body.collisionBitMask &= ~CollisionTypes.platform.rawValue

   }
    else {
        // Allow collisions if the hero is falling
        body.collisionBitMask |= CollisionTypes.platform.rawValue
Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44
  • I did not test this in playgrounds, let me know what error you get – Knight0fDragon Jul 05 '17 at 18:12
  • Thanks for the response! the "platform" in the if statement gave an "unresolved identifier error. I replaced that with JumpThroughPlatformObject from my OP. When I jump onto the platform, I instantly fall though, i suspect because right before I land on it my velocity becomes zero at the peak of the jump so it deactivates the collisions and i fall through. Is there a way to make this work with every platform on my .sks without having to reference each one in the game scene.swift like i do in the OP? – genericguy25 Jul 05 '17 at 21:28
  • I have a feeling you are going through the platform on the way down because you are intersecting I changed my answer – Knight0fDragon Jul 05 '17 at 21:39
  • the changes are basically if the bottom of the sprite != the top of the platform and intersecting happens – Knight0fDragon Jul 05 '17 at 21:55
  • Thanks. it seems now my character lands on the platforms top edge for a split second, then the collision goes away and he drops down. – genericguy25 Jul 05 '17 at 22:35
  • is anything else screwing with it? – Knight0fDragon Jul 05 '17 at 22:43
  • instead of != do – Knight0fDragon Jul 05 '17 at 22:44
  • I changed it to "<". If I'm jumping up through the platform from underneath, ill land on it and then fall off again like before, but since the contact bit mask is still active, I'm able to jump while the hero/sprite platforms are over lapped. When I do this I basically have a double jump, sometimes when I land back on the platform after this "double" jump I stay on it, other times ill still fall through a split second later after landing on it. – genericguy25 Jul 05 '17 at 23:02
  • and you are sure nothing else is changing collisionBitMask? – Knight0fDragon Jul 05 '17 at 23:04
  • I'm 100% sure nothing else is changing any collision masks. – genericguy25 Jul 05 '17 at 23:20
  • I don't know then, we are literally saying if we are moving up, or if we are inside of the platform then drop, otherwise do not drop. Where is this code being done? – Knight0fDragon Jul 05 '17 at 23:23
  • well that is why, needs to be done on didContactBegin and didContactEnd – Knight0fDragon Jul 05 '17 at 23:27
  • on both of them at the same time? – genericguy25 Jul 05 '17 at 23:30
  • it won't happen at the same time, `didBeginContact` is when your player touches a platform, `didEndContact` happens when your player leaves a platform – Knight0fDragon Jul 05 '17 at 23:51
  • so I need to put the entire if else statement in your post in both the didBeginContact and in the didEndContact? – genericguy25 Jul 06 '17 at 00:26
  • Normally you could just turn platforms back on with didEndContact, but you couldn't be touching any other platform in the process, so you need a counter to verify that you are not touching any other platforms, and if you are not, then turn it back on, or make sure that you use 2 types of platform categories when 2 platforms are in close proximity – Knight0fDragon Jul 06 '17 at 00:37
  • I tried moving it to didBegin and didEnd, Didn't have much luck, but Honestly I don't really know what I'm supposed to be putting where with those two functions. Please refer to the OP, ill post what i tried to do in there. – genericguy25 Jul 06 '17 at 01:13
  • kod (didn't read all comments) this doesn't work for me, because the DY is negative at start. I had to create an `initialDY` for player on `.update` for this to work. I used to just do `initialPosition` and check it against the platform :P but the dy is easier to implement. I'm posting an answer in a minute here to show what I'm talking about. – Fluidity Jul 07 '17 at 06:03
  • If dy is negative on start, then you hit the else condition turning platforms on – Knight0fDragon Jul 07 '17 at 11:55
0

Well, The answers above work well but those are very completed. Simple answer is use Platform effector 2D component. which applies various “platform” behavior such as one-way collisions, removal of side-friction/bounce etc. Check out this Unity's official tutorial for more clearance.

Jazib
  • 3
  • 2