ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】判定重複時に物理演算で吹っ飛ぶ挙動の制御について

time 2017/04/19

というわけで、ブログリニューアルの作業やリアルであれこれあったため、かなりひさびさの投稿になります。
今回のお題は「コライダー同士が重なっているときの物理演算」に関するお話です。

コライダー(判定)にコライダーがめり込んで重なってしまい、Unityの物理演算が戻そうとした結果、とんでもない勢いで吹っ飛んでしまうことがあります。
他のアクションゲームのようにスーっと判定外まで移動してくれるのが理想なのですが、Unity側が「どのようにずらすか」なんて取得しようがなかったので挫折しておりました。
4.1で使い始めた頃から抱えていた私的な悩みで、当時卒業制作として提出したアクションゲームには「たまに吹っ飛ぶのでキャラクター同士で近寄らないで下さい」と意味不明な注意文句を添えることになってしまいました。

Unityが5.6になり、(ある程度) 制御ができそうなのでご紹介します。
CharacterController付きプレイヤーキャラクターのめり込み時を想定していたため、本記事ではCharacterControllerCharacterMotorを使っていますが、RigidBodyでも似たようなロジックで実装できるかと思います。

処理の流れは以下のような感じです。
1. 自分のコライダーと重なっているコライダーがあるか判定
2. 物理演算を一時的に無効化
3. 重複解消のためのずらす方向・距離を計算
4. Transformを通してオブジェクトを移動

物理演算パラメータの見直し

そもそも「めり込むこと自体がイレギュラー」なわけで、めり込んでしまう原因にもよりますが、関連するパラメータをいじることで解決する可能性があります。
このあたりはテラシュールブログさんの記事で詳しく解説されております。

とはいえ、どう頑張って調整しても一瞬だけ入り込んでしまうことはあります。また、マシンスペックによっては処理落ちが発生する可能性も否めません。その際に「吹っ飛んだのでゲームをリセットしてください」では何の解決にもならず、前に作った卒業制作と同レベルです。ここはめり込んでしまうことをある程度想定して、その挙動を制御するほうがよいでしょう。

コライダーが重なっているか判定

当然ですが、まずコライダー同士が重なっているか判定する必要があります。
プレイヤークラスのUpdate()内で、Physics.OverlapCapsule()を使用します。

CharacterController cc = this.GetComponent<CharacterController>();
CharacterMotor charMotor = this.GetComponent<CharacterMotor>();

Collider[] overLapColliders = Physics.OverlapCapsule(
    transform.position + new Vector3(-cc.center.x, (cc.center.y - cc.height/2) + cc.radius, -cc.center.z),
    transform.position + new Vector3(-cc.center.x, (cc.center.y + cc.height/2) - cc.radius, -cc.center.z),
    cc.radius,
    LayerMask.GetMask("floor") | LayerMask.GetMask("Enemy")
);

if (0 < overLapColliders.Length) {
    foreach (Collider c in overLapColliders) {
        Debug.Log(c.gameObject);
    }
}

説明のためUpdate()内でGetComponent()していますが、実際はStart()内で取得したほうがよいです。「そんなこと分かってるよ!」という方は適当に脳内補完してください。

OverlapCapsule()の第1~2引数はカプセルの開始・終了位置にある球の中心を指定します。カプセル型コライダーのheightは半径込みの全体の高さなので注意が必要です。また主体となる軸が異なっているのか、X軸とZ軸が逆方向に動いてしまうので、上の例ではマイナスを指定しています。
ちなみにCharacterControllerColliderを継承しているそうなので、カプセルコライダーとして必要なパラメータはそのまま取得できます。但しCapsuleColliderから継承しているわけではないため、CapsuleCollider型として扱うことはできません。
第4引数のレイヤーマスクはゲームによると思うのでお好みで。

重なっているコライダーがある場合、Collider型の配列が返ってきます。
ifforeachで上手く制御してあげましょう。

物理演算を一時的に無効化する

