んななのゲーム開発備忘録

ゲーム制作の進捗と開発メモなど掲載しています。

Unityの衝突判定を数値化して不安定な接触を安定化する(UniRx編)

f:id:nyanya-nnana:20171201002933p:plain:w500
Unityの衝突判定を数値化して不安定な接触を安定化する
キャラクターが接地しているか判定するときに思った通りにならなくて困ったことはありませんか? 今回の記事ではそんな時に役立つ、接触が不安定な場合でも安定して接触を判定する処理について説明します。

本記事で紹介する接触判定クラスはGithub (https://github.com/nyanya-nnana/CollisionDetector)にソースをあげてあります。 よければ使用してみてください。使用するにはUniRxが必要です。UniRxを使用しないバージョンはコチラで紹介しています。 もし要望等あればお気軽に。

※ローパスフィルタの数式のc_{\rm{raw}}c_{\rm{prev}}が逆になっていたので修正しました(2017/12/03)

手っ取り早く使いたいという方は次の手順をどうぞ。

  • UniRxをAsset Store (https://www.assetstore.unity3d.com/jp/#!/content/17276)からインポート
  • CollisionDetectorコンポーネントを接触判定したいGameObjectにアタッチ
  • 接触対象のタグをチェックする場合はCheckTargetTagをtrueにして対象のタグをTargetTagで設定
  • 接触判定を使用したいスクリプトでCollisionDetectorをGetComponent
  • OnCollisionEnterSmoothedAsObservableでOnCollisionEnterでしたい処理を記述
  • OnCollisionStaySmoothedAsObservableでOnCollisionStayでしたい処理を記述
  • OnCollisionExitSmoothedAsObservableでOnCollisionExitでしたい処理を記述

目次はコチラ

接触判定が不安定になる要因

あるゲームオブジェクトに別のゲームオブジェクトが接触が始まった時にはMonobehaviourのOnCollisionEnterを使ってその時の処理を書くと思います。 接触中の処理も同様にOnCollisionStayを使うと思います。

でも、接触してそうなのにOnCollisionEnterとOnCollisionExitを繰り返していて安定しない、そんなときがあると思います。

原因はゲームオブジェクトが衝突が繰り返されるからです。相手のゲームオブジェクトに近づいて、跳ね返って、力がかかってるからまた近づいて、これの繰り返しが原因です。

f:id:nyanya-nnana:20171202201315p:plain:w400
揺れてる?
これは物理現象なので仕方ありません。 なので、接地の判定をするときは接地判定用のis TriggerなColliderをアタッチしてそれを使って通常は判定します。 こんな感じに。
f:id:nyanya-nnana:20171202201323p:plain:w400
トリガーがあれば揺れてもだいじょうぶ。でも立ってないときは...?
このようにTriggerのColliderを用意すると簡単に安定して接地の判定ができます。 でも、キャラクターが攻撃されて寝転んだ時など、足から接地しない場合はうまく判定できなくて、工夫しないといけません。

今回紹介したいのは、そういうことをしなくても衝突判定にちょっとフィルタをはさんであげればOnCollisionのイベントだけで案外安定してとれるんだよってお話です。

接触判定を安定させる原理

この項は数式が苦手な人は読み飛ばしちゃっても大丈夫です。 CollisionDetectorの実装はコチラ、使用方法はコチラ

ローパスフィルタ

接触判定の安定には1次のローパスフィルタというものを使用します。 式で表すとこんな感じ。

c = (1-a)c_{\rm{raw}} + ac_{\rm{prev}}

フィルタ処理した現在の接触判定が c、1サイクル前の接触判定が c_{\rm{prev}}、フィルタ処理していない生の接触判定が c_{\rm{raw}}です。

c_{\rm{raw}}と、c_{\rm{prev}}のどちらを優先するか、定数aを使って決定します。 aが大きいほど前の値を優先するのでc_{\rm{raw}}が激しく変化していてもあまり変化しません。

定数aはカットオフ周波数f_\rm{c}とサンプリング周波数T_\rm{s}というものを使って次のように表現します。

a = e^{-f_\rm{c}T_\rm{s}}

カットオフ周波数f_c以上の速い動きに影響されなくなるって認識で概ね大丈夫です。 サンプリング周波数にはTime.fixedDeltaTimeが入ります。 具体的な値としてf_\rm{c} = 5Hzを入れて接触判定をとってみると次のようになります。


CollisionDetector - ローパスフィルタの効果

カットオフ周波数が低いと生の接触判定c_{\rm{raw}}(緑色)が激しく変化しても水色の接触判定がゆるやかに追従しています。 これを使って接触判定の変化をマイルドにします。

ヒステリシス

ローパスフィルタを使うと接触判定の急激な変化をおさえられることがわかりました。 でも、それだけだと安定した接触判定は実現できません。 それはなぜかというと、次のようなことが起こるからです(f_\rm{c} = 30Hz)。


CollisionDetector - ヒステリシスなし

接触していないときを0、しているときを1として、0.6以上で接触していると判断した場合の接触判定をしていますが、 0.6を上回ったり下回ったりして判定が安定しません。 そこでヒステリシスというものを用います。

f:id:nyanya-nnana:20171202201333p:plain:w400
ヒステリシスの説明図
「一度接触していると判定されたら、逆の判定をする条件を厳しくする」ということをするために用います。 判定変化の条件が、trueになるときと、falseになるときで変えてしまえば判定が簡単には変化しなくなります。 これを用いて接触判定の上限を0.6、下限を0.1に設定すると次のように接触判定が安定します。


CollisionDetector - ヒステリシスあり

CollisionDetectorクラスの実装

CollisionDetectorクラスの実装にはUniRxというライブラリを使用しています。

これを使うとイベントが起きたときの処理が簡潔に書けます。

CollisionDetectorクラスのソースコードGithubのコチラをご覧ください。 長くなるので使用方法を知りたい方はコチラをどうぞ。

UniRxではイベントを通知するためのストリームのソースを作る方法がいくつかあります(コチラが参考になります)が、今回はSubjectとIObservableを使ってイベント通知を実装します。

// Subject:イベント発行
private Subject<Collision> _onCollisionEnterSubject = new Subject<Collision>();
private Subject<Collision> _onCollisionStaySbuject = new Subject<Collision>();
private Subject<Collision> _onCollisionExitSubject = new Subject<Collision>();
// IObservable:イベント購読
public IObservable<Collision> OnCollisionEnterSmoothedAsObservable
{
    get { return _onCollisionEnterSubject; }
}
public IObservable<Collision> OnCollisionStaySmoothedAsObservable{
    get { return _onCollisionStaySbuject; }
}
public IObservable<Collision> OnCollisionExitSmoothedAsObservable
{
    get { return _onCollisionExitSubject; }
}

SubjectはOnNextというメソッドを持っていてこれを実行すると対応するIObservableにイベントのメッセージが通知されます。

生の衝突判定とその時の衝突情報はOnCollisionEnter、Stay、Exitの処理中に更新して、これを使って衝突判定を数値化します。

// OnCollisionEnter
this.OnCollisionEnterAsObservable()
.Subscribe(other =>
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _onCollisionRaw = true;
        _otherOnEnter = other;
        _otherOnStay = other;
    }
});
// OnCollisionStay
this.OnCollisionStayAsObservable()
.Subscribe(other =>
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _otherOnStay = other;
    }
});
// OnCollisionExit
this.OnCollisionExitAsObservable()
.Subscribe(other =>
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _onCollisionRaw = false;
        _otherOnExit = other;
    }
});

