Unityで物理-RigidBody2Dと当たり判定

この記事のUnityのバージョン
Unity 2019.1.0f2

はじめに

Unityに触れてみたけど物理って難しいです。
単位が大きい・小さいと当たり判定が突き抜けたり思いもよらぬ挙動をしたりします。
なのでシンプルなところからテストしていきます。

2Dを描画する環境を用意しよう

Unityを立ち上げ新規プロジェクトを作成しようとすると、2Dか3Dかを聞かれます。
今回は2Dだから2Dでいいんじゃない?と2Dを選択する方は分かってる人なので問題ないでしょう。
ここでは3Dを選択してしまった人のための対処を記述しましょう。

まず、Hierarchy上のDictionaryLightを削除します。(Deleteキーで削除)

Main Cameraを選択して、以下のように値を変更します。

変更点

ClearFlags : Skybox -> SolidColor
Projection : Perspective -> Orthographic
Size : 5 -> 20
Clipping Plane Far : 1000 -> 100

ClearFlags:
画面の最背面の表示をどうするか、ということでデフォルトであるスカイボックスではなく単色塗りつぶしに設定します。

Projection:
投影(画面表示)の設定です。
Perspectiveは近いものほど視野が狭く物がより大きく、遠いものほど視野が広く物がより小さく見える、現実に近い表示になります。
Orthographicは、近くても遠くても視野は変わらず、どんなに遠くても同じサイズで表示されます。

Size:
表示するXYの大きさです。これはひとまず適当で構いません。後々自分の好きな値に調整してください。

Clipping Plane:
カメラが写す距離で、NearからFarの距離まで表示します。
カメラの向きによりますが、そこからの表示範囲です。

実は3Dも2Dも根本でやってることは変わらないです。なのでカメラをOrthographicにして3Dのモデルを表示すれば距離でサイズは変わりません。

今回の2Dでは遠近感は必要ないため、Orthographicにします。

球を用意しよう、の際にまず以下をやってください

まずは球を重力で落とすようにしたい、ということで
球のスプライトを用意します。
SpriteRenderer(3D空間上に2Dのスプライト表示)とuGUI(Unityの2DのUIシステム)の二種類の表示方法があります。
今回は色々な解像度に対応できるよう、uGUIを選択しました。
(が、結局結論から後ほどSpriteRendererに変えることに…)
uGUIのスプライトはImageと呼ばれているので、それを作成します。

GameObject->UI->Image

すると、Imageを作ったはずなのにいくつか他のものも作成されています。
CanvasとEventSystemです。

EventSystemはuGUIのボタンを押下した際などの面倒なことを管理してくれるものです。
そこまで知る必要の無い方は、おまじないだと思って無視しましょう。
※ボタンを実装したからといってボタンを押下した際の処理を実装するのは実装者です。

Canvasは、以下に書いてあるとおり、UI が配置、描画される抽象的な領域で、
すべての uGUI は Canvas コンポーネントがアタッチされたゲームオブジェクトの子でないといけません。

CanvasのGameObject内にアタッチされたCanvasScalerを上記のように書き換えます。

UI Scale Mode : Constant Pixel Size -> Scale With Screen Size

Refelence Resolution : 800×600 -> 720×1280

Screen Match Mode : Match Width Or Height -> Expand

上記設定は縦画面想定なので、横画面にしたければxとyの値を入れ替えてください。

なぜ設定をするかというと、AndroidやiPhone,iPadなど沢山の解像度があります。

その沢山の解像度に合わせてUIを構築していくのは非常に時間がかかるので
パラメータと値を設定することである程度見た目を
保ちつつある程度自動化してしまおうじゃないか、というものです。

このCanvasScalerに関して、興味があれば調べて
自分の一番良い解像度対応(iPhoneのノッチのような画面も増えているのでSafeArea設定に関しても)を
見つけるということもしてみてください。

球を用意しよう

Hierarchy上のImageを選択して、InspectorウィンドウでImageからballへ名前を変えてあげましょう。
座標は中央に出したいので、x,yをそれぞれゼロに設定しましょう。

また、Inspectorウィンドウ内にImage (Script)というスクリプトがアタッチ(付与)されています。
そのSourceImageを”Knob”にしてみてください。球の表示になります。

更に下のほうにAddComponentというボタンがあるので、
“RigidBody2d”と”CircleCollider2d”をアタッチし、
“CircleCollider2d”のRadiusは50にしてください。

ここまで来たら一度再生を押してみましょう。
球がゆーっくりと落ちていきます。
おめでとうございます、これが重力です。

えっ、落ちるのが遅すぎる?理由は後で書きましょう。

地面を用意しよう

同じように地面を用意しましょう。まずは球のように地面を作成し、”stage”という名前にしましょう。

変更点

Pos Y : -400
Width : 600
Rotation Z : -15

更にstageにBoxCollider2dをアタッチし
BoxCollider2dのSizeをWidthとHeightと同じ600×100にします。

この状態で再生してみましょう。
stageにballが当たったあと、コロコロと転がって落ちていきます。

重力、遅くない?


とても現実の重力に似ていると思えないほど重力の効きが弱いと感じます。
これは、何故でしょうか。
2Dの球の大きさが大きいためそう思えてしまうのでしょうか?
つまり、球のサイズはw100xh100で我々が直径1mくらいかな?と思っていたとしても、
直径100mの巨大な球が落ちているのを撮影している、と考えると妥当な速度かもしれません。

原因はなんでしょう。
とりあえず原因は置いておいて、対処としては以下が思い浮かびます。

1.重力を変えてしまう
2.時間経過を変えてしまう
3.各種サイズを調整する


1.の重力を変える方法は一般的で、よく解決策として提示されていますが、2つあります。

