はじめに
今回は前回作った動かせる反射レーザーのコードの改良。
動作は変えずにコードを改良していく、つまりリファクタリングをする(覚えた単語使いたいだけ)。
動作は変わらないので「Lazer.cs」だけ、この記事の改良後のコードに置き換えて次の記事に進んでも問題ない。
前回の記事はこちら
前回のコードの問題点
前回のコードを見つつ、自分が感じた問題点をあげていく。
改良を加えるのは「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()内に似ている部分がある。
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(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()内が前半部分は完全に一致している。
しかし、一致するのは当然で、この部分はレーザーの描画と反射の計算という同様の処理をしている。同じ処理を二つ書くことで、コードが長くなるだけでなく、コードの修正や機能の追加の際、二つとも書き換えないといけなくなることも問題である。
そのため、同じ処理はできるだけ関数にしていきたい。
改良後のコード
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文でしているかである。
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()で全く同じにできる。これで関数化の準備が整った。
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);
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()内で内容が変わらない。そのため新しく作る関数内で定義すればよい。
RaycastHit2D[] hits = Physics2D.RaycastAll(startPos, nextDirection, MAX_DISTANCE);
さらにhitsの定義に必要な変数は、元々新たに作る変数に必要なものなので、hitsの定義を関数内に入れることで引数を一つ減らすことができる。
戻り値
戻り値として必要なのは、次の繰り返しに引き渡す変数で、以下の二つ。
変数名 | 型 | 役割 |
---|---|---|
origin | Vector3 | 次のレーザーの原点 |
direction | Vector2 | 次のレーザーの方向 |
ここで一つの疑問が生まれる。
戻り値が二つってどうやるの?
戻り値が二つの関数を作る
次の記事を参考に戻り値が二つの関数を作った。
記事内で紹介されている方法の内、今回はTuple型を採用。
変数名で変数を呼び出せるように関数を定義する。(関数名はculcReflect)
private (Vector3 origin, Vector2 direction) culcReflect(Vector3 startPos, Vector2 nextDirection ,LineRenderer line){
}
このように関数を定義することで、「関数名().変数名」で戻り値を得ることができるようになる。
関数の中身を作る
中身を作ると言ってもほとんどコピペでOK。
戻り値として設定する変数の定義を追加している点に注意。
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()を使用するように書き換えます。
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);
}
}
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を介して戻り値を入手しています。
おわりに
今回はコードの改良をした。最初にコードを書く時点でいいコードを書くのは難しいので適宜、改良していきたい。
自分も正解はわからないので、なにかあったらコメントください。
次回はレーザーの色が変わるギミックの追加をする。
続き
参考