목차
※ GetComponent나 FindObjectsOfType 과 같은 방식은 많이 사용되는 것을 지양한다
퀴즈 게임 만들기
25.02.18
Heart Panel을 Quiz Card에 적용하기
>> Heart Count Text 삭제
>> Heart Image, Heart Remove Image, Heart Count Text의 Color 변경
: (242, 68, 149, 255)
>> QuizCardController.cs에 Heart Panel 바인딩
※ Action
: Delegate와 달리 별도의 선언 없이 함수를 매개변수로 전달 가능하다
>> TestButtons 삭제
현재 있는 버그
- Stage를 넘어가면 QuizCard가 하나씩 계속 쌓이는 버그
- IncorrectBackPanel에서 다시 도전을 광클하면 생명이 여러 개 깎이는 버그
Quiz Card를 띄우는 새로운 로직
- 앞에 있는 Card를 제거
- 뒤에 있는 Card를 앞으로 옮기기
- 새로운 Card를 뒤에 만들기
--> Queue를 활용
※ Stage의 마지막 문제를 풀 때는 뒤에 보이는 QuizCard가 없도록
활동
: Quiz Card 애니메이션 구현
- 카드가 회전하며 뒤집히는 애니메이션
- 다음 퀴즈로 넘어갈 때, 앞의 카드는 내려가고 뒤의 카드가 앞으로 나타나며 새로운 뒤의 카드가 등장하는 애니메이션
※ 카드 뒤집기는 180도를 돌리는 것이 아니라, 90도만 돌린 뒤 다시 역방향으로 다시 90도를 돌리면 시각적으로는 카드를 뒤집은 것 처럼 보이면서 글자 등이 뒤집어지지 않게 연출이 가능하다.
StatePattern를 사용하여 QuizCard의 위치 상태 구현
>> QuizCard에 Canvas Group을 추가
: 자식들의 Alpha값을 한번에 설정 가능
최종 코드
>> 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)
{
_currentState = state;
_currentState.Transition(false); // false는 내가 임의로 넣은 것
}
}
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)
{
}
}
// QuizCard가 사라질 상태를 처리할 상태 클래스
public class QuizCardPositionStateRemove: QuizCardPositionState, IQuizCardPositionState
{
public QuizCardPositionStateRemove(QuizCardController quizCardController) : base(quizCardController) { }
public void Transition(bool withAnimation, Action onComplete = null)
{
}
}
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;
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, QuizCardDelegate onCompleted)
{
// 1. 퀴즈
// 2. 설명
// 3. 타입 (0: OX퀴즈, 1: 보기 3개 객관식)
// 4. 정답
// 5. 보기 (1, 2, 3)
// 퀴즈 카드 인덱스 할당
_quizCardIndex = quizData.index;
// 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;
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)
{
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--;
heartPanelController.RemoveHeart(() =>
{
SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
// 타이머 초기화 및 시작
timer.InitTimer();
timer.StartTimer();
});
}
else
{
// 하트가 부족해서 Retry 불가
heartPanelController.EmptyHeart();
}
}
#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(GameManager.Instance.heartCount);
}
/// <summary>
/// Heart Panel에 하트 수 초기화
/// </summary>
/// <param name="heartCount">하트 수</param>
public void InitHeartCount(int heartCount)
{
_heartCount = heartCount;
heartCountText.text = _heartCount.ToString();
}
private void ChangeTextAnimation(bool isAdd, Action onComplete = null)
{
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(() =>
{
DOVirtual.DelayedCall(0.5f, () => onComplete?.Invoke());
});
});
}
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(Action onComplete = null)
{
// 효과음 재생
if (UserInformations.IsPlaySFX)
_audioSource.PlayOneShot(heartRemoveAudioClip);
// 하트 초기화
heartRemoveImageObject.SetActive(true);
heartRemoveImageObject.transform.localScale = Vector3.zero;
heartRemoveImageObject.GetComponent<Image>().color = new Color32(242, 68, 149, 255);
// 하트 사라지는 연출
heartRemoveImageObject.transform.DOScale(3f, 1f);
heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);
// 하트 개수 텍스트가 감소되는 연출
DOVirtual.DelayedCall(1f, () =>
{
ChangeTextAnimation(false, onComplete);
});
}
}
>> UserInformations.cs
using UnityEngine;
using UnityEditor;
public static class UserInformations
{
[MenuItem("Window/PlayerPrefs 초기화")]
private static void ResetPrefs() // 다른 수강생님이 만든 레지스트리 초기화 함수
{
PlayerPrefs.DeleteAll();
Debug.Log("PlayerPrefs has been reset.");
}
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); }
}
}
>> Constants.cs
public class Constants
{
// public const int MAX_QUIZ_COUNT = 5; // 한 스테이지에 나오는 퀴즈의 수, 구조 개선하면서 제거
public const int MAX_STAGE_COUNT = 3; // 전체 스테이지 수
}
>> 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<GameObject> _quizCardObjectQueue = new();
/// <summary>
/// 새로운 퀴즈 카드 추가하는 함수
/// </summary>
/// <param name="quizData"></param>
/// <param name="isInit"></param>
public void AddQuizCardObject(QuizData? quizData, bool isInit = false) // Queue를 이용한 새로운 로직
{
GameObject tempObject = null; // Animation을 재생하고 삭제되도록
// 1. 앞에 있는 QuizCard 제거
if (_quizCardObjectQueue.Count > 0 && !isInit)
{
tempObject = _quizCardObjectQueue.Dequeue();
}
// 2. 뒤에 있는 카드 앞으로 옮기기
if (_quizCardObjectQueue.Count > 0)
{
var firstQuizCardObject = _quizCardObjectQueue.Peek();
firstQuizCardObject.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
firstQuizCardObject.transform.localScale = Vector3.one;
firstQuizCardObject.transform.SetAsLastSibling();
firstQuizCardObject.GetComponent<QuizCardController>().SetVisible(true);
}
// 3. 새로운 카드 뒤에 만들기
if (quizData.HasValue)
{
var quizCardObject = ObjectPool.Instance.GetObject();
quizCardObject.GetComponent<QuizCardController>().SetQuiz(quizData.Value, OnCompletedQuiz);
quizCardObject.GetComponent<QuizCardController>().SetVisible(false);
quizCardObject.GetComponent<RectTransform>().anchoredPosition = new Vector2(0, 160f);
quizCardObject.transform.localScale = Vector3.one * 0.9f;
quizCardObject.transform.SetAsFirstSibling();
_quizCardObjectQueue.Enqueue(quizCardObject);
}
if (tempObject != null)
{
ObjectPool.Instance.ReturnObject(tempObject);
}
}
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
}
>> 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();
quizData.index = i - 1; // 추가
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;
}
}
C# 단기 교육 보강
10일차
게임수학
└ π(파이), 라디안
└ 삼각함수
>> 사인 법칙
: ASA(Angle - Side - Angle) 또는 AAS (Angle - Angle - Side)
>> 코사인 법칙
: SSS(Side - Side - Side) 또는 SAS(Side - Angle - Side)
>> 탄젠트 법칙
: 사인 법칙과 코사인 법칙으로 해결하기 어려운 경우
>> 그래프
- 0 ~ 90º : +Sinθ, +Cosθ, +Tanθ --> Sin, Cos, Tan 모두 양수
- 90º ~ 180º : +Sinθ, -Cosθ, -Tanθ --> Sin만 양수
- 180º ~ 270º : -Sinθ, -Cosθ, +Tanθ --> Tan만 양수
- 270º ~ 360º : -Sinθ, +Cosθ, -Tanθ --> Cos만 양수
└ 게임 수학 실습
: 'Game Math' Scene 생성
>> Point Light 생성
: PositionY 살짝 올려줌
>> LightIntensity.cs 생성
: Point Light에 추가
>> Turret 추가 및 TurretRotation.cs 생성
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TurretRotation : MonoBehaviour
{
public float rotationSpeed = 60f;
private void Update()
{
transform.Rotate(Vector3.up, Time.deltaTime * rotationSpeed);
}
}
>> 삼각함수를 이용하여 Turret 회전
: 360도 회전하는 게 아니라 원하는 만큼만 감시하도록 구현
└ 벡터(Vector)와 스칼라(Scalar)
: 속도와 속력의 차이
>> 벡터의 연산
--> 덧셈의 결과는 AB가 아니라 OC가 맞음
>> 벡터의 크기
--> AB가 아니라 OC
>> 내적
: 두 벡터의 관계
>> 외적
: 두 벡터의 수직 벡터
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 59일차 (1) | 2025.02.20 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 58일차 (0) | 2025.02.19 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 56일차 (0) | 2025.02.17 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 55일차 (0) | 2025.02.14 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 54일차 (0) | 2025.02.13 |