using UnityEngine; using UnityEngine.SceneManagement; using TMPro; using UnityEngine.UI; // 게임 오버 상태를 표현하고, 게임 점수와 UI를 관리하는 게임 매니저 // 씬에는 단 하나의 게임 매니저만 존재할 수 있다. public class GameManager : MonoBehaviour { public static GameManager instance; // 싱글톤을 할당할 전역 변수 public bool isGameover = false; // 게임 오버 상태 public TextMeshProUGUI scoreText; // 점수를 출력할 UI 텍스트 public GameObject gameoverUI; // 게임 오버시 활성화 할 UI 게임 오브젝트 private int score = 0; // 게임 점수 private int currentLife; // 현재 남은 목숨 private int stageMileage = 0; // 현재 플레이 중 획득한 마일리지 private int totalMileage = 0; // 누적 보유 마일리지 private string tutorialMessage = ""; // 튜토리얼 안내 문구 private Image[] lifeIcons; // 왼쪽 HUD의 목숨 도장 표시 private TextMeshProUGUI tutorialText; // 화면 하단 튜토리얼 안내 public int maxLife = 3; // 시작 목숨 public int initialMileage = 500; // 첫 시작시 지급할 초기 마일리지 public bool grantInitialMileage = true; // 초기 마일리지 지급 여부 public bool saveMileageImmediately = true; // 스테이지 정산 전까지는 획득 즉시 누적한다. private const string TotalMileageKey = "UniRun.TotalMileage"; private const string InitialMileageGrantedKey = "UniRun.InitialMileageGranted"; private const string CharacterFaceResourcePath = "UI_JJ_Face"; private const string PassportPanelResourcePath = "UI_PassportPanel"; private const string LifeStampResourcePath = "UI_LifeStamp"; private const string PassportItemSlotResourcePath = "UI_PassportItemSlot"; // 게임의 스피드를 올리는 변수 public float gameSpeed = 1f; public float minGameSpeed = 1f; public float speedIncreaseRate = 0.05f; public float maxGameSpeed = 2f; public float hitSpeedPenalty = 0.3f; // 게임 시작과 동시에 싱글톤을 구성 void Awake() { // 싱글톤 변수 instance가 비어있는가? if(instance == null) { // instance가 비어있다면(null) 그곳에 자기 자신을 할당 instance = this; } else { // instance에 이미 다른 GameManager 오브젝트가 할당되어 있는 경우 // 씬에 두개 이상의 GameManager 오브젝트가 존재한다는 의미. // 싱글톤 오브젝트는 하나만 존재해야 하므로 자신의 게임 오브젝트를 파괴 Debug.LogWarning("씬에 두개 이상의 게임 매니저가 존재합니다!"); Destroy(gameObject); } } private void Start() { currentLife = maxLife; totalMileage = PlayerPrefs.GetInt(TotalMileageKey, 0); if(grantInitialMileage && PlayerPrefs.GetInt(InitialMileageGrantedKey, 0) == 0) { totalMileage += initialMileage; PlayerPrefs.SetInt(TotalMileageKey, totalMileage); PlayerPrefs.SetInt(InitialMileageGrantedKey, 1); PlayerPrefs.Save(); } BuildRuntimeHUD(); UpdateScoreUI(); } void Update() { // 게임 오버 상태에서 게임을 재시작할 수 있게 하는 처리 if(isGameover && Input.GetMouseButtonDown(0)) { SceneManager.LoadScene(SceneManager.GetActiveScene().name); } if(!isGameover) { // 게임 진행시 게임 속도는 시간에 따라 증가 gameSpeed += speedIncreaseRate * Time.deltaTime; // 게임 최대 속도는 max를 넘어서지 못하게 gameSpeed = Mathf.Min(gameSpeed, maxGameSpeed); UpdateScoreUI(); } } // 점수를 증가시키는 메서드 public void AddScore(int newScore) { if(!isGameover) { score += newScore; UpdateScoreUI(); } } public void SetTutorialMessage(string message) { tutorialMessage = message; UpdateScoreUI(); } // 마일리지 토큰을 먹었을 때 호출되는 메서드 public void AddMileage(int amount) { if(isGameover) { return; } stageMileage += amount; score += Mathf.Max(1, amount / 10); if(saveMileageImmediately) { totalMileage += amount; PlayerPrefs.SetInt(TotalMileageKey, totalMileage); PlayerPrefs.Save(); } UpdateScoreUI(); } // 플레이어가 장애물에 부딪혔을 때 목숨과 속도를 줄이는 메서드 public bool TakeDamage(int damage = 1) { if(isGameover) { return false; } currentLife -= damage; currentLife = Mathf.Max(currentLife, 0); gameSpeed -= hitSpeedPenalty; gameSpeed = Mathf.Max(gameSpeed, minGameSpeed); UpdateScoreUI(); return currentLife <= 0; } // 플레이어 캐릭터가 사망시 게임 오버를 실행하는 메서드 public void OnPlayerDead() { isGameover = true; gameoverUI.SetActive(true); } private void UpdateScoreUI() { if(scoreText != null) { scoreText.text = "SCORE " + score + "\nMILE " + stageMileage + "\nTOTAL " + totalMileage + "\nSPD " + gameSpeed.ToString("0.0"); } if(lifeIcons != null) { for(int i = 0; i < lifeIcons.Length; i++) { if(lifeIcons[i] == null) { continue; } bool hasLife = i < currentLife; lifeIcons[i].color = hasLife ? Color.white : new Color(0.34f, 0.34f, 0.34f, 0.72f); } } if(tutorialText != null) { tutorialText.text = tutorialMessage; tutorialText.gameObject.SetActive(!string.IsNullOrEmpty(tutorialMessage)); } } private void BuildRuntimeHUD() { if(scoreText == null) { return; } Transform canvasTransform = scoreText.transform.parent; ConfigureRightInfoText(); CreateStatusHUD(canvasTransform); CreateTutorialText(canvasTransform); } private void ConfigureRightInfoText() { RectTransform rectTransform = scoreText.rectTransform; rectTransform.anchorMin = new Vector2(1f, 1f); rectTransform.anchorMax = new Vector2(1f, 1f); rectTransform.pivot = new Vector2(1f, 1f); rectTransform.anchoredPosition = new Vector2(-12f, -9f); rectTransform.sizeDelta = new Vector2(132f, 62f); scoreText.alignment = TextAlignmentOptions.TopRight; scoreText.fontSize = 9.5f; scoreText.lineSpacing = -10f; scoreText.color = new Color(0.94f, 0.97f, 1f, 0.92f); scoreText.raycastTarget = false; scoreText.textWrappingMode = TextWrappingModes.NoWrap; } private void CreateStatusHUD(Transform canvasTransform) { GameObject hudObject = new GameObject("Status HUD", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); hudObject.transform.SetParent(canvasTransform, false); RectTransform hudRect = hudObject.GetComponent(); hudRect.anchorMin = new Vector2(0f, 1f); hudRect.anchorMax = new Vector2(0f, 1f); hudRect.pivot = new Vector2(0f, 1f); hudRect.anchoredPosition = new Vector2(10f, -8f); hudRect.sizeDelta = new Vector2(176f, 54f); Image hudBackground = hudObject.GetComponent(); hudBackground.sprite = Resources.Load(PassportPanelResourcePath); hudBackground.color = hudBackground.sprite != null ? Color.white : new Color(0.04f, 0.08f, 0.11f, 0.68f); hudBackground.raycastTarget = false; CreateFaceIcon(hudObject.transform); CreateLifeIcons(hudObject.transform); CreateItemSlots(hudObject.transform); } private void CreateFaceIcon(Transform parent) { GameObject faceFrame = new GameObject("Face Frame", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); faceFrame.transform.SetParent(parent, false); RectTransform frameRect = faceFrame.GetComponent(); frameRect.anchorMin = new Vector2(0f, 1f); frameRect.anchorMax = new Vector2(0f, 1f); frameRect.pivot = new Vector2(0f, 1f); frameRect.anchoredPosition = new Vector2(9f, -10f); frameRect.sizeDelta = new Vector2(29f, 29f); Image frameImage = faceFrame.GetComponent(); frameImage.color = new Color(0.84f, 0.87f, 0.80f, 0.58f); frameImage.raycastTarget = false; GameObject faceObject = new GameObject("Character Face", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); faceObject.transform.SetParent(faceFrame.transform, false); RectTransform faceRect = faceObject.GetComponent(); faceRect.anchorMin = Vector2.zero; faceRect.anchorMax = Vector2.one; faceRect.offsetMin = new Vector2(2f, 2f); faceRect.offsetMax = new Vector2(-2f, -2f); Image faceImage = faceObject.GetComponent(); faceImage.sprite = Resources.Load(CharacterFaceResourcePath); faceImage.preserveAspect = true; faceImage.raycastTarget = false; } private void CreateLifeIcons(Transform parent) { Sprite lifeSprite = Resources.Load(LifeStampResourcePath); int iconCount = Mathf.Max(maxLife, 1); lifeIcons = new Image[iconCount]; for(int i = 0; i < iconCount; i++) { GameObject lifeObject = new GameObject("Life Stamp " + (i + 1), typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); lifeObject.transform.SetParent(parent, false); RectTransform lifeRect = lifeObject.GetComponent(); lifeRect.anchorMin = new Vector2(0f, 1f); lifeRect.anchorMax = new Vector2(0f, 1f); lifeRect.pivot = new Vector2(0f, 1f); lifeRect.anchoredPosition = new Vector2(50f + (i * 21f), -10f); lifeRect.sizeDelta = new Vector2(18f, 18f); Image lifeImage = lifeObject.GetComponent(); lifeImage.sprite = lifeSprite; lifeImage.color = Color.white; lifeImage.preserveAspect = true; lifeImage.raycastTarget = false; lifeIcons[i] = lifeImage; } } private void CreateItemSlots(Transform parent) { Sprite slotSprite = Resources.Load(PassportItemSlotResourcePath); for(int i = 0; i < 3; i++) { GameObject slotObject = new GameObject("Item Slot " + (i + 1), typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); slotObject.transform.SetParent(parent, false); RectTransform slotRect = slotObject.GetComponent(); slotRect.anchorMin = new Vector2(0f, 1f); slotRect.anchorMax = new Vector2(0f, 1f); slotRect.pivot = new Vector2(0f, 1f); slotRect.anchoredPosition = new Vector2(50f + (i * 21f), -31f); slotRect.sizeDelta = new Vector2(18f, 18f); Image slotImage = slotObject.GetComponent(); slotImage.sprite = slotSprite; slotImage.color = slotSprite != null ? new Color(1f, 1f, 1f, 0.92f) : new Color(0.92f, 0.97f, 1f, 0.22f); slotImage.preserveAspect = true; slotImage.raycastTarget = false; } } private void CreateTutorialText(Transform canvasTransform) { GameObject tutorialObject = new GameObject("Tutorial Text", typeof(RectTransform), typeof(CanvasRenderer)); tutorialObject.transform.SetParent(canvasTransform, false); RectTransform tutorialRect = tutorialObject.GetComponent(); tutorialRect.anchorMin = new Vector2(0.5f, 0f); tutorialRect.anchorMax = new Vector2(0.5f, 0f); tutorialRect.pivot = new Vector2(0.5f, 0f); tutorialRect.anchoredPosition = new Vector2(0f, 18f); tutorialRect.sizeDelta = new Vector2(360f, 24f); tutorialText = tutorialObject.AddComponent(); tutorialText.font = scoreText.font; tutorialText.alignment = TextAlignmentOptions.Center; tutorialText.fontSize = 9.5f; tutorialText.fontStyle = FontStyles.Bold; tutorialText.color = new Color(0.96f, 0.98f, 1f, 0.95f); tutorialText.raycastTarget = false; tutorialText.textWrappingMode = TextWrappingModes.NoWrap; tutorialText.gameObject.SetActive(false); } }