[A* 알고리즘] 07_path smoothing

jh Seo·2023년 9월 4일
1

A* 알고리즘

목록 보기
7/8

개요

https://www.youtube.com/playlist?list=PLFt_AvWsXl0cq5Umv3pMC9SPnKjfp9eGW
위의 재생목록 링크를 통해 유니티에서 A* 알고리즘을 공부하며 정리하는 글이다.

이번엔 08,09 영상을 한번에 보고 정리하는 글이다.
요약하자면 현재 simplify된 노드들을 따라갈때 경로가 노드쪽으로만 가므로
노드 방향이 꺾일때마다 부자연스럽게 방향을 바로 바꿔서 간다.
이걸 더 자연스럽게 가기 위해 방향이 꺾일때 회전을 줘서 부드럽게 가게 만든다.

Line구조체

각 노드는 이전 노드방향으로 turnDst값 만큼 이동한 후 해당 방향과 수직되는 기울기를 가지는
선을 갖는다.

위 그림처럼 n번째 노드의 선분은
n번째 노드에서 n-1번째 노드방향으로 turnDst만큼 이동 후, 현재 방향에 수직이되는 선이 된다.

이 선이 Line구조체다.

전체 Line 코드

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

public struct Line
{
	//분모가 0일걸 대비해서 엄청큰 기울기 저장
   const float verticalLineGradient = 1e5f;

   //현재 Line의 기울기
   float gradient;
   //y=ax+b 에서 b를 말하는 부분 y축과 닿아서 y_intercept라고 하나봄
   float y_intercept;

   //현재 Line위의 점 두개
   Vector2 pointOnLine_1;
   Vector2 pointOnLine_2;

   //인자로 넘어온 pointPerpendicularToLine의 선분의 기울기
   float gradientPerpendicular;
   //접근 방향(초기값은 false)
   bool approachSide;

   //점두개로 선분구성하는 함수
   public Line(Vector2 pointOnLine, Vector2 pointPerpendicularToLine)
   {
       float dx= pointOnLine.x - pointPerpendicularToLine.x;
       float dy= pointOnLine.y - pointPerpendicularToLine.y;

       
       if (dx == 0)
       {
           gradientPerpendicular = verticalLineGradient;
       }
       else
       {
           gradientPerpendicular = dy / dx;

       }

       if (gradientPerpendicular == 0)
       {
           gradient = verticalLineGradient;
       }
       else
       {
           gradient = -1 / gradientPerpendicular;
       }

       y_intercept = pointOnLine.y - gradient * pointOnLine.x;
       pointOnLine_1 = pointOnLine;
       pointOnLine_2 = pointOnLine + new Vector2(1,gradient) ;

       approachSide = false;
       approachSide = GetSide(pointPerpendicularToLine);
   }

   bool GetSide(Vector2 p)
   {
       return (p.x-pointOnLine_1.x) * (pointOnLine_2.y- pointOnLine_1.y) > (p.y - pointOnLine_1.y) * (pointOnLine_2.x- pointOnLine_1.x);
   }

   public bool HasCrossedLine(Vector2 p)
   {
       return GetSide(p) != approachSide;
   }

   public float DistanceFromPoint(Vector2 p)
   {
       float yInterceptPerpendicular = p.y - gradientPerpendicular * p.x;
       float intersectX = (yInterceptPerpendicular - y_intercept) / (gradient - gradientPerpendicular);
       float intersectY = gradient * intersectX + y_intercept;
       return Vector2.Distance(p, new Vector2(intersectX,intersectY));
   }
   public void DrawWithGizmos(float length)
   {
       //기울기 방향으로 라인
       Vector3 lineDir = new Vector3(1, 0, gradient).normalized;

       Vector3 lineCentre = new Vector3(pointOnLine_1.x, 0, pointOnLine_1.y) + Vector3.up;
       //lineCentre에서 length만큼 좌우로 그리는 연산
       Gizmos.DrawLine(lineCentre- lineDir*length/2f, lineCentre+ lineDir*length/2f);
   }
}

생성자

