241211

lililllilillll·2024년 12월 10일

개발 일지

목록 보기
17/350

✅ 오늘 한 일


  • Project Etude
  • C# 교과서 읽기


🎮 Project Etude


bpm에 맞춰서 움직이기

    [SerializeField] float bpm;
    float sec_per_quarter;
    float speed;

    void Start()
    {
        sec_per_quarter = 60f / bpm;
        speed = 1f / sec_per_quarter;
    }

의외로 간단

노트 블럭과 블럭 사이 점선

9 slice, tiled 적용했는데 뭔가 이상

aseprite 가서 그려버림

그려서 했는데도 안되길래 뭐지 왜 안바뀌지 했는데
생각해보니까 9 slice를 할 이유가 없었다
그냥 tiled 먹이면 예쁘게 점선 잘 그려짐

이제 점선을 이전 블럭 끝과 다음 블럭 시작점에 연결시키면 되는데

그렇게 하는 것보다 gpt가 추천해준 line renderer를 쓰는게 더 간편해보임
이건 점과 점만 주면 알아서 계산해서 연결해주는듯 함

아래가 tiled 먹인거
위가 line renderer

왜 이러는거임 도대체;

material tiling 조절하고
texture의 wrap mode를 repeat로 바꾸고
filter mode를 point로 바꿨더니
얼추 비슷해지긴 했는데 아직도 깨짐

점 y 좌표가 이상해서 그랬던거

높이 똑같이 맞추니까 안 깨지고 비슷해졌다.

문제는 대각선일 경우 이런 식으로 깨지는 걸 방지해야 된다는거.

좀 더 해상도 높은 점선 텍스처 그려서 적용시켜봤지만 깨지는 걸 막을 수 없었음.

Vectrosity라는 것도 있다고 하고, Github에서 벡터 선 그리는 방법 찾아볼 수도 있겠지만

계산 조금 번거롭더라도 tiled 먹인 일반 sprite renderer 쓰는게 정신 건강에 이로울듯?

맵 에디터 만들기

에디터에서 다음 블럭의 음표 길이와 방향을 드롭 다운으로 선택한다
sprite renderer의 bound를 이용해서 이전 블럭의 중앙점을 구한다 (아니면 pivot으로 바로 가져올 수 있나)
해당 블럭의 스크립트에서 음표 길이와 방향 정보를 가져온다
음표 길이만큼 점선의 width를 늘리고, 방향대로 rotate시킨다.
점선이 위치할 끝점에 다음 블럭을 놓고, 방향대로 rotate시킨다.

어차피 게임 내 맵 에디터 만들건데 굳이 유니티 에디터에서 맵 만들 이유가 있나 싶음.
구현 난이도도 비슷할 것 같아서 play mode일 때 맵 편집할 수 있는 기능 넣기.

현재 맵(씬)의 마지막 블럭이 무엇인지 기억하고 있어야 한다.
play 중 UI에서 드롭다운으로 노트 블럭의 길이와 방향을 선택한 뒤 생성 버튼을 누르면
노트 블럭과 점선이 생기고, 맵의 마지막 블럭이 갱신된다.
맵의 마지막 블럭은 09.JSON에 씬 이름대로 저장이 되고, 게임이 시작될 때 불러온다.

JSON에 시작 블럭도 저장해놓으면 좋을듯 (시작 블럭이 없으면 원점에 배치)
씬에 해당하는 JSON 파일이 없으면 새로 하나 만든다

#if UNITY_EDITOR
SaveToScene(newObject);
#endif

gpt가 짠 뼈대에 이런거 있길래
if문 풀면 실제 게임할 때도 작동하는거냐고 물어봤더니 아니라고 함. 왜 에디터에서만 되냐고 물어봤더니

실제 게임에서는 에셋이나 씬 데이터를 즉시 디스크에 저장하는 EditorUtility 및 SceneManager.SaveScene 같은 함수는 동작하지 않습니다.
대신, 데이터를 파일로 저장하거나 다른 지속적인 저장 방식(예: JSON, PlayerPrefs, SQLite 등)을 사용해야 합니다.

