1608 lines
60 KiB
C#
1608 lines
60 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using TMPro;
|
|
using UnityEngine.UI;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
// 게임 오버 상태를 표현하고, 게임 점수와 UI를 관리하는 게임 매니저
|
|
// 씬에는 단 하나의 게임 매니저만 존재할 수 있다.
|
|
public class GameManager : MonoBehaviour {
|
|
public enum TutorialGuideType {
|
|
None,
|
|
Jump,
|
|
DoubleJump,
|
|
Slide,
|
|
Damage,
|
|
Items
|
|
}
|
|
|
|
public enum TutorialItemType {
|
|
HealthSushi,
|
|
InvincibleRamen,
|
|
Shield,
|
|
SpeedShoes,
|
|
MileageCard
|
|
}
|
|
|
|
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 TutorialGuideType tutorialGuideType = TutorialGuideType.None; // 현재 튜토리얼 안내 이미지 종류
|
|
private string tutorialTitle = ""; // 튜토리얼 안내 제목
|
|
private string tutorialMessage = ""; // 튜토리얼 안내 문구
|
|
private Image[] lifeIcons; // 왼쪽 HUD의 목숨 도장 표시
|
|
private GameObject tutorialGuideObject; // 화면 상단 이미지 튜토리얼 패널
|
|
private Image tutorialMainImage; // 점프/슬라이드 대표 이미지
|
|
private TextMeshProUGUI tutorialMultiplierText; // 2단 점프 x2 표시
|
|
private TextMeshProUGUI tutorialTitleText; // 튜토리얼 제목
|
|
private TextMeshProUGUI tutorialBodyText; // 튜토리얼 설명
|
|
private Image[] tutorialItemImages; // 아이템 순서 설명 이미지
|
|
private TMP_FontAsset tutorialFontAsset; // 한글 튜토리얼 문구용 런타임 폰트
|
|
private float tutorialGuideSequenceStartTime; // 튜토리얼 안내 시작 시각
|
|
private int currentTutorialGuideSequenceIndex = -1; // 현재 표시 중인 안내 단계
|
|
private bool tutorialGuideSequenceComplete = false; // 기본 튜토리얼 안내 완료 여부
|
|
private MapDefinition currentMapDefinition; // 현재 플레이 중인 맵 데이터
|
|
private float stageElapsedTime = 0f; // 현재 맵 진행 시간
|
|
private float stageDistance = 0f; // 현재 맵 진행 거리
|
|
private bool stageCompleted = false; // 결과 화면이 이미 처리되었는지 여부
|
|
private Coroutine tutorialGuidePauseRoutine; // 안내를 보여주는 동안 잠깐 멈추는 코루틴
|
|
private bool tutorialGuidePauseActive = false; // 안내 일시정지 중인지 여부
|
|
private float tutorialGuidePreviousTimeScale = 1f; // 안내 일시정지 전 시간 배율
|
|
private int tutorialGuideVersion = 0; // 오래된 자동 숨김 요청을 무시하기 위한 버전
|
|
private FinishGate activeFinishGate; // 목표 지점에 나타나는 맵별 클리어 게이트
|
|
private bool finishGateSpawned = false; // 현재 맵에서 도착 게이트를 이미 만들었는지 여부
|
|
|
|
public int maxLife = 3; // 시작 목숨
|
|
public int initialMileage = 500; // 첫 시작시 지급할 초기 마일리지
|
|
public bool grantInitialMileage = true; // 초기 마일리지 지급 여부
|
|
public bool saveMileageImmediately = true; // 스테이지 정산 전까지는 획득 즉시 누적한다.
|
|
public bool playTutorialGuideSequence = false; // 코스 데이터가 없을 때만 시간 기준 안내를 보여준다.
|
|
public bool pauseGameplayForTutorialGuide = true; // 안내가 뜨는 동안 게임을 잠깐 멈춘다.
|
|
public float tutorialGuidePauseDuration = 2f; // 안내를 보여주고 자동으로 숨길 시간
|
|
public float itemInvincibleDuration = 2.2f; // 라멘 아이템 무적 지속 시간
|
|
public float itemSpeedBoostAmount = 0.35f; // 운동화 아이템 즉시 속도 보정량
|
|
public float mileageBonusDuration = 7f; // 마일리지 카드 보너스 지속 시간
|
|
public int mileageBonusMultiplier = 2; // 마일리지 카드 획득 배율
|
|
public float finishGateSpawnX = 20f; // 도착 게이트가 처음 나타나는 x 위치
|
|
public float finishGatePlayerX = -6f; // 게이트가 클리어 판정을 내릴 플레이어 기준 x 위치
|
|
public float finishGateGroundY = -2.42f; // 도착 게이트가 바닥에 붙는 y 위치
|
|
public float finishGateFallbackDistance = 18f; // 게이트 트리거를 놓쳤을 때의 안전 클리어 거리
|
|
|
|
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";
|
|
private const float TutorialJumpGuideDuration = 3.5f;
|
|
private const float TutorialDoubleJumpGuideDuration = 3.5f;
|
|
private const float TutorialSlideGuideDuration = 3.5f;
|
|
private const float TutorialItemGuideDuration = 5.2f;
|
|
|
|
// 게임의 스피드를 올리는 변수
|
|
public float gameSpeed = 1f;
|
|
public float minGameSpeed = 1f;
|
|
public float speedIncreaseRate = 0.05f;
|
|
public float maxGameSpeed = 2f;
|
|
public float hitSpeedPenalty = 0.3f;
|
|
private int shieldCharges = 0; // 방어막 아이템으로 막을 수 있는 남은 피격 횟수
|
|
private float itemInvincibleEndTime = 0f; // 라멘 무적 종료 시각
|
|
private float mileageBonusEndTime = 0f; // 마일리지 카드 보너스 종료 시각
|
|
|
|
public bool IsTutorialGuidePaused {
|
|
get { return tutorialGuidePauseActive; }
|
|
}
|
|
|
|
// 게임 시작과 동시에 싱글톤을 구성
|
|
void Awake() {
|
|
// 싱글톤 변수 instance가 비어있는가?
|
|
if(instance == null)
|
|
{
|
|
// instance가 비어있다면(null) 그곳에 자기 자신을 할당
|
|
instance = this;
|
|
}
|
|
else
|
|
{
|
|
// instance에 이미 다른 GameManager 오브젝트가 할당되어 있는 경우
|
|
|
|
// 씬에 두개 이상의 GameManager 오브젝트가 존재한다는 의미.
|
|
// 싱글톤 오브젝트는 하나만 존재해야 하므로 자신의 게임 오브젝트를 파괴
|
|
Debug.LogWarning("씬에 두개 이상의 게임 매니저가 존재합니다!");
|
|
Destroy(gameObject);
|
|
}
|
|
}
|
|
|
|
private void Start() {
|
|
currentLife = maxLife;
|
|
totalMileage = GameFlowManager.Instance != null
|
|
? GameFlowManager.Instance.SaveData.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();
|
|
BeginMap(GameFlowManager.Instance != null
|
|
? GameFlowManager.Instance.CurrentMap
|
|
: MapDatabase.GetMap(MapId.TutorialIncheon));
|
|
SyncTotalMileageToFlow();
|
|
UpdateScoreUI();
|
|
}
|
|
|
|
void Update() {
|
|
// 게임 오버 상태에서 게임을 재시작할 수 있게 하는 처리
|
|
if(isGameover && GameFlowManager.Instance == null && Input.GetMouseButtonDown(0))
|
|
{
|
|
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
|
|
}
|
|
|
|
if(!GameFlowManager.IsGameplayActive)
|
|
{
|
|
UpdateScoreUI();
|
|
return;
|
|
}
|
|
|
|
if(!isGameover)
|
|
{
|
|
UpdateTutorialGuideSequence();
|
|
|
|
stageElapsedTime += Time.deltaTime;
|
|
|
|
// 게임 진행시 게임 속도는 시간에 따라 증가
|
|
gameSpeed += speedIncreaseRate * Time.deltaTime;
|
|
// 게임 최대 속도는 max를 넘어서지 못하게
|
|
gameSpeed = Mathf.Min(gameSpeed, maxGameSpeed);
|
|
|
|
stageDistance += 10f * gameSpeed * Time.deltaTime;
|
|
UpdateFinishGate();
|
|
|
|
if(currentMapDefinition != null && currentMapDefinition.targetDistance > 0f && stageDistance >= currentMapDefinition.targetDistance)
|
|
{
|
|
if(!ShouldUseFinishGate() || stageDistance >= currentMapDefinition.targetDistance + finishGateFallbackDistance)
|
|
{
|
|
CompleteStage(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(currentMapDefinition != null && currentMapDefinition.timeLimit > 0f && stageElapsedTime >= currentMapDefinition.timeLimit)
|
|
{
|
|
CompleteStage(false);
|
|
return;
|
|
}
|
|
|
|
UpdateScoreUI();
|
|
}
|
|
}
|
|
|
|
public void BeginMap(MapDefinition mapDefinition) {
|
|
StopTutorialGuidePause(true);
|
|
Time.timeScale = 1f;
|
|
|
|
currentMapDefinition = mapDefinition != null
|
|
? mapDefinition
|
|
: MapDatabase.GetMap(MapId.TutorialIncheon);
|
|
BackgroundLoop.ApplyResourceBackground(currentMapDefinition.backgroundResourcePath);
|
|
|
|
score = 0;
|
|
stageMileage = 0;
|
|
currentLife = maxLife;
|
|
gameSpeed = minGameSpeed;
|
|
shieldCharges = 0;
|
|
itemInvincibleEndTime = 0f;
|
|
mileageBonusEndTime = 0f;
|
|
stageElapsedTime = 0f;
|
|
stageDistance = 0f;
|
|
stageCompleted = false;
|
|
finishGateSpawned = false;
|
|
isGameover = false;
|
|
tutorialGuideSequenceComplete = false;
|
|
currentTutorialGuideSequenceIndex = -1;
|
|
SetTutorialGuide(TutorialGuideType.None, "", "");
|
|
RemoveActiveFinishGate();
|
|
|
|
if(gameoverUI != null)
|
|
{
|
|
gameoverUI.SetActive(false);
|
|
}
|
|
|
|
PlatformSpawner platformSpawner = FindFirstObjectByType<PlatformSpawner>();
|
|
if(platformSpawner != null)
|
|
{
|
|
platformSpawner.ApplyMap(currentMapDefinition);
|
|
}
|
|
|
|
StartTutorialGuideSequence();
|
|
UpdateScoreUI();
|
|
}
|
|
|
|
// 점수를 증가시키는 메서드
|
|
public void AddScore(int newScore) {
|
|
|
|
if(!isGameover)
|
|
{
|
|
score += newScore;
|
|
UpdateScoreUI();
|
|
}
|
|
|
|
}
|
|
|
|
public void SetTutorialMessage(string message) {
|
|
SetTutorialGuide(TutorialGuideType.None, "", message);
|
|
}
|
|
|
|
public void SetTutorialGuide(TutorialGuideType guideType, string title, string message) {
|
|
tutorialGuideVersion++;
|
|
tutorialGuideType = guideType;
|
|
tutorialTitle = title;
|
|
tutorialMessage = message;
|
|
|
|
if(!HasTutorialGuideContent(guideType, title, message))
|
|
{
|
|
StopTutorialGuidePause(true);
|
|
UpdateScoreUI();
|
|
return;
|
|
}
|
|
|
|
UpdateScoreUI();
|
|
TryPauseForTutorialGuide(tutorialGuideVersion);
|
|
}
|
|
|
|
private bool HasTutorialGuideContent(TutorialGuideType guideType, string title, string message) {
|
|
return guideType != TutorialGuideType.None
|
|
|| !string.IsNullOrEmpty(title)
|
|
|| !string.IsNullOrEmpty(message);
|
|
}
|
|
|
|
private void TryPauseForTutorialGuide(int guideVersion) {
|
|
if(!pauseGameplayForTutorialGuide
|
|
|| tutorialGuidePauseDuration <= 0f
|
|
|| isGameover
|
|
|| !GameFlowManager.IsGameplayActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(!tutorialGuidePauseActive)
|
|
{
|
|
tutorialGuidePreviousTimeScale = Time.timeScale;
|
|
tutorialGuidePauseActive = true;
|
|
}
|
|
|
|
Time.timeScale = 0f;
|
|
|
|
if(tutorialGuidePauseRoutine != null)
|
|
{
|
|
StopCoroutine(tutorialGuidePauseRoutine);
|
|
}
|
|
|
|
tutorialGuidePauseRoutine = StartCoroutine(HideTutorialGuideAfterPause(guideVersion));
|
|
}
|
|
|
|
private IEnumerator HideTutorialGuideAfterPause(int guideVersion) {
|
|
yield return new WaitForSecondsRealtime(tutorialGuidePauseDuration);
|
|
|
|
tutorialGuidePauseRoutine = null;
|
|
|
|
if(guideVersion == tutorialGuideVersion)
|
|
{
|
|
tutorialGuideVersion++;
|
|
tutorialGuideType = TutorialGuideType.None;
|
|
tutorialTitle = "";
|
|
tutorialMessage = "";
|
|
UpdateScoreUI();
|
|
}
|
|
|
|
RestoreTutorialGuideTimeScale();
|
|
}
|
|
|
|
private void StopTutorialGuidePause(bool restoreTimeScale) {
|
|
if(tutorialGuidePauseRoutine != null)
|
|
{
|
|
StopCoroutine(tutorialGuidePauseRoutine);
|
|
tutorialGuidePauseRoutine = null;
|
|
}
|
|
|
|
if(restoreTimeScale)
|
|
{
|
|
RestoreTutorialGuideTimeScale();
|
|
}
|
|
}
|
|
|
|
private void RestoreTutorialGuideTimeScale() {
|
|
if(!tutorialGuidePauseActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Time.timeScale = Mathf.Approximately(tutorialGuidePreviousTimeScale, 0f)
|
|
? 1f
|
|
: tutorialGuidePreviousTimeScale;
|
|
tutorialGuidePauseActive = false;
|
|
}
|
|
|
|
private void StartTutorialGuideSequence() {
|
|
if(!ShouldUseTimeBasedTutorialGuide())
|
|
{
|
|
tutorialGuideSequenceComplete = true;
|
|
return;
|
|
}
|
|
|
|
tutorialGuideSequenceStartTime = Time.time;
|
|
tutorialGuideSequenceComplete = false;
|
|
currentTutorialGuideSequenceIndex = -1;
|
|
UpdateTutorialGuideSequence();
|
|
}
|
|
|
|
private void UpdateTutorialGuideSequence() {
|
|
if(!ShouldUseTimeBasedTutorialGuide() || tutorialGuideSequenceComplete)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float elapsedTime = Time.time - tutorialGuideSequenceStartTime;
|
|
int guideIndex = GetTutorialGuideSequenceIndex(elapsedTime);
|
|
|
|
if(guideIndex == currentTutorialGuideSequenceIndex)
|
|
{
|
|
return;
|
|
}
|
|
|
|
currentTutorialGuideSequenceIndex = guideIndex;
|
|
ApplyTutorialGuideSequenceStep(guideIndex);
|
|
}
|
|
|
|
private bool ShouldUseTimeBasedTutorialGuide() {
|
|
return playTutorialGuideSequence && !HasCourseDrivenTutorialGuide();
|
|
}
|
|
|
|
private bool HasCourseDrivenTutorialGuide() {
|
|
if(currentMapDefinition == null
|
|
|| currentMapDefinition.openingSteps == null
|
|
|| currentMapDefinition.openingSteps.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for(int i = 0; i < currentMapDefinition.openingSteps.Length; i++)
|
|
{
|
|
PlatformSpawner.TutorialStep step = currentMapDefinition.openingSteps[i];
|
|
if(step != null
|
|
&& (step.guideType != TutorialGuideType.None
|
|
|| !string.IsNullOrEmpty(step.title)
|
|
|| !string.IsNullOrEmpty(step.message)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private int GetTutorialGuideSequenceIndex(float elapsedTime) {
|
|
if(elapsedTime < TutorialJumpGuideDuration)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
elapsedTime -= TutorialJumpGuideDuration;
|
|
if(elapsedTime < TutorialDoubleJumpGuideDuration)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
elapsedTime -= TutorialDoubleJumpGuideDuration;
|
|
if(elapsedTime < TutorialSlideGuideDuration)
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
elapsedTime -= TutorialSlideGuideDuration;
|
|
if(elapsedTime < TutorialItemGuideDuration)
|
|
{
|
|
return 3;
|
|
}
|
|
|
|
return 4;
|
|
}
|
|
|
|
private void ApplyTutorialGuideSequenceStep(int guideIndex) {
|
|
switch(guideIndex)
|
|
{
|
|
case 0:
|
|
SetTutorialGuide(TutorialGuideType.Jump, "점프", "왼쪽 클릭으로 가볍게 뛰어오릅니다.");
|
|
break;
|
|
case 1:
|
|
SetTutorialGuide(TutorialGuideType.DoubleJump, "2단 점프", "공중에서 왼쪽 클릭을 한 번 더 누릅니다.");
|
|
break;
|
|
case 2:
|
|
SetTutorialGuide(TutorialGuideType.Slide, "슬라이드", "오른쪽 클릭을 누르고 있으면 낮게 지나갑니다.");
|
|
break;
|
|
case 3:
|
|
SetTutorialGuide(TutorialGuideType.Items, "아이템", "초밥 -> 라멘 -> 방어막 -> 운동화 -> 마일리지 카드 순서입니다.");
|
|
break;
|
|
default:
|
|
tutorialGuideSequenceComplete = true;
|
|
SetTutorialGuide(TutorialGuideType.None, "", "");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 마일리지 토큰을 먹었을 때 호출되는 메서드
|
|
public void AddMileage(int amount) {
|
|
if(isGameover)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int finalAmount = IsMileageBonusActive()
|
|
? amount * Mathf.Max(1, mileageBonusMultiplier)
|
|
: amount;
|
|
|
|
stageMileage += finalAmount;
|
|
score += Mathf.Max(1, finalAmount / 10);
|
|
|
|
if(saveMileageImmediately)
|
|
{
|
|
totalMileage += finalAmount;
|
|
PlayerPrefs.SetInt(TotalMileageKey, totalMileage);
|
|
PlayerPrefs.Save();
|
|
SyncTotalMileageToFlow();
|
|
}
|
|
|
|
UpdateScoreUI();
|
|
}
|
|
|
|
// 플레이어가 장애물에 부딪혔을 때 목숨과 속도를 줄이는 메서드
|
|
public bool TakeDamage(int damage = 1) {
|
|
if(isGameover || !GameFlowManager.IsGameplayActive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
currentLife -= damage;
|
|
currentLife = Mathf.Max(currentLife, 0);
|
|
|
|
gameSpeed -= hitSpeedPenalty;
|
|
gameSpeed = Mathf.Max(gameSpeed, minGameSpeed);
|
|
|
|
UpdateScoreUI();
|
|
|
|
return currentLife <= 0;
|
|
}
|
|
|
|
public void ApplyTutorialItem(TutorialItemType itemType) {
|
|
if(isGameover || !GameFlowManager.IsGameplayActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch(itemType)
|
|
{
|
|
case TutorialItemType.HealthSushi:
|
|
currentLife = Mathf.Min(maxLife, currentLife + 1);
|
|
AddScore(3);
|
|
if(ShouldShowItemEffectGuide())
|
|
{
|
|
SetTutorialGuide(TutorialGuideType.Items, "초밥", "방금 잃은 목숨을 1 회복합니다.");
|
|
}
|
|
break;
|
|
case TutorialItemType.InvincibleRamen:
|
|
itemInvincibleEndTime = Time.time + itemInvincibleDuration;
|
|
if(ShouldShowItemEffectGuide())
|
|
{
|
|
SetTutorialGuide(TutorialGuideType.Items, "라멘", "짧은 시간 동안 다음 충돌 피해를 받지 않습니다.");
|
|
}
|
|
break;
|
|
case TutorialItemType.Shield:
|
|
shieldCharges = Mathf.Max(shieldCharges, 1);
|
|
if(ShouldShowItemEffectGuide())
|
|
{
|
|
SetTutorialGuide(TutorialGuideType.Items, "방어막", "다음 충돌 1회를 방어막이 대신 막습니다.");
|
|
}
|
|
break;
|
|
case TutorialItemType.SpeedShoes:
|
|
gameSpeed = Mathf.Min(maxGameSpeed, gameSpeed + itemSpeedBoostAmount);
|
|
if(ShouldShowItemEffectGuide())
|
|
{
|
|
SetTutorialGuide(TutorialGuideType.Items, "운동화", "속도를 즉시 회복해 늦어진 일정을 따라잡습니다.");
|
|
}
|
|
break;
|
|
case TutorialItemType.MileageCard:
|
|
mileageBonusEndTime = Time.time + mileageBonusDuration;
|
|
if(ShouldShowItemEffectGuide())
|
|
{
|
|
SetTutorialGuide(TutorialGuideType.Items, "마일리지 카드", "잠시 동안 먹는 마일리지가 두 배로 들어옵니다.");
|
|
}
|
|
break;
|
|
}
|
|
|
|
UpdateScoreUI();
|
|
}
|
|
|
|
public bool TryBlockDamageWithItem() {
|
|
if(isGameover || !GameFlowManager.IsGameplayActive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if(IsItemInvincibleActive())
|
|
{
|
|
if(ShouldShowItemEffectGuide())
|
|
{
|
|
SetTutorialGuide(TutorialGuideType.Items, "라멘", "무적 중이라 이번 충돌은 피해 없이 지나갑니다.");
|
|
}
|
|
|
|
SpawnItemBlockFeedback(TutorialItemType.InvincibleRamen);
|
|
return true;
|
|
}
|
|
|
|
if(shieldCharges > 0)
|
|
{
|
|
shieldCharges--;
|
|
SpawnItemBlockFeedback(TutorialItemType.Shield);
|
|
if(ShouldShowItemEffectGuide())
|
|
{
|
|
SetTutorialGuide(TutorialGuideType.Items, "방어막", "방어막이 충돌 피해를 한 번 막았습니다.");
|
|
}
|
|
UpdateScoreUI();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void SpawnItemBlockFeedback(TutorialItemType itemType) {
|
|
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
|
|
if(playerObject == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ItemFeedbackEffect.Spawn(playerObject.transform.position, null, itemType, true);
|
|
}
|
|
|
|
private bool ShouldShowItemEffectGuide() {
|
|
return currentMapDefinition == null || !currentMapDefinition.isTutorial;
|
|
}
|
|
|
|
private bool IsItemInvincibleActive() {
|
|
return Time.time < itemInvincibleEndTime;
|
|
}
|
|
|
|
private bool IsMileageBonusActive() {
|
|
return Time.time < mileageBonusEndTime;
|
|
}
|
|
|
|
// 플레이어 캐릭터가 사망시 게임 오버를 실행하는 메서드
|
|
public void OnPlayerDead() {
|
|
isGameover = true;
|
|
|
|
if(gameoverUI != null)
|
|
{
|
|
gameoverUI.SetActive(GameFlowManager.Instance == null);
|
|
}
|
|
|
|
CompleteStage(false);
|
|
}
|
|
|
|
public void CompleteCurrentStageFromFinishGate() {
|
|
if(currentMapDefinition != null && currentMapDefinition.targetDistance > 0f)
|
|
{
|
|
stageDistance = Mathf.Max(stageDistance, currentMapDefinition.targetDistance);
|
|
}
|
|
|
|
CompleteStage(true);
|
|
}
|
|
|
|
private void CompleteStage(bool cleared) {
|
|
if(stageCompleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StopTutorialGuidePause(true);
|
|
RemoveActiveFinishGate();
|
|
|
|
stageCompleted = true;
|
|
isGameover = true;
|
|
|
|
if(GameFlowManager.Instance != null)
|
|
{
|
|
GameFlowManager.Instance.CompleteMap(cleared, score, stageMileage, stageElapsedTime, stageDistance);
|
|
}
|
|
else if(gameoverUI != null)
|
|
{
|
|
gameoverUI.SetActive(true);
|
|
}
|
|
}
|
|
|
|
private void UpdateFinishGate() {
|
|
if(!ShouldUseFinishGate() || finishGateSpawned || stageCompleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float spawnLeadDistance = Mathf.Max(0f, finishGateSpawnX - finishGatePlayerX);
|
|
float spawnDistance = Mathf.Max(0f, currentMapDefinition.targetDistance - spawnLeadDistance);
|
|
if(stageDistance < spawnDistance)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameObject gateObject = new GameObject("Finish Gate - " + currentMapDefinition.displayName);
|
|
gateObject.transform.position = new Vector3(finishGateSpawnX, finishGateGroundY, 0f);
|
|
activeFinishGate = gateObject.AddComponent<FinishGate>();
|
|
activeFinishGate.Setup(currentMapDefinition.finishGateStyle);
|
|
finishGateSpawned = true;
|
|
}
|
|
|
|
private bool ShouldUseFinishGate() {
|
|
return currentMapDefinition != null
|
|
&& currentMapDefinition.finishGateStyle != FinishGateStyle.None
|
|
&& currentMapDefinition.targetDistance > 0f;
|
|
}
|
|
|
|
private void RemoveActiveFinishGate() {
|
|
if(activeFinishGate != null)
|
|
{
|
|
Destroy(activeFinishGate.gameObject);
|
|
activeFinishGate = null;
|
|
}
|
|
}
|
|
|
|
private void SyncTotalMileageToFlow() {
|
|
if(GameFlowManager.Instance != null)
|
|
{
|
|
GameFlowManager.Instance.SetTotalMileage(totalMileage);
|
|
}
|
|
}
|
|
|
|
private void UpdateScoreUI() {
|
|
if(scoreText != null)
|
|
{
|
|
string mapName = currentMapDefinition != null ? currentMapDefinition.displayName : "RUN";
|
|
string distanceText = currentMapDefinition != null
|
|
? Mathf.FloorToInt(stageDistance) + "/" + Mathf.FloorToInt(currentMapDefinition.targetDistance) + "m"
|
|
: Mathf.FloorToInt(stageDistance) + "m";
|
|
string timeText = currentMapDefinition != null && currentMapDefinition.timeLimit > 0f
|
|
? Mathf.Max(0f, currentMapDefinition.timeLimit - stageElapsedTime).ToString("0") + "s"
|
|
: stageElapsedTime.ToString("0") + "s";
|
|
|
|
scoreText.text = mapName
|
|
+ "\nSCORE " + score
|
|
+ "\nMILE " + stageMileage
|
|
+ "\nTOTAL " + totalMileage
|
|
+ "\nDIST " + distanceText
|
|
+ "\nTIME " + timeText
|
|
+ "\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
|
|
? GetActiveLifeIconColor(lifeIcons[i])
|
|
: new Color(0.34f, 0.34f, 0.34f, 0.72f);
|
|
}
|
|
}
|
|
|
|
RefreshTutorialGuide();
|
|
}
|
|
|
|
private void RefreshTutorialGuide() {
|
|
if(tutorialGuideObject == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool hasGuide = tutorialGuideType != TutorialGuideType.None
|
|
|| !string.IsNullOrEmpty(tutorialTitle)
|
|
|| !string.IsNullOrEmpty(tutorialMessage);
|
|
tutorialGuideObject.SetActive(hasGuide);
|
|
|
|
if(!hasGuide)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(tutorialTitleText != null)
|
|
{
|
|
tutorialTitleText.text = tutorialTitle;
|
|
}
|
|
|
|
if(tutorialBodyText != null)
|
|
{
|
|
tutorialBodyText.text = tutorialMessage;
|
|
}
|
|
|
|
bool showItemRow = tutorialGuideType == TutorialGuideType.Items;
|
|
Sprite mainSprite = GetTutorialMainSprite(tutorialGuideType);
|
|
bool showFullText = !showItemRow && mainSprite == null;
|
|
UpdateTutorialTextLayout(showItemRow, showFullText);
|
|
|
|
if(tutorialMainImage != null)
|
|
{
|
|
tutorialMainImage.gameObject.SetActive(!showItemRow && mainSprite != null);
|
|
tutorialMainImage.sprite = mainSprite;
|
|
}
|
|
|
|
if(tutorialMultiplierText != null)
|
|
{
|
|
tutorialMultiplierText.gameObject.SetActive(tutorialGuideType == TutorialGuideType.DoubleJump);
|
|
}
|
|
|
|
if(tutorialItemImages != null)
|
|
{
|
|
for(int i = 0; i < tutorialItemImages.Length; i++)
|
|
{
|
|
if(tutorialItemImages[i] != null)
|
|
{
|
|
tutorialItemImages[i].gameObject.SetActive(showItemRow && tutorialItemImages[i].sprite != null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateTutorialTextLayout(bool showItemRow, bool showFullText) {
|
|
Vector2 textPosition = new Vector2(66f, -8f);
|
|
Vector2 textSize = new Vector2(268f, 17f);
|
|
Vector2 bodyPosition = new Vector2(66f, -26f);
|
|
Vector2 bodySize = new Vector2(268f, 31f);
|
|
|
|
if(showItemRow)
|
|
{
|
|
textPosition = new Vector2(154f, -8f);
|
|
textSize = new Vector2(178f, 17f);
|
|
bodyPosition = new Vector2(154f, -26f);
|
|
bodySize = new Vector2(178f, 31f);
|
|
}
|
|
else if(showFullText)
|
|
{
|
|
textPosition = new Vector2(16f, -8f);
|
|
textSize = new Vector2(316f, 17f);
|
|
bodyPosition = new Vector2(16f, -26f);
|
|
bodySize = new Vector2(316f, 31f);
|
|
}
|
|
|
|
if(tutorialTitleText != null)
|
|
{
|
|
tutorialTitleText.rectTransform.anchoredPosition = textPosition;
|
|
tutorialTitleText.rectTransform.sizeDelta = textSize;
|
|
}
|
|
|
|
if(tutorialBodyText != null)
|
|
{
|
|
tutorialBodyText.rectTransform.anchoredPosition = bodyPosition;
|
|
tutorialBodyText.rectTransform.sizeDelta = bodySize;
|
|
}
|
|
}
|
|
|
|
private Color GetActiveLifeIconColor(Image lifeIcon) {
|
|
if(lifeIcon != null && lifeIcon.sprite == null)
|
|
{
|
|
return new Color(0.94f, 0.18f, 0.12f, 0.9f);
|
|
}
|
|
|
|
return Color.white;
|
|
}
|
|
|
|
private void BuildRuntimeHUD() {
|
|
if(scoreText == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Transform canvasTransform = scoreText.transform.parent;
|
|
|
|
ConfigureRightInfoText();
|
|
CreateStatusHUD(canvasTransform);
|
|
CreateTutorialGuide(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(176f, 96f);
|
|
|
|
scoreText.alignment = TextAlignmentOptions.TopRight;
|
|
scoreText.fontSize = 8.5f;
|
|
scoreText.lineSpacing = -13f;
|
|
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<RectTransform>();
|
|
hudRect.anchorMin = new Vector2(0f, 1f);
|
|
hudRect.anchorMax = new Vector2(0f, 1f);
|
|
hudRect.pivot = new Vector2(0f, 1f);
|
|
hudRect.anchoredPosition = new Vector2(8f, -7f);
|
|
hudRect.sizeDelta = new Vector2(108f, 34f);
|
|
|
|
Image hudBackground = hudObject.GetComponent<Image>();
|
|
hudBackground.sprite = LoadHudSprite(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<RectTransform>();
|
|
frameRect.anchorMin = new Vector2(0f, 1f);
|
|
frameRect.anchorMax = new Vector2(0f, 1f);
|
|
frameRect.pivot = new Vector2(0f, 1f);
|
|
frameRect.anchoredPosition = new Vector2(6f, -6f);
|
|
frameRect.sizeDelta = new Vector2(20f, 20f);
|
|
|
|
Image frameImage = faceFrame.GetComponent<Image>();
|
|
frameImage.color = new Color(0.12f, 0.16f, 0.17f, 0.36f);
|
|
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<RectTransform>();
|
|
faceRect.anchorMin = Vector2.zero;
|
|
faceRect.anchorMax = Vector2.one;
|
|
faceRect.offsetMin = new Vector2(2f, 2f);
|
|
faceRect.offsetMax = new Vector2(-2f, -2f);
|
|
|
|
Image faceImage = faceObject.GetComponent<Image>();
|
|
faceImage.sprite = LoadHudSprite(CharacterFaceResourcePath);
|
|
faceImage.preserveAspect = true;
|
|
faceImage.raycastTarget = false;
|
|
faceImage.color = faceImage.sprite != null
|
|
? Color.white
|
|
: new Color(1f, 1f, 1f, 0f);
|
|
}
|
|
|
|
private void CreateLifeIcons(Transform parent) {
|
|
Sprite lifeSprite = LoadHudSprite(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<RectTransform>();
|
|
lifeRect.anchorMin = new Vector2(0f, 1f);
|
|
lifeRect.anchorMax = new Vector2(0f, 1f);
|
|
lifeRect.pivot = new Vector2(0f, 1f);
|
|
lifeRect.anchoredPosition = new Vector2(33f + (i * 13f), -7f);
|
|
lifeRect.sizeDelta = new Vector2(10f, 10f);
|
|
|
|
Image lifeImage = lifeObject.GetComponent<Image>();
|
|
lifeImage.sprite = lifeSprite;
|
|
lifeImage.color = lifeSprite != null
|
|
? Color.white
|
|
: new Color(0.94f, 0.18f, 0.12f, 0.9f);
|
|
lifeImage.preserveAspect = true;
|
|
lifeImage.raycastTarget = false;
|
|
lifeIcons[i] = lifeImage;
|
|
}
|
|
}
|
|
|
|
private void CreateItemSlots(Transform parent) {
|
|
Sprite slotSprite = LoadHudSprite(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<RectTransform>();
|
|
slotRect.anchorMin = new Vector2(0f, 1f);
|
|
slotRect.anchorMax = new Vector2(0f, 1f);
|
|
slotRect.pivot = new Vector2(0f, 1f);
|
|
slotRect.anchoredPosition = new Vector2(33f + (i * 13f), -20f);
|
|
slotRect.sizeDelta = new Vector2(10f, 10f);
|
|
|
|
Image slotImage = slotObject.GetComponent<Image>();
|
|
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 Sprite LoadHudSprite(string resourcePath) {
|
|
Sprite sprite = Resources.Load<Sprite>(resourcePath);
|
|
if(sprite != null)
|
|
{
|
|
return sprite;
|
|
}
|
|
|
|
Texture2D texture = Resources.Load<Texture2D>(resourcePath);
|
|
#if UNITY_EDITOR
|
|
if(texture == null)
|
|
{
|
|
sprite = AssetDatabase.LoadAssetAtPath<Sprite>("Assets/Resources/" + resourcePath + ".png");
|
|
if(sprite != null)
|
|
{
|
|
return sprite;
|
|
}
|
|
|
|
texture = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Resources/" + resourcePath + ".png");
|
|
}
|
|
#endif
|
|
if(texture == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return Sprite.Create(
|
|
texture,
|
|
new Rect(0f, 0f, texture.width, texture.height),
|
|
new Vector2(0.5f, 0.5f),
|
|
100f);
|
|
}
|
|
|
|
private void CreateTutorialGuide(Transform canvasTransform) {
|
|
tutorialGuideObject = new GameObject("Tutorial Guide", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
|
tutorialGuideObject.transform.SetParent(canvasTransform, false);
|
|
|
|
RectTransform guideRect = tutorialGuideObject.GetComponent<RectTransform>();
|
|
guideRect.anchorMin = new Vector2(0.5f, 1f);
|
|
guideRect.anchorMax = new Vector2(0.5f, 1f);
|
|
guideRect.pivot = new Vector2(0.5f, 1f);
|
|
guideRect.anchoredPosition = new Vector2(0f, -12f);
|
|
guideRect.sizeDelta = new Vector2(348f, 66f);
|
|
|
|
Image guideBackground = tutorialGuideObject.GetComponent<Image>();
|
|
guideBackground.color = new Color(0.035f, 0.055f, 0.07f, 0.82f);
|
|
guideBackground.raycastTarget = false;
|
|
|
|
tutorialFontAsset = CreateKoreanTutorialFontAsset();
|
|
|
|
CreateTutorialMainImage(tutorialGuideObject.transform);
|
|
CreateTutorialItemRow(tutorialGuideObject.transform);
|
|
tutorialTitleText = CreateTutorialText(tutorialGuideObject.transform, "Tutorial Title", new Vector2(66f, -8f), new Vector2(268f, 17f), 11.5f, FontStyles.Bold);
|
|
tutorialBodyText = CreateTutorialText(tutorialGuideObject.transform, "Tutorial Body", new Vector2(66f, -26f), new Vector2(268f, 31f), 8.5f, FontStyles.Normal);
|
|
tutorialGuideObject.SetActive(false);
|
|
}
|
|
|
|
private void CreateTutorialMainImage(Transform parent) {
|
|
GameObject imageObject = new GameObject("Tutorial Main Image", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
|
imageObject.transform.SetParent(parent, false);
|
|
|
|
RectTransform imageRect = imageObject.GetComponent<RectTransform>();
|
|
imageRect.anchorMin = new Vector2(0f, 0.5f);
|
|
imageRect.anchorMax = new Vector2(0f, 0.5f);
|
|
imageRect.pivot = new Vector2(0f, 0.5f);
|
|
imageRect.anchoredPosition = new Vector2(14f, 0f);
|
|
imageRect.sizeDelta = new Vector2(48f, 48f);
|
|
|
|
tutorialMainImage = imageObject.GetComponent<Image>();
|
|
tutorialMainImage.preserveAspect = true;
|
|
tutorialMainImage.raycastTarget = false;
|
|
|
|
GameObject multiplierObject = new GameObject("Double Jump Marker", typeof(RectTransform), typeof(CanvasRenderer));
|
|
multiplierObject.transform.SetParent(imageObject.transform, false);
|
|
|
|
RectTransform multiplierRect = multiplierObject.GetComponent<RectTransform>();
|
|
multiplierRect.anchorMin = new Vector2(1f, 0f);
|
|
multiplierRect.anchorMax = new Vector2(1f, 0f);
|
|
multiplierRect.pivot = new Vector2(1f, 0f);
|
|
multiplierRect.anchoredPosition = new Vector2(-2f, 1f);
|
|
multiplierRect.sizeDelta = new Vector2(22f, 15f);
|
|
|
|
tutorialMultiplierText = multiplierObject.AddComponent<TextMeshProUGUI>();
|
|
tutorialMultiplierText.font = scoreText.font;
|
|
tutorialMultiplierText.text = "x2";
|
|
tutorialMultiplierText.alignment = TextAlignmentOptions.Center;
|
|
tutorialMultiplierText.fontSize = 9f;
|
|
tutorialMultiplierText.fontStyle = FontStyles.Bold;
|
|
tutorialMultiplierText.color = new Color(1f, 0.91f, 0.33f, 1f);
|
|
tutorialMultiplierText.raycastTarget = false;
|
|
tutorialMultiplierText.textWrappingMode = TextWrappingModes.NoWrap;
|
|
tutorialMultiplierText.gameObject.SetActive(false);
|
|
}
|
|
|
|
private void CreateTutorialItemRow(Transform parent) {
|
|
string[] itemAssetPaths = {
|
|
"Assets/Sprites/Items/Item_Health_Sushi.png",
|
|
"Assets/Sprites/Items/Item_Invincible_Ramen.png",
|
|
"Assets/Sprites/Items/Item_Shield.png",
|
|
"Assets/Sprites/Items/Item_Speed_Shoes.png",
|
|
"Assets/Sprites/Items/Item_Mileage_Card.png"
|
|
};
|
|
|
|
tutorialItemImages = new Image[itemAssetPaths.Length];
|
|
|
|
for(int i = 0; i < itemAssetPaths.Length; i++)
|
|
{
|
|
GameObject itemObject = new GameObject("Tutorial Item " + (i + 1), typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
|
itemObject.transform.SetParent(parent, false);
|
|
|
|
RectTransform itemRect = itemObject.GetComponent<RectTransform>();
|
|
itemRect.anchorMin = new Vector2(0f, 0.5f);
|
|
itemRect.anchorMax = new Vector2(0f, 0.5f);
|
|
itemRect.pivot = new Vector2(0f, 0.5f);
|
|
itemRect.anchoredPosition = new Vector2(11f + (i * 27f), 0f);
|
|
itemRect.sizeDelta = new Vector2(24f, 24f);
|
|
|
|
Image itemImage = itemObject.GetComponent<Image>();
|
|
itemImage.sprite = LoadTutorialSprite(itemAssetPaths[i]);
|
|
itemImage.preserveAspect = true;
|
|
itemImage.raycastTarget = false;
|
|
itemImage.gameObject.SetActive(false);
|
|
tutorialItemImages[i] = itemImage;
|
|
}
|
|
}
|
|
|
|
private TextMeshProUGUI CreateTutorialText(Transform parent, string objectName, Vector2 anchoredPosition, Vector2 sizeDelta, float fontSize, FontStyles fontStyle) {
|
|
GameObject textObject = new GameObject(objectName, typeof(RectTransform), typeof(CanvasRenderer));
|
|
textObject.transform.SetParent(parent, false);
|
|
|
|
RectTransform textRect = textObject.GetComponent<RectTransform>();
|
|
textRect.anchorMin = new Vector2(0f, 1f);
|
|
textRect.anchorMax = new Vector2(0f, 1f);
|
|
textRect.pivot = new Vector2(0f, 1f);
|
|
textRect.anchoredPosition = anchoredPosition;
|
|
textRect.sizeDelta = sizeDelta;
|
|
|
|
TextMeshProUGUI text = textObject.AddComponent<TextMeshProUGUI>();
|
|
text.font = tutorialFontAsset != null ? tutorialFontAsset : scoreText.font;
|
|
text.alignment = TextAlignmentOptions.Left;
|
|
text.fontSize = fontSize;
|
|
text.fontStyle = fontStyle;
|
|
text.color = new Color(0.96f, 0.98f, 1f, 0.96f);
|
|
text.raycastTarget = false;
|
|
text.textWrappingMode = TextWrappingModes.Normal;
|
|
return text;
|
|
}
|
|
|
|
private TMP_FontAsset CreateKoreanTutorialFontAsset() {
|
|
string[] familyNames = {
|
|
"Malgun Gothic",
|
|
"맑은 고딕",
|
|
"Arial Unicode MS"
|
|
};
|
|
|
|
string[] styleNames = {
|
|
"Regular",
|
|
"Normal"
|
|
};
|
|
|
|
for(int familyIndex = 0; familyIndex < familyNames.Length; familyIndex++)
|
|
{
|
|
for(int styleIndex = 0; styleIndex < styleNames.Length; styleIndex++)
|
|
{
|
|
TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset(familyNames[familyIndex], styleNames[styleIndex], 80);
|
|
if(fontAsset != null)
|
|
{
|
|
fontAsset.name = "Runtime Korean Tutorial Font";
|
|
return fontAsset;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Sprite GetTutorialMainSprite(TutorialGuideType guideType) {
|
|
if(guideType == TutorialGuideType.Jump || guideType == TutorialGuideType.DoubleJump)
|
|
{
|
|
return LoadTutorialSprite("Assets/Sprites/JJ_Jump.png", "JJ_Jump_2");
|
|
}
|
|
|
|
if(guideType == TutorialGuideType.Slide)
|
|
{
|
|
return LoadTutorialSprite("Assets/Sprites/JJ_Slide.png", "JJ_Slide_0");
|
|
}
|
|
|
|
if(guideType == TutorialGuideType.Damage)
|
|
{
|
|
return LoadTutorialSprite("Assets/Resources/UI_LifeStamp.png");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Sprite LoadTutorialSprite(string assetPath, string spriteName = "") {
|
|
#if UNITY_EDITOR
|
|
Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath);
|
|
Sprite firstSprite = null;
|
|
|
|
if(assets != null && assets.Length > 0)
|
|
{
|
|
for(int i = 0; i < assets.Length; i++)
|
|
{
|
|
Sprite sprite = assets[i] as Sprite;
|
|
if(sprite == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if(firstSprite == null)
|
|
{
|
|
firstSprite = sprite;
|
|
}
|
|
|
|
if(sprite.name == spriteName)
|
|
{
|
|
return sprite;
|
|
}
|
|
}
|
|
|
|
if(firstSprite != null)
|
|
{
|
|
return firstSprite;
|
|
}
|
|
}
|
|
|
|
Sprite directSprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
|
|
if(directSprite != null)
|
|
{
|
|
return directSprite;
|
|
}
|
|
|
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
|
|
if(texture == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return Sprite.Create(
|
|
texture,
|
|
new Rect(0f, 0f, texture.width, texture.height),
|
|
new Vector2(0.5f, 0.5f),
|
|
100f);
|
|
#else
|
|
return null;
|
|
#endif
|
|
}
|
|
|
|
private class CourseMinimapView {
|
|
private struct MarkerInfo {
|
|
public string label;
|
|
public string kind;
|
|
public Color color;
|
|
}
|
|
|
|
private const float RootWidth = 452f;
|
|
private const float RootHeight = 72f;
|
|
private const float CourseWidth = 386f;
|
|
private const float CourseAreaHeight = 36f;
|
|
private const float CourseAreaY = -9f;
|
|
private const float GroundY = -22f;
|
|
private const float CeilingY = 3f;
|
|
private const float PlatformHeight = 6f;
|
|
private const float PlatformDistanceWidth = 7.4f;
|
|
private const float CourseDistancePerInterval = 10f;
|
|
private const int MaxCourseObjectCount = 96;
|
|
|
|
private readonly List<GameObject> markerObjects = new List<GameObject>();
|
|
private RectTransform rootRect;
|
|
private RectTransform progressFillRect;
|
|
private RectTransform playerMarkerRect;
|
|
private TextMeshProUGUI routeText;
|
|
private TextMeshProUGUI progressText;
|
|
private TMP_FontAsset fontAsset;
|
|
private bool hasActiveMap = false;
|
|
private MapId activeMapId;
|
|
private float activeTargetDistance = -1f;
|
|
|
|
public void Build(Transform canvasTransform, TMP_FontAsset minimapFont) {
|
|
fontAsset = minimapFont;
|
|
|
|
GameObject rootObject = new GameObject("Course Minimap", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
|
rootObject.transform.SetParent(canvasTransform, false);
|
|
|
|
rootRect = rootObject.GetComponent<RectTransform>();
|
|
rootRect.anchorMin = new Vector2(0.5f, 0f);
|
|
rootRect.anchorMax = new Vector2(0.5f, 0f);
|
|
rootRect.pivot = new Vector2(0.5f, 0f);
|
|
rootRect.anchoredPosition = new Vector2(0f, 9f);
|
|
rootRect.sizeDelta = new Vector2(RootWidth, RootHeight);
|
|
|
|
Image background = rootObject.GetComponent<Image>();
|
|
background.color = new Color(0.025f, 0.045f, 0.055f, 0.78f);
|
|
background.raycastTarget = false;
|
|
|
|
routeText = CreateText(rootObject.transform, "Route Label", new Vector2(-207f, 24f), new Vector2(270f, 14f), 8.4f, TextAlignmentOptions.Left);
|
|
progressText = CreateText(rootObject.transform, "Route Progress", new Vector2(174f, 24f), new Vector2(94f, 14f), 8.4f, TextAlignmentOptions.Right);
|
|
|
|
CreateBlock(rootObject.transform, "Course Drawing Area", new Vector2(0f, CourseAreaY), new Vector2(CourseWidth + 18f, CourseAreaHeight), new Color(0.04f, 0.08f, 0.095f, 0.78f));
|
|
|
|
GameObject progressObject = new GameObject("Track Progress", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
|
progressObject.transform.SetParent(rootObject.transform, false);
|
|
|
|
progressFillRect = progressObject.GetComponent<RectTransform>();
|
|
progressFillRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
progressFillRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
progressFillRect.pivot = new Vector2(0f, 0.5f);
|
|
progressFillRect.anchoredPosition = new Vector2(-CourseWidth * 0.5f, CourseAreaY);
|
|
progressFillRect.sizeDelta = new Vector2(0f, CourseAreaHeight);
|
|
|
|
Image progressImage = progressObject.GetComponent<Image>();
|
|
progressImage.color = new Color(0.18f, 0.55f, 0.68f, 0.28f);
|
|
progressImage.raycastTarget = false;
|
|
|
|
Image playerMarker = CreateBlock(rootObject.transform, "Player Marker", new Vector2(GetCourseX(0f), GroundY + 14f), new Vector2(7f, 24f), new Color(1f, 0.78f, 0.21f, 1f));
|
|
playerMarkerRect = playerMarker.rectTransform;
|
|
}
|
|
|
|
public void Refresh(MapDefinition mapDefinition, float stageDistance) {
|
|
if(rootRect == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(mapDefinition == null)
|
|
{
|
|
rootRect.gameObject.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
rootRect.gameObject.SetActive(true);
|
|
|
|
float targetDistance = Mathf.Max(1f, mapDefinition.targetDistance);
|
|
bool mapChanged = !hasActiveMap
|
|
|| activeMapId != mapDefinition.mapId
|
|
|| !Mathf.Approximately(activeTargetDistance, targetDistance);
|
|
|
|
if(mapChanged)
|
|
{
|
|
hasActiveMap = true;
|
|
activeMapId = mapDefinition.mapId;
|
|
activeTargetDistance = targetDistance;
|
|
RebuildMarkers(mapDefinition, targetDistance);
|
|
|
|
if(routeText != null)
|
|
{
|
|
routeText.text = !string.IsNullOrEmpty(mapDefinition.routeName)
|
|
? mapDefinition.routeName
|
|
: mapDefinition.displayName;
|
|
}
|
|
}
|
|
|
|
float progress = Mathf.Clamp01(stageDistance / targetDistance);
|
|
if(progressFillRect != null)
|
|
{
|
|
progressFillRect.sizeDelta = new Vector2(CourseWidth * progress, CourseAreaHeight);
|
|
}
|
|
|
|
if(playerMarkerRect != null)
|
|
{
|
|
playerMarkerRect.anchoredPosition = new Vector2(GetCourseX(progress), GroundY + 14f);
|
|
playerMarkerRect.SetAsLastSibling();
|
|
}
|
|
|
|
if(progressText != null)
|
|
{
|
|
progressText.text = Mathf.FloorToInt(stageDistance) + "/" + Mathf.FloorToInt(targetDistance) + "m";
|
|
}
|
|
}
|
|
|
|
private void RebuildMarkers(MapDefinition mapDefinition, float targetDistance) {
|
|
ClearMarkers();
|
|
|
|
float cursor = 0f;
|
|
AddStepMarkers(mapDefinition.openingSteps, ref cursor, targetDistance);
|
|
|
|
PlatformSpawner.TutorialStep[] loopSteps = mapDefinition.loopSteps;
|
|
int loopGuard = 0;
|
|
while(cursor < targetDistance
|
|
&& loopSteps != null
|
|
&& loopSteps.Length > 0
|
|
&& markerObjects.Count < MaxCourseObjectCount
|
|
&& loopGuard < 300)
|
|
{
|
|
AddStepMarkers(loopSteps, ref cursor, targetDistance);
|
|
loopGuard++;
|
|
}
|
|
}
|
|
|
|
private void AddStepMarkers(PlatformSpawner.TutorialStep[] steps, ref float cursor, float targetDistance) {
|
|
if(steps == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for(int i = 0; i < steps.Length; i++)
|
|
{
|
|
PlatformSpawner.TutorialStep step = steps[i];
|
|
float stepDistance = GetStepDistance(step);
|
|
|
|
if(markerObjects.Count < MaxCourseObjectCount)
|
|
{
|
|
float centerDistance = cursor + (stepDistance * 0.5f);
|
|
float centerProgress = Mathf.Clamp01(centerDistance / targetDistance);
|
|
float platformWidth = Mathf.Max(8f, CourseWidth * (PlatformDistanceWidth / targetDistance));
|
|
CreatePlatformPiece(centerProgress, platformWidth, GetPlatformColor(step));
|
|
|
|
if(TryGetMarkerInfo(step, out MarkerInfo markerInfo))
|
|
{
|
|
CreateCourseFeature(centerProgress, markerInfo);
|
|
}
|
|
}
|
|
|
|
cursor += stepDistance;
|
|
if(cursor >= targetDistance && markerObjects.Count >= MaxCourseObjectCount)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private float GetStepDistance(PlatformSpawner.TutorialStep step) {
|
|
if(step == null)
|
|
{
|
|
return CourseDistancePerInterval;
|
|
}
|
|
|
|
return Mathf.Max(0.3f, step.spawnInterval) * CourseDistancePerInterval;
|
|
}
|
|
|
|
private bool TryGetMarkerInfo(PlatformSpawner.TutorialStep step, out MarkerInfo markerInfo) {
|
|
markerInfo = new MarkerInfo();
|
|
|
|
if(step == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if(step.itemPattern != Platform.ItemPattern.None)
|
|
{
|
|
markerInfo.label = GetItemMarkerLabel(step.itemPattern);
|
|
markerInfo.color = new Color(0.38f, 0.93f, 0.57f, 0.98f);
|
|
markerInfo.kind = "Item";
|
|
return true;
|
|
}
|
|
|
|
if(step.coursePlatformType == PlatformSpawner.CoursePlatformType.ForceHit
|
|
|| step.platformPattern == Platform.PlatformPattern.ForceHit)
|
|
{
|
|
markerInfo.label = "!";
|
|
markerInfo.color = new Color(1f, 0.34f, 0.28f, 0.98f);
|
|
markerInfo.kind = "ForceHit";
|
|
return true;
|
|
}
|
|
|
|
if(step.guideType == TutorialGuideType.DoubleJump || step.spawnInterval >= 1.4f)
|
|
{
|
|
markerInfo.label = "2";
|
|
markerInfo.color = new Color(0.40f, 0.89f, 1f, 0.98f);
|
|
markerInfo.kind = "DoubleJump";
|
|
return true;
|
|
}
|
|
|
|
if(step.coursePlatformType == PlatformSpawner.CoursePlatformType.Slide
|
|
|| step.platformPattern == Platform.PlatformPattern.Slide)
|
|
{
|
|
markerInfo.label = "S";
|
|
markerInfo.color = new Color(0.47f, 0.63f, 1f, 0.98f);
|
|
markerInfo.kind = "Slide";
|
|
return true;
|
|
}
|
|
|
|
if(step.coursePlatformType == PlatformSpawner.CoursePlatformType.Jump
|
|
|| step.platformPattern == Platform.PlatformPattern.LowLeft
|
|
|| step.platformPattern == Platform.PlatformPattern.LowMid
|
|
|| step.platformPattern == Platform.PlatformPattern.LowRight)
|
|
{
|
|
markerInfo.label = "J";
|
|
markerInfo.color = new Color(1f, 0.83f, 0.28f, 0.98f);
|
|
markerInfo.kind = "Jump";
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private string GetItemMarkerLabel(Platform.ItemPattern itemPattern) {
|
|
switch(itemPattern)
|
|
{
|
|
case Platform.ItemPattern.HealthSushi:
|
|
return "+";
|
|
case Platform.ItemPattern.InvincibleRamen:
|
|
return "R";
|
|
case Platform.ItemPattern.Shield:
|
|
return "G";
|
|
case Platform.ItemPattern.SpeedShoes:
|
|
return "V";
|
|
case Platform.ItemPattern.MileageCard:
|
|
return "M";
|
|
default:
|
|
return "I";
|
|
}
|
|
}
|
|
|
|
private void CreatePlatformPiece(float progress, float width, Color color) {
|
|
Image platformImage = CreateBlock(rootRect, "Course Platform", new Vector2(GetCourseX(progress), GroundY), new Vector2(width, PlatformHeight), color);
|
|
markerObjects.Add(platformImage.gameObject);
|
|
}
|
|
|
|
private Color GetPlatformColor(PlatformSpawner.TutorialStep step) {
|
|
if(step != null && step.spawnInterval >= 1.4f)
|
|
{
|
|
return new Color(0.28f, 0.50f, 0.57f, 0.96f);
|
|
}
|
|
|
|
return new Color(0.33f, 0.63f, 0.70f, 0.96f);
|
|
}
|
|
|
|
private void CreateCourseFeature(float progress, MarkerInfo markerInfo) {
|
|
if(markerInfo.kind == "Slide")
|
|
{
|
|
CreateSlideFeature(progress, markerInfo);
|
|
return;
|
|
}
|
|
|
|
if(markerInfo.kind == "ForceHit")
|
|
{
|
|
CreateJumpFeature(progress, markerInfo);
|
|
CreateSlideFeature(progress, markerInfo);
|
|
return;
|
|
}
|
|
|
|
if(markerInfo.kind == "Item")
|
|
{
|
|
CreateItemFeature(progress, markerInfo);
|
|
return;
|
|
}
|
|
|
|
if(markerInfo.kind == "DoubleJump")
|
|
{
|
|
CreateDoubleJumpFeature(progress, markerInfo);
|
|
return;
|
|
}
|
|
|
|
CreateJumpFeature(progress, markerInfo);
|
|
}
|
|
|
|
private void CreateJumpFeature(float progress, MarkerInfo markerInfo) {
|
|
float x = GetCourseX(progress);
|
|
Image obstacle = CreateBlock(rootRect, "Jump Obstacle", new Vector2(x, GroundY + 8f), new Vector2(10f, 12f), markerInfo.color);
|
|
markerObjects.Add(obstacle.gameObject);
|
|
CreateTinyLabel(markerInfo.label, new Vector2(x, GroundY + 8f), new Color(0.02f, 0.04f, 0.05f, 0.95f));
|
|
}
|
|
|
|
private void CreateSlideFeature(float progress, MarkerInfo markerInfo) {
|
|
float x = GetCourseX(progress);
|
|
Image ceiling = CreateBlock(rootRect, "Slide Ceiling", new Vector2(x, CeilingY), new Vector2(32f, 6f), new Color(0.56f, 0.70f, 0.98f, 0.95f));
|
|
markerObjects.Add(ceiling.gameObject);
|
|
Image hanging = CreateBlock(rootRect, "Slide Hanging Obstacle", new Vector2(x, CeilingY - 8f), new Vector2(18f, 10f), markerInfo.color);
|
|
markerObjects.Add(hanging.gameObject);
|
|
CreateTinyLabel(markerInfo.label, new Vector2(x, CeilingY - 8f), new Color(0.02f, 0.04f, 0.05f, 0.95f));
|
|
}
|
|
|
|
private void CreateItemFeature(float progress, MarkerInfo markerInfo) {
|
|
float x = GetCourseX(progress);
|
|
GameObject markerObject = new GameObject("Course Marker " + markerInfo.label, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
|
markerObject.transform.SetParent(rootRect, false);
|
|
|
|
RectTransform markerRect = markerObject.GetComponent<RectTransform>();
|
|
markerRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
markerRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
markerRect.pivot = new Vector2(0.5f, 0.5f);
|
|
markerRect.anchoredPosition = new Vector2(x, GroundY + 15f);
|
|
markerRect.sizeDelta = new Vector2(11f, 11f);
|
|
|
|
Image markerImage = markerObject.GetComponent<Image>();
|
|
markerImage.color = markerInfo.color;
|
|
markerImage.raycastTarget = false;
|
|
|
|
TextMeshProUGUI markerText = CreateText(markerObject.transform, "Label", Vector2.zero, new Vector2(11f, 11f), 6.4f, TextAlignmentOptions.Center);
|
|
markerText.color = new Color(0.02f, 0.04f, 0.05f, 0.95f);
|
|
markerText.fontStyle = FontStyles.Bold;
|
|
markerText.text = markerInfo.label;
|
|
|
|
markerObjects.Add(markerObject);
|
|
}
|
|
|
|
private void CreateDoubleJumpFeature(float progress, MarkerInfo markerInfo) {
|
|
float x = GetCourseX(progress);
|
|
Image gapMarker = CreateBlock(rootRect, "Double Jump Gap", new Vector2(x, GroundY + 14f), new Vector2(24f, 3f), markerInfo.color);
|
|
markerObjects.Add(gapMarker.gameObject);
|
|
CreateTinyLabel(markerInfo.label, new Vector2(x, GroundY + 21f), markerInfo.color);
|
|
}
|
|
|
|
private void CreateTinyLabel(string label, Vector2 anchoredPosition, Color color) {
|
|
TextMeshProUGUI markerText = CreateText(rootRect, "Course Label " + label, anchoredPosition, new Vector2(14f, 11f), 6.4f, TextAlignmentOptions.Center);
|
|
markerText.color = color;
|
|
markerText.fontStyle = FontStyles.Bold;
|
|
markerText.text = label;
|
|
markerObjects.Add(markerText.gameObject);
|
|
}
|
|
|
|
private TextMeshProUGUI CreateText(Transform parent, string objectName, Vector2 anchoredPosition, Vector2 sizeDelta, float fontSize, TextAlignmentOptions alignment) {
|
|
GameObject textObject = new GameObject(objectName, typeof(RectTransform), typeof(CanvasRenderer));
|
|
textObject.transform.SetParent(parent, false);
|
|
|
|
RectTransform textRect = textObject.GetComponent<RectTransform>();
|
|
textRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
textRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
textRect.pivot = new Vector2(0.5f, 0.5f);
|
|
textRect.anchoredPosition = anchoredPosition;
|
|
textRect.sizeDelta = sizeDelta;
|
|
|
|
TextMeshProUGUI text = textObject.AddComponent<TextMeshProUGUI>();
|
|
text.font = fontAsset;
|
|
text.alignment = alignment;
|
|
text.fontSize = fontSize;
|
|
text.color = new Color(0.88f, 0.96f, 1f, 0.92f);
|
|
text.raycastTarget = false;
|
|
text.textWrappingMode = TextWrappingModes.NoWrap;
|
|
return text;
|
|
}
|
|
|
|
private Image CreateBlock(Transform parent, string objectName, Vector2 anchoredPosition, Vector2 sizeDelta, Color color) {
|
|
GameObject blockObject = new GameObject(objectName, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
|
blockObject.transform.SetParent(parent, false);
|
|
|
|
RectTransform blockRect = blockObject.GetComponent<RectTransform>();
|
|
blockRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
blockRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
blockRect.pivot = new Vector2(0.5f, 0.5f);
|
|
blockRect.anchoredPosition = anchoredPosition;
|
|
blockRect.sizeDelta = sizeDelta;
|
|
|
|
Image image = blockObject.GetComponent<Image>();
|
|
image.color = color;
|
|
image.raycastTarget = false;
|
|
return image;
|
|
}
|
|
|
|
private float GetCourseX(float progress) {
|
|
return (-CourseWidth * 0.5f) + (CourseWidth * Mathf.Clamp01(progress));
|
|
}
|
|
|
|
private void ClearMarkers() {
|
|
for(int i = 0; i < markerObjects.Count; i++)
|
|
{
|
|
if(markerObjects[i] != null)
|
|
{
|
|
Object.Destroy(markerObjects[i]);
|
|
}
|
|
}
|
|
|
|
markerObjects.Clear();
|
|
}
|
|
}
|
|
}
|