Tune tutorial coin arcs and finish flow
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
public enum FinishGateStyle {
|
||||
None,
|
||||
AirportDeparture,
|
||||
JapanTorii,
|
||||
ChinaPaifang,
|
||||
AmericaRoadSign
|
||||
AmericaLiberty
|
||||
}
|
||||
|
||||
public class FinishGate : MonoBehaviour {
|
||||
@@ -75,8 +78,8 @@ public class FinishGate : MonoBehaviour {
|
||||
case FinishGateStyle.ChinaPaifang:
|
||||
BuildChinaPaifang();
|
||||
break;
|
||||
case FinishGateStyle.AmericaRoadSign:
|
||||
BuildAmericaRoadSign();
|
||||
case FinishGateStyle.AmericaLiberty:
|
||||
BuildAmericaLibertyFallback();
|
||||
break;
|
||||
case FinishGateStyle.AirportDeparture:
|
||||
default:
|
||||
@@ -98,19 +101,7 @@ public class FinishGate : MonoBehaviour {
|
||||
return false;
|
||||
}
|
||||
|
||||
Sprite sprite = Resources.Load<Sprite>(resourcePath);
|
||||
if(sprite == null)
|
||||
{
|
||||
Texture2D texture = Resources.Load<Texture2D>(resourcePath);
|
||||
if(texture != null)
|
||||
{
|
||||
sprite = Sprite.Create(
|
||||
texture,
|
||||
new Rect(0f, 0f, texture.width, texture.height),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
RuntimeSpritePixelsPerUnit);
|
||||
}
|
||||
}
|
||||
Sprite sprite = LoadGateSprite(resourcePath);
|
||||
|
||||
if(sprite == null)
|
||||
{
|
||||
@@ -129,10 +120,55 @@ public class FinishGate : MonoBehaviour {
|
||||
float spriteHeight = Mathf.Max(0.01f, sprite.bounds.size.y);
|
||||
float scale = targetHeight / spriteHeight;
|
||||
spriteObject.transform.localScale = new Vector3(scale, scale, 1f);
|
||||
spriteObject.transform.localPosition = new Vector3(0f, targetHeight * 0.5f, 0f);
|
||||
spriteObject.transform.localPosition = new Vector3(0f, targetHeight * 0.5f - GetSpriteGateBottomInset(style), 0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Sprite LoadGateSprite(string resourcePath) {
|
||||
Sprite sprite = Resources.Load<Sprite>(resourcePath);
|
||||
if(sprite != null)
|
||||
{
|
||||
return sprite;
|
||||
}
|
||||
|
||||
Sprite[] sprites = Resources.LoadAll<Sprite>(resourcePath);
|
||||
if(sprites != null && sprites.Length > 0 && sprites[0] != null)
|
||||
{
|
||||
return sprites[0];
|
||||
}
|
||||
|
||||
Texture2D texture = Resources.Load<Texture2D>(resourcePath);
|
||||
if(texture != null)
|
||||
{
|
||||
return CreateRuntimeSprite(texture);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
string assetPath = "Assets/Resources/" + resourcePath + ".png";
|
||||
sprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
|
||||
if(sprite != null)
|
||||
{
|
||||
return sprite;
|
||||
}
|
||||
|
||||
texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
|
||||
if(texture != null)
|
||||
{
|
||||
return CreateRuntimeSprite(texture);
|
||||
}
|
||||
#endif
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Sprite CreateRuntimeSprite(Texture2D texture) {
|
||||
return Sprite.Create(
|
||||
texture,
|
||||
new Rect(0f, 0f, texture.width, texture.height),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
RuntimeSpritePixelsPerUnit);
|
||||
}
|
||||
|
||||
private string GetSpriteResourcePath(FinishGateStyle style) {
|
||||
switch(style)
|
||||
{
|
||||
@@ -142,8 +178,8 @@ public class FinishGate : MonoBehaviour {
|
||||
return "FinishGate_Japan_Torii";
|
||||
case FinishGateStyle.ChinaPaifang:
|
||||
return "FinishGate_China_Paifang";
|
||||
case FinishGateStyle.AmericaRoadSign:
|
||||
return "FinishGate_America_Highway";
|
||||
case FinishGateStyle.AmericaLiberty:
|
||||
return "FinishGate_America_Liberty";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -154,13 +190,23 @@ public class FinishGate : MonoBehaviour {
|
||||
{
|
||||
case FinishGateStyle.ChinaPaifang:
|
||||
return 4.65f;
|
||||
case FinishGateStyle.AmericaRoadSign:
|
||||
case FinishGateStyle.AmericaLiberty:
|
||||
return 4.45f;
|
||||
default:
|
||||
return 4.35f;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetSpriteGateBottomInset(FinishGateStyle style) {
|
||||
switch(style)
|
||||
{
|
||||
case FinishGateStyle.JapanTorii:
|
||||
return 0.36f;
|
||||
default:
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildAirportDepartureGate() {
|
||||
Color navy = new Color(0.05f, 0.2f, 0.34f, 1f);
|
||||
Color blue = new Color(0.08f, 0.42f, 0.67f, 1f);
|
||||
@@ -212,7 +258,7 @@ public class FinishGate : MonoBehaviour {
|
||||
CreateBlock("Paifang Lantern Right", new Vector2(0.78f, 2.18f), new Vector2(0.26f, 0.34f), red, 7);
|
||||
}
|
||||
|
||||
private void BuildAmericaRoadSign() {
|
||||
private void BuildAmericaLibertyFallback() {
|
||||
Color metal = new Color(0.48f, 0.56f, 0.58f, 1f);
|
||||
Color green = new Color(0.03f, 0.38f, 0.22f, 1f);
|
||||
Color yellow = new Color(1f, 0.8f, 0.18f, 1f);
|
||||
|
||||
@@ -62,6 +62,7 @@ public class GameManager : MonoBehaviour {
|
||||
private int tutorialGuideVersion = 0; // 오래된 자동 숨김 요청을 무시하기 위한 버전
|
||||
private FinishGate activeFinishGate; // 목표 지점에 나타나는 맵별 클리어 게이트
|
||||
private bool finishGateSpawned = false; // 현재 맵에서 도착 게이트를 이미 만들었는지 여부
|
||||
private PlatformSpawner activePlatformSpawner; // 현재 맵의 발판/장애물 생성기
|
||||
|
||||
public int maxLife = 3; // 시작 목숨
|
||||
public int initialMileage = 500; // 첫 시작시 지급할 초기 마일리지
|
||||
@@ -222,10 +223,10 @@ public class GameManager : MonoBehaviour {
|
||||
gameoverUI.SetActive(false);
|
||||
}
|
||||
|
||||
PlatformSpawner platformSpawner = FindFirstObjectByType<PlatformSpawner>();
|
||||
if(platformSpawner != null)
|
||||
activePlatformSpawner = FindFirstObjectByType<PlatformSpawner>();
|
||||
if(activePlatformSpawner != null)
|
||||
{
|
||||
platformSpawner.ApplyMap(currentMapDefinition);
|
||||
activePlatformSpawner.ApplyMap(currentMapDefinition);
|
||||
}
|
||||
|
||||
StartTutorialGuideSequence();
|
||||
@@ -646,11 +647,31 @@ public class GameManager : MonoBehaviour {
|
||||
return;
|
||||
}
|
||||
|
||||
float gateGroundY = finishGateGroundY;
|
||||
EnsureFinishGateLanding(gateGroundY);
|
||||
|
||||
GameObject gateObject = new GameObject("Finish Gate - " + currentMapDefinition.displayName);
|
||||
gateObject.transform.position = new Vector3(finishGateSpawnX, finishGateGroundY, 0f);
|
||||
gateObject.transform.position = new Vector3(finishGateSpawnX, gateGroundY, 0f);
|
||||
activeFinishGate = gateObject.AddComponent<FinishGate>();
|
||||
activeFinishGate.Setup(currentMapDefinition.finishGateStyle);
|
||||
finishGateSpawned = true;
|
||||
|
||||
if(activePlatformSpawner != null)
|
||||
{
|
||||
activePlatformSpawner.StopCourseSpawningAfterFinishGate();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureFinishGateLanding(float gateGroundY) {
|
||||
if(activePlatformSpawner == null)
|
||||
{
|
||||
activePlatformSpawner = FindFirstObjectByType<PlatformSpawner>();
|
||||
}
|
||||
|
||||
if(activePlatformSpawner != null)
|
||||
{
|
||||
activePlatformSpawner.SpawnFinishLandingPlatform(finishGateSpawnX, gateGroundY);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldUseFinishGate() {
|
||||
@@ -665,6 +686,11 @@ public class GameManager : MonoBehaviour {
|
||||
Destroy(activeFinishGate.gameObject);
|
||||
activeFinishGate = null;
|
||||
}
|
||||
|
||||
if(activePlatformSpawner != null)
|
||||
{
|
||||
activePlatformSpawner.ClearFinishLandingPlatform();
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncTotalMileageToFlow() {
|
||||
@@ -739,14 +765,15 @@ public class GameManager : MonoBehaviour {
|
||||
tutorialBodyText.text = tutorialMessage;
|
||||
}
|
||||
|
||||
bool showItemRow = tutorialGuideType == TutorialGuideType.Items;
|
||||
Sprite mainSprite = GetTutorialMainSprite(tutorialGuideType);
|
||||
bool showFullText = !showItemRow && mainSprite == null;
|
||||
UpdateTutorialTextLayout(showItemRow, showFullText);
|
||||
int activeItemImageIndex = GetTutorialItemImageIndex();
|
||||
bool showItemImage = tutorialGuideType == TutorialGuideType.Items && HasTutorialItemImage(activeItemImageIndex);
|
||||
Sprite mainSprite = showItemImage ? null : GetTutorialMainSprite(tutorialGuideType);
|
||||
bool showFullText = !showItemImage && mainSprite == null;
|
||||
UpdateTutorialTextLayout(showItemImage, showFullText);
|
||||
|
||||
if(tutorialMainImage != null)
|
||||
{
|
||||
tutorialMainImage.gameObject.SetActive(!showItemRow && mainSprite != null);
|
||||
tutorialMainImage.gameObject.SetActive(!showItemImage && mainSprite != null);
|
||||
tutorialMainImage.sprite = mainSprite;
|
||||
}
|
||||
|
||||
@@ -761,7 +788,7 @@ public class GameManager : MonoBehaviour {
|
||||
{
|
||||
if(tutorialItemImages[i] != null)
|
||||
{
|
||||
tutorialItemImages[i].gameObject.SetActive(showItemRow && tutorialItemImages[i].sprite != null);
|
||||
tutorialItemImages[i].gameObject.SetActive(showItemImage && i == activeItemImageIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -775,10 +802,10 @@ public class GameManager : MonoBehaviour {
|
||||
|
||||
if(showItemRow)
|
||||
{
|
||||
textPosition = new Vector2(154f, -8f);
|
||||
textSize = new Vector2(178f, 17f);
|
||||
bodyPosition = new Vector2(154f, -26f);
|
||||
bodySize = new Vector2(178f, 31f);
|
||||
textPosition = new Vector2(80f, -8f);
|
||||
textSize = new Vector2(252f, 17f);
|
||||
bodyPosition = new Vector2(80f, -26f);
|
||||
bodySize = new Vector2(252f, 31f);
|
||||
}
|
||||
else if(showFullText)
|
||||
{
|
||||
@@ -801,6 +828,49 @@ public class GameManager : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasTutorialItemImage(int itemImageIndex) {
|
||||
return tutorialItemImages != null
|
||||
&& itemImageIndex >= 0
|
||||
&& itemImageIndex < tutorialItemImages.Length
|
||||
&& tutorialItemImages[itemImageIndex] != null
|
||||
&& tutorialItemImages[itemImageIndex].sprite != null;
|
||||
}
|
||||
|
||||
private int GetTutorialItemImageIndex() {
|
||||
if(tutorialGuideType != TutorialGuideType.Items)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
string itemText = (tutorialTitle + " " + tutorialMessage).Trim();
|
||||
if(itemText.Contains("초밥"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(itemText.Contains("라멘"))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(itemText.Contains("방어막"))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if(itemText.Contains("운동화"))
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
if(itemText.Contains("마일리지"))
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Color GetActiveLifeIconColor(Image lifeIcon) {
|
||||
if(lifeIcon != null && lifeIcon.sprite == null)
|
||||
{
|
||||
@@ -1061,8 +1131,8 @@ public class GameManager : MonoBehaviour {
|
||||
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);
|
||||
itemRect.anchoredPosition = new Vector2(14f, 0f);
|
||||
itemRect.sizeDelta = new Vector2(54f, 54f);
|
||||
|
||||
Image itemImage = itemObject.GetComponent<Image>();
|
||||
itemImage.sprite = LoadTutorialSprite(itemAssetPaths[i]);
|
||||
|
||||
@@ -193,7 +193,7 @@ public static class MapDatabase {
|
||||
isTutorial = false,
|
||||
platformSkin = Platform.PlatformSkin.USA,
|
||||
backgroundResourcePath = "Map_America_NewYork",
|
||||
finishGateStyle = FinishGateStyle.AmericaRoadSign,
|
||||
finishGateStyle = FinishGateStyle.AmericaLiberty,
|
||||
targetDistance = 1700f,
|
||||
timeLimit = 125f,
|
||||
clearMileageReward = 500,
|
||||
|
||||
@@ -60,6 +60,8 @@ public class Platform : MonoBehaviour {
|
||||
private const float MileageColliderRadius = 0.4f;
|
||||
private const float ItemTokenScale = 0.68f;
|
||||
private const float ItemColliderRadius = 0.46f;
|
||||
private const float JumpMileageMinWidthScale = 1.8f;
|
||||
private const float JumpMileageMaxWidthScale = 2.5f;
|
||||
|
||||
public void ConfigureForTutorial(
|
||||
PlatformPattern pattern,
|
||||
@@ -619,15 +621,29 @@ public class Platform : MonoBehaviour {
|
||||
}
|
||||
|
||||
private void PlaceJumpMileageArc(float obstacleX) {
|
||||
float widthScale = GetJumpMileageArcWidthScale();
|
||||
Vector2[] positions = {
|
||||
new Vector2(obstacleX - 1.2f, 1.65f),
|
||||
new Vector2(obstacleX, 2.25f),
|
||||
new Vector2(obstacleX + 1.2f, 1.65f),
|
||||
new Vector2(obstacleX + 1.9f, 1.35f)
|
||||
new Vector2(obstacleX - 2.25f * widthScale, 1.88f),
|
||||
new Vector2(obstacleX - 1.15f * widthScale, 2.88f),
|
||||
new Vector2(obstacleX, 3.58f),
|
||||
new Vector2(obstacleX + 1.15f * widthScale, 2.88f),
|
||||
new Vector2(obstacleX + 2.25f * widthScale, 1.88f)
|
||||
};
|
||||
|
||||
PlaceMileageTokens(positions, smallMileageValue);
|
||||
SetPickupValue(1, bonusMileageValue);
|
||||
SetPickupValue(2, bonusMileageValue);
|
||||
}
|
||||
|
||||
private float GetJumpMileageArcWidthScale() {
|
||||
if(GameManager.instance == null)
|
||||
{
|
||||
return JumpMileageMinWidthScale;
|
||||
}
|
||||
|
||||
float minSpeed = Mathf.Max(0.1f, GameManager.instance.minGameSpeed);
|
||||
float maxSpeed = Mathf.Max(minSpeed + 0.1f, GameManager.instance.maxGameSpeed);
|
||||
float speedProgress = Mathf.InverseLerp(minSpeed, maxSpeed, GameManager.instance.gameSpeed);
|
||||
return Mathf.Lerp(JumpMileageMinWidthScale, JumpMileageMaxWidthScale, speedProgress);
|
||||
}
|
||||
|
||||
private void PlaceSlideMileageLine() {
|
||||
|
||||
@@ -84,6 +84,8 @@ public class PlatformSpawner : MonoBehaviour {
|
||||
private int courseStepIndex = 0; // 진행 중인 패턴 코스 순번
|
||||
private Platform.PlatformSkin mapPlatformSkin = Platform.PlatformSkin.Airport; // 현재 맵에서 사용할 발판 스킨
|
||||
private readonly List<PendingTutorialGuide> pendingTutorialGuides = new List<PendingTutorialGuide>(); // 실제 체험 직전에 띄울 설명들
|
||||
private GameObject activeFinishLandingPlatform; // 도착 게이트가 공중에 뜨지 않도록 깔아두는 전용 발판
|
||||
private bool courseSpawningStopped = false; // 도착 게이트 뒤로 새 발판/장애물이 나오지 않게 막는다.
|
||||
|
||||
private Vector2 poolPosition = new Vector2(0, -25); // 초반에 생성된 발판들을 화면 밖에 숨겨둘 위치
|
||||
private float lastSpawnTime; // 마지막 배치 시점
|
||||
@@ -100,7 +102,9 @@ public class PlatformSpawner : MonoBehaviour {
|
||||
tutorialSteps = MapDatabase.CloneSteps(mapDefinition.openingSteps);
|
||||
stagePatternSteps = MapDatabase.CloneSteps(mapDefinition.loopSteps);
|
||||
courseStepIndex = 0;
|
||||
courseSpawningStopped = false;
|
||||
pendingTutorialGuides.Clear();
|
||||
ClearFinishLandingPlatform();
|
||||
ResetPoolIndices();
|
||||
lastSpawnTime = Time.time;
|
||||
timeBetSpawn = 0f;
|
||||
@@ -109,6 +113,46 @@ public class PlatformSpawner : MonoBehaviour {
|
||||
DeactivateAllPlatformPools();
|
||||
}
|
||||
|
||||
public void SpawnFinishLandingPlatform(float spawnX, float groundY) {
|
||||
if(platformPrefab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearFinishLandingPlatform();
|
||||
activeFinishLandingPlatform = Instantiate(platformPrefab, poolPosition, Quaternion.identity);
|
||||
activeFinishLandingPlatform.SetActive(false);
|
||||
|
||||
TutorialStep landingStep = CreateStep(
|
||||
Platform.PlatformPattern.Empty,
|
||||
Platform.MileagePattern.None,
|
||||
SlideObstaclePattern.SlideLayout.Random,
|
||||
GameManager.TutorialGuideType.None,
|
||||
Mathf.Clamp(groundY, yMin, yMax),
|
||||
FallbackLandingSpacing,
|
||||
"",
|
||||
"",
|
||||
Platform.ItemPattern.None,
|
||||
CoursePlatformType.Basic);
|
||||
|
||||
ConfigurePlatform(activeFinishLandingPlatform, landingStep);
|
||||
activeFinishLandingPlatform.transform.position = new Vector2(spawnX, landingStep.yPosition);
|
||||
activeFinishLandingPlatform.SetActive(true);
|
||||
}
|
||||
|
||||
public void ClearFinishLandingPlatform() {
|
||||
if(activeFinishLandingPlatform != null)
|
||||
{
|
||||
Destroy(activeFinishLandingPlatform);
|
||||
activeFinishLandingPlatform = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopCourseSpawningAfterFinishGate() {
|
||||
courseSpawningStopped = true;
|
||||
pendingTutorialGuides.Clear();
|
||||
}
|
||||
|
||||
void Start() {
|
||||
EnsurePatternSteps();
|
||||
|
||||
@@ -205,6 +249,11 @@ public class PlatformSpawner : MonoBehaviour {
|
||||
return;
|
||||
}
|
||||
|
||||
if(courseSpawningStopped)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdatePendingTutorialGuides();
|
||||
if(GameManager.instance != null && GameManager.instance.IsTutorialGuidePaused)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using UnityEngine;
|
||||
// PlayerController는 플레이어 캐릭터로서 Player 게임 오브젝트를 제어한다.
|
||||
public class PlayerController : MonoBehaviour {
|
||||
public AudioClip deathClip; // 사망시 재생할 오디오 클립
|
||||
public float jumpForce = 700f; // 점프 힘
|
||||
public float jumpForce = 600f; // 점프 힘
|
||||
public int maxJumpCount = 2; // 최대 점프 횟수
|
||||
public float invincibleDuration = 1.5f; // 피해를 입은 뒤 무적 시간
|
||||
public float groundStickGraceTime = 0.12f; // 발판 이음새에서 접지가 아주 잠깐 끊겨도 유지하는 시간
|
||||
|
||||
Reference in New Issue
Block a user