본문 바로가기

유니티(하노이의 탑 2,UI Stack, Queue 오브젝트 풀, 알고리즘1) _ 멋쟁이사자처럼 유니티 부트캠프 후기 38회차

@salmu2025. 7. 10. 16:07
반응형

[38회차] 수업내용 250710

1. 하노이의 타워

2. UI Stack

3. Queue object pooling 

4. 알고리즘 1

 

 

1. 하노이의 탑  횟수 세는 코드 추가

using System.Collections;
using TMPro; // [추가] TextMeshPro 텍스트 사용
using UnityEngine;

public class HanoiTower : MonoBehaviour
{
    public enum HanoiLevel { Lv1 = 3, Lv2, Lv3 }
    public HanoiLevel hanoiLevel;

    public GameObject[] donutPrefabs;
    public BoardBar[] bars; // L, C, R

    public TextMeshProUGUI countTextUI; //  [추가] 이동 횟수를 표시할 UI 텍스트

    public static GameObject selectedDonut;
    public static bool isSelected;
    public static BoardBar currBar;     //  도넛을 꺼낸 막대를 기억하기 위한 변수
    public static int moveCount;        // [추가] 도넛 이동 횟수 저장 변수

    IEnumerator Start()
    {
        for (int i = (int)hanoiLevel - 1; i >= 0; i--) 
        {
            GameObject donut = Instantiate(donutPrefabs[i]); 
            donut.transform.position = new Vector3(-5f, 5f, 0); 

            bars[0].PushDonut(donut); 
            
            yield return new WaitForSeconds(1f); 
        }

        moveCount = 0;                          //  [추가] 시작 시 이동 횟수 초기화
        countTextUI.text = moveCount.ToString(); //  [추가] UI에 초기 이동 횟수 표시
    }

    void Update() // 키보드 입력감지는 update 나 fixed update에서만
    {
        if (Input.GetKeyDown(KeyCode.Escape)) //  [추가] ESC 키를 눌렀을 때
        {
            currBar.barStack.Push(selectedDonut); //  선택했던 도넛을 다시 원래 막대에 올림
            isSelected = false;
            selectedDonut = null;
        }

        countTextUI.text = moveCount.ToString(); //  [추가] 이동 횟수를 실시간으로 UI에 표시
    }
}

 

 

using System.Collections.Generic;
using UnityEngine;

public class BoardBar : MonoBehaviour
{
    public enum BarType { Left, Center, Right }
    public BarType barType;
    
    public Stack<GameObject> barStack = new Stack<GameObject>();

    void OnMouseDown() 
    {
        if (!HanoiTower.isSelected) /
            HanoiTower.selectedDonut = PopDonut();
        else 
            PushDonut(HanoiTower.selectedDonut);
    }

    public bool CheckDonut(GameObject donut)
    {
        if (barStack.Count > 0)
        {
            int pushNumber = donut.GetComponent<Donut>().donutNumber;
            
            GameObject peekDonut = barStack.Peek();
            int peekNumber = peekDonut.GetComponent<Donut>().donutNumber;

            if (pushNumber < peekNumber)
                return true;
            else
            {
                Debug.Log($"현재 넣으려는 도넛은 {pushNumber}이고, 해당 기둥의 제일 위의 도넛은 {peekNumber}입니다.");
                return false;
            }
        }

        return true;
    }
    
    public void PushDonut(GameObject donut)
    {
        if (!CheckDonut(donut))
            return;

        HanoiTower.moveCount++; //  [추가] 도넛이 성공적으로 쌓일 때 이동 횟수 증가
        HanoiTower.isSelected = false;
        HanoiTower.selectedDonut = null;

        donut.transform.position = transform.position + Vector3.up * 5f;
        donut.GetComponent<Rigidbody>().linearVelocity = Vector3.zero;
        donut.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
        
        barStack.Push(donut); // Stack에 GameObject를 넣는 기능
    }

