목차
강사님의 꿀팁
>> Object의 역할에 따라 구분짓는 것이 중요하다
: 아트, 기획, 프로그래밍을 각각 담당자를 정해서 나누는 것처럼
>> Scene과 Scene 사이의 연동은 후반에 구현하는 것이 좋다.
: 즉, Scene 하나만으로 구현이 되면 좋다.
퀴즈 게임 만들기
25.02.13
카드 전환 구현
: GamePanelController.cs에 구현
>> 임시로 버튼을 배치하여 카드를 전환하도록
※ 나중에 Animation 추가해보자
QuizCardController 관련
>> 대략적인 흐름
- GamePanelController에서 퀴즈 정보와 delegate(onCompleted)를 QuizCardController에게 전달
- 퀴즈 정보는 Quiz file(.csv)이 가지고 있다.
>> delegate와 event
event : 외부에서 delegate를 호출하지 못하게, null을 할당하지 못하게 만들어준다.
>> Struct vs Class
- 참조 타입의 Class는 데이터가 있는 위치를 가르키기 때문에 어디서 참조하든 같은 값을 가진다.
- 값 타입의 Struct는 복사하여 만드는 것이기 때문에 복사한 뒤, 실제 데이터가 변하면 실제와 복사한 것은 서로 다른 값을 가지게 된다.
>> 그럼 QuizData를 왜 Struct로 만들었는가?
: Quiz를 참조 형태로 전달 받으면 Quiz의 Data가 변할 가능성이 있기 때문에, 전달 받은 시점에서 그 상태 그대로 변하지 않도록 유지하기 위해 Struct를 사용한다.
※ 다른 수강생님의 답변
: 깊게 들어가면 구조체는 스택에 저장되고 클래스는 힙에 저장되는데 스택은 할당이 빠르고 가비지 컬렉션의 영향을 적게 받습니다. 큰 데이터 혹은 데이터 공유와 참조가 필요하면 클래스를 사용하는게 더 좋습니다. 구조체로 만들더라도 내부 값이 참조값인 배열과 같은게 잔뜩 들어 있다면 불변성은 보장되지 않아요
CSV파일로 퀴즈 만들기
※ Rons Data Edit 설치
https://www.ronsplace.ca/products/ronsdataedit
Professional CSV Editor - Rons Data Edit
Professional modern CSV file editor for editing files in any tabular text format, combining elegance, power and ease of use.
www.ronsplace.ca
>> 새 파일 만들기
>> Column 이름 변경
>> 내용 추가 및 저장
--> 'quiz-data' 로 저장...했었지만 이후 파일 이름 수정함 ('QuizData-0')
※ Visual code로 csv파일을 열면 Rainbow CSV 설치
--> 뜨는 팝업창을 눌러 설치 가능하다.
※ 혹시 팝업창을 껐다면 Extension에서 설치 가능
>> 적용된 모습
>> Rons Data Edit의 특징
: Column의 타입을 지정 가능
Quiz Data 추가
: QuizDataController.cs 생성 및 QuizData-0.csv 파일 추가
※ RegularExpressions (Regex) --> 정규표현식 (정규식)
>> 설명해놓은 사이트 (강사님이 알려주심)
https://learn.microsoft.com/ko-kr/dotnet/standard/base-types/regular-expressions
.NET 정규식 - .NET
.NET에서 정규식을 사용하여 특정 문자 패턴을 찾고, 텍스트의 유효성을 검사하고, 텍스트 부분 문자열로 작업하고, 추출된 문자열을 컬렉션에 추가합니다.
learn.microsoft.com
>> 정규식 생성기 (다른 수강생님의 정보)
https://regex-generator.olafneumann.org/
Regex Generator - Creating regex is easy again!
regex-generator.olafneumann.org
>> 하나의 Quiz Data 파일에 모든 Quiz가 포함되어 있을 필요는 없다.
: 이미 푼 문제도 있을테고 문제를 푸는 시간이 한정적이므로 모든 문제를 불러올 필요가 없다.
--> Quiz Data 파일을 여러 개로 나누고 Stage에 맞는 파일만 불러오기
--> 정한 파일 이름 형식대로 이름 변경
>> 테스트 중 자꾸 발생했던 오류
해결법 : QuizData-0.csv 파일의 ',,,,' 을 지우자 (혹시 마지막 줄에 빈 여백이 있다면 그것도 지우자)
>> 실제 파일의 이름도 변경해주고, 실제 파일에서 변경사항이 있었다면 기존 파일을 지우고 다시 추가하자!
Quiz Card 구성
>> Text 추가
>> Button 추가
: 빈 오브젝트로 Buttons를 만들어서 부모 오브젝트로 변경
--> Anchor는 Alt + Shift
--> Button들도 Width, Height 조정 및 Button의 Text도 중앙 정렬
>> CardController.cs에 바인딩
: Button을 바인딩 할 때 순서가 중요하다 --> Quiz의 정답에 대한 것을 Index로 담고 있기 때문
--> Description은 뒤에 나올 카드에서 바인딩 할 예정 (잠시 비워둠)
활동
>> 10개의 퀴즈 카드를 순서대로 보여주는 코드 작성
--> QuizData-1.csv 파일을 읽어서 10개의 퀴즈를 화면에 순서대로 표시하기 (강사님과 함께 작성함)
>> 폰트 설정하기
: 47일차 블로그 글 참고 (나눔고딕 볼드체 사용)
--> 적용된 모습
최종 코드
>> ObjectPool.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
[SerializeField] private int poolSize;
[SerializeField] private Transform parent; // Object Pool이 생성될 Parent
private Queue<GameObject> _pool;
private static ObjectPool _instance; // Singleton을 상속받지 않고 패턴만 사용
public static ObjectPool Instance
{
get { return _instance; }
}
private void Awake()
{
_instance = this;
_pool = new Queue<GameObject>();
}
/// <summary>
/// Object Pool에 새로운 Object 생성 Method
/// </summary>
private void CreateNewObject()
{
GameObject newObject = Instantiate(prefab, parent);
newObject.SetActive(false);
_pool.Enqueue(newObject);
}
/// <summary>
/// Object Pool에 있는 Object를 반환하는 Method
/// </summary>
/// <returns>Object Pool에 있는 Object</returns>
public GameObject GetObject()
{
if (_pool.Count == 0) CreateNewObject();
GameObject obj = _pool.Dequeue();
obj.SetActive(true);
return obj;
}
/// <summary>
/// 사용한 Object를 Object Pool로 되돌려 주는 Method
/// </summary>
/// <param name="obj">반환할 Object</param>
public void ReturnObject(GameObject obj)
{
obj.SetActive(false);
_pool.Enqueue(obj);
}
}
--> Awake()에서 poolSize만큼 반복하여 CreateNewObject() 해주던 것 삭제
>> GamePanelController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GamePanelController : MonoBehaviour
{
private GameObject _firstQuizCardObject;
private GameObject _secondQuizCardObject;
private List<QuizData> _quizDataList;
private int _lastGeneratedQuizIndex;
private const int MAX_QUIZ_COUNT = 10;
private void Start()
{
_quizDataList = QuizDataController.LoadQuizData(0); // 테스트 코드
InitQuizCard();
}
private void InitQuizCard()
{
_firstQuizCardObject = ObjectPool.Instance.GetObject();
_firstQuizCardObject.GetComponent<QuizCardController>().SetQuiz(_quizDataList[0], OnCompletedQuiz);
_secondQuizCardObject = ObjectPool.Instance.GetObject();
_secondQuizCardObject.GetComponent<QuizCardController>().SetQuiz(_quizDataList[1], OnCompletedQuiz);
//var thirdCardObject = ObjectPool.Instance.GetObject();
_secondQuizCardObject.GetComponent<Image>().color = Color.gray;
//thirdCardObject.GetComponent<Image>().color = Color.black;
SetQuizCardPosition(_firstQuizCardObject, 0);
SetQuizCardPosition(_secondQuizCardObject, 1);
// 마지막으로 생성된 Quiz Index
_lastGeneratedQuizIndex = 1;
}
private void OnCompletedQuiz(int cardIndex)
{
}
private void SetQuizCardPosition(GameObject quizCardObject, int index)
{
var quizCardTransform = quizCardObject.GetComponent<RectTransform>();
if (index == 0)
{
quizCardTransform.anchoredPosition = new Vector2(0, 0);
quizCardTransform.localScale = Vector3.one;
quizCardTransform.SetAsLastSibling(); // 같은 depth에서 마지막으로 이동 --> 카드가 앞으로 배치됨
}
else if (index == 1)
{
quizCardTransform.anchoredPosition = new Vector2(0, 160);
quizCardTransform.localScale = Vector3.one * 0.9f;
quizCardTransform.SetAsFirstSibling(); // 같은 depth에서 처음으로 이동
}
}
private void ChangeQuizCard()
{
if (_lastGeneratedQuizIndex >= MAX_QUIZ_COUNT) return;
var temp = _firstQuizCardObject;
_firstQuizCardObject = _secondQuizCardObject;
_secondQuizCardObject = ObjectPool.Instance.GetObject();
if (_lastGeneratedQuizIndex < _quizDataList.Count - 1)
{
_lastGeneratedQuizIndex++;
_secondQuizCardObject.GetComponent<QuizCardController>()
.SetQuiz(_quizDataList[_lastGeneratedQuizIndex], OnCompletedQuiz);
}
SetQuizCardPosition(_firstQuizCardObject, 0);
SetQuizCardPosition(_secondQuizCardObject, 1);
ObjectPool.Instance.ReturnObject(temp);
}
public void OnClickNextButton()
{
ChangeQuizCard();
}
}
>> QuizCardController.cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public struct QuizData
{
public string question;
public string description;
public int type;
public int answer;
public string firstOption; // 원래는 string[] Options로 했었다
public string secondOption;
public string thirdOption;
}
public class QuizCardController : MonoBehaviour
{
[SerializeField] private TMP_Text questionText; // 퀴즈
[SerializeField] private TMP_Text descriptionText; // 설명
[SerializeField] private Button[] optionButtons; // 보기 --> 타입을 TMP_Text로 해서 text를 직접 받아도 된다.
public delegate void QuizCardDelegate(int cardIndex);
private event QuizCardDelegate onCompleted;
public void SetQuiz(QuizData quizData, QuizCardDelegate onCompleted)
{
// 1. 퀴즈
// 2. 설명
// 3. 타입 (0: OX퀴즈, 1: 보기 3개 객관식)
// 4. 정답
// 5. 보기 (1, 2, 3)
// 퀴즈 데이터 표현
questionText.text = quizData.question;
//descriptionText.text = quizData.description;
var firstButtonText = optionButtons[0].GetComponentInChildren<TMP_Text>();
firstButtonText.text = quizData.firstOption;
var secondButtonText = optionButtons[1].GetComponentInChildren<TMP_Text>();
secondButtonText.text = quizData.secondOption;
var thirdButtonText = optionButtons[2].GetComponentInChildren<TMP_Text>();
thirdButtonText.text = quizData.thirdOption;
this.onCompleted = onCompleted;
}
}
>> QuizDataController.cs
using System.Collections.Generic;
using UnityEngine;
using System.Text.RegularExpressions; // Regex를 사용하기 위해 선언
public static class QuizDataController
{
static string ROW_SEPARATOR = @"\r\n|\n\r|\n|\r";
static string COL_SEPARATOR = @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))";
private static char[] TRIM_CHARS = { '\"' }; // Trim : 특정한 문자 제거
public static List<QuizData> LoadQuizData(int stageIndex)
{
// 퀴즈를 스테이지별로 나누기 위해 불러오는 quizData 파일의 이름 형식을 고정
var fileName = "QuizData-" + stageIndex;
// Resources.Load()는 Object 타입으로 반환하기 때문에 'as'로 형변환
TextAsset quizDataAsset = Resources.Load(fileName) as TextAsset;
var lines = Regex.Split(quizDataAsset.text, ROW_SEPARATOR);
var quizDataList = new List<QuizData>();
for (var i = 1; i < lines.Length; i++)
{
var values = Regex.Split(lines[i], COL_SEPARATOR);
QuizData quizData = new QuizData();
for (var j = 0; j < values.Length; j++)
{
var value = values[j];
// value의 시작(TrimStart)과 끝(TrimEnd)에 " 가 있으면 잘라주고 "\\"는 ""로 바꿔준다.
value = value.TrimStart(TRIM_CHARS).TrimEnd(TRIM_CHARS).Replace("\\", "");
switch (j)
{
case 0:
quizData.question = value;
break;
case 1:
quizData.description = value;
break;
case 2:
quizData.type = int.Parse(value); // int.Parse()를 통해 int로 변환
break;
case 3:
quizData.answer = int.Parse(value);
break;
case 4:
quizData.firstOption = value;
break;
case 5:
quizData.secondOption = value;
break;
case 6:
quizData.thirdOption = value;
break;
}
}
quizDataList.Add(quizData);
}
return quizDataList;
}
}
스스로 해보기
1. Animation 추가
2. Card 색 변경
C# 단기 교육 보강
8일차
Hanoi Tower
※ 자료구조 로직을 활용해서 만드는 것에 집중하고, 세부적인 예외처리나 UI는 만들지 않음
: 나중에 개인적으로 공부하면서 따로 완성해보자
--> 현재 하드코딩으로 구현 중이니 나중에 Pattern, Action, Event, Lambda, Callback 등을 활용해서 만들어보자
>> 3개의 Bar를 Board의 자식 오브젝트로 이동 후, Board를 Prefab화
>> MainCamera의 Position 변경
└ Donut
>> Shape의 Mesh Collider 삭제
: Mesh Collider와 Rigidbody가 같이 호환이 안 되기 때문에 --> Cube로 Collider 대체
>> Donut의 자식으로 Cube를 4개 만들어서 Donut을 감싸도록 만든 후, Cube의 Mesh Renderer를 꺼줌
※ Donut의 색상도 입혀줌
>> 이후 Donut의 변경사항을 Prefab에 저장 (Override)
--> 이후 Hierarchy에서 Donut 삭제
└ GameManager
>> 빈 게임 오브젝트로 Game Manager 생성 후, GameManager.cs 추가하고 바인딩
: GameManager.cs는 namespace 활용
--> GameManager라는 이름이 많이 쓰이므로 namespace로 구분 (사실 지금 필수는 아님)
>>Stack을 활용하여 Donut 옮기기 구현
└ Bar
1. 3개의 Bar에 모두 DonutBar.cs 추가 각 Bar에 맞게 Type 설정
2. 3개의 Bar에 모두 Capsule Collider를 하나 더 추가하고 아래 Collider에는 Trigger 체크 및 사이즈 조정
└ 오류 수정
: Hanoi Tower는 더 큰 원반이 작은 원반 위에 올라갈 수 없으므로 이를 수정
>> Donut.cs 생성하여 Donut에 추가
└ 최종 코드
>> GameManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Hanoi_Tower
{
public class GameManager : MonoBehaviour
{
[SerializeField] private GameObject donutPrefab;
public enum HanoiLevel { LV1 = 3, LV2 = 4, LV3 = 5 } // 생성되는 Donut 개수
public HanoiLevel hanoiLevel;
public DonutBar[] donutBars;
public static bool isSelected = false;
// Unity Inspector에서 보기 위해 억지로 하드코딩 --> 실제로는 쓰지 말자
public List<GameObject> leftBar = new();
public List<GameObject> centerBar = new();
public List<GameObject> rightBar = new();
public GameObject selectedDonut;
IEnumerator Start()
{
for (int i = (int)hanoiLevel; i >= 1; i--)
{
// 도넛 생성 위치
// Board의 X의 Scale이 10이라서 (int)DonutBar.BarType.LEFT에 0.1f를 곱할 필요가 없다.
Vector3 createPos = new Vector3((int)DonutBar.BarType.LEFT, 3.5f, 0f);
GameObject donutObj = Instantiate(donutPrefab, createPos, Quaternion.identity); // 도넛 생성
donutObj.name = "Donut_" + i; // 도넛 이름 설정
donutObj.transform.localScale = Vector3.one * (i * 0.3f + 1f); // 도넛 크기 설정
donutObj.GetComponent<Donut>().donutNumber = i; // 도넛에 번호 부여
donutBars[0].PushDonut(donutObj, true); // 생성한 Donut을 LeftBar에 Push (index 0 == left)
yield return new WaitForSeconds(1f);
}
}
}
}
>> DonutBar.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Hanoi_Tower; // namespace를 사용하기 위해 선언
public class DonutBar : MonoBehaviour
{
// position.x 값인데, enum은 int이므로 10배 크게 작성
public enum BarType { LEFT = -3, CENTER = 0, RIGHT = 3 }
public BarType barType;
public Stack<GameObject> stack = new();
// Unity Inspector에서 보기 위해 억지로 하드코딩 --> 실제로는 쓰지 말자
public GameManager gameManager;
void OnMouseDown()
{
if (!GameManager.isSelected) // 도넛을 가져올 기둥을 선택
{
gameManager.selectedDonut = PopDonut();
GameManager.isSelected = true;
}
else // 도넛을 넣을 기둥 선택
{
PushDonut(gameManager.selectedDonut, false);
}
}
public void PushDonut(GameObject donut, bool isInit)
{
if (!isInit) // 이것도 완전 하드코딩
{
if (stack.Count > 0)
{
var peekNumber = stack.Peek().GetComponent<Donut>().donutNumber;
var pushNumber = donut.GetComponent<Donut>().donutNumber;
if (pushNumber < peekNumber) // 큰 도넛이 작은 도넛 위에 올라가는 것 방지
{
GameManager.isSelected = false;
stack.Push(donut);
donut.transform.position = new Vector3((int)barType, 3.5f, 0f);
// Unity Inspector에서 보기 위해 억지로 하드코딩 --> 실제로는 쓰지 말자
switch (barType)
{
case BarType.LEFT:
gameManager.leftBar = stack.ToList();
break;
case BarType.CENTER:
gameManager.centerBar = stack.ToList();
break;
case BarType.RIGHT:
gameManager.rightBar = stack.ToList();
break;
}
}
else
{
Debug.Log($"놓으려는 도넛은 {pushNumber}이고, 해당 기둥의 도넛은 {peekNumber}입니다.");
}
}
else
{
GameManager.isSelected = false;
stack.Push(donut);
donut.transform.position = new Vector3((int)barType, 3.5f, 0f);
// Unity Inspector에서 보기 위해 억지로 하드코딩 --> 실제로는 쓰지 말자
switch (barType)
{
case BarType.LEFT:
gameManager.leftBar = stack.ToList();
break;
case BarType.CENTER:
gameManager.centerBar = stack.ToList();
break;
case BarType.RIGHT:
gameManager.rightBar = stack.ToList();
break;
}
}
}
else
{
stack.Push(donut);
donut.transform.position = new Vector3((int)barType, 3.5f, 0f);
switch (barType) // 하드코딩
{
case BarType.LEFT:
gameManager.leftBar = stack.ToList();
break;
case BarType.CENTER:
gameManager.centerBar = stack.ToList();
break;
case BarType.RIGHT:
gameManager.rightBar = stack.ToList();
break;
}
}
}
public GameObject PopDonut()
{
if (stack.Count > 0) return stack.Pop();
return null;
}
}
>> Donut.cs
using UnityEngine;
public class Donut : MonoBehaviour
{
public int donutNumber;
}
알고리즘
>> Big-O
: 코드의 성능 분석
>> 복잡도 비교
>> 선형 구조와 비선형 구조
└ 그래프 (Graph)
: 정점과 간선으로 구성된 자료구조
>> 깊이 우선 탐색(DFS)과 너비 우선 탐색(BFS)
└ 트리 (Tree)
: 정점과 간선으로 계층적 관계를 표현하는 자료구조
>> 트리의 순회
└ 재귀 함수 (Recursion)
: 자기 자신을 다시 호출하는 함수 --> 반복된 로직에 사용하면 좋다.
>> Hanoi Tower의 재귀
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 56일차 (0) | 2025.02.17 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 55일차 (0) | 2025.02.14 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 53일차 (0) | 2025.02.12 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 52일차 (0) | 2025.02.11 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 51일차 (0) | 2025.02.10 |