목차
퀴즈 게임 만들기
25.02.19
코드로 Quiz Card의 상태 패턴 구현
: Quiz Card Animation 구현 완료, Timer 버그 수정
--> 다음 퀴즈로 넘어갈 때, 앞의 카드가 내려가고 뒤의 카드가 앞으로 나오면서 새 카드가 뒤에 등장하는 Animation
활동
>> 카드 뒤집기를 상태 패턴으로 구현
: 다시 도전한 뒤에 버그 발생
--> 'QuizCardPositionStateContext'의 'SetState' 함수에서 아래 코드 부분이 문제를 일으킴
if (_currentState == state) return;
--> 'QuizCardPositionStateFlipNormal' 상태를 추가하여 해결
>> Game Scene 전체 구현하기 --> 게임 플레이 영상과 동일한 형태로 게임을 구현하기
- Button 이미지 적용
- 정답/오답 연출 구현
- 폰트 교체 --> 에스코어드림 폰트
- 스테이지 시작 구현
※ 아직 미구현 된 Game Scene
- 레벨(스테이지) 팝업
- 정답과 오답 시, 애니메이션
- 오답 시, 몇 문제 남았는지 표시
- 앞의 퀴즈 카드에 명암을 넣어서 입체적으로 표현
└ 활동 해보기
>> Button 이미지 적용
1. QuizCard Prefab의 O Button의 Source Image 변경 및 Width와 Height 조정
--> X Button도 마찬가지로 설정
2. Button의 Text를 제거하고 Image 추가하고 Color 조정
: 이름은 'O Image', Color는 (44, 55, 89, 255)
--> X Button에도 마찬가지로 추가
3. OX Buttons의 Width와 Height 조정
4. Options의 Width와 Height 조정 및 Horizontal Layout Group의 Chile Alignment 변경
5. First Option Button ~ Third Option Button의 Source Image 변경 및 Width와 Height 조정
--> 버튼 세개 다 마찬가지로 설정
6. Three Option Buttons의 Width와 Height 조정
>> 정답/오답 연출 구현
구상 (정답)
1. Front Panel에 Correct Marker 배치 및 SetActive(false)
2. 정답 클릭 시, Timer, Question Text를 비활성화하고 Correct Marker를 활성화
3. 클릭한 정답의 Option Button의 Color와 그 자식의 Text 또는 Image의 Color 변경
4. Game Panel의 Color를 변경하고 배경에 'pang_effect' 가 나왔다가 사라지도록 Animation 추가
5. Card의 잔상이 커지면서 흐려지는 Animation 추가
6. Text가 앞에서부터 나타나도록 구현 --> Coroutine 활용?
7. 이후 Description 화면으로 Flip하며 전환될 때, Game Panel의 Color가 서서히 변하도록 구현
구상 (오답)
1. Front Panel에 Incorrect Marker 배치 및 SetActive(false)
--> Anchor는 Alt + Shift
2. 정답 클릭 시, Timer, Question Text를 비활성화하고 Incorrect Marker를 활성화
3. 클릭한 오답의 Option Button의 Color와 그 자식의 Text 또는 Image의 Color 변경
4. Game Panel의 Color가 변경되도록 구현 --> 빠르게 바뀌는건지 그냥 바뀌는건지 모르겠다
5. Card가 흔들리는 Animation 추가
6. Text가 앞에서부터 나타나도록 구현 --> Coroutine 활용?
7. 이후 Retry 화면으로 Flip하며 전환될 때, Game Panel의 Color가 서서히 변하도록 구현
최종 코드
>> 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 Queue<QuizCardController> _quizCardQueue = new();
/// <summary>
/// 새로운 퀴즈 카드 추가하는 함수
/// </summary>
/// <param name="quizData">퀴즈 데이터</param>
/// <param name="isInit">초기화 여부</param>
public void AddQuizCardObject(QuizData? quizData, bool isInit = false) // Queue를 이용한 새로운 로직
{
QuizCardController tempQuizCardController = null; // 뺄 Quiz Card
// 1. First 영역의 카드 제거
void RemoveFirstQuizCard(Action onComplete = null)
{
tempQuizCardController = _quizCardQueue.Dequeue();
tempQuizCardController.SetQuizCardPosition(QuizCardController.QuizCardPositionType.Remove,
true, onComplete);
}
// 2. Second 영역의 카드를 First 영역으로 이동
void SecondQuizCardToFirst(Action onComplete = null)
{
var firstQuizCardController = _quizCardQueue.Peek();
firstQuizCardController.SetQuizCardPosition(QuizCardController.QuizCardPositionType.First,
true, onComplete);
}
// 3. 새로운 퀴즈 카드를 Second 영역에 생성
void AddNewQuizCard(Action onComplete = null)
{
if (quizData.HasValue)
{
var quizCardObject = ObjectPool.Instance.GetObject();
var quizCardController = quizCardObject.GetComponent<QuizCardController>();
quizCardController.SetQuiz(quizData.Value, OnCompletedQuiz);
_quizCardQueue.Enqueue(quizCardController);
quizCardController.SetQuizCardPosition(QuizCardController.QuizCardPositionType.Second,
true, onComplete);
}
}
// 애니메이션 처리
if (_quizCardQueue.Count > 0)
{
if (isInit)
{
SecondQuizCardToFirst();
AddNewQuizCard();
}
else
{
RemoveFirstQuizCard(() =>
SecondQuizCardToFirst(() =>
AddNewQuizCard(() =>
{
if (tempQuizCardController != null)
ObjectPool.Instance.ReturnObject(tempQuizCardController.gameObject);
})));
}
}
else
{
AddNewQuizCard();
}
}
private void Start()
{
_lastStageIndex = UserInformations.LastStageIndex;
InitQuizCard(_lastStageIndex);
}
private void InitQuizCard(int stageIndex)
{
_quizDataList = QuizDataController.LoadQuizData(stageIndex);
AddQuizCardObject(_quizDataList[0], true);
AddQuizCardObject(_quizDataList[1], true);
#region 구조 개선 전 코드
// _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;
#endregion
}
private void OnCompletedQuiz(int cardIndex)
{
if (cardIndex < _quizDataList.Count - 2)
{
AddQuizCardObject(_quizDataList[cardIndex + 2]);
}
else
{
AddQuizCardObject(null);
if (cardIndex == _quizDataList.Count - 1)
{
// TODO: 스테이지 클리어 연출
_lastStageIndex++;
// TODO: 스테이지 클리어 연출 후, 새로운 스테이지 시작
if (_lastStageIndex < Constants.MAX_STAGE_COUNT) // 임시 코드
InitQuizCard(_lastStageIndex);
}
}
#region 구조 개선 전 코드
// if (cardIndex >= Constants.MAX_QUIZ_COUNT - 1)
// {
// if (_lastStageIndex >= Constants.MAX_STAGE_COUNT - 1)
// {
// // TODO: 올 클리어 연출
//
// GameManager.Instance.QuitGame();
// }
// else
// {
// // TODO: 스테이지 클리어 연출
// InitQuizCard(++_lastStageIndex);
// return;
// }
// }
// ChangeQuizCard();
#endregion
}
#region 구조 개선 전 코드
// 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);
// }
#endregion
}
>> QuizCardController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public struct QuizData
{
public int index;
public string question;
public string description;
public int type;
public int answer;
public string firstOption; // 원래는 string[] Options로 했었다
public string secondOption;
public string thirdOption;
}
// QuizCard의 위치 상태를 정의할 클래스가 반드시 구현할 Method의 목록
public interface IQuizCardPositionState
{
void Transition(bool withAnimation, Action onComplete = null);
}
// QuizCard의 위치 상태 전이를 관리할 목적
public class QuizCardPositionStateContext
{
private IQuizCardPositionState _currentState;
public void SetState(IQuizCardPositionState state, bool withAnimation, Action onComplete = null)
{
if (_currentState == state) return;
_currentState = state;
_currentState.Transition(withAnimation, onComplete);
}
}
public class QuizCardPositionState
{
protected QuizCardController _quizCardController;
protected RectTransform _rectTransform;
protected CanvasGroup _canvasGroup;
public QuizCardPositionState(QuizCardController quizCardController)
{
_quizCardController = quizCardController;
_rectTransform = _quizCardController.gameObject.GetComponent<RectTransform>();
_canvasGroup = _quizCardController.gameObject.GetComponent<CanvasGroup>();
}
}
// QuizCard가 첫 번째 위치에 나타날 상태 클래스
public class QuizCardPositionStateFirst: QuizCardPositionState, IQuizCardPositionState
{
public QuizCardPositionStateFirst(QuizCardController quizCardController) : base(quizCardController) { }
public void Transition(bool withAnimation, Action onComplete = null)
{
var animationDuration = (withAnimation) ? 0.2f : 0f;
_rectTransform.DOAnchorPos(Vector2.zero, animationDuration);
_rectTransform.DOScale(1f, animationDuration);
_canvasGroup.DOFade(1f, animationDuration).OnComplete(() => onComplete?.Invoke());
_rectTransform.SetAsLastSibling();
}
}
// QuizCard가 두 번째 위치에 나타날 상태 클래스
public class QuizCardPositionStateSecond: QuizCardPositionState, IQuizCardPositionState
{
public QuizCardPositionStateSecond(QuizCardController quizCardController) : base(quizCardController) { }
public void Transition(bool withAnimation, Action onComplete = null)
{
var animationDuration = (withAnimation) ? 0.2f : 0f;
_rectTransform.DOAnchorPos(new Vector2(0f, 160f), 0);
_rectTransform.DOScale(0.9f, animationDuration);
_canvasGroup.DOFade(0.7f, animationDuration).OnComplete(() => onComplete?.Invoke());
_rectTransform.SetAsFirstSibling();
}
}
// QuizCard가 사라질 상태를 처리할 상태 클래스
public class QuizCardPositionStateRemove: QuizCardPositionState, IQuizCardPositionState
{
public QuizCardPositionStateRemove(QuizCardController quizCardController) : base(quizCardController) { }
public void Transition(bool withAnimation, Action onComplete = null)
{
var animationDuration = (withAnimation) ? 0.2f : 0f;
_rectTransform.DOAnchorPos(new Vector2(0f, -280f), animationDuration);
_canvasGroup.DOFade(0f, animationDuration).OnComplete(() => onComplete?.Invoke());
}
}
// QuizCard가 뒤집어지는 상태 클래스
public class QuizCardPositionStateFlip : QuizCardPositionState, IQuizCardPositionState
{
public QuizCardPositionStateFlip(QuizCardController quizCardController) : base(quizCardController) { }
public void Transition(bool withAnimation, Action onComplete = null)
{
var animationDuration = (withAnimation) ? 0.3f : 0f;
_rectTransform.DORotate(new Vector3(0f, 90f, 0f), animationDuration / 2)
.OnComplete(() =>
{
_rectTransform.DORotate(new Vector3(0f, 0f, 0f), animationDuration / 2)
.OnComplete(() => onComplete?.Invoke());
});
}
}
public class QuizCardPositionStateFlipNormal : QuizCardPositionState, IQuizCardPositionState
{
public QuizCardPositionStateFlipNormal(QuizCardController quizCardController) : base(quizCardController) { }
public void Transition(bool withAnimation, Action onComplete = null)
{
var animationDuration = (withAnimation) ? 0.3f : 0f;
_rectTransform.DORotate(new Vector3(0f, 90f, 0f), animationDuration / 2)
.OnComplete(() =>
{
_rectTransform.DORotate(new Vector3(0f, 0f, 0f), animationDuration / 2)
.OnComplete(() => onComplete?.Invoke());
});
}
}
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;
// Heart Panel
[SerializeField] private HeartPanelController heartPanelController;
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;
// Quiz Card 위치 상태
private IQuizCardPositionState _positionStateFirst;
private IQuizCardPositionState _positionStateSecond;
private IQuizCardPositionState _positionStateRemove;
private IQuizCardPositionState _positionStateFlip;
private IQuizCardPositionState _positionStateFlipNormal;
private QuizCardPositionStateContext _positionStateContext;
private void Awake()
{
// 숨겨진 패널의 좌표 저장
_correctBackPanelPosition = correctBackPanel.GetComponent<RectTransform>().anchoredPosition;
_incorrectBackPanelPosition = incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition;
// 상태 관리를 위한 Context 객체 생성
_positionStateContext = new QuizCardPositionStateContext();
_positionStateFirst = new QuizCardPositionStateFirst(this);
_positionStateSecond = new QuizCardPositionStateSecond(this);
_positionStateRemove = new QuizCardPositionStateRemove(this);
_positionStateFlip = new QuizCardPositionStateFlip(this);
_positionStateFlipNormal = new QuizCardPositionStateFlipNormal(this);
_positionStateContext.SetState(_positionStateRemove, false); // 카드 위치 초기화
}
private void Start()
{
timer.OnTimeout = () =>
{
// TODO: 오답 연출
SetQuizCardPanelActive(QuizCardPanelType.IncorrectBackPanel);
};
}
#region 구조 개선 전 코드
// public void SetVisible(bool isVisible)
// {
// if (isVisible)
// {
// timer.InitTimer();
// timer.StartTimer();
// }
// else
// {
// timer.InitTimer();
// }
// }
#endregion
public enum QuizCardPositionType { First, Second, Remove }
/// <summary>
/// Quiz Card 위치를 지정하는 Method
/// </summary>
/// <param name="quizCardPositionType">Quiz Card 위치</param>
/// <param name="withAnimation">애니메이션 여부</param>
/// <param name="onComplete">위치 지정 후 실행할 동작</param>
public void SetQuizCardPosition(QuizCardPositionType quizCardPositionType,
bool withAnimation, Action onComplete = null)
{
switch (quizCardPositionType)
{
case QuizCardPositionType.First:
_positionStateContext.SetState(_positionStateFirst, withAnimation, () =>
{
timer.InitTimer();
timer.StartTimer();
onComplete?.Invoke();
});
break;
case QuizCardPositionType.Second:
_positionStateContext.SetState(_positionStateSecond, withAnimation, () =>
{
timer.InitTimer();
onComplete?.Invoke();
});
break;
case QuizCardPositionType.Remove:
_positionStateContext.SetState(_positionStateRemove, withAnimation, onComplete);
break;
}
}
public void SetQuiz(QuizData quizData, QuizCardDelegate onCompleted)
{
// 1. 퀴즈
// 2. 설명
// 3. 타입 (0: OX퀴즈, 1: 보기 3개 객관식)
// 4. 정답
// 5. 보기 (1, 2, 3)
// 퀴즈 카드 인덱스 할당
_quizCardIndex = quizData.index;
// front Panel 표시
SetQuizCardPanelActive(QuizCardPanelType.FrontPanel, false);
// 퀴즈 데이터 표현
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;
heartPanelController.InitHeartCount(GameManager.Instance.heartCount);
}
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, bool withAnimation = true)
{
switch (quizCardPanelType)
{
case QuizCardPanelType.FrontPanel:
correctBackPanel.SetActive(false);
incorrectBackPanel.SetActive(false);
_positionStateContext.SetState(_positionStateFlipNormal, withAnimation, () =>
{
frontPanel.SetActive(true);
correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;
});
break;
case QuizCardPanelType.CorrectBackPanel:
frontPanel.SetActive(false);
incorrectBackPanel.SetActive(false);
_positionStateContext.SetState(_positionStateFlip, withAnimation, () =>
{
correctBackPanel.SetActive(true);
correctBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;
});
break;
case QuizCardPanelType.IncorrectBackPanel:
frontPanel.SetActive(false);
correctBackPanel.SetActive(false);
_positionStateContext.SetState(_positionStateFlip, withAnimation, () =>
{
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--;
heartPanelController.RemoveHeart(() =>
{
SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
// 타이머 초기화 및 시작
timer.InitTimer();
timer.StartTimer();
});
}
else
{
// 하트가 부족해서 Retry 불가
heartPanelController.EmptyHeart();
}
}
#endregion
}
>> 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;
headCapImage.gameObject.SetActive(true);
tailCapImage.gameObject.SetActive(true);
}
public void PauseTimer()
{
_isPaused = true;
}
public void InitTimer()
{
CurrentTime = 0;
fillImage.fillAmount = 1;
timeText.text = totalTime.ToString("F0");
headCapImage.gameObject.SetActive(false);
tailCapImage.gameObject.SetActive(false);
_isPaused = true;
}
}
C# 단기 교육 보강
11일차
게임 수학 실습 - 벡터(Vector)와 스칼라(Scalar)
: Plane의 Scale 변경
>> Turret이 Player를 감지하도록 구현
: Player는 Cube로 생성
--> Player의 Use Gravity를 체크 해제하기
※ Vector3.Magnitude() vs Distance() vs SqrMagnitude()
- Magnitude() vs Distance() 는 비슷하지만 굳이 따지면 Distance()가 더 연산이 가볍다
- SqrMagnitude()는 루트를 씌우지 않은 값이기 때문에 정확한 크기를 구하는 것이 아니라 단순히 크기의 비교를 위해서라면 SqrMagnitude()를 사용하는 것이 연산이 가볍다.
행렬 (Matrix)
>> 회전 행렬 (Rotation Matrix)
게임 수학 실습 - 행렬 (Matrix)
>> 빈 게임 오브젝트로 'Board Manager' 생성 후, BoardMatrix.cs 추가 및 바인딩
: Tile Prefab은 Package로 보내주셔서 다운 받아 사용함
>> MainCamera를 원하는 위치로 옮기기
1. Play 상태로 Scene View에서 원하는 위치로 옮긴 뒤, Main Camera를 선택하고 Ctrl + Shift + F 로 Camera 옮기기
2. 그 위치의 MainCamera의 Transform 값 복사
3. Play를 종료한 뒤, MainCamera의 Transform에 값 붙여넣기
>> Button으로 Turret 변경하기
1. Button 5개 생성
2. Turret Prefab 바인딩
3. 각 Button의 OnClick()에 함수 바인딩
: index는 0부터 4까지 순차적으로 적용
최종 코드
>> TurretRotation.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TurretRotation : MonoBehaviour
{
public Transform target;
public float rotationSpeed = 1f;
private float _theta;
public float radius = 10f;
public float angle = 120f;
public void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
target = other.transform;
}
}
private void Update()
{
Rotation();
SetTarget();
}
private void SetTarget()
{
if (target == null) return;
// 터렛과 타겟과의 거리
float distance = Vector3.Distance(transform.position, target.position);
if (distance <= radius)
{
Vector3 targetDir = (target.position - transform.position).normalized;
// 터렛의 정면과 타겟과의 각도
float targetAngle = Vector3.Angle(transform.forward, targetDir);
if (targetAngle <= angle / 2) // 터렛의 공격 각도 범위 내에 있는지
{
// 공격 가능 대상일 때 초록색 선
Debug.DrawRay(transform.position, targetDir * radius, Color.green);
transform.LookAt(targetDir);
}
else
{
// 공격 불가능 대상일 때 빨간색 선
Debug.DrawRay(transform.position, targetDir * radius, Color.red);
}
}
}
private void Rotation()
{
//transform.Rotate(Vector3.up, Time.deltaTime * rotationSpeed);
_theta += Time.deltaTime * rotationSpeed;
transform.rotation = Quaternion.Euler(Vector3.up * (45f * Mathf.Sin(_theta)));
Vector3 vecA = new Vector3(0, 3, 5);
Vector3 vecB = new Vector3(7, 2, 1);
// 벡터의 크기
float result1 = Vector3.Magnitude(vecB - vecA);
float result2 = Vector3.Distance(vecB, vecA);
}
private void OnDrawGizmos()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, radius);
Vector3 leftBoundary = Quaternion.Euler(0, -angle / 2, 0) * transform.forward * radius;
Vector3 rightBoundary = Quaternion.Euler(0, angle / 2, 0) * transform.forward * radius;
Gizmos.color = Color.blue;
Gizmos.DrawLine(transform.position, transform.position + leftBoundary);
Gizmos.DrawLine(transform.position, transform.position + rightBoundary);
}
}
>> BoardMatrix.cs
using System;
using UnityEngine;
public class BoardMatrix : MonoBehaviour
{
public GameObject tilePrefab;
public Vector2 boardSize = new Vector2(5, 5);
public int[,] tileArray;
public GameObject turretPrefab;
public GameObject[] turrets;
private void Start()
{
tileArray = new int[(int)boardSize.x, (int)boardSize.y];
for (int x = 0; x < boardSize.x; x++)
{
for (int z = 0; z < boardSize.y; z++)
{
GameObject tileObj = Instantiate(tilePrefab);
tileObj.transform.position = new Vector3(x, 0, z);
// tileArray[x, z] = 1;
}
}
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
int x = Mathf.RoundToInt(hit.collider.transform.position.x);
int z = Mathf.RoundToInt(hit.collider.transform.position.z);
if (tileArray[x, z] == 0)
{
GameObject turretObj = Instantiate(turretPrefab);
turretObj.transform.position = new Vector3(x, 0, z);
tileArray[x, z] = 1;
}
}
}
}
public void OnChangeTurret(int index)
{
turretPrefab = turrets[index];
}
}
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 60일차 (0) | 2025.02.21 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 59일차 (1) | 2025.02.20 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 57일차 (0) | 2025.02.18 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 56일차 (0) | 2025.02.17 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 55일차 (0) | 2025.02.14 |