프로젝트 일지/Unity

[Unity/TIL] 3D 개인 프로젝트 일지 (3) - 짭테런 후회와 마무리

톰마토 2025. 3. 11. 22:11

이번 프로젝트를 하면서 나의 첫 선택에 대한 후회가 많았다. 발제 문서를 읽고 ALTF4나 온리업 류의 게임을 만들어야 한다는 생각에 흥미가 느껴지지 않아서 그대로 따르지 않으려고 했다. 분명 다른 방식으로 필수 기능 가이드를 따를 방법이 많았을 텐데, 그당시에는 다른 걸 만들어야 겠다는 생각밖에 들지 않았다. 하지만 이때문에 시간이 더 지체되었고 그렇다고 멋진 퀄리티로 완성한 것이 아니라서 후회가 된다. 반성의 의미로 후회를 글로 남기려고 한다. 

 

필수 기능을 모두 담을 수 있으면서 나의 흥미를 이끌만한 게임을 직접 기획하기엔 능력도, 여유도 부족했다. 보통 학습용 프로젝트에서는 에셋을 찾다가 에셋에 기획을 맞추는 일이 잦은데, 이번에는 고집이 생겼는지.,.. 프로젝트에 애정을 붙이고 싶어서 억지로 좋아하는 게임을 만들기로 했다. 그래서 테일즈런너 류 게임을 만들자는 생각을 했다. 

 

이게 아주 잘못됐다는 것은 아니다. 어쨋든 필수 기능은 다 만들긴 했으니 아주 엎어졌다는 것은 아니다. 그런데 왜이렇게 후회가 되냐면!!! 

 

이번 과제의 필수 기능 가이드를 보면 3D 환경에서의 움직임과 상호작용을 구현하고, Scriptable Object로 데이터 관리하는 복습과 코루틴 사용 복습이 주된 학습 목표라고 생각된다. 그런데 필수 기능을 모두 구현하고 나서 자유롭게 도전하는 도전 기능 가이드를 보면 이번에는 첫 3D 개인 과제인 만큼 복습할 것이 더 많이 있었다. 그러나 소중한 개인 공부 기회를 놓치고 지나가게 되었다. 

도전 기능 가이드

하지만 나는 테일즈런너를 따라서 만든다는 생각에 플레이어의 매끄러운 이동, 시원한 점프, 묵직한 착지 대쉬와 매끄러운 카메라 회전 등의 주로 게임 조작감에 매몰되어 있었던 것 같다. 도전 기능 중에도 재미도 있고 복습할 내용이 많은데 후회가 크다!!!!!!!!!!!!!!!!!!!!!! 개발자가 하라는 개발만 잘하면 되는데 왜 갑자기 새로운 도전을 한걸까....... 과거의 나야 퍽퍽.......

 

나중에 혼자서 3D 개발 또 하면 되지....!!! 하하 그때는 게임이 아니라 기능 개발을 꼭 다 해봐야지. 특히 Navigation은 주말에 복습을 하든가 해야겠다. 


그래도 이번에 새로 공부하거나 복습한 것들도 많다. 공부할 양이 너무 많아서 덜했다는 거지 아예 못했다는 건 아니다. 사실 나에게는 3D 캐릭터 에셋을 사용해서 이동시키고 애니메이션 적용하는 것부터가 큰 도전이었다. 

 

만들면서 게임 조작감이 좋은 게임들을 더 동경하게 되었다. 점프 시 시원하게 올라가되 떨어질 땐 빠르고 묵직하게, 달릴 때는 점점 가볍게게 해야 한다. 레이싱 게임인만큼 달릴 때 와다다다 느낌이 나게(?)하면서 데미지입을 때는 잘 튕겨나가게 하는 것이 참 어려웠다..ㅋㅋㅋㅋ 

데미지 오브젝트에 부딪히면 플레이어가 경직되었다가 순간이동하듯이 빠르게 튕겨나간다.. 텔레포트 기능도 따로 만들었는데, 사실상 둘이 비슷해 보인다 ㅇㅅㅇ.. 그래서 사실 플레이어를 튕겨내는 데미지 오브젝트는 튜토리얼 맵에만 넣고 달리기 맵에서는 안넣었다. 아 튜토리얼 맵은 필수 기능 구현을 보여주기 위한 맵이다.

 

그리고 가장 만들고 싶었던 두가지는 카메라 회전착지 대쉬이다. 테일즈런너는 달리기를 할 때는 마우스로 회전하는 것이 아니라 맵의 특정 구간에서만 시점이 변경된다. 그리고 이동 방향과 반대로 역주행을 해도 반대로 회전이 잘 된다. 역방향만 없었으면 그냥 회전 강제하면 됐을 텐데, 역방향으로도 회전 보정이 잘 되게 하느라 힘들었다. 실제 테런에서 카메라 스프링암같은 것을 사용하는 건지 회전을 강제하는 건지 모르겠다.

 

나는 맵에서 회전을 강제하는 것으로 구현했다. 처음에는 여러 개의 판을 놓고 두 번째 판을 지나갈 때마자 직전 판과 현재 판의 사이각을 구해서 그만큼 회전하도록 만들어주려고 했다. 테일즈런너에서 실제로 시점 변화를 한 번에 시켜주는 것이 아니고 서서히 시켜주기 때문이다. (코너에 서있으면 각도가 돌다가 만 상태가 된다.) 

