Ch 5. 큐브(플레이어)의 움직임, 회전, 반동

Date:     Updated:

카테고리:

태그:

케이디님의 [유니티 강좌] 리듬 게임 유튜브 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

📜PlayerController

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

public class PlayerController : MonoBehaviour
{
    // 이동
    [SerializeField] float moveSpeed = 3;   // 1초에 3만큼 움직이도록
    Vector3 dir = new Vector3();
    Vector3 destPos = new Vector3();

    // 회전
    [SerializeField] float spinSpeed = 270f;  // 270도 
    Vector3 rotDir = new Vector3();
    Quaternion destRot = new Quaternion();

    bool canMove = true; // 쿠브 이동중에는 노트 판정이 불가능 하도록 (코루틴 중복 실행 차단)
    
    // 수직 반동
    [SerializeField] float recoilPosY = 0.25f;
    [SerializeField] float recoilSpeed = 1.5f;

    [SerializeField] Transform fakeCube = null;
    [SerializeField] Transform realCube = null;


    TimingManager theTimingManager;

image

image

    void Start()
    {
        theTimingManager = FindObjectOfType<TimingManager>();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.D) ||
            Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.S))
        {
            if (canMove)
            {
                // 판정 체크 : Perfect, Cool, Good, Bad 일 때만 True
                if (theTimingManager.CheckTiming())
                {
                    StartAction();
                }
            }
        }
    }

    void StartAction() // Perfect, Cool, Good, Bad 일 때만 한번 시랳ㅇ됨
    {
        // 큐브의 x축 -> 위 아래   큐브의 z축 -> 오른쪽, 왼쪽
        dir.Set(Input.GetAxisRaw("Vertical"), 0, Input.GetAxisRaw("Horizontal"));

        // 이동 목표값 게산
        destPos = transform.position + new Vector3(-dir.x, 0, dir.z);

        // 회전 목표값 계산
        rotDir = new Vector3(-dir.z, 0f, -dir.x);
        fakeCube.RotateAround(transform.position, rotDir, spinSpeed); //공전
        destRot = fakeCube.rotation;

        StartCoroutine(MoveCo());
        StartCoroutine(SpinCo());
        StartCoroutine(RecoilCo());
    }

    IEnumerator MoveCo()
    {
        canMove = false;
        while(Vector3.SqrMagnitude(transform.position - destPos) >= 0.001f)
        {
            transform.position = Vector3.MoveTowards(transform.position, destPos, moveSpeed * Time.deltaTime); 
            yield return null;
        }

        transform.position = destPos;
        canMove = true;
    }

    IEnumerator SpinCo()
    {
        while(Quaternion.Angle(realCube.rotation, destRot) > 0.5f)
        {
            realCube.rotation = Quaternion.RotateTowards(realCube.rotation, destRot, spinSpeed * Time.deltaTime); 
            yield return null;
        }

        realCube.rotation = destRot;
    }

    IEnumerator RecoilCo()
    {
        // 올라감
        while (realCube.position.y < recoilPosY)
        {
            realCube.position += new Vector3(0, recoilSpeed * Time.deltaTime, 0);
            yield return null;
        }

        // 내려감
        while (realCube.position.y > 0)
        {
            realCube.position -= new Vector3(0, recoilSpeed * Time.deltaTime, 0);
            yield return null;
        }

        realCube.localPosition = new Vector3(0, 0, 0);
    }
}

✈ 준비 과정

W A S D 입력이 들어 왔고, 현재 큐브가 이동중인 상태가 아니라면 👉 큐브의 이동 + 회전 + 반동

dir.Set(Input.GetAxisRaw("Vertical"), 0, Input.GetAxisRaw("Horizontal"));

image