    public GameObject PopDonut()
    {
        if (barStack.Count > 0)
        {
            HanoiTower.currBar = this;   //  [추가] ESC 누를 경우 도넛을 다시 되돌릴 막대를 저장
            HanoiTower.isSelected = true; // [이동] isSelected 설정을 Pop 쪽으로 이동 (onmousedown에서)
            GameObject donut = barStack.Pop(); // Stack에서 GameObject를 꺼내는 기능

            return donut; // 꺼낸 도넛을 반환
        }

        return null; //  [추가] 비어 있으면 null 반환 (안전성 증가)
    }
}

 

 

2. UI Stack

UI들을 스택 구조(후입선출, LIFO)로 관리해 한 번에 하나씩 표시하고, 닫을 때는 최근 UI부터 하나씩 제거하는 시스템

UIManager UI 전반을 관리하는 싱글톤 매니저
Stack<UIBase> 열린 UI들을 순서대로 저장
UIBase 모든 UI 창의 공통 부모 클래스
OpenUI() UI 열기, Stack에 Push
CloseUI() UI 닫기, Stack에서 Pop

 

 

 

2.1 UI 이동

using UnityEngine;
using UnityEngine.EventSystems;

// UI를 드래그해서 이동할 수 있게 하는 스크립트
// IPointerDownHandler, IDragHandler 인터페이스 구현 필요
public class UIHandler : MonoBehaviour, IPointerDownHandler, IDragHandler
{
    private RectTransform parentRect; // 드래그 대상 UI의 부모 RectTransform

    private Vector2 basePos;   // 드래그 시작 시 부모 UI의 위치 저장 변수
    private Vector2 startPos;  // 드래그 시작 시 마우스(터치) 위치 저장 변수
    private Vector2 moveOffset; // 드래그 중 이동한 거리

    void Awake()
    {
        // 부모 오브젝트에서 RectTransform 컴포넌트 가져오기
        parentRect = transform.parent.GetComponent<RectTransform>();
    }
    
    // UI를 클릭(터치)했을 때 호출되는 함수
    public void OnPointerDown(PointerEventData eventData)
    {
        // 부모 UI를 UI 계층 상(형제들중)에서 최상위로 올려 다른 UI 위에 표시되도록 함
        parentRect.SetAsLastSibling();

        // 현재 부모 UI 위치 저장
        basePos = parentRect.anchoredPosition;

        // 클릭(터치) 위치 저장
        startPos = eventData.position;
    }

    // 마우스(터치)를 드래그할 때 호출되는 함수
    public void OnDrag(PointerEventData eventData)
    {
        // 현재 터치 위치와 드래그 시작 위치 차이 계산 (이동 거리)
        moveOffset = eventData.position - startPos;

        // 부모 UI의 위치를 드래그 시작 위치에서 이동 거리만큼 옮김
        parentRect.anchoredPosition = basePos + moveOffset;
    }
}

* 리마인드 

 

인터페이스  = 무조건 선언된 모든 메서드를 구현해주어야함

- 유니티 인터페이스

IPointerDownHandler void OnPointerDown(PointerEventData e)
IDragHandler void OnDrag(PointerEventData e)
IPointerEnterHandler void OnPointerEnter(PointerEventData e)
IPointerClickHandler void OnPointerClick(PointerEventData e)

 

 

*parentRect.SetAsLastSibling(); 

: parentRect가 형제들 중 가장 뒤(=위)에 오도록 계층 순서를 바꿔, UI가 다른 UI들보다 위에 표시되게 만드는 코드

 

 

  • Unity UI 시스템에서 마우스 클릭, 터치, 드래그 등 포인터 기반 이벤트 정보를 담는 클래스 = PointerEventData
PointerEventData 타입 이름 (클래스)
eventData 매개변수로 전달된 해당 이벤트의 인스턴스
eventData.position 클릭(터치)된 위치 (스크린 좌표)

 

[주요 속성]

position Vector2 현재 포인터 위치 (스크린 좌표)
delta Vector2 이전 프레임 대비 포인터가 움직인 거리
pressPosition Vector2 처음 클릭/터치한 위치
clickTime float 마지막 클릭한 시간
pointerEnter GameObject 마우스가 올라간 오브젝트
pointerPress GameObject 클릭 중인 오브젝트
dragging bool 현재 드래그 중인지 여부
button PointerEventData.InputButton 클릭한 마우스 버튼 (왼쪽/오른쪽/가운데)

 

 

