말감로그

[Unity] Valley - DayCycle & 식물 성장 시스템 본문

TIL

[Unity] Valley - DayCycle & 식물 성장 시스템

habbn 2024. 10. 24. 01:37
728x90

 

DayCycle

하루 24시간이 지나면 다음 날로 변경이 되도록 DayCycle을 만들 것이다.

실제 시간 10초가 지나면 게임 상 시간은 10분이 흐르도록 설정 할 것이고, 24시가 되면 요일도 변경하게 할 것이다.

Fade In / Fade Out 효과를 줘서 시간의 흐름을 표현할 것이다.

using System;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

/* 시간 흐름 관리*/

public class TimeManager : MonoBehaviour
{
    public TextMeshProUGUI dayText;
    public Image fadeImg;

    private float timePerGameMinute = 1f;   
    private float currentTime = 0f;
    private int gameHour = 9;
    private int gameMinute = 0;
    private string[] daysOfWeek = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
    private int currentDayIndex = 0;
    private float fadeDuration = 4f;
    private bool isDayEnding = false;

    public event Action OnDayEnd;

    void Start()
    {
        UpdateTimeUI();
    }

    void Update()
    {
        if (isDayEnding) return;

        currentTime += Time.deltaTime;

        if (currentTime >= timePerGameMinute)
        {
            currentTime = 0f;
            gameMinute += 10;

            if (gameMinute >= 60)
            {
                gameMinute = 0;
                gameHour++;
            }
            
            if (gameHour >= 10)
            {
                StartCoroutine(EndDay());
            }

            UpdateTimeUI();
        }
    }

    IEnumerator EndDay()
    {
        isDayEnding = true;
        yield return StartCoroutine(FadeScreen(1f));

        NextDay();

        yield return StartCoroutine(FadeScreen(0f));
        isDayEnding = false;
    }

    IEnumerator FadeScreen(float targetAlpha)
    {
        float startAlpha = fadeImg.color.a;
        float elapsedTime = 0f;

        while (elapsedTime < fadeDuration)
        {
            elapsedTime += Time.deltaTime;
            float newAlpha = Mathf.Lerp(startAlpha, targetAlpha, elapsedTime / fadeDuration);
            fadeImg.color = new Color(0, 0, 0, newAlpha);
            yield return null;
        }

        fadeImg.color = new Color(0, 0, 0, targetAlpha);
        yield return new WaitForSeconds(1f);
    }

    void NextDay()
    {
        gameHour = 9;
        gameMinute = 0;
        currentDayIndex = (currentDayIndex + 1) % daysOfWeek.Length;

        OnDayEnd?.Invoke();
        UpdateTimeUI();

        Debug.Log("하루가 끝났습니다. 다음 날 시작");
    }

    void UpdateTimeUI()
    {
        dayText.text = $"{daysOfWeek[currentDayIndex]} {gameHour:D2}:{gameMinute:D2}";
    }
}

 

코루틴을 사용해서 Fade In/Out 을 구현하였고, 하루가 끝나면 식물이 성장하거나 / 판 아이템의 코인이 들어오거나 등등 기능을 하기 위해 OnDayEnd 액션 이벤트를 생성하여 관리할 예정이다.

 

현재는 우선 10시가 되면 하루가 지나도록 설정하였다. 

 

 

하루가 지나면 식물을 성장하게 만들기 ( 물을 준 식물만) 구현 중 에러가 발생했다.

 

InvalidOperationException: Collection was modified; enumeration operation may not execute. System.Collections.Generic.Dictionary2+Enumerator[TKey,TValue]

 

이 에러가 발생했고 발생한 위치는 TileManager.cs 의 OnDayEnd() 함수 안에서 딕셔너리에 접근하면서 발생한 에러였다.

찾아보니 이 에러는 주로 foreach 루프가 진행되는 동안 해당 컬렉션이 변경될 때 발생한다고 한다.

void OnDayEnd()
{
    foreach (var tile in wateredTiles)
    {
        if (tile.Value)
        {
            StartCoroutine(GrowPlant(tile.Key));
            wateredTiles[tile.Key] = false;
        }
    }
}

 

위에서 물을 준 타일을 찾아서 코루틴을 통해서 해당 타일을 성장시키고, wateredTiles의 Value를 false로 수정하는 부분에서 생긴 문제이다. 

에러를 방지하기 위해서는 foreach 문을 사용하기 전에 wateredTiles의 키를 미리 저장하고 그 리스트를 순회하도록 수정해야한다.  이렇게 되면 안전하게 반복문을 돌 수 있게된다.

