Ch 1. 노트 만들기, 노트 판정
카테고리: Unity Lesson 4
태그: Unity Game Engine
케이디님의 [유니티 강좌] 리듬 게임 유튜브 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
🚀 안드로이드 환경 세팅
- 빌드 세팅
- UnityHub 에서 Android Build Support와 더불어 SDK, JDK 까지 설치
- Build Setting 에서 안드로이드로 switch platform
- 해상도는 1920 x 1080 (폰 기종마다 다르지만 보통은 이렇다. 노트 9부터는 더 큰 해상도 필요)
- 이미지들은 모두 Sprite 타입으로
BGM 사운드 세팅
- BGM사운드들은
- Force To Mono 체크
- 모노와 스테레오의 차이
- 스테레오 : 하나 이상의 채널을 사용한다. 예를 들어 왼쪽 이어폰, 오른쪽 이어폰 소리 다른 이런 경우는 2 개의 채널을 사용하는 스테레오 사운드. 따라서 더 현장감 있다.
- 모노 : 하나의 채널만 사용. 어떤 스피커를 이용하든 모든 곳에서 동일한 사운드가 출력되게 된다.
- 컴퓨터는 스피커를 여러개 둘 수도 있는 환경이라 스테레오 사운드가 좋지만 모바일은 스피커가 하나라 모노 사운드로 해도 충분하다.
- 스테레오가 용량도 크기 때문에 굳이 스피커가 하나인 모바일에서 스테레오를 선택할 이유가 없다. 모바일은 용량 줄이는게 생명!
- 모노와 스테레오의 차이
- Load Type은 Streaming
- 디스크에서 바로 읽어들여서 재생. 데이터를 읽어 들이는 곳이 메모리가 아닌 디스크.
- BGM은 용량도 크고 길이도 긴 사운드이며 지연 시간 좀 있어도 상관 없기 때문에 메모리에 적재해서 재생하기보다는 디스크로부터 읽어들이는게 효율적인 메모리 사용상 좋을 것이다.
- 출처 및 오디오 클립 설정 참고하기
- 디스크에서 바로 읽어들여서 재생. 데이터를 읽어 들이는 곳이 메모리가 아닌 디스크.
- 파일 크기를 줄이고 싶다면 퀄리티 값을 낮춰도 좋다.
- Force To Mono 체크
🚀 노트 UI 만들기
캔버스의 Scale Mode 를 “Scale With Screen Size” 로 한다.
- 모바일은 이 모드가 적당하다. 캔버스의 사이즈가 유동적으로 맞춰지게 한다.
- Reference Resoulution 그 기준이 될 해상도
- 현재 쓰고 있는 해상도인 1920 x 1080 을 기준으로 한다.
- 이 해상도를 기준으로 캔버스 사이즈가 유동적으로 경우에 따라 변하게 된다.
- Math
- Width, Height 둘 중에서 어느 쪽으로 더 값을 땡기느냐에 따라 가로 세로 중 어느 쪽을 크기 변화시 왜곡하지 않게 할건지를 결정한다.
- 캔버스의 세로 크기는 유지하고 싶으므로 Height 쪽으로 최대로 끌어당겨 1 로 맞춘다.
- Reference Resoulution 그 기준이 될 해상도
Note
전체를 묶는 빈 오브젝트- 📜NoteManager 👉 노트들 전체적으로 관리. 노트를 생성시키고 제거함.
- 📜TimingManager 👉 전체 노트들의 판정 처리 (Perfect, Bad 같은)
- Box Collider 2D
- 노트가 화면 밖을 벗어나면 삭제되야 한다. 따라서 화면 오른쪽 바깥에 초록색 저게 바로 박스 콜라이더다. 저기에 노트가 닿으면(Trigger 충돌) 삭제되도록 할 것이다.
IsTrigger
체크
- 노트가 화면 밖을 벗어나면 삭제되야 한다. 따라서 화면 오른쪽 바깥에 초록색 저게 바로 박스 콜라이더다. 저기에 노트가 닿으면(Trigger 충돌) 삭제되도록 할 것이다.
Container
검정 배경이미지Line
빨간 줄 이미지CenterFlame
가운데 이미지. 저기에 노트가 들어가는 순간 터치해야 됨!- Box Collider 2D
- 노트와 충돌 판정
- Audio Source
- 📜CenterFlame
- 첫 번째 노트와 충돌되면 BGM 재생
- Box Collider 2D
NoteAppearLocation
빈 오브젝트- 노트가 생성될 위치. 화면 밖 왼쪽에 위치한
PlayerContorller
빈 오브젝트- 📜PlayerConotrller
- 매 프레임마다 Space Bar 입력을 체크하여 스페이스바 입력이 들어오면 📜TimingManager의 판정 체크 실행
- 나중에 스페이스바 말고 모바일에 맞는 터치로 바꿀 것
- 매 프레임마다 Space Bar 입력을 체크하여 스페이스바 입력이 들어오면 📜TimingManager의 판정 체크 실행
- 📜PlayerConotrller
아래 이미지들은 전부 투명한게 디폴트다. 구분을 위해 색을 넣어줌.
PerfectRect
이미지 : 노란 이미지. 노트가 이 영역에 들어와 있을 때 터치하면 Perfect 판정CoolRect
이미지 : 파란 이미지. 노트가 이 영역에 들어와 있을 때 터치하면 Cool 판정. Perfect보다 넓은 범위.GoodRect
이미지 : 초록 이미지. 노트가 이 영역에 들어와 있을 때 터치하면 Good 판정. Cool보다 넓은 범위.BadRect
이미지 : 주황 이미지. 노트가 이 영역에 들어와 있을 때 터치하면 Bad 판정. Good보다 넓은 범위.- 아무 이미지에도 판정되지 않았다면 Miss 판정하게 할 것
생성될 노트 프리팹이다.
- 태그는 “Note”
- 📜Note
- 하나 하나의 노트마다 실행되야할 사항들
- 노트는 매 프레임마다 오른쪽으로 움직여야 한다.
- 노트는 플레이어가 스페이스바 누르면 이미지를 비활성화 해야 한다.
- 노트 삭제는 화면 바깥에서 이루어져야 하고 스페이스바 누를시엔 임시적으로 노트 이미지만 안보이게 한다.
- Box Collider 2D
- 가운데에든 화면밖에서든 충돌 판정이 필요하므로
IsTrigger
체크
- Rigidbody 2D
- Trigger 충돌이 발생하기 위해선 한쪽에 Rigidbody 가 붙어있어야 하므로
- Type을
Kinematic
으로 하여 물리 처리 X Simulated
체크- 이게 반드시 체크 되어야 다른 Rigidbody 혹은 Collider 와 상호작용이 가능하다.
- 체크 해제 하면 물리 처리 되지 않음
Use FUll Kinematic Contacts
체크- 이 설정을 체크하면 Rigidbody 2D의 모든 Body Type과 충돌 상호작용 할 수 있다.
- 이게 체크 해제되면 Kinematic이 아닌 Dynamic 타입인 경우와만 충돌 상호작용 된다.
- 하나 하나의 노트마다 실행되야할 사항들
🚀 스크립트
📜Note
노트 프리팹에 붙는다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Note : MonoBehaviour
{
public float noteSpeed = 400;
private Image noteImage;
void Start()
{
noteImage = GetComponent<Image>();
}
public void HideNote()
{
noteImage.enabled = false;
}
void Update()
{
transform.localPosition += Vector3.right * noteSpeed * Time.deltaTime;
}
}
- 매 프레임마다 오른쪽으로 1초에
noteSpeed
만큼 이동한다.
📜CenterFlame
CenterFlame
이미지에 붙는다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CenterFlame : MonoBehaviour
{
AudioSource myAudio;
bool musicStart = false;
void Start()
{
myAudio = GetComponent<AudioSource>();
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (!musicStart)
{
if (collision.CompareTag("Note"))
{
myAudio.Play();
musicStart = true;
}
}
}
}
가운데에 첫 번째 노트가 들어와 Trigger 충돌이 일어나면 BGM을 재생한다.
📜TimingManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimingManager : MonoBehaviour
{
public List<GameObject> boxNoteList = new List<GameObject>();
[SerializeField] Transform center = null; // 판정 범위의 중심
[SerializeField] RectTransform[] timingRect = null; // 다양한 판정 범위
Vector2[] timingBoxs = null; // 판정 범위 최소값 x, 최대값 y
boxNoteList
- 생성된 노트들을 담음.
- 📜NoteManager 에서 노트 하나하나 생성할 때마다 이 리스트에 추가해줄 것이다.
- 어느 판정 범위(Perfect, Bad 등등)에 있는지 모든 노트들을 비교해야 함
- 생성된 노트들을 담음.
center
- 판정 범위의 중심.
- 즉,
CenterFlame
이 들어가게 됨
timingRect
- Perfect, Cool, Good, Bad 이렇게 4 개의 판정 범위 이미지가 들어간다.
- 각각의 범위 안에 들어오면 그렇게 판정한다.
timingBoxs
- Perfect, Cool, Good, Bad 이렇게 4 개의 판정 범위에 맞춰서 4 의 크기를 갖게 될 것
- Vector2 인데 x 는 판정 범위의 최소값 y 는 판정 범위의 최대값으로 쓸 것.
- 첫 번째 원소.x 👉 Perfect 의 최소값, 첫 번째 원소.y 👉 Perfect 의 최대값 이런식
- 판정 범위의 최소값은 중심에서 판정 범위 이미지 너비의 절반을 뺀 값이 되겠고
- 판정 범위의 최대값은 중심에서 판정 범위 이미지 너비의 절반을 더한 값이 되겠다.
- 위 판정 범위 안에 들어오면 해당 판정을 받았다고 볼 수 있다.
void Start()
{
timingBoxs = new Vector2[timingRect.Length];
for (int i = 0; i < timingRect.Length; i++)
{
timingBoxs[i].Set(center.localPosition.x - timingRect[i].rect.width / 2,
center.localPosition.x + timingRect[i].rect.width / 2);
}
}
public void CheckTiming()
{
for(int i = 0; i < boxNoteList.Count; i++)
{
float t_notePosX = boxNoteList[i].transform.localPosition.x;
// 판정 순서 : Perfect -> Cool -> Good -> Bad
for (int j = 0; j < timingBoxs.Length; j++)
{
if (timingBoxs[j].x <= t_notePosX && t_notePosX <= timingBoxs[j].y)
{
boxNoteList[i].GetComponent<Note>().HideNote();
boxNoteList.RemoveAt(i);
switch(j)
{
case 0:
Debug.Log("Perfect");
break;
case 1:
Debug.Log("Cool");
break;
case 2:
Debug.Log("Good");
break;
case 3:
Debug.Log("Bad");
break;
}
return;
}
}
}
Debug.Log("Miss");
}
}
- Start
- 게임이 시작되면
timingBoxs
도timingRect
와 크기가 동일한 배열로 만들어줌 timingBoxs
의 x, y 값 세팅timingBoxs[2].x
👉 Good 판정 범위의 최소값. 즉 중심위치에서 Good 판정 이미지 너비의 절반 뺀 값.y
는 그 반대
- 게임이 시작되면
- CheckTiming
- 현재 검사해야 하는 노드들을 전부 검사하면서 판정한다.
boxNoteList
리스트 검사 - if (timingBoxs[j].x <= t_notePosX && t_notePosX <= timingBoxs[j].y)
- 해당 범위 안에 들어오면 그 판정인 것이다.
- 이미지를 안보이게 한다. (삭제는 화면 밖에서 이루어질 것)
- 리스트에서 삭제
- 판정 했으니 return 하여 나간다. 한번 Cool 판정 받았으면 더 이상 다음 판정 검사할 필요가 없기 때문
- 해당 범위 안에 들어오면 그 판정인 것이다.
- 4 개의 판정 이미지 범위에 다 들어가지 못했다면 Miss 인 것이다.
- 현재 검사해야 하는 노드들을 전부 검사하면서 판정한다.
📜PlayerController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
TimingManager theTimingManager;
void Start()
{
theTimingManager = FindObjectOfType<TimingManager>();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
theTimingManager.CheckTiming(); // 판정 체크
}
}
}
스페이스바 입력이 들어오면 📜TimingManager의 CheckTiming 함수를 호출하여 그 시점에서의 모든 노트들을 판정 검사한다.
📜NoteManateer
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NoteManager : MonoBehaviour
{
public int bpm = 0; // 리듬게임 비트 단위. 1분당 몇 비트인지.
double currentTime = 0d; // 리듬 게임은 오차 적은게 중요해서 float보단 double
[SerializeField] Transform tfNoteAppear = null; // 노트 생성 위치 오브젝트
[SerializeField] GameObject goNote = null; // 생성할 노트 프리팹
TimingManager theTimingManager;
void Start()
{
theTimingManager = GetComponent<TimingManager>();
}
void Update()
{
currentTime += Time.deltaTime;
if (currentTime >= 60d / bpm)
{
GameObject t_note = Instantiate(goNote, tfNoteAppear.position, Quaternion.identity);
t_note.transform.SetParent(this.transform);
t_note.transform.localScale = new Vector3(1f, 1f, 0f);
theTimingManager.boxNoteList.Add(t_note);
currentTime -= 60d / bpm; // currentTime = 0 으로 리셋해주면 안된다.
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.CompareTag("Note"))
{
theTimingManager.boxNoteList.Remove(collision.gameObject);
Destroy(collision.gameObject);
}
}
}
- if (currentTime >= 60d / bpm)
- 60/bpm = 한 비트당 시간 (120bpm이라면 한 비트당 소요 시간은 0.5초)
- 즉, 60/bpm 마다 노트를 생성한다. 한 비트마다 노트를 생성하는 것
- 노트 생성 후 반드시 생성한 노트의 부모를 이 스크립트가 붙는 오브젝트이자 캔버스의 자식인
Note
의 자식으로 넣어준다.- ✨노트는 이미지다. 이러한 UI들은 반드시 캔버스의 자식이여야지만 보이기 때문이다✨ 이미지들은 반드시 캔버스 위에 있어야 보여진다.
- SetParent의 영향 때문인지 몰라도 자꾸 노트의 scale 값이 0.562 이렇게 더 작게 설정되어 생성되길래 localSalce 을 1, 1 로 원래 크기로 프리팹과 같게 맞춰주도록 했다.
- 📜TimingManager
boxNoteList
리스트에 해당 노트 추가 currentTime -= 60d / bpm;
- currentTime = 0 으로 리셋해주면 안된다.
- 120bpm이라면 currentTime이 딱 0.5가 되는게 아니라 0.51005551 이런식으로 약간의 오차가 더 해지게 되기 때문이다. 이 오차를 무시하고 0 으로 리셋해버리면 그 오차만큼의 시간 손실이 계속 누적 되어 부정확한 리듬게임이 될 수 있다.
- 60d/bpm 만큼 빼주면 오차만 남겠지! 그럼 다음 노트는 오차만큼 더 빨리 나오는 식으로 조정이 될 것이다.
- currentTime = 0 으로 리셋해주면 안된다.
- 120 bpm 1분당 120비트
- 노트 생성 위치 오브젝트는
NoteAppearLocation
- 생성할 노트 프리팹
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기