Unityで弾痕を壁の子オブジェクトにする理由|Transformの親子関係を理解しよう

広告

前回は、Raycastを使って、

クリックした場所を調べる
↓
弾痕を生成する
↓
3秒後に削除する

という処理を作りました。

弾痕の生成部分は、次のようになっていました。

GameObject bulletHole =
    Instantiate(
        bulletHolePrefab,
        position,
        rotation
    );

Destroy(
    bulletHole,
    lifeTime
);

これで、静止している壁なら問題なく見えます。

しかし、

壁が動いたらどうなるでしょうか?

動く壁で試してみる

たとえば、次のような壁があるとします。

        ● 弾痕
        │
    ┌────────┐
    │  Wall  │
    └────────┘

壁を撃つと、表面に弾痕が付きます。

ここまでは問題ありません。

ところが、そのWallを右へ移動すると、

● 弾痕

              ┌────────┐
              │  Wall  │
              └────────┘
                    →

となってしまいます。

壁だけ動いて、弾痕がその場に残ってしまう

のです。

なぜでしょうか。

弾痕と壁は別々のGameObject

Hierarchyを見ると分かりやすくなります。

弾痕を普通に Instantiate() すると、

Hierarchy

Wall

BulletHole

のようになります。

WallとBulletHoleは、

どちらも独立したGameObject

です。

したがって、

wall.transform.position += ...

としてWallを動かしても、

BulletHoleには何の関係もありません。

Unityから見れば、

Wallが動いただけ

だからです。

本当は弾痕も壁の一部として動いてほしい

弾痕は見た目として、

壁に貼り付いているもの

です。

そのため、Wallが動けばBulletHoleも一緒に動いてほしいでしょう。

イメージとしては、

Wall
└ BulletHole

という関係にしたいわけです。

Unityでは、これを

親子関係

で表現できます。

弾痕を壁の子にしていない場合と、子にした場合のHierarchyの違い

Transformの親子関係

UnityのGameObjectは、Transformを使って親子関係を作れます。

たとえばHierarchyで、

Car
└ Wheel

となっていれば、

Car が親、

Wheel が子

です。

Carを動かすと、

Car
└ Wheel

全体が一緒に移動します。

同じ考え方で、

Wall
└ BulletHole

とすれば、

Wallを動かしたとき、

BulletHoleも一緒に動きます。

親を動かすと子も動く

親子関係を簡単に考えると、

親
↓
子がついてくる

です。

たとえば、

Player
└ Weapon

なら、

Playerが動けばWeaponも動きます。

Car
└ Camera

なら、

Carが動けばCameraも動きます。

そして今回なら、

Wall
└ BulletHole

なので、

Wallが動けばBulletHoleも一緒に動きます。

RaycastHitには当たったColliderが入っている

ここで、これまで学んだ

hit.collider

が再び役に立ちます。

hit.collider は、

Rayが当たったCollider

でした。

さらに、

hit.collider.transform

とすれば、

当たったGameObjectのTransform

を取得できます。

つまり、

hit
↓
collider
↓
transform

とたどれば、

撃った壁のTransform

を取得できます。

弾痕の親を撃った壁にする

生成した弾痕は、

GameObject bulletHole =
    Instantiate(
        bulletHolePrefab,
        position,
        rotation
    );

で取得できています。

この弾痕のTransformは、

bulletHole.transform

です。

一方、撃った壁のTransformは、

hit.collider.transform

です。

そこで、

bulletHole.transform.SetParent(
    hit.collider.transform
);

とします。

これで、

BulletHoleの親を、Rayが当たったGameObjectにする

ことができます。

Unity 6の Transform.SetParent() はTransformの親を設定するためのAPIです。

Hierarchyはこうなる

撃つ前は、

Wall

だけです。

弾痕を生成したあと、

bulletHole.transform.SetParent(
    hit.collider.transform
);

を実行すると、

Hierarchyは、

Wall
└ BulletHole(Clone)

となります。

これでBulletHoleは、

Wallの子GameObject

になりました。

壁をクリックして弾痕を生成し、壁の子にして3秒後に消すまでの流れ

壁を動かしてみる

