큐 Queue
선입선출 FIFO(First In First Out)
선형 자료구조
주요 연산
Enqueue : 큐의 뒤쪽(near)에 새로운 요소 추가
Dequeue: 큐의 앞쪽(front)에서 요소를 제거하고 반환
Peek/Front : 큐의 맨 앞 요소 조회(제거X)
IsEmpty: 큐가 비어있는지 확인
Size: 큐에 있는 요소의 개수를 반환
노드 기반 큐 구현
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class QueueNode<T>
{
public T Data { get; set; } // 데이터를 저장하는 프로퍼티
public QueueNode<T> Next { get; set; } // 다음 노드를 가리키는 참조
public QueueNode(T data) // 생성자 : 데이터 초기화, 다음 노드 null로 설정
{
Data = data;
Next = null;
}
}
// 큐를 구현하는 클래스
public class NodeQueue<T>
{
private QueueNode<T> front; // 큐의 앞쪽(제일 먼저 들어간 노드)
private QueueNode<T> rear; // 큐의 뒤쪽(마지막으로 들어간 노드)
private int size; // 큐의 현재 크기
// 큐 생성자: 처음에는 큐가 비어 있으므로 모든 값을 초기화
public NodeQueue()
{
front = null; // 앞쪽 노드는 없음
rear = null; // 뒤쪽 노드도 없음
size = 0; // 큐의 크기는 0
}
// 큐에 데이터를 추가하는 메서드 (뒤쪽에 추가)
public void Enqueue(T item)
{
// 새로운 데이터를 가지는 노드 생성
QueueNode<T> newNode = new QueueNode<T>(item);
// 큐가 비어 있는 경우
if (isEmpty())
{
front = newNode; // 새로운 노드를 큐의 앞쪽으로 설정
rear = newNode; // 새로운 노드를 큐의 뒤쪽으로 설정
}
else
{
rear.Next = newNode; // 기존 뒤쪽 노드의 다음 노드를 새로운 노드로 연결
rear = newNode; // 뒤쪽 노드를 새로운 노드로 갱신
}
size++; // 큐의 크기 증가
}
// 큐가 비어 있는지 확인하는 메서드
public bool isEmpty()
{
return size == 0; // 크기가 0이면 큐는 비어 있음
}
// 큐에서 데이터를 제거하고 반환하는 메서드 (앞쪽에서 제거)
public T Dequeue()
{
// 큐가 비어 있으면 예외 발생
if (isEmpty())
{
throw new InvalidOperationException("큐가 비어있습니다");
}
// 앞쪽 노드의 데이터를 임시로 저장
T data = front.Data;
// 앞쪽 노드를 다음 노드로 변경
front = front.Next;
size--; // 큐의 크기 감소
// 큐가 비어 있으면 뒤쪽 노드도 null로 설정
if (isEmpty())
{
rear = null;
}
return data; // 제거된 데이터를 반환
}
// 큐의 맨 앞 데이터를 반환하지만 제거하지 않는 메서드
public T Peek()
{
// 큐가 비어 있으면 예외 발생
if (isEmpty())
{
throw new InvalidOperationException("큐가 비어있습니다");
}
return front.Data; // 앞쪽 노드의 데이터를 반환
}
// 큐의 현재 크기를 반환하는 메서드
public int Size()
{
return size; // 큐에 있는 노드의 개수
}
}
결과 확인
public class QueueExample : MonoBehaviour
{
void Start()
{
NodeQueue<string> queue = new NodeQueue<string>();
// 요소 추가
queue.Enqueue("첫 번째");
queue.Enqueue("두 번째");
queue.Enqueue("세 번째");
// 요소 제거 및 확인
Debug.Log(queue.Dequeue()); // 출력: "첫 번째"
Debug.Log(queue.Peek()); // 출력: "두 번째"
Debug.Log(queue.Size()); // 출력: "2"
}
}
우선순위 큐 Priority Queue
// PriorityQueue.cs
using System;
using System.Collections.Generic;
using UnityEngine;
public class QueueEvent : IComparable<QueueEvent>
{
public string name;
public int priority;
public QueueEvent(string name, int priority)
{
this.name = name;
this.priority = priority;
}
public int CompareTo(QueueEvent other)
{
if (ReferenceEquals(this, other)) return 0;
if (other is null) return 1;
var nameComparison = string.Compare(name, other.name, StringComparison.Ordinal);
if (nameComparison != 0) return nameComparison;
return priority.CompareTo(other.priority);
}
}
/// <summary>
/// 제네릭 우선순위 큐 구현
/// T는 반드시 IComparable<T> 인터페이스를 구현해야 함
/// 최소 힙(Min Heap) 구조를 사용하여 구현됨
/// </summary>
public class PriorityQueue<T> where T : IComparable<T>
{
// 힙 구조를 저장하기 위한 내부 리스트
private List<T> heap = new List<T>();
/// <summary>
/// 우선순위 큐에 새로운 항목을 추가
/// </summary>
/// <param name="item">추가할 항목</param>
public void Enqueue(T item)
{
// 새 항목을 힙의 마지막에 추가
heap.Add(item);
// 새로 추가된 항목의 인덱스
int currentIndex = heap.Count - 1;
// 힙 속성을 만족하도록 위로 재정렬
HeapifyUp(currentIndex);
}
/// <summary>
/// 우선순위가 가장 높은(값이 가장 작은) 항목을 제거하고 반환
/// </summary>
/// <returns>우선순위가 가장 높은 항목</returns>
/// <exception cref="InvalidOperationException">큐가 비어있을 경우 발생</exception>
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);
// 힙이 비어있지 않다면 힙 속성을 만족하도록 아래로 재정렬
if (heap.Count > 0)
HeapifyDown(0);
return root;
}
/// <summary>
/// 지정된 인덱스의 노드를 부모 노드와 비교하여 필요한 경우 위치를 교환
/// 최소 힙 속성을 유지하기 위해 상향식으로 재정렬
/// </summary>
/// <param name="index">재정렬을 시작할 노드의 인덱스</param>
private void HeapifyUp(int index)
{
while (index > 0)
{
// 부모 노드의 인덱스 계산
int parentIndex = (index - 1) / 2;
// 현재 노드가 부모 노드보다 크거나 같으면 중단
if (heap[index].CompareTo(heap[parentIndex]) >= 0)
break;
// 현재 노드가 부모 노드보다 작으면 위치 교환
Swap(index, parentIndex);
// 다음 비교를 위해 인덱스를 부모 인덱스로 업데이트
index = parentIndex;
}
}
/// <summary>
/// 지정된 인덱스의 노드를 자식 노드들과 비교하여 필요한 경우 위치를 교환
/// 최소 힙 속성을 유지하기 위해 하향식으로 재정렬
/// </summary>
/// <param name="index">재정렬을 시작할 노드의 인덱스</param>
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;
}
}
/// <summary>
/// 힙 내의 두 노드의 위치를 교환
/// </summary>
/// <param name="i">첫 번째 노드의 인덱스</param>
/// <param name="j">두 번째 노드의 인덱스</param>
private void Swap(int i, int j)
{
T temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
/// <summary>
/// 현재 우선순위 큐에 있는 항목의 개수를 반환
/// </summary>
public int Count => heap.Count;
/// <summary>
/// 우선순위 큐가 비어있는지 여부를 반환
/// </summary>
public bool IsEmpty => heap.Count == 0;
}
ExampleComp.cs에서 호출
// ExampleComp.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ExampleComp : MonoBehaviour
{
private PriorityQueue<QueueEvent> eventQueue = new PriorityQueue<QueueEvent>();
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);
}
}
}
힙 유니티로 구현
// HeapVisualizer.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
// 힙 데이터를 시각적으로 표시하는 클래스
public class HeapVisualizer : MonoBehaviour
{
public GameObject nodePrefab; // 힙 노드를 표시하는 UI 프리팹
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 클릭 시, 입력 필드의 값을 힙에 삽입
insertButton.onClick.AddListener(() => {
if (int.TryParse(inputField.text, out int value)) // 입력 값을 정수로 변환
{
InsertWithVisualization(value); // 값을 삽입하고 시각화 갱신
inputField.text = ""; // 입력 필드 초기화
}
});
// extractButton 클릭 시, 최댓값 제거 및 시각화 갱신
extractButton.onClick.AddListener(ExtractMinWithVisualization);
}
// 힙에 데이터를 추가하고 시각적으로 노드를 생성하는 메서드
private void InsertWithVisualization(int value)
{
GameObject nodeObj = Instantiate(nodePrefab, nodesContainer); // 노드 프리팹 생성
HeapNode node = nodeObj.GetComponent<HeapNode>(); // HeapNode 컴포넌트 가져오기
node.SetValue(value); // 노드에 값 설정
nodes.Add(node); // 노드를 리스트에 추가
heap.Insert(value); // 힙 자료구조에 값을 삽입
}
// 힙에서 최댓값을 제거하고 이를 시각적으로 반영
private void ExtractMinWithVisualization()
{
if (nodes.Count > 0) // 노드가 존재할 때만 실행
{
nodes[0].Highlight(); // 최댓값 노드를 강조 표시
Destroy(nodes[0].gameObject); // 최댓값 노드의 UI를 제거
nodes.RemoveAt(0); // 리스트에서 제거
heap.ExtractMax(); // 힙 자료구조에서 최댓값 제거
UpdateLines(); // 연결선을 갱신
}
}
// 힙 배열의 상태를 시각적으로 갱신
private void UpdateHeapVisualization(int[] heapArray)
{
// 기존 노드 UI 제거
foreach (var node in nodes)
{
if (node != null)
Destroy(node.gameObject);
}
nodes.Clear(); // 리스트 초기화
// 힙 배열에 따라 새로운 노드 UI 생성
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)
{
CreateLine(i, leftChild); // 부모-왼쪽 자식 간 연결선 생성
}
if (rightChild < nodes.Count)
{
CreateLine(i, rightChild); // 부모-오른쪽 자식 간 연결선 생성
}
}
}
// 부모와 자식 노드를 연결하는 선을 생성
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;
}
}
// HeapVisualizer.cs
// 힙 노드의 시각화를 담당하는 클래스
using UnityEngine;
using TMPro;
public class HeapNode : MonoBehaviour
{
public TextMeshPro valueText; // 노드의 값을 표시하는 UI 텍스트
public SpriteRenderer nodeSprite; // 노드의 외형
// 노드의 값을 설정하는 메서드
public void SetValue(int value)
{
valueText.text = value.ToString(); // 값 텍스트를 갱신
}
// 노드를 특정 위치로 이동시키는 메서드
public void MoveTo(Vector3 position)
{
transform.position = position; // 노드의 위치를 갱신
}
// 노드를 강조 표시하는 메서드
public void Highlight()
{
nodeSprite.color = Color.yellow; // 노드 색상을 노란색으로 변경
Invoke(nameof(ResetColor), 0.5f); // 0.5초 후 색상 초기화
}
// 강조 표시된 노드의 색상을 초기화
private void ResetColor()
{
nodeSprite.color = Color.white; // 기본 색상으로 복구
}
}
위의 스크립트 생성 후 Unity Editor에서
Prefab 생성
Node(Empty GameObj), Line(Effects/Line)
Line은 Order inLayer -2로 설정(맨 뒤로 보내기)
Node는 HeapNode.cs 넣어주기
GameObject, Text(TMP) 생성 후 스크립트 각 항목에 넣기
Node/GameObject 안에 Sprite Renderer 속성 추가하고 Order in Layer -1(글자보다 뒤로 보내기 위해) 설정
Text도 위치랑 크기 잡아주기
빈게임 오브젝트, UI 요소들 생성하고 HeapVisualizer에 HeapVisualizer 스크립트 넣어주기
각 항목 할당해주기
오브젝트 풀링 Object Pulling
부하 테스트
// 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>();
// Update is called once per frame
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();
}
}
}
스페이스바 누르면 생성되는데 나는 쫄아서 걍 조금씩만 했지만 강사님은 100000개씩 하시니까 렉 걸림
Object Pool 코드로 다시 실행해보면
// 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();
}
return null;
}
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();
}
}
}
똑같이 스페이스바 누르면 생성되는 방식이지만 작동 방식이 다름
두 코드의 차이 정리
특징 | ObjectPool.cs | ObjectCreator.cs |
오브젝트 생성 방식 | 미리 지정된 개수만큼 오브젝트를 생성해 재사용 | 필요할 떄마다 새로 생성(Instantiate) |
오브젝트 삭제 방식 | 오브젝트를 비활성화하고 풀로 반환 | 오브젝트를 완전히 삭제(Destroy) |
성능 | 오브젝트를 재사용하므로 메모리 할당과 해제 비용 절감 | 매번 새로 생성/삭제하므로 성능 부담 |
메모리 관리 | 메모리 사용량 일정 | 메모리 사용량 증가 및 감소 반복 |
적합한 상황 | 오브젝트를 반복적으로 생성/삭제하는 경우 | 오브젝트가 한 번 생성되고 소멸되는 경우 |
오브젝트 풀링(Object Pooling) 설명
오브젝트 풀링이란?
오브젝트 풀링은 오브젝트를 미리 생성한 뒤, 필요할 때 재사용하고, 사용이 끝나면 다시 반환하는 기법입니다. 새로운 오브젝트를 매번 생성하거나 삭제하지 않으므로 성능 최적화와 메모리 관리 측면에서 효과적입니다.
왜 오브젝트 풀링이 필요한가?
- Instantiate와 Destroy는 성능 부담이 크다:
- 오브젝트 생성(Instantiate)은 CPU 연산과 메모리 할당이 필요합니다.
- 오브젝트 삭제(Destroy)는 메모리를 해제하고 가비지 컬렉터가 이를 처리해야 하므로 성능에 영향을 미칩니다.
- 실시간 처리 상황에서 효율성:
- 게임처럼 매 프레임마다 반복적으로 생성과 삭제가 발생하는 환경에서는 오브젝트 풀링이 필요합니다.
오브젝트 풀링의 장점
- 성능 향상:
- 매번 오브젝트를 생성/삭제하지 않고, 기존 오브젝트를 재사용하므로 부하가 줄어듭니다.
- 메모리 안정성:
- 메모리 할당과 해제 과정이 줄어들어 메모리 사용량이 일정하게 유지됩니다.
- 예측 가능성:
- 미리 생성된 오브젝트를 재사용하므로 예상치 못한 지연 시간이 줄어듭니다.
오브젝트 풀링의 단점
- 초기 메모리 사용량:
- 미리 오브젝트를 생성하므로 초기 메모리 소비가 높을 수 있습니다.
- 관리 필요:
- 풀의 크기를 적절히 설정하고, 반환되지 않은 오브젝트를 추적해야 합니다.
코드 비교를 통해 배우는 점
ObjectPool.cs의 핵심
- 오브젝트 재사용:
- 미리 지정된 개수만큼 오브젝트를 생성하고 비활성화 상태로 유지합니다.
- 필요할 때 활성화하여 사용하고, 사용이 끝나면 다시 비활성화합니다.
- 효율적인 관리:
- Queue를 이용해 풀을 관리하며, 오브젝트가 부족할 경우 추가로 생성합니다.
ObjectCreator.cs의 문제점
- 매번 생성 및 삭제:
- Instantiate와 Destroy를 반복 사용해 CPU와 메모리 부하가 증가합니다.
- 실시간 시스템에서 부적합:
- 예를 들어, 총알처럼 매우 빠르게 생성되고 삭제되는 오브젝트에는 성능 문제가 발생할 수 있습니다.
오브젝트 풀링이 적합한 예
- 총알 시스템:
- FPS 게임에서 플레이어가 총을 쏠 때 매번 총알 오브젝트를 생성하면 성능 저하가 발생합니다. 미리 총알을 생성해 두고, 발사할 때 재사용합니다.
- 적 스폰 시스템:
- 웨이브 기반 적 생성 게임에서 적 오브젝트를 반복적으로 생성/삭제하기보다는 풀링을 통해 적 오브젝트를 재사용합니다.
- 파티클 효과:
- 폭발, 연기, 불꽃 등 자주 발생하는 효과를 풀링으로 관리하면 부드러운 그래픽 처리가 가능합니다.
리타겟팅
메시를 다운 받아서 임포트한다음 Rig로 가면 Generic하고 Humonuid가 대표적으로 사용되는데
애니메이션을 리타켓팅할때는 보통 Humoniud를 사용하고(인간형 타입) 그외에는 Generic을 사용
애니메이션이 없는 모델도 Type을 Humanoid로 설정하고 Apply 누른 뒤, Prefab화하면 Avatar가 생김, 그 Prefab을 Hierachy로 끌어다놓으면 Animator가 생겨있음
Animator Controller 를 생성해서 Controller에 넣어줌
fbx에 들어있는 애니메이션을 클릭하고 컨트롤+d를 누르면 애니메이션이 밖으로 빠짐
밑의 Wallking(Preview란)을 올려보면
걸어감
메시 속성 창에서 Configure를 눌러 본 설정 가능
인간형 본을 재할당하여 리 타켓팅하여 사용 가능
만약 애니메이션에서 월드 포지션이 움직이는게 싫다면 Apply Root Motion을 체크 해제하면 됨
'공부 > [TIL] Game Bootcamp' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : Unity C# (0) | 2024.12.12 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : 정렬 (0) | 2024.12.11 |
[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : LINQ (6) | 2024.12.05 |
[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : 자료구조(Stack) (2) | 2024.12.04 |
[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : 자료구조(LinkedList) (1) | 2024.12.03 |