2.2. UI Stack : 가장 나중에 킨 UI 끄기

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

public class UIStackManager : MonoBehaviour
{
    public Stack<GameObject> uiStack = new Stack<GameObject>(); 
    // 현재 열려있는 팝업 UI들을 후입선출 방식으로 관리하는 스택

    public Button[] buttons;       // 팝업을 열기 위한 버튼 배열
    public GameObject[] popupUIs;  // 각 버튼에 대응하는 팝업 UI 오브젝트 배열

    void Start()
    {
        // 각 버튼에 클릭 시 실행할 팝업 열기 함수 등록
        buttons[0].onClick.AddListener(PopupOn1);
        buttons[1].onClick.AddListener(PopupOn2);
        buttons[2].onClick.AddListener(PopupOn3);
    }

    void Update()
    {
        // ESC 키 입력 감지
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            // 스택에서 가장 마지막에 열린 팝업 UI를 꺼내서 비활성화 (닫기)
            GameObject currUI = uiStack.Pop();
            currUI.SetActive(false);
        }
    }
    
    // 버튼 0 클릭 시 호출: 첫 번째 팝업 UI 활성화 및 스택에 추가
    private void PopupOn1()
    {
        popupUIs[0].SetActive(true);
        uiStack.Push(popupUIs[0]);
    }
    
    // 버튼 1 클릭 시 호출: 두 번째 팝업 UI 활성화 및 스택에 추가
    private void PopupOn2()
    {
        popupUIs[1].SetActive(true);
        uiStack.Push(popupUIs[1]);
    }
    
    // 버튼 2 클릭 시 호출: 세 번째 팝업 UI 활성화 및 스택에 추가
    private void PopupOn3()
    {
        popupUIs[2].SetActive(true);
        uiStack.Push(popupUIs[2]);
    }
}

 

 

 

 

2.3 더블클릭 감지 / 풀스크린 기능

public class UIHandler: MonoBehaviour, IPointerDownHandler, IDragHandler
{
    // ...
    
    private Vector2 minAnchor, maxAnchor, anchorPos, deltaSize; 
    // 풀스크린 전 상태의 앵커와 위치, 크기 저장용 변수들
    // 동일한 타입 변수 여러 개를 한 줄에 선언할 때 쉼표로 구분해서 쓸 수 있음

    private float timer;                 // 더블 클릭 타이머
    private float doubleClickedTime = 0.15f; // 더블 클릭 인식 시간 간격 (0.15초)
    private bool isDoubleClicked = false;    // 더블 클릭 감지 상태 플래그
    private bool isFullScreen = false;        // 현재 UI가 풀스크린 상태인지 여부
    
    void Update()
    {
        if (isDoubleClicked) // 더블 클릭 감지 중일 때
        {
            timer += Time.deltaTime;  // 시간 누적
            if (timer >= doubleClickedTime) // 지정 시간 지나면 더블 클릭 상태 초기화
            {
                timer = 0f;
                isDoubleClicked = false;
            }
        }
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        if (!isDoubleClicked)       // 첫 번째 클릭이면 더블 클릭 상태로 설정
            isDoubleClicked = true;
        else                       // 두 번째 클릭이면 풀스크린 토글 함수 호출
            SetFullScreen();

        if (isFullScreen)           // 풀스크린 상태면 이후 처리 중단
            return;
        
        // ...
    }
    
    // ...
    
    private void SetFullScreen()
    {
        if (!isFullScreen) // 현재 풀스크린이 아니면 풀스크린으로 전환
        {
            // 현재 UI 앵커와 위치, 크기 저장
            minAnchor = parentRect.anchorMin;
            maxAnchor = parentRect.anchorMax;
            anchorPos = parentRect.anchoredPosition;
            deltaSize = parentRect.sizeDelta;
            
            // 앵커를 화면 전체로 맞추고 위치/크기를 초기화하여 풀스크린 모드 설정
            //(0,0)은 부모의 왼쪽 아래 (1,1)은 부모의 오른쪽 위 = 부모 영역 전체 
            parentRect.anchorMin = Vector2.zero;
            parentRect.anchorMax = Vector2.one;
            parentRect.anchoredPosition = Vector2.zero;
            parentRect.sizeDelta = Vector2.zero;
        }
        else // 풀스크린 상태면 이전 상태로 복원
        {
            parentRect.anchorMin = minAnchor;
            parentRect.anchorMax = maxAnchor;
            parentRect.anchoredPosition = anchorPos;
            parentRect.sizeDelta = deltaSize;
        }

        isFullScreen = !isFullScreen; // 상태 토글
    }
}

 

 

 