라고 함.

런타임 중 생성된 오브젝트를 저장하려면 다음 두 가지 단계가 필요합니다:

  • 오브젝트의 생성 정보를 저장 (게임 종료 시 파일에 저장).
  • 게임 시작 시 불러오기 (저장된 정보를 이용해 오브젝트를 재생성).

라고도 함.

아 그럼 JSON에 첫 블록과 마지막 블록이 뭔지만 기록하는게 아니라 모든 블록을 기록해둔 뒤에 플레이 시작할 땐 연결 리스트에 넣어놔야 되는 건가

중간에 끊긴채로 두면 경고 뜨고 저장 못하게 하는 것까진 너무 먼 미래고

일단 유니티 에디터에서만이라도 맵을 만들 수 있게 하자.

우선 UI 버튼 누르면 오브젝트가 생성되고, play mode를 꺼도 유지되는 간단한 기능부터 구현해보자.

버튼 누르면 오브젝트 생성

public class NoteBlock : MonoBehaviour
{
    public enum NoteLength
    {
        Whole,    // 온음표
        Half,     // 2분음표
        Quarter,  // 4분음표
        Eighth,    // 8분음표
        Sixteenth // 16분음표
    }

    // 방향 드롭다운
    public enum Direction
    {
        Right,
        Up,
        Left,
        Down
    }

    public NoteLength noteLength;
    public Direction direction;
}

모든 노트 블럭에는 이 스크립트 붙어있다

public class TestTest : MonoBehaviour
{
    public GameObject notePF;
    private Button myButton;
    [SerializeField] TMP_Dropdown DurationDropdown;
    [SerializeField] TMP_Dropdown DirectionDropdown;

    void Start()
    {
        myButton = GetComponent<Button>();
        myButton.onClick.AddListener(CreateBlock);
    }

    void CreateBlock()
    {
        GameObject block = Instantiate(notePF, Vector3.zero, Quaternion.identity);
        NoteBlock noteBlock = block.GetComponent<NoteBlock>();
        noteBlock.noteLength = (NoteBlock.NoteLength)DurationDropdown.value;
        noteBlock.direction = (NoteBlock.Direction)DirectionDropdown.value;
    }
}

여기까진 쉽다

로그 찍어서 데이터 잘 들어오는 것도 확인

드롭다운 옵션 따라 생성되는 노트 달라짐

    void Start()
    {
        InitCreateButton();
    }

    void InitCreateButton()
    {
        createButton = GetComponent<Button>();
        createButton.onClick.AddListener(CreateBlock);
    }

    void CreateBlock()
    {
        // block 종류 결정 및 생성
        switch (DurationDropdown.value)
        {
            case 0:
                noteBlock = Instantiate(WholeNote, Vector3.zero, Quaternion.identity);
                noteBlock.GetComponent<NoteBlock>().noteLength = NoteBlock.NoteLength.Whole;
                break;
            case 1:
                noteBlock = Instantiate(HalfNote, Vector3.zero, Quaternion.identity);
                noteBlock.GetComponent<NoteBlock>().noteLength = NoteBlock.NoteLength.Half;
                break;
            case 2:
                noteBlock = Instantiate(QuarterNote, Vector3.zero, Quaternion.identity);
                noteBlock.GetComponent<NoteBlock>().noteLength = NoteBlock.NoteLength.Quarter;
                break;
            case 3:
                noteBlock = Instantiate(EighthNote, Vector3.zero, Quaternion.identity);
                noteBlock.GetComponent<NoteBlock>().noteLength = NoteBlock.NoteLength.Eighth;
                break;
            case 4:
                noteBlock = Instantiate(SixteenthNote, Vector3.zero, Quaternion.identity);
                noteBlock.GetComponent<NoteBlock>().noteLength = NoteBlock.NoteLength.Sixteenth;
                break;
        }

        // block 방향 결정
        switch (DirectionDropdown.value)
        {
            case 0:
                noteBlock.transform.rotation = Quaternion.Euler(0, 0, 0);
                noteBlock.GetComponent<NoteBlock>().direction = NoteBlock.Direction.Right;
                break;
            case 1:
                noteBlock.transform.rotation = Quaternion.Euler(0, 0, 90);
                noteBlock.GetComponent<NoteBlock>().direction = NoteBlock.Direction.Up;
                break;
            case 2:
                noteBlock.transform.rotation = Quaternion.Euler(0, 0, 180);
                noteBlock.GetComponent<NoteBlock>().direction = NoteBlock.Direction.Left;
                break;
            case 3:
                noteBlock.transform.rotation = Quaternion.Euler(0, 0, -90);
                noteBlock.GetComponent<NoteBlock>().direction = NoteBlock.Direction.Down;
                break;
        }
    }

