본문 바로가기
Development/C#

[멋쟁이사자처럼 부트캠프 TIL 회고] Unity 게임 개발 3기 14일차

by Mobics 2024. 12. 7.

 

목차


    자료구조

    Queue

    : First In First Out (FIFO) 원칙을 따르는 선형 자료구조

     

    Queue의 주요 연산

    • Enqueue : Queue의 뒤쪽(rear)에 새로운 요소를 추가한다.
    • Dequeue : Queue의 앞쪽(front)에서 요소를 제거하고 반환한다.
    • Peek/Front : Queue의 맨 앞 요소를 조회한다. (제거 X)
    • IsEmpty : Queue가 비어있는지 확인한다.
    • Size : Queue에 있는 요소의 개수를 반환한다.

    Queue 구현해보기

    └ 배열 기반 Queue 구현하기

     

    구현한 Queue의 특징

    • Generic 타입을 사용하여 다양한 데이터 타입을 저장할 수 있다.
    • 배열을 기반으로 한 원형 Queue 구조를 사용하여 메모리를 효율적으로 사용한다.
    • Queue가 가득 찼을 때, 자동으로 크기를 조절하는 기능이 있다.
    • 기본적인 Queue 연산(Enqueue, Dequeue, Peek)과 상태 확인 메서드(IsEmpty, IsFull, Size)를 제공한다.

    >> 코드

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class QueueA<T>
    {
        private T[] _array;
        private int _front;
        private int _rear;
        private int _size;
        private int _capacity; // 배열의 크기
    
        public QueueA(int capacity = 10)
        {
            _capacity = capacity;
            _array = new T[capacity];
            _front = 0;
            _rear = -1;
            _size = 0;
        }
    
        public void Enqueue(T data)
        {
            if (IsFull())
            {
                ResizeArray();
            }
    
            _rear = (_rear + 1) % _capacity;
            _array[_rear] = data;
            _size++;
        }
    
        public T Dequeue()
        {
            if (IsEmpty())
            {
                throw new InvalidOperationException("큐가 비어있습니다.");
            }
    
            T data = _array[_front];
            _front = (_front + 1) % _capacity;
            _size--;
            return data;
        }
    
        public T Peek()
        {
            if (IsEmpty())
            {
                throw new InvalidOperationException("큐가 비어있습니다.");
            }
    
            return _array[_front];
        }
    
        public bool IsEmpty()
        {
            return _size == 0;
        }
    
        public bool IsFull()
        {
            return _size == _capacity;
        }
    
        public int Size()
        {
            return _size;
        }
    
        private void ResizeArray()
        {
            int newCapacity = _capacity * 2;
            T[] newArray = new T[newCapacity];
            
            // 기존 요소들을 새 배열로 복사
            for (int i = 0; i < _size; i++)
            {
                newArray[i] = _array[(_front + i) % _capacity];
            }
    
            _array = newArray;
            _front = 0;
            _rear = _size - 1;
            _capacity = newCapacity;
        }
    }
    
    public class QueueExample : MonoBehaviour
    {
        
    }

    ※ throw문 : 예외 만들기 --> throw 라는 키워드 자체가 컴파일러에게 에러 메시지를 던져준다는 의미

    예외로 처리하면 그저 로직을 중단시키고 끝내는 것이 아니라 사용자에게 부활 위치로 돌아갈지, 기다릴지 표시할 팝업을 띄우는 등 별도의 처리를 할 수 있음

    https://learn.microsoft.com/ko-kr/dotnet/csharp/fundamentals/exceptions/creating-and-throwing-exceptions

     

    예외 만들기 및 Throw - C#

    예외 만들기 및 throw에 대해 알아봅니다. 예외는 프로그램을 실행하는 동안 오류가 발생했음을 나타내는 데 사용됩니다.

    learn.microsoft.com

    ※ Enqueue와 Dequeue에서 ' % _capacity'로 나눠서 순환시키는 이유 --> 원형 배열

    : 순환되지 않으면 계속해서 배열은 늘어나고 배열의 앞 순서들은 사용하지 않는데, 자꾸만 배열 크기만 커지니 비효율적이기 때문

    └└ 원형 배열(Circular Array)

     

    : 원형 배열은 고정된 크기의 배열을 마치 양 끝이 연결된 것처럼 사용할 수 있게 한 자료구조이다. , 배열의 크기가 N일 때, 배열의 마지막 요소(N-1)에 도착하면, 다음 배열 요소는 첫 번째 요소(0)로 순환하는 구조이다.

    >> 원형 배열은 FIFO 구조의 데이터 버퍼에 적합하여 흔히 Queue를 구현할 때 사용된다.

    https://velog.io/@everybodya/Ccharp%EB%B0%B0%EC%97%B4Array

    ※ 원형 배열은 배열을 순환하는 구조로 만들어야 하므로, 배열 인덱스를 증가 시킬 때, mod 연산자를 사용하여 마지막 배열의 다음 인덱스가 첫 배열 인덱스로 돌아오게 한다.

    index = (index + 1) % capacity

    >> 위 표현식을 사용하여, A[7] 요소를 읽고 다음 요소로 이동한다면, (7 + 1) % 8 = 0 , A[0] 배열 요소로 이동하게 된다.

    └ 노드 기반 Queue 구현하기

    : Linked List 방식으로 구현된다.

     

    장점

    • 동적 크기 : 메모리를 효율적으로 사용하며 크기 제한이 없다.
    • 삽입 / 삭제 효율성 : 포인터만 변경하면 되므로 O(1) 시간 복잡도를 가진다.
    • 메모리 관리 : 필요한 만큼만 메모리를 사용하여 메모리 낭비가 적다.

    >> 코드

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class NodeQ<T>
    {
        public T Data { get; set; }
        public NodeQ<T> Next { get; set; }
        
        public NodeQ(T data)
        {
            Data = data;
            Next = null;
        }
    }
    
    public class NodeQueue<T>
    {
        private NodeQ<T> _front;
        private NodeQ<T> _rear;
        private int _size;
    
        public NodeQueue()
        {
            _front = null;
            _rear = null;
            _size = 0;
        }
    
        public void Enqueue(T data)
        {
            NodeQ<T> newNode = new NodeQ<T>(data);
            
            if (IsEmpty())
            {
                _front = newNode;
                _rear = newNode;
            }
            else
            {
                _rear.Next = newNode;
                _rear = newNode;
            }
    
            _size++;
        }
    
        public T Dequeue()
        {
            if (IsEmpty())
            {
                throw new InvalidOperationException("Queue가 비어있습니다.");
            } 
            
            T data = _front.Data;
            _front = _front.Next;
            _size--;
    
            if (IsEmpty())
                _rear = null;
            
            return data;
        }
    
        public T Peek()
        {
            if (IsEmpty())
            {
                throw new InvalidOperationException("Queue가 비어있습니다.");
            }
            
            return _front.Data;
        }
    
        public bool IsEmpty()
        {
            return _size == 0;
        }
    
        public int Size()
        {
            return _size;
        }
    }
    
    public class QueueExample : MonoBehaviour
    {
        
    }

     

    └ Priority Queue (우선순위 큐) 구현해보기

    : 일반적인 Queue와 비슷하지만, 각 요소가 우선순위를 가지고 있어 우선순위가 높은 요소가 먼저 처리되는 자료구조

    >> 정렬된 Queue

     

    Priority Queue는 다음과 같은 상황에서 유용하게 사용된다.

    • 게임 AI 행동 결정 : AI 캐릭터의 다양한 행동들을 우선순위에 따라 실행
    • 이벤트 시스템 : 게임 내 이벤트들을 중요도에 따라 처리
    • 자원 관리 : 제한된 자원을 우선순위에 따라 할당
    • 패스파인딩 : A* 알고리즘에서 다음 탐색할 노드 결정

     

    >> 코드

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    // T는 반드시 Icomparable<T> 인터페이스를 구현해야 한다.
    // 최소 힙(Min Heap) 구조를 사용하여 구현됨
    public class PriorityQueue<T> where T : IComparable<T>
    {
        // 힙 구조를 저장하기 위한 내부 리스트
        private List<T> heap = new List<T>();
    
        public void Enqueue(T data)
        {
            heap.Add(data);
            int currentIndex = heap.Count - 1;
            HeapifyUp(currentIndex); // heap 속성을 만족하도록 위로 재정렬
        }
        
        // 우선 순위가 가장 높은 (값이 가장 작은) 항목을 제거하고 반환
        public T Dequeue()
        {
            if (heap.Count == 0)
                throw new InvalidOperationException("우선순위 큐가 비어있습니다.");
            // 루트 노드(가장 작은 값)를 저장
            T root = heap[0];
            int lastIndex = heap.Count - 1;
            
            // 마지막 노드를 루트로 이동
            heap[0] = heap[lastIndex];
            // 마지막 노드 제거
            heap.RemoveAt(lastIndex);
            
            // heap이 비어있지 않다면 heap 속성을 만족하도록 아래로 재정렬
            if (heap.Count > 0)
                HeapifyDown(0);
    
            return root;
        }
    
        // 지정된 index의 노드를 부모 노트와 비교하여 필요한 경우 위치를 교환
        // 최소 heap 속성을 유지하기 위해 상향식으로 재정렬
        private void HeapifyUp(int index)
        {
            while (index > 0)
            {
                // 부모 노드의 Index 계산
                int parentIndex = (index - 1) / 2;
                
                // 현재 노드가 부모 노드보다 크거나 같으면 중단 
                if (heap[index].CompareTo(heap[parentIndex]) >= 0)
                    break;
                
                // 현재 노드가 부모 노드보다 작으면 위치 교환
                Swap(index, parentIndex);
                // 다음 비교를 위해 인덱스를 부모 인덱스로 업데이트
                index = parentIndex;
            }
        }
    
        private void HeapifyDown(int index)
        {
            int lastIndex = heap.Count - 1;
    
            while (true)
            {
                int smallest = index;
                // 왼쪽 자식 노드의 인덱스 계산
                int leftChild = 2 * index + 1;
                // 오른쪽 자식 노드의 인덱스 계산
                int rightChild = 2 * index + 2;
                
                // 왼쪽 자식이 현재 노드보다 작으면 교환 대상으로 표시
                if (leftChild <= lastIndex && heap[leftChild].CompareTo(heap[smallest]) < 0)
                    smallest = leftChild;
                    
                // 오른쪽 자식이 현재 교환 대상보다 작으면 교환 대상으로 표시
                if (rightChild <= lastIndex && heap[rightChild].CompareTo(heap[smallest]) < 0)
                    smallest = rightChild;
                    
                // 교환이 필요 없으면 중단
                if (smallest == index)
                    break;
                    
                // 현재 노드와 가장 작은 자식 노드의 위치 교환
                Swap(index, smallest);
                // 다음 비교를 위해 인덱스 업데이트
                index = smallest;
            }
        }
    
        // heap 내의 두 노드의 위치를 교환
        private void Swap(int i, int j)
        {
            //T temp = heap[i];
            //heap[i] = heap[j];
            //heap[j] = temp;
            (heap[i], heap[j]) = (heap[j], heap[i]); // Tuple 사용
        }
        
        // 현재 우선순위 Queue에 있는 항목의 개수를 반환
        public int Count => heap.Count;
        
        // 우선순위 Queue가 비어있는지 여부를 반환
        public bool IsEmpty => heap.Count == 0;
    }
    
    public class QueueExample : MonoBehaviour
    {
        
    }

    where T : IComparable<T>

    https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/where-generic-type-constraint

     

    where(제네릭 형식 제약 조건) - C# reference

    where(제네릭 형식 제약 조건) - C# 참조

    learn.microsoft.com

    Object1.CompareTo(Object2) : Object1과 지정한 다른 Object2를 비교하여 Object가 크면 양수, 같으면 0, 작으면 음수를 반환한다.

     

    >> Priority Queue 사용 예제

    // ExampleComp.cs
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ExampleComp : MonoBehaviour
    {
        private PriorityQueue<QueueEvent> eventQueue = new PriorityQueue<QueueEvent>();
        
        // Start is called before the first frame update
        void Start()
        {
    // 이벤트 추가
            eventQueue.Enqueue(new QueueEvent("일반 몬스터 생성", 3));
            eventQueue.Enqueue(new QueueEvent("보스 몬스터 생성", 1));
            eventQueue.Enqueue(new QueueEvent("아이템 생성", priority: 2));
    
    // 우선순위가 높은 순서대로 처리
            while (!eventQueue.IsEmpty)
            {
                QueueEvent nextEvent = eventQueue.Dequeue();
                Debug.Log(nextEvent.name);
            }
        }
    }

    Heap

    : 완전 이진 트리의 일종으로, 부모 노드와 자식 노드 간의 대소 관계가 일정한 규칙을 따르는 자료구조

    >> Priority Queue를 구현하는데 자주 사용되며, 정렬 알고리즘(힙 정렬)이나 그래프 알고리즘(다익스트라)에서도 중요한 역할을 한다.

    종류

    • Max Heap (최대 힙) : 부모 노드의 값이 자식 노드의 값보다 크거나 같은 힙
    • Min Heap (최소 힙) : 부모 노드의 값이 자식 노드의 값보다 작거나 같은 힙

    특징

    • 완전 이진 트리 : 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있어야 한다.
    • 힙 속성 : Max Heap의 경우 부모가 자식보다 크거나 같고, Min Heap의 경우 부모가 자식보다 작거나 같다.
    • 효율적인 삽입 / 삭제 : 최악의 경우에도 O(log n)의 시간 복잡도를 보장한다.

    주요 연산

    • 삽입 (Insert) : 새로운 요소를 Heap의 마지막에 추가하고, 힙 속성을 만족할 때까지 위로 이동시킨다.
    • 삭제 (Delete) : root 노드를 제거하고, 마지막 노드를 root로 이동시킨 후 힙 속성을 만족할 때까지 아래로 이동시킨다.
    • 피크 (Peek) : root 노드의 값을 확인한다.

    https://velog.io/@emplam27/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%ED%9E%99Heap

     

    Priority Queue를 Unity에서 사용해보기

    코드 1.

    // HeapNode.cs
    using UnityEngine;
    using TMPro;
    
    public class HeapNode : MonoBehaviour
    {
        public TextMeshPro valueText;
        public SpriteRenderer nodeSprite;
    
        public void SetValue(int value)
        {
            valueText.text = value.ToString();
        }
    
        public void SetValueWithAnimation(int value)
        {
            PulseAnimation();
            SetValue(value);
        }
    
        public void MoveTo(Vector3 position)
        {
            transform.position = position;
        }
    
        public void Highlight()
        {
            nodeSprite.color = Color.yellow;
            Invoke(nameof(ResetColor), 0.5f);
        }
    
        public void HighlightAsSwap()
        {
            nodeSprite.color = Color.green;
            Invoke(nameof(ResetColor), 0.5f);
        }
    
        public void HighlightAsComparison()
        {
            nodeSprite.color = Color.cyan;
            Invoke(nameof(ResetColor), 0.3f);
        }
    
        private void ResetColor()
        {
            nodeSprite.color = Color.white;
        }
    
        public void PulseAnimation()
        {
            // LeanTween.scale(gameObject, Vector3.one * 1.2f, 0.2f)
            //          .setEasePunch()
            //          .setOnComplete(() => {
            //              transform.localScale = Vector3.one;
            //          });
        }
    }
    
    public class MaxHeap
    {
        private int[] heap;
        private int size;
        private int capacity;
        
        public System.Action<int[]> OnHeapUpdated;
    
        public MaxHeap(int capacity)
        {
            this.capacity = capacity;
            this.size = 0;
            this.heap = new int[capacity];
        }
    
        private int Parent(int index) => (index - 1) / 2;
        private int LeftChild(int index) => 2 * index + 1;
        private int RightChild(int index) => 2 * index + 2;
    
        public void Insert(int value)
        {
            if (size >= capacity)
            {
                throw new System.InvalidOperationException("힙이 가득 찼습니다.");
            }
    
            heap[size] = value;
            int current = size;
            size++;
    
            // 부모보다 큰 값이면 위로 이동 (최대 힙으로 변경)
            HeapifyUp(current);
            
            OnHeapUpdated?.Invoke(GetHeapArray());
        }
    
        private void HeapifyUp(int index)
        {
            while (index > 0)
            {
                int parentIndex = Parent(index);
                // 현재 노드가 부모 노드보다 크면 교환 (부등호 방향 변경)
                if (heap[index] > heap[parentIndex])
                {
                    Swap(index, parentIndex);
                    index = parentIndex;
                }
                else
                {
                    break;
                }
            }
        }
    
        private void HeapifyDown(int index)
        {
            int largest = index;
            int left = LeftChild(index);
            int right = RightChild(index);
    
            // 왼쪽 자식이 더 큰 경우 (부등호 방향 변경)
            if (left < size && heap[left] > heap[largest])
            {
                largest = left;
            }
    
            // 오른쪽 자식이 더 큰 경우 (부등호 방향 변경)
            if (right < size && heap[right] > heap[largest])
            {
                largest = right;
            }
    
            if (largest != index)
            {
                Swap(index, largest);
                HeapifyDown(largest);
            }
        }
    
        public int ExtractMax()  // ExtractMin에서 ExtractMax로 이름 변경
        {
            if (size <= 0)
            {
                throw new System.InvalidOperationException("힙이 비어있습니다.");
            }
    
            int max = heap[0];  // max로 변수명 변경
            heap[0] = heap[size - 1];
            size--;
    
            if (size > 0)
                HeapifyDown(0);
    
            OnHeapUpdated?.Invoke(GetHeapArray());
            
            return max;
        }
    
        private void Swap(int i, int j)
        {
            (heap[i], heap[j]) = (heap[j], heap[i]);
        }
    
        public int[] GetHeapArray()
        {
            int[] currentHeap = new int[size];
            System.Array.Copy(heap, currentHeap, size);
            return currentHeap;
        }
    
        public int GetMax() => size > 0 ? heap[0] : throw new System.InvalidOperationException("힙이 비어있습니다.");
        public int GetSize() => size;
        public bool IsEmpty() => size == 0;
    }

     

    코드 2.

    // HeapVisualizer.cs
    using UnityEngine;
    using UnityEngine.UI;
    using System.Collections.Generic;
    
    public class HeapVisualizer : MonoBehaviour
    {
        public GameObject nodePrefab;
        public Transform nodesContainer;
        public Button insertButton;
        public Button extractButton;
        public TMPro.TMP_InputField inputField;
        public LineRenderer lineRendererPrefab;  // 노드 간 연결선을 그리기 위한 프리팹
    
        private List<HeapNode> nodes = new List<HeapNode>();
        private List<LineRenderer> lines = new List<LineRenderer>();
        private MaxHeap heap;
        
        private float horizontalSpacing = 3f;
        private float verticalSpacing = 2f;
        private Vector2 rootPosition = new Vector2(0, 0);
    
        private void Start()
        {
            heap = new MaxHeap(15);
            heap.OnHeapUpdated += UpdateHeapVisualization;
            
            insertButton.onClick.AddListener(() => {
                if (int.TryParse(inputField.text, out int value))
                {
                    InsertWithVisualization(value);
                    inputField.text = "";
                }
            });
    
            extractButton.onClick.AddListener(ExtractMinWithVisualization);
        }
    
        private void InsertWithVisualization(int value)
        {
            GameObject nodeObj = Instantiate(nodePrefab, nodesContainer);
            HeapNode node = nodeObj.GetComponent<HeapNode>();
            node.SetValue(value);
            nodes.Add(node);
    
            heap.Insert(value);
        }
    
        private void ExtractMinWithVisualization()
        {
            if (nodes.Count > 0)
            {
                nodes[0].Highlight();
                Destroy(nodes[0].gameObject);
                nodes.RemoveAt(0);
    
                heap.ExtractMax();
                UpdateLines();
            }
        }
    
        // HeapVisualizer.cs의 UpdateHeapVisualization 메서드를 수정
        private void UpdateHeapVisualization(int[] heapArray)
        {
            // 기존 노드들의 GameObject 제거
            foreach (var node in nodes)
            {
                if (node != null)
                    Destroy(node.gameObject);
            }
            nodes.Clear();
    
            // 힙 배열의 각 요소에 대해 새 노드 생성
            for (int i = 0; i < heapArray.Length; i++)
            {
                GameObject nodeObj = Instantiate(nodePrefab, nodesContainer);
                HeapNode node = nodeObj.GetComponent<HeapNode>();
                node.SetValue(heapArray[i]);
                nodes.Add(node);
                node.MoveTo(CalculateNodePosition(i));
            }
    
            UpdateLines();
        }
    
        private Vector3 CalculateNodePosition(int index)
        {
            int level = Mathf.FloorToInt(Mathf.Log(index + 1, 2));
            int levelStartIndex = (1 << level) - 1;
            int positionInLevel = index - levelStartIndex;
            int nodesInLevel = 1 << level;
        
            float xPos = (positionInLevel - (nodesInLevel - 1) / 2.0f) * horizontalSpacing;
            float yPos = -level * verticalSpacing;
        
            return new Vector3(xPos, yPos, 0);
        }
    
    
        private void UpdateLines()
        {
            // 기존 라인 제거
            foreach (var line in lines)
            {
                if (line != null)
                    Destroy(line.gameObject);
            }
            lines.Clear();
    
            // 새로운 라인 생성
            for (int i = 0; i < nodes.Count; i++)
            {
                int leftChild = 2 * i + 1;
                int rightChild = 2 * i + 2;
    
                if (leftChild < nodes.Count)
                {
                    LineRenderer line = Instantiate(lineRendererPrefab, nodesContainer);
                    Vector3 startPos = nodes[i].transform.position;
                    Vector3 endPos = nodes[leftChild].transform.position;
                
                    line.positionCount = 2;
                    line.SetPosition(0, startPos);
                    line.SetPosition(1, endPos);
                    line.startWidth = 0.1f;
                    line.endWidth = 0.1f;
                    lines.Add(line);
                }
    
                if (rightChild < nodes.Count)
                {
                    LineRenderer line = Instantiate(lineRendererPrefab, nodesContainer);
                    Vector3 startPos = nodes[i].transform.position;
                    Vector3 endPos = nodes[rightChild].transform.position;
                
                    line.positionCount = 2;
                    line.SetPosition(0, startPos);
                    line.SetPosition(1, endPos);
                    line.startWidth = 0.1f;
                    line.endWidth = 0.1f;
                    lines.Add(line);
                }
            }
        }
    
        private void CreateLine(int parentIndex, int childIndex)
        {
            LineRenderer line = Instantiate(lineRendererPrefab, nodesContainer);
            line.positionCount = 2;
            line.SetPosition(0, nodes[parentIndex].transform.position);
            line.SetPosition(1, nodes[childIndex].transform.position);
            lines.Add(line);
        }
    
        private void OnDestroy()
        {
            if (heap != null)
                heap.OnHeapUpdated -= UpdateHeapVisualization;
        }
    }

     

    1. Node Prefab 만들기

    - 빈 게임 오브젝트 만들어서 Heap Node Script 추가

    - 자식으로 빈 게임 오브젝트 만들어서 Add Component로 SpriteRenderer 추가 --> Color 설정 및 Scale 수정 (x : 3, y : 3, z : 1)

    - 자식으로 TextMeshPro - Text 추가 --> Font Size 수정 (5)

    - Heap Node Script에 자식 둘 다 바인딩

     

    2. Line Prefab 만들기

    - 빈 게임 오브젝트 만들어서 Add Component로 Line Renderer 추가 --> Size에 Width 0 으로 줄이고 Material로 Default - Line 추가

    - Line의 Order in Layer 를 -1로 변경

     

    3. UI 만들기

    - Canvas 만들고 버튼으로 Insert, Extract 생성

    - Input Field - TextMashPro 생성 --> Input Field를 펼치면 있는 PlaceHolder에서 Text Input 삭제

     

    4. 빈 게임 오브젝트 만들어서 Script 추가 및 바인딩

    - 빈 게임 오브젝트로 NodeContainer, HeapVisualizer 만들기

    - HeapVisualizer에 HeapVisualizer Script 추가한 뒤, 각각에 맞게 바인딩

     

    5. 세부 사항 수정

    - Camera의 Projection을 Orthographic 으로 변경 후 position 수정 (x : 0, y : -2, z : -5)

    - Button 위치 조정


    Object Pooling System

    : 게임에서 자주 생성되고 파괴되는 Object (예: 총알, 파티클 효과 등)를 효율적으로 관리하기 위해 Object Pooling System(오브젝트 풀링 시스템)을 구현할 때 Queue를 사용할 수 있다.

     

    >> 코드

    using UnityEngine;
    using System.Collections.Generic;
    
    public class ObjectPool : MonoBehaviour
    {
        public GameObject prefab;
        public int poolSize = 10;
    
        private Queue<GameObject> objectPool = new Queue<GameObject>();
    
        void Start()
        {
            for (int i = 0; i < poolSize; i++)
            {
                GameObject obj = Instantiate(prefab);
                obj.SetActive(false);
                objectPool.Enqueue(obj);
            }
        }
    
        public GameObject GetPooledObject()
        {
            if (objectPool.Count > 0)
            {
                GameObject obj = objectPool.Dequeue();
                obj.SetActive(true);
                return obj;
            }
            return null;
        }
    
        public void ReturnToPool(GameObject obj)
        {
            obj.SetActive(false);
            objectPool.Enqueue(obj);
        }
    }

    ※ 이 예제에서 큐는 비활성화된 오브젝트들을 저장하고 관리하는 데 사용된다.

      GetPooledObject() 메소드는 큐에서 오브젝트를 꺼내 (Dequeue) 사용하고, ReturnToPool() 메소드는 사용이 끝난 오브젝트를 다시 큐에 넣는다. (Enqueue)

    >> 이러한 방식으로 큐를 사용하면 게임 성능을 크게 향상시킬 수 있다. --> 오브젝트를 매번 생성하고 파괴하는 대신, 미리 생성된 오브젝트를 재사용함으로써 메모리 관리와 성능 최적화에 도움이 된다.

     

    ※ 더 발전시킨 코드

    // ObjectCreator.cs
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ObjectCreator : MonoBehaviour
    {
        public GameObject prefab;
        public int CreateCount;
        public List<GameObject> objects = new List<GameObject>();
        
        void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                for (int i = 0; i < CreateCount; i++)
                {
                    float x = Random.Range(-100, 100);
                    float y = Random.Range(-100, 100);
                    float z = Random.Range(-100, 100);
                
                    var go = Instantiate(prefab, new Vector3(x, y, z), Quaternion.identity);
                    objects.Add(go);
                }
            }
            else if (Input.GetKeyDown(KeyCode.Delete))
            {
                for (var i = 0; i < objects.Count; i++)
                {
                    Destroy(objects[i]);
                }
                
                objects.Clear();
            }
        }
    }
    // ObjectPool.cs
    
    using UnityEngine;
    using System.Collections.Generic;
    
    public class ObjectPool : MonoBehaviour
    {
        public GameObject prefab;
        public int poolSize = 10;
        public int CreateCount;
    
        private Queue<GameObject> objectPool = new Queue<GameObject>();
        public List<GameObject> objects = new List<GameObject>();
    
        void Start()
        {
            for (int i = 0; i < poolSize; i++)
            {
                GameObject obj = Instantiate(prefab);
                obj.SetActive(false);
                objectPool.Enqueue(obj);
            }
        }
    
        public GameObject GetPooledObject()
        {
            if (objectPool.Count > 0)
            {
                GameObject obj = objectPool.Dequeue();
                obj.SetActive(true);
                return obj;
            }
            else
            {
                for (int i = 0; i < poolSize; i++)
                {
                    GameObject obj = Instantiate(prefab);
                    obj.SetActive(false);
                    objectPool.Enqueue(obj);
                }
    
                return objectPool.Dequeue();
            }
        }
    
        public void ReturnToPool(GameObject obj)
        {
            obj.SetActive(false);
            objectPool.Enqueue(obj);
        }
        
        void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                for (int i = 0; i < CreateCount; i++)
                {
                    float x = Random.Range(-100, 100);
                    float y = Random.Range(-100, 100);
                    float z = Random.Range(-100, 100);
    
                    var go = GetPooledObject();
                    go.transform.position = new Vector3(x, y, z);
                    objects.Add(go);
                }
            }
            else if (Input.GetKeyDown(KeyCode.Delete))
            {
                for (var i = 0; i < objects.Count; i++)
                {
                    ReturnToPool(objects[i]);
                }
                
                objects.Clear();
            }
        }
    }

     

    └ Animation Retargeting (애니메이션 리타겟팅)

    >> 메시를 다운 받아서 Import한다음 Rig로 가면 Animation Type으로 Generic과 Humonuid가 대표적으로 사용되는데,

    애니메이션을 리타켓팅할때는 보통 Humoniud를 사용하고(인간형 타입) 그외에는 Generic을 사용한다.

     

    Animation이 없는 모델에 Animation 넣기

    1. Animation이 없는 모델을 Import하여 Humaniod로 설정하고 Apply

    2. 모델을 Prefab 시키고 만들면 자동으로 Animator가 붙는다 --> Animator Controller를 생성하여 Controller에 바인딩

    3. Animation이 있는 모델의 Animation을 클릭하고 Ctrl + D 로 복사하기 --> Animation이 밖으로 빠진다.

    ※ Roop Time 체크

    4. 복사한 Animation의 프리뷰에다가 Animation이 없는 모델 넣기

     

    ※ 인간형 본(Bone)을 재할당하여 리타겟팅해서 사용 가능

    ※ 만약 Animation에서 Wolrd Position이 움직이는게 싫다면 --> Apply Root Motion 확인하기 (Animation에 따라 다름)

     

    + Object Pooling System

    : 빈 게임 오브젝트 만들어서 Object Pool Script 추가하고 바인딩