//점 두개로 선분구성하는 함수
public Line(Vector2 pointOnLine, Vector2 pointPerpendicularToLine)
{
	float dx= pointOnLine.x - pointPerpendicularToLine.x;
	float dy= pointOnLine.y - pointPerpendicularToLine.y;

        
    if (dx == 0)
    {
    	gradientPerpendicular = verticalLineGradient;
	}
    else
    {
    	gradientPerpendicular = dy / dx;

	}

    if (gradientPerpendicular == 0)
    {
    	gradient = verticalLineGradient;
    }
    else
    {
		gradient = -1 / gradientPerpendicular;
    }

    y_intercept = pointOnLine.y - gradient * pointOnLine.x;
    pointOnLine_1 = pointOnLine;
    pointOnLine_2 = pointOnLine + new Vector2(1,gradient) ;

    approachSide = false;
    approachSide = GetSide(pointPerpendicularToLine);
}

첫번째 인자로는 선 위의 점 하나, 두번째 인자로 현재 line구조체와 수직을 이루는 선의 점하나를 받는다.

수직을 이루는 점을 받을 때, 현재점으로 접근방향을 알기 위해 현재 선분에서 현재 노드쪽이 아닌
이전 노드쪽의 점을 받는다. (위 그림으로 예시를 들면 n-1번 노드쪽 점 )

dy/dx값은 첫번째 인자와 두번째 인자로 구한 기울기이므로 line구조체와 수직을 이루는 선의 기울기다.

gradientPerpendicular = dy / dx;

수직인 두 선의 기울기를 곱하면 -1이 나오므로 원래 선분의 기울기는

gradient = -1/ gradientPerpendicular;

기울기와 선을 지나는 점을 알았으므로, y절편을 구할 수 있다.

y_intercept = pointOnLine.y - gradient * pointOnLine.x;

이 선분을 지나는 두 점을 구해보자.
한 점은 첫 번째 인자로 받은 pointOnLine이고, 두 번째 인자는 pointOnLine에 기울기만큼의 증가량을 더한 값이다.

pointOnLine + new Vector2(1,gradient);

이제 approach 값 즉 접근 방향을 구할 차례이다.
접근 방향은 후술할 GetSide함수로 접근방향을 구한다.
한가지 주의할 점은 approachSide를 초기화해줘야한다.

   approachSide = false;
   approachSide = GetSide(pointPerpendicularToLine);

GetSide(Vector2 p)함수

//(pointOnLine_2.y- pointOnLine_1.y) / (pointOnLine_2.x- pointOnLine_1.x) >
//(p.y - pointOnLine_1.y) / (p.x - pointOnLine_1.x)
// pointOnLine2와 pointOnLine1 사이 기울기와 p와 pointOnLine1 사이 기울기를 비교하는 방식이다.
//해당 값은 결국 현재 선분의 왼쪽에 위치하냐 오른쪽에 위치하냐를 나타내는 것으로 
//두 점의 GetSide값이 같으면 현재 선분 기준으로 같은 쪽에 위치하게된다.
  return (p.x-pointOnLine_1.x) * (pointOnLine_2.y- pointOnLine_1.y) > 
  (p.y - pointOnLine_1.y) *  (pointOnLine_2.x- pointOnLine_1.x);

이 식은 어디서 나온 식이냐 하면 사실 영상에서 다음 영상에서 알려준다 해놓고 안 알려줬다.
따라서 혼자 고민해서 나온 결과다.

위 식은 (pointOnLine_2.y- pointOnLine_1.y) / (pointOnLine_2.x- pointOnLine_1.x) >
(p.y - pointOnLine_1.y) / (p.x - pointOnLine_1.x) 으로 식을 풀어 쓸 수 있고,

이 값은 주석에 적은 것과 같이 pointOnLine2와 pointOnLine1 사이 기울기와 p와 pointOnLine1 사이 기울기를 비교하는 방식이다.

따라서 두 점의 GetSide함수의 반환값이 같다면 현재 선분 기준으로 같은 쪽에 있다고 말할 수 있다ㅏ.
반환값이 다르다면 두 점은 현재 선분 기준으로 다른쪽에 있다.

p가 p1-p2 선분 오른쪽에 있을 때와 p1-p2 선분 왼쪽에 있을 때가 기울기 값이 차이난다.
위 그림같은경우 p1-p2선분의 왼쪽에 있을때 p1-p2선분보다 기울기가 크고,
오른쪽에 있을때는 기울기가 작다.

HasCrossedLine(Vector p)함수

