내일배움캠프 TIL

내일배움캠프 52일차 TIL "Happy Harvest 작물 시스템 분석"

Jooglorystar 2024. 11. 29. 21:56

 

 

스타듀밸리 류의 게임을 개발하면서, 유니티에서 개발한 농장 타이쿤류 게임이 있다는 정보를 알게 되었다.

 

https://assetstore.unity.com/packages/essentials/tutorial-projects/happy-harvest-2d-sample-project-259218

 

Happy Harvest - 2D Sample Project | 자습서 | Unity Asset Store

Get the Happy Harvest - 2D Sample Project package from Unity Technologies and speed up your game development process. Find this & other 자습서 options on the Unity Asset Store.

assetstore.unity.com

 

 

Happy Harvest라는 게임으로, 캐릭터를 움직이고, 땅을 경작하고, 씨를 뿌리고, 물을 뿌리면 작물이 자라는 정도의 기능이 구현된 간단한 2D 샘플 프로젝트이다.

 

구현하고자 하는 게임과 비슷한 게임인 만큼, 보고 참고할 만한 부분이 있을 것이라고 생각하게 되었다.

 

내가 구현해야하는 부분은 씨앗을 심으면, 그것이 자라나는 성장부분이다.

작물의 정보를 받고, 저장하고, 그 성장을 반영하는 부분을 구현하기 위해

Happy Harvest의 코드를 살펴보게 되었다.

 

 

 

우선 땅을 갈고, 물을 주고, 작물을 심는 부분은 TerrainManager라는 부분에서 전담한다.

사전 지식으로 알아야하는 부분은 다음과 같다.

 

public Grid Grid;

public Tilemap GroundTilemap;
public Tilemap CropTilemap;

[Header("Watering")]
public Tilemap WaterTilemap;
public TileBase WateredTile;

[Header("Tilling")] 
public TileBase TilleableTile;
public TileBase TilledTile;
public VisualEffect TillingEffectPrefab;

private Dictionary<Vector3Int, GroundData> m_GroundData = new();
private Dictionary<Vector3Int, CropData> m_CropData = new();

 

TerrainManager는 기본적으로 타일맵을 세가지로 관리한다.

CroundTilemap은 경작이 가능한 땅이 그려진 타일맵이다.

땅을 갈때는 해당 타일맵서 경작된 타일맵으로 교체하는 식으로 경작을 구현했다.

WaterTilemap은 물을 줬을 때 구현되는 타일맵이다.

CropTilemap은 작물이 심어지는 타일맵이다.

 

또한 이러한 땅의 정보와 작물의 정보는 좌표와 그 해당 데이터로 이루어진 딕셔너리에서 관리를 했다.

 

우선 작물을 심는 PlantAt메서드부터 살펴볼 것이다.

 

public void PlantAt(Vector3Int target, Crop cropToPlant)
{
    var cropData = new CropData();
    
    cropData.GrowingCrop = cropToPlant;
    cropData.GrowthTimer = 0.0f;
    cropData.CurrentGrowthStage = 0;
    
    m_CropData.Add(target, cropData);
    
    UpdateCropVisual(target);

    if (!m_HarvestEffectPool.ContainsKey(cropToPlant))
    {
        InitHarvestEffect(cropToPlant);
    }
}

 

해당 메서드는 씨앗 봉투 도구를 사용할 때 호출되는 메서드이다.

 

사용시, 대상이 되는 좌표와, 심을 작물에 대한 Crop SO데이터, cropToPlant 를 매개변수로 받는다.

 

심기 전 CropData를 cropToPlant의 값에 맞게 초기화 하고, 딕셔너리에 좌표와 데이터를 넣는다.

그리고 UpdateCropVisual()메서드를 호출한다.

 

void UpdateCropVisual(Vector3Int target)
{
    if (!m_CropData.TryGetValue(target, out var data))
    {
        CropTilemap.SetTile(target, null);
    }
    else
    {
        CropTilemap.SetTile(target, data.GrowingCrop.GrowthStagesTiles[data.CurrentGrowthStage]);
    }
}

 

