【Unity】反射するレーザーを作ろう！#完成

## はじめに
前回からコードを大幅に変更したので、追加した機能と合わせて完成版として記事にさせていただきます。

実装方法の解説なども改めてするので、この記事だけ読めば全部、実装できます。

読まなくても問題ないけど前回の記事はこちら

https://qiita.com/TanakaNeko/items/881ece9354a2ff498154

## 概要
以下のような壁に当たると反射するレーザーを作りました。

![lazer_move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/efc87447-b8f4-f57d-f040-3731c7261c7b.gif)

このレーザーはRaycastHit2Dで当たり判定を行い、LineRendererで描画をしています。
キーボードの矢印キーで発射方向を回転できるようにしています。

また、以下の4種類の壁も作りました。
### レーザーの色を変える壁
レーザーの色が最後に触れた壁と同じ色になります。


<img width="420" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/7ef68691-88b6-6867-d532-2d7660756336.png>

### 色が変わる壁
壁の色が最後に触れたレーザーの色になります。

![Lazer_WallCC.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/96c80c6a-6e0c-86ef-7ed8-ff2b0eae7668.gif)

### 反射しない壁
レーザーを反射しない壁です。

![LazerStop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/e9a1200e-543a-b12a-8477-3a2a36fef066.gif)

### レーザーが当たると消える壁
レーザーが当たると消えます。

![LazerWallDestroy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/c13ac432-dfce-3728-d92c-e02d07936862.gif)

## 実装
ここでは実装方法を解説します。仕様やコードの解説は後ほど。

### 1.プロジェクトの用意
UnityHubから2Dの新規プロジェクトを作成します。

![CreatProject.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/23a57377-1757-117e-c9fa-4ea3427f6c06.png)

![NewProject.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/e6f2e44c-a53c-6507-3cf2-62ad6ffd5239.png)

### 2.スクリプトの用意
４つのスクリプトを用意します。以下のURLからもダウンロードできます。
https://github.com/tanakaneko/ReflectLazer

記事書いてる途中でレーザーのスペルが間違ってることに気づいた...。
Lazerになってますが正しくはLaserです。動作には問題ありませんが、気になるようなら直してください。


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

public class LazerStarter : MonoBehaviour
{
  [SerializeField] private GameObject lazerMother;
  [SerializeField] private Vector2 INITIAL_DIRECTION = new Vector2(0.3f,1); //最初の方向
  [SerializeField] private  float ROTATE_SPEED = 0.04f;  //回転速度
  [SerializeField] private Material START_COLOR;  //最初のマテリアル
  private GameObject newLazer;
  private LazerMother lazerMotherCS;

  public void Start(){
    newLazer = Instantiate(lazerMother, this.gameObject.transform.position, Quaternion.identity);
    lazerMotherCS = newLazer.GetComponent<LazerMother>();
    lazerMotherCS.creat(newLazer.transform.position, INITIAL_DIRECTION, START_COLOR);
  }

