【Unity】反射するレーザーを作ろう！#2「レーザーを動かす」

## はじめに
前に作った反射するレーザーの続き。

前の記事はこちら
https://qiita.com/TanakaNeko/items/fbd57b3c24b7fd4adb0d

今回は、キー操作で下のgifのようにレーザーが回転するようにした。

![lazer_move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679145/934c73d1-dd72-5e31-c6f4-28ed9c1c0838.gif)

## 概要
十字キーの左右でレーザーを回転できる。反射の計算と描画は引き続き、RaycastHit2DとLineRendererを使っている。

## 作り方
前回作ったスクリプトを以下のものに置き換える。

```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(0.3f,1); //最初の方向
  const float ROTATE_SPEED = 0.02f;  //回転速度
  private GameObject newLazer;
  private Lazer lazerCS;

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

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

```
レーザー発射のメソッドをStart()に変更し、キー操作を受け付けるためにUpdate()メソッドを追加している。

---

```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 = 5;  //反射回数
  private GameObject preWall;
  public LineRenderer[] lazers;
  public Vector3 nowOrigin;
  public Vector2 nowDirection;
  
  /**
   * レーザー生成関数
   * @param {Vector3} origin - レーザーの原点
   * @param {Vector3} direction - レーザーの方向
   * @param {int} n - 何本目か(0はじまり)
  **/
  public void creat(Vector3 origin, Vector2 direction, int n){
    if(n == 0){
      nowOrigin = origin;
      nowDirection = direction;
      lazers = new LineRenderer[REFLECT_NUM + 1];
      preWall = null;
    };
    if(n < REFLECT_NUM + 1){
      GameObject lazerChild = Instantiate(lazerPrefab, this.transform);
      lazerChild.name = "lazer_" + n;
      LineRenderer line = lazerChild.GetComponent<LineRenderer>();
      lazers[n] = line;
      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;
        }
      }    
    } 
  }

  /**
   * レーザー移動時に実行する関数
   * @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;
    foreach(LineRenderer line in lazers){
      RaycastHit2D[] hits = Physics2D.RaycastAll(startPos, nextDirection, MAX_DISTANCE);

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

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

```
レーザー移動時に実行する関数と現在のレーザーの情報を返すゲッターを追加している。

:::note warn
レーザーの発射方法を変更しているので注意
:::
前回、ボタンを押すと発射するようにしていたが、今回はStart()メソッドに変更。そのため、ボタンではなく任意のGameObjectにアタッチすることで、そのオブジェクトの生成時にレーザーが発射される。
（オブジェクトの座標からレーザーが発射されるのはそのまま）

スクリプトを置き換えたら、十字キーを押したときにレーザーが動くようになる。

## 解説
### 追加した変数
#### LazerStarter.cs
回転速度の変数と、Update()内でLazer.csの関数を呼び出すための変数を追加。
回転速度は任意の値に変更してOK。
| 変数名 | 型 | 役割 | 
|:-:|:-:|:-:|
| ROTATE_SPEED  | float  | キー入力をしたときのレーザーの回転速度 |
| newLazer  | GameObject  | 新たに生成されたLazerMother |
| lazerCS  | Lazer | 生成したLazerMotherにアタッチされているLazer.cs |
#### Lazer.cs
レーザーの状態が動的に変化するので、現在のレーザーの情報を入手するための変数を追加。
| 変数名 | 型 | 役割 | 
|:-:|:-:|:-:|
| lazers  | LineRenderer[] | 反射によって生成された各レーザーの持つLineRendererを順に格納する配列 |
| nowOrigin  | Vector3 | レーザーの現在の始点 |
| nowDirection | Vector2 | レーザーの現在の発射方向 |

変数の追加に伴い、いくつかコードを変更した。
```c#:LazerStater.cs Start()内
newLazer = Instantiate(lazerMother, this.gameObject.transform.position, Quaternion.identity);
lazerCS = newLazer.GetComponent<Lazer>();
lazerCS.creat(newLazer.transform.position, INITIAL_DIRECTION, 0);
```
newLazerをローカル変数からグローバル変数に変更し、LazerCSに値をいれる行を追加している。この二つをグローバル変数にすることでUpdate()内でも、これらの値を使用できる。

---

```c#:Lazer.cs 23~28行目
if(n == 0){
  nowOrigin = origin;
  nowDirection = direction;
  lazers = new LineRenderer[REFLECT_NUM + 1];
  preWall = null;
};
```
preWallを初期化していたif文内に、新しい変数の初期化の処理を追加。

---
```c#:Lazer.cs 33行目
lazers[n] = line;
```
LineRendererを配列に格納する処理を追加。

### 追加した関数
#### Update()
```c#:LazerStarter.cs Update()
public void Update(){
    // 左回転
    if (Input.GetKey (KeyCode.LeftArrow)) {
      lazerCS.move(lazerCS.getOrigin(), Quaternion.Euler(0f, 0f, ROTATE_SPEED) * lazerCS.getDirection());
    }
    // 右回転
    if (Input.GetKey (KeyCode.RightArrow)) {
      lazerCS.move(lazerCS.getOrigin(), Quaternion.Euler(0f, 0f, -ROTATE_SPEED) * lazerCS.getDirection());
    }
}
```
キー入力を受け付け、右矢印、左矢印が押されたときに、Lazer.csの関数move()を実行する。
move()の引数には現在のレーザーの原点の座標のVector3と、現在のレーザーの発射方向からROTATE_SPEEDだけ回転させたVector2を渡している。

回転の計算はQuaternion * Vector2で計算している。（よくはわかってはいないが、これで、Quaternionで指定した角度分回転させたベクトルを得ることができる）Vector2 * Quaternionの順では計算できないので注意が必要。



#### getter
```c#:Lazer.cs getter
//getter
public Vector3 getOrigin(){
    return nowOrigin;
}
public Vector2 getDirection(){
    return nowDirection;
}
```
LazerStarter.csからレーザーの情報を得るために使用する。

#### move(Vector3, Vector2)
```c#:Lazer.cs move(Vector3, Vector)
  /**
   * レーザー移動時に実行する関数
   * @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;
    foreach(LineRenderer line in lazers){
    RaycastHit2D[] hits = Physics2D.RaycastAll(startPos, nextDirection, MAX_DISTANCE);

    foreach(RaycastHit2D hit in hits){
        if (hit.collider.gameObject != preWall)
        {
          Vector3 endPos = hit.point;
          Vector2 reflectDirection = Vector2.Reflect(nextDirection, hit.normal);
          line.SetPosition(0, startPos);
          line.SetPosition(1, endPos);
          preWall = hit.collider.gameObject;
          startPos = endPos;
          nextDirection = reflectDirection;
          break;
        }
      }  
    }
  }
```
最初のレーザーの始点と方向を引数で受け取り、その後のレーザーの始点、終点もforeach内で計算し、配列lazersに格納されたLineRandererの値を更新する。

反射方向や座標の計算方法は、前回の記事で作ったcreat()メソッド内と同じ。

## おわりに
今回はレーザーを動的に動かせるよう変更した。前回作った反射の計算をそのまま使えたので、思っていたより簡単に作ることができた。今回は発射する方向のみ動かせるようにしたが、発射する位置を動かせるようにしても面白そう。

### 続き

https://qiita.com/TanakaNeko/items/c257a4e2e7342a05ae6c

## 参考
https://www.sejuku.net/blog/52747

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

https://baba-s.hatenablog.com/entry/2018/05/07/104000