LessonEX ラインを引いて囲む
EX-1 ラインを引く
マウスで円を描き、囲んだオブジェクトを取得してみましょう。
某モンスターをキャプチャするゲームのような処理を作ることができます。

まずはマウスで線を引く処理を実装します。
UnityにはLineRendererというコンポーネントがあり、頂点座標を指定するだけで線を描画することができます。ちなみにLineRenderer自体は2D用ではなく3D空間に線を描画することもできます。
まずは2Dプロジェクトを開いてください。
適当な空オブジェクトを作成してLine Rendererをアタッチしましょう。

アタッチ直後のLineRendererにはマテリアルが設定されていないので、まずはマテリアルを設定しましょう。
MaterialsのElement0をDefault-Lineマテリアルに設定してください。


Unity Tips!

実装の前にLine Rendererの機能をいくつか試してみましょう。
PositionsのSizeの数を適当に増やして、好きな座標を入力してください。入力した頂点を順番に繋ぐ線が描画されます。


Corner Vertices(曲がり角の頂点数)とEnd Cap Vertices(両端の頂点数)を増やすと、線の角が滑らかになります。


グラフを操作して太さを変更したり、Colorから線にグラデーションをつけることができます。
Colorの項目を選択するとGradient Editorが開きます。始点から終点にかけてどのように色が変化するか設定してください。



お好みの線を作成できたら、まずはマウスで線を引く処理を実装していきましょう。
(後ほど始点と終点を繋ぐので、サンプルでは線の太さは一定にしています)
LineGeneratorスクリプトを作成して、以下のように入力してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LineGenerator : MonoBehaviour
{
[SerializeField, Header("座標最大数")]
int MaxLinePosition = 100;
[SerializeField, Header("頂点を更新する移動量")]
float VerticesUpdateRange = 0.01f;
// 現在参照座標位置
int m_positionCount = 0;
// ラインの描画中
bool m_isDraw = false;
// キャッシュ
LineRenderer m_lineRenderer;
Camera m_mainCamera;
void Awake()
{
// 取得
m_lineRenderer = GetComponent<LineRenderer>();
m_mainCamera = Camera.main;
// ラインの座標指定を、このラインオブジェクトのローカル座標系を基準にするよう設定を変更
m_lineRenderer.useWorldSpace = false;
// ラインの頂点数を設定
m_lineRenderer.positionCount = MaxLinePosition;
// 最初はラインを非表示にする
m_lineRenderer.enabled = false;
}
void Update()
{
// このラインオブジェクトを、位置はカメラ前、回転はカメラと同じになるようキープさせる
transform.position = m_mainCamera.transform.position + m_mainCamera.transform.forward * 10;
transform.rotation = m_mainCamera.transform.rotation;
// 左クリックされている間、ライン座標を更新
if (Input.GetMouseButton(0))
{
DrawLine();
}
else
{
// 描画終了
if (m_isDraw)
{
m_isDraw = false;
}
}
}
void DrawLine()
{
// 現在のマウス座標をこのオブジェクトのローカル座標系に変換する
Vector3 mousePos = Input.mousePosition;
mousePos.z = 10.0f;
// マウススクリーン座標をワールド座標に直す
mousePos = m_mainCamera.ScreenToWorldPoint(mousePos);
// さらにそれをローカル座標に直す
mousePos = transform.InverseTransformPoint(mousePos);
// 初回処理
if (m_isDraw == false)
{
// 線の描画開始
m_lineRenderer.enabled = true;
// 頂点座標を初期位置で埋める
for (int i = 0; i < MaxLinePosition; i++)
{
m_lineRenderer.SetPosition(i, mousePos);
}
m_isDraw = true;
}
// マウスの移動量を取得
Vector2 mouse_move;
mouse_move.x = Input.GetAxis("Mouse X");
mouse_move.y = Input.GetAxis("Mouse Y");
// マウスが移動していないならここで処理を止める
if (mouse_move.magnitude <= VerticesUpdateRange)
{
return;
}
// 得られたローカル座標をラインレンダラーに追加する
if (m_positionCount < MaxLinePosition)
{
// 現在見ている項目以降の座標リストに現在のマウス座標を設定する
m_positionCount++;
for (int i = m_positionCount - 1; i < MaxLinePosition; i++)
{
m_lineRenderer.SetPosition(i, mousePos);
}
}
else
{
// 頂点を1つ上にずらす
for (int i = 0; i < MaxLinePosition - 1; i++)
{
m_lineRenderer.SetPosition(i, m_lineRenderer.GetPosition(i + 1));
}
// 現在座標の設定
m_lineRenderer.positionCount = MaxLinePosition;
m_lineRenderer.SetPosition(MaxLinePosition - 1, mousePos);
}
}
}
【プログラムの解説】
・最初に m_lineRenderer.useWorldSpace = false; を実行することで、LineRendererで使用する頂点座標をワールド座標系からローカル座標系に変更しています。
線の頂点がワールド座標系を基準にしていると、カメラが移動した際にラインが置いて行かれてしまいます。今回はカメラが移動してもラインはついてくるようにしたいため、ローカル座標系を使用するようにしましょう。