void OnDayEnd()
{
    // wateredTiles의 키를 미리 복사하여 List에 저장
    List<Vector3Int> wateredTilesKey = new List<Vector3Int>(wateredTiles.Keys);

    foreach (var position in wateredTilesKey)
    {
        if (wateredTiles[position])
        {
            StartCoroutine(GrowPlant(position));
            wateredTiles[position] = false;
        }

        TileBase tile = interactableMap.GetTile(position);
        if (tile != null)
            interactableMap.SetColor(position, Color.white);
    }
}

 

또한 하루가 지나면 물을 주면서 변한 색을 다시 원상복귀 시키게 했다.

 

식물의 성장 시스템을 임시로 코루틴을 사용하여 2초 간격으로 식물을 성장시키게끔 해놓았다. 이제 DayCycle을 구현하였으니 시간에 따라 관리하게 변경해야 한다.

 

식물 성장 단계마다 정해진 걸리는 시간이 있어서 그 시간이 되면 성장할 수 있게 바꾸고 싶은데 어떻게 관리해야 할지 도저히 생각이 나지 않아 gpt에게 물어봤다.

 

식물의 성장 단계를 시간에 따라 관리하려면 각 단계가 걸리는 시간을 저장하고, 씨앗을 심은 시점부터 경과한 시간을 비교해 단계별 성장을 결정하는 방식으로 코드를 수정해주었다.

 

growTimes 배열에 각 단계별 걸리는 시간을 설정해놓았으니, 씨앗을 심은 시점과 경과된 일수를 기록하여 적용하면 된다고 한다.

 

1. 딕셔너리를 통해 씨앗을 심은 시점과  현재 성장 단계를 저장한다.

private Dictionary<Vector3Int, int> plantGrowthDays = new Dictionary<Vector3Int, int>(); // 씨앗 심은 날 저장
private Dictionary<Vector3Int, int> currentGrowthStages = new Dictionary<Vector3Int, int>(); // 현재 성장 단계 저장

 

2. 씨앗을 심었다면, 씨앗을 심은 날과 현재 성장 단계를 0으로 초기화해준다.

public void PlantSeed(Vector3Int position, string seedType)
{
    if (tileStates.ContainsKey(position) && tileStates[position] == "Plowed")
    {
        tileStates[position] = "Seeded";
        seedMap.SetTile(position, plantedTile);

        plantGrowthDays[position] = 0;  // 씨앗 심은 날을 0으로 설정
        currentGrowthStages[position] = 0;  // 현재 성장 단계 0으로 초기화
    }
}

 

3. 하루가 지나면 씨앗을 심고 물을 준 타일만 성장 하도록한다.

 하루가 지났기 때문에 plantGrowthDays[position]을 1증가 시켜주고 코루틴을 실행시켜 성장 단계에 맞게 타일을 변경시켜 준 후 물을 준 사실을 false로 다시 초기화시킨다.

void OnDayEnd()
{
    // wateredTiles의 키를 미리 복사하여 List에 저장
    List<Vector3Int> wateredTilesKey = new List<Vector3Int>(wateredTiles.Keys);

    foreach (var position in wateredTilesKey)
    {
        if (wateredTiles[position] && plantGrowthDays.ContainsKey(position))
        {
            plantGrowthDays[position]++;

            StartCoroutine(GrowPlant(position));
            wateredTiles[position] = false;
        }

        TileBase tile = interactableMap.GetTile(position);
        if (tile != null)
            interactableMap.SetColor(position, Color.white);
    }
}
IEnumerator GrowPlant(Vector3Int position)
{
    if (!tileStates.ContainsKey(position) || tileStates[position] == "Grown")
    {
        yield break;
    }

    int currentStage = currentGrowthStages[position];
    int daysSincePlanted = plantGrowthDays[position];

    // 각 성장 단계별로 경과된 시간이 맞는지 확인
    if (currentStage < plantGrowthTiles.Length && daysSincePlanted >= growthTimes[currentStage])
    {
        seedMap.SetTile(position, plantGrowthTiles[currentStage]);
        plantGrowthTiles[currentStage].colliderType = Tile.ColliderType.Sprite;

        currentGrowthStages[position] = currentStage + 1;  // 성장 단계 1 증가

        // 모든 성장 단계를 완료했으면 "Grown" 상태로 변경
        if (currentGrowthStages[position] >= plantGrowthTiles.Length)
        {
            tileStates[position] = "Grown";
        }
        else
        {
            tileStates[position] = "Growing";
        }
    }
    yield return null;
}

 

 

씨앗을 심고 물을 준 식물만 성장하는 것을 볼 수 있다. 또한 물을 주지 않으면 성장하지 않는다.

 

 

다 성장한 식물을 호미를 들고 클릭하면 식물이 생성되도록 했다.

 

 

728x90