One way platform using 2D Colliders?

I am currently in the process of building a platform game and I am looking into porting it over to the new 2D physics engine that was added in 4.3. It looks like most of it should port over, but I have no idea how to platforms that can only be collided with from above.

My current solution using the 3D system is a script I altered that converts the current collider into a mesh collider that only has faces pointing upwards. This seems to work really well, but I can’t seem to find a similar solution using the 2D colliders.

I know that Box2D has something like a pre-collision event you can intercept before the physics system handles a collision, effectively giving you the option to ignore the collision, but it doesn’t look like that functionality exists in the Unity release.

Does anyone have a solution? Or perhaps know if an update may include the hooks that I am looking for?

Any help is much appreciated.

Cheers,
Sev

I'm using the code pasted above (in an Editor script), and it is working (in Unity 5.4).

11 Answers

11

I know this topic is a bit stale and I’m pretty new to Unity and Physics2D.IgnoreLayerCollision probably has been added not so long ago, but I solved one way platform with just one line in FixedUpdate().

Physics2D.IgnoreLayerCollision (playerLayer, platformLayer, rigidbody2D.velocity.y > 0);

That's not a solution. Let's say Mario jumps over a platform, but doesn't reach the top. Diagram: MM ----MM--- | MM | | | After he reaches the apex of his jump, his y velocity is now negative. But he should /not/ collide with the platform, because he isn't above it. It isn't a valid solution.

Good point Alexrose, but you can work around that. If you make your character's floor collider be a flat rectangle at his feet, this would work. Just put one-way platforms all on their own collision layer separate from the rest of the world. However, this then makes wall and ceiling collisions all a special case (possibly solved by ray or sphere casts.)

I solved it by setting the platform’s collider to trigger and it’s tag to “OneWayPlatform” and then adding the script below to the Player.

What the script does is in OnTriggerEnter/Stay, if the player is above the platform and moving downwards, it stops the player, and stores and disables it’s gravity.

Then, once the player exits this particular collider by going left/right, it sets it’s gravity back.

Edit: I optimized the code a bit and also increased the readability :wink:

Edit 2: Demo: OneWayPlatforms

#pragma strict

public var deadZone : float = 0.1;

private var g = 0;
private var touchingPlatforms : int = 0;
private var body : Rigidbody2D;

function Start () {
	g = rigidbody2D.gravityScale;
	body = rigidbody2D;
}

function OnTriggerEnter2D (c : Collider2D) {
	if(c.CompareTag("OneWayPlatform")) {

		touchingPlatforms++;

		var maxDistance = (c.transform.localScale.y + transform.localScale.y) / 2;
		if(body.velocity.y < 0 && transform.position.y - c.transform.position.y - body.velocity.y * Time.fixedDeltaTime > maxDistance - deadZone) {
			body.gravityScale = 0;
			body.velocity.y = 0;
			transform.position.y = c.transform.position.y + maxDistance;
		}
	}
}

function OnTriggerExit2D (c : Collider2D) {
	if(c.CompareTag("OneWayPlatform") && touchingPlatforms-- == 1)
         body.gravityScale = g;
}

–David

I'd love to read your code and learn something from it, but it's horribly indented and not at all nice to read. Could you upload a neater version please, as this seems like a winner?

Actually, it does use the native unity physics, it's not computationally intesive at all, and I just dragged it into another project I'm working on to fully test it and it just worked. which means all your accusations are false.

So this worked some, but I have steps. Steps I would like to run pass and when I turn around to go up, I should walk up. SO I guess One Way Collision on Steps.

This doesn't seem to work on my project. The code warps my character in the middle of the one way platform, instead of on top of it. But if the platform is a specific size, it suddenly works. Why would it do that?

You can use Physics2D.IgnoreLayerCollision and Physics2D.OverlapCircle to achieve one-way platforms.

This script would go on your player game object, with an empty game object placed at the feet of the player, used for the groundCheck transform.