・Input.mousePosition で現在のマウス座標を取得することができますが、ここで取得できる座標はスクリーン座標系になっています。スクリーン座標系は、ゲーム画面の左下を(X=0,Y=0)、右上を(X=画面の横サイズ , Y=画面の縦サイズ) とした座標系になります。

しかし、前述した通りLineRendererの頂点の座標系はローカル座標系になっています。そのため、スクリーン座標系で定義されているマウス座標をローカル座標系に変換する必要があります。
まずはCameraクラスの関数 ScreenToWorldPoint(スクリーン座標系の座標); で、スクリーン座標系からワールド座標系への変換を行っています。ちなみに、この関数を使うことでマウスでクリックした位置にオブジェクトを生成する…といったこともできます。
そしてTransformクラスの関数 InverseTransformPoint(ワールド座標系の座標); で、ワールド座標系の座標から、自身のローカル座標系の座標に変換しています。
まとめると、マウスの座標を「スクリーン座標系」→「ワールド座標系」→「ローカル座標系」と変換することで、使用する頂点座標を求めているという訳です。
・Input.GetAxis("Mouse X"); と Input.GetAxis("Mouse Y"); で直前のフレームと現在のフレームでのマウスの移動量を取得することができます。マウスが移動していない場合は頂点座標の更新を止めるために、移動量を計算しています。
・LineRendererのpositionCountを頻繁に変更するとゲームが重くなってしまうため、ここでは「最初に要素数を決めて、順番に座標を設定していく」手法を取っています。
また、古い座標を新しい座標で上書きしていくことで、古い線は消えていくという処理を行っています。線の長さに制限を設けたくない場合はこの手法を取る必要はありません。
コードが書けたら保存して、LineRendererをアタッチしたオブジェクトにLineGeneratorをアタッチしてください。

ゲームを実行して、マウスで線を描画できることを確認してみてください。
マウスが動かない限り線は残り続け、頂点数が増えると古い頂点から消えていきます。

頂点の更新方法は元ネタに近づけるために行っているものなので、作りたいゲームに応じて頂点の更新条件は変更するようにしてください。
LineRendererで引いた線に当たり判定をつけることで、某アルファベットのPの次なゲームの演出を行うこともできます。
(おまけですが割と長いので、興味がない人は右下のメニューからEX-2まで飛ばしてください)

今回は線を複数生成する必要があるため、LineRendrerは別のオブジェクトにしましょう。
空オブジェクトを作成して、LineRendrerをアタッチしてください。
パラメータの設定を行いましょう。
・PositionsのSizeを0にしてください。
・デフォルトのWidthでは少し太いので、線の太さを0.5にしています。この解説で行う方法では途中の線の太さの変更には対応していないので、グラフに変化は加えないでください。
・曲がり角の頂点数と両端の頂点数を増やしてください。
サンプルでは
Corner Vertices : 12
End Cap Vertices : 6
となっています。
・MaterialsのElement0には何かしらのマテリアルを設定してください(しなくてもOK)

