Ch 1. 노트 만들기, 노트 판정

Date:     Updated:

카테고리:

태그:

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

🚀 안드로이드 환경 세팅

  • 빌드 세팅
    • UnityHub 에서 Android Build Support와 더불어 SDK, JDK 까지 설치
    • Build Setting 에서 안드로이드로 switch platform
  • 해상도는 1920 x 1080 (폰 기종마다 다르지만 보통은 이렇다. 노트 9부터는 더 큰 해상도 필요)
  • 이미지들은 모두 Sprite 타입으로

BGM 사운드 세팅

image

  • BGM사운드들은
    • Force To Mono 체크
      • 모노와 스테레오의 차이
        • 스테레오 : 하나 이상의 채널을 사용한다. 예를 들어 왼쪽 이어폰, 오른쪽 이어폰 소리 다른 이런 경우는 2 개의 채널을 사용하는 스테레오 사운드. 따라서 더 현장감 있다.
        • 모노 : 하나의 채널만 사용. 어떤 스피커를 이용하든 모든 곳에서 동일한 사운드가 출력되게 된다.
      • 컴퓨터는 스피커를 여러개 둘 수도 있는 환경이라 스테레오 사운드가 좋지만 모바일은 스피커가 하나라 모노 사운드로 해도 충분하다.
        • 스테레오가 용량도 크기 때문에 굳이 스피커가 하나인 모바일에서 스테레오를 선택할 이유가 없다. 모바일은 용량 줄이는게 생명!
    • Load Type은 Streaming
      • 디스크에서 바로 읽어들여서 재생. 데이터를 읽어 들이는 곳이 메모리가 아닌 디스크.
        • BGM은 용량도 크고 길이도 긴 사운드이며 지연 시간 좀 있어도 상관 없기 때문에 메모리에 적재해서 재생하기보다는 디스크로부터 읽어들이는게 효율적인 메모리 사용상 좋을 것이다.
      • 출처 및 오디오 클립 설정 참고하기
    • 파일 크기를 줄이고 싶다면 퀄리티 값을 낮춰도 좋다.


🚀 노트 UI 만들기

image

캔버스의 Scale Mode 를 “Scale With Screen Size” 로 한다.

  • 모바일은 이 모드가 적당하다. 캔버스의 사이즈가 유동적으로 맞춰지게 한다.
    • Reference Resoulution 그 기준이 될 해상도
      • 현재 쓰고 있는 해상도인 1920 x 1080 을 기준으로 한다.
      • 이 해상도를 기준으로 캔버스 사이즈가 유동적으로 경우에 따라 변하게 된다.
    • Math
      • Width, Height 둘 중에서 어느 쪽으로 더 값을 땡기느냐에 따라 가로 세로 중 어느 쪽을 크기 변화시 왜곡하지 않게 할건지를 결정한다.
      • 캔버스의 세로 크기는 유지하고 싶으므로 Height 쪽으로 최대로 끌어당겨 1 로 맞춘다.

image

  • Note 전체를 묶는 빈 오브젝트
    • 📜NoteManager 👉 노트들 전체적으로 관리. 노트를 생성시키고 제거함.
    • 📜TimingManager 👉 전체 노트들의 판정 처리 (Perfect, Bad 같은)
    • Box Collider 2D
      • 노트가 화면 밖을 벗어나면 삭제되야 한다. 따라서 화면 오른쪽 바깥에 초록색 저게 바로 박스 콜라이더다. 저기에 노트가 닿으면(Trigger 충돌) 삭제되도록 할 것이다.
        • IsTrigger 체크
  • Container 검정 배경이미지
  • Line 빨간 줄 이미지
  • CenterFlame 가운데 이미지. 저기에 노트가 들어가는 순간 터치해야 됨!
    • Box Collider 2D
      • 노트와 충돌 판정
    • Audio Source
    • 📜CenterFlame
      • 첫 번째 노트와 충돌되면 BGM 재생
  • NoteAppearLocation 빈 오브젝트
    • 노트가 생성될 위치. 화면 밖 왼쪽에 위치한
  • PlayerContorller 빈 오브젝트
    • 📜PlayerConotrller
      • 매 프레임마다 Space Bar 입력을 체크하여 스페이스바 입력이 들어오면 📜TimingManager의 판정 체크 실행
        • 나중에 스페이스바 말고 모바일에 맞는 터치로 바꿀 것

image

아래 이미지들은 전부 투명한게 디폴트다. 구분을 위해 색을 넣어줌.

  • PerfectRect 이미지 : 노란 이미지. 노트가 이 영역에 들어와 있을 때 터치하면 Perfect 판정
  • CoolRect 이미지 : 파란 이미지. 노트가 이 영역에 들어와 있을 때 터치하면 Cool 판정. Perfect보다 넓은 범위.
  • GoodRect 이미지 : 초록 이미지. 노트가 이 영역에 들어와 있을 때 터치하면 Good 판정. Cool보다 넓은 범위.
  • BadRect 이미지 : 주황 이미지. 노트가 이 영역에 들어와 있을 때 터치하면 Bad 판정. Good보다 넓은 범위.
  • 아무 이미지에도 판정되지 않았다면 Miss 판정하게 할 것

image

생성될 노트 프리팹이다.

  • 태그는 “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
    • 게임이 시작되면 timingBoxstimingRect와 크기가 동일한 배열로 만들어줌
    • 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 인 것이다.

image


📜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 만큼 빼주면 오차만 남겠지! 그럼 다음 노트는 오차만큼 더 빨리 나오는 식으로 조정이 될 것이다.

image

  • 120 bpm 1분당 120비트
  • 노트 생성 위치 오브젝트는 NoteAppearLocation
  • 생성할 노트 프리팹


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

맨 위로 이동하기

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

댓글 남기기