OnCollisionEnterAsObservableはOnCollisionEnterをUniRx的に使えるようにしたもので、SubScribeのあとに続くブロックの中で行いたい処理を書きます。

Subscribe内ではラムダ式というものを使っていて、最初にotherと名前をつけた変数に衝突の情報が入っているようです。

衝突状態を1、していない状態を0としてFixedUpdate内で衝突判定を数値化します。_filterConstantはローパスフィルタの定数aにあたります。

// 衝突しているかどうかを数値化。_filterConstantが大きいほど1サイクル前の状態を優先
_onCollisionFloat = (1.0f - _filterConstant) * onCollisionCurrent + _filterConstant * _onCollisionFloat;

次のように衝突判定の変化にヒステリシスを持たせて、

if (onCollisionPrevious)
{
    // 1つ前のサイクルで衝突している場合、下限閾値を下回るまで状態を変化させない
    if (_onCollisionFloat <= detectionThresholdLower)
        _onCollision = false;
    else
        _onCollision = true;
}
else
{
    // 1つ前のサイクルで衝突していない場合、上限閾値を上回るまで状態を変化させない
    if (_onCollisionFloat >= detectionThresholdUpper)
        _onCollision = true;
    else
        _onCollision = false;
}

OnCollisionEnter、Stay、ExitになったときにOnNextを使って衝突情報を持ったイベントを通知します。

// OnCollisionEnterか、Stayか、Exitか判定
if (_onCollision == onCollisionPrevious)
{
    _onCollisionEnter = false;
    _onCollisionExit = false;
}
else
{
    if (_onCollision)
    {
        // OnNextでイベント通知
        _onCollisionEnter = true;
        _onCollisionEnterSubject.OnNext(_otherOnEnter);
    }
    else
    {
        // OnNextでイベント通知
        _onCollisionExit = true;
        _onCollisionExitSubject.OnNext(_otherOnExit);
    }
}
_onCollisionStay = _onCollision;
// OnNextでイベント通知
if (_onCollisionStay)
    _onCollisionStaySbuject.OnNext(_otherOnStay);

以上がCollisionDetectorの主な処理です。

CollisionDetectorクラスの使用方法

UniRxの導入

CollisionDetectorはUniRxというライブラリを使用しています。 まずはそちらをUnity Asset StoreからインポートもしくはGithubから導入してください。

CollisionDetectorの設定

導入が終わったら、衝突判定をしたいGameObjectにCollisionDetectorをAddComponentしてください。 CollisionDetectorの設定項目は次の通りです。

Check Ohter Tag

衝突したGameObjectのタグをチェックするかしないか設定できます。

Target Tag

Check Other Tagがtrueの場合はここにTargetのタグを設定します。

Cut Off Frequency

衝突判定の安定度。低いほど判定の変化がゆっくりになります。

Detection Threshold Upper/Lower

衝突判定の閾値。2つの差が大きいほど変化しにくくなります。

衝突判定の利用

使用したいスクリプト内で

using NnanaSoft.CollisionDetector;

を宣言してください。 あとはStartかAwake内でGetComponentして

    var SmoothedCollision = GetComponent<CollisionDetector>();

その中で行いたい処理を記述すればおっけーです。

// OnCollisionEnter
SmoothedCollision.OnCollisionEnterSmoothedAsObservable
    .Subscribe(other =>
    {
        // ここに処理を記述
        Debug.Log("OnCollisionEnterSmoothed");
    });
// OnCollisionStay
SmoothedCollision.OnCollisionStaySmoothedAsObservable
    .Subscribe(other =>
    {
        // ここに処理を記述
        Debug.Log("OnCollisionStaySmoothed");
    });
// OnCollisionExit
SmoothedCollision.OnCollisionEnterSmoothedAsObservable
    .Subscribe(other =>
    {
        // ここに処理を記述
        Debug.Log("OnCollisionExitSmoothed");
    });

使用するとこんな感じになります。


CollisionDetector - デモ