UniRxを使って上下からすり抜けられる足場を作ってみた

# スマブラの戦場とかにあるやつ！

#### 完成品
![kansei.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3802709/6afcbbfe-8763-5b9e-2db4-5de69be6e34b.gif)

3Dです。ジャンプで下からすり抜けて乗って、下入力ですり抜けて落下できる足場。UniRxのおかげでそこそこ綺麗に実装できたと思うので備忘録として置いておきます。

# 使用したパッケージ
UniRx : イベント処理に

https://github.com/neuecc/UniRx

NaughtyAttributes : インスペクタ表示の拡張に

https://github.com/dbrizov/NaughtyAttributes

# 仕組み
プレイヤーが足場に乗れるのは
**<font color="blue">青いコライダー</font>に触れていない<br>かつ<br><font color="red">赤いコライダー</font>に触れている**
状態のとき。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3802709/80b76fcf-e2ba-4222-599e-0ee9697dc231.png)

つまり、赤青それぞれのコライダーの衝突情報によって、プレイヤーが衝突するかどうかを切り替えてあげればいいのです。

Textが1の時に足場と衝突している。
![kansei_debug.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3802709/1c7c7af0-e016-d69b-6c0f-eda05538c5e7.gif)

今回はGameObjectのレイヤーを追加し、足場のみに当たらない状態を作っています。普段はお互い衝突せず、条件に合致したときだけレイヤーを変更して衝突させます。

#### 追加したレイヤー
* PassThroughPlatform : すり抜け足場
* TriggerPassThroughPlatform : すり抜け足場の赤と青のコライダー

:::note
ProjectSettings > Physics > LayerCollisionMatrix で PassThroughPlatform と Default の衝突を無効にする。
:::
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3802709/4e168e85-4c9a-552d-53c0-bbbd538222e8.png)

これでDefaultレイヤーとだけ衝突しないレイヤーができました。
プレイヤーのゲームオブジェクトのレイヤーをDefaultとPassThroughPlatform間で切り替えることで、足場と当たったり当たらなかったりします。

# ヒエラルキー
こんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3802709/ab1cf0d3-b36c-88fc-c5ee-e5078ad6044b.png)
Playerのタグを`Player`に設定。
PassThroughPlatformオブジェクトのレイヤーを、`PassThroughPlatform`に設定。
PassThroughPlatformの子オブジェクト2つのレイヤーを、`TriggerPassThroughPlatform`に設定。

# スクリプト
一部だけ乗せようと思ったのですが、上手く説明できる気がしなかったのでそのまま乗せてます。

### プレイヤーと足場との衝突を操作するインターフェース
足場からプレイヤーのレイヤーを変更するためのインターフェースを用意しました。

<details><summary>IPlatformColliderController.cs</summary>

```cs:IPlatformCollisionController.cs
/// <summary>
/// 足場用のコライダーを操作するインターフェース
/// </summary>
interface IPlatformColliderController
{
    /// <summary>
    /// 足場の衝突数を増やす
    /// </summary>
    public void IncrementPlatformCollisionCount();
    /// <summary>
    /// 足場の衝突数を減らす
    /// </summary>
    public void DecrementPlatformCollisionCount();
}
```
</details>

### プレイヤー
Spaceでジャンプ操作、Sですり抜け操作ができます。
上記のインターフェースを実装して、外からレイヤーを操作できるようにしています。プレイヤーと足場のレイヤーを同一にすることで衝突するようになり、足場に乗ることができます。足場のコライダーとの衝突数をカウントしておき、衝突しなくなったら自身でレイヤーを戻します。

<details><summary>Player.cs</summary>

```cs:Player.cs
using UnityEngine;
using NaughtyAttributes;

/// <summary>
/// プレイヤー
/// </summary>
public class Player : MonoBehaviour, IPlatformColliderController
{
    /// <summary>
    /// ジャンプ力
    /// </summary>
    [SerializeField]
    private float _jumpPower = 10f;
    /// <summary>
    /// 足場をすり抜ける入力
    /// </summary>
    private bool _isPassThroughPlatformInput = false;
    /// <summary>
    /// 衝突している足場のコライダーの数
    /// </summary>
    private int _platformColliderCount;
    /// <summary>
    /// 足場との衝突が有効なレイヤー
    /// </summary>
    [SerializeField, Layer]
    private int _enablePlatformCollisionLayer;
    /// <summary>
    /// 足場との衝突が無効なレイヤー
    /// </summary>
    [SerializeField, Layer]
    private int _disablePlatformCollisionLayer;
    /// <summary>
    /// リジッドボディ
    /// </summary>
    private Rigidbody _rigidbody;

    private void Awake()
    {
        _rigidbody = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        // ジャンプ
        if (Input.GetKeyDown(KeyCode.Space))
        {
            _rigidbody.AddForce(Vector3.up * _jumpPower, ForceMode.Impulse);
        }
        // 足場をすり抜ける
        if(Input.GetKeyDown(KeyCode.S))
        {
            _isPassThroughPlatformInput = true;
            // レイヤー切替
            gameObject.layer = _disablePlatformCollisionLayer;
        }
        else
        {
            _isPassThroughPlatformInput = false;
        }
    }

    public void IncrementPlatformCollisionCount()
    {
        // 長押しで連続で足場をすり抜けるため
        // すり抜け入力がある場合は有効にしない
        if (_isPassThroughPlatformInput)
            return;

        // 足場のコリジョンに触れている数を増やす
        _platformColliderCount++;
        // レイヤー切替
        gameObject.layer = _enablePlatformCollisionLayer;
    }

    public void DecrementPlatformCollisionCount()
    {
        if(_platformColliderCount > 0)
        {
            // 足場のコリジョンに触れている数を減らす
            _platformColliderCount--;
        }

        // 足場のコリジョンに1つ以上触れているなら
        if (_platformColliderCount > 0)
            return;

        // レイヤー切替
        gameObject.layer = _disablePlatformCollisionLayer;
    }
}
```
</details>