3. Queue Object pool

 

3.1 Scene 뷰 Gizmo 아이콘 

 

  • 크기 고정(3D 씬 내 크기와 무관), Scene에서 오브젝트를 쉽게 찾을 수 있음
  • Gizmo 토글이 꺼져 있으면 Scene 뷰에서 안 보임
  • GameView에는 보이지 않고, SceneView에서만 보임

 

3.2  오브젝트 풀링 

: 오브젝트를 필요할 때마다 만들고 없애는 대신, 미리 일정 개수를 만들어두고 필요할 때마다 '빌려 쓰고' 다 쓴 다음에는 '반납'하는 방식

: Instantiate (생성)와 Destroy (파괴) 연산에서 발생하는 성능 저하를 크게 줄임

: 총알, 적, 파티클 효과 등과 같이 자주 생성되고 파괴되는 오브젝트들에 유용

using System.Collections.Generic;
using UnityEngine;

// 오브젝트 풀링을 Queue(큐)로 관리하는 클래스
public class ObjectPoolQueue : MonoBehaviour
{
    public Queue<GameObject> objQueue = new Queue<GameObject>(); 
    // 미리 생성한 오브젝트들을 보관할 큐 (FIFO 구조)

    public GameObject objPrefab; // 생성할 오브젝트의 원형(Prefab)
    public Transform parent;     // 생성된 오브젝트들의 부모 Transform (계층 정리용)

    void Start()
    {
        CreateObject(); // 시작 시 오브젝트들을 미리 생성해서 풀링
    }

    private void CreateObject() // 오브젝트 풀을 채우는 함수
    {
        for (int i = 0; i < 100; i++) // 총 100개 생성
        {
            // objPrefab을 parent의 자식으로 생성
            GameObject obj = Instantiate(objPrefab, parent); 
              // Instantiate(GameObject original, Vector3 position, Quaternion rotation, Transform parent);

            EnqueueObject(obj); // 생성한 오브젝트를 풀에 집어넣음
        }
    }

    // 오브젝트를 큐에 다시 집어넣는 함수 (반환할 때 사용)
    public void EnqueueObject(GameObject newObj)
    {
        // 물리 속도 초기화 (움직임/회전 멈춤)
        newObj.GetComponent<Rigidbody>().linearVelocity = Vector3.zero;
        newObj.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;

        objQueue.Enqueue(newObj); // 큐에 넣기
        newObj.SetActive(false);  // 비활성화하여 화면에서 안 보이게
    }

    // 큐에서 오브젝트를 꺼내 쓰는 함수
    public GameObject DequeueObject()
    {
        GameObject obj = objQueue.Dequeue(); // 큐에서 맨 앞 오브젝트 꺼내기
        obj.SetActive(true);                 // 활성화해서 화면에 보이게

        return obj; // 꺼낸 오브젝트 반환
    }
}

 

*복습
Instantiate(prefab, parentTransform, false); 

으로 하면 부모에 붙으면서도 월드 좌표 유지. / true면 이동

 

 

 

3.2.1 총알 발사

using UnityEngine;

// 총알 발사를 제어하는 컨트롤러
public class ObjectPoolController : MonoBehaviour
{
    public ObjectPoolQueue pool;   // 오브젝트 풀링 시스템 (총알 풀)
    public Transform shootPos;     // 총알이 발사될 위치 (총구)

    void Update()
    {
        // 마우스 왼쪽 버튼 클릭 시
        if (Input.GetMouseButtonDown(0))
        {
            // 풀에서 총알 하나 꺼내오기
            GameObject bullet = pool.DequeueObject();

            // 총알의 위치를 총구 위치로 설정
            bullet.transform.position = shootPos.position;
        }
    }
}

 

 

using UnityEngine;