  public void Update(){
    // 左回転
    if (Input.GetKey (KeyCode.LeftArrow)) {
      lazerMotherCS.move(lazerMotherCS.getOrigin(), Quaternion.Euler(0f, 0f, ROTATE_SPEED) * lazerMotherCS.getDirection());
    }
    // 右回転
    if (Input.GetKey (KeyCode.RightArrow)) {
      lazerMotherCS.move(lazerMotherCS.getOrigin(), Quaternion.Euler(0f, 0f, -ROTATE_SPEED) * lazerMotherCS.getDirection());
    }
  }
}
```

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

public class LazerMother : MonoBehaviour
{
  [SerializeField] private GameObject lazerPrefab;
  const float MAX_DISTANCE = 20.0f;  //箱の対角線の長さ
  [SerializeField] private int REFLECT_NUM = 5;  //反射回数

  //レーザーを動かす
  private GameObject preWall;
  private GameObject[] lazers;
  private Lazer[] lazerCSs;
  public Vector3 nowOrigin;
  public Vector2 nowDirection;

  //色変更
  private Material startMaterial;
  private Material nowMaterial;

  //レーザーを停止
  private bool isReflect;
 
  /**
   * レーザー生成関数
   * @param {Vector3} origin - レーザーの原点
   * @param {Vector3} direction - レーザーの方向
   * @param {Material} m - レーザーの最初のマテリアル
  **/
  public void creat(Vector3 origin, Vector2 direction, Material m){
    lazers = new GameObject[REFLECT_NUM + 1];
    lazerCSs = new Lazer[REFLECT_NUM + 1];
    for(int n = 0; n < REFLECT_NUM + 1; n++){
      GameObject lazerChild = Instantiate(lazerPrefab, this.transform);
      lazers[n] = lazerChild;
      lazerCSs[n] = lazerChild.GetComponent<Lazer>();
      lazerCSs[n].SetUp();
    }
    lazerCSs[0].SetMaterial(m);
    startMaterial = m;
    move(origin,direction);
  }

  /**
   * レーザー移動時に実行する関数
   * @param {Vector3} origin - レーザーの原点
   * @param {Vector3} direction - レーザーの方向
  **/
  public void move(Vector3 origin, Vector2 direction){
    preWall = null;
    nowOrigin = origin;
    nowDirection = direction;
    Vector3 startPos = origin;
    Vector2 nextDirection = direction;
    nowMaterial = startMaterial;
    isReflect = true;
    for(int i = 0; i < REFLECT_NUM + 1; i++){
      if(isReflect){
        lazers[i].SetActive(true);
      }
      else{
        lazers[i].SetActive(false);
        continue;
      }
      //衝突した壁の検知
      RaycastHit2D[] hits = Physics2D.RaycastAll(startPos, nextDirection, MAX_DISTANCE);
      foreach(RaycastHit2D hit in hits){
        GameObject hitObj = hit.collider.gameObject;
        if (hitObj != preWall){
          //lineの描画
          Vector3 endPos = hit.point;
          lazerCSs[i].SetMaterial(nowMaterial);
          lazerCSs[i].SetPosition(startPos, endPos);
          //壁の種類の判別
          if(hitObj.tag == "Wall"){
            Vector2 reflectDirection = Vector2.Reflect(nextDirection, hit.normal);
            switch(hitObj.GetComponent<Wall>().type){
              case Wall.WallType.Default:
              break;
              case Wall.WallType.LazerColorChange:
              nowMaterial = hitObj.GetComponent<Renderer>().material;
              break;
              case Wall.WallType.WallColorChange:
              hitObj.GetComponent<Wall>().SetMaterial(nowMaterial);
              break;
              case Wall.WallType.NonReflect:
              isReflect = false;
              break;
              case Wall.WallType.DestroyWall:
              Destroy(hitObj);
              break;
            }
            //次の繰り返しに引き渡す変数
            startPos = endPos;
            nextDirection = reflectDirection;
            preWall = hitObj;
          }
          break;
        }
      }
    }
  }

  //getter
  public Vector3 getOrigin(){
    return nowOrigin;
  }
  public Vector2 getDirection(){
    return nowDirection;
  }
}
```

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

public class Lazer : MonoBehaviour{
  private LineRenderer line;

  //初期設定
  public void SetUp(){
    line = gameObject.GetComponent<LineRenderer>();
  }

  //マテリアルの設定
  public void SetMaterial(Material material){
    List<Material> materials = new List<Material>(){material};
    line.SetMaterials(materials);
  }

  //レーザーの位置を設定
  public void SetPosition(Vector3 startPos, Vector3 endPos){
    line.SetPosition(0, startPos);
    line.SetPosition(1, endPos);
  }

  //レーザーのマテリアルを返す
  public Material GetMaterial(){
    return line.material;
  }
}
```

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

public class Wall : MonoBehaviour
{
  public enum WallType{  //壁の種類
    Default,
    LazerColorChange,
    WallColorChange,
    NonReflect,
    DestroyWall
  }

  public WallType type;

  //壁のマテリアルを設定
  public void SetMaterial(Material m){
    gameObject.GetComponent<Renderer>().material = m;
  }
}
```

こんな感じでスクリプトのファイルが4つできてればOK。
![Scripts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/4160461d-6cd5-a7b5-39e3-6fce7806715e.png)

### 3.プレハブの用意
プロジェクト内で使用するプレハブを用意します。
#### Wallプレハブの作成

ここでは壁のプレハブを作ります。

ヒエラルキーウィンドウで右クリックし、「2D Object」→「Sprites」→「Square」を選択し、四角のオブジェクトを作ります。オブジェクト名は任意のものに変更してください。（例では「Wall」）

<img width="420" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/007e8c1d-3f42-ea21-7460-bf4d649369f1.png>

