본문 바로가기
프로그래밍/디자인패턴

공간 분할 패턴_유니티(Unity)에서 C# 으로 사용해 보는 디자인 패턴

by 리뷰하는 (게임)프로그래머_리프TV 2020. 6. 13.

오브젝트가 많을 때 오브젝트 간의 충돌을 검사 한다고 하면

모든 오브젝트를 검사하는데 비용이 매우 많이 들어감

 

그래서 전체 영역을 n개로 쪼개서 

쪼개진 부분과 동일한 곳에 있는 오브젝트끼리만 검사를 하는 패턴

 

using UnityEngine;

public class KMS_SpacePartition : MonoBehaviour
{
    public static KMS_Grid gridMap = new KMS_Grid();
    GameObject obj = null;
    private void Start()
    {
        obj = Instantiate(Resources.Load("Cube") as GameObject);
        Camera.main.gameObject.transform.parent = obj.transform;
    }

    private void Update()
    {
        // 충돌 체크
        if (Input.GetKeyDown(KeyCode.Space))
        {
            KMS_Unit unit = obj.GetComponent<KMS_Unit>();
            gridMap.HandleMelee(unit.x, unit.y);
        }
        if( Input.GetKeyUp(KeyCode.Space))
        {
            gridMap.ResetColor();
        }

        // 방향키로 메인 Object 이동
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            var unit = obj.GetComponent<KMS_Unit>();
            unit.Move(-(Time.deltaTime * 10.0f), 0);
        }
        else if (Input.GetKey(KeyCode.RightArrow))
        {
            var unit = obj.GetComponent<KMS_Unit>();
            unit.Move((Time.deltaTime * 10.0f), 0);
        }

        if (Input.GetKey(KeyCode.DownArrow))
        {
            var unit = obj.GetComponent<KMS_Unit>();
            unit.Move(0, -(Time.deltaTime * 10.0f));
        }
        else if (Input.GetKey(KeyCode.UpArrow))
        {
            var unit = obj.GetComponent<KMS_Unit>();
            unit.Move(0, (Time.deltaTime * 10.0f));
        }
    }
}

이동 위치에 따라 영역이 바뀐다.
같은 색상(영역)과 그 근처의 오브젝트끼리만 검사

물론 영역은 다르지만, 충돌 거리안으로 들어 오는 경우도 있을 듯, 해당 부분도 추가로 검사

 

크게 유닛과, 그리드로 나뉨

 

Unit.cs

using UnityEngine;

public class KMS_Unit : MonoBehaviour
{
    private void Start()
    {
        x = transform.localPosition.x;
        y = transform.localPosition.y;
        prev = null;
        next = null;
        // 생성이 완료 되면 gridMap에 넣는게 가장 중요
        KMS_SpacePartition.gridMap.Add(this);
        UpdateColor();
    }

    public float x, y;
    public KMS_Unit prev;
    public KMS_Unit next;
    public Color findColor = new Color(255, 255, 255);
    Renderer cubeRenderer = null;
    Color myColor;
    Color crashColor = new Color(0, 0, 255);

    // 키 입력을 받으면 이동
    public void Move(float _x, float _y)
    {
        if ((x + _x) >= KMS_Grid.NUM_CELL * KMS_Grid.CELL_SIZE)
            return;
        if ((x + _x) < 0)
            return;
        if ((y + _y) >= KMS_Grid.NUM_CELL * KMS_Grid.CELL_SIZE)
            return;
        if ((y + _y) < 0)
            return;

        KMS_SpacePartition.gridMap.Move(this, x + _x, y + _y);

        transform.localPosition = new Vector3(x, y, transform.localPosition.z);

        UpdateColor();
    }
    
    // 위치에 따른 컬러 표기 
    public void UpdateColor()
    {
        if (cubeRenderer == null)
            cubeRenderer = gameObject.GetComponentInChildren<Renderer>();

        if (cubeRenderer != null)
        {
            int cellX = (int)(x / KMS_Grid.CELL_SIZE);
            int cellY = (int)(y / KMS_Grid.CELL_SIZE);

            myColor = new Color(cellX / (float)KMS_Grid.CELL_SIZE,
                cellY / (float)KMS_Grid.CELL_SIZE, 0);

            cubeRenderer.material.SetColor("_Color", myColor);
        }
    }

    // 검사 대상자에 따른 컬러 표기
    public void UpdateFindColor()
    {
        if (cubeRenderer == null)
            cubeRenderer = gameObject.GetComponentInChildren<Renderer>();

        if (cubeRenderer != null)
        {
            cubeRenderer.material.SetColor("_Color", findColor);
        }
    }
}

 

 