// 풀에서 나온 오브젝트(총알)에 적용되는 동작 스크립트
public class PoolObject : MonoBehaviour
{
    private ObjectPoolQueue pool; // 자신을 다시 반환할 오브젝트 풀 참조

    public float bulletSpeed = 100f; // 총알 속도

    void Awake()
    {
        // 씬 내에서 ObjectPoolQueue를 찾아서 참조
        pool = FindFirstObjectByType<ObjectPoolQueue>();
    }

    void OnEnable()
    {
        // 오브젝트가 활성화될 때 3초 뒤에 ReturnPool() 실행 예약
        Invoke("ReturnPool", 3f);
    }

    void Update()
    {
        // 매 프레임마다 앞으로 이동 (Vector3.forward는 z축 기준)
        transform.position += Vector3.forward * Time.deltaTime * bulletSpeed;
    }

    // 풀로 총알 반환
    private void ReturnPool()
    {
        pool.EnqueueObject(gameObject);
    }
}

 

 

4. 알고리즘 

 

4.1 BIG-O (빅오 표기법)

:알고리즘의 시간 복잡도(연산 횟수 증가율)를 표현하는 표기법

:입력 크기(N)가 커질 때 성능이 어떻게 변하는지 근사적으로 나타낸 것

 

  • 코드나 알고리즘의 성능과 효율성 분석
  • 하드웨어와 무관하게 이론적 최대 성능 예측
O(1) 상수 시간 배열에서 인덱스로 바로 접근
O(log N) 로그 시간 이진 탐색
O(N) 선형 시간 단순 반복문
O(N log N) 로그 곱 선형 퀵 정렬
O(N²) 이차 시간 이중 반복문

 

 

 

4.1.1  O(1) 인덱서 

: 배열의 특정 인덱스에 접근

 

int[] numbers = { 1, 2, 3, 4, 5 };
int firstNumber = numbers[0];

 

 

4.1.2 O(n) - Fisher–Yates Shuffle(피셔-예이츠 셔플)

: 단순 반복문 -배열의 모든 요소를 정확히 한 번씩 총 n번 스왑

 

  • 진짜 공정한 랜덤 셔플 방식
  • 매우 빠르고 간단하며, 전 세계 카드 게임, 보드게임, 컴퓨터 게임의 표준 알고리즘
private void FisherYatesShuffle()
{
    for (int i = array.Length - 1; i > 0; i--)
    {
        int j = Random.Range(0, i + 1); // 0부터 i까지
        Swap(i, j);
    }
}