作ったオブジェクトをヒエラルキーウィンドウからプロジェクトウィンドウにドラッグアンドドロップし、プレハブ化します。ヒエラルキーウィンドウの方のオブジェクトは使わないので削除して大丈夫です。

<img width="420" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/93784d40-dab8-a218-507d-c293da3617d7.png>

作ったプレハブをダブルクリックして、プレハブの編集モードにします。
インスペクターに先ほど作った「Wall.cs」をドロップしてスクリプトをアタッチします。

<img width="420" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/1d35286a-bdda-5fa6-48a9-83d151622cc1.png>

インスペクターの「Add Component」から「BoxCollider2D」を検索し追加します。

<img width="200" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/33bd0618-73ef-c6db-376c-7d6c1e8bd0d7.png>

![BoxCollider2D.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/5878b35a-68af-c4e5-bc25-5cd80bbc6d86.png)

次にWallプレハブにタグを設定します。

インスペクターの上の方にある「Tag」→「AddTag」の順に選択します。

<img width="300" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/8a83f6af-92ce-0adc-62ef-4780acadbc58.png>

Tagsの＋ボタンを押してタグ「Wall」を作って保存してください。

<img width="300" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/c8ae5668-4f36-66fa-8920-e21827c4f841.png>

再び壁のインスペクターに戻って、Tagを押すとWallが選択できるようになっているはずなのでWallに変更します。

これで壁のプレハブは完成です。

<img width="200" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/796b3be8-8968-d3c7-7de8-60152c63d596.png>

#### LazerMotherプレハブの作成
まずは空のプレハブを作っていきます。

ヒエラルキーウィンドウで右クリックし「Create Empty」を選択し、空のオブジェクトを作ります。名前は「LazerMother」にしています。

Wallプレハブと同様にプロジェクトウィンドウにドロップしてプレハブ化します。

作ったプレハブをダブルクリックして、プレハブの編集モードにし、「LazerMother.cs」をインスペクターウィンドウにドロップして追加します。

「LazerMother.cs」の「LazerPrefab」には後ほど作るLazerプレハブを入れます。

![LazerPrefab.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/43974564-d171-575d-181d-04f6d2b653f6.png)

REFLECT_NUMは最大反射数を設定します。好きな値を設定してください。

:::note warn
大きすぎる値を設定すると重くなります。10前後がおすすめ。
:::

これでLazerMotherは完成です。

#### Lazerプレハブの作成
LazerMotherと同様の手順で空のプレハブを作ります。

ヒエラルキーウィンドウで右クリックし「Create Empty」を選択し、空のオブジェクトを作ります。名前は「Lazer」にしています。

Wallプレハブと同様にプロジェクトウィンドウにドロップしてプレハブ化します。

作ったプレハブをダブルクリックして、プレハブの編集モードにし、「Lazer.cs」をインスペクターウィンドウにドロップして追加します。

インスペクターの「Add Component」から「LineRenderer」を検索し追加します。

![AddLineRenderer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/26da8451-3097-2825-06c0-94bf36a175e7.png)

レーザーの幅が初期設定だと太い場合があるのでWidthの値は適宜、変更してください。
例では0.1に設定しています。

<img width="300" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/fabbe2a2-17df-47d1-5ae0-9a81673c7f08.png>

これで、Lazerプレハブは完成です。

最後に、LazerMotherプレハブの変数にこのプレハブをセットするのを忘れないようにしましょう。

### 4.マテリアルの用意
#### 新規マテリアルの作成
プロジェクトウィンドウ上で右クリックし、「Create」→「Material」の順に選択し、新しいマテリアルを作成します。

<img width="400" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/81993fd8-c858-4310-c9bd-af150f5cefe5.png>

#### Shaderの変更
インスペクターの上の方にあるShaderを変更します。

「Sprites」→「Default」の順に選択します。

<img width="400" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/38a598f4-7cdd-bfa9-c94b-7b55a40d4ff2.png>

![Shader02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/11711fe7-31ac-3fb9-239f-50ba5204bb69.png)

インスペクターが以下のような画面になってればOK

<img width="300" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/a58f2935-d1cc-575f-1a1d-95471993c51c.png>

:::note info
シェーダーの設定はLineRendererが正しく表示されるならなんでもOKです。
:::

#### 色の変更
インスペクターからTintを任意の色に設定してマテリアルは完成です。

