241218

lililllilillll·2024년 12월 18일

개발 일지

목록 보기
24/350

✅ 오늘 한 일


  • Project Etude


🎮 Project Etude


맵 저장, 불러오기

템플릿 애셋같은 것들 봐도 파일 많이 안 쪼개고 하나에 길게 적었던 이유가
Monobehaviour의 저주 때문에 그런게 아닐까
나도 그냥 region으로 구분해야 할듯
저장 불러오기 메서드 분리하지 말고 같은 클래스에 쓰자

아니 이건 또 왜 이러는데 같은 메서드잖아
아 매개변수랑 return값 있어서 그랬던거

드롭다운 변경할 때 상태 전환을 어떻게 할지 애매해서 얼불춤 확인해봤더니
여긴 탐색기를 사용하네
또 레이어 하나 더 들어가야 됨
판정 → 맵 편집 → 맵 저장 → 탐색기

https://github.com/gkngkc/UnityStandaloneFileBrowser

    private void LoadMapData()
    {
        // TODO: 탐색기를 이용해 easy save로 저장된 데이터를 불러와서 noteBlockDataList에 저장

        var paths = StandaloneFileBrowser.OpenFilePanel("Select a Map", Application.persistentDataPath, "etude", false);

        if (paths.Length > 0 && !string.IsNullOrEmpty(paths[0]))
        {
            string selectedFilePath = paths[0];
            Debug.Log("Selected File: " + selectedFilePath);

            // Easy Save로 선택한 파일 로드
            if (ES3.FileExists(selectedFilePath))
            {
                noteBlockDataList = ES3.Load<List<NoteBlockData>>("NoteBlocks", selectedFilePath);
                Debug.Log("Data loaded successfully.");
            }
            else
            {
                Debug.LogWarning("File not found or incompatible with Easy Save.");
            }
        }
        else
        {
            Debug.LogWarning("No file selected.");
        }
    }

탐색기로 불러오기 구현 완료
는 아니고 불러올 맵을 만들어야 한다

불러올 맵을 만들려면 일단 저장을 구현해야 하고
저장을 하려면 저장할 데이터를 만들어야 함

실제 화면에 있는 오브젝트들과
데이터로 관리하고 있는 리스트가 다름

오브젝트들은 MapRoot라는 공통 부모를 갖게 될 것

        // noteBlockDataList에 있는 데이터로 MapRoot 아래에 노트 블럭들 생성
        spawnPosition = Vector3.zero;
        foreach (NoteBlockData noteBlockData in noteBlockDataList)
        {
            GameObject noteBlock = Instantiate(notePrefabs[noteBlockData.noteLength], spawnPosition, Quaternion.identity);
            noteBlock.transform.rotation = Quaternion.Euler(0, 0, noteDirections[noteBlockData.direction]);
            noteBlock.GetComponent<NoteBlockData>().noteLength = noteBlockData.noteLength;
            noteBlock.GetComponent<NoteBlockData>().direction = noteBlockData.direction;
            noteBlock.GetComponent<NoteBlockData>().order = noteBlockData.order;
            noteBlock.transform.SetParent(MapRoot.transform);
            spawnPosition += GetDisplacement(noteBlockData);
        }

로드할 때 MapRoot 아래에 오브젝트 생성하는 코드
아 y축 좌표 순서에 맞게 곱해주는거 까먹음 나중에 하자

    private void AddMap()
    {
        // TODO: fileName은 별도 UI로 입력받아야 함
        string fileName = "NewMap.etude";
        string filePath = Path.Combine(Application.persistentDataPath, fileName);

        if (!File.Exists(filePath)) // 같은 파일 있는지 확인하고
        {
            File.Create(filePath).Close(); // 빈 파일 생성 후 스트림 닫기
            selectedFilePath = filePath;
            Debug.Log($"File created: {fileName}");
        }
        else
        {
            Debug.LogWarning($"File already exists: {filePath}");
        }
    }

경로는 임시로 대충 초기화해놓음
selectedFilePath는 전역 변수로 관리해서 savemapdata()에서 쓸 수 있게

    private void SaveMapData()
    {
        // noteBlockDataList에 있는 데이터를 easy save로 저장
        if (selectedFilePath != null)
        {
            ES3.Save<List<NoteBlockData>>("Map", noteBlockDataList, selectedFilePath);
            Debug.Log("Data saved successfully.");
        }
        else
        {
            Debug.LogWarning("No file selected to save.");
        }
    }

