Unityでクリックした場所に弾痕を残す|RaycastとDestroyを使ってみよう

広告

前回は、

hit.point

と、

hit.normal

を使って、

クリックした場所にマーカーを置き、その面の向きに合わせる

ところまで作りました。

今回は、その仕組みを少しゲームらしくしてみます。

今回作るのは、

クリックした場所に弾痕を残し、3秒後に自動的に消す

という処理です。

今回の完成イメージ

たとえば壁をクリックすると、

Camera
   ●
    \
     \ Ray
      \
       × ← hit.point
      [●] 弾痕
       │
       │ 壁
       │

クリックした場所に弾痕が表示されます。

さらに、

クリック
↓
弾痕を生成
↓
3秒間表示
↓
弾痕を削除

という動きにします。

前回のコードを確認する

前回は、次のようなコードでした。

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
        );

    Instantiate(
        markerPrefab,
        position,
        rotation
    );
}

この中では、

hit.point

で、

弾痕を置く位置

を決めています。

そして、

hit.normal

で、

弾痕をどちらへ向けるか

を決めています。RaycastHit.point はRayがColliderに当たったワールド座標、normal は当たった面の法線です。

クリック、Rayの作成、当たり判定までの手順

MarkerをBulletHoleに変える

今回は、前回のMarkerを

BulletHole

という弾痕Prefabに置き換えて考えます。

たとえば、

BulletHole
└ 弾痕画像

のようなPrefabを作っておきます。

弾痕Prefabの作り方:透明背景のPNG画像をPlaneに貼ってPrefabにする

スクリプト側では、

[SerializeField]
GameObject bulletHolePrefab;

とします。

まず弾痕を生成する

Rayが当たったら、

Instantiate(
    bulletHolePrefab,
    position,
    rotation
);

とすれば弾痕を生成できます。

コードは、

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

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

    Instantiate(
        bulletHolePrefab,
        position,
        rotation
    );
}

となります。

これで、

クリックした面に弾痕を貼る

ところまでは前回とほぼ同じです。

でも弾痕が増え続ける

このままゲームを続けると、

●
●
●
●
●
●
●
●

と、クリックするたびに弾痕が増えていきます。

小規模な実習ではすぐ問題にならなくても、

100個
500個
1000個

と生成し続ける設計は、あまりよくありません。

そこで今回は、

生成した弾痕を3秒後に消す

ようにしてみます。

Destroy()を使う

Unityには、

Destroy()

があります。

たとえば、

Destroy(gameObject);

とすると、指定したGameObjectを削除できます。

さらに、

Destroy(gameObject, 3f);

と書くこともできます。

この第2引数は、

何秒後に削除するか

です。

したがって、

Destroy(gameObject, 3f);

なら、

このGameObjectを3秒後に削除してください

という意味になります。Unity 6では Destroy(Object obj, float t) の t が削除までの遅延時間です。

でも、どのGameObjectをDestroyする?

ここで問題があります。

先ほどは、

Instantiate(
    bulletHolePrefab,
    position,
    rotation
);

とだけ書きました。

これでは、

今生成した弾痕

を後から指定するための変数がありません。

そこで、

Instantiate() の戻り値を受け取ります。

Instantiateした結果を変数に入れる

次のように書きます。

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

これで、

bulletHole

という変数が、

今回生成された弾痕GameObject

を指します。

イメージとしては、

bulletHolePrefab
      ↓
 Instantiate()
      ↓
新しい弾痕を生成
      ↓
 bulletHole

です。

prefabと生成物は別物

ここは重要です。

bulletHolePrefab

は、

これから生成するときの元になるPrefab

です。

一方、

bulletHole

は、

実際にシーンへ生成されたGameObject

です。

つまり、

bulletHolePrefab
      ↓ 設計図

Instantiate()

      ↓

bulletHole
      ↓ 実際に作られたもの

という関係です。

生成した弾痕を3秒後に削除する

生成したGameObjectを変数で受け取れたので、

Destroy(
    bulletHole,
    3f
);

と書けます。

まとめると、

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

Destroy(
    bulletHole,
    3f
);

です。

これで、

Instantiate
↓
弾痕を生成
↓
bulletHole変数で覚える
↓
Destroy(bulletHole, 3f)
↓
3秒後に削除

となります。

弾痕が生成された直後、1秒後、3秒後に自動的に消えるまでの流れ

完成コード

using UnityEngine;
using UnityEngine.InputSystem;

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

    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
                    );

                Destroy(
                    bulletHole,
                    3f
                );
            }
        }
    }
}

コードを順番に追ってみよう

まず、

if (Mouse.current.leftButton.wasPressedThisFrame)

で、

クリックされた?

を調べます。

次に、

Ray ray =
    Camera.main.ScreenPointToRay(
        Mouse.current.position.value
    );

で、

Camera
↓
マウス位置
↓
Ray

を作ります。

Raycastで壁を調べる

次に、

RaycastHit hit;