<img width="300" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/f7c28032-a9fe-1989-80af-ede8a69fb132.png>

例では5色のマテリアルを用意しました。

![Materials.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/974832d2-bcef-e2e7-6f9e-40125fb6c621.png)



### 5.レーザーの発射台の用意

レーザーの発射台を作成します。

#### オブジェクトの作成

ヒエラルキーウィンドウで右クリックし、「2D Object」→「Sprites」→「Circle」を選択し、円のオブジェクトを作ります。

:::note info
ここは見た目だけなので好きなスプライトで代用可能です。
:::

<img width="300" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/3329452d-f0b0-62c5-62b2-8bf7be96bfbb.png>

インスペクターに「LazerStarter.cs」をドロップし、スクリプトをアタッチします。

#### 値の設定
スクリプトを設定したら以下の変数の値を設定します。

![LazerStarterCS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/d61ebcb1-38a6-6e16-c481-016a8111efe7.png)

| 名前 | 型 | 説明 |
|:-:|:-:|:-:|
| LazerMother | GameObject | プレハブの「LazerMother」を設定します |
| INITIAL_DIRECTION | Vector2 | レーザーの発射方向を決める任意の方向ベクトルを設定します |
| ROTATE_SPEED | float | キーボードを入力したとき、レーザーが動く速度を設定します |
| START_COLOR | Material | 発射されるレーザーのマテリアルを設定します |

LazerMother以外は好きな値を入力してください。

:::note info
方向ベクトルはどんな値でもOK！どの座標の方向へ発射するかを指定しましょう。
:::


値を設定できたらレーザーの発射台は完成です。

### 6.壁の作成
ここでは壁のプレハブをもとに、シーン上に壁を作っていきます。

#### 外壁の作成
プレハブをシーンにドラッグアンドドロップし、壁を配置します。カメラに収まるように大きさを調整しながら４面設置しましょう。

![WallOuters.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/653b37ee-9556-83fd-9df5-ba940ce2e481.png)

初期設定でなっていると思いますが、念のため、「Wall.cs」の「type」が「Default」になっているかも確認してください。

![WallCSDefault.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/0113419c-802f-a171-3e44-8fa2a11602ad.png)

#### LazerMotherの書き換え
以下の定数「MAX_DISTANCE」の値を設置した外壁の対角線の長さより大きい値に設定してください。

```c#
const float MAX_DISTANCE = 20.0f;  //箱の対角線の長さ
```

#### 特殊な壁の設定方法
通常の反射する壁以外の壁を設置する場合は壁を設置した後、Wall.csのTypeを任意のものに変更してください。

![WallCSDefault.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/0113419c-802f-a171-3e44-8fa2a11602ad.png)

壁の色は、先ほど用意したマテリアルを設定して変更します。

![WallMaterial.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/8a390c14-9ba3-fa52-d85d-e8591df05e29.png)

:::note warn
各壁の設定はシーンに配置した壁に対して行ってください。
（プレハブを変更すると設置した壁すべてに適用されてしまいます）
:::
壁が置けたら全工程終了です。これで動くようになっているはず。

## 解説
### レーザーの実装方法
レーザーは概要の通り、RaycastHit2Dで当たり判定を行い、LineRendererで描画しています。

レーザーは以下の例のように、階層的に管理されます。

![Lazers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/2d2c7f55-952c-3732-3814-474d1d8964e7.png)

「LazerStarter」はレーザーの発射を管理するオブジェクトで、最初に「LazerMother」を生成します。「LazerMother」は反射方向の計算と壁との衝突判定を行うオブジェクトで反射のたびに新たな「Lazer」を生成します。各レーザーが「LineRenderer」を持っており、「LazerMother」の計算に従って描画が行われます。

単色のレーザーなら１つのLineRendererでも実現できるが、色を変えるために反射するたび、オブジェクトが新しくなるようにしている。
（１つのLineRendererの途中で色を変える方法が分からなかった...。グラデーションになってしまう）

#### レーザーの発射
「LazerStarter.cs」でレーザーの発射を管理しています。「LazerStarter.cs」がアタッチされているオブジェクトを基準に、変数「INITIAL_DIRECTION」で設定した方向にレーザーが発射されます。

関数Start()内でレーザーを発射しているので、発射台のオブジェクトが生成された時点でレーザーが発射されます。この関数を変更し、実行されるためのトリガーを変えれば自由なタイミングでレーザーを発射することができます。

