1월 19일에 개발한걸 이제야 씀니다..
오늘은 밭을 내마음대로 배치했다면 그 위에 씨앗을 심을 예정입니다
씨앗 - 새싹 - 열매 - 성체 순으로 점점 커지는 느낌으로 아래 에셋을 다운 받아요

일단 버튼으로 밭-토마토 중 하나를 골라서 심어야하니까 TmpText 먼저 만들어주기

32-126,44032-55203,12593-12643,8200-9900
저번에 한 밭을 투명하게 한 것 처럼 씨앗도 이렇게 해줌

씬뷰 확대하니까 도트같아서 귀여운데 ㅋ

일단 요런식으로 밭 - 토마토 버튼이 있어서 무조건 밭 위에 심을 수 있게 하려고 함


Crop이라는 빈 오브젝트를 만들어주고

using UnityEngine;
public class Crop : MonoBehaviour
{
private CropData data;
private int currentState = 0;
private GameObject currentModel;
public void Initialize(CropData cropData)
{
data = cropData;
currentState = 0; // 0단계부터 시작 보장
// 데이터가 정상인지 검사
if (data == null || data.growthStagePrefabs == null || data.growthStagePrefabs.Length == 0)
{
Debug.LogError("CropData 또는 성장 프리팹 배열이 비어있습니다.");
return;
}
UpdateModel();
// Invoke 지연 시간 재확인 (0이면 실행 안됨)
float interval = data.timeBetweenStages;
if (interval <= 0) interval = 2.0f; // 기본값 방어 코드
// 기존 예약된 Grow가 있다면 취소 후 재등록
CancelInvoke(nameof(Grow));
InvokeRepeating(nameof(Grow), interval, interval);
Debug.Log($"{data.cropName} 성장 시작. 주기: {interval}초");
}
void Grow()
{
if (currentState < data.growthStagePrefabs.Length - 1)
{
currentState++;
UpdateModel();
Debug.Log($"{gameObject.name}가 {currentState}단계로 성장했습니다.");
}
else
{
CancelInvoke("Grow");
}
}
void UpdateModel()
{
if(currentModel != null) Destroy(currentModel);
// 자식으로 생성하되, 생성된 모델의 Local Position을 0으로 맞춤
currentModel = Instantiate(data.growthStagePrefabs[currentState], transform);
currentModel.transform.localPosition = Vector3.zero;
currentModel.transform.localRotation = Quaternion.identity;
}
}
using UnityEngine;
[CreateAssetMenu(fileName = "CropData", menuName = "Scriptable Objects/CropData")]
public class CropData : ScriptableObject
{
public string cropName;
public GameObject[] growthStagePrefabs; // 4단계 프리팹(씨앗-새싹-성장-수확)
public float timeBetweenStages; //단계별 성장 시간
}
Scriptable Objects로 CropData도 만들어줌

