ゴミ箱

くだらないことからUnityの知識共有まで

画面に引いた線の認識をしたい

はじめに

 今回はとあるプロジェクトのプロトタイプで作っていたものが御破算になったので、未来の自分のための記録としてここにコードを残したいと思います。

元ネタ

www.google.com

 完成度の高いゲームです。画面に引いた線を縦、横、V字、逆V字、雷、ハートの6種類に判定し、敵を倒していくゲームです。ブラウザゲームなのでPCでもスマホでもできます。ぜひ一度やってみてください。

実装

 企画段階では画面に引く線を書道の線のようにしたい、とのことだったのでそれっぽくしています。

youtu.be

 とにかく実装できるかのテストだったのでif文ゴリゴリで書いてます。コード直すのも面倒だったので演出の処理も入っていますが気にしないでください。

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

public class LineCalculation : MonoBehaviour
{
    [Header("<設定用変数>")]
    [SerializeField] private float judgDist = 0.1f;
    [SerializeField] private float judgHriRange = 15;
    [SerializeField] private float judgVerRange = 20;
    [SerializeField] private float judgDiaRange = 50;
    [SerializeField] private float judgSirRange = 320;

    [SerializeField] private PRODUCTION productionType = PRODUCTION.AFTER_PARTICLE_EFFECT;

    [SerializeField] private float maxMoveDist = 5f;

    [SerializeField] private LineRenderer lrMain;
    [SerializeField] private Transform brush;

    [SerializeField] private ParticleSystem particle;

    [Header("<確認用変数>")]
    [SerializeField] private STATE state;
    [SerializeField] private LINE_TYPE lineType;

    /// <summary>フレームごとの点</summary>
    private Vector3 nowPos = Vector3.zero;
    private Vector3 beforePos = Vector3.zero;

    /// <summary>角度</summary>
    [SerializeField] private float theta;

    /// <summary>移動量を一時的に保存</summary>
    [SerializeField] private float moveDist;
    /// <summary>移動量の合計</summary>
    [SerializeField] private float totalMoveDist;

    private float firstLineHorizontal = 0;
    private float lastLineHorizontal = 0;

    private Vector3 firstHornPos;
    private float thetaHorn;

    [SerializeField] private bool isHorn = false;
    private int hornPosNum = 0;

    /// <summary>打った点座標</summary>
    [SerializeField] private List<Vector3> pos;

    /// <summary>計算するかしないか</summary>
    private bool isCalculation = false;

    private enum LINE_TYPE
    {
        NONE,
        HORIZONTAL,
        VERTICAL,
        V_CHARA,
        V_REVERSE,
        THUNDER,
        CIRCLE
    }

    Dictionary<LINE_TYPE, Color> lineTypeDic = new Dictionary<LINE_TYPE, Color>()
    {
        {LINE_TYPE.NONE, Color.black},
        {LINE_TYPE.HORIZONTAL, Color.red},
        {LINE_TYPE.VERTICAL, Color.blue},
        {LINE_TYPE.V_CHARA, Color.green},
        {LINE_TYPE.V_REVERSE, Color.yellow},
        {LINE_TYPE.THUNDER, Color.white},
        {LINE_TYPE.CIRCLE, Color.cyan}
    };

    private enum STATE
    {
        NONE,
        WAIT,
        MOVE,
    }

    /// <summary>演出の種類</summary>
    private enum PRODUCTION
    {
        NONE,
        AFTER_PARTICLE_EFFECT,
        CORRECT_LINE_DISPLAY
    }

    void Start ()
    {
        Init ();
    }

    void Update ()
    {
        if(Input.GetMouseButtonDown(0) && state == STATE.NONE)
        {
            state = STATE.MOVE;
        }

        if (Input.GetMouseButton(0) && state == STATE.MOVE)
        {   
            Move();
            GetMoveDist();
            JudgDist();
            Calculation();//計算
            UpdateLineColor();
            SetNextPos();
        }

        if (Input.GetMouseButtonUp(0) && state == STATE.MOVE)
        {
            state = STATE.WAIT;

            //! lineTypeによって何かする

            //! 演出の方法と初期化のタイミング
            switch(productionType)
            {
            case PRODUCTION.NONE:
                Init ();
                break;
            case PRODUCTION.AFTER_PARTICLE_EFFECT:
                StartCoroutine (EraseLine (() => {
                    Init ();
                }));
                break;
            case PRODUCTION.CORRECT_LINE_DISPLAY:
                Init ();
                break;
            }
        }
    }