`
//in your player controller script 

/**
 * empty game object used to check for ground, 
 * placed at the bottom of your character, with a small radius
 */
public Transform groundCheck;
public float groundCheckRadius = 0.1; 

/**
 * Layer mask to define what layers are walkable
 */
public LayerMask walkableLayerMask; 

void FixedUpdate()
{
	//first check if the ground is below the player
	bool isOnGround = Physics2D.OverlapCircle( groundCheck.position, 
												groundCheckRadius, 
												walkableLayerMask
											  );
	
	//Now ignore collisions between the Player and OneWayPlatform layers, 
	//unless you're on the ground or travelling up.
	Physics2D.IgnoreLayerCollision( LayerMask.NameToLayer("Player"), 
									LayerMask.NameToLayer("OneWayPlatform"), 
									!isOnGround || rigidbody2D.velocity.y > 0 
								   );
}
`

The condition for IgnoreLayerCollision needs both the ground and velocity conditions to prevent the player’s position from shifting when passing through the platform.

This assumes you have two layers (Player and OneWayPlatform) defined in Project Settings → Tags and Layers. I also use EdgeCollider2D components on the one way platforms.

Also, if you wanted to allow the player to fall through the playform if they are holding a down button, you could change your condition to something like: (!isOnGround || rigidbody2D.velocity.y > 0 || Input.GetButton(“Down”) )

Yes, its very limited until IgnoreCollision or OnPreCollision2D is implemented. Hopefully that is soon.

This helped me a lot, i tried a bunch of diffrent things - but nothing worked out before i tried this. Remember to Make new layers, and assign a layer for the player, and one for the oneway platforms, and then assign the oneway playform to the layers you want the player to go through.

This won’t work yet because there isn’t a Physics2D.IgnoreCollision yet, but it could be done with multiple layers. Add an EdgeCollider2D at the top. Define its height above the transform of the sprite it’s attached to as platformheight.

Then:

void FixedUpdate () {
        foreach (Rigidbody2D Object in FindObjectsOfType(typeof(Rigidbody2D)) as Rigidbody2D[])
        {
            float lowestpoint = Mathf.Infinity;
            foreach (Transform blah in Object.transform)
            {
                if (blah.name == "Edge")
                {
                    if(blah.transform.position.y<lowestpoint) lowestpoint = blah.transform.position.y;  
                }
            }
            if (lowestpoint > transform.position.y + platformheight)
            {
                Physics2D.IgnoreCollision(collider2D, Object.collider2D);
            }
        }
	}

Then stick four empty GameObjects called “Edge” on each side of your objects. If you’re just using square hitboxes for everything though you can just use lossyscale to work out whether they’re above or below. This will make the colliders only work if the object is above the platform.

But, again, Physics2D.IgnoreCollision isn’t an option yet, so Physics2D.IgnoreLayer could be used as a temporary fix.

e.g. You could put different platforms on different layers, and then snap the player’s height to the layer of the platform he’s in, and forbid collisions on the same layer. Which is a pain for now, and a bad idea in general.

But I would also say that just simply using triggers and killing velocity and gravity is a bad idea too - very fast objects will be able to bury themselves into walls and objects will float slightly above your platforms instead of resting on them.

I wanted to use Physics2D.IgnoreCollision too, but I guess we'll have to wait until Unity implements it.

I just requested it here: http://feedback.unity3d.com/suggestions/a-physics2d-dot-ignorecollision-function-is-vital-for-proper-one-way-platformers-in-unity2d If you feel like throwing a few votes towards it.

Thanks for that - I've thrown a bunch of votes behind it. I've got a temporary solution implemented that is a bit messy, but it won't be hard to swap it out to something like this if/when Physics2D.IgnoreCollision becomes available.