今度はWallを右へ動かしてみます。

撃った直後

    ●
┌────────┐
│  Wall  │
└────────┘

Wallを移動すると、

                ●
            ┌────────┐
            │  Wall  │
            └────────┘
                  →

となります。

BulletHoleもWallの子なので、

壁と一緒についてきます。

これで見た目として自然になります。

壁を移動させたとき、子にしていない弾痕は残り、子にした弾痕は一緒についてくる

コードを追加する

前回のコードに、

bulletHole.transform.SetParent(
    hit.collider.transform
);

を追加します。

GameObject bulletHole =
    Instantiate(
        bulletHolePrefab,
        position,
        rotation
    );

bulletHole.transform.SetParent(
    hit.collider.transform
);

Destroy(
    bulletHole,
    lifeTime
);

これだけです。

完成コード

using UnityEngine;
using UnityEngine.InputSystem;

public class BulletHoleShooter : MonoBehaviour
{
    [SerializeField]
    GameObject bulletHolePrefab;

    [SerializeField]
    float lifeTime = 3f;

    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            Ray ray =
                Camera.main.ScreenPointToRay(
                    Mouse.current.position.value
                );

            RaycastHit hit;

            if (Physics.Raycast(ray, out hit))
            {
                Vector3 position =
                    hit.point
                    + hit.normal * 0.01f;

                Quaternion rotation =
                    Quaternion.LookRotation(
                        hit.normal
                    );

                GameObject bulletHole =
                    Instantiate(
                        bulletHolePrefab,
                        position,
                        rotation
                    );

                bulletHole.transform.SetParent(
                    hit.collider.transform
                );

                Destroy(
                    bulletHole,
                    lifeTime
                );
            }
        }
    }
}

SetParent()は何をしている?

ここだけを見ると、

bulletHole.transform.SetParent(
    hit.collider.transform
);

です。

左側は、

bulletHole.transform

なので、

弾痕のTransform

です。

そして、

SetParent(...)

なので、

親を設定する

という意味です。

引数は、

hit.collider.transform

なので、

Rayが当たったGameObjectのTransformを親にする

となります。

日本語にすると、

今作った弾痕を、撃った壁の子にしてください

です。

なぜTransformを渡すの?

SetParent() に渡すのは、

GameObject

ではなく、

Transform

です。

だから、

hit.collider.gameObject

ではなく、

hit.collider.transform

を使います。

親子関係は、

Transform同士の関係

として管理されているからです。

GameObjectの親子関係に見えても、本体はTransform

Hierarchyを見ると、

Wall
└ BulletHole

なので、

GameObject同士が親子になっているように見えます。

しかしコードでは、

bulletHole.transform.SetParent(
    hit.collider.transform
);

です。

つまり実際には、

WallのTransform
        ↑
BulletHoleのTransform

という関係を作っています。

これはUnityのTransformを理解するうえで重要です。

Transformにはワールド座標とローカル座標がある

ここで親子関係を作ると、

もう一つ重要な考え方が出てきます。

それが、

ワールド座標
ローカル座標

です。

たとえばWallが、

World Position
X = 10

にあるとします。

BulletHoleがWallから、

右へ0.5

の場所にあるとします。

すると、BulletHoleの位置は、

ワールド座標

X = 10.5

ですが、

親であるWallから見れば、

ローカル座標

X = 0.5

と考えることができます。

親から見てどこにいるか

親子関係では、

世界全体から見た位置

だけではなく、

親から見た位置

が重要になります。

たとえば、

Wall
Position X = 10

    └ BulletHole
      Local Position X = 0.5

なら、

BulletHoleのワールド位置は、

10 + 0.5
=
10.5

あたりになります。

Wallが、

X = 20

へ移動すると、

BulletHoleのLocal Positionは、

0.5

のままです。

そのためワールド位置は、

20 + 0.5
=
20.5

へ変わります。

これが、

親を動かすと子もついてくる

仕組みの基本です。

SetParentすると位置が変わらないのはなぜ?

ここで実際に試すと、

bulletHole.transform.SetParent(
    hit.collider.transform
);

を実行しても、

弾痕が突然別の場所へ移動しないことに気づくでしょう。