次に当たり判定を生成する準備を行います。
LineRendrerをアタッチしたオブジェクトに、EdgeCollider2Dをアタッチしてください。
パラメータの設定を行いましょう。
・Edge Radiusは当たり判定の半径になります。
線の太さを0.5にしたので、半径はその半分である0.25にしてください。

EdgeCollider2Dは線分での当たり判定を生成することができるコライダーです。
ベースは線の当たり判定ですが、頂点を指定して半径を設定することでLineRendrerとほぼ同じ形状を作ることができます。

ただしEdgeCollider2Dには「EdgeCollider2D同士での衝突ができない」という問題点(仕様)があります。そのため、EdgeCollider2Dでは、線同士の物理演算を行うことはできません。

これを解決するために、EdgeCollider2DだけではなくCircleCollider2Dを併用して当たり判定を実装します。
EdgeCollider2D同士が衝突できないというだけで、CircleCollider2DとEdgeCollider2Dの衝突は可能です。よって、LineRendrerの各頂点の位置に円形のコライダーを生成することで、EdgeCollider2Dが動作するようにしています。


CircleCollider2Dはスクリプトで動的に生成するので、ライン用オブジェクトにはRigidbody2Dをアタッチしてください。
Rigidbody2DのInterpolateをNoneからInterpolateに変更しましょう。
ここで解説する手法は完全に最適化されたものではないので、複雑な図形を描くと負荷がかかりますが、座標の補間を行うことで画面のガタつきを抑えることができます。

Prefabフォルダを作ったら、ライン用のオブジェクトをプレハブ化して、シーン上にあるライン用オブジェクトを削除してください。