```c#:Start()
public void Start(){
  newLazer = Instantiate(lazerMother, this.gameObject.transform.position, Quaternion.identity);
  lazerMotherCS = newLazer.GetComponent<LazerMother>();
  lazerMotherCS.creat(newLazer.transform.position, INITIAL_DIRECTION, START_COLOR);
}
```

最初に「LazerMother」を生成し、「LazerMother.cs」の関数creat()に発射台の座標、発射方向、最初のマテリアルを渡しています。

```c#:creat()
/**
 * レーザー生成関数
 * @param {Vector3} origin - レーザーの原点
 * @param {Vector3} direction - レーザーの方向
 * @param {Material} m - レーザーの最初のマテリアル
**/
public void creat(Vector3 origin, Vector2 direction, Material m){
  lazers = new GameObject[REFLECT_NUM + 1];
  lazerCSs = new Lazer[REFLECT_NUM + 1];
  for(int n = 0; n < REFLECT_NUM + 1; n++){
    GameObject lazerChild = Instantiate(lazerPrefab, this.transform);
    lazers[n] = lazerChild;
    lazerCSs[n] = lazerChild.GetComponent<Lazer>();
    lazerCSs[n].SetUp();
  }
  lazerCSs[0].SetMaterial(m);
  startMaterial = m;
  move(origin,direction);
}
```
「LazerStarter.cs」から値を受け取ったcreat()はレーザーの生成時に一度だけ実行される関数で、レーザーの生成と初期設定をします。オブジェクト「Lazer」は、設定されている最大反射数+1個生成されます。（反射数に対して、描画する線分は+1個になるため）

生成されたレーザーは配列に格納され、管理されます。また、レーザーにアタッチされている「Lazer.cs」も配列で管理されます。

最後に各レーザーの初期設定であるSetUp()を実行し、最初のレーザーにマテリアルを設定し、最初の計算と描画をmove()関数で行っています。

#### 壁との衝突
レーザーの描画のために各線分の始点と終点の座標を知る必要があります。終点はレーザーが壁に衝突した座標なので、壁との衝突判定を最初にする必要があります。

<img width="500" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/6625d354-02a2-ed82-8f82-faa1ffd1c704.png>

そこで、「RaycastHit2D」を利用して当たり判定を行います。

RaycastHit2Dの公式ドキュメントはこちら

 https://docs.unity3d.com/ja/2019.4/ScriptReference/RaycastHit2D.html

公式ドキュメントを読むと

>2D 物理挙動の Raycast により検知されたオブジェクトについて返される情報

と書いてあります。つまり、Raycastによる当たり判定で得られる値をいろいろ持っているクラス？のようなものらしい。ドキュメントを読むとどんな値を持っているかがわかります。

「RaycastHit2D」を使うには「Raycast」を理解する必要がありそうです。ドキュメントを読んでみましょう。

Raycastの公式のドキュメントはこちら

https://docs.unity3d.com/ja/2021.1/ScriptReference/Physics.Raycast.html

読んでも難しい感じがしますが、要は「原点、方向、距離を指定して光線を出し、Colliderに対して当たり判定をする」ということです。

この光線はRay(レイ)と呼ばれる見えない光線で今回のような直線上のオブジェクトとの当たり判定で使用されるようです。

##### コードの解説
ここからは実装したコードの解説をしていきます。

```c#:move()内
RaycastHit2D[] hits = Physics2D.RaycastAll(startPos, nextDirection, MAX_DISTANCE);
  foreach(RaycastHit2D hit in hits){
    GameObject hitObj = hit.collider.gameObject;
    if (hitObj != preWall){
      //lineの描画
      Vector3 endPos = hit.point;
    //レーザーの描画などの処理が続く...
```

上記の１行目で「Physics2D.RaycastAll」を利用して、「RaycastHit2D」の配列を入手しています。これは１つの「Raycast」から得られたすべての「RaycastHit2D」が格納された配列です。

ドキュメントはこちら

https://docs.unity3d.com/ja/2019.4/ScriptReference/Physics2D.RaycastAll.html

<img width="500" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/7dcfa8e2-a3fb-ac7e-4ca0-28fae52b2335.png>

