지우너

[Unity] Lerp 사용법 본문

Programming/Tips

[Unity] Lerp 사용법

지옹 2023. 3. 3. 08:33

유튜브에서 봤던 내용이 유익하다고 생각돼서 개인적으로 정리해두려고 쓰는 글입니다.

[유니티] Lerp를 프로처럼 사용하는 방법 - 오늘코딩

오늘코딩 블로그

https://coding-of-today.tistory.com/

해당 영상에서 참고한 사이트

https://chicounity3d.wordpress.com/2014/05/23/how-to-lerp-like-a-pro/

https://gamedevbeginner.com/the-right-way-to-lerp-in-unity-with-examples/

 

 

보통 버튼을 눌렀을 때 이미지가 아래에서 위로 튀어나오게 보이도록 코딩하라고 하면 아래와 같이 코딩한다.

public class Move : MonoBehaviour
{
    public Transform startPosition;
    public Transform endPosition;

    void Start()
    {
    	this.transform.position = startPosition.position;
    }

    void Update()
    {
    	this.transform.position = Vector3.Lerp(this.transform.position, endPosition.position, 10*Time.deltaTime);
    }
}

Vector3.Lerp에다가 현재좌표, 도착지점의 좌표, 그리고 Time.deltaTime에 적당한 속도 값을 곱해서 움직임을 표현한다.

이 코드는 굉장히 잘못된 코드이고, 이렇게 코드를 짜게 되면 이후의 추가 작업이 굉장히 힘들어진다.

  기획자가 이미지가 움직이는 것을 보고, "0.5초 만에 도착 지점에 도착하게 해주세요"라고 이야기한다면 해당 코드를 수정하는 것은 굉장히 까다로워진다. 이 말은 Lerp를 잘못 쓰고 있다는 말이다.

 

방법1) 에셋스토어의 에셋을 이용하기

  이런 것들을 구현하기 위해 요새는 Tween류의 에셋들이 거의 필수처럼 사용되고 있다. 에셋스토어에 tween이라고만 검색해도 굉장히 많은 에셋이 나오는 것을 확인할 수 있다. 이런 에셋들을 이용하면 그런 움직임을 손쉽게 구현할 수 있다.

 

방법2) Lerp를 이용해서 직접 구현하기

  Vector3.Lerp()에는 매개변수 3개가 들어간다. Vector3.Lerp(시작좌표, 끝좌표, 0~1 사이의 보간값)

  실제로 값을 넣었다고 생각해보자. 시작좌표를 0, 끝좌표를 1이라고 한다면 3번째 매개변수로 들어간 보간값이 최종 좌표값을 결정하게 된다. 만약 0.5라면 시작좌표와 끝좌표의 정확히 중간값이 출력된다. 0.2라면 시작좌표에 근접, 0.8이라면 끝좌표에 더 근접한 좌표값이 반환된다. 

 

  위의 코드를 보면 매개변수로 현재위치, 도착 위치, 10*Time.deltaTime을 넣었다.

        ⭐️프레임 환경에 따라서 달라지지만 Time.deltaTime은 약 0.02f의 고정적인 값을 뱉어낸다. 

  따라서 3번째 매개변수로 들어간 10*Time.deltaTime은 단순하게 0.2f라고 쓴 것과 동일하다(사실 이것부터 잘못되었다).

  두 번째 문제는 시작좌표가 고정된 좌표가 아니라는 것이다. 단순하게 현재 위치를 넣었기 때문에 시작 좌표가 매번 달라질 수 있다. 이 코드를 실행하면 맨 처음 0.2에 해당하는 값을 반환하게 되고, 오브젝트의 위치는 그곳(0.2 이동한 좌표)으로 이동하게 된다. 다음 프레임에서는 변경된 위치를 0으로 취급해서 해당 좌표에서 0.2인 좌표를 계산, 시작 위치를 다시 그 위치로 이동한다. 계산할 때마다 기준점이 이렇게 바뀌기 때문에 반복될 때마다 프레임 별 이동거리가 짧아질 수밖에 없다.

  그래서 실제로 테스트를 해보면 처음에는 빠르게 올라오다가 도착하기 전에는 느려지는 것이다. 이 느낌을 의도했다 하더라도 좋은 방식은 아니다.

 

public class Move : MonoBehaviour
{
    public Transform startPosition;
    public Transform endPosition;
    
    float lerpTime = 0.5f;
    float currentTime = 0;

    void Start()
    {
    	this.transform.position = startPosition.position;
    }

