【Unity】反射するレーザーを作ろう！(2D)

## 記事について

:::note warn
この記事にコードの改良、機能の追加をしている完成版の記事があります。
:::

完成版の記事はこちら

https://qiita.com/TanakaNeko/items/72fe1de18765655fb31c


## はじめに

こんな感じの反射するレーザーのようなものを作った。
簡易的なものだが、自分用のメモとして一旦記事にしようと思う。

![reflectLazer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/b3c0e34b-cea7-fe80-b538-bd4613e9b5c7.png)

## 概要
ボタンを押すと壁で反射するレーザーが表示される。
レーザーが出る方向と反射数はプログラム側から変更可能。（今後、ゲーム画面から操作できるようにしたい）
レーザーはRaycastHit2Dで壁との当たり判定を行い、LineRendererで見た目を作る。

## 作り方
### 1. シーンにオブジェクトを配置
BoxCollider2DをアタッチしたSquareを壁として設置する。レーザーはこの壁に反射するので、もっと複雑な形で設置してもよい。今回はシンプルに四方を囲っただけ。

![wall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/3a2f8ee3-264c-da18-2904-a3aae9e84608.png)

次に、ボタンを設置。今回はボタンを使ったが、c#scriptの関数が実行できればなんでもよい。このボタンからレーザーが出るように作っていく。

### 2. レーザーのプレハブを用意
レーザーをまとめて管理する用の空のプレハブ「LazerMother」とLineRendererがアタッチされている「Lazer」の二つのプレハブを用意します。LineRendererをアタッチしたプレハブが１つあればレーザーの挙動を実装できますが、途中で色を変えることができなかった（グラデーションしかできなかった）ので、反射のたびに別のオブジェクトとしてレーザーを生成するようにしています。

![スクリーンショット 2024-04-07 102622.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/3e0c3edb-9597-1ebd-7582-b14db482644e.png)

### 3. スクリプトを作る
まず、ボタンを押したときに動く「LazerStarter.cs」とレーザーの挙動を制御する「Lazer.cs」を用意する。

```c#:LazerStarter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LazerStarter : MonoBehaviour
{
  [SerializeField] private GameObject lazerMother;
  private Vector2 INITIAL_DIRECTION = new Vector2(1,1); //最初の方向

  public void Click(){
    GameObject newLazer = Instantiate(lazer, this.gameObject.transform.position, Quaternion.identity);
    newLazer.GetComponent<Lazer>().creat(newLazer.transform.position, INITIAL_DIRECTION, 0);
  }
}

```

```c#:Lazer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Lazer : MonoBehaviour
{
  [SerializeField] private GameObject lazerPrefab;
  const float MAX_DISTANCE = 23.0f;  //箱の対角線の長さ（レーザーの最大長を設定）
  const int REFLECT_NUM = 3;  //反射回数
  private GameObject preWall;
  
  /**
   * レーザー生成関数
   * @param {Vector3} origin - レーザーの原点
   * @param {Vector3} direction - レーザーの方向
   * @param {int} n - 何本目か(0はじまり)
  **/
  public void creat(Vector3 origin, Vector2 direction, int n){
    if(n == 0)preWall = null;
    if(n < REFLECT_NUM + 1){
      GameObject lazerChild = Instantiate(lazerPrefab, this.transform);
      lazerChild.name = "lazer_" + n;
      LineRenderer line = lazerChild.GetComponent<LineRenderer>();
      RaycastHit2D[] hits = Physics2D.RaycastAll(origin, direction, MAX_DISTANCE);

      foreach(RaycastHit2D hit in hits){
        if (hit.collider.gameObject != preWall)
        {
          Vector3 endPos = hit.point;
          Vector2 reflectDirection = Vector2.Reflect(direction, hit.normal);
          
          line.SetPosition(0, origin);
          line.SetPosition(1, endPos);
          preWall = hit.collider.gameObject;

          creat(endPos, reflectDirection, ++n);
          
          break;
        }
      }    
    } 
  }
}
```

スクリプトが用意できたら、「LazerStarter.cs」をボタンにアタッチし、ボタンを押したときに関数Clickが実行されるように設定する。また、GameObjectのLazerMotherには先ほど作ったプレハブのLazerMotherを入れる。
「Lazer.cs」はLazerMotherにアタッチし、GameObjectのLazerPrefabには先ほど作ったプレハブのLazerを入れる。

![button.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/3995795e-4a66-29d8-0826-43a31ac9e47c.png) ![2024-04-07 124234.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/3138d084-0fa6-e75d-6fe5-8dcb98dc4265.png)

これで完成。ボタンを押すとレーザーが発射されるはず。LineRendererの設定によっては見えない場合があるので適宜マテリアルなどを割り当てる必要がある。

## 解説
### LazerStarter.cs
```c#:LazerStarter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LazerStarter : MonoBehaviour
{
  [SerializeField] private GameObject lazerMother;
  private Vector2 INITIAL_DIRECTION = new Vector2(1,1); //最初の方向

  public void Click(){
    GameObject newLazer = Instantiate(lazer, this.gameObject.transform.position, Quaternion.identity);
    newLazer.GetComponent<Lazer>().creat(newLazer.transform.position, INITIAL_DIRECTION, 0);
  }
}

```

ボタンを押したときに関数Click()を実行。プレハブのLazerMotherをインスタンス化し、Lazer.csの関数creat()を実行する。creatは引数にレーザーの初期位置と方向、何反射目かをもつので今回はボタンの位置からベクトル(1,1)の方向に0反射目のレーザーを生成するように値を渡している。