最初に当たるオブジェクトの「RaycastHit2D」さえ入手できればいいので「RaycastAll」ではなく、ただの「Raycast」でよいのではとも思ったのですがうまくいきませんでした。
原因はおそらく、レーザーの始点にあるオブジェクトとの当たり判定を拾ってしまっているからだと思います。そのため、一つ前に反射した壁を変数「preWall」に保存し、得られた「RaycastHit2D」に対応するオブジェクトが「preWall」と一致する場合は無視し、最初に「preWall」と一致しなかったオブジェクトを衝突したオブジェクトとして認識するようにしています。

今回のプログラムでは、「RaycastHit2D」から衝突した点の座標、衝突面の法線ベクトル、衝突したオブジェクトを入手しています。
衝突したオブジェクトは「RaycastHit2D」から直接入手することができないので「Collider」を通して入手しています。

<img width="500" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/030f4d7a-2ab3-9bcb-62bb-705ae07fc7de.png>

#### レーザーの描画
##### 概要
レーザーの描画は前述した通り、LineRendererで行われています。

「LineRenderer」のドキュメントはこちら

https://docs.unity3d.com/ja/2022.3/Manual/class-LineRenderer.html



各レーザーの持つ「LineRenderer」の内容をそれぞれの「Lazer.cs」から書き換えることでレーザーを描画しています。

##### 「Lazer.cs」の解説
###### コード

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

public class Lazer : MonoBehaviour{
  private LineRenderer line;

  //初期設定
  public void SetUp(){
    line = gameObject.GetComponent<LineRenderer>();
  }

  //マテリアルの設定
  public void SetMaterial(Material material){
    List<Material> materials = new List<Material>(){material};
    line.SetMaterials(materials);
  }

  //レーザーの位置を設定
  public void SetPosition(Vector3 startPos, Vector3 endPos){
    line.SetPosition(0, startPos);
    line.SetPosition(1, endPos);
  }

