Unityで弾痕を壁の子オブジェクトにする理由|Transformの親子関係を理解しよう
前回は、Raycastを使って、
クリックした場所を調べる
↓
弾痕を生成する
↓
3秒後に削除する
という処理を作りました。
弾痕の生成部分は、次のようになっていました。
GameObject bulletHole =
Instantiate(
bulletHolePrefab,
position,
rotation
);
Destroy(
bulletHole,
lifeTime
);
これで、静止している壁なら問題なく見えます。
しかし、
壁が動いたらどうなるでしょうか?
- 1. 動く壁で試してみる
- 2. 弾痕と壁は別々のGameObject
- 3. 本当は弾痕も壁の一部として動いてほしい
- 4. Transformの親子関係
- 5. 親を動かすと子も動く
- 6. RaycastHitには当たったColliderが入っている
- 7. 弾痕の親を撃った壁にする
- 8. Hierarchyはこうなる
- 9. 壁を動かしてみる
- 10. コードを追加する
- 11. 完成コード
- 12. SetParent()は何をしている?
- 13. なぜTransformを渡すの?
- 14. GameObjectの親子関係に見えても、本体はTransform
- 15. Transformにはワールド座標とローカル座標がある
- 16. 親から見てどこにいるか
- 17. SetParentすると位置が変わらないのはなぜ?
- 18. worldPositionStaysとは?
- 19. falseにするとどうなる?
- 20. 初心者向けにはtrueを書いてもよい
- 21. 回転する壁でもついてくる
- 22. ドアの例だと特に分かりやすい
- 23. ただしColliderのGameObjectが本当に親でよいか?
- 24. 壁のScaleがX・Y・Zで違うときは注意
- 25. Destroyは子になっても使える
- 26. 親が先にDestroyされた場合は?
- 27. 今回の完成コード
- 28. 新しく追加されたのは1行だけ
- 29. 親子関係は単なるHierarchy整理ではない
- 30. 今回の流れを整理する
- 31. まとめ
動く壁で試してみる
たとえば、次のような壁があるとします。
● 弾痕
│
┌────────┐
│ Wall │
└────────┘
壁を撃つと、表面に弾痕が付きます。
ここまでは問題ありません。
ところが、そのWallを右へ移動すると、
● 弾痕
┌────────┐
│ Wall │
└────────┘
→
となってしまいます。
壁だけ動いて、弾痕がその場に残ってしまう
のです。
なぜでしょうか。
弾痕と壁は別々のGameObject
Hierarchyを見ると分かりやすくなります。
弾痕を普通に Instantiate() すると、
Hierarchy
Wall
BulletHole
のようになります。
WallとBulletHoleは、
どちらも独立したGameObject
です。
したがって、
wall.transform.position += ...
としてWallを動かしても、
BulletHoleには何の関係もありません。
Unityから見れば、
Wallが動いただけ
だからです。
本当は弾痕も壁の一部として動いてほしい
弾痕は見た目として、
壁に貼り付いているもの
です。
そのため、Wallが動けばBulletHoleも一緒に動いてほしいでしょう。
イメージとしては、
Wall
└ BulletHole
という関係にしたいわけです。
Unityでは、これを
親子関係
で表現できます。

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
になりました。

壁を動かしてみる
今度は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の違い →












ディスカッション
コメント一覧
まだ、コメントがありません