[타워디펜스게임] #8. 타일 관리

치치·2025년 2월 15일
0

타워디펜스게임

목록 보기
8/18
post-thumbnail

타일 관리 스크립트를 만들었다
이 부분이 나한테는 가장 어렵고 구현도 오래걸리고 애를 많이 먹었던 부분인 거 같다
여러 방법을 시도해보면서 방법을 찾았는 데 더 좋은 방법이 있었을 거 같기도.. 조금 아쉽긴 하다


이미 작성한적이 있는 유닛 배치 스크립트에서, 타일 내에 유닛을 배치하는 걸 했다
유닛을 구매한 뒤 타일에 배치하는 것은 어렵지않았다
또한, 유닛을 구매한 뒤 바로 삭제하는 것도 어렵지 않았다

가장 어려웠던 부분은 '타일에 배치된 유닛을 다시 집어서 이동시키기' 였다

https://youtu.be/VMLZy6CSoN8

기획자님이 원하셨던 방향은 이렇다

  1. 타일 내에 유닛을 구매하고 배치할 수 있다

  2. 배치된 유닛을 다시 집어서 이동할 수 있다

  3. 삭제존으로 유닛을 이동 시 유닛을 제거할 수 있다

  4. 같은 타일 내에 유닛이 중복으로 배치되는 것은 불가능하다

타일에 유닛 배치

배치하는 구현은 이미 전 단계에서 해둔 상태이다
https://velog.io/@yangju058/타워디펜스게임-3.-타일-생성-유닛-배치


배치된 유닛을 다시 집어서 이동하기

현재 상태에서는 유닛이 배치되는 것만 가능하다

<해결방안>

  1. Tile스크립트를 만들고 해당 타일에 유닛이 배치되었다면, 그 유닛의 스프라이트와 객체의 정보를 저장한다
    -> 배치된 유닛을 다시 집었을때, 무슨 객체인지 알아야하기 때문이다

Tile 스크립트의 일부 함수

-> 유닛이 타일에 배치되면 이 함수가 호출되고 유닛의 정보가 저장된다

// 유닛(이미지, 오브젝트)를 타일에 저장하는 함수
public void PlaceUnit(GameObject unit, Sprite UnitSprite)
{
    StartCoroutine(HasUnitActive());
    placedUnit = unit;
    PlacedUnitSprite = UnitSprite;

    if (unit == null && UnitSprite == null)
    {
        hasUnits = false;
    }
}
// 유닛 가져오기
public GameObject GetplacedUnit()
{
    return placedUnit;
}

public Sprite GetplacedUnitSprite()
{
    return PlacedUnitSprite;
}

  1. 배치되는 유닛은 객체이고, 마우스가 들고있는 유닛은 스프라이트이다
    -> 그렇기 때문에 객체를 타일에 배치할때마다 생성하고 삭제하는 건 메모리 낭비다

  1. 임시로 객체를 놔둬둘 savezone 영역을 생성한다
    -> 유닛을 구매하여 마우스에 스프라이트가 따라오는 상태에서, 객체savezone 영역에 생성된 상태
    -> 유닛 스프라이트는 활성화 중

TileManager 스크립트의 일부 함수

public void BuyUnit(GameObject unitPrefab, Sprite sprite)
{
    // 유닛을 구매할 때만 Instantiate 호출
    if (currentUnit == null)
    {
        currentUnit = Instantiate(unitPrefab, saveTile.position, Quaternion.identity);
        currentUnit.SetActive(false); // 구매 후 대기 상태
    }

    currentUnitSprite = sprite; // 스프라이트 설정
    currentUnit.SetActive(true); // 유닛 활성화
}
  1. 각 타일마다 유닛이 들어오면 어떤 유닛인지 정보가 저장된다
    저장해주는 Tile스크립트가 모든 타일에 들어있기 때문에 각각의 정보가 각각의 타일에 저장된다

  2. 클릭한 타일에 유닛이 배치되어 있는 상태이고, 마우스가 현재 스프라이트를 가지지 않은 상태일때, 이 타일에 저장된 객체를 savezone 으로 이동시키고, 저장된 스프라이트를 활성화 시킨다

