[Unity] Project. Personal_9

Lingtea_luv·2025년 6월 17일

Project

목록 보기
26/38
post-thumbnail

Where Am I?


10일차

이번 프로젝트를 구상할 때 세웠던 목표 중 하나는 데이터 저장 기능을 구현해보는 것이었다. 하지만 생각보다 일정이 밀려 정해진 기간 내에 구현하기 힘들다는 결론을 내렸고, 결국 해당 기능은 포기하고 빌드 테스트를 할 수 밖에 없었다. 그런데 너무 아쉽다는 생각이 들기도 했고, 꼭 구현해보고 싶던 기능인데 마침 강사님이 특강으로 데이터 저장에 대해 알려주셔서 이를 활용해보기로 했다.

우선, 어떤 데이터를 저장할지 고민을 해야했다. 게임 플레이 중간에 저장할 수 있도록 구현한다면 맵 데이터, 미니맵, 아이템, 조합 아이템, 인벤토리, 몬스터 위치 등 생각보다 많은 데이터를 저장해야했고, 이를 위해서는 전체적인 구조 개선이 필요해보였다. 하지만 당장 모레부터 팀 프로젝트에 들어가기 때문에 단순히 아이템 봉인 단계만 저장하는 정도로 구현을 했다.

GameData

데이터를 저장하기 위한 클래스로 아이템 봉인 여부를 저장할 수 있도록 bool 타입 필드를 가지도록 했다.

// 디버그를 위한 직렬화 
[System. Serializable]
public class GameData
{
    public bool HasBackpack;
    public bool HasCompass;
    public bool HasLantern;

    private void Awake()
    {
        Init();
    }

	// 처음에는 전부 가지고 있는 상태로 초기화
    private void Init()
    {
        HasBackpack = true;
        HasCompass = true;
        HasLantern = true;
    }
}

DataManager

특강에서 배운 코드를 활용하여 데이터의 저장/불러오기 기능을 담당하도록 구현한 클래스로, GameData를 참조하여 데이터를 관리한다.

public class DataManager : Singleton<DataManager>
{
	// 데이터를 보관할 GameData 클래스
    public GameData GameData;
	
    // 데이터 파일의 기본 이름 설정
    public const string fileName = "SaveFile";
    
    // 디버그를 위한 전처리기 설정
    // 디버그 시 저장되는 경로(Assets 폴더)
#if UNITY_EDITOR
    public string path => Path.Combine(Application.dataPath, $"Data/{fileName}");
#else
	// 실제 빌드 시 저장되는 경로(user local 폴더)
    public string path => Path.Combine(Application.persistentDataPath, $"Data/{fileName}");
#endif

	// 데이터를 저장할 때 호출되는 이벤트
    public event Action OnFileChanged;
    // 데이터를 불러올 때 호출되는 이벤트
    public event Action OnGameLoaded;
    
    // Singleton 초기화 메서드, 데이터 유지를 위한 DontDestroyOnLoad
    protected override void Awake()
    {
        base.Awake();
        DontDestroyOnLoad(gameObject);
    }
    
    // 경로에 파일이 있는지 확인하는 메서드
    public bool ExistData(int slot)
    {
    	// 경로에 해당하는 폴더에 {path}_{slot}라는 이름의 파일이 없는 경우
        if (Directory.Exists((Path.GetDirectoryName($"{path}_{slot}"))) == false)
        {
        	// false 반환
            return false;
        }
        // 있는 경우 true 반환
        return File.Exists($"{path}_{slot}");
    }
     
    // 데이터를 저장하는 메서드
    public void SaveData(int slot)
    {
    	파일 유무를 확인하고 없는 경우
        if (!ExistData(slot))
        {
        	// 경로에 {path}_{slot}라는 이름의 파일 신규 생성
            Directory.CreateDirectory(Path.GetDirectoryName($"{path}_{slot}"));
        } 
        // GameData에 저장된 데이터를 JsonUtility의 메서드를 활용하여 문서화
        string json = JsonUtility.ToJson(GameData);
        
        // {path}_{slot} 파일의 내용을 json으로 설정
        File.WriteAllText($"{path}_{slot}",json);

		// 데이터가 저장되었으니 이벤트 호출
        OnFileChanged?.Invoke();
    }

    // 데이터를 불러오는 메서드
    public bool LoadData(int slot)
    {
    	// 데이터를 저장한 파일이 없는 경우 false 반환(스킵)
        if (!ExistData(slot)) return false;

		// 파일에 들어있는 텍스트를 string 변수에 저장
        string json = File.ReadAllText($"{path}_{slot}");
        
        // GameData를 JsonUtility의 메서드를 활용하여 파일에 저장된 데이터로 업데이트
        GameData = JsonUtility.FromJson<GameData>(json);
        
        // 데이터를 불러왔으니 이벤트 호출
        OnGameLoaded?.Invoke();
        
        // 데이터를 불러오는데 성공했다면 true 반환
        return true;
    }
}

ItemManager

데이터 기능을 구현하기 전에 아이템 봉인 여부를 담당하는 클래스로 이를 없애지 않고 DataManager와 연계하여 재사용하기로 했다.

public class ItemManager : Singleton<ItemManager>
{
	// 기존 아이템 봉인 여부 데이터를 담고 있던 bool 타입 Property
    public Property<bool> HasLantern;
    public Property<bool> HasCompass;
    public Property<bool> HasBackpack;