### Lazer.cs
```c#:Lazer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Lazer : MonoBehaviour
{
  [SerializeField] private GameObject lazerPrefab;
  const float MAX_DISTANCE = 23.0f;  //箱の対角線の長さ（レーザーの最大長を設定）
  const int REFLECT_NUM = 3;  //反射回数
  private GameObject preWall;
  
  /**
   * レーザー生成関数
   * @param {Vector3} origin - レーザーの原点
   * @param {Vector3} direction - レーザーの方向
   * @param {int} n - 何本目か(0はじまり)
  **/
  public void creat(Vector3 origin, Vector2 direction, int n){
    if(n == 0)preWall = null;
    if(n < REFLECT_NUM + 1){
      GameObject lazerChild = Instantiate(lazerPrefab, this.transform);
      lazerChild.name = "lazer_" + n;
      LineRenderer line = lazerChild.GetComponent<LineRenderer>();
      RaycastHit2D[] hits = Physics2D.RaycastAll(origin, direction, MAX_DISTANCE);

      foreach(RaycastHit2D hit in hits){
        if (hit.collider.gameObject != preWall)
        {
          Vector3 endPos = hit.point;
          Vector2 reflectDirection = Vector2.Reflect(direction, hit.normal);
          
          line.SetPosition(0, origin);
          line.SetPosition(1, endPos);
          preWall = hit.collider.gameObject;

          creat(endPos, reflectDirection, ++n);
          
          break;
        }
      }    
    } 
  }
}
```

レーザー生成関数のcreat()を持っており、反射するたびにcreat()を再帰的に呼び出すことで反射を実現している。概要にもある通り壁との当たり判定はRaycastHit2D、レーザーの見た目はLineRendererで実現している。

変数とその役割は以下の通り。
| 変数名 | 型 | 役割 |
|:-:|:-:|:-:|
| MAX_DISTANCE  | float | レーザーが取りうる線分の最大長を入れる（長めでいい）  |
| REFLECT_NUM  | int |反射してほしい回数（任意の値） |
| preWall | GameObject | 反射した壁への当たり判定を無視するために壁の情報を入れるために使用|

### RaycastHit2D
公式のドキュメント： https://docs.unity3d.com/ja/2019.4/ScriptReference/RaycastHit2D.html

公式のドキュメントを読むと
> 2D 物理挙動の Raycast により検知されたオブジェクトについて返される情報

と書いてある。つまり、Raycastによる当たり判定で得られる値をいろいろ持っているらしい。どんな値をもっているかも公式のドキュメントで見ることができる。

ちなみに、Raycastは見えない直線を指定された方向に伸ばし、その直線上のオブジェクトに対して当たり判定をおこなうもの（と自分は解釈している）。

LineRendererで線を描画するためには始点と終点の座標が必要なので、それをRaycast2Dを利用して求める。
```c#
RaycastHit2D[] hits = Physics2D.RaycastAll(origin, direction, MAX_DISTANCE);
```
この一行で、原点から指定した方向へ延びるRaycastによって当たり判定を受けたオブジェクトをすべて配列hitsにいれる。

```c#
foreach(RaycastHit2D hit in hits){
```
foreachで配列の要素を取り出しながら繰り返しの処理を行う。
```c#
if (hit.collider.gameObject != preWall)
```
最初にpreWallと一致しないオブジェクトに当たったとき、その点を反射点とする。RaycastHit2Dでは衝突したオブジェクトを直接入手できないので一度colliderを入手し、そこからgameObjectを入手している。
```c#
Vector3 endPos = hit.point;
```
この一行でRaycastHit2Dから衝突した座標を得る。この座標を反射点とする。つまり一本目のレーザーの終点で二本目のレーザーの始点。

![lazer_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/54e0318d-542e-8f6c-fade-35c3be0cc9ce.jpeg)

![lazer_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/633a793b-8328-a632-5614-c7d1578a3ef9.jpeg)

ここまでで一本目の始点、終点と二本目の始点は得られている。
二本目の終点を知るためには反射方向に新たなRayを伸ばす必要がある。Vector2の関数にReflect(Vector2,Vector2)があり、面に向かうベクトルと面の法線ベクトルを引数に与えると反射方向のベクトルを返してくれる。この関数を利用して新たなRayの方向ベクトルを得る。
Vector2の公式ドキュメント： https://learn.microsoft.com/ja-jp/dotnet/api/system.numerics.vector2?view=net-8.0

```c#
Vector2 reflectDirection = Vector2.Reflect(direction, hit.normal);
```
入射するベクトルは引数で受け取った方向ベクトル。壁面の法線ベクトルはRaycastHit2Dから.normalで得ることができる。

### 再帰処理
```c#
line.SetPosition(0, origin);
line.SetPosition(1, endPos);
```
LineRendererで一本目の線を描画する。

```c#
preWall = hit.collider.gameObject;

creat(endPos, reflectDirection, ++n);

break;
```
衝突した壁をpreWallに入れる。
一本目の終点（反射点）を始点、求めた反射方向のベクトル、n+1を引数としてcreat()を再帰的に呼び出す。
呼び出したらforeachからは抜ける（break）。

## おわりに
今回は、反射する機能だけを作った。見た目やさらに複雑な機能はまたの機会にしようと思う。LineRendererがなかなか手ごわそうな予感...

### 続き

https://qiita.com/TanakaNeko/items/15bdcf442ce9b28ce2f1