일단 되긴 함

switch문 좀 더 시인성 있게 바꾸고 싶어서 gpt한테 물어봤더니

    private void SetType()
    {
        GameObject[] notePrefabs = { WholeNote, HalfNote, QuarterNote, EighthNote, SixteenthNote };
        NoteBlock.NoteLength[] noteLengths =
        {
            NoteBlock.NoteLength.Whole,
            NoteBlock.NoteLength.Half,
            NoteBlock.NoteLength.Quarter,
            NoteBlock.NoteLength.Eighth,
            NoteBlock.NoteLength.Sixteenth
        };
        int durationIndex = DurationDropdown.value;

        noteBlock = Instantiate(notePrefabs[durationIndex], Vector3.zero, Quaternion.identity);
        noteBlock.GetComponent<NoteBlock>().noteLength = noteLengths[durationIndex];
    }

이런 느낌으로 좀 더 깔끔하게 바꿔줌.
내가 원하던 건 이런게 아니었긴 한데

어쨌든 더 나아졌고, 생각해보니 NoteBlock.cs에 있는 enum정보를 여기서 사용하려면 switch문으로 돌아가야되는데다 길이도 길어지고 곤란할 것 같아서 일단 이걸로 타협.

내일 할 거

자꾸 가상의 시나리오를 지어내면서 모든 상황에 대응할만한 코드와 기능을 만들려고 하는데, 그러지 말고 정확히 내가 필요한 기능만 만들자.

내가 필요한 기능은 각 씬에서 노트를 편집할 수 있는 맵 에디터다.

향후 확장성 고려하는 건 좋은데, 그렇다고 모든 경우의 수 따지면서 아직 한참 먼 미래 얘기를 굳이 생각할 필요는 없다. 그건 그때가서 추가하면 됨.

내일 할 거 정리하면

  • 클릭하면 해당 오브젝트 선택. 그 후 삭제 버튼 누르면 삭제.
  • 씬 저장 기능 Inspector 체크박스로 넣어놓기 (easy save 기능 알아보기)
  • 점과 점 사이를 선으로 연결하기
  • 플레이어 박자선 만들기
  • 박자 맞춰서 누르면 이펙트
  • 박자 얼마나 정확하게 맞췄는지 따라 종류 구분하여 저장


📖 C# 교과서


28 제네릭 사용하기

Cup<T> : Cup of T라고 발음한다. 형식 매개변수인 T에 따른 Cup 클래스의 개체를 생성한다.

제네릭: 넘어오는 데이터 형식에 따라 해당 개체 성격을 변경하는 구조

제네릭 컬렉션은 다른 데이터 형식을 추가할 수 없도록 형식 안정성을 적용한다.
고전적인 컬렉션 클래스와 달리 제네릭 컬렉션 클래스는 요소를 다룰 때 데이터 형식 변환 등 작업이 따로 필요하지 않다.

28.2 Stack 제네릭 클래스 사용하기

> using Systemn.Collections.Generic;
> Stack<string> stack = new Stack<string>();
> stack.Push("First");

제네릭 클래스 사용의 장점