public bool HasCrossedLine(Vector2 p)
{
	return GetSide(p) != approachSide;
}

p값으로 GetSide함수를 호출한 후, 미리 구한 선분의 approachSide값과 비교해
같은지 판단한다.

DistanceFromPoint(Vector2 p)함수

public float DistanceFromPoint(Vector2 p)
{
	float yInterceptPerpendicular = p.y - gradientPerpendicular * p.x;
    float intersectX = (yInterceptPerpendicular - y_intercept) / (gradient - gradientPerpendicular);
    float intersectY = gradient * intersectX + y_intercept;
    return Vector2.Distance(p, new Vector2(intersectX,intersectY));
}

p벡터에서 현재 line 구조체와 얼마나 떨어져있는지 체크하기 위한 함수다.
사용하는 곳은 후술할 Unit클래스에서 이뤄질 easing 부분이다.

먼저 현재 line구조체와 수직을 이루는 기울기는 이미 gradientPerpendicular로 구했다.

따라서 점 p를 지나며 gradientPerpendicular을 기울기로 두는 함수는
y - p.y = gradientPerpendicular(x - p.x)이다.

해당 함수의 y절편값인 yInterceptPerpendicular는 p.y - gradientPerpendicular * p.x 가 된다.

해당 선분과 현재 선분의 교차점을 구해보자
y= ax+b, y= cx+d 이 란 두식의 교차점을 구하면
ax+b= cx+d
=> (a-c) x = d-b
=> x = ( d - b ) / ( a - c ) , y = a ( d - b ) / (a - c ) * a +b가 된다.
이 식에 따라 위 식의 x절편은 ( yInterceptPerpendicular - y_intercept ) / ( gradient - gradientPerpendicular ) 가 되고,
y절편은 gradient * intersectX + y_intercept가 된다.

이제 해당 x절편, y절편 벡터와 p벡터의 Distance를 구해 리턴하면 된다.

DrawWithGizmos함수

    public void DrawWithGizmos(float length)
    {
        //기울기 방향으로 라인
        Vector3 lineDir = new Vector3(1, 0, gradient).normalized;

        Vector3 lineCentre = new Vector3(pointOnLine_1.x, 0, pointOnLine_1.y) + Vector3.up;
        //lineCentre에서 length만큼 좌우로 그리는 연산
        Gizmos.DrawLine(lineCentre- lineDir*length/2f, lineCentre+ lineDir*length/2f);
    }

현재 line구조체를 그려주는 함수다.
중심을 그리고, 중심에서 받아온 length/2만큼 양옆으로 늘린다.
Unit 클래스에서 path클래스의 DrawWithGizmos함수를 OnDrawGizmos에서 호출하고,
path클래스의 DrawWithGizmos함수에서 line클래스의 DrawWithGizmos를 호출한다.

Path클래스

전체 경로를 simplify한 값을 인자값으로 받아와서 각 노드에 매칭되는 Line구조체를
turnBoundaries배열에 저장해준다.

마지막 노드는 특성상 해당 노드로 바로 unit 오브젝트가 바로 와야하기 때문에, line구조체를 가지지 않는다.
따라서 마지막 노드는 finishLineIndex로 따로 관리해준다.
easing작업을 위해 slowDownIndex도 따로 빼줬다.

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

public class Path 
{
    public readonly Vector3[] lookPoints;
    public readonly Line[] turnBoundaries;
    public readonly int finishLineIndex;
    public readonly int slowDownIndex;

