본문 바로가기
Unity/Unity Overall

오리를 클릭하면 도망갔다가 돌아오는 로직 만들기 (feat. 닷트윈(DoTween))

by mati-as 2023. 10. 26.
반응형

나름 실력 키우겠다고,닷트윈 안쓰고 코루틴으로 끙끙거리면서 만들다 보니, 개발속도가 너무 늦어지고, 한 달밖에 안 지났는데 코드가 너무 복잡해서 유지보수가 너무 힘들었습니다. 이에 닷트윈을 활용해서 리팩토링을 해보았습니다.(사실상 코드 갈아엎다시피 해서 새로 기능구현 한 것이나 마찬가지긴 합니다..ㅎㅎ). 구현하고 나니까 하나의 예제로서 포스팅하기도 좋고, DOTween의 여러 기능을 한 번에 보여드릴 수 있을 것 같아 공유합니다. 이번 포스팅으로 닷트윈에서 나름 유용하게 쓸 수 있는 닷트윈의 7가지 API를 구경하실 수 있습니다.  당연하게도 모두 DOTween 다큐멘테이션에 있는 내용을 예제에 맞게 설명한 포스팅이니 혹시나 API설명이 부족하다고 느끼시거나 자세한 사용법, 파라미터를 알고 싶으시면 꼭 Dotween Documetation 참고하시는 것 추천드립니다.

 

일단 결과물부터 볼까요?

다른 부분 제하고, 다리 끝에 놓인 오리만 보시면 됩니다. 그나저나 아직 어색하고, 다듬을 부분이 좀 많긴 하네요..ㅎㅎ 먼저 큰 그림을 볼 겸 어떻게 구현했는지 알아볼게요

너무 지나친 요약이지만, 과정을 세 개로 나누면 위와 같습니다. 첫째로 에셋을 구해옵니다. 에셋은 직접 만든 건 아니고, 유니티 에셋스토어에서 구했습니다. 그다음 두 번째로 애니메이션을 토대로 애니메이션을 어떻게 구상하고 어떻게 움직이게 할지 설계합니다. 마지막으로 이를 토대로 필요한 기능을 구현했습니다.

 

이번에 새삼 느낀 점이 하나 있었는데요, 보통의 경우에, 기능을 토대로 애니메이션이 제작되는 경우가 대부분이라고 무의식적으로 생각했었는데, 애니메이션이 어느 정도 기획이 되어야 세부기능이 나오는 경우도 많이 있겠구나라는 것을 알게 되었습니다. 예를 들면 새가 날아가는 애니메이션을 넣을지 안 넣을지에 따라, 혹은 어떤 타이밍에 들어갈지에 따라 새의 움직임, 물리 관련 컴포넌트 설정이 많이 달라지겠죠. 즉  애니메이션 기획 또한 여타 시스템기획처럼 탄탄해야 구현이 수월해진다는 것을 느꼈습니다 ㅎㅎ.

 

애니메이션 설계(기획)

애니메이션을 어떻게 기획했는지, 또한 상태머신 설계도 어느 정도 보여드릴 것이지만, 아무래도 이번 포스팅은 닷트윈을 주제로 하다 보니 가볍게 설명해 보겠습니다. 또한 아래 애니메이션은 특정 로직에 의해 설계된 것이니, 다음 항목에서 로직을 설명하는 부분이랑 유기적으로 봐주셨으면 합니다.

왼쪽: 사용하고 있는 bool 값 파라미터, 오른쪽:애니메이션FSM 구조

위에 나온 오리의 애니메이션 변경 흐름은, 오리를 클릭했을 때만 순환하듯이 움직입니다. 즉 클린 하게 되면 Jump(Run)하게 되고, 그다음 물에서 Swim애니메이션을 재생하며, 땅 및 다시 돌아올 때 Walk애니메이션으로 돌아오게 됩니다. 

 

사용한 파라미터와 구체적으로 말씀드려 보겠습니다. 먼저 IDLE상태에 진입한 다음, Run 파라미터가 True가 되면 Jump(run) 애니메이션으로 전이가 됩니다, 참고로 점프하면서 발을 구르는 모션을 기획했기에 애니메이션 이름에 Jump가 들어있습니다.) 그다음도 Swim, Idle 등 파라미터에 설정에 따라 각각 Walk, Idle상태로 돌아오게 됩니다. 또한 참고로 Swim의 경우에는 Swim == false인 경우에 Walk애니메이션으로 이동할 수 있도록 했기에 따로 Walk 파라미터를 만들진 않았습니다. 사실 이 부분은 설계하기 나름인지라..

 

로직( DoTween)

코드 전문을 가져왔으나, 모든 로직을 다루기엔 양이 너무 많아서.. 일단 코드 전체를 보신다음,  animator랑 DoTween사용 부분만 다시 한번 봅시다. 예시로 만들다 보니 매직넘버가 좀 섞여있는 점 양해부탁드립니다.

 

코드전문을 보려면 아래 클릭

더보기
using UnityEngine;
using UnityEngine.EventSystems;
using DG.Tweening;

#if UNITY_EDITOR
using MyCustomizedEditor;
#endif

