Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

One-way platform collisions in Sprite Kit

I am making a Doodle Jump clone game in Swift and the issue is that when the player jumps, it hits its head on the bottom of the platform and doesn't pass through. How can I make the player pass over the platform and jump through them? I have my code here:

import SpriteKit

class GameScene: SKScene, SKPhysicsContactDelegate {

    var hero = SKSpriteNode(imageNamed: "hero");
    var stepSizeTest = SKSpriteNode(imageNamed: "step");
    var start = false;
    var jumpSpeed = CGFloat(0);
    var gravity = CGFloat(0);
    var stepPositionDivision:CGFloat = 0 //allows the step to spawn on specific places on y axes
    var setpPositionHeightIncrease:CGFloat = 0;

    var positionX:CGFloat = 0;
    var timeInterval:NSTimeInterval = 0;
    var CurTime:NSTimeInterval = 0;
    var TimeWhenThePreviousStepSpawned:NSTimeInterval = 0;
    var standart:Bool = false;
    var move:Bool = false;
    var oneJumpOnly:Bool = false;
    var cracked:Bool = false;
    var jumped:Bool = false;

    struct PhysicsCategory{
        static var None: UInt32 = 0;
        static var step: UInt32 = 0b1;
        static var hero: UInt32 = 0b10;
        static var All: UInt32 = UInt32.max;
    }

    func didBeginContact(contact: SKPhysicsContact) {
        var contactBody1: SKPhysicsBody;
        var contactBody2: SKPhysicsBody;
        if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask){
            contactBody1 = contact.bodyA;
            contactBody2 = contact.bodyB;
        }
        else{
            contactBody1 = contact.bodyB;
            contactBody2 = contact.bodyA;
        }

        if (contactBody1.categoryBitMask == PhysicsCategory.step && contactBody2.categoryBitMask == PhysicsCategory.hero){
            jumpSpeed = CGFloat(self.frame.size.height*0.01320422535);

        }
//        if (contactBody1.categoryBitMask == PhysicsCategory.ground && contactBody2.categoryBitMask == PhysicsCategory.hero){
//
//        }
    }

    override func didMoveToView(view: SKView) {
        /* Setup your scene here */
        stepPositionDivision = self.frame.size.height/23;

        jumpSpeed = CGFloat(self.frame.size.height*0.01320422535);
        gravity = CGFloat(self.frame.size.height*(-0.0003521126761));

        self.physicsWorld.contactDelegate = self;

        self.physicsWorld.gravity = CGVectorMake(0,0);

        let sceneBody = SKPhysicsBody(edgeLoopFromRect: self.frame);
        sceneBody.friction = 0;
        self.physicsBody = sceneBody;


        self.hero.anchorPoint = CGPoint(x: 0.5, y: 0.5);
        self.hero.position = CGPoint(x:self.frame.size.width/2, y:self.hero.size.height/2);
        self.hero.physicsBody = SKPhysicsBody(rectangleOfSize: self.hero.size)
        self.hero.physicsBody?.restitution = 1;
        self.hero.physicsBody?.affectedByGravity = false;
        self.hero.physicsBody?.friction = 0;
        self.hero.physicsBody?.categoryBitMask = PhysicsCategory.hero;
        self.hero.physicsBody?.collisionBitMask = PhysicsCategory.step;
        self.hero.physicsBody?.contactTestBitMask = PhysicsCategory.step;

        self.addChild(self.hero);
    }

    func ranPositionX() -> CGFloat{
        let stepSise = UInt32(self.stepSizeTest.size.width)
        let posX = arc4random_uniform(UInt32(self.frame.size.width) - stepSise) + stepSise
        return CGFloat(posX)
    }

    func stepSpawn(){
        if (setpPositionHeightIncrease < self.frame.size.height){
            let step = SKSpriteNode(imageNamed: "step");
            let posX = ranPositionX()

            step.anchorPoint = CGPoint(x: 0.5, y: 0.5);
            step.position = CGPoint(x:posX, y:setpPositionHeightIncrease);
            step.physicsBody = SKPhysicsBody(rectangleOfSize: step.size);
            step.physicsBody?.affectedByGravity = false;
            step.physicsBody?.dynamic = true;
            step.physicsBody?.friction = 0;
            step.physicsBody?.categoryBitMask = PhysicsCategory.step;
            step.physicsBody?.collisionBitMask = PhysicsCategory.None;
            step.physicsBody?.contactTestBitMask = PhysicsCategory.hero;

            self.addChild(step);
            setpPositionHeightIncrease += stepPositionDivision
        }
    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
       /* Called when a touch begins */
        for touch in touches {

        }
    }

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */

        self.hero.position.y += jumpSpeed
        jumpSpeed += gravity

        if (start == false){
            //self.hero.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 40))
            start = true;
        }
        stepSpawn()
    }
}
like image 458
GeorGios Avatar asked Sep 10 '16 17:09

