Ch 5. 큐브(플레이어)의 움직임, 회전, 반동
카테고리: Unity Lesson 4
태그: Unity Game Engine
케이디님의 [유니티 강좌] 리듬 게임 유튜브 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 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;
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"));
dir
은 이동, 회전 축에 필요한 Vector3 로 사용할 것.
- “Horizontal” 👉
A
키 누르면 -1,D
키 누르면 1A
키 누르면 큐브가 Z 축의 반대 방향으로 이동하고,D
키 누르면 Z 축으로 이동하게 할 것이라dir
의 Z 축에 Input.GetAxisRaw(“Horizontal”) 할당.A
키 누르면dir.z
는 1이 되며,D
키 누르면dir.z
는 -1이 됨.
- “Vertical” 👉
S
키 누르면 -1,W
키 누르면 1W
키 누르면 큐브가 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
해준 것
- 목표 위치. 예를 들어 A 키와 W 키를 동시에 눌렀다면 (-1, 0, -1) 만큼 이동
destRot
- 목표 회전 값.
- 먼저 빈 오브젝트인
fakeCube
를 현재 큐브의 위치의rotDir
축을 기준으로 270도 (spinSpeed
) 먼저 회전 시킨다. 코루틴으로 이동, 회전, 반동을 실제로 실행하기 전에, 먼저fakeCube
를 자전으로 회전 시켜놔서 그 회전값을 얻어 놓는 것이다.- 코루틴 순서 상 이동이 먼저 이루어진 후 회전이 이루어지기 때문에 이동 전의 현재 위치의 상태에서 자전에서 회전했을 떄의 그 회전값을 목표로 이동 후에 회전을 해야 하므로 fakeCube를 하나 만들어 그걸 이동 전에 미리 회전시킨 후 최종적인 그 fakeCube의 월드 회전값을 목표 회전값으로 미리 저장해두는 것이다.
fakeCube
는transform
(이 스크립트가 붙는 플레이어 큐브)와 위치가 일치한 상태다. 그러니 자전하듯 회전할 것이다.
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;
}
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 을 먼저 따로 뺌.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기