public class IdleLakeDuckOnBridgeController : MonoBehaviour
{
    public  readonly int IDLE_ANIM = Animator.StringToHash("idle");
    public  readonly int EAT_ANIM = Animator.StringToHash("Eat");
    public  readonly int RUN_ANIM = Animator.StringToHash("Run");
    public  readonly int SWIM_ANIM = Animator.StringToHash("Swim");
    
#if UNITY_EDITOR

[NamedArrayAttribute(new[]
{
    "Start", "Max_Height","End"
})]
#endif
public Transform[] duckFlyRoute = new Transform[3];

private Vector3[] _duckFlyRouteAVector = new Vector3[3];


#if UNITY_EDITOR

[NamedArrayAttribute(new[]
{
    "Start", "Max_Height","End"
})]
#endif
public Transform[] duckAwayRoute = new Transform[3];
private Vector3[] _duckAwayRouteVector = new Vector3[4];

private Animator _animator;
private bool _isClickedAnimStarted;
private float _defaultAnimSpeed;


private void Awake()
{
    _animator = GetComponent<Animator>();
    _defaultAnimSpeed = _animator.speed;
    
    //DOPath에서 사용할 경로를 읽어옵니다. 
    for (int i = 0; i < 3; i++)
    {
        _duckFlyRouteAVector[i] = duckFlyRoute[i].position;
      
    }
    for (int i = 0; i < 4; i++)
    {
        _duckAwayRouteVector[i] = duckAwayRoute[i].position;
      
    }
   
}


private void Start()
{
   //start에서는 클릭이벤트를 받아오기위한 설정을 진행합니다. 
    var trigger = GetComponent<EventTrigger>();
    var entry = new EventTrigger.Entry();
    entry.eventID = EventTriggerType.PointerClick;
    entry.callback.AddListener(data => { OnClicked(); });
    trigger.triggers.Add(entry);
}

public ParticleSystem waterEffect;
[Range(0,40)]
public float comingBackDuration;
[Range(0,40)]
public float increasedAnimationSpeed;
private void OnClicked()
{

    if (!_isClickedAnimStarted)
    {
        // FSM에서 직접 다루거나 별도의 빠른재생용 애니메이션 로직 설정하지 않고, 
        animator의 Speed로직을 설정합니다.
        _animator.SetBool(RUN_ANIM, true);
        
        _isClickedAnimStarted = true;
        var duration = 0.9f; // 움직임의 전체 기간 설정

        transform.DOPath(_duckFlyRouteAVector, duration, PathType.CatmullRom)
            .SetEase(Ease.InOutQuad)
            .OnComplete(() =>
            {
           
                waterEffect.transform.position = duckFlyRoute[2].position;
                waterEffect.Play();
                
                _animator.SetBool(RUN_ANIM, false);
              
                //오리가 회전할 방향을 계산하기
                var directionToLook = _duckAwayRouteVector[1] - transform.transform.position;
                var lookRotation = Quaternion.LookRotation(directionToLook);
             
                //오리 회전시키기
                transform.DORotate(lookRotation.eulerAngles, 1.6f)
                    .OnComplete(() =>
                    {   애니메이션 스피드 설정
                       _animator.speed = increasedAnimationSpeed;
                       
                       //DOPath를 사용해 오리가 경로를 따라 다시 다리위로 돌아오게하기
                        DOTween.Sequence()
                            .Append(transform.DOPath(_duckAwayRouteVector, comingBackDuration, PathType.CatmullRom)
                                .SetDelay(0f)
                                .SetLookAt(0.01f)
                                .SetEase(Ease.InOutQuad))
                            .InsertCallback(7f, // InsertCall 사용을 위해 애니메이션 시퀀스로 구성.
                                () => _animator.speed = _defaultAnimSpeed) // 애니메이션 중간에 속도를 기본값으로 설정
                            .OnComplete(() =>
                            {
                                _animator.SetBool(SWIM_ANIM, false);
                                _isClickedAnimStarted = false;
                            });
                    });
            });
    }  
  }
}

 

 

OnClick 메서드