Stack이 아닌 Stack<T> 클래스를 사용하면
값형 데이터를 참조형 데이터로 변환하는 박싱 작업과
참조형 데이터를 값형 데이터로 변환하는 언방식 작업을 하지 않아도 되므로
성능이 향상된다.

28.3 List<T> 제네릭 클래스 사용하기

> List<int> lstNumbers = new List<int<();
> lstNumbers.Add(30);

28.4 Enumerable 클래스로 컬렉션 만들기

> Enumerable.Range(1,10)
RangeIterator { 1,2,3,4,5,6,7,8,9,10 }
> Enumerable.Repeat(100,5)
RepeatIterator { 100,100,100,100,100 }

28.5 Dictionary<T,T> 제네릭 클래스 사용하기

> var data = new Dictionary<string, string>();
> data.Add("cs", "C#");
> foreach (var item in data)
. {
.    Console.WriteLine(item.Key, item.Value);
. }
// cs 키가 있으면 csharp 변수에 담음
data.TryGetValue("cs", out var csharp)

// 키 값이 있는지 없는지
data.ContainsKey("json")

29 널(null) 다루기

참조형 변수에 아무런 값을 설정하지 않음
null값과 빈 값(Empty, "")은 다르다

29.2 null 가능 형식: Nullable<T> 형식

간단하게 설명하면, 값형 변수에는 원래 null 할당 못하고 에러 나는데, 할당할 수 있게 바꿔주는 거.
bool을 예로 들면

  • bool: true, false
  • Nullable<bool>: true, false, null

줄여서 표현하면 bool?, int?처럼 데이터 형식 뒤에 ?(물음표) 기호 붙이면 됨.

Nullable<T> 형식이 제공하는 주요 멤버

  • HasValue: 값이 있으면 true, null 값이면 false
  • Value: 값 반환
  • GetValueOrDefulat: 값 또는 기본값 반환

29.3 null 값을 다루는 연산자 소개하기

?? 연산자(null 병합 연산자)

> nullValue = null;
> message = nullValue ?? "null일 경우 반환"
"null일 경우 반환"
> int? x = null;
> int y = x ?? default(int);

null 가능 형식과 default 키워드를 함께 사용한 예제.

> double? d = null;
> d?,ToString()
null

?. 연산자는 해당 값이 null인지 테스트

> List<string> list = null;
> num = list?.Count ?? 0;

list?.Count ?? 0 형태의 코드 익히면 좋다.

30 LINQ

닷넷에서는 특정 형식에 원래는 없던 기능을 덧붙이는 개념으로 확장 메서드(extension method)를 제공한다.
using System.Linq 네임스페이스 선언하면 사용할 수 있다.

주요 메서드

  • Sum()
  • Count()
  • Average()
  • Max()
  • Min()

30.3 화살표 연산자와 람다 식으로 조건 처리

> Func<int, bool> isEven = x => x % 2 == 0;
> isEven(2)
true
> Action<string> greet = name => { var message = "Hello"; Console.WriteLine(message); };
> greet()
Hello

람다 식 자체는 하나의 함수고, 함수를 가리키는 함수 포인터이다.

Where() 메서드로 IEnumerable<T> 형태의 데이터 가져오기

> int[] numbers = { 1,2,3,4,5 }
> IEnumerable<int> newNumbers = numbers.Where(number => number > 3);

Where() 메서드에 람다 식을 제공하여 새로운 컬렉션을 가져올 수 있다.

All()과 Any() 메서드로 조건 판단하기

LINQ의 All()과 Any() 메서드는 배열 또는 컬렉션에서 모든 조건을 만족하거나 하나의 조건이라도 만족해야 하는 경우를 판단.

> bool[] completes = { true, false, true };
> completes.All(c => c == true);
false

All()은 조건식을 모두 만족해야 true

> bool[] completes = { true, false, true };
> completes.Any(c => c == false);
true

Any()는 하나라도 조건식을 만족하면 true

Take()와 Skip() 메서드로 필요한 건수의 데이터 가져오기