これは SetParent() の第2引数を省略した場合、

worldPositionStays = true

として動作するためです。

つまり、

親は変えるけれど、現在のワールド上の見た目の位置や向きは保つ

という動作になります。

今回の弾痕には、とても都合がよい動作です。

worldPositionStaysとは?

SetParent() には、

SetParent(
    Transform parent,
    bool worldPositionStays
)

という書き方もあります。

たとえば、

bulletHole.transform.SetParent(
    hit.collider.transform,
    true
);

と書けます。

true なら、

現在のワールド位置・回転・スケールを維持するように、親から見た相対値を調整

します。

今回はこちらが適しています。

falseにするとどうなる?

一方、

bulletHole.transform.SetParent(
    hit.collider.transform,
    false
);

とすると意味が変わります。

false では、

それまでのローカル側のTransform値を基準に、新しい親の下へ入れる

動作になります。

今回のように、

まずワールド上の正しい位置に弾痕を生成
↓
そのあと壁の子にする

という流れなら、

SetParent(
    hit.collider.transform,
    true
);

と考える方が分かりやすいでしょう。

なお、

SetParent(hit.collider.transform);

だけでもデフォルトが true なので同じ考え方になります。

初心者向けにはtrueを書いてもよい

教材では、あえて、

bulletHole.transform.SetParent(
    hit.collider.transform,
    true
);

と書いてもよいと思います。

なぜなら、

true
=
今のワールド位置を保って親にする

という意図がコード上にはっきり見えるからです。

最終的には、

bulletHole.transform.SetParent(
    hit.collider.transform
);

でも十分ですが、

学習段階では第2引数を見せる価値があります。

回転する壁でもついてくる

親子関係の効果は、移動だけではありません。

たとえばWallが回転すれば、

Wall
└ BulletHole

なので、

BulletHoleも一緒に回転します。

つまり、

親の位置
親の回転
親のスケール

の影響を子が受けます。

そのため、

動く壁
回転する扉
動く車
昇降する床

などに付いた弾痕も自然についていきます。

壁を回転させたとき、子にしていない弾痕は残り、子にした弾痕は一緒に回転する

ドアの例だと特に分かりやすい

たとえば開閉するDoorを撃ったとします。

子にしていない場合、

閉じたDoor

● 弾痕
│Door│

Doorを開くと、

● ← 弾痕だけ残る


        / Door

となります。

これは不自然です。

一方、

Door
└ BulletHole

にしておけば、

Doorが回転すると、

        /●
       / Door

弾痕も一緒に回ります。

これなら、

本当にドアへ弾痕が付いている

ように見えます。

ただしColliderのGameObjectが本当に親でよいか?

ここから少し発展です。

今回、

hit.collider.transform

を親にしました。

単純なCubeやWallなら問題ありません。

しかし、GameObjectの構造が、

MovingWall
└ Visual
   └ BoxCollider

のようになっている場合があります。

この場合、

hit.collider.transform

は、

BoxColliderが付いている子GameObject

になるかもしれません。

つまり、

どのTransformを親にするべきか

は、ゲームオブジェクトの構成によって変わります。

初心者向け実習では、

Wall
├ Mesh Renderer
└ Box Collider

のように同じGameObjectにしておくと分かりやすいでしょう。

壁のScaleがX・Y・Zで違うときは注意

ここも少し発展です。

子オブジェクトは、親の

位置
回転
スケール

の影響を受けます。

たとえば薄い壁を作るために、Cubeを

Scale (10, 5, 0.1)

のように、X・Y・Zで違う値にしている場合があります。

このように、方向ごとに倍率が違うスケールを、非一様なスケールと呼びます。

親がこのようなスケールだと、子の弾痕もその影響を受けて、引き伸ばされたり、斜めに歪んで見えたりすることがあります。

特に、弾痕を壁の子にしたあとで壁を回転させると、目立ちやすくなります。

初心者向けの実習では、

壁のScaleは (1, 1, 1) にする
または、X・Y・Zが同じ値の倍率にする
(たとえば Cube を (2, 2, 2) にするなど)

ようにしておくと、弾痕の見た目が崩れにくくなります。

