[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문으로 돌아가야되는데다 길이도 길어지고 곤란할 것 같아서 일단 이걸로 타협.
자꾸 가상의 시나리오를 지어내면서 모든 상황에 대응할만한 코드와 기능을 만들려고 하는데, 그러지 말고 정확히 내가 필요한 기능만 만들자.
내가 필요한 기능은 각 씬에서 노트를 편집할 수 있는 맵 에디터다.
향후 확장성 고려하는 건 좋은데, 그렇다고 모든 경우의 수 따지면서 아직 한참 먼 미래 얘기를 굳이 생각할 필요는 없다. 그건 그때가서 추가하면 됨.
내일 할 거 정리하면
Cup<T> : Cup of T라고 발음한다. 형식 매개변수인 T에 따른 Cup 클래스의 개체를 생성한다.
제네릭: 넘어오는 데이터 형식에 따라 해당 개체 성격을 변경하는 구조
제네릭 컬렉션은 다른 데이터 형식을 추가할 수 없도록 형식 안정성을 적용한다.
고전적인 컬렉션 클래스와 달리 제네릭 컬렉션 클래스는 요소를 다룰 때 데이터 형식 변환 등 작업이 따로 필요하지 않다.
> using Systemn.Collections.Generic;
> Stack<string> stack = new Stack<string>();
> stack.Push("First");
Stack이 아닌 Stack<T> 클래스를 사용하면
값형 데이터를 참조형 데이터로 변환하는 박싱 작업과
참조형 데이터를 값형 데이터로 변환하는 언방식 작업을 하지 않아도 되므로
성능이 향상된다.
List<T> 제네릭 클래스 사용하기> List<int> lstNumbers = new List<int<();
> lstNumbers.Add(30);
> 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 }
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")
참조형 변수에 아무런 값을 설정하지 않음
null값과 빈 값(Empty, "")은 다르다
Nullable<T> 형식간단하게 설명하면, 값형 변수에는 원래 null 할당 못하고 에러 나는데, 할당할 수 있게 바꿔주는 거.
bool을 예로 들면
Nullable<bool>: true, false, null줄여서 표현하면 bool?, int?처럼 데이터 형식 뒤에 ?(물음표) 기호 붙이면 됨.
Nullable<T> 형식이 제공하는 주요 멤버
> 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 형태의 코드 익히면 좋다.
닷넷에서는 특정 형식에 원래는 없던 기능을 덧붙이는 개념으로 확장 메서드(extension method)를 제공한다.
using System.Linq 네임스페이스 선언하면 사용할 수 있다.
주요 메서드
> Func<int, bool> isEven = x => x % 2 == 0;
> isEven(2)
true
> Action<string> greet = name => { var message = "Hello"; Console.WriteLine(message); };
> greet()
Hello
람다 식 자체는 하나의 함수고, 함수를 가리키는 함수 포인터이다.
IEnumerable<T> 형태의 데이터 가져오기> int[] numbers = { 1,2,3,4,5 }
> IEnumerable<int> newNumbers = numbers.Where(number => number > 3);
Where() 메서드에 람다 식을 제공하여 새로운 컬렉션을 가져올 수 있다.
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
> 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()은 지정한 수만큼 데이터를 제외한 컬렉션을 반환
> var data = Enumerable.Repeat(3,5);
> var result = data.Distinct();
> result
DistinctIterator {3}
> string[] colors = { "Red", "Green", "Blue" };
> var sortedColors = colors.OrderBy(name => name); // Blue, Green, Red
> 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()과 SingeOrDefault()는 컬렉션에서 조건에 맞는 값을 단 하나만 가져오는 확장 메서드이다.
> string black = colors.Single(color => color == "Black"); // black 없으면 오류 남
> string black = colors.SingleOrDefault(color => color == "Black"); // black 없으면 null 반환
컬렉션에서 첫 번째 요소를 가져온다.
> 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 }
두 가지 문법으로 같은 결과를 낼 수 있다.
쿼리 구문에 메서드를 붙일 수도 있다.
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()가 필요.> 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문 사용 안 하고 각 요소에 대해 반복 돌릴 수 있다