그러나 나는 판을 두 개만 놓았고, 두 번째 판을 지나갈 때 회전하게 했다. 그리고 각 판이 의미하는 벡터가 있고 bool 값을 갖는다. 

  • 두 벡터간의 각도를 구해서 그 angle만큼 회전하고,
  • 두번째 판이 bool값이 true인 판이면 +angle, false면 -angle만큼 회전하게 했다. 

카메라 커브 존 / 우 : 노란 화살표는 맵 진행 방향

 

근데 사실 모든 구간이 90도 회전이고, 모든 CameraCurveZone 오브젝트의 벡터를 90도가 나오도록 통일해놔서 사이각 연산은 의미가 없어졌다. 판이 두개라서 방향 체크가 될 뿐이다. 

// 판에 붙는 CurveDetect 컴포넌트
public class CurveDetect : MonoBehaviour
{
    public CurveZone zone;
    public Vector3 vec;
    public bool isCorrectDir; // 나중에 부딪히는 오브젝트가 true면 +90도, false면 -90도 회전시킴

    Coroutine coroutine;

    private void OnTriggerEnter(Collider other)
    {
        if (zone.prev == Vector3.zero) zone.prev = vec;
        else
        {
            if (zone.prev == vec) return;
            zone.next = vec;

            // prev~next 회전
            zone.RotateStart(isCorrectDir);

            if(coroutine != null) StopCoroutine(coroutine);
            coroutine = StartCoroutine(Refresh());
        }
    }

    IEnumerator Refresh()
    {
        GetComponent<Collider>().enabled = false;   
        yield return new WaitForSeconds(0.02f);
        GetComponent<Collider>().enabled = true;
    }
}

 

// 커브존 오브젝트에 붙인 컴포넌트
public class CurveZone : MonoBehaviour
{    
    [HideInInspector]public Vector3 prev = Vector3.zero;
    [HideInInspector]public Vector3 next = Vector3.zero;
    
    [HideInInspector] public Coroutine coroutine;
    [HideInInspector] public float rotationSpeed = 5;


    public void RotateStart(bool isCorrectDir)
    {
        if(coroutine != null) StopCoroutine(coroutine);

        float dotProduct = Vector3.Dot(prev, next);
        float angle = Mathf.Acos(dotProduct) * Mathf.Rad2Deg; // 사이각을 각도로 구함
        if(!isCorrectDir) angle = -angle;
        coroutine = StartCoroutine(RotateCoroutine(angle));

        prev = next = Vector3.zero;
    }

    IEnumerator RotateCoroutine(float angle)
    {
        Transform t = CharacterManager.Instance.Player.transform;
        Quaternion startRotation = t.rotation;
        Quaternion targetRotation = Quaternion.Euler(t.eulerAngles.x, t.eulerAngles.y + angle, t.eulerAngles.z);

        float passTime = 0f;
        while (passTime < 2f)
        {
            t.rotation = Quaternion.Lerp(startRotation, targetRotation, passTime);
            passTime += Time.deltaTime * rotationSpeed;
            yield return null;
        }

        t.rotation = targetRotation; 
    }
}

둘 다 카메라 회전 보여주는 영상이다. 거꾸로도 되는 걸 꼭 보여주고 싶었다.

테일즈런너를 플레이할 때 각도 변환이 신기해서 맵에서 계속 돌아다녀보기도 했었는데, 비슷하게나마 만들어내서 이점은 즐거웠다. 


그리고 착지대쉬는 IsGrounded 함수를 만들 때와 비슷하게 IsAlmostGround 함수를 만들어서 생각보다는 간단하게 구현할 수 있었다. 실제 테런과 같은 조건으로 만들지는 않은 것 같다. 나는 공중에서 땅에 닿을 때면 항상 착지 대쉬를 사용할 수 있도록 했다.

히히 착지 대쉬와 대쉬 점프 가능

public void OnDash(InputAction.CallbackContext context)
{
    if (context.phase == InputActionPhase.Started && CharacterManager.Instance.Player.condition.CanUseStamina())
    {
        isDashMode = true;
        if((isJumping || isDamaged) && IsAlmostGround())
        {
            // 착지 대쉬
            CurMoveSpeed = PublicDefinitions.MaxSpeed;
            CharacterManager.Instance.Player.condition.AddStamina(20);
            StartCoroutine(SpeedEffect());
        }
    }
    else if(context.phase == InputActionPhase.Canceled)
    {
        isDashMode = false;
    }
}

IsAlmostGround는 Raycast로 체크한다. 길이가 5나 되기 때문에 착지 판정이 매우 후하다고 생각한다. 

 private bool IsAlmostGround()
 {
     Ray ray = new Ray(transform.position + transform.up * 0.01f, Vector3.down);
     return Physics.Raycast(ray, 5f, groundLayerMask);
 }

이제는 나의 목적을 잃지 않고 고분고분하게 프로젝트에 임하겠다... 나는 공부하는 중이고, 개발자이다!!! 정말 이번에는 귀신씌였나보다. 앞으로 잘하면 되겠지.. 너무 상심하지 말고 새로 주어질 플젝이나 잘하자