transform.DOPath(_duckFlyRouteAVector, duration, PathType.CatmullRom)
    .SetEase(Ease.InOutQuad)
    .OnComplete(() =>
    {
        //파티클 위치 이동해서 재생하기 
        waterEffect.transform.position = duckFlyRoute[2].position;
        waterEffect.Play();
        
         //애니메이션 속도 바꾸기
        _animator.SetBool(FAST_RUN_ANIM, false);
       
         //어떻게 움직일지 계산하기
        var directionToLook = _duckAwayRouteVector[1] - transform.transform.position;
        var lookRotation = Quaternion.LookRotation(directionToLook);

전체 코드중에서 마지막부분의 OnClicked() 함수를 알아봅시다. 오늘 다루고자 하는 내용이 전부 들어있습니다. 리스트 형식으로 나열해서, 어떻게 사용했는지 덧붙여보겠습니다.   

사용한 DOTween API 설명

코드 때문에 아찔하실까 봐, 하나하나 뜯어보겠습니다. 

-1.DoPath (경로, 지속시간, 움직이는 방식) 

: 지속시간만큼 정해진 경로로 움직입니다. CatmullRom방식을 사용해서 3개의 위치만 저장했는데도 알아서 곡선값으로 보간하여 부드럽게 경로를 이동시켜 줍니다.

 

- 2.SetEase : Easing, 혹은 Tweening함수라고 부르는데, 자세한 정보는 여기서 보실 수 있습니다. 각종 자연스러운 움직임을 쉽게 구현하는데 많이 사용됩니다. 등속도, 등가속도, 혹은 통통 튀는 운동 등 다양한 함수가 Dotween에서는 바로 사용할 수 있게끔 API가 있습니다.

 

- 3.OnComplete: 닷트윈 애니메이션이 모두 끝난 후, 즉 여기서는 DoPath의 Duration 만큼 시간이 지난 후, 실행될 함수들을 설정할 수 있습니다. 위처럼 여러 실행문을 쓰고 싶다면 (()=> 로직 1; 로직 2;.....) 이런 식으로 사용하게 됩니다. 한 함수만 콜백 하고 싶다면 OnComplete(<함수명>)처럼도 쓸 수 있습니다.

 

저의 경우에는 두 개의 DoPath를 사용했는데요, 처음 DoPath는 클릭직후, 오리가 경로에 따라 점프하는 듯한 움직임을 구현하기 위한 DoPath입니다. 그리고 경로이동이 끝나자마자 실행하고 싶은 로직을 아래에 적은 것이죠. 

 

 

 transform.DORotate(lookRotation.eulerAngles, 1.6f)
              .OnComplete(() =>
              {   _animator.speed = increasedAnimationSpeed;
                 
                 
                 DOTween.Sequence()
                   .Append(transform.DOPath(_duckAwayRouteVector, comingBackDuration, PathType.CatmullRom)
                   .SetDelay(0f)
                   .SetEase(Ease.InOutQuad))
                   .InsertCallback(7f, // InsertCall 사용을 위해 애니메이션 시퀀스로 구성.
                   () => _animator.speed = _defaultAnimSpeed) // 애니메이션 중간에 속도를 기본값으로 설정
                   .OnComplete(() =>
                    {
                     _animator.SetBool(SWIM_ANIM, false);
                     _isClickedAnimStarted = false;
                    });
              });

위에선 DoPath를 먼저 알아보았는데, 사실 DoPath와 관련해서 SetEase, OnComplete 등을 사용하실 수 있다면, DORotate, DOMove 등 여러 움직임 관련 API도 손쉽게 사용하실 수 있으실 겁니다. 그럼 이어서 DORotate등등을 알아볼까요?

 

- 4. -DORotate(바라 볼 방향(오일러각), 지속시간)

: 직관적이게, 특정방향을 특정시간 동안 천천히 돌게끔 하는 API입니다. 

 

그다음 DOTween.Sequence()라는 API를 사용하였는데요. 순서대로 로직을 착착 진행되게끔 하고 싶은 경우에 유용합니다. 저는 DoPath실행 중간에 실행하고 싶은 로직이 있어서 이 API를 참고하게 되었는데요, 아래에 다시 리스트로 정리하면서 알아보죠.

 

- 5. -Append(사용할 시퀀스) : DoPath나 DoRotate 등은 모두 시퀀스타입으로 되어있습니다. 원하는 트윈함수를 Append에 넣어서 사용하면 됩니다.

 

- 6. -SetDelay( (시퀀스 자체를)  지연시킬 시간) : DOTween.Sequence() 실행을 얼마나 지연시킬 건지 정합니다. 특정 로직이 발동해서 시퀀스를 실행하고 싶은데, 어느 정도 지연실행이 되게끔 하고 싶을 때 유용합니다. 

 

- 7 -InsertCallBack((시퀀스시작 후) 지연시킬 시간, 실행할 함수) : 사실 시퀀스를 사용한 가장 큰 이유인데요, 이 API를 사용하면, 예를 들어 DoPath를 사용해서 오리가 움직이는 와중에 특정 로직을 실행할 수 있습니다. 이 부분이 특히나 코루틴으로 구현하자니 엄청 귀찮고 로직도 복잡해졌는데, 이 API덕분에 한시름 놨네요..ㅎㅎ

 

-참고(DOTween 아님!!) animator의 속도를 빠르게 하고 싶을 때, animator = 재생속도 이렇게 지정할 수 있습니다.

 

사실 닷트윈 기초 내용을 포스팅한다고 생각했는데, 나름 고급(?)으로 쓰는 느낌이 살짝 있는 것 같습니다. 혹시나 닷트윈을 완전 처음 보시는 분이라면, DOMove, DOAnchorPosition 등으로 간단하게 움직이는 것부터 차근차근 구현해 보시면 금방 익히실 수 있을 겁니다! 머리 안 좋은 저도 금방 이해했거든요 ㅎㅎ 다음에 기회가 되면 닷트윈을 UI에 활용한 것도 보여드리도록 하겠습니다. 

 

 

 

 

 

반응형

'Unity > Unity Overall' 카테고리의 다른 글

Unity의.Net은 MS의.Net과 다르다  (0) 2023.08.22