save 구현은 간단

하지 않았다 오류 남

아오 분명 그냥 저장해도 된데매

using UnityEngine;

public class NoteBlockData : MonoBehaviour
{
    public string noteLength;
    public string direction;
    public int order;

    // 데이터를 저장용 DTO로 변환
    public NoteBlockDataDTO ToDTO()
    {
        return new NoteBlockDataDTO(noteLength, direction, order);
    }

    // DTO를 다시 NoteBlockData로 변환
    public void FromDTO(NoteBlockDataDTO dto)
    {
        noteLength = dto.noteLength;
        direction = dto.direction;
        order = dto.order;
    }
}

각 노트 블럭에 붙이려면 MonoBehaviour는 어쩔 수 없이 붙어야 되니까
DTO(Data Transfer Object) 방식을 사용하라고 함. 그니까 클래스를 또 파라는거. 돌아버리겠네.

MonoBehaviour를 포기할 순 없음
그래야 플레이할 때 중간에 있는 오브젝트 선택해서 편집하는게 가능함

그저께 내가 생각해냈던 방법이 이거랑 비슷하면서 더 간결. 노트 블럭 리스트랑 저장용 리스트 2개의 일관성을 맞춰야해서 귀찮긴 한데 다른 방법이 안 보인다. gpt가 말한 것처럼 메서드 쓰는 건 두 정보 사이를 이을 수가 없음. 나는 같은 인덱스로 관리해서 해결할거.


public class NoteBlockIndex : MonoBehaviour
{
    public int noteBlockIndex;
}

근데 이래도 안됨

어우 이게 문제였네
빈 파일 만들면 안됐던거
easy save 믿고 경로만 설정한 후에 신뢰의 도약 했어야됨

데이터도 잘 들어가는 거 확인

이제 클래스에서 Serializeable 떼도 잘 들어간다

근데 데이터들을 어디에 어떻게 관리해야할지 아직도 모르겠고 빙빙 헤매게 돼서 한 번 정리해보면

  1. loadmap()으로 .etude 파일에 들어있던 NoteBlockData들을 읽어서 GameObject들을 만든다.
  2. 노트 블럭을 추가하면 NoteBlock.csNoteBlockIndex가 함께 기록된다. NoteBlockIndex는 해당 블록의 List<NoteBlockData>에서의 인덱스를 나타낸다. 또한, List<NoteBlockData>에도 NoteBlockData를 추가한다.
  3. 노트 블럭을 편집하려면 클릭해서 GameObject를 선택하면 된다. GameObject에 있는 NoteBlockIndex를 기반으로 편집이 이루어진다.
  4. 노트 블럭을 편집하면 NoteBlockIndexList<NoteBlockData>에 있는 정보들이 변할 수 있다.
  5. 맵을 저장하면 지정된 경로에 List<NoteBlockData>를 저장한다.

자료형이랑 로직 좀 바꾸니까 코드를 처음부터 끝까지 다 수정하고 검토해줘야했다;

와캬퍄

힘들었지만 완성

블럭 거리에 맞게 떨어져서 생성

    private void InstantiateBlock()
    {
        string duration = DurationDropdown.options[DurationDropdown.value].text;
        string direction = DirectionDropdown.options[DirectionDropdown.value].text;

        NewBlock = Instantiate(notePrefabs[duration], spawnPosition, Quaternion.identity);
        NewBlock.transform.rotation = Quaternion.Euler(0, 0, noteDirections[direction]);
        NewBlock.transform.SetParent(MapRoot.transform);
        spawnPosition += GetDisplacement(new NoteBlockData { noteLength = duration, direction = direction });
    }