  //レーザーのマテリアルを返す
  public Material GetMaterial(){
    return line.material;
  }
}
```

###### 概要
このスクリプトは「LazerMother」から生成される各レーザーにアタッチされています。
各レーザーの「LineRenderer」に関する処理を担っています。

###### 変数「line」と関数「SetUp()」 

変数「line」には各レーザーの「LineRenderer」が入ります。関数SetUp()で最初に、オブジェクトにアタッチされている「LineRenderer」を入手しています。

###### 関数「SetMaterial()」
レーザーにアタッチされた「LineRenderer」にマテリアルを設定する関数です。引数でマテリアルを受け取り、「LineRenderer」の関数「SetMaterials()」を利用してマテリアルを設定します。

「LineRenderer」のマテリアルは「List」で格納されるので、新しい「List」を作成してから関数に渡しています。

「LineRenderer」の関数のドキュメント

https://docs.unity3d.com/ja/2022.3/ScriptReference/LineRenderer.html

「List」の参考にした記事

https://qiita.com/yaaabu51/items/1ea42b29e4893dffb9ec

###### 関数「SetPosition()」
レーザーにアタッチされた「LineRenderer」の始点と終点を設定する関数です。
引数でそれぞれの座標を受け取り、「LineRenderer」の関数「SetPosition()」で座標を設定します。

#### レーザーの反射
##### 概要
レーザーの反射は、壁に当たるたび、衝突点と反射方向を渡しながら繰り返し新たなレーザーを生成することで実装しています。

##### 関数「move()」
###### 概要
```c#
/**
 * レーザー移動時に実行する関数
 * @param {Vector3} origin - レーザーの原点
 * @param {Vector3} direction - レーザーの方向
**/
public void move(Vector3 origin, Vector2 direction){
  //描画、反射方向の計算、当たり判定の処理など行われる
}
```

関数「move()」が一本目のレーザーの原点と方向を受け取り、各処理を行います。

###### 初期化

```c#:move()内 1~7行目
preWall = null;  //前回の反射で触れた壁
nowOrigin = origin;  //一本目のレーザーの現在の始点
nowDirection = direction;  //一本目のレーザーの現在の方向ベクトル
Vector3 startPos = origin;  //次描画するレーザーの始点
Vector2 nextDirection = direction;  //次描画するレーザーの方向ベクトル
nowMaterial = startMaterial;  //次描画するレーザーのマテリアル
isReflect = true;  //反射するかどうか（初期設定では反射する）
```

まず、最初の7行で変数の定義や初期化を行っています。

###### 繰り返し

```c#
for(int i = 0; i < REFLECT_NUM + 1; i++){
  //描画、反射方向の計算、当たり判定の処理など行われる    
}
```
次の行から繰り返しが始まります。一回の繰り返しで一本レーザーの線分が描画されます。以下の図のように必要な線分の数は反射回数+1になるため、「REFLECT_NUM + 1」回繰り返しています。

<img width="300" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/411e2f51-a4ad-09fb-0759-299762891685.png>

繰り返し内で「Rayの生成」→「当たり判定」→「終点の計算」→「描画」の手順でレーザーを描画します。

次の繰り返しの前に次のレーザーの始点と方向を計算して変数に格納します。

<img width="500" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/ba4d6635-75b5-f34e-702a-66feb7cb81b6.png>

図のように次のレーザーの始点は前のレーザーの終点になるため、描画前の計算結果がそのまま使えます。

問題は反射方向のベクトルですが、「Vector2」の関数に「Reflect(Vector2, Vector2)」という便利な関数があったのでそれを使います。

Vector2のドキュメントはこちら

https://learn.microsoft.com/ja-jp/dotnet/api/system.numerics.vector2?view=net-8.0

「Reflect(Vector2, Vector2)」は第一引数に入射してくるベクトル、第二引数に反射面の法線ベクトル（反射する面に対して垂直なベクトル）を入れると反射方向のベクトルを返してくれるというもの。

このプログラムではレーザーの方向をベクトルで渡しながら繰り返しているので、その方向ベクトルを第一引数に入れればOK！

第二引数は、「RaycastHit2D」から「RaycastHit2D.normal」で反射面の法線ベクトルが得られるので、その値を使います。法線ベクトルを用意してくれてるのありがたい。

```c#:反射方向の計算
Vector2 reflectDirection = Vector2.Reflect(nextDirection, hit.normal);
```

これで帰ってきた値を次の繰り返しで使って反射を実現しています。

```c#:次の繰り返しに渡す値を変数に入れる
//次の繰り返しに引き渡す変数
startPos = endPos;
nextDirection = reflectDirection;
preWall = hitObj;
```

#### レーザーを動かす

レーザーを動かす処理は「LazerStarter.cs」内で行われています。

```c#
public void Update(){
  // 左回転
  if (Input.GetKey (KeyCode.LeftArrow)) {
    lazerMotherCS.move(lazerMotherCS.getOrigin(), Quaternion.Euler(0f, 0f, ROTATE_SPEED) * lazerMotherCS.getDirection());
  }
  // 右回転
  if (Input.GetKey (KeyCode.RightArrow)) {
    lazerMotherCS.move(lazerMotherCS.getOrigin(), Quaternion.Euler(0f, 0f, - ROTATE_SPEED) * lazerMotherCS.getDirection());
  }
}
```

この例では、Update()内でキー入力を待って、入力があるとレーザーが回転するようになっています。

move()に発射位置と発射方向が渡せればよいので、レーザーの動かし方はなんでもOKです。

マウスの方向に発射とかも良さそう。

### 壁の実装方法
#### 壁の判別

壁の判別は「タグ」と「enum」を使って行われます。
衝突したオブジェクトが壁かどうかをタグで判別し、壁だった場合、それがどの種類の壁かの判別にenumを使用しています。

##### タグ
Wallのオブジェクトにタグを設定しているので、プログラムでは衝突したオブジェクトのタグがWallかどうかを確認します。

ここではオブジェクトを壁しか作っていないのでif文で判定していますが、タグで識別するオブジェクトの種類を増やす場合はSwitch文を使うといいでしょう。

```c#
if(hitObj.tag == "Wall")
```

##### enum
「Wall.cs」内で壁の種類を管理するenumを定義しています。
enumは列挙型とも呼ばれ、こういった同じ種類の定数を列挙する場合に適しています。（今回は壁の種類を列挙）

ドキュメントはこちら

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/enum

今回のプログラムでは壁の種類を５種類定義しています。

```c#:Wall.cs「enumの定義」
public enum WallType{  //壁の種類
  Default,
  LazerColorChange,
  WallColorChange,
  NonReflect,
  DestroyWall
}
```

各壁が「type」という変数で壁の種類を保持しているので、衝突した壁の「type」を参照することで壁の種類を判別します。

```c#:壁の判別
switch(hitObj.GetComponent<Wall>().type){
  case Wall.WallType.Default:
  break;
  case Wall.WallType.LazerColorChange:
  nowMaterial = hitObj.GetComponent<Renderer>().material;
  break;
  case Wall.WallType.WallColorChange:
  hitObj.GetComponent<Wall>().SetMaterial(nowMaterial);
  break;
  case Wall.WallType.NonReflect:
  isReflect = false;
  break;
  case Wall.WallType.DestroyWall:
  Destroy(hitObj);
  break;
}
```
#### 特殊な壁
##### レーザーの色を変える壁

<img width="420" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/7ef68691-88b6-6867-d532-2d7660756336.png>

レーザーの色を変える壁は「WallType」で「LazerColorChange」と定義されています。
レーザーがこの壁に触れると、それ以降のレーザーの色が壁と一緒になります。

衝突した壁のマテリアルを取得し、レーザーのマテリアルとして設定することで実現しています。

```c#:typeがLazerColorChange
case Wall.WallType.LazerColorChange:
  nowMaterial = hitObj.GetComponent<Renderer>().material;
  break;