	// 데이터가 저장 중일 때 중복 호출 방지를 위해 사용할 태그
    private bool _isLoadingData;
    
    // Singleton 초기화 메서드, 데이터 유지를 위한 DontDestroyOnLoad, 자체 초기화 메서드
    protected override void Awake()
    {
        base.Awake();
        if (transform.parent == null)
        {
            DontDestroyOnLoad(gameObject);
        }
        Init();
    }

	// 외부 이벤트 구독은 Start에서 처리
    private void Start()
    {
        DataManager.Instance.OnGameLoaded += LoadItemData;
    }
    
    // GameClear씬의 각 봉인 Button에 등록된 메서드
    public void GetLantern()
    {
        HasLantern.Value = false;
    }

    public void GetCompass()
    {
        HasCompass.Value = false;
    }

    public void GetBackpack()
    {
        HasBackpack.Value = false;
    }

	// 데이터를 불러올 때 같이 호출되는 메서드
    // 실제 게임에 적용되는 내부 데이터를 GameData로 업데이트
    private void LoadItemData()
    {
    	// 중복 호출을 방지하기 위한 태그
        _isLoadingData = true;
        
        HasBackpack.Value = DataManager.Instance.GameData.HasBackpack;
        HasCompass.Value = DataManager.Instance.GameData.HasCompass;
        HasLantern.Value = DataManager.Instance.GameData.HasLantern;

		// 데이터 불러오기가 끝났으면 false처리
        _isLoadingData = false;
    }

	// GameData에 게임 클리어 결과를 반영하여 업데이트
    private void SaveItemData(bool value)
    {
    	// 데이터가 로드 중일 때는 호출x
        if (_isLoadingData) return;
        
        DataManager.Instance.GameData.HasBackpack = HasBackpack.Value;
        DataManager.Instance.GameData.HasCompass = HasCompass.Value;
        DataManager.Instance.GameData.HasLantern = HasLantern.Value;
    }
    
    private void Init()
    {
    	// Property 초기화
        HasBackpack = new Property<bool>(true);
        HasCompass = new Property<bool>(true);
        HasLantern = new Property<bool>(true);
        
        // 내부 이벤트 구독은 Awake에서 처리
        HasBackpack.OnChanged += SaveItemData;
        HasCompass.OnChanged += SaveItemData;
        HasLantern.OnChanged += SaveItemData;

        _isLoadingData = false;
    }
}

SaveUI / LoadUI

데이터의 저장과 불러오기를 하나의 UI가 처리하는 것이 아닌 2개로 나누어 각각의 UI에서 처리할 수 있도록 구현했다.

// 데이터 저장을 담당하는 UI
public class SaveUI : MonoBehaviour
{
    [Header("Drag&Drop")] 
    [SerializeField] private List<Button> _slots;
    [SerializeField] private List<TMP_Text> _texts;
    [SerializeField] private Button _exitBtn;

    private void Start()
    {
        Init();
    }

    private void Init()
    {
    	// Button에 slot별 데이터 저장 메서드를 등록
        for (int i = 0; i < _slots.Count; i++)
        {
        	// 람다 캡처에 따른 참조 형식을 지역 변수로 캐싱하여 값 형식으로 바꾸어 사용
            int slotIndex = i;
            _slots[slotIndex].onClick.AddListener(()=>DataManager.Instance.SaveData(slotIndex));
        }
        
        // 버튼 옆의 문구 초기화
        SetText();
        
        // 데이터가 저장될 때 호출되는 메서드에 SetText 추가
        DataManager.Instance.OnFileChanged += SetText;
        
        // 나가기 버튼에 UI 비활성화 연동
        _exitBtn.onClick.AddListener(()=> gameObject.SetActive(false));
    }
	
    // 데이터가 저장되면 출력되는 문구를 변경하는 메서드(지금보니 비효율적인 것 같다)
    private void SetText()
    {
        for (int i = 0; i < _texts.Count; i++)
        {
        	// 해당 형식은 람다식이 아니기 때문에 값 형식으로 작동하여 지역 변수로 캐싱할 필요x
            _texts[i].text = DataManager.Instance.ExistData(i) ? "File Exist" : "Empty";
        }
    }
}

public class LoadUI : MonoBehaviour
{
    [Header("Drag&Drop")] 
    [SerializeField] private List<Button> _slots;
    [SerializeField] private List<TMP_Text> _texts;
    [SerializeField] private Button _exitBtn;

    private void Start()
    {
        Init();
    }

    private void Init()
    {
        for (int i = 0; i < _slots.Count; i++)
        {
        	// 람다 캡처에 따른 참조 형식을 지역 변수로 캐싱하여 값 형식으로 바꾸어 사용
            int slotIndex = i;
            
            // 각 버튼에 데이터 로드 메서드와 게임 시작 메서드를 등록
            _slots[slotIndex].onClick.AddListener(()=>
            {
                if (DataManager.Instance.LoadData(slotIndex))
                {
                    GameManager.Instance.GameStart();
                }
            });
        }      
        SetText();
        DataManager.Instance.OnFileChanged += SetText;
        _exitBtn.onClick.AddListener(()=> gameObject.SetActive(false));
    }
    
    private void SetText()
    {
        for (int i = 0; i < _texts.Count; i++)
        {
            _texts[i].text = DataManager.Instance.ExistData(slotIndex) ? "File Exist" : "Empty";
        }
    }
}
profile
뚠뚠뚠뚠

0개의 댓글