    void Update()
    {
    	// currentTime은 시간 흐름에 따라 증가함
    	currentTime += Time.deltaTime;
        // 이 코드를 넣어주면 0부터 증가하다가 최대 0.5(lerpTime)까지만 증가함
        if (currentTime>=lerpTime)
        {
        	currentTime = lerpTime;
        }
        // currentTime/lerpTime 로 보간값을 넣어주면, 결과적으로 프레임마다 0~1까지 서서히 증가하게 된다.
    	this.transform.position = Vector3.Lerp(startPosition.position, endPosition.position, currentTime/lerpTime);
    }
}

  위의 코드를 제대로 고치기 위해서 우선 시작 좌표에 현재 위치를 넣는 것을 수정할 수 있다.

        this.transform.positionstartPosition.position

  현재 위치를 넣는 게 아니라 이런 식으로 고정된 위치를 정해놓고 항상 변하지 않도록 유지해주는 것이 좋다(영상 제작자 분은 start, end. 오브젝트를 만들어서 그 위치를 할당해서 넣어주었다. 이렇게 하지 않고 좌표값을 직접 넣어줘도 상관은 없다고 한다).

  다음으로 처리할 것은 보간값이다. 변수를 생성해주었다.

        float lerpTime = 0.5; float currentTime = 0; (lerpTime은 진행될 총 시간을 의미)

  currentTime에 Time.deltaTime을 곱해주면 시간 흐름에 따라 증가하게 되고, 조건문으로 currentTime이 lerpTime까지만 증가하도록 만들어준다. 이제 매개변수로 currentTime/lerpTime을 넣어주게 되면 프레임마다 0~1까지 서서히 증가하게 된다. 

        (왜지?? 보간값이 0~1 사이 값이어야 해서 그런 건가?? currentTime이 있고 없고가 무슨 차이지...? lerpTime을 3번째 매개 변수로 넣는 것과 currentTime을 0.5까지만 증가하도록 만들어서 lerpTime으로 나눈 값을 넣는 건 무슨 차이가 있지? 찾아보면서 글을 좀 더 수정해야겠다...)

  이런 식의 코딩이 효율적인 이유가 무엇이냐면, lerpTime을 어떻게 변경하든, 몇 초로 수정하든, 아래 Update()에 써둔 코드를 수정할 일은 전혀 없다는 것이다. lerpTime을 public으로 수정한 후, 기획자에게 "여기에 lerpTime을 직접 입력하시면 원하시는 속도로 조절할 수 있습니다." 이렇게 말하면 된다.

 

  이 코드의 문제가 있다면 맨 처음 코드(잘못된 코드라고 말했던 것)는 도착지점에 가까워질수록 느려지는 효과가 있었던 반면, 이 코드는 고정된 좌표에서 보간값이 일정하게 증가하기 때문에 일정한 속도로 올라온다는 것이다. 아까 전처럼 도착 쯤에 속도가 느려지게 하려면 어떻게 해야 하는가.

  이걸 위해서 스무스스텝이라는 걸 사용해야 한다. 이것의 원리는 정말 수학적인 것이기 때문에 설명하기 조금 어렵다.

https://chicounity3d.wordpress.com/2014/05/23/how-to-lerp-like-a-pro/

  이 사이트에 접속해보면 그래프들이 보인다. 그래프의 (0, 0)이 시작좌표고, 오른쪽 끝이 도착 좌표라고 생각하면서 보면 된다. 

t = 1f - Mathf.Cos(t * Mathf.PI * 0.5f)

  그래서 위와 같이 계산을 하게 되면 속도가 천천히 증가하다가 일정하게 유지되도록 만들 수 있고, 아래와 같이 계산하게 되면 속도가 천천히 증가하다가 최고 속도를 유지하게 되고, 도착에 가까워질수록 점점 느려지는 그런 움직임도 표현이 가능하다.

t = t*t*t * (t * (6f*t - 15f) + 10f)

  능력이 되는 분들은 수식을 직접 계산하셔도 좋지만, 그렇지 않은 분들은 smoothstep을 인터넷에 검색해보면 나오는 많은 계산 방식을 가져다 쓰면 된다. 

 

  이제 smoothstep을 코드에 적용시켜보자.

public class Move : MonoBehaviour
{
    public Transform startPosition;
    public Transform endPosition;
    
    float lerpTime = 0.5f;
    float currentTime = 0;

    void Start()
    {
    	this.transform.position = startPosition.position;
    }

    void Update()
    {
    	// currentTime은 시간 흐름에 따라 증가함
    	currentTime += Time.deltaTime;
        // 이 코드를 넣어주면 0부터 증가하다가 최대 0.5(lerpTime)까지만 증가함
        if (currentTime>=lerpTime)
        {
        	currentTime = lerpTime;
        }
        // 이전에 넣어뒀던 보간값을 t에 저장하고
        float t = currentTime/lerpTime;
        // sine 형태의 계산을 가져온다.
        t = Mathf.Sin(t * Mathf.PI * 0.5f);
        
    	this.transform.position = Vector3.Lerp(startPosition.position, endPosition.position, t);
    }
}

 

  계산을 어떻게 하냐에 따라 효과를 더 드라마틱하게 줄 수도 있고, 덜하게 줄 수도 있으니, 본인의 상황에 맞는 적절한 smoothstep을 찾아서 계산을 넣어주면 된다.

  사실 더 좋은 방법은 Update문에 이렇게 쓰는 게 아니라 별도의 코루틴을 하나 만들어서 필요한 만큼만 코루틴이 동작해서 움직이게 만드는 것이 더 좋다. Update문에 쓰게 되면 매 프레임마다 호출되기 때문에 최대한  Update문에 안 쓰는 것이 좋다(많이 쓰면 성능이 안 좋아짐).

 

  기획자가 요구를 할 때마다 코드를 갈아엎어야 하는 개발자 vs. 기획자가 직접 테스트 할 수 있도록 구현해주는 개발자. 둘 중 어떤 개발자가 더 좋은 개발자인지는 명확하다.

 

아래는 유튜브 댓글에 달린 팁!