    public Path(Vector3[] lookPoints, Vector3 startPos , float turnDist, float stoppingDst)
    {
        this.lookPoints = lookPoints;
        turnBoundaries = new Line[lookPoints.Length];
        finishLineIndex= turnBoundaries.Length-1;

        Vector2 previousPoint = V3ToV2(startPos);
        for(int i = 0; i < lookPoints.Length; i++)
        {
            Vector2 currentPoint= V3ToV2(lookPoints[i]);
            Vector2 dirToCurrentPoint= (currentPoint - previousPoint).normalized;
            //i가 lookPoints의 마지막 인덱스일때 해당벡터에서 turnDIst를 뺀 벡터로 향하는게 아닌 마지막 벡터로 곧장 가야함.  
            Vector2 turnBoundaryPoint =(i==finishLineIndex)? currentPoint : currentPoint - dirToCurrentPoint * turnDist;
            //만약 turnDist가 previousPoint~ currentPoint까지의 거리보다 더 길어버리면 Line에서 approachside를 잘못 지정함. 
            //따라서 turnBoundaryPoint에서 previousPoint로 가는게 아니라 previousPoint - turnDist값으로 가야함
            turnBoundaries[i] = new Line(turnBoundaryPoint, previousPoint - dirToCurrentPoint * turnDist);
            previousPoint = turnBoundaryPoint;
        }
        float dstFromEndPoint = 0;
        //끝점에서부터 시작점까지 순회하며 stoppingDst보다 큰 첫번째 index가 속도줄어드는 구간의 시작
        for(int i = lookPoints.Length - 1; i > 0; i--)
        {
            dstFromEndPoint += Vector3.Distance(lookPoints[i], lookPoints[i - 1]);
            if(dstFromEndPoint > stoppingDst)
            {
                slowDownIndex = i;
                break;
            }
        }
    }
    
    //게임 시점에서 y축을 나타내는 축이 z축이다.
    private Vector2 V3ToV2(Vector3 v3)
    {
        return new Vector2(v3.x, v3.z);
    }
    
    public void DrawWithGizmos()
    {
        Gizmos.color = Color.black;
        foreach(Vector3 p in lookPoints)
        {
            Gizmos.DrawCube(p + Vector3.up, Vector3.one);
        }
        Gizmos.color = Color.white;
        foreach(Line l in turnBoundaries)
        {
            l.DrawWithGizmos(10);
        }
    }
}

생성자

public Path(Vector3[] lookPoints, Vector3 startPos , float turnDist, float stoppingDst)
{
	this.lookPoints = lookPoints;
    turnBoundaries = new Line[lookPoints.Length];
    finishLineIndex= turnBoundaries.Length-1;

    Vector2 previousPoint = V3ToV2(startPos);
    for(int i = 0; i < lookPoints.Length; i++)
    {
    	Vector2 currentPoint= V3ToV2(lookPoints[i]);
        Vector2 dirToCurrentPoint= (currentPoint - previousPoint).normalized;
        //i가 lookPoints의 마지막 인덱스일때 해당벡터에서 turnDIst를 뺀 벡터로 향하는게 아닌 마지막 벡터로 곧장 가야함.  
        Vector2 turnBoundaryPoint =(i==finishLineIndex)? currentPoint : currentPoint - dirToCurrentPoint * turnDist;
        //만약 turnDist가 previousPoint~ currentPoint까지의 거리보다 더 길어버리면 Line에서 approachside를 잘못 지정함. 
        //따라서 turnBoundaryPoint에서 previousPoint로 가는게 아니라 previousPoint - turnDist값으로 가야함
        turnBoundaries[i] = new Line(turnBoundaryPoint, previousPoint - dirToCurrentPoint * turnDist);
        previousPoint = turnBoundaryPoint;
	}
    float dstFromEndPoint = 0;
    //끝점에서부터 시작점까지 순회하며 stoppingDst보다 큰 첫번째 index가 속도줄어드는 구간의 시작
    for(int i = lookPoints.Length - 1; i > 0; i--)
    {
    	dstFromEndPoint += Vector3.Distance(lookPoints[i], lookPoints[i - 1]);
        if(dstFromEndPoint > stoppingDst)
        {
        	slowDownIndex = i;
            break;
		}
	}
}

1번째 인자 lookPoints벡터 배열은 pathfinding클래스의 FindPath와 simplifyPath함수를 통해 얻어온 벡터 배열이다.
2번째 인자는 경로의 startpos 이고,
3번째 인자는 turnDst로 line구조체가 얼마만큼 노드에서 떨어져있는지 받아오는 변수다.
4번째 인자는 easing작업에 필요한 멈추기 시작하는 거리다.

반복문 내부는 lookpoint를 순회하며 각 노드의 line구조체를 생성해 turnBoundaries에 넣어주는 작업이다.
주의할 점은 turnBoundaries의 마지막 finishLineIndex인덱스의 line구조체는
마지막 노드에서 이전 노드로 turnDst만큼 이동한 위치가 아닌 마지막 노드 위치에 있어야한다.

turnBoundaries배열을 다 채웠다면 그 다음은 반대로 lookpoint.length-1값 부터 1까지 인덱스를 감소시키며
노드끼리의 거리를 dstFromEndPoint에 더해간다.