```

##### レーザーの色に変わる壁

![Lazer_WallCC.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/96c80c6a-6e0c-86ef-7ed8-ff2b0eae7668.gif)

レーザーの色に変わる壁は「WallType」で「WallColorChange」と定義されています。
この壁にレーザーが当たると壁の色がレーザーの色になります。

衝突した壁に対して現在のレーザーのマテリアルを設定することで実現しています。また、このために「Wall.cs」にマテリアルを設定する関数を用意しています。

```c#:typeがWallColorChange
case Wall.WallType.WallColorChange:
  hitObj.GetComponent<Wall>().SetMaterial(nowMaterial);
  break;
```

```c#:Wall.cs
//壁のマテリアルを設定
public void SetMaterial(Material m){
  gameObject.GetComponent<Renderer>().material = m;
}
```

##### 反射しない壁

![LazerStop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/e9a1200e-543a-b12a-8477-3a2a36fef066.gif)

この壁にレーザーが当たっても反射しません。

レーザーの反射の有無を変数「isReflect」で管理することにより実現しています。

```c#:typeがNonReflect
case Wall.WallType.NonReflect:
  isReflect = false;
  break;
```

繰り返しの最初にこれ以上反射するかを判定し、繰り返さない場合はレーザーのアクティブ状態をfalseにし、continueで以降の処理を行わないようにしています。

```c#:反射するかの分岐
if(isReflect){
  lazers[i].SetActive(true);
}
else{
  lazers[i].SetActive(false);
  continue;
}
```

下の画像のように反射が終了し、描画しないレーザーは非アクティブになります。

![LazerNonActive.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/bee585c2-439d-cf43-d487-000eb559c7a1.png)

##### レーザーに当たると消える壁

![LazerWallDestroy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/c13ac432-dfce-3728-d92c-e02d07936862.gif)

レーザーがこの壁に触れると壁が消えます。

関数「Destroy()」を利用して、壁のオブジェクトを破壊しています。

https://docs.unity3d.com/ja/2021.1/ScriptReference/Object.Destroy.html

```c#:typeがDestroyWall
case Wall.WallType.DestroyWall:
  Destroy(hitObj);
  break;
```

## おわりに
これで反射するレーザーの記事は終了です。

シリーズっぽい感じでやってましたが、途中でコードを変えまくったので１つの記事にまとめちゃいました。次は最初から完成形を想定して、書き換える必要のないコーディングができたらいいなと感じました。記事にまとめる段階で問題点が見えてくる。

とはいえ、実装したかった機能はできたし十分でしょう。自分用のメモみたいなもんですが誰かの参考になればと思います。

## 参考

https://docs.unity3d.com/ja/2019.4/ScriptReference/RaycastHit2D.html

https://docs.unity3d.com/ja/2021.1/ScriptReference/Physics.Raycast.html

https://docs.unity3d.com/ja/2019.4/ScriptReference/Physics2D.RaycastAll.html

https://docs.unity3d.com/ja/2022.3/ScriptReference/LineRenderer.html

https://qiita.com/yaaabu51/items/1ea42b29e4893dffb9ec

https://learn.microsoft.com/ja-jp/dotnet/api/system.numerics.vector2?view=net-8.0

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/enum

https://docs.unity3d.com/ja/2021.1/ScriptReference/Object.Destroy.html