どうしても薄い壁にしたい場合は、弾痕が歪んでいないかをSceneビューで確認しながら調整しましょう。

Destroyは子になっても使える

BulletHoleをWallの子にしても、

Destroy(
    bulletHole,
    lifeTime
);

はそのまま使えます。

つまり、

Wall
└ BulletHole

となっていても、

3秒後には、

Wall

となります。

親であるWallが消えるわけではありません。

Destroy() に渡しているのは、

bulletHole

だからです。

親が先にDestroyされた場合は?

逆に、BulletHoleが残っている間に、

親のWallそのものが削除された場合はどうでしょうか。

親GameObjectを破棄すると、

その階層下にある子GameObjectも一緒に破棄されます。

これは、

Wall
└ BulletHole

が一つの階層になっているためです。

ゲームとしても、

壁が完全に消えたのに弾痕だけ空中に残る

という状況を防げます。

今回の完成コード

学習用として true を明示した形にすると、次のようになります。

using UnityEngine;
using UnityEngine.InputSystem;

public class BulletHoleShooter : MonoBehaviour
{
    [SerializeField]
    GameObject bulletHolePrefab;

    [SerializeField]
    float lifeTime = 3f;

    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            Ray ray =
                Camera.main.ScreenPointToRay(
                    Mouse.current.position.value
                );

            RaycastHit hit;

            if (Physics.Raycast(ray, out hit))
            {
                Vector3 position =
                    hit.point
                    + hit.normal * 0.01f;

                Quaternion rotation =
                    Quaternion.LookRotation(
                        hit.normal
                    );

                GameObject bulletHole =
                    Instantiate(
                        bulletHolePrefab,
                        position,
                        rotation
                    );

                bulletHole.transform.SetParent(
                    hit.collider.transform,
                    true
                );

                Destroy(
                    bulletHole,
                    lifeTime
                );
            }
        }
    }
}

新しく追加されたのは1行だけ

前回との差は、ほぼここだけです。

bulletHole.transform.SetParent(
    hit.collider.transform,
    true
);

しかし、この1行によって、

独立した弾痕

だったものが、

壁に属する弾痕

になります。

ゲームの見た目だけでなく、

GameObject同士の関係をコードで表現している

ところが重要です。

親子関係は単なるHierarchy整理ではない

Unityを始めたころは、Hierarchyの親子関係を、

GameObjectを整理するフォルダのようなもの

と考えてしまうことがあります。

しかし実際には、それだけではありません。

親子関係には、

位置
回転
スケール

の継承関係があります。

つまり、

親子関係
=
Transformの関係

なのです。

今回の弾痕は、その性質が非常に分かりやすく現れる例です。

今回の流れを整理する

今回までの処理を全部つなげると、

マウスクリック
↓
Camera.main
↓
ScreenPointToRay()
↓
Ray
↓
Physics.Raycast()
↓
RaycastHit
↓
hit.point
位置を決める
↓
hit.normal
向きを決める
↓
Instantiate()
弾痕を生成
↓
SetParent()
撃った壁の子にする
↓
Destroy()
一定時間後に削除

となります。

まとめ

弾痕をただ生成すると、

Wall

BulletHole

という独立したGameObjectになります。

そのためWallが動いても、

BulletHoleはついてきません。

そこで、

bulletHole.transform.SetParent(
    hit.collider.transform,
    true
);

とします。

すると、

Wall
└ BulletHole

という親子関係になり、

Wallが、

移動
回転
スケール変更

されると、BulletHoleもその影響を受けます。

今回のポイントを一言でまとめると、

弾痕が「壁に付いている」ことを、
Transformの親子関係で表現する

ということです。

これによって、

Hierarchyの親子関係は単なる整理ではなく、3D空間でオブジェクト同士の位置関係を表す仕組み

だということが見えてきます。


【シリーズ】Raycastで弾痕を作りながら学ぶ(5/9)
← 前の記事:クリックした場所に弾痕を残す(Destroy) | シリーズ目次 | 次の記事:positionとlocalPositionの違い →

訪問数 3 回, 今日の訪問数 3回

広告

Physics,Unity

Posted by hidepon