- Home /
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
Answer by dunenkoff · Jan 04, 2014 at 06:38 PM
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 $$anonymous$$ario jumps over a platform, but doesn't reach the top. Diagram:
$$anonymous$$$$anonymous$$
----$$anonymous$$$$anonymous$$---
| $$anonymous$$$$anonymous$$ |
| |
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 $$anonymous$$rose, 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.)
Answer by DavidDebnar · Nov 17, 2013 at 03:55 PM
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 ;)
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?
Upload a neater version or explain what everything does. You could do that with comments in the code or in your answer.
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.
I don't wanna argue, really, but there are no calculations, which are applied every frame. Also, stopping the rigibody and turning it's gravity off, when it lands, is 'the native way' in any platformer. Then, I didn't use an empty project, but a huge complex platformer. Also, movement states and such won't break anything. Next, denying gravity doesn't have to do anything with the character being grounded or not. I've been program$$anonymous$$g for the better part of my life, and I'm pretty sure I cut out most of my program$$anonymous$$g habits by now.
What are bad program$$anonymous$$g habits on the other hand: changing the player's layer. It's the worst idea, mainly in bigger projects, because Unity limits you to 16 layers, and having 2 layers dedicated solely to one-way playforms is very bad. It's also more computationally intensive, since PhysX has to change it's list of active rigidbodies.
"Also, stopping the rigibody and turning it's gravity off, when it lands, is 'the native way' in any platformer."
If you do that with a trigger and an object is going very fast, for instance, because you aren't doing a proper verlet integration to test where it's going to be in a couple of frames, it's going to find itself completely buried in the middle of your platform.
Also, that is OnTriggerEnter. So if something falls on top of your object, it will set it in motion again. There's nothing keeping it from its geodesic once another object is involved. If you were using OnTriggerStay then.. you would have some really weird behaviour too with collisions.
Another problem is if you want a platform that e.g. moves up and down. For instance, I want to have characters be able to jump on each other's head. Using triggers won't solve that problem.
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.
Answer by zobot · Jan 06, 2014 at 05:57 PM
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") )
Uhh.. if your y velocity is positive you should be able to jump through platforms. Otherwise you'll hit your head on the tops of platforms, which defeats the whole point.
Obviously to do one way platforms all you need are the two conditions:
The player's feet are above the platform.
The player has a y velocity less or equal to 0.
You don't need OverlapCircle for that, you can just check the y position of the GameObject attached to the player's foot against the y position vertex of the top edge collider on the platform. If it's higher, he's above it.
But using a single layer doesn't work if you have two one way platforms like "steps" like this:
CC
.----CC---.
| CC |
| .---------.
| | |
| | |
Where C is your character and the rest are the two platforms (the bottom of which he's standing on). If your character is standing on the bottom platform, he is "grounded", so he will collide with the top platform, even though he's in front of it.
Another example where it doesn't work: say you have projectiles like grenades that should bounce off the top of objects. Each one has to have its own check to see if it's on the platform. You can't assign a layer to every enemy, character, projectile, box etc. in the entire scene. There's a limited number of layers, and it would be extremely convoluted and a waste of resources.
There should be an IgnoreCollision function so you can choose two specific objects.
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 $$anonymous$$ake 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.
Answer by Alexrose12345 · Nov 22, 2013 at 08:35 PM
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:
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.
Answer by nicloay · Mar 17, 2014 at 12:45 PM
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
Here is a full prefab structure
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)