ProjectSettings -> Physics2d より重力を直接-9.8から-50くらいに変える方法(すべてに影響を与える)

RigidBody2DのGravityScale を 1 から 5くらいに変える方法(そのオブジェクトのみに影響を与える)

どちらも良いのですが、数値を大きくすると調整しにくくなる可能性もあるのでひとまず置いておきましょう。

2.時間経過を変えてしまう
あまりやらないほうがいいでしょう。根本的な解決になっていません。


3.各種サイズを調整する
見た目を小さくしてカメラでの描画範囲も狭めれば
移動量が大きく見えるのではないか、というアプローチです。

残念ながらこちらも現実的な解決案ではありません。

そもそもで原因は何?というところを結論で記述します。

結論

そもそも物理演算でScale値が変わるようなことはしないほうが安牌です。
これは、次のJoint周りで思いがけない現象に出会ったことで判明します。
シンプルに、SpriteRendererでグローバル座標系に配置してしまったほうが良いです。
どうしてもuGUI上で表現したい、というのであれば CanvasのScreenspace-OverlayをやめてScreenSpace-Cameraなどにするか
1.の重力の設定を変えるかが望ましいです。

理由は以下の通りです。

高校物理で、速度\(v\)と位置\(x\)の関係、加速度\(a\)と速度\(v\)の関係は以下のものだという話がありました。
ここで、\( v_0 \)は初速度です。

\begin{eqnarray} x &=& \frac{1}{2}a t^2 + v_0 t \\ v &=& at + v_0 \\ \end{eqnarray}
計算としては、初速度は0なので計算しない、1秒後にどの辺りにY座標が移動していないといけないのか、を計算すると
-9.8×0.5=-4.9m Y軸を落ちていないといけません。

そのため、以下の設定をします。

Edit -> Project Settings -> Time -> Fixed Timestep : 0.02 -> 0.0166667

これは物理演算を何秒ごとに行うか、の設定です。
秒間60回は割り切れない(0.0166666…)ので近しい値にしましょう。

そして以下クラスを生成して適当なゲームオブジェクトに貼り付けて実行してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Stop after 60 frame updating 
public class Frame60Stop : MonoBehaviour
{
    public int frame;
    public int fixedupdateframe;

    private void Awake()
    {
        Application.targetFrameRate = 60; //60FPSに設定
    }

    // Update is called once per frame
    void Update()
    {
        frame++;
    }
    // Update is called once per frame
    void FixedUpdate()
    {
        fixedupdateframe++;
        if (fixedupdateframe >= 60)
        {
            if (UnityEditor.EditorApplication.isPlaying)
            {
                UnityEditor.EditorApplication.isPaused = true;
            }
        }
    }
}

普通に実行するとFrame値が35辺りでFixedUpdateFrame値が60になります。
(少なくとも私のMacBookProでは)
これは、起動時にかかった負荷分のFixedUpdateが呼ばれるためだと思われます。
そのため、まずPauseを押下し、Playしたあと動作が安定してからPauseを外してください。
そうするとFrame値が60を示したときにPauseします。

さて、その時点でボールはどの位置にいるでしょうか。
私の環境では、-12.13514の位置にいます。
-4.9mとはかけ離れた大きい値になってしまっています。

じつはuGUIではCanvasにスケールをかけて多解像度対応をしているため、
このスケール値が物理演算後の座標値に影響を与えています。
Gameウィンドウのサイズによってスケールが変わりますが、
私の環境ではCanvasは0.4109375のスケールがかかっています。
(これもゲームウィンドウのサイズが変化すれば動的に変わるため固定ではありません)
その倍率をそのまま-12.13514に掛け合わせると-4.98674094となり、
確かに1秒間でグローバル座標で−4.9の移動をしていることがわかります。
つまりスケールなどに影響されない、グローバルな座標の移動をしているということになります。

では、-4.9という値はどの辺りに存在するのが良いのでしょうか。

まずはカメラの位置を0,0,-1辺りにしてください。
BoxでもSpriteRendererでも良いので、0,-4.9,0 の地点に配置しましょう。
更に、SpriteRendererを(1,0,0)の位置から重力落下させてみましょう。

画像左が再生前、右が再生後1秒経ったものです。
左から
・グローバル座標(-1, −4.9, 0)に配置したCube、
・uGUIのスプライト(0,0,0)->(0,-12.13514,0)// 親のCanvasのScaleは0.4109375
・SpriteRendererのスプライト(1,0,0)->(1,-4.91,0)
グローバルな座標なはずなのに位置が違うのは何故…と思われるでしょう。

これは、CanvasがScreenSpace-Overlayの場合、グローバル座標がScreen Space(UI座標)ベースになっているからです。
そのため、CanvasをScreenSpace-CameraもしくはWorldSpaceにしてしまえば
グローバル座標はワールド座標になるためSpriteRendererの位置と同じ箇所から始まり同じ箇所まで落ちるようになります。

というわけでuGUIで物理をしようとした際にハマったことを記述しました。
次回のJointでuGUIで物理やめよう…となるんですけどね。

実際は物理以外で悩んでいたりも。

何が正解か、それを理解せずに実装してもゲームは作れます。
そのアプローチは間違っていません。
ユーザにとって挙動が正しいかは二の次で、面白いかが重要です。
しかし、挙動が変だ、直さないと。となったときにどこから見直そう?となったときに
ここまではしっかりチェックしているから大丈夫、という認識が無いとどツボにハマります。

今回の場合は重力が遅い原因はグローバル座標が違う、
という部分に引っかかっていたわけで
物理ムズカシイ…というところの前でなんでだろう?となったわけです。

次回はJoint周りのことを記述します。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください