목차
퀴즈 게임 만들기
25.02.17
타이머 만들기
>> Head Cap, Tail Cap 생성
: Image로 생성
--> Anchor는 Alt + Shift
>> Timer의 Width, Height 조정
>> Fill Image 수정
Clockwise : Image를 시계 방향으로 차오르게 할 지(체크), 반시계 방향으로 차오르게 할 지(체크 해제)
>> Mobics Timer.cs에 HeadCap, TailCap 바인딩
>> Timer Prefab화
: 이후 Hierarchy에서 Timer 제거
>> GamePanel의 GamePanelController를 다시 활성화
>> Quiz Card Prefab에 Timer 추가
--> Anchor는 Alt + Shift
>> Timer의 색 변경
- Fill Image, Head Cap, Tail Cap, Time Text : (44, 55, 89, 255)
- Background Image (242, 242, 242, 255)
>> Timer에 Time Text 바인딩
※ Prefab이 아닌 Hierarchy에서 수정한 값을 Prefab에 개별적으로 적용하기
활동
- 타이머를 Quiz Card에 추가
- 10초 동안 퀴즈를 못 풀면 오답 처리
- 현재 진행 중인 퀴즈의 타이머만 동작하게 구현
- 타이머가 끝나기 전에 퀴즈를 풀었을 때 타이머 처리
>> 구현
: 대부분 코드로 구현, QuizCardController.cs에 Timer 바인딩
Heart 구현
: Game Panel의 자식으로, 빈 게임 오브젝트를 생성
--> 이름을 HeartPanel로 수정하고 Width 수정하고 HeartPanelController.cs 추가
>> Heart Image, Heart Count 생성
: Heart Panel의 자식으로, Heart Image는 Image로, Heart Count Text (TMP)는 Text로 생성
--> Anchor는 Alt + Shift
--> 'Heart Count'를 'Heart Count Text (TMP)'로 이름 변경
└ 하트 감소 구현
: Heart Image의 자식으로 Image를 'Heart Remove Image'라는 이름으로 생성
>> HeartPanelController.cs에 바인딩
: Heart Remove Image, Heart Count Text 바인딩
>> 하트 수의 자릿수에 따라 텍스트 위치 조정
: 하트 수가 1자리 수일 때와 2자리 수일 때가 Text의 간격이 다르기 때문에 이를 조정
--> HeartPanel의 Width를 글자 수에 따라 변경
--> HeartPanel의 Width가 한 자리 수일 때는 130, 두 자리 수 일 때는 160이 적당하다
--> HeartCountText의 Width가 한 자리 수일 때는 31 정도, 두 자리 수 일 때는 61 정도가 적당하다
--> 코드로 이를 조절
※ 'Content Size Filter' 라는 Component를 추가해서 조정하는 방법도 있음
: Heart Count Text에 추가 --> 이 방법 사용
>> Test용 버튼 추가
: 'Canvas'의 자식으로 빈 게임 오브젝트로 Test Buttons 추가 및 Vertical Layout Group 추가
--> PosX, PosY, Width, Height는 임의로 조정한 것
>> Test Buttons의 자식으로 Button 추가
: Width, Height 조정, Text 수정
--> Button 3개 전부 동일하게 적용
--> Text는 각각 'Remove Heart', 'Add Heart', 'Empty Heart'
>> 각 Button의 OnClick()에 함수 바인딩
--> 나머지 버튼에도 각각 'AddHeart(int)', 'EmptyHeart()' 바인딩
--> AddHeart(int)의 int값은 상관없지만 일단 5로 설정
└ 하트 증가 구현
: 하트 감소 코드와 거의 동일하기 때문에 하트 감소 코드를 따로 빼서 ChangeTextAnimation()으로 만듦
※ DOTween.Sequence()
: 여러 복잡한 애니메이션을 순차적으로 적용
└ 하트가 없을 때 구현
: DOPunchPosition을 이용하여 하트 수가 흔들리는 Animation 추가
└ Heart Sound 추가
※ Asset 추가
https://assetstore.unity.com/packages/audio/sound-fx/free-casual-game-sfx-pack-54116
FREE Casual Game SFX Pack | 음향 효과음 | Unity Asset Store
Layer in the sounds of FREE Casual Game SFX Pack from Dustyroom for your next project. Browse all audio options on the Unity Asset Store.
assetstore.unity.com
>> 각 동작에 맞는 효과음 바인딩
>> 유저의 설정값에 따라 효과음을 재생하도록 구현
>> Heart Panel Prefab화 후, Hierarchy에서 제거
: TestButtons에 있는 Button들의 OnClick()이 전부 해제됐을 것, 테스트할 때 사용하려면 다시 연결해주어야 한다.
활동
: Heart Panel을 Quiz Card에 적용하기
└ 활동 해보기
1. Quiz Card Prefab에 Incorrect Back Panel의 자식으로 HeartPanel 추가
2. QuizCardController.cs에 HeartPanel 바인딩
3. 배경이랑 같은 흰색이라 잘 안 보이기 때문에 Color 조정
--> 'Heart Image', 'Heart Remove Image'를 (242, 242, 242, 255)
--> 'Heart Count Text'를 (0, 0, 0, 255)
최종 코드
>> MobicsTimer.cs
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class MobicsTimer : MonoBehaviour
{
[SerializeField] private Image fillImage;
[SerializeField] private float totalTime;
[SerializeField] private Image headCapImage;
[SerializeField] private Image tailCapImage;
[SerializeField] private TMP_Text timeText;
public float CurrentTime { get; private set; } // 현재 시간 저장
private bool _isPaused; // 현재 Pause 상태인지 체크
public delegate void MobicsTimerDelegate();
public MobicsTimerDelegate OnTimeout;
private void Awake()
{
_isPaused = true;
}
private void Update()
{
if (!_isPaused)
{
CurrentTime += Time.deltaTime;
if (CurrentTime >= totalTime)
{
headCapImage.gameObject.SetActive(false);
tailCapImage.gameObject.SetActive(false);
_isPaused = true;
OnTimeout?.Invoke();
}
else
{
fillImage.fillAmount = (totalTime - CurrentTime) / totalTime;
headCapImage.transform.localRotation = Quaternion.Euler(new Vector3(0, 0, fillImage.fillAmount * 360));
var timeTextTime = totalTime - CurrentTime;
timeText.text = timeTextTime.ToString("F0"); // "F0" : 소수점을 표현하지 않고 정수로 표현
}
}
}
public void StartTimer()
{
_isPaused = false;
}
public void PauseTimer()
{
_isPaused = true;
}
public void InitTimer()
{
CurrentTime = 0;
fillImage.fillAmount = 1;
timeText.text = totalTime.ToString("F0");
headCapImage.gameObject.SetActive(true);
tailCapImage.gameObject.SetActive(true);
_isPaused = true;
}
}
>> 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 int _lastStageIndex;
private void Start()
{
_lastStageIndex = UserInformations.LastStageIndex;
InitQuizCard(_lastStageIndex);
}
private void InitQuizCard(int stageIndex)
{
_quizDataList = QuizDataController.LoadQuizData(stageIndex);
_firstQuizCardObject = ObjectPool.Instance.GetObject();
_firstQuizCardObject.GetComponent<QuizCardController>()
.SetQuiz(_quizDataList[0], 0, OnCompletedQuiz);
_secondQuizCardObject = ObjectPool.Instance.GetObject();
_secondQuizCardObject.GetComponent<QuizCardController>()
.SetQuiz(_quizDataList[1], 1, OnCompletedQuiz);
SetQuizCardPosition(_firstQuizCardObject, 0);
SetQuizCardPosition(_secondQuizCardObject, 1);
// 마지막으로 생성된 Quiz Index
_lastGeneratedQuizIndex = 1;
}
private void OnCompletedQuiz(int cardIndex)
{
if (cardIndex >= Constants.MAX_QUIZ_COUNT - 1)
{
if (_lastStageIndex >= Constants.MAX_STAGE_COUNT - 1)
{
// TODO: 올 클리어 연출
GameManager.Instance.QuitGame();
}
else
{
// TODO: 스테이지 클리어 연출
InitQuizCard(++_lastStageIndex);
return;
}
}
ChangeQuizCard();
}
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에서 마지막으로 이동 --> 카드가 앞으로 배치됨
quizCardObject.GetComponent<QuizCardController>().SetVisible(true);
}
else if (index == 1)
{
quizCardTransform.anchoredPosition = new Vector2(0, 160);
quizCardTransform.localScale = Vector3.one * 0.9f;
quizCardTransform.SetAsFirstSibling(); // 같은 depth에서 처음으로 이동
quizCardObject.GetComponent<QuizCardController>().SetVisible(false);
}
}
private void ChangeQuizCard()
{
if (_lastGeneratedQuizIndex >= Constants.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], _lastGeneratedQuizIndex, OnCompletedQuiz);
}
SetQuizCardPosition(_firstQuizCardObject, 0);
SetQuizCardPosition(_secondQuizCardObject, 1);
ObjectPool.Instance.ReturnObject(temp);
}
}
>> QuizCardController.cs
using System;
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 GameObject frontPanel;
[SerializeField] private GameObject correctBackPanel;
[SerializeField] private GameObject incorrectBackPanel;
// Front Panel
[SerializeField] private TMP_Text questionText; // 퀴즈
[SerializeField] private TMP_Text descriptionText; // 설명
[SerializeField] private Button[] optionButtons; // 보기 --> 타입을 TMP_Text로 해서 text를 직접 받아도 된다.
[SerializeField] private GameObject threeOptionButtons; // 퀴즈 타입에 따른 버튼
[SerializeField] private GameObject oxButtons; // 퀴즈 타입에 따른 버튼
// Incorrect Back Panel
[SerializeField] private TMP_Text heartCountText;
// Timer
[SerializeField] private MobicsTimer timer;
private enum QuizCardPanelType { FrontPanel, CorrectBackPanel, IncorrectBackPanel }
public delegate void QuizCardDelegate(int cardIndex);
private event QuizCardDelegate onCompleted;
private int _answer;
private int _quizCardIndex;
private Vector2 _correctBackPanelPosition;
private Vector2 _incorrectBackPanelPosition;
private void Awake()
{
// 숨겨진 패널의 좌표 저장
_correctBackPanelPosition = correctBackPanel.GetComponent<RectTransform>().anchoredPosition;
_incorrectBackPanelPosition = incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition;
}
private void Start()
{
timer.OnTimeout = () =>
{
// TODO: 오답 연출
SetQuizCardPanelActive(QuizCardPanelType.IncorrectBackPanel);
};
}
public void SetVisible(bool isVisible)
{
if (isVisible)
{
timer.InitTimer();
timer.StartTimer();
}
else
{
timer.InitTimer();
}
}
public void SetQuiz(QuizData quizData, int quizCardIndex, QuizCardDelegate onCompleted)
{
// 1. 퀴즈
// 2. 설명
// 3. 타입 (0: OX퀴즈, 1: 보기 3개 객관식)
// 4. 정답
// 5. 보기 (1, 2, 3)
// 퀴즈 카드 인덱스 할당
_quizCardIndex = quizCardIndex;
// front Panel 표시
SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
// 퀴즈 데이터 표현
questionText.text = quizData.question;
_answer = quizData.answer;
descriptionText.text = quizData.description;
if (quizData.type == 0) // 3지선다 퀴즈
{
threeOptionButtons.SetActive(true);
oxButtons.SetActive(false);
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;
}
else if (quizData.type == 1) // OX 퀴즈
{
oxButtons.SetActive(true);
threeOptionButtons.SetActive(false);
}
this.onCompleted = onCompleted;
// Incorrect Back Panel
heartCountText.text = GameManager.Instance.heartCount.ToString();
}
public void OnClickOptionButton(int buttonIndex)
{
// Timer 일시 정지
timer.PauseTimer();
if (buttonIndex == _answer) // 정답
{
Debug.Log("정답!");
// TODO: 정답 연출
SetQuizCardPanelActive(QuizCardPanelType.CorrectBackPanel);
}
else // 오답
{
Debug.Log("오답!");
// TODO: 오답 연출
SetQuizCardPanelActive(QuizCardPanelType.IncorrectBackPanel);
}
}
private void SetQuizCardPanelActive(QuizCardPanelType quizCardPanelType)
{
switch (quizCardPanelType)
{
case QuizCardPanelType.FrontPanel:
frontPanel.SetActive(true);
correctBackPanel.SetActive(false);
incorrectBackPanel.SetActive(false);
correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;
break;
case QuizCardPanelType.CorrectBackPanel:
frontPanel.SetActive(false);
correctBackPanel.SetActive(true);
incorrectBackPanel.SetActive(false);
correctBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;
break;
case QuizCardPanelType.IncorrectBackPanel:
frontPanel.SetActive(false);
correctBackPanel.SetActive(false);
incorrectBackPanel.SetActive(true);
correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
break;
}
}
public void OnClickExitButton()
{
}
#region Correct Back panel
/// <summary>
/// 다음 버튼 이벤트
/// </summary>
public void OnClickNextQuizButton()
{
onCompleted?.Invoke(_quizCardIndex);
}
#endregion
#region Incorrect Back Panel
/// <summary>
/// 다시 도전 버튼 이벤트
/// </summary>
public void OnClickRetryQuizButton()
{
if (GameManager.Instance.heartCount > 0)
{
GameManager.Instance.heartCount--;
heartCountText.text = GameManager.Instance.heartCount.ToString(); // Incorrect Back Panel에 heartCount 표시
SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
// 타이머 초기화 및 시작
timer.InitTimer();
timer.StartTimer();
}
else
{
// 하트가 부족해서 Retry 불가
// TODO: 하트 부족 알림 구현
}
}
#endregion
}
>> HeartPanelController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
using TMPro;
using UnityEngine.UI;
[RequireComponent(typeof(AudioSource))]
public class HeartPanelController : MonoBehaviour
{
[SerializeField] private GameObject heartRemoveImageObject;
[SerializeField] private TMP_Text heartCountText;
[SerializeField] private AudioClip heartRemoveAudioClip;
[SerializeField] private AudioClip heartAddAudioClip;
[SerializeField] private AudioClip heartEmptyAudioClip;
private AudioSource _audioSource;
private int _heartCount;
// 1. 하트 추가 연출
// 2. 하트 감소 연출
// 3. 하트 부족 연출
private void Awake()
{
_audioSource = GetComponent<AudioSource>();
}
private void Start()
{
heartRemoveImageObject.SetActive(false);
InitHeartCount(10);
}
/// <summary>
/// Heart Panel에 하트 수 초기화
/// </summary>
/// <param name="heartCount">하트 수</param>
public void InitHeartCount(int heartCount)
{
_heartCount = heartCount;
heartCountText.text = _heartCount.ToString();
}
private void ChangeTextAnimation(bool isAdd)
{
float duration = 0.2f;
float yPos = 40f;
heartCountText.rectTransform.DOAnchorPosY(-yPos, duration);
heartCountText.DOFade(0, duration).OnComplete(() =>
{
if (isAdd)
{
var currentHeartCount = heartCountText.text;
heartCountText.text = (int.Parse(currentHeartCount) + 1).ToString();
}
else
{
var currentHeartCount = heartCountText.text;
heartCountText.text = (int.Parse(currentHeartCount) - 1).ToString();
}
// Heart Panel의 Width를 글자 수에 따라 변경
var textLength = heartCountText.text.Length;
GetComponent<RectTransform>().sizeDelta = new Vector2(100 + textLength * 30f, 100f);
// 새로운 하트 수 추가 애니메이션
heartCountText.rectTransform.DOAnchorPosY(yPos, 0);
heartCountText.rectTransform.DOAnchorPosY(0, duration);
heartCountText.DOFade(1, duration).OnComplete(() =>
{
});
});
}
public void AddHeart(int heartCount)
{
Sequence sequence = DOTween.Sequence();
for (int i = 0; i < 3; i++)
{
sequence.AppendCallback(() =>
{
ChangeTextAnimation(true);
// 효과음 재생
// 이 방식이 결코 좋은 방식은 아님 --> 레지스트리에서 매번 읽어오는 방식이기 때문에 나중에 많아지면 안 좋다.
if (UserInformations.IsPlaySFX)
_audioSource.PlayOneShot(heartAddAudioClip);
});
sequence.AppendInterval(0.5f); // 연결된 동작들이 0.5f마다 동작하도록
}
}
public void EmptyHeart()
{
// 효과음 재생
if (UserInformations.IsPlaySFX)
_audioSource.PlayOneShot(heartEmptyAudioClip);
// 주먹으로 친 것처럼 흔들리는 애니메이션
GetComponent<RectTransform>().DOPunchPosition(new Vector3(20f, 0, 0), 1f, 7);
}
public void RemoveHeart()
{
// 효과음 재생
if (UserInformations.IsPlaySFX)
_audioSource.PlayOneShot(heartRemoveAudioClip);
// 하트 초기화
heartRemoveImageObject.SetActive(true);
heartRemoveImageObject.transform.localScale = Vector3.zero;
heartRemoveImageObject.GetComponent<Image>().color = Color.white;
// 하트 사라지는 연출
heartRemoveImageObject.transform.DOScale(3f, 1f);
heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);
// 하트 개수 텍스트가 감소되는 연출
DOVirtual.DelayedCall(1f, () =>
{
ChangeTextAnimation(false);
});
}
}
>> UserInfromations.cs
using UnityEngine;
public static class UserInformations
{
private const string HEART_COUNT = "HeartCount"; // string key 값 저장
private const string LAST_STAGE_INDEX = "LastStageIndex";
// 하트 수
public static int HeartCount
{
get
{
// "HeartCount"라는 이름의 정보를 Int로 가져오는데, Default 값은 5 --> 최초로 게임이 시작되면 저장된 값이 없기 때문
return PlayerPrefs.GetInt(HEART_COUNT, 5);
}
set
{
// value 값으로 PlayerPrefs에 저장
PlayerPrefs.SetInt(HEART_COUNT, value);
}
}
// 스테이지 클리어 정보
public static int LastStageIndex
{
get { return PlayerPrefs.GetInt(LAST_STAGE_INDEX, 0); }
set { PlayerPrefs.GetInt(LAST_STAGE_INDEX, value); }
}
// 효과음 재생 여부
public static bool IsPlaySFX
{
get { return PlayerPrefs.GetInt("IsPlaySFX", 1) == 1; }
set { PlayerPrefs.SetInt("IsPlaySFX", value ? 1 : 0); }
}
// 배경음악 재생 여부
public static bool IsPlayBGM
{
get { return PlayerPrefs.GetInt("IsPlayBGM", 1) == 1; }
set { PlayerPrefs.SetInt("IsPlayBGM", value ? 1 : 0); }
}
}
C# 단기 교육 보강
9일차
Hanoi Tower 복잡한 코드 수정
>> 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 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--)
{
// 생성한 Donut을 LeftBar에 Push (index 0 == left)
donutBars[0].PushDonut(CreateDonut(donutPrefab, i));
yield return new WaitForSeconds(1f);
}
}
private GameObject CreateDonut(GameObject prefab, int i)
{
GameObject obj = Instantiate(prefab); // 도넛 생성
// 도넛 생성 위치, 회전 설정
// Board의 X의 Scale이 10이라서 (int)DonutBar.BarType.LEFT에 0.1f를 곱할 필요가 없다.
obj.transform.SetPositionAndRotation
(new Vector3((int)DonutBar.BarType.LEFT, 3.5f, 0f), Quaternion.identity);
obj.name = "Donut_" + i; // 도넛 이름 설정
obj.GetComponent<Donut>().donutNumber = i; // 도넛에 번호 부여
obj.transform.localScale = Vector3.one * (i * 0.3f + 1f); // 도넛 크기 설정
return obj;
}
}
}
>> 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();
}
else // 도넛을 넣을 기둥 선택
{
PushDonut(gameManager.selectedDonut);
}
}
private bool CheckDonutNumber(GameObject pushDonut)
{
bool result = true; // 처음에 도넛을 Push해야하기 때문에 Default 값을 true로 설정
if (stack.Count > 0)
{
int pushNumber = pushDonut.GetComponent<Donut>().donutNumber;
int peekNumber = stack.Peek().GetComponent<Donut>().donutNumber;
result = pushNumber < peekNumber; // 도넛이 하노이 로직에 맞는지 확인
if (!result)
Debug.Log($"놓으려는 도넛은 {pushNumber}이고, 해당 기둥의 도넛은 {peekNumber}입니다.");
}
return result;
}
public void PushDonut(GameObject pushDonut)
{
if (!CheckDonutNumber(pushDonut)) return; // 도넛 넘버가 하노이 로직에 맞지 않음
gameManager.isSelected = false;
gameManager.selectedDonut = null;
stack.Push(pushDonut);
pushDonut.transform.SetPositionAndRotation(new Vector3((int)barType, 3.5f, 0f), Quaternion.identity);
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()
{
GameObject obj = null;
if (stack.Count > 0)
{
obj = stack.Pop();
gameManager.isSelected = true;
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;
}
}
return obj;
}
}
Hanoi Tower에 재귀함수 적용
: Button을 누르면 Debug.Log()로 정답을 알려주도록 구현
>> Button 추가 및 OnClick()에 GameManager 바인딩
>> 코드로 구현
: 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 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--)
{
// 생성한 Donut을 LeftBar에 Push (index 0 == left)
donutBars[0].PushDonut(CreateDonut(donutPrefab, i));
yield return new WaitForSeconds(1f);
}
}
private GameObject CreateDonut(GameObject prefab, int i)
{
GameObject obj = Instantiate(prefab); // 도넛 생성
// 도넛 생성 위치, 회전 설정
// Board의 X의 Scale이 10이라서 (int)DonutBar.BarType.LEFT에 0.1f를 곱할 필요가 없다.
obj.transform.SetPositionAndRotation
(new Vector3((int)DonutBar.BarType.LEFT, 3.5f, 0f), Quaternion.identity);
obj.name = "Donut_" + i; // 도넛 이름 설정
obj.GetComponent<Donut>().donutNumber = i; // 도넛에 번호 부여
obj.transform.localScale = Vector3.one * (i * 0.3f + 1f); // 도넛 크기 설정
return obj;
}
public void OnShowAnswer()
{
ShowAnswer((int)hanoiLevel, 0, 1, 2);
}
/// <summary>
/// 재귀함수를 이용하여 정답을 알려주는 함수
/// </summary>
/// <param name="count">원반 개수</param>
/// <param name="from">시작 기둥</param>
/// <param name="temp">임시 기둥</param>
/// <param name="to">목표 기둥</param>
private void ShowAnswer(int count, int from, int temp, int to)
{
if (count == 0) return;
if (count == 1)
Debug.Log($"{count}번 도넛을 {from}에서 {to}로 이동");
else
{
ShowAnswer(count - 1, from, to, temp);
Debug.Log($"{count}번 도넛을 {from}에서 {to}로 이동");
ShowAnswer(count - 1, temp, from, to);
}
}
}
}
Lotto
: Swap, Shuffle 등을 활용
>> 빈 게임 오브젝트로 'Lotto Creator'를 만들고, LottoCreator.cs 추가'
>> 화면 비율 조정
>> Image 생성 후 위치 조정
: 저는 Vertical Layout Group를 사용
>> Width, Height 조정
>> 마지막 공은 Bonus 공이니 따로 색을 바꿔 표시
>> 공에 전부 Text 추가
--> Text를 복붙
: 첫 Text를 복붙하고 각각 Image의 자식 오브젝트로 옮긴 뒤, PosX를 0으로 바꾸면 사진과 같이 자기 자리를 찾아감
>> 이름을 Lotto Ball로 바꾸고 LottoBall.cs 추가
>> Lotto Ball들을 LottoCreator.cs에 바인딩
>> 버튼 생성
: 위치와 크기 바꿔주고 OnClick() 함수 바인딩
└ 코드
>> LottoCreator.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
public class LottoCreator : MonoBehaviour
{
// Swap 자리 바꾸기
public int[] numbers = new int[45]; // 45자리의 숫자 배열 생성
public LottoBall[] lottoBalls; // 7개의 로또볼
private int _shakeCount = 1000; // 섞는 횟수
private void Start()
{
for (int i = 0; i < 45; i++) // numbers의 개수는 45개로 고정이기 때문에 numbers.Length로 할 필요 없다.
numbers[i] = i + 1; // 로또번호를 1부터 45까지 등록
}
public void OnCreateLotto()
{
for (int i = 0; i < _shakeCount; i++) // 셔플 기능
{
int ranInt1 = Random.Range(0, numbers.Length);
int ranInt2 = Random.Range(0, numbers.Length);
//var temp = numbers[ranInt1];
//numbers[ranInt1] = numbers[ranInt2];
//numbers[ranInt2] = temp;
(numbers[ranInt1], numbers[ranInt2]) = (numbers[ranInt2], numbers[ranInt1]);
}
// 섞은 숫자 배열의 앞 7개를 오름차순 정렬
int[] sortArray = new int[7];
for (int i = 0; i < 7; i++)
sortArray[i] = numbers[i];
Array.Sort(sortArray);
for (int i = 0; i < lottoBalls.Length; i++) // 로또볼에 적용
lottoBalls[i].textNumber.text = sortArray[i].ToString();
StartCoroutine(ShowBall());
}
IEnumerator ShowBall()
{
foreach (var ball in lottoBalls)
{
ball.gameObject.SetActive(true);
yield return new WaitForSeconds(0.5f);
}
}
private void Swap()
{
int ranInt1 = Random.Range(0, numbers.Length);
int ranInt2 = Random.Range(0, numbers.Length);
int temp = 0;
temp = numbers[ranInt1];
numbers[ranInt1] = numbers[ranInt2];
numbers[ranInt2] = temp;
}
private void Shuffle()
{
for (int i = 0; i < _shakeCount; i++)
{
Swap();
}
}
}
>> LottoBalls.cs
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class LottoBall : MonoBehaviour
{
public TMP_Text textNumber;
private bool _isScale;
private void Start()
{
textNumber = transform.GetChild(0).GetComponent<TMP_Text>();
transform.localScale = Vector3.zero;
gameObject.SetActive(false);
}
private void Update()
{
if (!_isScale)
{
transform.localScale += Vector3.one * (Time.deltaTime * 2f);
if (transform.localScale.x >= 1f)
{
_isScale = true;
transform.localScale = Vector3.one;
}
}
}
}
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 58일차 (0) | 2025.02.19 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 57일차 (0) | 2025.02.18 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 55일차 (0) | 2025.02.14 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 54일차 (0) | 2025.02.13 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 53일차 (0) | 2025.02.12 |