【Unity】あみだくじの横線を引く

C#

現在(2024/08/16)、制作中のあみだくじ型対戦ゲームのコードのメモ。

作ったもの

以下のように、あみだくじの横線を引けるようなコードを書いた。

コード

MouseController.cs

using System;
using UnityEngine;
using UnityEngine.Events;

public class MouseController : MonoBehaviour
{
    public UnityEvent<RaycastHit> MouseOnEvent;
    public UnityEvent MouseDragEvent;
    public UnityEvent MouseUpEvent;
    [SerializeField] private Camera cam;

    void Update()
    {
        Ray ray = cam.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit))
        {
            MouseOnEvent?.Invoke(hit);

            if (Input.GetMouseButton(0))
            {
                MouseDragEvent?.Invoke();
            }

            if (Input.GetMouseButtonUp(0))
            {
                MouseUpEvent?.Invoke();
            }
        }
    }
}

LinesController.cs

using System;
using System.Collections.Generic;
using UnityEngine;

public class LinesController : MonoBehaviour
{
    private List<LineHorizontal> lineCSs = new List<LineHorizontal> {};
    private bool isMouseDown = false;
    private Vector3 startPos; // 始点
    private Vector3 endPos; // 終点
    private Vector3 onLinePos; // 線上の座標
    private Vector3 mousePos; // マウスの座標 
    
    [Header("縦線の設定")]
    [SerializeField] private int VLINE_NUM = 4; // 縦線の本数
    [SerializeField] private float VLINE_SPACE = 8.0f; // 縦線と縦線の間の広さ
    [SerializeField] private float VLINE_INIT_DISTANCE = 40.0f; // 縦線の長さ
    
    [Header("GameObject")]
    [SerializeField] private GameObject startPosObj;
    [SerializeField] private GameObject endPosObj;
    [SerializeField] private GameObject hLineObj;
    [SerializeField] private GameObject vLineObj;
    
    [Header("マウスコントローラー")]
    [SerializeField] private MouseController mouseCS;

    void Start()
    {
        mouseCS.MouseOnEvent.AddListener(MouseOnVLine);
        GenerateVLine();        
    }

    // 縦線の生成
    private void GenerateVLine(){
        Vector3 posInit = new Vector3(0, 0, 0);
        if (VLINE_NUM % 2 == 0)
        {
            posInit = new Vector3(VLINE_SPACE * (-VLINE_NUM / 2 + 0.5f), 0, 0);
        }
        else
        {
            posInit = new Vector3(VLINE_SPACE * (-(VLINE_NUM - 1) / 2), 0, 0);
        }
        Vector3 vpos = posInit;
        for (int i = 0; i < VLINE_NUM; i++)
        {
            GameObject vLine = Instantiate(vLineObj, this.transform);
            vLine.name = "VLine_" + i;
            LineVertical vLineCS = vLine.GetComponent<LineVertical>();
            vLineCS.SetPosition(vpos, vpos + new Vector3(0, 0, VLINE_INIT_DISTANCE));
            vLineCS.space = VLINE_SPACE;
            vLineCS.SetColliderPos();
            vpos += new Vector3(VLINE_SPACE, 0, 0);
        }
    }

    // マウスが触れているときの処理
    public void MouseOnVLine(RaycastHit hit)
    {
        mousePos = new Vector3(hit.point.x, 0, hit.point.z);
        if (hit.collider.gameObject.tag == "VLine")
        {
            mouseCS.MouseDragEvent.AddListener(OnMouseDrag);
            mouseCS.MouseUpEvent.AddListener(OnMouseUp);
            onLinePos = new Vector3(hit.collider.gameObject.transform.position.x, 0, hit.point.z);  
            startPosObj.transform.position = onLinePos;
        }
        else if(hit.collider.gameObject.tag != "HLine"){
            mouseCS.MouseDragEvent.RemoveAllListeners();
            mouseCS.MouseUpEvent.RemoveAllListeners();
            if (isMouseDown){
                Destroy(lineCSs[lineCSs.Count - 1].gameObject);
                lineCSs.RemoveAt(lineCSs.Count - 1);
                isMouseDown = false;
            }
        }
    }

