Skip to content
HY Devlog
Go back

A One-Way Platform Is an Angle, Not a Direction

I clipped this while building a 2D platformer and looking for how to handle floating platforms. The author got stuck in the same place — making floating platforms for a 2D endless runner, stopping at “if the character jumps from under the platform it should pass through and land on top,” and writing up the answer in 2023. The post records where it got stuck and how the search went, which makes it good reading. That the same question comes out of two different 2D projects is itself what the author calls the genre’s unwritten standard.

At first I thought of this. 1. Have the player cast downward and toggle the platform’s collider on/off? 2. Or have the platform do an overlap check and only turn the collider on when the player is above?

Both are workable, and setting them aside to find the component the engine already ships is this post’s achievement. PlatformEffector2D is the right answer.

But what the post tells you stops at turn on two checkboxes. So when it works fine and then snags oddly at an edge, or a character with two colliders gets caught, there’s nowhere obvious to look. What this component actually evaluates is an angle, not a direction — and that angle is a value sitting in the Inspector.

Table of Contents

Table of Contents

Finding the Component Is the Right Call

Start with what’s right. The setup procedure is accurate.

Attach a collider to the platform first, check Used By Effector on the collider, then add this component. Use One Way has to be checked of course, but it’s checked by default.

The class description in the Scripting Reference is short and says the same thing.

Applies “platform” behaviour such as one-way collisions etc.

And the observation in the conclusion holds up too.

In my opinion the biggest difference between 2D and 3D in Unity is the physics simulation. … The collider components are separate for 2D, rigidbodies are separate, and even casting a ray goes through the Physics2D class separately.

That’s a fair summary. I covered the 2D rigidbody side separately in the FreezePositionY post.

One note: the documentation link the clipping gives is the Unity 5.3 Korean manual. It’s a 2015 document, so some of it doesn’t match today’s API. One instance shows up below.

One-Way Is an Angle, Not a Direction

This component’s key property is Surface Arc, and it never appears in the clipping. From the manual:

Surface Arc — The angle of an arc centered on the local ‘up’ defines the surface which doesn’t allow colliders to pass. Anything outside of this arc is considered for one-way collision.

The Scripting Reference agrees.

surfaceArc — The angle of an arc that defines the surface of the platform centered of the local ‘up’ of the effector.

Read it and the criterion isn’t “did it come from above or below.” It’s whether the contact normal falls inside that arc. Which brings these consequences:

Symptoms like “sometimes I don’t land on the edge” come from here. Hit the corner of a platform at an angle and the normal falls outside the arc, so it’s treated as a pass-through. That’s not a bug, it’s a setting.

Desired behaviorSurface Arc
Standard one-way180° (upper half)
Catch reliably even at cornersWider than 180°
Let glancing contacts throughNarrower than 180°
Effectively a normal colliderClose to 360°

And the arc can be rotated. From the Scripting Reference:

rotationalOffset — The rotational offset angle from the local ‘up’.

For a one-way ceiling — punch through from below, can’t come back down — set this to 180. The 5.3 manual page the clipping links doesn’t have this entry. The current scripting API does.

Two Checkboxes the Clipping Doesn’t Mention

Use One Way Grouping

This is the answer to a character with more than one collider getting stuck. The manual spells out the use case.

Ensures that all contacts disabled by the one-way behavior act on all colliders. This is useful when using multiple colliders on the object passing through the platform and they all need to act together as a group.

A character with separate body and foot colliders, or one carrying a weapon hitbox, runs into this. When one collider passes and another blocks, the character ends up half-embedded in the platform. It’s one checkbox.

Use Collider Mask / Collider Mask

The clipping’s initial worry was “computation,” and this is the way to narrow what’s involved without scripting.

Use Collider Mask — Select this option to indicate the use of the Collider Mask property. If this isn’t selected, the global collision matrix is chosen as the default for all colliders.

Collider Mask — The mask used to select specific layers allowed to interact with the Effector.

You can make this platform respond only to the player layer without touching the global collision matrix. That’s where you go when enemies or projectiles should just pass by.

The remaining three, for completeness:

PropertyDescription
Side ArcAn arc defining the sides, centered on the Effector’s local ‘left’ and ‘right’. Normals within it are considered for the ‘side’ behaviors
Use Side FrictionWhether friction is used on the platform sides
Use Side BounceWhether bounce is used on the platform sides

There’s a reason turning side friction off is the common choice: it stops a character that jumped into the platform’s side from hanging there instead of sliding off.

Dropping Down With the Down Key — Docs Instead of a Video

The clipping stops here.