> var data = Enumerable.Range(0, 100);
> data.Take(5);
TakeIterator { 0,1,2,3,4 }
> data.Where(n => n % 2 == 0).Take(5);
TakeIterator { 0,2,4,6,8 }

Take()는 지정한 개수만큼 데이터 가져온다

> var data = Enumerable.Range(0, 100)
> var next = data.Skip(10).Take(5); // 10개를 제외하고 5개 가져오기
> next
TakeIterator {10,11,12,13,14}

Skip()은 지정한 수만큼 데이터를 제외한 컬렉션을 반환

Distinct() 확장 메서드로 중복 제거하기

> var data = Enumerable.Repeat(3,5);
> var result = data.Distinct();
> result
DistinctIterator {3}

30.4 데이터 정렬과 검색

OrderBy() 메서드로 문자열 컬렉션 오름차순 정렬하기

> string[] colors = { "Red", "Green", "Blue" };
> var sortedColors = colors.OrderBy(name => name); // Blue, Green, Red

OrderByDescending() 메서드로 문자열 컬렉션 내림차순 정렬하기

> var colors = new List<string> { "Red", "Blue", "Green" };
> var sortedColors = colors.OrderByDescending(c => c); // Red Green Blue

특정 문자열을 포함하는 컬렉션 가져오기

> var colors = new List<string> { "red", "green", "blue" };
> var newColors = colors.Where(c => c.Contains("ee")); // green

Single()과 SingleOrDefault() 확장 메서드

Single()과 SingeOrDefault()는 컬렉션에서 조건에 맞는 값을 단 하나만 가져오는 확장 메서드이다.

  • Single(): null 값이면 예외가 발생한다. 즉, 에러가 발생한다.
  • SingleOrDefault(): 값이 없으면 null 값을 반환한다.
> string black = colors.Single(color => color == "Black"); // black 없으면 오류 남
> string black = colors.SingleOrDefault(color => color == "Black"); // black 없으면 null 반환

First()와 FirstOrDefault() 확장 메서드

컬렉션에서 첫 번째 요소를 가져온다.

  • First(): 첫 번째 요소가 없으면 에러가 발생
  • FirstOrDefault(): 첫 번째 요소가 없으면 기본값 반환

30.5 메서드 구문과 쿼리 구문

  • 메서드 구문: Where()같은 메서드를 사용하여 컬렉션 다루기
  • 쿼리 구문: from, where, select 같은 키워드를 사용하여 쿼리 형태로 컬렉션 다루기
> numbers.Where(n => n % 2 == 0).OrderByDescending(n => n)
OrderedEnumerable<int, int> { 10,8,6,4,2 }
> (from n in numbers where n % 2 == 0 orderby n descending select n)
OrderedEnumerable<int, int> { 10,8,6,4,2 }

두 가지 문법으로 같은 결과를 낼 수 있다.
쿼리 구문에 메서드를 붙일 수도 있다.

30.6 Select() 확장 메서드를 사용하여 새로운 형태로 가공하기

Select()는 컬렉션에서 새로운 형태의 데이터로 만들어 사용할 수 있다.

var nums = numbers.Select(n => n * n);

foreach( var num in nums)

Select()의 결괏값은 따로 클래스 이름이 정해지지 않은 익명 형식이기에 반드시 var과 함께 사용해야 한다.

// 점수가 80 이상인 학생의 이름만 선택
var highScorers = students
    .Where(s => s.Score >= 80)       // 필터링
    .Select(s => s.Name);            // 변환
  • 단순히 조건을 적용해서 데이터를 골라낼 때는 where()만 사용하면 충분.
  • 데이터를 변환하거나 새로운 형식으로 바꾸어야 할 때는 select()가 필요.

30.7 ForEach() 메서드로 반복 출력하기

> var numbers = new List<int>() {10,20,30,40,50};
> numbers.Where(n => n <= 20).ToList().ForEach(n => Console.WriteLine(n));
10
20

for문이나 foreach문 사용 안 하고 각 요소에 대해 반복 돌릴 수 있다



profile
너 정말 **핵심**을 찔렀어

0개의 댓글