GridManager 스크립트 수정
using UnityEngine;
public enum TileType { Empty, Mud, Crop };
/// <summary>
/// 농장의 물리적인 공간 데이터를 관리하는 클래스
/// 어떤 타일이 비어있고, 어떤 타일이 점유되었는지를 2차원 배열로 저장함
/// </summary>
public class GridManager : MonoBehaviour
{
[Header("농장 크기 설정")]
public int width = 10; // 가로 타일 개수
public int height = 10; // 세로 타일 개수
private TileType[,] tileTypes; // bool 대신 TileType 사용
/// <summary>
/// 게임 시작 시 설정된 크기에 맞춰 배열 메모리를 할당
/// </summary>
private void Awake()
{
tileTypes = new TileType[width, height];
}
/// <summary>
/// 특정 좌표(x, z)에 오브젝트를 설치할 수 있는지 검사
/// </summary>
/// <param name="x">검사할 그리드 X 인덱스</param>
/// <param name="z">검사할 그리드 Z 인덱스</param>
/// <returns>해당 칸이 Empty일때만 true, 불가능하면 false</returns>
public bool CanPlace(int x, int z)
{
// 맵 범위 체크
if (x < 0 || x >= width || z < 0 || z >= height)
{
return false;
}
// 해당 칸이 Empty 상태일 때만 true 반환
// (isOccupied 배열을 삭제했으므로 tileTypes만 확인하면 됩니다)
return tileTypes[x, z] == TileType.Empty;
}
/// <summary>
/// 오브젝트 설치가 확정되었을 때 데이터 갱신
/// </summary>
public void PlaceObject(int x, int z, TileType type)
{
// 맵 범위 안일 때만 실행
if (x >= 0 && x < width && z >= 0 && z < height)
{
tileTypes[x, z] = type;
Debug.Log($"<color=yellow>데이터 갱신:</color> [{x},{z}] 번지 타일이 {type} 상태가 되었습니다.");
}
}
/// <summary>
/// 현재 타일의 타입을 반환하는 함수
/// </summary>
public TileType GetTileType(int x, int z)
{
if(x < 0 || x >= width || z < 0 || z>=height) return TileType.Empty;
return tileTypes[x, z];
}
}
GridSystem 수정
using System;
using UnityEditor.ShaderGraph;
using UnityEngine;
/// <summary>
/// 마우스 입력을 받아 그리드 상에 오브젝트를 배치하고
/// 배치 가능 여부를 시각적으로 보여주는 클래스
/// </summary>
public class GridSystem : MonoBehaviour
{
[Header("설정")]
[SerializeField] private float cellSize = 10f; // 그리드 한 칸의 크기 (기본 Plane 10x10에 맞춤)
[SerializeField] private LayerMask groundLayer; // 레이캐스트가 감지할 바닥 레이어
[Header("밭 설정")]
[SerializeField] private GameObject mudPreviewPrefab; // 설치 전 보여줄 투명한 미리보기용 프리팹
[SerializeField] private GameObject mudPrefab; // 실제로 설치될 완성된 오브젝트 프리팹
[Header("작물 설정")]
[SerializeField] private GameObject previewCropPrefab;
[SerializeField] private GameObject cropPrefab; // Crop 스크립트가 붙어있는 빈 오브젝트 프리팹
[Header("선택된 상태")]
[SerializeField] private bool isMudSelect = false; // 기본적으로 아무것도 선택X
[SerializeField] private CropData currentSelectedCrop; // 선택된 씨앗 데이터
private GridManager gridManager; // 설치 데이터를 관리하는 GridManager 참조
private GameObject previewInstance; // 씬에 생성되어 따라다닐 미리보기 인스턴스
void Start()
{
/* // 미리보기 인스턴스를 생성하고 초기 설정 진행
if (mudPreviewPrefab != null)
{
previewInstance = Instantiate(mudPreviewPrefab);
mudPreviewPrefab.SetActive(false);
// TryGetComponent: GameObject에 존재하는 경우 지정된 유형의 컴포넌트를 검색하려고 시도하고,
// 발견되면 true, 발견되지 않으면 false를 반환한다.
//public bool TryGetComponent<T>(out T component) where T : Component;
*//* T: 가져오려는 컴포넌트의 타입
component: 컴포넌트를 가져올 때 사용되는 out 매개변수 *//*
// 미리보기 오브젝트가 마우스 레이캐스트를 방해하지 않도록 콜라이더 비활성화
if (previewInstance.TryGetComponent<Collider>(out Collider col)) col.enabled = false;
}*/
// 씬에 존재하는 GridManager를 찾아 참조 연결
if (gridManager == null) gridManager = FindAnyObjectByType<GridManager>();
}
void Update()
{
// 마우스 위치로부터 화면 안쪽으로 레이(광선)를 쏜다.
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
//Ray가 어떤 Collider에 맞으면 true를 반환하고, 그 충돌 정보는 hit에 담긴다.
// 지정된 groundLayer(바닥)에 레이가 맞았을 때만 로직 실행
if (Physics.Raycast(ray, out hit, Mathf.Infinity, groundLayer))
{
// [좌표 계산] 맞은 지점의 월드 좌표를 그리드 인덱스(0, 1, 2...)로 변환
int xIdx = Mathf.FloorToInt(hit.point.x / cellSize);
int zIdx = Mathf.FloorToInt(hit.point.z / cellSize);
// [스냅 좌표] 바닥이 그리드 정중앙에 오도록 좌표 보정 (+cellSize/2)
Vector3 snapPos = new Vector3(xIdx * cellSize + (cellSize / 2), 0, zIdx * cellSize + (cellSize / 2));
// 모드에 따른 높이 결정
// 밭 설치 모드면 0, 작물 설치 모드면 0.16f
float targetY = isMudSelect ? 0f : 0.16f;
// 프리뷰 처리 (targetY 반영)
HandlePreview(snapPos, targetY, xIdx, zIdx);
// 마우스 왼쪽 클릭 시 해당 위치에 실제 설치 시도
if (Input.GetMouseButtonDown(0))
{
PlaceAt(snapPos, xIdx, targetY, zIdx);
}
}
else
{
// 바닥을 벗어나면 미리보기를 숨김
if (previewInstance != null)
{
previewInstance.SetActive(false);
}
}
}
/// <summary>
/// 선택된 모드에 맞춰 미리보기(Preview) 모델을 교체합니다.
/// </summary>
void UpdatePreviewInstance(GameObject newPrefab)
{
// 기존 미리보기가 있다면 삭제
if(previewInstance != null)
{
Destroy(previewInstance);
}
//OnClickSelectCrop 새로운 프리팹으로 생성
if (newPrefab != null)
{
// 원본 프리팹을 복제하여 previewInstance 변수에 할당
previewInstance = Instantiate(newPrefab);
previewInstance.SetActive(false);
// 레이캐스트 방해 방지 (콜라이더 끄기)
if (previewInstance.TryGetComponent<Collider>(out Collider col))
col.enabled = false;
}
}
/// <summary>
/// 밭 선택 버튼
/// </summary>
public void OnClickSelectMud()
{
isMudSelect = true;
currentSelectedCrop = null;
// 미리보기를 밭용으로 교체
UpdatePreviewInstance(mudPreviewPrefab);
Debug.Log("밭 선택 모드");
}
public void OnClickSelectCrop(CropData cropdata)
{
isMudSelect = false;
currentSelectedCrop = cropdata;
// 미리보기를 작물용으로 교체
UpdatePreviewInstance(previewCropPrefab);
Debug.Log($"{cropdata.cropName} 씨앗 선택");
}
/// <summary>
/// 미리보기 오브젝트를 마우스 위치로 이동시키고, 설치 가능 여부에 따라 색상을 변경하는 메서드
/// </summary>
void HandlePreview(Vector3 pos, float y, int x, int z)
{
if (previewInstance == null)
{
return;
}
// 아무 모드도 선택되지 않았다면 프리뷰를 끄고 리턴
if (!isMudSelect && currentSelectedCrop == null)
{
previewInstance.SetActive(false);
return;
}
pos.y = y;
previewInstance.SetActive(true);
previewInstance.transform.position = pos;
if (gridManager == null) return;
bool canPlaceVisually = false;
if (isMudSelect)
{
//밭 설치 모드 : 해당 칸이 Empty여야 초록색
canPlaceVisually = gridManager.CanPlace(x, z);
}
else if (currentSelectedCrop != null)
{
//씨앗 심기 모드 : 해당 칸이 Mud 여야 초록색(이미 Crop인 곳은 false가 됨)
canPlaceVisually = (gridManager.GetTileType(x, z) == TileType.Mud);
}
// 시각적 피드백: 가능하면 초록색(Green), 불가능하면 빨간색(Red) 반투명 처리
MeshRenderer mr = previewInstance.GetComponentInChildren<MeshRenderer>();
if (mr != null)
{
mr.material.color = canPlaceVisually ? new Color(0, 1, 0, 0.5f) : new Color(1, 0, 0, 0.5f);
}
}
/// <summary>
/// 최종적으로 GridManager의 승인을 받아 실제 오브젝트를 생성하고 점유 데이터를 기록하는 메서드
/// </summary>
void PlaceAt(Vector3 pos, int x, float y, int z)
{
// 밭 설치 모드일 때
if (isMudSelect && gridManager.CanPlace(x, z))
{
pos.y = 0f; // 밭은 무조건 바닥에 설치
Instantiate(mudPrefab, pos, Quaternion.identity);
gridManager.PlaceObject(x, z, TileType.Mud);
Debug.Log($"[{x}, {z}] 위치에 밭 설치 성공");
}
// 작물 설치 모드 (현재 칸이 정확히 Mud 상태여야만 함)
// 만약 이미 작물을 심어서 TileType.Crop으로 변했다면, GetTileType은 Mud가 아니게 됨
else if (!isMudSelect && currentSelectedCrop != null)
{
TileType currentTile = gridManager.GetTileType(x, z);
if (gridManager.GetTileType(x, z) == TileType.Mud)
{
pos.y = 0.16f;
PlantCrop(pos, x, z);
}
else if (currentTile == TileType.Crop)
{
Debug.Log("여기는 이미 작물이 자라고 있습니다!"); // 이 로그가 찍히는지 확인하세요.
}
else
{
Debug.Log("밭이 아닌 곳에는 심을 수 없습니다.");
}
}
}
void PlantCrop(Vector3 pos, int x, int z)
{
// 작물 프리팹 생성
GameObject newCrop = Instantiate(cropPrefab, pos, Quaternion.identity);
gridManager.PlaceObject(x, z, TileType.Crop);
// 생성된 작물에 Crop 스크립트를 가져와 데이터 전달
if (newCrop.TryGetComponent<Crop>(out Crop crop))
{
crop.Initialize(currentSelectedCrop);
Debug.Log($"{currentSelectedCrop.cropName}을 심었습니다/");
}
else
{
Debug.LogError($"cropPrefab 루트에 Crop 컴포넌트가 없습니다! 프리팹을 확인하세요: {newCrop.name}");
gridManager.PlaceObject(x, z, TileType.Mud);
Destroy(newCrop);
}
}
}
GridSystem.cs (입력 및 로직 제어기)isMudSelect와 currentSelectedCrop 변수를 통해 현재 모드를 관리함GridManager.cs (데이터 관리자)TileType[,])로 저장하는 데이터 장부PlaceObject를 통해 설치된 객체의 타입을 기록하여 중복 설치를 방지하고 작물 설치 조건을 검사CanPlace, GetTileType 등의 함수를 통해서만 접근하게 하여 버그를 최소화Crop.cs & CropData.cs (객체 및 데이터 구조)CropData는 작물의 정보를 담는 설계도이며, Crop은 실제 월드에서 시간에 따라 자라나는 식물임ScriptableObject를 사용하여 기획 데이터(성장 시간, 메쉬 등)를 코드와 분리InvokeRepeating을 사용하여 정해진 주기마다 성장 단계를 갱신하고 모델을 교체