내일배움캠프 TIL

내일배움캠프 67일차 TIL "중간발표 기능 정리"

Jooglorystar 2024. 12. 20. 23:48

 

 

중간 발표를 위해 그동안 개발한 것의 정리가 필요했다.

 

기능이 어느정도 완성되었기에 발표를 할 만한 맡은 부분은 TimeManger와 CropManager이라고 판단했다.

 

TimeManager를 발표하기 위해서는 이와 연관이 깊은 TimeUI와 LightCycle과의 연동되는 설명이 필요하고,

CropManager의 경우 TileManager와의 정보 연동을 설명이 필요하다고 느껴졌다.

 

 

1. TimeManager의 대표적인 특징

 

1.1. 인게임 시간 진행

// 시간을 흐르게 하는 메서드
private void StartTime()
{
    _lastCheckedRealTime += Time.deltaTime;
    if (_lastCheckedRealTime >= _realTimeSecondPerInGameMinuteUnit)
    {
        _inGameMinute += _inGameMinuteUnit;

        CallTimeCheck();

        _lastCheckedRealTime = 0f;
    }
}

// 인게임 시간 계산 메서드
private void CalculateInGameTime()
{
    // 분->시 계산
    if (_inGameMinute >= 60)
    {
        _inGameHour += (_inGameMinute / 60);
        _inGameMinute %= 60;
    }

    // 00시 01시 02시 계산용
    if (_inGameHour >= 24)
    {
        _inGameHour %= 24;
    }

    // 2시에 시간을 멈추게 함
    if (_inGameHour == 2)
    {
        IsPlayingTime = false;
    }
}

 

TimeManager의 대표적인 기능중 하나는 현실 시간이 지나면 특정 시간을 더해주는 기능이다.

이 값은 [SerializeField]으로 선언되어 인스펙터창에서 자유롭게 값을 변경하며 테스트를 할 수도 있다.

 

 

1.2. 진행일에 따라 계산 가능한 날짜

 

public int InGameYear
{
    get
    {
        UpdateDate();
        return _currentGameYear;
    }
}
public ESeason CurrentSeason
{
    get
    {
        UpdateDate();
        return _currentSeason;
    }
}
public EWeekday CurrentWeekDay
{
    get
    {
        UpdateDate();
        return _currentWeekday;
    }
}
public int InGameDay
{
    get
    {
        UpdateDate();
        return _currentGameDay;
    }
}


private void UpdateDate()
{
    if (_lastCalculatedDay != _inGameTotalDay)
    {
        _currentGameYear = (_inGameTotalDay / 112) + 1;
        _currentSeason = (ESeason)((_inGameTotalDay / 28) % 4);
        _currentWeekday = (EWeekday)(_inGameTotalDay % 7);
        _currentGameDay = (_inGameTotalDay % 28) + 1;

        _lastCalculatedDay = _inGameTotalDay;
    }
}

 

 

개발중인 게임에서 역법은 스타듀밸리와 같이 계절과 28일로 이루어졌기에, 간단하게 수식화 할 수 있을 것이라 생각이 들었다.

이를 통해 얻을 수 있는 것은 저장시 _inGameTotalDay 값만 저장해도 진행일을 온전히 저장할 수 있다는 점이다.

 

1.3. 이벤트 기반 구조

 

public event Action OnTimeCheck;
public event Action OnDayCheck;
public event Action OnSeasonCheck;
public event Action OnYearCheck;

// TimeManager의 메서드
private void Start()
{
    OnTimeCheck += CalculateInGameTime;
}


// LightCycle의 메서드
private void Start()
{
    GameManager.Instance.TimeM.OnTimeCheck += LerpLight;
}

// TimeUI의 메서드
private void Start()
{
    GameManager.Instance.TimeM.OnTimeCheck += UpdateTimeText;
    GameManager.Instance.TimeM.OnDayCheck += UpdateDayText;
    GameManager.Instance.TimeM.OnSeasonCheck += UpdateSeasonText;
    GameManager.Instance.TimeM.OnYearCheck += UpdateYearText;
}

 

TimeManager의 가장 특징적인 부분을 뽑으라면 이벤트 기반의 구조를 가지고 있다는 점이다.

시간당 작동해야하는 이벤트와, 그 외에 적절한 순간의 이벤트를 작성했고,  해당하는 메서드를 구독하여 구현할 수 있게 했다.  

 

또한, TimeManager와 TimeUI와 LightCycle은 서로 이벤트를 통해 결합되기에 TimeManager는 TimeUI와 LightCycle에 의존하지 않고 독립적으로 작동한다.

 

 

2. CropManager의 대표적인 특징

 

2.1. 작물 생성 제거 및 성장 관리

 

public class CropState
{
    public int growthStage;
    public int growingDate;
    public SeedSO seedSO;
    public SpriteResolver spriteResolver;
    public bool isPlanted;
}

public Dictionary<Vector3Int, CropState> cropStateDictionary = new Dictionary<Vector3Int, CropState>();

 

 

우선 CropManager는 cropStateDictionary를 통해 좌표와 CropState를 저장한다.

이를 통해 작물이 심어진 좌표와, 그 작물의 상태를 알 수 있다.

 