    // マウスをドラッグ中の処理
    private void OnMouseDrag()
    {
        if (isMouseDown)
        {
            endPosObj.transform.position = onLinePos;
            endPos = mousePos;
            lineCSs[lineCSs.Count - 1].SetPosition(startPos, endPos);
        }
        else
        {
            GameObject childLine = Instantiate(hLineObj, this.transform);
            childLine.name = "HLine_" + lineCSs.Count;
            lineCSs.Add(childLine.GetComponent<LineHorizontal>());
            startPos = onLinePos;
            isMouseDown = true;
        }
    }

    // マウスを離したときの処理
    private void OnMouseUp()
    {
        if (isMouseDown)
        {
            if (Math.Abs(startPos.x - onLinePos.x) > VLINE_SPACE || startPos.x == onLinePos.x)
            {
                Destroy(lineCSs[lineCSs.Count - 1].gameObject);
                lineCSs.RemoveAt(lineCSs.Count - 1);
            }
            else
            {
                lineCSs[lineCSs.Count - 1].ConfirmPos(startPos, onLinePos);
            }
            isMouseDown = false;
        }
    }
}

LineBase.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 線の親クラス
public class LineBase : MonoBehaviour
{
    [System.NonSerialized] public LineRenderer lineRenderer;
    [System.NonSerialized] public BoxCollider lineCollider;
    public Vector3 startPos { get; set; }
    public Vector3 endPos { get; set; }

    protected virtual void Awake()
    {
        lineRenderer = gameObject.GetComponent<LineRenderer>();
        lineCollider = gameObject.GetComponent<BoxCollider>();
    }

    // 線の始点と終点を設定
    public void SetPosition(Vector3 start, Vector3 end){
        startPos = start;
        endPos = end;
        lineRenderer.SetPosition(0, start);
        lineRenderer.SetPosition(1, end);
        transform.position = (startPos + endPos) / 2;
    }

    public float GetDistance(){
        return (endPos - startPos).magnitude;
    }

    public Vector3 GetDirection(){
        return (endPos - startPos).normalized;
    }

    public float GetSlant(){
        return (endPos.z - startPos.z) / (endPos.x - startPos.x);
    }

    public Vector3 GetCenter(){
        Vector3 pos = (startPos + endPos) / 2;
        return new Vector3(pos.x, pos.z, pos.y);
    }
}

LineHorizontal.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 横線のクラス
public class LineHorizontal : LineBase
{
    protected override void Awake(){
        base.Awake();
        lineCollider.enabled = false;
    }

    // 線の始点と終点を確定
    public void ConfirmPos(Vector3 start, Vector3 end){
        // Debug.Log(gameObject.name + "確定!");
        SetPosition(start, end);
        lineRenderer.SetPosition(0, start);
        lineRenderer.SetPosition(1, end);
        SetColliderPos();
        this.gameObject.transform.Rotate(0.0f, 0.0f, (float)Mathf.Atan(GetSlant()) * Mathf.Rad2Deg);   
    }

    public void SetColliderPos(){
        lineCollider.enabled = true;
        lineCollider.size = new Vector3(GetDistance() + 0.5f, 1.0f, 1.0f);
    }
}

LineVertical.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 縦線のクラス
public class LineVertical : LineBase
{
    public float space { get; set; }

    public void SetColliderPos(){
        lineCollider.size = new Vector3(space, GetDistance(), 0.1f);
    }
}

解説

MouseController.cs

マウス入力を受け取るクラス。解説は下の記事でしている。

LineBase.cs

線クラスの基底クラス。始点、終点、線の傾きや長さを返すメソッドを持つ。

また、線の描画にはLinrendererを利用している。

