【Unity】反射するレーザーを作ろう！#2.5「リファクタリング」

## はじめに
今回は前回作った動かせる反射レーザーのコードの改良。

動作は変えずにコードを改良していく、つまりリファクタリングをする（覚えた単語使いたいだけ）。

動作は変わらないので「Lazer.cs」だけ、この記事の改良後のコードに置き換えて次の記事に進んでも問題ない。

前回の記事はこちら

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

## 前回のコードの問題点
前回のコードを見つつ、自分が感じた問題点をあげていく。
改良を加えるのは「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 = 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;
  }
}
```

前回の記事の通り、このコードでも問題なく動作するのだが、少しコードが長い。
そこで、コードをよく見ると、creat()内とmove()内に似ている部分がある。
```c#:creat()内
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;
  }
}
```
```c#:move()
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()内が前半部分は完全に一致している。

しかし、一致するのは当然で、この部分はレーザーの描画と反射の計算という同様の処理をしている。同じ処理を二つ書くことで、コードが長くなるだけでなく、コードの修正や機能の追加の際、二つとも書き換えないといけなくなることも問題である。

そのため、同じ処理はできるだけ関数にしていきたい。

## 改良後のコード
```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);
      var reflect = culcReflect(origin, direction, line);
      creat(reflect.origin, reflect.direction, ++n);  
    }
  }


  /**
   * レーザー移動時に実行する関数
   * @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){
      var reflect = culcReflect(startPos, nextDirection, line);
      startPos = reflect.origin;
      nextDirection = reflect.direction;
    }
  }


  /**
   * レーザーを描画し、反射角を計算し、原点と反射方向を返す
   * @return {Vector3} origin - 反射後のレーザーの原点
   * @return {Vector3} direction - 反射後のレーザーの方向
   * @param startPos {Vector3} - 反射前のレーザーの原点
   * @param nextDirection {Vector2} - 反射前のレーザーの方向
   * @param line {LineRenderer} - 描画するレーザーのLineRenderer
  **/
  private (Vector3 origin, Vector2 direction) culcReflect(Vector3 startPos, Vector2 nextDirection ,LineRenderer line){
    RaycastHit2D[] hits = Physics2D.RaycastAll(startPos, nextDirection, MAX_DISTANCE);
    Vector3 endPos = new Vector3(0,0,0);
    Vector2 reflectDirection = new Vector2(0,0);
    foreach(RaycastHit2D hit in hits){
      GameObject hitObj = hit.collider.gameObject;
      if (hitObj != preWall){
        endPos = hit.point;
        reflectDirection = Vector2.Reflect(nextDirection, hit.normal);

        line.SetPosition(0, startPos);
        line.SetPosition(1, endPos);
        preWall = hitObj;

        break;
      }
    }
    return (endPos, reflectDirection);
  }


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

```

レーザーの描画と反射の計算をする処理を関数culcReflect()にまとめた。

## 解説
もともとのコード（下記はcreat内のもの）はforeach内で計算とレーザーの描画をし、次の繰り返しに引き継ぐ処理もしている。creat()の処理とmove()の違いは繰り返しの処理を再帰的にしているかfor文でしているかである。

```c#:元のコード
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;
  }
}
```

さらに、次の繰り返しに引き継ぐ処理はforeachの最後に実行されるので、外に出すことができる。そのためforeach内のコードをcreat()とmove()で全く同じにできる。これで関数化の準備が整った。

```c#:creat()内
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;
    
    break;
  }
}
//再帰をforeachの外に出せる
creat(endPos, reflectDirection, ++n);
```
```c#:move()内
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;

    break;
  }
}
//変数の書き換えをforeachの外に出せる
startPos = endPos;
nextDirection = reflectDirection;
```

### 引数、戻り値

関数化の前に受け取るべき引数と返すべき戻り値を整理する。

#### 引数
foreach内で使われているが、foreachの外で定義されている変数を引数として受け取る必要がある。該当する変数は以下の通り。