dir은 이동, 회전 축에 필요한 Vector3 로 사용할 것.

  • “Horizontal” 👉 A 키 누르면 -1, D 키 누르면 1
    • A키 누르면 큐브가 Z 축의 반대 방향으로 이동하고, D키 누르면 Z 축으로 이동하게 할 것이라 dir의 Z 축에 Input.GetAxisRaw(“Horizontal”) 할당.
      • A 키 누르면 dir.z는 1이 되며, D키 누르면 dir.z는 -1이 됨.
  • “Vertical” 👉 S 키 누르면 -1, W 키 누르면 1
    • W키 누르면 큐브가 X 축의 반대 방향으로 이동하고, S키 누르면 X 축으로 이동하게 할 것이라 dir의 X 축에 Input.GetAxisRaw(“Vertical”) 할당.
      • S 키 누르면 dir.x는 1이 되며, W키 누르면 dir.x는 -1이 됨.
  • dir.y 수직으로 이동하거나 y 축을 중심으로 회전하진 않을 것이라서 0
        // 이동 목표값 게산
        destPos = transform.position + new Vector3(-dir.x, 0, dir.z);

        // 회전 목표값 계산
        rotDir = new Vector3(-dir.z, 0f, -dir.x);
        fakeCube.RotateAround(transform.position, rotDir, spinSpeed); //공전
        destRot = fakeCube.rotation;
  • destPos
    • 목표 위치. 예를 들어 A 키와 W 키를 동시에 눌렀다면 (-1, 0, -1) 만큼 이동
      • S가 X축과 일치하고 W는 X축의 반대라 -dir.x 해준 것
  • destRot
    • 목표 회전 값.
    • 먼저 빈 오브젝트인 fakeCube를 현재 큐브의 위치의 rotDir축을 기준으로 270도 (spinSpeed) 먼저 회전 시킨다. 코루틴으로 이동, 회전, 반동을 실제로 실행하기 전에, 먼저 fakeCube를 자전으로 회전 시켜놔서 그 회전값을 얻어 놓는 것이다.
      • 코루틴 순서 상 이동이 먼저 이루어진 후 회전이 이루어지기 때문에 이동 전의 현재 위치의 상태에서 자전에서 회전했을 떄의 그 회전값을 목표로 이동 후에 회전을 해야 하므로 fakeCube를 하나 만들어 그걸 이동 전에 미리 회전시킨 후 최종적인 그 fakeCube의 월드 회전값을 목표 회전값으로 미리 저장해두는 것이다.
      • fakeCubetransform(이 스크립트가 붙는 플레이어 큐브)와 위치가 일치한 상태다. 그러니 자전하듯 회전할 것이다.
        StartCoroutine(MoveCo());
        StartCoroutine(SpinCo());
        StartCoroutine(RecoilCo());
  1. 이동
  2. 회전
  3. 반동


✈ 이동

    IEnumerator MoveCo()
    {
        canMove = false;
        while(Vector3.SqrMagnitude(transform.position - destPos) >= 0.001f)
        {
            transform.position = Vector3.MoveTowards(transform.position, destPos, moveSpeed * Time.deltaTime); 
            yield return null;
        }

        transform.position = destPos;
        canMove = true;
    }
  • canMove 이 이동 코루틴을 실행 중일 때는 StartAction() 이 또 호출되지 않도록 한다.
    • 아직 코루틴들 실행이 다 마치지도 않았는데 Perfect 판정 눌려서 또 새로운 이동 회전 반동 실행을 해야 하면 버벅.. 부자연스럽..
  • 부드럽게 동작하도록 한 프레임씩 쉬면서(코루틴) transform.position 현재 위치가 destPos가 될 때까지 현재 위치를 게속 업뎃! 1초에 moveSpeed 만큼.
  • 오차를 두어서 while문을 돌렸기 떄문에 다시 제대로 destPos 로 세팅

이 스크립트는 Player에 붙는다. 즉, 실제 realCube의 부모 오브젝트이자 빈 부모오브젝트에 붙는 것이다. 그래서 transform은 사실 Player이다. 부모인 Player도 함께 이동하기 떄문에 나중에 반동 코루틴을 마친 후 realCube를 다시 제자리로 할 때 realCube.position = Vector3.zero 가 아닌, realCube.localPosition = Vector3.zero 으로 할 수 있게 된다. 전자는 월드 상의 원점 좌표로 이동해버리는 것이고 후자는 부모와 위치를 일치시키는 것이기 때문이다.