TileManager 스크립트의 일부 내용

 // 타일에 유닛이 있고, 클릭했고, 스프라이트 없을 때
 if (hit.collider != null && hit.collider.GetComponent<Tile>() != null
     && hit.collider.GetComponent<Tile>().hasUnits && currentUnitSprite == null)
 {
     if (Input.GetMouseButtonDown(0))
     {
         Tile tile = hit.collider.GetComponent<Tile>();

         if (tile.placedUnit != null)
         {
             unitCounting.DelCounting();
             currentUnit = tile.GetplacedUnit(); // 타일에서 유닛 가져오기
             currentUnitSprite = tile.GetplacedUnitSprite();

             tile.MoveTileUnit(null, null); // 타일에서 유닛 제거 & 타일 hasUnit false
             currentUnit.transform.position = saveTile.position; // 세이브존으로 이동

         }
     }
 }

삭제 영역

유닛을 들고있는 상태에서 삭제 존으로 이동하고 클릭하면 유닛이 삭제되도록 해야한다

근데 문제는 유닛목록창과 삭제영역이 겹친다
-> 그 의미는 유닛목록에 다시 가져다가 클릭하면 유닛이 삭제되게 하려는 거임

  • 이게 겹치면 안된다고 생각했다
    -> 왜냐하면 이미 유닛목록창에 버튼 컴포넌트가 있고 클릭하면 유닛 스프라이트가 활성되게 해둔 상태인데, 그 영역에 삭제 영역을 둔다 하더라도 계속 스프라이트가 생성되기만 할뿐이다

삭제 영역 - 1. 배치되지 않은 상태의 유닛 제거

  • 유닛을 구매한 뒤 바로 삭제 영역에 가져가서 삭제하는 경우
    -> 1. 유닛 목록창에 가져다 대서 삭제
    -> 2. 유닛 삭제 활성화 영역 중 빈 영역에 가져다 대서 삭제

  • 1번의 경우를 해결하기 위해서 유닛 목록창에서 유닛을 클릭해서 구매했을 경우, 유닛 목록창의 버튼 컴포넌트 Button UI 요소의 상호작용을 비활성화
    -> button.interactable = false;


TileManager 스크립트의 일부