LineGeneratorPhysicsスクリプトを作成して、以下のように入力してください。
ベースは上記のLineGeneratorスクリプトになっていますが、所々異なるので注意しましょう。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LineGeneratorPhysics : MonoBehaviour
{
[SerializeField, Header("ラインオブジェクト")]
GameObject LineObject;
[SerializeField, Header("頂点を更新する移動量")]
float VerticesUpdateRange = 0.04f;
// 頂点座標を保存
List<Vector3> linePos = new();
// ラインの描画中
bool m_isDraw = false;
// キャッシュ
LineRenderer m_lineRenderer;
EdgeCollider2D m_edgeCollider2D;
Rigidbody2D m_rigidbody2D;
Camera m_mainCamera;
void Awake()
{
// 取得
m_mainCamera = Camera.main;
}
void Update()
{
// このラインオブジェクトを、位置はカメラ前、回転はカメラと同じになるようキープさせる
transform.position = m_mainCamera.transform.position + m_mainCamera.transform.forward * 10;
transform.rotation = m_mainCamera.transform.rotation;
// 左クリックされている間、ライン座標を更新
if (Input.GetMouseButton(0))
{
DrawLine();
}
else
{
// 描画終了
if (m_isDraw)
{
m_isDraw = false;
// 落下を有効にする
m_rigidbody2D.bodyType = RigidbodyType2D.Dynamic;
}
}
}
void DrawLine()
{
// 現在のマウス座標をこのオブジェクトのローカル座標系に変換する
Vector3 mousePos = Input.mousePosition;
mousePos.z = 10.0f;
// マウススクリーン座標をワールド座標に直す
mousePos = m_mainCamera.ScreenToWorldPoint(mousePos);
// さらにそれをローカル座標に直す
mousePos = transform.InverseTransformPoint(mousePos);
// 初回処理
if (m_isDraw == false)
{
// ラインオブジェクトを生成
GameObject lineObject = Instantiate(LineObject, transform.position, transform.rotation);
// コンポーネントを取得
m_lineRenderer = lineObject.GetComponent<LineRenderer>();
m_edgeCollider2D = lineObject.GetComponent<EdgeCollider2D>();
m_rigidbody2D = lineObject.GetComponent<Rigidbody2D>();
// リストの初期化
linePos.Clear();
// 最初は落下を止める
m_rigidbody2D.bodyType = RigidbodyType2D.Static;
// ラインの座標指定を、このラインオブジェクトのローカル座標系を基準にするよう設定を変更
m_lineRenderer.useWorldSpace = false;
// 線の描画開始
m_lineRenderer.enabled = true;
m_isDraw = true;
}
// マウスの移動量を取得
Vector2 mouse_move;
mouse_move.x = Input.GetAxis("Mouse X");
mouse_move.y = Input.GetAxis("Mouse Y");
// マウスが移動していないならここで処理を止める
if (mouse_move.magnitude <= VerticesUpdateRange)
{
return;
}
// 得られたローカル座標をラインレンダラーに追加する
linePos.Add(mousePos);
m_lineRenderer.positionCount = linePos.Count;
m_lineRenderer.SetPositions(linePos.ToArray());
// CircleColliderの生成
CircleCollider2D circleCollider = m_lineRenderer.gameObject.AddComponent<CircleCollider2D>();
circleCollider.offset = mousePos;
circleCollider.radius = 0.25f;
// EdgeColliderの設定
List<Vector2> positions = new();
for (int i = 0; i < m_lineRenderer.positionCount; i++)
{
positions.Add(m_lineRenderer.GetPosition(i));
}
m_edgeCollider2D.SetPoints(positions);
}
}
【プログラムの解説】
・AddComponent<クラス名>(); では動的にコンポーネントを追加することができます。
戻り値として追加したコンポーネントを返します。
コードが書けたら適当な空オブジェクトにLineGeneratorPhysicsスクリプトをアタッチして、LineObjectには先ほどプレハブ化したライン用オブジェクトを設定してください。

これで当たり判定のある線を描けるようになりました。
ゲームを実行して確認してみてください。
現在の仕様では線の長さに制限はなく、描画途中の線にも当たり判定があります。この辺りの仕様はお好みで調整してください。

Unity Tips!

EX-2 囲み判定を取る
線を引き終わったら終点と始点を結んで円を完成させ、オブジェクトが円に囲まれたかどうか判定できるようにしましょう。
まずは判定対象として適当なカプセルを作成しましょう。
オブジェクトにEnemyタグを設定し、CapsuleCollider2DとRigidbody2Dをアタッチしてください。このままでは重力で落下してしまうので、Rigidbody2DのGravityScaleを0に設定してください。

LineRendererをアタッチしているオブジェクトにPolygonCollider2Dをアタッチしてください。
Is Triggerにチェックを入れておきましょう。

PolygonCollider2Dはポリゴンによる自由な形状のコライダーです。ポリゴンの頂点を直接指定することで、元画像に近い形の当たり判定を形成できます。