이 값이 stoppingDst값보다 처음으로 커지는 인덱스값을 slowDownIndex로 저장한다.

V3ToV2 함수와 DrawWithGizmos함수

    //게임 시점에서 y축을 나타내는 축이 z축이다.
    private Vector2 V3ToV2(Vector3 v3)
    {
        return new Vector2(v3.x, v3.z);
    }
    
    public void DrawWithGizmos()
    {
        Gizmos.color = Color.black;
        foreach(Vector3 p in lookPoints)
        {
            Gizmos.DrawCube(p + Vector3.up, Vector3.one);
        }
        Gizmos.color = Color.white;
        foreach(Line l in turnBoundaries)
        {
            l.DrawWithGizmos(10);
        }
    }

V3ToV2함수는 이제 우리가 x,z평면에서 실행하므로 생긴 함수이다.
vector3값을 우리가 사용할 때 y축은 어차피 고려를 안하므로 vector2로 사용하기 위해
(v3.x, v3.z)로 변환해 사용한다.

DrawWithGizmos함수는
위 line 구조체의 DrawWithGizmos함수를 호출하는 함수로 Unit클래스에서 호출된다.
경로의 각 노드를 검정 큐브로 그리고, 각 turnBoundaries의 line구조체마다 길이 10의 선분으로 그린다.

Unit클래스 수정

시작할때 바로 pathrequestManager에 requestPath를 하는것이 아닌 UpdatePath함수를 따로 구현했다.

이 코루틴을 Start함수에서 실행시킴으로 타겟위치를 변경해도 새로운 경로를 탐색 후, 해당 경로로 unit이 이동한다.

FollowPath함수도 변경해, turnBoundaries의 line구조체를 만나 HasCrossedLine함수가 true일 시,
다음 노드쪽으로 회전하며 자연스럽게 이동한다.

이제 pathIndex가 위 path클래스에서 구한 slowDownIndex보다 크거나 같게 되면,
speedPercent변수를 이용해 속도를 점차 감소시킨다.

주석처리한 부분들은 원래 있던 부분들이다.

Unit클래스 전체 코드

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

public class Units : MonoBehaviour
{
    const float minPathUpdateTime = .2f;
    const float pathUpdateMoveThreshold = .5f;

    public Transform target;
    public float speed=5f;
    public float turnSpeed = 3f;
    public float turnDst=5f;
    //for easying
    public float stoppingDist = 10;

    //Vector3[] path;
    //int targetIndex=0;
    Path path; 

    private void Start()
    {
        StartCoroutine(UpdatePath());
    }
    public void OnPathFound(Vector3[] newPath, bool pathSuccessful)
    {
        if (pathSuccessful)
        {
            //path = newPath;
            //targetIndex = 0;
            path = new Path(newPath, transform.position, turnDst,stoppingDist);
            StopCoroutine("FollowPath");    
            StartCoroutine("FollowPath");    
        }
    }