UpdateCropVisual은 좌표를 매개변수로 받는 메서드이다.

우선 크롭데이터 딕셔너리에서 해당 좌표를 키값으로 가진 크롭데이터가 있는지 확인하고, 없을 경우 타일을 비운다.

만약 타일이 있을 경우, 크롭데이터의 해당하는 타일로 교체하게 된다.

 

 

크롭SO의 GrowthStagesTiles는 작물의 모든 성장 모습 타일이 담겨있다.

'data.CurrentGrowthStage' 부분에 적절한 숫자를 넣음으로서, 적절한 모습으로 교체가 가능하다.

 

해당 메서드는 당연하게도 자라거나, 작물이 죽는 등, 상태가 업데이트가 필요할 때마다 호출이 된다.

PlantAt 메서드의 경우  cropData.CurrentGrowthStage를 0으로 초기화 했기 때문에, 자라지 않은 것에 해당하는 타일을 생성할 것이다.

 

 

심은 뒤에는, Happy Harvest는 Update문에서 관리르 했다.

 

private void Update()
{
    foreach (var (cell, groundData) in m_GroundData)
    {
        if (groundData.WaterTimer > 0.0f)
        {
            groundData.WaterTimer -= Time.deltaTime;

            if (groundData.WaterTimer <= 0.0f)
            {
                WaterTilemap.SetTile(cell, null);
                //GroundTilemap.SetColor(cell, Color.white);
            }
        }

        if (m_CropData.TryGetValue(cell, out var cropData))
        {
            if (groundData.WaterTimer <= 0.0f)
            {
                cropData.DyingTimer += Time.deltaTime;
                if (cropData.DyingTimer > cropData.GrowingCrop.DryDeathTimer)
                {
                    m_CropData.Remove(cell);
                    UpdateCropVisual(cell);
                }
            }
            else
            {
                cropData.DyingTimer = 0.0f;
                cropData.GrowthTimer = Mathf.Clamp(cropData.GrowthTimer + Time.deltaTime, 0.0f,
                    cropData.GrowingCrop.GrowthTime);
                cropData.GrowthRatio = cropData.GrowthTimer / cropData.GrowingCrop.GrowthTime;
                int growthStage = cropData.GrowingCrop.GetGrowthStage(cropData.GrowthRatio);

                if (growthStage != cropData.CurrentGrowthStage)
                {
                    cropData.CurrentGrowthStage = growthStage;
                    UpdateCropVisual(cell);
                }
            }
        }
    }
}

 

foreach문으로 모든 땅 데이터의 좌표와 정보를 가져오며, 일일히 체크를 했다.

WaterTimer부분은 물을 준 뒤, 일정시간 뒤에 물을 준 상태를 없애는 부분으로 알고 넘어 갔고, 유심히 본 부분은 밑의 작물 부분이었다.

 

 

m_CropData.TryGetValue(cell, out var cropData)를 이용해 우선 해당 좌표에 작물 정보가 있는지를 우선 체크하고, 없다면 넘어가며, 작물이 있는 경우에만 및의 작업을 하였다.

윗부분의 if문은 물이 없을 때, 작물이 죽는 타이머를 작동시키는 거로 이해하고 넘어갔다.

만약 물이 있는 상태면, 식물은 죽지 않고, 자라는 타이머가 실행된다.

그리고 타이머에 따라 GrowthStage를 체크하고, 그에 맞는 growthStage를 설정하고, UpdateCropVisual(cell)를 통해 설정된 성장 단계에 맞는 타일로 교체한다.

 

 

 

Happy Harvest의 코드를 살펴보면서, 주의깊게 본 부분은 땅 데이터와, 작물 정보를 각각 딕셔너리로 관리 하는 것과, 작물 정보가 담긴 프리팹에서 Tilebase배열로 이미지를 관리하는 부분이었다.

해당 부분을 참고하면 구현을 더 쉽게 할 수 있을 것으로 생각된다.