본문 바로가기
유니티 & C#

[C#, Unity] 오버워치 트레이서 점멸, 시간역행 구현해보기

by 17번 일개미 2022. 6. 18.
728x90

2023.10.24 결론 : DOTween 을 쓰자!

 

 

 

  •   점멸은 시작 지점에서 Ray를 쏴 전방에 부딪힌 장애물이 있다면 장애물 위치까지만 점멸하고, 장애물이 없다면 기존 점멸 사거리만큼 정상적으로 이동한다. 단, 물체를 관통하거나 바닥에 박히는 일이 없도록 num이라는 상수를 통해 이동거리를 보정하고, y축은 카메라 시점과 일치시켜 보정시킨다.

https://youtu.be/KvbRFXK08i4

점멸과 시간역행

 

영상을 보면 알겠지만, 점멸 자체는 정상적으로 작동하는데

점멸을 하는 이펙트나 사운드가 없으니 유저 입장에서 큰 체감이 되지 않는 것 같다.

 

점멸 코드

void Blink()
{
    canvas.alpha = 1;
    Vector3 start = Camera.position;
    Vector3 end = Camera.forward;
    // 점멸 방향
    if (Input.GetKey(KeyCode.W)) // 각 방향에 따른 독립적인 점멸 구현
    {
        end += Camera.forward;
    }
    if (Input.GetKey(KeyCode.S))
    {
        end += -Camera.forward;
    }
    if (Input.GetKey(KeyCode.A))
    {
        end += -Camera.right;
    }
    if (Input.GetKey(KeyCode.D))
    {
        end += Camera.right;
    }
    RaycastHit hit;
    if (Physics.Raycast(start, end, out hit, range)) // 무언가 장애물이 있으면 맞은 위치에 떨어짐
    {
        //Debug.DrawLine(Camera.position, hit.point * num, Color.red,2);
        destination = hit.point * num; // 목적지보다 num이라는 상수(0.8~0.9)배 만큼 덜 간다. 관통 / 버그 방지
    }
    else // 전방에 장애물이 없으면 점멸 사거리만큼 앞으로 도약
    {
        //Debug.Log("no Hit");
        destination = (start + end.normalized * range) * num;
    }
    destination.y += cameraHeight; // 점멸과정 중 바닥에 파묻히는걸 막기 위해 시점 높이만큼 y축을 조절함
    isBlink = true;
}

결국엔 destination 위치로 이동하게 되는데 장애물이 있냐 없냐를 판별해서 그 값이 조금 달라진다.

만약 판별하지 않고 일정거리를 무조건 이동하게 되면, 벽을 통과하거나 구조물 사이로 끼는 일이 발생할 것이다.

따라서, 장애물이 있으면 장애물 바로 앞까지만 이동하게 하는 것이다.


시간역행 코드

void SavePosition(bool start)
{
    if (start)
    {
        timer += Time.deltaTime;
        if (timer >= 0.05f)
        {
            if (originalPosition.Count < 60) // 3초 전까지 저장
            {
                originalPosition.Add(transform.position);
                //Debug.Log(originalPosition.Count);

            }
            else
            {
                originalPosition.RemoveAt(0); // 최대 이상으로 저장되면 첫번째 요소부터 지운다. 자동으로 인덱스 당겨짐
                originalPosition.Add(transform.position);
                //Debug.Log(originalPosition.Count);
            }
            timer = 0f;
        }
    }
    else return;
}

어떻게 구현하면 좋을까 생각해보니 오버워치에서 트레이서가 시간역행을 할 때,

단순히 몇 초 전의 위치로 이동하는 것이 아니라

몇 초 동안의 경로를 그대로 돌아간다는 것이 생각났다.

 

그러기 위해선, 일정 시간마다 지나온 경로들을 저장해오고, 또 갱신해줘야 할 것 같았다.

적당한 자료구조가 배열이라고 생각했고 매 3초마다의 이동경로를 배열에 Transform으로 저장했다.

저장하는 간격은 0.05초,  총 3초 동안 60번의 위치를 저장하는 셈이다.

 

그리고 시간역행버튼을 누르면 저장된 위치를 거꾸로 순회하며 캐릭터를 이동시키려고 한다.

 

  IEnumerator TimeTravel()
 {
     canvas.alpha = 1; // 밋밋해서 화면이 파래지는 이펙트를 살짝 주었다.
     isSave = false;
     for (int i = originalPosition.Count - 1; i >= 0; i--)
     {
         //transform.position = Vector3.MoveTowards(transform.position, (Vector3)originalPosition[i], Time.deltaTime * speed * 2);
         transform.position = Vector3.Lerp(transform.position, (Vector3)originalPosition[i], 0.2f);
         //Debug.Log(originalPosition[i]);
         yield return new WaitForSeconds(0.01f); // 총 1.5 초가 걸려서 돌아온다.
     }
     isSave = true;
     canvas.alpha = 0;
 }

canvas.alpha 는 점멸에서 이펙트가 너무 없길래 시간역행에서는 간단하게 UI를 파랗게 하는 형태로 뭔가 만들어봤다.

 

이제, 저장해온 경로들을 역순으로 순회하며 캐릭터를 이동 시킬건데,

문제는 3초동안 200번 300번 이렇게 촘촘하게 위치를 저장하지 않았기 때문에 돌아가면서 뚝뚝 끊기는 것처럼 보일 것이다.

 

따라서, 저장한 위치 60개를 Lerp로 보간하면서 최대한 부드러운 경로 움직임을 만들어 주면서 캐릭터를 이동시켰다.

WaitForSeconds 를 0.01초로 설정한 이유는, 시간역행의 느낌을 살리기 위해서다.

 

실제로 게임에서 시간역행을 해보면 트레이서가 반짝하며 돌아오는 것이 아니라 빨려들어가는 것처럼 어느정도 시간이 걸리면서 돌아온다. 그러한 지연효과의 느낌을 주기위해서 딜레이를 살짝 주었더니 적당하게 구현된 것 같았다.


한계

처음에는 다양한 캐릭터들을 구현해보려고 했는데, 모델이랑 애니메이션이 없으니 할 맛이 좀 안나는 것 같다.

그나마 구현한 트레이서도 눈으로 보기에 기능은 구현이 되었지만, 과연 프로그래밍 적으로 Well - Done 한 코드인지는 피드백이 없어 잘 모르겠다.  약 2년이 지난 시점에서 보니 한심하기 짝이 없다. 

728x90