後はLineGeneratorに「線を描き終わったタイミングでPolygonCollider2Dに頂点を教える」処理を追加しましょう。
LineGeneratorを開いて、赤い部分のコードを追加してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LineGenerator : MonoBehaviour
{
[SerializeField, Header("座標最大数")]
int MaxLinePosition = 100;
[SerializeField, Header("頂点を更新する移動量")]
float VerticesUpdateRange = 0.01f;
// 現在参照座標位置
int m_positionCount = 0;
// ラインの描画中
bool m_isDraw = false;
// 当たり判定中
bool m_isColliderCheck = false;
// キャッシュ
LineRenderer m_lineRenderer;
Camera m_mainCamera;
PolygonCollider2D m_polygonCollider2D;
void Awake()
{
// 取得
m_lineRenderer = GetComponent<LineRenderer>();
m_polygonCollider2D = GetComponent<PolygonCollider2D>();
m_mainCamera = Camera.main;
// ラインの座標指定を、このラインオブジェクトのローカル座標系を基準にするよう設定を変更
m_lineRenderer.useWorldSpace = false;
// ラインの頂点数を設定
m_lineRenderer.positionCount = MaxLinePosition;
// 最初はラインを非表示にする
m_lineRenderer.enabled = false;
// 当たり判定を最初は無効にする
m_polygonCollider2D.enabled = false;
}
void Update()
{
// コライダーが消えるまで待つ
if (m_isColliderCheck)
{
return;
}
// このラインオブジェクトを、位置はカメラ前、回転はカメラと同じになるようキープさせる
transform.position = m_mainCamera.transform.position + m_mainCamera.transform.forward * 10;
transform.rotation = m_mainCamera.transform.rotation;
// 左クリックされている間、ライン座標を更新
if (Input.GetMouseButton(0))
{
DrawLine();
}
else
{
// 描画終了
if (m_isDraw)
{
m_isDraw = false;
DrawEnd();
}
}
}
void DrawLine()
{
// 現在のマウス座標をこのオブジェクトのローカル座標系に変換する
Vector3 mousePos = Input.mousePosition;
~後略~
~前略~
// 頂点を1つ上にずらす
for (int i = 0; i < MaxLinePosition - 1; i++)
{
m_lineRenderer.SetPosition(i, m_lineRenderer.GetPosition(i + 1));
}
// 現在座標の設定
m_lineRenderer.positionCount = MaxLinePosition;
m_lineRenderer.SetPosition(MaxLinePosition - 1, mousePos);
}
}
// ポリゴンの配列を作成
Vector2[] CreatePolygonList()
{
Vector2[] polygons = new Vector2[MaxLinePosition];
for (int i = 0; i < MaxLinePosition; i++)
{
polygons[i] = m_lineRenderer.GetPosition(i);
}
return polygons;
}
// ライン描画の終わり
void DrawEnd()
{
// 始点の座標を一番最後に追加する
if (m_positionCount < MaxLinePosition)
{
m_positionCount++;
for (int i = m_positionCount - 1; i < MaxLinePosition; i++)
{
m_lineRenderer.SetPosition(i, m_lineRenderer.GetPosition(0));
}
}
else
{
m_lineRenderer.SetPosition(m_positionCount - 1, m_lineRenderer.GetPosition(0));
}
// ポリゴンコライダー生成
m_polygonCollider2D.enabled = true;
m_polygonCollider2D.SetPath(0, CreatePolygonList());
// コライダーを有効にする
m_isColliderCheck = true;
// しばらくしたら止める
Invoke("ColliderEnd", 0.5f);
}
void ColliderEnd()
{
m_polygonCollider2D.enabled = false;
m_lineRenderer.enabled = false;
m_isColliderCheck = false;
}
private void OnTriggerStay2D(Collider2D collision)
{
// 敵が衝突したら削除
if (collision.CompareTag("Enemy"))
{
Debug.Log("ヒット");
Destroy(collision.gameObject);
}
}
}
【プログラムの解説】
・LineRendererのGetPosition関数で座標を取得して、頂点用の配列を作成しています。
それをPolygonCollider2DにSetPath関数を用いて設定することで当たり判定に反映しています。
コードが書けたら保存して、ゲームを実行してみてください。
線でオブジェクトを囲むと、囲まれたオブジェクトが削除されます。ゲームを一時停止すると、ラインの頂点に対応した当たり判定が生成されていることが確認できます。

今回は囲んだ時にシンプルに削除していますが、作りたいゲームに応じて処理は変更してください。
LineRendererを活用することで、弾の発射地点から着弾点を頂点にして予測線を表示したり、線の色を任意で変える処理を実装することでお絵描き機能を作ったりと、ゲームの演出の幅を広げることができます。ぜひ色々試してみてください。
【参考サイト】