＊変数名の()はcreat()内とmove()内で名前が違うもの（役割は同じ）
| 変数名 | 型 | 役割 |
|:-:|:-:|:-:|
| hits | RaycastHit2D[] | 反射方向への衝突を検知するRaycastHit2Dの配列 |
| direction(nextDirection)  | Vector2 | 描画するレーザーの方向ベクトル  |
| origin(StartPos) | Vector3 | 描画するレーザーの原点座標 |
| line | LineRenderer | 描画するレーザーのLineRenderer |

foreachはhits（RaycastHit2Dの配列）からRaycastHit2Dを受け取り、繰り返している。
hitsを変数として受け取ってもよいが、hitsの定義はforeachの直前にされており、creat()内とmove()内で内容が変わらない。そのため新しく作る関数内で定義すればよい。

```c#:hitsの定義
RaycastHit2D[] hits = Physics2D.RaycastAll(startPos, nextDirection, MAX_DISTANCE);
```
さらにhitsの定義に必要な変数は、元々新たに作る変数に必要なものなので、hitsの定義を関数内に入れることで引数を一つ減らすことができる。

#### 戻り値
戻り値として必要なのは、次の繰り返しに引き渡す変数で、以下の二つ。

| 変数名 | 型 | 役割 |
|:-:|:-:|:-:|
| origin | Vector3 | 次のレーザーの原点 |
| direction | Vector2 | 次のレーザーの方向 |

ここで一つの疑問が生まれる。

戻り値が二つってどうやるの？

### 戻り値が二つの関数を作る
次の記事を参考に戻り値が二つの関数を作った。

https://qiita.com/rait0/items/b2ec3f3768943a70f893

記事内で紹介されている方法の内、今回はTuple型を採用。
変数名で変数を呼び出せるように関数を定義する。（関数名はculcReflect）

```c#
private (Vector3 origin, Vector2 direction) culcReflect(Vector3 startPos, Vector2 nextDirection ,LineRenderer line){

}
```
このように関数を定義することで、「関数名().変数名」で戻り値を得ることができるようになる。

### 関数の中身を作る
中身を作ると言ってもほとんどコピペでOK。
戻り値として設定する変数の定義を追加している点に注意。

```c#:culcReflect()
private (Vector3 origin, Vector2 direction) culcReflect(Vector3 startPos, Vector2 nextDirection ,LineRenderer line){
  RaycastHit2D[] hits = Physics2D.RaycastAll(startPos, nextDirection, MAX_DISTANCE);
  Vector3 endPos = new Vector3(0,0,0);
  Vector2 reflectDirection = new Vector2(0,0);
  foreach(RaycastHit2D hit in hits){
    GameObject hitObj = hit.collider.gameObject;
    if (hitObj != preWall){
      endPos = hit.point;
      reflectDirection = Vector2.Reflect(nextDirection, hit.normal);

      line.SetPosition(0, startPos);
      line.SetPosition(1, endPos);
      preWall = hitObj;

      break;
    }
  }
  return (endPos, reflectDirection);
}
```

### creat()とmove()を書き換える
それぞれの関数を新たに作ったculcReflect()を使用するように書き換えます。

```c#:creat()
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;
    //以下が変更された行
    var reflect = culcReflect(origin, direction, line);
    creat(reflect.origin, reflect.direction, ++n);  
  }
}
```

```c#:move()
public void move(Vector3 origin, Vector2 direction){
  preWall = null;
  nowOrigin = origin;
  nowDirection = direction;
  Vector3 startPos = origin;
  Vector2 nextDirection = direction;
  foreach(LineRenderer line in lazers){
    //以下が変更された行
    var reflect = culcReflect(startPos, nextDirection, line);
    startPos = reflect.origin;
    nextDirection = reflect.direction;
  }
}
```
何度もculcReflectを呼び出さないように、新たに定義した変数reflectを介して戻り値を入手しています。

## おわりに
今回はコードの改良をした。最初にコードを書く時点でいいコードを書くのは難しいので適宜、改良していきたい。
自分も正解はわからないので、なにかあったらコメントください。

次回はレーザーの色が変わるギミックの追加をする。

### 続き

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

## 参考

https://qiita.com/rait0/items/b2ec3f3768943a70f893

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