### すり抜け足場のコライダー
赤と青のやつ。すり抜け足場の子オブジェクト2つにアタッチされています。
ReactiveCollectionに衝突しているプレイヤーのコライダーを追加します。ReactiveCollectionは複数のReactivePropertyをリストのように扱えるクラスです。(初めて使った)

<details><summary>PassThroughPlatformCollider.cs</summary>

```cs:PassThroughPlatformCollider.cs
using UnityEngine;
using UniRx;
using NaughtyAttributes;

/// <summary>
/// すり抜け足場のコライダー
/// </summary>
public class PassThroughPlatformCollider : MonoBehaviour
{
    /// <summary>
    /// プレイヤーのタグ
    /// </summary>
    [SerializeField, Tag]
    private string _playerTag;
    /// <summary>
    /// プレイヤーのコライダーコレクション
    /// </summary>
    private ReactiveCollection<Collider> _playerColliders = new ReactiveCollection<Collider>();
    public IReadOnlyReactiveCollection<Collider> PlayerCollider => _playerColliders;

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag(_playerTag))
        {
            // 進入したコライダーを追加
            _playerColliders.Add(other);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag(_playerTag))
        {
            // 退出したコライダーを削除
            _playerColliders.Remove(other);
        }
    }
}

```
</details>

### すり抜け足場
上記のスクリプトのReactiveCollectionを参照し、衝突したプレイヤーのコライダーの追加と削除を監視しています。**<font color="blue">青いコライダー</font>に触れていないかつ<font color="red">赤いコライダー</font>に触れていたら**プレイヤーのインターフェースを経由して、衝突カウントを増やします。

<details><summary>PassThroughPlatform.cs</summary>

``` cs:PassThroughPlatform.cs
using UnityEngine;
using UniRx;

/// <summary>
/// すり抜け足場
/// </summary>
public class PassThroughPlatform : MonoBehaviour
{
    /// <summary>
    /// 上側のコライダー
    /// </summary>
    private PassThroughPlatformCollider _topCollider;
    /// <summary>
    /// 下側のコライダー
    /// </summary>
    private PassThroughPlatformCollider _bottomCollider;
    /// <summary>
    /// プレイヤーのコライダーを操作するためのインターフェース
    /// </summary>
    private IPlatformColliderController _platformColliderController;

    private void Awake()
    {
        // コライダーの取得
        _topCollider = transform.GetChild(0).GetComponent<PassThroughPlatformCollider>();
        _bottomCollider = transform.GetChild(1).GetComponent<PassThroughPlatformCollider>();

        // 上側のコライダーへの進入
        _topCollider.PlayerCollider.ObserveAdd()
            .Subscribe(_ => TryEnableCollision())
            .AddTo(gameObject);
        // 下側のコライダーからの進入
        _bottomCollider.PlayerCollider.ObserveAdd()
            .Subscribe(_ => TryEnableCollision())
            .AddTo(gameObject);
        // 下側のコライダーからの退出
        _bottomCollider.PlayerCollider.ObserveRemove()
            .Subscribe(_ => TryEnableCollision())
            .AddTo(gameObject);

        // 上側のコライダーからの退出
        _topCollider.PlayerCollider.ObserveRemove()
            .Subscribe(_ => TryDisableCollision())
            .AddTo(gameObject);
    }

    /// <summary>
    /// 衝突を有効にしようとする
    /// </summary>
    private void TryEnableCollision()
    {
        // 下のコライダーにプレイヤーが衝突していないかつ上のコライダーにプレイヤーが衝突している
        if (_bottomCollider.PlayerCollider.Count == 0 && _topCollider.PlayerCollider.Count > 0)
        {
            if (_platformColliderController == null)
            {
                // プレイヤーからIPlatformCollisionControllerを取得する
                _platformColliderController = _topCollider.PlayerCollider[0].GetComponent<IPlatformColliderController>();
            }

            if (_platformColliderController != null)
            {
                // すり抜け足場コライダーを有効にする
                _platformColliderController.IncrementPlatformCollisionCount();
            }
        }
    }

    /// <summary>
    /// 衝突を無効にしようとする
    /// </summary>
    private void TryDisableCollision()
    {
        if (null == _platformColliderController)
            return;

        // すり抜け足場コライダーを無効にする
        _platformColliderController.DecrementPlatformCollisionCount();

        if (_topCollider.PlayerCollider.Count == 0)
        {
            _platformColliderController = null;
        }
    }
}
```
</details>

# 完成！
プレイヤーが足場をすり抜ける処理で「足場オブジェクトの当たり判定消すのはなんか違うよね」って思ったところから考え始めました。

実際のプロジェクトでは複数の足場が隣あっていたり、プレイヤーのコライダーが2つ以上だったりと条件が複雑ですが、今回の仕組みで対応できるようになっています。

このコードでは足場に乗る際、プレイヤーと足場のレイヤーを同一のものにしています。接地判定とかする場合不都合になりそうなので多分変えた方がいいです。

下側のコライダーを大きくしているのは、真横から進入した場合もすり抜けるようにするため。

# まとめ
UniRxを導入してから、複雑な処理がとっても作りやすくなりました。楽しい。でもリアクティブスパゲティにだけは注意。