private void GetCropObject(Vector3Int p_cropPosition, CropState p_cropState)
{
    if (p_cropState.isPlanted) return;

    GameObject cropObject = GameManager.Instance.PoolM.GetObject(PoolTag.CropGrowthPrefab);
    p_cropState.isPlanted = true;
    cropObject.name = $"{p_cropPosition.x}_{p_cropPosition.y}";
    cropObject.transform.position = GameManager.Instance.TilemapM.GroundTilemap.GetCellCenterWorld(p_cropPosition);
    
    SpriteResolver spriteResolver = cropObject.GetComponent<SpriteResolver>();

    p_cropState.spriteResolver = spriteResolver;

    if (!cropStateDictionary.ContainsKey(p_cropPosition))
        cropStateDictionary.Add(p_cropPosition, p_cropState);

    GameManager.Instance.TilemapM.tileDataMap[p_cropPosition].Seed = p_cropState.seedSO;

    UpdateCropSprite(p_cropPosition);
}

public ItemSO RemoveCrop(Vector3Int p_cropPosition)
{
    if (cropStateDictionary.TryGetValue(p_cropPosition, out CropState cropState))
    {
        ItemSO harvestedCrop = cropState.seedSO.linkedCrop;

        ReleaseCropObject(cropState);

        cropStateDictionary.Remove(p_cropPosition);

        GameManager.Instance.TilemapM.tileDataMap[p_cropPosition].Seed = null;

        return harvestedCrop;
    }

    return null;
}

 

작물은 심어질 때, copStateDictionary에 정보를 저장하고, 제거되면 제거한다.

 

 

public void UpdateGrowth()
{
    foreach (var (tilePosition, tileData) in GameManager.Instance.TilemapM.tileDataMap)
    {
        if (cropStateDictionary.TryGetValue(tilePosition, out CropState cropState))
        {
            if (tileData.IsWatered)
            {
                cropState.growingDate++;

                if (cropState.growingDate >= cropState.seedSO.growTime && cropState.growthStage < cropState.seedSO.maxGrowthStage)
                {
                    cropState.growthStage++;
                    cropState.growingDate = 0;
                }
            }
            UpdateCropSprite(tilePosition);
        }
        SetCropSaveData(tilePosition, cropState);
    }
}


private void UpdateCropSprite(Vector3Int p_cropPosition)
{
    if (cropStateDictionary.TryGetValue(p_cropPosition, out CropState cropState))
    {
        cropState.spriteResolver.SetCategoryAndLabel(cropState.seedSO.itemName, cropState.growthStage.ToString());
    }
    CheckDecayCrop(cropState);
}

 

또한 업데이트 시 TilemapManager의 tileDataMap의 정보와 연계하여 작물이 심어진 타일이 물을 받았는지를 체크하고, 그에 맞는 성장을 구현한다.

 

작물의 스프라이트를 업데이트 하는 것은 스프라이트 라이브러리를 이용해, spriteResolver에서 적절한 카테고리와 라벨 값을 넣어 갱신하도록 했다.

 

 

2.2. 작물 저장 기능

 

게임의 저장 데이터는 현재 json값으로 저장을 하고 있는데 CropState에서 SO와 SpriteResolver는 json 값으로 저장하기가 곤란했다.

 

그래서 json으로 직렬화가 가능한 클래스를 따로 만들어 저장이 용이하도록 했다.

// CropSaveData 클래스
public class CropSaveData
{
    public Vector3Int cropPosition;
    public int growthStage;
    public int growingDate;
    public int seedItemCode;

    public CropSaveData(Vector3Int p_cropPosition, CropState p_cropState)
    {
        cropPosition = p_cropPosition;
        growthStage = p_cropState.growthStage;
        growingDate = p_cropState.growingDate;
        seedItemCode = p_cropState.seedSO.itemCode;
    }
}

// CropState를 CropSaveData로 전환하여 저장하는 메서드
private void SetCropSaveData(Vector3Int p_cropPosition, CropState p_cropState)
{
    if (p_cropState == null)
    {
        foreach (CropSaveData cropSaveData in cropSaveDatas)
        {
            if (cropSaveData.cropPosition == p_cropPosition)
            {
                cropSaveDatas.Remove(cropSaveData);
                break;
            }
        }
        return;
    }
    CropSaveData existingCropData = null;

    foreach (CropSaveData cropSaveData in cropSaveDatas)
    {
        if (cropSaveData.cropPosition == p_cropPosition)
        {
            existingCropData = cropSaveData;
            break;
        }
    }

    if (existingCropData != null)
    {
        existingCropData.growthStage = p_cropState.growthStage;
        existingCropData.growingDate = p_cropState.growingDate;
        existingCropData.seedItemCode = p_cropState.seedSO.itemCode;
    }
    else
    {
        CropSaveData newCropData = new CropSaveData(p_cropPosition, p_cropState);

        cropSaveDatas.Add(newCropData);
    }
}

// CropSaveData에서 CropState로 바꾸고 작물을 설치하는 메서드
public void LoadFromGameState()
{
    GameManager.Instance.DataBaseM.ItemDatabase.Init();
    foreach (CropSaveData cropSaveData in cropSaveDatas)
    {
        CropState cropState = new CropState();

        cropState.seedSO = (SeedSO)GameManager.Instance.DataBaseM.ItemDatabase.GetByID(cropSaveData.seedItemCode);
        cropState.growthStage = cropSaveData.growthStage;
        cropState.growingDate = cropSaveData.growingDate;

        GetCropObject(cropSaveData.cropPosition, cropState);
    }
}

 

저장할 때 CropState정보를 CropSaveData로 전환하고, 로드할 때는 다시 CropSaveData를 CropState로 전환하는 식이다.