Here is my workaround, because of bug (OnTriggerEnter2D call multiple times, i use 2 triggers, one for OnTriggerEnter, another for OnTriggerExit.

For triggers, i use EdgeColliders and on scene it looks like this
[23716-screen+shot+2014-03-17+at+16.35.53.png|23716]

Here is a full prefab structure

alt text

This code for enter Trigger

public class OneWayPlatformTriggerEnterController : MonoBehaviour {
	public int playerLayerId = 8;
	public int enabledPlatformId = 9;
	public int disabledPlatformId = 10;
	GameObject parentGO;

	void Start(){
		parentGO = gameObject.transform.parent.gameObject;
	}

	void OnTriggerEnter2D(Collider2D other){
		if (other.gameObject.layer == playerLayerId && parentGO.layer == enabledPlatformId){

			parentGO.layer = disabledPlatformId;
		}
	}
}

and this for exit

public class OneWayPlatformTriggerExitController : MonoBehaviour {
	public int playerLayerId = 8;
	public int enabledPlatformId = 9;
	public int disabledPlatformId = 10;
	GameObject parentGO;
	
	void Start(){
		parentGO = gameObject.transform.parent.gameObject;
	}

	void OnTriggerExit2D(Collider2D other){
		if (other.gameObject.layer == playerLayerId){
			parentGO.layer = enabledPlatformId;
		}
	}
}

As you can guess, my player has one layer Id, and oneWayPlatoform has 2 layer, one Layer when platform active and player can walk on it, and another layer which does not interact with player because of switched off in LayerCollision Matrix (Kevin is my player layer)

alt text

Since my character has the ability to grow and shrink I couldn’t really use many of these solutions. Here’s the one that I used.

First create 3 layers in your editor

  • Player
  • AbovePlatform
  • BelowPlatform

Next make sure that “BelowPlatform” Layer ignores the Player Layer in your Physics2D settings

[26682-screen+shot+2014-05-20+at+2.39.14+am.png|26682]

Add this code to your platform

//Platform.cs

using System.Collections;
using System.Collections.Generic;
	
public class Platform : MonoBehaviour {
	public bool passThrough;

	void Start (){
		    //Set the layer based on whether the platform is solid or passthrough
	        if(passThrough){
                  gameObject.layer = LayerMask.NameToLayer("BelowPlatform");
            }else{
                  gameObject.layer = LayerMask.NameToLayer("AbovePlatform");
            }
	}

	void FixedUpdate(){
            if(passThrough){
                    //Set the layer based whether they are above or below the platform on the y coordinate
	 	        bool ignore =  Player.FootPosition < transform.position.y;
                
                    if(ignore){
		                 gameObject.layer = LayerMask.NameToLayer("BelowPlatform");  
                    }else{
                         gameObject.layer =  LayerMask.NameToLayer("AbovePlatform");
                    }
            }
	}   
}

Add this code to your Player character

//Player.cs

using UnityEngine;
using System.Collections;


public class Player : MonoBehaviour {
        public BoxCollider2D boxCollider;

        public static float FootPosition{
                //We want to return a foot position slightly lower than our characters actutal 
                //foot position to avoid jerkiness when passing through the platform
		get{return transform.position.y- (boxCollider.bounds* 0.75f);}
	}

}

I hope this will help some of you till Unity releases version 5.

Try making a collider just below the platform and deactivating the platform collider whenever the player collides with that collider. Try out the link For the Tutorial Newtonians' Blog 3D: One Way Platform : Unity Tutorial

I ended up writing a script that projects a ray up and down from the character. If it collised with a platform that is marked as one way, it uses IgnoreCollisions to update teh physics items.

It took a bit of tweaking to get it working, but it seems to work pretty well.

I think the best method to achieve the one way platform is to make a edge collider around the part you want things to collide with and a polygon collider for the area under and in the middle of the object as a trigger with this script attached.

#pragma strict
var PutTheEdgeColliderHere : Collider2D;

function OnTriggerEnter2D (other : Collider2D)
{
	if (other.gameObject.tag == "TheTagThePlayerUses")
	{
		other.gameObject.tag = "ThroughOneWay";
		Physics2D.IgnoreCollision(defaultcollider,other.gameObject.collider2D,true);
	}
	
}

function OnTriggerExit2D (other : Collider2D)
{
	Physics2D.IgnoreCollision(defaultcollider,other.gameObject.collider2D,false);
	other.gameObject.tag = "TheTagThePlayerUses";
}

Do get this system wotking you need to:

1st Make a EdgeCollider2D and set it around the whole object.
alt text

2nd Then make a PolygonCollider2D a bit smaller but longer at the bottom.
http://i.gyazo.com/0278a84b5024acf36aeff9d696d627e6.png

3rd Assign the script and drag the EdgeCollider2D into the defaultcollider slot.
alt text

4th The last thing you need to do is make a tag named “ThroughOneWay” or something, just be sure to name it the same in both script and in unity editor.

alt text

The simplest method I found is similar to nicloay’s but I set the platforms collider to trigger.

public class PlatformDetector : MonoBehaviour
{
	EdgeCollider2D colPlatform;

	void OnTriggerEnter2D(Collider2D col)
	{
		if (col.gameObject.tag == "Platform")
		{
			col.gameObject.GetComponent<EdgeCollider2D>().isTrigger = false;
		}
	}

	void OnTriggerExit2D(Collider2D col)
	{
		if (col.gameObject.tag == "Platform")
		{
			col.gameObject.GetComponent<EdgeCollider2D>().isTrigger = true;
		}
	}
}

This code is placed on a platform check that is beneath the player. Hope this helps!

I’ve composed a solution that supports edge cases for ground checks, fallthrough platforms, multiple colliders on a player, and is even optimized to prevent performance issues. This can easily be expanded to work with any nearby entity by wrapping the platform with a simple trigger and applying the appropriate ignore collision logic.

using UnityEngine;
using System.Collections;

public class Platform : MonoBehaviour {
	SpriteRenderer playerGraphic;
	Rigidbody2D playerBody;
	BoxCollider2D playerBox;
	CircleCollider2D playerCircle;
	
	float padTop = -0.05f; // Padding applied to the top of feet platform checks (prevents player from barely standing on platform)
	bool visible; // Is the player able to interact with the platform
	
	[SerializeField] bool enableDrop;
	
	void Start () {
		GameObject player = PlayerAPI.current.gameObject;
		playerGraphic = player.GetComponent<SpriteRenderer>();
		playerBody = player.GetComponent<Rigidbody2D>();
		playerBox = player.GetComponent<BoxCollider2D>();
		playerCircle = player.GetComponent<CircleCollider2D>();
		
		ToggleCollision(playerGraphic.bounds.min.y > collider2D.bounds.max.y + padTop, playerBody.velocity.y, true);
	}
	
	void Update () {
		if (!IsDropping()) {
			ToggleCollision(playerGraphic.bounds.min.y > collider2D.bounds.max.y + padTop, playerBody.velocity.y);
		} else {
			ToggleCollision(false);
		}
	}
	
	bool IsDropping () {
		if (!enableDrop) return false;
		return Input.GetAxis("Vertical") < -0.1f && Input.GetButton("Jump");
	}
	
	void ToggleCollision (bool isEnabled) {
		// Fake a submission to get the values toggled
		if (isEnabled) {
			ToggleCollision(true, -5f);
		} else {
			ToggleCollision(false, 0f);
		}
	}
	
	void ToggleCollision (bool above, float velY, bool force = false) {
		if (above == visible && !force) return;
		
		// Check if falling and and above the platform
		if (above && velY < 0.1f) {
			ToggleColliders(false);
			SetDepth(0f);
			visible = true;
		} else {
			ToggleColliders(true);
			SetDepth(-5f);
			visible = false;
		}
	}
	
	void ToggleColliders (bool ignore) {
		Physics2D.IgnoreCollision(collider2D, playerBox, ignore);
		Physics2D.IgnoreCollision(collider2D, playerCircle, ignore);
	}

	// With setting the depth you can prevent ground checks from firing false positives
	void SetDepth (float depth) {
		Vector3 pos = transform.position;
		pos.z = depth;
		transform.position = pos;
		
	}
}