안녕하세요 투니드입니다!
이번 편에서는 작물에 대한 데이터 컨테이너를 구현해 보겠습니다!
이것을 하기 위해 먼저 해줘야 할 것이 있습니다! 저희가 전에 씬을 분할해서 두 개의 씬을 동시에 로드해서 쓰고 있잖아요?
하는 어느씬에서든 필요한 데이터를 모아둔 Essential 씬 하나는 맵 씬 이렇게 두 개를 쓰고 있는데
이렇게 쓰다 보니 맵씬에 있는 타일맵의 데이터를 받아오지 못해서 밭을 갈 수가 없게 됐습니다!(알고 계셨나요?)

이렇게 타일의 정보가 계속 Marker만 찍히죠! 왜냐면 Essential 씬에 Marker TileMap만 가져왔었잖아요! 그 때문입니다!
이걸 먼저 해결해 보죠!
TileMapReadController.cs 로 갑니다!

타일맵에 정보를 가져오는 곳에 타일맵이 NULL이라면 태그로 지정해서 같이 로드되고 있는 씬의 베이스 타일맵을 가져와 참조를 시켜줍니다! 만약 태그로 찾아봐도 없다면 에러를 발생시키도록 유효성 검사를 해줬습니다!
TileMapReadController.cs/GetTileBase() 에도 위에 가드절들을 똑같이 적어주시면 됩니다!


코드를 작성해 주신 다음에는 이렇게 각 타일맵에 태그를 만들어서 붙여줍니다! 그리고 나서! CropsManager.cs로 갑니다!
실질적으로 타일맵의 데이터를 받아와서 작업을 처리하는 건 여기죠? targetTilemap역시 NULL로 처리가 되고 있는데
이것도 고쳐보도록 하겠습니다! 먼저 프로퍼티를 하나 선언해 줍니다!

타겟 타일맵이 없으면 똑같은 방식으로 CropsTilemap을 찾는 로직입니다! 선언을 이렇게 해줬다면 이 프로퍼티 선언부를 제외한 현재 문서의 모든 targetTilemap 으로 쓰인 변수를 전부 TargetTilemap 프로퍼티로 변경해주시면 됩니다!
그리고 나서 Tick(), Check(Vector3Int position), Seed(Vector3Int position, Crop toSeed), Plow(Vector3Int position),
PickUp(Vector3Int gridPosition) 메서드들의 첫 시작 부분에
if (TargetTilemap == null) return; 타겟 타일맵이 없다면 실행하지 않도록 만들어 주시면 됩니다!
이렇게 만들어주면 다시 정상적으로 수확까지 할 수 있는 걸 확인하실 수 있습니다!

근데 문제가 하나 있습니다!

씨앗을 심고 다른 씬을 다녀오면 밭이 초기화돼 보인다던가 하는 문제가 있어요! 먼저 밭이 초기화돼 보이는 문제를 해결하려면 해당씬의 작물 데이터를 게임 내내 저장하고 있는 저장소가 있어야 합니다! 지금부터 만들어볼게요!
CropsContainer.cs파일을 만들어줍니다!

먼저 스크립터블 오브젝트로 만들어주고요!
CropsManager.cs 에 구현된 모든 내용을 일부 구조변경을 해서 다른 곳으로 옮겨갈 거에요!
그럼 옮기기 시작해 보겠습니다!
CropsManager.cs 안에 선언된 딕셔너리인 cropsTile을 잘라내서 가져옵니다! 근데 딕셔너리가 아닌 List로 변경해 줄 겁니다! 왜냐면 딕셔너리는 유니티에서 직렬화가 되질 않기 때문에 구조를 변경해야 합니다!
(딕셔너리 -> 리스트로 구조를 변경한 것에 대한 대가는 엄청 큽니다 ㅠㅠ)


그리고 나서 CropsContainer를 통해 스크립터블 오브젝트를 바로 하나 만들어줬습니다! 이름은 MainScene에서 사용할 거라는 걸 명시해 주고요!
(지금 현재 에디터상에 에러가 엄청 많이 나오고 있을 겁니다! 당연한 수순이니 천천히 디버깅해가도록 하죠!)
그리고 TileMapCropsManager.cs파일을 만들어서 CropsTilemap에 붙여줍니다!