GeorGios


2 Answers

One way to do this is to toggle the appropriate collision bit(s) on/off, if the hero is falling or jumping. The hero is falling if its velocity's dy property is less than zero and jumping (or bouncing up) if dy is greater than zero. Here's an example of how to do this:

// Add this to the update method
if let body = hero.physicsBody {
    let dy = body.velocity.dy
    if dy > 0 {
        // Prevent collisions if the hero is jumping
        body.collisionBitMask &= ~PhysicsCategory.step
    }
    else {
        // Allow collisions if the hero is falling
        body.collisionBitMask |= PhysicsCategory.step
    }
}
like image 137
0x141E Avatar answered Nov 09 '22 00:11

0x141E


The best way to go about this is to use 2 categories, let's call it CollisionPlatform, and NoCollisionPlatform.

Let's call your SKSpriteNode Hero

Set all platforms to NoCollisionPlatform

Your platform SKPhysicsBody should be a line, not a box. It is better to use edge loops when creating these bodies, will put less strain on the physics world.

The Hero should only have CollisionPlatform in the collisionBitMask, but both CollisionPlatform and NoCollisionPlatform in the contactTestBitMask.

Your Hero should also know its previous position.

On your didBeginContact method, when you collide with NoCollisionPlatform, you check if the bottom of the Hero is equal to or below the platform and the previous position of the Hero is above the platform. If this is true, then on your platform, change the categoryBitMask of the platform to CollisionPlatform

On your didEndContact method set the categoryBitMask of the platform back to NoCollisionPlatform

Now, be careful with how you do it, because if you move to the next platform that lies on the same x axis, you may fall through due to rounding errors. If you are not dealing with 1/2 pixel positions, I recommend rounding or casting to Int

To achieve a forced down jump, simply remember the last platform that the Hero was touching, and on the command, set the categoryBitMask back to NoCollisionPlatform.

At this point I would recommend sub classing your Hero, and keeping a pointer to the platform he last touched inside this sub class along with his previous position. Be sure to clear it to nil when he no longer touches it.

Also note, that doing it this way means you do not need to apply any physics forces to your Hero other than gravity, so SKAction's will work hand in hand with this. If you do not apply gravity, then I do not know what happens when you hit the platform, since the velocity of your Hero will be 0, who knows what kind of push reaction (if any) will happen. With going left and right (even up), this will happen though, so what you can do is apply a small amount of velocity to your sprite so that the system knows the direction you are moving, but is weak enough that it won't move a pixel.

Doing all of this should also handle the side detection, since you only have 1 pixel to worry about for your platform, the chances of your Hero being above it the previous frame, and below it the next frame in such a way that you expect your Hero to not land on the platform is really slim (He would need to drop at an angle of like 1 degree over a good length of pixels in 1 frame to achieve this, I say let the guy land in this case.

If you want to get even more advanced, reserve a bit on the mask to account for this, and just set when needed, this way you can have multiple categories, with only 1 reserved to do the collision test, instead of having to make multiple entries. I like reserving bit 30 to handle this case, bit 31 I reserve to check if the sprite is alive or dead.

Do note that this will only work with 1 Hero

If you want to have multiple Heroes, then you have to work another way.

One is you do the swapping on the individual Hero's collisionBitMask instead. The problem with this approach is if you create a staircase of platforms, you may end up colliding into the side of the 2nd platform.

The other thing you can do, is use my advance method, and reserve certain bits of the categoryBitMask for each Hero

like image 29
Knight0fDragon Avatar answered Nov 09 '22 01:11

Knight0fDragon