If you want to implement the pattern common in platformers, “press down to drop through,” the video above should help.

The part handed off to a video is the one most often needed next. Written from what’s documented, it comes out like this.

The core is Physics2D.IgnoreCollision.

public static void IgnoreCollision(Collider2D collider1, Collider2D collider2, bool ignore = true);

Makes the collision detection system ignore all collisions/triggers between collider1 and collider2.

And the caveat the docs attach matters.

It is not persistent. This means that the ignore collision state will not be stored in the editor when saving a Scene.

On top of that, deactivating either collider loses the ignore state, so you have to reapply it. In an endless runner using object pooling, that sentence becomes a bug directly: the moment a platform is toggled off, the ignore is gone.

using System.Collections;
using UnityEngine;

/// <summary>
/// Drops through a one-way platform on the down key.
/// Attach to the player.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class OneWayDropper : MonoBehaviour
{
    private const float DROP_DURATION = 0.35f;

    [Header("Drop")]
    [SerializeField, Range(0.1f, 1f), Tooltip("How long collisions are ignored (seconds)")]
    private float _dropDuration = DROP_DURATION;

    [SerializeField, Tooltip("The layer the one-way platforms are on")]
    private LayerMask _platformLayers;

    private Collider2D _collider;
    private Collider2D _standingOn;
    private bool _isDropping;

    private void Awake()
    {
        TryGetComponent(out _collider);
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        // Remember the platform we're standing on. That's what we pass through.
        if (IsPlatform(collision.collider))
        {
            _standingOn = collision.collider;
        }
    }

    private void OnCollisionExit2D(Collision2D collision)
    {
        if (collision.collider == _standingOn)
        {
            _standingOn = null;
        }
    }

    /// <summary>Called from the input side.</summary>
    public void RequestDrop()
    {
        if (_isDropping || _standingOn == null)
        {
            return;
        }

        StartCoroutine(DropThrough(_standingOn));
    }

    private IEnumerator DropThrough(Collider2D platform)
    {
        _isDropping = true;

        Physics2D.IgnoreCollision(_collider, platform, true);

        yield return new WaitForSeconds(_dropDuration);

        // Docs: not persistent, and the state is lost if either side is deactivated.
        // So check it's still alive before restoring.
        if (platform != null)
        {
            Physics2D.IgnoreCollision(_collider, platform, false);
        }

        _isDropping = false;
    }

    private bool IsPlatform(Collider2D other)
    {
        return (_platformLayers.value & (1 << other.gameObject.layer)) != 0;
    }
}

That platform != null check exists because of the docs’ “not persistent” sentence. Touching a platform that’s been returned to the pool and deactivated achieves nothing, and since it’s a Unity object, the check has to be != null, not ?..

Rotating rotationalOffset to 180 is another route, but it applies to everyone standing on that platform. In multiplayer, or when an enemy is up there too, IgnoreCollision is the right one.

Where and Why You’d Use It

PlatformEffector2D is the default for any 2D game that needs platforms you jump up onto. It beats hand-rolling the “cast and toggle the collider” idea the clipping started from — instead of a check every frame, the physics engine decides from the normal at contact time.

One Floating Platform, Configured

What to fill in the Inspector and why:

ItemValueReason
Collider 2D → Used By EffectorCheckedWithout it the effector never picks up the collider
Use One WayCheckedThe one-way behavior itself
Surface Arc180 (default)Blocks the upper half
Use One Way GroupingCheckedRequired if the player has more than one collider
Use Collider MaskAs neededWhen each platform should target its own layers
Use Side FrictionOffStops hanging on the side

I’d make leaving Use One Way Grouping on the default. The symptom of not having it is “it gets stuck sometimes,” which is unusually hard to track down later.

When to Touch the Angle

Map symptoms to a single value and this component stops being “a checkbox that either works or doesn’t.”

Where Not to Use It

Wrapping Up

It’s a feature that ends in a checkbox, so posts about it tend to end at the checkbox too. But this component exposes eight values in the Inspector, and half of them map precisely onto “works fine, then occasionally weird” symptoms. Knowing where to look when it goes odd after you turn it on lasts longer than knowing how to turn it on.


References

The starting point for this post was Oniboogie — [Unity2D] 공중 플랫폼(일방통행 플랫폼) 만들기 (2023-01-25). I followed its path to the component as written, then checked what that component actually evaluates against the current manual and Scripting Reference. Quotes from it are my translations.


Share this post:

Previous Post
What You Pay For With Claude Code Isn't MCP
Next Post
On Android, escape Is the Back Button