TileMapCropsManager.cs 에서는 해당씬에 작물들의 상태에 대한 데이터를 가지게 만들겁니다!
CropsManager.cs -> TileMapCropsManager.cs로 구조를 옮겨올 건데.. 이거는 포스팅으로 하기엔 답이 없어 보입니다...! 코드를 공유해 드릴게요!
TileMapCropsManager.cs
using UnityEngine;
using UnityEngine.Tilemaps;
namespace MyStardewValleylikeGame
{
public class TileMapCropsManager : TimeAgent
{
#region Variables
//밭을 간 상태(갈아엎어진 땅)의 타일
[SerializeField] TileBase plowed;
//씨앗을 심은 상태의 타일
[SerializeField] TileBase seeded;
//타일이 배치될 Tilemap 오브젝트
Tilemap targetTilemap;
//작물의 스프라이트 이미지를 적용시킬 프리팹
[SerializeField] GameObject cropsSpritePrefab;
// 작물 정보를 저장하는 컨테이너
[SerializeField] CropsContainer cropsContainer;
#endregion
private void Start()
{
// GameManager에서 CropsManager를 초기화
GameManager.Instance.GetComponent<CropsManager>().cropsManager = this;
// Tilemap 컴포넌트를 가져옴
targetTilemap = GetComponent<Tilemap>();
// Time Agent의 onTimeTick이 호출될 때마다 Tick 메서드 호출
onTimeTick += Tick;
// Time Agent의 초기화 메서드 호출
Init();
}
//시간 이벤트 발생 주기에 따라 호출되는 메서드
public void Tick()
{
// 타일맵이 null이면 에러 로그를 출력하고 반환
if (targetTilemap == null)
{
Debug.LogError("Target Tilemap is null");
return;
}
//모든 밭을 순회하며 성장 시간을 체크
foreach (CropTile cropTile in cropsContainer.crops)
{
//해당 위치에 심어진 작물이 없다면 다음으로 넘어감
if (cropTile.crop == null) continue;
//틱당 데미지 0.02씩 누적
cropTile.damage += 0.02f;
//데미지가 1이상되면 작물과 땅을 밭이 갈린상태로 초기화
if (cropTile.damage >= 1f)
{
// 작물 상태 초기화
cropTile.Harvested();
// 타일을 갈아엎은 상태로 설정
targetTilemap.SetTile(cropTile.position, plowed);
continue;
}
//성장한 시간이 작물의 총 성장가능 시간과 같거나 크다면
if (cropTile.Complete)
{
//더이상 자라지않게 만듬
Debug.Log("Crop is Ready to Harvest");
continue;
}
//성장한 시간을 1씩 증가
cropTile.growTimer++;
// 성장 시간이 현재 성장 단계의 지정된 시간에 도달했으면,
if (cropTile.growTimer >= cropTile.crop.growthStageTime[cropTile.growStage])
{
cropTile.renderer.gameObject.SetActive(true);
// 해당 성장 단계에 맞는 스프라이트를 적용
cropTile.renderer.sprite = cropTile.crop.sprites[cropTile.growStage];
// 성장 단계를 하나 증가시켜서 다음 단계로 진행
cropTile.growStage++;
}
}
}
// 특정 위치에 심어진 작물이 있는지 확인하는 메서드
public bool Check(Vector3Int position)
{
// 해당 위치의 CropTile이 존재하면 true 반환, 없으면 false
return cropsContainer.Get(position) != null;
}
// 씨앗을 심을 수 있는지 확인하는 메서드 (현재 이름이 적절한지 고민 중)
public void Seed(Vector3Int position, Crop toSeed)
{
// 해당 위치의 CropTile 객체 가져오기
CropTile tile = cropsContainer.Get(position);
// CropTile이 null이면, 즉 해당 위치에 타일이 없다면 아무것도 하지 않음
if (tile == null) return;
// 해당 위치에 씨앗을 심은 타일을 배치
targetTilemap.SetTile(position, seeded);
// 해당 위치의 CropTile 객체에 씨앗 정보를 저장
tile.crop = toSeed;
// 작물의 렌더러가 비활성화되어 있으면 활성화하고, 씨앗에 맞는 스프라이트를 적용
//tile.renderer.gameObject.SetActive(true);
//tile.renderer.sprite = toSeed.sprites[0]; // 첫 번째 성장 단계(씨앗)의 스프라이트
}
//특정 위치의 땅을 가는 메서드
public void Plow(Vector3Int position)
{
// 해당 위치에 밭을 갈기 위해 CreatePlowedTile 메서드 호출
CreatePlowedTile(position);
}
// 실제로 타일을 변경하는 메서드 (밭을 간 상태로 만듦)
private void CreatePlowedTile(Vector3Int position)
{
// 새 CropTile 객체 생성
CropTile crop = new CropTile();
cropsContainer.Add(crop); // CropTile을 cropsContainer에 추가
// 새 GameObject를 인스턴스화하여 해당 위치에 배치
GameObject go = Instantiate(cropsSpritePrefab);
go.transform.position = targetTilemap.CellToWorld(position); // 월드 좌표로 변환
go.SetActive(false); // 초기 상태에서는 스프라이트가 보이지 않도록 설정
crop.renderer = go.GetComponent<SpriteRenderer>(); // SpriteRenderer 컴포넌트 할당
crop.position = position; // 해당 위치 저장
// 해당 위치에 밭을 간 타일을 배치
targetTilemap.SetTile(position, plowed);
}
// 주어진 위치에서 작물을 수확하는 메서드
public void PickUp(Vector3Int gridPosition)
{
// gridPosition을 Vector2Int로 변환하여 타일의 위치를 추출
Vector2Int position = (Vector2Int)gridPosition;
// 해당 위치의 CropTile 객체를 가져옴
CropTile tile = cropsContainer.Get(gridPosition);
// 해당 위치에 심어진 작물이 없다면 수확을 진행하지 않고 종료
if (tile == null) return;
// 작물이 완전히 성장했으면 수확을 진행
if (tile.Complete)
{
// 수확된 아이템을 월드에 생성
ItemSpawnManager.Instance.SpawnItem(
targetTilemap.CellToWorld(gridPosition) + new Vector3(0.5f, 0.5f, 0), // 월드 좌표로 변환된 타일 위치
tile.crop.yield, // 수확할 아이템 타입
tile.crop.yieldAmount // 수확할 아이템의 수량
);
// 수확 후, 밭이 갈려있는 상태로 돌려놓음
targetTilemap.SetTile(gridPosition, plowed);
// 수확 후, 작물의 상태 초기화 (작물이 더 이상 존재하지 않게 됨)
tile.Harvested();
}
}
}
}
먼저 TimeAgent를 상속받아서 시간흐름에대한 이벤트 처리를 구독 / 해제 시스템을 통해서 제어 받을겁니다!
그리고 아래 내용들은 딕셔너리를 List로 바꿈으로써 바뀌는 내용들이 일단은 전부입니다!
(문제가 있다면 Sprite Renderer를 설정하는 부분이 지금 정상적이지 않습니다)
CropsContainer.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MyStardewValleylikeGame
{
// "Crops Container"라는 이름으로 ScriptableObject를 생성할 수 있도록 메뉴에 추가
[CreateAssetMenu(menuName = "Data/Crops Container")]
public class CropsContainer : ScriptableObject
{
#region Variables
// CropTile 객체를 저장하는 리스트 (작물 정보 저장)
public List<CropTile> crops;
#endregion
// 주어진 위치에 해당하는 CropTile을 반환하는 메서드
// 위치에 맞는 CropTile이 없다면 null을 반환
public CropTile Get(Vector3Int position)
{
// crops 리스트에서 위치가 일치하는 첫 번째 CropTile을 반환
return crops.Find(c => c.position == position);
}
// 새로운 CropTile을 crops 리스트에 추가하는 메서드
public void Add(CropTile crop)
{
// 리스트에 새 CropTile 객체를 추가
crops.Add(crop);
}
}
}
CropsManager.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.UIElements;
namespace MyStardewValleylikeGame
{
[Serializable]
public class CropTile
{
#region Variables
public int growTimer; //성장한 시간
public int growStage; //성장한 단계
public Crop crop; //작물 정보
public SpriteRenderer renderer;//작물의 SpriteRenderer를 컨트롤할 변수
public float damage; //시간이 지남에따라 작물에 누적될 데미지
public Vector3Int position; //작물이 심어진 위치값을 저장시킬 변수
#region 중복체크 (투니드)
public bool isPlanted; //작물이 이미 심어졌는지 확인하는 변수
#endregion
public bool Complete
{
get
{
// crop이 null이면 false를 반환
if (crop == null) { return false; }
// 성장 시간이 crop의 성장 시간을 초과하거나 같으면 수확이 완료된 것으로 간주
return growTimer >= crop.timeToGrow;
}
}
#endregion
public void Harvested()
{
// 수확 후 작물의 상태를 초기화하는 메서드
// 성장 시간과 단계, crop 정보를 초기화하여 타일을 비워줌
growTimer = 0; // 성장 시간 초기화
growStage = 0; // 성장 단계 초기화
crop = null; // 현재 심어진 crop 제거
isPlanted = false; // 작물이 심어진 상태를 초기화
damage = 0; // 데미지 초기화
renderer.gameObject.SetActive(false); // 작물의 스프라이트 렌더러를 비활성화하여 화면에서 사라지게 함
}
}
//밭을 가는 상태를 관리하는 클래스
public class CropsManager : MonoBehaviour
{
#region Variables
// TileMapCropsManager를 참조 (작물 관련 관리)
public TileMapCropsManager cropsManager;
#endregion
// cropsManager가 null인지 확인하는 공통 메서드
private bool CheckCropsManager()
{
if (cropsManager == null)
{
Debug.LogError("CropsManager is null");
return false;
}
return true;
}
// 수확하기 기능 (주어진 위치에서 작물을 수확)
public void PickUp(Vector3Int position)
{
if (!CheckCropsManager()) return;
cropsManager.PickUp(position); // cropsManager에서 PickUp 메서드 호출
}
// 해당 위치에 씨앗을 심을 수 있는지 확인 (작물이 심어졌는지 확인)
public bool Check(Vector3Int position)
{
// cropsManager가 null이면 에러 메시지 출력 후 false 반환
if (!CheckCropsManager()) return false;
return cropsManager.Check(position); // cropsManager에서 Check 메서드 호출
}
// 씨앗을 심는 기능 (주어진 위치에 씨앗을 심음)
public void Seed(Vector3Int position, Crop toSeed)
{
if (!CheckCropsManager()) return;
cropsManager.Seed(position, toSeed); // cropsManager에서 Seed 메서드 호출
}
// 해당 위치의 밭을 갈아엎는 기능
public void Plow(Vector3Int position)
{
if (!CheckCropsManager()) return;
cropsManager.Plow(position); // cropsManager에서 Plow 메서드 호출
}
}
}
이 페이지는 설명이 어려울 것 같습니다 ㅠㅠ 주석을 자세하게 달아놨으니 참고를 부탁드립니다!
'StardewValleyLikeGameCloneCoding' 카테고리의 다른 글
Stardew Valley like Game in Unity Episode 21 Audio and Music manager (0) | 2025.03.26 |
---|---|
Stardew Valley like Game in Unity Episode 20 Processing and reading data container (0) | 2025.03.26 |
Stardew Valley like Game in Unity / GitHub (0) | 2025.03.25 |
Stardew Valley like Game in Unity Episode 18-2 Crafting (1) | 2025.03.24 |
Stardew Valley like Game in Unity Episode 18-1 Crafting (0) | 2025.03.24 |