    /// <summary>
    /// 線の点ごとに消す
    /// </summary>
    private IEnumerator EraseLine(System.Action callback)
    {
        brush.gameObject.SetActive (false);
        int count = lrMain.positionCount;
        for(int i = 0; i < count; i++)
        {
//         yield return new WaitForSeconds (0.001f);
            yield return null;
            Vector3[] lrNext = new Vector3[lrMain.positionCount - 1];
            for(int j = 0; j < lrNext.Length; j++)
            {
                lrNext [j] = lrMain.GetPosition (j + 1);
            }
            particle.gameObject.transform.position = lrMain.GetPosition (0);
            particle.Emit(10);
            lrMain.positionCount = lrNext.Length;
            lrMain.SetPositions(lrNext);
        }
        particle.Stop ();
        callback ();
        yield break;
    }

    /// <summary>
    /// 初期化
    /// </summary>
    private void Init()
    {
        lrMain.positionCount = 0;
        brush.gameObject.SetActive (false);

        nowPos = Vector3.zero;
        beforePos = Vector3.zero;
        moveDist = 0;
        totalMoveDist = 0;
        theta = 0;
        firstLineHorizontal = 0;
        lastLineHorizontal = 0;

        isHorn = false;
        thetaHorn = 0;

        pos = new List<Vector3> ();
        state = STATE.NONE;
        lineType = LINE_TYPE.NONE;
    }

    /// <summary>
    /// 移動
    /// </summary>
    private void Move()
    {
        //移動制限
        if (totalMoveDist > maxMoveDist) return;

        nowPos = Camera.main.ScreenToWorldPoint (new Vector3 (Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane + 1.0f));
    }

    /// <summary>
    /// 前のフレームと今のフレームでの位置から距離を計算
    /// </summary>
    private void GetMoveDist()
    {
        if(beforePos != Vector3.zero)
        {
            float frameDist = Vector3.Distance (nowPos, beforePos);
            moveDist += frameDist;
            totalMoveDist += frameDist;
        }
    }

    /// <summary>
    /// 次フレームで使うためのベクトルをセット
    /// </summary>
    private void SetNextPos()
    {
        beforePos = nowPos;
    }

    /// <summary>
    /// 一定距離離れたかの判定
    /// </summary>
    private void JudgDist()
    {
        if(pos.Count == 0)
        {
            lrMain.positionCount++;
            lrMain.SetPosition (lrMain.positionCount - 1, nowPos);
            pos.Add(nowPos);//初期点

            brush.gameObject.SetActive (true);
            brush.position = nowPos;
        }

        // 一定距離以上のとき点を追加する
        if(moveDist > judgDist)
        {
            isCalculation = true;


            int count = (int)(moveDist / judgDist);
            for(int i = 0; i < count; i++)
            {
                Vector3 lerp = Vector3.Lerp (beforePos, nowPos, (1f / (float)count) * ((float)i + 1f));
                lrMain.positionCount++;
                lrMain.SetPosition (lrMain.positionCount - 1, lerp);
                moveDist -= judgDist;
            }
            pos.Add (nowPos);
        }
    }