✈ 회전

    IEnumerator SpinCo()
    {
        while(Quaternion.Angle(realCube.rotation, destRot) > 0.5f)
        {
            realCube.rotation = Quaternion.RotateTowards(realCube.rotation, destRot, spinSpeed * Time.deltaTime); 
            yield return null;
        }

        realCube.rotation = destRot;
    }
  • 부드럽게 동작하도록 한 프레임씩 쉬면서(코루틴) realCube.rotation 큐브의 회전 값이 destRot가 될 때까지 realCube의 현재 회전값을 게속 업뎃!


✈ 수직 반동

    IEnumerator RecoilCo()
    {
        // 올라감
        while (realCube.position.y < recoilPosY)
        {
            realCube.position += new Vector3(0, recoilSpeed * Time.deltaTime, 0);
            yield return null;
        }

        // 내려감
        while (realCube.position.y > 0)
        {
            realCube.position -= new Vector3(0, recoilSpeed * Time.deltaTime, 0);
            yield return null;
        }

        realCube.localPosition = new Vector3(0, 0, 0);
    }

y 방향, 즉 수직 방향으로 살짝 통통 튀는 느낌을 주도록!

  • y 방향으로 살 짝 뜰 때
  • 다시 제자리로 내려갈 때
  • 다시 제자리로 확실히 설정
    • realCube.position 으로 설정하면 월드 좌표상 원점으로 가기 때문에 함께 이동해왓던 부모를 기준의 localPosition 사용.


🚀 버그 고침

    void Update()
    {
        if (GameManager.instance.isStartGame)
        {
            CheckFalling();

            if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.D) ||
                Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.S))
            {
                if (canMove && s_canPressKey && !isFalling)
                {
                    MoveCalc();

                    // 판정 체크 : Perfect, Cool, Good, Bad 일 때만 True
                    if (theTimingManager.CheckTiming())
                    {
                        RotateCalc();
                        StartAction();
                    }
                }
            }
        }
    }

    void MoveCalc()
    {
        // 큐브의 x축 -> 위 아래   큐브의 z축 -> 오른쪽, 왼쪽
        dir.Set(Input.GetAxisRaw("Vertical"), 0, Input.GetAxisRaw("Horizontal"));

        // 이동 목표값 게산
        destPos = transform.position + new Vector3(-dir.x, 0, dir.z);
    }

    void RotateCalc()
    {
        // 회전 목표값 계산
        rotDir = new Vector3(-dir.z, 0f, -dir.x);
        Debug.DrawRay(transform.position, rotDir, Color.red);
        fakeCube.RotateAround(transform.position, rotDir, spinSpeed); //공전
        destRot = fakeCube.rotation;
    }

강의에선 설명되지 않았던 부분인데 큐브 움직임이 이상하여 고침

판정이 일어나지 않을 때, 즉 노트가 센터를 지나가지 않을 때도 마구 키보드를 연타하여 입력을 하면 큐브가 이상하게 회전하는 버그가 있었다. 이는 큐브가 이동하지 말아야할 때(노트 판정 X)에도 키보드 입력만 들어오면 fakeCube를 회전시키고 이에 맞게 destRot을 업뎃시켰기 때문이다. 다음에 큐브가 이동해야할 때는 이 잘못된 destRot을 참고하여 회전하기 때문에 이상하게 회전하였던 것이다. 따라서 코드를 위와같이 변경하였다. 이동 목표 값 계산과 회전 목표 값 계산을 분리시켰고 회전 목표 값 갱신은 오직 판정 체크가 일어날 때만 갱신되도록 하였다. CheckTiming 에서 업뎃된 destPos를 사용해야 하기 때문에 MoveCalc 을 먼저 따로 뺌.



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 4 카테고리 내 다른 글 보러가기

댓글 남기기