Line Renderer - Unity マニュアル
Line Renderer (ラインレンダラー) コンポーネントは、3D 空間の 2 つ以上の点の配列をとり、それぞれの間に直線を描きます。1 つのラインレンダラーを使用して、単純な直線から複雑な螺旋まで、どんなものでも描画することができま...

LineHorizontal.cs

横線のクラス。LineBaseを継承しており、線の位置が確定したときの処理をするメソッドを持っている。位置の確定をし、Colliderを有効にする。

あみだくじ上をオブジェクトを動かす際に干渉しないように、位置が確定するまで、Colliderを無効にするようにしている。

LineVertical.cs

縦線のクラス。LineBaseを継承しており、Colliderの大きさを決めるメソッドを持っている。

縦線の当たり判定は隣の縦線とつながるように広く取っている。

LinesController.cs

線を引く処理の根幹はこのクラスにある。マウスの入力をトリガーに各メソッドが実行されることで線が引ける。また、縦線の生成もこのクラスでしている。

縦線の生成

GenerateVLine()メソッドで縦線を生成している。

線が偶数のときと、奇数のときで分けて、左端の縦線の座標を計算し、そこからVLINE_SPACE分ずらしながら設置していく。

マウスが線上にあるとき

マウスと縦線が触れているとき、マウスの位置とそれに対応する縦線上の位置を取得する。

// マウスの座標
mousePos = new Vector3(hit.point.x, 0, hit.point.z);
// 線上の対応する座標
onLinePos = new Vector3(hit.collider.gameObject.transform.position.x, 0, hit.point.z);  

X座標は触れているオブジェクトの座標、Y、Z座標はmousePosと同じにしている。

ここでhitはRaycastHitの変数で、GameObjectを直接取得することはできないので、一度衝突しているcolliderを取得してからオブジェクトを取得し、その座標を取得している。

RaycastHitが持っている値は以下のページで確認できる。

RaycastHit - Unity スクリプトリファレンス
レイキャストによる情報を得るための構造体

また、MouseOnVLine関数自体はマウスが何らかのオブジェクトに触れているときに実行されるので、縦線以外に触れているときは線を引けないようにする処理も後半部分に書いている。

else if(hit.collider.gameObject.tag != "HLine"){
    mouseCS.MouseDragEvent.RemoveAllListeners();
    mouseCS.MouseUpEvent.RemoveAllListeners();
    if (isMouseDown){
        Destroy(lineCSs[lineCSs.Count - 1].gameObject);
        lineCSs.RemoveAt(lineCSs.Count - 1);
        isMouseDown = false;
    }
}

マウスがドラッグされているとき

ドラッグ中は横線のプレビューを表示する。

ドラッグ中は、始点はマウスを押したときのonLinePos、終点をmousePosとする線を表示する。

プレハブからオブジェクトを生成し、そのLineRendererの始点と終点を設定する。オブジェクトの生成は、マウスドラッグが開始された最初のフレームだけなので、ドラッグ中か判定する変数、isMouseDownを導入している。

1フレーム目の処理

GameObject childLine = Instantiate(hLineObj, this.transform);
childLine.name = "HLine_" + lineCSs.Count;
lineCSs.Add(childLine.GetComponent<LineHorizontal>());
startPos = onLinePos;
isMouseDown = true;

マウスが離されたとき

マウスを離したときに、線の位置を確定する。始点と終点が同じ縦線上になってしまったときもしくは、縦線をまたぐような線になるときは線を消去しキャンセルする。

横線を引く場合は横線のConfirmPos()を呼び出して確定する。

参考

Line Renderer - Unity マニュアル
Line Renderer (ラインレンダラー) コンポーネントは、3D 空間の 2 つ以上の点の配列をとり、それぞれの間に直線を描きます。1 つのラインレンダラーを使用して、単純な直線から複雑な螺旋まで、どんなものでも描画することができま...
RaycastHit - Unity スクリプトリファレンス
レイキャストによる情報を得るための構造体
【Unity】C#における継承とは?初心者向けに解説