    private IEnumerator UpdatePath()
    {
        //시작하고 몇프레임동안은 time.deltatime이 비정상적
        if (Time.timeSinceLevelLoad < .3f)
        {
            yield return new WaitForSeconds(.3f);
        }
        PathRequestManager.RequestPath(transform.position,target.position, OnPathFound);

        float sqrMoveThresHold = pathUpdateMoveThreshold * pathUpdateMoveThreshold;
        Vector3 targetPosOld = target.position;

        while (true)
        {
            yield return new WaitForSeconds(minPathUpdateTime);
            if((target.position-targetPosOld).magnitude > sqrMoveThresHold)
            {
                PathRequestManager.RequestPath(transform.position, target.position, OnPathFound);
                targetPosOld=target.position;
            }
        }
    }
    private IEnumerator FollowPath()
    {
        //Vector3 currentWayPoint = path[0];
        bool followingPath = true;
        int pathIndex = 0;
        if (path !=null)
        transform.LookAt(path.lookPoints[0]);

        float speedPercent = 1;

        while (followingPath)
        {
            Vector2 pos2D = new Vector2(transform.position.x, transform.position.z);
            //속도를 증가시키면 한프레임당 여러 포인트를 뛰어넘을수있어서 if 를 while로 
            while (path.turnBoundaries[pathIndex].HasCrossedLine(pos2D))
            {
                if (pathIndex == path.finishLineIndex)
                {
                    followingPath = false;
                    break;
                }
                else
                    pathIndex++;
            }
            if (followingPath)
            {
                //slowDownIndex부터 속도 줄이기시작
                if (pathIndex >= path.slowDownIndex && stoppingDist > 0)
                {
                    speedPercent = Mathf.Clamp01(path.turnBoundaries[path.finishLineIndex].DistanceFromPoint(pos2D) / stoppingDist);
                    //너무 느려져서 해당 점에 도달하기까지 많이 걸릴수 있으므로 적당히 느려지면 컷
                    if (speedPercent < 0.01f)
                    {
                        followingPath = false;
                    }
                }
                Quaternion targetRotation = Quaternion.LookRotation(path.lookPoints[pathIndex] - transform.position);
                transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, Time.deltaTime * turnSpeed);
                transform.Translate(Vector3.forward * Time.deltaTime * speed*speedPercent, Space.Self);
            }
            //if(transform.position == currentWayPoint)
            //{
            //    targetIndex++;
            //    if (targetIndex >= path.Length)
            //        yield break;
            //    currentWayPoint = path[targetIndex];
            //}

            //transform.position = Vector3.MoveTowards(transform.position, currentWayPoint, speed * Time.deltaTime);
            yield return null;
        }
    }
    private void OnDrawGizmos()
    {
        if (path != null)
        {
            //for(int i = targetIndex; i < path.Length; i++)
            //{
            //    Gizmos.color = Color.black;

            //    if (i == targetIndex)
            //    {
            //        Gizmos.DrawLine(transform.position, path[i]);
            //    }
            //    else
            //        Gizmos.DrawLine(path[i - 1], path[i]);
            //    Gizmos.DrawCube(path[i], Vector3.one);
            //}
            path.DrawWithGizmos();
        }
    }
}

OnPathFound()함수

public void OnPathFound(Vector3[] newPath, bool pathSuccessful)
{
	if (pathSuccessful)
    {
    	//path = newPath;
    	//targetIndex = 0;
        path = new Path(newPath, transform.position, turnDst,stoppingDist);
        StopCoroutine("FollowPath");    
        StartCoroutine("FollowPath");    
	}
}

원래는 벡터배열 path에 인자로 들어온 배열 newPath를 할당했지만,
이제 새로 turnBoundaries 배열에 line 구조체를 채워야하므로, Path 클래스를 생성해준다.

UpdatePath()함수

   private IEnumerator UpdatePath()
    {
        //시작하고 몇프레임동안은 time.deltatime이 비정상적
        if (Time.timeSinceLevelLoad < .3f)
        {
            yield return new WaitForSeconds(.3f);
        }
        PathRequestManager.RequestPath(transform.position,target.position, OnPathFound);

        float sqrMoveThresHold = pathUpdateMoveThreshold * pathUpdateMoveThreshold;
        Vector3 targetPosOld = target.position;

        while (true)
        {
            yield return new WaitForSeconds(minPathUpdateTime);
            if((target.position-targetPosOld).magnitude > sqrMoveThresHold)
            {
                PathRequestManager.RequestPath(transform.position, target.position, OnPathFound);
                targetPosOld=target.position;
            }
        }
    }

타겟이 움직일 때 모든 자잘한 움직임에 새로 경로를 계산하는건 낭비이다.
sqrMoveThresHold변수가 의미하는 건, 경로갱신이 이뤄지는 거리 변화의 임계점이다.

반복문을 통해, 미리 지정한 minPathUpdateTime초마다 타겟의 위치를 이전위치와 비교해본다.
sqrMoveThresHold변수만큼 위치가 이동되었다면 새로운 경로를 요청후 ,
타겟의 이전 위치값을 현재위치로 갱신해준다.

requestPath함수가 시행되면 경로찾기가 끝나고 OnPathFound함수를 실행하기때문에 다시 길찾기가 활성화되고 unit이 움직인다.

함수 처음에 구현된 시간 비교하는 구문은 왜 있나 궁금할것이다.

//시작하고 몇프레임동안은 time.deltatime이 비정상적
if (Time.timeSinceLevelLoad < .3f)
{
	yield return new WaitForSeconds(.3f);
}