public void Swap(int i, int j)
{
    var temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

 

*실습에서는 100번 swap했으나 피셔예이츠는 차례대로 n번 

 

 

 

4.1.3  O(2^n) : 피보나치 수열 

using UnityEngine;

// 피보나치 수열 계산 예제 (재귀 방식)
public class Fibonacci : MonoBehaviour
{
    void Start()
    {
        // 0부터 9까지의 피보나치 수를 계산하고 출력
        for (int i = 0; i < 10; i++)
        {
            int result = FibonacciFunction(i);
            Debug.Log(result);
        }
    }

    // 피보나치 수열의 n번째 값을 재귀적으로 구하는 함수
    private int FibonacciFunction(int n)
    {
        // n이 0 또는 1이면 그대로 반환 (기저 조건)
        if (n <= 1)
            return n;

        // n번째 값은 n-1번째 + n-2번째 값의 합
        // 이로 인해 재귀 호출이 계속 분기됨
        return FibonacciFunction(n - 1) + FibonacciFunction(n - 2);
    }
}

 

F(4)
├─ F(3)
│  ├─ F(2)
│  │  ├─ F(1)
│  │  └─ F(0)
│  └─ F(1)
└─ F(2)
   ├─ F(1)
   └─ F(0)

 

 

 

 

 

4.1.4 O(n) 팩토리얼 

using UnityEngine;

// 팩토리얼 계산 예제 (재귀 방식)
public class Factorial : MonoBehaviour
{
    void Start()
    {
        // 0부터 9까지의 팩토리얼 값을 계산하고 출력
        for (int i = 0; i < 10; i++)
        {
            int result = FactorialFunction(i);
            Debug.Log(result);
        }
    }

    // n의 팩토리얼(n!)을 재귀적으로 계산하는 함수
    private int FactorialFunction(int n)
    {
        // 기저 조건: 0!은 1
        if (n == 0)
            return 1;
        else
            // n! = n * (n-1)!
            return n * FactorialFunction(n - 1);
    }
}

 

  • 재귀가 n번 호출되기 때문에 선형 시간 복잡도

 

 

4.1.5 하노이의 타워 재귀 = O(2ⁿ)

// 하노이의 탑 문제를 해결하는 메인 함수
public void HanoiAnswer()
{
    // hanoiLevel 높이의 탑을 0번 기둥에서 2번 기둥으로 옮김 (1번은 보조)
    HanoiRoutine((int)hanoiLevel, 0, 1, 2);
}

// 재귀적으로 하노이의 탑을 푸는 함수
private void HanoiRoutine(int n, int from, int temp, int to)
{
    // 원판이 1개 남으면 바로 목적지로 이동
    if (n == 1)
        Debug.Log($"{n}번 도넛을 {from}에서 {to}로 이동");
    else
    {
        // 1단계: n-1개의 원판을 보조 기둥(temp)으로 이동
        HanoiRoutine(n - 1, from, to, temp);

        // 2단계: 가장 큰 원판(n)을 목적지로 이동
        Debug.Log($"{n}번 도넛을 {from}에서 {to}로 이동");

        // 3단계: 보조 기둥에 있던 n-1개의 원판을 목적지로 이동
        HanoiRoutine(n - 1, temp, from, to);
    }
}

 

 

 

4.1.6 O(n!) 순열

using System.Collections.Generic;
using UnityEngine;

public class StudyAlgorithm : MonoBehaviour
{
    // 숫자 리스트 초기화 (순열을 만들 숫자들)
    private List<int> nums = new List<int> { 1, 2, 3 };

    void Start()
    {
        // 순열 만들기 시작 (0번째 인덱스부터 시작)
        Permute(nums, 0);
    }

    // 순열을 만드는 함수
    void Permute(List<int> nums, int start)
    {
        // start가 리스트의 길이와 같으면 순열이 완성된 상태
        if (start == nums.Count)
        {
            // 리스트의 숫자들을 쉼표와 공백(", ")으로 구분한 문자열로 변환해서 출력
            // 예: nums = [1, 2, 3] 이면 "1, 2, 3" 출력
            Debug.Log(string.Join(", ", nums));
            return;
        }

        // start부터 끝까지 i를 돌면서 숫자를 교환
        for (int i = start; i < nums.Count; i++)
        {
            // start 위치와 i 위치의 숫자를 교환 (swap)
            int temp = nums[start];
            nums[start] = nums[i];
            nums[i] = temp;

            // 다음 위치(start + 1)에 대해 다시 순열 생성
            Permute(nums, start + 1);

            // 순열 생성이 끝난 뒤 숫자를 원래대로 돌려놓음 (백트래킹)
            temp = nums[start];
            nums[start] = nums[i];
            nums[i] = temp;
        }
    }
}

//1, 2, 3
//1, 3, 2
//2, 1, 3
//2, 3, 1
//3, 2, 1
//3, 1, 2

 

 

<적어보자>

1,2,3

Start 0
	i=0  1,2,3 -permute start = 1.
		i=1  1,2,3 permute start =2 
			i= 2 1,2,3, permute start =3 -> {1,2,3} 출력 
		i=2	1,3,2 permute start =2 
			i=2 1,3,2 permute start =3 -> {1,3,2} 출력
	i=1  2, 1, 3 permute start =1
		i=1 2,1,3, permute start = 2 
			i=2 2,1,3 , permute start=3 -> {2,1,3}출력 
		i=2 2,3,1  permute start =2 
			i=2 2,3,1 , permute start=3 -> {2,3,1} 출력
	i=2 3,2,1  permute start = 1 
		i=1 3,2,1 permute start =2 
			i=2 3,2,1 permute start=3 -> {3,2,1} 출력
		i=2 3,1,2, permute start =2 
			i=2 3,1,2 permute start=3 -> {3,1,2}출력

 

 

 

반응형
salmu
@salmu :: SMU 각종 기록

목차