새로운 블럭을 생성할 때마다 해당 블럭의 길이와 방향에 맞게 다음 spawnposition을 지정해준다.

    private Vector3 GetDisplacement(NoteBlockData noteBlockData)
    {
        Dictionary<string, float> noteLengths = new Dictionary<string, float>
        {
            { "DottedWhole", 6.0f },
            { "Whole", 4.0f },
            { "DottedHalf", 3.0f },
            { "Half", 2.0f },
            { "DottedQuarter", 1.5f },
            { "Quarter", 1.0f },
            { "DottedEighth", 0.75f },
            { "Eighth", 0.5f },
            { "DottedSixteenth", 0.375f },
            { "Sixteenth", 0.25f }
        };

        Dictionary<string, Vector3> directions = new Dictionary<string, Vector3>
        {
            { "Up", new Vector3(0, 1, 0) },
            { "Down", new Vector3(0, -1, 0) },
            { "Right", new Vector3(1, 0, 0) },
            { "Left", new Vector3(-1, 0, 0) },
            { "UpRight", new Vector3(Mathf.Sqrt(2) / 2, Mathf.Sqrt(2) / 2, 0) },
            { "UpLeft", new Vector3(-Mathf.Sqrt(2) / 2, Mathf.Sqrt(2) / 2, 0) },
            { "DownRight", new Vector3(Mathf.Sqrt(2) / 2, -Mathf.Sqrt(2) / 2, 0) },
            { "DownLeft", new Vector3(-Mathf.Sqrt(2) / 2, -Mathf.Sqrt(2) / 2, 0) }
        };

        float displacement = noteLengths[noteBlockData.noteLength];
        Vector3 directionVector = directions[noteBlockData.direction];

        return directionVector * displacement;
    }

딕셔너리에서 길이와 방향에 맞는 변위를 구하여 반환

이제 노트 길이만큼 떨어져서 생성된다

노트 변경하거나 삭제하면 이후 블럭 전부 삭제

한 가지 실수했던게 있다.
블럭의 정보를 저장할 때 List<GameObject>가 아닌 List<NoteBlock>을 사용해서,
각 오브젝트에 있는 인덱스만으로는 다음 블럭에 접근을 하지 못한다.

각 노트 블럭에 연결 리스트로 이후 블럭을 연결해놓으면 최소한의 변경으로 문제 해결 가능하긴 할텐데
나중에 위치 변경시 블러 효과 넣을지 말지 결정되는건 해결 불가

근데 그건 어차피 List<GameObject>로 갖고 있어도
최종 저장할 때 전부 순회하거나 raycast 한 번씩 쏴서 이벤트 지정해줘야하는 거일듯
일단 먼 얘기니까 생각하지 말자

public class NoteBlockIndex : MonoBehaviour
{
    public int noteBlockIndex;
    public GameObject prevNoteBlock;
    public GameObject nextNoteBlock;
}

NoteBlockIndex에 이전과 이후 레퍼런스를 달아주고

    void CreateNewBlock()
    {
        InstantiateBlock();
        AddNewBlockDataToList(); // 새 블럭 정보 저장용 리스트에 추가
        NewBlock.GetComponent<NoteBlockIndex>().noteBlockIndex = NoteAllocateIndex++; // 접근용 인덱스 할당

        // 이전 블럭과 쌍방 연결
        if (LastBlock != null)
        {
            NewBlock.GetComponent<NoteBlockIndex>().prevNoteBlock = LastBlock;
            LastBlock.GetComponent<NoteBlockIndex>().nextNoteBlock = NewBlock;
            LastBlock = NewBlock;
        }
    }

CreateNewBlock(), ChangeBlock(), DeleteBlock()에 정보 갱신 관리 로직 추가하여
연결 리스트 생성 완료

    /// <summary>
    /// 해당 블럭과 이후의 모든 블럭을 삭제
    /// </summary>
    private void WipeBlocksFromTheBlock(GameObject TheBlock)
    {
        while (TheBlock != null)
        {
            GameObject temp = TheBlock.GetComponent<NoteBlockIndex>().nextNoteBlock;
            Destroy(TheBlock);
            TheBlock = temp;
        }
    }

원래는 변경이나 삭제하면 이후 블럭들 재배치도 하려고 했는데 좀 복잡해지기도 하고
맵 편집할 때 의미있는 기능이 될지 의문이 들어서 일단 전부 삭제로 구현

이렇게되면 종류 변경이 살짝 의미 없어지긴 하는데
혹시 모르니까 굳이 지우진 말고 이대로 두기

이제 맵 편집기는 거의 다 완성했다

저장 후 불러오면 경고 뜨면서 안 불러와지긴 하는데 이거만 내일 고치면 됨.

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

0개의 댓글