// 마우스 콜라이더와 (유닛목록에서 구매한) 유닛이 충돌한다면
if (hit.collider && currentUnit)
{
    foreach (Button button in unitButtons)
    {
        button.interactable = false;
    }

  • 이제 유닛 목록창의 버튼의 상호작용이 가능하지 않기 때문에 클릭해도 새로운 유닛 스프라이트가 활성화 되지 않는다
    -> 이제 삭제존에서 삭제가 가능하고, 유닛 목록창에 클릭해도 삭제가 가능해진다
    -> 원래 유닛 목록창에서 유닛을 구매하면 코스트도 차감되는 현상이 있는데, 버튼의 상호작용을 막았기 때문에 코스트 차감되지 않는다!

  • 삭제된 후, 유닛 목록 버튼은 coroutine을 사용하여 1초뒤에 상호작용이 활성화 되게 하였다
    -> 1초의 텀이 필요한 이유 : 유닛 목록창을 눌러서 삭제 할 경우, 텀이 없으면 삭제되자마자 바로 클릭한 유닛이 생성된다


// 삭제존 클릭 시
if (Input.GetMouseButtonDown(0) && hit.collider.gameObject.layer == LayerMask.NameToLayer("DeleteZone"))
{
    Debug.Log("Delete Unit");
    currentUnit.transform.position = saveTile.position; // 세이브존으로 이동
    currentUnit.SetActive(false); // 유닛 비활성화
    currentUnitSprite = null;
    currentUnit = null;

    SettingText.gameObject.SetActive(false); // 메시지 비활성화

    Destroy(currentUnit);
    
    StartCoroutine(UnitButtonActive());
    return;
}



Tile 스크립트 전체코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tile : MonoBehaviour
{
    public bool hasUnits;

    public static GameObject[,] grid;

    static int gridRows = 5; // 세로
    static int gridColumns = 10; // 가로

    // 배치된 유닛 오브젝트를 저장하는 변수
    public GameObject placedUnit;

    public Sprite PlacedUnitSprite;


    private void Awake()
    {
         grid = new GameObject[gridRows,gridColumns];

        // 각 칸의 Square 오브젝트를 배열에 저장
        for (int row = 0; row < gridRows; row++)
        {
            for (int col = 0; col < gridColumns; col++)
            {
                // 오브젝트를 직접 찾거나 생성
                GameObject square = GameObject.Find($"Square_{row}_{col}");
                if (square != null)
                {
                    grid[row, col] = square;
                }
            }
        }
    }

    // 코루틴 사용해서 hasUnit이 1초뒤에 true가 되게
    IEnumerator HasUnitActive()
    {
        yield return new WaitForSeconds(0.5f);

        hasUnits = true;
    }

    // 유닛(이미지, 오브젝트)를 타일에 저장하는 함수
    public void PlaceUnit(GameObject unit, Sprite UnitSprite)
    {
        StartCoroutine(HasUnitActive());
        placedUnit = unit;
        PlacedUnitSprite = UnitSprite;

        if (unit == null && UnitSprite == null)
        {
            hasUnits = false;
        }
    }

    public void MoveTileUnit(GameObject unit,Sprite UnitSprite)
    {
        placedUnit = unit;
        PlacedUnitSprite = UnitSprite;

        if (unit == null && UnitSprite == null)
        {
            hasUnits = false;
        }
    }
    // 유닛 가져오기
    public GameObject GetplacedUnit()
    {
        return placedUnit;
    }

    public Sprite GetplacedUnitSprite()
    {
        return PlacedUnitSprite;
    }


    public void MoveUnitToHiddenTile(Transform hiddenTile)
    {
        if (placedUnit != null)
        {
            placedUnit.transform.position = hiddenTile.position; // 대기 타일로 이동
        }
    }
}


TileManager 스크립트 전체 코드

  • 당시에 대부분의 컴포넌트들을 [SerializeField]로 만든것이 실수인 거 같다
    -> 메모리를 너무 많이 쓴다
    -> 나중에 수정하자

  • 그리고 Update문에서 대부분의 작업을 다 수행한다
    -> 너무 비효율적. 함수단위로 나눠서 작업하는 게 좋은데,, 아직은 실력이 모자라다
    -> 다시 수정하자

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;


public class TileManager : MonoBehaviour
{
    // 저장한 유닛정보
    public GameObject selectUnit;

    public Sprite selectUnitSprite;

    // 구입한 유닛정보
    public GameObject currentUnit;

    public Sprite currentUnitSprite;

    public Transform tiles;

    // 레이어
    public LayerMask tileMask;

    public LayerMask DeleteZone;

    // 삭제 존
    public GameObject Deletezone;

    [SerializeField] GameObject UnitPoint;

    [SerializeField] Text SettingText;

    [SerializeField] Transform canvas; // UI를 표시할 캔버스

    // 버튼 배열
    [SerializeField] Button[] unitButtons;

    private UnitCount unitCounting;

    // 배치된 유닛 리스트
    private List<int> units = new List<int>();

    [SerializeField] UnitCount unitCount;

    // 숨겨둔 저장 타일 위치
    [SerializeField] Transform saveTile;

    Unit1Arrow Unit1Arrow;

    private void Start()
    {
        unitCounting = gameObject.AddComponent<UnitCount>();

        Unit1Arrow = GetComponent<Unit1Arrow>();

        UnitOver();
    }

    public void BuyUnit(GameObject unitPrefab, Sprite sprite)
    {
        // 유닛을 구매할 때만 Instantiate 호출
        if (currentUnit == null)
        {
            currentUnit = Instantiate(unitPrefab, saveTile.position, Quaternion.identity);
            currentUnit.SetActive(false); // 구매 후 대기 상태
        }

        currentUnitSprite = sprite; // 스프라이트 설정
        currentUnit.SetActive(true); // 유닛 활성화
    }

    private void UnitOver()
    {
        // 유닛의 갯수가 한계 갯수와 같아지면 더이상 생성불가
        if (unitCount.GetUnitTotalCount() == unitCount.GetUnitCount())
        {
            foreach (Button button in unitButtons)
            {
                button.interactable = false;
            }
        }
        else
        {
            return;
        }
    }

    // 코루틴 사용해서 버튼이 1초뒤에 활성화되게
    IEnumerator UnitButtonActive()
    {
        yield return new WaitForSeconds(1.0f);

        // 유닛 목록 버튼 활성화
        foreach (Button button in unitButtons)
        {
            button.interactable = true;
        }
    }

    private void Update()
    {
        int mask = tileMask | DeleteZone;

        RaycastHit2D hit =
            Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition),
            Vector2.zero, Mathf.Infinity, mask);

        // 반복문으로 타일 가져오고 비활성화
        foreach (Transform tile in tiles)
        {
            tile.GetComponent<SpriteRenderer>().enabled = false;
        }

       

        // 마우스 콜라이더와 (유닛목록에서 구매한) 유닛이 충돌한다면
        if (hit.collider && currentUnit)
        {
            foreach (Button button in unitButtons)
            {
                button.interactable = false;
            }
            

          

            // 삭제존 클릭 시
            if (Input.GetMouseButtonDown(0) && hit.collider.gameObject.layer == LayerMask.NameToLayer("DeleteZone"))
            {
                Debug.Log("Delete Unit");
                currentUnit.transform.position = saveTile.position; // 세이브존으로 이동
                currentUnit.SetActive(false); // 유닛 비활성화
                currentUnitSprite = null;
                currentUnit = null;

                SettingText.gameObject.SetActive(false); // 메시지 비활성화

                Destroy(currentUnit);

                StartCoroutine(UnitButtonActive());
                return;
            }

            // 유닛 스프라이트 활성화
            if (currentUnitSprite != null)
            {
                hit.collider.GetComponent<SpriteRenderer>().sprite = currentUnitSprite;
                hit.collider.GetComponent<SpriteRenderer>().enabled = true;
            }

            // 메세지 활성화        
            SettingText.gameObject.SetActive(true);

            // 타일 마우스로 클릭 시
            if (Input.GetMouseButtonDown(0))
            {
                // 유닛이 없으면 생성 
                if (!hit.collider.GetComponent<Tile>().hasUnits)
                {
                    Tile tile = hit.collider.GetComponent<Tile>();

                    // 저장된 곳에서 충돌된 타일로 이동
                    currentUnit.transform.position = hit.collider.transform.position; // 타일로 이동
                    tile.PlaceUnit(currentUnit, currentUnitSprite); // 타일에 유닛 저장
                    
                    unitCount.Counting(); // 유닛 카운트 증가
                    
                    // 타일에 유닛이 있으면 메세지 비활성화
                    if (tile != null)
                    {
                        SettingText.gameObject.SetActive(false); // 메시지 비활성화
                    }

                    currentUnit = null;
                    currentUnitSprite = null;

                    StartCoroutine(UnitButtonActive());
                }
            }
        }

        // 타일에 유닛이 있고, 클릭했고, 스프라이트 없을 때
        if (hit.collider != null && hit.collider.GetComponent<Tile>() != null
            && hit.collider.GetComponent<Tile>().hasUnits && currentUnitSprite == null)
        {
            if (Input.GetMouseButtonDown(0))
            {
                Tile tile = hit.collider.GetComponent<Tile>();

                if (tile.placedUnit != null)
                {
                    unitCounting.DelCounting();
                    currentUnit = tile.GetplacedUnit(); // 타일에서 유닛 가져오기
                    currentUnitSprite = tile.GetplacedUnitSprite();

                    tile.MoveTileUnit(null, null); // 타일에서 유닛 제거 & 타일 hasUnit false
                    currentUnit.transform.position = saveTile.position; // 세이브존으로 이동

                }
            }
        }
    }
}


시행착오












타일 관리 후기

제일 오래걸렸고, 정리가 제일 안되어 스파게티 코드를 만들어낸 거 같다,,
함수단위로 좀 쪼개서 정리하고 싶었는데 계속 한 곳에서 작업하게 되었다
클린 코드 연습이 필요할 거 같다 ..

profile
뉴비 개발자

0개의 댓글