C#
Unity

UnityでListにした座標を辿る

はじめに

入力でパスを引いてオブジェクトをその上を動くようにしたいので、サンプルを作りました。

今回のコードを下記に置きました。
https://github.com/jnhtt/FollowPointList

完成はこんな感じ
FollowPointList.gif

環境

  • MacBook Pro(15-inch, 2017)
  • Unity 2018.2.2f1

概要

パスを引く処理とパスに沿って動く処理を分けて説明します。

パスを引く

LayerにGroundとMoverを追加します。
地面になるオブジェクトのLayerをGroundに、動くオブジェクトのLayerをMoverに設定します。

概要

  • Input.GetMouseDownでLayer設定がMoverにカーソルが合っているか判定
  • Input.GetMouseでLayer設定がGroundになっているものにカーソルが合っている間は位置を保存します。
  • Input.GetMouseUpで保存した位置のリストをMoverに渡して移動を開始します。
マウス操作
    private void Update()
    {
        if (Input.GetMouseButtonDown(0)) {
            SelectMover(Input.mousePosition);
        } else if (Input.GetMouseButton(0)) {
            DrawPath(Input.mousePosition);
        } else if (Input.GetMouseButtonUp(0)) {
            mover.StartMove();
            selected = false;
        }
    }

今回はInputを使いましたが、uGUIのEventSystemsのOnBeginDrag, OnDrag, OnEndDragを使うことでUI上の操作の衝突を最小限にすることも可能です。

オブジェクトの選択

マウスを押下した時にMoverレイヤーのオブジェクト上だった場合に、パスの生成を開始します。
mover.ShowPath(true);は引いているパスを表示開始する処理です。

オブジェクト選択
    private void SelectMover(Vector3 screenPos)
    {
        int layerMask = 1 << LayerMask.NameToLayer("Mover");
        Ray ray = refCamera.ScreenPointToRay(screenPos);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, float.MaxValue, layerMask)) {
            pathPointList.Clear();
            pathPointList.Add(hit.collider.gameObject.transform.position + UP_OFFSET);
            mover.ShowPath(true);
            selected = true;
        }
    }

パスの生成

MoverレイヤーをInput.GetMouseDownで選択できていないとreturnで直ぐに関数を抜けます。
pathPointListにマウス位置からRayを飛ばしてGroundレイヤーと交差した位置を保存し続けます。これがパスになります。
mover.SetPathPointListでパスを渡すとLineRendererがパスを表示します。

パス生成
    private void DrawPath(Vector3 screenPos)
    {
        if (!selected) {
            return;
        }
        int layerMask = 1 << LayerMask.NameToLayer("Ground");
        Ray ray = refCamera.ScreenPointToRay(screenPos);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, float.MaxValue, layerMask)) {
            pathPointList.Add(hit.point + UP_OFFSET);
            mover.SetPathPointList(pathPointList);
        }
    }

今回はUpdateの度に実行されますが、EventSystemsのOnDragを使うと処理負荷はマシになります。動かさないとOnDragイベントが飛ばないためです。

パスの作成終了

Input.GetMouseUpのタイミングでパス生成を終了し、mover.StartMove();でパスに沿った移動を開始します。

パスに沿って動かす

Mover.StartMoveを呼び出した後からUpdateでパスに沿って動く処理が実行されます。

Mover.Update
    private void Update()
    {
        float deltaTime = Time.deltaTime;
        if (pathMove != null && !pathMove.IsEnd) {
            transform.position = pathMove.GetPosition(deltaTime, RemovePoint);
        }
    }

パスのリストから位置を取得

パスを成す2点間から現在位置を計算します。
最近通過した点のインデックスをcurrentIndex、2点間のどの位置にいるかをmovedDistanceで保持します。
movedDistancePが2点間の距離を超えたらcurrentIndex`を進めます。
これをパスの最後のインデックスまで続けます。

また、onRemoveは通過済み点をLineRendererから削除するコールバックです。

PathMove.csの一部抜粋
...
public class PathMove
{
    ...
    protected int currentIndex;
    protected float movedDistance;
    ...

    public virtual Vector3 GetPosition(float deltaTime, System.Action onRemove)
    {
        if (pathPointList == null) {
            return Vector3.zero;
        }
        int last = pathPointList.Count - 1;
        int startIndex = currentIndex;
        int endIndex = currentIndex + 1;
        if (last < endIndex)
        {
            IsEnd = true;
            return pathPointList[pathPointList.Count - 1];
        }

        Vector3 start = pathPointList[startIndex];
        Vector3 end = pathPointList[endIndex];
        float dist = Vector3.Distance(end, start);

        movedDistance += deltaTime * speed;

        while (dist <= movedDistance && startIndex < last)
        {
            currentIndex++;
            if (onRemove != null)
            {
                onRemove();
            }
            startIndex = currentIndex;
            endIndex = currentIndex + 1;
            movedDistance -= dist;
            if (last < endIndex)
            {
                endIndex = startIndex;
            }

            start = pathPointList[startIndex];
            end = pathPointList[endIndex];
            dist = Vector3.Distance(end, start);
        }

        float t = dist <= 0f ? 1f : movedDistance / dist;
        return t * end + (1f - t) * start;
    }
}

さいごに

マウスでMoverにレイヤー設定したオブジェクトをドラッグするとLineRendererがパスを生成して、マウス操作を終了するとパスに沿って動くようになったと思います。
操作できるレイヤーが複数ある場合や、操作としてピンチイン/ピンチアウトがあると面倒な処理が増えそうです。

パスを生成してオブジェクトを動かせるようになると、ゲームのイメージが湧いてくるので作れるようになると夢が広がる気がします。