Grid.cs

using UnityEngine;

public class KMS_Grid
{
    public const int NUM_CELL = 4;
    public const int CELL_SIZE = 2;
    public const float ATTACK_DISTANCE = 1.5f;

    // 모든 Unit은 Add를 통해 등록이 되어야 함
    private KMS_Unit[,] cells = new KMS_Unit[NUM_CELL, NUM_CELL];

    public void Add(KMS_Unit unit)
    {
        int cellX = (int)(unit.x / CELL_SIZE);
        int cellY = (int)(unit.y / CELL_SIZE);

        unit.prev = null;
        unit.next = cells[cellX, cellY];
        cells[cellX, cellY] = unit;

        if (unit.next != null)
        {
            unit.next.prev = unit;
        }
    }

    public void HandleMelee(float unitX, float unitY)
    {
        int cellX = (int)(unitX / CELL_SIZE);
        int cellY = (int)(unitY / CELL_SIZE);

        // 내 유닛이 있는 곳만 검색
        HandleCell(cellX, cellY);
    }

    void HandleCell(int x, int y)
    {
        KMS_Unit unit = cells[x, y];

        while (unit != null)
        {
            HandleUnit(unit, unit.next);

            if (x > 0) HandleUnit(unit, cells[x - 1, y]);
            if (y > 0) HandleUnit(unit, cells[x, y - 1]);
            if (x > 0 && y > 0) HandleUnit(unit, cells[x - 1, y - 1]);
            if (x > 0 && y < NUM_CELL - 1) HandleUnit(unit, cells[x - 1, y + 1]);

            if (x < NUM_CELL - 1) HandleUnit(unit, cells[x + 1, y]);
            if (y < NUM_CELL - 1) HandleUnit(unit, cells[x, y + 1]);
            if (x < NUM_CELL - 1 && y < NUM_CELL - 1) HandleUnit(unit, cells[x + 1, y + 1]);
            if (x < NUM_CELL - 1 && y > 0) HandleUnit(unit, cells[x + 1, y - 1]);

            unit = unit.next;
        }
    }

    void HandleAttack(KMS_Unit attackUnit, KMS_Unit defenceUnit)
    {
        if (Vector3.Distance(attackUnit.transform.localPosition, defenceUnit.transform.localPosition) <= ATTACK_DISTANCE)
        {
            // 충돌 성공 시 로그 출력
            Debug.Log(defenceUnit.gameObject.name + "&" + attackUnit.gameObject.name + " Hit");
        }
    }

    public void Move(KMS_Unit unit, float x, float y)
    {
        // 위치 정보를 업데이트
        int oldCellX = (int)(unit.x / CELL_SIZE);
        int oldCellY = (int)(unit.y / CELL_SIZE);

        int cellX = (int)(x / CELL_SIZE);
        int cellY = (int)(y / CELL_SIZE);

        unit.x = x;
        unit.y = y;
        // 여기까지가 위치 정보 수정이고

        // 여기서 부터는 검색 cell 위치 수정
        if (oldCellX == cellX && oldCellY == cellY)
        {
            return;
        }

        if (unit.prev != null)
        {
            unit.prev.next = unit.next;
        }

        if (unit.next != null)
        {
            unit.next.prev = unit.prev;
        }

        if (cells[oldCellX, oldCellY] == unit)
        {
            cells[oldCellX, oldCellY] = unit.next;
        }

        Add(unit);

        Debug.Log(string.Format("Current Cell Pos : {0}, {1}", cellX, cellY));
    }

    public void HandleUnit(KMS_Unit unit, KMS_Unit other)
    {
        while (other != null)
        {
            if (Distance(unit, other) < ATTACK_DISTANCE)
            {
                HandleAttack(unit, other);
            }
            other = other.next;
        }
    }

    float Distance(KMS_Unit unit, KMS_Unit other)
    {
        // 거리 계산, 대상 오브젝트들은 컬러를 변경해서 눈에 잘 보이게 
        unit.UpdateFindColor();
        other.UpdateFindColor();
        return Vector3.Distance(unit.transform.localPosition, other.transform.localPosition);
    }

    public void ResetColor()
    {
        for (int x = 0; x < NUM_CELL; ++x)
        {
            for (int y = 0; y < NUM_CELL; ++y)
            {
                KMS_Unit unit = cells[x, y];
                while (unit != null )
                {
                    unit.UpdateColor();
                    unit = unit.next;
                }
            }
        }
    }
}