내 컴퓨터에선 정상적으로 되는 것 같은데 이분 영상에서 보면
시작하고 한스텝씩 움직여보면 첫 몇프레임동안 비정상적으로 unit가 빨리 움직인다.

이 분 설명을 들어보면 유니티가 play모드 들어가고 처음 몇 프레임은 Time.deltatime이 비정상적으로 작동한다고 한다.
그래서 Time.timeSinceLevelLoad을 이용해 level이 loaded되고 처음 0.3초정도는 실행을 하지 않고 기다려준다.

FollowPath함수

   private IEnumerator FollowPath()
    {
        //Vector3 currentWayPoint = path[0];
        bool followingPath = true;
        int pathIndex = 0;
        if (path !=null)
        transform.LookAt(path.lookPoints[0]);

        float speedPercent = 1;

        while (followingPath)
        {
            Vector2 pos2D = new Vector2(transform.position.x, transform.position.z);
            //if 를 while로 변경
            while (path.turnBoundaries[pathIndex].HasCrossedLine(pos2D))
            {
                if (pathIndex == path.finishLineIndex)
                {
                    followingPath = false;
                    break;
                }
                else
                    pathIndex++;
            }
            if (followingPath)
            {
                //slowDownIndex부터 속도 줄이기시작
                if (pathIndex >= path.slowDownIndex && stoppingDist > 0)
                {
                    speedPercent = Mathf.Clamp01(path.turnBoundaries[path.finishLineIndex].DistanceFromPoint(pos2D) / stoppingDist);
                    //너무 느려져서 해당 점에 도달하기까지 많이 걸릴수 있으므로 적당히 느려지면 컷
                    if (speedPercent < 0.01f)
                    {
                        followingPath = false;
                    }
                }
                Quaternion targetRotation = Quaternion.LookRotation(path.lookPoints[pathIndex] - transform.position);
                transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, Time.deltaTime * turnSpeed);
                transform.Translate(Vector3.forward * Time.deltaTime * speed*speedPercent, Space.Self);
            }
            //if(transform.position == currentWayPoint)
            //{
            //    targetIndex++;
            //    if (targetIndex >= path.Length)
            //        yield break;
            //    currentWayPoint = path[targetIndex];
            //}

            //transform.position = Vector3.MoveTowards(transform.position, currentWayPoint, speed * Time.deltaTime);
            yield return null;
        }
    }

반복문 내부를 설명해보자면, 현재 위치를 pos2D에 저장해놓는다.
위 OnPathFound에서 생성한 path의 turnBoundary를 순회하며 현재 pos2D가 넘지 않은
line구조체를 찾는다.

그 후, 아직 도착하지 않은 상태라면, 다음 if문으로 진입한다.
만약 현재 index가 slowDownIndex이고, 멈추기 시작하는 거리인 stoppingDst가 0이라면
speedPercent값을 조절하며 감속시킨다.

감속시키는 방법은 마지막 노드에서 현재 위치까지의 거리를

path.turnBoundaries[path.finishLineIndex].DistanceFromPoint(pos2D)

으로 알아낸 다음, stoppingDst값으로 나눈 후
Mathf.Clamp01함수를 통해 감속수치를 0~1사이 float형변수로 나타낸다.

pathIndex == finishLineIndex되기전에 느려져서 오래 걸릴걸 대비해
speedPercent가 0.01f보다 작아지면 followingPath를 false로 설정해 끝나게 한다.

이제 speedPercent구하기 까지 끝났으면
Quaternion.LookRotation함수를 이용해 다음 노드를 향한 방향을 알아낸다.

Quaternion targetRotation = Quaternion.LookRotation(path.lookPoints[pathIndex] - transform.position);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, Time.deltaTime * turnSpeed);
transform.Translate(Vector3.forward * Time.deltaTime * speed*speedPercent, Space.Self);

해당 방향으로 turnSpeed만큼 회전한 후, speedPercent값에 비례해 움직인다.

실행 예


이런식으로 경로 노드 앞에 line구조체들이 생긴 것을 gizmos를 통해 볼 수가 있다.


중간에 타겟 위치를 바꾸면 경로가 새로 설정되고 해당 경로로 unit이 잘 움직인다.

레퍼런스

sebastian Lague님의 유튜브 링크

profile
코딩 창고!

0개의 댓글