    /// <summary>
    /// 内積の加算と判定
    /// </summary>
    private void Calculation()
    {
        // 一定距離進んだ時以外は判定しない
        if (!isCalculation) return;

        isCalculation = false;

        // 点座標リストが少ないときは計算しない
        if (pos.Count < 3) return;

        //点ベクトルから線ベクトルへ
        Vector3 lineVecA = pos[pos.Count - 2] - pos[pos.Count - 3];
        Vector3 lineVecB = pos[pos.Count - 1] - pos[pos.Count - 2];

        // シータに加算
        theta += GetInnerProduct(lineVecA, lineVecB);
        // 初期座標から最終点までの線分ベクトルと、Xの単位ベクトルで内積
        firstLineHorizontal = GetInnerProduct(pos[pos.Count - 1] - pos[0], Vector3.right);
        // 中点から最終点の線分ベクトルと、Xの単位ベクトルで内積
        lastLineHorizontal = GetInnerProduct (pos[(int)(pos.Count / 2)] - pos[0], Vector3.right);

        // 横線の判定
        if(-judgHriRange < firstLineHorizontal && firstLineHorizontal < judgHriRange && -judgHriRange < lastLineHorizontal && lastLineHorizontal < judgHriRange ||
            180 - judgHriRange  < firstLineHorizontal && firstLineHorizontal < 180 + judgHriRange && 180 - judgHriRange < lastLineHorizontal && lastLineHorizontal < 180 + judgHriRange ||
            180 - judgHriRange < firstLineHorizontal && firstLineHorizontal < 180 + judgHriRange && - judgHriRange < lastLineHorizontal && lastLineHorizontal < judgHriRange)
        {
            lineType = LINE_TYPE.HORIZONTAL;
        }
        // V字判定
        else if(110 - judgDiaRange < theta && theta < 110 + judgDiaRange)
        {
            if(pos[0].y > pos[(int)(pos.Count/2)].y && pos[(int)(pos.Count/2)].y < pos[pos.Count - 1].y)
            {
                lineType = LINE_TYPE.V_CHARA;
            }
            else if(pos[0].y < pos[(int)(pos.Count/2)].y && pos[(int)(pos.Count/2)].y > pos[pos.Count - 1].y)
            {
                lineType = LINE_TYPE.V_REVERSE;
            }

            if (!isHorn)
            {
                isHorn = true;
                firstHornPos = pos [pos.Count - 2];
                hornPosNum = pos.Count - 2;
            }
        }
        // 縦線の判定
        else if(90 - judgVerRange < firstLineHorizontal && firstLineHorizontal < 90 + judgVerRange && 90 - judgVerRange <  lastLineHorizontal && lastLineHorizontal < 90 + judgVerRange)
        {
            lineType = LINE_TYPE.VERTICAL;
        }
        else if(judgSirRange < theta)
        {
            lineType = LINE_TYPE.CIRCLE;
        }
        else
        {
            lineType = LINE_TYPE.NONE;
        }

        // かみなりの判定
        if(isHorn)
        {
            thetaHorn = GetInnerProduct (firstHornPos - pos[hornPosNum + (int)((pos.Count - hornPosNum) / 2)], pos[hornPosNum + (int)((pos.Count - hornPosNum) / 2)] - pos[pos.Count - 1]);
            if(110 - judgDiaRange < thetaHorn && thetaHorn < 110 + judgDiaRange)
            {
                if(pos[0].y > firstHornPos.y && firstHornPos.y > pos[pos.Count - 1].y || 
                    pos[0].y < firstHornPos.y && firstHornPos.y < pos[pos.Count - 1].y)
                {
                    lineType = LINE_TYPE.THUNDER;
                }
            }
        }
    }

    /// <summary>
    /// 線の色を更新
    /// </summary>
    private void UpdateLineColor()
    {
        Color blendColor = lineTypeDic [lineType] - (Color.white / totalMoveDist * 0.5f);
        lrMain.startColor = Color.black;
        lrMain.endColor = blendColor;
    }

    /// <summary>
    /// 内積計算
    /// </summary>
    private float GetInnerProduct(Vector3 lineVecA, Vector3 lineVecB)
    {
        return Mathf.Acos (Vector3.Dot (lineVecA, lineVecB) / (lineVecA.magnitude * lineVecB.magnitude)) * Mathf.Rad2Deg;
    }
}

 一応、一式をパッケージ化したものを置いておきます。 github.com

終わりに

 考え方としては点を打ってベクトルの内積で角度を出してその角度で判定するって感じです。かなりいい加減ですが参考になれば幸いです。