if (Physics.Raycast(ray, out hit))

で、

Ray
↓
Collider
↓
当たった?

を調べます。

当たった場合、hit の中に、

hit.point
hit.normal
hit.collider
hit.distance

などの情報が入ります。

弾痕を置く位置を決める

Vector3 position =
    hit.point
    + hit.normal * 0.01f;

です。

基本位置は、

hit.point

です。

しかし、そのままでは壁と弾痕の面が重なる可能性があります。

そこで、

hit.normal * 0.01f

を足して、

表面からほんの少し外側

へ移動しています。

弾痕の向きを決める

次に、

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

です。

Quaternion.LookRotation() は、指定されたforward方向へローカルZ軸を合わせる回転を作ります。

つまり、

BulletHoleのZ方向
        ↓
    hit.normal

となります。

そのため、

床なら上向き
壁なら横向き
斜面なら斜め

と、クリックした面に合わせることができます。

hit.pointに弾痕を配置し、hit.normalに合わせて向きを決める

Instantiate()の戻り値に注目

今回の新しいポイントはここです。

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

以前は、

Instantiate(...);

だけでした。

今回は、

GameObject bulletHole =

を付けています。

なぜでしょう。

その直後に、

Destroy(bulletHole, 3f);

をしたいからです。

「作る」と「その後どうするか」

この考え方はUnityで非常によく出てきます。

たとえば、

GameObject enemy =
    Instantiate(enemyPrefab);

なら、

生成した敵を、

enemy

で扱えます。

同じように、

GameObject bullet =
    Instantiate(bulletPrefab);

なら、

生成した弾を、

bullet

で扱えます。

今回も、

GameObject bulletHole =
    Instantiate(...);

としておけば、

生成した弾痕に対して、

削除する
移動する
回転させる
色を変える
Componentを取得する

といった処理を続けられます。

Destroy(bulletHole, 3f)の3fとは?

Destroy(bulletHole, 3f);

の、

3f

は、

3秒

です。

したがって、

0秒
↓
Instantiate
弾痕が出る

1秒
↓
まだある

2秒
↓
まだある

3秒
↓
Destroy
消える

という流れになります。

実際には Destroy は指定時間後に削除を予約し、Unityの更新処理との関係で実際の破棄タイミングが管理されます。

初心者の段階では、

3秒後に削除される

と理解しておけば十分です。

3秒をフィールドにするとさらに良い

ここまで理解できたら、

3f

を直接書くのではなく、

[SerializeField]
float lifeTime = 3f;

としてもよいでしょう。

そして、

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
                    );

                Destroy(
                    bulletHole,
                    lifeTime
                );
            }
        }
    }
}

これならInspectorから、

Life Time

1
3
5
10

などに変更できます。

ここまでで学んだものを整理する

この小さな実習には、かなり多くのUnityの基本が入っています。

Mouse
↓
クリックを検出

Camera.main
↓
メインカメラを取得

ScreenPointToRay()
↓
Rayを作る

Physics.Raycast()
↓
Colliderに当たったか判定

RaycastHit
↓
当たった情報を取得

hit.point
↓
位置

hit.normal
↓
向き

Instantiate()
↓
Prefabを生成

Destroy()
↓
生成したGameObjectを削除

となります。

最初の1行からかなり発展した

このシリーズの最初は、

Ray ray =
    Camera.main.ScreenPointToRay(
        Mouse.current.position.value
    );

という1行でした。

そこから、

Camera.mainとは?
↓
ScreenPointToRayとは?
↓
Physics.Raycastとは?
↓
RaycastHitとは?
↓
hit.pointとは?
↓
hit.normalとは?
↓
Instantiateとは?
↓
Destroyとは?

と知識がつながってきました。

一つのゲームらしい処理を作りながら、

それぞれのコードが何のために存在するのか

を理解できます。

まとめ

今回の重要な部分は、

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

Destroy(
    bulletHole,
    3f
);

です。

Instantiate() で、

弾痕を作る

だけではなく、

GameObject bulletHole =

として、生成したGameObjectを受け取っています。

そして、

Destroy(bulletHole, 3f);

で、

その弾痕を3秒後に削除

しています。

一言で整理すると、

Instantiate
   ↓
作る

戻り値
   ↓
作ったものを覚える

Destroy
   ↓
消す

です。

そして前回までの内容と合わせれば、

hit.point
   ↓
どこに作る?

hit.normal
   ↓
どちらを向ける?

Instantiate
   ↓
弾痕を作る

Destroy
   ↓
一定時間後に消す

となります。

これでRaycastの学習が、単なる当たり判定から、

実際のゲームで見かける処理

へ一段進みました。


【シリーズ】Raycastで弾痕を作りながら学ぶ(4/9)
← 前の記事:hit.pointとhit.normalでマーカーを置く | シリーズ目次 | 次の記事:弾痕を壁の子にする(SetParent) →

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

広告

Physics,Unity

Posted by hidepon