コライダーが重なっていた場合、物理演算を一時的に無効化します。これが抜けているとUnityの物理エンジンが作用し、盛大に吹っ飛ばしてしまうので注意します。
あれこれ試してみましたが、CharacterMotorの場合はenabledでコンポーネントを無効化するしかなさそうです。RigidBodyの場合はIsKinematicを有効にすればいけそう。

if (0 < overLapColliders.Length) {
    charMotor.canControl = false;
    charMotor.enabled = false;

    foreach (Collider c in overLapColliders) {
        Debug.Log(c.gameObject);
    }
} else if (!charMotor.enabled) {
    charMotor.canControl = true;
    charMotor.enabled = true;
}

ifの分岐を少し修正します。
上の例ではCharacterMotor.canControlfalseにしました。
ずらしている間に移動入力を受け付けると面倒なことになりそうなので・・・。

重なっている場合にオブジェクトの位置をずらす

めり込んでしまったコライダーからキャラクターのコライダーが重ならないように移動させる必要があります。但し物理演算を無効化しているので、Transformを直接いじる必要があります。
この時の移動方向や距離がなかなかどうしてだったのですが、5.6からPhysics.ComputePenetration()というジャストな関数が登場しました!

if (0 < overLapColliders.Length) {
    charMotor.canControl = false;
    charMotor.enabled = false;

    Vector3 direction;
    float distance;

    foreach (Collider c in overLapColliders) {
        bool penetrat = Physics.ComputePenetration (
            cc, transform.position, transform.rotation,
            c.GetComponent<Collider>(), c.transform.position, c.transform.rotation,
            out direction, out distance
        );

        if (penetrat) {
            transform.position = transform.position + (direction * distance);
            charMotor.movement.velocity = Vector3.zero;
        }
    }
} else if (!charMotor.enabled) {
    charMotor.canControl = true;
    charMotor.enabled = true;
}

引数は多いですが、特に難しい部分はありません。先程あげた通り、CharacterControllerCollider型を継承しているので、第1引数にそのまま指定できます。
第7引数にずらす方向、第8引数にずらす距離が格納されます。このずらす距離と方向はUnityが計算した「重複しているコライダーから抜け出すための最短距離」になるようです。これをtransform.positionに足してあげれば、プレイヤーの位置は重複したコライダーから抜け出した場所に移動するはずです。

移動後はCharacterMotormovement.velocityに零ベクトルを入れ、どの方向にも速度が働かない状態にしておきます。

上手くいかない点

実際にテストしてみると以下のような問題があり、実用的なレベルまでもっていくのはまだ難しい印象です。
結構いい線まではいくので、用途を選べば使えそうではあります。

プレイヤーキャラのscaleを1以外にしている場合

キャラクターオブジェクトのscaleが1以外になっている場合、ずらす際の挙動が歪になります。
OverlapCapsule()に関してはコライダーの高さや半径にscaleの値を掛けることで想定通りの動作になりましたが、ComputePenetration()directiondistanceで0が返ってくることがあり、重なっているのに移動しなくなります。CharacterControllerを渡しているのがまずいかと思い、動的にCapsuleColliderを追加して渡すことも試しましたが、挙動は変わりませんでした。

ずらす方向がよろしくないことがある

ComputePenetration()で計算された方向がアクションゲーム的にまずい場合があります。Y軸にマイナスが渡されると地面にめり込んでしまう他、2Dアクションであれば手前や奥にずれてもNGです。
ComputePenetration()で返された方向を適当に変えるにしても、「Y軸にマイナスだけ」とか来ると詰んでしまいます。

あとがき

そんなわけで、中途半端ではありますが判定重複時の物理演算制御について考えてみました。
そもそも今までは「自前でずらす方向を計算」なんてできなかったので、その手の関数が追加されただけでもやりやすさは変わってきます。
まだゲームにすらなっていない状態でつめてもしょうがないので、また暇があったら考えてみます。

余談ですが、職場の新入社員の方がUnityを知っていたりして、話相手が増えてちょっとうれしい今日この頃です。
もっとUnity人口が増えるといいですね!

down

コメントする